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:
1 parent
8f54841c2c
commit
0e256764d5
41 files changed
+2081
-154
No files matched your search
+39
-2
@@ -27,6 +27,8 @@
|
||||
#include "model.hpp"
|
||||
#include "plot_host.hpp"
|
||||
#include "search.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "ui_prefs.hpp"
|
||||
#include "shortcuts.hpp"
|
||||
#include "status_text.hpp"
|
||||
#include "waveform.hpp"
|
||||
@@ -77,6 +79,10 @@ struct App {
|
||||
/// The editor's buttons, in the order they were added, so they can be taken off the
|
||||
/// overlay again. Rebuilt when the row count changes, repositioned when the rows move.
|
||||
std::vector<GtkWidget*> layout_buttons;
|
||||
/// The boundary a resize drag is on, and the weights it started from: a drag is *relative* to where it
|
||||
/// began, so the beginning has to be kept -- reading the weights while dragging would compound.
|
||||
int layout_drag_row = -1;
|
||||
std::vector<float> layout_drag_start;
|
||||
/// The editor's way out: one `Done` button in the plot area's top-right corner while
|
||||
/// the editor is up. Not part of `layout_buttons`, which is one button per row and per
|
||||
/// boundary -- the count the layout probe asserts.
|
||||
@@ -362,6 +368,9 @@ struct App {
|
||||
/// `event_recording` is the trigger panel's switch and defaults on -- the cache is only useful if
|
||||
/// it was already on when the event you wanted happened.
|
||||
std::unique_ptr<EventStore> event_store;
|
||||
/// Whether the cache holds anything, refreshed once a second: what keeps the history button on the bar
|
||||
/// for a source whose events are already written, even with recording switched off.
|
||||
bool events_cached = false;
|
||||
bool event_recording = false;
|
||||
/// The events pane: its list, the revealer that holds it, the recording switch (in the trigger
|
||||
/// panel, where the trigger's options live), and `events_thumb` -- the picture of the newest
|
||||
@@ -385,6 +394,9 @@ struct App {
|
||||
plot::Plot placeholder_plot_b;
|
||||
plot::gtk4::PlotArea* area_b = nullptr;
|
||||
GtkWidget* area_b_widget = nullptr;
|
||||
/// The figure the second area was last pointed at. `set_figure()` rebuilds its per-row input
|
||||
/// controllers, so it is only called when the `RowSet` really has a new figure.
|
||||
plot::Figure* area_b_figure = nullptr;
|
||||
std::unique_ptr<RowSet> rows_b;
|
||||
GtkWidget* plot_split = nullptr;
|
||||
bool comparing = false;
|
||||
@@ -407,6 +419,15 @@ struct App {
|
||||
/// live source. Nothing arrives in this mode, so everything that would report on a stream --
|
||||
/// the popover's Stream row, the starvation notice -- reports on the file instead.
|
||||
std::string waveform_name;
|
||||
/// The preferences, as read at start-up and written by the dialog. Held here rather than read where
|
||||
/// they are used: the dialog edits a copy, saves it and hands it back, so the file and the running
|
||||
/// program cannot disagree about what a preference is.
|
||||
Settings settings;
|
||||
/// The preferences window while it is up, and its controls: one at a time, like the configurations
|
||||
/// dialog -- a second window editing the same settings would let the two disagree.
|
||||
/// The preferences window. A `PrefsForm` rather than fields here because the source chooser opens
|
||||
/// the same dialog and runs before `App` exists -- see `ui_prefs.hpp`.
|
||||
PrefsForm prefs;
|
||||
/// The configurations dialog, while it is up: one at a time, and this is what makes a second
|
||||
/// `Configurations…` a present rather than a second window.
|
||||
GtkWidget* config_dialog = nullptr;
|
||||
@@ -979,6 +1000,10 @@ constexpr int kRowKeyBias = 1;
|
||||
constexpr int kLayoutButtonPx = 24;
|
||||
constexpr int kLayoutButtonPad = 4;
|
||||
constexpr const char* kLayoutKindKey = "udpscope-layout-insert";
|
||||
|
||||
/// The ↕ handles' own key: what they are is *not* what the `+` is, and `sync_layout_editor()`
|
||||
/// places them differently -- one boundary, two controls.
|
||||
constexpr const char* kLayoutResizeKey = "udpscope-layout-resize";
|
||||
constexpr gint64 kMeasureRefreshUs = 100 * 1000;
|
||||
constexpr gint64 kMeasureHeavyUs = 5 * 1000;
|
||||
constexpr const char* kCursorModeNames[] = {"vertical", "horizontal", "both", "mix"};
|
||||
@@ -992,8 +1017,8 @@ constexpr double kMathZoomStep = 1.1;
|
||||
constexpr double kWirePick = 6.0;
|
||||
constexpr int kZoomGestureSteps = 8;
|
||||
constexpr int kZoomWindowSteps = 8;
|
||||
constexpr int kEventThumbW = 126;
|
||||
constexpr int kEventThumbH = 42;
|
||||
constexpr int kEventThumbW = 138;
|
||||
constexpr int kEventThumbH = 46;
|
||||
inline int g_stats_phase = 0;
|
||||
inline std::vector<std::uint64_t> g_stats_n_before;
|
||||
inline double g_trigrenum_edge = 0.0;
|
||||
@@ -1090,6 +1115,18 @@ void update_search_panel(App& app);
|
||||
void sync_events_availability(App& app);
|
||||
/// Restates the events pane's divider position once a frame; see the definition.
|
||||
void sync_events_divider(App& app);
|
||||
/// The preferences dialog: one window, opened from the overflow menu. `apply_theme()` is separate
|
||||
/// because the theme is the one preference that takes effect without a restart.
|
||||
void open_preferences(App& app);
|
||||
gboolean prefs_probe(gpointer user);
|
||||
gboolean boxzoom_probe(gpointer user);
|
||||
/// A labelled row whose control keeps its natural width -- what a `GtkSwitch` needs and what
|
||||
/// `labeled_box()`'s expanding child denies it.
|
||||
GtkWidget* labeled_switch_row(const char* label, GtkWidget* control);
|
||||
/// The layout editor's boundary drag: the press starts it, movement sizes the two rows it separates.
|
||||
void on_layout_drag_begin(GtkGestureDrag* gesture, double x, double y, gpointer user);
|
||||
void on_layout_drag_update(GtkGestureDrag* gesture, double offset_x, double offset_y, gpointer user);
|
||||
void on_layout_drag_end(GtkGestureDrag* gesture, double offset_x, double offset_y, gpointer user);
|
||||
void on_trigger_toggled(GtkToggleButton* button, gpointer user);
|
||||
void on_go_live(GtkButton*, gpointer user);
|
||||
plot::u32 measure_color(std::size_t index);
|
||||
|
||||
Reference in new issue
Block a user