# udpscope — user guide A viewer for a UDPStreamer source: it connects, decodes the CONFIG, plots the signals you enable, and lets you freeze, measure and derive from them. This is the *user* guide; `README.md` documents what was built and why, and `SPECS.md` is the requirement document. ![the main window](img/main-window.png) *The main window with a source streaming: the signal panel on the right, the plot in the middle, the status line directly under it, and the toolbar above. The window control — `2 s` — is the first thing on the left.* ## 1. Connecting ```sh udpscope # asks which source to open udpscope --server 10.0.0.5 --port 8000 # connect straight away udpscope --server 239.1.2.3 --port 8000 --multicast 239.1.2.3 --interface 10.0.0.9 ``` With no `--server` the app shows a chooser: the sessions it has stored, each naming its source, plus a form for a new host. A source opened from the list reconnects to the multicast group and interface it was stored with. **A unicast source serves one client at a time.** A second viewer's `CONNECT` displaces the first, and they take turns — the status line says so when it happens. Watching one source from several instances needs `--multicast ` with `--interface `. Every launch is its own process, so running two scopes side by side is fine. ## 2. The status line Directly under the plot, it reads `● · · `, and the ⓘ beside it opens the detail: packets, frames, gaps, per-channel sample counts, the buffer's span and its decimation. - **The state word is one word** from a fixed set: `live`, `paused`, `armed`, `capturing`, `event held`, `disarmed`. `no data` appears in the rate's place when nothing is arriving. - **The rate is the *stored* rate**, which is the wire rate only while the whole requested window fits in the buffer. A window wider than the budget allows is bought by averaging groups of samples, so the rate falls and the status line says so — that is the anti-alias filter doing its job. - **`connected, but no data is arriving`** appears after three seconds in which no frame arrived while connected: the usual cause is the unicast point above. It clears the moment data returns. - **`N reconnects`** is appended when the client has had to re-establish the session. ## 3. Signals The signal panel lists every signal the CONFIG described, with a switch each. **Signals start disabled — enabling one is what starts acquiring it**, and a signal nobody enables costs nothing. Drag a signal from the panel onto a plot row to add a trace; drag it again to another row to plot the same signal twice. The colour is per *signal*, so the same signal is the same colour wherever it appears. The panel also holds the per-signal calibration: gain and offset, applied at ingest, so the measurements and the export carry the same numbers the plot shows. ## 4. The window and navigation The header's window control (`2 s`) sets the time span on screen; the popover takes a number and a unit, with a decade stepper either side. The trigger's event window is a share of *this* span, so a window change moves the trigger with it. - **Wheel** zooms about the pointer; **Shift+wheel** on x only, **Ctrl+wheel** y only. - **Ctrl `+` / `-`** zooms x, **Alt `+` / `-`** y, about the plot centre. - **Drag** pans, **right-drag** too (the right button also grabs a cursor at the pointer). - **Ctrl+drag** frames x to a box, **Alt+drag** y. - **Fit to data** (menu) auto-scales both axes and frames the whole buffered range. Each plot has its own **right-click menu**, in the same compact row style as the app's menu: **Y axis: auto scale**, **Y axis: fixed range...**, **Fit X axis**, **Fit all** (`f`), and **Remove signal** — which swaps the menu to one entry per trace on that plot plus *All traces on this plot*, with a **Back** row to return. That is how a trace comes off a row without going through the layout editor. The view is bounded by the **time window**, not by the buffer: zooming out stops at the span you set, and panning stops where the data starts. When the buffer holds *less* than the window — right after a signal is enabled — the window is left empty on its left, which is what a scope that has just started should look like. ## 5. Trigger ![a capture held](img/trigger-capture.png) *Single mode with a capture held: the axis is the event's own `[-pre, +post]`, the trigger marker sits at the edge, and the status line reads `event held`.* The trigger panel has the edge, the pre/post split (as a share of the event span), Normal or Single, and Arm/Disarm. Selecting a signal for the trigger enables it. - **While a capture fills** the plot keeps drawing live data on the event's clock, so the post side fills in view; the axis does not move. - **A held event is navigable within its own data** — zooming in works, panning stops at the capture's edges — and it is never quietly replaced by live data. - **Disarming mid-capture** publishes what the capture got and holds that, so the picture on screen when you disarm is the picture you keep. - The event window is counted in **stream seconds** end to end, so a source whose clock advances in jumps still gets the window it asked for. ## 6. Pause Pause freezes the *picture*, not the acquisition: the producer keeps filling the buffer behind it, and the frozen view is a snapshot of the window that was on screen. Resuming shows the data that arrived meanwhile — there is no hole. A paused view is held like a held event, so zooming in works and panning does not walk it off its snapshot. ## 7. Cursors The cursors toolbar toggle switches on two instruments per row: - **vertical** — two times, shared by every row; - **horizontal** — two rulers per row; - **both** — the two together; - **mix** — the two times, read off each *trace* where they cross it. The pane beside it reports each trace's values at the markers, and **Reset** puts them back at the window's edges. Values read `--` when a marker is past the newest sample and `partial` when only one marker is over data. ## 8. Measurements The measurements pane computes statistics of the **visible window** of a signal: average, top, bottom, min, max, peak to peak, amplitude, pulse duration, period and frequency. `+` adds one, the row's pencil edits it, and **show on plot** draws its result as a ruler (levels) or markers (times). `top` and `bottom` take a *split* as a share of the window's own range — the percentage travels between signals where a threshold in volts would not. `period` and `frequency` deliberately have no split: they count rising crossings of the mid level, one edge per cycle, and a split there is a setting you can only get wrong. The values are refreshed on a cadence — at most every 100 ms while the window is scrolling, once when it stops, and once per mode change — so a live view cannot cost you frames. The pane's cost is reported in the exit dump with the app's self-check. ## 9. Math channels ![the math channel editor](img/math-editor.png) *The compact node editor: each node is a box, its inputs the squares on the left edge and its output the square on the right. The orange dot marks the node that carries the channel's value.* A math channel is derived from the others and is then an ordinary channel: enable it and it is acquired, plotted, measured and exported like a streamed signal, and another math channel can use it as a source. Operations: `channel` (a source), `gain + offset`, `abs`, `sqrt`, `clamp`, `low pass`, `high pass`, `band pass` (biquads, corner in Hz against the stored rate), `sum`, `difference`, `product`, `ratio`. - **Click** a node to select it; its parameters appear in the form beside the canvas. - **Click an output square, then an input square**, to wire them. An input is only offered the nodes above it — the graph is a list walked in order, so a cycle cannot be drawn. - **Drag** a box to move it. **add node**, **delete node** and **set as output** are below the form. - The **output is the last node** of a graph you build top to bottom, and the orange dot says which; *set as output* changes it. A graph whose output is the source is valid and just passes the input through — that is the usual reason an operation "does nothing". - **Save** refuses an incomplete graph and says why: an unconnected input, no output, or a source that is not a channel the scope is acquiring. The samples are produced at ingest, once per stored slot, so the filters carry real history — a low pass is the filter you asked for rather than a window recomputed every frame. ## 10. Sessions Everything above is saved per source in `~/.config/udpscope/_.toml`: the window, the rows and which signal each slot holds, the calibration, **which signals were enabled**, **the math channels with their graphs** and **the measurements**. Reopening the same source brings the scope back as you left it. The file is plain TOML and safe to edit — a malformed line is skipped rather than refusing the whole file. Deleting it starts from the defaults. ## 11. Export **Export visible range…** writes the plotted signals over the visible x range at the *stored* rate to `.npy` or `.csv`, picked by the file name's extension. The table is collected when you click, not when the chooser closes, so a live view cannot scroll under the dialog. The `.npy` is a 2-D array: one row per sample, `time`, `stream_time`, then one column per signal. ## 12. Keyboard | key | action | |---|---| | `space` | pause / resume | | `a` | arm / disarm the trigger | | `t` | the trigger panel | | `l` | the layout editor | | `g` | go live | | `f` | fit to data | | `d` | add the first free signal to the focused row | | `c` | cursors on/off | | `m` | measurements pane | | `e` | export | | `?` | shortcuts | `Ctrl+E` exports, `Ctrl+Q` quits, `Ctrl+plus`/`Ctrl+minus` zoom x and `Alt+plus`/`Alt+minus` zoom y. A keystroke is ignored while a text field has focus, so typing in the window control never arms the trigger. ## 13. Troubleshooting | symptom | cause | |---|---| | `connected, but no data is arriving` | another client holds a unicast source — use `--multicast` | | the plot is empty, the status reads `no data` | nothing is enabled: enable a signal in the panel | | the trace is blank while the trigger is armed | `Waiting` — armed and nothing latched yet | | the axis has a margin at the right | a capture is filling: the post side arrives as it is acquired | | a math channel equals its input | its output node is the source — see §9 | | a very wide window shows a low rate | the ingest's anti-alias filter, not a fault | | a level reads `--` | the measurement is invalid in this window (e.g. a period with one edge) |