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

+8 -11
View File
@@ -17,17 +17,12 @@
namespace udpscope {
namespace {
/// One directory under the cache holds *every* source's events, and each source gets a folder inside
/// it: `<cache>/udpscope/events/<source>/`. The other way round -- a folder per source with an
/// `events` inside -- spreads one feature over as many directories as there are sources, and turns
/// "where are my events" into a question about which host you last used.
constexpr const char* kEventsSubdir = "/events";
void mkdirs(const std::string& path) {
std::string cur;
for (std::size_t i = 0; i < path.size(); ++i) {
cur.push_back(path[i]);
if (path[i] != '/' && i + 1 != path.size()) continue;
::mkdir(cur.c_str(), 0755);
}
}
/// `2026-10-09T11:32:00` in local time, plus the sequence number: two events inside one second are
/// ordinary (a 5 Hz trigger latches five times a second), and a stem that collides would overwrite
/// the event it collided with.
@@ -236,7 +231,9 @@ bool event_meta_parse(const std::string& text, EventRecord* out) {
return any;
}
EventStore::EventStore(std::string source) {
std::string default_events_dir() { return std::string(cache_dir()) + kEventsSubdir; }
EventStore::EventStore(std::string source, std::string base_dir) {
if (source.empty()) return;
std::string safe;
for (char c : source) {
@@ -244,7 +241,7 @@ EventStore::EventStore(std::string source) {
c == '.' || c == '_' || c == '-';
safe.push_back(ok ? c : '_');
}
dir_ = cache_dir() + "/" + safe + kEventsSubdir;
dir_ = (base_dir.empty() ? default_events_dir() : base_dir) + "/" + safe;
}
std::string EventStore::save(const ExportTable& table, const EventRecord& meta, std::size_t keep) {