udpscope: an oscilloscope view for a UDPStreamer source
GTK4 and libadwaita. It receives one UDP stream through the vendored udpsclient, buffers it in a large ring, and draws it with the vendored plotter: a trigger with a pre/post event window, a pause that freezes the window, cursors, measurements, math channels in a node editor, an event cache, named configurations, preferences, and an export of the visible window to .npy/.csv. Waveform files can be opened and analysed in place. The interface is two threads -- a producer that owns the socket and the GTK main thread that owns every widget and the GL context -- sharing one model under one mutex, with samples crossing through lock-free rings. deps/plotter is a submodule pinned to the commit this tree builds against; deps/udpsclient is vendored in place. docs/claude/ documents the rules that cost a defect to learn, and SPECS.md is the requirement document.
This commit is contained in:
1 parent
8f54841c2c
commit
0e256764d5
41 files changed
+2081
-154
No files matched your search
@@ -128,7 +128,16 @@ because a tick a hair under a rung's top rounds to 1000 at three significant fig
|
||||
`%.3g` writes that `1e+03`, which is how a one-second tick read `1e+3ms`. All four carry
|
||||
upstream tests — `the_atlas_is_filtered_not_point_sampled`,
|
||||
`grid_hairlines_land_on_whole_pixels`, `the_tick_at_the_origin_reads_zero` and
|
||||
`format_time_never_lets_a_field_reach_its_radix`. The minor
|
||||
`format_time_never_lets_a_field_reach_its_radix`. The box-zoom selection was the one gesture with no
|
||||
feedback at all: the controller tracked the rect and nothing drew it. The drawing went upstream with
|
||||
the rest — `Plot::set_selection_rect()` (the plot carries a rect the host hands it and learns nothing
|
||||
about pointers), `Controller::selection_preview()` (the mask decides which axis is *not* zoomed, and
|
||||
the preview is widened along it, because the swept sliver of a horizontal Ctrl drag is the gesture and
|
||||
the band across the plot is what the release will apply) and `Renderer::draw_selection_frame()` — and
|
||||
it carries three tests: `a_ctrl_box_zoom_previews_the_band_it_will_zoom`,
|
||||
`the_selection_frame_is_carried_and_taken_off` and `the_box_zoom_frame_lands_exactly_where_it_says`,
|
||||
the last of which reads the headless framebuffer back and compares the same pixels with and without
|
||||
the frame. The minor
|
||||
grid's look is set there too, not here: `grid_minor` carries 50% alpha, `grid_minor_width` is
|
||||
0.75 (sub-pixel, expressed as coverage at 1x) and `grid_minor_dash_on`/`_off` are 3/3 — a
|
||||
fragment discard, no extra geometry — so the subdivisions read as texture under the labelled
|
||||
|
||||
@@ -218,6 +218,18 @@ come back with a different answer.
|
||||
Relative time's `slot_base_` also survives: it is samples-since-the-stream-began, the user's
|
||||
chosen origin, and must not jump backwards because the link blipped.
|
||||
|
||||
## There may be no store at all
|
||||
|
||||
`rebuild_locked()` calls `store_.reset()` for a stream with **no channels** — the model's state until the
|
||||
first CONFIG arrives — so `store_` is legitimately null, and `ScopeModel::live()` dereferences it. Every
|
||||
path reachable without a stream has to ask first: **pausing a scope that is not connected** is one (the
|
||||
button works, and freezing a view means taking a window from the rings), and it crashed inside
|
||||
`live_window_locked()` on a null store. The guards belong at the entry points the UI can reach
|
||||
(`live_window_locked()`, `publish_pause_view()`), not in `live()`, which cannot express "nothing there"
|
||||
through a reference. The paths that only run for a *bound* trace — the series' gather, the SlotSeries'
|
||||
`shared_channel()` — are safe by construction: a slot with no channel binds no series at all.
|
||||
`UDPSCOPE_PROBE_PAUSE_MS` against a port nothing is listening on is the check.
|
||||
|
||||
## A waveform is a source that never arrives
|
||||
|
||||
`--open FILE` (and the chooser's Waveforms page) puts an exported `.npy` into the model instead of a
|
||||
|
||||
@@ -210,3 +210,35 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
- **`find_accel()` masks the modifiers before comparing.** Lock, Num-Lock and the pointer buttons
|
||||
all arrive in `GdkModifierType`, so an unmasked compare matches no table row at all once
|
||||
Caps Lock is on.
|
||||
- **A UI file is data, and a mistake in it is a runtime abort, not a build failure.** `.ui` files live in
|
||||
`src/ui/` and are compiled into the binary by the third GResource (`udpscope-ui.gresource.xml`,
|
||||
`prefix=/org/udpscope/ui`), loaded with `gtk_builder_new_from_resource()`. `preprocess="xml-stripblanks"`
|
||||
catches a *malformed* file at build time -- and nothing else does, which is why it is on every file in
|
||||
every bundle. What it cannot catch is a file that is valid XML but wrong for GTK: `GtkBuilder` reports
|
||||
that with a **`Gtk-ERROR`, which aborts the process** by default, so a window built by hand from bad
|
||||
markup takes the application down rather than failing to appear. The `[help]` probe opening every
|
||||
window is what catches that. Two specifics already paid for: a `GIcon` property takes an **object**, not
|
||||
a name (`<property name="icon">foo</property>` is read as a reference to another object and fails with
|
||||
"Object with ID foo not found" -- the icon is a nested `GThemedIcon`), and a `.ui` big enough to exceed
|
||||
4095 characters needs `-Wno-overlength-strings` on its generated source, because glib-compile-resources
|
||||
writes each file as one C string literal and the warning fires on generated *data* (the icon and help
|
||||
bundles are small enough per file never to need it).
|
||||
- **Deprecated widgets may be the right widget, as long as no deprecated *function* is called.** The
|
||||
shortcut overview is a `GtkShortcutsWindow` (`gtk/deprecated/`, deprecated in 4.20) because libadwaita's
|
||||
successor, `AdwShortcutsDialog`, cannot draw a mouse gesture: `AdwShortcutsItem` has no icon property at
|
||||
all, so its shortcut cell can only be an accelerator, and its cell reads "No Shortcut" when there is
|
||||
none. `GtkShortcutsShortcut` has one generic gesture type whose `icon` *is* drawn. Loading the window
|
||||
from a `.ui` and presenting it as a `GtkWindow` names no deprecated function, so the build stays at its
|
||||
four warnings. When such a window closes, its builder is **replaced on the next opening**, not kept: the
|
||||
window belongs to the builder's object list and a destroyed `GtkWindow` cannot be presented again.
|
||||
- **A dialog reachable from two windows cannot own its state in either.** The preferences open from
|
||||
the main window's menu *and* from the source chooser, which exists before `App` does (`main()`
|
||||
chooses a source, then builds the model and the UI). The widgets therefore live in a `PrefsForm` the
|
||||
caller owns — `App::prefs`, `Chooser::prefs` — and what a change *means* is a callback, because it
|
||||
means different things on the two sides. Both are plain members of their owners, on the same stack
|
||||
frame, so nothing can outlive the frame that built it.
|
||||
- **A transient window is destroyed before its parent.** `chooser_finish()` destroys the preferences
|
||||
window before the chooser, and nulls the pointer *before* the destroy so `close-request` cannot
|
||||
re-enter a dead window. GTK does not promise to take a transient down with its parent, and a window
|
||||
whose handlers point into a returned stack frame is how this project already earned one
|
||||
use-after-free.
|
||||
@@ -75,3 +75,9 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
tells a real gesture from its own echo, so writing the group alone is reverted to the zoomed
|
||||
range on the next frame. `fit_all()` is the y fit plus this, shared rather than duplicated so
|
||||
the two cannot drift apart.
|
||||
- **Relative time is decided by the newest statement, not by the default.** `Config::relative_time_specified`
|
||||
is set by `--relative-time` and by the preferences — which the source chooser can reach before the run
|
||||
is planned, so that is the one place a preference still changes *this* run. Both the list's own copy
|
||||
(`apply_source()`) and a source's stored configuration (`apply_stored_settings()`) only fill in when
|
||||
nothing newer was said. Without the marker, clicking a source row after applying the preference
|
||||
silently reverted it — `[sources] picked ... relative=0`, with the dialog having just written `1`.
|
||||
@@ -65,6 +65,19 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
data the scope never had. The two are one branch in `tick()` for that reason. A paused view
|
||||
pins the same way (`pin_pause_window()` sets `pinned_` from the frozen range), so the frozen
|
||||
picture is equally immovable; resuming or leaving trigger mode clears it.
|
||||
- **An opened file is a pinned view too, and *how* it is pinned matters as much as that it is.** The
|
||||
file view states its pin from the ring's own stamps every frame, and it used to do that with
|
||||
`pin_event_window()` -- which writes the group's range as well as the pin's numbers -- so a box zoom
|
||||
drew its frame and changed nothing: the controller's `set_x_range` went onto the *member*, the next
|
||||
frame's re-statement put the group's range back, and the branch it fell into (`clamp_to_data()`,
|
||||
because a file is neither `event_held` nor `Paused` nor `capturing`) never adopts a member into a
|
||||
group that is not following. That is the wheel-in-single-trigger defect again, in the one mode nobody
|
||||
re-tested. Two things hold it now: the branch is `Paused || capturing || rows->window_pinned()`, so a
|
||||
pinned view is always clamped by `hold_pinned_window()`; and the file's per-frame statement uses
|
||||
`set_data_pin()` -- the pin's numbers -- unless `pinned_window_is()` says the pin is a *different*
|
||||
window, which is a re-open with a new extent and is a new event, stated in full. `[boxzoom-probe]`'s
|
||||
`zoomed=` is the guard, and it is a reading to take in **both** modes: live and file must both read
|
||||
`1`.
|
||||
- **A pinned view is navigable *within its data*, and that is the bound -- not the request.**
|
||||
Two things had to agree for this, and both were wrong in the same direction:
|
||||
the **pause** pinned on the window it asked for rather than on the snapshot it got (a cut
|
||||
|
||||
@@ -564,7 +564,15 @@ Two lines of the self-check output are assertions in disguise:
|
||||
so the panes the tick rewrites are measured directly. The `[width-probe]` lines are the
|
||||
per-change detail behind the `axes.w changes` count, and a step over 100 px dumps the widget
|
||||
tree's wide subtrees — that is what the documented, intermittent ~320 px step wants read.
|
||||
- `[help] pages=14 parsed=14 shortest='Choosing a source' 1563 B; shortcuts: app=12 plot=13; about version=… commit=… built=… settings=… icon=org.udpscope.app; closed ok` (stderr, `UDPSCOPE_PROBE_HELP_MS`) — **`parsed` must equal `pages`** and no page may be empty: a missing resource or markup Pango rejects renders a one-line stand-in instead of failing, so the window *looks* fine and only this count notices. `settings=` must be a real path (the same one the About dialog shows). **`icon=` must read `org.udpscope.app`** — it is read back through `adw_about_dialog_get_application_icon()` while the dialog is open, so the dialog is asserted to *carry* the icon rather than merely to open (a `set_application_icon()` lost in an edit leaves a dialog that opens, looks almost right, and shows a generic placeholder). `closed ok` with **no `Gtk-CRITICAL`** is as much a crash test of three windows built, presented and closed in one run as it is a state check, and a stronger one than it was: the three are now closed by **two different calls**, so a mismatched pair crashes or leaks here. One of the three is a `GtkWindow` (the help window, `gtk_window_close()`); the About dialog and the shortcut overview are `AdwDialog`s and close with `adw_dialog_close()`, cleared on `::closed`. `shortest=` is the page title — a page whose body was never compiled into the binary is absent rather than empty, and a title alone is the stand-in's shape.
|
||||
- `[help] pages=14 parsed=14 shortest='Pause and play' 1627 B; shortcuts: app=12 plot=14 tables=26 ui=26 ui_accel=19 ui_icon=7 ui_bare=0; about version=… commit=… built=… settings=… icon=org.udpscope.app; closed ok` (stderr, `UDPSCOPE_PROBE_HELP_MS`) — **`parsed` must equal `pages`** and no page may be empty: a missing resource or markup Pango rejects renders a one-line stand-in instead of failing, so the window *looks* fine and only this count notices. The `shortcuts:` numbers are the shortcut overview four ways.
|
||||
`app` and `plot` are the rows in the C tables -- what the application actually *handles* -- and `tables`
|
||||
is their sum. `ui`, `ui_accel`, `ui_icon` and `ui_bare` are what `src/ui/shortcuts.ui` *declares*, read
|
||||
out of the resource with a markup parse rather than off the built widgets, because a tree walk counts
|
||||
every row twice: `GtkShortcutsWindow` reaches each one from more than one place in its own tree.
|
||||
**`ui` must equal `tables`** -- two lists of one thing, and a row in one but not the other is a gesture
|
||||
that works and is not documented, or is documented and does nothing -- and **`ui_bare` must be zero**: a
|
||||
row with no accelerator and no icon is a cell reading "No Shortcut". `ui_accel + ui_icon == ui`, so those
|
||||
two also catch a row whose accelerator went missing. `settings=` must be a real path (the same one the About dialog shows). **`icon=` must read `org.udpscope.app`** — it is read back through `adw_about_dialog_get_application_icon()` while the dialog is open, so the dialog is asserted to *carry* the icon rather than merely to open (a `set_application_icon()` lost in an edit leaves a dialog that opens, looks almost right, and shows a generic placeholder). `closed ok` with **no `Gtk-CRITICAL`** is as much a crash test of three windows built, presented and closed in one run as it is a state check, and a stronger one than it was: the three are now closed by **two different calls**, so a mismatched pair crashes or leaks here. One of the three is a `GtkWindow` (the help window, `gtk_window_close()`); the About dialog and the shortcut overview are `AdwDialog`s and close with `adw_dialog_close()`, cleared on `::closed`. `shortest=` is the page title — a page whose body was never compiled into the binary is absent rather than empty, and a title alone is the stand-in's shape.
|
||||
- `[menu] N 'label' accel='key' h=P label_x=X accel_right=R/W order=D ok=O`, one line per row,
|
||||
then `[menu] rows=R expected=E order=D rows_bad=B popover_visible=V mapped=M button_mapped=B`
|
||||
and `[menu] status pos=top bubble=WxH button_top=T button_right=R fits=F` (stderr,
|
||||
@@ -892,6 +900,33 @@ popover row reads `failed: <strerror>` — one stderr line, no retry storm again
|
||||
| `known:` after a rename | one row, under the new name | a rename that leaves the old file, so the chooser lists the same session twice |
|
||||
| `known:` for a settings-only configuration | a row, though `signals=0` | the listing filter treating "no signals yet" as "nobody configured this", which hides every host the form set up |
|
||||
|
||||
## `[sources] prefs`
|
||||
|
||||
```
|
||||
[sources] prefs: opened=1 apply=1
|
||||
[sources] prefs: dir=/tmp/udpscope-prefs-events keep=7 rel=1 theme=dark cfg_relative=1
|
||||
```
|
||||
|
||||
`UDPSCOPE_PROBE_PICK_MS` + `UDPSCOPE_PROBE_CHOOSER_PREFS=1` opens the preferences from the *chooser*
|
||||
rather than from the main window's menu, fills the form through the same `prefs_probe_fill_and_apply()`
|
||||
the app's probe uses, and reports both the file and `cfg_relative` -- what the run about to start will
|
||||
actually use.
|
||||
|
||||
- The second line must echo the probe's input, exactly as `[prefs-probe]` does.
|
||||
- `cfg_relative=1` is the chooser's half of the contract: relative time is the one preference that can
|
||||
still change *this* run, because `main()` plans the buffers after this dialog returns.
|
||||
- The run leaves the preferences window **open** on purpose: the pick that follows destroys the chooser
|
||||
with the window still up. A `Gtk-CRITICAL` or a crash here is the transient-window lifetime being
|
||||
wrong, not a cosmetic failure.
|
||||
- A `[sources] prefs: could not write <path>` line, with the second line reading the *defaults*
|
||||
(`dir= keep=32 rel=0 theme=auto`), is the signature of the config directory not having been created:
|
||||
the chooser is the first thing that writes preferences on a fresh install, so it is where a
|
||||
`mkdir()` that does not make its parents shows up. This reading is why that is now caught here rather
|
||||
than reported by a user whose settings quietly did not stick.
|
||||
- The line after it, `[sources] picked ... relative=1`, is the pair that matters: the preference stated
|
||||
in the dialog has to survive the row click that follows, which overwrites the rest of the config from
|
||||
the source's stored session.
|
||||
|
||||
## `[sources] settings from configuration`
|
||||
|
||||
```
|
||||
@@ -940,12 +975,12 @@ configuration that carries settings, is the defect this exists for — a saved h
|
||||
[events] live, recording off: button_visible=0 pane=0 pane_w=0 tick=1 recording=0
|
||||
[events] recording on, no trigger: button_visible=1 pane=0 pane_w=0 tick=1 recording=1
|
||||
[events] after trigger on: button_visible=1 pane=0 pane_w=0 tick=1 recording=1
|
||||
[events] dir=/tmp/k5/cache/udpscope/127.0.0.1-21023/events n=1 newest: dur=2.000000 pre=1.000000 post=1.000000 edge=1.000100 signals=4 samples=20001 rate=10000 seq=1 thumb_bytes=230454 recorded=1 pane=1
|
||||
[events] dir=/tmp/k5/cache/udpscope/events/127.0.0.1-21023 n=1 newest: dur=2.000000 pre=1.000000 post=1.000000 edge=1.000100 signals=4 samples=20001 rate=10000 seq=1 thumb_bytes=230454 recorded=1 pane=1
|
||||
[events] activated row0: replaying=1 pane=1 pane_w=240 tick=0 window=2.000 thumbnail=1 thumb_size=126x42 thumb_visible=1
|
||||
[events] replayed: channels=4 samples=20001 replaying=1 waveform=1
|
||||
[events] live again: replaying=0 waveform=0
|
||||
[events] recording off, trigger off: button_visible=0 pane=0 pane_w=0 tick=1 recording=0
|
||||
[events] recording off, trigger on: button_visible=0 pane=0 pane_w=0 tick=1 recording=0
|
||||
[events] recording off, trigger off: button_visible=1 pane=1 pane_w=240 tick=1 recording=0
|
||||
[events] recording off, trigger on: button_visible=1 pane=1 pane_w=240 tick=1 recording=0
|
||||
[events] compare buttons: cached=2 selected=2 compare=1 accumulate=1
|
||||
[events] compare: kind=1 rows=1 channels=8 samples0=32964 traces0=4 traces1=-1 window=2.000 recording=1 pane=1
|
||||
[events] compare plots: area_b=1 rows_a=1 rows_b=1 list=4 x_a=[-2.000,0.000] x_b=[-2.000,0.000] same_x=1
|
||||
@@ -967,7 +1002,7 @@ the font and the display's scale factor; what it must never be is `0x0`.
|
||||
| `pane` | 1 once the history button has been clicked | the button's handler not revealing the pane |
|
||||
| `live` | `button_visible=0 pane=0** | a pane or a button on screen with no trigger mode: a column of nothing beside a live plot |
|
||||
| `after trigger on` | `button_visible=1 pane=0` | the pane revealed by trigger mode alone (it must be on demand from the toolbar button), or the button missing in trigger mode |
|
||||
| `recording off, trigger off` and `recording off, trigger on` | `button_visible=0 pane=0 recording=0` in both | the pane surviving its own switch being turned off -- the empty column the item exists to prevent -- or the button coming back with trigger mode while the cache is disabled |
|
||||
| `recording off, trigger off` and `recording off, trigger on` | `button_visible=1 pane=1 recording=0` in both **once the cache holds anything** (the probe's own run does by this point) | the button going away with the switch when events are already written -- an event on disk outlives the switch that wrote it -- or `pane_w` not being 240 with the pane open. With an *empty* cache the same two lines read `button_visible=0 pane=0`, which is the empty column the item exists to prevent |
|
||||
| `thumbnail`, `thumb_size`, `thumb_visible` | 1, a non-zero size, and 1 | the picture loaded through `gtk_picture_set_filename()`, which fails silently (the paint-able is there, the allocation is there, nothing is drawn), or a `GtkPicture` given a size request and no cap -- its natural size is its content, so the row's text is squeezed off. `thumb_size` is the reading that catches both |
|
||||
| `replaying` (last line) | 0 | a replay with no way back: *Live* must start a fresh client and thread |
|
||||
| `pane_w` | the divider's position: 0 while the pane is closed, its remembered width (240 by default) while it is open | the pane reserving a column while closed (a paned hands its start child its minimum unless the position says otherwise), or a width that is not remembered |
|
||||
@@ -979,3 +1014,67 @@ the font and the display's scale factor; what it must never be is `0x0`.
|
||||
| `compare sync` | `mirrored=1`, with `y_a` and `y_b` equal to the range the probe set on the first plot | the mirror not running, or running the wrong way: a y (or x span) change in one plot that the other never sees |
|
||||
| `accumulate` | `kind=2 rows=1 channels=8 samples0=33045 traces0=8 window=2.000` | the second event's layers not landing: `traces0` stays at the four signals the layout holds when the slots were not free, or the row named after a previous group's suffix so nothing binds |
|
||||
| `group left` | `replaying=0 kind=0 waveform=0` | the group surviving *Live*: a file view with no file behind it |
|
||||
|
||||
## `[boxzoom-probe]`
|
||||
|
||||
```
|
||||
[boxzoom-probe] open: swept=200x10 frame=100,0 200x853 axes_h=853 band=1 axis_before=[0.000000..2.000000]
|
||||
[boxzoom-probe] closed: frame=0x0 gone=1 axis_after=[0.000000..0.553167] zoomed=1
|
||||
```
|
||||
|
||||
`UDPSCOPE_PROBE_BOXZOOM_MS` injects a Ctrl box zoom straight into row 0's controller -- the way a
|
||||
headless probe has to, since the area's gestures arrive from GTK -- and reads the frame back off the
|
||||
*plot* one frame later. That is what makes this the adapter's per-frame sync being exercised: the
|
||||
library measures and draws the rect in tests of its own.
|
||||
|
||||
- `band=1` is the reading that matters. The pointer swept `200x10`, and the frame is `200` wide and
|
||||
`1665` -- the height of the axes -- tall. Ctrl zooms **x only**, so the band across the plot is what
|
||||
the release will apply, while the sliver the pointer actually swept is only the gesture. Its `x` and
|
||||
`y` must be the press point's.
|
||||
- `closed: frame=0x0 gone=1` is the other half of the contract: a finished gesture leaves no frame, so
|
||||
nothing goes on claiming a selection is open. Both lines together are the whole feature.
|
||||
- `axis_before`/`axis_after`/`zoomed` are the *settled* axis, read a few ticks after the release so
|
||||
that whatever pin or clamp the view has has already had its say -- which is the point: a rect can
|
||||
report the gesture and nothing about whether it did anything. **Run this in both modes and both must
|
||||
read `zoomed=1`.** A file opened with `--open` reading `zoomed=0` while the live view reads `1` is the
|
||||
defect this reading exists for, and it was invisible from the frame alone: the file view pinning its
|
||||
axis from the ring each frame is what swallowed the gesture.
|
||||
|
||||
## `[prefs-probe]`
|
||||
|
||||
```
|
||||
[prefs-probe] opened=1 apply=1 dir=/tmp/k5/cache/udpscope/events store=/tmp/k5/cache/udpscope/events/127.0.0.1-8000
|
||||
[prefs-probe] dir=/tmp/udpscope-prefs-events keep=7 rel=1 theme=dark live=1
|
||||
[prefs-probe] default restored: file=''
|
||||
[prefs-probe] browse: toplevels=3 extra=1 icon_ok=1 field='/tmp/k5/cache/udpscope/events'
|
||||
```
|
||||
|
||||
`UDPSCOPE_PROBE_PREFS_MS` opens the preferences dialog, fills its form, clicks **Apply** and reads
|
||||
the settings file *back from disk* -- not from `App::settings`, because a dialog that edits its own
|
||||
copy and never writes is exactly the failure this has to catch.
|
||||
|
||||
- `opened=1` is the window having come up; `apply=0` would mean its *Apply* button was never stored,
|
||||
which would make the click below test nothing.
|
||||
- `dir` is the directory the dialog *offers* -- the preference if there is one, otherwise the default
|
||||
written out in full (it is deliberately not a description: "the standard location" is not something a
|
||||
user can act on) -- and `store` is the directory the cache is really using. **`store` must be `dir`
|
||||
plus the source's folder name**, which is the reading that says the field shows the path in force
|
||||
rather than a plausible string. `dir` here is the default because the file had no preference yet.
|
||||
- The second line must echo the probe's own input -- directory `/tmp/udpscope-prefs-events`, keep
|
||||
`7`, relative time `1`, theme `dark`. Any field that differs means the form, the write
|
||||
or the parse dropped it.
|
||||
- `live=1` is the theme having reached the running style manager rather than only the file.
|
||||
- `browse: toplevels=N extra=1 field='…'` is the folder chooser: opened from its button, then cancelled
|
||||
from *outside* it. `extra=1` is the new toplevel that is the dialog -- a count, because the dialog is
|
||||
not named after anything a probe could match on -- and the field must still read the directory it read
|
||||
before it opened, because **a cancelled picker changes nothing**. This is the async-dialog crash
|
||||
surface (a callback landing after its dialog went away), which is why the preferences window is closed
|
||||
a beat later than the cancel and why the callback tolerates every widget being null.
|
||||
- `icon_ok=1` is the icon theme knowing `folder-open-symbolic`, the same question the toolbar-icons
|
||||
line asks of the bundled six. It is a reading rather than a convention because the button is an icon
|
||||
and *nothing else*: a name the theme does not carry draws a missing-image placeholder, and nothing
|
||||
else in a run would say so.
|
||||
- `default restored: file=''` closes the loop on the same idea from the other side: the probe types the
|
||||
default path back into the field and presses *Apply*, and the file must end up with **no**
|
||||
`event_cache_dir` at all. A path in the file is a user's choice, and typing the default is not one --
|
||||
pinning it there would stop the setting following the cache directory it is named after.
|
||||
@@ -301,9 +301,11 @@ as distinct from the fault injection below, which exercises ingest:
|
||||
`[events] activated row0: replaying=1 thumbnail=1 thumb_size=108x36 thumb_visible=1` (the size is
|
||||
what says the picture is *on screen*: a paint-able alone is not), `[events] replayed: channels=4
|
||||
samples=20001 replaying=1 waveform=1` and `[events] live again: replaying=0 waveform=0`. It ends on
|
||||
the pane's other precondition, through the trigger panel's switch: `[events] recording off, trigger
|
||||
off` and `[events] recording off, trigger on` must **both** read `button_visible=0 pane=0
|
||||
recording=0` -- the pane goes with its switch, in either trigger state. **`samples` must equal the
|
||||
the pane's availability, through the trigger panel's switch: `[events] recording off, trigger off` and
|
||||
`[events] recording off, trigger on` -- once the cache holds events (the probe's own run has written
|
||||
some by this point) both read `button_visible=1 pane=1 recording=0`, because a button that shows the
|
||||
cache must not go away with the switch that wrote it; against an *empty* cache the same two lines read
|
||||
`button_visible=0 pane=0`, which is the empty column the item exists to prevent. **`samples` must equal the
|
||||
export's row count for the same window** (`20001` for a 2 s event at 10 kS/s), `thumb_bytes` must be
|
||||
a real BMP (`54 + 480*160*3`-ish, i.e. hundreds of kB, not 0), `thumbnail` must be 1 (the row loaded
|
||||
the picture) and the last line must read `replaying=0` -- a replay with no way back is a one-way
|
||||
|
||||
Reference in new issue
Block a user