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
+11
-1
@@ -31,6 +31,12 @@ std::string sanitize(const std::string& host) {
|
||||
return out.empty() ? "_" : out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/// Creates `path` and every missing component, the last one included, like `mkdir -p`. Outside the
|
||||
/// anonymous namespace because the preferences file is written through it too: a bare `mkdir()` of
|
||||
/// `<config>/udpscope` fails when `<config>` does not exist yet, which is what a fresh install looks
|
||||
/// like -- and it fails *silently*, leaving the settings dialog reporting a write it did not make.
|
||||
void mkdirs(const std::string& path) {
|
||||
// **Every** component, the last one included. The condition used to be "the path has a trailing
|
||||
// slash", so the final directory was never created: `~/.config/udpscope` did not exist, the
|
||||
@@ -44,6 +50,8 @@ void mkdirs(const std::string& path) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::string toml_escape(const std::string& s) {
|
||||
std::string out = "\"";
|
||||
for (char c : s) {
|
||||
@@ -504,6 +512,8 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
||||
parse_string_array(value.substr(1, value.size() - 2), names);
|
||||
}
|
||||
st.rows.back().signals = names;
|
||||
} else if (key == "weight") {
|
||||
st.rows.back().weight = std::strtod(value.c_str(), nullptr);
|
||||
}
|
||||
} else if (section == Section::Math && !st.math.empty()) {
|
||||
MathChannel& m = st.math.back();
|
||||
@@ -620,7 +630,7 @@ bool save_layout(const std::string& path, const LayoutState& state) {
|
||||
o << "relative_time = " << (state.relative_time ? "true" : "false") << "\n";
|
||||
if (state.capture_packets > 0) o << "capture_packets = " << state.capture_packets << "\n";
|
||||
for (const auto& row : state.rows) {
|
||||
o << "\n[[plot]]\nsignals = [";
|
||||
o << "\n[[plot]]\nweight = " << row.weight << "\nsignals = [";
|
||||
for (std::size_t i = 0; i < row.signals.size(); ++i) {
|
||||
if (i != 0) o << ", ";
|
||||
o << toml_escape(row.signals[i]);
|
||||
|
||||
Reference in new issue
Block a user