Implemented new C++ logic

This commit is contained in:
Martino Ferrari
2026-06-12 15:25:13 +02:00
parent 617b5bd712
commit f25bd7f08e
220 changed files with 39185 additions and 850 deletions
+319
View File
@@ -0,0 +1,319 @@
/**
* @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{20000};
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;
/** @brief True if the stats panel is open. */
bool& showStats() { return showStats_; }
bool& showAddSrc() { return showAddSrc_; }
bool& showTrigBar() { return showTrigBar_; }
/** @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 onMaxPointsUpdated(const std::string& json);
/* ---- Rendering ------------------------------------------------------ */
void renderMenuBar();
void renderToolbar();
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_ = 20000u;
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;
/* 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];
uint32_t nextZoomReqId_ = 1;
/* UI state */
bool showStats_ = false;
bool showAddSrc_ = false;
bool showTrigBar_ = 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 */