/** * @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 #include #include #include #include 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(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(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(PlotLayout::kCount); i++) { bool sel = (static_cast(layout_) == i); if (ImGui::MenuItem(PlotLayoutNames[i], nullptr, sel)) { setLayout(static_cast(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(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(PlotLayout::kCount); li++) { bool sel = (static_cast(layout_) == li); ImVec2 p = ImGui::GetCursorScreenPos(); /* Invisible button sized to the icon */ if (ImGui::InvisibleButton(PlotLayoutNames[li], kIconSz)) { setLayout(static_cast(li)); ImGui::CloseCurrentPopup(); } DrawLayoutIcon(ldl, p, kIconSz, kIconCols[li], kIconRows[li], sel); if (li < static_cast(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( 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(maxPoints_); if (ImGui::InputInt("MaxPts", &mp, 0, 0, ImGuiInputTextFlags_EnterReturnsTrue)) { if (mp >= 2) { maxPoints_ = static_cast(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( (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(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(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 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 infos; ParseSources(json, infos); std::lock_guard 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 metas; if (!ParseConfig(json, sourceId, publishMode, metas)) { return; } std::lock_guard 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> statsVec; ParseStats(json, statsVec); std::lock_guard 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(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; } void App::onMaxPointsUpdated(const std::string& json) { uint32_t mp = 0; ParseMaxPointsUpdated(json, mp); if (mp >= 2) { maxPoints_ = mp; std::lock_guard lk(srcMutex_); for (auto& src : sources_) { for (auto& sig : src.signals) { sig.buf.setCapacity(mp); } } } } } /* namespace StreamHubClient */