Brings the Go hub and web SPA work developed on feature/udpscope onto main, without the udpscope client itself. The trigger engine could not capture a sporadic event: it armed on the live tail only, so a burst shorter than one push window was already past by the time the FSM looked for it. It now searches the ring history for the crossing, which also makes a capture reproducible from the same data rather than dependent on push timing (wshub/trigger.go, ringbuf.go, history.go). Adds CSV/JSON export of the visible window (wshub/export.go) and reworks the SPA: per-signal axis controls, a readable trigger panel, and a fix for the flicker caused by repainting on every push instead of on a frame tick (static/app.js, index.html, style.css). BUFFER_AND_TRIGGER.md documents the ring/decimation/trigger interaction, which is otherwise only inferable from the three files that implement it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
273 lines
14 KiB
HTML
273 lines
14 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>UDP Scope</title>
|
||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||
<link rel="stylesheet" href="/uPlot.min.css">
|
||
<link rel="stylesheet" href="/style.css">
|
||
<script src="/uPlot.iife.min.js"></script>
|
||
</head>
|
||
<body>
|
||
<!-- ── Top bar ───────────────────────────────────────────────── -->
|
||
<div id="topbar">
|
||
<button id="btn-sidebar" class="ctrl-btn active" title="Toggle sidebar">☰</button>
|
||
<span id="app-title">UDP Scope</span>
|
||
<div class="topbar-vsep"></div>
|
||
<button id="btn-layout" class="ctrl-btn layout-toggle" title="Select layout">⊞ 1×1 ▾</button>
|
||
<div class="topbar-sep"></div>
|
||
<div id="cursor-readout">
|
||
<span id="cur-ta">A: —</span><span class="cur-sep">│</span>
|
||
<span id="cur-tb">B: —</span><span class="cur-sep">│</span>
|
||
<span id="cur-dt">ΔT: —</span>
|
||
<span id="ruler-readout" style="display:none">
|
||
<span class="cur-sep">│</span>
|
||
<span id="cur-y1">Y1: —</span><span class="cur-sep">│</span>
|
||
<span id="cur-y2">Y2: —</span><span class="cur-sep">│</span>
|
||
<span id="cur-dy">ΔY: —</span>
|
||
</span>
|
||
</div>
|
||
<span class="ctrl-label" id="lbl-window">Window:</span>
|
||
<select id="window-select" class="ctrl-select">
|
||
<option value="1">1 s</option><option value="2">2 s</option>
|
||
<option value="5" selected>5 s</option><option value="10">10 s</option>
|
||
<option value="15">15 s</option><option value="30">30 s</option>
|
||
<option value="60">60 s</option><option value="120">2 min</option>
|
||
<option value="300">5 min</option><option value="600">10 min</option>
|
||
</select>
|
||
<button id="btn-cursor" class="ctrl-btn">Cursors</button>
|
||
<button id="btn-cursor-reset" class="ctrl-btn" style="display:none" title="Bring cursors A/B back into the visible window">↔ Reset</button>
|
||
<button id="btn-ruler" class="ctrl-btn" title="Horizontal value rulers">Rulers</button>
|
||
<button id="btn-zoom-back" class="ctrl-btn" style="display:none">← Back</button>
|
||
<button id="btn-zoom-fit" class="ctrl-btn">Fit</button>
|
||
<select id="export-select" class="ctrl-select" title="Export the visible signals">
|
||
<option value="" disabled selected>⬇ Export</option>
|
||
<option value="csv" title="Export the visible signals as CSV (decimated to a bounded row count)">CSV</option>
|
||
<option value="parquet" title="Export every stored sample (full resolution, no holes) as Parquet — requires the Go hub">Parquet</option>
|
||
</select>
|
||
<button id="btn-sync-resume" class="ctrl-btn resume-btn" style="display:none">↺ Auto</button>
|
||
<button id="btn-trigger" class="ctrl-btn">⚡ Trigger</button>
|
||
<button id="btn-pause-global" class="ctrl-btn">⏸ Pause</button>
|
||
<label class="ctrl-check" title="Snap jittery inter-frame timestamps to ideal spacing (eliminates overlaps/gaps from software-dispatch jitter)">
|
||
<input type="checkbox" id="cb-monotonic">
|
||
Sync TS
|
||
</label>
|
||
</div>
|
||
<!-- ── Trigger bar ───────────────────────────────────────────── -->
|
||
<div id="trigbar">
|
||
<div class="trig-group">
|
||
<span class="trig-label">Signal</span>
|
||
<select id="trig-signal" class="trig-select"><option value="">— none —</option></select>
|
||
</div>
|
||
<div class="trig-sep"></div>
|
||
<div class="trig-group">
|
||
<span class="trig-label">Edge</span>
|
||
<select id="trig-edge" class="trig-select">
|
||
<option value="rising">Rising ↑</option>
|
||
<option value="falling">Falling ↓</option>
|
||
<option value="both">Both ↕</option>
|
||
</select>
|
||
</div>
|
||
<div class="trig-sep"></div>
|
||
<div class="trig-group">
|
||
<span class="trig-label">Threshold</span>
|
||
<input id="trig-threshold" class="trig-input" type="number" value="0" step="any">
|
||
</div>
|
||
<div class="trig-sep"></div>
|
||
<div class="trig-group">
|
||
<span class="trig-label">Window</span>
|
||
<select id="trig-window" class="trig-select">
|
||
<option value="0.0001">100 μs</option><option value="0.0002">200 μs</option>
|
||
<option value="0.0005">500 μs</option><option value="0.001">1 ms</option>
|
||
<option value="0.002">2 ms</option><option value="0.005">5 ms</option>
|
||
<option value="0.01">10 ms</option><option value="0.02">20 ms</option>
|
||
<option value="0.05">50 ms</option><option value="0.1">100 ms</option>
|
||
<option value="0.2">200 ms</option><option value="0.5">500 ms</option>
|
||
<option value="1" selected>1 s</option><option value="2">2 s</option>
|
||
<option value="5">5 s</option><option value="10">10 s</option>
|
||
<option value="20">20 s</option><option value="30">30 s</option>
|
||
<option value="60">60 s</option>
|
||
<option value="120">2 m</option>
|
||
<option value="300">5 m</option>
|
||
<option value="600">10 m</option>
|
||
</select>
|
||
</div>
|
||
<div class="trig-sep"></div>
|
||
<div class="trig-group">
|
||
<span class="trig-label">Pre</span>
|
||
<input id="trig-pre" class="trig-range" type="range" min="0" max="100" value="20">
|
||
<span class="trig-range-val" id="trig-pre-val">20%</span>
|
||
</div>
|
||
<div class="trig-sep"></div>
|
||
<div class="trig-group">
|
||
<span class="trig-label" title="Re-arm delay after a capture — prevents double triggering">Holdoff</span>
|
||
<input id="trig-holdoff" class="trig-input" type="number" min="0" max="60" step="0.01" value="0.2">
|
||
<span class="trig-label">s</span>
|
||
</div>
|
||
<div class="trig-group">
|
||
<span class="trig-label">Mode</span>
|
||
<select id="trig-mode" class="trig-select">
|
||
<option value="normal">Normal</option>
|
||
<option value="single">Single</option>
|
||
</select>
|
||
</div>
|
||
<div class="trig-sep"></div>
|
||
<div class="trig-group" style="gap:8px">
|
||
<span id="trig-status-badge">IDLE</span>
|
||
<button id="btn-trig-force" title="Capture now, ignoring the threshold">Force</button>
|
||
<button id="btn-trig-stop" style="display:none">Stop</button>
|
||
<button id="btn-trig-rearm">Rearm</button>
|
||
</div>
|
||
</div>
|
||
<!-- ── Body ─────────────────────────────────────────────────── -->
|
||
<div id="body">
|
||
<div id="sidebar">
|
||
<div id="sidebar-header">Signals</div>
|
||
<div id="signal-list"></div>
|
||
</div>
|
||
<div id="main">
|
||
<div id="plot-grid" class="l1x1"></div>
|
||
</div>
|
||
</div>
|
||
<!-- ── Stats panel ─────────────────────────────────────────────── -->
|
||
<div id="stats-panel">
|
||
<div id="stats-panel-hdr">
|
||
<span class="stats-hdr-label">Source Statistics</span>
|
||
<select id="stats-source-sel" class="stats-source-sel"></select>
|
||
<button id="btn-stats-close">✕</button>
|
||
</div>
|
||
<div id="stats-body"></div>
|
||
</div>
|
||
<!-- ── Status bar ─────────────────────────────────────────────── -->
|
||
<div id="statusbar">
|
||
<div id="sb-left">
|
||
<div id="status-led"></div>
|
||
<span id="status-text">Disconnected</span>
|
||
<span id="sb-tsage"></span>
|
||
<button id="btn-stats" class="ctrl-btn" style="height:16px;padding:0 7px;font-size:10px;line-height:1">📊 Stats</button>
|
||
<button id="history-badge" style="display:none" title="Disk history — click to set the per-signal budget"></button>
|
||
</div>
|
||
<span id="build-version"></span>
|
||
</div>
|
||
<!-- ── History budget popup ──────────────────────────────────── -->
|
||
<div id="history-panel" style="display:none">
|
||
<div class="ctx-menu-header">Disk history budget</div>
|
||
<div class="ctx-row">
|
||
<label>Budget</label>
|
||
<input type="number" id="hist-budget" class="ctx-num" min="0.001" step="1">
|
||
<span class="ctx-range-val">MPts/signal</span>
|
||
</div>
|
||
<div class="hist-note">
|
||
The budget buys resolution, not duration: a signal too fast to store
|
||
sample-for-sample is archived as a min/max envelope wide enough to fit,
|
||
so the configured window is always covered.
|
||
</div>
|
||
<div id="hist-signal-res"></div>
|
||
<div class="hist-note hist-warn">Applying re-creates the history files — archived data is lost.</div>
|
||
<div class="ctx-row" style="margin:0;justify-content:flex-end">
|
||
<button class="ctx-btn" id="btn-hist-cancel">Cancel</button>
|
||
<button class="ctx-btn" id="btn-hist-apply">Apply</button>
|
||
</div>
|
||
</div>
|
||
<div id="layout-menu"></div>
|
||
<!-- ── Signal style context menu ─────────────────────────────── -->
|
||
<div id="sig-ctx-menu" style="display:none">
|
||
<div class="ctx-menu-header">Style:
|
||
<span id="ctx-menu-key" class="ctx-menu-key"></span></div>
|
||
<div class="ctx-row">
|
||
<label>Color</label>
|
||
<input type="color" id="ctx-color">
|
||
</div>
|
||
<div class="ctx-row">
|
||
<label>Width</label>
|
||
<div class="ctx-btns" id="ctx-width-btns">
|
||
<button class="ctx-btn" data-w="1">1px</button>
|
||
<button class="ctx-btn active" data-w="1.5">1.5</button>
|
||
<button class="ctx-btn" data-w="2">2px</button>
|
||
<button class="ctx-btn" data-w="3">3px</button>
|
||
</div>
|
||
</div>
|
||
<div class="ctx-row">
|
||
<label>Line</label>
|
||
<div class="ctx-btns" id="ctx-dash-btns">
|
||
<button class="ctx-btn active" data-dash="solid">——</button>
|
||
<button class="ctx-btn" data-dash="dashed">╌╌</button>
|
||
<button class="ctx-btn" data-dash="dotted">·····</button>
|
||
</div>
|
||
</div>
|
||
<div class="ctx-row">
|
||
<label>Marker</label>
|
||
<div class="ctx-btns" id="ctx-marker-btns">
|
||
<button class="ctx-btn active" data-marker="none">none</button>
|
||
<button class="ctx-btn" data-marker="circle">●</button>
|
||
<button class="ctx-btn" data-marker="square">■</button>
|
||
<button class="ctx-btn" data-marker="cross">✕</button>
|
||
<button class="ctx-btn" data-marker="diamond">◆</button>
|
||
</div>
|
||
</div>
|
||
<div class="ctx-row">
|
||
<label>Size</label>
|
||
<input type="range" class="ctx-range" id="ctx-marker-size" min="2" max="10" value="4">
|
||
<span class="ctx-range-val" id="ctx-marker-size-val">4px</span>
|
||
</div>
|
||
</div>
|
||
<!-- ── Array index picker (trigger signal) ──────────────────────── -->
|
||
<div id="array-idx-picker" style="display:none">
|
||
<div class="ctx-menu-header">Element index:
|
||
<span id="aip-sig" class="ctx-menu-key"></span></div>
|
||
<div class="ctx-row">
|
||
<label>Index</label>
|
||
<input type="number" id="aip-idx" class="ctx-num" min="0" step="1" value="0">
|
||
<span id="aip-range" style="font-size:10px;color:var(--overlay0)"></span>
|
||
</div>
|
||
<div class="ctx-row" style="justify-content:flex-end;gap:6px">
|
||
<button class="ctx-btn" id="aip-cancel">Cancel</button>
|
||
<button class="ctx-btn active" id="aip-ok">OK</button>
|
||
</div>
|
||
</div>
|
||
<!-- ── VScale toolbar (moved into plot card when active) ─────────── -->
|
||
<div id="vscale-menu" style="display:none">
|
||
<div class="vstb-header">
|
||
<span class="vstb-label"><span id="vscale-menu-title">V-Scale</span>:
|
||
<span id="vscale-menu-key" class="ctx-menu-key"></span></span>
|
||
<div class="ctx-btns" id="vscale-mode-btns">
|
||
<button class="ctx-btn active" data-mode="auto">Auto</button>
|
||
<button class="ctx-btn" data-mode="range">Range</button>
|
||
<button class="ctx-btn" data-mode="manual">Manual</button>
|
||
</div>
|
||
<div id="vscale-manual-row" style="display:none;align-items:center;gap:4px">
|
||
<label class="vstb-lbl">V/div</label>
|
||
<input type="number" id="vscale-vdiv" class="ctx-num" min="1e-30" step="any" value="1">
|
||
</div>
|
||
<div id="vscale-offset-row" style="display:none;align-items:center;gap:4px">
|
||
<label class="vstb-lbl" title="Raw value at screen centre — unbounded, may lie outside the plotted range">Offset</label>
|
||
<input type="number" id="vscale-offset" class="ctx-num" step="any" value="0">
|
||
</div>
|
||
<div id="vscale-type-row" style="display:none;align-items:center;gap:4px">
|
||
<label class="vstb-lbl">Type</label>
|
||
<div class="ctx-btns" id="vscale-type-btns">
|
||
<button class="ctx-btn active" data-type="analog">Analog</button>
|
||
<button class="ctx-btn" data-type="digital">Digital</button>
|
||
</div>
|
||
</div>
|
||
<div class="vstb-sep"></div>
|
||
<div id="vscale-cal-row" style="display:flex;align-items:center;gap:4px">
|
||
<label class="vstb-lbl" id="vscale-cal-lbl" title="Data calibration: value = raw × Scale + Offset. Applies to the plot, cursors, hover readout, CSV export and trigger threshold.">Cal</label>
|
||
<label class="vstb-lbl">Scale</label>
|
||
<input type="number" id="vscale-cal-scale" class="ctx-num ctx-num-sm" step="any" value="1">
|
||
<label class="vstb-lbl">Offset</label>
|
||
<input type="number" id="vscale-cal-offset" class="ctx-num ctx-num-sm" step="any" value="0">
|
||
<label class="vstb-lbl">Unit</label>
|
||
<input type="text" id="vscale-cal-unit" class="ctx-num ctx-num-xs" placeholder="—">
|
||
<button class="ctx-btn" id="btn-cal-reset" title="Clear this signal's calibration">Reset</button>
|
||
</div>
|
||
<button id="btn-vscale-close" class="vstb-close" title="Close">✕</button>
|
||
</div>
|
||
</div>
|
||
<!-- Follows the mouse over a plot: time + per-trace values. -->
|
||
<div id="hover-readout" style="display:none"></div>
|
||
<script src="/calibration.js"></script>
|
||
<script src="/app.js"></script>
|
||
</body>
|
||
</html> |