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
View File
@@ -526,6 +526,13 @@ double ScopeModel::channel_dt(std::size_t channel) const {
BufferStore& ScopeModel::live() { return *store_; }
void ScopeModel::live_window_locked(double lo_s, double hi_s, u64& first, u64& last) {
// There may be **no store at all**: `rebuild_locked()` resets it for a stream with no channels, which
// is what the model is in until a CONFIG arrives. `live()` dereferences the pointer, so every path
// that can be reached without a stream has to ask first -- pausing a scope that is not connected did
// not, and walked into this function through `publish_pause_view()`, a null dereference.
first = 0;
last = 0;
if (store_ == nullptr) return;
BufferStore& src = live();
plot::data::RingBuffer<f64>& tsrc = src.time();
@@ -578,6 +585,10 @@ void ScopeModel::live_window_locked(double lo_s, double hi_s, u64& first, u64& l
}
void ScopeModel::publish_pause_view(double lo_s, double hi_s, double x_offset) {
// Nothing has arrived yet, so there is nothing to freeze: pausing a scope that is not connected is a
// legitimate gesture (the button works), and it must not take a window from a store that does not
// exist.
if (store_ == nullptr) return;
// Timed from before the lock: the wait for it is part of what freezing a view costs the thread
// doing it, and this one is the UI's.
const std::uint64_t t0 = now_ns();