# 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 header bar above (the window control — `2 s` — first on the left), the plot, the badge bar under it with one badge per signal, the status line at the foot, and the dock on the right with the cursor and measurement readouts and the events block.* The window is a plot with three things around it: - the **badge bar** under the plot: a badge for each signal you have switched on, a `+` to add one (past six they fold into one `N signals` badge), and, on the right, **Trigger** and — only for a source with controls of its own — **Source**; - the **side panel**, which a badge opens over the plot's right edge, one page at a time: the trigger, a signal, the list of signals to add, the cursors, the measurements, the events, or a source's own controls. A second click on the badge, `Esc`, **Close** or a click on the plot closes it, and it never resizes the plot; - the **dock** on the right, which the dock button in the header bar (`s`) shows and hides: read-only **Readouts** of the cursors and of the measurements, each with an **Edit** button that opens its page, and an **Events** block. With the dock hidden, or in a narrow window where the dock slides over the plot, the readouts show in a one-line strip under the plot instead (the cursors' deltas and each measurement's value; **Cursors** and **Measures** open the pages); the strip is there only while the cursors are on or there is a measurement. ## 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 At the foot of the window, under the badge bar, 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 **`+` badge** (or `g`) opens the **Signals** page, which lists what can be added: the signals the CONFIG described that are not acquired yet, each with a tick. **Signals start disabled — ticking one is what starts acquiring it**, and a signal nobody enables costs nothing. Its badge then appears in the badge bar. Drag the badge onto a plot row to add a trace; drag it again to another row to plot the same signal twice, or onto another badge to reorder the badges. The colour is per *signal*, so the same signal is the same colour wherever it appears. Clicking a signal's badge opens its own page. Directly under the name are its **Unit**, **Gain** and **Offset** — each with a pencil button that edits that one value — and then what the stream declared about it (type, size, range, sample rate, timing). Below come the colour and the **Plot on** buttons for the rows. Gain and offset are applied at ingest, so the measurements and the export carry the same numbers the plot shows; a renamed unit is used there too and is saved with the session. ### Splitting a row Press `l` (or pick **Layout editor** from the overflow menu) to arrange the plots: a **`×`** at the top left of each plot removes it, a **`+`** on each boundary between rows adds a row, and a **Done** button in the plot's top-right corner leaves the editor. The plots can also be put **side by side**: a row holds up to **4** plots, and the figure up to **8** in all. - **To split a row**, click the `+` at its left or right edge, or the `+` between two of its plots. The new plot is empty and the plots after it keep their traces. The `+` is dim at 4 plots in the row or 8 in the figure. - **To resize**, drag the arrow between two plots left or right. The two neighbours trade width (each stays at least about 96 px wide); the sizes are saved with the session. - **To remove a plot**, click its `×`. In a split row it removes just that plot and its traces; in a row of one plot it removes the row. Every plot shares **one time window**: panning or zooming in any of them moves them all, the crosshair sits at the same time in each, and the vertical cursors are the same two times in each. Each plot has its **own y axis**, its own horizontal rulers and its own fit, and only the bottom row shows the time labels. A plot of a split row is named by its row and a letter, `2a` to `2d`: that is what the signals page's **Plot on** buttons, the status line and the cursors readout call it, and a signal dragged from its badge goes onto the plot under the pointer. The arrangement is saved per source, and a session saved before rows could be split opens with one plot per row, as it was. ## 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 button in the header bar (`t`) turns trigger mode on. The **Trigger** badge opens the trigger page, which has the signal, the condition, 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. - **Events.** With *record events* on (trigger page), each latch is stored. The dock's **Events** block lists them newest first and **one click replays** one; **Manage** opens the full list as a page, where a click selects, a double click replays, and **Compare**, **Accumulate** and **Live** act on the selection or go back to the stream. The block is dimmed until something is recorded. ## 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 toggle in the header bar (`c`) switches on two instruments per row: - **vertical** — two times, shared by every row (and every plot of a split row); - **horizontal** — two rulers per plot; - **both** — the two together; - **mix** — the two times, read off each *trace* where they cross it. The dock reports each trace's values at the markers; **Edit** beside **Cursors** opens the page with the mode and **Reset**, which 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 A measurement is a statistic of the **visible window** of a signal: average, top, bottom, min, max, peak to peak, amplitude, pulse duration, period and frequency. `m`, or **Edit** beside **Measurements** in the dock, opens the page; its `+` adds one, a click on a row (in the page or in the dock) edits it, and **show on plot** draws its result as a ruler (levels) or markers (times). The dock shows each measurement's value. `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 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. The **Signals** page lists them under a `MATH` heading whose `+` creates one; a math channel's own page has **Edit graph** and **Delete**. 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. - **Drag from an output square to 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` | enable / disable trigger mode | | `c` | enable / disable the cursors | | `m` | open or close the Measurements page | | `g` | open or close the Signals page (the `+` badge opens it too) | | `l` | show / hide the layout editor | | `s` | show / hide the dock | | `Esc` | close the slide-over page (the guided tour takes it first) | | `f` | fit to data (the plot's own key) | | `Ctrl+E` | export visible range | | `F1` | open the manual | | `Ctrl+/` | open the shortcut overview | | `Ctrl+T` | start the guided tour | | `Ctrl+H` | turn the contextual help on or off | | `Ctrl+Q` | quit | The plot also answers `+`/`-` (zoom both axes), `Ctrl+plus`/`Ctrl+minus` (zoom x only), `Alt+plus`/`Alt+minus` (zoom y only) and the arrow keys (pan). 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) |