From 0d8c583bbbd184e56f77c2179fc73e5c54b23506 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Mon, 2 Nov 2026 12:19:23 +0100 Subject: [PATCH] e1608: a configurable generator update rate, and the achieved rate in the Analog out panel Co-Authored-By: Claude Opus 4.6 --- README.md | 8 +++- docs/claude/architecture.md | 2 +- docs/claude/build-and-run.md | 2 +- docs/claude/probe-readings.md | 15 ++++++- docs/claude/verification.md | 9 ++++ src/source/e1608/e1608_kind.cpp | 9 ++-- src/source/e1608/e1608_panels.cpp | 30 ++++++++++++- src/source/e1608/e1608_source.cpp | 31 +++++++++++++- src/source/e1608/e1608_source.hpp | 6 +++ src/ui.cpp | 3 ++ src/ui_app.hpp | 6 +++ src/ui_probe.cpp | 71 +++++++++++++++++++++++++++++++ 12 files changed, 181 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2e62249..ded6c36 100644 --- a/README.md +++ b/README.md @@ -123,11 +123,17 @@ click on a result fills the address, or **Use MAC** fills the MAC and leaves the saved: only the filled field is. A MAC with no subnet is looked up by a broadcast sent on every up interface: a single limited broadcast leaves through the default route only, and on 2026-11-02 it missed a real board on `net0` while the default route was `wlan0`. The form also groups the acquisition settings (scan rate, external clock, -trigger, restart after an overrun, factory calibration, the digital poll rate), a range per analog input, and the +trigger, restart after an overrun, factory calibration, the digital poll rate, the generator update rate), a range per analog input, and the direction and initial value of each digital line. The analog inputs are the plotted channels; each digital input and the counter is an extra channel only if you ask for it. Two side-stack tabs belong to it: **Analog out** (per DAC: a static level, or a sine, square, triangle or ramp from 0.1 to 10 Hz) and **Digital I/O** (a live LED per input bit, a switch per output bit, the counter with a reset); they are insensitive while no board is connected. +The waveform generator writes the DACs at the **generator update rate** (`gen_rate`, 1 to 200 Hz, default 50), which +is separate from the input poll rate (`poll_rate`, which only reads the digital inputs and the counter). Each update +is a synchronous command to the board, so a board that answers slowly achieves less than was asked: the **Analog out** +tab ends with an **Update rate** row that shows the rate the generator actually achieved (measured over about a +second) next to the requested one, or `off` when no generator runs. The waveform frequency itself is still limited +to 0.1 to 10 Hz. Acquisition is hardware-paced only (the scan rate or an external clock), the channels times the rate must stay at or below 250 kS/s, and the form refuses more. With the command-line options, a run that names no `aiN` key gets `ai0=BIP10V`; naming any `aiN` key drops that default, so `ai1=BIP5V` alone is channel 1 alone. diff --git a/docs/claude/architecture.md b/docs/claude/architecture.md index 9b21c8d..b295015 100644 --- a/docs/claude/architecture.md +++ b/docs/claude/architecture.md @@ -51,7 +51,7 @@ What feeds `Acquisition` is a `DataSource`, one of the registered *kinds*. never reaches the UI files. - The E-1608 source runs **everything inside `poll()`** on the producer thread: posted commands, the connect state machine (Down, Scanning, Halted), the scan read, the 500 ms status check (its not-running restart is skipped when a hardware trigger is configured), the digital - input/counter poll and the 20 ms waveform tick. There is no second thread. A connect attempt (discovery, + input/counter poll and the waveform tick (every `1 / gen_rate` s, 20 ms by default; one synchronous DAC write per running channel, so a slow board lowers the rate it achieves, which the Analog out panel reports). There is no second thread. A connect attempt (discovery, handshake, calibration read, scan start) can block `poll()` for as long as the command timeout allows each step; that delays only the command queue. Every exception is caught inside `poll()`: an escape would end the producer thread, and `drain_commands()` runs inside the same `try` for that reason. diff --git a/docs/claude/build-and-run.md b/docs/claude/build-and-run.md index 9cbff1a..4e78ed1 100644 --- a/docs/claude/build-and-run.md +++ b/docs/claude/build-and-run.md @@ -118,7 +118,7 @@ is exported when the option is absent, so GTK's own choice stands. `--source e1608` selects the MCC E-1608 Ethernet DAQ; its settings are `--source-opt` keys, all optional (`address` or `mac`+`subnet`, `port` 54211, `code` 0, `timeout_ms` 2000, `scan_rate` 1000 per channel, `ext_clock`, `trigger` `none|rising|falling|high|low`, `auto_restart` 1, `calibrate` 1, `poll_rate` 10, -`mode` `se|diff`, `ai0`..`ai7` `BIP10V|BIP5V|BIP2V|BIP1V` (an absent key = off, **and any `aiN` key at all +`gen_rate` 50 (Hz, 1 to 200: the waveform generator's DAC update rate), `mode` `se|diff`, `ai0`..`ai7` `BIP10V|BIP5V|BIP2V|BIP1V` (an absent key = off, **and any `aiN` key at all drops the default**: only a run with no `aiN` key gets `ai0=BIP10V`, so `ai1=BIP5V` alone is channel 1 alone, and a value that is not a range name is a channel that is off), `dir0`..`dir7` `i|o`, `plot0`..`plot7`, `plot_counter`, `do_init`, `ao0_init`, `ao1_init`). Either `address` or `mac` is diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index 0b58c9a..36faa0d 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -1838,7 +1838,7 @@ kind, so a run without it never shows that kind. The `form` line appears once pe - `kinds=udps,e1608,flatudp,probe`: a missing kind means `register_builtin_sources()` was not called before `parse_args()`, or the env var gate is wrong. - `round_trip=0`: a form's `write` and `read` disagree about a key -- a renamed `SourceConfig` key, or an - absent key that was not defaulted. `valid=0` is a default configuration the form refuses. The e1608 form's sample sets address, port, scan_rate, mode, ai1, trigger, dir1 and plot0; a mismatch names one of those. The flatudp sample sets port, bind, multicast, iface, rate, endian and signals. + absent key that was not defaulted. `valid=0` is a default configuration the form refuses. The e1608 form's sample sets address, port, scan_rate, mode, ai1, trigger, dir1, plot0 and gen_rate; a mismatch names one of those. The flatudp sample sets port, bind, multicast, iface, rate, endian and signals. - `panel disconnected: sensitive=0 present=1`: **disable, don't hide.** `present=0` is a panel that was hidden or never added; `sensitive=1` is a panel that stays live with no source behind it. - `panel connected: sensitive=1`: anything else means `apply_source_panels` acted on the wrong widget (it must @@ -1865,6 +1865,8 @@ The UDPS run has no `source-` page in `side_stack`, because the UDPS kind has no [e1608] scan socket closed by the board [e1608] reconnect: connects=2 connected=1 ok=1 [e1608] resumed: frames_before=612 frames_after=662 ok=1 +[e1608] generator: requested=100.0 achieved=99.7 panel='100 Hz (requested 100 Hz)' ok=1 +[e1608] generator stopped: achieved=0.0 panel='off (requested 100 Hz)' ok=1 [e1608] scan: found=1 ip=172.21.121.113 mac=00:80:2f:00:16:08 status='1 board found' fill_addr=1 fill_mac=1 rescan_replaced=1 ok=1 ``` @@ -1901,6 +1903,17 @@ the one `[e1608] handshake with ...: Connection refused` line, `stream: ... sign drop (neither the scan read nor the status check raised it), `resumed: ok=0` is a reconnect that did not restart the scan (it also reads 0 when `connects` is still 1, since a frame count that grows on a link that never dropped proves nothing). +- `generator: requested=100.0 achieved=99.7 ... ok=1`, `generator stopped: ... ok=1` + (`UDPSCOPE_PROBE_E1608_GEN=1500`, a run of its own with `gen_rate=100`): `requested` is `gen_rate`, `achieved` + the source's own measurement (generator ticks that wrote, per second of wall time, over a window of about 1 s), + `panel` the subtitle of the Analog out panel's "Update rate" row. `ok` needs `achieved` within 70% to 110% of + `requested` **and** the panel to quote the requested rate; the mock answers at once, so it reads within a + percent of `requested`. Far below is a `run_timers()` that re-arms with a constant (the 20 ms the setting + replaced reads 50) or a tick that runs late; `panel='off'` while `achieved` is non-zero is a panel that + did not refresh (it ticks only while its page is on screen) or one that reads the wrong readout. This line says + nothing about a real board, whose DAC writes are slower; there the same row is the measurement. The stopped + line must read `achieved=0.0` and a panel that starts with `off`; a non-zero value is a rate that + `update_gen_flags()` did not reset when the last generator stopped. - `scan: found=1 ... ok=1`: the form's **Scan network** row against the mock. `found` is the result rows after the first scan; `0` with the second line `did not answer` is an environment with no up, non-loopback broadcast interface that reaches the mock (or a refused broadcast), **not a pass**. `ip` is the listed address diff --git a/docs/claude/verification.md b/docs/claude/verification.md index 8b86b8e..92c7393 100644 --- a/docs/claude/verification.md +++ b/docs/claude/verification.md @@ -437,6 +437,15 @@ precedent, not an exception. levels `1.5, 2.5, 1.5` (the log prints the quantised voltage, so `1.4999, 2.4999, 1.4999` is the expected text), the DOUT bytes `0x00, 0x02, 0x00`, one `COUNTER_RESET`, one `DROP`; - **a rejected code:** mock `--code 1234`, app `code=7`: one error line, the retries stop. +- `UDPSCOPE_PROBE_E1608_GEN=1500` with `UDPSCOPE_EXIT_AFTER_MS=5500`, run as `--source e1608 --source-opt + address=127.0.0.1 --source-opt port=54711 --source-opt ao0_init=0 --source-opt gen_rate=100` against + `tools/mock_e1608.py --port 54711 --log /tmp/e1608-gen100.log` -- the generator's update rate: it starts a Sine on + DAC 0 through the Analog out panel (and switches the side stack to that page, since the panel refreshes on its + frame clock, which does not tick for a page that is off screen), waits 2.5 s, reads the achieved rate and the + panel's "Update rate" row, stops the generator and reads both again 400 ms later. Two lines, in + `probe-readings.md`. Then count the mock log's `AOUT` lines: about 250 with `gen_rate=100` (the generator ran + for about 2.5 s) and about 125 for a run without the option (default 50 Hz). Short and serial, the mock killed + afterwards. It enables nothing else, so it can run alone. - `UDPSCOPE_PROBE_E1608_RECONNECT=8000` -- reads the source after the mock dropped the link and 500 ms later checks the frame count moved on. The reading's own entry is in `probe-readings.md`. - `UDPSCOPE_PROBE_E1608_SCAN=1000` with `UDPSCOPE_EXIT_AFTER_MS=6500`, run as `--source e1608 --source-opt diff --git a/src/source/e1608/e1608_kind.cpp b/src/source/e1608/e1608_kind.cpp index 9ba2d16..7d3d851 100644 --- a/src/source/e1608/e1608_kind.cpp +++ b/src/source/e1608/e1608_kind.cpp @@ -77,7 +77,7 @@ bool on_of(GtkWidget* w) { return adw_switch_row_get_active(ADW_SWITCH_ROW(w)); /// closure shares; the raw widget pointers are valid while the chooser keeps the rows. struct Form { GtkWidget *address, *mac, *subnet, *port, *code, *timeout; - GtkWidget *rate, *ext_clock, *trigger, *auto_restart, *calibrate, *poll_rate; + GtkWidget *rate, *ext_clock, *trigger, *auto_restart, *calibrate, *poll_rate, *gen_rate; GtkWidget *mode, *ai[8], *ai_group; GtkWidget *dir[8], *do_init, *plot_counter, *ao_init[2]; ChangeCb changed = std::make_shared>(); @@ -121,6 +121,7 @@ struct Form { c.set("auto_restart", on_of(auto_restart) ? "1" : "0"); c.set("calibrate", on_of(calibrate) ? "1" : "0"); c.set("poll_rate", num(value_of(poll_rate))); + c.set("gen_rate", num(value_of(gen_rate))); c.set("mode", diff() ? "diff" : "se"); static const char* const kRange[] = {"", "BIP10V", "BIP5V", "BIP2V", "BIP1V"}; for (int i = 0; i < 8; ++i) { @@ -156,6 +157,7 @@ struct Form { adw_switch_row_set_active(ADW_SWITCH_ROW(auto_restart), s.auto_restart); adw_switch_row_set_active(ADW_SWITCH_ROW(calibrate), s.calibrate); adw_spin_row_set_value(ADW_SPIN_ROW(poll_rate), s.poll_rate); + adw_spin_row_set_value(ADW_SPIN_ROW(gen_rate), s.gen_rate); adw_combo_row_set_selected(ADW_COMBO_ROW(mode), s.diff ? 1 : 0); for (int i = 0; i < 8; ++i) { adw_combo_row_set_selected( @@ -356,6 +358,7 @@ class E1608Kind final : public SourceKind { f->auto_restart = switch_row("_Restart after an overrun", true); f->calibrate = switch_row("Apply _factory calibration", true); f->poll_rate = spin_row("_Input poll rate (Hz)", 0.1, 100, 1, 10, 1); + f->gen_rate = spin_row("_Generator update rate (Hz)", 1, 200, 1, 50, 0); f->mode = combo_row("Input _mode", kMode); for (int i = 0; i < 8; ++i) { const std::string title = "Channel " + std::to_string(i); @@ -384,7 +387,7 @@ class E1608Kind final : public SourceKind { GtkWidget* conn = expander("Connection", {f->scan_row, f->address, f->mac, f->subnet, f->port, f->code, f->timeout}); GtkWidget* acq = expander("Acquisition", {f->rate, f->ext_clock, f->trigger, f->auto_restart, - f->calibrate, f->poll_rate}); + f->calibrate, f->poll_rate, f->gen_rate}); GtkWidget* ai = expander("Analog inputs", {f->mode, f->ai[0], f->ai[1], f->ai[2], f->ai[3], f->ai[4], f->ai[5], f->ai[6], f->ai[7]}); GtkWidget* dio = expander("Digital I/O", {f->dir[0], f->dir[1], f->dir[2], f->dir[3], f->dir[4], @@ -395,7 +398,7 @@ class E1608Kind final : public SourceKind { for (GtkWidget* w : {conn, acq, ai, dio}) hook_destroy(w, f); for (GtkWidget* w : {f->address, f->mac, f->subnet}) hook_entry(w, f); - for (GtkWidget* w : {f->port, f->code, f->timeout, f->rate, f->poll_rate, f->do_init, + for (GtkWidget* w : {f->port, f->code, f->timeout, f->rate, f->poll_rate, f->gen_rate, f->do_init, f->ao_init[0], f->ao_init[1]}) hook_notify(w, "notify::value", f); for (GtkWidget* w : {f->ext_clock, f->auto_restart, f->calibrate, f->plot_counter}) diff --git a/src/source/e1608/e1608_panels.cpp b/src/source/e1608/e1608_panels.cpp index ecae53e..18408b3 100644 --- a/src/source/e1608/e1608_panels.cpp +++ b/src/source/e1608/e1608_panels.cpp @@ -55,7 +55,7 @@ void set_text(GtkWidget* label, const char* text) { if (std::strcmp(gtk_label_get_text(GTK_LABEL(label)), text) != 0) gtk_label_set_text(GTK_LABEL(label), text); } -GtkWidget* panel_root(GtkWidget* a, GtkWidget* b = nullptr) { +GtkWidget* panel_root(GtkWidget* a, GtkWidget* b = nullptr, GtkWidget* c = nullptr) { GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 18); gtk_widget_set_margin_top(box, 12); gtk_widget_set_margin_bottom(box, 12); @@ -63,6 +63,7 @@ GtkWidget* panel_root(GtkWidget* a, GtkWidget* b = nullptr) { gtk_widget_set_margin_end(box, 12); gtk_box_append(GTK_BOX(box), a); if (b != nullptr) gtk_box_append(GTK_BOX(box), b); + if (c != nullptr) gtk_box_append(GTK_BOX(box), c); return box; } @@ -77,6 +78,7 @@ struct AoCtx { E1608Source* src = nullptr; std::shared_ptr ro; AoWidgets w[2]; + GtkWidget* gen_rate = nullptr; ///< the "Update rate" row: the achieved rate against the requested one Hook hooks[2]; bool updating = false; ///< set while the panel writes its own widgets: those writes post nothing unsigned epoch_seen = 0; @@ -148,6 +150,22 @@ void ao_refresh(AoCtx* c) { if (cur == nullptr || std::strcmp(cur, b) != 0) adw_action_row_set_subtitle(ADW_ACTION_ROW(c->w[ch].last), b); } + + // The achieved generator rate: ticks completed per second, measured by the producer thread. Below + // the requested rate, the board's command latency is what limits it. + char r[64]; + const double req = c->src->settings().gen_rate; + const bool running = c->ro->gen_on[0].load() || c->ro->gen_on[1].load(); + const double hz = c->ro->gen_hz.load(); + if (!running) + std::snprintf(r, sizeof r, "off (requested %.0f Hz)", req); + else if (hz <= 0.0) + std::snprintf(r, sizeof r, "measuring... (requested %.0f Hz)", req); + else + std::snprintf(r, sizeof r, "%.0f Hz (requested %.0f Hz)", hz, req); + const char* cur = adw_action_row_get_subtitle(ADW_ACTION_ROW(c->gen_rate)); + if (cur == nullptr || std::strcmp(cur, r) != 0) + adw_action_row_set_subtitle(ADW_ACTION_ROW(c->gen_rate), r); } gboolean ao_tick(GtkWidget*, GdkFrameClock*, gpointer user) { @@ -198,7 +216,15 @@ GtkWidget* build_ao(E1608Source* src) { ao_sensitivity(w); } - GtkWidget* root = panel_root(groups[0], groups[1]); + c->gen_rate = adw_action_row_new(); + gtk_widget_set_name(c->gen_rate, "e1608-gen-rate"); + adw_preferences_row_set_title(ADW_PREFERENCES_ROW(c->gen_rate), "Update rate"); + adw_action_row_set_subtitle(ADW_ACTION_ROW(c->gen_rate), "off"); + GtkWidget* gen_group = adw_preferences_group_new(); + adw_preferences_group_set_title(ADW_PREFERENCES_GROUP(gen_group), "Generator"); + adw_preferences_group_add(ADW_PREFERENCES_GROUP(gen_group), c->gen_rate); + + GtkWidget* root = panel_root(groups[0], groups[1], gen_group); g_object_set_data_full(G_OBJECT(root), "e1608-ao-ctx", c, +[](gpointer p) { delete static_cast(p); }); gtk_widget_add_tick_callback(root, ao_tick, c, nullptr); diff --git a/src/source/e1608/e1608_source.cpp b/src/source/e1608/e1608_source.cpp index 7e85e95..fb66793 100644 --- a/src/source/e1608/e1608_source.cpp +++ b/src/source/e1608/e1608_source.cpp @@ -61,6 +61,7 @@ E1608Settings E1608Settings::from(const SourceConfig& c) { s.auto_restart = c.get_bool("auto_restart", true); s.calibrate = c.get_bool("calibrate", true); s.poll_rate = c.get_double("poll_rate", 10.0); + s.gen_rate = c.get_double("gen_rate", 50.0); s.diff = lower(c.get("mode", "se")) == "diff"; bool any_key = false; @@ -133,6 +134,7 @@ std::string E1608Settings::problem() const { } if (aggregate() < kMinAggregateHz) return "The aggregate rate must be at least 0.02 S/s"; if (!(poll_rate >= 0.1 && poll_rate <= 100.0)) return "The input poll rate must be 0.1 to 100 Hz"; + if (!(gen_rate >= 1.0 && gen_rate <= 200.0)) return "The generator update rate must be 1 to 200 Hz"; return {}; } @@ -230,6 +232,7 @@ void E1608Source::fail(const std::string& msg, bool fatal) { any_gen_ = false; readouts_->gen_on[0].store(false); readouts_->gen_on[1].store(false); + gen_rate_reset(); if (fatal) { state_ = State::Halted; } else { @@ -327,7 +330,7 @@ void E1608Source::connect_and_start() { const auto now = Clock::now(); next_slow_ = now; next_status_ = now + std::chrono::milliseconds(500); - next_gen_ = now + std::chrono::milliseconds(20); + next_gen_ = now + gen_period(); slow_read(); state_ = State::Scanning; @@ -409,7 +412,7 @@ void E1608Source::run_timers() { check_status(); } if (any_gen_ && now >= next_gen_) { - next_gen_ = now + std::chrono::milliseconds(20); + next_gen_ = now + gen_period(); // from now, not from the old deadline: a slow board gets no catch-up burst generator_tick(now); } } @@ -475,22 +478,46 @@ void E1608Source::write_ao(int ch, double volts) { readouts_->ao[ch].store(dev_.aout(ch, volts, cal_)); } +E1608Source::Clock::duration E1608Source::gen_period() const { + return std::chrono::duration_cast(std::chrono::duration(1.0 / cfg_.gen_rate)); +} + +void E1608Source::gen_rate_reset() { + gen_win_ticks_ = 0; + gen_win_t0_ = Clock::now(); + readouts_->gen_hz.store(0.0); +} + void E1608Source::update_gen_flags() { + const bool was_gen = any_gen_; any_gen_ = false; for (int ch = 0; ch < 2; ++ch) { const bool on = ao_[ch].mode != AoMode::Level && ao_[ch].run; readouts_->gen_on[ch].store(on); any_gen_ = any_gen_ || on; } + if (any_gen_ != was_gen) gen_rate_reset(); // a generator started or the last one stopped } void E1608Source::generator_tick(Clock::time_point now) { + bool wrote = false; for (int ch = 0; ch < 2; ++ch) { const AoSettings& s = ao_[ch]; if (s.mode == AoMode::Level || !s.run) continue; const double t = std::chrono::duration(now - gen_t0_[ch]).count(); const double phase = std::fmod(t * s.freq_hz, 1.0); write_ao(ch, clamp(s.offset + s.amplitude * wave(s.mode, phase), -10.0, 10.0)); + wrote = true; + } + if (!wrote) return; + // The achieved rate: ticks that wrote, per second of wall time, published once a window closes. + // The DAC writes are synchronous, so a slow board shows up here as a rate below the requested one. + ++gen_win_ticks_; + const double elapsed = std::chrono::duration(Clock::now() - gen_win_t0_).count(); + if (elapsed >= 1.0) { + readouts_->gen_hz.store(gen_win_ticks_ / elapsed); + gen_win_ticks_ = 0; + gen_win_t0_ = Clock::now(); } } diff --git a/src/source/e1608/e1608_source.hpp b/src/source/e1608/e1608_source.hpp index 81bb1e7..dfc7ad3 100644 --- a/src/source/e1608/e1608_source.hpp +++ b/src/source/e1608/e1608_source.hpp @@ -36,6 +36,7 @@ struct E1608Settings { bool auto_restart = true; bool calibrate = true; double poll_rate = 10.0; ///< Hz, for the digital inputs and the counter + double gen_rate = 50.0; ///< Hz, the waveform generator's DAC update rate bool diff = false; bool ai_on[8] = {}; Range ai_range[8] = {}; @@ -62,6 +63,7 @@ struct E1608Readouts { std::atomic counter{0}; std::atomic ao[2]; ///< the last DAC voltage written; NaN before any std::atomic gen_on[2]; + std::atomic gen_hz{0.0}; ///< generator ticks completed per second, over ~1 s; 0 when off std::atomic epoch{0}; ///< bumped at each (re)connect: panels re-sync from it E1608Readouts() { for (int i = 0; i < 2; ++i) { @@ -117,6 +119,8 @@ class E1608Source final : public DataSource { void generator_tick(Clock::time_point now); void write_ao(int ch, double volts); void update_gen_flags(); + void gen_rate_reset(); + Clock::duration gen_period() const; void fail(const std::string& msg, bool fatal); void note(const std::string& msg); int wait_ms(int timeout_ms, Clock::time_point now) const; @@ -153,6 +157,8 @@ class E1608Source final : public DataSource { AoSettings ao_[2]; Clock::time_point gen_t0_[2]; bool any_gen_ = false; + Clock::time_point gen_win_t0_; ///< start of the current rate-measurement window + unsigned gen_win_ticks_ = 0; ///< ticks that wrote since `gen_win_t0_` Clock::time_point retry_at_; Clock::time_point next_slow_, next_status_, next_gen_; diff --git a/src/ui.cpp b/src/ui.cpp index 632d46e..c6c715d 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -6034,6 +6034,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit 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_GEN")) { + g_timeout_add(static_cast(std::atoi(env)), e1608_gen_probe, &app); + } if (const char* env = g_getenv("UDPSCOPE_PROBE_E1608_RECONNECT")) { g_timeout_add(static_cast(std::atoi(env)), e1608_probe_reconnect, &app); } diff --git a/src/ui_app.hpp b/src/ui_app.hpp index 414f087..8437d53 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -1881,6 +1881,12 @@ gboolean e1608_probe(gpointer user); gboolean e1608_probe_report(gpointer user); gboolean e1608_probe_reconnect(gpointer user); gboolean e1608_probe_resumed(gpointer user); +/// The generator's update rate: starts a Sine on DAC 0 through the panel, reads the achieved rate the +/// source measured and the Analog out panel's "Update rate" row (`_report`, 2.5 s later), then stops +/// it and checks both read "off" (`_stopped`). +gboolean e1608_gen_probe(gpointer user); +gboolean e1608_gen_probe_report(gpointer user); +gboolean e1608_gen_probe_stopped(gpointer user); /// The E-1608 form's "Scan network" row: presses Scan against the mock, then (phases `_fill`, /// `_rescan`) clicks a result to fill the address, "Use MAC" to fill the MAC, scans again and checks /// the results were replaced, not added to. diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index d2a9bbc..fe5dc8c 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -1923,6 +1923,7 @@ gboolean source_kinds_probe(gpointer user) { c.set("trigger", "rising"); c.set("dir1", "o"); c.set("plot0", "1"); + c.set("gen_rate", "120"); } else if (id == "flatudp") { c.set("port", "44600"); c.set("bind", "10.0.0.8"); @@ -2428,6 +2429,76 @@ gboolean e1608_probe(gpointer user) { return G_SOURCE_REMOVE; } +/// Phase 3 of the generator probe: the generator switched off 400 ms ago; the achieved rate and the +/// panel must both have gone back to "off". +gboolean e1608_gen_probe_stopped(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; + GtkWidget* row = find_named(source_panel_content(a, "source-ao"), "e1608-gen-rate"); + const char* sub = row != nullptr ? adw_action_row_get_subtitle(ADW_ACTION_ROW(row)) : nullptr; + const double hz = src->readouts()->gen_hz.load(); + const bool off = sub != nullptr && std::strncmp(sub, "off", 3) == 0; + std::fprintf(stderr, "[e1608] generator stopped: achieved=%.1f panel='%s' ok=%d\n", hz, + sub != nullptr ? sub : "", hz == 0.0 && off ? 1 : 0); + std::fflush(stderr); + return G_SOURCE_REMOVE; +} + +/// Phase 2 of the generator probe, 2.5 s after a Sine generator was started on DAC 0: the achieved +/// update rate the source measured, and the text the Analog out panel shows for it. +gboolean e1608_gen_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; + GtkWidget* ao = source_panel_content(a, "source-ao"); + GtkWidget* row = find_named(ao, "e1608-gen-rate"); + const char* sub = row != nullptr ? adw_action_row_get_subtitle(ADW_ACTION_ROW(row)) : nullptr; + const double req = src->settings().gen_rate; + const double hz = src->readouts()->gen_hz.load(); + // The mock answers at once, so the achieved rate sits at the requested one; the band is loose. + const bool in_range = hz >= 0.7 * req && hz <= 1.1 * req; + char want[32]; + std::snprintf(want, sizeof want, "(requested %.0f Hz)", req); + const bool shown = sub != nullptr && std::strstr(sub, want) != nullptr && std::strstr(sub, "Hz") != nullptr; + std::fprintf(stderr, "[e1608] generator: requested=%.1f achieved=%.1f panel='%s' ok=%d\n", req, hz, + sub != nullptr ? sub : "", in_range && shown ? 1 : 0); + std::fflush(stderr); + GtkWidget* run = find_named(ao, "e1608-ao0-run"); + if (run != nullptr) adw_switch_row_set_active(ADW_SWITCH_ROW(run), FALSE); + g_timeout_add(400, e1608_gen_probe_stopped, a); + return G_SOURCE_REMOVE; +} + +/// Starts a Sine generator on DAC 0 through the panel's own controls, then reads it 2.5 s later. +gboolean e1608_gen_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] generator: no e1608 source (run with --source e1608)\n"); + std::fflush(stderr); + return G_SOURCE_REMOVE; + } + GtkWidget* ao = source_panel_content(a, "source-ao"); + GtkWidget* mode = find_named(ao, "e1608-ao0-mode"); + GtkWidget* run = find_named(ao, "e1608-ao0-run"); + if (mode == nullptr || run == nullptr) { + std::fprintf(stderr, "[e1608] generator: controls missing (mode=%d run=%d)\n", mode != nullptr ? 1 : 0, + run != nullptr ? 1 : 0); + std::fflush(stderr); + return G_SOURCE_REMOVE; + } + // The panel refreshes on its frame clock, which only ticks while the page is on screen. + if (a->side_stack != nullptr) adw_view_stack_set_visible_child_name(ADW_VIEW_STACK(a->side_stack), "source-ao"); + adw_combo_row_set_selected(ADW_COMBO_ROW(mode), 1); // Sine + adw_switch_row_set_active(ADW_SWITCH_ROW(run), TRUE); + g_timeout_add(2500, e1608_gen_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);