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

+17
View File
@@ -153,6 +153,15 @@ class RowSet {
/// belongs with `assign()` -- set it before the assignments that follow it, or they keep the alpha
/// the previous view left behind.
void set_trace_alpha(float alpha) noexcept { trace_alpha_ = alpha; }
/// The rows' shares of the figure's height, normalised by the plotter; an empty vector means equal
/// rows. Kept here, and re-applied after every rebuild: a `Figure` is built afresh whenever a row's
/// trace set or the row count changes, so weights that lived only in the figure would be lost on the
/// first drop or row change after a user had sized their layout.
void set_row_weights(std::vector<float> weights);
/// Sizes the stored weights to the rows that exist and hands them to the figure.
void apply_row_weights();
const std::vector<float>& row_weights() const noexcept { return row_weights_; }
/// Shows `rows` rows. A changed count rebuilds the figure, since a `Figure`'s row
/// count is fixed at construction.
void set_row_count(int rows);
@@ -376,6 +385,13 @@ class RowSet {
/// Whether a window is pinned: a held event, a filling capture or a paused view. The condition
/// `fit_x()` branches on, which is why the fit probe reports it.
bool window_pinned() const noexcept { return pinned_; }
/// True when the pin already *is* this window. A caller that re-states a pin every frame -- the file
/// view does, from the ring's own extent -- uses this to update the pin's numbers with
/// `set_data_pin()` instead of writing the group's range again with `pin_event_window()`, which would
/// undo a gesture that `hold_pinned_window()` has not adopted yet.
bool pinned_window_is(double pre_s, double post_s) const noexcept {
return pinned_ && pinned_pre_ == pre_s && pinned_post_ == post_s;
}
/// Adopts the instruments a gesture is moving, on `drag_row` (the row whose controller holds
/// a cursor), and nothing otherwise.
@@ -497,6 +513,7 @@ class RowSet {
plot::u32 x_group_ = 0;
int rows_ = 1;
float trace_alpha_ = 1.0f;
std::vector<float> row_weights_;
double window_s_ = 2.0;
double trigger_marker_x_ = 0.0;
bool trigger_marker_on_ = false;