|
|
@@ -38,6 +38,7 @@
|
|
|
#include "idle_detector.h"
|
|
|
#include "pose_detector.h"
|
|
|
#include "display_output.h"
|
|
|
+#include "remote_source.h"
|
|
|
|
|
|
using namespace std;
|
|
|
using json = nlohmann::json;
|
|
|
@@ -115,6 +116,15 @@ struct ChannelState {
|
|
|
mutex mtx;
|
|
|
};
|
|
|
|
|
|
+// 对端通道的最新画面缓存(主节点拉取对端时使用)
|
|
|
+struct PeerChan {
|
|
|
+ mutex m;
|
|
|
+ cv::Mat frame;
|
|
|
+ std::atomic<uint64_t> seq{0};
|
|
|
+ std::atomic<int64_t> last_ms{0};
|
|
|
+};
|
|
|
+
|
|
|
+
|
|
|
static json loadJson(const string& path) {
|
|
|
ifstream ifs(path);
|
|
|
if (!ifs.is_open()) return json::object();
|
|
|
@@ -400,6 +410,48 @@ int main(int argc, char* argv[]) {
|
|
|
// 显示帧生成帧率(所有输出端HDMI/MJPEG/手机最高约10-12fps, 无需每输入帧都生成)
|
|
|
int display_fps = clamp(cfg.value("display_fps", 12), 1, 30);
|
|
|
int display_interval_ms = 1000 / display_fps;
|
|
|
+
|
|
|
+ // ========== 双机主从(16路单屏) ==========
|
|
|
+ // 角色: master=显示16格+聚合, slave=仅采集推理供主节点拉取, standalone=单机
|
|
|
+ string node_role = cfg.value("node_role", "standalone");
|
|
|
+ // 本机通道全局编号起点(节点1=0, 节点2=8; 与角色无关, 保证编号稳定)
|
|
|
+ int local_base = clamp(cfg.value("local_base", 0), 0, 8);
|
|
|
+ // 对端节点(主节点拉取对端画面/报警)
|
|
|
+ bool peer_enabled = false;
|
|
|
+ string peer_host;
|
|
|
+ int peer_port = 8080;
|
|
|
+ string peer_user = "admin", peer_pass = "admin";
|
|
|
+ int peer_base = (local_base == 0) ? 8 : 0;
|
|
|
+ int peer_count = 8;
|
|
|
+ int remote_fps = 8;
|
|
|
+ if (cfg.contains("peer") && cfg["peer"].is_object()) {
|
|
|
+ auto& p = cfg["peer"];
|
|
|
+ peer_enabled = p.value("enabled", false);
|
|
|
+ peer_host = p.value("host", "");
|
|
|
+ peer_port = p.value("port", 8080);
|
|
|
+ peer_user = p.value("username", "admin");
|
|
|
+ peer_pass = p.value("password", "admin");
|
|
|
+ peer_base = p.value("local_base", peer_base);
|
|
|
+ peer_count = clamp(p.value("count", 8), 1, 8);
|
|
|
+ remote_fps = clamp(p.value("fps", 8), 1, 15);
|
|
|
+ }
|
|
|
+ bool is_master = (node_role == "master");
|
|
|
+ bool peer_active = is_master && peer_enabled && !peer_host.empty();
|
|
|
+ // 防止误配成本机自身(会导致状态查询递归死锁)
|
|
|
+ if (peer_active && (peer_host == "127.0.0.1" || peer_host == "localhost") &&
|
|
|
+ peer_port == web_port) {
|
|
|
+ cerr << "[Node] 对端地址不能是本机自身, 已禁用对端拉取" << endl;
|
|
|
+ peer_active = false;
|
|
|
+ }
|
|
|
+ // slave 强制关闭本机HDMI显示(省CPU, 供主节点拉取)
|
|
|
+ if (node_role == "slave") {
|
|
|
+ grid_output = false;
|
|
|
+ steel_output = false;
|
|
|
+ enable_bucket = false;
|
|
|
+ }
|
|
|
+ cout << "[Node] 角色=" << node_role << " 本机通道起点=" << local_base
|
|
|
+ << " 对端=" << (peer_active ? (peer_host + ":" + to_string(peer_port)) : "无")
|
|
|
+ << (peer_active ? (" 对端起点=" + to_string(peer_base)) : "") << endl;
|
|
|
cout << "[Display] 网格输出: " << (grid_output ? "启用" : "禁用")
|
|
|
<< " 钢包位输出: " << (steel_output ? "启用" : "禁用")
|
|
|
<< " 工位功能: " << (enable_bucket ? "启用" : "禁用")
|
|
|
@@ -648,7 +700,8 @@ int main(int argc, char* argv[]) {
|
|
|
}
|
|
|
|
|
|
// ========== 通道容器 ==========
|
|
|
- static const int kMaxChannels = 8;
|
|
|
+ static const int kMaxChannels = 16; // 全局通道上限(本地8+对端8)
|
|
|
+ static const int kMaxLocalChannels = 8;
|
|
|
mutex channels_mtx;
|
|
|
vector<shared_ptr<ChannelState>> channels;
|
|
|
|
|
|
@@ -661,6 +714,9 @@ int main(int argc, char* argv[]) {
|
|
|
WebServer web;
|
|
|
DisplayOutput displayGrid; // 普通通道网格(自动布局)
|
|
|
DisplayOutput displayBucket; // 钢包位(当前工位左右摄像头)
|
|
|
+ shared_ptr<RemoteClient> remote; // 对端访问客户端(主节点)
|
|
|
+ vector<shared_ptr<PeerChan>> peer_chans; // 对端通道缓存
|
|
|
+ vector<shared_ptr<RemoteClient>> peer_clients; // 每路独立HTTP客户端
|
|
|
web.setPort(web_port);
|
|
|
|
|
|
// 登录凭据(默认admin/admin), 持久化于config.json的web_auth
|
|
|
@@ -730,19 +786,20 @@ int main(int argc, char* argv[]) {
|
|
|
return ch;
|
|
|
};
|
|
|
auto registerChannelProviders = [&](shared_ptr<ChannelState> ch) {
|
|
|
- web.setFrameProvider(ch->id, [ch]() -> cv::Mat {
|
|
|
+ int gid = local_base + ch->id;
|
|
|
+ web.setFrameProvider(gid, [ch]() -> cv::Mat {
|
|
|
lock_guard<mutex> lk(ch->mtx);
|
|
|
if (!ch->display_frame.empty())
|
|
|
return ch->display_frame.clone();
|
|
|
return cv::Mat(360, 640, CV_8UC3, cv::Scalar(0, 0, 0));
|
|
|
});
|
|
|
- web.setFrameSeqProvider(ch->id, [ch]() -> uint64_t {
|
|
|
+ web.setFrameSeqProvider(gid, [ch]() -> uint64_t {
|
|
|
return ch->disp_seq.load(std::memory_order_relaxed);
|
|
|
});
|
|
|
};
|
|
|
|
|
|
json rtsp_list = cfg.value("rtsp", json::array());
|
|
|
- for (int i = 0; i < (int)rtsp_list.size() && i < kMaxChannels; i++) {
|
|
|
+ for (int i = 0; i < (int)rtsp_list.size() && i < kMaxLocalChannels; i++) {
|
|
|
auto ch = makeChannel(rtsp_list[i].value("id", i),
|
|
|
rtsp_list[i].value("name", ""),
|
|
|
rtsp_list[i].value("url", ""));
|
|
|
@@ -751,8 +808,8 @@ int main(int argc, char* argv[]) {
|
|
|
lock_guard<mutex> lk(channels_mtx);
|
|
|
channels.push_back(std::move(ch));
|
|
|
}
|
|
|
- cout << "[Main] 视频通道: " << channels.size() << "/"
|
|
|
- << kMaxChannels << " 路" << endl;
|
|
|
+ cout << "[Main] 本地视频通道: " << channels.size() << "/"
|
|
|
+ << kMaxLocalChannels << " 路" << endl;
|
|
|
|
|
|
// 钢包位专用通道(占位创建, 工位激活时替换为工位左右摄像头)
|
|
|
if (enable_bucket) {
|
|
|
@@ -827,11 +884,13 @@ int main(int argc, char* argv[]) {
|
|
|
s["fall_relays"] = fall_relays;
|
|
|
s["grid_output"] = grid_output;
|
|
|
s["steel_output"] = steel_output;
|
|
|
+ s["node_role"] = node_role;
|
|
|
+ s["local_base"] = local_base;
|
|
|
json arr = json::array();
|
|
|
{
|
|
|
lock_guard<mutex> lk(channels_mtx);
|
|
|
for (auto& ch : channels) {
|
|
|
- json c; c["id"] = ch->id;
|
|
|
+ json c; c["id"] = local_base + ch->id;
|
|
|
c["name"] = ch->capture->name();
|
|
|
c["url"] = ch->capture->url();
|
|
|
c["connected"] = ch->capture->isRunning();
|
|
|
@@ -843,9 +902,45 @@ int main(int argc, char* argv[]) {
|
|
|
c["pose_running"] = ch->pose_running;
|
|
|
c["fall_count"] = ch->pose_fall_count;
|
|
|
c["falls"] = (int)ch->fall_poses.size();
|
|
|
+ c["remote"] = false;
|
|
|
arr.push_back(c);
|
|
|
}
|
|
|
}
|
|
|
+ // 合入对端通道
|
|
|
+ if (peer_active) {
|
|
|
+ json pj;
|
|
|
+ bool ok = remote && remote->getJson("/api/status", pj);
|
|
|
+ for (int i = 0; i < peer_count; i++) {
|
|
|
+ json c;
|
|
|
+ c["id"] = peer_base + i;
|
|
|
+ c["name"] = "远端" + to_string(i + 1);
|
|
|
+ c["url"] = "";
|
|
|
+ c["remote"] = true;
|
|
|
+ auto pc = peer_chans[i];
|
|
|
+ int64_t lm = pc ? pc->last_ms.load(std::memory_order_relaxed) : 0;
|
|
|
+ int age = lm ? (int)(nowMs() - lm) : -1;
|
|
|
+ c["online"] = (age >= 0 && age < 5000);
|
|
|
+ c["connected"] = c["online"];
|
|
|
+ c["frame_age_ms"] = age;
|
|
|
+ c["width"] = 640; c["height"] = 360;
|
|
|
+ if (ok && pj.contains("channels") && pj["channels"].is_array()) {
|
|
|
+ for (auto& rc2 : pj["channels"]) {
|
|
|
+ if (rc2.value("id", -1) == peer_base + i) {
|
|
|
+ c["name"] = rc2.value("name", c["name"]);
|
|
|
+ c["width"] = rc2.value("width", 640);
|
|
|
+ c["height"] = rc2.value("height", 360);
|
|
|
+ c["pose_running"] = rc2.value("pose_running", false);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ arr.push_back(c);
|
|
|
+ }
|
|
|
+ // 按全局id排序, 保证显示顺序稳定
|
|
|
+ std::sort(arr.begin(), arr.end(), [](const json& a, const json& b) {
|
|
|
+ return a.value("id", 0) < b.value("id", 0);
|
|
|
+ });
|
|
|
+ }
|
|
|
s["channels"] = arr;
|
|
|
return s.dump();
|
|
|
});
|
|
|
@@ -1335,7 +1430,11 @@ int main(int argc, char* argv[]) {
|
|
|
json j = json::parse(body);
|
|
|
string name = j.value("name", "");
|
|
|
string url = j.value("url", "");
|
|
|
- int id = j.value("id", -1);
|
|
|
+ int gid = j.value("id", -1);
|
|
|
+ // 前端用全局id, 转成本地id
|
|
|
+ int id = (gid < 0) ? -1 : (gid - local_base);
|
|
|
+ if (gid >= 0 && (id < 0 || id >= kMaxLocalChannels))
|
|
|
+ return R"({"ok":false,"error":"非本机通道"})";
|
|
|
if (url.rfind("rtsp://", 0) != 0)
|
|
|
return R"({"ok":false,"error":"URL必须以rtsp://开头"})";
|
|
|
if (name.empty()) name = "通道";
|
|
|
@@ -1350,8 +1449,8 @@ int main(int argc, char* argv[]) {
|
|
|
if (!old)
|
|
|
return R"({"ok":false,"error":"通道不存在"})";
|
|
|
} else {
|
|
|
- if ((int)channels.size() >= kMaxChannels)
|
|
|
- return R"({"ok":false,"error":"已达最大通道数8"})";
|
|
|
+ if ((int)channels.size() >= kMaxLocalChannels)
|
|
|
+ return R"({"ok":false,"error":"已达最大本地通道数8"})";
|
|
|
int max_id = -1;
|
|
|
for (auto& ch : channels) max_id = std::max(max_id, ch->id);
|
|
|
nu->id = id = max_id + 1;
|
|
|
@@ -1369,7 +1468,7 @@ int main(int argc, char* argv[]) {
|
|
|
persistRtspList();
|
|
|
cout << "[Channel] " << (old ? "修改" : "添加")
|
|
|
<< " CH" << id << ": " << url << endl;
|
|
|
- json r; r["ok"] = true; r["id"] = id;
|
|
|
+ json r; r["ok"] = true; r["id"] = local_base + id;
|
|
|
return r.dump();
|
|
|
} catch (exception& e) {
|
|
|
return string(R"({"ok":false,"error":")") + e.what() + R"("})";
|
|
|
@@ -1379,7 +1478,10 @@ int main(int argc, char* argv[]) {
|
|
|
web.setPostHandler("/api/channels/delete", [&](const string& body) -> string {
|
|
|
try {
|
|
|
json j = json::parse(body);
|
|
|
- int id = j.value("id", -1);
|
|
|
+ int gid = j.value("id", -1);
|
|
|
+ int id = (gid < 0) ? -1 : (gid - local_base);
|
|
|
+ if (gid >= 0 && (id < 0 || id >= kMaxLocalChannels))
|
|
|
+ return R"({"ok":false,"error":"非本机通道"})";
|
|
|
shared_ptr<ChannelState> victim;
|
|
|
{
|
|
|
lock_guard<mutex> lk(channels_mtx);
|
|
|
@@ -1393,7 +1495,7 @@ int main(int argc, char* argv[]) {
|
|
|
}
|
|
|
if (!victim)
|
|
|
return R"({"ok":false,"error":"通道不存在"})";
|
|
|
- web.removeFrameProvider(id);
|
|
|
+ web.removeFrameProvider(local_base + id);
|
|
|
stopChannelInfer(victim); // 先停推理线程(避免访问已释放的ChannelState)
|
|
|
victim->capture->stop(); // 锁外停止并回收解码线程
|
|
|
persistRtspList();
|
|
|
@@ -1551,6 +1653,92 @@ int main(int argc, char* argv[]) {
|
|
|
} catch (...) { return R"({"ok":false})"; }
|
|
|
});
|
|
|
|
|
|
+ // ========== 双机角色配置(网页配置, 重启生效) ==========
|
|
|
+ web.setGetHandler("/api/node", [&]() -> string {
|
|
|
+ json r;
|
|
|
+ r["role"] = node_role;
|
|
|
+ r["local_base"] = local_base;
|
|
|
+ r["peer"] = {
|
|
|
+ {"enabled", peer_enabled},
|
|
|
+ {"host", peer_host},
|
|
|
+ {"port", peer_port},
|
|
|
+ {"username", peer_user},
|
|
|
+ {"password", peer_pass},
|
|
|
+ {"local_base", peer_base},
|
|
|
+ {"count", peer_count},
|
|
|
+ {"fps", remote_fps}
|
|
|
+ };
|
|
|
+ return r.dump();
|
|
|
+ });
|
|
|
+
|
|
|
+ web.setPostHandler("/api/node", [&](const string& body) -> string {
|
|
|
+ try {
|
|
|
+ json j = json::parse(body);
|
|
|
+ json full = loadJson(config_path);
|
|
|
+ if (j.contains("role") && j["role"].is_string()) {
|
|
|
+ string r = j["role"].get<string>();
|
|
|
+ if (r == "master" || r == "slave" || r == "standalone")
|
|
|
+ full["node_role"] = r;
|
|
|
+ }
|
|
|
+ if (j.contains("local_base")) {
|
|
|
+ int b = j["local_base"].get<int>();
|
|
|
+ if (b == 0 || b == 8) full["local_base"] = b;
|
|
|
+ }
|
|
|
+ if (j.contains("peer") && j["peer"].is_object()) {
|
|
|
+ if (!full.contains("peer") || !full["peer"].is_object())
|
|
|
+ full["peer"] = json::object();
|
|
|
+ auto& p = j["peer"];
|
|
|
+ for (auto& k : {"enabled","host","port","username","password",
|
|
|
+ "local_base","count","fps"})
|
|
|
+ if (p.contains(k)) full["peer"][k] = p[k];
|
|
|
+ }
|
|
|
+ saveJson(config_path, full);
|
|
|
+ cout << "[Node] 角色配置已保存(重启生效)" << endl;
|
|
|
+ return R"({"ok":true,"restart_required":true})";
|
|
|
+ } catch (exception& e) {
|
|
|
+ return string(R"({"ok":false,"error":")") + e.what() + R"("})";
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 对端报警截图代理
|
|
|
+ web.setGetHandlerQ("/api/peer/image", [&](const map<string, string>& q) -> string {
|
|
|
+ auto it = q.find("file");
|
|
|
+ if (it == q.end() || it->second.empty()) return R"({"ok":false})";
|
|
|
+ // 仅允许文件名, 防目录穿越
|
|
|
+ string f = it->second;
|
|
|
+ if (f.find('/') != string::npos || f.find("..") != string::npos)
|
|
|
+ return R"({"ok":false})";
|
|
|
+ if (!remote) return R"({"ok":false})";
|
|
|
+ string raw, ctype;
|
|
|
+ if (!remote->getRaw("/data/screenshots/" + f, raw, ctype))
|
|
|
+ return R"({"ok":false})";
|
|
|
+ // 以base64返回, 避免与JSON混用
|
|
|
+ static const char* b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
|
+ string out;
|
|
|
+ size_t i = 0;
|
|
|
+ while (i + 2 < raw.size()) {
|
|
|
+ unsigned v = (unsigned char)raw[i] << 16 |
|
|
|
+ (unsigned char)raw[i+1] << 8 |
|
|
|
+ (unsigned char)raw[i+2];
|
|
|
+ out += b64[(v >> 18) & 63]; out += b64[(v >> 12) & 63];
|
|
|
+ out += b64[(v >> 6) & 63]; out += b64[v & 63];
|
|
|
+ i += 3;
|
|
|
+ }
|
|
|
+ if (i < raw.size()) {
|
|
|
+ unsigned v = (unsigned char)raw[i] << 16;
|
|
|
+ bool two = (i + 1 < raw.size());
|
|
|
+ if (two) v |= (unsigned char)raw[i+1] << 8;
|
|
|
+ out += b64[(v >> 18) & 63]; out += b64[(v >> 12) & 63];
|
|
|
+ out += two ? b64[(v >> 6) & 63] : '=';
|
|
|
+ out += '=';
|
|
|
+ }
|
|
|
+ json r;
|
|
|
+ r["ok"] = true;
|
|
|
+ r["ctype"] = ctype;
|
|
|
+ r["b64"] = out;
|
|
|
+ return r.dump();
|
|
|
+ });
|
|
|
+
|
|
|
web.setGetHandler("/api/system", [&]() -> string {
|
|
|
json s;
|
|
|
s["reboot_enabled"] = reboot_enabled;
|
|
|
@@ -1574,6 +1762,34 @@ int main(int argc, char* argv[]) {
|
|
|
return R"({"ok":true})";
|
|
|
});
|
|
|
|
|
|
+ // URL编码(报警时间查询参数含空格)
|
|
|
+ auto urlenc = [](const string& v) -> string {
|
|
|
+ static const char* hex = "0123456789ABCDEF";
|
|
|
+ string o;
|
|
|
+ for (unsigned char c : v) {
|
|
|
+ if (isalnum(c) || c=='-' || c=='_' || c=='.' || c=='~' || c==':')
|
|
|
+ o += (char)c;
|
|
|
+ else { o += '%'; o += hex[c >> 4]; o += hex[c & 15]; }
|
|
|
+ }
|
|
|
+ return o;
|
|
|
+ };
|
|
|
+ // 拼接报警查询串(不含channel, 由调用方决定)
|
|
|
+ auto buildAlarmQs = [&](const map<string,string>& q, int lch) -> string {
|
|
|
+ auto gets = [&](const string& k) -> string {
|
|
|
+ auto it = q.find(k); return it == q.end() ? string() : it->second;
|
|
|
+ };
|
|
|
+ string qs;
|
|
|
+ auto add = [&](const string& k, const string& v) {
|
|
|
+ if (v.empty()) return;
|
|
|
+ qs += qs.empty() ? "?" : "&";
|
|
|
+ qs += k + "=" + urlenc(v);
|
|
|
+ };
|
|
|
+ add("channel", lch >= 0 ? to_string(lch) : string());
|
|
|
+ add("start", gets("start"));
|
|
|
+ add("end", gets("end"));
|
|
|
+ return qs;
|
|
|
+ };
|
|
|
+
|
|
|
web.setGetHandlerQ("/api/alarms", [&](const map<string, string>& q) -> string {
|
|
|
auto geti = [&](const string& k, int def) -> int {
|
|
|
auto it = q.find(k);
|
|
|
@@ -1589,21 +1805,49 @@ int main(int argc, char* argv[]) {
|
|
|
string start = gets("start");
|
|
|
string end = gets("end");
|
|
|
|
|
|
+ bool wantLocal = (ch < 0) || (ch >= local_base && ch < local_base + 8);
|
|
|
+ bool wantPeer = peer_active && ((ch < 0) || (ch >= peer_base && ch < peer_base + 8));
|
|
|
+
|
|
|
json arr = json::array();
|
|
|
- auto alarms = alarmHistory.getAlarms(limit, 0, ch, start, end);
|
|
|
- for (auto& a : alarms) {
|
|
|
- json aj;
|
|
|
- aj["id"] = a.id;
|
|
|
- aj["timestamp"] = a.timestamp;
|
|
|
- aj["channel"] = a.channel;
|
|
|
- aj["fence_name"] = a.fence_name;
|
|
|
- aj["class_name"] = a.class_name;
|
|
|
- aj["confidence"] = a.confidence;
|
|
|
- aj["screenshot"] = a.screenshot_path;
|
|
|
- arr.push_back(aj);
|
|
|
+ int total = 0;
|
|
|
+ if (wantLocal) {
|
|
|
+ int lch = (ch < 0) ? -1 : (ch - local_base);
|
|
|
+ auto alarms = alarmHistory.getAlarms(limit, 0, lch, start, end);
|
|
|
+ for (auto& a : alarms) {
|
|
|
+ json aj;
|
|
|
+ aj["id"] = a.id;
|
|
|
+ aj["timestamp"] = a.timestamp;
|
|
|
+ aj["channel"] = a.channel + local_base;
|
|
|
+ aj["fence_name"] = a.fence_name;
|
|
|
+ aj["class_name"] = a.class_name;
|
|
|
+ aj["confidence"] = a.confidence;
|
|
|
+ aj["screenshot"] = a.screenshot_path;
|
|
|
+ aj["src"] = "local";
|
|
|
+ arr.push_back(aj);
|
|
|
+ }
|
|
|
+ total += alarmHistory.getAlarmCount(lch, start, end);
|
|
|
+ }
|
|
|
+ if (wantPeer && remote) {
|
|
|
+ json pj;
|
|
|
+ string path = "/api/alarms" + buildAlarmQs(q, ch);
|
|
|
+ if (remote->getJson(path, pj)) {
|
|
|
+ total += pj.value("total", 0);
|
|
|
+ for (auto& a : pj["items"]) {
|
|
|
+ a["src"] = "peer";
|
|
|
+ arr.push_back(a);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ std::sort(arr.begin(), arr.end(), [](const json& a, const json& b) {
|
|
|
+ return a.value("timestamp", string()) > b.value("timestamp", string());
|
|
|
+ });
|
|
|
+ if ((int)arr.size() > limit) {
|
|
|
+ json cut = json::array();
|
|
|
+ for (int i = 0; i < limit; i++) cut.push_back(arr[i]);
|
|
|
+ arr = cut;
|
|
|
}
|
|
|
json r;
|
|
|
- r["total"] = alarmHistory.getAlarmCount(ch, start, end);
|
|
|
+ r["total"] = total;
|
|
|
r["items"] = arr;
|
|
|
return r.dump();
|
|
|
});
|
|
|
@@ -1612,6 +1856,13 @@ int main(int argc, char* argv[]) {
|
|
|
try {
|
|
|
json j = json::parse(body);
|
|
|
int64_t id = j.value("id", 0);
|
|
|
+ string src = j.value("src", "local");
|
|
|
+ if (src == "peer") {
|
|
|
+ json fwd; fwd["id"] = id;
|
|
|
+ if (remote && remote->postJson("/api/alarms/delete", fwd.dump()))
|
|
|
+ return R"({"ok":true})";
|
|
|
+ return R"({"ok":false})";
|
|
|
+ }
|
|
|
if (alarmHistory.deleteAlarm(id)) return R"({"ok":true})";
|
|
|
} catch (...) {}
|
|
|
return R"({"ok":false})";
|
|
|
@@ -1619,12 +1870,18 @@ int main(int argc, char* argv[]) {
|
|
|
|
|
|
web.setPostHandler("/api/alarms/clear", [&](const string& body) -> string {
|
|
|
alarmHistory.clearAll();
|
|
|
+ if (remote) remote->postJson("/api/alarms/clear", "{}");
|
|
|
return R"({"ok":true})";
|
|
|
});
|
|
|
|
|
|
web.setGetHandler("/api/alarms/count", [&]() -> string {
|
|
|
json r;
|
|
|
- r["count"] = alarmHistory.getAlarmCount();
|
|
|
+ int n = alarmHistory.getAlarmCount();
|
|
|
+ if (peer_active && remote) {
|
|
|
+ json pj;
|
|
|
+ if (remote->getJson("/api/alarms/count", pj)) n += pj.value("count", 0);
|
|
|
+ }
|
|
|
+ r["count"] = n;
|
|
|
return r.dump();
|
|
|
});
|
|
|
|
|
|
@@ -1645,20 +1902,49 @@ int main(int argc, char* argv[]) {
|
|
|
string start = gets("start");
|
|
|
string end = gets("end");
|
|
|
|
|
|
+ bool wantLocal = (ch < 0) || (ch >= local_base && ch < local_base + 8);
|
|
|
+ bool wantPeer = peer_active && ((ch < 0) || (ch >= peer_base && ch < peer_base + 8));
|
|
|
+
|
|
|
json arr = json::array();
|
|
|
- auto list = alarmHistory.getPoseAlarms(limit, 0, ch, action, start, end);
|
|
|
- for (auto& a : list) {
|
|
|
- json aj;
|
|
|
- aj["id"] = a.id;
|
|
|
- aj["timestamp"] = a.timestamp;
|
|
|
- aj["channel"] = a.channel;
|
|
|
- aj["action"] = a.action;
|
|
|
- aj["score"] = a.score;
|
|
|
- aj["screenshot"] = a.screenshot_path;
|
|
|
- arr.push_back(aj);
|
|
|
+ int total = 0;
|
|
|
+ if (wantLocal) {
|
|
|
+ int lch = (ch < 0) ? -1 : (ch - local_base);
|
|
|
+ auto list = alarmHistory.getPoseAlarms(limit, 0, lch, action, start, end);
|
|
|
+ for (auto& a : list) {
|
|
|
+ json aj;
|
|
|
+ aj["id"] = a.id;
|
|
|
+ aj["timestamp"] = a.timestamp;
|
|
|
+ aj["channel"] = a.channel + local_base;
|
|
|
+ aj["action"] = a.action;
|
|
|
+ aj["score"] = a.score;
|
|
|
+ aj["screenshot"] = a.screenshot_path;
|
|
|
+ aj["src"] = "local";
|
|
|
+ arr.push_back(aj);
|
|
|
+ }
|
|
|
+ total += alarmHistory.getPoseAlarmCount(lch, action, start, end);
|
|
|
+ }
|
|
|
+ if (wantPeer && remote) {
|
|
|
+ json pj;
|
|
|
+ string qs = buildAlarmQs(q, ch);
|
|
|
+ if (!action.empty()) qs += (qs.empty() ? "?" : "&") + string("action=") + urlenc(action);
|
|
|
+ if (remote->getJson("/api/pose/alarms" + qs, pj)) {
|
|
|
+ total += pj.value("total", 0);
|
|
|
+ for (auto& a : pj["items"]) {
|
|
|
+ a["src"] = "peer";
|
|
|
+ arr.push_back(a);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ std::sort(arr.begin(), arr.end(), [](const json& a, const json& b) {
|
|
|
+ return a.value("timestamp", string()) > b.value("timestamp", string());
|
|
|
+ });
|
|
|
+ if ((int)arr.size() > limit) {
|
|
|
+ json cut = json::array();
|
|
|
+ for (int i = 0; i < limit; i++) cut.push_back(arr[i]);
|
|
|
+ arr = cut;
|
|
|
}
|
|
|
json r;
|
|
|
- r["total"] = alarmHistory.getPoseAlarmCount(ch, action, start, end);
|
|
|
+ r["total"] = total;
|
|
|
r["items"] = arr;
|
|
|
return r.dump();
|
|
|
});
|
|
|
@@ -1667,6 +1953,13 @@ int main(int argc, char* argv[]) {
|
|
|
try {
|
|
|
json j = json::parse(body);
|
|
|
int64_t id = j.value("id", 0);
|
|
|
+ string src = j.value("src", "local");
|
|
|
+ if (src == "peer") {
|
|
|
+ json fwd; fwd["id"] = id;
|
|
|
+ if (remote && remote->postJson("/api/pose/alarms/delete", fwd.dump()))
|
|
|
+ return R"({"ok":true})";
|
|
|
+ return R"({"ok":false})";
|
|
|
+ }
|
|
|
if (alarmHistory.deletePoseAlarm(id)) return R"({"ok":true})";
|
|
|
} catch (...) {}
|
|
|
return R"({"ok":false})";
|
|
|
@@ -1674,12 +1967,18 @@ int main(int argc, char* argv[]) {
|
|
|
|
|
|
web.setPostHandler("/api/pose/alarms/clear", [&](const string&) -> string {
|
|
|
alarmHistory.clearPoseAlarms();
|
|
|
+ if (remote) remote->postJson("/api/pose/alarms/clear", "{}");
|
|
|
return R"({"ok":true})";
|
|
|
});
|
|
|
|
|
|
web.setGetHandler("/api/pose/alarms/count", [&]() -> string {
|
|
|
json r;
|
|
|
- r["count"] = alarmHistory.getPoseAlarmCount();
|
|
|
+ int n = alarmHistory.getPoseAlarmCount();
|
|
|
+ if (peer_active && remote) {
|
|
|
+ json pj;
|
|
|
+ if (remote->getJson("/api/pose/alarms/count", pj)) n += pj.value("count", 0);
|
|
|
+ }
|
|
|
+ r["count"] = n;
|
|
|
return r.dump();
|
|
|
});
|
|
|
|
|
|
@@ -1883,12 +2182,74 @@ int main(int argc, char* argv[]) {
|
|
|
});
|
|
|
|
|
|
web.start();
|
|
|
- // 网格布局固定为已配置的rtsp通道数, 避免启动/断帧时布局跳动
|
|
|
- int grid_fixed = (int)channels.size();
|
|
|
+ // 网格布局固定为本地+对端通道数, 避免启动/断帧时布局跳动
|
|
|
+ int grid_fixed = (int)channels.size() + (peer_active ? peer_count : 0);
|
|
|
displayGrid.start(grid_output, hdmi_grid_cols, app_name + " - 通道网格",
|
|
|
hdmi_fullscreen, 100, grid_fixed);
|
|
|
displayBucket.start(steel_output && enable_bucket, 2, app_name + " - 钢包位",
|
|
|
hdmi_fullscreen, 100, 2);
|
|
|
+
|
|
|
+ // ========== 对端节点(双机16路): 拉取画面 + 供本地显示/推流 ==========
|
|
|
+ if (peer_active) {
|
|
|
+ remote = make_shared<RemoteClient>();
|
|
|
+ remote->configure(peer_host, peer_port, peer_user, peer_pass);
|
|
|
+ // 对端报警截图代理(主节点直接返回图片字节)
|
|
|
+ web.setPeerImageProvider([remote](const string& file,
|
|
|
+ string& bytes, string& ctype) {
|
|
|
+ if (!remote) return false;
|
|
|
+ return remote->getRaw("/data/screenshots/" + file, bytes, ctype);
|
|
|
+ });
|
|
|
+ peer_chans.resize(peer_count);
|
|
|
+ peer_clients.resize(peer_count);
|
|
|
+ for (int i = 0; i < peer_count; i++) {
|
|
|
+ auto pc = make_shared<PeerChan>();
|
|
|
+ auto pcli = make_shared<RemoteClient>();
|
|
|
+ pcli->configure(peer_host, peer_port, peer_user, peer_pass);
|
|
|
+ peer_clients[i] = pcli;
|
|
|
+ peer_chans[i] = pc;
|
|
|
+ int gid = peer_base + i;
|
|
|
+ // 注册对端通道的帧提供器(供 web/mjpeg/手机 访问)
|
|
|
+ web.setFrameProvider(gid, [pc]() -> cv::Mat {
|
|
|
+ lock_guard<mutex> lk(pc->m);
|
|
|
+ if (!pc->frame.empty()) return pc->frame.clone();
|
|
|
+ return cv::Mat(360, 640, CV_8UC3, cv::Scalar(20, 20, 20));
|
|
|
+ });
|
|
|
+ web.setFrameSeqProvider(gid, [pc]() -> uint64_t {
|
|
|
+ return pc->seq.load(std::memory_order_relaxed);
|
|
|
+ });
|
|
|
+ // 每路一个拉取线程
|
|
|
+ std::thread([&displayGrid, pc, pcli, i, gid, remote_fps, peer_base]() {
|
|
|
+ const int interval = 1000 / max(1, remote_fps);
|
|
|
+ int fail = 0;
|
|
|
+ while (g_running.load()) {
|
|
|
+ cv::Mat img;
|
|
|
+ bool ok = pcli->getJpeg("/stream/" + to_string(peer_base + i), img) && !img.empty();
|
|
|
+ if (ok) {
|
|
|
+ {
|
|
|
+ lock_guard<mutex> lk(pc->m);
|
|
|
+ pc->frame = img;
|
|
|
+ }
|
|
|
+ pc->seq.fetch_add(1, std::memory_order_relaxed);
|
|
|
+ pc->last_ms.store(nowMs(), std::memory_order_relaxed);
|
|
|
+ if (displayGrid.isRunning()) {
|
|
|
+ char lbl[64];
|
|
|
+ snprintf(lbl, sizeof(lbl), "CH%d %s", gid, "远端");
|
|
|
+ displayGrid.setFrame(gid, img, lbl);
|
|
|
+ }
|
|
|
+ fail = 0;
|
|
|
+ this_thread::sleep_for(chrono::milliseconds(interval));
|
|
|
+ } else {
|
|
|
+ fail++;
|
|
|
+ this_thread::sleep_for(chrono::milliseconds(
|
|
|
+ min(3000, 200 * fail)));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }).detach();
|
|
|
+ }
|
|
|
+ cout << "[Peer] 已启动对端画面拉取: " << peer_count << "路 from "
|
|
|
+ << peer_host << ":" << peer_port << endl;
|
|
|
+ }
|
|
|
+
|
|
|
cout << "[Main] Ctrl+C 退出" << endl;
|
|
|
|
|
|
// ========== 看门狗: 主循环卡死(如rknn_run挂起)时自动退出, 由systemd重启 ==========
|
|
|
@@ -2533,10 +2894,11 @@ int main(int argc, char* argv[]) {
|
|
|
}
|
|
|
// 输出到本机显示器(HDMI)
|
|
|
if (displayGrid.isRunning() && ch->id < 100) {
|
|
|
+ int gid = local_base + ch->id;
|
|
|
char dlbl[64];
|
|
|
snprintf(dlbl, sizeof(dlbl), "CH%d %s",
|
|
|
- ch->id, ch->capture->name().c_str());
|
|
|
- displayGrid.setFrame(ch->id, display, dlbl);
|
|
|
+ gid, ch->capture->name().c_str());
|
|
|
+ displayGrid.setFrame(gid, display, dlbl);
|
|
|
}
|
|
|
if (displayBucket.isRunning() && ch->id >= 100) {
|
|
|
char dlbl[64];
|