/** * @file TriggerPanel.cpp * @brief Trigger configuration bar — hub-side trigger semantics. * * 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" #include "App.h" #include "Protocol.h" #include "imgui.h" #include "Icons.h" #include #include #include 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 us", "1 ms", "10 ms", "100 ms", "1 s", "10 s"}; static const int kNumWins = 6; static const char* kEdgeLabels[] = {"Rising", "Falling", "Both"}; static const char* kEdgeWire[] = {"rising", "falling", "both"}; /* Send the current config to the hub. */ static void sendTrigConfig(App& app) { auto& trig = app.trigger(); if (trig.signalKey.empty()) { return; } app.ws().sendText(BuildSetTrigger( trig.signalKey, kEdgeWire[trig.edge], trig.threshold, trig.windowSec, trig.prePercent, trig.single ? "single" : "normal")); } void RenderTriggerPanel(App& app) { auto& trig = app.trigger(); auto& sources = app.sources(); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 4.f)); bool cfgChanged = false; /* ── Signal selector ──────────────────────────────────────────────── */ ImGui::SetNextItemWidth(150.f); if (ImGui::BeginCombo("##trigsig", trig.signalKey.empty() ? "" : trig.signalKey.c_str())) { for (const auto& src : sources) { for (const auto& sig : src.signals) { char key[192]; snprintf(key, sizeof(key), "%s:%s", src.id.c_str(), sig.meta.name.c_str()); bool sel = (trig.signalKey == key); if (ImGui::Selectable(key, sel)) { trig.signalKey = key; cfgChanged = true; } if (sel) { ImGui::SetItemDefaultFocus(); } } } ImGui::EndCombo(); } ImGui::SameLine(); /* ── Edge ─────────────────────────────────────────────────────────── */ ImGui::SetNextItemWidth(70.f); if (ImGui::Combo("##trigedge", &trig.edge, kEdgeLabels, 3)) { cfgChanged = true; } ImGui::SameLine(); /* ── Threshold ───────────────────────────────────────────────────── */ ImGui::SetNextItemWidth(65.f); if (ImGui::InputDouble("Thr##trig", &trig.threshold, 0.0, 0.0, "%.4g", ImGuiInputTextFlags_EnterReturnsTrue)) { cfgChanged = true; } ImGui::SameLine(); /* ── Window ──────────────────────────────────────────────────────── */ int winIdx = 3; /* default 100 ms */ for (int w = 0; w < kNumWins; w++) { if (std::fabs(trig.windowSec - kWinVals[w]) < kWinVals[w] * 1e-6) { winIdx = w; break; } } ImGui::SetNextItemWidth(75.f); if (ImGui::Combo("Win##trig", &winIdx, kWinLabels, kNumWins)) { trig.windowSec = kWinVals[winIdx]; cfgChanged = true; } ImGui::SameLine(); /* ── Pre-trigger % ───────────────────────────────────────────────── */ ImGui::SetNextItemWidth(70.f); float preP = static_cast(trig.prePercent); if (ImGui::SliderFloat("Pre##trig", &preP, 0.f, 100.f, "%.0f%%")) { trig.prePercent = static_cast(preP); } if (ImGui::IsItemDeactivatedAfterEdit()) { cfgChanged = true; } ImGui::SameLine(); /* ── Mode ────────────────────────────────────────────────────────── */ int singleInt = trig.single ? 1 : 0; if (ImGui::RadioButton("Norm##trig", &singleInt, 0)) { cfgChanged = true; } ImGui::SameLine(); if (ImGui::RadioButton("1x##trig", &singleInt, 1)) { cfgChanged = true; } trig.single = (singleInt != 0); ImGui::SameLine(0.f, 10.f); /* ── Status badge ────────────────────────────────────────────────── */ ImVec4 badgeColor; const char* badgeText; if (trig.status == "armed") { badgeColor = ImVec4(1.f,0.8f,0.f,1.f); badgeText = "ARMED"; } else if (trig.status == "collecting") { badgeColor = ImVec4(0.4f,0.7f,1.f,1.f); badgeText = "COLLECTING"; } 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); ImGui::SameLine(); /* ── Arm / Disarm / Stop ────────────────────────────────────────── */ bool canArm = !trig.signalKey.empty(); if (!canArm) { ImGui::BeginDisabled(); } if (ImGui::SmallButton(ICON_FA_BOLT " Arm")) { sendTrigConfig(app); app.ws().sendText(BuildArm()); } if (!canArm) { ImGui::EndDisabled(); } ImGui::SameLine(); if (ImGui::SmallButton("Disarm")) { app.ws().sendText(BuildDisarm()); } ImGui::SameLine(); if (!trig.single) { 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(); } /* 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()) { sendTrigConfig(app); } ImGui::PopStyleVar(); ImGui::Separator(); } } /* namespace StreamHubClient */