fix(streamhub): stop the trigger going deaf between captures
TriggerEngine::CheckSample returned early in every state but ARMED, so an edge arriving while a capture was being collected or handed out was dropped, and the automatic rearm then waited for a FRESH edge. The engine was therefore blind from its own trigger point until the capture had been harvested — a post-window — and for the holdoff on top of that. On a sparse pulse train that rounds the capture spacing up to a whole pulse period: at the default 1 s window the blind stretch is 1 s, so a 1 Hz train was caught at 0.5 Hz and a wider window lost whole multiples. The comparator now keeps running through COLLECTING and TRIGGERED and remembers the first edge at or past trigTime + max(postSec, holdoffSec). The holdoff guards against re-triggering on the ringing of the same event and is measured from the trigger point, so it overlaps the post-window rather than adding to it. Rearm() fires on the remembered edge; it also keeps the tracked level, so the first sample after it has a real predecessor instead of being spent seeding one. Arm() stays the operator's arm and discards the held edge — they asked for the next event, not one already been and gone — and SetConfig() and Disarm() drop it too, since it was never judged against the new window. This is the same defect and the same remedy already validated in the Go hub (wshub/trigger.go, trigger_sporadic_test.go); the C++ hub had been left with the original semantics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
fbae7d712c
commit
092fd3c775
@@ -107,12 +107,31 @@ Hub-side, web-client semantics (`setTrigger` fields in
|
||||
|
||||
```
|
||||
IDLE --arm--> ARMED --edge crossing--> COLLECTING --every source past trigTime+postSec+0.15s--> TRIGGERED
|
||||
TRIGGERED --rearm (single) / auto ~200ms (normal, unless stopped)--> ARMED
|
||||
TRIGGERED --rearm (single) / auto after holdoffSec (normal, unless stopped)--> ARMED
|
||||
└─ or straight to COLLECTING on a held edge
|
||||
any --disarm--> IDLE
|
||||
```
|
||||
|
||||
`UDPSourceSession` calls `TriggerEngine::CheckSample` for every decoded sample
|
||||
of the configured signal (signal index cached per config epoch). Each source is
|
||||
of the configured signal (signal index cached per config epoch).
|
||||
|
||||
The comparator keeps running through COLLECTING and TRIGGERED. It cannot fire
|
||||
there — the capture in flight owns that stretch — but it remembers the first
|
||||
edge at or past `trigTime + max(postSec, holdoffSec)`, and `Rearm()` fires on
|
||||
that remembered edge instead of waiting for a fresh one. Without this the engine
|
||||
is deaf from its own trigger point until the capture has been harvested and the
|
||||
holdoff has run, which on a sparse pulse train rounds the capture spacing up to
|
||||
a whole pulse period: at a 1 s window a 1 Hz train was caught at 0.5 Hz, and a
|
||||
wider window lost whole multiples. The capture is built from the edge's own
|
||||
timestamp out of rings that still hold everything around it, so honouring it
|
||||
costs nothing.
|
||||
|
||||
`Arm()` and `Rearm()` differ only in this: `Arm()` is the operator's own arm and
|
||||
discards the held edge (they asked for the next event), while `Rearm()` is the
|
||||
automatic end-of-capture arm and consumes it. `Rearm()` also keeps the tracked
|
||||
level, so the first sample after it is compared against its real predecessor
|
||||
rather than being spent seeding one. `SetConfig()` and `Disarm()` drop the held
|
||||
edge as well — it was never judged against the new window. Each source is
|
||||
read `[trigTime−preSec, trigTime+postSec]`, LTTB-capped to 20 000 pts/signal and
|
||||
appended to a binary **version 2** capture frame; every FSM transition
|
||||
broadcasts a `triggerState` event.
|
||||
|
||||
Reference in New Issue
Block a user