main.cpp 95 KB

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