Implemented new C++ logic
This commit is contained in:
@@ -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 */
|
||||
Reference in New Issue
Block a user