docs: the three new features are described where each of their readers looks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
aefac2d796
commit
ffc3ec1b4d
5 files changed
+231
No files matched your search
@@ -108,3 +108,21 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
`enters`, same `count[]`, identical `first_enter`/`last_enter` to 0.0 -- which is the assertion
|
||||
that the fast path is a fast path and not an approximation. The app's reading is
|
||||
`[measures-probe]` against `--noise 0.1`.
|
||||
- **A statistic's observation is one *recompute*, not one sample.** The pane already refreshes on
|
||||
a cadence (at most one pass per 100 ms while the window scrolls, exactly once when it
|
||||
settles), and the accumulator is fed from that pass's result — so `n` counts windows measured.
|
||||
Feeding it per sample would make `n` a sample count and the mean a mean of *samples*, which is
|
||||
not what any of these measurements measure: a frequency over a window is not a property of a
|
||||
sample. The reading that names this defect is `[stats-probe]`'s `n` in the thousands rather
|
||||
than the tens.
|
||||
- **The accumulator is Welford's, and the reason is the same one the time ring is `f64` for.** A
|
||||
measurement's values are routinely large with a small spread — an absolute stream time is the
|
||||
obvious case — and `Σx² - (Σx)²/n` cancels to noise there. The standalone check is that the
|
||||
spread of `{2,4,4,4,5,5,7,9}` survives a `+1e9` offset unchanged.
|
||||
- **What resets the statistics is "what is being measured changed", not "what is in view
|
||||
changed".** An explicit Reset, an add/edit/delete (all three funnel through
|
||||
`rebuild_measures_pane()`), a CONFIG, and a change of display window each change the
|
||||
*question*, so the history is cleared. A pause, a resume, a trigger latch, a pan and a zoom do
|
||||
not: they change which samples answer it, which is precisely what averaging over many windows
|
||||
is for. The pane-wide toggle does not reset either — it only shows and hides labels that are
|
||||
always created and always fed, so reading the plot for a moment does not cost the history.
|
||||
@@ -122,3 +122,21 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
export.** Both need the same absolute index range over the time ring and every value
|
||||
ring, or an export and a capture of the same window would hold different samples. Call
|
||||
it under `mutex()`.
|
||||
- **`EventDetector` owns every condition shape and holds no arming state; `TriggerEngine` owns
|
||||
the arming, the hold-off and the post window and holds no condition.** The split is what lets
|
||||
the buffer search ask the trigger's exact question without owning a trigger, and it is only
|
||||
true as long as neither half leaks into the other: a detector that knew about the hold-off
|
||||
would answer a search differently from the live trigger, which is the one thing the
|
||||
`agree=1` cross-check in `[search-probe]` exists to catch.
|
||||
- **The detector is fed on *every* sample, including during the hold-off and during a capture.**
|
||||
A pulse width, a window excursion and a dropout timeout are all *stateful over several
|
||||
samples*: skipping the feed while the engine is busy loses the opening edge, and the condition
|
||||
then measures from whatever sample the engine happened to resume on. The hold-off is applied
|
||||
to the detector's *answer*, not to its input — `process()` feeds first and tests
|
||||
`now_s < holdoff_until_s_` afterwards.
|
||||
- **An event's time is the detector's, not the sample's.** A pulse-width event is detected at
|
||||
the closing edge but could in principle be timed at the opening one, and a dropout is detected
|
||||
one sample after the timeout expires. `feed()` therefore returns the event's *own* time and
|
||||
`process()` uses that for `last_trigger_s_`, `pending_start_s_` **and** the pre-window gate —
|
||||
all three, or the marker, the cut and the gate would disagree with each other by the width of
|
||||
a pulse.
|
||||
@@ -588,3 +588,114 @@ Two lines of the self-check output are assertions in disguise:
|
||||
- `[color] '<name>' at row R slot S: trace=#RRGGBB chip_agrees=1 (want 1)`, then `[color] override: trace=#123456 chip_agrees=1`, `[color] session: entries=1 rgb=#123456` and `[color] default: trace=#RRGGBB chip_agrees=1` (stderr, `UDPSCOPE_PROBE_COLOR_MS`) — **all three `chip_agrees` must be 1**, the override line's `trace` must be `#123456`, the session line must read `entries=1 rgb=#123456`, and the default line's `trace` must be back to the first line's. Run it with `UDPSCOPE_ENABLE_ALL_MS` ahead of it, or there is no bound trace and the probe prints `no bound trace to recolour` instead of asserting anything; on the standard mock the first line reads `'sine' at row 0 slot 0: trace=#4FA3FF`. Two things make this an assertion rather than a tautology. The colours are read out of **`plot::Plot::trace().color`** — what the renderer will stroke, reached through `Row::plot` *after* `relayout_areas()`, since a new colour is a new figure — and out of the chip's own `<span foreground="…">` markup, not by asking `RowSet::signal_color()` twice, which would pass against a chip that never reached the trace. And the override is `#123456`, deliberately in no palette slot: an override equal to the entry it replaces passes against an implementation that ignores overrides entirely. The session leg round-trips `collect_layout()` → `save_layout()` → `load_layout()` on a temp path and deletes it, because a probe must not leave a colour in the real session file for a later run to inherit.
|
||||
- `[zoomkeys] ctrl+plus x=[..]->[..] y_held=1 | alt+plus y=[..]->[..] x_held=1` (stderr, `UDPSCOPE_PROBE_ZOOMKEYS_MS`) — **both `_held` must be 1 and both zooms must still happen.** Run it inside a pinned state, where a frame cannot re-frame the other axis for its own reasons: `UDPSCOPE_PROBE_PAUSE_MS=1500 UDPSCOPE_PROBE_ZOOMKEYS_MS=2200`. A `_held=0` is the modifier not reaching the plotter's controller; an unchanged range on the *zoomed* axis is the opposite failure, the key doing nothing. Ctrl restricts to x and Alt to y; Ctrl+Alt zooms neither (no special case needed — neither flag alone fires).
|
||||
- `status bar: glance=[● live · 1.09 MS/s · 2 s window] max_glance_chars=53 detail_fields=11 popover_rebuilds=0` (exit dump) — **`max_glance_chars` must stay small** (under about 60) and **`popover_rebuilds` must be 0**. The status row is directly above the plot, so this is the "layout must not depend on content" rule read from the bar's own side: the state field is one word from a fixed set (`connecting`, `live`, `paused`, `armed`, `capturing`, `event held`, `disarmed` — a word added to it must be **no wider than the widest already there**, which is what let `connecting` join at ten characters beside `event held`; the dead-port glance reads `max_glance_chars=51` against a live run's 54), the rate field is `%.3g` (so `10 kS/s`, never `10.0 kS/s`), and the two fields a *state* adds are both elapsed times — trigger mode's `last event … ago` and a pause's `for <elapsed>` — which step units rather than growing digits (`format_elapsed()`). A **paused** glance is not in the dump at all: it is read by `[pause-probe]`'s `glance=` field, since the dump's own line is taken live. `detail_fields` is the popover's label count, a literal, so adding a label without thinking about layout shows up here. The glance rate format matters: `%.3g` drops the trailing zero that a `%.4g` or a fixed-precision format would keep — a rate that reads differently in the dump and in the help page is a format disagreement, and `14-status.txt`'s example must match it.
|
||||
|
||||
## `[condition-probe]`
|
||||
|
||||
`UDPSCOPE_PROBE_CONDITION_MS`, with the mock's `--glitch-at`/`--glitch-width` (legs 0 and 1) or
|
||||
`--quiet-at` (leg 3).
|
||||
|
||||
Four legs, selected by `UDPSCOPE_PROBE_CONDITION_SHAPE`. Every leg prints one line of the form:
|
||||
|
||||
```
|
||||
[condition-probe] type=T cmp=C width=W t0=T0 measured=M at0=A earlier=E level2=L2 mode=D
|
||||
```
|
||||
|
||||
Measured readings (`--rate 10000 --freq 5`, each on a fresh port):
|
||||
|
||||
```
|
||||
shape=0 type=1 cmp=0 width=0.010000 t0=1.842100 measured=0.002000 at0=nan earlier=nan
|
||||
shape=1 type=1 cmp=1 width=0.010000 t0=2.160100 measured=0.200000 at0=nan earlier=nan
|
||||
shape=2 type=2 cmp=0 width=0.010000 t0=1.706700 measured=nan at0=-0.500907 earlier=-0.498185
|
||||
shape=3 type=3 cmp=0 width=0.010000 t0=4.120200 measured=0.300100 at0=nan earlier=nan
|
||||
```
|
||||
|
||||
`measured` is the reading, not a phase. It is the pulse width read back from the held snapshot's
|
||||
own stamps, which are edge-relative: the snapshot is the ground truth, not the mock's absolute
|
||||
clock. Three things are worth knowing before calling a reading wrong:
|
||||
|
||||
- **Leg 0**: `measured=0.002` is the injected glitch width. A `measured` near the signal's own
|
||||
half-period means the width qualifier did not qualify — the trigger caught a normal edge instead
|
||||
of the runt.
|
||||
- **Leg 1**: `measured=0.200` is an ordinary square half-period; every square pulse satisfies
|
||||
`> 0.010`.
|
||||
- **Leg 2**: `at0` must lie **outside** `[-0.5, +0.5]` and `earlier` **inside** it — those are the
|
||||
two sample values around the band crossing. The probe prints `nan` for any field a leg cannot
|
||||
measure. **This leg uses `sine`**, not `square`: the square is exactly ±1 and has no sample
|
||||
inside the `[-0.5, +0.5]` band, so a window condition on the square can never fire. **Use a
|
||||
1.0 s window with 50 % pre** or the probe cannot reach the opening edge of a 0.2 s pulse.
|
||||
- **Leg 3**: `measured=0.300` is the dropout timeout. **This leg also uses `sine`**. A timeout of
|
||||
0.050 s against a sine crossing zero every 0.1 s latches on a healthy signal, so use 0.300 s
|
||||
or longer; **use a 1.0 s window with 50 % pre** as for leg 2.
|
||||
|
||||
The channel-selection line `[condition-probe] <name>=<index> of <N> channels` is printed before
|
||||
the trigger fires; if the channel is not found this reads `-1` and the probe fails immediately.
|
||||
A failure line `[condition-probe] no latch after N polls` means the condition never fired — a
|
||||
wrong shape, a missing mock flag, or the wrong signal selected.
|
||||
|
||||
## `[stats-probe]`
|
||||
|
||||
`UDPSCOPE_PROBE_STATS_MS`. Needs `UDPSCOPE_ENABLE_ALL_MS` ahead of it and
|
||||
`UDPSCOPE_PROBE_MEASURE_MS` to add measurements. Run against the fast mock
|
||||
(`--rate 1300000 --batch 2000 --freq 1`) for readable figures.
|
||||
|
||||
Three phases, one line each. Every line carries `phase=`, `on=` (the toggle), `passes=` (the
|
||||
pane's recompute count since start) and one `| n=… mean=… sd=… lo=… hi=…` group per
|
||||
measurement. Phase 1 adds `dn=` per group.
|
||||
|
||||
Measured readings (fast mock, 1 Hz sine, with
|
||||
`UDPSCOPE_ENABLE_ALL_MS=800 UDPSCOPE_PROBE_MEASURE_MS=1500 UDPSCOPE_PROBE_STATS_MS=2500`):
|
||||
|
||||
```
|
||||
phase=0 on=1 passes=36 | n=30 mean=-0.0370313 sd=0.0953509 lo=-0.246367 hi=0.105167 | n=30 mean=0.633511 … | n=30 mean=1.99567 … hi=2 | n=30 mean=0.997834 … hi=1
|
||||
phase=1 … dn=0 (on all four measurements)
|
||||
phase=2 … n=1 mean=-4.84467e-14 | n=1 mean=0.63662 | n=1 mean=2 | n=1 mean=1
|
||||
```
|
||||
|
||||
- `phase=0` — the running count. **`n` must be in the low tens**, not thousands: a count in the
|
||||
thousands means the accumulator is fed per sample, not per recompute. `mean=0.633511` (≈ 2/π)
|
||||
for the `top` measurement on a unit sine is the check that the threshold types are correct, not
|
||||
merely plausible. `sd` must be 0 exactly when `n < 2`, and `lo`/`hi` must bracket `mean`.
|
||||
- `phase=1` — after a two-second pause. **`dn` must be 0 or 1**: a pinned window recomputes once
|
||||
when it settles and then never again. A `dn` near 120 is the second defect — accumulating per
|
||||
frame, which adds a hundred-odd observations of the identical window and collapses `sd` towards
|
||||
zero. Note that `n` is **not** reset by the pause or the resume; `passes` continues growing.
|
||||
- `phase=2` — after Reset. **`n` must be 0 or 1** (0 if the report ran before the next recompute,
|
||||
1 if after); anything larger means Reset cleared nothing.
|
||||
|
||||
## `[search-probe]`
|
||||
|
||||
`UDPSCOPE_PROBE_SEARCH_MS`, scheduled after `UDPSCOPE_PROBE_CONDITION_MS`. Run with
|
||||
`--rate 2000 --batch 64 --freq 5 --glitch-at 0.25 --glitch-width 0.004` and
|
||||
`UDPSCOPE_EXIT_AFTER_MS=12000`.
|
||||
|
||||
Measured output (port 8419):
|
||||
|
||||
```
|
||||
[condition-probe] type=1 cmp=0 width=0.010000 t0=1.710000 measured=0.004000 at0=nan earlier=nan level2=0.500000 mode=2
|
||||
[search-probe] started on channel 0, armed t0=5.710000
|
||||
[search-probe] state=2 hits=12 scanned=4.5115 total=4.5115 chunks=2 worst=0.106ms total=0.116ms armed t0=5.710000 vs scanned first=5.710000 err=0 agree=1
|
||||
[search-probe] jumped to hit 0 t=1.710000 mode=4 axis=[-0.1000..0.1000] pos=1
|
||||
[search-probe] marks placed=1 of 1 in view (12 found) locked=1 mode=4 axis=[-0.1000..0.1000]
|
||||
[udpscope] search marks: placed=1 hits=12 locked=1
|
||||
```
|
||||
|
||||
- **`agree=1` is the load-bearing line of the whole feature.** The search and the live trigger run
|
||||
the same `EventDetector` over the same stamps, so `agree=0` means one of the two is reading a
|
||||
different clock — which is exactly the class of defect a search that merely "returns plausible
|
||||
times" hides. Here `err=0`: exact agreement, not within tolerance.
|
||||
- `state=2` is `BufferSearch::State::Done` (the enum's ordinal). `state=1` would be `Running`.
|
||||
- `mode=4` after the jump is `DisplayMode::Paused` (the enum's ordinal). `mode=2` would be
|
||||
`Capturing`.
|
||||
- **`placed=1 of 1 in view` is the correct answer, not an under-count.** The jump frames a 0.2 s
|
||||
window and the glitches are 0.4 s apart (one per square period at 5 Hz / square half-frequency),
|
||||
so only one hit can be in view at a time. `1 of 1` against `(12 found)` is the marks agreeing
|
||||
with the window, not with the hit list.
|
||||
- **`total=4.5115` far short of the run is not a defect.** `condition_probe` enables the channel
|
||||
(which starts acquisition) and then calls `apply_time_window()`, and each of those rebuilds the
|
||||
buffer and drops history. 4.51 s was the genuinely retained span.
|
||||
- `locked=1` says the marks are out of the pointer's reach, like every other readout. The exit
|
||||
dump's `search marks: placed=1 hits=12 locked=1` is the same assertion from the dump path; its
|
||||
`placed=` counts marks in the **visible window at exit**, which is the live view after the probe
|
||||
disarmed the trigger — so `placed=0` there is legitimate and asserts nothing about the search.
|
||||
The load-bearing `placed=` is the `[search-probe] marks` line, emitted a frame after the jump,
|
||||
which quotes the window the scope has actually moved to.
|
||||
@@ -177,6 +177,29 @@ precedent, not an exception.
|
||||
`/tmp/udpscope-export`) — collect the export table and write it both ways, skipping
|
||||
only the file chooser.
|
||||
|
||||
- `UDPSCOPE_PROBE_CONDITION_MS` — arms a qualified trigger against the mock and reports the latched
|
||||
`t0`, the measured pulse or band width, and sample values for the window condition. Schedules its
|
||||
own arm and report stages. `UDPSCOPE_PROBE_CONDITION_SHAPE` picks the leg: `0` pulse width `<`
|
||||
(the glitch, the default, needs `--glitch-at`/`--glitch-width`), `1` pulse width `>`, `2` window
|
||||
(uses `sine` with a 1.0 s window and 50 % pre), `3` dropout (needs `--quiet-at`; also uses
|
||||
`sine` with a 1.0 s window and 50 % pre).
|
||||
- `UDPSCOPE_PROBE_STATS_MS` — reports every measurement's accumulator (`n`, mean, sd, lo, hi),
|
||||
pauses, reports again with `dn=` per measurement, then resets and reports a third time. Three
|
||||
phases: `phase=0`, `phase=1` (after pause) and `phase=2` (after Reset). Needs
|
||||
`UDPSCOPE_ENABLE_ALL_MS` ahead of it and `UDPSCOPE_PROBE_MEASURE_MS` to add measurements first.
|
||||
- `UDPSCOPE_PROBE_SEARCH_MS` — runs a buffer search for the armed condition, cross-checks the live
|
||||
trigger's own `t0` against the nearest hit (`agree=1` is the load-bearing reading), and jumps to
|
||||
hit 0. Reports the resulting `mode=`, `axis=` and `marks placed=`.
|
||||
|
||||
The following mock waveform features are what the qualified-condition probes are checked against,
|
||||
as distinct from the fault injection below, which exercises ingest:
|
||||
|
||||
- `--glitch-at S` / `--glitch-width W` invert the `square` signal for `W` seconds at the start of
|
||||
every square period from stream second `S` onwards — a runt pulse of a known width, which is
|
||||
what a `<` pulse-width trigger and a glitch search are checked against.
|
||||
- `--quiet-at S` freezes `sine` at its held value from stream second `S` onwards, so it stops
|
||||
crossing zero: the dropout trigger's input.
|
||||
|
||||
The mock also carries one **fault injection** that no probe schedules, because what it exercises is
|
||||
the ingest rather than the UI: `--time-glitch-at <stream seconds> --time-glitch-by <seconds>` makes
|
||||
**one** packet's time signal jump ahead and then carries on from where the true timeline was, which
|
||||
|
||||
Reference in new issue
Block a user