| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112 |
- #include <iostream>
- #include <fstream>
- #include <string>
- #include <vector>
- #include <thread>
- #include <atomic>
- #include <chrono>
- #include <mutex>
- #include <map>
- #include <set>
- #include <array>
- #include <algorithm>
- #include <cstring>
- #include <cctype>
- #include <ctime>
- #include <memory>
- #include <sys/socket.h>
- #include <sys/poll.h>
- #include <arpa/inet.h>
- #include <netinet/in.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <unistd.h>
- #include <signal.h>
- #include <cstdlib>
- #include <opencv2/opencv.hpp>
- #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<bool> 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<RtspCapture> capture;
- RgaProcessor rga;
- vector<Detection> detections;
- vector<FenceEvent> events;
- vector<float> rgb_buf;
- vector<PersonPose> poses; // 姿态识别结果
- vector<PersonPose> fall_poses; // 判定为跌倒/异常姿态的人员
- bool pose_running = false; // 本通道姿态推理开关
- int pose_fall_count = 0; // 连续命中帧数(防抖)
- std::atomic<uint64_t> 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<uint64_t> 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<FenceRegion>& fences,
- const cv::Size& size, float s) {
- fill = cv::Mat::zeros(size, CV_8UC3);
- vector<vector<cv::Point>> contours;
- for (auto& f : fences) {
- if (!f.active || f.polygon.size() < 3) continue;
- vector<cv::Point> 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<FenceRegion>& fences, float s) {
- for (auto& f : fences) {
- if (!f.active || f.polygon.size() < 3) continue;
- vector<cv::Point> pts;
- for (auto& p : f.polygon)
- pts.push_back(cv::Point((int)(p.x * s), (int)(p.y * s)));
- vector<vector<cv::Point>> 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<FenceEvent>& 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<string> 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<Detection>& dets,
- const vector<FenceEvent>& 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<PersonPose>& poses,
- const vector<PersonPose>& 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<string> 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<string> 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<string>());
- if (pose_alarm_classes.empty())
- pose_alarm_classes = {"person"};
- // 跌倒触发联动继电器(默认复用围栏报警继电器 y01)
- vector<string> 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<string>());
- 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<string> alarm_classes;
- if (cfg.contains("alarm_classes") && cfg["alarm_classes"].is_array())
- for (auto& c : cfg["alarm_classes"])
- alarm_classes.push_back(c.get<string>());
- 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<string, vector<string>> 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<string>());
- // DI/DO通道别名: {"x1":"大门雷达","y02":"声光报警器",...}
- map<string, string> 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<string>();
- // 输出通道模式: [{"channel":"y01","mode":"normal|cycle","interval_sec":1},...]
- array<YChanMode, 8> 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> rfidClient;
- unique_ptr<ModbusClient> 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>();
- 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>();
- modbusClient->setConfig(modbusCfg);
- modbusClient->setDataCallback([&](const vector<bool>& 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<shared_ptr<ChannelState>> channels;
- // 钢包位专用通道(id=100左, 101右), 跟随工位切换, 独立于普通rtsp通道
- static const int kBucketLeftId = 100;
- static const int kBucketRightId = 101;
- mutex bucket_channels_mtx;
- vector<shared_ptr<ChannelState>> 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<ChannelState>();
- ch->id = id;
- ch->skip_frames = skip_frames;
- ch->pose_running = pose_ok; // 姿态识别默认开启(模型加载成功时)
- ch->rga.init(input_size, input_size);
- ch->capture = make_shared<RtspCapture>(id, url, name);
- return ch;
- };
- auto registerChannelProviders = [&](shared_ptr<ChannelState> ch) {
- web.setFrameProvider(ch->id, [ch]() -> cv::Mat {
- lock_guard<mutex> 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<mutex> 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<mutex> 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<mutex> lk(bucket_channels_mtx);
- for (auto& ch : bucket_channels) ch->capture->stop();
- cerr << "[Bucket] 停止钢包位摄像头" << endl;
- }
- // 启动新工位钢包位摄像头
- if (cur && cur->id >= 0) {
- lock_guard<mutex> 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<RtspCapture>(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<RtspCapture>(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<mutex> 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<string>();
- bucketManager.setSwitchMode(mode == "auto" ? SwitchMode::AUTO : SwitchMode::HAND);
- }
- if (j.contains("activate")) {
- int id = j["activate"].get<int>();
- 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<string>();
- 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<int>());
- }
- 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<int> ids;
- {
- lock_guard<mutex> 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<int> ids;
- {
- lock_guard<mutex> 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<FenceRegion> 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<string>());
- 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<YChanMode, 8>& 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<YChanMode, 8> 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<double>();
- }
- 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<string>();
- if (!h.empty()) c.host = h;
- }
- if (j.contains("port")) {
- int p = j["port"].get<int>();
- if (p >= 1 && p <= 65535) c.port = p;
- }
- if (j.contains("device_addr")) {
- int a = j["device_addr"].get<int>();
- if (a >= 0 && a <= 254) c.device_addr = a;
- }
- if (j.contains("pulse_sec")) {
- int v = j["pulse_sec"].get<int>();
- if (v >= 1 && v <= 3600) c.pulse_sec = v;
- }
- if (j.contains("rearm_sec")) {
- int v = j["rearm_sec"].get<int>();
- if (v >= 1 && v <= 600) rearm_sec = v;
- }
- if (j.contains("alarm_max_sec")) {
- int v = j["alarm_max_sec"].get<int>();
- 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<string, vector<string>> nb;
- for (auto& [k, v] : j.items()) {
- if (!v.is_array()) continue;
- vector<string> chans;
- for (auto& c : v)
- if (c.is_string()) {
- string ch = c.get<string>();
- 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<string, string> na;
- for (auto& [k, v] : j.items())
- if (v.is_string()) {
- string key = normAliasKey(k);
- if (!key.empty()) na[key] = v.get<string>();
- }
- 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<mutex> 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<ChannelState> old;
- auto nu = makeChannel(id, name, url);
- {
- lock_guard<mutex> 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<ChannelState> victim;
- {
- lock_guard<mutex> 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<int>();
- lock_guard<mutex> 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<bool>();
- full["target_enabled"] = en;
- target_enabled = en;
- cout << "[Target] 目标识别: " << (en ? "启用" : "禁用")
- << " (重启后生效)" << endl;
- }
- if (j.contains("pose_enabled")) {
- bool en = j["pose_enabled"].get<bool>();
- full["pose_enabled"] = en;
- pose_enabled = en;
- cout << "[Pose] 姿态识别: " << (en ? "启用" : "禁用")
- << " (重启后生效)" << endl;
- }
- if (j.contains("grid_output")) {
- bool v = j["grid_output"].get<bool>();
- full["grid_output"] = v;
- grid_output = v;
- cout << "[Display] 网格输出: " << (v ? "启用" : "禁用")
- << " (重启后生效)" << endl;
- }
- if (j.contains("steel_output")) {
- bool v = j["steel_output"].get<bool>();
- full["steel_output"] = v;
- steel_output = v;
- cout << "[Display] 钢包位输出: " << (v ? "启用" : "禁用")
- << " (重启后生效)" << endl;
- }
- if (j.contains("alarm_retention_days")) {
- int v = j["alarm_retention_days"].get<int>();
- 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<string> ac;
- for (auto& c : j["alarm_classes"])
- if (c.is_string()) ac.push_back(c.get<string>());
- 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<bool>();
- full["auto_reboot"]["enabled"] = en;
- reboot_enabled = en;
- }
- if (ar.contains("cycle")) {
- int c = ar["cycle"].get<int>();
- 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<string>().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<string, string>& 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<string, string>& 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<mutex> 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<float>();
- 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<int>();
- 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<string>());
- full["fall_relays"] = fall_relays;
- }
- if (j.contains("channels") && j["channels"].is_object()) {
- lock_guard<mutex> 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<bool>();
- 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<bool>();
- 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<uint8_t, 8> prev_x{};
- // DI联动状态: latched=回合进行中(触发过, 等待信号复位解锁)
- // high_since=本回合起始时刻(回合上限计时)
- // last_high=最后高电平时刻(解锁计时用)
- array<bool, 8> di_latched_{{false}};
- array<chrono::steady_clock::time_point, 8> di_last_high_{};
- array<chrono::steady_clock::time_point, 8> 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<chrono::hours>(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<chrono::seconds>(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<uint8_t, 8> 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<chrono::seconds>(
- 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<chrono::milliseconds>(
- now_tc - di_high_since_[i]).count() <
- (long long)alarm_max_sec * 1000;
- if (!within_round) continue;
- auto dms = chrono::duration_cast<chrono::milliseconds>(
- 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<shared_ptr<ChannelState>> ch_snap;
- {
- lock_guard<mutex> lk(channels_mtx);
- ch_snap = channels;
- }
- // 钢包位专用通道(跟随工位切换)
- {
- lock_guard<mutex> 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<Detection> dets;
- vector<PersonPose> 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<cv::Point2f> centers;
- vector<int> cids;
- vector<string> cnames;
- vector<float> 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<mutex> lk(ch->mtx);
- ch->detections = dets;
- ch->events = events;
- }
- // ========== 姿态识别(跌倒检测) ==========
- if (pose_ok && ch->pose_running) {
- vector<PersonPose> 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<mutex> lk(ch->mtx);
- ch->poses = poses;
- ch->fall_poses = hit ? poses : vector<PersonPose>{};
- 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<uint8_t> 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<chrono::seconds>(
- 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<chrono::milliseconds>(
- now - ch->episode_start).count() <
- (long long)alarm_max_sec * 1000;
- bool do_action =
- episode_start ||
- (within_round &&
- chrono::duration_cast<chrono::milliseconds>(
- now - last_pulse).count() >
- rc.pulse_sec * 1000);
- if (do_action) {
- last_pulse = now;
- // 合并所有触发围栏绑定的输出通道, 去重后统一脉冲
- set<string> 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<string>(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<uint8_t> 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<Detection> dets_copy;
- vector<FenceEvent> evs_copy;
- {
- lock_guard<mutex> 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<chrono::seconds>(
- now2 - ch->last_alarm_ts).count() >= rearm_sec)
- ch->alarm_latched = false;
- }
- }
- // 显示帧: 每帧都生成(视频不跳帧), 推理仍按 skip_frames 跳帧
- // 检测框/围栏只在推理帧更新, 非推理帧复用最近一次推理结果
- cv::Mat display;
- {
- std::vector<uint8_t> 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<mutex> 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<mutex> 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<mutex> lk(channels_mtx);
- for (auto& ch : channels) ch->capture->stop();
- }
- relay.stop(); // 先停继电器线程, 避免HTTP处理线程阻塞在transact上拖住web.stop()
- web.stop();
- cout << "[Main] 已退出" << endl;
- return 0;
- }
|