/** * @file Hub.cpp */ #include "Hub.h" #include "Theme.h" #include #include using namespace StreamHubClient; namespace shq { Hub::Hub(QObject* parent) : QObject(parent) { connect(&ws_, &WsClient::connectedChanged, this, &Hub::onConnected); connect(&ws_, &WsClient::textReceived, this, &Hub::onText); connect(&ws_, &WsClient::binaryReceived, this, &Hub::onBinary); } void Hub::start(const QString& host, uint16_t port) { ws_.connectTo(host, port); } void Hub::reconnect(const QString& host, uint16_t port) { ws_.reconnectTo(host, port); } /* ── connection ──────────────────────────────────────────────────────────── */ void Hub::onConnected(bool connected) { if (connected) { sendGetSources(); sendGetStats(); sendHistoryInfo(); } Q_EMIT connectedChanged(connected); } /* ── dispatch ────────────────────────────────────────────────────────────── */ void Hub::onText(const QString& jsonQ) { const std::string json = jsonQ.toStdString(); const std::string type = ParseType(json); if (type == "sources") { onSources(json); } else if (type == "config") { onConfig(json); } else if (type == "stats") { onStats(json); } else if (type == "triggerState") { onTriggerState(json); } else if (type == "zoom") { onZoom(json); } else if (type == "historyZoom") { onHistoryZoom(json); } else if (type == "historyInfo") { onHistoryInfo(json); } else if (type == "maxPointsUpdated") { onMaxPointsUpdated(json); } /* pong: ignore */ } void Hub::onBinary(const QByteArray& bytes) { if (bytes.isEmpty()) { return; } const uint8_t* data = reinterpret_cast(bytes.constData()); const size_t len = static_cast(bytes.size()); if (data[0] == 2u) { /* trigger capture frame */ CaptureFrame cf; if (ParseCaptureFrame(data, len, cf)) { capture_ = std::move(cf); hasCapture_ = true; trigger_.status = "triggered"; trigger_.trigTime = capture_.trigTime; trigger_.hasTrigTime = true; Q_EMIT captureReceived(); } return; } DataFrame frame; /* v1 live push */ if (!ParseBinaryFrame(data, len, frame)) { return; } int srcIdx = findSource(frame.sourceId); if (srcIdx < 0) { return; } Source& src = sources_[srcIdx]; for (const auto& fs : frame.signals) { int sigIdx = findSignal(src, fs.name); if (sigIdx < 0) { continue; } size_t n = std::min(fs.t.size(), fs.v.size()); auto& buf = src.signals[sigIdx].buf; for (size_t i = 0; i < n; i++) { buf.push(fs.t[i], fs.v[i]); } } } /* ── JSON handlers (port of App.cpp) ─────────────────────────────────────── */ void Hub::onSources(const std::string& json) { std::vector infos; ParseSources(json, infos); for (const auto& info : infos) { int idx = findSource(info.id); if (idx < 0) { Source s; s.id = info.id; s.label = info.label; s.addr = info.addr; s.port = info.port; s.state = info.state; sources_.push_back(std::move(s)); } else { sources_[idx].state = info.state; sources_[idx].label = info.label; } } sources_.erase(std::remove_if(sources_.begin(), sources_.end(), [&infos](const Source& s) { for (const auto& i : infos) { if (i.id == s.id) { return false; } } return true; }), sources_.end()); /* Request config for any source not yet configured. */ for (auto& s : sources_) { if (!s.configured) { sendGetConfig(s.id); } } Q_EMIT sourcesChanged(); } void Hub::onConfig(const std::string& json) { std::string sourceId; int publishMode = 0; std::vector metas; if (!ParseConfig(json, sourceId, publishMode, metas)) { return; } int idx = findSource(sourceId); if (idx < 0) { return; } Source& src = sources_[idx]; src.configured = true; src.publishMode = publishMode; for (size_t m = 0; m < metas.size(); m++) { int si = findSignal(src, metas[m].name); if (si < 0) { SignalView sig; sig.meta = metas[m]; sig.color = tracePalette(static_cast(src.signals.size())); src.signals.push_back(std::move(sig)); } else { src.signals[si].meta = metas[m]; } } Q_EMIT configChanged(QString::fromStdString(sourceId)); } void Hub::onStats(const std::string& json) { std::vector> statsVec; ParseStats(json, statsVec); for (const auto& kv : statsVec) { int idx = findSource(kv.first); if (idx >= 0) { sources_[idx].stats = kv.second; sources_[idx].state = kv.second.state; } } Q_EMIT statsChanged(); } void Hub::onTriggerState(const std::string& json) { TriggerStateMsg msg; if (!ParseTriggerState(json, msg)) { return; } trigger_.status = msg.state; trigger_.stopped = msg.stopped; if (msg.hasTrigTime) { trigger_.trigTime = msg.trigTime; trigger_.hasTrigTime = true; } if (msg.state == "idle") { trigger_.hasTrigTime = false; } Q_EMIT triggerStateChanged(); } static void routeZoom(PlotZoomCache* caches, const ZoomResponse& resp, int& outPlot) { outPlot = -1; for (int i = 0; i < kMaxPlotSlots; i++) { auto& zc = caches[i]; if (zc.pending && zc.reqId == resp.reqId) { double dataT0 = 1e300, dataT1 = -1e300; bool anyData = false; for (const auto& zs : resp.signals) { for (double tv : zs.t) { if (tv < dataT0) { dataT0 = tv; } if (tv > dataT1) { dataT1 = tv; } anyData = true; } } if (anyData) { zc.pts = resp.signals; zc.t0 = dataT0; zc.t1 = dataT1; zc.valid = true; } zc.pending = false; outPlot = i; return; } } } void Hub::onZoom(const std::string& json) { ZoomResponse resp; if (!ParseZoom(json, resp)) { return; } int plot = -1; routeZoom(zoomCache_, resp, plot); if (plot >= 0) { Q_EMIT zoomReceived(plot); } } void Hub::onHistoryZoom(const std::string& json) { ZoomResponse resp; if (!ParseZoom(json, resp)) { return; } int plot = -1; routeZoom(histZoomCache_, resp, plot); if (plot >= 0) { Q_EMIT historyZoomReceived(plot); } } void Hub::onHistoryInfo(const std::string& json) { ParseHistoryInfo(json, historyInfo_); Q_EMIT historyInfoChanged(); } void Hub::onMaxPointsUpdated(const std::string& json) { uint32_t mp = 0; ParseMaxPointsUpdated(json, mp); if (mp >= 2) { maxPoints_ = mp; for (auto& src : sources_) { for (auto& sig : src.signals) { sig.buf.setCapacity(mp); } } Q_EMIT maxPointsChanged(mp); } } /* ── helpers ─────────────────────────────────────────────────────────────── */ int Hub::findSource(const std::string& id) const { for (int i = 0; i < static_cast(sources_.size()); i++) { if (sources_[i].id == id) { return i; } } return -1; } int Hub::findSignal(const Source& src, const std::string& name) const { for (int i = 0; i < static_cast(src.signals.size()); i++) { if (src.signals[i].meta.name == name) { return i; } } return -1; } std::string Hub::slotKey(const PlotAssignment& a) const { if (a.sourceIdx < 0 || a.sourceIdx >= static_cast(sources_.size())) { return std::string(); } const Source& src = sources_[a.sourceIdx]; if (a.signalIdx < 0 || a.signalIdx >= static_cast(src.signals.size())) { return std::string(); } return src.id + ":" + src.signals[a.signalIdx].meta.name; } /* ── commands ────────────────────────────────────────────────────────────── */ void Hub::sendGetSources() { ws_.sendText(BuildGetSources()); } void Hub::sendGetStats() { ws_.sendText(BuildGetStats()); } void Hub::sendGetConfig(const std::string& id) { ws_.sendText(BuildGetConfig(id)); } void Hub::sendHistoryInfo(){ ws_.sendText(BuildHistoryInfo()); } void Hub::sendAddSource(const std::string& label, const std::string& addr, const std::string& mcast, uint16_t dataPort) { ws_.sendText(BuildAddSource(label, addr, mcast, dataPort)); } void Hub::sendRemoveSource(const std::string& id) { ws_.sendText(BuildRemoveSource(id)); } void Hub::sendSaveSources() { ws_.sendText(BuildSaveSources()); } void Hub::sendSetMaxPoints(uint32_t n) { ws_.sendText(BuildSetMaxPoints(n)); } void Hub::sendSetTrigger(const std::string& key, const std::string& edge, double threshold, double windowSec, double prePercent, const std::string& mode) { ws_.sendText(BuildSetTrigger(key, edge, threshold, windowSec, prePercent, mode)); } void Hub::sendArm() { ws_.sendText(BuildArm()); } void Hub::sendDisarm() { ws_.sendText(BuildDisarm()); } void Hub::sendRearm() { ws_.sendText(BuildRearm()); } void Hub::sendTrigStop(bool s) { ws_.sendText(BuildTrigStop(s)); } void Hub::requestZoom(int plotIdx, double t0, double t1, const std::string& csv) { if (plotIdx < 0 || plotIdx >= kMaxPlotSlots) { return; } if (!ws_.isConnected() || csv.empty()) { return; } auto& zc = zoomCache_[plotIdx]; zc.reqId = nextZoomReqId_++; zc.reqT0 = t0; zc.reqT1 = t1; zc.pending = true; ws_.sendText(BuildZoom(zc.reqId, t0, t1, 2400, csv)); } void Hub::requestHistoryZoom(int plotIdx, double t0, double t1, const std::string& csv) { if (plotIdx < 0 || plotIdx >= kMaxPlotSlots) { return; } if (!ws_.isConnected() || csv.empty()) { return; } auto& hc = histZoomCache_[plotIdx]; hc.reqId = nextZoomReqId_++; hc.reqT0 = t0; hc.reqT1 = t1; hc.pending = true; ws_.sendText(BuildHistoryZoom(hc.reqId, t0, t1, 2400, csv)); } } /* namespace shq */