Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
264 lines
15 KiB
Markdown
264 lines
15 KiB
Markdown
# 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 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 <group>` with `--interface <local address>`.
|
||
|
||
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 `● <state> · <rate> · <window>`, 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 double-headed 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. The plots of a split
|
||
row also share **one y range**: the y labels are drawn at the left of the first plot only, the
|
||
horizontal rulers are the same two values in every plot, and a y zoom, pan, fit or fixed range in any of
|
||
them applies to the whole row. The time labels sit under the bottom row, and
|
||
under any row above it whose columns differ from the row below. 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 and the status line 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
|
||
|
||

|
||
|
||
*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 row (every plot of a split row shows the same two values);
|
||
- **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 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/<host>_<port>.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) |
|