#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "json.hpp" #include "rtsp_capture.h" #include "yolo_detector.h" #include "rga_processor.h" #include "fence_engine.h" #include "relay_ctrl.h" #include "web_server.h" #include "alarm_history.h" #include "config_loader.h" #include "rfid_client.h" #include "modbus_client.h" #include "bucket_manager.h" #include "idle_detector.h" #include "pose_detector.h" #include "display_output.h" using namespace std; using json = nlohmann::json; static atomic g_running{true}; static void sigHandler(int sig) { // 第一次: 优雅退出; 第二次: 强制退出(防止清理流程卡死) if (!g_running.exchange(false)) { signal(sig, SIG_DFL); _exit(130); } } struct ChannelState { int id; shared_ptr capture; RgaProcessor rga; vector detections; vector events; vector rgb_buf; vector poses; // 姿态识别结果 vector fall_poses; // 判定为跌倒/异常姿态的人员 bool pose_running = false; // 本通道姿态推理开关 int pose_fall_count = 0; // 连续命中帧数(防抖) std::atomic fall_seq{0}; // 姿态事件序号(沿触发) bool fall_latched = false; // 姿态报警沿触发锁存 chrono::steady_clock::time_point fall_latch_ts{}; string pose_action; // 当前命中的姿态动作 float pose_score = 0.0f; // 当前动作得分 cv::Mat display_frame; cv::Mat fence_fill; // 围栏填充缓存(纯色图, 与帧内容无关) uint64_t fence_ver = 0; // 对应 FenceEngine 版本 std::atomic disp_seq{0}; int frame_count = 0; int skip_frames = 3; bool size_recorded = false; bool alarm_latched = false; // 报警沿触发: 每次新报警只截一张 chrono::steady_clock::time_point last_alarm_ts{}; chrono::steady_clock::time_point episode_start{}; // 本回合报警开始时刻 mutex mtx; }; static json loadJson(const string& path) { ifstream ifs(path); if (!ifs.is_open()) return json::object(); json j; ifs >> j; return j; } static void saveJson(const string& path, const json& j) { ofstream ofs(path); ofs << j.dump(4) << endl; } static void buildFenceFill(cv::Mat& fill, const vector& fences, const cv::Size& size, float s) { fill = cv::Mat::zeros(size, CV_8UC3); vector> contours; for (auto& f : fences) { if (!f.active || f.polygon.size() < 3) continue; vector pts; for (auto& p : f.polygon) pts.push_back(cv::Point((int)(p.x * s), (int)(p.y * s))); contours.push_back(std::move(pts)); } if (!contours.empty()) cv::drawContours(fill, contours, -1, cv::Scalar(0, 200, 0), -1); } static void drawFenceEdges(cv::Mat& frame, const vector& fences, float s) { for (auto& f : fences) { if (!f.active || f.polygon.size() < 3) continue; vector pts; for (auto& p : f.polygon) pts.push_back(cv::Point((int)(p.x * s), (int)(p.y * s))); vector> pp = {pts}; cv::polylines(frame, pp, true, cv::Scalar(0, 255, 0), 2); if (!f.name.empty()) cv::putText(frame, f.name, pts[0], cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(0, 255, 0), 2); } } static void drawAlarmBanner(cv::Mat& shot, int channel, const vector& events) { char tbuf[32]; time_t t = time(nullptr); struct tm tmv; localtime_r(&t, &tmv); strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", &tmv); const double fs = 0.45; const int thick = 1, lh = 18, pad = 4; vector lines; lines.push_back(string("!! ALARM !! CH") + to_string(channel) + " " + tbuf); size_t n = events.size() < 3 ? events.size() : 3; for (size_t i = 0; i < n; i++) { char b[160]; snprintf(b, sizeof(b), "%s %.0f%% -> %s", events[i].detection_class_name.c_str(), events[i].confidence * 100.0f, events[i].fence_name.c_str()); lines.push_back(b); } int w = 0, bh = pad * 2 + lh * (int)lines.size(); for (auto& l : lines) { int tw = 0, th2 = 0; cv::getTextSize(l, cv::FONT_HERSHEY_SIMPLEX, fs, thick, &th2); if (tw > w) w = tw; } w += pad * 2; if (w > shot.cols) w = shot.cols; cv::rectangle(shot, cv::Rect(0, 0, w, bh), cv::Scalar(0, 0, 255), -1); for (size_t i = 0; i < lines.size(); i++) cv::putText(shot, lines[i], cv::Point(pad, pad + 13 + (int)i * lh), cv::FONT_HERSHEY_SIMPLEX, fs, cv::Scalar(255, 255, 255), thick, cv::LINE_AA); } static void drawDets(cv::Mat& frame, const vector& dets, const vector& events, float s, bool show_alarm_text = true) { for (auto& det : dets) { bool in_fence = false; for (auto& ev : events) if (det.bbox.contains(ev.point)) { in_fence = true; break; } cv::Scalar color = in_fence ? cv::Scalar(0, 0, 255) : cv::Scalar(0, 255, 0); int thick = in_fence ? 3 : 2; cv::Rect rb((int)(det.bbox.x * s), (int)(det.bbox.y * s), (int)(det.bbox.width * s), (int)(det.bbox.height * s)); cv::rectangle(frame, rb, color, thick); char buf[128]; snprintf(buf, sizeof(buf), "%s %.0f%%", det.class_name.c_str(), det.confidence * 100); cv::putText(frame, buf, cv::Point(rb.x, rb.y - 6), cv::FONT_HERSHEY_SIMPLEX, 0.5, color, 1); } if (show_alarm_text && !events.empty()) { cv::putText(frame, "!! ALARM !!", cv::Point(10, 30), cv::FONT_HERSHEY_SIMPLEX, 1.0, cv::Scalar(0, 0, 255), 3); char ebuf[256]; snprintf(ebuf, sizeof(ebuf), "%s in %s (%.0f%%)", events[0].detection_class_name.c_str(), events[0].fence_name.c_str(), events[0].confidence * 100); cv::putText(frame, ebuf, cv::Point(10, 60), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 0, 255), 2); } } // COCO 17 关键点骨架连接(用于姿态叠加显示) static const int POSE_SKELETON[][2] = { {0,1},{0,2},{1,3},{2,4},{5,6},{5,7},{7,9},{6,8},{8,10}, {5,11},{6,12},{11,12},{11,13},{13,15},{12,14},{14,16} }; static const char* POSE_KPT_NAMES[] = { "nose","l_eye","r_eye","l_ear","r_ear", "l_sho","r_sho","l_elb","r_elb","l_wri","r_wri", "l_hip","r_hip","l_knee","r_knee","l_ank","r_ank" }; static void drawPoseDets(cv::Mat& frame, const vector& poses, const vector& falls, float s, bool show_skeleton = true) { for (auto& p : poses) { bool is_fall = false; for (auto& f : falls) if (f.bbox == p.bbox) { is_fall = true; break; } cv::Scalar color = is_fall ? cv::Scalar(0, 0, 255) : cv::Scalar(255, 255, 0); cv::Rect rb((int)(p.bbox.x * s), (int)(p.bbox.y * s), (int)(p.bbox.width * s), (int)(p.bbox.height * s)); cv::rectangle(frame, rb, color, 2); if (show_skeleton && p.keypoints.size() == 17) { // 关键点连线 for (auto& sk : POSE_SKELETON) { int a = sk[0], b = sk[1]; if (p.keypoint_conf.size() == 17 && p.keypoint_conf[a] > 0.3f && p.keypoint_conf[b] > 0.3f) { cv::line(frame, cv::Point((int)(p.keypoints[a].x * s), (int)(p.keypoints[a].y * s)), cv::Point((int)(p.keypoints[b].x * s), (int)(p.keypoints[b].y * s)), color, 2); } } // 关键点 for (int k = 0; k < 17; k++) { if (p.keypoint_conf[k] > 0.3f) { cv::circle(frame, cv::Point((int)(p.keypoints[k].x * s), (int)(p.keypoints[k].y * s)), 3, cv::Scalar(0, 255, 255), -1); } } } } for (auto& f : falls) { cv::Rect rb((int)(f.bbox.x * s), (int)(f.bbox.y * s), (int)(f.bbox.width * s), (int)(f.bbox.height * s)); cv::rectangle(frame, rb, cv::Scalar(0, 0, 255), 3); cv::putText(frame, "FALL", cv::Point(rb.x, rb.y - 6), cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(0, 0, 255), 2); } } static void drawPoseBanner(cv::Mat& shot, int channel, const PersonPose& fall, const string& action = "跌倒") { char tbuf[32]; time_t t = time(nullptr); struct tm tmv; localtime_r(&t, &tmv); strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", &tmv); string line1 = "!! " + action + " !! CH" + to_string(channel) + " " + tbuf; char line2[128]; snprintf(line2, sizeof(line2), "person %.0f%% 姿态%s", fall.confidence * 100, action.c_str()); const double fs = 0.45; const int thick = 1, lh = 18, pad = 4; vector lines = {line1, line2}; int w = 0, bh = pad * 2 + lh * (int)lines.size(); for (auto& l : lines) { int tw = 0, th2 = 0; cv::getTextSize(l, cv::FONT_HERSHEY_SIMPLEX, fs, thick, &th2); if (tw > w) w = tw; } w += pad * 2; if (w > shot.cols) w = shot.cols; cv::rectangle(shot, cv::Rect(0, 0, w, bh), cv::Scalar(0, 0, 255), -1); for (size_t i = 0; i < lines.size(); i++) cv::putText(shot, lines[i], cv::Point(pad, pad + 13 + (int)i * lh), cv::FONT_HERSHEY_SIMPLEX, fs, cv::Scalar(255, 255, 255), thick, cv::LINE_AA); } int main(int argc, char* argv[]) { signal(SIGINT, sigHandler); signal(SIGTERM, sigHandler); string config_path = (argc > 1) ? argv[1] : "config/config.json"; json cfg = loadJson(config_path); if (cfg.empty()) { cerr << "配置文件不存在: " << config_path << endl; return 1; } int skip_frames = cfg.value("skip_frames", 3); int web_port = cfg.value("web_port", 8080); string model_path = cfg.value("model_path", "models/yolov5s_rk3576_int8.rknn"); int input_size = cfg.value("input_size", 640); float conf_thresh = cfg.value("conf_thresh", 0.35f); float nms_thresh = cfg.value("nms_thresh", 0.45f); string fence_file = cfg.value("fence_file", "config/fences.json"); int alarm_retention_days = cfg.value("alarm_retention_days", 30); // ========== 本机画面输出(HDMI显示器) ========== string app_name = cfg.value("app_name", "钢包工位视频监测系统"); // 工位功能总开关(临时关闭可禁用钢包位/工位切换/空闲检测) bool enable_bucket = cfg.value("enable_bucket", true); // 两个独立显示器输出: 网格(普通通道) + 钢包位(工位左右摄像头) bool grid_output = cfg.value("grid_output", true); bool steel_output = cfg.value("steel_output", true); int hdmi_grid_cols = cfg.value("hdmi_grid_cols", 0); bool hdmi_fullscreen = cfg.value("hdmi_fullscreen", true); cout << "[Display] 网格输出: " << (grid_output ? "启用" : "禁用") << " 钢包位输出: " << (steel_output ? "启用" : "禁用") << " 工位功能: " << (enable_bucket ? "启用" : "禁用") << " 网格列=" << hdmi_grid_cols << " 全屏=" << (hdmi_fullscreen ? "是" : "否") << endl; // ========== 姿态识别(跌倒检测) ========== string pose_model_path = cfg.value("pose_model_path", "models/yolov5s-pose_rk3588_int8.rknn"); int pose_input_size = cfg.value("pose_input_size", 640); float pose_conf_thresh = cfg.value("pose_conf_thresh", 0.35f); float pose_nms_thresh = cfg.value("pose_nms_thresh", 0.45f); float fall_thresh = cfg.value("fall_thresh", 0.5f); int fall_frames = cfg.value("fall_frames", 3); bool target_enabled = cfg.value("target_enabled", true); bool pose_enabled = cfg.value("pose_enabled", false); vector pose_alarm_classes; if (cfg.contains("pose_alarm_classes") && cfg["pose_alarm_classes"].is_array()) for (auto& c : cfg["pose_alarm_classes"]) pose_alarm_classes.push_back(c.get()); if (pose_alarm_classes.empty()) pose_alarm_classes = {"person"}; // 跌倒触发联动继电器(默认复用围栏报警继电器 y01) vector fall_relays; if (cfg.contains("fall_relays") && cfg["fall_relays"].is_array()) for (auto& c : cfg["fall_relays"]) if (c.is_string()) fall_relays.push_back(c.get()); if (fall_relays.empty()) fall_relays = {"y01"}; cout << "[Target] 目标识别: " << (target_enabled ? "启用" : "禁用") << endl; cout << "[Pose] 姿态识别: " << (pose_enabled ? "启用" : "禁用") << " 模型=" << pose_model_path << " 跌倒阈值=" << fall_thresh << " 连续" << fall_frames << "帧确认" << endl; vector alarm_classes; if (cfg.contains("alarm_classes") && cfg["alarm_classes"].is_array()) for (auto& c : cfg["alarm_classes"]) alarm_classes.push_back(c.get()); if (alarm_classes.empty()) alarm_classes = {"person", "bicycle", "car", "motorcycle", "bus", "truck"}; bool reboot_enabled = false; int rb_cycle = 1; int rb_h = 3, rb_m = 30; string rb_done_date; if (cfg.contains("auto_reboot")) { auto& ar = cfg["auto_reboot"]; reboot_enabled = ar.value("enabled", false); rb_cycle = ar.value("cycle", 1); if (rb_cycle < 1) rb_cycle = 1; sscanf(ar.value("time", "03:30").c_str(), "%d:%d", &rb_h, &rb_m); if (rb_h < 0 || rb_h > 23 || rb_m < 0 || rb_m > 59) { rb_h = 3; rb_m = 30; } } cout << "[System] 定时重启: " << (reboot_enabled ? "启用" : "禁用") << " 周期" << rb_cycle << "天 时间 " << rb_h << ":" << (rb_m < 10 ? "0" : "") << rb_m << endl; RelayConfig rc; json rj = cfg.value("relay", json::object()); rc.host = rj.value("host", "192.168.1.253"); rc.port = rj.value("port", 1030); rc.device_addr = rj.value("device_addr", 1); // 脉冲时长统一用秒; 兼容旧配置pulse_ms rc.pulse_sec = max(1, rj.value("pulse_sec", rj.value("pulse_ms", 3000) / 1000)); int rearm_sec = clamp(rj.value("rearm_sec", 10), 1, 600); // 单回合上限: 报警开始后最多动作N秒(0=不限), 之后静默直到重新武装 int alarm_max_sec = clamp(rj.value("alarm_max_sec", 30), 0, 3600); // DI输入→输出联动绑定: {"x1":["y01","y02"],...} map> di_bindings; if (rj.contains("di_bindings") && rj["di_bindings"].is_object()) for (auto& [k, v] : rj["di_bindings"].items()) if (v.is_array()) for (auto& c : v) if (c.is_string()) di_bindings[k].push_back(c.get()); // DI/DO通道别名: {"x1":"大门雷达","y02":"声光报警器",...} map rly_alias; if (rj.contains("aliases") && rj["aliases"].is_object()) for (auto& [k, v] : rj["aliases"].items()) if (v.is_string()) rly_alias[k] = v.get(); // 输出通道模式: [{"channel":"y01","mode":"normal|cycle","interval_sec":1},...] array ycfg; if (rj.contains("y_config") && rj["y_config"].is_array()) for (auto& e : rj["y_config"]) { string ch = e.value("channel", ""); if (ch.size() == 3 && ch[0] == 'y') { int idx = (ch[1] - '0') * 10 + (ch[2] - '0'); if (idx >= 1 && idx <= 8) { string m = e.value("mode", "normal"); ycfg[idx - 1].mode = (m == "cycle") ? 1 : 0; ycfg[idx - 1].interval_sec = e.value("interval_sec", 1.0); } } } cout << "========================================" << endl; cout << " ZoneAlarm - RK3576 NPU 车辆检测电子围栏" << endl; cout << " 推理: RKNN NPU" << endl; cout << " 解码: MPP 硬解" << endl; cout << " Web端口: " << web_port << endl; cout << " 报警策略: 脉冲" << rc.pulse_sec << "秒/回合上限" << alarm_max_sec << "秒/重新武装" << rearm_sec << "秒" << endl; cout << "========================================" << endl; // NPU 核心分配: 检测绑 Core0, 姿态绑 Core1, 两模型并行推理 int det_core = cfg.value("det_core_mask", 1); // 默认 Core0 int pose_core = cfg.value("pose_core_mask", 2); // 默认 Core1 YoloDetector detector; if (target_enabled && !detector.load(model_path, input_size, conf_thresh, nms_thresh, det_core)) { cerr << "RKNN模型加载失败!" << endl; return 1; } cout << "[RKNN] 检测核心掩码: " << det_core << endl; PoseDetector poseDetector; FallAnalyzer fallAnalyzer; fallAnalyzer.setThreshold(fall_thresh); bool pose_hand_raise = cfg.value("pose_hand_raise", true); bool pose_crouch = cfg.value("pose_crouch", true); fallAnalyzer.setHandRaiseEnabled(pose_hand_raise); fallAnalyzer.setCrouchEnabled(pose_crouch); cout << "[Pose] 动作类型: 跌倒/倒地(固定)" << " 举手呼救=" << (pose_hand_raise ? "开" : "关") << " 异常蹲坐=" << (pose_crouch ? "开" : "关") << endl; bool pose_ok = false; if (pose_enabled) { if (poseDetector.load(pose_model_path, pose_input_size, pose_conf_thresh, pose_nms_thresh, pose_core)) { pose_ok = true; cout << "[Pose] 姿态模型加载成功, 跌倒检测已就绪, 核心掩码: " << pose_core << endl; } else { cerr << "[Pose] 姿态模型加载失败: " << pose_model_path << " (继续使用纯围栏检测)" << endl; } } FenceEngine fenceEngine; fenceEngine.loadFences(fence_file); RelayCtrl relay; relay.setConfig(rc); relay.setModes(ycfg); relay.start(); AlarmHistory alarmHistory; alarmHistory.init("data/alarms.db", "data/screenshots"); alarmHistory.cleanup(alarm_retention_days); cout << "[Alarm] 历史清理: 保留" << alarm_retention_days << "天" << endl; // ========== 工位管理模块 ========== BucketManager bucketManager; bucketManager.loadConfig(cfg); IdleDetector idleDetector(cfg.value("idle_timeout", 5)); // RFID 或 Modbus string module_type = cfg.value("module_type", "RFID"); unique_ptr rfidClient; unique_ptr modbusClient; if (module_type == "RFID" && cfg.contains("rfid")) { RfidConfig rfidCfg; auto& r = cfg["rfid"]; rfidCfg.ip = r.value("ip", ""); rfidCfg.port = r.value("port", 60000); rfidCfg.stability_time = r.value("stability_time", 5); rfidCfg.stability_count = r.value("stability_count", 3); rfidClient = make_unique(); rfidClient->setConfig(rfidCfg); rfidClient->setTagCallback([&](const string& tag) { idleDetector.feed(tag); if (!tag.empty() && enable_bucket) { bucketManager.checkAutoSwitch(tag); } }); rfidClient->setStateCallback([&](bool online) { cout << "[RFID] 状态: " << (online ? "在线" : "离线") << endl; }); if (rfidClient->connect()) { cout << "[RFID] 初始化成功" << endl; } } else if (module_type == "Modbus" && cfg.contains("modbus")) { ModbusConfig modbusCfg; auto& m = cfg["modbus"]; modbusCfg.ip = m.value("ip", ""); modbusCfg.port = m.value("port", 502); modbusCfg.frequency = m.value("frequency", 100); modbusCfg.stability_count = m.value("stability_count", 3); modbusClient = make_unique(); modbusClient->setConfig(modbusCfg); modbusClient->setDataCallback([&](const vector& coils) { idleDetector.feed(coils); if (enable_bucket) bucketManager.checkAutoSwitch(coils); }); modbusClient->setStateCallback([&](bool online) { cout << "[Modbus] 状态: " << (online ? "在线" : "离线") << endl; }); if (modbusClient->connect()) { cout << "[Modbus] 初始化成功" << endl; } } // ========== 通道容器 ========== static const int kMaxChannels = 8; mutex channels_mtx; vector> channels; // 钢包位专用通道(id=100左, 101右), 跟随工位切换, 独立于普通rtsp通道 static const int kBucketLeftId = 100; static const int kBucketRightId = 101; mutex bucket_channels_mtx; vector> bucket_channels; WebServer web; DisplayOutput displayGrid; // 普通通道网格(自动布局) DisplayOutput displayBucket; // 钢包位(当前工位左右摄像头) web.setPort(web_port); // 登录凭据(默认admin/admin), 持久化于config.json的web_auth string web_user = "admin", web_pass = "admin"; { json wa = cfg.value("web_auth", json::object()); web_user = wa.value("username", "admin"); web_pass = wa.value("password", "admin"); } web.setAuthHooks({ [&](const string& u, const string& p) { return u == web_user && p == web_pass; }, [&]() -> string { return web_user; }, [&](const string& oldp, const string& u, const string& np) -> bool { if (oldp != web_pass || np.empty()) return false; if (!u.empty()) web_user = u; web_pass = np; json full = loadJson(config_path); full["web_auth"] = {{"username", web_user}, {"password", web_pass}}; saveJson(config_path, full); cout << "[System] 登录凭证已更新: 用户 " << web_user << endl; return true; } }); auto makeChannel = [&](int id, const string& name, const string& url) { auto ch = make_shared(); ch->id = id; ch->skip_frames = skip_frames; ch->pose_running = pose_ok; // 姿态识别默认开启(模型加载成功时) ch->rga.init(input_size, input_size); ch->capture = make_shared(id, url, name); return ch; }; auto registerChannelProviders = [&](shared_ptr ch) { web.setFrameProvider(ch->id, [ch]() -> cv::Mat { lock_guard lk(ch->mtx); if (!ch->display_frame.empty()) return ch->display_frame.clone(); return cv::Mat(360, 640, CV_8UC3, cv::Scalar(0, 0, 0)); }); web.setFrameSeqProvider(ch->id, [ch]() -> uint64_t { return ch->disp_seq.load(std::memory_order_relaxed); }); }; json rtsp_list = cfg.value("rtsp", json::array()); for (int i = 0; i < (int)rtsp_list.size() && i < kMaxChannels; i++) { auto ch = makeChannel(rtsp_list[i].value("id", i), rtsp_list[i].value("name", ""), rtsp_list[i].value("url", "")); ch->capture->start(); registerChannelProviders(ch); lock_guard lk(channels_mtx); channels.push_back(std::move(ch)); } cout << "[Main] 视频通道: " << channels.size() << "/" << kMaxChannels << " 路" << endl; // 钢包位专用通道(占位创建, 工位激活时替换为工位左右摄像头) if (enable_bucket) { auto chL = makeChannel(kBucketLeftId, "钢包位左", ""); auto chR = makeChannel(kBucketRightId, "钢包位右", ""); lock_guard lk(bucket_channels_mtx); bucket_channels.push_back(chL); bucket_channels.push_back(chR); cout << "[Main] 钢包位专用通道: 左CH" << kBucketLeftId << " 右CH" << kBucketRightId << endl; } // ========== 工位切换回调 ========== // 钢包位专用通道跟随工位切换(左右摄像头), 普通rtsp通道不受影响 bucketManager.setActivateCallback([&](const BucketInfo* old, const BucketInfo* cur) { if (!enable_bucket) { idleDetector.reset(); return; } cerr << "[Bucket] 回调触发: old=" << (old ? old->description : "null") << " cur=" << (cur ? cur->description : "null") << endl; // 停止旧工位钢包位摄像头 if (old && old->id >= 0) { lock_guard lk(bucket_channels_mtx); for (auto& ch : bucket_channels) ch->capture->stop(); cerr << "[Bucket] 停止钢包位摄像头" << endl; } // 启动新工位钢包位摄像头 if (cur && cur->id >= 0) { lock_guard lk(bucket_channels_mtx); cerr << "[Bucket] 激活工位: " << cur->description << endl; for (auto& ch : bucket_channels) { if (ch->id == kBucketLeftId && !cur->leftCamera.url.empty()) { ch->capture->stop(); ch->capture = make_shared(kBucketLeftId, cur->leftCamera.url, cur->description + "_左"); ch->capture->start(); cerr << "[Bucket] 钢包位左摄像头: " << cur->leftCamera.url << endl; } if (ch->id == kBucketRightId && !cur->rightCamera.url.empty()) { ch->capture->stop(); ch->capture = make_shared(kBucketRightId, cur->rightCamera.url, cur->description + "_右"); ch->capture->start(); cerr << "[Bucket] 钢包位右摄像头: " << cur->rightCamera.url << endl; } } } else { cerr << "[Bucket] 工位取消激活" << endl; } idleDetector.reset(); cerr << "[Bucket] 回调完成" << endl; }); // ========== Web API: 工位管理 ========== web.setGetHandler("/api/status", [&]() -> string { json s; s["running"] = true; s["model"] = model_path; s["npu"] = "RKNN"; s["skip_frames"] = skip_frames; s["conf_thresh"] = conf_thresh; s["alarm_retention_days"] = alarm_retention_days; s["alarm_classes"] = alarm_classes; s["target_enabled"] = target_enabled; s["pose_enabled"] = pose_ok; s["pose_cfg_enabled"] = pose_enabled; s["pose_model"] = pose_model_path; s["fall_thresh"] = fall_thresh; s["fall_frames"] = fall_frames; s["fall_relays"] = fall_relays; s["grid_output"] = grid_output; s["steel_output"] = steel_output; json arr = json::array(); { lock_guard lk(channels_mtx); for (auto& ch : channels) { json c; c["id"] = ch->id; c["name"] = ch->capture->name(); c["url"] = ch->capture->url(); c["connected"] = ch->capture->isRunning(); c["online"] = ch->capture->online(); c["frame_age_ms"] = ch->capture->frameAgeMs(); c["frames"] = ch->capture->framesDecoded(); c["width"] = ch->capture->frameWidth(); c["height"] = ch->capture->frameHeight(); c["pose_running"] = ch->pose_running; c["fall_count"] = ch->pose_fall_count; c["falls"] = (int)ch->fall_poses.size(); arr.push_back(c); } } s["channels"] = arr; return s.dump(); }); // ========== Web API: 工位管理 ========== web.setGetHandler("/api/buckets", [&]() -> string { json r; r["buckets"] = bucketManager.saveConfig(); r["active_id"] = bucketManager.getActiveBucketId(); r["mode"] = bucketManager.getSwitchMode() == SwitchMode::AUTO ? "auto" : "hand"; return r.dump(); }); web.setPostHandler("/api/buckets", [&](const string& body) -> string { try { json j = json::parse(body); if (j.contains("buckets") && j["buckets"].is_array()) { // 保存工位列表 json full = loadJson(config_path); full["buckets"] = j["buckets"]; saveJson(config_path, full); bucketManager.loadConfig(full); cout << "[Bucket] 配置已保存" << endl; } if (j.contains("mode")) { string mode = j["mode"].get(); bucketManager.setSwitchMode(mode == "auto" ? SwitchMode::AUTO : SwitchMode::HAND); } if (j.contains("activate")) { int id = j["activate"].get(); bucketManager.activateBucket(id); } return R"({"ok":true})"; } catch (exception& e) { return string(R"({"ok":false,"error":")") + e.what() + R"("})"; } }); web.setGetHandler("/api/buckets/status", [&]() -> string { json r; r["module_type"] = module_type; if (rfidClient) { r["rfid"]["online"] = rfidClient->isOnline(); r["rfid"]["last_tag"] = rfidClient->lastTag(); } if (modbusClient) { r["modbus"]["online"] = modbusClient->isOnline(); } r["active_id"] = bucketManager.getActiveBucketId(); r["mode"] = bucketManager.getSwitchMode() == SwitchMode::AUTO ? "auto" : "hand"; return r.dump(); }); web.setPostHandler("/api/buckets/activate", [&](const string& body) -> string { try { json j = json::parse(body); int id = j.value("id", -1); bucketManager.setSwitchMode(SwitchMode::HAND); bucketManager.activateBucket(id); return R"({"ok":true})"; } catch (...) { return R"({"ok":false})"; } }); web.setPostHandler("/api/buckets/mode", [&](const string& body) -> string { try { json j = json::parse(body); string mode = j.value("mode", "auto"); bucketManager.setSwitchMode(mode == "auto" ? SwitchMode::AUTO : SwitchMode::HAND); return R"({"ok":true})"; } catch (...) { return R"({"ok":false})"; } }); // ========== Web API: 通讯配置 ========== web.setGetHandler("/api/module/config", [&]() -> string { json r; r["module_type"] = module_type; if (rfidClient) r["rfid"] = { {"ip", rfidClient->getConfig().ip}, {"port", rfidClient->getConfig().port}, {"stability_time", rfidClient->getConfig().stability_time}, {"stability_count", rfidClient->getConfig().stability_count} }; if (modbusClient) r["modbus"] = { {"ip", modbusClient->getConfig().ip}, {"port", modbusClient->getConfig().port}, {"frequency", modbusClient->getConfig().frequency}, {"stability_count", modbusClient->getConfig().stability_count} }; r["idle_timeout"] = cfg.value("idle_timeout", 5); return r.dump(); }); web.setPostHandler("/api/module/config", [&](const string& body) -> string { try { json j = json::parse(body); json full = loadJson(config_path); if (j.contains("module_type")) { module_type = j["module_type"].get(); full["module_type"] = module_type; } if (j.contains("rfid")) { full["rfid"] = j["rfid"]; if (rfidClient) { RfidConfig rfidCfg; rfidCfg.ip = j["rfid"].value("ip", ""); rfidCfg.port = j["rfid"].value("port", 60000); rfidCfg.stability_time = j["rfid"].value("stability_time", 5); rfidCfg.stability_count = j["rfid"].value("stability_count", 3); rfidClient->setConfig(rfidCfg); } } if (j.contains("modbus")) { full["modbus"] = j["modbus"]; if (modbusClient) { ModbusConfig modbusCfg; modbusCfg.ip = j["modbus"].value("ip", ""); modbusCfg.port = j["modbus"].value("port", 502); modbusCfg.frequency = j["modbus"].value("frequency", 100); modbusCfg.stability_count = j["modbus"].value("stability_count", 3); modbusClient->setConfig(modbusCfg); } } if (j.contains("idle_timeout")) { full["idle_timeout"] = j["idle_timeout"]; idleDetector.setTimeout(j["idle_timeout"].get()); } saveJson(config_path, full); cout << "[Config] 通讯配置已保存" << endl; return R"({"ok":true})"; } catch (exception& e) { return string(R"({"ok":false,"error":")") + e.what() + R"("})"; } }); web.setGetHandler("/api/fences", [&]() -> string { json r = json::object(); vector ids; { lock_guard lk(channels_mtx); for (auto& ch : channels) ids.push_back(ch->id); } for (int cid : ids) { auto fs = fenceEngine.getFences(cid); json fa = json::array(); for (auto& f : fs) { json fj; fj["id"] = f.id; fj["name"] = f.name; fj["active"] = f.active; json pts = json::array(); for (auto& p : f.polygon) pts.push_back({p.x, p.y}); fj["polygon"] = pts; json rl = json::array(); for (auto& r : f.relays) rl.push_back(r); fj["relays"] = rl; fa.push_back(fj); } r[to_string(cid)] = fa; } return r.dump(); return r.dump(); }); web.setPostHandler("/api/fences", [&](const string& body) -> string { try { json j = json::parse(body); vector ids; { lock_guard lk(channels_mtx); for (auto& ch : channels) ids.push_back(ch->id); } for (int cid : ids) { string key = to_string(cid); if (j.contains(key)) { vector fences; for (auto& fj : j[key]) { FenceRegion fr; fr.id = fj.value("id", 0); fr.name = fj.value("name", ""); fr.active = fj.value("active", true); if (fj.contains("relays") && fj["relays"].is_array()) for (auto& r : fj["relays"]) if (r.is_string()) fr.relays.push_back(r.get()); for (auto& pt : fj["polygon"]) fr.polygon.push_back(cv::Point2f(pt[0], pt[1])); fences.push_back(fr); } fenceEngine.setFences(cid, fences); } } fenceEngine.saveFences(fence_file); return R"({"ok":true})"; } catch (exception& e) { return string(R"({"ok":false,"error":")") + e.what() + R"("})"; } }); auto validYChannel = [](const string& ch) -> bool { if (ch.size() != 3 || ch[0] != 'y') return false; if (!isdigit((unsigned char)ch[1]) || !isdigit((unsigned char)ch[2])) return false; int n = (ch[1] - '0') * 10 + (ch[2] - '0'); return n >= 1 && n <= 8; }; // 持久化输出通道模式到 config.json auto saveYConfig = [&](const array& modes) { json full = loadJson(config_path); if (!full.contains("relay") || !full["relay"].is_object()) full["relay"] = json::object(); json arr = json::array(); for (int i = 0; i < 8; i++) { arr.push_back({ {"channel", "y0" + to_string(i + 1)}, {"mode", modes[i].mode == 1 ? "cycle" : "normal"}, {"interval_sec", modes[i].interval_sec} }); } full["relay"]["y_config"] = arr; saveJson(config_path, full); }; web.setPostHandler("/api/relay", [&](const string& body) -> string { try { json j = json::parse(body); string ch = j.value("channel", ""); int st = j.value("state", -1); if (!validYChannel(ch) || (st != 0 && st != 1)) return R"({"ok":false,"error":"bad channel/state"})"; int idx = (ch[1] - '0') * 10 + (ch[2] - '0') - 1; if (idx >= relay.getOutCount()) return R"({"ok":false,"error":"no such channel"})"; // 周期模式下的通道被手动控制时, 自动切回正常模式 auto modes = relay.getModes(); if (modes[idx].mode == 1) { modes[idx].mode = 0; relay.setModes(modes); saveYConfig(modes); cout << "[Relay] " << ch << " 手动控制, 已切回正常模式" << endl; } if (relay.setChannel(ch, st)) { alarmHistory.recordAction("手动", ch + (st ? " 手动闭合" : " 手动断开"), ch); return R"({"ok":true})"; } return R"({"ok":false,"error":"offline"})"; } catch (...) {} return R"({"ok":false})"; }); web.setGetHandler("/api/relay/status", [&]() -> string { json s; s["online"] = relay.isOnline(); auto xs = relay.getX(); auto ys = relay.getY(); json xa = json::array(), ya = json::array(); for (int i = 0; i < 8; i++) { xa.push_back(xs[i]); ya.push_back(ys[i]); } s["x"] = xa; s["y"] = ya; s["nx"] = relay.getInCount(); // 实际输入路数 s["ny"] = relay.getOutCount(); // 实际输出路数 auto modes = relay.getModes(); json ma = json::array(), ca = json::array(); for (int i = 0; i < 8; i++) { ma.push_back(modes[i].mode == 1 ? "cycle" : "normal"); ca.push_back(modes[i].interval_sec); } s["ymode"] = ma; s["ycyc"] = ca; RelayConfig c = relay.getConfig(); s["host"] = c.host; s["port"] = c.port; s["device_addr"] = c.device_addr; s["pulse_sec"] = c.pulse_sec; s["rearm_sec"] = rearm_sec; s["alarm_max_sec"] = alarm_max_sec; { json al = json::object(); for (auto& [k, v] : rly_alias) al[k] = v; s["alias"] = al; } return s.dump(); }); web.setPostHandler("/api/relay/mode", [&](const string& body) -> string { try { json j = json::parse(body); if (!j.contains("items") || !j["items"].is_array()) return R"({"ok":false,"error":"items required"})"; array modes = relay.getModes(); for (auto& e : j["items"]) { string ch = e.value("channel", ""); if (ch.size() != 3 || ch[0] != 'y') continue; int idx = (ch[1] - '0') * 10 + (ch[2] - '0'); if (idx < 1 || idx > 8) continue; string m = e.value("mode", ""); if (m != "normal" && m != "cycle") continue; modes[idx - 1].mode = (m == "cycle") ? 1 : 0; if (e.contains("interval_sec")) modes[idx - 1].interval_sec = e["interval_sec"].get(); } relay.setModes(modes); saveYConfig(modes); cout << "[Relay] 输出模式更新" << endl; return R"({"ok":true})"; } catch (...) { return R"({"ok":false})"; } }); web.setPostHandler("/api/relay/config", [&](const string& body) -> string { try { json j = json::parse(body); RelayConfig c = relay.getConfig(); if (j.contains("host") && j["host"].is_string()) { string h = j["host"].get(); if (!h.empty()) c.host = h; } if (j.contains("port")) { int p = j["port"].get(); if (p >= 1 && p <= 65535) c.port = p; } if (j.contains("device_addr")) { int a = j["device_addr"].get(); if (a >= 0 && a <= 254) c.device_addr = a; } if (j.contains("pulse_sec")) { int v = j["pulse_sec"].get(); if (v >= 1 && v <= 3600) c.pulse_sec = v; } if (j.contains("rearm_sec")) { int v = j["rearm_sec"].get(); if (v >= 1 && v <= 600) rearm_sec = v; } if (j.contains("alarm_max_sec")) { int v = j["alarm_max_sec"].get(); if (v >= 0 && v <= 3600) alarm_max_sec = v; } rc = c; relay.setConfig(c); json full = loadJson(config_path); if (!full.contains("relay") || !full["relay"].is_object()) full["relay"] = json::object(); full["relay"]["host"] = c.host; full["relay"]["port"] = c.port; full["relay"]["device_addr"] = c.device_addr; full["relay"]["pulse_sec"] = c.pulse_sec; full["relay"]["rearm_sec"] = rearm_sec; full["relay"]["alarm_max_sec"] = alarm_max_sec; full["relay"].erase("channel"); full["relay"].erase("pulse_ms"); full["relay"].erase("alarm_mode"); full["relay"].erase("di_pulse_sec"); saveJson(config_path, full); cout << "[Relay] 配置更新: " << c.host << ":" << c.port << " 地址" << c.device_addr << " 脉冲" << c.pulse_sec << "秒" << " 回合上限" << alarm_max_sec << "秒" << " 重新武装" << rearm_sec << "秒" << endl; return R"({"ok":true})"; } catch (...) { return R"({"ok":false})"; } }); web.setGetHandler("/api/relay/bindings", [&]() -> string { json b = json::object(); for (int i = 1; i <= 8; i++) { string k = "x" + to_string(i); json arr = json::array(); auto it = di_bindings.find(k); if (it != di_bindings.end()) for (auto& c : it->second) arr.push_back(c); b[k] = arr; } return b.dump(); }); web.setPostHandler("/api/relay/bindings", [&](const string& body) -> string { try { json j = json::parse(body); map> nb; for (auto& [k, v] : j.items()) { if (!v.is_array()) continue; vector chans; for (auto& c : v) if (c.is_string()) { string ch = c.get(); if (validYChannel(ch)) chans.push_back(ch); } if (!chans.empty()) nb[k] = chans; } di_bindings = nb; json full = loadJson(config_path); if (!full.contains("relay") || !full["relay"].is_object()) full["relay"] = json::object(); json db = json::object(); for (auto& [k, v] : di_bindings) db[k] = v; full["relay"]["di_bindings"] = db; saveJson(config_path, full); return R"({"ok":true})"; } catch (...) { return R"({"ok":false})"; } }); // ---- 通道别名 ---- auto normAliasKey = [](string k) -> string { if (k.empty()) return ""; k[0] = (char)tolower((unsigned char)k[0]); if (k[0] != 'x' && k[0] != 'y') return ""; int n = 0; try { n = stoi(k.substr(1)); } catch (...) { return ""; } if (n < 1 || n > 8) return ""; if (k[0] == 'y') return "y" + to_string(n).insert(0, n < 10 ? "0" : ""); return "x" + to_string(n); }; web.setGetHandler("/api/relay/aliases", [&]() -> string { json a = json::object(); for (auto& [k, v] : rly_alias) a[k] = v; return a.dump(); }); web.setPostHandler("/api/relay/aliases", [&](const string& body) -> string { try { json j = json::parse(body); map na; for (auto& [k, v] : j.items()) if (v.is_string()) { string key = normAliasKey(k); if (!key.empty()) na[key] = v.get(); } rly_alias = na; json full = loadJson(config_path); if (!full.contains("relay") || !full["relay"].is_object()) full["relay"] = json::object(); json aj = json::object(); for (auto& [k, v] : rly_alias) aj[k] = v; full["relay"]["aliases"] = aj; saveJson(config_path, full); cout << "[Relay] 别名已更新: " << rly_alias.size() << " 条" << endl; return R"({"ok":true})"; } catch (...) { return R"({"ok":false})"; } }); // ---- 继电器动作历史 ---- web.setGetHandler("/api/relay/actions", [&]() -> string { json arr = json::array(); auto acts = alarmHistory.getActions(200); for (auto& a : acts) { json aj; aj["id"] = a.id; aj["timestamp"] = a.timestamp; aj["source"] = a.source; aj["detail"] = a.detail; aj["channels"] = a.channels; arr.push_back(aj); } json r; r["items"] = arr; return r.dump(); }); web.setPostHandler("/api/relay/actions/clear", [&](const string&) -> string { alarmHistory.clearActions(); return R"({"ok":true})"; }); // ---------- 通道管理: 增删改查 + 热插拔 ---------- auto persistRtspList = [&]() { json full = loadJson(config_path); json arr = json::array(); { lock_guard lk(channels_mtx); for (auto& ch : channels) { arr.push_back({{"id", ch->id}, {"name", ch->capture->name()}, {"url", ch->capture->url()}}); } } full["rtsp"] = arr; saveJson(config_path, full); }; web.setPostHandler("/api/channels/save", [&](const string& body) -> string { try { json j = json::parse(body); string name = j.value("name", ""); string url = j.value("url", ""); int id = j.value("id", -1); if (url.rfind("rtsp://", 0) != 0) return R"({"ok":false,"error":"URL必须以rtsp://开头"})"; if (name.empty()) name = "通道"; shared_ptr old; auto nu = makeChannel(id, name, url); { lock_guard lk(channels_mtx); if (id >= 0) { for (auto& ch : channels) if (ch->id == id) { old = ch; break; } if (!old) return R"({"ok":false,"error":"通道不存在"})"; } else { if ((int)channels.size() >= kMaxChannels) return R"({"ok":false,"error":"已达最大通道数8"})"; int max_id = -1; for (auto& ch : channels) max_id = std::max(max_id, ch->id); nu->id = id = max_id + 1; } nu->capture->start(); for (auto& c : channels) if (c.get() == old.get()) { c = nu; break; } if (!old) channels.push_back(nu); } registerChannelProviders(nu); if (old) old->capture->stop(); // 锁外停旧流(join可能耗时) persistRtspList(); cout << "[Channel] " << (old ? "修改" : "添加") << " CH" << id << ": " << url << endl; json r; r["ok"] = true; r["id"] = id; return r.dump(); } catch (exception& e) { return string(R"({"ok":false,"error":")") + e.what() + R"("})"; } }); web.setPostHandler("/api/channels/delete", [&](const string& body) -> string { try { json j = json::parse(body); int id = j.value("id", -1); shared_ptr victim; { lock_guard lk(channels_mtx); for (auto it = channels.begin(); it != channels.end(); ++it) { if ((*it)->id == id) { victim = *it; channels.erase(it); break; } } } if (!victim) return R"({"ok":false,"error":"通道不存在"})"; web.removeFrameProvider(id); victim->capture->stop(); // 锁外停止并回收解码线程 persistRtspList(); cout << "[Channel] 删除 CH" << id << endl; return R"({"ok":true})"; } catch (...) { return R"({"ok":false})"; } }); web.setPostHandler("/api/channels/test", [&](const string& body) -> string { try { json j = json::parse(body); string url = j.value("url", ""); if (url.rfind("rtsp://", 0) != 0) return R"({"ok":false,"error":"URL必须以rtsp://开头"})"; // 解析 host[:port] (忽略用户信息与路径) string rest = url.substr(7); size_t at = rest.find('@'); if (at != string::npos) rest = rest.substr(at + 1); size_t slash = rest.find('/'); if (slash != string::npos) rest = rest.substr(0, slash); string host = rest; int port = 554; size_t colon = rest.find(':'); if (colon != string::npos) { host = rest.substr(0, colon); port = stoi(rest.substr(colon + 1)); } int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) return R"({"ok":false,"error":"socket失败"})"; int fl = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, fl | O_NONBLOCK); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); inet_pton(AF_INET, host.c_str(), &addr.sin_addr); bool ok = false; string err; if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == 0) { ok = true; } else if (errno == EINPROGRESS) { struct pollfd pfd = {fd, POLLOUT, 0}; if (poll(&pfd, 1, 3000) > 0) { int e = 0; socklen_t el = sizeof(e); getsockopt(fd, SOL_SOCKET, SO_ERROR, &e, &el); ok = (e == 0); if (!ok) err = strerror(e); } else { err = "连接超时"; } } else { err = strerror(errno); } close(fd); json r; r["ok"] = ok; r["host"] = host + ":" + to_string(port); if (!err.empty()) r["error"] = err; cout << "[Channel] 连通测试 " << host << ":" << port << " -> " << (ok ? "OK" : err) << endl; return r.dump(); } catch (exception& e) { return string(R"({"ok":false,"error":")") + e.what() + R"("})"; } }); web.setPostHandler("/api/config", [&](const string& body) -> string { try { json j = json::parse(body); json full = loadJson(config_path); if (j.contains("skip_frames")) { full["skip_frames"] = j["skip_frames"]; skip_frames = j["skip_frames"].get(); lock_guard lk(channels_mtx); for (auto& ch : channels) ch->skip_frames = skip_frames; } if (j.contains("conf_thresh")) full["conf_thresh"] = j["conf_thresh"]; if (j.contains("target_enabled")) { bool en = j["target_enabled"].get(); full["target_enabled"] = en; target_enabled = en; cout << "[Target] 目标识别: " << (en ? "启用" : "禁用") << " (重启后生效)" << endl; } if (j.contains("pose_enabled")) { bool en = j["pose_enabled"].get(); full["pose_enabled"] = en; pose_enabled = en; cout << "[Pose] 姿态识别: " << (en ? "启用" : "禁用") << " (重启后生效)" << endl; } if (j.contains("grid_output")) { bool v = j["grid_output"].get(); full["grid_output"] = v; grid_output = v; cout << "[Display] 网格输出: " << (v ? "启用" : "禁用") << " (重启后生效)" << endl; } if (j.contains("steel_output")) { bool v = j["steel_output"].get(); full["steel_output"] = v; steel_output = v; cout << "[Display] 钢包位输出: " << (v ? "启用" : "禁用") << " (重启后生效)" << endl; } if (j.contains("alarm_retention_days")) { int v = j["alarm_retention_days"].get(); if (v < 1) v = 1; full["alarm_retention_days"] = v; alarm_retention_days = v; } if (j.contains("alarm_classes") && j["alarm_classes"].is_array()) { vector ac; for (auto& c : j["alarm_classes"]) if (c.is_string()) ac.push_back(c.get()); full["alarm_classes"] = ac; alarm_classes = ac; } if (j.contains("auto_reboot") && j["auto_reboot"].is_object()) { auto& ar = j["auto_reboot"]; if (!full.contains("auto_reboot") || !full["auto_reboot"].is_object()) full["auto_reboot"] = json::object(); if (ar.contains("enabled")) { bool en = ar["enabled"].get(); full["auto_reboot"]["enabled"] = en; reboot_enabled = en; } if (ar.contains("cycle")) { int c = ar["cycle"].get(); if (c < 1) c = 1; if (c > 365) c = 365; full["auto_reboot"]["cycle"] = c; rb_cycle = c; } if (ar.contains("time") && ar["time"].is_string()) { int h = -1, m = -1; sscanf(ar["time"].get().c_str(), "%d:%d", &h, &m); if (h >= 0 && h <= 23 && m >= 0 && m <= 59) { char norm[8]; snprintf(norm, sizeof(norm), "%02d:%02d", h, m); full["auto_reboot"]["time"] = norm; rb_h = h; rb_m = m; rb_done_date.clear(); } } cout << "[System] 定时重启更新: " << (reboot_enabled ? "启用" : "禁用") << " 周期" << rb_cycle << "天 时间" << rb_h << ":" << (rb_m < 10 ? "0" : "") << rb_m << endl; } saveJson(config_path, full); return R"({"ok":true})"; } catch (...) { return R"({"ok":false})"; } }); web.setGetHandler("/api/system", [&]() -> string { json s; s["reboot_enabled"] = reboot_enabled; s["reboot_cycle"] = rb_cycle; char tbuf[8]; snprintf(tbuf, sizeof(tbuf), "%02d:%02d", rb_h, rb_m); s["reboot_time"] = tbuf; return s.dump(); }); web.setPostHandler("/api/reboot", [&](const string&) -> string { cout << "[System] Web触发立即重启" << endl; cout.flush(); std::thread([]() { this_thread::sleep_for(chrono::milliseconds(500)); sync(); system("(sleep 1; systemctl reboot -i) >/dev/null 2>&1 &"); fflush(nullptr); _exit(0); }).detach(); return R"({"ok":true})"; }); web.setGetHandlerQ("/api/alarms", [&](const map& q) -> string { auto geti = [&](const string& k, int def) -> int { auto it = q.find(k); if (it == q.end() || it->second.empty()) return def; return atoi(it->second.c_str()); }; auto gets = [&](const string& k) -> string { auto it = q.find(k); return it == q.end() ? string() : it->second; }; int limit = clamp(geti("limit", 200), 1, 500); int ch = geti("channel", -1); string start = gets("start"); string end = gets("end"); json arr = json::array(); auto alarms = alarmHistory.getAlarms(limit, 0, ch, start, end); for (auto& a : alarms) { json aj; aj["id"] = a.id; aj["timestamp"] = a.timestamp; aj["channel"] = a.channel; aj["fence_name"] = a.fence_name; aj["class_name"] = a.class_name; aj["confidence"] = a.confidence; aj["screenshot"] = a.screenshot_path; arr.push_back(aj); } json r; r["total"] = alarmHistory.getAlarmCount(ch, start, end); r["items"] = arr; return r.dump(); }); web.setPostHandler("/api/alarms/delete", [&](const string& body) -> string { try { json j = json::parse(body); int64_t id = j.value("id", 0); if (alarmHistory.deleteAlarm(id)) return R"({"ok":true})"; } catch (...) {} return R"({"ok":false})"; }); web.setPostHandler("/api/alarms/clear", [&](const string& body) -> string { alarmHistory.clearAll(); return R"({"ok":true})"; }); web.setGetHandler("/api/alarms/count", [&]() -> string { json r; r["count"] = alarmHistory.getAlarmCount(); return r.dump(); }); // ========== 姿态报警历史 API ========== web.setGetHandlerQ("/api/pose/alarms", [&](const map& q) -> string { auto geti = [&](const string& k, int def) -> int { auto it = q.find(k); if (it == q.end() || it->second.empty()) return def; return atoi(it->second.c_str()); }; auto gets = [&](const string& k) -> string { auto it = q.find(k); return it == q.end() ? string() : it->second; }; int limit = clamp(geti("limit", 200), 1, 500); int ch = geti("channel", -1); string action = gets("action"); string start = gets("start"); string end = gets("end"); json arr = json::array(); auto list = alarmHistory.getPoseAlarms(limit, 0, ch, action, start, end); for (auto& a : list) { json aj; aj["id"] = a.id; aj["timestamp"] = a.timestamp; aj["channel"] = a.channel; aj["action"] = a.action; aj["score"] = a.score; aj["screenshot"] = a.screenshot_path; arr.push_back(aj); } json r; r["total"] = alarmHistory.getPoseAlarmCount(ch, action, start, end); r["items"] = arr; return r.dump(); }); web.setPostHandler("/api/pose/alarms/delete", [&](const string& body) -> string { try { json j = json::parse(body); int64_t id = j.value("id", 0); if (alarmHistory.deletePoseAlarm(id)) return R"({"ok":true})"; } catch (...) {} return R"({"ok":false})"; }); web.setPostHandler("/api/pose/alarms/clear", [&](const string&) -> string { alarmHistory.clearPoseAlarms(); return R"({"ok":true})"; }); web.setGetHandler("/api/pose/alarms/count", [&]() -> string { json r; r["count"] = alarmHistory.getPoseAlarmCount(); return r.dump(); }); // 通道帧提供器已在创建时逐路注册(registerChannelProviders) // ========== 姿态识别 Web API ========== web.setGetHandler("/api/pose/status", [&]() -> string { json r; r["pose_enabled"] = pose_ok; r["pose_cfg_enabled"] = pose_enabled; r["model"] = pose_model_path; r["fall_thresh"] = fall_thresh; r["fall_frames"] = fall_frames; r["fall_relays"] = fall_relays; json chs = json::array(); { lock_guard lk(channels_mtx); for (auto& ch : channels) { json c; c["id"] = ch->id; c["running"] = ch->pose_running; c["fall_count"] = ch->pose_fall_count; c["falls"] = (int)ch->fall_poses.size(); chs.push_back(c); } } r["channels"] = chs; return r.dump(); }); web.setPostHandler("/api/pose/config", [&](const string& body) -> string { try { json j = json::parse(body); json full = loadJson(config_path); if (j.contains("fall_thresh")) { float v = j["fall_thresh"].get(); if (v >= 0.0f && v <= 1.0f) { fall_thresh = v; fallAnalyzer.setThreshold(v); full["fall_thresh"] = v; } } if (j.contains("fall_frames")) { int v = j["fall_frames"].get(); if (v >= 1 && v <= 30) { fall_frames = v; full["fall_frames"] = v; } } if (j.contains("fall_relays") && j["fall_relays"].is_array()) { fall_relays.clear(); for (auto& c : j["fall_relays"]) if (c.is_string()) fall_relays.push_back(c.get()); full["fall_relays"] = fall_relays; } if (j.contains("channels") && j["channels"].is_object()) { lock_guard lk(channels_mtx); for (auto& [k, v] : j["channels"].items()) { int cid = atoi(k.c_str()); if (!v.is_object() || !v.contains("running")) continue; bool run = v["running"].get(); for (auto& ch : channels) if (ch->id == cid) { ch->pose_running = run && pose_ok; if (!ch->pose_running) ch->pose_fall_count = 0; } } } saveJson(config_path, full); cout << "[Pose] 配置已更新: 阈值=" << fall_thresh << " 连续" << fall_frames << "帧" << endl; return R"({"ok":true})"; } catch (exception& e) { return string(R"({"ok":false,"error":")") + e.what() + R"("})"; } }); // ========== MJPEG推流开关(临时关闭省CPU) ========== web.setGetHandler("/api/mjpeg/status", [&]() -> string { json r; r["enabled"] = web.mjpegEnabled(); return r.dump(); }); web.setPostHandler("/api/mjpeg/toggle", [&](const string& body) -> string { try { json j = json::parse(body); if (!j.contains("enabled")) return R"({"ok":false,"error":"enabled required"})"; bool en = j["enabled"].get(); web.setMjpegEnabled(en); cout << "[Web] MJPEG推流: " << (en ? "启用" : "临时关闭") << " (省CPU)" << endl; json r; r["ok"] = true; r["enabled"] = en; return r.dump(); } catch (exception& e) { return string(R"({"ok":false,"error":")") + e.what() + R"("})"; } }); web.start(); // 网格布局固定为已配置的rtsp通道数, 避免启动/断帧时布局跳动 int grid_fixed = (int)channels.size(); displayGrid.start(grid_output, hdmi_grid_cols, app_name + " - 通道网格", hdmi_fullscreen, 100, grid_fixed); displayBucket.start(steel_output && enable_bucket, 2, app_name + " - 钢包位", hdmi_fullscreen, 100, 2); cout << "[Main] Ctrl+C 退出" << endl; // 自动激活第一个工位(如果有配置) if (enable_bucket) { auto bs = bucketManager.getBuckets(); if (!bs.empty()) { cout << "[Main] 自动激活第一个工位: " << bs[0].description << endl; bucketManager.setSwitchMode(SwitchMode::AUTO); bucketManager.activateBucket(bs[0].id); } } auto last_pulse = chrono::steady_clock::now() - chrono::hours(1); auto last_di_pulse = chrono::steady_clock::now(); array prev_x{}; // DI联动状态: latched=回合进行中(触发过, 等待信号复位解锁) // high_since=本回合起始时刻(回合上限计时) // last_high=最后高电平时刻(解锁计时用) array di_latched_{{false}}; array di_last_high_{}; array di_high_since_{}; auto last_cleanup = chrono::steady_clock::now(); auto last_rb_check = chrono::steady_clock::now() - chrono::seconds(20); while (g_running.load()) { this_thread::sleep_for(chrono::milliseconds(1)); auto now_tc = chrono::steady_clock::now(); if (chrono::duration_cast(now_tc - last_cleanup).count() >= 1) { alarmHistory.cleanup(alarm_retention_days); cout << "[Alarm] 定时清理完成: 保留" << alarm_retention_days << "天" << endl; last_cleanup = now_tc; } // 定时重启(每10秒检查一次, 每个周期日只触发一次) if (reboot_enabled && chrono::duration_cast(now_tc - last_rb_check).count() >= 10) { last_rb_check = now_tc; time_t t = time(nullptr); struct tm tmv; localtime_r(&t, &tmv); char today[16], hhmm[8]; strftime(today, sizeof(today), "%Y-%m-%d", &tmv); strftime(hhmm, sizeof(hhmm), "%H:%M", &tmv); char target[8]; snprintf(target, sizeof(target), "%02d:%02d", rb_h, rb_m); long day_num = (long)(t / 86400); bool cycle_hit = (rb_cycle <= 1) || (day_num % rb_cycle == 0); if (strcmp(hhmm, target) == 0 && cycle_hit && rb_done_date != today) { rb_done_date = today; cout << "[System] 定时重启触发: " << hhmm << endl; cout.flush(); this_thread::sleep_for(chrono::milliseconds(800)); sync(); system("(sleep 1; systemctl reboot -i) >/dev/null 2>&1 &"); fflush(nullptr); _exit(0); } } // RFID/Modbus空闲检测 if (rfidClient) rfidClient->checkIdle(); if (enable_bucket && idleDetector.isIdle() && bucketManager.getActiveBucketId() >= 0) { cout << "[Bucket] 空闲超时,取消激活" << endl; bucketManager.activateBucket(-1); } // DI输入联动: 与围栏报警同一套策略 // 触发出现(上升沿) ──▶ 立即输出一个脉冲 // 信号保持期间 ──▶ 每 pulse_sec 秒重复, 累计满 alarm_max_sec 秒后静默 // 信号复位持续 rearm_sec 秒 ──▶ 重新武装, 下次触发再来一轮 { array xs = relay.getX(); for (int i = 0; i < 8; i++) { bool rise = (xs[i] && !prev_x[i]); // 解锁判定: 信号复位持续 rearm_sec 秒(高电平会刷新计时) if (di_latched_[i]) { if (xs[i]) di_last_high_[i] = now_tc; else if (chrono::duration_cast( now_tc - di_last_high_[i]).count() >= rearm_sec) di_latched_[i] = false; } else if (xs[i]) { di_last_high_[i] = now_tc; } auto it = di_bindings.find("x" + to_string(i + 1)); bool bound = it != di_bindings.end() && !it->second.empty(); // 首次触发: 上升沿立即动作, 开启新回合 if (rise && !bound) continue; if (rise && !di_latched_[i]) { last_di_pulse = now_tc; // 首发后重新起算重复节奏 relay.pulseChannels(it->second, rc.pulse_sec); di_latched_[i] = true; di_high_since_[i] = now_tc; cout << "[DI] X" << (i + 1) << " 联动触发 -> " << it->second.size() << "路继电器" << endl; string chans; for (auto& c : it->second) { if (!chans.empty()) chans += ","; chans += c; } alarmHistory.recordAction("输入联动", "X" + to_string(i + 1) + " 信号有效", chans); continue; } // 回合内重复: 信号保持 且 未超回合上限, 按节奏窗口重复 if (!xs[i] || !di_latched_[i] || !bound) continue; bool within_round = alarm_max_sec == 0 || chrono::duration_cast( now_tc - di_high_since_[i]).count() < (long long)alarm_max_sec * 1000; if (!within_round) continue; auto dms = chrono::duration_cast( now_tc - last_di_pulse).count(); if (dms > rc.pulse_sec * 1000) { last_di_pulse = now_tc; relay.pulseChannels(it->second, rc.pulse_sec); } } prev_x = xs; } // 通道快照: 热插拔期间主循环安全遍历 vector> ch_snap; { lock_guard lk(channels_mtx); ch_snap = channels; } // 钢包位专用通道(跟随工位切换) { lock_guard lk(bucket_channels_mtx); for (auto& bc : bucket_channels) ch_snap.push_back(bc); } for (auto& ch : ch_snap) { Nv12Frame nv12; if (!ch->capture->grabNv12(nv12)) { if (ch->frame_count == 0 && ch->capture->isRunning()) this_thread::sleep_for(chrono::milliseconds(10)); if (ch->frame_count == 0 && ch->id == 0) { static int dbg_cnt = 0; if (++dbg_cnt % 50 == 1) cerr << "[CH" << ch->id << "] waiting for frame..." << endl; } continue; } ch->frame_count++; if (!ch->size_recorded && nv12.width > 0 && nv12.height > 0) { fenceEngine.setChannelSize(ch->id, nv12.width, nv12.height); ch->size_recorded = true; cout << "[CH" << ch->id << "] 分辨率: " << nv12.width << "x" << nv12.height << endl; } bool do_infer = (ch->frame_count % (ch->skip_frames + 1) == 0); if (ch->frame_count <= 5) { cout << "[CH" << ch->id << "] frame=" << ch->frame_count << " size=" << nv12.width << "x" << nv12.height << " stride=" << nv12.stride << endl; } float scale = 1.0f; int pad_x = 0, pad_y = 0; if (do_infer) ch->rga.process(nv12, ch->rgb_buf, scale, pad_x, pad_y); if (do_infer && !ch->rgb_buf.empty()) { if (ch->frame_count <= 4) cout << "[CH" << ch->id << "] infer frame=" << ch->frame_count << " rgb=" << ch->rgb_buf.size() << endl; // 检测与姿态并行推理(不同NPU核), 缩短单帧处理时间 vector dets; vector poses; bool do_det = target_enabled; bool do_pose = (pose_ok && ch->pose_running); if (do_det && do_pose) { std::thread tPose([&]() { poses = poseDetector.detect(ch->rgb_buf.data(), nv12.width, nv12.height, scale, pad_x, pad_y); }); dets = detector.detect(ch->rgb_buf.data(), nv12.width, nv12.height, scale, pad_x, pad_y); tPose.join(); } else if (do_det) { dets = detector.detect(ch->rgb_buf.data(), nv12.width, nv12.height, scale, pad_x, pad_y); } else if (do_pose) { poses = poseDetector.detect(ch->rgb_buf.data(), nv12.width, nv12.height, scale, pad_x, pad_y); } vector centers; vector cids; vector cnames; vector confs; for (auto& d : dets) { // 只统计勾选的入侵目标类别 if (!alarm_classes.empty() && find(alarm_classes.begin(), alarm_classes.end(), d.class_name) == alarm_classes.end()) continue; centers.push_back(cv::Point2f( d.bbox.x + d.bbox.width / 2.0f, d.bbox.y + d.bbox.height / 2.0f)); cids.push_back(d.class_id); cnames.push_back(d.class_name); confs.push_back(d.confidence); } auto events = fenceEngine.evaluate( ch->id, centers, cids, cnames, confs); { lock_guard lk(ch->mtx); ch->detections = dets; ch->events = events; } // ========== 姿态识别(跌倒检测) ========== if (pose_ok && ch->pose_running) { vector falls; // 对每个人做多动作分析, 取最高分动作 PoseAction bestAction = PoseAction::NONE; float bestScore = 0.0f; for (auto& p : poses) { if (pose_alarm_classes.empty() || find(pose_alarm_classes.begin(), pose_alarm_classes.end(), "person") == pose_alarm_classes.end()) continue; auto ar = fallAnalyzer.analyze(p); if (ar.action != PoseAction::NONE && ar.action != PoseAction::LYING && ar.score > bestScore) { bestAction = ar.action; bestScore = ar.score; } } bool hit = (bestAction != PoseAction::NONE); // 连续 N 帧确认(防抖), 并按上升沿触发动作 if (hit) ch->pose_fall_count++; else ch->pose_fall_count = 0; bool pose_rising = (ch->pose_fall_count == fall_frames); { lock_guard lk(ch->mtx); ch->poses = poses; ch->fall_poses = hit ? poses : vector{}; ch->pose_action = hit ? poseActionName(bestAction) : ""; ch->pose_score = bestScore; } if (pose_rising) { ch->fall_seq.fetch_add(1, std::memory_order_relaxed); cout << "[Pose] CH" << ch->id << " 检测到" << poseActionName(bestAction) << "(" << ch->pose_fall_count << "帧) score=" << bestScore << endl; } if (pose_rising && !ch->fall_latched) { ch->fall_latched = true; ch->fall_latch_ts = chrono::steady_clock::now(); string actName = poseActionName(bestAction); // 联动报警: 触发继电器(可配置 fall_relays) if (!fall_relays.empty()) { relay.pulseChannels(fall_relays, rc.pulse_sec); string chans; for (auto& t : fall_relays) { if (!chans.empty()) chans += ","; chans += t; } alarmHistory.recordAction("姿态报警", "CH" + to_string(ch->id) + " 检测到" + actName, chans); } // 截图记录到姿态报警历史 std::vector frgb; int fout_w, fout_h; ch->rga.processToRgb(nv12, frgb, fout_w, fout_h); if (!frgb.empty()) { cv::Mat fshot(fout_h, fout_w, CV_8UC3, frgb.data()); cv::cvtColor(fshot, fshot, cv::COLOR_RGB2BGR); float fs = (nv12.width > 0) ? (float)fshot.cols / nv12.width : 1.0f; drawPoseDets(fshot, poses, poses, fs); drawPoseBanner(fshot, ch->id, poses.empty() ? PersonPose() : poses[0], actName); alarmHistory.recordPoseAlarm(ch->id, actName, bestScore, fshot); } } // 动作消失(或恢复正常)持续 rearm_sec 秒后重新武装 if (ch->fall_latched && !hit) { auto fnow = chrono::steady_clock::now(); if (chrono::duration_cast( fnow - ch->fall_latch_ts).count() >= rearm_sec) ch->fall_latched = false; } } else if (!pose_ok || !ch->pose_running) { ch->pose_fall_count = 0; } if (!events.empty()) { auto now = chrono::steady_clock::now(); // 动作判定(统一策略): // 首次检测立即动作开启回合; 之后每 pulse_sec 秒重复, // 回合累计满 alarm_max_sec 秒后静默, // 直到目标消除持续 rearm_sec 秒重新武装 bool episode_start = !ch->alarm_latched; if (episode_start) ch->episode_start = now; bool within_round = alarm_max_sec == 0 || chrono::duration_cast( now - ch->episode_start).count() < (long long)alarm_max_sec * 1000; bool do_action = episode_start || (within_round && chrono::duration_cast( now - last_pulse).count() > rc.pulse_sec * 1000); if (do_action) { last_pulse = now; // 合并所有触发围栏绑定的输出通道, 去重后统一脉冲 set trig; auto fs = fenceEngine.getFences(ch->id); for (auto& ev : events) for (auto& f : fs) if (f.id == ev.fence_id) for (auto& r : f.relays) trig.insert(r); if (!trig.empty()) { cout << "[Event] CH" << ch->id << " " << events[0].detection_class_name << " in " << events[0].fence_name << " -> " << trig.size() << "路继电器" << endl; relay.pulseChannels( vector(trig.begin(), trig.end()), rc.pulse_sec); string chans; for (auto& t : trig) { if (!chans.empty()) chans += ","; chans += t; } alarmHistory.recordAction("围栏报警", events[0].detection_class_name + " 入侵 " + events[0].fence_name + " (CH" + to_string(ch->id) + ")", chans); } } // 截图与动作配对: 每次动作必有对应截图证据 bool take_screenshot = do_action; ch->alarm_latched = true; ch->last_alarm_ts = now; if (take_screenshot) { std::vector rgb8; int out_w, out_h; ch->rga.processToRgb(nv12, rgb8, out_w, out_h); if (!rgb8.empty()) { cv::Mat shot(out_h, out_w, CV_8UC3, rgb8.data()); cv::cvtColor(shot, shot, cv::COLOR_RGB2BGR); float sds = (nv12.width > 0) ? (float)shot.cols / nv12.width : 1.0f; // 围栏: 填充+描边 auto fences = fenceEngine.getFences(ch->id); bool hf = false; for (auto& f : fences) if (f.active && f.polygon.size() >= 3) { hf = true; break; } if (hf) { cv::Mat fill; buildFenceFill(fill, fences, shot.size(), sds); cv::addWeighted(fill, 0.15, shot, 0.85, 0, shot); drawFenceEdges(shot, fences, sds); } // 目标框(报警目标红色高亮) vector dets_copy; vector evs_copy; { lock_guard lk(ch->mtx); dets_copy = ch->detections; evs_copy = ch->events; } drawDets(shot, dets_copy, evs_copy, sds, false); // 左上角红底白字详细信息 drawAlarmBanner(shot, ch->id, events); alarmHistory.record(ch->id, events[0].fence_name, events[0].detection_class_name, events[0].confidence, shot); } else { cerr << "[Alarm] 截图失败: RGA输出为空 CH" << ch->id << endl; } } } else { // 报警消失需持续 rearm_sec 秒才重新武装, // 防止目标检测抖动误判为"离开" auto now2 = chrono::steady_clock::now(); if (ch->alarm_latched && chrono::duration_cast( now2 - ch->last_alarm_ts).count() >= rearm_sec) ch->alarm_latched = false; } } // 显示帧: 每帧都生成(视频不跳帧), 推理仍按 skip_frames 跳帧 // 检测框/围栏只在推理帧更新, 非推理帧复用最近一次推理结果 cv::Mat display; { std::vector rgb8; int out_w, out_h; ch->rga.processToRgb(nv12, rgb8, out_w, out_h); if (!rgb8.empty()) { display = cv::Mat(out_h, out_w, CV_8UC3, rgb8.data()).clone(); cv::cvtColor(display, display, cv::COLOR_RGB2BGR); } } if (!display.empty()) { float ds = (nv12.width > 0) ? (float)display.cols / nv12.width : 1.0f; auto fences = fenceEngine.getFences(ch->id); uint64_t fv = fenceEngine.version(); bool has_fence = false; for (auto& f : fences) if (f.active && f.polygon.size() >= 3) { has_fence = true; break; } if (has_fence) { if (ch->fence_ver != fv || ch->fence_fill.size() != display.size()) { buildFenceFill(ch->fence_fill, fences, display.size(), ds); ch->fence_ver = fv; } cv::addWeighted(ch->fence_fill, 0.15, display, 0.85, 0, display); drawFenceEdges(display, fences, ds); } else { ch->fence_ver = 0; ch->fence_fill.release(); } { lock_guard lk(ch->mtx); drawDets(display, ch->detections, ch->events, ds); if (pose_ok && ch->pose_running) drawPoseDets(display, ch->poses, ch->fall_poses, ds); } char lbl[32]; snprintf(lbl, sizeof(lbl), "CH%d", ch->id); cv::putText(display, lbl, cv::Point(display.cols - 80, 25), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255, 255, 255), 2); lock_guard lk(ch->mtx); ch->display_frame = display; ch->disp_seq.fetch_add(1, std::memory_order_relaxed); } // 输出到本机显示器(HDMI) if (displayGrid.isRunning() && ch->id < 100) { char dlbl[64]; snprintf(dlbl, sizeof(dlbl), "CH%d %s", ch->id, ch->capture->name().c_str()); displayGrid.setFrame(ch->id, display, dlbl); } if (displayBucket.isRunning() && ch->id >= 100) { char dlbl[64]; snprintf(dlbl, sizeof(dlbl), "%s", ch->capture->name().c_str()); displayBucket.setFrame(ch->id, display, dlbl); } } } cout << "[Main] 正在退出..." << endl; { lock_guard lk(channels_mtx); for (auto& ch : channels) ch->capture->stop(); } relay.stop(); // 先停继电器线程, 避免HTTP处理线程阻塞在transact上拖住web.stop() web.stop(); cout << "[Main] 已退出" << endl; return 0; }