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
+129 -54
View File
@@ -39,8 +39,10 @@ struct DragPayload { int srcIdx; int sigIdx; };
static const int kPlotSlotsMax = 8; /* mirrors App::kMaxPlotSlots */
/* Live windows at or below this width refresh via hub hi-res zoom fetches
* instead of (in addition to) the decimated push stream. */
static const double kLiveHiResMaxWin = 2.0; /* seconds */
* instead of (in addition to) the decimated push stream. Wide windows
* benefit too: LTTB on the server ring gives much better coverage than
* the client's accumulated push-stream data. */
static const double kLiveHiResMaxWin = 600.0; /* seconds (effectively always) */
/* ── Helpers ─────────────────────────────────────────────────────────────── */
@@ -198,18 +200,30 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
(zc.t1 - zc.t0) >= app.windowSec() * 0.9 &&
(wallNow - zc.t1) < 3.0;
const bool useZoomData = !trigView && zc.valid &&
const bool useZoomData = !trigView && !paused && zc.valid &&
(liveHiRes ||
(!live &&
zc.t0 <= app.plotXMin(plotIdx) + 1e-9 &&
zc.t1 >= app.plotXMax(plotIdx) - 1e-9));
/* History zoom cache (disk-backed, for ranges beyond in-memory ring) */
/* History zoom cache (disk-backed, for ranges beyond in-memory ring).
* Zoom responses carry the *requested* t0/t1 (not actual data bounds),
* so a zoom cache can report coverage even when the ring had no data.
* Prefer history over zoom when the in-memory ring cannot span the full
* visible window — that is the typical history-browse case. */
auto& hc = app.histZoomCache(plotIdx);
const bool useHistData = !trigView && !live && !useZoomData &&
hc.valid &&
hc.t0 <= app.plotXMin(plotIdx) + 1e-9 &&
hc.t1 >= app.plotXMax(plotIdx) - 1e-9;
const bool haveHistCover = hc.valid &&
hc.t0 <= app.plotXMin(plotIdx) + 1e-9 &&
hc.t1 >= app.plotXMax(plotIdx) - 1e-9;
bool useHistData = !trigView && !paused && !live && haveHistCover;
if (useHistData) {
/* Check that at least one signal has actual data points */
bool anyData = false;
for (const auto& hs : hc.signals) {
if (!hs.t.empty()) { anyData = true; break; }
}
if (!anyData) { useHistData = false; }
}
/* Fill tStore/vStore[si] from the frozen view (paused) or the live ring */
auto readBase = [&](size_t si, const Signal& sig, const std::string& key) {
@@ -369,8 +383,8 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
ImGui::PopStyleColor(2);
}
/* Back / Fit (zoom history) — only meaningful when not live */
if (!live && !trigView) {
/* Back / Fit / Reset (zoom history) */
if (!live || (trigView && app.trigZoomed(plotIdx))) {
ImGui::SameLine();
auto& hist = app.zoomHist(plotIdx);
if (hist.empty()) { ImGui::BeginDisabled(); }
@@ -380,16 +394,24 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
}
if (hist.empty()) { ImGui::EndDisabled(); }
ImGui::SameLine();
if (ImGui::SmallButton(ICON_FA_EXPAND " Fit##zf")) {
double mn = 1e300, mx = -1e300;
for (size_t si = 0; si < slots.size(); si++) {
if (tStore[si].empty()) continue;
mn = std::min(mn, tStore[si].front());
mx = std::max(mx, tStore[si].back());
if (trigView) {
/* Reset to full capture window */
if (ImGui::SmallButton(ICON_FA_EXPAND " Reset##zr")) {
app.trigZoomed(plotIdx) = false;
app.zoomHist(plotIdx).clear();
}
if (mx > mn) {
app.pushZoomHist(plotIdx);
app.setPlotX(plotIdx, mn, mx);
} else {
if (ImGui::SmallButton(ICON_FA_EXPAND " Fit##zf")) {
double mn = 1e300, mx = -1e300;
for (size_t si = 0; si < slots.size(); si++) {
if (tStore[si].empty()) continue;
mn = std::min(mn, tStore[si].front());
mx = std::max(mx, tStore[si].back());
}
if (mx > mn) {
app.pushZoomHist(plotIdx);
app.setPlotX(plotIdx, mn, mx);
}
}
}
}
@@ -401,13 +423,23 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
ICON_FA_CLOCK_ROTATE_LEFT " HIST");
}
/* Norm mode + cursors toggles */
/* Norm/Dig/Mix mode compact toggle buttons matching SmallButton height */
ImGui::SameLine();
{
static const char* kVModes[] = {"Norm", "Dig", "Mix"};
ImGui::SetNextItemWidth(58.f);
char vmId[16]; snprintf(vmId, sizeof(vmId), "##vm%d", plotIdx);
ImGui::Combo(vmId, &vMode, kVModes, 3);
static const char* kVLabels[] = {"N", "D", "M"};
static const char* kVTooltips[] = {"Normal", "Digital", "Mixed"};
for (int vm = 0; vm < 3; vm++) {
char vmId[16]; snprintf(vmId, sizeof(vmId), "%s##vm%d_%d", kVLabels[vm], plotIdx, vm);
bool sel = (vMode == vm);
if (sel) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.537f,0.706f,0.980f,1.f));
}
if (ImGui::SmallButton(vmId)) { vMode = vm; }
if (sel) { ImGui::PopStyleColor(2); }
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", kVTooltips[vm]); }
if (vm < 2) { ImGui::SameLine(0.f, 1.f); }
}
}
/* ── VScale toolbar (shown when an active signal is selected) ───────── */
@@ -500,10 +532,17 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
/* Y axis always ±4 div space */
ImPlot::SetupAxisLimits(ImAxis_Y1, -4.05, 4.05, ImGuiCond_Always);
/* X axis: trig view → capture window; live → wall clock; else stored */
/* X axis: trig view → capture window (zoomable); live → wall clock; else stored */
double xMin, xMax;
bool& trigZm = app.trigZoomed(plotIdx);
if (trigView) {
xMin = -cap->preSec; xMax = cap->postSec;
if (trigZm) {
xMin = app.plotXMin(plotIdx);
xMax = app.plotXMax(plotIdx);
} else {
xMin = -cap->preSec;
xMax = cap->postSec;
}
} else if (live && !paused) {
if (liveHiRes) {
/* Anchor to the latest fetched hi-res slice so the trace
@@ -574,43 +613,54 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
/* Zoom-history debounce: coalesce rapid scroll steps into one entry */
static double lastHistPush[kPlotSlotsMax] = {};
if (overPlot && !trigView) {
if (overPlot) {
ImGuiIO& io2 = ImGui::GetIO();
const float wheel = io2.MouseWheel;
const bool ctrl = io2.KeyCtrl;
const bool shift = io2.KeyShift;
const double now = ImGui::GetTime();
/* Helper: enter zoomed mode for trigger view (seed from capture window) */
auto enterTrigZoom = [&]() {
if (trigView && !trigZm) {
app.setPlotX(plotIdx, -cap->preSec, cap->postSec);
trigZm = true;
}
};
/* Helper: X-zoom the stored range by factor around center */
auto xZoomStored = [&](double factor) {
if (trigView) { enterTrigZoom(); }
if (now - lastHistPush[plotIdx] > 0.6) {
app.pushZoomHist(plotIdx);
lastHistPush[plotIdx] = now;
}
double cx = (app.plotXMin(plotIdx) + app.plotXMax(plotIdx)) * 0.5;
double half = (app.plotXMax(plotIdx) - app.plotXMin(plotIdx)) * 0.5 * factor;
app.setPlotX(plotIdx, cx - half, cx + half);
};
if (wheel != 0.f) {
/* Zoom factor: scroll up = zoom in (×0.8), down = zoom out (×1.25) */
const double zoomIn = 0.8;
const double zoomOut = 1.25;
double factor = (wheel > 0.f) ? zoomIn : zoomOut;
if (ctrl) {
/* ── X zoom ─────────────────────────────────────────── */
if (live) {
if (!trigView && live) {
app.setWindowSec(app.windowSec() * factor);
} else {
if (now - lastHistPush[plotIdx] > 0.6) {
app.pushZoomHist(plotIdx);
lastHistPush[plotIdx] = now;
}
double cx = (app.plotXMin(plotIdx) + app.plotXMax(plotIdx)) * 0.5;
double half = (app.plotXMax(plotIdx) - app.plotXMin(plotIdx)) * 0.5 * factor;
app.setPlotX(plotIdx, cx - half, cx + half);
xZoomStored(factor);
}
} else if (shift) {
/* ── Y offset of active signal ───────────────────────── */
if (actSlot >= 0 && actSlot < (int)slots.size()) {
auto& a = slots[actSlot];
/* Switch to manual so position is retained */
if (a.vs.mode != 2) {
a.vs.divValue = std::max(a.vs.resolvedDiv, 1e-30);
a.vs.offset = a.vs.resolvedOffset;
a.vs.mode = 2;
}
/* Each scroll step moves by 0.5 screen divisions */
a.vs.screenPos += (wheel > 0.f) ? 0.5 : -0.5;
}
} else {
@@ -624,27 +674,21 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
}
a.vs.divValue = std::max(a.vs.divValue * factor, 1e-30);
} else {
/* No active signal: plain scroll falls back to X zoom
* so the wheel always does something useful. */
if (live) {
/* No active signal: plain scroll X zoom */
if (!trigView && live) {
app.setWindowSec(app.windowSec() * factor);
} else {
if (now - lastHistPush[plotIdx] > 0.6) {
app.pushZoomHist(plotIdx);
lastHistPush[plotIdx] = now;
}
double cx = (app.plotXMin(plotIdx) + app.plotXMax(plotIdx)) * 0.5;
double half = (app.plotXMax(plotIdx) - app.plotXMin(plotIdx)) * 0.5 * factor;
app.setPlotX(plotIdx, cx - half, cx + half);
xZoomStored(factor);
}
}
}
}
/* Right-drag → X pan (non-live). Transition live→non-live on drag start. */
/* Right-drag → X pan. Transition live→non-live on drag start;
* in trigger view, enter trigger-zoom mode. */
if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) {
if (live) {
/* Seed stored range from current live window */
if (trigView) { enterTrigZoom(); }
if (!trigView && live) {
app.initPlotX(plotIdx, wallNow);
live = false;
lastHistPush[plotIdx] = now;
@@ -662,8 +706,8 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
}
}
/* ── Hi-res WS zoom requests ────────────────────────────────────── */
if (!trigView) {
/* ── Hi-res WS zoom requests (suppressed while paused) ──────────── */
if (!trigView && !paused) {
std::string csv;
for (const auto& a : slots) {
std::string k = app.slotKey(a);
@@ -714,6 +758,28 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
}
}
/* ── Clip data to visible X range before LTTB ─────────────────── *
* The ring buffer may hold far more data than the visible window.
* LTTB must operate only on the visible slice so it picks the best
* 2400 points for what's actually on screen. */
if (xMax > xMin) {
for (size_t si = 0; si < slots.size(); si++) {
auto& tv = tStore[si];
auto& vv = vStore[si];
if (tv.empty()) continue;
auto lo = std::lower_bound(tv.begin(), tv.end(), xMin);
auto hi = std::upper_bound(lo, tv.end(), xMax);
if (lo != tv.begin()) --lo; /* keep one sample before window */
if (hi != tv.end()) ++hi; /* keep one sample after window */
size_t i0 = (size_t)(lo - tv.begin());
size_t i1 = (size_t)(hi - tv.begin());
if (i0 > 0 || i1 < tv.size()) {
tv = std::vector<double>(tv.begin() + i0, tv.begin() + i1);
vv = std::vector<double>(vv.begin() + i0, vv.begin() + i1);
}
}
}
/* ── Plot each signal ──────────────────────────────────────────── */
static const size_t kMaxPush = 2400;
static thread_local std::vector<double> tDec, vDec, vNorm;
@@ -762,8 +828,17 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
1.5f, ImPlotDragToolFlags_NoInputs);
}
/* Cursors A/B — global: same position on every plot, drag anywhere */
/* Cursors A/B — global: same position on every plot, drag anywhere.
* Re-seed to visible range if both cursors are outside the view
* (e.g. switching between live and trigger mode). */
if (app.cursorsOn()) {
bool aBeyond = (app.cursorA() < xMin || app.cursorA() > xMax);
bool bBeyond = (app.cursorB() < xMin || app.cursorB() > xMax);
if (aBeyond && bBeyond) {
double range = xMax - xMin;
app.cursorA() = xMin + range * 0.25;
app.cursorB() = xMin + range * 0.75;
}
static const ImVec4 kCursA{0.980f,0.702f,0.529f,0.9f};
static const ImVec4 kCursB{0.796f,0.651f,0.969f,0.9f};
ImPlot::DragLineX(910, &app.cursorA(), kCursA, 1.2f);