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

+11 -2
View File
@@ -29,7 +29,7 @@ struct EventRecord {
std::uint64_t seq = 0; ///< the run's own event counter, so two events in one second differ
};
/// The cache of captured events: `<cache_dir>/<source>/events/`, one `.npy` per event written in
/// The cache of captured events: `<cache_dir>/events/<source>/`, one `.npy` per event written in
/// the **export format** -- so `--open` and the chooser's Waveforms page read it back with the
/// loader that already exists -- beside a `.txt` sidecar holding what the list shows without
/// loading the samples, and whatever thumbnail the caller rendered.
@@ -37,10 +37,19 @@ struct EventRecord {
/// No GTK: the caller renders the thumbnail and hands over a path, exactly as it hands over the
/// table. The store owns naming, the sidecar, pruning and listing, which is what makes it testable
/// without a window.
/// The directory every source's event folder is created under when no preference says otherwise:
/// `<cache>/udpscope/events`. Exposed because the preferences dialog shows it -- "the standard
/// location" is not an answer a user can act on, and a second copy of the path computed in the dialog
/// is a second thing to get wrong.
std::string default_events_dir();
class EventStore {
public:
/// `source` is the `<host>-<port>` pair, the same key the packet capture uses.
explicit EventStore(std::string source = {});
/// `base_dir` is the directory every source's own folder is created under; empty means
/// `<cache>/udpscope/events`. It comes from the preferences (item 9), because a user with a small
/// home partition and a second disk should not have to choose between events and free space.
explicit EventStore(std::string source = {}, std::string base_dir = {});
/// How many events to keep, newest first. The oldest beyond it are deleted on every save; item 9's
/// preferences are where this becomes the user's number.