From d1ab5cb3916d83aa2c1f926c451ca7409986a88a Mon Sep 17 00:00:00 2001
From: "Ryan Johnson (ntninja)" Why these
runner-up (excellent single-binary story, gentler curve) — Rust wins on hardware
integration and race-day robustness. Electron is easiest for a JS-only
team but loses the single-binary/no-deps goal. The frontend framework
- (React vs Svelte) is intentionally still open (see §11).
+ is Svelte (resolved — §11); the shell is Tauri, shipped as the three-OS portables.
ClassId (planned: promoted into the events crate, so
+ a class is identified by a ClassId (promoted into the events crate, so
log entries can be tagged by class).RoundId
- (planned: promoted into the events crate alongside ClassId). The
+ (promoted into the events crate alongside ClassId). The
HeatScheduled log entry that creates a heat is tagged with its class, round,
- and per-pilot channel/frequency assignments (planned, v0.4 Slice 0 — the
- log-tagging slice).- Planned — the event workspace is a sequence of editable stage-pages, not a + As built — the event workspace is a sequence of editable stage-pages, not a “Setup” tab. Inside an event the workspace is a left-to-right sequence of stage-pages you can revisit and edit at any time: - Classes · Roster · Rounds+Heats · Live control · - Marshaling · Results (Rounds and Heats are combined on one page). There + Timers · Classes & Roster · Rounds & Heats · + Race control · Marshaling · Results · Audit. There is no separate “Setup” phase or tab — setting an event up is just visiting its early stage-pages. A guided setup wizard is a thin first-pass over - those same pages for the common path, and is built last (v0.4 Slice 7) so - the pages exist before the wizard that walks them. One event flies one track. + those same pages, auto-opened once when an event is created (the manual relaunch button was + retired in the v1 chrome trim). Race audio — procedure tones + spoken lap + callouts — is an app-shell controller that follows the live stream on every page. One event flies one track. (The race-running redesign; Setup, Mains, Roadmap v0.4.)
@@ -206,9 +207,9 @@cargo build -p gridfpv-app --features live serves
the API + RD console at :8080. The desktop crate is excluded
from the root Cargo workspace, so cargo xtask ci never compiles it (CI runners
- have no webkit2gtk/GTK); the Tauri build is a separate, deliberate command. Windows packaging
- is deferred — the crate is Windows-ready in principle, but no Windows artifact is built and no
- cross-compile is attempted.
+ have no webkit2gtk/GTK); the Tauri build is a separate, deliberate command. The gated
+ release-builds workflow builds the portables for Linux, Windows, and
+ macOS (all three shipped with v0.4.0-alpha.1, live RH adapter included).
- As built (v0.4): the frontend uses vitest; the suite is green (78 tests — 20 component, - 7 protocol-client, 51 RD console). + As built (v0.4): the frontend uses vitest; the suite is green (the counts grow with every slice — see CI).
cargo xtask defines every check. Local and CI
run the identical suite: cargo xtask ci = fmt --check &&
clippy --all-targets -D warnings && test --all &&
- the Rust→TS bindings drift-check. The GitLab pipeline is thin — it only calls
- cargo xtask ci. Never put check logic in .gitlab-ci.yml; add it
- to xtask so local and CI can't drift.
.github/workflows/ci.yml): the Rust job calls cargo xtask ci
+ verbatim, alongside a cargo audit job and a frontend job
+ (build / type-check / lint / test / Director build / contract suite);
+ the gated release-builds.yml builds the Tauri portables. Never put check
+ logic in the workflow files; add it to xtask so local and CI can't drift.
+ One product version, bumped by cargo xtask version.
+ The version lives in [workspace.package].version
+ (0.x milestones, -alpha.N/-beta.N pre-releases) and is
+ kept in lock-step with src-tauri, tauri.conf.json, and the console
+ package.json by cargo xtask version <x.y.z[-pre]>. It is
+ surfaced by GET /about and the console build-stamp;
+ CONTRACT_VERSION is the separate wire-compat integer.
This phase leans on the optional Cloud — the analytics warehouse and season linking — so it is intentionally light here and will get its own treatment alongside the Cloud - design (Architecture, coming soon). The single-event pipeline above stands on its own + design (Architecture). The single-event pipeline above stands on its own without it.
diff --git a/docs/format-model.html b/docs/format-model.html index da57e0c..15af7d2 100644 --- a/docs/format-model.html +++ b/docs/format-model.html @@ -28,8 +28,7 @@Release state (2026-06-30, PR #327) — this release is primitives-first. The @@ -73,13 +72,13 @@
rounds, default 3; 0 = open-ended — generate the next rotation on demand)tournaments-snapshot with the bracket machinery (PR #327) — not in the current console.round_robin generator. Deferred — carved to tournaments-snapshot (PR #327); rebuilt post-v1.single_elim. Deferred — carved to tournaments-snapshot (PR #327); rebuilt post-v1.StageStagedStartArmedSkipCountdown (override)RunningForceEnd (override)FinishedSkipCountdown (wire-only — retired from the console)RunningForceEnd (the console's Stop button)FinishedFinalizeFinalizedAdvanceAdvancedRevertRevertedAbortAbortedRestartRestartedDiscardDiscardedDiscardDiscarded@@ -118,9 +118,11 @@
Armed → Running and Running → Unofficial — have no everyday
- command: they are appended by the runtime clock (§4). The override commands
+ command: they are appended by the runtime clock (§4). The manual commands
SkipCountdown and ForceEnd record exactly the same transitions
- the auto-path would, as race-day escape hatches.
+ the auto-path would, as race-day escape hatches. (Console reality: ForceEnd
+ surfaces as the plain Stop button; SkipCountdown is
+ wire-only — the console retired it, and the “override” styling concept with it.)
Armed → Running
If the clock can't be trusted — a stuck countdown, or a race that must go now — the RD's
- SkipCountdown override forces Armed → Running immediately,
+ SkipCountdown command (wire-only) forces Armed → Running immediately,
recording the same Running transition the driver would.
Running → UnofficialWin condition, then the grace window, then auto-Unofficial.
Once the race-end criterion is met the driver holds the heat in Running for
the round's grace window — so a trailing pilot's final lap still counts —
- and only then appends Running → Unofficial. The ForceEnd override
+ and only then appends Running → Unofficial. The ForceEnd command (the console's Stop)
forces the same step now when the completion clock must be bypassed.
User-facing documentation (guides for race directors, pilots, and - self-hosters) is a separate concern and will live in its own repository - later. Keep that material out of this repo. + self-hosters) is a separate concern and lives in its own repository — + published at docs.gridfpv.com. Keep that + material out of this repo.
- Release state (2026-06-30): this is a primitives-first release —
+ Release state: v0.4.0-alpha.1 released 2026-07-05
+ (tagged on main; Linux/Windows/macOS desktop portables with the live RH
+ adapter). It is a primitives-first release —
three round types (Practice / Time Trials / Head-to-Head) ship; the tournament structures are
carved to the tournaments-snapshot branch (PR #327) for a post-v1 rebuild.
- Two imports matter today: the MultiGP chapter roster (at setup) and + Two imports matter first (neither built yet, as of v0.4.0-alpha.1): the MultiGP chapter roster (at setup) and the Velocidrone leaderboard (for qualifying seeding). Both bring data in from outside, and both deliberately live here / with qualifying — not in the timer-adapter layer. diff --git a/docs/live-state-clock.html b/docs/live-state-clock.html index 915a03a..b00c709 100644 --- a/docs/live-state-clock.html +++ b/docs/live-state-clock.html @@ -50,12 +50,13 @@
current_heatHeatScheduled /
- HeatStateChanged appears last in the log. A heat that reached the
+ HeatStateChanged / CurrentHeatSelected wins (a freshly-scheduled
+ heat must not steal focus — the fill-no-steal rule), with the first
+ HeatScheduled as the fallback before anything has run. A heat that reached the
terminal Final phase is still reported as current until a newer heat
takes the timer — mirroring what an overlay shows between heats ("last heat, now scored").
- (Planned: a manual-selection override so the RD can pin the live view to a chosen heat
- regardless of last-scheduled order.)CurrentHeatSelected, which the fold honors — pinning the live view without racing it.)phaseHeatPhase — the linear status
Scheduled → Staged → Armed → Running → Unofficial → Final. The engine's off-ramp
diff --git a/docs/marshaling-plan.html b/docs/marshaling-plan.html
index be9af71..b5b60ee 100644
--- a/docs/marshaling-plan.html
+++ b/docs/marshaling-plan.html
@@ -12,6 +12,19 @@
GridFPV
Internal design doc · the sliced implementation plan for marshaling
+ ++ SUPERSEDED — fully shipped (v0.4.0-alpha.1). Every slice this plan + sequences is built: signal capture + the RSSI graph, lap-level marshaling + the audit, + the provisional → official lifecycle with the protest gate, full adjudication, and — + beyond the plan — the D25 shared removal record, the D26 min-lap floor, and Tune + Detection (the re-detect this plan deferred). The as-built design lives in + Marshaling; this file is kept as the historical build + record only. The §2 refinements (no per-person provenance; protest window optional and + OFF by default; RD-only writes) all carried through to the shipped model. +
+diff --git a/docs/marshaling.html b/docs/marshaling.html index 27c35a6..9d7011b 100644 --- a/docs/marshaling.html +++ b/docs/marshaling.html @@ -96,10 +96,14 @@
@@ -152,7 +156,7 @@
FPVTrackside-grade lap editing, but for any timer. Laps are enter/exit detections; the
marshal can add, remove, split, edit-time (with the neighbour auto-adjusting), and reversibly DQ,
- with the deferred smart-minimum so borderline laps aren't invalidated prematurely. This is built
+ with the D26 min-lap floor shipping instead of the once-planned deferred smart-minimum: an under-floor crossing is auto-removed immediately, visibly, with a marshal Restore override. This is built
on the logged correction primitives that already exist in
Architecture §3 — DetectionVoided,
LapInserted, LapAdjusted, PenaltyApplied — extended with
@@ -187,12 +191,12 @@
Decision: v1 is signal-AS-EVIDENCE, not a re-implemented detector. - We persist and display the trace as the ground truth to review against, but we do - not re-run detection in v1. The full RotorHazard-style "Recalculate" with - draggable thresholds (re-deriving laps from the stored signal) is explicitly deferred - — it is a clean later add-on on top of the stored trace, not a v1 requirement. Building evidence - first, re-detection later, keeps the differentiating governance layer from waiting on a signal - algorithm we don't yet need.
+ We persist and display the trace as the ground truth to review against — and the + full RotorHazard-style "Recalculate" shipped as Tune Detection: + draggable enter/exit levels over the captured trace, a live preview in the lap list, committed + as canonical void/insert rulings. It shares the D25 removal record, so a crossing the marshal + removed is never re-proposed. (The original v1 plan deferred this; it landed ahead of + schedule, Grid-side.)IRLRotorHazard heats carry an RSSI trace; the graph is the evidence @@ -295,9 +299,9 @@
gridfpv_calibrate,
+ plugin S4).
- As built (v0.4) — the Rounds form. A bracket round is created on the
+ Recorded design (pre-carve v0.4 form — NOT the current console) A bracket round is created on the
Rounds+Heats stage-page: a Label, a Format (friendly name —
- Single/Double Elimination, Multi-Main, Round Robin),
+ Single/Double Elimination, Multi-Main, Round Robin — these formats now live on tournaments-snapshot; the current Add-round picker offers only the three primitives),
then dynamic per-format fields — a single-select class (a round targets one
class), a win condition, a seeding rule (From roster
or From ranking with a source round + top-N — the cut is now labeled
@@ -74,7 +74,9 @@
FromRanking — single-elim is a clean winners-chain, double-elim's cross-bracket
+ FromHeatWinners (the advancement seeding is built server-side,
+ form-preserved but not form-creatable; the builders are what's carved) — single-elim
+ is a clean winners-chain, double-elim's cross-bracket
losers-of feed is the design problem (D13, #217). Heat-filling has
two v1 modes: manual and format-generated (the generators);
a “Generate heats” action fills the whole deterministic round in
diff --git a/docs/pipeline-practice.html b/docs/pipeline-practice.html
index 0f79cdd..a6c82ae 100644
--- a/docs/pipeline-practice.html
+++ b/docs/pipeline-practice.html
@@ -18,7 +18,7 @@ open_practice round. In the
race-running redesign there is no separate “Practice phase” with its own modes.
Practice is just a round on the event whose format is
- open_practice (friendly name “Open Practice”),
+ open_practice (friendly name “Practice”),
created from the same Rounds+Heats stage-page as every other round. Selecting that format
swaps the round form's class/win-condition/seeding block for an active-channels
picker and an optional time-limit (minutes) field — nothing else.
@@ -59,7 +59,7 @@
- Open Practice is channel-seated, not pilot-seated. The round form's
+ Practice is channel-seated, not pilot-seated. The round form's
active-channels picker selects which of the primary timer's node seats are
live; the engine then auto-creates a single open heat whose “lineup”
is those channels (competitor refs are node-0, node-1, … — the seats,
diff --git a/docs/pipeline-qualifying.html b/docs/pipeline-qualifying.html
index 4729f98..64065e7 100644
--- a/docs/pipeline-qualifying.html
+++ b/docs/pipeline-qualifying.html
@@ -24,7 +24,7 @@
BestLap, N>1 BestConsecutive). The usual last-lap rule applies to timed
windows: a lap whose start/finish crossing came before the limit completes and counts even
if it ends after the buzzer.rounds param (labeled “Heats per pilot”, default
3) sets how many heats each pilot flies; the
engine builds a deterministic, channel-balanced plan (one pilot per distinct
- channel per heat, capped by the timer's node count) across all of them.rounds stays editable), and like every round it carries the D26
+ min-lap floor (auto-removes double-detection echoes, marshal-restorable).heat_size, timed_qual chunks each pass into multiple heats of
heat_size (heat ids tq-r{n}-h{m}), so every pilot still flies the
configured number of heats.vtx_types (a multi-select of
@@ -88,7 +86,7 @@ meta table (#115).race_ended_at, server-time authoritative — see
Live State & the Race Clock) all ship. Per-gate
splits remain a later refinement (the lap-count + last-lap pair is the live core).
- The Tauri shell + 3-OS single-binary packaging are still not built (#57/#58)
- — the RD console runs as a web app served by the Director, not yet a native window.
+ The Tauri shell + 3-OS packaging SHIPPED (#57/#58) with v0.4.0-alpha.1:
+ self-contained portables for Linux / Windows / macOS (console embedded, live RH adapter in),
+ built by the gated release-builds workflow. The web-served console remains the
+ hosted mode — same app either way.
@@ -161,6 +163,9 @@ GET /healthGET /aboutGET /timeDate.now()GET /plugin/gridfpv.zipGET /events · POST /events · DELETE /events/{id}GET·PUT /active-eventActiveEvent / SetActiveEventRequestGET·POST /timers · PUT·DELETE /timers/{id}channel_capability / node_count / available_channels (§ below). Open read; RD-gated writes; Mock undeletablePUT /classes · PUT /classes/{class}/membershipPOST /rounds · PUT·DELETE /rounds/{round}RoundDef / NewRoundReq / UpdateRoundReqGET /heatsHeatSummary[]) the Heats UI reads. Open readGET /rounds/{round}/ranking · GET /classes/{class}/standingsGET /rounds/{round}/ranking · GET /rounds/{round}/standings · GET /classes/{class}/standingsGET /auditPUT /roster · POST·DELETE /roster/{pilot}PUT /timers · PUT /primary-timerGET /snapshot/event/{event}localhost). A request whose source address is loopback is the operator at the
keyboard by definition, so it gets full control with no login, no token,
- no passphrase — the RD opens the app and is in. This is enforced by source-IP
- middleware (peer.ip().is_loopback()), and it is safe because the OS drops
+ no passphrase — the RD opens the app and is in. This is the decided design — the source-IP
+ middleware (peer.ip().is_loopback()) is not built yet (#80);
+ today control is open unless an RD token (GRIDFPV_RD_TOKEN) is configured
+ (the dev/local-trust posture, §9.4). It will be safe because the OS drops
spoofed-loopback "martian" packets at the kernel before they ever reach the server: a
remote host cannot forge a loopback source address. The practical consequence is that
there is no local auth UI — no login screen in front of the event picker,
diff --git a/docs/race-engine.html b/docs/race-engine.html
index 5de9cbc..cf4aa5f 100644
--- a/docs/race-engine.html
+++ b/docs/race-engine.html
@@ -90,7 +90,7 @@ Abort, Restart, Discard, Revert, and
the two race-day overrides SkipCountdown (force
Armed → Running now) and ForceEnd (force
- Running → Unofficial now) for when the clock can't be trusted. Notably,
+ Running → Unofficial now) for when the clock can't be trusted. (Console reality: ForceEnd surfaces as the plain Stop button; SkipCountdown is wire-only — the console retired it, the countdown just runs.) Notably,
both Abort and Restart now reset the heat all the way
to Scheduled (the RD re-Stages it): Abort is legal
from Staged/Armed/Running, Restart
from Armed/Running/Unofficial.
Revert re-opens a finalized result (Final → Unofficial);
- Discard queues a finalized heat for a clean re-run
- (Final → Scheduled).
+ Discard throws a heat out for a clean re-run — legal from Unofficial or Final (both land back at Scheduled).
In the log these are canonical events: a heat is created by a HeatScheduled
@@ -313,11 +312,11 @@
Multi-class scheduling. Several classes can be live at once, each with
its own format, sharing scarce resources — the timer and, for IRL, its channels.
- The engine interleaves heats round-robin across classes in a fixed
- rotation (so no class starves; a format that emits several heats at once has them buffered
- and drained one per turn rather than monopolising the serialized timer), and
- allocates each IRL heat's video channels by deterministic first-fit in seed
- order — the engine's half of the split (§7.3): it decides who flies on what channel, the
+ Heat production is RD-driven per round (fill-round / generate-next; a
+ cross-class round-robin rotation scheduler remains future work), and the engine
+ allocates each IRL heat's video channels by IMD-aware set
+ selection (the cleanest third-order-intermod set, #209) then deterministic
+ seed-order assignment — the engine's half of the split (§7.3): it decides who flies on what channel, the
adapter applies the tuning. The channel set is defined by the selected timer,
not a free-floating event pool — the standard FPV catalog plus the adapter's fixed-or-flexible
capability (custom raw-MHz where allowed) — and the timer's node/slot count caps the
@@ -387,7 +386,8 @@ GridFPV Internal design doc · the phased build plan, and how it lives in GitLab (free tier) Internal design doc · the phased build plan, and how it lives on GitHub
The sequence we build in, and how that sequence is tracked. Written last, on purpose —
it orders the now-settled design into release-versioned phases, each a shippable step,
- and maps cleanly onto the GitLab free-tier planning features so the plan
- can become real milestones and issues without paid add-ons.
+ tracked as GitHub issues and pull requests on
+ github.com/GridFPV/gridfpv.
- This doc owns the phase plan and the GitLab planning model.
+ This doc owns the phase plan and how it is tracked on GitHub.
It sequences the work the other docs specify — it does not redefine their designs; each
phase links to the doc that owns its detail. It is a living plan
(authoritative but revisable): phases and scope will shift as we
@@ -80,9 +80,9 @@
- Status (2026-06): v0.1 (walking skeleton),
- v0.2 (first adapters + replay harness), and v0.3
- (race engine) are complete and merged — the SQLite append-only log,
- projection engine, Rust→TS generation, RotorHazard + Velocidrone adapters,
- recorded-session replay golden tests, live + emulated-signal dockerized-RH tests, the
- heat-loop state machine, scoring/marshaling, and the format generators are all in
- the tree — since the primitives-first carve-out (2026-06-30, PR #327) the registry ships
- three primitive generators (Practice / Time Trials / Head-to-Head); the
- tournament structures are preserved on
- The v0.4 foundation is also built: the embedded axum
- protocol server (snapshot + WS stream, scoping, auth), the
- Svelte console shell + generated types, and the
- contract-suite / observability backbone. After a hands-on UI review,
- the remaining v0.4 work — actually directing a race from the console — is being
- rebuilt as vertical feature slices (see §1 and the v0.4 section below).
+ Between v0.3 and the alpha, v0.4 grew well past "direct a single race": the six-slice
+ marshaling build (signal-as-evidence RSSI traces, re-detection with
+ tune-commit, the shared removal record D25, native
+ min-lap D26, penalties/protests/audit, the Final
+ freeze), app-wide race audio (procedure tones + spoken lap callouts),
+ the pre-release audit (D19–D24),
+ and the alpha versioning scheme (0.x milestones with
+ -alpha.N/-beta.N pre-releases, one workspace version via Goal: turn passes + a format into a run race. (Race Engine) Done when: a full event (qualifying → bracket → winner) runs from a recorded source with correct results and marshaling; generators are table-tested. Done when (met): a full event ran from a recorded source with correct results and marshaling; generators table-tested. The shipping format set was later narrowed to the three primitives (see the status note above). Goal: a person can run a real event end to end from the RD console. (Protocol, Clients) Goal (met): a person can run a real event end to end from the RD console. (Protocol, Clients)
The horizontal foundation — embedded axum protocol server (snapshot + WS
stream, scoping, bearer + LAN join-token auth), the Svelte console shell +
shared component library + generated types, and the contract-suite/observability backbone —
- is built. The remaining work is the director workflow itself, delivered as
- vertical slices (each backend → API → UI, working before the
- next):
+ plus the director workflow, delivered as vertical slices (each
+ backend → API → UI). The slice record, with outcomes:
@@ -186,13 +191,16 @@ Done when: a complete event — registration → heats → live races → marshaling → results, across a multi-heat format — runs live from the RD console against real/dockerized RH, and the single binary launches on all three OSes. Done when (met, 2026-07-05): a complete event — registration → heats → live
+ races → marshaling → results, across a multi-heat format — runs live from the RD console against
+ real/dockerized RH, and the single binary launches on all three OSes. Released as
+ v0.4.0-alpha.1.
- GitLab puts Epics, the Roadmap timeline view, and Iterations behind paid tiers, so the
- plan is built from the free-tier primitives and emulates the rest. We use a
- single project.
-
- What we deliberately don't rely on: Epics, the Roadmap view, Iterations,
- scoped-label enforcement, group-level multi-boards, and blocking links — all paid. The
- tracking-issue + milestone + label-convention model covers the same ground on free tier,
- and this doc remains the source of truth either way.
- The agreed sequence once this roadmap is finalized:
- Seeding depth: break v0.1 down fully into issues now, create the
- tracking issues for all phases as placeholders, and detail later phases just-in-time — so the
- board reflects what's actionable without pretending we've planned the far future to the task level.
+ The plan lives on GitHub (migrated from
+ GitLab 2026-06-20, issue numbers preserved): issues for the work,
+ labels for the axes, pull requests into
- Harness reality:
The heat loop already appends the events broadcast wants (Race Engine §2):
-
@@ -309,7 +309,18 @@
+ Since the D16 plugin pivot the primary hands-on layer is the plugin-equipped RH
+ rig:
The same emulated-signal mechanism (§5.1) is also a runnable, interactive tool
for hands-on marshaling testing: drive a real dockerized RotorHazard with a controllable
@@ -352,7 +363,7 @@ Stage and Start — and seat a pilot on a fed node.
The two ways a mock-feed heat silently records no laps: (a) the heat never
@@ -414,7 +425,7 @@
+ Status (v0.4.0-alpha.1): the design lives at
+ RotorHazard Plugin; the plugin itself ships
+ in-repo (7. Open decisions
Heat Lifecycle; rationale in
Decisions.
RandomizedDelay — gate-start-vs-staggered
+ configurability never landed; add a variant if needed); the first crossing of the start/finish
gate after arming opens lap 1; splits derive per the gate model in
Timer Adapters.Roadmap
- 2. The phases
flowchart LR
- v01["v0.1<br/>Walking skeleton"] --> v02["v0.2<br/>First adapters"]
- v02 --> v03["v0.3<br/>Race engine"]
- v03 --> v04["v0.4<br/>Direct a single race (RD console)"]
+ v01["v0.1 ✓<br/>Walking skeleton"] --> v02["v0.2 ✓<br/>First adapters"]
+ v02 --> v03["v0.3 ✓<br/>Race engine"]
+ v03 --> v04["v0.4 ✓ RELEASED<br/>Direct a single race (v0.4.0-alpha.1)"]
v04 --> v05["v0.5<br/>Racer/spectator PWA"]
v05 --> v06["v0.6<br/>Streaming / broadcast"]
v06 --> v07["v0.7<br/>Cloud"]
@@ -96,28 +96,34 @@
-
+
2. The phases
classDef live fill:#eaf7e1,stroke:#43b301,stroke-width:2px;
class v04 live
tournaments-snapshot, rebuilt post-v1.
- As designed, v0.2 already ingests a live (dockerized) source — "shadow mode."
+ Status (2026-07-05): v0.4 is RELEASED as
+ v0.4.0-alpha.1 —
+ tagged on main, with self-contained Tauri desktop portables for
+ Linux / Windows / macOS, all carrying the live RotorHazard adapter.
+ v0.1 (walking skeleton), v0.2 (first adapters +
+ replay harness, live "shadow mode"), and v0.3 (race engine) merged
+ before it; since the primitives-first carve-out (2026-06-30, PR #327) the registry
+ ships three primitive generators (Practice / Time Trials /
+ Head-to-Head), with the tournament structures preserved on
+ tournaments-snapshot for the post-v1 rebuild.
cargo xtask version,
+ GET /about + the console build-stamp; CONTRACT_VERSION stays
+ the independent wire integer).
v0.3 — Race engine
- tournaments-snapshot with the rest of the structures — PR #327; ZippyQ built then shelved as the dynamic-generator honesty check — D10); advancement; multi-class + engine-side frequency allocation.v0.4 — Direct a single race (RD console) — vertical slices
- v0.4 — Direct a single race (RD console) — RELEASED (v0.4.0-alpha.1)
+ v0.4 — Direct a single race (RD console) — vertical slices
(auto-add + auto-bind name matches).
run_event's quali→bracket carry (#84).tournaments-snapshot (PR #327). What shipped instead is
+ hand-chained FromRanking seeding (a quali → top-N final) plus engine-level
+ FromHeatWinners advancement; the generate-full-bracket helper (#30) and bracket
+ container/viz (#31) are the rebuild's first steps, on the D13 level-per-round model.v0.4 — Direct a single race (RD console) — vertical slices
IMD-aware channel assignment — scoring a heat's channels by third-order intermod
gap and auto-picking the cleanest set (#209), a race-quality refinement on top of the v0.4 channel
model that cuts interference-related video dropouts; seasons (#129) stay local-first for now;
- multi-cloud (#132) is later. Packaging follows alongside: a Tauri shell +
- single-binary build for Windows/Linux/macOS (#57/#58).
+ multi-cloud (#132) is later. Packaging shipped with the alpha: the
+ Tauri desktop portables for Windows/Linux/macOS (#57/#58), self-contained
+ with the live RotorHazard adapter.
- In flight — between the alpha and v0.5
+
+
tournaments-snapshot on the
+ D13 level-per-round model): small fixes #32/#33 → the generate-full-bracket helper #30 → the
+ bracket container + tree viz #31; double-elim and multi-main first-class.v0.5 — Racer/spectator PWA
@@ -266,8 +290,10 @@ RH integration — a required GridFPV RotorHazard plugin
longer a post-1.0 firmware project — it is the integration path.
"Required" is a guided one-step install (Grid detects a missing plugin and
offers to install it), not a hard-fail, keeping the "try it fast" feel. The plugin becomes the
- primary RH integration; the full plugin design doc + sliced build plan are
- forthcoming (D16).
+ primary RH integration; the design doc lives at
+ RotorHazard Plugin (floor: RHAPI 1.3 / RH 4.3.0+),
+ slice 0 (the RH 4.4.0 mock harness) is built, and the released alpha integrates via the
+ live RH adapter while the plugin path lands (D16).
3. Cross-cutting workstreams
@@ -282,66 +308,39 @@ 3. Cross-cutting workstreams
Every milestone is bookended by docs: read the owning docs before breaking the
milestone into issues (so the build follows the design), and run a doc-reconciliation pass after,
before merging — fixing drift, resolving now-validated open decisions, and recording new conventions. Doc updates
- are part of the milestone's definition-of-done and ride along in the same milestone MR.
+ are part of the milestone's definition-of-done and ride along in the same PR.
- 4. The GitLab planning model (free tier)
-
-
-
-
-
-
- Plan concept Free-tier mechanism
- Phase / release Milestone (release-versioned, e.g. "v0.2 — First adapters") with start/due dates. The milestone list + dates is our timeline.
- Epic / feature A tracking issue whose description holds a task checklist of child issues ( - [ ] #123); GitLab shows % progress.
- Work item An issue, assigned to its milestone + labels, listed under a tracking issue.
- Type / area Labels by convention: type/feature, type/chore, type/bug; area/adapters, area/engine, area/protocol, area/clients, area/cloud, area/streaming, area/integrations. (Scoped-label exclusivity is paid; these are plain prefixes.)
- Workflow / status A single project issue board with columns Open → Doing → Review → Done (board lists by status label).
- Dependencies "Relates to" links (blocking/blocked-by links are paid); express ordering in the tracking issue.
-
- Timeline narrative This doc (and optionally a Wiki page) — the Roadmap timeline view is paid. 5. From plan to GitLab
-
-
+ planning → devel (which promotes to main on releases).4. Planning on GitHub
devel
+ for slices (GitFlow: devel is the working trunk, main is releases;
+ both protected — PR + CI required), and GitHub Actions for CI
+ (cargo xtask ci + the frontend job) plus the manual/main-gated
+ release-builds workflow for the desktop portables. Releases are
+ devel → main PRs, tagged (v0.4.0-alpha.1 is the first), with the
+ portables attached to the GitHub Release.
6. Open decisions
+ 5. Open decisions
planning merges into
- devel; devel promotes to main on releases (main stays
- release-stable).devel (the working trunk); devel promotes to
+ main on releases (main stays release-stable, tagged). The GitLab-era
+ planning branch is history.tournaments-snapshot on the D13
+ level-per-round model.cruwaller/rotorhazard:latest is stale at
+ Harness reality (built, S0): cruwaller/rotorhazard:latest is stale at
4.0.0-beta.4 (only :latest is published) — below our floor. So the
- harness bump (S0) is not a tag swap: we build a mock-node image ourselves from the
- RotorHazard v4.4.0 source. The MockInterface reads RSSI/history from
- a mock_data CSV, so the emulated-signal slice (S2) is testable against the mock once
- the image is current.
+ harness is not a tag swap: docker/rotorhazard/ builds a mock-node image
+ from the RotorHazard v4.4.0 source (MOCK_NODE_SIGNAL=1 required).
+ The MockInterface reads RSSI/history from a mock_data CSV, and the
+ emulated-signal slice (S2) was validated against it.
7. Build plan — five slices
leaves the socket fallback intact until S3 deletes it.
-
docker/rotorhazard/ image from RH v4.4.0 with
MockInterface + an emulated-signal CSV, and extend cargo xtask live /
rh-mock to mount the GridFPV plugin into the container's plugins/ dir.
- Unblocks testing every later slice against current RH. (A local
- rh-plugin-dev-env image already exists — fold it in.)gridfpv_mock test plugin and the race-day autopilot harness on top.
+ plugins/gridfpv/__init__.py (+ manifest.json, required_rhapi_version 1.3)
registering gridfpv_hello via socket_listen (version/capabilities). Grid-side: the handshake probe, the
required-with-guided-install flow (§5), and the adapter learning to prefer the
plugin transport (socket fallback retained). Ships: Grid recognizes a plugin-equipped RH and walks
- a missing one through install.GET /plugin/gridfpv.zip (always matches the
+ running Director); the handshake replies via a separate gridfpv_hello_ack event
+ (not a socket.io ack) carrying protocol_version (checked for exact
+ equality) and prestage_secs_was.
+ gridfpv_signal channel (current_rssi heartbeat +
per-pass history_values window); Grid consumes it as the pass's signal context and the
heat's trace. Retires the path-2 save_laps→load_data→get_pilotrace
- pull. Validated against the emulated-signal harness. Headline marshaling-fidelity win.gridfpv_signal is incremental
+ (per-node base index, append-vs-replace, a final full flush on race
+ stop, a 20k-sample window cap, 2 Hz decimation) — which resolves risk #5 (volume). The
+ save-then-pull is suppressed while live signal is active and retained as the stock-RH
+ fallback.
+ seat (via rhapi.db), start
(race.stage()), stop (race.stop()), and emits passes from
- RACE_LAP_RECORDED attributed by node seat. Grid still owns the timing. Deletes the
- alter_race_format staging-zero, the add_pilot/alter_heat
- dance, and the prestage fight. The socket fallback's workaround code is removed here.RACE_LAP_RECORDED attributed by node seat. Grid still owns the timing. Status: live_pass (native RACE_LAP_RECORDED passes, deduped against
+ current_laps) shipped; the prestage zeroing shipped
+ (zero_prestage_pad). Clean start/stop has not — the Stage-time
+ socket prepare/seat flow (staging-zero, seating dance) is still the production control path,
+ recently hardened rather than deleted (effective-format re-prepare, 5 s stage retry,
+ per-arming pass gating). Deleting those workarounds is this slice's remaining work.
+ recalculate that replays detection over the stored dense trace at marshal-chosen
enter/exit levels and proposes lap changes (Grid/marshal commits; RH truth is not mutated
under the RD), plus an optional gridfpv_calibrate write-back via
frequencyset_alter. The marshaling graph gets draggable thresholds over the captured
- trace. The payoff feature owning-the-stack was for — delivered on RH's detection.gridfpv_calibrate write-back remains future.
@@ -308,20 +332,22 @@
8. Risks & open decisions
an RH plugin). Any future non-RH timer is a protocol adapter, not a plugin host, and the
canonical event model already provides that seam — so we build concretely for RH and do not design
a speculative cross-RMS plugin protocol.
- RACE_LAP_RECORDED attributes by node seat regardless of RH's seated-pilot gate (so we
- need no socket-era seating just to make passes count), or seat via rhapi.db if RH
- still gates.history_values can be large; bound the streamed window and
- decimate the heartbeat so the broadcast stays cheap on a Pi.RACE_LAP_RECORDED
+ only fires for laps RH records — so seating remains required even with live_pass.
+ The Stage-time socket seating stays until S3 replaces it with in-plugin
+ rhapi.db seating.redetect.ts) over the captured trace — preview-only,
+ committed through the ordinary marshaling rulings. In-plugin recalc is off the table.base-indexed appends, a 20k-sample window cap, 2 Hz decimation, and a
+ full flush only at race stop.3.1 How it connects
3.2 Mapping race-state to scenes
staged, armed, started (running),
- landed, scored, advance. A scene
+ Staged, Armed, Running,
+ Finished (results pending), Finalized (results final),
+ Advanced. A scene
map associates phases of that lifecycle with OBS scenes; as race-state changes,
the subsystem requests the mapped scene.
3.2 Mapping race-state to scenes
- Race-state event Typical broadcast intent Default mapping
- stagedfield is on the line; show who's up Staging / grid scene
- armedcountdown imminent (hold staging, arm lower-third)
- started (running)race is live Race / split-screen scene
- landedflying done, result pending (hold race, results lower-third)
+ scoredresult is final Results / replay scene
+ Stagedfield is on the line; show who's up Staging / grid scene
+ Armedcountdown imminent (hold staging, arm lower-third)
+ Runningrace live Live race scene
+ Finishedflying done, result pending (Unofficial) (hold race, results lower-third) Finalizedresult is official (Final) Results / replay scene
@@ -300,7 +301,7 @@ advancemoving to next heat / podium Bracket or podium scene 5. Overlay catalogue & theming
video. Heat-bound; reacts to running passes.
scored/advance.Finalized/Advanced.
advance and updated standings.2. Adapters — golden cases over pure translators
A recorded Velocidrone WebSocket message stream Velocidrone The exact ordered Pass / lifecycle / CompetitorSeen events, with split indices and source timestamps
- A recorded RotorHazard Socket.IO frame log RotorHazard Passes with per-node signal context attached, calibration/lifecycle events
- A recorded LapRF binary serial capture LapRF Passes from PassingRecord + RSSI stats, device-clock timestamps
+ A scripted sequence of manual UI entries Manual Passes with wall-clock timestamps, no signal/calibration
+ A recorded LapRF binary serial capture LapRF Passes from PassingRecord + RSSI stats, device-clock timestamps (aspirational — no LapRF adapter exists)A scripted sequence of manual UI entries Manual Passes with wall-clock timestamps, no signal/calibration (aspirational — no manual adapter exists) 5.1 Emulated-signal races — driving RH's real pipeline
canonical-log fixtures (§4).
5.2 The interactive mock-signal harness —
+ cargo xtask rh-mock5.2 The plugin-era hands-on harnesses
+ docker/rotorhazard/ builds RH v4.4.0 from source
+ with the gridfpv plugin plus the test-only gridfpv_mock plugin, and
+ cargo xtask race-day runs the autopilot (scenario personalities
+ clean / varied / messy / pack) that emulates the seated nodes whenever the
+ Director stages a heat — see mock-race-day. The older
+ rh-mock harness below remains for low-level signal poking.
+ 5.3 The interactive mock-signal harness —
cargo xtask rh-mock5.2 The interactive mock-signal harness —
cargo xtask rh-mock
# a heat left only Staged never races, so it records no laps).
# 4. After the heat, dump the captured signal values for that heat:
-cargo xtask rh-mock dump http://localhost:3000 practice q-1
+cargo xtask rh-mock dump http://localhost:8080 practice q-1
7. Testing the format generators
assert exactly who moves up, who is eliminated, and the seeds of the next heats.
tournaments-snapshot — the shipping registry is the three primitives)
(Race Engine §3) — a table that adds rounds on demand and
asserts the generator produces sensible next heats from current state, never a
precomputed schedule.2. The canonical event model
player name, transponder id), later bound to a GridFPV pilot.
8. Other sources (sketch)
not connect only while a heat runs); a persistent driver maintains the link and
reconnects with backoff, surfacing a TimerStatus of
Ready / Configured / Connecting / Connected / Disconnected / Error. RH support is
- gated behind the Director's --features live build. When a heat enters
- Running, the bridge arms the heat (remapping node seats onto the lineup) and tunes the
- nodes; stage_race() stops any prior race, discards old laps, stages, and waits up
- to ~15 s for RH to reach racing; leaving Running disarms the race but
- keeps the connection alive.
+ gated behind the Director's --features live build. At the heat's Stage
+ transition the bridge prepares RH (tunes nodes, zeroes the current format's
+ staging delays for an instant start, resets to READY) and seats a fresh RH
+ heat with the bound pilots (RH's pass gate rejects crossings on unseated nodes); at
+ Armed → Running — Grid's go — it emits a single stage_race.
+ Because seating can switch RH's effective format to the seated heat's class format,
+ the instant-start prepare is re-run right before the stage; a stage a busy RH
+ drops ("status is not 'ready'") is retried every 5 s inside the ~15 s
+ settle; and lap records are gated on RH confirming RACING for this arming, so
+ a replayed previous-race snapshot can never contaminate the new heat. Every (re)connect also
+ probes the GridFPV plugin handshake (missing / present / incompatible,
+ surfaced on the timer row). With multiple timers selected, all stay hot-standby-connected and
+ only the active source's passes feed the log — primary-preferred, non-sticky failover (#112);
+ the switchover window is not yet buffered (a known deferred gap). Leaving Running
+ disarms the race but keeps the connection alive.
8. Other sources (sketch)
marshaling feature). It reuses RH's proven stack (node firmware + RMS + detection); GridFPV
owns start/stop timing while RH owns start/stop/get-data. "Required" is a guided
one-step install, not a hard-fail. The plugin supersedes the socket
- workarounds and becomes the primary RH integration; its full design
- + build plan are forthcoming separate work (this records the direction). It also
+ workarounds and becomes the primary RH integration. It also
replaces the dropped own-the-stack timer-server (D15,
superseded).
+ plugins/gridfpv/), is served by the Director
+ at GET /plugin/gridfpv.zip (embedded at compile time so the bundle always matches
+ the Director), and installs via the guided one-step flow. Shipped capabilities:
+ hello (versioned handshake), live_signal (incremental dense RSSI,
+ suppressing the save-then-pull), live_pass (native per-node passes, deduped
+ against current_laps). Clean start/stop ownership and calibration write-back are
+ the remaining slices; until then the Stage-time socket prepare/seat flow above IS the
+ production control path.
+
8. Other sources (sketch)
the session start/end edges, and the per-node lap_number is carried as the pass
sequence and anchors dedup. Built in parallel with Velocidrone as the first real-world
target; the dockerized RH both feeds recorded fixtures and is allowed for live
- integration tests (a local class — Docker required). The harness runs
- cruwaller/rotorhazard with 8 mock nodes on port 5000,
+ integration tests (a local class — Docker required). The harness builds
+ RotorHazard v4.4.0 from source (the cruwaller image went stale at
+ 4.0.0-beta.4; MOCK_NODE_SIGNAL=1 required) with 8 mock nodes,
attached via the timer's URL; the test driver emits stage_race over Socket.IO and
injects passes with simulate_lap {node}, bypassing signal detection (see
docker/rotorhazard/). The guardrail bans
@@ -356,8 +382,10 @@ 9. Open decisions
is retained as an event-local sidecar — not pushed to the Cloud by
default — so a missed lap can be re-derived. Pairs with the storage tiers in
Architecture §4. Implemented today: a per-pass
- rssi_peak; the enter/exit window and bounded-RSSI sidecar arrive with the
- hardware-signal extension.rssi_peak, coarse streamed SignalChunk traces +
+ SignalThresholds (enter/exit levels), and the dense per-tick
+ SignalHistory — live via the plugin's gridfpv_signal, or the
+ post-race save-then-pull on stock RH.
Pass carries a gate index. A lap is a
start/finish-gate crossing and splits are sector times; the default is a single gate.Vision
the principles we will not violate. It deliberately stays at the level of
intent. Architecture, the timer-adapter spec, the data model, and the phase
plan live in their own docs (see Architecture, Timer Adapters,
- and Roadmap — coming soon). When this doc and an architecture
+ and Roadmap). When this doc and an architecture
decision disagree, the principles here win until we deliberately change them.
+ Status at v0.4.0-alpha.1 (2026-07-05): setup/registration, qualifying
+ (Time Trials), live heats with race audio, deep marshaling, and results/standings are
+ shipped — as native desktop portables for Linux/Windows/macOS with the
+ live RotorHazard adapter. Bracket builders are deliberately deferred
+ (primitives-first carve, rebuilt post-v1 from tournaments-snapshot; hand-chained
+ seeding covers a quali → top-N final today). Streaming/overlays, the cloud tier, and league
+ push-back remain future (v0.6+).
+
Two kinds of racing are equal first-class citizens by design, not bolt-ons: real-world (IRL) racing through physical timers, and @@ -209,15 +220,25 @@
Filling the gap doesn't excuse us from the basics. To be taken seriously, GridFPV - has to match what race directors already expect: a guided setup wizard; the common - bracket and qualifying formats (round robin, FIA-style single/double elimination); - automated advancement, seeding, rotation, and frequency management; championships - with automatic standings; live marshaling; and heat/channel push notifications. - We are honest that the incumbents lead on some of this polish today — closing that - gap, in the open, is the work. + has to match what race directors already expect. Where that stands at the alpha: + the guided setup wizard, seeding/advancement, frequency management, and + live marshaling ship — and marshaling has become a + differentiator, not a table stake: signal-as-evidence RSSI review, + re-detection with tune-commit, the shared removal record, a native min-lap floor, and a + full audited ruling model that no incumbent matches. The incumbents still lead on the + bracket and qualifying structures (round robin, FIA-style single/double + elimination — ours are deferred by decision, rebuilt post-v1), championships with + automatic standings, and heat/channel push notifications. + Closing those, in the open, is the work.
Every other FPV timing system treats results as disposable — corrections overwrite data, nothing records who changed what, and "official" is a verbal convention. GridFPV is the