Feature/source plugins #1

Merged
martino merged 130 commits from feature/source-plugins into main 2026-11-04 07:35:21 +01:00
3 changed files with 201 additions and 1 deletions
Showing only changes of commit 2e9f9664cf - Show all commits

No files matched your search

+6
View File
@@ -6112,6 +6112,12 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
if (const char* env = g_getenv("UDPSCOPE_PROBE_SOURCE_KINDS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), source_kinds_probe, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_E1608")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), e1608_probe, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_E1608_RECONNECT")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), e1608_probe_reconnect, &app);
}
// Our own options were consumed by parse_args(); GTK would reject them, so only
// the program name is handed to g_application_run().
+7
View File
@@ -1876,6 +1876,13 @@ gboolean side_tab_probe_report(gpointer user);
gboolean side_tab_probe(gpointer user);
gboolean source_kinds_probe_report(gpointer user);
gboolean source_kinds_probe(gpointer user);
/// MCC E-1608 end to end against `tools/mock_e1608.py`: the stream, the calibration, the panels and
/// their three controls, the capture button and a hand-checked decode. `_report` is the second phase.
/// `_reconnect` reads the state after the mock dropped the link; `_resumed` shows the stream moved on.
gboolean e1608_probe(gpointer user);
gboolean e1608_probe_report(gpointer user);
gboolean e1608_probe_reconnect(gpointer user);
gboolean e1608_probe_resumed(gpointer user);
gboolean pan_probe(gpointer user);
gboolean arm_probe(gpointer user);
gboolean trigrenum_probe_step3(gpointer user);
+188 -1
View File
@@ -7,6 +7,7 @@
// live in a translation unit of their own.
#include "source/probe_source.hpp"
#include "source/e1608/e1608_source.hpp"
#include "source/registry.hpp"
#include "ui_app.hpp"
@@ -1927,6 +1928,15 @@ gboolean source_kinds_probe(gpointer user) {
c.set("interface", "10.0.0.9");
} else if (id == "probe") {
c.set("rate", "750");
} else if (id == "e1608") {
c.set("address", "10.0.0.5");
c.set("port", "54300");
c.set("scan_rate", "2000");
c.set("mode", "diff");
c.set("ai1", "BIP5V");
c.set("trigger", "rising");
c.set("dir1", "o");
c.set("plot0", "1");
}
return c;
};
@@ -1970,7 +1980,7 @@ gboolean source_kinds_probe(gpointer user) {
// Click the panel's button: it posts a command, and a later `poll()` runs it.
ProbeSource* src = a->acq != nullptr ? dynamic_cast<ProbeSource*>(a->acq->source()) : nullptr;
g_source_kinds_before = src != nullptr ? src->commands_applied() : 0;
if (page != nullptr) {
if (page != nullptr && src != nullptr) {
if (GtkWidget* poke = gtk_widget_get_last_child(page->content)) {
g_signal_emit_by_name(poke, "clicked");
}
@@ -1980,6 +1990,183 @@ gboolean source_kinds_probe(gpointer user) {
return G_SOURCE_REMOVE;
}
namespace {
/// The first widget named `name` at or under `root` (the panels name their controls).
GtkWidget* find_named(GtkWidget* root, const char* name) {
if (root == nullptr) return nullptr;
if (std::strcmp(gtk_widget_get_name(root), name) == 0) return root;
for (GtkWidget* c = gtk_widget_get_first_child(root); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
if (GtkWidget* f = find_named(c, name)) return f;
}
return nullptr;
}
GtkWidget* source_panel_content(App* a, const char* page) {
for (const SourcePanelPage& p : a->source_panels) {
if (p.name == page) return p.content;
}
return nullptr;
}
e1608::E1608Source* e1608_source(App* a) {
return a->acq != nullptr ? dynamic_cast<e1608::E1608Source*>(a->acq->source()) : nullptr;
}
std::uint64_t g_e1608_frames_before = 0;
/// A fixed raw buffer decoded against volts worked out by hand: single-ended, ch0 on BIP10V with
/// intercept 100 (slope 1), ch1 on BIP5V uncalibrated. `(code - 32768) * fs / 32768`, the code
/// clamped to 0..65535.
bool e1608_decode_check() {
using namespace e1608;
Calibration cal;
cal.se[0] = {1.0, 100.0};
const std::vector<QueueEntry> q = {{0, Range::Bip10V}, {1, Range::Bip5V}};
const std::uint16_t raw[] = {32668, 49152, 49052, 16384, 65535, 0}; // 3 scans x 2 channels
std::vector<std::vector<double>> out;
decode_scans(raw, 3, q, cal, out);
const double want0[] = {0.0, 5.0, 32767.0 * 10.0 / 32768.0};
const double want1[] = {2.5, -2.5, -5.0};
if (out.size() != 2 || out[0].size() != 3 || out[1].size() != 3) return false;
for (int i = 0; i < 3; ++i) {
if (std::fabs(out[0][static_cast<std::size_t>(i)] - want0[i]) > 1e-9) return false;
if (std::fabs(out[1][static_cast<std::size_t>(i)] - want1[i]) > 1e-9) return false;
}
return true;
}
} // namespace
/// Phase 2, 1 s after the controls were driven: the stream, the calibration, the capture button,
/// the source's state and the decode.
gboolean e1608_probe_report(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
e1608::E1608Source* src = e1608_source(a);
if (src == nullptr) return G_SOURCE_REMOVE;
const e1608::E1608Settings& s = src->settings();
const bool up = a->model->stats().connected.load(std::memory_order_relaxed);
const std::vector<SignalInfo>& sigs = a->model->signals();
std::size_t plotted = 0;
for (bool p : s.plot) plotted += p ? 1 : 0;
const std::size_t expect =
static_cast<std::size_t>(s.active_channels()) + plotted + (s.plot_counter ? 1 : 0);
std::string names;
for (const SignalInfo& si : sigs) names += (names.empty() ? "" : ",") + si.name;
const double rate = sigs.empty() ? 0.0 : sigs.front().sampling_rate;
const std::uint64_t frames = a->model->stats().frames.load(std::memory_order_relaxed);
const bool stream_ok = up && sigs.size() == expect && std::fabs(rate - s.scan_rate) < 1e-6 &&
frames > 0;
std::fprintf(stderr,
"[e1608] stream: connected=%d signals=%zu expect=%zu names=%s frames=%" G_GUINT64_FORMAT
" rate=%.3f ok=%d\n",
up ? 1 : 0, sigs.size(), expect, names.c_str(), frames, rate, stream_ok ? 1 : 0);
// The mock's AI0 is 0.5 V DC under a 5 Hz sine: two periods average to the DC.
double mean = std::nan("");
if (up && a->model->channel_count() > 0 && !a->model->channels().empty() &&
a->model->channels().front().name == "AI0") {
const ChannelBuffer& ch = a->model->live().channel(0);
const u64 n = ch.size();
const u64 want = static_cast<u64>(std::lround(2.0 * s.scan_rate / 5.0));
if (want > 0 && n > want + 50) {
std::vector<f32> v(static_cast<std::size_t>(want));
ch.raw(n - want - 50, n - 50, v.data()); // stay clear of the head the producer is writing
double sum = 0.0;
for (f32 x : v) sum += double(x);
mean = sum / double(v.size());
}
}
std::fprintf(stderr, "[e1608] calibration: ai0_mean=%.4f expect=0.5000 ok=%d\n", mean,
std::fabs(mean - 0.5) < 0.02 ? 1 : 0);
std::fprintf(stderr, "[e1608] capture button: present=%d sensitive=%d\n",
a->capture_button != nullptr ? 1 : 0,
a->capture_button != nullptr && gtk_widget_get_sensitive(a->capture_button) ? 1 : 0);
std::fprintf(stderr, "[e1608] state: connects=%u message='%s'\n", src->connects(),
src->last_message().c_str());
std::fprintf(stderr, "[e1608] decode: ok=%d\n", e1608_decode_check() ? 1 : 0);
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
/// Phase 1: the panels, then the three controls (a DAC level, a digital output, the counter reset).
gboolean e1608_probe(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
e1608::E1608Source* src = e1608_source(a);
if (src == nullptr) {
std::fprintf(stderr, "[e1608] no e1608 source (run with --source e1608)\n");
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
const bool up = a->model->stats().connected.load(std::memory_order_relaxed);
for (const char* id : {"ao", "dio"}) {
const std::string page = std::string("source-") + id;
GtkWidget* content = source_panel_content(a, page.c_str());
const bool present =
content != nullptr && a->side_stack != nullptr &&
adw_view_stack_get_child_by_name(ADW_VIEW_STACK(a->side_stack), page.c_str()) != nullptr;
std::fprintf(stderr, "[e1608] panel %s: present=%d sensitive=%d connected=%d\n", id,
present ? 1 : 0, content != nullptr && gtk_widget_get_sensitive(content) ? 1 : 0,
up ? 1 : 0);
}
if (up) {
// All the stream's signals, in one structural change; the plots do not matter to these readings.
std::vector<std::size_t> all(a->model->signals().size());
for (std::size_t i = 0; i < all.size(); ++i) all[i] = i;
if (!all.empty()) a->model->set_signals_enabled(all, true);
GtkWidget* ao = source_panel_content(a, "source-ao");
GtkWidget* dio = source_panel_content(a, "source-dio");
GtkWidget* level = find_named(ao, "e1608-ao0-level");
GtkWidget* out1 = find_named(dio, "e1608-do1");
GtkWidget* reset = find_named(dio, "e1608-counter-reset");
// 2.5 V must differ from the configured initial level, or the spin row notifies nothing.
if (level != nullptr) adw_spin_row_set_value(ADW_SPIN_ROW(level), 2.5);
if (out1 != nullptr) adw_switch_row_set_active(ADW_SWITCH_ROW(out1), TRUE);
if (reset != nullptr) g_signal_emit_by_name(reset, "clicked");
std::fprintf(stderr, "[e1608] actions: ao0_level=%d do1=%d counter_reset=%d\n",
level != nullptr ? 1 : 0, out1 != nullptr ? 1 : 0, reset != nullptr ? 1 : 0);
} else {
std::fprintf(stderr, "[e1608] actions: skipped (not connected)\n");
}
std::fflush(stderr);
g_timeout_add(1000, e1608_probe_report, a);
return G_SOURCE_REMOVE;
}
/// After the mock dropped the link: has the source reconnected, and does the stream move again?
gboolean e1608_probe_resumed(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
const std::uint64_t after = a->model->stats().frames.load(std::memory_order_relaxed);
std::fprintf(stderr, "[e1608] resumed: frames_before=%" G_GUINT64_FORMAT " frames_after=%" G_GUINT64_FORMAT
" ok=%d\n",
g_e1608_frames_before, after, after > g_e1608_frames_before ? 1 : 0);
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
gboolean e1608_probe_reconnect(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
e1608::E1608Source* src = e1608_source(a);
if (src == nullptr) return G_SOURCE_REMOVE;
const bool up = a->model->stats().connected.load(std::memory_order_relaxed);
g_e1608_frames_before = a->model->stats().frames.load(std::memory_order_relaxed);
std::fprintf(stderr, "[e1608] reconnect: connects=%u connected=%d ok=%d\n", src->connects(),
up ? 1 : 0, src->connects() >= 2 && up ? 1 : 0);
std::fflush(stderr);
g_timeout_add(500, e1608_probe_resumed, a);
return G_SOURCE_REMOVE;
}
gboolean pan_probe(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr || a->rows->row_count() <= 0) return G_SOURCE_REMOVE;