// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #include "source/probe_source.hpp" #include "source/registry.hpp" #include #include #include #include #include #include #include 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(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(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>*>(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 make(const SourceConfig& cfg) const override { return std::make_unique(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>(); g_signal_connect_data(rate, "notify::value", G_CALLBACK(on_rate_changed), new std::shared_ptr>(cb), +[](gpointer d, GClosure*) { delete static_cast>*>(d); }, static_cast(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(c.get_int("rate", 1000))); }; f.read = [=]() { SourceConfig c; c.set("rate", std::to_string(static_cast(adw_spin_row_get_value(ADW_SPIN_ROW(rate))))); return c; }; f.valid = []() { return true; }; f.on_change = [cb](std::function fn) { *cb = std::move(fn); }; return f; } std::vector make_panels(DataSource& source) const override { GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); gtk_widget_set_margin_top(box, 12); gtk_widget_set_margin_bottom(box, 12); gtk_widget_set_margin_start(box, 12); gtk_widget_set_margin_end(box, 12); GtkWidget* label = gtk_label_new("Test panel"); gtk_widget_set_halign(label, GTK_ALIGN_START); gtk_box_append(GTK_BOX(box), label); // The probe finds the button as the box's last child. It reaches the source only through // `post()`: the command runs inside the next `poll()`, on the producer thread. GtkWidget* poke = gtk_button_new_with_label("Poke"); gtk_box_append(GTK_BOX(box), poke); DataSource* src = &source; g_signal_connect_data( poke, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) { DataSource* s = static_cast(user); s->post([s] { static_cast(s)->note_applied(); }); }), src, nullptr, static_cast(0)); SourcePanel p; p.id = "poke"; p.title = "Probe"; p.widget = box; return {p}; } }; } // namespace std::unique_ptr make_probe_kind() { return std::make_unique(); } } // namespace udpscope