208 lines
11 KiB
Markdown
208 lines
11 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 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 <group>` with `--interface <local address>`.
|
|
|
|
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 `● <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 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
|
|
|
|

|
|
|
|
*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 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/<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` | 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) |
|