0a209becb18d3c36213ed092af1be6b6f68eaa66
92
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0a209becb1 |
fix: a still frame's measurements are the frame's, not the viewport's
A measurement is a statistic of the window the pane is *about*, and on a pinned view -- a held event, a paused stream, an opened file -- that is the frame, not the part of it on screen. The pane followed the viewport instead, so a wheel on a latched event moved `average` and `top`: the reported "when measuring a still frame, moving the zoom makes measurements such as average or top change". Nothing about the data had changed; the user was only looking closer. The window is `RowSet::pinned_range()` while the pin frames the view and the viewport otherwise, and the choice is made in `update_measures()` -- the call site -- rather than inside `MeasureWindowReader`, because it is *booked* as well as read: that window is what the "it moved" test compares against `App::measure_window`, so a gesture inside the pin spends no recompute either, and a reader that chose its own would leave a pinned view recomputing every gesture frame for a number that cannot change. `pinned_range()` is now the one expression of the pin's extent, shared with the framing (`hold_pinned_window()`, `fit_x()`), and the reader is handed the window instead of reading the plot. Live is unchanged: there the window really moves and the statistic follows it, and the accumulator's rule about what resets the history is about neither. `UDPSCOPE_PROBE_MEASUREPIN_MS` is the guard, three legs of before/after a gesture on the row's own axis. On a latched event zoomed to a quarter (`view=[-1..1]` -> `[-0.25..0.25]`, `pin` unchanged): `same=4/4 for_same=1 pass_delta=0`, every value bit-identical -- `average=0.000000 top=0.957975 peak to peak=2.000000 amplitude=1.915950` both sides. The same on a pause taken from it. Since the key `for=` follows `pin=` now, `[measure-trace]` gained the field, so a held run reads `for==pin` with `swin` free to sit inside it. On a live view a zoom to an eighth reads `same=0/4 for_same=0 pass_delta=3 span_after=0.125000 follows=1` with the values moving -- the case the rule must not swallow. Before the fix the same probe reads `same=1/4 for_same=0 pass_delta=1 span_after=0.500000` on the held event (`top` `0.957975` -> `0.957966`) and `same=0/4 span_after=0.125000` on the pause (`top` `0.957944` -> `0.358614`). Live reads the same either way. Also fixed, found by the pause leg: pausing while an event is *held* froze an **empty** picture. `on_pause_toggled()` read `stream_at_x0()` after setting the pause state, and that accessor follows the display -- `paused` with no pause view published yet answers `-display_epoch()`, which on a held event is 0, so the cut searched the ring at edge-relative x, found nothing, and the frame bound an empty snapshot: every measurement `--`, every ruler gone (`ev=650001` on the probe's line now, `ev=0` before). The offset and the window are read first, which is the offset the axis is actually displaying. The live case is unaffected, which is why the pause probe, which pauses a live view, never saw it. Docs corrected where they asserted the old half-truth: the invariants now separate the accumulator's rule (unchanged) from the window a value is computed over (this change), the trigger/pinning file names the pin as load-bearing for both the framing and the numbers and records the pause ordering rule, `verification.md`/`probe-readings.md` describe the new probe and `pin=`, and the README and the manual say what a still frame measures. Checked: `cmake --build build --clean-first -j4` 0 errors and the documented three warnings; `make -C build test-ui` 13 ok; `make -C build test-help` green; an exit-dump run 0 Gtk-WARNING/Gtk-CRITICAL and `gtk warnings: 0`; `[measurepin-probe]`, `[pan-probe]` (`held=1`, `allowed=1`, `limited=1`), `[pause-probe]` (`event_size` non-zero exactly while paused, `axis` inside `frozen_x`, `skips` 0 live / 30 paused), `[stats-probe]` (`dn=1` across the pause), `[zoom-probe]` (`gap_px=0.0` at every step) and `[measure-probe]` + the dump's `measures window:` (all-types run `marks_in_window=4/4`, `pin=off` live) on the fast mock. `measure cost:` is unchanged in kind: `raw=37298 (0.0%)` of `covered=121704537`, 1.04 ms a pass -- the runs that read higher are the probe's own zoomed-to-a-slice windows, where the window is legitimately shorter than a bucket. |
||
|
|
d91957a2fe |
fix: a recompute that renumbered its ring, and readouts frozen under a zoom
Three defects, all of them a window describing data that is not there. A math edit refilled the derived ring with `clear()`, which renumbers it from 0, leaving `produced()` short by the inputs' retained base. Since `live_window_locked()` clamps against every channel's `produced()`, that one lagging channel answered `n = 0` for the live view, every pause view and every trigger capture until a whole ring of fresh data had gone by -- the reported freeze, invisible until the ring first wraps, hence its intermittence. It now restarts with `clear_to(base)`, and `ChannelBuffer::republish_from(base)` puts the published window back: `acquire()` is incremental, so a rewrite in place published nothing and the pyramid went on describing the samples the edit replaced. The trigger's channel is an index, re-resolved by name after a renumbering -- but only in `rebuild_enabled_locked()`, so a math add or delete went through `set_math_channels()` with no restore, the index was clamped to -1, and `process()` returned at once while the panel still named the channel. The restore moves into `rebuild_channels()`, which *is* the renumbering, so no caller can forget it. And `update_measures()` suppressed its "the window moved" test while an event was held -- the one mode where a gesture is the only thing that can move it. Zooming a latched event left the pane describing the whole captured event. A pinned window needs no exemption: it lands on the range it already measured and fails the 2 % test by itself. The checks that missed all three: `UDPSCOPE_PROBE_MATHWRAP_MS` edits a graph whose ring has already wrapped, which `math_probe` structurally cannot reach; `[measure-trace]` now prints the mode, the window on screen and the window the numbers describe; and the dump's `marks_in_window` counts against the window on screen instead of against the one the marks were computed over, where it was a tautology. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
074cb611bc |
fix: a clock that stalled on a re-plan, and a dialog that could not close
**The clock.** Trigger mode showed a blank plot and never latched when the session defined a math
channel, because such a session gives the model a plan *before* the stream's own signals are in it:
one derived channel, declared rate 0, so `plan_partition()`'s 1 kHz fallback is the interval those
early frames are stamped at -- 1300x too fast for a 1.3 MS/s stream -- and they have nowhere to go,
since the value loop skips derived channels. Relative mode then stamped a *position* times the plan's
*current* interval, so when the CONFIG's channels landed and the plan took the real rate (769 ns) the
same positions produced times 1300x *smaller* than what was already in the ring, and the monotonic
clamp held the clock at the old value for the rest of the run: one repeated stamp, `history=0`, no
latch, and a window the renderer cannot draw. The same family as the restarted-client poisoning
fixed in
|
||
|
|
27c9b34277 |
fix: a claimed gap must be corroborated before it is believed, and its hole must fit
The user's real host reported "connected and live" with one missing-data band over the whole window
and no trace in it. The counter is the only thing that claims a packet existed, and a reservation
was booked on its word alone: a source whose DATA counter counts samples, or frames of an inner
layer, advances by more than 1 per datagram, the client cannot check that claim (it has no clock of
its own to compare against), so it holds each packet, saturates its reorder ring, delivers it in
order and reports the skipped counters as lost. Every frame therefore reported a phantom gap, and
`reserve_hole()` wrote `lost * batch_` NaN samples with no bound but the 256-PACKET cap -- the
sample form of that rule ("the number to weigh against the ring is 256 * batch_, not the 256") was
never implemented. With relative time on -- which that session carries -- each phantom gap is
materialised as NaN slots by construction: the window filled with reservations, there was never
anything missing to repair them, and the gaps also stretched the relative clock, which is why the
status bar read live at ten times the true rate.
`Acquisition::gap_plausible()` now gates every reservation on two independent tests: the arrival
clock must corroborate the claim (that many missing packets would take that many frame intervals;
deliberately permissive, because a false refusal costs a shading and never data), and the ring must
be able to hold the hole. The verdict is taken before the stamps, and relative mode's slot advance
is skipped with it, since the advance and the reservation are the same claim. Refusals are counted
in `StreamStats::gaps_refused` and said once per connection with the numbers, naming the wire shape
they describe.
tools/mock_udpstreamer.py gains `--counter-step N`: a counter that is not a packet counter on an
intact link, which is the one shape the mock could not produce and the shape the report came from.
Verified in both directions. A counter stepping 1000 per datagram reads `holes=0 hole_slots=0
backfilled=0` with the traces present (3 traces, 1.4 M samples) against 7.9 M claimed gaps, and the
warning above names the cause. Every other packet withheld -- a real loss -- still reads
`holes=3579`, so genuine gaps are still reserved: a fix that stopped believing real losses would
have traded one wrong picture for another. The user's control read as "the math channel is the
problem" and was confounded -- disabling it is a structural change, which rebuilds the buffer and
clears the accumulated NaN.
|
||
|
|
22e7a8efcc |
measure: the split's own means are back as above/below/between
`top`, `bottom` and `amplitude` became the *flat levels* in
|
||
|
|
e83ad86b8f |
measure: top/bottom/amplitude read the flat levels, not the extremes
A bench scope separates Vtop/Vbase from Vmax/Vmin because a band-limited edge overshoots and rings: the mock's `square` at 30 harmonics reaches ±1.179 where its plateau is ±1. On the rippled square the old readings were a property of the edges rather than of the waveform -- `top` 0.986496, `amplitude` 1.179350 -- and `amplitude` reported the overshoot. `top`, `bottom` and `amplitude` are now the flat levels and their delta. A flat level is a density question -- the plateau holds most of the samples and the ringing a few percent -- so `measure_scan_buckets()` folds a 64-bin histogram of the whole buckets' midpoints while it has the list in hand (one observation per bucket, each bin carrying its buckets' exact sum and finite count), and `measure_flat_levels()` takes, either side of the split, the densest bin holding at least half the densest one's buckets -- the outermost of those, which is what makes a waveform with no plateau read its peak -- and the exact mean of the samples of the buckets within 5 % of the window's peak-to-peak of it. 5 % is deliberately under the ~9 %-of-swing Gibbs overshoot. `amplitude` is `top - bottom` (a scope's Vamplitude), not the half-swing it was. Measured, fast mock, 2 s window, harmonics 0 -> 30: top 1.000000 -> 0.999983, bottom -1.000000 -> -0.999983, amplitude 2.000000 -> 1.999966, while `peak to peak` keeps the extremes (2.000000 -> 2.358701). On the 1 Hz sine top is 0.957988 (it was 0.636620, the half-wave mean; a sine has no plateau, so the densest band is at its peak) with sd 0.0014 over a scrolling window, and `peak to peak` is still exactly 2. No raw pass and no new fetch: the histogram is the same fetched bucket list read a second time, gated on a pane that holds one of the three types. `raw` is unchanged (0.1% clean, 2.1% with the --noise 0.1 interferer, against 0.1%/2.1% before) for 0.44 -> 0.50 ms and 0.52 -> 0.65 ms a pass. A window the pyramid cannot summarize -- an hf-filtered walk, or one shorter than a bucket -- keeps the old readings: the mean of each side of the split, and the window's peak to peak for amplitude. Docs: the flat-level invariant (and the two 2/pi assertions it replaces), the probe readings, the manual's measurements page and the README. |
||
|
|
afd2a6cdc1 |
mock: the square's edges ring, and the one probe that wants a step asks for it
The square was the mock's only ideal step, which is the one shape a real producer does not put on the wire. It is now the truncated Fourier series of the step -- `--square-harmonics N` odd harmonics, default 30 -- so each edge overshoots by the Gibbs constant (about 9% of the 2 V swing, y = +/-1.179) and then rings, and `--square-harmonics 0` is the ideal +/-1 step as before. Odd harmonics only, so the series keeps the ideal wave's edge instants and its polarity: the level-0 trigger, the glitch's inverted pulse and the buffer search land where they did. Re-run in pairs (`--square-harmonics 0` against the default), search, condition legs 0/1, trigrenum, disarm, trig trace, measures and events all read exactly what they read before, with only `t0` moving. The reading that moved is the noise probe's group-delay leg, whose `want` is the analytic `tau*ln2` of a step: a band-limited edge reads `delay=0.002770 ratio=1.255`, the same figure on every run. That recipe now asks for the ideal step, so its documented number stays exact; probe-readings says why. The series is vectorised: as a per-sample Python sum it cost ~15 sin() calls a sample, which at the fast mock's 1.3 MS/s made the producer the bottleneck and halved the rate the app saw (the noise probe's span-dependent counts read 22 against 48). Vectorised it adds ~0.5 ms to a 2000-sample packet, so the fast mock runs at about nine tenths of its declared pace and those counts read within a few percent. |
||
|
|
41cd8e5f38 |
help: recommend the system-wide install and record the location trap
A Mallard set under $XDG_DATA_HOME/help is one Yelp cannot draw the figures of: its sandbox exposes the system help directories but not the user's data directory, and each figure is a separate file the page references. The pages open without their images, and through the app's help: URI the viewer spins for ever because the failed image fetch means the page's load event never fires -- which reads as a navigation failure, not a missing resource. The alert and the manual both recommended install-help-user as the no-root route, so they led users into exactly that case. Name the system-wide install first and state the user-local caveat where the advice is given, and record the finding in the GTK traps invariant so the next figure that will not draw is not chased as a markup or a file problem. |
||
|
|
fe499bfe41 | help: the search block's (i) on its header, a 12 px mark, and a real search page | ||
|
|
d549223889 |
fix: the guided tour selects the page a stop's target lives on
A stop whose target is on another page of a stack was entered without switching to that page, so the spotlight aimed at a widget that was not on screen. A stop now declares the page it stands on (`TourStop::page`) and the tour selects it as the step is entered -- the window's trigger and signals stops put the sidebar on Signals, the chooser's tabs stop shows the Waveforms page and stepping off it brings Sources back. The stop filter moved into `tour_begin()` and now runs on `gtk_widget_get_mapped()`: GTK 4.24's `is_visible()` ignores `child_visible`, so it kept a stop on another page *and* one in a collapsed pane alike. A wrong page is kept and selected; a collapsed pane, whose stack is unmapped too, is still dropped. |
||
|
|
829176248a |
help: a tinier (i), hints beside their controls, and the tour starting in the chooser
The contextual help becomes the affordance the ask names: a plain (i) (`dialog-information-symbolic`) at **16x16**, and one that sits beside the control it explains rather than only in a pane's heading. - The mark: `GtkMenuButton` ignores `min-width` on the widget and a size request, so the size is made in `install_css()` on all three nodes (outer `menubutton`, its `button` subnode, the glyph). Measured 16x16 against the default 34x34; the dump prints it as `hint=WxH`, measured with `gtk_widget_compute_bounds()` on the first *visible* hint. - More sites: `events_record` (the "record events" switch in the trigger panel), `trigger_advanced` (the pre-trigger share row), `signals_config` (the signals pane's hint line, wrapped in a box so a hint can join it). Hints join as siblings, so none can cover a switch, pencil or dropdown. - The source chooser gets its own layer -- `chooser_sources` (a new heading above the saved-source list) and `chooser_waveform` (the waveforms action row) -- built with the one `context_help_button()` both windows use and re-synced by its Help menu's toggle. The form's `AdwEntryRow`s cannot host a sibling hint, so none is invented for the host/port fields. - The tour is one implementation over two windows: `ui_impl::TourView` plus a list of `TourStop`s; each window keeps its own table. A walk that passes its last stop calls `on_done`, and the chooser's sets `Config::tour_pending`, so a first run is one tour that crosses from the chooser into the app. The chooser's tour auto-starts from `settings.tour_seen` exactly as the window's does, and its "Guided tour" row now starts *it* instead of only requesting the window's. - Probes: `UDPSCOPE_PROBE_CHOOSER_TOUR=1` / `UDPSCOPE_PROBE_CHOOSER_TOUR_MS`, the hint size in the `help layer:`/`[helpui] contextual on:` lines, and the chooser's hints in its `chooser ui:` line. Docs updated in invariants-gtk-and-chrome.md, probe-readings.md and verification.md. |
||
|
|
395c9caec3 |
help: send readers to the install targets, in the alert and the manual
The failure alert told people to run `cmake --install build --prefix ~/.local`; that still works, but `cmake --build build --target install-help-user` is the form that needs no prefix guessing and prints the path it wrote, so the alert names it and stays its former length. The manual gains a short "Installing this manual" section on the overview page naming both targets -- the no-root user one and the prefix-driven one for a system-wide or packaged install, which the caller wraps in sudo -- and README and the GTK invariants doc follow the alert's new command. |
||
|
|
669db6df9a |
build: install the manual alone, into the prefix or the user datadir
The manual is the only thing udpscope installs, but the only way to get it where `help:` resolves was a whole `cmake --install`. Two opt-in targets now install just the set, each printing where it wrote: - install-help goes through the install machinery into CMAKE_INSTALL_PREFIX (reachable via the new Help component), so a packager or a system-wide user points it at /usr and wraps it in sudo; - install-help-user resolves the user data directory the way the app does ($XDG_DATA_HOME, else $HOME/.local/share) and copies there without root. Neither is built by default and neither changes what `cmake --install` does; the install() rule keeps its destination. |
||
|
|
bb62b8178c |
fix: place the guided tour's card from its allocation, and count GTK warnings
The tour measured its card with a height-for-width request whose for_size came from the natural width minus the margins the tour sets for positioning. A measure reports margins *included*, so that for_size landed a margin short of the minimum the box answers with and GTK warned once per frame: Trying to measure GtkBox 0x... for width of N, but it needs at least M (about 9/s for as long as the tour was up). The card now takes its size from gtk_widget_compute_bounds() once a frame has run -- its allocation rectangle, not gtk_widget_get_width(), which is only the content box -- and falls back to a measure with no for_size at all only on a frame with nothing allocated. GTK is built with structured logging, so its messages reach the log writer and never the handler API; no existing probe can read one. The exit dump now counts them (`gtk warnings:`, must read 0) through a writer function chained to g_log_writer_default(), which leaves stderr unchanged. |
||
|
|
d8a79b96ba |
help: default the contextual layer off, and carry the help into the chooser
The contextual help was on by default, so a first run met a window with a "?" beside every surface it had not asked to have explained; `contextual_help` now defaults to false and the layer is asked for from the Help menu, from the chooser's new help menu, or with Ctrl+H. The source chooser runs before `App` exists, which makes it the window a user who does not know how to start is looking at, so it now carries the same three help affordances the window's Help view does: Documentation (the manual is another process and needs nothing of the app), Enable contextual help (a row whose check *is* the setting, written to the file the app reads a moment later), and Guided tour. The tour is drawn on the app's own window, so the chooser can only record the wish: it sets `Config::tour_pending` -- the value `main()` owns and hands to both `choose_source()` and `run_ui()` -- and `on_activate()` starts it once the window is up and laid out, through its own callback so the readings can tell a request from the first-run auto-start. The rows are the window's own `menu_button()`/`menu_toggle()`; `install_css()` therefore runs from `choose_source()` (it is idempotent now) so the popover gets the `.udpscope-menu` rules before it is shown. Opening the manual could also fail with a message that blamed the handler whatever was missing. `help_installed_path()` now resolves `help:<id>` the way the specification does and the failure alert says which of the two it is: with the set installed it names the viewer, without it it names the identifier, the expected installed path and the install command. `show_help()` gained an optional reporter, which `UDPSCOPE_PROBE_HELP_MS` uses for a new reading -- `[help] launch attempted= done= ok= error='' installed=` -- since a successful launch leaves no window of ours to observe. Tested: clean --clean-first build, three warnings as documented; test-ui 13/13 and test-help pass; no Gtk-CRITICAL in the help, helpui and chooser runs. |
||
|
|
1baa58b8fd |
feat: the help is a submenu, with a contextual-help layer and a guided tour
The hamburger menu's help rows were three flat entries and a window. They are a **`Help` submenu** now, which is where every GNOME menu keeps them, with the four things a first run cannot find: - Keyboard shortcuts Ctrl+/ (the existing overview, `show_shortcuts()`) - Documentation F1 (the existing manual, `show_help()`) - Guided tour Ctrl+T (new) - Enable contextual help (new, a stateful row: its check *is* the setting) The submenu *replaces* the menu's rows rather than floating beside them, the same shape and the same reason as the plot's right-click menu: a submenu torn out of the menu it belongs to is a second thing to dismiss, and the `GtkMenuButton` that owned one toggled itself out of sync the moment anything else closed its popover. It is the pages of a `GtkStack`, not a rebuild of the box and not a swap of the popover's child: the main view holds `app.layout_toggle`, which the tick's `sync_layout_editor()` and the `L` accel read, so rebuilding would leave that pointer on a destroyed widget, and a swapped child would leave the view that is not showing without an owner. **The contextual help is one table and a "?" per surface** (`src/ui_context_help.cpp`): the plot, the trigger, the cursors, the measurements, the math channels, the signals, the event history, the time window, the status line, and the math editor's identity row. Each row names the *heading box* its button joins -- a sibling of the title, never an overlay child on a corner, which is what would cover the switch or the [edit] button already there -- so adding a surface is a row and a widget pointer. Off means *gone*, not greyed: the buttons are removed, and they are found by a data key rather than a list of pointers because the math editor's heading is built afresh on every opening and a list would go stale one dialog later. `contextual_help` and `tour_seen` are keys of `settings.toml`, written through the same `Settings`/`load_settings()`/`save_settings()` the Preferences dialog uses, whose own rows start from `load_settings()` and so carry them through untouched. **The guided tour is five steps on a `GtkOverlay`** above the window's content: the plot, the side panel, the trigger, the signals and the status line. There is no popover in it and therefore no pointer grab to leak -- four dim rectangles around the hole are what make it modal -- and the probe asserts that as a reading (`controllers=` against the count taken before it opened) rather than as a claim. Everything is placed from `gtk_widget_compute_bounds()` re-read on every frame, because a widget's allocation is 0 on the frame it is first shown and a window resize moves it under a one-shot measurement; the hole is kept from the last frame that could measure the target, because a frame that lands between two layout passes would otherwise empty it and make the spotlight jump. `Skip`, `Finish` and `Escape` all hide every child and leave nothing behind. A first run gets the tour once, decided by `tour_seen` and not by "the settings file did not exist" (a user who changed the theme has a file and has still never seen it), and the flag is written when the tour *starts* rather than when it finishes: a user who skips it in the first second has still seen it. `UDPSCOPE_PROBE_HELPUI_MS` drives all of it -- the auto-start, the menu's seven rows and the submenu's five, the toggle's effect read as a count of affordances on either side of it, and the tour started, stepped forwards and back and dismissed -- and the exit dump gains `help layer:` and `help figures:`, the latter being the ten surfaces' window-coordinate rectangles that the manual's annotated screenshots crop their boxes from. Two of them are what the D-Bus-free headless run can still not decide, and both are limitations rather than defects: export carries no "?" (it is a menu action with no surface of its own, and a popover inside the menu's popover is the nested-popover trap the invariants already warn about), and the tour card cannot miss its target when the target is the whole content -- which is what the plot is at the window's 756 px minimum, where the split view overlays the side panel instead of putting it beside the plot. The probe prints both rectangles rather than asserting a placement, so that case reads as the geometry it is. Also fixed while it was found: `gtk_widget_measure()` includes the widget's own margins (measured: a 280 px box with `margin-start 453` reports 733), so a measure that is used to *place* the widget is a feedback loop -- that is what made the tour card walk between the corner and the target from frame to frame. |
||
|
|
c1460a6977 |
feat: the manual is a Mallard set opened in Yelp, not a GResource in the binary
The help was 14 Pango-markup `.txt` bodies in a second GResource, drawn into a paged `GtkWindow` built in `help_window.cpp`. That made F1 a viewer of ours with no search index, no cross-references and no translation, and it kept the manual in the *binary* when every other desktop application's is a document on disk. It is now a real Mallard set: help/C/org.udpscope.app/ index.page + 14 topic pages + figures/*.png and `show_help()` hands `help:org.udpscope.app` to whatever the desktop registered for the `help:` scheme -- Yelp on GNOME -- through a `GtkUriLauncher`, reporting a launch failure with a `GtkAlertDialog` rather than a click that silently does nothing. The paged window's builder, the page table and `help_self_check()`'s markup validation are gone with the resource. The document identifier is the desktop file's own basename (its desktop-file id), derived in CMakeLists from `data/org.udpscope.app.desktop.in`, so the same string is the `.desktop` name, the GApplication id and the install directory at once. The set is installed by a new `install()` rule to `<datadir>/help/C/org.udpscope.app/` -- where the freedesktop Help System Specification's `help:` URI resolves `help:<id>` -- so the manual is read from a datadir by another process even though the binary has no install step. `UDPSCOPE_HELP_NAME` and `UDPSCOPE_HELP_DIR` are compiled in from that one place. The 14 pages were migrated fact for fact, but restructured the Mallard way: a `<desc>` per page (what a search result shows), `<steps>` for procedures, `<terms>` for option lists, `<gui>`/`<key>`/`<keyseq>` for interface words and keys, `<note>` for the asides, and a topic graph via `<link type="guide" xref="index#main"/>`. The figures are annotated screenshots produced by `tools/make-help-shots.sh`, which drives the app headlessly against `tools/mock_udpstreamer.py` and draws the boxes with ImageMagick; the side panel's box is measured from the app's own `side=NNNx` dump reading, the header and status bands come from a documented table, and a crop whose standard deviation is 0 fails the script. `cmake --build build --target test-help` checks the set the way `test-ui` checks the `.ui` files: `yelp-check`'s offline subcommands (links, media, orphans, ids) and a `yelp-build html` render. `yelp-check validate` is deliberately absent -- it fetches the Mallard RelaxNG schema over the network and fails offline for that reason, not for a fault in the pages. The `[help]` probe reading changes with the resource it was written for. `parsed == pages` over in-binary markup cannot exist now, so `help_self_check()` scans `UDPSCOPE_HELP_DIR` and the line reads `topics=14 index=1 figures=3`; `topics=0 index=0` is the manual missing from the tree, which is the finding the old count named. Verified: clean build with the documented three warnings (the help bundle's `-Woverlength-strings` is gone, so the baseline moves from four to three); test-ui 14/14; test-help clean (the four subcommands and 15 pages rendered); a temp-prefix install lands 15 `.page` files and 3 figures at `share/help/C/org.udpscope.app/`; the headless run reads `[help] topics=14 index=1 figures=3 dir=...` with 0 Gtk-CRITICAL. Yelp's own window could not be exercised on this box -- it starts under Xvfb, activates its portals and never maps, the same WebKit/portal limitation that stops a grab -- so its rendering and its search index are unverified here; the content pipeline was (`yelp-build` renders the whole set). Docs: the manual's build/install/open story in `build-and-run.md` and README; the warning baseline in `verification.md`/CLAUDE.md; the help bullets in `invariants-gtk-and-chrome.md`; the `[help]` line in `probe-readings.md`. |
||
|
|
620473a5be |
feat: the locked trigger pane carries Arm/Disarm, and one function states both arm buttons
The compact line -- the pane's locked, "what is it set to" mode -- showed the arm state in words and gave
no way to change it, so arming from there meant unfolding the panel first, which is the thing the lock
exists to avoid. The control is built into the pane's *heading*, the row that stays on screen when the
body goes, and it is wired to the same `on_arm_clicked` the body's button uses, so both do one thing.
Its label, sensitivity and visibility are stated by `sync_arm_buttons()`, called from the four places any of
its inputs can move: the click, the mode toggle, the curator's per-tick re-read (a single-shot latch disarms
by itself) and `apply_pane_lock()`. The label comes from the producer's `trigger_armed()` atomic, the other
two from the toolbar toggle that owns `enabled`, so no caller needs the model lock. Visibility is
`enabled && locked`: with trigger mode off the line beside it already reads "stream (trigger off)" and there
is no engine to arm, and with the pane open the body's own button is the one on screen. Never both -- two
arm buttons would be two controls for one state.
Verified with the arm probe, which locks the pane the way a user does (the [edit] click):
compact arm with the trigger off: visible=0; with the trigger on and the pane locked:
visible=1 sensitive=1 width=51 label=Disarm
[udpscope] compact arm probe: locked=1 enabled=1 own=1 effective=1 width=51 panel_width=-1 label=Disarm
`panel_width=-1` is the "not both" assertion: the body's button is not on screen while the compact one is.
The width cost a defect to read. The control is created hidden and shown for the first time inside the
handler, so a same-tick `gtk_widget_get_width()` reads 0 while `gtk_widget_is_visible()` already says 1 --
and the body button's *stale* allocation (270 px, from when the body was still up) reads as a live one,
because `gtk_widget_get_visible()` is the widget's own flag and stays true under a hidden parent. The
reading is therefore taken one frame later, by a step of its own, and the control is compared with
`is_visible()`. Both defects are in the invariants, because the next control read this way will meet them.
0 Gtk-CRITICAL; clean build with the documented four warnings (the help resource re-measured); test-ui
14/14. Docs: the rule and both readings in `invariants-trigger-and-pinning.md`, the probe's entry in
`probe-readings.md`, and the user-facing sentence in `src/help/08-trigger.txt`.
|
||
|
|
908286ee89 |
feat: the export writes an MDF 3.30 file, so a window loads in ScryLab
A third format beside `.npy` and `.csv`, written by hand in `src/export.cpp` -- MF4 is HDF5 and would mean a library, MF3 is a handful of fixed blocks. The layout is the asammdf 8.8.27 reader's own field list, byte-diffed against a known-good 3.30 file it wrote; the shape is the one ScryLab's data-source contract asks for: one channel group, record-interleaved, the master as the x axis, and the unit in the channel's conversion block. So in MDF the master is `abs_seconds`, named `time` -- the clock that is the same in every display mode -- and the plotted axis goes out as an ordinary channel, `time_relative`. `ExportTable::Column` gained the CONFIG's unit, which only this writer spells out; the `.npy`/`.csv` bytes are unchanged, and `write_export()` now picks the writer for both the dialog and the probe. |
||
|
|
761615c2fe |
fix: a restarted client is a new stream, so its history and its monotonic mark go
Going live from a file view left the display frozen for seconds: the per-second line showed
`ring=[2.5200..2.5200] n=567000 -> n=1131000 -> n=1689000` — the sample count climbing at the stream's rate
while the retained span was one value, the file view's last stamp. The status bar was green because
everything about the *connection* was fine.
It is the poisoning `invariants-data-path.md` already describes, entered through a different door. The
new client's stream clock sits *behind* the stamps the old stream left in `Acquisition::last_stamp_` (the
file view's samples are still in the store), so the advance test rejects every packet of a perfectly good
stream, four rejections abandon the declared time signal, and the arrival-clock fallback derives its epoch
from the surviving mark — every sample stamped flat at the old value. The guard did exactly what it was
built to do; the *stream* was the thing that had changed and nothing had told it.
So `Acquisition::start()` — which is the one place a client (re)starts — now says so:
`ScopeModel::reset_stream()` drops the previous stream's history (`store_->reset()`, which the data-path
invariants require to leave nothing behind) and moves `plan_generation_`, which is what the producer
already watches to drop the state tied to the old store; and `last_stamp_` is re-based to `-infinity` so
the monotonic guard re-bases on the new clock instead of pinning it. The declaration is kept: the first
packet of the new stream passes, and monotonicity holds from there.
One self-inflicted crash on the way, fixed before this: `reset_stream()` dereferenced `store_`, which is
**null until the first CONFIG has planned anything** — at start-up there is none — so the app died with
SIGSEGV and no output at all. Caught by running it plainly rather than trusting the change.
Verified with the events probe (a replay and a group, then *Live*):
live restored (after replay): … pending=0
trig engine: … history=0.4377/0.4000 s now_t=0.298 ring=[-0.1392..0.2985] n=569000
trig engine: … history=0.8723/0.4000 s now_t=0.733 ring=[-0.1392..0.7331] n=1134000 (pending=1: it latched)
trig engine: … history=1.2954/0.4000 s now_t=1.156 ring=[-0.1392..1.1562] n=1684000
`ring` moves within a round-trip, `history` fills, and the trigger latches the frame after the restore;
`make test-ui` 14 files, zero `Gtk-CRITICAL`, and the plain run is clean. Docs: the restart's own door into
the poisoning in `invariants-data-path.md`, and the `ring=` reading's entry in `probe-readings.md` now says
what it must do after *Go live*.
|
||
|
|
62b5f14504 |
test: the per-second engine line shows the store's retained span, which pins a stalled clock
Diagnosing "going live from an event view does nothing for ~5 s, with the status bar green" needed a
distinction the engine's own fields cannot make. `n` counts every stored sample, so it climbs whenever data
arrives at all; what says *why* nothing moves is the store's stamp pair beside it. So the line gains
`ring=[oldest..newest] n=`:
trig engine: … history=0.0000/0.4000 s … now_t=2.520 holdoff_until=2.520 ring=[2.5200..2.5200] n=567000
trig engine: … ring=[2.5200..2.5200] n=1131000
trig engine: … ring=[2.5200..2.5200] n=1689000
Three seconds apart, after `<live>`: the sample count climbs at the stream's rate (the data *is* arriving and
being stored) while the retained span is a single value — the file view's last stamp. So the producer's clock
is **frozen** after the restart, not lagging, which is why `history` never fills, why an armed trigger never
latches, and why a live view with the trigger off does not advance either: one cause, two faces, and it is
not the trigger.
The reading is what tells that apart from a stalled *feed* (ring moving, `now_t` still), which is a different
bug with a different fix — and neither is visible from `now_t` alone. `n` climbing beside a still `ring` is
the clock; a moving `ring` beside a still `now_t` is the feed.
The fix itself is the next piece of work: the producer's clock has to advance across a client restart (or the
file view's retained history has to go when going live), and this pair is the assertion it must satisfy —
after `<live>`, `ring` must move within a stream round-trip. Verified: `make test-ui` 14 files; the fields
observed in the transition run above; no `Gtk-CRITICAL`.
|
||
|
|
4e29b958a2 |
fix: going live restores immediately instead of after a few trigger toggles
The symptom was "view a recorded event, click `<live>`, then toggle the trigger a few times before it
starts streaming again". The mechanism was the restore's own gate: `restore_live_state()` waits for the
*stream's* channel list, because the trigger's stored channel is a **name** and a file view that renamed its
channels (`square (A)`) must be gone before the name means the right signal — and that wait was gated on
`generation()` having moved since *Go live* was pressed.
That gate could never be satisfied in the common case. A **single** event keeps the stream's own channel
names, and the restarted client's CONFIG is identical — which the model treats as *no change on purpose*
(`same_stream_signals()`: "the honest recovery is nothing") — so nothing bumped the generation and the
restore sat there. Toggling the trigger *is* a structural change and moved it, which is exactly the
workaround the report describes.
The gate is now the question it was actually asking: is the snapshot's channel name resolvable against the
live channels (or did the snapshot name none)? For a single event that is true as soon as the client has a
channel list at all, so going live is immediate; for a group the names are renamed, so it waits for the
stream's own CONFIG exactly as before — which is what the gate was for. `live_restore_generation` is still
captured and printed (`gen=a/b`) as information, not as the gate.
Verified against the documented reading, both ways into a file view, from a session with a window and a
trigger so the paired fields have something to disagree with:
[events] live restored (after replay): window=0.500000 (want 0.500000) trigger=1 (want 1) channel='square' (want 'square') level=0 (want 0) enabled=1 armed=1 pinned=0 held=0 mode=2 pending=0 gen=6/3
[events] live restored (after a group): window=0.500000 (want 0.500000) trigger=1 (want 1) channel='square' (want 'square') level=0 (want 0) enabled=1 armed=1 pinned=0 held=0 mode=2 pending=0 gen=16/12
Every want satisfied, `pending=0` in both, `make test-ui` 14 files, zero `Gtk-CRITICAL`. Docs: the
invariants' bullet rewritten around what the restore waits for and why the generation was the wrong
question, and `probe-readings.md` gaining the thing that cost me two runs to find — those four paired
fields are the probe's capture of the live view, so a bare first run reads the application's defaults and
is not a defect; reproduce the documented line from a session that has a window and a trigger.
|
||
|
|
8b2dae7655 |
fix: the overlaid sidebar steps aside for a drag, and the compact view follows the trigger
Two reports, both real, and neither was about the lock itself.
**A drag out of an overlaid sidebar had nowhere to land.** In a window too narrow to show both columns,
`AdwOverlaySplitView` puts the sidebar *on* the plot — that is what `collapsed` means — so the pointer
stays over the list and never over a drop target: a signal could be dragged but not dropped, which reads
as "dragging does not work". `row_drag_sidebar()` now hides the sidebar on `drag-begin` and restores it on
`drag-end`, and only when collapsed: side by side the plot is already visible, and hiding the list the drag
started in would be startling. The reading begins a drag through the *source* with the state forced
collapsed, the way the sidebar probe forces it:
pane drag: sidebar hidden for the drag=1 restored after it=1
**The compact view did not follow the trigger switch.** The locked line's first branch reads
`tc.enabled` — with trigger mode off it says `stream (trigger off)` and nothing else — and
`on_trigger_toggled()` never re-synced it, so the line kept saying whichever it had been. One call to
`sync_trigger_summary()` beside the switch sync. The dump's two `panes:` lines were already the test and
now pass it: with a session whose trigger is off, the line as found reads `stream (trigger off)` and the
live one reads the settings, because the exit probe turns the trigger on in between. Both reading `stream`
was the defect.
Verified: the two lines above; `panes: the same line as the probe found it: 'mode: stream (trigger off)'`
against the live `'signal: square | condition: window, rising | level: 0.3 .. 0.6 | mode: normal'`;
`make test-ui` 14 files; zero `Gtk-CRITICAL`. Docs: the `pane drag:` reading and the invariants bullet for
the overlay, and the `panes:` pair's second job — the refresh test — written down so the next reader knows
what both lines reading `stream` means.
|
||
|
|
a43d3830f0 |
test: the locked signal list keeps its drag, and the reading says so
Asked for: dragging a signal onto a plot must still work in the compact (view) list. It does — the drag
source is on the *row* and was never conditional on the lock — but nothing said so, and "the lock rebuilds
this list and drops three of its controls" is exactly the change that could take the drag with it. Now it
is a reading: **`drag_sources` must be `rows - 1`**, one per signal row, the `MATH` divider not being
draggable.
signals panel: … rows=3 signals=2 math=0 divider=1 drag_sources=2 (locked)
signals panel: … rows=5 signals=4 math=0 divider=1 drag_sources=4 (configuration mode)
Measured the direct way first, with a throwaway walk of the list's rows and
`gtk_widget_observe_controllers()`: `locked=1 rows=3 rows_with_a_drag_source=2`, the third row being the
divider. That is where the number in the reading comes from, and the walk is what found the shape of the
answer rather than assuming it.
One consequence worth recording, because it is an improvement rather than a cost: a locked row can be
dragged from *anywhere* on it, the colour chip included. In configuration mode the chip carries a click
gesture that claims the event sequence precisely so a drag cannot start there (it opens the colour
chooser); the locked list does not build that gesture, so the chip is inert and the row's drag source
sees the press.
What the lock does *not* change is which signals are listed: only the ones that are on. A signal nobody
acquires draws nothing on a plot, so a row for it would be a drag that ends in an empty slot.
Verified: both lines above, from sessions differing only in whether one exists; `make test-ui` 14 files;
zero `Gtk-CRITICAL`. Docs: the `drag_sources` field with what a `0` would mean, the invariants' lock
bullet, and the README's note.
|
||
|
|
d8705a369f |
ui: Arm is disabled with trigger mode off, and the redundant Go-live button is gone
**`Arm` is now disabled while trigger mode is off**, and labelled `Arm` again: it is the one control in that pane that is *runtime* rather than setting, so with no engine running there is nothing to arm. It is also the only thing in the body that stays disabled — the settings themselves stay editable, which is the point of the previous commit. `arm sensitive with the trigger off=0` is the reading. **`Go live (trigger off)` is deleted.** Its handler was `trigger_toggle → FALSE`, i.e. exactly the toolbar's trigger switch and the pane's own switch, which is what it was for: "the way out of trigger mode, from inside the panel that froze the view: it is the toolbar's trigger switch, so there is exactly one piece of state." With that switch now in the pane's own heading, the button was the same control drawn a third time. The status line that told users about it says "or switch the trigger off to stream" instead. The events pane's own `events_live` button is a different button on a different path — the file view restore — and is untouched. **And one silent regression this work uncovered.** The `trigger panel:` frame reading had been walking the *wrapper* box the app puts around the `.ui`'s panel (heading row + locked summary + panel) ever since the heading moved out of the file in the lock commit, so it reported `boxes=2 … search_inside=0/4` against a panel that was entirely correct. It reads `trigger_body` — the file's own panel — again, and now says so in its comment, because a reading that can silently start describing the wrong widget is the failure it exists to catch. Verified: `trigger panel: boxes=15 controls_inside=15/15 search_widgets=4/4 search_inside=4/4` (the documented shape, restored); `arm sensitive with the trigger off=0`; `trigger panel sensitive …=1`; `panes:`/`panel switches:`/`pane edits:` unchanged; `make test-ui` 14 files; zero `Gtk-CRITICAL`. |
||
|
|
796dda4ad5 |
fix: the trigger stays editable while it is off, and the locked pane says "stream"
Two things the lock got wrong, and they are the same mistake seen twice: trigger-off was treated as "the
pane is disabled".
**The configuration was unreachable exactly when it was wanted.** `on_trigger_toggled()` set the trigger
*body* insensitive with the mode — a rule carried over from the five-revealer sidebar, where a closed pane
had nothing to edit. A trigger is configured *before* it is switched on, so switching it off to change it,
or to read what it is set to, is the ordinary way to work. The body is now never disabled; only the
*cursors'* readout still is, which is the pane whose content has nothing to show with the instruments off.
**The locked pane described a trigger that was not running.** With trigger mode off the summary is one
line — `mode: stream (trigger off)` — because the four lines it used to show (signal, condition, level,
mode) describe a trigger that is not there. With the trigger on it is those four plus whatever is not at
its default (`noise band`, `HF reject`, `pre-trigger`, `hold-off`).
The reading changes with it: `trigger panel sensitive …=1` is now a **regression guard** rather than a
state — the trigger's body is always live, so a `0` there means the settings have been locked out again.
Verified in both states, from sessions whose only difference is `trigger_enabled`:
panes: trigger=locked signals=locked body_visible=0 summary='mode: stream (trigger off)'
panes: trigger=locked signals=locked body_visible=0 summary='signal: square | condition: window, rising | level: 0.3 .. 0.6 | mode: single | pre-trigger: 20 %'
trigger panel sensitive before the probe toggled it: 1
The locked trigger pane is 72 px with the trigger off and 192 px with it on. Docs: the invariants' rule
for which body is disabled and why the trigger's is not, the `summary`'s two forms with what each means,
the README's deviation note, and the second `panes:` line renamed to say what it is — the line as the
probe found it, because only a run whose *session* has the trigger off shows the stream form.
|
||
|
|
4f4851f4e3 |
fix: the [edit] buttons' icons, and the locked pane says four lines not one
Two things the lock shipped wrong. **The "done" icon did not exist.** `emblem-ok-symbolic` is a breeze icon; Adwaita — which is what this app runs under — does not have it, so the button that leaves configuration mode drew a missing-image placeholder, i.e. nothing. Both names are now Adwaita's own: `document-edit-symbolic` (the pencil: edit) and `object-select-symbolic` (the check: done). This is exactly the silent failure `toolbar icons:` exists for, so the two `[edit]` buttons get the same treatment: `pane edits: locked=…(ok) config=…(ok)` is read by clicking into configuration mode and back, so one line carries both names and `gtk_icon_theme_has_icon()`'s verdict for each. The invariants now carry the rule, including the trap that a name found under `breeze` is not an answer. **The locked pane was too compact.** One line could not hold what a user opens the pane for, so it is now a small definition list: `signal`, `condition`, `level`, `mode` on four lines, and after that only the figures that are *not* at their defaults — `noise band`, `HF reject`, `pre-trigger`, `hold-off` — because a line reading "band 0" is a line that says nothing. The channel is resolved from the acquired channels and the condition quotes the figures its own shape reads (a width and its comparison, two levels, or a timeout), never one it ignores. The pane is ~192 px tall locked against 580 in configuration mode. One self-inflicted critical on the way: the probe read the summary with `gtk_label_get_text()`, and the container is a grid now — `Gtk-CRITICAL: assertion 'GTK_IS_LABEL (self)' failed`. The text comes from the sync that writes it instead. Verified: `pane edits: locked=document-edit-symbolic(ok) config=object-select-symbolic(ok)`; `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'`; `make test-ui` 14 files; zero `Gtk-CRITICAL` and no missing icon. |
||
|
|
121abbefae |
feat: locked panes for a returning user, and the trigger is persisted
Two panes, two modes. **Locked** 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 (the Adwaita `emblem-system-symbolic`,
swapping to `emblem-ok-symbolic` in configuration mode) that flips its pane. A **first run** — no session
file for that source — never reaches `apply_layout()` and comes up in configuration mode, because
configuring the host is what a first run is for. The mode is a mode and not a setting, so it is
deliberately not persisted.
The locked signal list drops the rows of every signal that is off, which is the whole of "show active
signals": the list already filters on nothing, so the lock is one `continue`. With the rows go the acquire
tick, the overflow menu (Information, Calibrate…, Delete, rename) and the `MATH` divider's `+` — and the
colour chip stops being clickable, because choosing a colour is configuration too. Dragging a signal onto
a plot stays: that is which view shows what, not what the scope is set up to do.
**The trigger is persisted, settings and state both** — the settings *even while it is off*, which is the
point: a trigger configured once and switched off is still the trigger you come back to. `LayoutState`
gains fifteen flat `trigger_*` keys; the channel is stored by *name* (every rebuild renumbers the
channels, the model's own rule) and the two enumerations as **the words the panel's dropdowns show**,
because an index ties the file to a declaration order — the lesson `math_op_at()` already carries. The
words are shared with the panel now (`trigger_type_text()` and friends build the dropdown arrays), so the
two cannot drift.
Two ordering traps, both found by running it:
- **The channel name resolves twice over.** `rebuild_trigger_signals()` picks up
`App::pending_trigger_channel` when the stream arrives *after* the load, and `apply_trigger_layout()`
resolves it itself when the channels are already there — without the second path the name stayed
pending past the one rebuild that would have used it, and the trigger silently sat on channel 0
(`summary='pulse width · sine …'` where the session said `square`).
- **The trigger's state is applied by pressing the control, not by setting a flag**: the toggle is set
*outside* the `updating_panels` window `apply_layout()` uses, because a toggle set inside it emits
`toggled` into a handler that returns at once and nothing would happen — no arming, no events button,
no sensitivity.
Verified, in both modes and both directions:
panes: trigger=locked signals=locked body_visible=0 summary='window · square · 0.3…0.6 · 20 % pre'
panes: trigger=config signals=config body_visible=1 summary=''
signals panel: labels=2 list=1 rows=7 signals=2 math=4 divider=1
The locked trigger pane is 42 px tall against 580 unlocked. A hand-written session round-trips: every
value it named came back in the panel's own controls, `trigger=1/1` (the state arrived and the pane's
switch followed it), and the file the app wrote back carried all fifteen keys with the untouched ones at
their defaults. `[side-tab-probe] … advanced=4` on its documented recipe; `make test-ui` 14 files; zero
`Gtk-CRITICAL` and no missing icon.
Docs: the `panes:` reading with what each field means, the corrected `labels=` note (the panel's frame is
counted above the list now, because the heading sits in a heading row), the invariants for both the
persistence and the lock, the README's deviation note, and the removal of its claim that the trigger has
never been persisted — it is now, and the locked view is built from exactly that.
|
||
|
|
cc8335c514 |
ui: each pane carries its own switch, the same one the header bar has
The trigger pane and the cursors pane now have a `GtkSwitch` in their heading, and it is *one control in
two places* with the header bar's button: both run the same action (`run_app_action()`), and
`sync_panel_switches()` re-reads each pair from the toolbar's state after any change, so clicking either
end moves both. A pane whose feature is off is disabled, which is exactly the state you need the switch
in, so it belongs *in* the pane rather than only in the toolbar.
Three things it needed:
- **The sensitive target moved to the pane's body.** A control inside an insensitive parent cannot be
clicked, so a switch that greyed out with its own pane could never turn it back on. `on_trigger_toggled`
and `on_cursors_toggled` now disable `trigger_body` / `cursors_body`, and so does the startup default;
the headings, with their switches, stay live.
- **The trigger's heading moved out of `trigger-panel.ui`** and is built in `build_trigger_panel()`,
which returns the heading row and the panel together. It has to be a sibling of the body for the switch
to stay usable, and keeping it in the file would have meant the file owning a widget the code must
arrange. The file's own panel is unchanged otherwise, so the `[trigger panel:]` counts still read.
- **A guard on the sync.** Setting a `GtkSwitch` emits `notify::active` for a programmatic write just as
it does for a click, so without `App::panel_switch_sync` the program's own write would come back as a
click and flip the feature again.
Readings, both directions in one line:
trigger panel sensitive before the probe toggled it: 0; … ; pane switches live while their panes are off=1
panel switches: trigger=1/1 cursors=1/1 drove_toolbar=1 (pane switch / header bar button)
`pane switches live …=1` is the design's point and is read before anything turns either pane on; `0` would
be a switch drawn, greyed and useless. The trigger pair is moved from the *toolbar* end and the cursors
pair from the *pane* end, so the line carries both halves of the sync, and `drove_toolbar=1` is the pane's
own switch having turned the cursors on with the button following. `trigger panel sensitive …=0` is the
body, not the panel.
Verified: the two lines above; `[side-tab-probe] … advanced=4`; `make test-ui` 14 files; zero
`Gtk-CRITICAL` and no missing icon. Docs: the new reading in `probe-readings.md` with what a `0` means,
the invariants bullet for the one-control-in-two-places rule and its two traps, the sidebar bullet's
sensitive target, and the README note on why the switch is in the pane.
|
||
|
|
d9287ea826 |
ui: the trigger's wave is a full period again
The bold pass had turned the trigger into half a period — a signal rising above its level — and that lost the thing the glyph is for: the crossings. A crest and a trough read as a wave; a hump reads as a hill, and a trigger that shows no crossing names the "above the level" half of an event. The full-period sine with the dashed level through it is restored, exactly as it was, and the Signals pill carries the same wave again so the pill and the toolbar button stay one picture. The Measure glyph keeps the bold redesign, which is the part that was better: two cursors (a cap over two 4px dashes) beside a ruler of two bands with three 2x4 marks and no end caps. The Signals pill also keeps its two bold channel rows. Verified: `toolbar icons: trigger=udpscope-trigger-symbolic(ok)`; `page icons: measure=udpscope-tab-measure-symbolic(ok) signals=udpscope-tab-signals-symbolic(ok)`; `switcher: policy=narrow pills=2 labels=0 names=2 icon=32x32 size=308x36`; `[side-tab-probe] … advanced=4`; the Preferences dialog opened in the same run with no "not present"; `make test-ui` 14 files; zero `Gtk-CRITICAL`. The icons' blob is 12352 bytes, still over the C99 limit, so the clean-build warning count stays four. |
||
|
|
8c9c6ff7fd |
ui: bolder tab glyphs, and the trigger shares the Signals pill's picture
The previous pair were detailed miniatures — 1px ticks, 2px dashes, a 13-column wave — and at 32x16 that reads as *texture* rather than as a shape, which is what "not clear" looks like. All three glyphs are redrawn bold: one thick band, two thick dashes, few features. - **Measure** is two cursors beside the ruler they read against. Each cursor is a cap over two bold dashes (4px), where three 2px dashes read as a dotted column; the ruler is two bands with three 2x4 marks between them and **no end caps**, because a closed frame reads as a rectangle and costs a fifth of the ink on two lines that say nothing. This is the layout whose first attempt looked like two icons in a box — the difference is entirely the weight and the open ends. - **Signals** is a signal rising above its level beside two bold channel rows. Half a period instead of a full one: at this size a full period is a zigzag and a hump is unmistakably a wave, and the level's marks survive on both sides of it instead of being swallowed by the crest. - **The trigger's own icon** is now the same picture as the Signals pill, so the pill and the button that turns the trigger on are recognisably about the same thing. Each was rendered and read back as an ink map at 1:1 and at 2x before it was installed, and three candidates were compared for the Measure glyph (markers *on* a scale, cursors *over* a ruler, and this): the first two turned the marks and the shafts into blobs at the crossings. Ink is 18%, 30% and 29%, inside the range the icons above them set, and everything stays inset two pixels. Verified: `toolbar icons: trigger=udpscope-trigger-symbolic(ok)`; `page icons: measure=udpscope-tab-measure-symbolic(ok) signals=udpscope-tab-signals-symbolic(ok)`; `switcher: policy=narrow pills=2 labels=0 names=2 icon=32x32 size=308x36`; `[side-tab-probe] … advanced=4`; the Preferences dialog opened in the same run with no "not present"; `make test-ui` 14 files; zero `Gtk-CRITICAL`. The icons' blob is 11774 bytes, still over the C99 limit, so the clean-build warning count stays four. |
||
|
|
574f47f769 |
ui: the Measure pill is two cursors on a scale, not a boxed ruler beside them
The previous Measure glyph was two icons sharing a box: a heavy closed ruler frame on the right and two light caps on the left, with nothing relating them. It is now one instrument — two cursors *standing on* the scale they read against, each marker centred on one of the scale's marks, so the pair reads the way the pane works: bracket a window, read it against the axis. Three things the render said, each fixed: - **The closed frame was most of the ink.** Two lines and two end caps cost about a fifth of the glyph's ink and say nothing a ruler's marks do not, so the scale is now one band with alternating marks — long, short, long — hanging above it. - **The caps were dashes, not grips.** 5x2 at 32px read as a horizontal stroke; they are 5x3, like the caps on the toolbar's cursors icon. - **The ticks were finer than everything else.** At 1px they were thinner than the 2px weight every icon here uses, so they are 2px, and every mark alternates in height the way a ruler's do. The Markers' shafts are dashed and meet the band rather than hovering over it, and the whole glyph is inset two pixels like the others, so nothing is flush with an edge that a button's padding or a crop takes a pixel off. Ink: 27%, inside the range the icons above it set. Verified: `page icons: measure=udpscope-tab-measure-symbolic(ok) signals=udpscope-tab-signals-symbolic(ok)`; `switcher: policy=narrow pills=2 labels=0 names=2 icon=32x32 size=308x36`; `[side-tab-probe] … advanced=4`; the Preferences dialog opened in the same run with no "not present" (the plain ruler it uses is untouched); `make test-ui` 14 files; zero `Gtk-CRITICAL`. The icons' blob is 12624 bytes, still over the C99 limit, so the clean-build warning count stays four. |
||
|
|
75aa62cc3c |
ui: the trigger glyph is a sine crossing its level, and every glyph is inset
The trigger's toolbar icon was a rising edge, which names only one of the four shape triggers the panel offers. It is now a sine with a dashed level through it — the trigger's actual job, find the signal where it crosses this — and the Signals tab's pill carries the *same* picture beside the channel list, so the pill and the button that turns the trigger on read as the same idea. Both are one period of one-pixel stairs, the way the plot rasterises its own traces. Everything is inset two pixels now. The previous drafts ran their level and their bars flush to the cell edge, which is what makes art look shaved wherever a button's padding or a crop takes a pixel: nothing in either tab glyph, or in the trigger's, touches an edge. Each shape was drawn and then read back as an ink map rather than trusted — the first sine came out as a thin antialiased diagonal (a polygon through whole-pixel vertices has diagonal *edges*), which is why the band is now per-column stairs; the level-through-the-centre arrangement was chosen over level-above-the- centre after rendering both, since at these sizes a level nearer the crest merges with it into a blob. The four glyphs measure 18%, 22%, 25% and 25% ink, inside the range the icons above them set. Verified: `toolbar icons: trigger=udpscope-trigger-symbolic(ok)` and `page icons: measure=udpscope-tab-measure-symbolic(ok) signals=udpscope-tab-signals-symbolic(ok)`; `switcher: policy=narrow pills=2 labels=0 names=2 icon=32x32 size=308x36`; `[side-tab-probe] … advanced=4`; `make test-ui` 14 files; zero `Gtk-CRITICAL` and no missing icon. The icons' GResource blob is 12408 bytes — the SVG sources are embedded, comments included — still over the C99 limit, so the clean-build warning count stays four. |
||
|
|
dbe6377415 |
ui: the tab glyphs are 32x16 pairs, drawn twice as wide
Each tab's icon is now two motifs side by side instead of one in a 16px square: the Measure pill is the two cursors beside the ruler, the Signals pill the trigger beside three channel rows. A 16px cell carries one motif — squeezing a pair into it is what turned both into a smear — and the pill has the width for a pair in a 300px column with two pills in it. Two measurements decided the size, both of which I had wrong from the outside: - **GDK paints an icon into a square box whatever the document's aspect.** `gtk_icon_theme_lookup_icon()` at 32 reports a `32x32` paintable for a 32x16 document, and librsvg fits it: rendered with aspect kept, the artwork comes out `32x16` inside a 32 request. So the pair is letterboxed, not stretched, and `pixel_size` 32 draws it at exactly the size it is written — 1:1, so the 2px bars stay crisp. A fractional scale (24 or 48) is what would blur them, which is why nothing here is drawn at one. - **The bar does not get taller for it.** `switcher: … icon=32x32 size=308x36`: 36px is what the switcher measured with 16px icons too, because the pill's fixed height takes a 32px box without growing. Both glyphs were drawn and then read back as ink maps rather than trusted: the first drafts came out at 39% and 44% ink (a blot beside the house 25–36%), and the tab bar is the last place a shape can be checked, since no screenshot works under this X setup. The finished two measure 23% and 33%. Verified: `switcher: policy=narrow pills=2 labels=0 names=2 icon=32x32 size=308x36`; `page icons: measure=udpscope-tab-measure-symbolic(ok) signals=udpscope-tab-signals-symbolic(ok)`; `[side-tab-probe] … advanced=4`; the Preferences dialog opened in the same run with no "not present"; `make test-ui` 14 files; zero `Gtk-CRITICAL`. The icons' GResource blob is 11661 bytes, still over the C99 limit, so the clean-build warning count stays four. |
||
|
|
e615465366 |
ui: each tab's glyph is a pair, not one icon
The Measure tab is now `udpscope-tab-measure-symbolic` — the two cursors standing across a ruler — and the Signals tab is `udpscope-tab-signals-symbolic` — the trigger's rising edge over the channel list it acts on. Each is a 16px composite rather than one of the icons the toolbar carries: a glyph the toolbar already uses for one thing means two things the moment a tab shows it too, which is why the ruler alone and the trigger alone were both wrong here. The plain ruler stays exactly where it was. `udpscope-measure-symbolic` is the Preferences window's Plot page icon and is untouched; the tab's pair is a new file beside it, so nothing that already meant "the measurement ruler" changed meaning. Seven action icons bundled, nine with the two app icons. Both composites are at the limit of what 16px holds — four features each — so each was drawn and then *measured*: rasterized and read back as an ink map, since the shapes have to be checked as pixels and the house range is 25-36% ink (trigger 25, cursors 27, ruler 36). The Signals list is two rows and not three, so the edge and the list get half the cell each; its bars are all one length, because unequal ones read as a bar chart and a chart is not a list. The Measure ruler's ends are open on purpose: a closed frame puts four more pixels of ink at the left and right edges, which is what turns a ruler into a rectangle at this size. First drafts came out at 39% and 44% ink and were rebalanced to 32% and 36%. Verified: `page icons: measure=udpscope-tab-measure-symbolic(ok) signals=udpscope-tab-signals-symbolic(ok)`; `switcher: policy=narrow pills=2 labels=0 names=2`; `[side-tab-probe] … advanced=4` (switching by page name still works); the Preferences dialog opened in the same run with no "not present"; `make test-ui` 14 files; zero `Gtk-CRITICAL`. The icons' GResource blob is now 11309 bytes, still over C99's 4095-byte string-literal limit, so the clean-build warning count is still four. |
||
|
|
0b146f1d53 |
ui: the tabs are icons, and the pills keep their names
Each sidebar page gets its own glyph and no visible text. The Measure tab reuses the ruler
(`udpscope-measure-symbolic`, the icon its deleted toolbar toggle used and the Plot preferences page
still carries); the Signals tab gets a new `udpscope-signals-symbolic` — three rows of a colour chip
and a name, which is what that tab mostly is — rather than a second copy of the trigger glyph, since
one shape meaning two things in one window is worse than a new one. Its bars are all one length: a
set of unequal ones reads as a bar chart, and a chart is not a list.
`AdwViewSwitcherPolicy.narrow` alone does **not** give an icon-only bar. Measured on libadwaita 1.10
by walking the switcher's tree: with `narrow` and a title, each pill holds a `GtkImage` *and* a
`GtkLabel` — 48px of "Measure" inside a 54px pill. So the pages' `title` is **empty** (their `name` is
what identifies them to `select_side_tab()` and to the session file, and both are unchanged) and the
words are re-attached in code: `name_side_tabs()` runs on the first tick — the pills are
`AdwViewSwitcher`'s own and exist only once it is mapped — and gives each pill a **tooltip and an
accessible name** from the page's name, then hides the empty label that would otherwise leave the
icon off-centre. An icon-only pill is still a named control for a screen reader, and the page icon
that does not resolve leaves a *blank* pill, which is why it is read like the toolbar's icons are.
Two readings, because the claim is two claims:
switcher: policy=narrow pills=2 labels=0 names=2
page icons: measure=udpscope-measure-symbolic(ok) signals=udpscope-signals-symbolic(ok)
`labels` counts *mapped labels with text* — a plain mapped count reads 2 either way, because the
narrow pill keeps an empty label in its layout — and `pills=2` is the guard that makes it mean
something: with nothing mapped the rest of the line is void rather than a pass.
Docs: the icon census (six action icons, eight bundled, and what each of the two new users is), the
switcher bullet in `invariants-gtk-and-chrome.md` with the measurement above, the two new dump lines
in `probe-readings.md`, the re-measured GResource figures in `verification.md` (icons 8613 -> 9842
bytes, help 514660 after this session's help rewrite; the count stays four), and the README recording
that the design spec had the switcher showing titles and neither page an icon.
Verified: `make test-ui` 14 files; `[side-tab-probe] … advanced=4` (tab switching by name still
works with empty titles); `signals panel: … divider=1`; `[math-probe] 3 math channels`; zero
`Gtk-CRITICAL` and no missing icon.
|
||
|
|
6492bb910f |
test: a probe reads the hidden tab's accumulator, and the docs follow the tabs
`UDPSCOPE_PROBE_SIDE_TAB_MS` is the one reading for the design's §5 rule -- the statistics accumulator keeps running on the hidden page -- which nothing tested before. It switches to Measure, forces one pass so there is a baseline, switches away to Signals and reads again two seconds later: `advanced=4` is the assertion, and `advanced=0` is the defect where the accumulation is gated on the visible tab, which would leave a gap in min/max/avg for exactly as long as the trigger was on screen. Two things the probe has to do for itself, and getting either wrong makes `advanced=0` a lie about the tab: - it **enables its own signal**, because a signal starts disabled (enabling is what starts acquisition) and a probe that waited for one to be on would be reading another probe's side effects -- and `UDPSCOPE_ENABLE_ALL_MS` is not part of this run; - it **assigns that channel to a plot**, because a channel with no trace leaves the live window *still*, and a still window is precisely the state `update_measures()` is supposed to skip. Measured the hard way: without the assignment the probe read `advanced=0 min_n=0` with the view pinned and only four passes in twelve seconds, none of them the tab's doing. Docs: `probe-readings.md` gains the probe's entry and the signals panel's new shape (`rows == signals + 1 + math`, `divider=0` a list that lost its `+`), loses the whole `math panel:` entry, and follows `pane_sensitive=`, the gone `revealed=` field, the six toolbar controls and the tab-reading `m`/`g` key pairs. `verification.md` gains the probe's bullet and the re-measured warning figures. The invariants record the sidebar as a switcher above a stack with a scroller per page, the hidden-page trap that replaces the unrevealed-revealer one for it, that a feature's on/off is sensitivity rather than visibility, that a math channel is a row of the signal list, and that a cursors-off readout blanks to `--` with the panel left on screen. `shortcuts.ui` and the help page's window and sidebar bullets stop describing five revealers. The README records the deviation from `SPECS.md` that names the panes as separate collapsible sections. Verified: `[side-tab-probe] page=signals rows=4 accums=4 advanced=4 min_n=16`; `signals panel: labels=2 list=1 rows=9 signals=4 math=4 divider=1`; `panels: … (page_current=1) (sensitive=0) switcher=1`; `toolbar icons:` six controls with every name `(ok)`, with the Preferences dialog opened in the same run; zero `Gtk-CRITICAL` and no missing icon. |
||
|
|
4235ee6225 |
fix: a file view states its pin once, so the axis holds still
`set_data_pin()` takes a range and `pin_event_window()` a pre/post split; the file view handed the split to both. On a recording whose stamps start before zero that stored a pin that no longer held the data, so every second frame found the pin wrong and stated it in full again -- rewriting the group's x range and discarding the user's gesture with it. The view alternated between the file's span and the corrupted one at the frame rate: a flicker in which no pan, zoom or click survived. `[drop-probe]` now reports the view and the count of pin statements, which must stay at two for a whole run. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
345145c52b |
fix: a loaded recording is filtered into its buffer, not truncated
The loader pushed one slot per file row while the plan decimated by eight, so a 10 M-row file overran the ring and only its last 4.19 s survived -- a 20 s recording that read as a 4 s one. Each group of `time_stride` rows is now averaged into one slot at the group's mean stamp, as the packet path already does, and the load line reports rows and decimation so a filtered file cannot be mistaken for a truncated one. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
50a1ddca62 | added loading screen | ||
|
|
881b52748e | fixed event/live transition | ||
|
|
bb99be5889 | improved perfs and event management | ||
|
|
667ed0c100 |
docs: the noise probe arms at 4 s, not 3, and why
`UDPSCOPE_PROBE_NOISE_MS=3000` collides with `enable_all_probe`'s own last act, which fires at
`ENABLE_ALL_MS + 2000` = 3 s: that lambda walks `signal_list` for the front channel's acquire
tick and clicks it, and a probe that installs a math graph at the same instant rebuilds the
signal panel under it. The off-step then emits `active` on a freed `GtkCheckButton` --
`GLib-GObject-CRITICAL: instance with invalid (NULL) class pointer`, stack in
`enable_all_probe()::{lambda}` -- which the baseline ("no Gtk-CRITICAL at all") counts as a
finding. Confirmed by bisection: armed at 3 s it is reproducible, at 4 s it is gone, and a plain
exit-dump run shows none.
That fragility is the probe's, not the feature's, and it is now documented beside the recipe: every
documented probe time is offset from `ENABLE_ALL_MS + 2000`, not merely from the other probes.
The reading times move with it (legs ~11.5 s, de-noise ~16.5 s, both inside the same 18 s exit),
and the corrected run reproduces every documented figure bit for bit.
Readings at 4 s: `setup channels=1`, `trigger band: bare=49/2 band=7/2 ratio=7.00`,
`trigger hf: hf=7/2 band+hf=7/2 band=7`, `pw`/`period band`/`period hf` exactly 0.200000,
`measure windows=2`, `denoise raw=2.987696/1 denoised=2.000000/1 ratio=0.669 channels=1`, no
`Gtk-CRITICAL`.
|
||
|
|
749b34a2cb |
docs: record the three noise-rejection controls and their invariants
README gains the three controls where each lives -- the trigger's `band`/`reject`, the measurement's `noise`/`hf`, and the math channel's `de-noise` node -- each with its unit, what "off" is, and what it costs, plus the two deliberate deviations from the design spec next to the features they belong to: the band applies to `edge` and `window` only (a pulse's width is measured from a crossing to its return, so a band there changes the measured width rather than rejecting noise; a dropout is an absence with no level to compare), and the band is verified through `BufferSearch` hit counts rather than `[condition-probe]` latch times, because probe-readings.md states that a `t0` is not an assertion. The invariant docs gain the rules a later change must not break: `EventCondition`'s two new fields and the once-per-packet `configure(dt)` that must not reset; the crossing rule that a change is between two *decided* sides; the measurement path's exception to "a readout quotes a stored sample", why the band moves the edge decision only, why `measure_accumulate` loops samples outer, and why a filtered window disables both bucket fast paths; that `MathOp` is append-only (the index *is* the serialisation), that the median window is pooled in the runtime rather than held per node, why the pole reuses `b0`/`z1`, and that median-then-pole is the design; the session's named `noise`/`hf` keys and its thirteen-field node string that still reads twelve; and the two GTK traps -- positionally brace-initialised dialogs, and a control that cannot apply being hidden rather than disabled. |
||
|
|
c38630643d |
test: the noise probe reads the de-noise node, and the math probe builds one
The probe gains a staged chain rather than one phase, and the schedule moves with it: `UDPSCOPE_PROBE_NOISE_MS` now times the *setup* stage, which installs the de-noise graph through `apply_math_channels()` -- a structural rebuild that replaces the buffer and drops its history -- five seconds before anything scans it. The legs and the five measurements then run at about 10.5 s, and the reading that says the median rejected every impulse at 15.5 s, so the documented run needs a 20 s mock and an 18 s exit. The de-noise channel is installed **disabled** and enabled only at 13 s, for its own reading. An acquired channel is a share of the ring, so a fifth one shortens every channel's retained span -- and the six `BufferSearch` leg counts and `pkpk bare` are all peaks and counts *of that window*. Installing it enabled (as the plan had it) moved `bare` from 49 to 21, `ratio` from 7.00 to 3.00 and `pkpk bare` from 2.988 to 2.488, i.e. it would have rewritten the older feature's documented proof to record a regression that is not one. With it enabled only at the end, every reading from Tasks 4 and 8 is bit-for-bit what the doc records, and the new line reads `denoise raw=2.987696/1 denoised=2.000000/1 ratio=0.669 channels=1` -- `denoised` exactly the clean sine's 2.0, which is the claim the node makes. Two other findings, both folded in: a derived channel whose source is switched off through the panel's checkbox follows it off and does *not* come back when the source returns (the enable-all probe's last act switches `sine` off at ~3 s), and the probes reload leftover math channels from `$XDG_CONFIG_HOME/udpscope`, which is why a `setup channels=` count above 1 means an earlier run's session, not a defect. `math_probe` gains a third channel with a `DeNoise` node so the editor's median row is built and synced on a baseline run: `3 math channels on 'sine' and 'noise'`, every other line as before, `math cost: 103.3 ns/slot` (one reading of the same path). verification.md: the recipe, and why the two stages may not be collapsed -- a probe that rebuilt the buffer under its own legs reads `bare=0` and looks like a broken comparator. Also that the noise and math probes may not share a run: both write `app.math`. |
||
|
|
2c0e21fd20 |
test: the noise probe reads a measurement's band and HF reject
Five measurements on the same spiked `sine`, asking for corners 0, 50, 0, 0, 50 on one
channel: two windows under the (channel, fc) key, which is what exercises it.
measure pkpk bare noise=10.0 hf=0.0 value=2.987692 the stimulus, impulses included
measure pkpk hf noise=10.0 hf=50.0 value=1.990075 the waveform: pass one is filtered
measure period noband noise=0.0 hf=0.0 value=0.021290 supposed to be WRONG -- the control
measure period band noise=25.0 hf=0.0 value=0.200000 the band alone
measure period hf noise=0.0 hf=50.0 value=0.200000 the reject alone
measure windows=2 the corner is part of the key
`pkpk hf` is the reading that says the filter reached *pass one* and not only pass two; a
value near 3 means it did not. `period noband` is deliberately the defect the band exists to
fix, and a future reader who "fixes" it has removed the check.
The band leg asks for **25 %** rather than the 10 % default, and that is arithmetic rather
than a workaround: the band is a share of the *window's* peak-to-peak, which with the
impulses in it is about 3, so 10 % is a half-band of 0.3 -- less than the 0.5 an impulse
moves the signal by. `MeasureOptions::noise_pct`'s own rationale says 10 % holds to a 0.3
interferer, and this mock's is 0.5, so the default could not clear it. The default band is
what the `period noband` and `period hf` legs are about.
verification.md now says the noise and measurement probes must not share a run: both push
onto `app.measures` and the later one would read the earlier one's rows.
|
||
|
|
84aebf4116 |
ui: a measurement's noise band and HF reject are settable and persisted
Two rows in the measure dialog, built the way the threshold and side rows are and appended
beside them: the band as a percentage of the window's own peak-to-peak (the unit the engine
takes), the corner in Hz with 0 off. They are seeded when editing, shown only where they do
something -- the band for the crossing types, since it moves the edge decision and nothing
else, the reject for all of them, since it sits before both walks -- and read into
`MeasureOptions` on apply.
They persist as two *named* keys, `noise` and `hf`, on the same `o <<` chain as the keys
beside them and read back through the same keyed reader, so a session written before this
change loads with 10.0 and 0.0: today's behaviour exactly, with no version check.
[configs] now proves the round trip rather than assuming it. The probe sets the first
measurement's band and corner to 7.5 and 250 before saving, so a key that never reached the
file reads back as the defaults:
[configs] round trip read=1 comment='written by the configs probe' ... measures=4 math=0 noise=7.5 hf=250
The rest of that line is unchanged, and the run is free of Gtk-CRITICAL.
|
||
|
|
fec0a1032e |
test: the noise probe reads the trigger's band and HF reject
Six legs over one retained buffer, each a full rising-edge search through `BufferSearch` at
level 0, run back to back without yielding so every leg sees the same samples: the bare
comparator on a spiked sine, a 0.8 band, a 50 Hz reject, both, and the reject's group delay
on `square` -- which carries no impulses, so a one-pole's crossing of zero after a step is
exactly `tau*ln2` and the delay is an exact prediction rather than a coincidence.
trigger band: bare=49/2 band=7/2 ratio=7.00
trigger hf: hf=7/2 band+hf=7/2 band=7
trigger delay: clean=4 filtered=4 delay=0.002206 want=0.002206 ratio=1.000
`ratio >= 5`, both HF legs within 2 of the band, `ratio` between 0.7 and 1.4, `state=2` on
every leg, no Gtk-CRITICAL. `bare` and `band` are proportional to the retained buffer, so
neither is a fixed number; the ratios and the agreement between legs are the assertions.
The probe enables the signals it measures itself rather than relying on another probe's side
effects -- `enable_all_probe` ends by switching one off again, which is how `sine` was missing
from the first run of this one -- and arms in two phases because that enable is a structural
change that replaces the store, so the ring has to fill before there is anything to search.
Its "no stream" line now names the channels that *are* there: an index alone says a probe
found nothing, not what it saw.
|
||
|
|
1583741e03 |
ui: the trigger panel offers a noise band and an HF reject
Two label-only rows in trigger-panel.ui (band_row, hf_row, after holdoff_row and before
mode_row) and the two spins the code appends into them: the band on the level spins' scale
in signal units, the HF reject at 0-1e7 Hz with 0 off. Both are published with the other
fields -- read before the model lock is taken, assigned inside it -- and both join the
value-changed loop, so editing either republishes the condition.
The rows are shown per type: the band for every type but Dropout, which is an absence a
band cannot describe, and the HF reject for all of them, since it sits before every
comparison. `sync_trigger_rows()` resolves a row as its spin's parent rather than keeping a
second handle to it: the row comes from the file and holds one child.
The exit dump counts the panel's controls, so it reads controls_inside=15/15 now -- and the
probe confirms it, along with both search readings and no Gtk-CRITICAL:
[udpscope] trigger panel: boxes=15 controls_inside=15/15 search_widgets=4/4 search_inside=4/4
|
||
|
|
d5a9b3c5d5 |
test: the mock can inject an alternating-sign impulse train on sine
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |