e1608: a configurable generator update rate, and the achieved rate in the Analog out panel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
0c6e69957d
commit
0d8c583bbb
12 files changed
+181
-11
No files matched your search
@@ -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
|
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
|
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,
|
`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
|
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**
|
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
|
(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.
|
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
|
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
|
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.
|
`ai0=BIP10V`; naming any `aiN` key drops that default, so `ai1=BIP5V` alone is channel 1 alone.
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ What feeds `Acquisition` is a `DataSource`, one of the registered *kinds*.
|
|||||||
never reaches the UI files.
|
never reaches the UI files.
|
||||||
- The E-1608 source runs **everything inside `poll()`** on the producer thread: posted commands, the
|
- 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
|
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
|
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
|
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.
|
the producer thread, and `drain_commands()` runs inside the same `try` for that reason.
|
||||||
|
|||||||
@@ -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
|
`--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,
|
(`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,
|
`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
|
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`,
|
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
|
`plot0`..`plot7`, `plot_counter`, `do_init`, `ao0_init`, `ao1_init`). Either `address` or `mac` is
|
||||||
|
|||||||
@@ -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()`,
|
- `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.
|
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
|
- `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
|
- `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.
|
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
|
- `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] scan socket closed by the board
|
||||||
[e1608] reconnect: connects=2 connected=1 ok=1
|
[e1608] reconnect: connects=2 connected=1 ok=1
|
||||||
[e1608] resumed: frames_before=612 frames_after=662 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
|
[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
|
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
|
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).
|
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
|
- `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
|
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
|
broadcast interface that reaches the mock (or a refused broadcast), **not a pass**. `ip` is the listed address
|
||||||
|
|||||||
@@ -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
|
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`;
|
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.
|
- **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
|
- `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`.
|
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
|
- `UDPSCOPE_PROBE_E1608_SCAN=1000` with `UDPSCOPE_EXIT_AFTER_MS=6500`, run as `--source e1608 --source-opt
|
||||||
|
|||||||
@@ -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.
|
/// closure shares; the raw widget pointers are valid while the chooser keeps the rows.
|
||||||
struct Form {
|
struct Form {
|
||||||
GtkWidget *address, *mac, *subnet, *port, *code, *timeout;
|
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 *mode, *ai[8], *ai_group;
|
||||||
GtkWidget *dir[8], *do_init, *plot_counter, *ao_init[2];
|
GtkWidget *dir[8], *do_init, *plot_counter, *ao_init[2];
|
||||||
ChangeCb changed = std::make_shared<std::function<void()>>();
|
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("auto_restart", on_of(auto_restart) ? "1" : "0");
|
||||||
c.set("calibrate", on_of(calibrate) ? "1" : "0");
|
c.set("calibrate", on_of(calibrate) ? "1" : "0");
|
||||||
c.set("poll_rate", num(value_of(poll_rate)));
|
c.set("poll_rate", num(value_of(poll_rate)));
|
||||||
|
c.set("gen_rate", num(value_of(gen_rate)));
|
||||||
c.set("mode", diff() ? "diff" : "se");
|
c.set("mode", diff() ? "diff" : "se");
|
||||||
static const char* const kRange[] = {"", "BIP10V", "BIP5V", "BIP2V", "BIP1V"};
|
static const char* const kRange[] = {"", "BIP10V", "BIP5V", "BIP2V", "BIP1V"};
|
||||||
for (int i = 0; i < 8; ++i) {
|
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(auto_restart), s.auto_restart);
|
||||||
adw_switch_row_set_active(ADW_SWITCH_ROW(calibrate), s.calibrate);
|
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(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);
|
adw_combo_row_set_selected(ADW_COMBO_ROW(mode), s.diff ? 1 : 0);
|
||||||
for (int i = 0; i < 8; ++i) {
|
for (int i = 0; i < 8; ++i) {
|
||||||
adw_combo_row_set_selected(
|
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->auto_restart = switch_row("_Restart after an overrun", true);
|
||||||
f->calibrate = switch_row("Apply _factory calibration", 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->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);
|
f->mode = combo_row("Input _mode", kMode);
|
||||||
for (int i = 0; i < 8; ++i) {
|
for (int i = 0; i < 8; ++i) {
|
||||||
const std::string title = "Channel " + std::to_string(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,
|
GtkWidget* conn = expander("Connection", {f->scan_row, f->address, f->mac, f->subnet, f->port,
|
||||||
f->code, f->timeout});
|
f->code, f->timeout});
|
||||||
GtkWidget* acq = expander("Acquisition", {f->rate, f->ext_clock, f->trigger, f->auto_restart,
|
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],
|
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]});
|
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],
|
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 : {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->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]})
|
f->ao_init[0], f->ao_init[1]})
|
||||||
hook_notify(w, "notify::value", f);
|
hook_notify(w, "notify::value", f);
|
||||||
for (GtkWidget* w : {f->ext_clock, f->auto_restart, f->calibrate, f->plot_counter})
|
for (GtkWidget* w : {f->ext_clock, f->auto_restart, f->calibrate, f->plot_counter})
|
||||||
|
|||||||
@@ -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);
|
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);
|
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 18);
|
||||||
gtk_widget_set_margin_top(box, 12);
|
gtk_widget_set_margin_top(box, 12);
|
||||||
gtk_widget_set_margin_bottom(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_widget_set_margin_end(box, 12);
|
||||||
gtk_box_append(GTK_BOX(box), a);
|
gtk_box_append(GTK_BOX(box), a);
|
||||||
if (b != nullptr) gtk_box_append(GTK_BOX(box), b);
|
if (b != nullptr) gtk_box_append(GTK_BOX(box), b);
|
||||||
|
if (c != nullptr) gtk_box_append(GTK_BOX(box), c);
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +78,7 @@ struct AoCtx {
|
|||||||
E1608Source* src = nullptr;
|
E1608Source* src = nullptr;
|
||||||
std::shared_ptr<E1608Readouts> ro;
|
std::shared_ptr<E1608Readouts> ro;
|
||||||
AoWidgets w[2];
|
AoWidgets w[2];
|
||||||
|
GtkWidget* gen_rate = nullptr; ///< the "Update rate" row: the achieved rate against the requested one
|
||||||
Hook hooks[2];
|
Hook hooks[2];
|
||||||
bool updating = false; ///< set while the panel writes its own widgets: those writes post nothing
|
bool updating = false; ///< set while the panel writes its own widgets: those writes post nothing
|
||||||
unsigned epoch_seen = 0;
|
unsigned epoch_seen = 0;
|
||||||
@@ -148,6 +150,22 @@ void ao_refresh(AoCtx* c) {
|
|||||||
if (cur == nullptr || std::strcmp(cur, b) != 0)
|
if (cur == nullptr || std::strcmp(cur, b) != 0)
|
||||||
adw_action_row_set_subtitle(ADW_ACTION_ROW(c->w[ch].last), b);
|
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) {
|
gboolean ao_tick(GtkWidget*, GdkFrameClock*, gpointer user) {
|
||||||
@@ -198,7 +216,15 @@ GtkWidget* build_ao(E1608Source* src) {
|
|||||||
ao_sensitivity(w);
|
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,
|
g_object_set_data_full(G_OBJECT(root), "e1608-ao-ctx", c,
|
||||||
+[](gpointer p) { delete static_cast<AoCtx*>(p); });
|
+[](gpointer p) { delete static_cast<AoCtx*>(p); });
|
||||||
gtk_widget_add_tick_callback(root, ao_tick, c, nullptr);
|
gtk_widget_add_tick_callback(root, ao_tick, c, nullptr);
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ E1608Settings E1608Settings::from(const SourceConfig& c) {
|
|||||||
s.auto_restart = c.get_bool("auto_restart", true);
|
s.auto_restart = c.get_bool("auto_restart", true);
|
||||||
s.calibrate = c.get_bool("calibrate", true);
|
s.calibrate = c.get_bool("calibrate", true);
|
||||||
s.poll_rate = c.get_double("poll_rate", 10.0);
|
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";
|
s.diff = lower(c.get("mode", "se")) == "diff";
|
||||||
|
|
||||||
bool any_key = false;
|
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 (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 (!(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 {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +232,7 @@ void E1608Source::fail(const std::string& msg, bool fatal) {
|
|||||||
any_gen_ = false;
|
any_gen_ = false;
|
||||||
readouts_->gen_on[0].store(false);
|
readouts_->gen_on[0].store(false);
|
||||||
readouts_->gen_on[1].store(false);
|
readouts_->gen_on[1].store(false);
|
||||||
|
gen_rate_reset();
|
||||||
if (fatal) {
|
if (fatal) {
|
||||||
state_ = State::Halted;
|
state_ = State::Halted;
|
||||||
} else {
|
} else {
|
||||||
@@ -327,7 +330,7 @@ void E1608Source::connect_and_start() {
|
|||||||
const auto now = Clock::now();
|
const auto now = Clock::now();
|
||||||
next_slow_ = now;
|
next_slow_ = now;
|
||||||
next_status_ = now + std::chrono::milliseconds(500);
|
next_status_ = now + std::chrono::milliseconds(500);
|
||||||
next_gen_ = now + std::chrono::milliseconds(20);
|
next_gen_ = now + gen_period();
|
||||||
slow_read();
|
slow_read();
|
||||||
|
|
||||||
state_ = State::Scanning;
|
state_ = State::Scanning;
|
||||||
@@ -409,7 +412,7 @@ void E1608Source::run_timers() {
|
|||||||
check_status();
|
check_status();
|
||||||
}
|
}
|
||||||
if (any_gen_ && now >= next_gen_) {
|
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);
|
generator_tick(now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -475,22 +478,46 @@ void E1608Source::write_ao(int ch, double volts) {
|
|||||||
readouts_->ao[ch].store(dev_.aout(ch, volts, cal_));
|
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() {
|
void E1608Source::update_gen_flags() {
|
||||||
|
const bool was_gen = any_gen_;
|
||||||
any_gen_ = false;
|
any_gen_ = false;
|
||||||
for (int ch = 0; ch < 2; ++ch) {
|
for (int ch = 0; ch < 2; ++ch) {
|
||||||
const bool on = ao_[ch].mode != AoMode::Level && ao_[ch].run;
|
const bool on = ao_[ch].mode != AoMode::Level && ao_[ch].run;
|
||||||
readouts_->gen_on[ch].store(on);
|
readouts_->gen_on[ch].store(on);
|
||||||
any_gen_ = any_gen_ || 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) {
|
void E1608Source::generator_tick(Clock::time_point now) {
|
||||||
|
bool wrote = false;
|
||||||
for (int ch = 0; ch < 2; ++ch) {
|
for (int ch = 0; ch < 2; ++ch) {
|
||||||
const AoSettings& s = ao_[ch];
|
const AoSettings& s = ao_[ch];
|
||||||
if (s.mode == AoMode::Level || !s.run) continue;
|
if (s.mode == AoMode::Level || !s.run) continue;
|
||||||
const double t = std::chrono::duration<double>(now - gen_t0_[ch]).count();
|
const double t = std::chrono::duration<double>(now - gen_t0_[ch]).count();
|
||||||
const double phase = std::fmod(t * s.freq_hz, 1.0);
|
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));
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ struct E1608Settings {
|
|||||||
bool auto_restart = true;
|
bool auto_restart = true;
|
||||||
bool calibrate = true;
|
bool calibrate = true;
|
||||||
double poll_rate = 10.0; ///< Hz, for the digital inputs and the counter
|
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 diff = false;
|
||||||
bool ai_on[8] = {};
|
bool ai_on[8] = {};
|
||||||
Range ai_range[8] = {};
|
Range ai_range[8] = {};
|
||||||
@@ -62,6 +63,7 @@ struct E1608Readouts {
|
|||||||
std::atomic<std::uint32_t> counter{0};
|
std::atomic<std::uint32_t> counter{0};
|
||||||
std::atomic<double> ao[2]; ///< the last DAC voltage written; NaN before any
|
std::atomic<double> ao[2]; ///< the last DAC voltage written; NaN before any
|
||||||
std::atomic<bool> gen_on[2];
|
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
|
std::atomic<unsigned> epoch{0}; ///< bumped at each (re)connect: panels re-sync from it
|
||||||
E1608Readouts() {
|
E1608Readouts() {
|
||||||
for (int i = 0; i < 2; ++i) {
|
for (int i = 0; i < 2; ++i) {
|
||||||
@@ -117,6 +119,8 @@ class E1608Source final : public DataSource {
|
|||||||
void generator_tick(Clock::time_point now);
|
void generator_tick(Clock::time_point now);
|
||||||
void write_ao(int ch, double volts);
|
void write_ao(int ch, double volts);
|
||||||
void update_gen_flags();
|
void update_gen_flags();
|
||||||
|
void gen_rate_reset();
|
||||||
|
Clock::duration gen_period() const;
|
||||||
void fail(const std::string& msg, bool fatal);
|
void fail(const std::string& msg, bool fatal);
|
||||||
void note(const std::string& msg);
|
void note(const std::string& msg);
|
||||||
int wait_ms(int timeout_ms, Clock::time_point now) const;
|
int wait_ms(int timeout_ms, Clock::time_point now) const;
|
||||||
@@ -153,6 +157,8 @@ class E1608Source final : public DataSource {
|
|||||||
AoSettings ao_[2];
|
AoSettings ao_[2];
|
||||||
Clock::time_point gen_t0_[2];
|
Clock::time_point gen_t0_[2];
|
||||||
bool any_gen_ = false;
|
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 retry_at_;
|
||||||
Clock::time_point next_slow_, next_status_, next_gen_;
|
Clock::time_point next_slow_, next_status_, next_gen_;
|
||||||
|
|||||||
@@ -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")) {
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_E1608")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), e1608_probe, &app);
|
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")) {
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_E1608_RECONNECT")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), e1608_probe_reconnect, &app);
|
g_timeout_add(static_cast<guint>(std::atoi(env)), e1608_probe_reconnect, &app);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1881,6 +1881,12 @@ gboolean e1608_probe(gpointer user);
|
|||||||
gboolean e1608_probe_report(gpointer user);
|
gboolean e1608_probe_report(gpointer user);
|
||||||
gboolean e1608_probe_reconnect(gpointer user);
|
gboolean e1608_probe_reconnect(gpointer user);
|
||||||
gboolean e1608_probe_resumed(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`,
|
/// 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
|
/// `_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.
|
/// the results were replaced, not added to.
|
||||||
|
|||||||
@@ -1923,6 +1923,7 @@ gboolean source_kinds_probe(gpointer user) {
|
|||||||
c.set("trigger", "rising");
|
c.set("trigger", "rising");
|
||||||
c.set("dir1", "o");
|
c.set("dir1", "o");
|
||||||
c.set("plot0", "1");
|
c.set("plot0", "1");
|
||||||
|
c.set("gen_rate", "120");
|
||||||
} else if (id == "flatudp") {
|
} else if (id == "flatudp") {
|
||||||
c.set("port", "44600");
|
c.set("port", "44600");
|
||||||
c.set("bind", "10.0.0.8");
|
c.set("bind", "10.0.0.8");
|
||||||
@@ -2428,6 +2429,76 @@ gboolean e1608_probe(gpointer user) {
|
|||||||
return G_SOURCE_REMOVE;
|
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?
|
/// After the mock dropped the link: has the source reconnected, and does the stream move again?
|
||||||
gboolean e1608_probe_resumed(gpointer user) {
|
gboolean e1608_probe_resumed(gpointer user) {
|
||||||
App* a = static_cast<App*>(user);
|
App* a = static_cast<App*>(user);
|
||||||
|
|||||||
Reference in new issue
Block a user