166 lines
5.3 KiB
C++
166 lines
5.3 KiB
C++
// 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;
|
|
}
|
|
|
|
std::vector<SourcePanel> 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<DataSource*>(user);
|
|
s->post([s] { static_cast<ProbeSource*>(s)->note_applied(); });
|
|
}),
|
|
src, nullptr, static_cast<GConnectFlags>(0));
|
|
SourcePanel p;
|
|
p.id = "poke";
|
|
p.title = "Probe";
|
|
p.widget = box;
|
|
return {p};
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
std::unique_ptr<SourceKind> make_probe_kind() { return std::make_unique<ProbeKind>(); }
|
|
|
|
} // namespace udpscope
|