main.cpp 88 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112
  1. #include <iostream>
  2. #include <fstream>
  3. #include <string>
  4. #include <vector>
  5. #include <thread>
  6. #include <atomic>
  7. #include <chrono>
  8. #include <mutex>
  9. #include <map>
  10. #include <set>
  11. #include <array>
  12. #include <algorithm>
  13. #include <cstring>
  14. #include <cctype>
  15. #include <ctime>
  16. #include <memory>
  17. #include <sys/socket.h>
  18. #include <sys/poll.h>
  19. #include <arpa/inet.h>
  20. #include <netinet/in.h>
  21. #include <fcntl.h>
  22. #include <unistd.h>
  23. #include <unistd.h>
  24. #include <signal.h>
  25. #include <cstdlib>
  26. #include <opencv2/opencv.hpp>
  27. #include "json.hpp"
  28. #include "rtsp_capture.h"
  29. #include "yolo_detector.h"
  30. #include "rga_processor.h"
  31. #include "fence_engine.h"
  32. #include "relay_ctrl.h"
  33. #include "web_server.h"
  34. #include "alarm_history.h"
  35. #include "config_loader.h"
  36. #include "rfid_client.h"
  37. #include "modbus_client.h"
  38. #include "bucket_manager.h"
  39. #include "idle_detector.h"
  40. #include "pose_detector.h"
  41. #include "display_output.h"
  42. using namespace std;
  43. using json = nlohmann::json;
  44. static atomic<bool> g_running{true};
  45. static void sigHandler(int sig) {
  46. // 第一次: 优雅退出; 第二次: 强制退出(防止清理流程卡死)
  47. if (!g_running.exchange(false)) {
  48. signal(sig, SIG_DFL);
  49. _exit(130);
  50. }
  51. }
  52. struct ChannelState {
  53. int id;
  54. shared_ptr<RtspCapture> capture;
  55. RgaProcessor rga;
  56. vector<Detection> detections;
  57. vector<FenceEvent> events;
  58. vector<float> rgb_buf;
  59. vector<PersonPose> poses; // 姿态识别结果
  60. vector<PersonPose> fall_poses; // 判定为跌倒/异常姿态的人员
  61. bool pose_running = false; // 本通道姿态推理开关
  62. int pose_fall_count = 0; // 连续命中帧数(防抖)
  63. std::atomic<uint64_t> fall_seq{0}; // 姿态事件序号(沿触发)
  64. bool fall_latched = false; // 姿态报警沿触发锁存
  65. chrono::steady_clock::time_point fall_latch_ts{};
  66. string pose_action; // 当前命中的姿态动作
  67. float pose_score = 0.0f; // 当前动作得分
  68. cv::Mat display_frame;
  69. cv::Mat fence_fill; // 围栏填充缓存(纯色图, 与帧内容无关)
  70. uint64_t fence_ver = 0; // 对应 FenceEngine 版本
  71. std::atomic<uint64_t> disp_seq{0};
  72. int frame_count = 0;
  73. int skip_frames = 3;
  74. bool size_recorded = false;
  75. bool alarm_latched = false; // 报警沿触发: 每次新报警只截一张
  76. chrono::steady_clock::time_point last_alarm_ts{};
  77. chrono::steady_clock::time_point episode_start{}; // 本回合报警开始时刻
  78. mutex mtx;
  79. };
  80. static json loadJson(const string& path) {
  81. ifstream ifs(path);
  82. if (!ifs.is_open()) return json::object();
  83. json j; ifs >> j; return j;
  84. }
  85. static void saveJson(const string& path, const json& j) {
  86. ofstream ofs(path); ofs << j.dump(4) << endl;
  87. }
  88. static void buildFenceFill(cv::Mat& fill, const vector<FenceRegion>& fences,
  89. const cv::Size& size, float s) {
  90. fill = cv::Mat::zeros(size, CV_8UC3);
  91. vector<vector<cv::Point>> contours;
  92. for (auto& f : fences) {
  93. if (!f.active || f.polygon.size() < 3) continue;
  94. vector<cv::Point> pts;
  95. for (auto& p : f.polygon)
  96. pts.push_back(cv::Point((int)(p.x * s), (int)(p.y * s)));
  97. contours.push_back(std::move(pts));
  98. }
  99. if (!contours.empty())
  100. cv::drawContours(fill, contours, -1, cv::Scalar(0, 200, 0), -1);
  101. }
  102. static void drawFenceEdges(cv::Mat& frame, const vector<FenceRegion>& fences, float s) {
  103. for (auto& f : fences) {
  104. if (!f.active || f.polygon.size() < 3) continue;
  105. vector<cv::Point> pts;
  106. for (auto& p : f.polygon)
  107. pts.push_back(cv::Point((int)(p.x * s), (int)(p.y * s)));
  108. vector<vector<cv::Point>> pp = {pts};
  109. cv::polylines(frame, pp, true, cv::Scalar(0, 255, 0), 2);
  110. if (!f.name.empty())
  111. cv::putText(frame, f.name, pts[0], cv::FONT_HERSHEY_SIMPLEX,
  112. 0.6, cv::Scalar(0, 255, 0), 2);
  113. }
  114. }
  115. static void drawAlarmBanner(cv::Mat& shot, int channel,
  116. const vector<FenceEvent>& events) {
  117. char tbuf[32];
  118. time_t t = time(nullptr);
  119. struct tm tmv;
  120. localtime_r(&t, &tmv);
  121. strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", &tmv);
  122. const double fs = 0.45;
  123. const int thick = 1, lh = 18, pad = 4;
  124. vector<string> lines;
  125. lines.push_back(string("!! ALARM !! CH") + to_string(channel)
  126. + " " + tbuf);
  127. size_t n = events.size() < 3 ? events.size() : 3;
  128. for (size_t i = 0; i < n; i++) {
  129. char b[160];
  130. snprintf(b, sizeof(b), "%s %.0f%% -> %s",
  131. events[i].detection_class_name.c_str(),
  132. events[i].confidence * 100.0f,
  133. events[i].fence_name.c_str());
  134. lines.push_back(b);
  135. }
  136. int w = 0, bh = pad * 2 + lh * (int)lines.size();
  137. for (auto& l : lines) {
  138. int tw = 0, th2 = 0;
  139. cv::getTextSize(l, cv::FONT_HERSHEY_SIMPLEX, fs, thick, &th2);
  140. if (tw > w) w = tw;
  141. }
  142. w += pad * 2;
  143. if (w > shot.cols) w = shot.cols;
  144. cv::rectangle(shot, cv::Rect(0, 0, w, bh), cv::Scalar(0, 0, 255), -1);
  145. for (size_t i = 0; i < lines.size(); i++)
  146. cv::putText(shot, lines[i], cv::Point(pad, pad + 13 + (int)i * lh),
  147. cv::FONT_HERSHEY_SIMPLEX, fs, cv::Scalar(255, 255, 255), thick,
  148. cv::LINE_AA);
  149. }
  150. static void drawDets(cv::Mat& frame, const vector<Detection>& dets,
  151. const vector<FenceEvent>& events, float s,
  152. bool show_alarm_text = true) {
  153. for (auto& det : dets) {
  154. bool in_fence = false;
  155. for (auto& ev : events)
  156. if (det.bbox.contains(ev.point)) { in_fence = true; break; }
  157. cv::Scalar color = in_fence ? cv::Scalar(0, 0, 255)
  158. : cv::Scalar(0, 255, 0);
  159. int thick = in_fence ? 3 : 2;
  160. cv::Rect rb((int)(det.bbox.x * s), (int)(det.bbox.y * s),
  161. (int)(det.bbox.width * s), (int)(det.bbox.height * s));
  162. cv::rectangle(frame, rb, color, thick);
  163. char buf[128];
  164. snprintf(buf, sizeof(buf), "%s %.0f%%",
  165. det.class_name.c_str(), det.confidence * 100);
  166. cv::putText(frame, buf,
  167. cv::Point(rb.x, rb.y - 6),
  168. cv::FONT_HERSHEY_SIMPLEX, 0.5, color, 1);
  169. }
  170. if (show_alarm_text && !events.empty()) {
  171. cv::putText(frame, "!! ALARM !!", cv::Point(10, 30),
  172. cv::FONT_HERSHEY_SIMPLEX, 1.0, cv::Scalar(0, 0, 255), 3);
  173. char ebuf[256];
  174. snprintf(ebuf, sizeof(ebuf), "%s in %s (%.0f%%)",
  175. events[0].detection_class_name.c_str(),
  176. events[0].fence_name.c_str(),
  177. events[0].confidence * 100);
  178. cv::putText(frame, ebuf, cv::Point(10, 60),
  179. cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 0, 255), 2);
  180. }
  181. }
  182. // COCO 17 关键点骨架连接(用于姿态叠加显示)
  183. static const int POSE_SKELETON[][2] = {
  184. {0,1},{0,2},{1,3},{2,4},{5,6},{5,7},{7,9},{6,8},{8,10},
  185. {5,11},{6,12},{11,12},{11,13},{13,15},{12,14},{14,16}
  186. };
  187. static const char* POSE_KPT_NAMES[] = {
  188. "nose","l_eye","r_eye","l_ear","r_ear",
  189. "l_sho","r_sho","l_elb","r_elb","l_wri","r_wri",
  190. "l_hip","r_hip","l_knee","r_knee","l_ank","r_ank"
  191. };
  192. static void drawPoseDets(cv::Mat& frame, const vector<PersonPose>& poses,
  193. const vector<PersonPose>& falls, float s,
  194. bool show_skeleton = true) {
  195. for (auto& p : poses) {
  196. bool is_fall = false;
  197. for (auto& f : falls)
  198. if (f.bbox == p.bbox) { is_fall = true; break; }
  199. cv::Scalar color = is_fall ? cv::Scalar(0, 0, 255)
  200. : cv::Scalar(255, 255, 0);
  201. cv::Rect rb((int)(p.bbox.x * s), (int)(p.bbox.y * s),
  202. (int)(p.bbox.width * s), (int)(p.bbox.height * s));
  203. cv::rectangle(frame, rb, color, 2);
  204. if (show_skeleton && p.keypoints.size() == 17) {
  205. // 关键点连线
  206. for (auto& sk : POSE_SKELETON) {
  207. int a = sk[0], b = sk[1];
  208. if (p.keypoint_conf.size() == 17 &&
  209. p.keypoint_conf[a] > 0.3f && p.keypoint_conf[b] > 0.3f) {
  210. cv::line(frame,
  211. cv::Point((int)(p.keypoints[a].x * s),
  212. (int)(p.keypoints[a].y * s)),
  213. cv::Point((int)(p.keypoints[b].x * s),
  214. (int)(p.keypoints[b].y * s)),
  215. color, 2);
  216. }
  217. }
  218. // 关键点
  219. for (int k = 0; k < 17; k++) {
  220. if (p.keypoint_conf[k] > 0.3f) {
  221. cv::circle(frame,
  222. cv::Point((int)(p.keypoints[k].x * s),
  223. (int)(p.keypoints[k].y * s)),
  224. 3, cv::Scalar(0, 255, 255), -1);
  225. }
  226. }
  227. }
  228. }
  229. for (auto& f : falls) {
  230. cv::Rect rb((int)(f.bbox.x * s), (int)(f.bbox.y * s),
  231. (int)(f.bbox.width * s), (int)(f.bbox.height * s));
  232. cv::rectangle(frame, rb, cv::Scalar(0, 0, 255), 3);
  233. cv::putText(frame, "FALL", cv::Point(rb.x, rb.y - 6),
  234. cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(0, 0, 255), 2);
  235. }
  236. }
  237. static void drawPoseBanner(cv::Mat& shot, int channel, const PersonPose& fall,
  238. const string& action = "跌倒") {
  239. char tbuf[32];
  240. time_t t = time(nullptr);
  241. struct tm tmv;
  242. localtime_r(&t, &tmv);
  243. strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", &tmv);
  244. string line1 = "!! " + action + " !! CH" + to_string(channel) + " " + tbuf;
  245. char line2[128];
  246. snprintf(line2, sizeof(line2), "person %.0f%% 姿态%s",
  247. fall.confidence * 100, action.c_str());
  248. const double fs = 0.45;
  249. const int thick = 1, lh = 18, pad = 4;
  250. vector<string> lines = {line1, line2};
  251. int w = 0, bh = pad * 2 + lh * (int)lines.size();
  252. for (auto& l : lines) {
  253. int tw = 0, th2 = 0;
  254. cv::getTextSize(l, cv::FONT_HERSHEY_SIMPLEX, fs, thick, &th2);
  255. if (tw > w) w = tw;
  256. }
  257. w += pad * 2;
  258. if (w > shot.cols) w = shot.cols;
  259. cv::rectangle(shot, cv::Rect(0, 0, w, bh), cv::Scalar(0, 0, 255), -1);
  260. for (size_t i = 0; i < lines.size(); i++)
  261. cv::putText(shot, lines[i], cv::Point(pad, pad + 13 + (int)i * lh),
  262. cv::FONT_HERSHEY_SIMPLEX, fs, cv::Scalar(255, 255, 255), thick,
  263. cv::LINE_AA);
  264. }
  265. int main(int argc, char* argv[]) {
  266. signal(SIGINT, sigHandler);
  267. signal(SIGTERM, sigHandler);
  268. string config_path = (argc > 1) ? argv[1] : "config/config.json";
  269. json cfg = loadJson(config_path);
  270. if (cfg.empty()) {
  271. cerr << "配置文件不存在: " << config_path << endl;
  272. return 1;
  273. }
  274. int skip_frames = cfg.value("skip_frames", 3);
  275. int web_port = cfg.value("web_port", 8080);
  276. string model_path = cfg.value("model_path", "models/yolov5s_rk3576_int8.rknn");
  277. int input_size = cfg.value("input_size", 640);
  278. float conf_thresh = cfg.value("conf_thresh", 0.35f);
  279. float nms_thresh = cfg.value("nms_thresh", 0.45f);
  280. string fence_file = cfg.value("fence_file", "config/fences.json");
  281. int alarm_retention_days = cfg.value("alarm_retention_days", 30);
  282. // ========== 本机画面输出(HDMI显示器) ==========
  283. string app_name = cfg.value("app_name", "钢包工位视频监测系统");
  284. // 工位功能总开关(临时关闭可禁用钢包位/工位切换/空闲检测)
  285. bool enable_bucket = cfg.value("enable_bucket", true);
  286. // 两个独立显示器输出: 网格(普通通道) + 钢包位(工位左右摄像头)
  287. bool grid_output = cfg.value("grid_output", true);
  288. bool steel_output = cfg.value("steel_output", true);
  289. int hdmi_grid_cols = cfg.value("hdmi_grid_cols", 0);
  290. bool hdmi_fullscreen = cfg.value("hdmi_fullscreen", true);
  291. cout << "[Display] 网格输出: " << (grid_output ? "启用" : "禁用")
  292. << " 钢包位输出: " << (steel_output ? "启用" : "禁用")
  293. << " 工位功能: " << (enable_bucket ? "启用" : "禁用")
  294. << " 网格列=" << hdmi_grid_cols
  295. << " 全屏=" << (hdmi_fullscreen ? "是" : "否") << endl;
  296. // ========== 姿态识别(跌倒检测) ==========
  297. string pose_model_path = cfg.value("pose_model_path",
  298. "models/yolov5s-pose_rk3588_int8.rknn");
  299. int pose_input_size = cfg.value("pose_input_size", 640);
  300. float pose_conf_thresh = cfg.value("pose_conf_thresh", 0.35f);
  301. float pose_nms_thresh = cfg.value("pose_nms_thresh", 0.45f);
  302. float fall_thresh = cfg.value("fall_thresh", 0.5f);
  303. int fall_frames = cfg.value("fall_frames", 3);
  304. bool target_enabled = cfg.value("target_enabled", true);
  305. bool pose_enabled = cfg.value("pose_enabled", false);
  306. vector<string> pose_alarm_classes;
  307. if (cfg.contains("pose_alarm_classes") && cfg["pose_alarm_classes"].is_array())
  308. for (auto& c : cfg["pose_alarm_classes"])
  309. pose_alarm_classes.push_back(c.get<string>());
  310. if (pose_alarm_classes.empty())
  311. pose_alarm_classes = {"person"};
  312. // 跌倒触发联动继电器(默认复用围栏报警继电器 y01)
  313. vector<string> fall_relays;
  314. if (cfg.contains("fall_relays") && cfg["fall_relays"].is_array())
  315. for (auto& c : cfg["fall_relays"])
  316. if (c.is_string()) fall_relays.push_back(c.get<string>());
  317. if (fall_relays.empty()) fall_relays = {"y01"};
  318. cout << "[Target] 目标识别: " << (target_enabled ? "启用" : "禁用") << endl;
  319. cout << "[Pose] 姿态识别: " << (pose_enabled ? "启用" : "禁用")
  320. << " 模型=" << pose_model_path
  321. << " 跌倒阈值=" << fall_thresh
  322. << " 连续" << fall_frames << "帧确认" << endl;
  323. vector<string> alarm_classes;
  324. if (cfg.contains("alarm_classes") && cfg["alarm_classes"].is_array())
  325. for (auto& c : cfg["alarm_classes"])
  326. alarm_classes.push_back(c.get<string>());
  327. if (alarm_classes.empty())
  328. alarm_classes = {"person", "bicycle", "car", "motorcycle", "bus", "truck"};
  329. bool reboot_enabled = false;
  330. int rb_cycle = 1;
  331. int rb_h = 3, rb_m = 30;
  332. string rb_done_date;
  333. if (cfg.contains("auto_reboot")) {
  334. auto& ar = cfg["auto_reboot"];
  335. reboot_enabled = ar.value("enabled", false);
  336. rb_cycle = ar.value("cycle", 1);
  337. if (rb_cycle < 1) rb_cycle = 1;
  338. sscanf(ar.value("time", "03:30").c_str(), "%d:%d", &rb_h, &rb_m);
  339. if (rb_h < 0 || rb_h > 23 || rb_m < 0 || rb_m > 59) { rb_h = 3; rb_m = 30; }
  340. }
  341. cout << "[System] 定时重启: " << (reboot_enabled ? "启用" : "禁用")
  342. << " 周期" << rb_cycle << "天 时间 "
  343. << rb_h << ":" << (rb_m < 10 ? "0" : "") << rb_m << endl;
  344. RelayConfig rc;
  345. json rj = cfg.value("relay", json::object());
  346. rc.host = rj.value("host", "192.168.1.253");
  347. rc.port = rj.value("port", 1030);
  348. rc.device_addr = rj.value("device_addr", 1);
  349. // 脉冲时长统一用秒; 兼容旧配置pulse_ms
  350. rc.pulse_sec = max(1, rj.value("pulse_sec",
  351. rj.value("pulse_ms", 3000) / 1000));
  352. int rearm_sec = clamp(rj.value("rearm_sec", 10), 1, 600);
  353. // 单回合上限: 报警开始后最多动作N秒(0=不限), 之后静默直到重新武装
  354. int alarm_max_sec = clamp(rj.value("alarm_max_sec", 30), 0, 3600);
  355. // DI输入→输出联动绑定: {"x1":["y01","y02"],...}
  356. map<string, vector<string>> di_bindings;
  357. if (rj.contains("di_bindings") && rj["di_bindings"].is_object())
  358. for (auto& [k, v] : rj["di_bindings"].items())
  359. if (v.is_array())
  360. for (auto& c : v)
  361. if (c.is_string()) di_bindings[k].push_back(c.get<string>());
  362. // DI/DO通道别名: {"x1":"大门雷达","y02":"声光报警器",...}
  363. map<string, string> rly_alias;
  364. if (rj.contains("aliases") && rj["aliases"].is_object())
  365. for (auto& [k, v] : rj["aliases"].items())
  366. if (v.is_string()) rly_alias[k] = v.get<string>();
  367. // 输出通道模式: [{"channel":"y01","mode":"normal|cycle","interval_sec":1},...]
  368. array<YChanMode, 8> ycfg;
  369. if (rj.contains("y_config") && rj["y_config"].is_array())
  370. for (auto& e : rj["y_config"]) {
  371. string ch = e.value("channel", "");
  372. if (ch.size() == 3 && ch[0] == 'y') {
  373. int idx = (ch[1] - '0') * 10 + (ch[2] - '0');
  374. if (idx >= 1 && idx <= 8) {
  375. string m = e.value("mode", "normal");
  376. ycfg[idx - 1].mode = (m == "cycle") ? 1 : 0;
  377. ycfg[idx - 1].interval_sec = e.value("interval_sec", 1.0);
  378. }
  379. }
  380. }
  381. cout << "========================================" << endl;
  382. cout << " ZoneAlarm - RK3576 NPU 车辆检测电子围栏" << endl;
  383. cout << " 推理: RKNN NPU" << endl;
  384. cout << " 解码: MPP 硬解" << endl;
  385. cout << " Web端口: " << web_port << endl;
  386. cout << " 报警策略: 脉冲" << rc.pulse_sec << "秒/回合上限"
  387. << alarm_max_sec << "秒/重新武装" << rearm_sec << "秒" << endl;
  388. cout << "========================================" << endl;
  389. // NPU 核心分配: 检测绑 Core0, 姿态绑 Core1, 两模型并行推理
  390. int det_core = cfg.value("det_core_mask", 1); // 默认 Core0
  391. int pose_core = cfg.value("pose_core_mask", 2); // 默认 Core1
  392. YoloDetector detector;
  393. if (target_enabled && !detector.load(model_path, input_size, conf_thresh, nms_thresh, det_core)) {
  394. cerr << "RKNN模型加载失败!" << endl;
  395. return 1;
  396. }
  397. cout << "[RKNN] 检测核心掩码: " << det_core << endl;
  398. PoseDetector poseDetector;
  399. FallAnalyzer fallAnalyzer;
  400. fallAnalyzer.setThreshold(fall_thresh);
  401. bool pose_hand_raise = cfg.value("pose_hand_raise", true);
  402. bool pose_crouch = cfg.value("pose_crouch", true);
  403. fallAnalyzer.setHandRaiseEnabled(pose_hand_raise);
  404. fallAnalyzer.setCrouchEnabled(pose_crouch);
  405. cout << "[Pose] 动作类型: 跌倒/倒地(固定)"
  406. << " 举手呼救=" << (pose_hand_raise ? "开" : "关")
  407. << " 异常蹲坐=" << (pose_crouch ? "开" : "关") << endl;
  408. bool pose_ok = false;
  409. if (pose_enabled) {
  410. if (poseDetector.load(pose_model_path, pose_input_size,
  411. pose_conf_thresh, pose_nms_thresh, pose_core)) {
  412. pose_ok = true;
  413. cout << "[Pose] 姿态模型加载成功, 跌倒检测已就绪, 核心掩码: " << pose_core << endl;
  414. } else {
  415. cerr << "[Pose] 姿态模型加载失败: " << pose_model_path
  416. << " (继续使用纯围栏检测)" << endl;
  417. }
  418. }
  419. FenceEngine fenceEngine;
  420. fenceEngine.loadFences(fence_file);
  421. RelayCtrl relay;
  422. relay.setConfig(rc);
  423. relay.setModes(ycfg);
  424. relay.start();
  425. AlarmHistory alarmHistory;
  426. alarmHistory.init("data/alarms.db", "data/screenshots");
  427. alarmHistory.cleanup(alarm_retention_days);
  428. cout << "[Alarm] 历史清理: 保留" << alarm_retention_days << "天" << endl;
  429. // ========== 工位管理模块 ==========
  430. BucketManager bucketManager;
  431. bucketManager.loadConfig(cfg);
  432. IdleDetector idleDetector(cfg.value("idle_timeout", 5));
  433. // RFID 或 Modbus
  434. string module_type = cfg.value("module_type", "RFID");
  435. unique_ptr<RfidClient> rfidClient;
  436. unique_ptr<ModbusClient> modbusClient;
  437. if (module_type == "RFID" && cfg.contains("rfid")) {
  438. RfidConfig rfidCfg;
  439. auto& r = cfg["rfid"];
  440. rfidCfg.ip = r.value("ip", "");
  441. rfidCfg.port = r.value("port", 60000);
  442. rfidCfg.stability_time = r.value("stability_time", 5);
  443. rfidCfg.stability_count = r.value("stability_count", 3);
  444. rfidClient = make_unique<RfidClient>();
  445. rfidClient->setConfig(rfidCfg);
  446. rfidClient->setTagCallback([&](const string& tag) {
  447. idleDetector.feed(tag);
  448. if (!tag.empty() && enable_bucket) {
  449. bucketManager.checkAutoSwitch(tag);
  450. }
  451. });
  452. rfidClient->setStateCallback([&](bool online) {
  453. cout << "[RFID] 状态: " << (online ? "在线" : "离线") << endl;
  454. });
  455. if (rfidClient->connect()) {
  456. cout << "[RFID] 初始化成功" << endl;
  457. }
  458. } else if (module_type == "Modbus" && cfg.contains("modbus")) {
  459. ModbusConfig modbusCfg;
  460. auto& m = cfg["modbus"];
  461. modbusCfg.ip = m.value("ip", "");
  462. modbusCfg.port = m.value("port", 502);
  463. modbusCfg.frequency = m.value("frequency", 100);
  464. modbusCfg.stability_count = m.value("stability_count", 3);
  465. modbusClient = make_unique<ModbusClient>();
  466. modbusClient->setConfig(modbusCfg);
  467. modbusClient->setDataCallback([&](const vector<bool>& coils) {
  468. idleDetector.feed(coils);
  469. if (enable_bucket) bucketManager.checkAutoSwitch(coils);
  470. });
  471. modbusClient->setStateCallback([&](bool online) {
  472. cout << "[Modbus] 状态: " << (online ? "在线" : "离线") << endl;
  473. });
  474. if (modbusClient->connect()) {
  475. cout << "[Modbus] 初始化成功" << endl;
  476. }
  477. }
  478. // ========== 通道容器 ==========
  479. static const int kMaxChannels = 8;
  480. mutex channels_mtx;
  481. vector<shared_ptr<ChannelState>> channels;
  482. // 钢包位专用通道(id=100左, 101右), 跟随工位切换, 独立于普通rtsp通道
  483. static const int kBucketLeftId = 100;
  484. static const int kBucketRightId = 101;
  485. mutex bucket_channels_mtx;
  486. vector<shared_ptr<ChannelState>> bucket_channels;
  487. WebServer web;
  488. DisplayOutput displayGrid; // 普通通道网格(自动布局)
  489. DisplayOutput displayBucket; // 钢包位(当前工位左右摄像头)
  490. web.setPort(web_port);
  491. // 登录凭据(默认admin/admin), 持久化于config.json的web_auth
  492. string web_user = "admin", web_pass = "admin";
  493. {
  494. json wa = cfg.value("web_auth", json::object());
  495. web_user = wa.value("username", "admin");
  496. web_pass = wa.value("password", "admin");
  497. }
  498. web.setAuthHooks({
  499. [&](const string& u, const string& p) {
  500. return u == web_user && p == web_pass;
  501. },
  502. [&]() -> string { return web_user; },
  503. [&](const string& oldp, const string& u, const string& np) -> bool {
  504. if (oldp != web_pass || np.empty()) return false;
  505. if (!u.empty()) web_user = u;
  506. web_pass = np;
  507. json full = loadJson(config_path);
  508. full["web_auth"] = {{"username", web_user}, {"password", web_pass}};
  509. saveJson(config_path, full);
  510. cout << "[System] 登录凭证已更新: 用户 " << web_user << endl;
  511. return true;
  512. }
  513. });
  514. auto makeChannel = [&](int id, const string& name, const string& url) {
  515. auto ch = make_shared<ChannelState>();
  516. ch->id = id;
  517. ch->skip_frames = skip_frames;
  518. ch->pose_running = pose_ok; // 姿态识别默认开启(模型加载成功时)
  519. ch->rga.init(input_size, input_size);
  520. ch->capture = make_shared<RtspCapture>(id, url, name);
  521. return ch;
  522. };
  523. auto registerChannelProviders = [&](shared_ptr<ChannelState> ch) {
  524. web.setFrameProvider(ch->id, [ch]() -> cv::Mat {
  525. lock_guard<mutex> lk(ch->mtx);
  526. if (!ch->display_frame.empty())
  527. return ch->display_frame.clone();
  528. return cv::Mat(360, 640, CV_8UC3, cv::Scalar(0, 0, 0));
  529. });
  530. web.setFrameSeqProvider(ch->id, [ch]() -> uint64_t {
  531. return ch->disp_seq.load(std::memory_order_relaxed);
  532. });
  533. };
  534. json rtsp_list = cfg.value("rtsp", json::array());
  535. for (int i = 0; i < (int)rtsp_list.size() && i < kMaxChannels; i++) {
  536. auto ch = makeChannel(rtsp_list[i].value("id", i),
  537. rtsp_list[i].value("name", ""),
  538. rtsp_list[i].value("url", ""));
  539. ch->capture->start();
  540. registerChannelProviders(ch);
  541. lock_guard<mutex> lk(channels_mtx);
  542. channels.push_back(std::move(ch));
  543. }
  544. cout << "[Main] 视频通道: " << channels.size() << "/"
  545. << kMaxChannels << " 路" << endl;
  546. // 钢包位专用通道(占位创建, 工位激活时替换为工位左右摄像头)
  547. if (enable_bucket) {
  548. auto chL = makeChannel(kBucketLeftId, "钢包位左", "");
  549. auto chR = makeChannel(kBucketRightId, "钢包位右", "");
  550. lock_guard<mutex> lk(bucket_channels_mtx);
  551. bucket_channels.push_back(chL);
  552. bucket_channels.push_back(chR);
  553. cout << "[Main] 钢包位专用通道: 左CH" << kBucketLeftId
  554. << " 右CH" << kBucketRightId << endl;
  555. }
  556. // ========== 工位切换回调 ==========
  557. // 钢包位专用通道跟随工位切换(左右摄像头), 普通rtsp通道不受影响
  558. bucketManager.setActivateCallback([&](const BucketInfo* old, const BucketInfo* cur) {
  559. if (!enable_bucket) {
  560. idleDetector.reset();
  561. return;
  562. }
  563. cerr << "[Bucket] 回调触发: old=" << (old ? old->description : "null")
  564. << " cur=" << (cur ? cur->description : "null") << endl;
  565. // 停止旧工位钢包位摄像头
  566. if (old && old->id >= 0) {
  567. lock_guard<mutex> lk(bucket_channels_mtx);
  568. for (auto& ch : bucket_channels) ch->capture->stop();
  569. cerr << "[Bucket] 停止钢包位摄像头" << endl;
  570. }
  571. // 启动新工位钢包位摄像头
  572. if (cur && cur->id >= 0) {
  573. lock_guard<mutex> lk(bucket_channels_mtx);
  574. cerr << "[Bucket] 激活工位: " << cur->description << endl;
  575. for (auto& ch : bucket_channels) {
  576. if (ch->id == kBucketLeftId && !cur->leftCamera.url.empty()) {
  577. ch->capture->stop();
  578. ch->capture = make_shared<RtspCapture>(kBucketLeftId,
  579. cur->leftCamera.url, cur->description + "_左");
  580. ch->capture->start();
  581. cerr << "[Bucket] 钢包位左摄像头: " << cur->leftCamera.url << endl;
  582. }
  583. if (ch->id == kBucketRightId && !cur->rightCamera.url.empty()) {
  584. ch->capture->stop();
  585. ch->capture = make_shared<RtspCapture>(kBucketRightId,
  586. cur->rightCamera.url, cur->description + "_右");
  587. ch->capture->start();
  588. cerr << "[Bucket] 钢包位右摄像头: " << cur->rightCamera.url << endl;
  589. }
  590. }
  591. } else {
  592. cerr << "[Bucket] 工位取消激活" << endl;
  593. }
  594. idleDetector.reset();
  595. cerr << "[Bucket] 回调完成" << endl;
  596. });
  597. // ========== Web API: 工位管理 ==========
  598. web.setGetHandler("/api/status", [&]() -> string {
  599. json s;
  600. s["running"] = true;
  601. s["model"] = model_path;
  602. s["npu"] = "RKNN";
  603. s["skip_frames"] = skip_frames;
  604. s["conf_thresh"] = conf_thresh;
  605. s["alarm_retention_days"] = alarm_retention_days;
  606. s["alarm_classes"] = alarm_classes;
  607. s["target_enabled"] = target_enabled;
  608. s["pose_enabled"] = pose_ok;
  609. s["pose_cfg_enabled"] = pose_enabled;
  610. s["pose_model"] = pose_model_path;
  611. s["fall_thresh"] = fall_thresh;
  612. s["fall_frames"] = fall_frames;
  613. s["fall_relays"] = fall_relays;
  614. s["grid_output"] = grid_output;
  615. s["steel_output"] = steel_output;
  616. json arr = json::array();
  617. {
  618. lock_guard<mutex> lk(channels_mtx);
  619. for (auto& ch : channels) {
  620. json c; c["id"] = ch->id;
  621. c["name"] = ch->capture->name();
  622. c["url"] = ch->capture->url();
  623. c["connected"] = ch->capture->isRunning();
  624. c["online"] = ch->capture->online();
  625. c["frame_age_ms"] = ch->capture->frameAgeMs();
  626. c["frames"] = ch->capture->framesDecoded();
  627. c["width"] = ch->capture->frameWidth();
  628. c["height"] = ch->capture->frameHeight();
  629. c["pose_running"] = ch->pose_running;
  630. c["fall_count"] = ch->pose_fall_count;
  631. c["falls"] = (int)ch->fall_poses.size();
  632. arr.push_back(c);
  633. }
  634. }
  635. s["channels"] = arr;
  636. return s.dump();
  637. });
  638. // ========== Web API: 工位管理 ==========
  639. web.setGetHandler("/api/buckets", [&]() -> string {
  640. json r;
  641. r["buckets"] = bucketManager.saveConfig();
  642. r["active_id"] = bucketManager.getActiveBucketId();
  643. r["mode"] = bucketManager.getSwitchMode() == SwitchMode::AUTO ? "auto" : "hand";
  644. return r.dump();
  645. });
  646. web.setPostHandler("/api/buckets", [&](const string& body) -> string {
  647. try {
  648. json j = json::parse(body);
  649. if (j.contains("buckets") && j["buckets"].is_array()) {
  650. // 保存工位列表
  651. json full = loadJson(config_path);
  652. full["buckets"] = j["buckets"];
  653. saveJson(config_path, full);
  654. bucketManager.loadConfig(full);
  655. cout << "[Bucket] 配置已保存" << endl;
  656. }
  657. if (j.contains("mode")) {
  658. string mode = j["mode"].get<string>();
  659. bucketManager.setSwitchMode(mode == "auto" ? SwitchMode::AUTO : SwitchMode::HAND);
  660. }
  661. if (j.contains("activate")) {
  662. int id = j["activate"].get<int>();
  663. bucketManager.activateBucket(id);
  664. }
  665. return R"({"ok":true})";
  666. } catch (exception& e) {
  667. return string(R"({"ok":false,"error":")") + e.what() + R"("})";
  668. }
  669. });
  670. web.setGetHandler("/api/buckets/status", [&]() -> string {
  671. json r;
  672. r["module_type"] = module_type;
  673. if (rfidClient) {
  674. r["rfid"]["online"] = rfidClient->isOnline();
  675. r["rfid"]["last_tag"] = rfidClient->lastTag();
  676. }
  677. if (modbusClient) {
  678. r["modbus"]["online"] = modbusClient->isOnline();
  679. }
  680. r["active_id"] = bucketManager.getActiveBucketId();
  681. r["mode"] = bucketManager.getSwitchMode() == SwitchMode::AUTO ? "auto" : "hand";
  682. return r.dump();
  683. });
  684. web.setPostHandler("/api/buckets/activate", [&](const string& body) -> string {
  685. try {
  686. json j = json::parse(body);
  687. int id = j.value("id", -1);
  688. bucketManager.setSwitchMode(SwitchMode::HAND);
  689. bucketManager.activateBucket(id);
  690. return R"({"ok":true})";
  691. } catch (...) {
  692. return R"({"ok":false})";
  693. }
  694. });
  695. web.setPostHandler("/api/buckets/mode", [&](const string& body) -> string {
  696. try {
  697. json j = json::parse(body);
  698. string mode = j.value("mode", "auto");
  699. bucketManager.setSwitchMode(mode == "auto" ? SwitchMode::AUTO : SwitchMode::HAND);
  700. return R"({"ok":true})";
  701. } catch (...) {
  702. return R"({"ok":false})";
  703. }
  704. });
  705. // ========== Web API: 通讯配置 ==========
  706. web.setGetHandler("/api/module/config", [&]() -> string {
  707. json r;
  708. r["module_type"] = module_type;
  709. if (rfidClient) r["rfid"] = {
  710. {"ip", rfidClient->getConfig().ip},
  711. {"port", rfidClient->getConfig().port},
  712. {"stability_time", rfidClient->getConfig().stability_time},
  713. {"stability_count", rfidClient->getConfig().stability_count}
  714. };
  715. if (modbusClient) r["modbus"] = {
  716. {"ip", modbusClient->getConfig().ip},
  717. {"port", modbusClient->getConfig().port},
  718. {"frequency", modbusClient->getConfig().frequency},
  719. {"stability_count", modbusClient->getConfig().stability_count}
  720. };
  721. r["idle_timeout"] = cfg.value("idle_timeout", 5);
  722. return r.dump();
  723. });
  724. web.setPostHandler("/api/module/config", [&](const string& body) -> string {
  725. try {
  726. json j = json::parse(body);
  727. json full = loadJson(config_path);
  728. if (j.contains("module_type")) {
  729. module_type = j["module_type"].get<string>();
  730. full["module_type"] = module_type;
  731. }
  732. if (j.contains("rfid")) {
  733. full["rfid"] = j["rfid"];
  734. if (rfidClient) {
  735. RfidConfig rfidCfg;
  736. rfidCfg.ip = j["rfid"].value("ip", "");
  737. rfidCfg.port = j["rfid"].value("port", 60000);
  738. rfidCfg.stability_time = j["rfid"].value("stability_time", 5);
  739. rfidCfg.stability_count = j["rfid"].value("stability_count", 3);
  740. rfidClient->setConfig(rfidCfg);
  741. }
  742. }
  743. if (j.contains("modbus")) {
  744. full["modbus"] = j["modbus"];
  745. if (modbusClient) {
  746. ModbusConfig modbusCfg;
  747. modbusCfg.ip = j["modbus"].value("ip", "");
  748. modbusCfg.port = j["modbus"].value("port", 502);
  749. modbusCfg.frequency = j["modbus"].value("frequency", 100);
  750. modbusCfg.stability_count = j["modbus"].value("stability_count", 3);
  751. modbusClient->setConfig(modbusCfg);
  752. }
  753. }
  754. if (j.contains("idle_timeout")) {
  755. full["idle_timeout"] = j["idle_timeout"];
  756. idleDetector.setTimeout(j["idle_timeout"].get<int>());
  757. }
  758. saveJson(config_path, full);
  759. cout << "[Config] 通讯配置已保存" << endl;
  760. return R"({"ok":true})";
  761. } catch (exception& e) {
  762. return string(R"({"ok":false,"error":")") + e.what() + R"("})";
  763. }
  764. });
  765. web.setGetHandler("/api/fences", [&]() -> string {
  766. json r = json::object();
  767. vector<int> ids;
  768. {
  769. lock_guard<mutex> lk(channels_mtx);
  770. for (auto& ch : channels) ids.push_back(ch->id);
  771. }
  772. for (int cid : ids) {
  773. auto fs = fenceEngine.getFences(cid);
  774. json fa = json::array();
  775. for (auto& f : fs) {
  776. json fj; fj["id"] = f.id; fj["name"] = f.name;
  777. fj["active"] = f.active;
  778. json pts = json::array();
  779. for (auto& p : f.polygon) pts.push_back({p.x, p.y});
  780. fj["polygon"] = pts;
  781. json rl = json::array();
  782. for (auto& r : f.relays) rl.push_back(r);
  783. fj["relays"] = rl;
  784. fa.push_back(fj);
  785. }
  786. r[to_string(cid)] = fa;
  787. }
  788. return r.dump();
  789. return r.dump();
  790. });
  791. web.setPostHandler("/api/fences", [&](const string& body) -> string {
  792. try {
  793. json j = json::parse(body);
  794. vector<int> ids;
  795. {
  796. lock_guard<mutex> lk(channels_mtx);
  797. for (auto& ch : channels) ids.push_back(ch->id);
  798. }
  799. for (int cid : ids) {
  800. string key = to_string(cid);
  801. if (j.contains(key)) {
  802. vector<FenceRegion> fences;
  803. for (auto& fj : j[key]) {
  804. FenceRegion fr;
  805. fr.id = fj.value("id", 0);
  806. fr.name = fj.value("name", "");
  807. fr.active = fj.value("active", true);
  808. if (fj.contains("relays") && fj["relays"].is_array())
  809. for (auto& r : fj["relays"])
  810. if (r.is_string())
  811. fr.relays.push_back(r.get<string>());
  812. for (auto& pt : fj["polygon"])
  813. fr.polygon.push_back(cv::Point2f(pt[0], pt[1]));
  814. fences.push_back(fr);
  815. }
  816. fenceEngine.setFences(cid, fences);
  817. }
  818. }
  819. fenceEngine.saveFences(fence_file);
  820. return R"({"ok":true})";
  821. } catch (exception& e) {
  822. return string(R"({"ok":false,"error":")") + e.what() + R"("})";
  823. }
  824. });
  825. auto validYChannel = [](const string& ch) -> bool {
  826. if (ch.size() != 3 || ch[0] != 'y') return false;
  827. if (!isdigit((unsigned char)ch[1]) || !isdigit((unsigned char)ch[2]))
  828. return false;
  829. int n = (ch[1] - '0') * 10 + (ch[2] - '0');
  830. return n >= 1 && n <= 8;
  831. };
  832. // 持久化输出通道模式到 config.json
  833. auto saveYConfig = [&](const array<YChanMode, 8>& modes) {
  834. json full = loadJson(config_path);
  835. if (!full.contains("relay") || !full["relay"].is_object())
  836. full["relay"] = json::object();
  837. json arr = json::array();
  838. for (int i = 0; i < 8; i++) {
  839. arr.push_back({
  840. {"channel", "y0" + to_string(i + 1)},
  841. {"mode", modes[i].mode == 1 ? "cycle" : "normal"},
  842. {"interval_sec", modes[i].interval_sec}
  843. });
  844. }
  845. full["relay"]["y_config"] = arr;
  846. saveJson(config_path, full);
  847. };
  848. web.setPostHandler("/api/relay", [&](const string& body) -> string {
  849. try {
  850. json j = json::parse(body);
  851. string ch = j.value("channel", "");
  852. int st = j.value("state", -1);
  853. if (!validYChannel(ch) || (st != 0 && st != 1))
  854. return R"({"ok":false,"error":"bad channel/state"})";
  855. int idx = (ch[1] - '0') * 10 + (ch[2] - '0') - 1;
  856. if (idx >= relay.getOutCount())
  857. return R"({"ok":false,"error":"no such channel"})";
  858. // 周期模式下的通道被手动控制时, 自动切回正常模式
  859. auto modes = relay.getModes();
  860. if (modes[idx].mode == 1) {
  861. modes[idx].mode = 0;
  862. relay.setModes(modes);
  863. saveYConfig(modes);
  864. cout << "[Relay] " << ch << " 手动控制, 已切回正常模式" << endl;
  865. }
  866. if (relay.setChannel(ch, st)) {
  867. alarmHistory.recordAction("手动",
  868. ch + (st ? " 手动闭合" : " 手动断开"), ch);
  869. return R"({"ok":true})";
  870. }
  871. return R"({"ok":false,"error":"offline"})";
  872. } catch (...) {}
  873. return R"({"ok":false})";
  874. });
  875. web.setGetHandler("/api/relay/status", [&]() -> string {
  876. json s;
  877. s["online"] = relay.isOnline();
  878. auto xs = relay.getX();
  879. auto ys = relay.getY();
  880. json xa = json::array(), ya = json::array();
  881. for (int i = 0; i < 8; i++) { xa.push_back(xs[i]); ya.push_back(ys[i]); }
  882. s["x"] = xa;
  883. s["y"] = ya;
  884. s["nx"] = relay.getInCount(); // 实际输入路数
  885. s["ny"] = relay.getOutCount(); // 实际输出路数
  886. auto modes = relay.getModes();
  887. json ma = json::array(), ca = json::array();
  888. for (int i = 0; i < 8; i++) {
  889. ma.push_back(modes[i].mode == 1 ? "cycle" : "normal");
  890. ca.push_back(modes[i].interval_sec);
  891. }
  892. s["ymode"] = ma;
  893. s["ycyc"] = ca;
  894. RelayConfig c = relay.getConfig();
  895. s["host"] = c.host;
  896. s["port"] = c.port;
  897. s["device_addr"] = c.device_addr;
  898. s["pulse_sec"] = c.pulse_sec;
  899. s["rearm_sec"] = rearm_sec;
  900. s["alarm_max_sec"] = alarm_max_sec;
  901. {
  902. json al = json::object();
  903. for (auto& [k, v] : rly_alias) al[k] = v;
  904. s["alias"] = al;
  905. }
  906. return s.dump();
  907. });
  908. web.setPostHandler("/api/relay/mode", [&](const string& body) -> string {
  909. try {
  910. json j = json::parse(body);
  911. if (!j.contains("items") || !j["items"].is_array())
  912. return R"({"ok":false,"error":"items required"})";
  913. array<YChanMode, 8> modes = relay.getModes();
  914. for (auto& e : j["items"]) {
  915. string ch = e.value("channel", "");
  916. if (ch.size() != 3 || ch[0] != 'y') continue;
  917. int idx = (ch[1] - '0') * 10 + (ch[2] - '0');
  918. if (idx < 1 || idx > 8) continue;
  919. string m = e.value("mode", "");
  920. if (m != "normal" && m != "cycle") continue;
  921. modes[idx - 1].mode = (m == "cycle") ? 1 : 0;
  922. if (e.contains("interval_sec"))
  923. modes[idx - 1].interval_sec = e["interval_sec"].get<double>();
  924. }
  925. relay.setModes(modes);
  926. saveYConfig(modes);
  927. cout << "[Relay] 输出模式更新" << endl;
  928. return R"({"ok":true})";
  929. } catch (...) { return R"({"ok":false})"; }
  930. });
  931. web.setPostHandler("/api/relay/config", [&](const string& body) -> string {
  932. try {
  933. json j = json::parse(body);
  934. RelayConfig c = relay.getConfig();
  935. if (j.contains("host") && j["host"].is_string()) {
  936. string h = j["host"].get<string>();
  937. if (!h.empty()) c.host = h;
  938. }
  939. if (j.contains("port")) {
  940. int p = j["port"].get<int>();
  941. if (p >= 1 && p <= 65535) c.port = p;
  942. }
  943. if (j.contains("device_addr")) {
  944. int a = j["device_addr"].get<int>();
  945. if (a >= 0 && a <= 254) c.device_addr = a;
  946. }
  947. if (j.contains("pulse_sec")) {
  948. int v = j["pulse_sec"].get<int>();
  949. if (v >= 1 && v <= 3600) c.pulse_sec = v;
  950. }
  951. if (j.contains("rearm_sec")) {
  952. int v = j["rearm_sec"].get<int>();
  953. if (v >= 1 && v <= 600) rearm_sec = v;
  954. }
  955. if (j.contains("alarm_max_sec")) {
  956. int v = j["alarm_max_sec"].get<int>();
  957. if (v >= 0 && v <= 3600) alarm_max_sec = v;
  958. }
  959. rc = c;
  960. relay.setConfig(c);
  961. json full = loadJson(config_path);
  962. if (!full.contains("relay") || !full["relay"].is_object())
  963. full["relay"] = json::object();
  964. full["relay"]["host"] = c.host;
  965. full["relay"]["port"] = c.port;
  966. full["relay"]["device_addr"] = c.device_addr;
  967. full["relay"]["pulse_sec"] = c.pulse_sec;
  968. full["relay"]["rearm_sec"] = rearm_sec;
  969. full["relay"]["alarm_max_sec"] = alarm_max_sec;
  970. full["relay"].erase("channel");
  971. full["relay"].erase("pulse_ms");
  972. full["relay"].erase("alarm_mode");
  973. full["relay"].erase("di_pulse_sec");
  974. saveJson(config_path, full);
  975. cout << "[Relay] 配置更新: " << c.host << ":" << c.port
  976. << " 地址" << c.device_addr
  977. << " 脉冲" << c.pulse_sec << "秒"
  978. << " 回合上限" << alarm_max_sec << "秒"
  979. << " 重新武装" << rearm_sec << "秒" << endl;
  980. return R"({"ok":true})";
  981. } catch (...) { return R"({"ok":false})"; }
  982. });
  983. web.setGetHandler("/api/relay/bindings", [&]() -> string {
  984. json b = json::object();
  985. for (int i = 1; i <= 8; i++) {
  986. string k = "x" + to_string(i);
  987. json arr = json::array();
  988. auto it = di_bindings.find(k);
  989. if (it != di_bindings.end())
  990. for (auto& c : it->second) arr.push_back(c);
  991. b[k] = arr;
  992. }
  993. return b.dump();
  994. });
  995. web.setPostHandler("/api/relay/bindings", [&](const string& body) -> string {
  996. try {
  997. json j = json::parse(body);
  998. map<string, vector<string>> nb;
  999. for (auto& [k, v] : j.items()) {
  1000. if (!v.is_array()) continue;
  1001. vector<string> chans;
  1002. for (auto& c : v)
  1003. if (c.is_string()) {
  1004. string ch = c.get<string>();
  1005. if (validYChannel(ch)) chans.push_back(ch);
  1006. }
  1007. if (!chans.empty()) nb[k] = chans;
  1008. }
  1009. di_bindings = nb;
  1010. json full = loadJson(config_path);
  1011. if (!full.contains("relay") || !full["relay"].is_object())
  1012. full["relay"] = json::object();
  1013. json db = json::object();
  1014. for (auto& [k, v] : di_bindings) db[k] = v;
  1015. full["relay"]["di_bindings"] = db;
  1016. saveJson(config_path, full);
  1017. return R"({"ok":true})";
  1018. } catch (...) { return R"({"ok":false})"; }
  1019. });
  1020. // ---- 通道别名 ----
  1021. auto normAliasKey = [](string k) -> string {
  1022. if (k.empty()) return "";
  1023. k[0] = (char)tolower((unsigned char)k[0]);
  1024. if (k[0] != 'x' && k[0] != 'y') return "";
  1025. int n = 0;
  1026. try { n = stoi(k.substr(1)); } catch (...) { return ""; }
  1027. if (n < 1 || n > 8) return "";
  1028. if (k[0] == 'y') return "y" + to_string(n).insert(0, n < 10 ? "0" : "");
  1029. return "x" + to_string(n);
  1030. };
  1031. web.setGetHandler("/api/relay/aliases", [&]() -> string {
  1032. json a = json::object();
  1033. for (auto& [k, v] : rly_alias) a[k] = v;
  1034. return a.dump();
  1035. });
  1036. web.setPostHandler("/api/relay/aliases", [&](const string& body) -> string {
  1037. try {
  1038. json j = json::parse(body);
  1039. map<string, string> na;
  1040. for (auto& [k, v] : j.items())
  1041. if (v.is_string()) {
  1042. string key = normAliasKey(k);
  1043. if (!key.empty()) na[key] = v.get<string>();
  1044. }
  1045. rly_alias = na;
  1046. json full = loadJson(config_path);
  1047. if (!full.contains("relay") || !full["relay"].is_object())
  1048. full["relay"] = json::object();
  1049. json aj = json::object();
  1050. for (auto& [k, v] : rly_alias) aj[k] = v;
  1051. full["relay"]["aliases"] = aj;
  1052. saveJson(config_path, full);
  1053. cout << "[Relay] 别名已更新: " << rly_alias.size() << " 条" << endl;
  1054. return R"({"ok":true})";
  1055. } catch (...) { return R"({"ok":false})"; }
  1056. });
  1057. // ---- 继电器动作历史 ----
  1058. web.setGetHandler("/api/relay/actions", [&]() -> string {
  1059. json arr = json::array();
  1060. auto acts = alarmHistory.getActions(200);
  1061. for (auto& a : acts) {
  1062. json aj;
  1063. aj["id"] = a.id;
  1064. aj["timestamp"] = a.timestamp;
  1065. aj["source"] = a.source;
  1066. aj["detail"] = a.detail;
  1067. aj["channels"] = a.channels;
  1068. arr.push_back(aj);
  1069. }
  1070. json r; r["items"] = arr;
  1071. return r.dump();
  1072. });
  1073. web.setPostHandler("/api/relay/actions/clear",
  1074. [&](const string&) -> string {
  1075. alarmHistory.clearActions();
  1076. return R"({"ok":true})";
  1077. });
  1078. // ---------- 通道管理: 增删改查 + 热插拔 ----------
  1079. auto persistRtspList = [&]() {
  1080. json full = loadJson(config_path);
  1081. json arr = json::array();
  1082. {
  1083. lock_guard<mutex> lk(channels_mtx);
  1084. for (auto& ch : channels) {
  1085. arr.push_back({{"id", ch->id},
  1086. {"name", ch->capture->name()},
  1087. {"url", ch->capture->url()}});
  1088. }
  1089. }
  1090. full["rtsp"] = arr;
  1091. saveJson(config_path, full);
  1092. };
  1093. web.setPostHandler("/api/channels/save", [&](const string& body) -> string {
  1094. try {
  1095. json j = json::parse(body);
  1096. string name = j.value("name", "");
  1097. string url = j.value("url", "");
  1098. int id = j.value("id", -1);
  1099. if (url.rfind("rtsp://", 0) != 0)
  1100. return R"({"ok":false,"error":"URL必须以rtsp://开头"})";
  1101. if (name.empty()) name = "通道";
  1102. shared_ptr<ChannelState> old;
  1103. auto nu = makeChannel(id, name, url);
  1104. {
  1105. lock_guard<mutex> lk(channels_mtx);
  1106. if (id >= 0) {
  1107. for (auto& ch : channels)
  1108. if (ch->id == id) { old = ch; break; }
  1109. if (!old)
  1110. return R"({"ok":false,"error":"通道不存在"})";
  1111. } else {
  1112. if ((int)channels.size() >= kMaxChannels)
  1113. return R"({"ok":false,"error":"已达最大通道数8"})";
  1114. int max_id = -1;
  1115. for (auto& ch : channels) max_id = std::max(max_id, ch->id);
  1116. nu->id = id = max_id + 1;
  1117. }
  1118. nu->capture->start();
  1119. for (auto& c : channels)
  1120. if (c.get() == old.get()) { c = nu; break; }
  1121. if (!old) channels.push_back(nu);
  1122. }
  1123. registerChannelProviders(nu);
  1124. if (old) old->capture->stop(); // 锁外停旧流(join可能耗时)
  1125. persistRtspList();
  1126. cout << "[Channel] " << (old ? "修改" : "添加")
  1127. << " CH" << id << ": " << url << endl;
  1128. json r; r["ok"] = true; r["id"] = id;
  1129. return r.dump();
  1130. } catch (exception& e) {
  1131. return string(R"({"ok":false,"error":")") + e.what() + R"("})";
  1132. }
  1133. });
  1134. web.setPostHandler("/api/channels/delete", [&](const string& body) -> string {
  1135. try {
  1136. json j = json::parse(body);
  1137. int id = j.value("id", -1);
  1138. shared_ptr<ChannelState> victim;
  1139. {
  1140. lock_guard<mutex> lk(channels_mtx);
  1141. for (auto it = channels.begin(); it != channels.end(); ++it) {
  1142. if ((*it)->id == id) {
  1143. victim = *it;
  1144. channels.erase(it);
  1145. break;
  1146. }
  1147. }
  1148. }
  1149. if (!victim)
  1150. return R"({"ok":false,"error":"通道不存在"})";
  1151. web.removeFrameProvider(id);
  1152. victim->capture->stop(); // 锁外停止并回收解码线程
  1153. persistRtspList();
  1154. cout << "[Channel] 删除 CH" << id << endl;
  1155. return R"({"ok":true})";
  1156. } catch (...) { return R"({"ok":false})"; }
  1157. });
  1158. web.setPostHandler("/api/channels/test", [&](const string& body) -> string {
  1159. try {
  1160. json j = json::parse(body);
  1161. string url = j.value("url", "");
  1162. if (url.rfind("rtsp://", 0) != 0)
  1163. return R"({"ok":false,"error":"URL必须以rtsp://开头"})";
  1164. // 解析 host[:port] (忽略用户信息与路径)
  1165. string rest = url.substr(7);
  1166. size_t at = rest.find('@');
  1167. if (at != string::npos) rest = rest.substr(at + 1);
  1168. size_t slash = rest.find('/');
  1169. if (slash != string::npos) rest = rest.substr(0, slash);
  1170. string host = rest;
  1171. int port = 554;
  1172. size_t colon = rest.find(':');
  1173. if (colon != string::npos) {
  1174. host = rest.substr(0, colon);
  1175. port = stoi(rest.substr(colon + 1));
  1176. }
  1177. int fd = socket(AF_INET, SOCK_STREAM, 0);
  1178. if (fd < 0) return R"({"ok":false,"error":"socket失败"})";
  1179. int fl = fcntl(fd, F_GETFL, 0);
  1180. fcntl(fd, F_SETFL, fl | O_NONBLOCK);
  1181. struct sockaddr_in addr;
  1182. memset(&addr, 0, sizeof(addr));
  1183. addr.sin_family = AF_INET;
  1184. addr.sin_port = htons(port);
  1185. inet_pton(AF_INET, host.c_str(), &addr.sin_addr);
  1186. bool ok = false;
  1187. string err;
  1188. if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == 0) {
  1189. ok = true;
  1190. } else if (errno == EINPROGRESS) {
  1191. struct pollfd pfd = {fd, POLLOUT, 0};
  1192. if (poll(&pfd, 1, 3000) > 0) {
  1193. int e = 0;
  1194. socklen_t el = sizeof(e);
  1195. getsockopt(fd, SOL_SOCKET, SO_ERROR, &e, &el);
  1196. ok = (e == 0);
  1197. if (!ok) err = strerror(e);
  1198. } else {
  1199. err = "连接超时";
  1200. }
  1201. } else {
  1202. err = strerror(errno);
  1203. }
  1204. close(fd);
  1205. json r;
  1206. r["ok"] = ok;
  1207. r["host"] = host + ":" + to_string(port);
  1208. if (!err.empty()) r["error"] = err;
  1209. cout << "[Channel] 连通测试 " << host << ":" << port
  1210. << " -> " << (ok ? "OK" : err) << endl;
  1211. return r.dump();
  1212. } catch (exception& e) {
  1213. return string(R"({"ok":false,"error":")") + e.what() + R"("})";
  1214. }
  1215. });
  1216. web.setPostHandler("/api/config", [&](const string& body) -> string {
  1217. try {
  1218. json j = json::parse(body);
  1219. json full = loadJson(config_path);
  1220. if (j.contains("skip_frames")) {
  1221. full["skip_frames"] = j["skip_frames"];
  1222. skip_frames = j["skip_frames"].get<int>();
  1223. lock_guard<mutex> lk(channels_mtx);
  1224. for (auto& ch : channels) ch->skip_frames = skip_frames;
  1225. }
  1226. if (j.contains("conf_thresh"))
  1227. full["conf_thresh"] = j["conf_thresh"];
  1228. if (j.contains("target_enabled")) {
  1229. bool en = j["target_enabled"].get<bool>();
  1230. full["target_enabled"] = en;
  1231. target_enabled = en;
  1232. cout << "[Target] 目标识别: " << (en ? "启用" : "禁用")
  1233. << " (重启后生效)" << endl;
  1234. }
  1235. if (j.contains("pose_enabled")) {
  1236. bool en = j["pose_enabled"].get<bool>();
  1237. full["pose_enabled"] = en;
  1238. pose_enabled = en;
  1239. cout << "[Pose] 姿态识别: " << (en ? "启用" : "禁用")
  1240. << " (重启后生效)" << endl;
  1241. }
  1242. if (j.contains("grid_output")) {
  1243. bool v = j["grid_output"].get<bool>();
  1244. full["grid_output"] = v;
  1245. grid_output = v;
  1246. cout << "[Display] 网格输出: " << (v ? "启用" : "禁用")
  1247. << " (重启后生效)" << endl;
  1248. }
  1249. if (j.contains("steel_output")) {
  1250. bool v = j["steel_output"].get<bool>();
  1251. full["steel_output"] = v;
  1252. steel_output = v;
  1253. cout << "[Display] 钢包位输出: " << (v ? "启用" : "禁用")
  1254. << " (重启后生效)" << endl;
  1255. }
  1256. if (j.contains("alarm_retention_days")) {
  1257. int v = j["alarm_retention_days"].get<int>();
  1258. if (v < 1) v = 1;
  1259. full["alarm_retention_days"] = v;
  1260. alarm_retention_days = v;
  1261. }
  1262. if (j.contains("alarm_classes") && j["alarm_classes"].is_array()) {
  1263. vector<string> ac;
  1264. for (auto& c : j["alarm_classes"])
  1265. if (c.is_string()) ac.push_back(c.get<string>());
  1266. full["alarm_classes"] = ac;
  1267. alarm_classes = ac;
  1268. }
  1269. if (j.contains("auto_reboot") && j["auto_reboot"].is_object()) {
  1270. auto& ar = j["auto_reboot"];
  1271. if (!full.contains("auto_reboot") || !full["auto_reboot"].is_object())
  1272. full["auto_reboot"] = json::object();
  1273. if (ar.contains("enabled")) {
  1274. bool en = ar["enabled"].get<bool>();
  1275. full["auto_reboot"]["enabled"] = en;
  1276. reboot_enabled = en;
  1277. }
  1278. if (ar.contains("cycle")) {
  1279. int c = ar["cycle"].get<int>();
  1280. if (c < 1) c = 1;
  1281. if (c > 365) c = 365;
  1282. full["auto_reboot"]["cycle"] = c;
  1283. rb_cycle = c;
  1284. }
  1285. if (ar.contains("time") && ar["time"].is_string()) {
  1286. int h = -1, m = -1;
  1287. sscanf(ar["time"].get<string>().c_str(), "%d:%d", &h, &m);
  1288. if (h >= 0 && h <= 23 && m >= 0 && m <= 59) {
  1289. char norm[8];
  1290. snprintf(norm, sizeof(norm), "%02d:%02d", h, m);
  1291. full["auto_reboot"]["time"] = norm;
  1292. rb_h = h; rb_m = m;
  1293. rb_done_date.clear();
  1294. }
  1295. }
  1296. cout << "[System] 定时重启更新: "
  1297. << (reboot_enabled ? "启用" : "禁用")
  1298. << " 周期" << rb_cycle << "天 时间"
  1299. << rb_h << ":" << (rb_m < 10 ? "0" : "") << rb_m << endl;
  1300. }
  1301. saveJson(config_path, full);
  1302. return R"({"ok":true})";
  1303. } catch (...) { return R"({"ok":false})"; }
  1304. });
  1305. web.setGetHandler("/api/system", [&]() -> string {
  1306. json s;
  1307. s["reboot_enabled"] = reboot_enabled;
  1308. s["reboot_cycle"] = rb_cycle;
  1309. char tbuf[8];
  1310. snprintf(tbuf, sizeof(tbuf), "%02d:%02d", rb_h, rb_m);
  1311. s["reboot_time"] = tbuf;
  1312. return s.dump();
  1313. });
  1314. web.setPostHandler("/api/reboot", [&](const string&) -> string {
  1315. cout << "[System] Web触发立即重启" << endl;
  1316. cout.flush();
  1317. std::thread([]() {
  1318. this_thread::sleep_for(chrono::milliseconds(500));
  1319. sync();
  1320. system("(sleep 1; systemctl reboot -i) >/dev/null 2>&1 &");
  1321. fflush(nullptr);
  1322. _exit(0);
  1323. }).detach();
  1324. return R"({"ok":true})";
  1325. });
  1326. web.setGetHandlerQ("/api/alarms", [&](const map<string, string>& q) -> string {
  1327. auto geti = [&](const string& k, int def) -> int {
  1328. auto it = q.find(k);
  1329. if (it == q.end() || it->second.empty()) return def;
  1330. return atoi(it->second.c_str());
  1331. };
  1332. auto gets = [&](const string& k) -> string {
  1333. auto it = q.find(k);
  1334. return it == q.end() ? string() : it->second;
  1335. };
  1336. int limit = clamp(geti("limit", 200), 1, 500);
  1337. int ch = geti("channel", -1);
  1338. string start = gets("start");
  1339. string end = gets("end");
  1340. json arr = json::array();
  1341. auto alarms = alarmHistory.getAlarms(limit, 0, ch, start, end);
  1342. for (auto& a : alarms) {
  1343. json aj;
  1344. aj["id"] = a.id;
  1345. aj["timestamp"] = a.timestamp;
  1346. aj["channel"] = a.channel;
  1347. aj["fence_name"] = a.fence_name;
  1348. aj["class_name"] = a.class_name;
  1349. aj["confidence"] = a.confidence;
  1350. aj["screenshot"] = a.screenshot_path;
  1351. arr.push_back(aj);
  1352. }
  1353. json r;
  1354. r["total"] = alarmHistory.getAlarmCount(ch, start, end);
  1355. r["items"] = arr;
  1356. return r.dump();
  1357. });
  1358. web.setPostHandler("/api/alarms/delete", [&](const string& body) -> string {
  1359. try {
  1360. json j = json::parse(body);
  1361. int64_t id = j.value("id", 0);
  1362. if (alarmHistory.deleteAlarm(id)) return R"({"ok":true})";
  1363. } catch (...) {}
  1364. return R"({"ok":false})";
  1365. });
  1366. web.setPostHandler("/api/alarms/clear", [&](const string& body) -> string {
  1367. alarmHistory.clearAll();
  1368. return R"({"ok":true})";
  1369. });
  1370. web.setGetHandler("/api/alarms/count", [&]() -> string {
  1371. json r;
  1372. r["count"] = alarmHistory.getAlarmCount();
  1373. return r.dump();
  1374. });
  1375. // ========== 姿态报警历史 API ==========
  1376. web.setGetHandlerQ("/api/pose/alarms", [&](const map<string, string>& q) -> string {
  1377. auto geti = [&](const string& k, int def) -> int {
  1378. auto it = q.find(k);
  1379. if (it == q.end() || it->second.empty()) return def;
  1380. return atoi(it->second.c_str());
  1381. };
  1382. auto gets = [&](const string& k) -> string {
  1383. auto it = q.find(k);
  1384. return it == q.end() ? string() : it->second;
  1385. };
  1386. int limit = clamp(geti("limit", 200), 1, 500);
  1387. int ch = geti("channel", -1);
  1388. string action = gets("action");
  1389. string start = gets("start");
  1390. string end = gets("end");
  1391. json arr = json::array();
  1392. auto list = alarmHistory.getPoseAlarms(limit, 0, ch, action, start, end);
  1393. for (auto& a : list) {
  1394. json aj;
  1395. aj["id"] = a.id;
  1396. aj["timestamp"] = a.timestamp;
  1397. aj["channel"] = a.channel;
  1398. aj["action"] = a.action;
  1399. aj["score"] = a.score;
  1400. aj["screenshot"] = a.screenshot_path;
  1401. arr.push_back(aj);
  1402. }
  1403. json r;
  1404. r["total"] = alarmHistory.getPoseAlarmCount(ch, action, start, end);
  1405. r["items"] = arr;
  1406. return r.dump();
  1407. });
  1408. web.setPostHandler("/api/pose/alarms/delete", [&](const string& body) -> string {
  1409. try {
  1410. json j = json::parse(body);
  1411. int64_t id = j.value("id", 0);
  1412. if (alarmHistory.deletePoseAlarm(id)) return R"({"ok":true})";
  1413. } catch (...) {}
  1414. return R"({"ok":false})";
  1415. });
  1416. web.setPostHandler("/api/pose/alarms/clear", [&](const string&) -> string {
  1417. alarmHistory.clearPoseAlarms();
  1418. return R"({"ok":true})";
  1419. });
  1420. web.setGetHandler("/api/pose/alarms/count", [&]() -> string {
  1421. json r;
  1422. r["count"] = alarmHistory.getPoseAlarmCount();
  1423. return r.dump();
  1424. });
  1425. // 通道帧提供器已在创建时逐路注册(registerChannelProviders)
  1426. // ========== 姿态识别 Web API ==========
  1427. web.setGetHandler("/api/pose/status", [&]() -> string {
  1428. json r;
  1429. r["pose_enabled"] = pose_ok;
  1430. r["pose_cfg_enabled"] = pose_enabled;
  1431. r["model"] = pose_model_path;
  1432. r["fall_thresh"] = fall_thresh;
  1433. r["fall_frames"] = fall_frames;
  1434. r["fall_relays"] = fall_relays;
  1435. json chs = json::array();
  1436. {
  1437. lock_guard<mutex> lk(channels_mtx);
  1438. for (auto& ch : channels) {
  1439. json c;
  1440. c["id"] = ch->id;
  1441. c["running"] = ch->pose_running;
  1442. c["fall_count"] = ch->pose_fall_count;
  1443. c["falls"] = (int)ch->fall_poses.size();
  1444. chs.push_back(c);
  1445. }
  1446. }
  1447. r["channels"] = chs;
  1448. return r.dump();
  1449. });
  1450. web.setPostHandler("/api/pose/config", [&](const string& body) -> string {
  1451. try {
  1452. json j = json::parse(body);
  1453. json full = loadJson(config_path);
  1454. if (j.contains("fall_thresh")) {
  1455. float v = j["fall_thresh"].get<float>();
  1456. if (v >= 0.0f && v <= 1.0f) {
  1457. fall_thresh = v;
  1458. fallAnalyzer.setThreshold(v);
  1459. full["fall_thresh"] = v;
  1460. }
  1461. }
  1462. if (j.contains("fall_frames")) {
  1463. int v = j["fall_frames"].get<int>();
  1464. if (v >= 1 && v <= 30) {
  1465. fall_frames = v;
  1466. full["fall_frames"] = v;
  1467. }
  1468. }
  1469. if (j.contains("fall_relays") && j["fall_relays"].is_array()) {
  1470. fall_relays.clear();
  1471. for (auto& c : j["fall_relays"])
  1472. if (c.is_string()) fall_relays.push_back(c.get<string>());
  1473. full["fall_relays"] = fall_relays;
  1474. }
  1475. if (j.contains("channels") && j["channels"].is_object()) {
  1476. lock_guard<mutex> lk(channels_mtx);
  1477. for (auto& [k, v] : j["channels"].items()) {
  1478. int cid = atoi(k.c_str());
  1479. if (!v.is_object() || !v.contains("running")) continue;
  1480. bool run = v["running"].get<bool>();
  1481. for (auto& ch : channels)
  1482. if (ch->id == cid) {
  1483. ch->pose_running = run && pose_ok;
  1484. if (!ch->pose_running) ch->pose_fall_count = 0;
  1485. }
  1486. }
  1487. }
  1488. saveJson(config_path, full);
  1489. cout << "[Pose] 配置已更新: 阈值=" << fall_thresh
  1490. << " 连续" << fall_frames << "帧" << endl;
  1491. return R"({"ok":true})";
  1492. } catch (exception& e) {
  1493. return string(R"({"ok":false,"error":")") + e.what() + R"("})";
  1494. }
  1495. });
  1496. // ========== MJPEG推流开关(临时关闭省CPU) ==========
  1497. web.setGetHandler("/api/mjpeg/status", [&]() -> string {
  1498. json r;
  1499. r["enabled"] = web.mjpegEnabled();
  1500. return r.dump();
  1501. });
  1502. web.setPostHandler("/api/mjpeg/toggle", [&](const string& body) -> string {
  1503. try {
  1504. json j = json::parse(body);
  1505. if (!j.contains("enabled"))
  1506. return R"({"ok":false,"error":"enabled required"})";
  1507. bool en = j["enabled"].get<bool>();
  1508. web.setMjpegEnabled(en);
  1509. cout << "[Web] MJPEG推流: " << (en ? "启用" : "临时关闭")
  1510. << " (省CPU)" << endl;
  1511. json r;
  1512. r["ok"] = true;
  1513. r["enabled"] = en;
  1514. return r.dump();
  1515. } catch (exception& e) {
  1516. return string(R"({"ok":false,"error":")") + e.what() + R"("})";
  1517. }
  1518. });
  1519. web.start();
  1520. // 网格布局固定为已配置的rtsp通道数, 避免启动/断帧时布局跳动
  1521. int grid_fixed = (int)channels.size();
  1522. displayGrid.start(grid_output, hdmi_grid_cols, app_name + " - 通道网格",
  1523. hdmi_fullscreen, 100, grid_fixed);
  1524. displayBucket.start(steel_output && enable_bucket, 2, app_name + " - 钢包位",
  1525. hdmi_fullscreen, 100, 2);
  1526. cout << "[Main] Ctrl+C 退出" << endl;
  1527. // 自动激活第一个工位(如果有配置)
  1528. if (enable_bucket) {
  1529. auto bs = bucketManager.getBuckets();
  1530. if (!bs.empty()) {
  1531. cout << "[Main] 自动激活第一个工位: " << bs[0].description << endl;
  1532. bucketManager.setSwitchMode(SwitchMode::AUTO);
  1533. bucketManager.activateBucket(bs[0].id);
  1534. }
  1535. }
  1536. auto last_pulse = chrono::steady_clock::now() - chrono::hours(1);
  1537. auto last_di_pulse = chrono::steady_clock::now();
  1538. array<uint8_t, 8> prev_x{};
  1539. // DI联动状态: latched=回合进行中(触发过, 等待信号复位解锁)
  1540. // high_since=本回合起始时刻(回合上限计时)
  1541. // last_high=最后高电平时刻(解锁计时用)
  1542. array<bool, 8> di_latched_{{false}};
  1543. array<chrono::steady_clock::time_point, 8> di_last_high_{};
  1544. array<chrono::steady_clock::time_point, 8> di_high_since_{};
  1545. auto last_cleanup = chrono::steady_clock::now();
  1546. auto last_rb_check = chrono::steady_clock::now() - chrono::seconds(20);
  1547. while (g_running.load()) {
  1548. this_thread::sleep_for(chrono::milliseconds(1));
  1549. auto now_tc = chrono::steady_clock::now();
  1550. if (chrono::duration_cast<chrono::hours>(now_tc - last_cleanup).count() >= 1) {
  1551. alarmHistory.cleanup(alarm_retention_days);
  1552. cout << "[Alarm] 定时清理完成: 保留" << alarm_retention_days << "天" << endl;
  1553. last_cleanup = now_tc;
  1554. }
  1555. // 定时重启(每10秒检查一次, 每个周期日只触发一次)
  1556. if (reboot_enabled &&
  1557. chrono::duration_cast<chrono::seconds>(now_tc - last_rb_check).count() >= 10) {
  1558. last_rb_check = now_tc;
  1559. time_t t = time(nullptr);
  1560. struct tm tmv;
  1561. localtime_r(&t, &tmv);
  1562. char today[16], hhmm[8];
  1563. strftime(today, sizeof(today), "%Y-%m-%d", &tmv);
  1564. strftime(hhmm, sizeof(hhmm), "%H:%M", &tmv);
  1565. char target[8];
  1566. snprintf(target, sizeof(target), "%02d:%02d", rb_h, rb_m);
  1567. long day_num = (long)(t / 86400);
  1568. bool cycle_hit = (rb_cycle <= 1) || (day_num % rb_cycle == 0);
  1569. if (strcmp(hhmm, target) == 0 && cycle_hit && rb_done_date != today) {
  1570. rb_done_date = today;
  1571. cout << "[System] 定时重启触发: " << hhmm << endl;
  1572. cout.flush();
  1573. this_thread::sleep_for(chrono::milliseconds(800));
  1574. sync();
  1575. system("(sleep 1; systemctl reboot -i) >/dev/null 2>&1 &");
  1576. fflush(nullptr);
  1577. _exit(0);
  1578. }
  1579. }
  1580. // RFID/Modbus空闲检测
  1581. if (rfidClient) rfidClient->checkIdle();
  1582. if (enable_bucket && idleDetector.isIdle() && bucketManager.getActiveBucketId() >= 0) {
  1583. cout << "[Bucket] 空闲超时,取消激活" << endl;
  1584. bucketManager.activateBucket(-1);
  1585. }
  1586. // DI输入联动: 与围栏报警同一套策略
  1587. // 触发出现(上升沿) ──▶ 立即输出一个脉冲
  1588. // 信号保持期间 ──▶ 每 pulse_sec 秒重复, 累计满 alarm_max_sec 秒后静默
  1589. // 信号复位持续 rearm_sec 秒 ──▶ 重新武装, 下次触发再来一轮
  1590. {
  1591. array<uint8_t, 8> xs = relay.getX();
  1592. for (int i = 0; i < 8; i++) {
  1593. bool rise = (xs[i] && !prev_x[i]);
  1594. // 解锁判定: 信号复位持续 rearm_sec 秒(高电平会刷新计时)
  1595. if (di_latched_[i]) {
  1596. if (xs[i]) di_last_high_[i] = now_tc;
  1597. else if (chrono::duration_cast<chrono::seconds>(
  1598. now_tc - di_last_high_[i]).count() >= rearm_sec)
  1599. di_latched_[i] = false;
  1600. } else if (xs[i]) {
  1601. di_last_high_[i] = now_tc;
  1602. }
  1603. auto it = di_bindings.find("x" + to_string(i + 1));
  1604. bool bound = it != di_bindings.end() && !it->second.empty();
  1605. // 首次触发: 上升沿立即动作, 开启新回合
  1606. if (rise && !bound) continue;
  1607. if (rise && !di_latched_[i]) {
  1608. last_di_pulse = now_tc; // 首发后重新起算重复节奏
  1609. relay.pulseChannels(it->second, rc.pulse_sec);
  1610. di_latched_[i] = true;
  1611. di_high_since_[i] = now_tc;
  1612. cout << "[DI] X" << (i + 1) << " 联动触发 -> "
  1613. << it->second.size() << "路继电器" << endl;
  1614. string chans;
  1615. for (auto& c : it->second) {
  1616. if (!chans.empty()) chans += ",";
  1617. chans += c;
  1618. }
  1619. alarmHistory.recordAction("输入联动",
  1620. "X" + to_string(i + 1) +
  1621. " 信号有效", chans);
  1622. continue;
  1623. }
  1624. // 回合内重复: 信号保持 且 未超回合上限, 按节奏窗口重复
  1625. if (!xs[i] || !di_latched_[i] || !bound) continue;
  1626. bool within_round =
  1627. alarm_max_sec == 0 ||
  1628. chrono::duration_cast<chrono::milliseconds>(
  1629. now_tc - di_high_since_[i]).count() <
  1630. (long long)alarm_max_sec * 1000;
  1631. if (!within_round) continue;
  1632. auto dms = chrono::duration_cast<chrono::milliseconds>(
  1633. now_tc - last_di_pulse).count();
  1634. if (dms > rc.pulse_sec * 1000) {
  1635. last_di_pulse = now_tc;
  1636. relay.pulseChannels(it->second, rc.pulse_sec);
  1637. }
  1638. }
  1639. prev_x = xs;
  1640. }
  1641. // 通道快照: 热插拔期间主循环安全遍历
  1642. vector<shared_ptr<ChannelState>> ch_snap;
  1643. {
  1644. lock_guard<mutex> lk(channels_mtx);
  1645. ch_snap = channels;
  1646. }
  1647. // 钢包位专用通道(跟随工位切换)
  1648. {
  1649. lock_guard<mutex> lk(bucket_channels_mtx);
  1650. for (auto& bc : bucket_channels)
  1651. ch_snap.push_back(bc);
  1652. }
  1653. for (auto& ch : ch_snap) {
  1654. Nv12Frame nv12;
  1655. if (!ch->capture->grabNv12(nv12)) {
  1656. if (ch->frame_count == 0 && ch->capture->isRunning())
  1657. this_thread::sleep_for(chrono::milliseconds(10));
  1658. if (ch->frame_count == 0 && ch->id == 0) {
  1659. static int dbg_cnt = 0;
  1660. if (++dbg_cnt % 50 == 1)
  1661. cerr << "[CH" << ch->id << "] waiting for frame..." << endl;
  1662. }
  1663. continue;
  1664. }
  1665. ch->frame_count++;
  1666. if (!ch->size_recorded && nv12.width > 0 && nv12.height > 0) {
  1667. fenceEngine.setChannelSize(ch->id, nv12.width, nv12.height);
  1668. ch->size_recorded = true;
  1669. cout << "[CH" << ch->id << "] 分辨率: "
  1670. << nv12.width << "x" << nv12.height << endl;
  1671. }
  1672. bool do_infer = (ch->frame_count % (ch->skip_frames + 1) == 0);
  1673. if (ch->frame_count <= 5) {
  1674. cout << "[CH" << ch->id << "] frame=" << ch->frame_count
  1675. << " size=" << nv12.width << "x" << nv12.height
  1676. << " stride=" << nv12.stride << endl;
  1677. }
  1678. float scale = 1.0f;
  1679. int pad_x = 0, pad_y = 0;
  1680. if (do_infer)
  1681. ch->rga.process(nv12, ch->rgb_buf, scale, pad_x, pad_y);
  1682. if (do_infer && !ch->rgb_buf.empty()) {
  1683. if (ch->frame_count <= 4)
  1684. cout << "[CH" << ch->id << "] infer frame=" << ch->frame_count
  1685. << " rgb=" << ch->rgb_buf.size() << endl;
  1686. // 检测与姿态并行推理(不同NPU核), 缩短单帧处理时间
  1687. vector<Detection> dets;
  1688. vector<PersonPose> poses;
  1689. bool do_det = target_enabled;
  1690. bool do_pose = (pose_ok && ch->pose_running);
  1691. if (do_det && do_pose) {
  1692. std::thread tPose([&]() {
  1693. poses = poseDetector.detect(ch->rgb_buf.data(),
  1694. nv12.width, nv12.height,
  1695. scale, pad_x, pad_y);
  1696. });
  1697. dets = detector.detect(ch->rgb_buf.data(),
  1698. nv12.width, nv12.height,
  1699. scale, pad_x, pad_y);
  1700. tPose.join();
  1701. } else if (do_det) {
  1702. dets = detector.detect(ch->rgb_buf.data(),
  1703. nv12.width, nv12.height,
  1704. scale, pad_x, pad_y);
  1705. } else if (do_pose) {
  1706. poses = poseDetector.detect(ch->rgb_buf.data(),
  1707. nv12.width, nv12.height,
  1708. scale, pad_x, pad_y);
  1709. }
  1710. vector<cv::Point2f> centers;
  1711. vector<int> cids;
  1712. vector<string> cnames;
  1713. vector<float> confs;
  1714. for (auto& d : dets) {
  1715. // 只统计勾选的入侵目标类别
  1716. if (!alarm_classes.empty() &&
  1717. find(alarm_classes.begin(), alarm_classes.end(),
  1718. d.class_name) == alarm_classes.end())
  1719. continue;
  1720. centers.push_back(cv::Point2f(
  1721. d.bbox.x + d.bbox.width / 2.0f,
  1722. d.bbox.y + d.bbox.height / 2.0f));
  1723. cids.push_back(d.class_id);
  1724. cnames.push_back(d.class_name);
  1725. confs.push_back(d.confidence);
  1726. }
  1727. auto events = fenceEngine.evaluate(
  1728. ch->id, centers, cids, cnames, confs);
  1729. {
  1730. lock_guard<mutex> lk(ch->mtx);
  1731. ch->detections = dets;
  1732. ch->events = events;
  1733. }
  1734. // ========== 姿态识别(跌倒检测) ==========
  1735. if (pose_ok && ch->pose_running) {
  1736. vector<PersonPose> falls;
  1737. // 对每个人做多动作分析, 取最高分动作
  1738. PoseAction bestAction = PoseAction::NONE;
  1739. float bestScore = 0.0f;
  1740. for (auto& p : poses) {
  1741. if (pose_alarm_classes.empty() ||
  1742. find(pose_alarm_classes.begin(),
  1743. pose_alarm_classes.end(),
  1744. "person") == pose_alarm_classes.end())
  1745. continue;
  1746. auto ar = fallAnalyzer.analyze(p);
  1747. if (ar.action != PoseAction::NONE &&
  1748. ar.action != PoseAction::LYING &&
  1749. ar.score > bestScore) {
  1750. bestAction = ar.action;
  1751. bestScore = ar.score;
  1752. }
  1753. }
  1754. bool hit = (bestAction != PoseAction::NONE);
  1755. // 连续 N 帧确认(防抖), 并按上升沿触发动作
  1756. if (hit) ch->pose_fall_count++;
  1757. else ch->pose_fall_count = 0;
  1758. bool pose_rising = (ch->pose_fall_count == fall_frames);
  1759. {
  1760. lock_guard<mutex> lk(ch->mtx);
  1761. ch->poses = poses;
  1762. ch->fall_poses = hit ? poses : vector<PersonPose>{};
  1763. ch->pose_action = hit ? poseActionName(bestAction) : "";
  1764. ch->pose_score = bestScore;
  1765. }
  1766. if (pose_rising) {
  1767. ch->fall_seq.fetch_add(1, std::memory_order_relaxed);
  1768. cout << "[Pose] CH" << ch->id << " 检测到"
  1769. << poseActionName(bestAction) << "("
  1770. << ch->pose_fall_count << "帧) score="
  1771. << bestScore << endl;
  1772. }
  1773. if (pose_rising && !ch->fall_latched) {
  1774. ch->fall_latched = true;
  1775. ch->fall_latch_ts = chrono::steady_clock::now();
  1776. string actName = poseActionName(bestAction);
  1777. // 联动报警: 触发继电器(可配置 fall_relays)
  1778. if (!fall_relays.empty()) {
  1779. relay.pulseChannels(fall_relays, rc.pulse_sec);
  1780. string chans;
  1781. for (auto& t : fall_relays) {
  1782. if (!chans.empty()) chans += ",";
  1783. chans += t;
  1784. }
  1785. alarmHistory.recordAction("姿态报警",
  1786. "CH" + to_string(ch->id) + " 检测到" + actName,
  1787. chans);
  1788. }
  1789. // 截图记录到姿态报警历史
  1790. std::vector<uint8_t> frgb;
  1791. int fout_w, fout_h;
  1792. ch->rga.processToRgb(nv12, frgb, fout_w, fout_h);
  1793. if (!frgb.empty()) {
  1794. cv::Mat fshot(fout_h, fout_w, CV_8UC3, frgb.data());
  1795. cv::cvtColor(fshot, fshot, cv::COLOR_RGB2BGR);
  1796. float fs = (nv12.width > 0)
  1797. ? (float)fshot.cols / nv12.width : 1.0f;
  1798. drawPoseDets(fshot, poses, poses, fs);
  1799. drawPoseBanner(fshot, ch->id,
  1800. poses.empty() ? PersonPose() : poses[0],
  1801. actName);
  1802. alarmHistory.recordPoseAlarm(ch->id, actName,
  1803. bestScore, fshot);
  1804. }
  1805. }
  1806. // 动作消失(或恢复正常)持续 rearm_sec 秒后重新武装
  1807. if (ch->fall_latched && !hit) {
  1808. auto fnow = chrono::steady_clock::now();
  1809. if (chrono::duration_cast<chrono::seconds>(
  1810. fnow - ch->fall_latch_ts).count() >= rearm_sec)
  1811. ch->fall_latched = false;
  1812. }
  1813. } else if (!pose_ok || !ch->pose_running) {
  1814. ch->pose_fall_count = 0;
  1815. }
  1816. if (!events.empty()) {
  1817. auto now = chrono::steady_clock::now();
  1818. // 动作判定(统一策略):
  1819. // 首次检测立即动作开启回合; 之后每 pulse_sec 秒重复,
  1820. // 回合累计满 alarm_max_sec 秒后静默,
  1821. // 直到目标消除持续 rearm_sec 秒重新武装
  1822. bool episode_start = !ch->alarm_latched;
  1823. if (episode_start) ch->episode_start = now;
  1824. bool within_round =
  1825. alarm_max_sec == 0 ||
  1826. chrono::duration_cast<chrono::milliseconds>(
  1827. now - ch->episode_start).count() <
  1828. (long long)alarm_max_sec * 1000;
  1829. bool do_action =
  1830. episode_start ||
  1831. (within_round &&
  1832. chrono::duration_cast<chrono::milliseconds>(
  1833. now - last_pulse).count() >
  1834. rc.pulse_sec * 1000);
  1835. if (do_action) {
  1836. last_pulse = now;
  1837. // 合并所有触发围栏绑定的输出通道, 去重后统一脉冲
  1838. set<string> trig;
  1839. auto fs = fenceEngine.getFences(ch->id);
  1840. for (auto& ev : events)
  1841. for (auto& f : fs)
  1842. if (f.id == ev.fence_id)
  1843. for (auto& r : f.relays) trig.insert(r);
  1844. if (!trig.empty()) {
  1845. cout << "[Event] CH" << ch->id
  1846. << " " << events[0].detection_class_name
  1847. << " in " << events[0].fence_name
  1848. << " -> " << trig.size() << "路继电器" << endl;
  1849. relay.pulseChannels(
  1850. vector<string>(trig.begin(), trig.end()),
  1851. rc.pulse_sec);
  1852. string chans;
  1853. for (auto& t : trig) {
  1854. if (!chans.empty()) chans += ",";
  1855. chans += t;
  1856. }
  1857. alarmHistory.recordAction("围栏报警",
  1858. events[0].detection_class_name + " 入侵 " +
  1859. events[0].fence_name + " (CH" +
  1860. to_string(ch->id) + ")", chans);
  1861. }
  1862. }
  1863. // 截图与动作配对: 每次动作必有对应截图证据
  1864. bool take_screenshot = do_action;
  1865. ch->alarm_latched = true;
  1866. ch->last_alarm_ts = now;
  1867. if (take_screenshot) {
  1868. std::vector<uint8_t> rgb8;
  1869. int out_w, out_h;
  1870. ch->rga.processToRgb(nv12, rgb8, out_w, out_h);
  1871. if (!rgb8.empty()) { cv::Mat shot(out_h, out_w, CV_8UC3, rgb8.data());
  1872. cv::cvtColor(shot, shot, cv::COLOR_RGB2BGR);
  1873. float sds = (nv12.width > 0)
  1874. ? (float)shot.cols / nv12.width : 1.0f;
  1875. // 围栏: 填充+描边
  1876. auto fences = fenceEngine.getFences(ch->id);
  1877. bool hf = false;
  1878. for (auto& f : fences)
  1879. if (f.active && f.polygon.size() >= 3) { hf = true; break; }
  1880. if (hf) {
  1881. cv::Mat fill;
  1882. buildFenceFill(fill, fences, shot.size(), sds);
  1883. cv::addWeighted(fill, 0.15, shot, 0.85, 0, shot);
  1884. drawFenceEdges(shot, fences, sds);
  1885. }
  1886. // 目标框(报警目标红色高亮)
  1887. vector<Detection> dets_copy;
  1888. vector<FenceEvent> evs_copy;
  1889. {
  1890. lock_guard<mutex> lk(ch->mtx);
  1891. dets_copy = ch->detections;
  1892. evs_copy = ch->events;
  1893. }
  1894. drawDets(shot, dets_copy, evs_copy, sds, false);
  1895. // 左上角红底白字详细信息
  1896. drawAlarmBanner(shot, ch->id, events);
  1897. alarmHistory.record(ch->id, events[0].fence_name,
  1898. events[0].detection_class_name,
  1899. events[0].confidence, shot);
  1900. } else {
  1901. cerr << "[Alarm] 截图失败: RGA输出为空 CH"
  1902. << ch->id << endl;
  1903. }
  1904. }
  1905. } else {
  1906. // 报警消失需持续 rearm_sec 秒才重新武装,
  1907. // 防止目标检测抖动误判为"离开"
  1908. auto now2 = chrono::steady_clock::now();
  1909. if (ch->alarm_latched &&
  1910. chrono::duration_cast<chrono::seconds>(
  1911. now2 - ch->last_alarm_ts).count() >= rearm_sec)
  1912. ch->alarm_latched = false;
  1913. }
  1914. }
  1915. // 显示帧: 每帧都生成(视频不跳帧), 推理仍按 skip_frames 跳帧
  1916. // 检测框/围栏只在推理帧更新, 非推理帧复用最近一次推理结果
  1917. cv::Mat display;
  1918. {
  1919. std::vector<uint8_t> rgb8;
  1920. int out_w, out_h;
  1921. ch->rga.processToRgb(nv12, rgb8, out_w, out_h);
  1922. if (!rgb8.empty()) {
  1923. display = cv::Mat(out_h, out_w, CV_8UC3, rgb8.data()).clone();
  1924. cv::cvtColor(display, display, cv::COLOR_RGB2BGR);
  1925. }
  1926. }
  1927. if (!display.empty()) {
  1928. float ds = (nv12.width > 0) ? (float)display.cols / nv12.width : 1.0f;
  1929. auto fences = fenceEngine.getFences(ch->id);
  1930. uint64_t fv = fenceEngine.version();
  1931. bool has_fence = false;
  1932. for (auto& f : fences)
  1933. if (f.active && f.polygon.size() >= 3) { has_fence = true; break; }
  1934. if (has_fence) {
  1935. if (ch->fence_ver != fv || ch->fence_fill.size() != display.size()) {
  1936. buildFenceFill(ch->fence_fill, fences, display.size(), ds);
  1937. ch->fence_ver = fv;
  1938. }
  1939. cv::addWeighted(ch->fence_fill, 0.15, display, 0.85, 0, display);
  1940. drawFenceEdges(display, fences, ds);
  1941. } else {
  1942. ch->fence_ver = 0;
  1943. ch->fence_fill.release();
  1944. }
  1945. {
  1946. lock_guard<mutex> lk(ch->mtx);
  1947. drawDets(display, ch->detections, ch->events, ds);
  1948. if (pose_ok && ch->pose_running)
  1949. drawPoseDets(display, ch->poses, ch->fall_poses, ds);
  1950. }
  1951. char lbl[32];
  1952. snprintf(lbl, sizeof(lbl), "CH%d", ch->id);
  1953. cv::putText(display, lbl,
  1954. cv::Point(display.cols - 80, 25),
  1955. cv::FONT_HERSHEY_SIMPLEX, 0.7,
  1956. cv::Scalar(255, 255, 255), 2);
  1957. lock_guard<mutex> lk(ch->mtx);
  1958. ch->display_frame = display;
  1959. ch->disp_seq.fetch_add(1, std::memory_order_relaxed);
  1960. }
  1961. // 输出到本机显示器(HDMI)
  1962. if (displayGrid.isRunning() && ch->id < 100) {
  1963. char dlbl[64];
  1964. snprintf(dlbl, sizeof(dlbl), "CH%d %s",
  1965. ch->id, ch->capture->name().c_str());
  1966. displayGrid.setFrame(ch->id, display, dlbl);
  1967. }
  1968. if (displayBucket.isRunning() && ch->id >= 100) {
  1969. char dlbl[64];
  1970. snprintf(dlbl, sizeof(dlbl), "%s",
  1971. ch->capture->name().c_str());
  1972. displayBucket.setFrame(ch->id, display, dlbl);
  1973. }
  1974. }
  1975. }
  1976. cout << "[Main] 正在退出..." << endl;
  1977. {
  1978. lock_guard<mutex> lk(channels_mtx);
  1979. for (auto& ch : channels) ch->capture->stop();
  1980. }
  1981. relay.stop(); // 先停继电器线程, 避免HTTP处理线程阻塞在transact上拖住web.stop()
  1982. web.stop();
  1983. cout << "[Main] 已退出" << endl;
  1984. return 0;
  1985. }