diff --git a/src/ui.cpp b/src/ui.cpp index e8dfaaa..bb04d6a 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -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(std::atoi(env)), source_kinds_probe, &app); } + if (const char* env = g_getenv("UDPSCOPE_PROBE_E1608")) { + g_timeout_add(static_cast(std::atoi(env)), e1608_probe, &app); + } + if (const char* env = g_getenv("UDPSCOPE_PROBE_E1608_RECONNECT")) { + g_timeout_add(static_cast(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(). diff --git a/src/ui_app.hpp b/src/ui_app.hpp index 9fc6106..a92a403 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -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); diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index 64c0fe7..510608b 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -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(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(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 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> 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(i)] - want0[i]) > 1e-9) return false; + if (std::fabs(out[1][static_cast(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(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& 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(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(std::lround(2.0 * s.scan_rate / 5.0)); + if (want > 0 && n > want + 50) { + std::vector v(static_cast(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(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 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(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(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(user); if (a->window == nullptr || a->rows->row_count() <= 0) return G_SOURCE_REMOVE;