Merge feature/gen-rate-and-tabs into feature/source-plugins
This commit is contained in:
commit
3ac7a8f230
16 files changed
+274
-40
No files matched your search
@@ -39,7 +39,7 @@ The detail lives in `docs/claude/`. Read the files that touch the area you are e
|
||||
| File | What it covers |
|
||||
| --- | --- |
|
||||
| [build-and-run.md](docs/claude/build-and-run.md) | Configuring and building, the desktop-entry target, the command-line options, and the two vendored dependencies (`deps/plotter`, `deps/udpsclient`) including what was changed upstream and why. |
|
||||
| [verification.md](docs/claude/verification.md) | How to run the headless self-check, the **two baselines every run is read against** (exactly three build warnings; no `Gtk-CRITICAL`), and the environment variable that schedules each probe. |
|
||||
| [verification.md](docs/claude/verification.md) | How to run the headless self-check, the **two baselines every run is read against** (exactly one build warning; no `Gtk-CRITICAL`), and the environment variable that schedules each probe. |
|
||||
| [probe-readings.md](docs/claude/probe-readings.md) | The per-line reference for the self-check's output: what every probe and dump line means, the value it must read, and the defect a wrong reading names. |
|
||||
|
||||
### How the application is put together
|
||||
|
||||
+1
-1
@@ -151,7 +151,7 @@ add_custom_command(
|
||||
# The generated source carries each file as one C string literal, and a `.ui` is big enough to pass the
|
||||
# 4095-character minimum the standard requires a compiler to support: `-Woverlength-strings` fires on
|
||||
# generated *data*, not on anything anyone wrote. Silenced for this one generated file (the icon bundle
|
||||
# is small enough per file never to need it), so the build stays at its three warnings. The manual is
|
||||
# is small enough per file never to need it), so the build stays at its one warning. The manual is
|
||||
# no longer compiled in, so the help bundle's own warning is gone with it.
|
||||
set_source_files_properties(${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
|
||||
PROPERTIES COMPILE_OPTIONS "-Wno-overlength-strings")
|
||||
|
||||
@@ -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.
|
||||
@@ -2909,7 +2915,7 @@ now — they are listed so they are not mistaken for anything else:
|
||||
unlocked, because configuring the host is what a first run is for. Each pane's heading also carries its **own switch**, the same control as the
|
||||
header bar's button — clicking either moves both — because a pane that is *disabled* while its
|
||||
feature is off is a pane whose controls you would otherwise have to leave to turn it back on. The
|
||||
switcher's policy is `wide` and each page has a title. With the math
|
||||
switcher's policy is `wide` and each page has a title and no icon (the switcher's own placeholder icon is hidden). With the math
|
||||
list folded into the signal list under a `MATH` divider whose `+` creates a channel — a derived channel already *is* a channel in
|
||||
the model, so the two lists were the same list drawn twice. A tab is one-of-*N*, so no
|
||||
toggle hides a pane any more: cursors-off **disables** its readout and leaves it in place, and
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -252,7 +252,12 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
session file. An icon-only version existed (policy `narrow`, empty titles, the pills named in code by
|
||||
`name_side_tabs()`, because `narrow` still draws a label beside an icon); it was reversed because the
|
||||
glyphs were not understandable. A page added with an empty title is a blank pill, and
|
||||
`switcher: … pills=N labels=N pages=N` reads it as `labels` below `pills`.
|
||||
`switcher: … pills=N labels=N images=0 pages=N` reads it as `labels` below `pills`. **No icon at all**
|
||||
is not what "no `icon-name`" gives you: `AdwViewSwitcherButton` always builds a `GtkImage` and falls
|
||||
back to `image-missing` (the `[x]` placeholder, which also squeezed the title to "Meas..."), and no
|
||||
property turns it off. `keep_tabs_icon_free()` (`src/ui_source.cpp`) hides every image in the switcher,
|
||||
re-hides one that is shown again, and re-runs on the stack's `items-changed` (connected *after* the
|
||||
switcher's own, so the new button exists) and on the switcher's `map`; `images=0` is the reading.
|
||||
- **An *overlaid* sidebar covers the plot, so a drag out of it has nowhere to land.** `collapsed` in an
|
||||
`AdwOverlaySplitView` means the sidebar sits *on* the plot rather than beside it: the pointer is over the
|
||||
list, never over a drop target, and a signal can be dragged but not dropped. `row_drag_sidebar()` hides
|
||||
|
||||
@@ -479,9 +479,15 @@ Two lines of the self-check output are assertions in disguise:
|
||||
substitutes a missing-image placeholder, the button still works, and every other line of the
|
||||
dump still passes. Changing an icon name is a one-line change in `build_toolbar()` and this
|
||||
is the only check that it names something that exists.
|
||||
- `switcher: policy=wide pills=4 labels=4 pages=4 size=308x34` (in the exit dump, beside `panels:`) — the
|
||||
- `switcher: policy=wide pills=4 labels=4 images=0 pages=4 size=308x34` (in the exit dump, beside `panels:`) — the
|
||||
sidebar tabs as the switcher actually draws them, and they are **words**: `labels` counts *mapped labels
|
||||
with text*, so `pills=labels=pages` says every page has a visible title. `pages` is read from the stack
|
||||
with text*, so `pills=labels=pages` says every page has a visible title. **`images` must be `0`**: it
|
||||
counts *mapped* `GtkImage`s inside the switcher. `AdwViewSwitcherButton` builds an icon in every tab, and
|
||||
a page with a title and no `icon-name` gets `image-missing` -- the `[x]` placeholder; the tabs hide it
|
||||
(`keep_tabs_icon_free()`, which also covers pages `build_source_panels()` adds). A nonzero `images`
|
||||
names a tab still drawing an icon or its placeholder (`images=pages` is the unfixed state), and
|
||||
`pills`/`labels` never saw it. Read it on a source with extra pages (`--source probe` under
|
||||
`UDPSCOPE_PROBE_SOURCE_KINDS`) as well as on UDPS. `pages` is read from the stack
|
||||
(2 for UDPS, 3 for the probe source, 4 for the E-1608 with its Analog out and Digital I/O tabs). A
|
||||
`labels` below `pills` is a page added with an empty title; `pills=0` means nothing is mapped, which
|
||||
makes the rest of the line meaningless rather than a pass. `policy=wide` is read rather than assumed.
|
||||
@@ -1832,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
|
||||
@@ -1859,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
|
||||
```
|
||||
|
||||
@@ -1895,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
|
||||
|
||||
+18
-11
@@ -15,23 +15,21 @@ xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
|
||||
|
||||
Two baselines every run is read against. The UDPS runs must be **unchanged by the source-plugin work**:
|
||||
a UDPS self-check, number-normalised (`sed 's/[0-9.]\+/N/g'`), diffs empty against one taken before it.
|
||||
A clean build emits **three** warnings and no more: two
|
||||
`-Wmissing-field-initializers` on `MathNode::source` and one `-Woverlength-strings` on the icons'
|
||||
A clean build emits **one** warning and no more: a `-Woverlength-strings` on the icons'
|
||||
generated GResource `.c`, whose blob is **9248** against C99's 4095-byte string-literal limit with
|
||||
the five action icons and the two app icons that remain (a note about a generated file, which GCC
|
||||
itself supports). The *fourth* was the help pages' generated `.c` (**516044** bytes); the manual is
|
||||
now a Mallard set installed for Yelp rather than a GResource compiled into the binary, so that
|
||||
bundle — and its warning — is gone, and the baseline moved from four to **three**. Re-measure by
|
||||
touching each `*.gresource.xml` under `src/` and rebuilding; a third `-Wmissing-field-initializers`
|
||||
or any other warning is a finding. (The fifth warning that used to appear here was an unused
|
||||
`output_built` in `math_probe()`; it was **deleted** as dead code — redundant with the
|
||||
`output_after_add` already printed on the same line — which dropped the baseline from five to
|
||||
four.) And an exit-dump run emits **no** `Gtk-CRITICAL` at all, nor any `Gtk-WARNING`: the dump's
|
||||
itself supports). History: the help pages' generated `.c` (**516044** bytes) went when the manual
|
||||
became a Mallard set installed for Yelp; an unused `output_built` in `math_probe()` was deleted as
|
||||
dead code; and the last two, `-Wmissing-field-initializers` on `MathNode::source` in `math_probe()`,
|
||||
went when the probe's `MathNode{MathOp::Gain, {}}` / `MathNode{MathOp::LowPass, {}}` named the
|
||||
field (a partial brace-initialiser on `MathNode` re-adds one: `source` has no default member
|
||||
initialiser). Re-measure by touching `src/icons/udpscope.gresource.xml` and rebuilding; any
|
||||
second warning is a finding. And an exit-dump run emits **no** `Gtk-CRITICAL` at all, nor any `Gtk-WARNING`: the dump's
|
||||
`gtk warnings:` line counts GTK's own log (through a writer function — GTK is built with structured
|
||||
logging, so the handler API never sees its messages) and must read `0`; see
|
||||
[probe-readings.md](probe-readings.md).
|
||||
|
||||
An *incremental* build reads **one** of those three: the generated GResource `.c` is not recompiled
|
||||
An *incremental* build reads **zero**: the generated GResource `.c` is not recompiled
|
||||
when only `src/` changed, so its `-Woverlength-strings` does not reappear.
|
||||
|
||||
A third reading every run offers for free: **how many `[udpscope] buffer allocated:` lines it
|
||||
@@ -439,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
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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_;
|
||||
|
||||
+13
-4
@@ -3060,8 +3060,11 @@ void note_panel_controls(App& app) {
|
||||
// pill count reads 0 and the label count means nothing, rather than passing vacuously.
|
||||
int pills = 0;
|
||||
int drawn_labels = 0;
|
||||
int drawn_images = 0;
|
||||
const std::function<void(GtkWidget*)> count_drawn = [&](GtkWidget* w) {
|
||||
if (GTK_IS_TOGGLE_BUTTON(w) && gtk_widget_get_mapped(w)) ++pills;
|
||||
// A *mapped* image is an icon (or its missing-icon placeholder) on a tab: tabs are words only.
|
||||
if (GTK_IS_IMAGE(w) && gtk_widget_get_mapped(w)) ++drawn_images;
|
||||
// A *mapped* label with text: an empty label would be counted by "mapped" alone.
|
||||
if (GTK_IS_LABEL(w) && gtk_widget_get_mapped(w) && gtk_label_get_text(GTK_LABEL(w))[0] != '\0') {
|
||||
++drawn_labels;
|
||||
@@ -3074,6 +3077,7 @@ void note_panel_controls(App& app) {
|
||||
if (app.side_switcher != nullptr) count_drawn(app.side_switcher);
|
||||
app.dbg_switcher_pills = pills;
|
||||
app.dbg_switcher_labels = drawn_labels;
|
||||
app.dbg_switcher_images = drawn_images;
|
||||
// Walked through `get_pages()`, not the widget tree: an `AdwViewStackPage` is a plain `GObject`.
|
||||
app.dbg_switcher_pages =
|
||||
app.side_stack != nullptr
|
||||
@@ -5203,7 +5207,9 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
G_CALLBACK(on_side_tab_changed), app);
|
||||
}
|
||||
// The source kind's own tabs (a generator, digital I/O), added before the first tick so the
|
||||
// pages-versus-pills reading below counts them too.
|
||||
// pages-versus-pills reading below counts them too. The tabs are words, so the switcher's icons
|
||||
// are hidden first and kept hidden for the pages this call adds.
|
||||
keep_tabs_icon_free(*app);
|
||||
build_source_panels(*app);
|
||||
|
||||
// The left side is the events pane *and* the plot, in one row: the pane is a column of its own
|
||||
@@ -6028,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);
|
||||
}
|
||||
@@ -6240,10 +6249,10 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
app.dbg_panel_switch_trigger, app.dbg_toolbar_switch_trigger,
|
||||
app.dbg_panel_switch_cursors, app.dbg_toolbar_switch_cursors,
|
||||
app.dbg_panel_drove_toolbar);
|
||||
std::printf("[udpscope] switcher: policy=%s pills=%d labels=%d pages=%d size=%dx%d\n",
|
||||
std::printf("[udpscope] switcher: policy=%s pills=%d labels=%d images=%d pages=%d size=%dx%d\n",
|
||||
app.dbg_switcher_policy == 0 ? "narrow" : (app.dbg_switcher_policy == 1 ? "wide" : "?"),
|
||||
app.dbg_switcher_pills, app.dbg_switcher_labels, app.dbg_switcher_pages,
|
||||
app.dbg_switcher_w, app.dbg_switcher_h);
|
||||
app.dbg_switcher_pills, app.dbg_switcher_labels, app.dbg_switcher_images,
|
||||
app.dbg_switcher_pages, app.dbg_switcher_w, app.dbg_switcher_h);
|
||||
// The help affordance layer: what the contextual-help toggle left on the surfaces, and where the
|
||||
// guided tour stood when the last capture ran. Both are captured on the tick (`capture_help_state`),
|
||||
// so neither asks a destroyed widget anything.
|
||||
|
||||
@@ -444,6 +444,9 @@ struct App {
|
||||
/// are words (`wide` policy, a title per page), so the label count must equal the page count.
|
||||
int dbg_switcher_pills = -1;
|
||||
int dbg_switcher_labels = -1;
|
||||
/// How many `GtkImage`s the switcher has *mapped*: a tab is its title and nothing else, so this
|
||||
/// must be 0 -- an icon, or the missing-icon placeholder GTK draws for a page with no `icon-name`.
|
||||
int dbg_switcher_images = -1;
|
||||
/// How many pages the sidebar stack holds: `pills` and `labels` must both equal it.
|
||||
int dbg_switcher_pages = -1;
|
||||
/// The switcher's own size, beside it: what the tab bar costs the column.
|
||||
@@ -1560,6 +1563,11 @@ void config_dialog_refresh(ConfigDialog& d);
|
||||
void open_configurations(App& app);
|
||||
void save_layout_now(App& app);
|
||||
|
||||
/// The sidebar's tabs are words: hide the icon every `AdwViewSwitcherButton` draws (the
|
||||
/// missing-icon placeholder, for a page with a title and no `icon-name`), for the buttons that exist
|
||||
/// and for any page added later. Call once, before `build_source_panels()`.
|
||||
void keep_tabs_icon_free(App& app);
|
||||
|
||||
/// Capability panels (`src/ui_source.cpp`). `build_source_panels` adds the source kind's panels as
|
||||
/// pages of `side_stack`, once, before the first tick. `apply_source_panels` sets every panel's
|
||||
/// sensitivity from `connected` -- disabled, never hidden: the tab switcher owns visibility.
|
||||
@@ -1873,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.
|
||||
|
||||
+78
-8
@@ -467,7 +467,7 @@ gboolean math_probe(gpointer user) {
|
||||
MathChannel gain;
|
||||
gain.name = "math x2";
|
||||
gain.enabled = true;
|
||||
gain.nodes = {MathNode{MathOp::Source, name}, MathNode{MathOp::Gain}};
|
||||
gain.nodes = {MathNode{MathOp::Source, name}, MathNode{MathOp::Gain, {}}};
|
||||
gain.nodes[1].in_a = 0;
|
||||
gain.nodes[1].gain = 2.0;
|
||||
gain.output = 1;
|
||||
@@ -476,14 +476,14 @@ gboolean math_probe(gpointer user) {
|
||||
MathChannel lp;
|
||||
lp.name = "math lp";
|
||||
lp.enabled = true;
|
||||
lp.nodes = {MathNode{MathOp::Source, late_name}, MathNode{MathOp::LowPass}};
|
||||
lp.nodes = {MathNode{MathOp::Source, late_name}, MathNode{MathOp::LowPass, {}}};
|
||||
lp.nodes[1].in_a = 0;
|
||||
lp.nodes[1].fc = 20.0;
|
||||
lp.output = 1;
|
||||
channels.push_back(lp);
|
||||
|
||||
// A third channel so the editor's new form row is built and synced on a baseline run. Assigned field
|
||||
// by field: a partial brace-initialiser would add a fifth build warning.
|
||||
// by field: a partial brace-initialiser would add a build warning.
|
||||
MathChannel dn;
|
||||
dn.name = "math denoise";
|
||||
dn.enabled = true;
|
||||
@@ -1269,8 +1269,8 @@ gboolean mathwrap_probe(gpointer user) {
|
||||
MathChannel gain;
|
||||
gain.name = "math wrap";
|
||||
gain.enabled = true;
|
||||
// Field by field: a partial brace-initialiser would add a fourth build warning, and the baseline
|
||||
// is exactly three.
|
||||
// Field by field: a partial brace-initialiser would add a build warning, and the baseline
|
||||
// is exactly one on a clean build.
|
||||
MathNode src;
|
||||
src.op = MathOp::Source;
|
||||
src.source = name;
|
||||
@@ -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);
|
||||
@@ -5843,9 +5914,8 @@ int noise_channel(App* a, const char* name) {
|
||||
// the older feature's proof, so the new channel stays out of the acquisition plan until they have
|
||||
// all run, and only the de-noise comparison is taken with it in.
|
||||
//
|
||||
// Fields assigned rather than brace-initialised: a partial brace-initialiser on `MathNode` is
|
||||
// what the two `-Wmissing-field-initializers` in this file already are, and the build baseline
|
||||
// is exactly four warnings.
|
||||
// Fields assigned rather than brace-initialised: a partial brace-initialiser on `MathNode` leaves
|
||||
// `source` out and warns (`-Wmissing-field-initializers`), and the build baseline is one warning.
|
||||
gboolean noise_probe_setup(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
|
||||
@@ -11,6 +11,48 @@
|
||||
namespace udpscope {
|
||||
namespace ui_impl {
|
||||
|
||||
namespace {
|
||||
|
||||
/// Hide every `GtkImage` inside `w`, and keep it hidden. `AdwViewSwitcherButton` always builds an
|
||||
/// icon -- in both of its layouts -- and a page with no `icon-name` gets `image-missing`, which is
|
||||
/// the placeholder the sidebar's text tabs used to draw. There is no property or policy that turns
|
||||
/// the icon off, so the image is hidden (a hidden child takes no space: the label gets the pill) and
|
||||
/// a `notify::visible` handler puts it back to hidden if the button ever shows it again.
|
||||
void hide_tab_icons(GtkWidget* w) {
|
||||
if (GTK_IS_IMAGE(w)) {
|
||||
if (g_object_get_data(G_OBJECT(w), "udpscope-icon-hidden") == nullptr) {
|
||||
g_object_set_data(G_OBJECT(w), "udpscope-icon-hidden", GINT_TO_POINTER(1));
|
||||
g_signal_connect(w, "notify::visible", G_CALLBACK(+[](GObject* o, GParamSpec*, gpointer) {
|
||||
GtkWidget* img = GTK_WIDGET(o);
|
||||
if (gtk_widget_get_visible(img)) gtk_widget_set_visible(img, FALSE);
|
||||
}),
|
||||
nullptr);
|
||||
}
|
||||
gtk_widget_set_visible(w, FALSE);
|
||||
return;
|
||||
}
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr; c = gtk_widget_get_next_sibling(c)) {
|
||||
hide_tab_icons(c);
|
||||
}
|
||||
}
|
||||
|
||||
void on_tab_pages_changed(GListModel*, guint, guint, guint, gpointer sw) {
|
||||
hide_tab_icons(GTK_WIDGET(sw));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void keep_tabs_icon_free(App& app) {
|
||||
if (app.side_switcher == nullptr || app.side_stack == nullptr) return;
|
||||
hide_tab_icons(app.side_switcher);
|
||||
// A page added later gets a button when the model reports it; this handler is connected *after*
|
||||
// the switcher's own, so the button already exists when it runs.
|
||||
g_signal_connect_after(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)), "items-changed",
|
||||
G_CALLBACK(on_tab_pages_changed), app.side_switcher);
|
||||
// And once more when the switcher is (re)mapped, which also covers a rebuild of the buttons.
|
||||
g_signal_connect_swapped(app.side_switcher, "map", G_CALLBACK(hide_tab_icons), app.side_switcher);
|
||||
}
|
||||
|
||||
void build_source_panels(App& app) {
|
||||
if (app.side_stack == nullptr || app.acq == nullptr || app.acq->source() == nullptr) return;
|
||||
SourceKind* kind = find_kind(app.source_kind);
|
||||
|
||||
Reference in new issue
Block a user