Files
MARTe-Integrated-Components/Client/streamhub/App.h
2026-06-25 00:45:45 +02:00

348 lines
13 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file App.h
* @brief Top-level application state and frame dispatch.
*/
#pragma once
#include "WSClient.h"
#include "Protocol.h"
#include "SignalBuffer.h"
#include <string>
#include <vector>
#include <memory>
#include <mutex>
#include <cstdint>
#include "imgui.h"
namespace StreamHubClient {
/*---------------------------------------------------------------------------*/
/* Domain types */
/*---------------------------------------------------------------------------*/
/** One signal as known to the client. */
struct Signal {
SignalMeta meta;
SignalBuffer buf{1000000};
ImVec4 color{0.4f, 0.8f, 1.0f, 1.0f};
float lineWidth = 1.5f;
int marker = -1; /* ImPlotMarker (-1 = none) */
bool visible = true;
};
/** One connected UDPStreamer source. */
struct Source {
std::string id;
std::string label;
std::string addr;
std::string state = "disconnected";
uint32_t port = 0;
bool configured = false;
int publishMode = 0;
std::vector<Signal> signals;
SourceStats stats;
};
/** Trigger configuration and live state (hub-side trigger semantics). */
struct TriggerState {
/* Config (sent to the hub via setTrigger) */
std::string signalKey; /* full key "src:sig" or "src:sig[i]" */
int edge = 0; /* 0=rising 1=falling 2=both */
double threshold = 0.0;
double windowSec = 0.1; /* 100 µs .. 10 s */
double prePercent = 20.0;
bool single = false; /* false=normal true=single */
/* Live (driven by hub triggerState broadcasts) */
std::string status = "idle"; /* idle|armed|collecting|triggered */
bool stopped = false;
bool hasTrigTime = false;
double trigTime = 0.0;
};
/** Per-signal vertical scale state (oscilloscope style). */
struct VScale {
int mode = 0; /* 0=auto 1=range 2=manual */
double divValue = 1.0; /* units per division (manual mode) */
double offset = 0.0; /* raw value at center (manual mode) */
double screenPos = 0.0; /* position offset in divisions from center */
bool digitalInMixed = false; /* quantize this trace in mixed plot mode */
/* Resolved each frame (read-only): */
double resolvedDiv = 1.0;
double resolvedOffset = 0.0;
};
/** Assignment of one signal to a plot panel. */
struct PlotAssignment {
int sourceIdx = -1;
int signalIdx = -1;
VScale vs;
};
/*---------------------------------------------------------------------------*/
/* Plot layouts */
/*---------------------------------------------------------------------------*/
/* Layout set mirrors the web UI: cols×rows */
enum class PlotLayout {
kLayout1x1 = 0,
kLayout2x1, /* 2 side by side */
kLayout1x2, /* 2 stacked */
kLayout3x1,
kLayout1x3,
kLayout2x2,
kLayout4x1,
kLayout1x4,
kCount
};
static inline const char* const PlotLayoutNames[] = {
"1×1", "2×1", "1×2", "3×1", "1×3", "2×2", "4×1", "1×4"
};
static inline void PlotLayoutDims(PlotLayout l, int& cols, int& rows) {
switch (l) {
case PlotLayout::kLayout1x1: cols=1; rows=1; break;
case PlotLayout::kLayout2x1: cols=2; rows=1; break;
case PlotLayout::kLayout1x2: cols=1; rows=2; break;
case PlotLayout::kLayout3x1: cols=3; rows=1; break;
case PlotLayout::kLayout1x3: cols=1; rows=3; break;
case PlotLayout::kLayout2x2: cols=2; rows=2; break;
case PlotLayout::kLayout4x1: cols=4; rows=1; break;
case PlotLayout::kLayout1x4: cols=1; rows=4; break;
default: cols=1; rows=1; break;
}
}
/*---------------------------------------------------------------------------*/
/* App */
/*---------------------------------------------------------------------------*/
class App {
public:
App();
~App();
/** @brief Initialise and start the WebSocket connection. */
void init(const std::string& host, uint16_t port);
/**
* @brief Call once per ImGui frame.
* Drains the WS receive queue, dispatches messages, renders all panels.
*/
void update();
/** @brief Disconnect and clean up. */
void shutdown();
/* ---- Accessors used by sub-panels ----------------------------------- */
std::vector<Source>& sources() { return sources_; }
const std::vector<Source>& sources() const { return sources_; }
TriggerState& trigger() { return trigger_; }
WSClient& ws() { return ws_; }
uint32_t maxPoints() const { return maxPoints_; }
/** @brief Last trigger capture received from the hub (nullptr if none). */
const CaptureFrame* capture() const { return hasCapture_ ? &capture_ : nullptr; }
void clearCapture() { hasCapture_ = false; }
/** @brief Get the plot assignment vector for plot index i. */
std::vector<PlotAssignment>& plotSlot(int i) { return plotSlots_[i]; }
int numPlotSlots() const;
PlotLayout plotLayout() const { return layout_; }
void setLayout(PlotLayout l);
/** @brief True if all plots are paused (global pause). */
bool globalPaused() const { return globalPaused_; }
void setGlobalPaused(bool p) { globalPaused_ = p; }
/** @brief Live-follow state per plot (true = auto-scroll with newest data). */
bool& liveFollow(int i) { return liveFollow_[i]; }
/** @brief Which slot index is "active" (selected) in the given plot (-1 = none). */
int& activeSlot(int i) { return activeSlot_[i]; }
/** @brief Time window width in seconds used by live scroll. */
double windowSec() const { return windowSec_; }
void setWindowSec(double s){ if (s >= 1e-4 && s <= 3600.0) windowSec_ = s; }
/** @brief Stored X range for non-live (zoom/pan) mode per plot. */
double plotXMin(int i) const { return plotXMin_[i]; }
double plotXMax(int i) const { return plotXMax_[i]; }
void setPlotX(int i, double mn, double mx) {
if (mx > mn + 1e-9) { plotXMin_[i] = mn; plotXMax_[i] = mx; }
}
/** @brief Call when transitioning live→non-live to seed the stored X range. */
void initPlotX(int i, double tMax) {
plotXMin_[i] = tMax - windowSec_;
plotXMax_[i] = tMax;
}
/** @brief Per-plot vertical normalisation: 0=normal 1=digital 2=mixed. */
int& plotVMode(int i) { return plotVMode_[i]; }
/* ---- Cursors A/B (global: shared & synchronised across all plots) ---- */
bool& cursorsOn() { return cursorsOn_; }
double& cursorA() { return cursorA_; }
double& cursorB() { return cursorB_; }
/* ---- Paused view snapshot (per plot) ---------------------------------- *
* Double-buffer scheme: the per-signal ring buffers stay "active" (always
* written by the WS thread-drain); a paused plot freezes a "view" copy of
* its slots' data at pause time and renders from it, so the picture does
* not drain away while the rings keep rolling. */
struct PlotSnapshot {
bool valid = false;
std::vector<std::string> keys; /* slot key "src:sig" */
std::vector<std::vector<double>> t, v;
};
PlotSnapshot& plotSnap(int i) { return plotSnap_[i]; }
/* ---- Zoom history (per plot) ----------------------------------------- */
std::vector<std::pair<double,double>>& zoomHist(int i) { return zoomHist_[i]; }
/** @brief Push the current stored X range onto the history (capped). */
void pushZoomHist(int i) {
auto& h = zoomHist_[i];
if (!h.empty() && h.back().first == plotXMin_[i] &&
h.back().second == plotXMax_[i]) { return; }
if (h.size() >= 64) { h.erase(h.begin()); }
h.emplace_back(plotXMin_[i], plotXMax_[i]);
}
/* ---- Hi-res WS zoom (per plot) ---------------------------------------- */
struct PlotZoomCache {
bool valid = false; /* signals[] match [t0,t1] */
bool pending = false; /* request in flight */
uint32_t reqId = 0;
double t0 = 0.0, t1 = 0.0; /* range of valid data */
double reqT0 = 0.0, reqT1 = 0.0; /* range of pending request */
std::vector<ZoomSignal> signals;
};
PlotZoomCache& zoomCache(int i) { return zoomCache_[i]; }
/** @brief Send a hub zoom request for plot i over [t0,t1] (2400 pts). */
void requestZoom(int plotIdx, double t0, double t1,
const std::string& signalsCsv);
/** @brief Full key "src:sig" for an assignment (empty if invalid). */
std::string slotKey(const PlotAssignment& a) const;
/* ---- History ------------------------------------------------------------ */
/** @brief History info received from the hub. */
const HistoryInfoMsg& historyInfo() const { return historyInfo_; }
/** @brief Send a historyZoom request. */
void requestHistoryZoom(int plotIdx, double t0, double t1,
const std::string& signalsCsv);
/** @brief Hi-res history zoom cache (per plot, parallel to zoomCache_). */
PlotZoomCache& histZoomCache(int i) { return histZoomCache_[i]; }
/* ---- Trigger zoom (per plot, used in trigger capture view) ----------- */
bool& trigZoomed(int i) { return trigZoomed_[i]; }
void resetTrigZoom() { for (int i = 0; i < kMaxPlotSlots; i++) trigZoomed_[i] = false; }
/** @brief True if the stats panel is open. */
bool& showStats() { return showStats_; }
bool& showAddSrc() { return showAddSrc_; }
bool& showTrigBar() { return showTrigBar_; }
bool& showHistBar() { return showHistBar_; }
/** @brief Find source index by id (-1 if not found). */
int findSource(const std::string& id) const;
/** @brief Find signal index in source by name (-1 if not found). */
int findSignal(const Source& src, const std::string& name) const;
private:
/* ---- WS message dispatch ------------------------------------------- */
void handleJSON(const std::string& json);
void handleBinary(const uint8_t* data, size_t len);
void onSources(const std::string& json);
void onConfig(const std::string& json);
void onStats(const std::string& json);
void onTriggerState(const std::string& json);
void onZoom(const std::string& json);
void onHistoryZoom(const std::string& json);
void onHistoryInfo(const std::string& json);
void onMaxPointsUpdated(const std::string& json);
/* ---- Rendering ------------------------------------------------------ */
void renderToolbar();
void renderHistoryBar();
void renderTriggerBar();
void renderSidebar();
void renderPlotGrid();
void renderStatsModal();
void renderAddSourceModal();
/* ---- State ---------------------------------------------------------- */
WSClient ws_;
std::vector<Source> sources_;
mutable std::mutex srcMutex_; /* protects sources_ */
TriggerState trigger_;
PlotLayout layout_ = PlotLayout::kLayout1x1;
uint32_t maxPoints_ = 1000000u;
bool globalPaused_ = false;
/* Plot slots: one vector of assignments per plot panel */
static const int kMaxPlotSlots = 8;
std::vector<PlotAssignment> plotSlots_[kMaxPlotSlots];
bool plotPaused_[kMaxPlotSlots] = {};
bool liveFollow_[kMaxPlotSlots] = {}; /* true = live-scroll X axis */
int activeSlot_[kMaxPlotSlots]; /* which slot is "active" (-1 = none) */
double windowSec_ = 10.0; /* live scroll window width */
double plotXMin_[kMaxPlotSlots] = {}; /* stored X min for non-live mode */
double plotXMax_[kMaxPlotSlots] = {}; /* stored X max for non-live mode */
int plotVMode_[kMaxPlotSlots] = {}; /* 0=normal 1=digital 2=mixed */
/* Cursors (global) */
bool cursorsOn_ = false;
double cursorA_ = 0.0;
double cursorB_ = 0.0;
/* Trigger zoom per plot (capture view zoom state) */
bool trigZoomed_[kMaxPlotSlots] = {};
/* Frozen view per plot (valid while the plot is paused) */
PlotSnapshot plotSnap_[kMaxPlotSlots];
/* Zoom history + hi-res zoom cache */
std::vector<std::pair<double,double>> zoomHist_[kMaxPlotSlots];
PlotZoomCache zoomCache_[kMaxPlotSlots];
PlotZoomCache histZoomCache_[kMaxPlotSlots];
uint32_t nextZoomReqId_ = 1;
/* History info from the hub */
HistoryInfoMsg historyInfo_;
double lastHistInfoReq_ = 0.0;
/* UI state */
bool showStats_ = false;
bool showAddSrc_ = false;
bool showTrigBar_ = false;
bool showHistBar_ = false;
bool sidebarOpen_ = true;
/* Connection fields (populated by init(), editable in menu bar) */
char hostBuf_[64] = "127.0.0.1";
char portBuf_[8] = "8090";
/* Track connect transition to send initial queries */
bool prevConnected_ = false;
/* Last trigger capture (version=2 binary frame; render thread only) */
CaptureFrame capture_;
bool hasCapture_ = false;
};
} /* namespace StreamHubClient */