diff --git a/CLAUDE.md b/CLAUDE.md index fc90e9f..abe516a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,6 +17,9 @@ Options: `--server`, `--port`, `--multicast`, `--interface`, `--buffer-mb` (per buffer, default 200), `--max-window`, `--holdoff`, `--recv-buffer`, `--plots` (the row count to *start* with, 1..`RowSet::kMaxRows`; the layout editor owns it afterwards). +Run with no `--server` and udpscope asks which source to open: the sessions it has stored, plus a +form for a new host. Every probe below passes `--server`, so none of them sees it. + `deps/plotter` is built from source with only the GTK4 adapter enabled (`PLOT_BUILD_GTK4`); its tests and examples are forced off. `deps/udpsclient` is a vendored C99 source pair compiled into a static lib. @@ -445,6 +448,22 @@ Two lines of the self-check output are assertions in disguise: substitutes a missing-image placeholder, the button still works, and every other line of the dump still passes. Changing an icon name is a one-line change in `build_toolbar()` and this is the only check that it names something that exists. +- `[sources] N known: HOST:PORT plots=P signals=S [multicast=G] [interface=I] | ...` (stderr, + `UDPSCOPE_PROBE_SOURCES=1`) — the startup chooser's discovery, printed and exited **before + `gtk_init()`**, so this one needs no display and no `xvfb-run`. **Only sources with a named + signal may appear**: a development machine accumulates one session file per probe run on a + throwaway port (140 of them when this was written), and the filter in `known_sources()` is + what keeps the list the sources a user configured. Order is newest first by file mtime. +- `[sources] picked HOST:PORT multicast=G interface=I` (stderr, `UDPSCOPE_PROBE_PICK_MS`) — + shows the chooser, then activates row 0 the way a click does and continues into the real + application, so a full run with **no `--server`** must reach the scope and emit every other + dump line unchanged. `multicast=`/`interface=` coming back non-empty from a stored row is the + assertion that a multicast session reconnects to its group, and it needs **no multicast + networking**: write the session file by hand and read the line. With + `UDPSCOPE_PROBE_NEWHOST=host:port` it drives the form instead — `[sources] form open, + connect_sensitive=1` then a `picked` line, with no SIGSEGV, which is as much a crash test of a + dialog built and destroyed as it is a state check. An empty list must print `picked nothing` + rather than dereferencing a null row. - **The session layout round trip** (`~/.config/udpscope/_.toml`) — run once with a drop and a window change, then again with no interaction: the second run's dump must read `layout loaded from ...` and `layout: rows=R window_s=W row0=[...]` with the @@ -1234,4 +1253,24 @@ far the largest file. `gtk_icon_theme_has_icon()` is checked for every toolbar button and reported by the dump (see `toolbar icons:` under Verification): a resource path that was never added does not raise an error, it draws `image-missing`. +- **The chooser runs before `ScopeModel` exists, and `--server` is what decides.** `Config` carries + `server_specified`, set only by the `--server` branch, and `main()` asks only when it is false — + so declining costs no buffer and no socket, and `ScopeModel`, `Acquisition`, `RowSet` and + `run_ui()` need no notion of "no source yet". It is `--server` alone and not "no arguments": + `--multicast` still needs a server to fetch CONFIG from. **Every documented probe passes + `--server`**, which is what makes the feature invisible to the regression net — a probe added + without it would reach the chooser and hang. `gtk_init()` plus a nested `GMainLoop`, and then + `run_ui()`'s `GtkApplication` afterwards in the same process, is a verified combination + (`gtk_init()` is idempotent; running your own loop is the documented GTK4 replacement for the + removed `gtk_dialog_run()`). +- **A session file's name cannot say how its source was reached, so the file says it.** + `_.toml` carries neither the multicast group nor the interface, so a chooser row + could not reconnect to a multicast source at all. `LayoutState::multicast`/`interface_addr` are + written in the **top block, before the first `[[table]]`** — a bare key after a table header is + that table's key in TOML, which is exactly how `enabled` was parsed into the last + `[[calibration]]` record and lost without a word. A file from an older build has neither key and + loads as unicast on the default route. The chooser's list filters to sessions that **name at + least one signal**, because a probe run leaves a file per throwaway port and all of them hold + `signals = ["", "", "", ""]`; `known_sources()` splits the filename at the **last** `_`, since + `sanitize()` turns anything unsafe in a host into one. - Every source file carries an SPDX MIT header; match it in new files. diff --git a/README.md b/README.md index c8f0748..21a056d 100644 --- a/README.md +++ b/README.md @@ -458,6 +458,30 @@ a math channel's name and that name only exists as a signal once its graph is in reopened on the same source comes back as the one that was closed — including a signal that was enabled only to feed a math channel. +**Launched with no `--server`, udpscope asks which source to open.** It used to connect to +`127.0.0.1:8000` because that is the default in `Config` — fine for one streamer, useless for +several, and there was no way to see which sources the app already knew about. The chooser lists +one row per stored session: the server and port it was named after, how many plots and signals it +held, and its multicast group when it had one, newest first. `New host…` is a four-field form — +server, port, multicast group, interface — and nothing more: `--buffer-mb` and the rest are tuning +rather than "which source", and a form that asks for them is a form you have to read before using +the app. Cancel exits before anything is allocated, because declining to choose a source is not a +failure. + +It runs **before the model exists**, which is the whole reason it is cheap: no buffer, no socket +and no window until a source is known, and `ScopeModel`, `Acquisition` and `run_ui()` are +untouched by the feature. Every headless probe passes `--server`, so none of them reaches it. + +Two things the list had to get right. A session file is named `_.toml` and that name +cannot say whether the source was multicast or which interface it joined on, so the file now +records both — in the top block, before the first table, because a bare key written after a +`[[table]]` header belongs to that table in TOML, which is how the `enabled` key was silently lost +once already. And the list is **filtered to sessions that name a signal**: a development machine +accumulates one file per probe run on a throwaway port — 140 of them here, every one with +`signals = ["", "", "", ""]` — so an unfiltered chooser opens as a wall of ports nobody chose. A +row's `×` forgets a source by deleting its session file, with no confirmation: the gesture is the +confirmation, and what it destroys is one layout the app rewrites on the next clean exit. + **Math channels.** A math channel is derived from the others by a graph of operations and is then an *ordinary channel*: the model synthesises a signal entry for it, so an enabled one is acquired, plotted, measured, exported and listed in the signal panel exactly like a streamed signal — and