udpscope: an oscilloscope view for a UDPStreamer source

GTK4 and libadwaita. It receives one UDP stream through the vendored
udpsclient, buffers it in a large ring, and draws it with the vendored
plotter: a trigger with a pre/post event window, a pause that freezes the
window, cursors, measurements, math channels in a node editor, an event cache,
named configurations, preferences, and an export of the visible window to
.npy/.csv. Waveform files can be opened and analysed in place.

The interface is two threads -- a producer that owns the socket and the GTK
main thread that owns every widget and the GL context -- sharing one model
under one mutex, with samples crossing through lock-free rings.

deps/plotter is a submodule pinned to the commit this tree builds against;
deps/udpsclient is vendored in place. docs/claude/ documents the rules that
cost a defect to learn, and SPECS.md is the requirement document.
This commit is contained in:
Martino Ferrari committed 2026-10-11 15:21:40 +02:00
1 parent 8f54841c2c
commit 0e256764d5
41 files changed
+2081 -154

No files matched your search

+12
View File
@@ -218,6 +218,18 @@ come back with a different answer.
Relative time's `slot_base_` also survives: it is samples-since-the-stream-began, the user's
chosen origin, and must not jump backwards because the link blipped.
## There may be no store at all
`rebuild_locked()` calls `store_.reset()` for a stream with **no channels** — the model's state until the
first CONFIG arrives — so `store_` is legitimately null, and `ScopeModel::live()` dereferences it. Every
path reachable without a stream has to ask first: **pausing a scope that is not connected** is one (the
button works, and freezing a view means taking a window from the rings), and it crashed inside
`live_window_locked()` on a null store. The guards belong at the entry points the UI can reach
(`live_window_locked()`, `publish_pause_view()`), not in `live()`, which cannot express "nothing there"
through a reference. The paths that only run for a *bound* trace — the series' gather, the SlotSeries'
`shared_channel()` — are safe by construction: a slot with no channel binds no series at all.
`UDPSCOPE_PROBE_PAUSE_MS` against a port nothing is listening on is the check.
## A waveform is a source that never arrives
`--open FILE` (and the chooser's Waveforms page) puts an exported `.npy` into the model instead of a