Skip to content

Navigation Menu

Sign in
Sign up

Gate bounce is its own rule: a tunable per-timer same_pass_window, classified in the fold (#517) - #520

Merged
ryan-johnson2 merged 2 commits into
devel from
feat/517-same-pass-window
Sep 7, 2026
Merged

Gate bounce is its own rule: a tunable per-timer same_pass_window, classified in the fold (#517) #520
ryan-johnson2 merged 2 commits into
devel from
feat/517-same-pass-window

Conversation

@ryan-johnson2

@ryan-johnson2 ryan-johnson2 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Closes #517.

A bouncy gate put a removal row between every pair of real laps in Marshaling — three or four per pass — so the laps an RD came to marshal were the minority of the list.

The cause was that one concept had two owners, and neither was the fold: the console suppressed reflection tones with a hardcoded 1 s constant the server knew nothing about (#503), while the fold lumped every too-close crossing under UnderMinLap — conflating a competition rule ("too soon to count as a lap") with a hardware fact ("that did not happen twice"). Same disposition on the wire, so the console could not collapse a reflection burst without also hiding a genuinely short lap.

What changed

  • Timer::same_pass_window_micros — timer-level, because bounce is a property of the gate. RoundDef::min_lap_secs stays put; that one is a racing rule. Default 1 s, hard-capped at 2 s.
  • VoidReason::SamePassBounce / CrossingDisposition::RejectedSamePass — checked before the floor, measured from the last surviving pass, honouring the marshal exemption.
  • Marshaling collapses a burst to one muted line, expanding to the ordinary rows with per-crossing Restore. No bulk restore.
  • CROSSING_TONE_COOLDOWN_MICROS deleteduseCrossingTones reads the disposition. Behaviour-preserving: the fold measures from the last surviving pass exactly as the cooldown measured from the last sounded tone.

Pinned at the arm

min_lap_secs freezes once a round has raced so results cannot shift under it. Timers have no such freeze, so a timer-level fold input read live would re-score every past race on that timer. Event::HeatDetectionPinned records the window in force at the arm and the fold reads it from the log — the same shape as HeatStarting / HeatFinalizing / RaceExpired. The general rule is #518.

⚠️ Reviewer's eye here: the pin is included in the heat/class windows by tag and un-gated. It is appended at the arm and current_run_start opens the window at Running, so a gated scan drops it and the live view then disagrees with the lap list about a suppressed pass — the D26 failure of #409. I hit exactly that mid-build; live_crossings.rs now asserts it cannot recur.

Verification

cargo xtask ci (1636 passed) · frontend build/check/lint/test (1248) · contract (141) · cargo xtask e2e (46/46).

Also driven end-to-end against dockerized RotorHazard, via a new bouncy race-day scenario (echo bursts per pass plus one genuine sub-floor crossing):

disposition n
Counted 8
Holeshot 4
RejectedSamePass 11
RejectedTooShort 1

One pilot's list shows both cases side by side — 2 collapsed bounces and a real 3 s crossing that keeps its full row, its Restore and its tone. And with the race finished, setting the timer's window to 0 left the lap list byte-identical; a live read would have flipped all 15 bounces to UnderMinLap.

Also here

A second commit corrects src-tauri/README.md, which claimed Windows packaging was deferred and no Windows artifact was produced — untrue since the release workflow grew its windows-latest leg, and contradicted by the same file's own build section.

🤖 Generated with Claude Code

https://claude.ai/code/session_011XCMrqJjroyaZ7M2gF6JhU

ryan-johnson2 and others added 2 commits September 7, 2026 02:54
...red" section
`src-tauri/README.md` said Windows packaging was deferred and "no Windows
artifact is produced here". That has not been true since the release workflow
grew its `windows-latest` leg: it builds `gridfpv-desktop.exe`, smoke-tests that
it starts, and uploads it as `gridfpv-windows-portable`. The same README's own
build section already listed the `.exe` output path, so the file contradicted
itself.
Replaced with what actually happens, and — since the question comes up every
time someone looks at building locally — WHY no cross-compile is attempted:
building the exe from Linux is feasible, but it cannot *launch* the result, and
the startup smoke-test is the point (#58 is done when the binary launches on all
three OSes, not when it compiles for them). Same note for the macOS leg, which
stays on Apple hardware for licensing reasons.
Also drops the "(deferred — not yet built)" qualifier from the Windows row of
the data-dir table, which was stale for the same reason.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XCMrqJjroyaZ7M2gF6JhU 
...ss_window (#517)
A bouncy gate put a removal row between every pair of real laps in Marshaling:
 ∅ crossing at 1:02.4s — under min lap, auto-removed [Restore]
three or four of them per pass, so the laps an RD came to marshal were the
minority of the list. The cause was that ONE concept had TWO owners and neither
was the fold: the console suppressed reflection tones with a hardcoded 1s
constant the server knew nothing about (#503), while the fold lumped every
too-close crossing under `UnderMinLap` — conflating a competition rule ("too
soon to count as a lap") with a hardware fact ("that did not happen twice").
Because they were the same disposition on the wire, the console could not
collapse a reflection burst without also hiding a genuinely short lap.
So the fold now decides, once, and both consumers read it.
* `Timer::same_pass_window_micros` — timer-level, because bounce is a property
 of the GATE (antenna, RSSI thresholds, placement), identical across every
 round run on it. `RoundDef::min_lap_secs` stays where it is; that one is a
 racing rule. Default 1s, hard-capped at 2s: no lap is that short, so the
 setting can only ever collapse repeat detections of one physical pass.
* `VoidReason::SamePassBounce` / `CrossingDisposition::RejectedSamePass` —
 checked BEFORE the floor (it is the narrower claim), measured from the last
 SURVIVING pass so a burst collapses against one anchor instead of ratcheting
 forward off its own echoes, and honouring the same marshal exemption.
* Marshaling collapses a burst to one muted line, expanding to the ordinary
 rows with per-crossing Restore. No bulk restore — restoring a whole burst is
 never wanted and one misclick would add that many phantom laps.
* `CROSSING_TONE_COOLDOWN_MICROS` is deleted, not aliased. `useCrossingTones`
 reads the disposition instead. Behaviour is unchanged: the fold measures from
 the last surviving pass exactly as the cooldown measured from the last sounded
 tone, and a suppressed crossing extends neither.
## Pinned at the arm, so a timer edit cannot re-judge a finished race
`min_lap_secs` freezes once a round has raced, precisely so results cannot shift
under it. Timers have no such freeze — they are edited freely — so a timer-level
fold input read live would re-score every past race on that timer.
`Event::HeatDetectionPinned` records the window in force when the heat arms, and
the fold reads it from the log. Same shape as `HeatStarting` / `HeatFinalizing` /
`RaceExpired`: the runtime resolves a value once, at emission time, and logs it
as a fact. `(Staged, Start) -> Armed` is the only arm in the heat FSM, so every
run pins exactly once and a re-arm re-pins. Verified live: with a race finished,
turning the timer's window off left the lap list byte-identical. The general
rule is #518.
The pin is included in the heat and class windows BY TAG and un-gated — it is
appended at the arm and `current_run_start` opens the window at Running, so a
gated scan would drop it and the live view would then disagree with the lap list
about a suppressed pass. That is the D26 failure of #409; `live_crossings.rs`
now asserts it cannot recur.
`docker/rotorhazard/race_day.py` gains a `bouncy` scenario — echo bursts per
pass plus one genuine sub-floor crossing — which is the fixture this needs.
Verified against dockerized RH: 11 RejectedSamePass, 1 RejectedTooShort, 8
counted laps, and the two sitting side by side in one pilot's list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XCMrqJjroyaZ7M2gF6JhU 
ryan-johnson2 merged commit f2ac8c8 into devel Sep 7, 2026
4 checks passed
ryan-johnson2 deleted the feat/517-same-pass-window branch September 7, 2026 03:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Gate bounce is its own rule: a tunable per-timer same_pass_window, classified in the fold (#397/#503 follow-up)

1 participant

AltStyle によって変換されたページ (->オリジナル) /