Feature/source plugins #1
No files matched your search
@@ -241,6 +241,7 @@ add_executable(udpscope
|
|||||||
src/source/registry.cpp
|
src/source/registry.cpp
|
||||||
src/source/udps_source.cpp
|
src/source/udps_source.cpp
|
||||||
src/source/udps_kind.cpp
|
src/source/udps_kind.cpp
|
||||||
|
src/source/probe_source.cpp
|
||||||
src/plot_host.cpp
|
src/plot_host.cpp
|
||||||
src/export.cpp
|
src/export.cpp
|
||||||
src/search.cpp
|
src/search.cpp
|
||||||
|
|||||||
@@ -322,6 +322,10 @@ std::vector<KnownSource> known_sources() {
|
|||||||
s.capture_packets = st.capture_packets;
|
s.capture_packets = st.capture_packets;
|
||||||
s.multicast = st.multicast;
|
s.multicast = st.multicast;
|
||||||
s.interface_addr = st.interface_addr;
|
s.interface_addr = st.interface_addr;
|
||||||
|
if (!st.source_kind.empty()) {
|
||||||
|
s.kind = st.source_kind;
|
||||||
|
s.opts = st.source_opts;
|
||||||
|
}
|
||||||
s.rows = static_cast<int>(st.rows.size());
|
s.rows = static_cast<int>(st.rows.size());
|
||||||
for (const LayoutState::Row& row : st.rows) {
|
for (const LayoutState::Row& row : st.rows) {
|
||||||
for (const std::string& sig : row.signals) {
|
for (const std::string& sig : row.signals) {
|
||||||
@@ -558,6 +562,11 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
|||||||
st.multicast = strip_quote(value);
|
st.multicast = strip_quote(value);
|
||||||
} else if (key == "interface") {
|
} else if (key == "interface") {
|
||||||
st.interface_addr = strip_quote(value);
|
st.interface_addr = strip_quote(value);
|
||||||
|
} else if (key == "source_kind") {
|
||||||
|
st.source_kind = strip_quote(value);
|
||||||
|
} else if (key.rfind("source_opt_", 0) == 0) {
|
||||||
|
const std::string k = key.substr(11);
|
||||||
|
if (SourceConfig::valid_key(k)) st.source_opts.set(k, strip_quote(value));
|
||||||
} else if (key == "comment") {
|
} else if (key == "comment") {
|
||||||
st.comment = strip_quote(value);
|
st.comment = strip_quote(value);
|
||||||
} else if (key == "buffer_mb") {
|
} else if (key == "buffer_mb") {
|
||||||
@@ -699,6 +708,12 @@ bool save_layout(const std::string& path, const LayoutState& state) {
|
|||||||
if (!state.interface_addr.empty()) {
|
if (!state.interface_addr.empty()) {
|
||||||
o << "interface = " << toml_escape(state.interface_addr) << "\n";
|
o << "interface = " << toml_escape(state.interface_addr) << "\n";
|
||||||
}
|
}
|
||||||
|
if (!state.source_kind.empty() && state.source_kind != "udps") {
|
||||||
|
o << "source_kind = " << toml_escape(state.source_kind) << "\n";
|
||||||
|
for (const auto& kv : state.source_opts.items()) {
|
||||||
|
o << "source_opt_" << kv.first << " = " << toml_escape(kv.second) << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
// Top level, before any table. Emitted after the `[[calibration]]` tables it belonged to the last
|
// Top level, before any table. Emitted after the `[[calibration]]` tables it belonged to the last
|
||||||
// of them -- a bare key after a header is that table's key in TOML -- so the parser, correctly,
|
// of them -- a bare key after a header is that table's key in TOML -- so the parser, correctly,
|
||||||
// never saw it and a session's enable state was silently lost.
|
// never saw it and a session's enable state was silently lost.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "math_channel.hpp"
|
#include "math_channel.hpp"
|
||||||
#include "measure.hpp"
|
#include "measure.hpp"
|
||||||
|
#include "source/source_config.hpp"
|
||||||
|
|
||||||
namespace udpscope {
|
namespace udpscope {
|
||||||
|
|
||||||
@@ -54,6 +55,12 @@ struct LayoutState {
|
|||||||
/// unicast; empty `interface_addr` is the default route.
|
/// unicast; empty `interface_addr` is the default route.
|
||||||
std::string multicast;
|
std::string multicast;
|
||||||
std::string interface_addr;
|
std::string interface_addr;
|
||||||
|
/// Which source kind this session belongs to. Empty means `udps`: every file written before the
|
||||||
|
/// plugins existed. Only a kind other than `udps` is written, so a UDPS session file is
|
||||||
|
/// byte-identical to what earlier versions wrote.
|
||||||
|
std::string source_kind;
|
||||||
|
/// That kind's settings (`SourceConfig`), stored as top-level `source_opt_<key>` entries.
|
||||||
|
SourceConfig source_opts;
|
||||||
|
|
||||||
/// The derived channels, graphs and all: the editor's work is part of the session, not of the
|
/// The derived channels, graphs and all: the editor's work is part of the session, not of the
|
||||||
/// process. A graph is stored as one string per node so the file stays in this store's TOML
|
/// process. A graph is stored as one string per node so the file stays in this store's TOML
|
||||||
@@ -144,6 +151,8 @@ struct KnownSource {
|
|||||||
std::uint16_t port = 0;
|
std::uint16_t port = 0;
|
||||||
std::string multicast; ///< empty = unicast
|
std::string multicast; ///< empty = unicast
|
||||||
std::string interface_addr; ///< empty = the default route
|
std::string interface_addr; ///< empty = the default route
|
||||||
|
std::string kind = "udps"; ///< the session's source kind
|
||||||
|
SourceConfig opts; ///< that kind's settings (empty for UDPS, whose fields are above)
|
||||||
int rows = 0; ///< plots the session had
|
int rows = 0; ///< plots the session had
|
||||||
int signals = 0; ///< named slots across those plots
|
int signals = 0; ///< named slots across those plots
|
||||||
std::int64_t mtime = 0; ///< orders the list; never shown
|
std::int64_t mtime = 0; ///< orders the list; never shown
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||||
|
|
||||||
|
#include "source/probe_source.hpp"
|
||||||
|
|
||||||
|
#include "source/registry.hpp"
|
||||||
|
|
||||||
|
#include <adwaita.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace udpscope {
|
||||||
|
|
||||||
|
ProbeSource::ProbeSource(const SourceConfig& cfg)
|
||||||
|
: rate_hz_(std::max(1.0, cfg.get_double("rate", 1000.0))), batch_(kBatch, 0.0) {
|
||||||
|
SignalInfo s;
|
||||||
|
s.name = "probe";
|
||||||
|
s.unit = "V";
|
||||||
|
s.type_code = 9; // float64
|
||||||
|
s.rows = 1;
|
||||||
|
s.cols = 1;
|
||||||
|
s.range_min = -1.0;
|
||||||
|
s.range_max = 1.0;
|
||||||
|
s.time_mode = kTimePacket;
|
||||||
|
s.sampling_rate = rate_hz_;
|
||||||
|
signals_.push_back(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProbeSource::open(SourceSink& sink) {
|
||||||
|
sink_ = &sink;
|
||||||
|
configured_ = false;
|
||||||
|
open_.store(true, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProbeSource::close() { open_.store(false, std::memory_order_relaxed); }
|
||||||
|
|
||||||
|
SourceStats ProbeSource::stats() const {
|
||||||
|
SourceStats s;
|
||||||
|
s.packets = frames_;
|
||||||
|
s.frames = frames_;
|
||||||
|
s.connected = open_.load(std::memory_order_relaxed);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProbeSource::poll(int timeout_ms) {
|
||||||
|
drain_commands();
|
||||||
|
if (!open_.load(std::memory_order_relaxed) || sink_ == nullptr) return;
|
||||||
|
if (!configured_) {
|
||||||
|
configured_ = true;
|
||||||
|
sink_->on_config(signals_, /*publish_mode=*/1);
|
||||||
|
sink_->on_event(SourceEvent::Connected, "");
|
||||||
|
}
|
||||||
|
// One batch takes kBatch / rate seconds to "arrive".
|
||||||
|
const int period_ms = std::max(1, static_cast<int>(1000.0 * kBatch / rate_hz_));
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(std::min(period_ms, timeout_ms)));
|
||||||
|
const double step = 2.0 * M_PI * 5.0 / rate_hz_; // a 5 Hz sine
|
||||||
|
for (std::uint32_t k = 0; k < kBatch; ++k) {
|
||||||
|
batch_[k] = std::sin(phase_);
|
||||||
|
phase_ += step;
|
||||||
|
}
|
||||||
|
values_.values = batch_.data();
|
||||||
|
values_.count = kBatch;
|
||||||
|
Frame f;
|
||||||
|
f.counter = counter_++;
|
||||||
|
f.lost = 0;
|
||||||
|
f.late = false;
|
||||||
|
f.recv_time = std::chrono::duration<double>(std::chrono::system_clock::now().time_since_epoch())
|
||||||
|
.count();
|
||||||
|
f.publish_mode = 1;
|
||||||
|
f.num_samples = kBatch;
|
||||||
|
f.num_signals = 1;
|
||||||
|
f.signals = signals_.data();
|
||||||
|
f.values = &values_;
|
||||||
|
++frames_;
|
||||||
|
sink_->on_data(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void on_rate_changed(GObject*, GParamSpec*, gpointer user) {
|
||||||
|
const auto& cb = *static_cast<std::shared_ptr<std::function<void()>>*>(user);
|
||||||
|
if (cb && *cb) (*cb)();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProbeKind final : public SourceKind {
|
||||||
|
public:
|
||||||
|
std::string id() const override { return "probe"; }
|
||||||
|
std::string display_name() const override { return "Probe (test)"; }
|
||||||
|
std::string identity(const SourceConfig& cfg) const override {
|
||||||
|
return "rate" + std::to_string(cfg.get_int("rate", 1000));
|
||||||
|
}
|
||||||
|
std::unique_ptr<DataSource> make(const SourceConfig& cfg) const override {
|
||||||
|
return std::make_unique<ProbeSource>(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigForm make_config_form() const override {
|
||||||
|
GtkWidget* rate = GTK_WIDGET(adw_spin_row_new_with_range(1, 100000, 1));
|
||||||
|
adw_preferences_row_set_title(ADW_PREFERENCES_ROW(rate), "_Rate (Hz)");
|
||||||
|
adw_preferences_row_set_use_underline(ADW_PREFERENCES_ROW(rate), TRUE);
|
||||||
|
adw_spin_row_set_value(ADW_SPIN_ROW(rate), 1000);
|
||||||
|
|
||||||
|
auto cb = std::make_shared<std::function<void()>>();
|
||||||
|
g_signal_connect_data(rate, "notify::value", G_CALLBACK(on_rate_changed),
|
||||||
|
new std::shared_ptr<std::function<void()>>(cb),
|
||||||
|
+[](gpointer d, GClosure*) {
|
||||||
|
delete static_cast<std::shared_ptr<std::function<void()>>*>(d);
|
||||||
|
},
|
||||||
|
static_cast<GConnectFlags>(0));
|
||||||
|
|
||||||
|
ConfigForm f;
|
||||||
|
f.rows = {rate};
|
||||||
|
f.description = "A synthetic 5 Hz sine, for the self-check";
|
||||||
|
f.focus = rate;
|
||||||
|
f.write = [=](const SourceConfig& c) {
|
||||||
|
adw_spin_row_set_value(ADW_SPIN_ROW(rate), static_cast<double>(c.get_int("rate", 1000)));
|
||||||
|
};
|
||||||
|
f.read = [=]() {
|
||||||
|
SourceConfig c;
|
||||||
|
c.set("rate", std::to_string(static_cast<long>(adw_spin_row_get_value(ADW_SPIN_ROW(rate)))));
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
f.valid = []() { return true; };
|
||||||
|
f.on_change = [cb](std::function<void()> fn) { *cb = std::move(fn); };
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::unique_ptr<SourceKind> make_probe_kind() { return std::make_unique<ProbeKind>(); }
|
||||||
|
|
||||||
|
} // namespace udpscope
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "source/data_source.hpp"
|
||||||
|
#include "source/source_config.hpp"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace udpscope {
|
||||||
|
|
||||||
|
/// A synthetic source for the headless self-check: one 1 V sine signal, `kBatch` samples per
|
||||||
|
/// frame at `rate` Hz, no socket. It exists to prove the framework end to end (open, CONFIG,
|
||||||
|
/// data, commands, panels) and is registered only when `UDPSCOPE_PROBE_SOURCE_KINDS` is set.
|
||||||
|
class ProbeSource final : public DataSource {
|
||||||
|
public:
|
||||||
|
static constexpr std::uint32_t kBatch = 10;
|
||||||
|
|
||||||
|
explicit ProbeSource(const SourceConfig& cfg);
|
||||||
|
|
||||||
|
void open(SourceSink& sink) override;
|
||||||
|
void poll(int timeout_ms) override;
|
||||||
|
void close() override;
|
||||||
|
SourceStats stats() const override;
|
||||||
|
bool connected() const override { return open_.load(std::memory_order_relaxed); }
|
||||||
|
|
||||||
|
/// What a panel command does, so the probe can see that `post()` reached the producer thread.
|
||||||
|
void note_applied() noexcept { applied_.fetch_add(1, std::memory_order_relaxed); }
|
||||||
|
std::uint64_t commands_applied() const noexcept {
|
||||||
|
return applied_.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
double rate_hz_;
|
||||||
|
SourceSink* sink_ = nullptr;
|
||||||
|
std::atomic<bool> open_{false};
|
||||||
|
bool configured_ = false;
|
||||||
|
std::uint32_t counter_ = 0;
|
||||||
|
std::uint64_t frames_ = 0;
|
||||||
|
double phase_ = 0.0;
|
||||||
|
std::vector<SignalInfo> signals_;
|
||||||
|
std::vector<double> batch_;
|
||||||
|
SignalValues values_;
|
||||||
|
std::atomic<std::uint64_t> applied_{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace udpscope
|
||||||
@@ -50,7 +50,7 @@ void register_builtin_sources() {
|
|||||||
if (done) return;
|
if (done) return;
|
||||||
done = true;
|
done = true;
|
||||||
register_kind(make_udps_kind());
|
register_kind(make_udps_kind());
|
||||||
// Task 6: register make_probe_kind() when UDPSCOPE_PROBE_SOURCE_KINDS is set
|
if (std::getenv("UDPSCOPE_PROBE_SOURCE_KINDS") != nullptr) register_kind(make_probe_kind());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string session_host(const std::string& kind_id, const SourceConfig& cfg) {
|
std::string session_host(const std::string& kind_id, const SourceConfig& cfg) {
|
||||||
|
|||||||
+10
-2
@@ -33,6 +33,7 @@
|
|||||||
#include "model.hpp"
|
#include "model.hpp"
|
||||||
#include "plot_host.hpp"
|
#include "plot_host.hpp"
|
||||||
#include "search.hpp"
|
#include "search.hpp"
|
||||||
|
#include "source/registry.hpp"
|
||||||
#include "source_dialog.hpp"
|
#include "source_dialog.hpp"
|
||||||
|
|
||||||
#include <adwaita.h>
|
#include <adwaita.h>
|
||||||
@@ -100,8 +101,7 @@ void open_configurations(App& app) {
|
|||||||
|
|
||||||
GtkWidget* title = gtk_label_new(nullptr);
|
GtkWidget* title = gtk_label_new(nullptr);
|
||||||
gtk_label_set_markup(GTK_LABEL(title),
|
gtk_label_set_markup(GTK_LABEL(title),
|
||||||
("<b>" + app.source_host + ":" + std::to_string(app.source_port) +
|
("<b>" + app.source_title + "</b>")
|
||||||
"</b>")
|
|
||||||
.c_str());
|
.c_str());
|
||||||
gtk_widget_set_halign(title, GTK_ALIGN_START);
|
gtk_widget_set_halign(title, GTK_ALIGN_START);
|
||||||
gtk_box_append(GTK_BOX(box), title);
|
gtk_box_append(GTK_BOX(box), title);
|
||||||
@@ -5918,6 +5918,14 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
|||||||
app.source_port = cfg.open_waveform.empty() ? cfg.session_port() : 0;
|
app.source_port = cfg.open_waveform.empty() ? cfg.session_port() : 0;
|
||||||
app.source_multicast = cfg.open_waveform.empty() ? cfg.multicast : std::string();
|
app.source_multicast = cfg.open_waveform.empty() ? cfg.multicast : std::string();
|
||||||
app.source_interface = cfg.interface_addr;
|
app.source_interface = cfg.interface_addr;
|
||||||
|
app.source_kind = cfg.source_kind;
|
||||||
|
app.source_opts = cfg.source_opts;
|
||||||
|
if (cfg.source_kind != "udps" && find_kind(cfg.source_kind) != nullptr) {
|
||||||
|
app.source_title = find_kind(cfg.source_kind)->display_name() + " " +
|
||||||
|
find_kind(cfg.source_kind)->identity(cfg.source_config());
|
||||||
|
} else {
|
||||||
|
app.source_title = app.source_host + ":" + std::to_string(app.source_port);
|
||||||
|
}
|
||||||
// The event cache is keyed on the source, like the packet capture. A waveform has no source, so a
|
// The event cache is keyed on the source, like the packet capture. A waveform has no source, so a
|
||||||
// file-mode run has no cache either -- there are no latches in a file.
|
// file-mode run has no cache either -- there are no latches in a file.
|
||||||
if (!app.source_host.empty() && app.source_port != 0) {
|
if (!app.source_host.empty() && app.source_port != 0) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
#include "plot_host.hpp"
|
#include "plot_host.hpp"
|
||||||
#include "search.hpp"
|
#include "search.hpp"
|
||||||
#include "settings.hpp"
|
#include "settings.hpp"
|
||||||
|
#include "source/source_config.hpp"
|
||||||
#include "ui_prefs.hpp"
|
#include "ui_prefs.hpp"
|
||||||
#include "shortcuts.hpp"
|
#include "shortcuts.hpp"
|
||||||
#include "status_text.hpp"
|
#include "status_text.hpp"
|
||||||
@@ -837,6 +838,11 @@ struct App {
|
|||||||
/// chooser needs both to reconnect to a multicast source from a stored row.
|
/// chooser needs both to reconnect to a multicast source from a stored row.
|
||||||
std::string source_multicast;
|
std::string source_multicast;
|
||||||
std::string source_interface;
|
std::string source_interface;
|
||||||
|
/// The source kind and its settings, so the session can say which kind it belongs to, and the
|
||||||
|
/// words that name the source in dialogs (`host:port` for UDPS, `<kind> <identity>` otherwise).
|
||||||
|
std::string source_kind = "udps";
|
||||||
|
SourceConfig source_opts;
|
||||||
|
std::string source_title;
|
||||||
guint layout_save_source = 0;
|
guint layout_save_source = 0;
|
||||||
bool layout_loaded = false;
|
bool layout_loaded = false;
|
||||||
unsigned signal_generation = ~0u;
|
unsigned signal_generation = ~0u;
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ LayoutState collect_layout(App& app) {
|
|||||||
// The connection is the session's too: the file's name carries only host and port.
|
// The connection is the session's too: the file's name carries only host and port.
|
||||||
st.multicast = app.source_multicast;
|
st.multicast = app.source_multicast;
|
||||||
st.interface_addr = app.source_interface;
|
st.interface_addr = app.source_interface;
|
||||||
|
st.source_kind = app.source_kind;
|
||||||
|
st.source_opts = app.source_opts;
|
||||||
// The configuration's own identity and the startup settings it carries. The buffer size is a
|
// The configuration's own identity and the startup settings it carries. The buffer size is a
|
||||||
// *startup* parameter -- the rings are allocated before any stream is known -- so a configuration
|
// *startup* parameter -- the rings are allocated before any stream is known -- so a configuration
|
||||||
// is the only place it can be remembered per source, and the chooser applies it on open.
|
// is the only place it can be remembered per source, and the chooser applies it on open.
|
||||||
|
|||||||
Reference in new issue
Block a user