Feature/source plugins #1

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

No files matched your search

+12 -7
View File
@@ -104,7 +104,13 @@ src/ui_signal_page.cpp the per-signal slide-over pages (`signal:<name>`)
**The window.** The plot is the window. Under it sits a fixed-height **badge bar** (36 px). Its left
part is the **signals**: one badge per *enabled* signal (a dot in the signal's colour, filled while the
signal is plotted, and its name), then a `+` badge that opens the full signal list (`g` does the same).
signal is plotted, and its name), then a `+` badge that opens the **Add a signal** page (`g` does the same). That page proposes only what can be
*added*: the streamed signals that are not acquired and the math channels that are not computed, each row with
the tick that acquires it (the row then leaves the page and the signal's badge appears), plus the `MATH`
divider's `+` for a new math channel; when every signal is acquired a dim "every signal is acquired" line
stands in for the list. While the bar has overflowed into the "N signals" badge the page lists the acquired
signals too, since their rows are then the only way to each signal's page. There is no signal lock: a
signal's page and the list always allow acquire, colour, calibration, the math editor and delete.
A signal badge opens that signal's own page in the slide-over -- colour, calibration and the *Plot on N*
buttons -- and is also the drag handle: drag it onto a plot to draw the signal there, or onto another
badge to reorder them. Past **six** signal badges the bar shows a single **"N signals"** badge in their
@@ -112,7 +118,7 @@ place (it opens the list, and `+` is hidden): the E-1608's 8 analog inputs and c
otherwise. The signal badges sit in a scroller, so the bar's minimum width, and with it the window's,
never depends on how many signals there are. The badge order is kept **for the session only**; it is not
written to the session file. The right-hand end carries two words-only badges: **Trigger** (`off`, `armed`, `capturing`, `triggered`
or `disarmed`) and **Source** (the kind's name and `connected`/`disconnected`; a read-out, not a button,
or `disarmed`) and **Source** (the kind's name only -- the link state is the status bar's; a read-out, not a button,
for a source with no controls of its own, and for a file). A badge opens the **slide-over**: a 360 px
panel laid over the plot's right edge, opaque, with its own title, a *Close* button and one scroller per
page. The Trigger badge opens the trigger form; the Source badge opens the source kind's own panels
@@ -2937,11 +2943,10 @@ now — they are listed so they are not mistaken for anything else:
tabs did not fit 320 px. It is **one dock** now, with no tab switcher: the cursors and the
measurements in one scroller, shown from the start and insensitive until the stream has signals; `m`
scrolls it to the cursors; the **signal list** is the `signals` slide-over page (`g`, or the `+` badge). The **trigger form** and a **source's own
controls** are pages of the slide-over, reached from the badge bar. Both panes also have a **locked** mode, which is what a returning user gets: the
trigger pane shows one line saying what it is set to, the signal list shows only the signals that are
on, and neither offers a control that changes the configuration -- each heading has an `[edit]` button
to switch to the full pane, and back. Dragging a signal's badge onto a plot still works while locked --
filling plots is not configuration. A signal's badge is dragged onto a plot, or onto another badge to
controls** are pages of the slide-over, reached from the badge bar. The trigger pane also has a **locked** mode, which is what a returning user gets: it
shows one line saying what it is set to and offers no control that changes the configuration -- its
heading has an `[edit]` button to switch to the full pane, and back. The signals have no lock: the
signals page only proposes what can be added, and the signal pages always allow editing. A signal's badge is dragged onto a plot, or onto another badge to
reorder the badges (the order is kept for the session only); the slide-over closes for the length of the
drag so there is something under the pointer to drop onto. A **first run** (no session file for that source) comes up
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
+1 -1
View File
@@ -123,7 +123,7 @@ overlay is built (the tour's overlay wraps the whole window, so its dim rectangl
too); the trigger form is registered as
a page by `build_trigger_panel`; `build_source_panels()` registers the source pages; and
`build_badge_bar()` runs *after* all of them, because the Source badge needs to know the first page.
Per-tick state (the Trigger word, the Source name and `connected`, the open marks) is written by
Per-tick state (the Trigger word, the Source name, the open marks) is written by
`sync_badge_bar()`, called from `tick()` and from `slideover_open`/`slideover_close`; it writes a label
only when its text changed. Nothing in the badge bar or the slide-over may be read from the producer
thread.
+2 -2
View File
@@ -255,8 +255,8 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
`gtk_box_reorder_child_after` and rewrites `App::badge_order` from the widgets, and **never rebuilds the badges** --
the dragged badge and the target whose `drop` emission is running must both survive it. A foreign drag whose string is not a
signal index is refused. `[sigbadges]` reads the controllers, the payload and the reorder; `pane drag:` reads the close.
- **A stock icon name has to be one *Adwaita* has, and a blank button is the failure mode.** The two
panes' `[edit]` buttons are `document-edit-symbolic` (the pencil: edit) and `object-select-symbolic`
- **A stock icon name has to be one *Adwaita* has, and a blank button is the failure mode.** The
trigger pane's `[edit]` button is `document-edit-symbolic` (the pencil: edit) and `object-select-symbolic`
(the check: done). The done one was `emblem-ok-symbolic`, which breeze ships and **Adwaita does not** —
so the button that leaves configuration mode was blank, which no other reading could see: the button
worked, the layout was unchanged, and only `pane edits:` (the two names and
+3 -3
View File
@@ -116,8 +116,8 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
- **The badge bar has a fixed height and sits outside the overlay.** `badge_bar` is 36 px
(`height-request`) between `plot_overlay` and the status line, so the plot above it never moves when a
badge's text changes (`bar_h` = `bar_h_after` in `[badges]`). This is "layout must not depend on
content" for the new row: a badge's state is a word from a short fixed set and the Source badge's name
is a kind's display name, so what changes with the stream is the row's *width* (the bar has room to
content" for the new row: the Trigger badge's state is a word from a short fixed set and the Source badge
is a kind's display name only (no connection state: that is the status bar's), so what changes with the stream is the row's *width* (the bar has room to
spare on the right) and never its height. The badge labels are not ellipsized: a name long enough to
need it would be a defect of the kind's `display_name()`. Rows that appear by trigger condition appear
inside the slide-over's scroller, never in the bar.
@@ -127,7 +127,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
(the E-1608: eight inputs and the counter) would push the window's minimum width past a laptop screen.
Past `kMaxSignalBadges` (6) the row is replaced by one "N signals" badge, which also stands for the `+`.
The bar's height stays 36 px in every one of these states (`bar_h_fixed` in `[sigbadges]`), and
`bar_min_w` reads the same with one signal, nine signals and the overflow (329 on the E-1608, 336 on UDPS:
`bar_min_w` reads the same with one signal, nine signals and the overflow (244 on the E-1608, 251 on UDPS:
the Source badge of each kind reads differently). A change that makes it grow with the signal count has
taken the scroller out of the measurement.
- **The dock's width is fixed too** (`side_column` `width-request` 300, the split view's 320 px floor
+3 -3
View File
@@ -13,9 +13,9 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
(`rebuild_trigger_signals()` picks up `App::pending_trigger_channel`, or `apply_trigger_layout()`
resolves it itself when they are already there) — a name that resolved to nothing would silently move
the trigger to channel 0.
- **A session that loaded is what locks the two panes.** Locked is the returning user's mode: the
trigger pane shows one compact line, the signal list only the signals that are on, and neither offers
a control that changes the configuration. A *first run* — no session file for that source — never
- **A session that loaded is what locks the trigger pane.** Locked is the returning user's mode: the
trigger pane shows one compact line and offers no control that changes the configuration (the signals
have no lock). A *first run* — no session file for that source — never
reaches `apply_layout()` and stays in configuration mode, which is what a first run is for. The mode
is a mode and not a setting: it is deliberately not persisted, so a scope reopened tomorrow starts
where a returning user expects to start. **Dragging a signal onto a plot is not configuration and survives
+33 -29
View File
@@ -1706,8 +1706,8 @@ library measures and draws the rect in tests of its own.
```
[udpscope] pane drag: slide-over closed for the drag=1
[udpscope] pane edits: locked=document-edit-symbolic(ok) config=object-select-symbolic(ok)
[udpscope] panes: trigger=locked signals=locked body_visible=0 summary='signal: square | condition: window, falling | level: 0.3 .. 0.6 | mode: single | noise band: 0.2 | pre-trigger: 20 % | hold-off: 1000 ms'
[udpscope] panes: trigger=config signals=config body_visible=1 summary=''
[udpscope] panes: trigger=locked body_visible=0 summary='signal: square | condition: window, falling | level: 0.3 .. 0.6 | mode: single | noise band: 0.2 | pre-trigger: 20 % | hold-off: 1000 ms'
[udpscope] panes: trigger=config body_visible=1 summary=''
[udpscope] panes: the same line as the probe found it: 'mode: stream (trigger off)'
```
@@ -1720,18 +1720,18 @@ or more than six, where the badges are replaced by the "N signals" overflow). It
`sidebar hidden for the drag=1 restored after it=1` pair, which tested `row_drag_sidebar()` -- the list rows were
the drag handle and the overlaid sidebar had to step aside; both are gone.
**`pane edits:`** is the two `[edit]` buttons in each mode, by name and verdict, and it exists because of
**`pane edits:`** is the trigger pane's `[edit]` button in each mode, by name and verdict, and it exists because of
a real defect: the "done" icon was `emblem-ok-symbolic`, which **Adwaita does not have** (breeze does), so
the button that leaves configuration mode was blank — and a blank icon button is invisible in every other
reading, exactly as `toolbar icons:` describes for the toolbar. The probe clicks into configuration mode
and back, so one line carries both names. **Both must read `(ok)`**, and the pair is
`document-edit-symbolic` (the pencil: edit) and `object-select-symbolic` (the check: done).
Both panes have two modes. **Locked** is what a returning user gets — the trigger pane shows the one line
in `summary`, the signal list shows only the signals that are on, and neither offers a control that
changes the configuration — and **config** is the full pane, which is what a *first run* gets, because a
session file that loaded is what "we have been here before" means. Each heading's `[edit]` button flips
its pane.
The trigger pane has two modes. **Locked** is what a returning user gets — the pane shows the one line
in `summary` and offers no control that changes the configuration — and **config** is the full pane, which
is what a *first run* gets, because a session file that loaded is what "we have been here before" means.
The heading's `[edit]` button flips it. (The signals have no lock: the badges and the signal pages are
direct manipulation, and `panes:` no longer carries a `signals=` field.)
- **With trigger mode off the summary is one line**: `mode: stream (trigger off)`. The four lines below
describe a trigger that is not running, so quoting them would describe nothing. The dump carries the line
@@ -1765,9 +1765,8 @@ its pane.
`body_visible=0` is a locked pane showing nothing at all, which is the defect this reads for.
- **`body_visible=0`** is the settings being off screen, which is what makes the pane a couple of hundred
pixels instead of 580 in `panels:`.
- **The locked signal list drops the rows of the signals that are off**, so `rows` counts what is on:
`signals panel: … rows=3 signals=2 divider=1` is two on signals and the `MATH` divider, not four
signals. Its `+` goes with them — a `+` on a locked list opens the editor the lock exists to keep shut.
- **The signals list has no lock.** It lists what can be *added* (see `signals panel:`), and the signal
pages always allow acquire, colour, calibrate, math edit and delete.
## `panel switches:` and the trigger panel's default sensitivity
@@ -2305,19 +2304,26 @@ prints without a probe: `depth`, the retention (`kept`/`bytes`/`budget`/`dropped
## `signals panel:`
```
[udpscope] signals panel: labels=2 list=1 rows=5 signals=4 math=0 divider=1
[udpscope] signals panel: labels=2 list=1 rows=9 signals=4 math=4 divider=1
[udpscope] signals panel: labels=3 list=1 rows=5 signals=4 math=0 divider=1 drag_sources=0 addable=4 addable_ok=1 empty_label=0
[udpscope] signals panel: labels=3 list=1 rows=2 signals=1 math=0 divider=1 drag_sources=0 addable=1 addable_ok=1 empty_label=0
[udpscope] signals panel: labels=3 list=1 rows=1 signals=0 math=0 divider=1 drag_sources=0 addable=0 addable_ok=1 empty_label=1
```
The signals panel's frame read from the window, and in the same line the *shape* of the one channel list:
the heading and the hint the `.ui` gives it, the list the rows go in, how many rows that list holds
(`rows`), how many of them are streamed signals (`signals`), how many math channels the app has (`math`),
and `divider`, which is 1 when `rows == signals + 1 + math` and 0 otherwise. **`drag_sources` must be
The signals panel's frame read from the window, and in the same line the *shape* of the one list. The page
is "Add a signal": it proposes only what can be added, so the list holds the streamed signals that are not
acquired (`signals`), the math channels that are not computed (`math`), and the `MATH` divider with its `+`
(`divider`, 1 when `rows == signals + 1 + math`, 0 otherwise). An acquired signal has a badge in the bar and
is not listed -- **unless the badge bar has overflowed** (more than 6 enabled signals, the "N signals" badge),
when the acquired ones are listed too, since the bar no longer shows them. `addable` is the same count
worked out from the model (unacquired listed signals, or all of them when overflowed) and `addable_ok=1` is
`signals + math == addable`; `0` is a list filter that drifted from the rule. `empty_label=1` is the dim
"every signal is acquired" label shown, which must happen exactly when `addable=0` and the stream declares
signals (the divider row stays, so a math channel can still be created). **`drag_sources` must be
`0`**: dragging a signal onto a plot is the signal *badge's* job now (see `[sigbadges]`, which counts the
badges' own sources), and the list's rows only open the signal's page; a non-zero reading is a second, competing
drag handle on the list. `labels=1` is a `.ui` that lost
one of the two labels — counted over the panel's subtree *above* the list, because the rows' own name
and unit labels are the code's and the heading sits in a heading row beside the pane's `[edit]` button — `list=0` one that lost the list -- both of which otherwise show as a panel quietly
drag handle on the list. `labels=2` is a `.ui` that lost
one of the three labels (heading, hint, the empty-state label) — counted over the panel's subtree *above* the list, because the rows' own name
and unit labels are the code's — `list=0` one that lost the list -- both of which otherwise show as a panel quietly
missing a piece -- and **`divider=0` is the merged list that lost its `+`**, the one control that can create
a math channel. The first line above is a run with no math channels at all: the divider is there anyway,
because a `+` that only appeared once a channel existed could never create the first one. `panels: …
@@ -2640,17 +2646,16 @@ left the caller's `false` alone would read `dock_shown=0 default=1 ok=0`. The pr
## `[signalpage]`
```
[signalpage] name=sine open=1 pages=1 enable_visible=1 plot_buttons=1 plot_on_row0=1 plot_on_row0_disabled=1 locked_enable_sensitive=0 removed=1 btn_row0=1 back_to_list=1 row_opens=1 pruned=1 other_page_kept=1 rows=1
[signalpage] name=sine open=1 pages=1 enable_visible=1 plot_buttons=1 plot_on_row0=1 plot_on_row0_disabled=1 removed=1 btn_row0=1 back_to_list=1 row_opens=1 pruned=1 other_page_kept=1 rows=1
```
`UDPSCOPE_PROBE_SIGNAL_PAGES_MS` builds and drives the page of the first streamed signal. `open=1` (the slide-over shows `signal:<name>`), `pages=1`
(one page in `App::signal_pages`), `enable_visible=1` (the acquire tick is shown in live mode; `0` in a file view), `plot_buttons=` equal to
`rows=` (one "Plot on" button per figure row), `plot_on_row0=1` (`assign_signal_to_row()` returned true and the signal is plotted, from a state where the probe first took it off every plot), `plot_on_row0_disabled=1`
(after the page refilled with the signal on plot 1, that plot's "Plot on" button is insensitive, "already on this plot"; `0` is a button that would put the signal in a second slot of
the same plot), `locked_enable_sensitive=0` (with `signals_locked` set and `signal_pages_sync()` run, the page's Acquire/Compute tick is insensitive; `1` is a page that bypasses
the signal lock -- the same goes for a math channel's Edit graph and Delete, which the probe does not read; the probe restores the lock), `removed=1`
(the page's "Remove from plots" took it off), `btn_row0=1` (the "Plot on 1" button, the user's path, put it back), `back_to_list=1` ("All signals"
target), `row_opens=1` (activating the list row opens the page), and `pruned=1`: a math channel's page was opened, its **Delete** button clicked, and
the same plot), `removed=1`
(the page's "Remove from plots" took it off), `btn_row0=1` (the "Plot on 1" button, the user's path, put it back), `back_to_list=1` ("Back to signals"
target), `row_opens=1` (the probe switches the signal off so it is addable, activates its list row and the page opens; the signal is then switched back on), and `pruned=1`: a math channel's page was opened, its **Delete** button clicked, and
the slide-over closed and the page was dropped by `signal_pages_sync()`. `pruned=-1` means no math channel could be added; `0` is a stale page. `other_page_kept=1`: with the `trigger` page open, a signal page that is not on show was pruned (a math channel added, its page built, the
channel removed) and the slide-over was still on `trigger`, both by `slideover_page()` and by the stack's visible child; `0` is `signal_pages_sync()` parking the stack on the
signals list while the title and the badge marker still name `trigger`; `-1` when there is no `trigger` page or no math channel could be added.
@@ -2658,14 +2663,14 @@ signals list while the title and the badge marker still name `trigger`; `-1` whe
## `[sigbadges]`
```
[sigbadges] enabled=4 badges=4 overflow=0 overflow_text=- plus_visible=1 dot_plotted=4 bar_h=36 bar_h_fixed=1 bar_min_w=336 opens=1 plus_opens=1
[sigbadges] enabled=4 badges=4 overflow=0 overflow_text=- plus_visible=1 dot_plotted=4 bar_h=36 bar_h_fixed=1 bar_min_w=251 opens=1 plus_opens=1
```
`UDPSCOPE_PROBE_SIGNAL_BADGES_MS` reads the signal badges. `badges` must equal `enabled` (the enabled, listed signals) while `enabled` is at most 6
(`kMaxSignalBadges`); beyond that `badges=0 overflow=1 overflow_text=<N> signals plus_visible=0` (the "N signals" badge replaces the badges and the "+"). `dot_plotted` is the number of
badges whose signal is on a plot (its dot is then the trace colour, not the unplotted grey). `bar_h` is the bar's allocated height and `bar_h_fixed=1` that neither it nor its natural
height moved while one signal was switched off and on again (`rebuild_signal_panels()` each time), and that the badge count followed; `0` is a bar whose height depends on its content.
`bar_min_w` is only a record: the scroller must keep it the same however many signals there are (it reads 336 with the UDPS mock's 4 and with the overflow forced on, and 329 against the E-1608 mock with one signal and with nine -- the overflow, `enabled=9 badges=0 overflow=1 overflow_text=9 signals plus_visible=0 bar_h=36 bar_h_fixed=1`; a value that grows with the signal count is a scroller that has stopped constraining the bar). `opens=1`: clicking the
`bar_min_w` is only a record: the scroller must keep it the same however many signals there are (it reads 251 with the UDPS mock's 4 and with the overflow forced on, and 244 against the E-1608 mock with one signal and with nine -- the overflow, `enabled=9 badges=0 overflow=1 overflow_text=9 signals plus_visible=0 bar_h=36 bar_h_fixed=1`; a value that grows with the signal count is a scroller that has stopped constraining the bar). `opens=1`: clicking the
first signal badge opens `signal:<name>` with the badge marked open and a second click closes it (`-1` when overflowed, there being no signal badges); `plus_opens=1` the same for the "+"
(the overflow badge when overflowed) and the `signals` page.
@@ -2697,8 +2702,7 @@ any is a badge that does not say its panel is open, or one that stays marked aft
**Line 2** is the Source badge, and every field has a value it must read:
- `name=` the source kind's name and `state=` `connected`/`disconnected`, as the badge shows them. `connected` against a live mock; a badge stuck on
`disconnected` is the status not reaching the badge.
- `name=` the source kind's name, as the badge shows it. The badge carries no link state (the status bar says it); there is no `state=` field.
- `pages=` the number of slide-over pages the source kind contributes (`source-<id>`): `0` for UDPS, `2` for the E-1608 (`source-ao`, `source-dio`).
`can_target=0` with `pages=0` is the UDPS badge being a read-out rather than a button; `can_target=1` with `pages>0` is the button being live.
`can_target=1` with `pages=0` is a dead button that opens nothing.
+4 -3
View File
@@ -472,7 +472,7 @@ precedent, not an exception.
a sensitivity, a visibility or a value needs no open page.
**The source kind's panels are slide-over pages too** (`source-<id>`: the E-1608's `source-ao` and
`source-dio`, Flat UDP's, the probe kind's), so the same rule holds for them. The probe appends a second
`[badges]` line for the Source badge: `[badges] source name=<kind> state=<connected|disconnected> pages=<n>
`[badges]` line for the Source badge: `[badges] source name=<kind> pages=<n>
switch_visible=<0|1> can_target=<0|1> second_page=<id|-> second_ok=<0|1> open_marked=<0|1|-1>
closed_unmarked=1`. `pages` is the number of source pages, `switch_visible` is the segmented switch
being on screen after the first source page was opened (`1` for the E-1608's two pages, `0` for one page
@@ -1035,8 +1035,9 @@ stages into one is the failure mode**: a probe that rebuilt the buffer at 11.5 s
immediately would read `bare=0` on every search leg, which looks exactly like a broken comparator.
**Why 4 s and not 3.** `enable_all_probe`'s last act is a timeout of its own, `ENABLE_ALL_MS + 2000` -- 3 s
under this recipe -- which walks `signal_list` for the front channel's acquire tick and clicks it. A probe
that installs a math graph at that same instant rebuilds the signal panel under it, and the off-step then
under this recipe -- which opens the front channel's signal page and clicks its "Acquire this signal" tick (an
acquired signal is not in the Add-a-signal list, so the list cannot be walked). A probe
that installs a math graph at that same instant rebuilds the signal panels under it, and the off-step then
emits `active` on a freed `GtkCheckButton`: `GLib-GObject-CRITICAL: instance with invalid (NULL) class
pointer`, with the backtrace in `enable_all_probe()::{lambda}`. It is a real fragility of that probe's lookup
and not of the feature, and it is why every documented probe time is offset from `ENABLE_ALL_MS + 2000`, not
+1 -1
View File
@@ -127,7 +127,7 @@
and port.</p>
</item>
<item>
<p><em>Enable a signal</em> in the signal list. The plot stays empty for the first
<p><em>Enable a signal</em> in the <gui>Add a signal</gui> page (the <gui>+</gui> badge). The plot stays empty for the first
fraction of a second while the first window fills — the buffer holds only what has
arrived.</p>
</item>