Files
MARTe-Integrated-Components/Client/streamhub/App.cpp
T
2026-06-12 15:25:13 +02:00

709 lines
26 KiB
C++

/**
* @file App.cpp
* @brief Application state machine implementation.
*/
#include "App.h"
#include "SourcePanel.h"
#include "PlotPanel.h"
#include "TriggerPanel.h"
#include "StatsPanel.h"
#include "imgui.h"
#include "implot.h"
#include "Icons.h"
#include <cstring>
#include <cstdio>
#include <cmath>
#include <chrono>
#include <algorithm>
namespace StreamHubClient {
/* ── Layout icon helper ──────────────────────────────────────────────────── */
static void DrawLayoutIcon(ImDrawList* dl, ImVec2 tl, ImVec2 sz,
int cols, int rows, bool selected) {
ImU32 bg = selected ? IM_COL32(49,50,68,255) : IM_COL32(24,24,37,255);
ImU32 cell = selected ? IM_COL32(137,180,250,200) : IM_COL32(88,91,112,160);
ImU32 bord = selected ? IM_COL32(137,180,250,255) : IM_COL32(69,71,90,255);
dl->AddRectFilled(tl, ImVec2(tl.x+sz.x, tl.y+sz.y), bg, 4.f);
dl->AddRect(tl, ImVec2(tl.x+sz.x, tl.y+sz.y), bord, 4.f, 0, 1.f);
float cw = sz.x / cols, rh = sz.y / rows;
for (int c = 0; c < cols; c++) {
for (int r = 0; r < rows; r++) {
ImVec2 p0(tl.x + c*cw + 2.f, tl.y + r*rh + 2.f);
ImVec2 p1(tl.x + (c+1)*cw - 2.f, tl.y + (r+1)*rh - 2.f);
dl->AddRectFilled(p0, p1, cell, 2.f);
}
}
}
/* ── Signal ─────────────────────────────────────────────────────────────── */
/* ── App ─────────────────────────────────────────────────────────────────── */
App::App() {
for (int i = 0; i < kMaxPlotSlots; i++) {
plotPaused_[i] = false;
liveFollow_[i] = true;
activeSlot_[i] = -1;
}
}
App::~App() {
shutdown();
}
void App::init(const std::string& host, uint16_t port) {
snprintf(hostBuf_, sizeof(hostBuf_), "%s", host.c_str());
snprintf(portBuf_, sizeof(portBuf_), "%u", static_cast<unsigned>(port));
ws_.connect(host, port);
/* Initial queries are sent on first connect transition in update() */
}
void App::shutdown() {
ws_.disconnect();
}
/* ── Per-frame update ────────────────────────────────────────────────────── */
void App::update() {
/* Send initial queries on first successful connect */
bool nowConnected = ws_.isConnected();
if (nowConnected && !prevConnected_) {
ws_.sendText(BuildGetSources());
ws_.sendText(BuildGetStats());
}
prevConnected_ = nowConnected;
/* Drain WebSocket receive queue */
ws_.poll([this](const WSMessage& msg) {
if (msg.isBinary) {
handleBinary(msg.data.data(), msg.data.size());
} else {
std::string json(reinterpret_cast<const char*>(msg.data.data()),
msg.data.size());
handleJSON(json);
}
});
/* ── Full-screen dockspace ─────────────────────────────────────────── */
ImGuiViewport* vp = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(vp->WorkPos);
ImGui::SetNextWindowSize(vp->WorkSize);
ImGuiWindowFlags wf = ImGuiWindowFlags_NoTitleBar
| ImGuiWindowFlags_NoCollapse
| ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoBringToFrontOnFocus
| ImGuiWindowFlags_NoNavFocus
| ImGuiWindowFlags_MenuBar;
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f));
ImGui::Begin("##main", nullptr, wf);
ImGui::PopStyleVar(3);
renderMenuBar();
renderToolbar();
if (showTrigBar_) { renderTriggerBar(); }
/* Horizontal split: sidebar | plot grid */
float sidebarW = sidebarOpen_ ? 220.f : 0.f;
float avail = ImGui::GetContentRegionAvail().x;
float plotW = avail - sidebarW;
if (sidebarOpen_) {
ImGui::BeginChild("##sidebar", ImVec2(sidebarW, 0.f), false,
ImGuiWindowFlags_NoMove);
RenderSourcePanel(*this);
ImGui::EndChild();
ImGui::SameLine();
}
ImGui::BeginChild("##plots", ImVec2(plotW, 0.f), false);
renderPlotGrid();
ImGui::EndChild();
ImGui::End();
/* Overlays / modals */
if (showStats_) { renderStatsModal(); }
if (showAddSrc_) { renderAddSourceModal(); }
}
/* ── Menu bar ────────────────────────────────────────────────────────────── */
void App::renderMenuBar() {
if (!ImGui::BeginMenuBar()) { return; }
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("Quit", "Alt+F4")) {
/* Signal quit via SDL event — handled in main.cpp via flag */
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("View")) {
ImGui::MenuItem("Sidebar", nullptr, &sidebarOpen_);
ImGui::MenuItem("Trigger Bar", nullptr, &showTrigBar_);
ImGui::MenuItem("Statistics", nullptr, &showStats_);
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Sources")) {
if (ImGui::MenuItem(ICON_FA_PLUS " Add Source...")) { showAddSrc_ = true; }
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Layout")) {
for (int i = 0; i < static_cast<int>(PlotLayout::kCount); i++) {
bool sel = (static_cast<int>(layout_) == i);
if (ImGui::MenuItem(PlotLayoutNames[i], nullptr, sel)) {
setLayout(static_cast<PlotLayout>(i));
}
}
ImGui::EndMenu();
}
/* Right-aligned: [host] : [port] [Connect/Reconnect] ● status */
bool connected = ws_.isConnected();
const char* btnLbl = connected ? "Reconnect" : "Connect";
const char* ledLbl = connected ? ICON_FA_CIRCLE " Connected"
: ICON_FA_CIRCLE " Disconnected";
/* Measure widget widths to right-align the whole group */
const ImGuiStyle& st = ImGui::GetStyle();
const float kHostW = 130.f;
const float kPortW = 52.f;
const float kBtnW = ImGui::CalcTextSize(btnLbl).x + st.FramePadding.x * 2.f;
const float kLedW = ImGui::CalcTextSize(ledLbl).x;
const float kGroupW = kHostW + 8.f + kPortW + 8.f + kBtnW + 8.f + kLedW;
ImGui::SetCursorPosX(
ImGui::GetCursorPosX() +
ImGui::GetContentRegionAvail().x - kGroupW);
ImGui::SetNextItemWidth(kHostW);
ImGui::InputText("##hubhost", hostBuf_, sizeof(hostBuf_));
ImGui::SameLine(0.f, 2.f);
ImGui::TextUnformatted(":");
ImGui::SameLine(0.f, 2.f);
ImGui::SetNextItemWidth(kPortW);
ImGui::InputText("##hubport", portBuf_, sizeof(portBuf_),
ImGuiInputTextFlags_CharsDecimal);
ImGui::SameLine(0.f, 6.f);
if (ImGui::Button(btnLbl)) {
ws_.reconnect(hostBuf_, static_cast<uint16_t>(atoi(portBuf_)));
}
ImGui::SameLine(0.f, 8.f);
ImVec4 ledColor = connected
? ImVec4(0.1f, 0.9f, 0.3f, 1.f)
: ImVec4(0.9f, 0.2f, 0.2f, 1.f);
ImGui::TextColored(ledColor, "%s", ledLbl);
ImGui::EndMenuBar();
}
/* ── Toolbar ─────────────────────────────────────────────────────────────── */
void App::renderToolbar() {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 4.f));
/* Layout picker — pictogram popup */
static const int kIconCols[] = {1,2,1,3,1,2,4,1};
static const int kIconRows[] = {1,1,2,1,3,2,1,4};
static const ImVec2 kIconSz = ImVec2(36.f, 24.f);
if (ImGui::Button(ICON_FA_TABLE_CELLS_LARGE " Layout")) {
ImGui::OpenPopup("##layout_pop");
}
ImGui::SameLine();
if (ImGui::BeginPopup("##layout_pop")) {
ImGui::TextDisabled("Select layout");
ImGui::Separator();
ImDrawList* ldl = ImGui::GetWindowDrawList();
for (int li = 0; li < static_cast<int>(PlotLayout::kCount); li++) {
bool sel = (static_cast<int>(layout_) == li);
ImVec2 p = ImGui::GetCursorScreenPos();
/* Invisible button sized to the icon */
if (ImGui::InvisibleButton(PlotLayoutNames[li], kIconSz)) {
setLayout(static_cast<PlotLayout>(li));
ImGui::CloseCurrentPopup();
}
DrawLayoutIcon(ldl, p, kIconSz,
kIconCols[li], kIconRows[li], sel);
if (li < static_cast<int>(PlotLayout::kCount) - 1) {
ImGui::SameLine(0.f, 4.f);
}
}
ImGui::EndPopup();
}
/* Global pause */
if (ImGui::Button(globalPaused_ ? ICON_FA_PLAY " Resume"
: ICON_FA_PAUSE " Pause")) {
globalPaused_ = !globalPaused_;
for (int i = 0; i < numPlotSlots(); i++) {
plotPaused_[i] = globalPaused_;
if (!globalPaused_) { liveFollow_[i] = true; }
}
}
ImGui::SameLine();
/* Cursors A/B (global: shared & synchronised across all plots) */
{
if (cursorsOn_) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
}
if (ImGui::Button(ICON_FA_CROSSHAIRS " Cursors")) {
cursorsOn_ = !cursorsOn_;
if (cursorsOn_) {
/* seed cursors at 25% / 75% of the live window */
const double wallNow = std::chrono::duration<double>(
std::chrono::system_clock::now().time_since_epoch()).count();
const double x0 = wallNow - windowSec_;
cursorA_ = x0 + windowSec_ * 0.25;
cursorB_ = x0 + windowSec_ * 0.75;
}
}
if (cursorsOn_) { ImGui::PopStyleColor(); }
}
ImGui::SameLine();
/* Trigger bar toggle */
if (ImGui::Button(ICON_FA_BOLT " Trigger")) { showTrigBar_ = !showTrigBar_; }
ImGui::SameLine();
/* Stats */
if (ImGui::Button(ICON_FA_CHART_COLUMN " Stats")) { showStats_ = !showStats_; }
ImGui::SameLine();
/* Add source */
if (ImGui::Button(ICON_FA_PLUS " Source")) { showAddSrc_ = true; }
ImGui::SameLine();
/* Time window presets (Ctrl+scroll on a plot still adjusts freely) */
static const double kWinPresets[] = {1.0, 5.0, 10.0, 30.0, 60.0};
static const char* kWinLabels[] = {"1 s", "5 s", "10 s", "30 s", "60 s"};
char winPreview[16];
snprintf(winPreview, sizeof(winPreview), "%.3g s", windowSec_);
ImGui::SetNextItemWidth(70.f);
if (ImGui::BeginCombo("Win", winPreview)) {
for (int wi = 0; wi < 5; wi++) {
bool sel = std::fabs(windowSec_ - kWinPresets[wi]) < 1e-9;
if (ImGui::Selectable(kWinLabels[wi], sel)) {
windowSec_ = kWinPresets[wi];
}
if (sel) { ImGui::SetItemDefaultFocus(); }
}
ImGui::EndCombo();
}
ImGui::SameLine();
/* Max points control */
ImGui::SetNextItemWidth(80.f);
int mp = static_cast<int>(maxPoints_);
if (ImGui::InputInt("MaxPts", &mp, 0, 0,
ImGuiInputTextFlags_EnterReturnsTrue)) {
if (mp >= 2) {
maxPoints_ = static_cast<uint32_t>(mp);
ws_.sendText(BuildSetMaxPoints(maxPoints_));
}
}
ImGui::PopStyleVar();
ImGui::Separator();
}
/* ── Plot grid ───────────────────────────────────────────────────────────── */
void App::renderPlotGrid() {
int cols, rows;
PlotLayoutDims(layout_, cols, rows);
/* Per-cell fraction arrays (reset when layout changes) */
static float rowFrac[8] = {1,1,1,1,1,1,1,1};
static float colFrac[8] = {1,1,1,1,1,1,1,1};
static PlotLayout prevLayout = PlotLayout::kCount;
if (layout_ != prevLayout) {
for (int i = 0; i < 8; i++) { rowFrac[i] = 1.f; colFrac[i] = 1.f; }
prevLayout = layout_;
}
const float kSep = 5.f; /* separator strip width/height in px */
float avW = ImGui::GetContentRegionAvail().x;
float avH = ImGui::GetContentRegionAvail().y;
float cellTotalW = avW - kSep * (cols - 1);
float cellTotalH = avH - kSep * (rows - 1);
/* Normalise fractions */
float rowSum = 0.f; for (int r = 0; r < rows; r++) rowSum += rowFrac[r];
float colSum = 0.f; for (int c = 0; c < cols; c++) colSum += colFrac[c];
if (rowSum < 1e-6f) rowSum = 1.f;
if (colSum < 1e-6f) colSum = 1.f;
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.f, 0.f));
for (int r = 0; r < rows; r++) {
float rh = std::max(30.f, cellTotalH * rowFrac[r] / rowSum);
for (int c = 0; c < cols; c++) {
float cw = std::max(60.f, cellTotalW * colFrac[c] / colSum);
int plotIdx = r * cols + c;
char cellId[32]; snprintf(cellId, sizeof(cellId), "##cell%d", plotIdx);
ImGui::BeginChild(cellId, ImVec2(cw, rh), false,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
if (plotIdx < kMaxPlotSlots) {
RenderPlotPanel(*this, plotIdx, plotPaused_[plotIdx]);
}
ImGui::EndChild();
/* ── Column separator ── */
if (c < cols - 1) {
ImGui::SameLine(0.f, 0.f);
char csId[32]; snprintf(csId, sizeof(csId), "##cs%d_%d", r, c);
ImVec2 p0 = ImGui::GetCursorScreenPos();
ImGui::InvisibleButton(csId, ImVec2(kSep, rh));
if (ImGui::IsItemActive()) {
float dx = ImGui::GetIO().MouseDelta.x;
colFrac[c] += dx * colSum / cellTotalW;
colFrac[c+1] -= dx * colSum / cellTotalW;
colFrac[c] = std::max(0.05f * colSum, colFrac[c]);
colFrac[c+1] = std::max(0.05f * colSum, colFrac[c+1]);
}
if (ImGui::IsItemHovered() || ImGui::IsItemActive())
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
ImGui::GetWindowDrawList()->AddRectFilled(
p0, ImVec2(p0.x + kSep, p0.y + rh), IM_COL32(49,50,68,255));
ImGui::SameLine(0.f, 0.f);
}
} /* end col loop */
/* ── Row separator ── */
if (r < rows - 1) {
char rsId[32]; snprintf(rsId, sizeof(rsId), "##rs%d", r);
/* Reposition X to left content edge before drawing full-width strip */
ImGui::SetCursorPosX(0.f);
ImVec2 p0 = ImGui::GetCursorScreenPos();
ImGui::InvisibleButton(rsId, ImVec2(avW, kSep));
if (ImGui::IsItemActive()) {
float dy = ImGui::GetIO().MouseDelta.y;
rowFrac[r] += dy * rowSum / cellTotalH;
rowFrac[r+1] -= dy * rowSum / cellTotalH;
rowFrac[r] = std::max(0.05f * rowSum, rowFrac[r]);
rowFrac[r+1] = std::max(0.05f * rowSum, rowFrac[r+1]);
}
if (ImGui::IsItemHovered() || ImGui::IsItemActive())
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
ImGui::GetWindowDrawList()->AddRectFilled(
p0, ImVec2(p0.x + avW, p0.y + kSep), IM_COL32(49,50,68,255));
}
} /* end row loop */
ImGui::PopStyleVar();
}
/* ── Stats modal ─────────────────────────────────────────────────────────── */
void App::renderStatsModal() {
ImGui::SetNextWindowSize(ImVec2(700.f, 300.f), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Statistics", &showStats_)) {
RenderStatsPanel(*this);
}
ImGui::End();
}
/* ── Add-source modal ────────────────────────────────────────────────────── */
void App::renderAddSourceModal() {
ImGui::SetNextWindowSize(ImVec2(360.f, 260.f), ImGuiCond_Always);
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
ImGuiCond_Always, ImVec2(0.5f, 0.5f));
if (ImGui::Begin("Add Source", &showAddSrc_,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) {
static char label[128]= "";
static char host[64] = "127.0.0.1";
static int port = 44500;
static char mcast[64] = "";
static int dataPort = 0;
ImGui::InputText("Label", label, sizeof(label));
ImGui::InputText("Host", host, sizeof(host));
ImGui::InputInt("Port", &port);
ImGui::InputText("Multicast", mcast, sizeof(mcast));
ImGui::InputInt("Data Port", &dataPort);
ImGui::Spacing();
if (ImGui::Button("Connect", ImVec2(100.f, 0.f))) {
if (host[0] != '\0' && port > 0 && port < 65536) {
char addr[96];
snprintf(addr, sizeof(addr), "%s:%d", host, port);
ws_.sendText(BuildAddSource(label, addr, mcast,
static_cast<uint16_t>(
(dataPort > 0 && dataPort < 65536)
? dataPort : 0)));
showAddSrc_ = false;
label[0] = '\0';
}
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(80.f, 0.f))) {
showAddSrc_ = false;
}
}
ImGui::End();
}
/* ── Trigger bar ─────────────────────────────────────────────────────────── */
void App::renderTriggerBar() {
RenderTriggerPanel(*this);
}
/* ── Layout ──────────────────────────────────────────────────────────────── */
int App::numPlotSlots() const {
int cols, rows;
PlotLayoutDims(layout_, cols, rows);
return cols * rows;
}
void App::setLayout(PlotLayout l) {
layout_ = l;
}
/* ── Source helpers ──────────────────────────────────────────────────────── */
int App::findSource(const std::string& id) const {
for (int i = 0; i < static_cast<int>(sources_.size()); i++) {
if (sources_[i].id == id) { return i; }
}
return -1;
}
int App::findSignal(const Source& src, const std::string& name) const {
for (int i = 0; i < static_cast<int>(src.signals.size()); i++) {
if (src.signals[i].meta.name == name) { return i; }
}
return -1;
}
/* ── WS message dispatch ─────────────────────────────────────────────────── */
void App::handleJSON(const std::string& json) {
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 == "maxPointsUpdated") { onMaxPointsUpdated(json); }
/* pong: ignore */
}
void App::handleBinary(const uint8_t* data, size_t len) {
if (len == 0) { return; }
/* Version 2: trigger capture frame */
if (data[0] == 2u) {
CaptureFrame cf;
if (ParseCaptureFrame(data, len, cf)) {
capture_ = std::move(cf);
hasCapture_ = true;
trigger_.status = "triggered";
trigger_.trigTime = capture_.trigTime;
trigger_.hasTrigTime = true;
}
return;
}
/* Version 1: live push frame. The hub pushes only samples that are new
* since the previous tick (non-overlapping), so push verbatim. */
DataFrame frame;
if (!ParseBinaryFrame(data, len, frame)) { return; }
std::lock_guard<std::mutex> lk(srcMutex_);
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; }
/* Always push: the ring is the "active" buffer; paused plots render
* from their frozen view snapshot, so no data is ever dropped. */
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 event handlers ─────────────────────────────────────────────────── */
void App::onSources(const std::string& json) {
std::vector<SourceInfo> infos;
ParseSources(json, infos);
std::lock_guard<std::mutex> lk(srcMutex_);
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;
}
}
/* Remove sources that are no longer listed */
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());
}
void App::onConfig(const std::string& json) {
std::string sourceId;
int publishMode = 0;
std::vector<SignalMeta> metas;
if (!ParseConfig(json, sourceId, publishMode, metas)) { return; }
std::lock_guard<std::mutex> lk(srcMutex_);
int idx = findSource(sourceId);
if (idx < 0) { return; }
Source& src = sources_[idx];
src.configured = true;
src.publishMode = publishMode;
/* Colour palette — Catppuccin Mocha trace colours */
static const ImVec4 kPalette[] = {
{0.537f,0.706f,0.980f,1.f}, /* blue */
{0.651f,0.890f,0.631f,1.f}, /* green */
{0.953f,0.545f,0.659f,1.f}, /* red */
{0.980f,0.702f,0.529f,1.f}, /* peach */
{0.796f,0.651f,0.969f,1.f}, /* mauve */
{0.580f,0.886f,0.835f,1.f}, /* teal */
{0.537f,0.863f,0.922f,1.f}, /* sky */
{0.706f,0.745f,0.996f,1.f}, /* lavender */
};
/* Merge: add new signals, keep existing ring buffers */
for (size_t m = 0; m < metas.size(); m++) {
int si = findSignal(src, metas[m].name);
if (si < 0) {
Signal sig;
sig.meta = metas[m];
sig.color = kPalette[src.signals.size() % 8];
src.signals.push_back(std::move(sig));
} else {
src.signals[si].meta = metas[m];
}
}
}
void App::onStats(const std::string& json) {
std::vector<std::pair<std::string, SourceStats>> statsVec;
ParseStats(json, statsVec);
std::lock_guard<std::mutex> lk(srcMutex_);
for (const auto& kv : statsVec) {
int idx = findSource(kv.first);
if (idx >= 0) {
sources_[idx].stats = kv.second;
sources_[idx].state = kv.second.state;
}
}
}
void App::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;
}
/* Double-buffer semantics: the last recorded capture stays on display
* (even while re-armed/collecting) and is only replaced when a new
* capture frame has been fully received and parsed (handleBinary v2). */
if (msg.state == "idle") {
trigger_.hasTrigTime = false;
}
}
void App::onZoom(const std::string& json) {
ZoomResponse resp;
if (!ParseZoom(json, resp)) { return; }
/* Route the reply to the plot that requested it */
for (int i = 0; i < kMaxPlotSlots; i++) {
auto& zc = zoomCache_[i];
if (zc.pending && zc.reqId == resp.reqId) {
zc.signals = std::move(resp.signals);
zc.t0 = zc.reqT0;
zc.t1 = zc.reqT1;
zc.valid = true;
zc.pending = false;
return;
}
}
}
void App::requestZoom(int plotIdx, double t0, double t1,
const std::string& signalsCsv) {
if (plotIdx < 0 || plotIdx >= kMaxPlotSlots) { return; }
if (!ws_.isConnected() || signalsCsv.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, signalsCsv));
}
std::string App::slotKey(const PlotAssignment& a) const {
if (a.sourceIdx < 0 || a.sourceIdx >= static_cast<int>(sources_.size())) {
return std::string();
}
const Source& src = sources_[a.sourceIdx];
if (a.signalIdx < 0 || a.signalIdx >= static_cast<int>(src.signals.size())) {
return std::string();
}
return src.id + ":" + src.signals[a.signalIdx].meta.name;
}
void App::onMaxPointsUpdated(const std::string& json) {
uint32_t mp = 0;
ParseMaxPointsUpdated(json, mp);
if (mp >= 2) {
maxPoints_ = mp;
std::lock_guard<std::mutex> lk(srcMutex_);
for (auto& src : sources_) {
for (auto& sig : src.signals) {
sig.buf.setCapacity(mp);
}
}
}
}
} /* namespace StreamHubClient */