Implemented client datasource
This commit is contained in:
+322
-115
@@ -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;
|
||||
}
|
||||
|
||||
+13
-3
@@ -26,7 +26,7 @@ namespace StreamHubClient {
|
||||
/** One signal as known to the client. */
|
||||
struct Signal {
|
||||
SignalMeta meta;
|
||||
SignalBuffer buf{20000};
|
||||
SignalBuffer buf{1000000};
|
||||
ImVec4 color{0.4f, 0.8f, 1.0f, 1.0f};
|
||||
float lineWidth = 1.5f;
|
||||
int marker = -1; /* ImPlotMarker (-1 = none) */
|
||||
@@ -244,10 +244,15 @@ public:
|
||||
/** @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;
|
||||
@@ -270,8 +275,8 @@ private:
|
||||
void onMaxPointsUpdated(const std::string& json);
|
||||
|
||||
/* ---- Rendering ------------------------------------------------------ */
|
||||
void renderMenuBar();
|
||||
void renderToolbar();
|
||||
void renderHistoryBar();
|
||||
void renderTriggerBar();
|
||||
void renderSidebar();
|
||||
void renderPlotGrid();
|
||||
@@ -285,7 +290,7 @@ private:
|
||||
|
||||
TriggerState trigger_;
|
||||
PlotLayout layout_ = PlotLayout::kLayout1x1;
|
||||
uint32_t maxPoints_ = 20000u;
|
||||
uint32_t maxPoints_ = 1000000u;
|
||||
bool globalPaused_ = false;
|
||||
|
||||
/* Plot slots: one vector of assignments per plot panel */
|
||||
@@ -304,6 +309,9 @@ private:
|
||||
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];
|
||||
|
||||
@@ -315,11 +323,13 @@ private:
|
||||
|
||||
/* 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) */
|
||||
|
||||
+129
-54
@@ -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);
|
||||
|
||||
@@ -553,9 +553,8 @@ bool ParseHistoryInfo(const std::string& json, HistoryInfoMsg& out) {
|
||||
out.signals.clear();
|
||||
out.enabled = false;
|
||||
|
||||
char tmp[16] = "";
|
||||
jsonGetStr(json.c_str(), "enabled", tmp, sizeof(tmp));
|
||||
out.enabled = (strcmp(tmp, "true") == 0);
|
||||
/* "enabled" is a JSON boolean (unquoted true/false), not a string */
|
||||
out.enabled = (strstr(json.c_str(), "\"enabled\":true") != nullptr);
|
||||
|
||||
double df = 0.0;
|
||||
jsonGetDouble(json.c_str(), "durationHours", df);
|
||||
|
||||
@@ -100,6 +100,18 @@ void RenderSourcePanel(App& app) {
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
/* ── Add / Save sources (inline at the bottom of the sidebar) ──────── */
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::Button(ICON_FA_PLUS " Add Source", ImVec2(-1.f, 0.f))) {
|
||||
app.showAddSrc() = true;
|
||||
}
|
||||
if (ImGui::Button(ICON_FA_FLOPPY_DISK " Save Sources", ImVec2(-1.f, 0.f))) {
|
||||
app.ws().sendText(BuildSaveSources());
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace StreamHubClient */
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
* The trigger runs in the C++ StreamHub: this panel only edits the config
|
||||
* (signal/edge/threshold/window/pre%/mode), sends setTrigger + arm/disarm/
|
||||
* rearm/trigStop commands, and reflects the hub triggerState broadcasts.
|
||||
*
|
||||
* Single-row compact layout: all controls fit on one line on a ≥1280 px window;
|
||||
* ImGui wraps naturally on narrower screens without a scrollbar.
|
||||
*/
|
||||
|
||||
#include "TriggerPanel.h"
|
||||
@@ -22,12 +25,10 @@ namespace StreamHubClient {
|
||||
|
||||
/* Window presets (mirrors the web UI: 100 µs .. 10 s) */
|
||||
static const double kWinVals[] = {1e-4, 1e-3, 1e-2, 1e-1, 1.0, 10.0};
|
||||
static const char* kWinLabels[] = {"100 µs", "1 ms", "10 ms", "100 ms", "1 s", "10 s"};
|
||||
static const char* kWinLabels[] = {"100 us", "1 ms", "10 ms", "100 ms", "1 s", "10 s"};
|
||||
static const int kNumWins = 6;
|
||||
|
||||
static const char* kEdgeLabels[] = {ICON_FA_ARROW_TREND_UP " Rising",
|
||||
ICON_FA_ARROW_TREND_DOWN " Falling",
|
||||
ICON_FA_ARROWS_UP_DOWN " Both"};
|
||||
static const char* kEdgeLabels[] = {"Rising", "Falling", "Both"};
|
||||
static const char* kEdgeWire[] = {"rising", "falling", "both"};
|
||||
|
||||
/* Send the current config to the hub. */
|
||||
@@ -44,20 +45,14 @@ void RenderTriggerPanel(App& app) {
|
||||
auto& trig = app.trigger();
|
||||
auto& sources = app.sources();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.15f, 0.15f, 0.20f, 1.f));
|
||||
ImGui::BeginChild("##trigbar", ImVec2(0.f, 90.f), true);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.f, 4.f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 4.f));
|
||||
|
||||
bool cfgChanged = false;
|
||||
|
||||
/* ── Signal selector (full keys "src:sig") ─────────────────────────── *
|
||||
* Vector signals are flattened time-series on the hub: one key per
|
||||
* signal; the trigger engine checks every decoded sample of it. */
|
||||
ImGui::SetNextItemWidth(180.f);
|
||||
if (ImGui::BeginCombo("Signal##trig", trig.signalKey.empty()
|
||||
? "<select>" : trig.signalKey.c_str())) {
|
||||
/* ── Signal selector ──────────────────────────────────────────────── */
|
||||
ImGui::SetNextItemWidth(150.f);
|
||||
if (ImGui::BeginCombo("##trigsig", trig.signalKey.empty()
|
||||
? "<signal>" : trig.signalKey.c_str())) {
|
||||
for (const auto& src : sources) {
|
||||
for (const auto& sig : src.signals) {
|
||||
char key[192];
|
||||
@@ -76,14 +71,14 @@ void RenderTriggerPanel(App& app) {
|
||||
ImGui::SameLine();
|
||||
|
||||
/* ── Edge ─────────────────────────────────────────────────────────── */
|
||||
ImGui::SetNextItemWidth(95.f);
|
||||
if (ImGui::Combo("Edge##trig", &trig.edge, kEdgeLabels, 3)) {
|
||||
ImGui::SetNextItemWidth(70.f);
|
||||
if (ImGui::Combo("##trigedge", &trig.edge, kEdgeLabels, 3)) {
|
||||
cfgChanged = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
/* ── Threshold ───────────────────────────────────────────────────── */
|
||||
ImGui::SetNextItemWidth(80.f);
|
||||
ImGui::SetNextItemWidth(65.f);
|
||||
if (ImGui::InputDouble("Thr##trig", &trig.threshold, 0.0, 0.0, "%.4g",
|
||||
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
cfgChanged = true;
|
||||
@@ -97,7 +92,7 @@ void RenderTriggerPanel(App& app) {
|
||||
winIdx = w; break;
|
||||
}
|
||||
}
|
||||
ImGui::SetNextItemWidth(85.f);
|
||||
ImGui::SetNextItemWidth(75.f);
|
||||
if (ImGui::Combo("Win##trig", &winIdx, kWinLabels, kNumWins)) {
|
||||
trig.windowSec = kWinVals[winIdx];
|
||||
cfgChanged = true;
|
||||
@@ -105,24 +100,21 @@ void RenderTriggerPanel(App& app) {
|
||||
ImGui::SameLine();
|
||||
|
||||
/* ── Pre-trigger % ───────────────────────────────────────────────── */
|
||||
ImGui::SetNextItemWidth(100.f);
|
||||
ImGui::SetNextItemWidth(70.f);
|
||||
float preP = static_cast<float>(trig.prePercent);
|
||||
if (ImGui::SliderFloat("Pre%##trig", &preP, 0.f, 100.f, "%.0f%%")) {
|
||||
if (ImGui::SliderFloat("Pre##trig", &preP, 0.f, 100.f, "%.0f%%")) {
|
||||
trig.prePercent = static_cast<double>(preP);
|
||||
}
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) { cfgChanged = true; }
|
||||
ImGui::SameLine();
|
||||
|
||||
/* ── Mode ────────────────────────────────────────────────────────── */
|
||||
ImGui::TextUnformatted("Mode:");
|
||||
ImGui::SameLine();
|
||||
int singleInt = trig.single ? 1 : 0;
|
||||
if (ImGui::RadioButton("Normal", &singleInt, 0)) { cfgChanged = true; }
|
||||
if (ImGui::RadioButton("Norm##trig", &singleInt, 0)) { cfgChanged = true; }
|
||||
ImGui::SameLine();
|
||||
if (ImGui::RadioButton("Single", &singleInt, 1)) { cfgChanged = true; }
|
||||
if (ImGui::RadioButton("1x##trig", &singleInt, 1)) { cfgChanged = true; }
|
||||
trig.single = (singleInt != 0);
|
||||
|
||||
ImGui::NewLine();
|
||||
ImGui::SameLine(0.f, 10.f);
|
||||
|
||||
/* ── Status badge ────────────────────────────────────────────────── */
|
||||
ImVec4 badgeColor;
|
||||
@@ -132,44 +124,38 @@ void RenderTriggerPanel(App& app) {
|
||||
else if (trig.status == "triggered") { badgeColor = ImVec4(0.2f,0.9f,0.3f,1.f); badgeText = "TRIGGERED"; }
|
||||
else { badgeColor = ImVec4(0.6f,0.6f,0.6f,1.f); badgeText = "IDLE"; }
|
||||
ImGui::TextColored(badgeColor, "[%s]", badgeText);
|
||||
if (trig.hasTrigTime &&
|
||||
(trig.status == "collecting" || trig.status == "triggered")) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("t=%.6f", trig.trigTime);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
/* ── Arm / Disarm / Rearm / Stop ─────────────────────────────────── */
|
||||
/* ── Arm / Disarm / Stop ────────────────────────────────────────── */
|
||||
bool canArm = !trig.signalKey.empty();
|
||||
if (!canArm) { ImGui::BeginDisabled(); }
|
||||
if (ImGui::Button(ICON_FA_BOLT " Arm")) {
|
||||
if (ImGui::SmallButton(ICON_FA_BOLT " Arm")) {
|
||||
sendTrigConfig(app);
|
||||
app.ws().sendText(BuildArm());
|
||||
}
|
||||
if (!canArm) { ImGui::EndDisabled(); }
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button("Disarm")) {
|
||||
if (ImGui::SmallButton("Disarm")) {
|
||||
app.ws().sendText(BuildDisarm());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
if (trig.single && trig.status == "triggered") {
|
||||
if (ImGui::Button("Rearm")) {
|
||||
app.ws().sendText(BuildRearm());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
if (!trig.single) {
|
||||
if (ImGui::Button(trig.stopped ? ICON_FA_PLAY " Run" : ICON_FA_STOP " Stop")) {
|
||||
if (ImGui::SmallButton(trig.stopped ? ICON_FA_PLAY " Run" : ICON_FA_STOP " Stop")) {
|
||||
trig.stopped = !trig.stopped;
|
||||
app.ws().sendText(BuildTrigStop(trig.stopped));
|
||||
}
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
ImGui::NewLine();
|
||||
/* Trigger time readout (inline) */
|
||||
if (trig.hasTrigTime &&
|
||||
(trig.status == "collecting" || trig.status == "triggered")) {
|
||||
ImGui::TextDisabled("t=%.6f", trig.trigTime);
|
||||
} else {
|
||||
ImGui::NewLine();
|
||||
}
|
||||
|
||||
/* Config edits while armed take effect immediately on the hub */
|
||||
if (cfgChanged && !trig.signalKey.empty()) {
|
||||
@@ -177,7 +163,6 @@ void RenderTriggerPanel(App& app) {
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::EndChild();
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user