Implemented client datasource

This commit is contained in:
Martino Ferrari
2026-06-25 00:45:45 +02:00
parent dca4872976
commit 0412c20edd
28 changed files with 3448 additions and 618 deletions
+322 -115
View File
@@ -75,9 +75,21 @@ void App::update() {
if (nowConnected && !prevConnected_) {
ws_.sendText(BuildGetSources());
ws_.sendText(BuildGetStats());
ws_.sendText(BuildHistoryInfo());
}
prevConnected_ = nowConnected;
/* Re-request history info periodically until signals are populated
* (the hub opens history files after the first data packet, which is
* after our initial connect-time query). */
if (nowConnected && historyInfo_.enabled && historyInfo_.signals.empty()) {
double now = ImGui::GetTime();
if (now - lastHistInfoReq_ > 2.0) {
ws_.sendText(BuildHistoryInfo());
lastHistInfoReq_ = now;
}
}
/* Drain WebSocket receive queue */
ws_.poll([this](const WSMessage& msg) {
if (msg.isBinary) {
@@ -99,8 +111,7 @@ void App::update() {
| ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoBringToFrontOnFocus
| ImGuiWindowFlags_NoNavFocus
| ImGuiWindowFlags_MenuBar;
| ImGuiWindowFlags_NoNavFocus;
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
@@ -108,8 +119,8 @@ void App::update() {
ImGui::Begin("##main", nullptr, wf);
ImGui::PopStyleVar(3);
renderMenuBar();
renderToolbar();
if (showHistBar_) { renderHistoryBar(); }
if (showTrigBar_) { renderTriggerBar(); }
/* Horizontal split: sidebar | plot grid */
@@ -136,90 +147,33 @@ void App::update() {
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();
}
/* renderMenuBar() removed — all functionality merged into toolbar + sidebar */
/* ── Toolbar ─────────────────────────────────────────────────────────────── */
void App::renderToolbar() {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 4.f));
/* Layout picker — pictogram popup */
/* ── Sidebar toggle (leftmost) ───────────────────────────────────────── */
{
if (sidebarOpen_) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
}
if (ImGui::Button(ICON_FA_BARS "##sidebar")) { sidebarOpen_ = !sidebarOpen_; }
if (sidebarOpen_) { ImGui::PopStyleColor(); }
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Toggle sidebar"); }
}
ImGui::SameLine();
/* ── 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")) {
if (ImGui::Button(ICON_FA_TABLE_CELLS_LARGE "##layout")) {
ImGui::OpenPopup("##layout_pop");
}
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Layout"); }
ImGui::SameLine();
if (ImGui::BeginPopup("##layout_pop")) {
ImGui::TextDisabled("Select layout");
@@ -228,7 +182,6 @@ void App::renderToolbar() {
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();
@@ -242,7 +195,7 @@ void App::renderToolbar() {
ImGui::EndPopup();
}
/* Global pause */
/* ── Global pause ────────────────────────────────────────────────────── */
if (ImGui::Button(globalPaused_ ? ICON_FA_PLAY " Resume"
: ICON_FA_PAUSE " Pause")) {
globalPaused_ = !globalPaused_;
@@ -253,45 +206,77 @@ void App::renderToolbar() {
}
ImGui::SameLine();
/* Cursors A/B (global: shared & synchronised across all plots) */
/* ── Cursors A/B toggle ──────────────────────────────────────────────── */
{
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 (ImGui::Button(ICON_FA_CROSSHAIRS "##curs")) { cursorsOn_ = !cursorsOn_; }
if (cursorsOn_) { ImGui::PopStyleColor(); }
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Cursors A/B"); }
}
ImGui::SameLine();
/* Trigger bar toggle */
if (ImGui::Button(ICON_FA_BOLT " Trigger")) { showTrigBar_ = !showTrigBar_; }
/* ── Trigger bar toggle ──────────────────────────────────────────────── */
{
if (showTrigBar_) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
}
if (ImGui::Button(ICON_FA_BOLT "##trig")) { showTrigBar_ = !showTrigBar_; }
if (showTrigBar_) { ImGui::PopStyleColor(); }
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Trigger"); }
}
ImGui::SameLine();
/* Stats */
if (ImGui::Button(ICON_FA_CHART_COLUMN " Stats")) { showStats_ = !showStats_; }
/* ── History browse ──────────────────────────────────────────────────── */
{
bool histAvail = historyInfo_.enabled && !historyInfo_.signals.empty();
if (!histAvail) { ImGui::BeginDisabled(); }
if (showHistBar_) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
}
if (ImGui::Button(ICON_FA_CLOCK_ROTATE_LEFT "##hist")) {
if (histAvail) {
showHistBar_ = !showHistBar_;
if (showHistBar_) {
/* On first open, jump to the full history range */
double ht0 = 1e300, ht1 = -1e300;
for (const auto& hs : historyInfo_.signals) {
if (hs.t0 < ht0) ht0 = hs.t0;
if (hs.t1 > ht1) ht1 = hs.t1;
}
if (ht1 > ht0) {
for (int i = 0; i < numPlotSlots(); i++) {
liveFollow_[i] = false;
setPlotX(i, ht0, ht1);
zoomCache_[i].valid = false;
zoomCache_[i].pending = false;
histZoomCache_[i].valid = false;
histZoomCache_[i].pending = false;
zoomHist_[i].clear();
}
}
} else {
/* Closing the bar → back to live */
for (int i = 0; i < numPlotSlots(); i++) {
liveFollow_[i] = true;
}
}
}
}
if (showHistBar_) { ImGui::PopStyleColor(); }
if (!histAvail) { ImGui::EndDisabled(); }
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("History browse"); }
}
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) */
/* ── Time window presets ─────────────────────────────────────────────── */
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)) {
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)) {
@@ -301,18 +286,213 @@ void App::renderToolbar() {
}
ImGui::EndCombo();
}
/* ── Right-aligned group: [Stats] [Connection ▼] ● LED ──────────────── */
bool connected = ws_.isConnected();
const char* connLabel = connected
? ICON_FA_CIRCLE " Connected"
: ICON_FA_CIRCLE " Disconnected";
const ImGuiStyle& st = ImGui::GetStyle();
const float kStatsW = ImGui::CalcTextSize(ICON_FA_CHART_COLUMN).x
+ st.FramePadding.x * 2.f;
const float kConnBtnW = ImGui::CalcTextSize(ICON_FA_LINK).x
+ st.FramePadding.x * 2.f;
const float kLedW = ImGui::CalcTextSize(connLabel).x + 8.f;
const float kGroupW = kStatsW + 4.f + kConnBtnW + 4.f + kLedW;
float rightX = ImGui::GetCursorPosX() +
ImGui::GetContentRegionAvail().x - kGroupW;
if (rightX > ImGui::GetCursorPosX()) {
ImGui::SameLine(rightX);
} else {
ImGui::SameLine();
}
/* Stats toggle */
{
if (showStats_) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
}
if (ImGui::Button(ICON_FA_CHART_COLUMN "##stats")) { showStats_ = !showStats_; }
if (showStats_) { ImGui::PopStyleColor(); }
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Statistics"); }
}
ImGui::SameLine(0.f, 4.f);
/* Connection dropdown */
if (ImGui::Button(ICON_FA_LINK "##conn")) {
ImGui::OpenPopup("##conn_pop");
}
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Connection"); }
if (ImGui::BeginPopup("##conn_pop")) {
ImGui::TextDisabled("Hub Connection");
ImGui::Separator();
ImGui::SetNextItemWidth(160.f);
ImGui::InputText("Host##hubhost", hostBuf_, sizeof(hostBuf_));
ImGui::SetNextItemWidth(80.f);
ImGui::InputText("Port##hubport", portBuf_, sizeof(portBuf_),
ImGuiInputTextFlags_CharsDecimal);
ImGui::Spacing();
const char* btnLbl = connected ? "Reconnect" : "Connect";
if (ImGui::Button(btnLbl, ImVec2(120.f, 0.f))) {
ws_.reconnect(hostBuf_, static_cast<uint16_t>(atoi(portBuf_)));
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
ImGui::SameLine(0.f, 4.f);
/* Status LED */
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", connLabel);
ImGui::PopStyleVar();
ImGui::Separator();
}
/* ── History browsing toolbar ────────────────────────────────────────────── */
void App::renderHistoryBar() {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 4.f));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.980f,0.702f,0.529f,1.f));
/* Title */
ImGui::TextUnformatted(ICON_FA_CLOCK_ROTATE_LEFT " History");
ImGui::PopStyleColor();
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_));
/* ── Live button ── */
if (ImGui::SmallButton(ICON_FA_TOWER_BROADCAST " Live")) {
showHistBar_ = false;
for (int i = 0; i < numPlotSlots(); i++) {
liveFollow_[i] = true;
}
ImGui::PopStyleVar();
return;
}
ImGui::SameLine();
ImGui::TextDisabled("|"); ImGui::SameLine();
/* ── Current time range display ── */
double t0 = 0.0, t1 = 0.0;
bool haveRange = false;
for (int i = 0; i < numPlotSlots(); i++) {
if (!liveFollow_[i]) {
t0 = plotXMin_[i]; t1 = plotXMax_[i];
haveRange = true;
break;
}
}
const double wallNow = std::chrono::duration<double>(
std::chrono::system_clock::now().time_since_epoch()).count();
if (haveRange) {
double span = t1 - t0;
double ago = wallNow - t1;
char rangeLabel[128];
if (ago < 1.0) {
snprintf(rangeLabel, sizeof(rangeLabel),
"%.3g s span | now", span);
} else if (ago < 60.0) {
snprintf(rangeLabel, sizeof(rangeLabel),
"%.3g s span | %.0f s ago", span, ago);
} else if (ago < 3600.0) {
snprintf(rangeLabel, sizeof(rangeLabel),
"%.3g s span | %.1f min ago", span, ago / 60.0);
} else {
snprintf(rangeLabel, sizeof(rangeLabel),
"%.3g s span | %.1f h ago", span, ago / 3600.0);
}
ImGui::TextDisabled("%s", rangeLabel);
} else {
ImGui::TextDisabled("No plot range");
}
ImGui::SameLine();
/* ── Navigation: pan ← → ── */
if (ImGui::SmallButton(ICON_FA_CHEVRON_LEFT "##hleft")) {
for (int i = 0; i < numPlotSlots(); i++) {
if (!liveFollow_[i]) {
double span = plotXMax_[i] - plotXMin_[i];
setPlotX(i, plotXMin_[i] - span * 0.75, plotXMax_[i] - span * 0.75);
}
}
}
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Pan left (75%% of window)"); }
ImGui::SameLine(0.f, 2.f);
if (ImGui::SmallButton(ICON_FA_CHEVRON_RIGHT "##hright")) {
for (int i = 0; i < numPlotSlots(); i++) {
if (!liveFollow_[i]) {
double span = plotXMax_[i] - plotXMin_[i];
double newMax = plotXMax_[i] + span * 0.75;
/* Clamp: don't go past wallNow */
if (newMax > wallNow) {
newMax = wallNow;
double newMin = newMax - span;
setPlotX(i, newMin, newMax);
} else {
setPlotX(i, plotXMin_[i] + span * 0.75, newMax);
}
}
}
}
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Pan right (75%% of window)"); }
ImGui::SameLine();
ImGui::TextDisabled("|"); ImGui::SameLine();
/* ── Jump presets ── */
static const double kJumpSec[] = { 10.0, 30.0, 60.0,
300.0, 600.0, 1800.0, 3600.0};
static const char* kJumpLabel[] = {"10 s", "30 s", "1 min",
"5 min","10 min","30 min","1 h"};
for (int j = 0; j < 7; j++) {
char jid[32];
snprintf(jid, sizeof(jid), "%s##hj%d", kJumpLabel[j], j);
if (ImGui::SmallButton(jid)) {
for (int i = 0; i < numPlotSlots(); i++) {
liveFollow_[i] = false;
double span = plotXMax_[i] - plotXMin_[i];
if (span <= 0.0) { span = windowSec_; }
setPlotX(i, wallNow - kJumpSec[j] - span,
wallNow - kJumpSec[j]);
}
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Jump to %s ago", kJumpLabel[j]);
}
if (j < 6) { ImGui::SameLine(0.f, 2.f); }
}
ImGui::SameLine();
/* ── All history ── */
bool canAll = historyInfo_.enabled && !historyInfo_.signals.empty();
if (!canAll) { ImGui::BeginDisabled(); }
if (ImGui::SmallButton("All")) {
double ht0 = 1e300, ht1 = -1e300;
for (const auto& hs : historyInfo_.signals) {
if (hs.t0 < ht0) ht0 = hs.t0;
if (hs.t1 > ht1) ht1 = hs.t1;
}
if (ht1 > ht0) {
for (int i = 0; i < numPlotSlots(); i++) {
liveFollow_[i] = false;
setPlotX(i, ht0, ht1);
zoomCache_[i].valid = false;
zoomCache_[i].pending = false;
histZoomCache_[i].valid = false;
histZoomCache_[i].pending = false;
zoomHist_[i].clear();
}
}
}
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Show all available history"); }
if (!canAll) { ImGui::EndDisabled(); }
ImGui::PopStyleVar();
ImGui::Separator();
@@ -521,6 +701,7 @@ void App::handleBinary(const uint8_t* data, size_t len) {
trigger_.status = "triggered";
trigger_.trigTime = capture_.trigTime;
trigger_.hasTrigTime = true;
resetTrigZoom();
}
return;
}
@@ -660,10 +841,24 @@ void App::onZoom(const std::string& json) {
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;
/* Compute the actual time range from the returned data so the
* coverage check in PlotPanel can distinguish between "ring had
* data for the full window" and "ring only had partial data". */
double dataT0 = 1e300, dataT1 = -1e300;
bool anyData = false;
for (const auto& zs : resp.signals) {
for (double tv : zs.t) {
if (tv < dataT0) { dataT0 = tv; }
if (tv > dataT1) { dataT1 = tv; }
anyData = true;
}
}
if (anyData) {
zc.signals = std::move(resp.signals);
zc.t0 = dataT0;
zc.t1 = dataT1;
zc.valid = true;
}
zc.pending = false;
return;
}
@@ -717,10 +912,22 @@ void App::onHistoryZoom(const std::string& json) {
for (int i = 0; i < kMaxPlotSlots; i++) {
auto& hc = histZoomCache_[i];
if (hc.pending && hc.reqId == resp.reqId) {
hc.signals = std::move(resp.signals);
hc.t0 = hc.reqT0;
hc.t1 = hc.reqT1;
hc.valid = true;
/* Compute the actual time range from returned data. */
double dataT0 = 1e300, dataT1 = -1e300;
bool anyData = false;
for (const auto& zs : resp.signals) {
for (double tv : zs.t) {
if (tv < dataT0) { dataT0 = tv; }
if (tv > dataT1) { dataT1 = tv; }
anyData = true;
}
}
if (anyData) {
hc.signals = std::move(resp.signals);
hc.t0 = dataT0;
hc.t1 = dataT1;
hc.valid = true;
}
hc.pending = false;
return;
}