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
@@ -343,6 +343,26 @@ array of per-node structs was measured and bought **nothing** — at six nodes t
|
|||||||
L1-resident either way. It was kept because a node's state belonging on one cache line is the
|
L1-resident either way. It was kept because a node's state belonging on one cache line is the
|
||||||
right shape for a graph that grows, not because it showed up.
|
right shape for a graph that grows, not because it showed up.
|
||||||
|
|
||||||
|
### Statistics
|
||||||
|
|
||||||
|
**Statistics** under the measurements list adds a second line to every measurement: `n`, the
|
||||||
|
mean, the standard deviation, and the range. The observations are the pane's own **recomputes**
|
||||||
|
— one per refresh, not one per sample — so `n` is a count of *windows measured*, which is what
|
||||||
|
makes "the mean frequency over the last four hundred windows" the claim it reads as. The pane
|
||||||
|
refreshes at most ten times a second while the window scrolls and exactly once when it settles,
|
||||||
|
so a settled window contributes one observation and then stops.
|
||||||
|
|
||||||
|
The accumulator is Welford's, not a sum of squares: a measurement whose values sit at 1e9 with
|
||||||
|
a spread of 1e-3 is an ordinary case here (an absolute stream time is one), and the naive form
|
||||||
|
loses that spread entirely.
|
||||||
|
|
||||||
|
**Reset** clears every accumulator. So does editing, adding or deleting a measurement, a CONFIG
|
||||||
|
that renames the signals, and a change of the display window — each of those changes *what is
|
||||||
|
being measured*, and a mean across the change describes nothing. A pause, a resume, a trigger
|
||||||
|
latch, a pan and a zoom do **not** reset: they change which samples are in view, which is
|
||||||
|
exactly what a statistic over many windows is meant to average. The toggle itself does not
|
||||||
|
reset either, so turning the line off to read the plot and back on again keeps the history.
|
||||||
|
|
||||||
## The session layout file
|
## The session layout file
|
||||||
|
|
||||||
What you arranged is remembered per data source, in
|
What you arranged is remembered per data source, in
|
||||||
@@ -652,6 +672,47 @@ zooming in past the shift showed nothing at all. The self-check now prints `data
|
|||||||
`drawn=` side by side for the bound trace so the contract is checked rather than
|
`drawn=` side by side for the bound trace so the contract is checked rather than
|
||||||
assumed.
|
assumed.
|
||||||
|
|
||||||
|
### Qualified trigger conditions
|
||||||
|
|
||||||
|
The level-and-edge trigger is one of four shapes the **Shape** dropdown offers, and all four
|
||||||
|
are the *same* trigger: the arming, the hold-off, the pre/post event window and the single/auto
|
||||||
|
modes are unchanged, and only the question "did an event happen at this sample" differs.
|
||||||
|
|
||||||
|
- **edge** — a crossing of the level in the chosen direction. The original behaviour, bit for
|
||||||
|
bit.
|
||||||
|
- **pulse width** — a crossing *and* its return, qualified by how long the excursion lasted:
|
||||||
|
`<`, `>`, `within` or `outside` a pair of widths. The event is timed at the **closing** edge,
|
||||||
|
because that is the first sample at which the width is known. `<` a width shorter than the
|
||||||
|
signal's own pulses is the glitch trigger: nothing but a runt satisfies it.
|
||||||
|
- **window** — the signal entering or leaving a band between two levels. A band is two levels,
|
||||||
|
so the panel refuses a zero-width one rather than quietly degrading to an edge.
|
||||||
|
- **dropout** — the *absence* of a crossing for longer than a timeout. One event per outage:
|
||||||
|
the condition re-arms only when a crossing returns.
|
||||||
|
|
||||||
|
A shape's own rows are the only ones shown; the rest are hidden, so the panel never offers a
|
||||||
|
width to a window trigger. The condition is **not** persisted in the session file — the session
|
||||||
|
has never persisted the trigger — so a restart comes up on an edge trigger.
|
||||||
|
|
||||||
|
### Searching the buffer
|
||||||
|
|
||||||
|
**Search the buffer**, at the foot of the trigger panel, runs the trigger's *own* condition over
|
||||||
|
the whole retained ring and lists every place it holds. There is no second condition editor: a
|
||||||
|
second place to spell the same question would be a second place to get it wrong.
|
||||||
|
|
||||||
|
The scan runs on the GTK thread in a few milliseconds of work per frame, taking the model's
|
||||||
|
mutex for one chunk at a time, so neither the frame rate nor the producer stalls on it. Up to
|
||||||
|
10 000 hits are kept; past that the scan stops and says so. If the stream overtakes the oldest
|
||||||
|
samples mid-scan the search skips forward to what is still there and reports `the stream
|
||||||
|
overtook the oldest samples` rather than returning a hit at a time that no longer exists.
|
||||||
|
|
||||||
|
The arrows step through the hits. A step **pauses** the view on the hit, centred, exactly as the
|
||||||
|
Pause button does — so the frozen window is pinned and navigable within its own data, and
|
||||||
|
resuming returns to the live view. Up to 16 of the hits **in the visible window** are marked on
|
||||||
|
every row at once, nearest the centre first and the selected one always among them in its own
|
||||||
|
colour; when there are more in view than there are marks the panel says `16 of 38 marks shown —
|
||||||
|
zoom in to see the rest`, which is a true statement about the picture rather than a silent
|
||||||
|
truncation.
|
||||||
|
|
||||||
**The live view had two clipping bugs of its own**, both wearing the same face as that
|
**The live view had two clipping bugs of its own**, both wearing the same face as that
|
||||||
one: the older part of the trace blank, the newest part fine, and the crosshair still
|
one: the older part of the trace blank, the newest part fine, and the crosshair still
|
||||||
reading values in the blank part. Neither is a data bug — the samples were always there.
|
reading values in the blank part. Neither is a data bug — the samples were always there.
|
||||||
|
|||||||
@@ -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
|
`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
|
that the fast path is a fast path and not an approximation. The app's reading is
|
||||||
`[measures-probe]` against `--noise 0.1`.
|
`[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
|
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
|
ring, or an export and a capture of the same window would hold different samples. Call
|
||||||
it under `mutex()`.
|
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.
|
- `[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).
|
- `[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.
|
- `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
|
`/tmp/udpscope-export`) — collect the export table and write it both ways, skipping
|
||||||
only the file chooser.
|
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 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
|
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
|
**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