Quellcode durchsuchen

HDMI输出拆分为网格+钢包位双模式

- displayGrid: 普通通道网格(固定rtsp通道, 按通道数量自动布局)
- displayBucket: 钢包位专用通道(CH100左/CH101右, 跟随工位自动切换)
- 工位切换只影响钢包位专用通道, 不影响普通rtsp通道
- config: 新增 grid_output/steel_output 两个开关(系统配置checkbox)
- Web: /api/config 支持动态修改, /api/status 返回状态
dev vor 1 Woche
Ursprung
Commit
eb024be973
3 geänderte Dateien mit 172 neuen und 44 gelöschten Zeilen
  1. 34 13
      config/config.json
  2. 83 28
      src/main.cpp
  3. 55 3
      web/config.html

+ 34 - 13
config/config.json

@@ -45,7 +45,15 @@
         }
     ],
     "conf_thresh": 0.3499999940395355,
+    "fall_frames": 3,
+    "fall_relays": [
+        "y01"
+    ],
+    "fall_thresh": 0.5,
     "fence_file": "config/fences.json",
+    "grid_output": true,
+    "hdmi_fullscreen": true,
+    "hdmi_grid_cols": 0,
     "idle_timeout": 5,
     "input_size": 640,
     "modbus": {
@@ -57,22 +65,14 @@
     "model_path": "models/yolov5s_rk3588_int8.rknn",
     "module_type": "RFID",
     "nms_thresh": 0.45,
-    "pose_enabled": true,
-    "pose_model_path": "models/yolov8n-pose_rk3588_fp.rknn",
-    "pose_input_size": 640,
-    "pose_conf_thresh": 0.35,
-    "pose_nms_thresh": 0.45,
-    "fall_thresh": 0.5,
-    "fall_frames": 3,
     "pose_alarm_classes": [
         "person"
     ],
-    "fall_relays": [
-        "y01"
-    ],
-    "hdmi_output": true,
-    "hdmi_grid_cols": 0,
-    "hdmi_fullscreen": true,
+    "pose_conf_thresh": 0.35,
+    "pose_enabled": true,
+    "pose_input_size": 640,
+    "pose_model_path": "models/yolov8n-pose_rk3588_fp.rknn",
+    "pose_nms_thresh": 0.45,
     "relay": {
         "alarm_max_sec": 30,
         "aliases": {
@@ -149,10 +149,31 @@
             "id": 1,
             "name": "122",
             "url": "rtsp://admin:hd135792468@192.168.1.122:554/Streaming/Channels/102"
+        },
+        {
+            "id": 2,
+            "name": "3",
+            "url": "rtsp://admin:hd135792468@192.168.1.123:554/Streaming/Channels/102"
+        },
+        {
+            "id": 3,
+            "name": "4",
+            "url": "rtsp://admin:hd135792468@192.168.1.124:554/Streaming/Channels/102"
+        },
+        {
+            "id": 4,
+            "name": "5",
+            "url": "rtsp://admin:hd135792468@192.168.1.125:554/Streaming/Channels/102"
+        },
+        {
+            "id": 5,
+            "name": "6",
+            "url": "rtsp://admin:hd135792468@192.168.1.126:554/Streaming/Channels/102"
         }
     ],
     "rtsp_timeout": 10,
     "skip_frames": 3,
+    "steel_output": true,
     "web_auth": {
         "password": "admin",
         "username": "admin"

+ 83 - 28
src/main.cpp

@@ -300,10 +300,13 @@ int main(int argc, char* argv[]) {
 
     // ========== 本机画面输出(HDMI显示器) ==========
     string app_name = cfg.value("app_name", "钢包工位视频监测系统");
-    bool hdmi_output = cfg.value("hdmi_output", true);
+    // 两个独立显示器输出: 网格(普通通道) + 钢包位(工位左右摄像头)
+    bool grid_output = cfg.value("grid_output", true);
+    bool steel_output = cfg.value("steel_output", true);
     int hdmi_grid_cols = cfg.value("hdmi_grid_cols", 0);
     bool hdmi_fullscreen = cfg.value("hdmi_fullscreen", true);
-    cout << "[Display] HDMI画面输出: " << (hdmi_output ? "启用" : "禁用")
+    cout << "[Display] 网格输出: " << (grid_output ? "启用" : "禁用")
+         << " 钢包位输出: " << (steel_output ? "启用" : "禁用")
          << " 网格列=" << hdmi_grid_cols
          << " 全屏=" << (hdmi_fullscreen ? "是" : "否") << endl;
 
@@ -500,8 +503,15 @@ int main(int argc, char* argv[]) {
     mutex channels_mtx;
     vector<shared_ptr<ChannelState>> channels;
 
+    // 钢包位专用通道(id=100左, 101右), 跟随工位切换, 独立于普通rtsp通道
+    static const int kBucketLeftId = 100;
+    static const int kBucketRightId = 101;
+    mutex bucket_channels_mtx;
+    vector<shared_ptr<ChannelState>> bucket_channels;
+
     WebServer web;
-    DisplayOutput displayOutput;
+    DisplayOutput displayGrid;    // 普通通道网格(自动布局)
+    DisplayOutput displayBucket;  // 钢包位(当前工位左右摄像头)
     web.setPort(web_port);
 
     // 登录凭据(默认admin/admin), 持久化于config.json的web_auth
@@ -562,42 +572,50 @@ int main(int argc, char* argv[]) {
     cout << "[Main] 视频通道: " << channels.size() << "/"
          << kMaxChannels << " 路" << endl;
 
+    // 钢包位专用通道(占位创建, 工位激活时替换为工位左右摄像头)
+    {
+        auto chL = makeChannel(kBucketLeftId, "钢包位左", "");
+        auto chR = makeChannel(kBucketRightId, "钢包位右", "");
+        lock_guard<mutex> lk(bucket_channels_mtx);
+        bucket_channels.push_back(chL);
+        bucket_channels.push_back(chR);
+    }
+    cout << "[Main] 钢包位专用通道: 左CH" << kBucketLeftId
+         << " 右CH" << kBucketRightId << endl;
+
     // ========== 工位切换回调 ==========
-    // 工位切换时,始终使用 channel 0 (左) 和 channel 1 (右)
+    // 钢包位专用通道跟随工位切换(左右摄像头), 普通rtsp通道不受影响
     bucketManager.setActivateCallback([&](const BucketInfo* old, const BucketInfo* cur) {
         cerr << "[Bucket] 回调触发: old=" << (old ? old->description : "null")
              << " cur=" << (cur ? cur->description : "null") << endl;
-        // 停止旧工位摄像头
+        // 停止旧工位钢包位摄像头
         if (old && old->id >= 0) {
-            lock_guard<mutex> lk(channels_mtx);
-            for (auto& ch : channels) {
-                if (ch->id == 0 || ch->id == 1) {
-                    ch->capture->stop();
-                }
-            }
-            cerr << "[Bucket] 停止工位 " << old->description << " 摄像头" << endl;
+            lock_guard<mutex> lk(bucket_channels_mtx);
+            for (auto& ch : bucket_channels) ch->capture->stop();
+            cerr << "[Bucket] 停止钢包位摄像头" << endl;
         }
-        // 启动新工位摄像头
+        // 启动新工位钢包位摄像头
         if (cur && cur->id >= 0) {
-            lock_guard<mutex> lk(channels_mtx);
-            cerr << "[Bucket] channels数量: " << channels.size() << endl;
-            for (auto& ch : channels) {
-                cerr << "[Bucket] 检查channel id=" << ch->id << endl;
-                if (ch->id == 0 && !cur->leftCamera.url.empty()) {
+            lock_guard<mutex> lk(bucket_channels_mtx);
+            cerr << "[Bucket] 激活工位: " << cur->description << endl;
+            for (auto& ch : bucket_channels) {
+                if (ch->id == kBucketLeftId && !cur->leftCamera.url.empty()) {
                     ch->capture->stop();
-                    cerr << "[Bucket] 创建左摄像头 capture url=" << cur->leftCamera.url << endl;
-                    ch->capture = make_shared<RtspCapture>(0, cur->leftCamera.url, cur->description + "_左");
+                    ch->capture = make_shared<RtspCapture>(kBucketLeftId,
+                        cur->leftCamera.url, cur->description + "_左");
                     ch->capture->start();
-                    cerr << "[Bucket] 启动 " << cur->description << " 左摄像头 CH0" << endl;
+                    cerr << "[Bucket] 钢包位左摄像头: " << cur->leftCamera.url << endl;
                 }
-                if (ch->id == 1 && !cur->rightCamera.url.empty()) {
+                if (ch->id == kBucketRightId && !cur->rightCamera.url.empty()) {
                     ch->capture->stop();
-                    cerr << "[Bucket] 创建右摄像头 capture url=" << cur->rightCamera.url << endl;
-                    ch->capture = make_shared<RtspCapture>(1, cur->rightCamera.url, cur->description + "_右");
+                    ch->capture = make_shared<RtspCapture>(kBucketRightId,
+                        cur->rightCamera.url, cur->description + "_右");
                     ch->capture->start();
-                    cerr << "[Bucket] 启动 " << cur->description << " 右摄像头 CH1" << endl;
+                    cerr << "[Bucket] 钢包位右摄像头: " << cur->rightCamera.url << endl;
                 }
             }
+        } else {
+            cerr << "[Bucket] 工位取消激活" << endl;
         }
         idleDetector.reset();
         cerr << "[Bucket] 回调完成" << endl;
@@ -619,6 +637,8 @@ int main(int argc, char* argv[]) {
         s["fall_thresh"] = fall_thresh;
         s["fall_frames"] = fall_frames;
         s["fall_relays"] = fall_relays;
+        s["grid_output"] = grid_output;
+        s["steel_output"] = steel_output;
         json arr = json::array();
         {
             lock_guard<mutex> lk(channels_mtx);
@@ -1261,6 +1281,27 @@ int main(int argc, char* argv[]) {
             }
             if (j.contains("conf_thresh"))
                 full["conf_thresh"] = j["conf_thresh"];
+            if (j.contains("pose_enabled")) {
+                bool en = j["pose_enabled"].get<bool>();
+                full["pose_enabled"] = en;
+                pose_enabled = en;
+                cout << "[Pose] 姿态识别: " << (en ? "启用" : "禁用")
+                     << " (重启后生效)" << endl;
+            }
+            if (j.contains("grid_output")) {
+                bool v = j["grid_output"].get<bool>();
+                full["grid_output"] = v;
+                grid_output = v;
+                cout << "[Display] 网格输出: " << (v ? "启用" : "禁用")
+                     << " (重启后生效)" << endl;
+            }
+            if (j.contains("steel_output")) {
+                bool v = j["steel_output"].get<bool>();
+                full["steel_output"] = v;
+                steel_output = v;
+                cout << "[Display] 钢包位输出: " << (v ? "启用" : "禁用")
+                     << " (重启后生效)" << endl;
+            }
             if (j.contains("alarm_retention_days")) {
                 int v = j["alarm_retention_days"].get<int>();
                 if (v < 1) v = 1;
@@ -1394,6 +1435,7 @@ int main(int argc, char* argv[]) {
     web.setGetHandler("/api/pose/status", [&]() -> string {
         json r;
         r["pose_enabled"] = pose_ok;
+        r["pose_cfg_enabled"] = pose_enabled;
         r["model"] = pose_model_path;
         r["fall_thresh"] = fall_thresh;
         r["fall_frames"] = fall_frames;
@@ -1462,7 +1504,8 @@ int main(int argc, char* argv[]) {
     });
 
     web.start();
-    displayOutput.start(hdmi_output, hdmi_grid_cols, app_name, hdmi_fullscreen);
+    displayGrid.start(grid_output, hdmi_grid_cols, app_name + " - 通道网格", hdmi_fullscreen);
+    displayBucket.start(steel_output, 2, app_name + " - 钢包位", hdmi_fullscreen);
     cout << "[Main] Ctrl+C 退出" << endl;
 
     // 自动激活第一个工位(如果有配置)
@@ -1595,6 +1638,12 @@ int main(int argc, char* argv[]) {
             lock_guard<mutex> lk(channels_mtx);
             ch_snap = channels;
         }
+        // 钢包位专用通道(跟随工位切换)
+        {
+            lock_guard<mutex> lk(bucket_channels_mtx);
+            for (auto& bc : bucket_channels)
+                ch_snap.push_back(bc);
+        }
         for (auto& ch : ch_snap) {
             Nv12Frame nv12;
             if (!ch->capture->grabNv12(nv12)) {
@@ -1885,11 +1934,17 @@ int main(int argc, char* argv[]) {
                 ch->disp_seq.fetch_add(1, std::memory_order_relaxed);
             }
             // 输出到本机显示器(HDMI)
-            if (displayOutput.isRunning()) {
+            if (displayGrid.isRunning() && ch->id < 100) {
                 char dlbl[64];
                 snprintf(dlbl, sizeof(dlbl), "CH%d %s",
                          ch->id, ch->capture->name().c_str());
-                displayOutput.setFrame(ch->id, display, dlbl);
+                displayGrid.setFrame(ch->id, display, dlbl);
+            }
+            if (displayBucket.isRunning() && ch->id >= 100) {
+                char dlbl[64];
+                snprintf(dlbl, sizeof(dlbl), "%s",
+                         ch->capture->name().c_str());
+                displayBucket.setFrame(ch->id, display, dlbl);
             }
         }
     }

+ 55 - 3
web/config.html

@@ -133,6 +133,29 @@ body.rail .logo .t,body.rail .nav-label,body.rail .side-foot .nav-label{display:
       <h3>系统配置</h3>
       <div class="form-row">
         <div class="form-group"><label>RTSP超时(秒)</label><input type="number" id="rtspTimeout" min="1" max="60" value="10"></div>
+        <div class="form-group">
+          <label>姿态识别(跌倒检测)</label>
+          <select id="poseEnabled">
+            <option value="true">启用</option>
+            <option value="false">禁用</option>
+          </select>
+        </div>
+      </div>
+      <div class="form-row">
+        <div class="form-group">
+          <label>输出通道网格到显示器</label>
+          <select id="gridOutput">
+            <option value="true">启用</option>
+            <option value="false">禁用</option>
+          </select>
+        </div>
+        <div class="form-group">
+          <label>输出钢包位到显示器</label>
+          <select id="steelOutput">
+            <option value="true">启用</option>
+            <option value="false">禁用</option>
+          </select>
+        </div>
       </div>
       <div class="btn-row">
         <button class="btn btn-primary" onclick="saveSystemConfig()">保存系统配置</button>
@@ -146,15 +169,32 @@ let buckets = [];
 
 async function loadConfig() {
   try {
-    const [modRes, bucketRes] = await Promise.all([
+    const [modRes, bucketRes, poseRes, statusRes] = await Promise.all([
       fetch('/api/module/config'),
-      fetch('/api/buckets')
+      fetch('/api/buckets'),
+      fetch('/api/pose/status'),
+      fetch('/api/status')
     ]);
     const modData = await modRes.json();
     const bucketData = await bucketRes.json();
+    let poseEnabled = true;
+    try {
+      const poseData = await poseRes.json();
+      if (poseData.pose_cfg_enabled !== undefined) poseEnabled = poseData.pose_cfg_enabled;
+      else if (poseData.pose_enabled !== undefined) poseEnabled = poseData.pose_enabled;
+    } catch (e) {}
+    let gridOutput = true, steelOutput = true;
+    try {
+      const statusData = await statusRes.json();
+      if (statusData.grid_output !== undefined) gridOutput = statusData.grid_output;
+      if (statusData.steel_output !== undefined) steelOutput = statusData.steel_output;
+    } catch (e) {}
 
     document.getElementById('moduleType').value = modData.module_type || 'RFID';
     document.getElementById('idleTimeout').value = modData.idle_timeout || 5;
+    document.getElementById('poseEnabled').value = poseEnabled ? 'true' : 'false';
+    document.getElementById('gridOutput').value = gridOutput ? 'true' : 'false';
+    document.getElementById('steelOutput').value = steelOutput ? 'true' : 'false';
     onModuleTypeChange();
 
     if (modData.rfid) {
@@ -305,7 +345,19 @@ async function saveModuleConfig() {
   } catch (e) { alert('保存失败: ' + e.message); }
 }
 
-async function saveSystemConfig() { alert('系统配置已保存'); }
+async function saveSystemConfig() {
+  const poseEnabled = document.getElementById('poseEnabled').value === 'true';
+  const gridOutput = document.getElementById('gridOutput').value === 'true';
+  const steelOutput = document.getElementById('steelOutput').value === 'true';
+  try {
+    await fetch('/api/config', {
+      method: 'POST',
+      headers: {'Content-Type': 'application/json'},
+      body: JSON.stringify({ pose_enabled: poseEnabled, grid_output: gridOutput, steel_output: steelOutput })
+    });
+    alert('系统配置已保存' + (poseEnabled ? '' : '(姿态识别需重启后生效)'));
+  } catch (e) { alert('保存失败: ' + e.message); }
+}
 
 loadConfig();
 </script>