Feature/source plugins #1

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

No files matched your search

+7 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+14 -1
View File
@@ -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
+9
View File
@@ -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
+6 -3
View File
@@ -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<std::function<void()>>();
@@ -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})
+28 -2
View File
@@ -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<E1608Readouts> 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<AoCtx*>(p); });
gtk_widget_add_tick_callback(root, ao_tick, c, nullptr);
+29 -2
View File
@@ -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<Clock::duration>(std::chrono::duration<double>(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<double>(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<double>(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();
}
}
+6
View File
@@ -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<std::uint32_t> counter{0};
std::atomic<double> ao[2]; ///< the last DAC voltage written; NaN before any
std::atomic<bool> gen_on[2];
std::atomic<double> gen_hz{0.0}; ///< generator ticks completed per second, over ~1 s; 0 when off
std::atomic<unsigned> 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_;
+3
View File
@@ -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<guint>(std::atoi(env)), e1608_probe, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_E1608_GEN")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), e1608_gen_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);
}
+6
View File
@@ -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.
+71
View File
@@ -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<App*>(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<App*>(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<App*>(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<App*>(user);