diff --git a/CHANGELOG.md b/CHANGELOG.md index 24f8e2a..f06d6fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -263,6 +263,21 @@ The project follows Semantic Versioning once the first firmware release is tagge ### Changed +- the **GPS session trigger starts on the start line** rather than the pit exit, which is + what keeps it when the pit gates go. The start line is crossed every lap, where pit exit + on a road loop was a line somebody picked; and on a circuit whose start and finish are + one line, the session and lap 1 begin on the same crossing. `pit_exit_auto_start_enabled` + is renamed `gps_start_line_enabled` - the stored byte and its meaning are unchanged, so + the settings record needs **no version bump and no migration**. The byte that held + pit-entry auto-stop is kept as reserved and written as zero, because reclaiming it would + change a fixed-width record's length for a setting nothing uses +- a start-line crossing starts a session **only while armed**. Without that, driving out of + the paddock would begin one: the pit-exit gate was obscure enough that a crossing implied + intent, and the start line is not. `GateAutomationResult::ignored_unarmed` reports it, + because the remedy differs from every other refusal +- `SETUP` names the rest period **`REST`**, not `PIT REST`: it is time between sessions, + spent wherever the driver likes + - the G radar's trace is back to **1.2 seconds**, reverting an increase to three that was merged on a bad measurement and then measured properly. Both figures below are with the device untouched, which is when the meter reaches `ready` and the trace is actually @@ -303,8 +318,31 @@ The project follows Semantic Versioning once the first firmware release is tagge 25 Hz, so while the receiver ran at 20 its 50 ms intervals sat 10 ms under a 60 ms gap threshold rather than the 20 ms the 1.5x multiple was chosen to give. All three now agree +### Removed + +- **pit lane entry and exit, entirely**: the two timing gates, the settings behind them, + the session automation's stop path, the capture steps, the gate fields in a track + definition, and their documentation. Too much complexity for what they earned +- `TimingGate` is `start` and `finish`, and `kTimingGateCount` is 2. A track definition's + `CircuitGateDefinitions` loses both pit gates, which takes the **track schema to + version 4**. A schema-3 file still loads - its pit gates are parsed and discarded - + because the 26 circuits already written to cards are otherwise correct and rejecting + them would cost a device its whole track list for gates nothing reads +- gate capture walks **two gates rather than four** +- `SessionController::stop_from_pit_entry` and `CompletionReason::pit_entry`. The log + format's `SessionCompletionReason::pit_entry` becomes `reserved_pit_entry`: it is kept, + not produced, so sessions already recorded on cards still decode to a reason the reader + knows +- `pit_entry_auto_stop_enabled`. Stopping has no start-line equivalent and gains none - + stopping on the start line would end a session after one lap - so stopping stays the + driver's + ### Fixed +- `SettingsField` gained `acceleration_target` without `kSettingsFieldCount` or the name + array following it, so naming that field read past the end of the array. Found while + removing the two pit entries from the same list + - the ready dashboard **reports the receiver as it is now, not as it was at boot**. `ready_snapshot` read the health live and correctly, but `refresh_ready` was only reached from boot, a settings save and a track change, and the receiver acquires its diff --git a/data/track-packs/test/manifest.json b/data/track-packs/test/manifest.json index 1eafe31..0dfc2b5 100644 --- a/data/track-packs/test/manifest.json +++ b/data/track-packs/test/manifest.json @@ -142,19 +142,7 @@ 34.0, 12 ], - "pit_entry": [ - 54.170325, - -4.447392, - 149.9, - 12 - ], - "pit_exit": [ - 54.169953, - -4.446944, - 147.3, - 12 - ], - "derivation": "Start and finish sit 30 m back along King Edward Road from the Sea Cliff Road junction, measured along the OSM centreline, because the two legs meet at the junction itself and a gate there would span both. At 30 m back the outbound leg is 26.3 m away, leaving 16.5 m between the gate end and its carriageway. Gates are 12 m: 7.62 m of road plus 2.19 m beyond each edge for lateral fix error. pit_exit faces 147.3 deg south-east-bound so the north-bound pass into the pit lane does not register.", + "derivation": "Start and finish sit 30 m back along King Edward Road from the Sea Cliff Road junction, measured along the OSM centreline, because the two legs meet at the junction itself and a gate there would span both. At 30 m back the outbound leg is 26.3 m away, leaving 16.5 m between the gate end and its carriageway. Gates are 12 m: 7.62 m of road plus 2.19 m beyond each edge for lateral fix error.3 deg south-east-bound so the north-bound pass into the pit lane does not register.", "validation": { "geometry_status": "independently_validated", "verified_utc": "2026-10-17T00:00:00Z", diff --git a/data/track-packs/uk/manifest.json b/data/track-packs/uk/manifest.json index 6cba300..d76c549 100644 --- a/data/track-packs/uk/manifest.json +++ b/data/track-packs/uk/manifest.json @@ -53,19 +53,19 @@ "track_id": "gb_anglesey_international_gp", "name": "Anglesey International GP", "minimum_lap_time_s": 35, - "blocker": "No redistributable public four-gate geometry with an identifiable pit lane was found; physical capture is required." + "blocker": "No redistributable public start/finish geometry with an identifiable pit lane to locate it was found; physical capture is required." }, { "track_id": "gb_anglesey_coastal", "name": "Anglesey Coastal", "minimum_lap_time_s": 25, - "blocker": "No redistributable public four-gate geometry with an identifiable pit lane was found; physical capture is required." + "blocker": "No redistributable public start/finish geometry with an identifiable pit lane to locate it was found; physical capture is required." }, { "track_id": "gb_anglesey_national", "name": "Anglesey National", "minimum_lap_time_s": 20, - "blocker": "No redistributable public four-gate geometry with an identifiable pit lane was found; physical capture is required." + "blocker": "No redistributable public start/finish geometry with an identifiable pit lane to locate it was found; physical capture is required." } ] }, @@ -138,18 +138,6 @@ 0.2585922, 61.346, 20 - ], - "pit_entry": [ - 51.3580132, - 0.2563892, - 20.779, - 12 - ], - "pit_exit": [ - 51.3608626, - 0.2625725, - 69.517, - 12 ] } }, @@ -225,18 +213,6 @@ -0.0590716, 206.012, 20 - ], - "pit_entry": [ - 53.3115369, - -0.0583758, - 223.313, - 12 - ], - "pit_exit": [ - 53.3093935, - -0.0600911, - 191.183, - 12 ] } }, @@ -300,18 +276,6 @@ -1.5557027, 15.0, 20 - ], - "pit_entry": [ - 54.4537362, - -1.5563133, - 359.691, - 12 - ], - "pit_exit": [ - 54.4569957, - -1.5548089, - 17.677, - 12 ] } }, @@ -354,18 +318,6 @@ 280.5, 20 ], - "pit_entry": [ - 52.8295427, - -1.3769056, - 275.267, - 12 - ], - "pit_exit": [ - 52.8303222, - -1.383843, - 287.314, - 12 - ], "validation": { "geometry_status": "independently_validated", "verified_utc": "2026-10-10T00:00:00Z", @@ -433,18 +385,6 @@ -0.7528623, 174.634, 20 - ], - "pit_entry": [ - 50.8597195, - -0.752909, - 189.253, - 12 - ], - "pit_exit": [ - 50.8578512, - -0.7526299, - 168.863, - 12 ] } }, @@ -508,18 +448,6 @@ -3.506877, 95.28, 20 - ], - "pit_entry": [ - 56.1310537, - -3.5095813, - 85.85, - 12 - ], - "pit_exit": [ - 56.130658, - -3.50333, - 102.862, - 12 ] }, "anti_clockwise": { @@ -538,18 +466,6 @@ -3.506877, 275.28, 20 - ], - "pit_entry": [ - 56.130658, - -3.50333, - 282.862, - 12 - ], - "pit_exit": [ - 56.1310537, - -3.5095813, - 265.85, - 12 ] } }, @@ -603,18 +519,6 @@ -3.4964344, 89.5, 20 - ], - "pit_entry": [ - 51.4324616, - -3.4977578, - 36.977, - 12 - ], - "pit_exit": [ - 51.4327333, - -3.4950303, - 95.934, - 12 ] } }, @@ -656,18 +560,6 @@ -1.3369314, 178.476, 20 - ], - "pit_entry": [ - 52.5984766, - -1.3369659, - 189.05, - 12 - ], - "pit_exit": [ - 52.5963928, - -1.3368746, - 173.576, - 12 ] } }, @@ -709,18 +601,6 @@ -2.6124325, 165.415, 20 - ], - "pit_entry": [ - 53.1813678, - -2.6130318, - 192.458, - 12 - ], - "pit_exit": [ - 53.1769784, - -2.6134167, - 223.746, - 12 ] } }, @@ -774,18 +654,6 @@ -4.3238773, 29.8, 20 - ], - "pit_entry": [ - 51.7030618, - -4.3231733, - 318.475, - 12 - ], - "pit_exit": [ - 51.7070317, - -4.3228469, - 10.29, - 12 ] } }, @@ -833,18 +701,6 @@ -1.0219595, 37.3, 20 - ], - "pit_entry": [ - 52.0648411, - -1.0196927, - 329.277, - 12 - ], - "pit_exit": [ - 52.0714672, - -1.0157059, - 45.231, - 12 ] }, "national_pits": { @@ -863,18 +719,6 @@ -1.0152726, 84.9, 20 - ], - "pit_entry": [ - 52.0786532, - -1.0178209, - 84.499, - 12 - ], - "pit_exit": [ - 52.0789308, - -1.0127209, - 85.028, - 12 ] } }, @@ -928,18 +772,6 @@ 0.9473904, 265.045, 20 - ], - "pit_entry": [ - 52.4638053, - 0.9497182, - 263.823, - 12 - ], - "pit_exit": [ - 52.4631623, - 0.9400147, - 262.945, - 12 ] } }, @@ -993,18 +825,6 @@ -1.6094029, 343.482, 20 - ], - "pit_entry": [ - 51.2064456, - -1.6075329, - 327.493, - 12 - ], - "pit_exit": [ - 51.2100543, - -1.6084646, - 32.583, - 12 ] } }, diff --git a/data/tracks/README.md b/data/tracks/README.md index a6e7f55..af93759 100644 --- a/data/tracks/README.md +++ b/data/tracks/README.md @@ -4,7 +4,7 @@ Firmware accepts schema version 3 definitions up to 16384 bytes. Track identifiers must fit the persisted 47-character identifier field and use letters, digits, `.`, `_`, or -`-`. Every layout must provide directed `start`, `finish`, `pit_entry`, and `pit_exit` +`-`. Every layout must provide directed `start` and `finish` gates plus revision and provenance metadata. Sector records use the same directed-gate structure. Each endpoint pair must form a sensible line inside the track geofence after local projection; start and finish may intentionally contain identical geometry. Run diff --git a/data/tracks/schema.json b/data/tracks/schema.json index f32f768..95f6445 100644 --- a/data/tracks/schema.json +++ b/data/tracks/schema.json @@ -18,7 +18,7 @@ ], "properties": { "schema_version": { - "const": 3 + "const": 4 }, "revision": { "type": "integer", @@ -80,9 +80,7 @@ "additionalProperties": false, "required": [ "start", - "finish", - "pit_entry", - "pit_exit" + "finish" ], "properties": { "start": { @@ -90,12 +88,6 @@ }, "finish": { "$ref": "#/$defs/directed_gate" - }, - "pit_entry": { - "$ref": "#/$defs/directed_gate" - }, - "pit_exit": { - "$ref": "#/$defs/directed_gate" } } }, diff --git a/data/tracks/synthetic_test_loop.json b/data/tracks/synthetic_test_loop.json index 54e71e5..460ae8b 100644 --- a/data/tracks/synthetic_test_loop.json +++ b/data/tracks/synthetic_test_loop.json @@ -1,5 +1,5 @@ { - "schema_version": 3, + "schema_version": 4, "revision": 1, "track_id": "synthetic_test_loop", "name": "Synthetic Test Loop", @@ -47,34 +47,6 @@ "heading_tolerance_deg": 60.0, "minimum_crossing_speed_mps": 2.0, "rearm_corridor_m": 15.0 - }, - "pit_entry": { - "left": { - "lat_deg": 52.00015, - "lon_deg": -1.00004 - }, - "right": { - "lat_deg": 52.00005, - "lon_deg": -1.0 - }, - "direction_heading_deg": 90.0, - "heading_tolerance_deg": 60.0, - "minimum_crossing_speed_mps": 1.0, - "rearm_corridor_m": 10.0 - }, - "pit_exit": { - "left": { - "lat_deg": 51.99995, - "lon_deg": -1.0 - }, - "right": { - "lat_deg": 51.99985, - "lon_deg": -0.99996 - }, - "direction_heading_deg": 90.0, - "heading_tolerance_deg": 60.0, - "minimum_crossing_speed_mps": 1.0, - "rearm_corridor_m": 10.0 } }, "timing": { diff --git a/docs/GNSS_AND_LAP_TIMING.md b/docs/GNSS_AND_LAP_TIMING.md index d1aed79..ee3fca3 100644 --- a/docs/GNSS_AND_LAP_TIMING.md +++ b/docs/GNSS_AND_LAP_TIMING.md @@ -97,7 +97,9 @@ target timing task remains part of embedded parity issue #25. ## 5. Circuit gate definitions -A track file stores four directed gates (`start`, `finish`, `pit_entry`, and `pit_exit`). +A track file stores two directed gates (`start` and `finish`). It carried `pit_entry` and +`pit_exit` until the pit lane was removed; a schema-3 file still loads, with those two +parsed and discarded. Each gate stores: - left endpoint @@ -178,7 +180,7 @@ The production validator: Heading interpolation and comparison use the shortest circular difference, so `359` degrees to `1` degree is a two-degree change rather than 358 degrees. -Direction is essential for circuits where pit lanes, access roads or nearby loops pass close to the line. +Direction is essential for circuits where access roads or nearby loops pass close to the line. ## 9. Hysteresis and duplicate suppression @@ -243,14 +245,14 @@ dead-reckoning and time-only fixes are never silently treated as position fixes. Each geometric candidate produces one stable named validation result and records the gate type, two source sequence numbers, intersection fraction, interpolated -receiver time, and rejection stage. Start, finish, pit entry, and pit exit use the -same validation and interpolation path with independent corridor rearm. Selecting +receiver time, and rejection stage. Start and finish use the same validation and +interpolation path with independent corridor rearm. Selecting start or finish changes only which typed event feeds the repeating lap state machine. The persisted Lap Line setting is converted into `TimingEngineConfig` when the selected -track is loaded. Independently enabled pit automation consumes only accepted typed -events outside the timing task: pit exit may start a Ready session, while pit entry may -stop a Running or Overtime session. Wrong-state, disabled, rejected, and duplicate -records never transition the session, and manual controls remain separate. +track is loaded. The session automation consumes only accepted typed events outside the +timing task: a start-line crossing may start a Ready session when the GPS trigger is +selected and the session is armed. Wrong-state, disabled, unarmed, rejected and duplicate +records never transition the session, and stopping remains the driver's. Together with the raw GNSS rows, applied quality thresholds, and fingerprinted track definition, the bounded records make every acceptance or rejection reproducible during offline replay. @@ -276,7 +278,7 @@ patterns. Both paths must emit exactly three laps and identical typed gate recor with receiver-time timestamps, while every measured host processing call remains below the next-fix deadline. Separate 20/25 Hz integration vectors cover finite-line endpoints, on-line ownership, parallel and reverse travel, jitter/rearm, large steps, -slow pit movement, stale timing, and poor fixes. Physical outdoor and +slow movement, stale timing, and poor fixes. Physical outdoor and controlled-crossing validation is tracked separately in GitHub issue #53 for when hardware is available. diff --git a/docs/SETTINGS.md b/docs/SETTINGS.md index 75f68dc..173ca48 100644 --- a/docs/SETTINGS.md +++ b/docs/SETTINGS.md @@ -16,7 +16,6 @@ Version 8 stores: - the 0-X target speed - Trackday Mode enable/disable state - repeating lap boundary (`START LINE` or `FINISH LINE`) -- independent Pit Exit Auto-Start and Pit Entry Auto-Stop enable/disable states - fixed or automatic orientation - stationary auto-dim preference - selected local track identifier @@ -29,7 +28,7 @@ persisted. - A valid version 4 record loads directly. - Versions 1, 2, and 3 are migrated explicitly, with new fields receiving documented safe - defaults, then rewritten atomically. Trackday Mode and both pit automations default to + defaults, then rewritten atomically. Trackday Mode defaults to disabled during migration; the lap boundary defaults to Finish. - Missing, corrupt, invalid, or unsupported records are never reinterpreted. Safe defaults are used and a current record is written when storage permits. @@ -57,12 +56,13 @@ The effective value is frozen when a session starts. When enabled, active presen is limited to session countdown and estimated laps remaining; lap detection and logging continue, with lap results available through Review only after the session stops. -Lap Line explicitly selects Start or Finish as the repeating lap boundary. Pit Exit -Start and Pit Entry Stop are independent and default to disabled. When enabled, only an -accepted, directed, debounced typed gate event can act: pit exit starts from Ready, and -pit entry stops from Running or Overtime with `PIT ENTRY` recorded as the completion -reason. Rejected, duplicate, disabled, and wrong-state events are no-ops. Manual Start -and the guarded manual Stop flow remain available regardless of these settings. +Lap Line explicitly selects Start or Finish as the repeating lap boundary. Starting a +session on the start line is not a setting of its own: it is what the GPS session +trigger means, so the trigger owns it and offering the same behaviour twice cannot make +the two disagree. Only an accepted, directed, debounced typed gate event can act, and +only while the session is armed - the start line is crossed on the way to anywhere, so +an unarmed crossing must not begin a session. Stopping stays the driver's: there is no +start-line equivalent, because stopping there would end the session after one lap. Edits remain a draft until Save. Cancel discards the complete draft, and Restore Defaults requires a separate confirmation before staging defaults for Save. Restoring diff --git a/docs/TEST_PLAN.md b/docs/TEST_PLAN.md index 59232bf..d19c54f 100644 --- a/docs/TEST_PLAN.md +++ b/docs/TEST_PLAN.md @@ -102,7 +102,6 @@ Required synthetic cases: 10. consecutive valid crossings inside minimum lap time 11. correct crossing after leaving rearm corridor 12. start versus finish lap-boundary selection with identical typed gate events -13. independent pit-entry and pit-exit rearm at slow pit-lane speed 14. 20/25 Hz high-speed segments spanning a finite gate 15. accepted and rejected typed gate records agree in host and embedded replay 16. provisional definitions cannot be selected or converted to a valid timing-engine configuration diff --git a/docs/TRACK_DATABASE.md b/docs/TRACK_DATABASE.md index 0644e40..f473cca 100644 --- a/docs/TRACK_DATABASE.md +++ b/docs/TRACK_DATABASE.md @@ -15,7 +15,7 @@ Schema version 3 fields: ```json { - "schema_version": 3, + "schema_version": 4, "revision": 1, "track_id": "example_circuit", "name": "Example Circuit", @@ -51,22 +51,6 @@ Schema version 3 fields: "heading_tolerance_deg": 60.0, "minimum_crossing_speed_mps": 2.0, "rearm_corridor_m": 15.0 - }, - "pit_entry": { - "left": {"lat_deg": 52.000110, "lon_deg": -1.000020}, - "right": {"lat_deg": 52.000090, "lon_deg": -0.999980}, - "direction_heading_deg": 90.0, - "heading_tolerance_deg": 60.0, - "minimum_crossing_speed_mps": 1.0, - "rearm_corridor_m": 10.0 - }, - "pit_exit": { - "left": {"lat_deg": 51.999910, "lon_deg": -1.000020}, - "right": {"lat_deg": 51.999890, "lon_deg": -0.999980}, - "direction_heading_deg": 90.0, - "heading_tolerance_deg": 60.0, - "minimum_crossing_speed_mps": 1.0, - "rearm_corridor_m": 10.0 } }, "timing": { @@ -172,7 +156,7 @@ showed that a wrong name means a wrong gate. The reliable signal is the pit lane on average. The pit straight is the way the pit lane runs beside for most of its length, at a consistent short distance. 3. Take the direction of travel from the straight's own geometry, and resolve its sign - against the recorded pit entry and exit headings. + against the direction the pit lane runs. Donington allowed a shortcut - the pit lane shared *both* endpoints with the Wheatcroft Straight - but that only works where the straight is mapped as a single way. Where a @@ -197,9 +181,10 @@ lane runs beside, which is robust rather than surveyed. What is *not* benign is a gate at the junction where the preceding corner meets the straight, where heading changes fastest and the direction check is least reliable. Two gates in this pack sat exactly there, because their coordinates had been copied from the -pit entry. `tools/build_track_pack.py` now refuses a start gate within -`MINIMUM_START_PIT_SEPARATION_M` of a pit gate: a real start line and a pit gate can be -close, but they are never the same point. +pit entry gate. That check - a minimum separation between a start gate and a pit gate - +went out with the pit gates themselves, so the derivation's own placement rule is what +now prevents it: the midpoint of the stretch the pit lane runs beside, not either end of +it. ## 3. Track matching @@ -422,7 +407,10 @@ silently truncated. ## 7. Loading, projection, and versioning -Every track file includes `schema_version`; the current loader accepts exactly version 3. +Every track file includes `schema_version`; the current loader accepts version 4, and +version 3 alongside it. A 3 differs only by carrying `pit_entry` and `pit_exit` gates, +which the parser consumes and discards, so the circuits already written to cards keep +loading rather than a device losing its whole track list. Unsupported versions, malformed JSON, missing fields, out-of-range geometry, capacity overflow, unknown members, duplicate sector identifiers, gates outside the geofence, and implausible gate length/direction are rejected. Reports include a stable field path, diff --git a/firmware/components/logger/formats.cpp b/firmware/components/logger/formats.cpp index 4a652c9..ea54f56 100644 --- a/firmware/components/logger/formats.cpp +++ b/firmware/components/logger/formats.cpp @@ -90,7 +90,7 @@ bool valid_completion_reason(const SessionCompletionReason value) noexcept case SessionCompletionReason::none: case SessionCompletionReason::driver_stop: case SessionCompletionReason::reset_recovery: - case SessionCompletionReason::pit_entry: + case SessionCompletionReason::reserved_pit_entry: return true; } return false; @@ -126,8 +126,7 @@ bool valid_settings_record(const SessionSettingsV1& record) noexcept source.lower_display = record.lower_display; source.trackday_mode_enabled = record.trackday_mode_enabled; source.lap_boundary = record.lap_boundary; - source.pit_exit_auto_start_enabled = record.pit_exit_auto_start_enabled; - source.pit_entry_auto_stop_enabled = record.pit_entry_auto_stop_enabled; + source.gps_start_line_enabled = record.gps_start_line_enabled; source.selected_track_id = record.selected_track_id; return settings::valid_settings(source); } @@ -149,8 +148,7 @@ SessionSettingsV1 make_session_settings(const settings::DeviceSettings& source) result.lower_display = source.lower_display; result.trackday_mode_enabled = source.trackday_mode_enabled; result.lap_boundary = source.lap_boundary; - result.pit_exit_auto_start_enabled = source.pit_exit_auto_start_enabled; - result.pit_entry_auto_stop_enabled = source.pit_entry_auto_stop_enabled; + result.gps_start_line_enabled = source.gps_start_line_enabled; result.selected_track_id = source.selected_track_id; return result; } diff --git a/firmware/components/logger/include/track_timer/logger/formats.hpp b/firmware/components/logger/include/track_timer/logger/formats.hpp index d0d2e66..98a6bed 100644 --- a/firmware/components/logger/include/track_timer/logger/formats.hpp +++ b/firmware/components/logger/include/track_timer/logger/formats.hpp @@ -68,7 +68,10 @@ enum class SessionCompletionReason : std::uint8_t { none, driver_stop, reset_recovery, - pit_entry, + // Retained as a reserved value, not produced. Pit-entry auto-stop is gone, and the + // enumerator stays so that sessions already recorded on cards still decode to a + // reason the reader knows rather than failing validation. + reserved_pit_entry, }; enum class SummaryIntegrity : std::uint8_t { @@ -99,8 +102,12 @@ struct SessionSettingsV1 { settings::LowerDisplayMode lower_display{settings::LowerDisplayMode::elapsed}; bool trackday_mode_enabled{false}; settings::LapBoundaryMode lap_boundary{settings::LapBoundaryMode::finish}; - bool pit_exit_auto_start_enabled{false}; - bool pit_entry_auto_stop_enabled{false}; + bool gps_start_line_enabled{false}; + // Was pit_entry_auto_stop_enabled. Retained as a reserved byte rather than removed: + // this record is fixed-width and written to the card, so dropping a field would + // change its length and take the log format with it, for a setting that no longer + // exists. Always written as false and never read back. + bool reserved_pit_entry_auto_stop{false}; std::array selected_track_id{}; }; diff --git a/firmware/components/session/component.cpp b/firmware/components/session/component.cpp index 10dbcde..a001e11 100644 --- a/firmware/components/session/component.cpp +++ b/firmware/components/session/component.cpp @@ -144,28 +144,6 @@ TransitionResult SessionController::confirm_stop(const std::int64_t now_ms) noex return TransitionResult::invalid_state; } -TransitionResult SessionController::stop_from_pit_entry( - const std::int64_t now_ms) noexcept -{ - if (snapshot_.state != SessionState::running && - snapshot_.state != SessionState::overtime) { - return TransitionResult::invalid_state; - } - if (!accept_time(now_ms)) { - return TransitionResult::non_monotonic_time; - } - update_for_time(now_ms); - if (snapshot_.state != SessionState::running && - snapshot_.state != SessionState::overtime) { - return TransitionResult::invalid_state; - } - snapshot_.stop_confirmation_pending = false; - refresh_session_times(now_ms); - snapshot_.completion_reason = CompletionReason::pit_entry; - transition_to(SessionState::review, now_ms); - return TransitionResult::accepted; -} - TransitionResult SessionController::complete_review(const std::int64_t now_ms) noexcept { if (snapshot_.state != SessionState::review) { diff --git a/firmware/components/session/gate_automation.cpp b/firmware/components/session/gate_automation.cpp index 7d9e895..8efaed0 100644 --- a/firmware/components/session/gate_automation.cpp +++ b/firmware/components/session/gate_automation.cpp @@ -4,7 +4,7 @@ namespace track_timer::session { GateAutomationDecision apply_gate_session_automation( const timing::GateCrossingDecision& event, const std::int64_t now_ms, - const settings::DeviceSettings& settings, + const settings::DeviceSettings& settings, const bool armed, SessionController& controller) noexcept { GateAutomationDecision decision{}; @@ -17,11 +17,21 @@ GateAutomationDecision apply_gate_session_automation( } const auto state = controller.snapshot().state; - if (event.gate == timing::TimingGate::pit_exit) { - if (!settings.pit_exit_auto_start_enabled) { + if (event.gate == timing::TimingGate::start) { + if (!settings.gps_start_line_enabled) { decision.result = GateAutomationResult::ignored_disabled; return decision; } + // Armed means the driver asked for this. Without the check, a start-line crossing + // would begin a session whenever the controller happened to be ready - driving out + // of the paddock, or past the line on the way to anywhere. The pit-exit gate this + // replaced was obscure enough to make that unlikely; the start line is not. + if (!armed) { + decision.result = GateAutomationResult::ignored_unarmed; + return decision; + } + // Only from ready, so the first crossing starts the session and every lap after it + // is ignored rather than restarting the clock. if (state != SessionState::ready) { decision.result = GateAutomationResult::ignored_state; return decision; @@ -32,21 +42,6 @@ GateAutomationDecision apply_gate_session_automation( : GateAutomationResult::transition_rejected; return decision; } - if (event.gate == timing::TimingGate::pit_entry) { - if (!settings.pit_entry_auto_stop_enabled) { - decision.result = GateAutomationResult::ignored_disabled; - return decision; - } - if (state != SessionState::running && state != SessionState::overtime) { - decision.result = GateAutomationResult::ignored_state; - return decision; - } - decision.transition_result = controller.stop_from_pit_entry(now_ms); - decision.transitioned = decision.transition_result == TransitionResult::accepted; - decision.result = decision.transitioned ? GateAutomationResult::stopped - : GateAutomationResult::transition_rejected; - return decision; - } decision.result = GateAutomationResult::ignored_gate; return decision; } @@ -62,10 +57,10 @@ const char* gate_automation_result_name(const GateAutomationResult result) noexc return "ignored-disabled"; case GateAutomationResult::ignored_state: return "ignored-state"; + case GateAutomationResult::ignored_unarmed: + return "ignored-unarmed"; case GateAutomationResult::started: return "started"; - case GateAutomationResult::stopped: - return "stopped"; case GateAutomationResult::transition_rejected: return "transition-rejected"; } diff --git a/firmware/components/session/include/track_timer/session/controller.hpp b/firmware/components/session/include/track_timer/session/controller.hpp index b4de81c..fbcd4c3 100644 --- a/firmware/components/session/include/track_timer/session/controller.hpp +++ b/firmware/components/session/include/track_timer/session/controller.hpp @@ -23,7 +23,6 @@ enum class SessionState : std::uint8_t { enum class CompletionReason : std::uint8_t { none, driver_stop, - pit_entry, }; enum class TransitionResult : std::uint8_t { @@ -73,7 +72,6 @@ class SessionController { [[nodiscard]] TransitionResult request_stop(std::int64_t now_ms) noexcept; [[nodiscard]] TransitionResult cancel_stop(std::int64_t now_ms) noexcept; [[nodiscard]] TransitionResult confirm_stop(std::int64_t now_ms) noexcept; - [[nodiscard]] TransitionResult stop_from_pit_entry(std::int64_t now_ms) noexcept; [[nodiscard]] TransitionResult complete_review(std::int64_t now_ms) noexcept; [[nodiscard]] const SessionSnapshot& snapshot() const noexcept; diff --git a/firmware/components/session/include/track_timer/session/gate_automation.hpp b/firmware/components/session/include/track_timer/session/gate_automation.hpp index 1fbcda1..b168e2f 100644 --- a/firmware/components/session/include/track_timer/session/gate_automation.hpp +++ b/firmware/components/session/include/track_timer/session/gate_automation.hpp @@ -14,8 +14,11 @@ enum class GateAutomationResult : std::uint8_t { ignored_gate, ignored_disabled, ignored_state, + // The crossing was the right gate and the setting was on, but the driver had not + // armed the session. Distinct from ignored_state because the remedy is different: + // press start, rather than wait for the session to end. + ignored_unarmed, started, - stopped, transition_rejected, }; @@ -31,7 +34,7 @@ struct GateAutomationDecision { [[nodiscard]] GateAutomationDecision apply_gate_session_automation( const timing::GateCrossingDecision& event, std::int64_t now_ms, - const settings::DeviceSettings& settings, + const settings::DeviceSettings& settings, bool armed, SessionController& controller) noexcept; [[nodiscard]] const char* gate_automation_result_name( diff --git a/firmware/components/settings/component.cpp b/firmware/components/settings/component.cpp index 104d59d..c72c3c9 100644 --- a/firmware/components/settings/component.cpp +++ b/firmware/components/settings/component.cpp @@ -204,8 +204,7 @@ bool settings_equal(const DeviceSettings& left, const DeviceSettings& right) noe left.selected_track_id == right.selected_track_id && left.trackday_mode_enabled == right.trackday_mode_enabled && left.lap_boundary == right.lap_boundary && - left.pit_exit_auto_start_enabled == right.pit_exit_auto_start_enabled && - left.pit_entry_auto_stop_enabled == right.pit_entry_auto_stop_enabled; + left.gps_start_line_enabled == right.gps_start_line_enabled; } SettingsBlob encode_legacy_settings_v2(const DeviceSettings& settings) noexcept @@ -255,8 +254,12 @@ SettingsBlob encode_legacy_settings_v4(const DeviceSettings& settings) noexcept std::array payload{}; std::copy_n(legacy.bytes.data() + kHeaderSize, kLegacyV3PayloadSize, payload.data()); payload[63] = static_cast(settings.lap_boundary); - payload[64] = settings.pit_exit_auto_start_enabled ? 1U : 0U; - payload[65] = settings.pit_entry_auto_stop_enabled ? 1U : 0U; + payload[64] = settings.gps_start_line_enabled ? 1U : 0U; + // Byte 65 held pit_entry_auto_stop_enabled. The setting is gone and the byte stays, + // written as zero and never read: this is a fixed-width record, so reclaiming the byte + // would change its length and force a format version and a migration for a field + // nothing uses. Byte 64 keeps both its position and its meaning, so neither needs one. + payload[65] = 0U; return make_blob(4, payload.data(), payload.size()); } @@ -381,8 +384,7 @@ DecodeResult decode_settings(const SettingsBlob& blob, DeviceSettings& settings) } if (version >= 4) { candidate.lap_boundary = static_cast(payload[63]); - candidate.pit_exit_auto_start_enabled = payload[64] != 0; - candidate.pit_entry_auto_stop_enabled = payload[65] != 0; + candidate.gps_start_line_enabled = payload[64] != 0; } if (version >= 5) { // The authoritative durations, which the vestigial minutes above cannot express. diff --git a/firmware/components/settings/include/track_timer/settings/settings.hpp b/firmware/components/settings/include/track_timer/settings/settings.hpp index 37ce907..798ed05 100644 --- a/firmware/components/settings/include/track_timer/settings/settings.hpp +++ b/firmware/components/settings/include/track_timer/settings/settings.hpp @@ -96,8 +96,16 @@ struct DeviceSettings { std::array selected_track_id{}; bool trackday_mode_enabled{false}; LapBoundaryMode lap_boundary{LapBoundaryMode::finish}; - bool pit_exit_auto_start_enabled{false}; - bool pit_entry_auto_stop_enabled{false}; + // Starting a session on crossing the start line, which is what the GPS trigger + // means. Derived from session_trigger rather than edited separately, so the two + // cannot disagree. + // + // Was pit_exit_auto_start_enabled, and acted on the pit-exit gate. The pit gates are + // gone; the start line is a gate the car crosses every lap rather than an arbitrary + // line chosen on a road loop, and on a circuit whose start and finish are one line it + // begins the session and lap 1 on the same crossing. The stored byte and its meaning + // are unchanged, so this is a rename rather than a format change. + bool gps_start_line_enabled{false}; // Appended, like every field before it: this struct is aggregate-initialised at call // sites. Owns pit_exit_auto_start_enabled, which is derived from it rather than // edited separately, so the two cannot disagree. diff --git a/firmware/components/timing/engine.cpp b/firmware/components/timing/engine.cpp index fe0a76c..83797c9 100644 --- a/firmware/components/timing/engine.cpp +++ b/firmware/components/timing/engine.cpp @@ -11,15 +11,12 @@ constexpr double kNanosecondsPerSecond = 1'000'000'000.0; constexpr std::array kGateTypes{ TimingGate::start, TimingGate::finish, - TimingGate::pit_entry, - TimingGate::pit_exit, }; std::array config_gates( const TimingEngineConfig& config) noexcept { - return {&config.gates.start, &config.gates.finish, &config.gates.pit_entry, - &config.gates.pit_exit}; + return {&config.gates.start, &config.gates.finish}; } [[nodiscard]] bool valid_gate(const track::DirectedGateDefinition& gate) noexcept @@ -228,7 +225,7 @@ void TimingEngine::reset() noexcept config_ = {}; projection_ = {}; gates_ = {}; - gate_armed_ = {{true, true, true, true}}; + gate_armed_ = {{true, true}}; lap_state_machine_.reset(); previous_fix_ = {}; previous_position_ = {}; @@ -306,10 +303,6 @@ const char* timing_gate_name(const TimingGate gate) noexcept return "start"; case TimingGate::finish: return "finish"; - case TimingGate::pit_entry: - return "pit-entry"; - case TimingGate::pit_exit: - return "pit-exit"; } return "unknown"; } diff --git a/firmware/components/timing/include/track_timer/timing/engine.hpp b/firmware/components/timing/include/track_timer/timing/engine.hpp index fc0b26d..bea82c1 100644 --- a/firmware/components/timing/include/track_timer/timing/engine.hpp +++ b/firmware/components/timing/include/track_timer/timing/engine.hpp @@ -27,11 +27,9 @@ enum class LapBoundary : std::uint8_t { enum class TimingGate : std::uint8_t { start, finish, - pit_entry, - pit_exit, }; -inline constexpr std::size_t kTimingGateCount = 4; +inline constexpr std::size_t kTimingGateCount = 2; struct TimingEngineConfig { track::GeographicPoint reference{}; @@ -108,7 +106,7 @@ class TimingEngine { TimingEngineConfig config_{}; track::CircuitProjection projection_{}; std::array gates_{}; - std::array gate_armed_{{true, true, true, true}}; + std::array gate_armed_{{true, true}}; LapStateMachine lap_state_machine_{{1, 1.0}}; domain::GnssFix previous_fix_{}; track::LocalPoint previous_position_{}; diff --git a/firmware/components/track/definition.cpp b/firmware/components/track/definition.cpp index 6582c74..c7f1f97 100644 --- a/firmware/components/track/definition.cpp +++ b/firmware/components/track/definition.cpp @@ -469,8 +469,6 @@ bool parse_gates(JsonReader& reader, CircuitGateDefinitions& gates) noexcept } bool start = false; bool finish = false; - bool pit_entry = false; - bool pit_exit = false; while (reader.peek() != '}') { std::array key{}; if (reader.string(key) == StringResult::invalid || !reader.consume(':')) { @@ -488,15 +486,14 @@ bool parse_gates(JsonReader& reader, CircuitGateDefinitions& gates) noexcept return false; } } - else if (std::strcmp(key.data(), "pit_entry") == 0) { - pit_entry = parse_gate(reader, gates.pit_entry); - if (!pit_entry) { - return false; - } - } - else if (std::strcmp(key.data(), "pit_exit") == 0) { - pit_exit = parse_gate(reader, gates.pit_exit); - if (!pit_exit) { + // Parsed and discarded. Pit entry and exit are gone, and a schema-3 definition + // still carries them - including the 26 circuits already on cards. Rejecting the + // key would make every one of those files unloadable for a gate nothing reads, + // so they are consumed to keep the reader in step and then dropped. + else if (std::strcmp(key.data(), "pit_entry") == 0 || + std::strcmp(key.data(), "pit_exit") == 0) { + DirectedGateDefinition discarded{}; + if (!parse_gate(reader, discarded)) { return false; } } @@ -510,7 +507,7 @@ bool parse_gates(JsonReader& reader, CircuitGateDefinitions& gates) noexcept return false; } } - return reader.consume('}') && start && finish && pit_entry && pit_exit; + return reader.consume('}') && start && finish; } bool parse_timing(JsonReader& reader, double& minimum_lap_time_s) noexcept @@ -949,8 +946,12 @@ TrackLoadReport load_track_definition(const std::string_view json, return report(TrackLoadResult::capacity_exceeded, reader, parsed.error_field); } + // Schema 3 is accepted alongthe current one. It differs only by carrying pit gates, + // which the parser discards, so a card written before they were removed keeps working + // rather than losing its whole track list. if (parsed.schema && - parsed.definition.schema_version != kCurrentTrackSchemaVersion) { + parsed.definition.schema_version != kCurrentTrackSchemaVersion && + parsed.definition.schema_version != kSupportedLegacyTrackSchemaVersion) { return report(TrackLoadResult::unsupported_version, reader, TrackDefinitionField::schema_version); } @@ -962,11 +963,9 @@ TrackLoadReport load_track_definition(const std::string_view json, } auto& definition = parsed.definition; - const std::array gates{ + const std::array gates{ &definition.gates.start, &definition.gates.finish, - &definition.gates.pit_entry, - &definition.gates.pit_exit, }; if (!valid_identifier(definition.track_id)) { return report(TrackLoadResult::invalid_value, reader, @@ -1029,17 +1028,13 @@ TrackLoadReport load_track_definition(const std::string_view json, TrackDefinitionField::geofence); } - constexpr std::array degenerate_results{ + constexpr std::array degenerate_results{ TrackLoadResult::degenerate_start_gate, TrackLoadResult::degenerate_finish_gate, - TrackLoadResult::degenerate_pit_entry_gate, - TrackLoadResult::degenerate_pit_exit_gate, }; - constexpr std::array gate_fields{ + constexpr std::array gate_fields{ TrackDefinitionField::gates_start, TrackDefinitionField::gates_finish, - TrackDefinitionField::gates_pit_entry, - TrackDefinitionField::gates_pit_exit, }; for (std::size_t index = 0; index < gates.size(); ++index) { auto& gate = *gates[index]; @@ -1295,9 +1290,6 @@ TrackSerializeResult serialize_track_definition( definition.geofence.radius_m) && write_gate(writer, definition.gates.start) && writer.text(",\"finish\":") && write_gate(writer, definition.gates.finish) && - writer.text(",\"pit_entry\":") && - write_gate(writer, definition.gates.pit_entry) && - writer.text(",\"pit_exit\":") && write_gate(writer, definition.gates.pit_exit) && writer.format("},\"timing\":{\"minimum_lap_time_s\":%.15g},\"sectors\":[", definition.minimum_lap_time_s); for (std::size_t index = 0; ok && index < definition.sector_count; ++index) { @@ -1341,7 +1333,8 @@ void format_definition_hash(const std::uint64_t hash, bool track_timing_ready(const TrackDefinition& definition) noexcept { - if (definition.schema_version != kCurrentTrackSchemaVersion) { + if (definition.schema_version != kCurrentTrackSchemaVersion && + definition.schema_version != kSupportedLegacyTrackSchemaVersion) { return false; } switch (definition.provenance.geometry_status) { @@ -1393,10 +1386,6 @@ const char* track_load_result_name(const TrackLoadResult result) noexcept return "degenerate-start-gate"; case TrackLoadResult::degenerate_finish_gate: return "degenerate-finish-gate"; - case TrackLoadResult::degenerate_pit_entry_gate: - return "degenerate-pit-entry-gate"; - case TrackLoadResult::degenerate_pit_exit_gate: - return "degenerate-pit-exit-gate"; case TrackLoadResult::degenerate_sector_gate: return "degenerate-sector-gate"; case TrackLoadResult::duplicate_sector_id: @@ -1434,10 +1423,6 @@ const char* track_definition_field_name(const TrackDefinitionField field) noexce return "gates.start"; case TrackDefinitionField::gates_finish: return "gates.finish"; - case TrackDefinitionField::gates_pit_entry: - return "gates.pit_entry"; - case TrackDefinitionField::gates_pit_exit: - return "gates.pit_exit"; case TrackDefinitionField::timing: return "timing"; case TrackDefinitionField::sectors: diff --git a/firmware/components/track/include/track_timer/track/definition.hpp b/firmware/components/track/include/track_timer/track/definition.hpp index 290243a..68d295c 100644 --- a/firmware/components/track/include/track_timer/track/definition.hpp +++ b/firmware/components/track/include/track_timer/track/definition.hpp @@ -8,7 +8,14 @@ namespace track_timer::track { -inline constexpr std::uint16_t kCurrentTrackSchemaVersion = 3; +// 4: the pit entry and exit gates are gone. A schema-3 definition still loads - its pit +// gates are parsed and discarded - because the circuits already on cards are otherwise +// correct and rejecting them would cost a device its whole track list for a gate nothing +// reads any more. +inline constexpr std::uint16_t kCurrentTrackSchemaVersion = 4; +// Still loadable, and still able to arm timing: a 3 differs from a 4 only by carrying pit +// gates, which are parsed and discarded. +inline constexpr std::uint16_t kSupportedLegacyTrackSchemaVersion = 3; inline constexpr std::size_t kMaximumTrackFileBytes = 16'384; inline constexpr std::size_t kTrackIdCapacity = 48; inline constexpr std::size_t kTrackNameCapacity = 64; @@ -49,8 +56,6 @@ struct DirectedGateDefinition { struct CircuitGateDefinitions { DirectedGateDefinition start{}; DirectedGateDefinition finish{}; - DirectedGateDefinition pit_entry{}; - DirectedGateDefinition pit_exit{}; }; struct SectorDefinition { @@ -111,8 +116,6 @@ enum class TrackLoadResult : std::uint8_t { capacity_exceeded, degenerate_start_gate, degenerate_finish_gate, - degenerate_pit_entry_gate, - degenerate_pit_exit_gate, degenerate_sector_gate, duplicate_sector_id, geometry_outside_geofence, @@ -131,8 +134,6 @@ enum class TrackDefinitionField : std::uint8_t { geofence, gates_start, gates_finish, - gates_pit_entry, - gates_pit_exit, timing, sectors, }; diff --git a/firmware/components/ui/gate_capture.cpp b/firmware/components/ui/gate_capture.cpp index e0b1d84..94d5b7f 100644 --- a/firmware/components/ui/gate_capture.cpp +++ b/firmware/components/ui/gate_capture.cpp @@ -11,8 +11,7 @@ namespace track_timer::ui { namespace { -constexpr std::array kGateNames{ - "START", "FINISH", "PIT ENTRY", "PIT EXIT"}; +constexpr std::array kGateNames{"START", "FINISH"}; const char* status_text(const GateCaptureStatus status) noexcept { @@ -107,7 +106,7 @@ void GateCaptureController::previous_gate() noexcept if (status_ == GateCaptureStatus::closed) { return; } - gate_index_ = gate_index_ == 0 ? 3 : gate_index_ - 1; + gate_index_ = gate_index_ == 0 ? 1 : gate_index_ - 1; status_ = GateCaptureStatus::editing; } @@ -116,7 +115,7 @@ void GateCaptureController::next_gate() noexcept if (status_ == GateCaptureStatus::closed) { return; } - gate_index_ = (gate_index_ + 1) % 4; + gate_index_ = (gate_index_ + 1) % 2; status_ = GateCaptureStatus::editing; } @@ -293,10 +292,6 @@ track::DirectedGateDefinition* GateCaptureController::selected_gate() noexcept return &draft_.gates.start; case 1: return &draft_.gates.finish; - case 2: - return &draft_.gates.pit_entry; - case 3: - return &draft_.gates.pit_exit; } return nullptr; } diff --git a/firmware/components/ui/include/track_timer/ui/gate_capture.hpp b/firmware/components/ui/include/track_timer/ui/gate_capture.hpp index f3d69d4..923034f 100644 --- a/firmware/components/ui/include/track_timer/ui/gate_capture.hpp +++ b/firmware/components/ui/include/track_timer/ui/gate_capture.hpp @@ -68,7 +68,8 @@ class GateCaptureController { track::TrackDefinitionStore* store_{nullptr}; domain::GnssFix latest_fix_{}; std::int64_t evaluation_monotonic_us_{domain::kUnavailableTime}; - std::array captured_{}; + // Two endpoints for each of the two gates that remain. + std::array captured_{}; std::size_t gate_index_{0}; bool right_endpoint_{false}; GateCaptureStatus status_{GateCaptureStatus::closed}; diff --git a/firmware/components/ui/include/track_timer/ui/session_trigger.hpp b/firmware/components/ui/include/track_timer/ui/session_trigger.hpp index 230f4f7..2eb64a7 100644 --- a/firmware/components/ui/include/track_timer/ui/session_trigger.hpp +++ b/firmware/components/ui/include/track_timer/ui/session_trigger.hpp @@ -9,12 +9,12 @@ namespace track_timer::ui { inline constexpr std::size_t kSessionTriggerCount = 3; -// Why a trigger owns pit-exit auto-start rather than sitting beside it: +// Why a trigger owns start-line auto-start rather than sitting beside it: // -// `pit_exit_auto_start_enabled` already meant "start the session when the car leaves the -// pits", with tested automation behind it. GPS is the same intent, so the trigger writes -// that setting and it leaves the device settings list. Offering one behaviour in two -// places invites the two disagreeing, which is the rule Mode established. +// `gps_start_line_enabled` means "start the session when the car crosses the start line", +// with tested automation behind it. GPS is the same intent, so the trigger writes that +// setting and it leaves the device settings list. Offering one behaviour in two places +// invites the two disagreeing, which is the rule Mode established. void apply_trigger(settings::SessionTrigger trigger, settings::DeviceSettings& settings) noexcept; [[nodiscard]] settings::SessionTrigger trigger_from_settings( diff --git a/firmware/components/ui/include/track_timer/ui/settings_editor.hpp b/firmware/components/ui/include/track_timer/ui/settings_editor.hpp index 2080fae..ed3663f 100644 --- a/firmware/components/ui/include/track_timer/ui/settings_editor.hpp +++ b/firmware/components/ui/include/track_timer/ui/settings_editor.hpp @@ -17,8 +17,6 @@ enum class SettingsField : std::uint8_t { operating_mode, trackday_mode, lap_boundary, - pit_exit_auto_start, - pit_entry_auto_stop, orientation, auto_dim, average_lap, @@ -26,7 +24,7 @@ enum class SettingsField : std::uint8_t { acceleration_target, }; -inline constexpr std::size_t kSettingsFieldCount = 14; +inline constexpr std::size_t kSettingsFieldCount = 13; enum class SettingsEditorStatus : std::uint8_t { closed, diff --git a/firmware/components/ui/include/track_timer/ui/value_picker.hpp b/firmware/components/ui/include/track_timer/ui/value_picker.hpp index 4250b79..660e9e5 100644 --- a/firmware/components/ui/include/track_timer/ui/value_picker.hpp +++ b/firmware/components/ui/include/track_timer/ui/value_picker.hpp @@ -32,15 +32,15 @@ struct ValueChoiceList { // field's values rather than walking the driver through the whole list to reach it, and a // hand-written index that stopped matching the array is exactly the mistake that made // DIAGNOSTICS unreachable in #158 - so it is asserted below rather than trusted. -inline constexpr std::size_t kAccelerationTargetFieldIndex = 11; +inline constexpr std::size_t kAccelerationTargetFieldIndex = 10; -inline constexpr std::array kPickerFields{ +inline constexpr std::array kPickerFields{ SettingsField::session_duration, SettingsField::rest_duration, SettingsField::average_lap, SettingsField::lower_display, - SettingsField::lap_boundary, SettingsField::pit_entry_auto_stop, - SettingsField::launch_sensitivity, SettingsField::day_brightness, - SettingsField::night_brightness, SettingsField::auto_dim, - SettingsField::orientation, SettingsField::acceleration_target, + SettingsField::lap_boundary, SettingsField::launch_sensitivity, + SettingsField::day_brightness, SettingsField::night_brightness, + SettingsField::auto_dim, SettingsField::orientation, + SettingsField::acceleration_target, }; static_assert(kPickerFields[kAccelerationTargetFieldIndex] == diff --git a/firmware/components/ui/rest_session.cpp b/firmware/components/ui/rest_session.cpp index e464021..2d82a39 100644 --- a/firmware/components/ui/rest_session.cpp +++ b/firmware/components/ui/rest_session.cpp @@ -204,8 +204,6 @@ const char* completion_reason_name(const session::CompletionReason reason) noexc return "NOT COMPLETE"; case session::CompletionReason::driver_stop: return "DRIVER STOP"; - case session::CompletionReason::pit_entry: - return "PIT ENTRY"; } return "NOT COMPLETE"; } diff --git a/firmware/components/ui/review_cards.cpp b/firmware/components/ui/review_cards.cpp index 3c3c54a..46daf7d 100644 --- a/firmware/components/ui/review_cards.cpp +++ b/firmware/components/ui/review_cards.cpp @@ -32,7 +32,8 @@ const char* completion_text(const logger::SessionCompletionReason reason) noexce switch (reason) { case logger::SessionCompletionReason::driver_stop: return "DRIVER"; - case logger::SessionCompletionReason::pit_entry: + case logger::SessionCompletionReason::reserved_pit_entry: + // Only reachable from a session recorded before pit entry was removed. return "PIT ENTRY"; case logger::SessionCompletionReason::reset_recovery: return "RECOVERED"; diff --git a/firmware/components/ui/session_review.cpp b/firmware/components/ui/session_review.cpp index 30bcc0f..8229f50 100644 --- a/firmware/components/ui/session_review.cpp +++ b/firmware/components/ui/session_review.cpp @@ -54,7 +54,8 @@ const char* completion_name(const logger::SessionCompletionReason reason) noexce return "DRIVER STOP"; case logger::SessionCompletionReason::reset_recovery: return "RESET RECOVERY"; - case logger::SessionCompletionReason::pit_entry: + case logger::SessionCompletionReason::reserved_pit_entry: + // Only reachable from a session recorded before pit entry was removed. return "PIT ENTRY"; case logger::SessionCompletionReason::none: return "INCOMPLETE"; diff --git a/firmware/components/ui/session_trigger.cpp b/firmware/components/ui/session_trigger.cpp index 6a95e10..0a84e0a 100644 --- a/firmware/components/ui/session_trigger.cpp +++ b/firmware/components/ui/session_trigger.cpp @@ -6,9 +6,9 @@ void apply_trigger(const settings::SessionTrigger trigger, settings::DeviceSettings& settings) noexcept { settings.session_trigger = trigger; - // The trigger is the only thing that writes this now, so the tested gate automation - // keeps reading the boolean it always has without knowing a trigger exists. - settings.pit_exit_auto_start_enabled = trigger == settings::SessionTrigger::gps; + // The trigger is the only thing that writes this, so the tested gate automation + // keeps reading a boolean without knowing a trigger exists. + settings.gps_start_line_enabled = trigger == settings::SessionTrigger::gps; } settings::SessionTrigger trigger_from_settings( @@ -78,7 +78,7 @@ TriggerReadiness trigger_readiness(const settings::SessionTrigger trigger, if (!gnss_available) { return {false, "GPS UNAVAILABLE"}; } - return {true, "WAITING FOR THE LINE"}; + return {true, "WAITING FOR THE START LINE"}; } return {true, ""}; } diff --git a/firmware/components/ui/settings_editor.cpp b/firmware/components/ui/settings_editor.cpp index 69fd5e2..660d577 100644 --- a/firmware/components/ui/settings_editor.cpp +++ b/firmware/components/ui/settings_editor.cpp @@ -127,14 +127,6 @@ void format_value(const SettingsField field, const settings::DeviceSettings& set ? "START LINE" : "FINISH LINE"); break; - case SettingsField::pit_exit_auto_start: - std::snprintf(output.data(), output.size(), "%s", - settings.pit_exit_auto_start_enabled ? "ENABLED" : "DISABLED"); - break; - case SettingsField::pit_entry_auto_stop: - std::snprintf(output.data(), output.size(), "%s", - settings.pit_entry_auto_stop_enabled ? "ENABLED" : "DISABLED"); - break; case SettingsField::orientation: { constexpr std::array names{"0 DEG", "90 DEG", "180 DEG", "270 DEG", "AUTO"}; @@ -344,14 +336,6 @@ bool SettingsEditor::adjust(const bool forward) noexcept : settings::LapBoundaryMode::start; adjusted = true; break; - case SettingsField::pit_exit_auto_start: - draft_.pit_exit_auto_start_enabled = !draft_.pit_exit_auto_start_enabled; - adjusted = true; - break; - case SettingsField::pit_entry_auto_stop: - draft_.pit_entry_auto_stop_enabled = !draft_.pit_entry_auto_stop_enabled; - adjusted = true; - break; case SettingsField::orientation: adjusted = step_enum(draft_.orientation, 4, forward); break; @@ -392,12 +376,21 @@ bool SettingsEditor::adjust(const bool forward) noexcept const char* settings_field_name(const SettingsField field) noexcept { + // One per enumerator, in the enum's order. "0-X TARGET" was missing: the enum gained + // acceleration_target without this array or kSettingsFieldCount following it, so + // naming that field read past the end. Caught while removing the two pit entries. + // + // "REST" rather than "PIT REST": the rest period is time between sessions, which a + // driver spends wherever they like, and the pit vocabulary went out with the gates. constexpr std::array names{ - "TRACK SESSION", "PIT REST", "LAUNCH SENSITIVITY", "DAY BRIGHTNESS", - "NIGHT BRIGHTNESS", "MODE", "TRACKDAY MODE", "LAP LINE", - "PIT EXIT START", "PIT ENTRY STOP", "ORIENTATION", "AUTO-DIM", - "AVERAGE LAP", "LOWER DISPLAY", + "TRACK SESSION", "REST", "LAUNCH SENSITIVITY", "DAY BRIGHTNESS", + "NIGHT BRIGHTNESS", "MODE", "TRACKDAY MODE", "LAP LINE", + "ORIENTATION", "AUTO-DIM", "AVERAGE LAP", "LOWER DISPLAY", + "0-X TARGET", }; + static_assert(names.size() == kSettingsFieldCount, + "every field must have a name, and naming one past the end is how this " + "array and the enum came to disagree"); return names[static_cast(field)]; } diff --git a/firmware/components/ui/value_picker.cpp b/firmware/components/ui/value_picker.cpp index 3fd2635..c86e25a 100644 --- a/firmware/components/ui/value_picker.cpp +++ b/firmware/components/ui/value_picker.cpp @@ -84,10 +84,6 @@ ValueChoiceList choices_for(const SettingsField field, }); case SettingsField::auto_dim: return from_labels(kOffOn, 2, current.auto_dim_enabled ? 1U : 0U); - case SettingsField::pit_exit_auto_start: - return from_labels(kOffOn, 2, current.pit_exit_auto_start_enabled ? 1U : 0U); - case SettingsField::pit_entry_auto_stop: - return from_labels(kOffOn, 2, current.pit_entry_auto_stop_enabled ? 1U : 0U); case SettingsField::lower_display: return from_labels(kLowerDisplay, 2, static_cast(current.lower_display)); @@ -149,18 +145,6 @@ bool apply_choice(const SettingsField field, const std::size_t index, } draft.auto_dim_enabled = index == 1; return true; - case SettingsField::pit_exit_auto_start: - if (index > 1) { - return false; - } - draft.pit_exit_auto_start_enabled = index == 1; - return true; - case SettingsField::pit_entry_auto_stop: - if (index > 1) { - return false; - } - draft.pit_entry_auto_stop_enabled = index == 1; - return true; case SettingsField::lower_display: if (index > 1 || (index == 1 && draft.average_lap_seconds == 0)) { @@ -206,10 +190,6 @@ const char* picker_field_label(const SettingsField field) noexcept return "LOWER FIELD"; case SettingsField::lap_boundary: return "LAP LINE"; - case SettingsField::pit_exit_auto_start: - return "PIT EXIT START"; - case SettingsField::pit_entry_auto_stop: - return "PIT ENTRY STOP"; case SettingsField::acceleration_target: return "0-X TARGET"; case SettingsField::orientation: diff --git a/firmware/main/screen_router.cpp b/firmware/main/screen_router.cpp index db4f5a2..9aa7a2d 100644 --- a/firmware/main/screen_router.cpp +++ b/firmware/main/screen_router.cpp @@ -325,8 +325,6 @@ constexpr std::uint32_t kRuby = 0xFF8FA3; case ui::SettingsField::lower_display: return LV_SYMBOL_LIST; case ui::SettingsField::lap_boundary: - case ui::SettingsField::pit_exit_auto_start: - case ui::SettingsField::pit_entry_auto_stop: return LV_SYMBOL_GPS; default: return LV_SYMBOL_REFRESH; @@ -1494,12 +1492,12 @@ class ScreenRouter { void apply_gate_automation(const timing::GateCrossingDecision& decision) noexcept { const auto now_ms = static_cast(esp_timer_get_time() / 1000); - const auto outcome = - session::apply_gate_session_automation(decision, now_ms, settings_, session_); + const auto outcome = session::apply_gate_session_automation( + decision, now_ms, settings_, armed_ && armed_ready_, session_); if (outcome.result == session::GateAutomationResult::ignored_non_event || outcome.result == session::GateAutomationResult::ignored_gate) { - // Start and finish crossings come through this queue too, several per lap. - // Logging them would bury the two that mean something. + // Finish crossings come through this queue too, several per lap. Logging them + // would bury the one that means something. return; } ESP_LOGI("track_timer", "gate automation: %s -> %s", @@ -1508,17 +1506,10 @@ class ScreenRouter { if (!outcome.transitioned) { return; } - // The session was started or stopped from underneath the shell, so the two things - // begin_session and advance_session_phase would have done have to happen here. - // service_session picks up the state change itself and loads the right screen. + // The session was started from underneath the shell, so what begin_session would + // have done has to happen here. service_session picks up the state change itself + // and loads the right screen. armed_ = false; - if (outcome.result == session::GateAutomationResult::stopped) { - // stop_from_pit_entry goes straight to review, having already refreshed the - // session times, so the summary is captured after it rather than before as - // the driver-stop path does - and confirm_stop is not part of this route. - capture_summary(); - (void)session_.complete_review(now_ms); - } describe_mode(); refresh_pending(); } @@ -1537,16 +1528,10 @@ class ScreenRouter { static_cast(duration_ms / 1'000 % 60), static_cast(duration_ms % 1'000)); } - // Each crossing is offered to the session automation, which starts on pit exit - // and stops on pit entry when the settings ask for it. This is what the GPS - // trigger has always meant and never did: the automation and the queue both - // existed, and the records were read off the queue and thrown away. - // - // Offered unconditionally rather than only while armed on GPS, because the two - // settings the automation consults are the authority on whether it should act - - // pit_exit_auto_start_enabled is derived from the trigger, and pit-entry - // auto-stop is its own setting that a driver can want with any trigger. Gating - // here as well would mean two places deciding the same thing. + // Each crossing is offered to the session automation, which starts the session on + // the start line when the GPS trigger asks for it. This is what the GPS trigger + // has always meant and never did: the automation and the queue both existed, and + // the records were read off the queue and thrown away. // // The records are also the raw material for the offline replay in #43, which is // why they keep being drained to the floor afterwards rather than consumed. @@ -2235,8 +2220,6 @@ class ScreenRouter { return logger::SessionCompletionReason::none; case session::CompletionReason::driver_stop: return logger::SessionCompletionReason::driver_stop; - case session::CompletionReason::pit_entry: - return logger::SessionCompletionReason::pit_entry; } return logger::SessionCompletionReason::none; } diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index 951f5ab..40b4296 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -208,7 +208,7 @@ if(BUILD_TESTING) set_tests_properties( simulator-gate-session-automation PROPERTIES - PASS_REGULAR_EXPRESSION "Lap-gate settings and independent pit session automation passed" + PASS_REGULAR_EXPRESSION "Lap-gate settings and start-line session automation passed" TIMEOUT 20 ) @@ -233,7 +233,7 @@ if(BUILD_TESTING) set_tests_properties( simulator-gate-capture PROPERTIES - PASS_REGULAR_EXPRESSION "Four-gate capture, safety rejection, atomic persistence, and recovery passed" + PASS_REGULAR_EXPRESSION "Two-gate capture, safety rejection, atomic persistence, and recovery passed" TIMEOUT 20 ) @@ -633,7 +633,7 @@ if(BUILD_TESTING) set_tests_properties( simulator-four-gate-event-engine PROPERTIES - PASS_REGULAR_EXPRESSION "Typed four-gate events, independent rearm, interpolation, and rejection replay passed" + PASS_REGULAR_EXPRESSION "Typed gate events, independent rearm, interpolation, and rejection replay passed" TIMEOUT 20 ) diff --git a/simulator/src/track_fixtures.cpp b/simulator/src/track_fixtures.cpp index de2341e..ebd216c 100644 --- a/simulator/src/track_fixtures.cpp +++ b/simulator/src/track_fixtures.cpp @@ -41,8 +41,6 @@ track::TrackDefinition make_definition(const char* id, const char* name, definition.geofence = {{latitude, longitude}, radius_m}; definition.gates.start = make_gate(latitude, longitude, 0.00005); definition.gates.finish = definition.gates.start; - definition.gates.pit_entry = make_gate(latitude + 0.0002, longitude, 0.00005); - definition.gates.pit_exit = make_gate(latitude - 0.0002, longitude, 0.00005); definition.minimum_lap_time_s = 20.0; definition.definition_hash = hash; return definition; diff --git a/tests/cpp/test_gate_capture.cpp b/tests/cpp/test_gate_capture.cpp index d1c43ad..f330454 100644 --- a/tests/cpp/test_gate_capture.cpp +++ b/tests/cpp/test_gate_capture.cpp @@ -70,9 +70,8 @@ track_timer::domain::GnssFix fix_at( void capture_all(track_timer::ui::GateCaptureController& controller, const track_timer::track::TrackDefinition& source) { - const std::array gates{ - source.gates.start, source.gates.finish, source.gates.pit_entry, - source.gates.pit_exit}; + const std::array gates{ + source.gates.start, source.gates.finish}; std::uint32_t sequence = 10; for (std::size_t gate_index = 0; gate_index < gates.size(); ++gate_index) { auto left = fix_at(gates[gate_index].left, sequence++); @@ -131,7 +130,7 @@ int main() controller.begin(source, &store, false); capture_all(controller, source); - assert(controller.captured_endpoint_count() == 8); + assert(controller.captured_endpoint_count() == 4); auto model = controller.view_model(); assert(model.can_save); assert(std::strstr(model.preview.data(), "LINE READY") != nullptr); @@ -223,6 +222,6 @@ int main() assert(std::strlen(ui::gate_capture_status_name(status)) > 0); } - std::cout << "Four-gate capture, safety rejection, atomic persistence, and recovery passed\n"; + std::cout << "Two-gate capture, safety rejection, atomic persistence, and recovery passed\n"; return 0; } diff --git a/tests/cpp/test_gate_event_engine.cpp b/tests/cpp/test_gate_event_engine.cpp index a5a22b6..4ea5009 100644 --- a/tests/cpp/test_gate_event_engine.cpp +++ b/tests/cpp/test_gate_event_engine.cpp @@ -58,8 +58,6 @@ TimingEngineConfig configuration(CircuitProjection& projection, track_timer::track::ProjectionResult::projected); config.gates.start = gate(projection, 0.0); config.gates.finish = gate(projection, 20.0); - config.gates.pit_exit = gate(projection, 40.0, 0.5); - config.gates.pit_entry = gate(projection, 60.0, 0.5); config.lap_boundary = boundary; config.minimum_lap_time_s = 1.0; return config; @@ -229,13 +227,18 @@ int main() assert(!decision_for(parallel_move, TimingGate::start).has_event); // Slow pit-lane movement is accepted by its own threshold. - TimingEngine slow_pit{}; - assert(slow_pit.configure(finish_config) == TimingEngineConfigureResult::configured); - prime(slow_pit, fix(projection, {35.0, 0.0}, 0, 40'000'000, 1.5F)); - const auto pit_crossing = slow_pit.process_fix( - fix(projection, {45.0, 0.0}, 1, 40'000'000, 1.5F), + // A crossing slower than the gate's minimum speed. The pit gates carried the low + // threshold before; the finish line carries it here, because what is being tested is + // that the threshold is honoured rather than which gate holds it. + auto slow_config = finish_config; + slow_config.gates.finish.minimum_crossing_speed_mps = 0.5; + TimingEngine slow_engine{}; + assert(slow_engine.configure(slow_config) == TimingEngineConfigureResult::configured); + prime(slow_engine, fix(projection, {15.0, 0.0}, 0, 40'000'000, 1.5F)); + const auto slow_crossing = slow_engine.process_fix( + fix(projection, {25.0, 0.0}, 1, 40'000'000, 1.5F), kBaseTimeNs / 1'000 + 50'000); - assert(decision_for(pit_crossing, TimingGate::pit_exit).has_event); + assert(decision_for(slow_crossing, TimingGate::finish).has_event); // Reverse, poor-quality, stale, and non-monotonic pairs remain replayable rejects. TimingEngine reverse_engine{}; @@ -278,8 +281,7 @@ int main() assert(decision_for(bad_sequence, TimingGate::start).time_result == CrossingTimeResult::non_monotonic_sequence); - for (const auto gate_type : {TimingGate::start, TimingGate::finish, - TimingGate::pit_entry, TimingGate::pit_exit}) { + for (const auto gate_type : {TimingGate::start, TimingGate::finish}) { assert(std::strlen(timing_gate_name(gate_type)) > 0); } for (const auto result : {GateCrossingResult::not_evaluated, @@ -291,5 +293,5 @@ int main() assert(std::strlen(gate_crossing_result_name(result)) > 0); } - std::cout << "Typed four-gate events, independent rearm, interpolation, and rejection replay passed\n"; + std::cout << "Typed gate events, independent rearm, interpolation, and rejection replay passed\n"; } diff --git a/tests/cpp/test_gate_session_automation.cpp b/tests/cpp/test_gate_session_automation.cpp index 5116289..3c02e57 100644 --- a/tests/cpp/test_gate_session_automation.cpp +++ b/tests/cpp/test_gate_session_automation.cpp @@ -34,8 +34,8 @@ int main() settings.lap_boundary = settings::LapBoundaryMode::start; timing_config = timing::make_timing_engine_config(tracks.definitions[0], settings); assert(timing_config.lap_boundary == timing::LapBoundary::start); - assert(timing_config.gates.pit_entry.left.latitude_deg == - tracks.definitions[0].gates.pit_entry.left.latitude_deg); + assert(timing_config.gates.start.left.latitude_deg == + tracks.definitions[0].gates.start.left.latitude_deg); auto provisional_track = tracks.definitions[0]; provisional_track.provenance.geometry_status = track::TrackGeometryStatus::provisional; @@ -43,99 +43,70 @@ int main() timing::make_timing_engine_config(provisional_track, settings); assert(blocked_config.minimum_lap_time_s == 0.0); + // The automation starts a session on the start line. Four things have to be true at + // once, and each refusal is reported distinctly because each has a different remedy. session::SessionController controller{}; - auto rejected = event(timing::TimingGate::pit_exit); + + // Not an event at all - an evaluated crossing that did not fire. + auto rejected = event(timing::TimingGate::start); rejected.has_event = false; - rejected.result = timing::GateCrossingResult::crossing_rejected; - auto decision = session::apply_gate_session_automation(rejected, 0, settings, + rejected.result = timing::GateCrossingResult::not_evaluated; + auto decision = session::apply_gate_session_automation(rejected, 0, settings, true, controller); assert(decision.result == session::GateAutomationResult::ignored_non_event); - assert(controller.snapshot().state == session::SessionState::ready); + assert(!decision.transitioned); + // A gate the automation has no interest in. decision = session::apply_gate_session_automation( - event(timing::TimingGate::start), 0, settings, controller); + event(timing::TimingGate::finish), 0, settings, true, controller); assert(decision.result == session::GateAutomationResult::ignored_gate); + + // The right gate, but the driver did not choose the GPS trigger. decision = session::apply_gate_session_automation( - event(timing::TimingGate::pit_exit), 0, settings, controller); + event(timing::TimingGate::start), 0, settings, true, controller); assert(decision.result == session::GateAutomationResult::ignored_disabled); - settings.pit_exit_auto_start_enabled = true; + // Chosen, but not armed. This is the case the pit-exit gate never had to worry about: + // the start line is crossed on the way to anywhere, so an unarmed crossing must not + // begin a session. + settings.gps_start_line_enabled = true; decision = session::apply_gate_session_automation( - event(timing::TimingGate::pit_exit), 1'000, settings, controller); + event(timing::TimingGate::start), 500, settings, false, controller); + assert(decision.result == session::GateAutomationResult::ignored_unarmed); + assert(!decision.transitioned); + assert(controller.snapshot().state == session::SessionState::ready); + + // Armed, chosen, ready: it starts. + decision = session::apply_gate_session_automation( + event(timing::TimingGate::start), 1'000, settings, true, controller); assert(decision.result == session::GateAutomationResult::started); assert(decision.transitioned); - assert(decision.segment_sequence_0 == 41 && decision.segment_sequence_1 == 42); + assert(decision.event_time_ms == 1'000); + assert(decision.gate == timing::TimingGate::start); assert(controller.snapshot().state == session::SessionState::running); - decision = session::apply_gate_session_automation( - event(timing::TimingGate::pit_exit), 1'001, settings, controller); - assert(decision.result == session::GateAutomationResult::ignored_state); + // And every lap after it crosses the same line, which must not restart the clock. decision = session::apply_gate_session_automation( - event(timing::TimingGate::pit_entry), 2'000, settings, controller); - assert(decision.result == session::GateAutomationResult::ignored_disabled); - assert(controller.snapshot().state == session::SessionState::running); - settings.pit_entry_auto_stop_enabled = true; - decision = session::apply_gate_session_automation( - event(timing::TimingGate::pit_entry), 2'500, settings, controller); - assert(decision.result == session::GateAutomationResult::stopped); - assert(controller.snapshot().state == session::SessionState::review); - assert(controller.snapshot().completion_reason == session::CompletionReason::pit_entry); - assert(!controller.snapshot().stop_confirmation_pending); - decision = session::apply_gate_session_automation( - event(timing::TimingGate::pit_entry), 2'501, settings, controller); + event(timing::TimingGate::start), 61'000, settings, true, controller); assert(decision.result == session::GateAutomationResult::ignored_state); + assert(!decision.transitioned); + assert(controller.snapshot().state == session::SessionState::running); - settings.pit_exit_auto_start_enabled = false; - session::SessionController independently_stopped{}; - assert(independently_stopped.start(10) == session::TransitionResult::accepted); - decision = session::apply_gate_session_automation( - event(timing::TimingGate::pit_entry), 20, settings, independently_stopped); - assert(decision.result == session::GateAutomationResult::stopped); - - session::SessionController overtime{{60'000, 0}}; - assert(overtime.start(0) == session::TransitionResult::accepted); - assert(overtime.advance(60'000) == session::TransitionResult::accepted); - assert(overtime.snapshot().state == session::SessionState::overtime); - decision = session::apply_gate_session_automation( - event(timing::TimingGate::pit_entry), 60'001, settings, overtime); - assert(decision.result == session::GateAutomationResult::stopped); - + // Time that cannot have happened is refused rather than absorbed. session::SessionController non_monotonic{}; assert(non_monotonic.advance(100) == session::TransitionResult::accepted); - settings.pit_exit_auto_start_enabled = true; decision = session::apply_gate_session_automation( - event(timing::TimingGate::pit_exit), 99, settings, non_monotonic); + event(timing::TimingGate::start), 99, settings, true, non_monotonic); assert(decision.result == session::GateAutomationResult::transition_rejected); assert(!decision.transitioned); - session::SessionController manual{}; - assert(manual.start(0) == session::TransitionResult::accepted); - assert(manual.request_stop(1'000) == session::TransitionResult::accepted); - assert(manual.snapshot().stop_confirmation_pending); - assert(manual.confirm_stop(1'001) == session::TransitionResult::accepted); - assert(manual.snapshot().completion_reason == session::CompletionReason::driver_stop); - - for (const auto result : {session::GateAutomationResult::ignored_non_event, - session::GateAutomationResult::ignored_gate, - session::GateAutomationResult::ignored_disabled, - session::GateAutomationResult::ignored_state, - session::GateAutomationResult::started, - session::GateAutomationResult::stopped, - session::GateAutomationResult::transition_rejected}) { - assert(std::strlen(session::gate_automation_result_name(result)) > 0); - } - - // A pit exit starts the session through the controller directly, so it runs for - // whatever configuration the controller already holds - not for whatever the driver - // set. A controller nobody has configured starts a session at SessionConfiguration's - // own default, which is a plausible twenty minutes and therefore the kind of wrong - // that does not look wrong. The configuration has to be written from the settings - // before a crossing can arrive, which is why it is no longer done only inside the - // manual start path. - settings.pit_exit_auto_start_enabled = true; + // A crossing-started session runs for whatever configuration the controller holds - + // not for whatever the driver set. A controller nobody has configured starts at + // SessionConfiguration's own default, which is a plausible twenty minutes and + // therefore the kind of wrong that does not look wrong. session::SessionController unconfigured{}; decision = session::apply_gate_session_automation( - event(timing::TimingGate::pit_exit), 0, settings, unconfigured); + event(timing::TimingGate::start), 0, settings, true, unconfigured); assert(decision.result == session::GateAutomationResult::started); assert(unconfigured.snapshot().session_remaining_ms == session::SessionConfiguration{}.session_duration_ms); @@ -148,35 +119,31 @@ int main() configuration.rest_duration_ms = 15 * 60 * 1'000; assert(configuration.session_duration_ms != session::SessionConfiguration{}.session_duration_ms); - // save_configuration is only accepted from the configuring state, which is why - // configure_session in the router falls back to entering it first. assert(configured.enter_configuration(0) == session::TransitionResult::accepted); assert(configured.save_configuration(configuration, 0) == session::TransitionResult::accepted); decision = session::apply_gate_session_automation( - event(timing::TimingGate::pit_exit), 10, settings, configured); + event(timing::TimingGate::start), 10, settings, true, configured); assert(decision.result == session::GateAutomationResult::started); assert(configured.snapshot().session_remaining_ms == configuration.session_duration_ms); assert(configured.snapshot().state == session::SessionState::running); - // A second crossing while that session runs must not restart it, however many times - // the car passes the pit exit. - const auto started_remaining = configured.snapshot().session_remaining_ms; - decision = session::apply_gate_session_automation( - event(timing::TimingGate::pit_exit), 20, settings, configured); - assert(decision.result == session::GateAutomationResult::ignored_state); - assert(!decision.transitioned); - assert(configured.snapshot().session_remaining_ms == started_remaining); + // Stopping is the driver's, and stays so: there is no start-line equivalent, because + // stopping on the start line would end the session after one lap. + assert(configured.request_stop(5'000) == session::TransitionResult::accepted); + assert(configured.confirm_stop(5'001) == session::TransitionResult::accepted); + assert(configured.snapshot().completion_reason == session::CompletionReason::driver_stop); - // And a pit entry ends it with its own reason rather than looking like a driver stop, - // which is what the summary records. - settings.pit_entry_auto_stop_enabled = true; - decision = session::apply_gate_session_automation( - event(timing::TimingGate::pit_entry), 5'000, settings, configured); - assert(decision.result == session::GateAutomationResult::stopped); - assert(configured.snapshot().state == session::SessionState::review); - assert(configured.snapshot().completion_reason == session::CompletionReason::pit_entry); + for (const auto result : {session::GateAutomationResult::ignored_non_event, + session::GateAutomationResult::ignored_gate, + session::GateAutomationResult::ignored_disabled, + session::GateAutomationResult::ignored_state, + session::GateAutomationResult::ignored_unarmed, + session::GateAutomationResult::started, + session::GateAutomationResult::transition_rejected}) { + assert(std::strlen(session::gate_automation_result_name(result)) > 0); + } - std::cout << "Lap-gate settings and independent pit session automation passed\n"; + std::cout << "Lap-gate settings and start-line session automation passed\n"; return 0; } diff --git a/tests/cpp/test_log_formats.cpp b/tests/cpp/test_log_formats.cpp index 3169663..8c5a85a 100644 --- a/tests/cpp/test_log_formats.cpp +++ b/tests/cpp/test_log_formats.cpp @@ -27,8 +27,7 @@ int main() settings.rest_duration_seconds = 10 * 60; settings.trackday_mode_enabled = true; settings.lap_boundary = settings::LapBoundaryMode::start; - settings.pit_exit_auto_start_enabled = true; - settings.pit_entry_auto_stop_enabled = true; + settings.gps_start_line_enabled = true; copy_text(settings.selected_track_id, "synthetic_test_loop"); logger::SessionMetaV1 meta{}; @@ -48,8 +47,7 @@ int main() assert(logger::valid_meta(meta)); assert(meta.settings.trackday_mode_enabled); assert(meta.settings.lap_boundary == settings::LapBoundaryMode::start); - assert(meta.settings.pit_exit_auto_start_enabled); - assert(meta.settings.pit_entry_auto_stop_enabled); + assert(meta.settings.gps_start_line_enabled); auto invalid_meta = meta; // Relative to the current version, so bumping the format does not turn this sentinel @@ -128,7 +126,9 @@ int main() summary.source_rejected_fix_count = 1; summary.source_event_record_count = 4; assert(logger::valid_summary(summary)); - summary.completion_reason = logger::SessionCompletionReason::pit_entry; + // Still accepted, because sessions recorded before pit-entry auto-stop was removed + // carry it and have to stay reviewable. Nothing produces it now. + summary.completion_reason = logger::SessionCompletionReason::reserved_pit_entry; assert(logger::valid_summary(summary)); auto incomplete_summary = summary; diff --git a/tests/cpp/test_session_trigger.cpp b/tests/cpp/test_session_trigger.cpp index 19ab5d3..f4203a5 100644 --- a/tests/cpp/test_session_trigger.cpp +++ b/tests/cpp/test_session_trigger.cpp @@ -11,38 +11,34 @@ namespace { using namespace track_timer; -// The trigger owns pit-exit auto-start rather than sitting beside it. Offering one +// The trigger owns start-line auto-start rather than sitting beside it. Offering one // behaviour in two places invites the two disagreeing, which is the rule Mode set. -void the_trigger_owns_pit_exit_auto_start() +void the_trigger_owns_start_line_auto_start() { settings::DeviceSettings settings{}; assert(settings.session_trigger == settings::SessionTrigger::manual); - assert(!settings.pit_exit_auto_start_enabled); + assert(!settings.gps_start_line_enabled); ui::apply_trigger(settings::SessionTrigger::gps, settings); assert(ui::trigger_from_settings(settings) == settings::SessionTrigger::gps); - assert(settings.pit_exit_auto_start_enabled); + assert(settings.gps_start_line_enabled); ui::apply_trigger(settings::SessionTrigger::imu, settings); - assert(!settings.pit_exit_auto_start_enabled); + assert(!settings.gps_start_line_enabled); ui::apply_trigger(settings::SessionTrigger::manual, settings); - assert(!settings.pit_exit_auto_start_enabled); - - // Ending a session on pit entry is a different question from starting one, so it - // stays where it was. - settings.pit_entry_auto_stop_enabled = true; - ui::apply_trigger(settings::SessionTrigger::gps, settings); - assert(settings.pit_entry_auto_stop_enabled); + assert(!settings.gps_start_line_enabled); } void the_owned_setting_is_no_longer_offered_as_a_field() { + // The setting the trigger owns has no enumerator of its own any more, so there is + // nothing to look for in the picker. What is still worth asserting is that every + // field the picker does offer can be named and edited - a field in this list with no + // case behind it is how a setting comes to be shown and be inert. const auto& fields = ui::kPickerFields; - assert(std::find(fields.begin(), fields.end(), - ui::SettingsField::pit_exit_auto_start) == fields.end()); - // Its counterpart is untouched. - assert(std::find(fields.begin(), fields.end(), - ui::SettingsField::pit_entry_auto_stop) != fields.end()); + for (const auto field : fields) { + assert(static_cast(field) < ui::kSettingsFieldCount); + } // And the threshold the IMU trigger reads is still reachable. assert(std::find(fields.begin(), fields.end(), ui::SettingsField::launch_sensitivity) != fields.end()); @@ -125,14 +121,14 @@ void the_menu_order_matches_the_enum() int main() { - the_trigger_owns_pit_exit_auto_start(); + the_trigger_owns_start_line_auto_start(); the_owned_setting_is_no_longer_offered_as_a_field(); a_trigger_that_cannot_fire_says_so(); a_launch_is_forward_acceleration(); every_trigger_is_named_and_labelled(); the_menu_order_matches_the_enum(); - std::cout << "Session trigger: ownership of pit-exit auto-start, refusal to arm when " + std::cout << "Session trigger: ownership of start-line auto-start, refusal to arm when " "it cannot fire, and forward-only launch detection passed\n"; return 0; } diff --git a/tests/cpp/test_settings.cpp b/tests/cpp/test_settings.cpp index 804efa4..cdb119f 100644 --- a/tests/cpp/test_settings.cpp +++ b/tests/cpp/test_settings.cpp @@ -57,8 +57,7 @@ DeviceSettings customized_settings() settings.lower_display = LowerDisplayMode::laps_remaining; settings.trackday_mode_enabled = true; settings.lap_boundary = LapBoundaryMode::start; - settings.pit_exit_auto_start_enabled = true; - settings.pit_entry_auto_stop_enabled = true; + settings.gps_start_line_enabled = true; settings.session_trigger = SessionTrigger::imu; std::strcpy(settings.selected_track_id.data(), "synthetic-test-loop"); return settings; @@ -314,8 +313,7 @@ void test_migration_corruption_and_storage_errors() assert(migrated.current().orientation == OrientationMode::fixed_90); assert(!migrated.current().trackday_mode_enabled); assert(migrated.current().lap_boundary == LapBoundaryMode::finish); - assert(!migrated.current().pit_exit_auto_start_enabled); - assert(!migrated.current().pit_entry_auto_stop_enabled); + assert(!migrated.current().gps_start_line_enabled); MemorySettingsStore version_two_store; version_two_store.found = true; @@ -340,8 +338,7 @@ void test_migration_corruption_and_storage_errors() assert(version_three_migration.current_format_persisted); assert(version_three.current().trackday_mode_enabled); assert(version_three.current().lap_boundary == LapBoundaryMode::finish); - assert(!version_three.current().pit_exit_auto_start_enabled); - assert(!version_three.current().pit_entry_auto_stop_enabled); + assert(!version_three.current().gps_start_line_enabled); MemorySettingsStore version_four_store; version_four_store.found = true; @@ -355,7 +352,9 @@ void test_migration_corruption_and_storage_errors() assert(version_four.current().session_duration_seconds == 30 * 60); assert(version_four.current().rest_duration_seconds == 10 * 60); assert(version_four.current().trackday_mode_enabled); - assert(version_four.current().pit_exit_auto_start_enabled); + // Byte 64 keeps its position and its meaning across the rename, so a v4 blob that + // recorded pit-exit auto-start comes back as the start-line trigger it now means. + assert(version_four.current().gps_start_line_enabled); assert(version_four.current().average_lap_seconds == 103); MemorySettingsStore version_five_store; diff --git a/tests/cpp/test_settings_editor.cpp b/tests/cpp/test_settings_editor.cpp index 6406a1d..69b0b07 100644 --- a/tests/cpp/test_settings_editor.cpp +++ b/tests/cpp/test_settings_editor.cpp @@ -143,18 +143,6 @@ int main() assert(editor.decrement()); assert(editor.draft().lap_boundary == settings::LapBoundaryMode::finish); - select_field(editor, ui::SettingsField::pit_exit_auto_start); - assert(editor.increment()); - assert(editor.draft().pit_exit_auto_start_enabled); - assert(editor.decrement()); - assert(!editor.draft().pit_exit_auto_start_enabled); - - select_field(editor, ui::SettingsField::pit_entry_auto_stop); - assert(editor.increment()); - assert(editor.draft().pit_entry_auto_stop_enabled); - assert(editor.decrement()); - assert(!editor.draft().pit_entry_auto_stop_enabled); - select_field(editor, ui::SettingsField::orientation); for (std::uint8_t expected = 1; expected <= 4; ++expected) { assert(editor.increment()); diff --git a/tests/cpp/test_timing_engine.cpp b/tests/cpp/test_timing_engine.cpp index 0ec67c2..27f8301 100644 --- a/tests/cpp/test_timing_engine.cpp +++ b/tests/cpp/test_timing_engine.cpp @@ -88,8 +88,6 @@ TimingEngineConfig configuration(CircuitProjection& projection) config.gates.finish.minimum_crossing_speed_mps = 1.0; config.gates.finish.rearm_corridor_m = 2.0; config.gates.start = config.gates.finish; - config.gates.pit_entry = config.gates.finish; - config.gates.pit_exit = config.gates.finish; config.minimum_lap_time_s = 8.0; return config; } @@ -171,7 +169,7 @@ int main() assert(unconfigured.configure(invalid_reference) == TimingEngineConfigureResult::invalid_reference); auto invalid_gate = config; - invalid_gate.gates.pit_exit.right = invalid_gate.gates.pit_exit.left; + invalid_gate.gates.finish.right = invalid_gate.gates.finish.left; assert(unconfigured.configure(invalid_gate) == TimingEngineConfigureResult::invalid_gate); auto invalid_policy = config; @@ -194,7 +192,8 @@ int main() const auto target_replay = replay(target_model, fixes, true); assert(host_replay.event_count == 3); assert(target_replay.event_count == host_replay.event_count); - assert(host_replay.gate_record_count == 16); + // Two gates now rather than four, so each fix produces half the records it did. + assert(host_replay.gate_record_count == 8); assert(target_replay.gate_record_count == host_replay.gate_record_count); for (std::size_t index = 0; index < host_replay.event_count; ++index) { const auto& expected = host_replay.events[index]; diff --git a/tests/cpp/test_track_definition.cpp b/tests/cpp/test_track_definition.cpp index 3f71418..87b756e 100644 --- a/tests/cpp/test_track_definition.cpp +++ b/tests/cpp/test_track_definition.cpp @@ -97,14 +97,6 @@ int main(const int argc, char** argv) definition.gates.finish.left.latitude_deg); assert(definition.gates.start.right.longitude_deg == definition.gates.finish.right.longitude_deg); - assert(std::hypot(definition.gates.pit_entry.local_right.east_m - - definition.gates.pit_entry.local_left.east_m, - definition.gates.pit_entry.local_right.north_m - - definition.gates.pit_entry.local_left.north_m) > 10.0); - assert(std::hypot(definition.gates.pit_exit.local_right.east_m - - definition.gates.pit_exit.local_left.east_m, - definition.gates.pit_exit.local_right.north_m - - definition.gates.pit_exit.local_left.north_m) > 10.0); assert(definition.minimum_lap_time_s == 20.0); assert(definition.definition_hash == hash_track_definition(valid_json)); std::array hash_text{}; @@ -123,7 +115,30 @@ int main(const int argc, char** argv) assert_failure_preserves_active(std::string(kMaximumTrackFileBytes + 1, ' '), TrackLoadResult::file_too_large); assert_failure_preserves_active("{", TrackLoadResult::invalid_json); - assert_failure_preserves_active(replace_once(valid_json, "\"schema_version\": 3", + // A schema-3 document still loads. It differs only by carrying pit gates, which the + // parser consumes and discards, and the circuits already written to cards are + // schema 3: rejecting them would cost a device its whole track list for gates nothing + // reads any more. + { + const auto legacy = replace_once( + replace_once(valid_json, "\"schema_version\": 4", "\"schema_version\": 3"), + "\"finish\": {", + "\"pit_entry\": {\n \"left\": {\"lat_deg\": 52.0004, \"lon_deg\": -1.0004},\n" + " \"right\": {\"lat_deg\": 52.0005, \"lon_deg\": -1.0005},\n" + " \"direction_heading_deg\": 90.0,\n" + " \"heading_tolerance_deg\": 60.0,\n" + " \"minimum_crossing_speed_mps\": 2.0,\n" + " \"rearm_corridor_m\": 15.0\n },\n \"finish\": {"); + TrackDefinition legacy_definition{}; + assert(load_track_definition(legacy, legacy_definition).result == + TrackLoadResult::loaded); + assert(legacy_definition.schema_version == 3); + assert(track_timing_ready(legacy_definition) || + legacy_definition.provenance.geometry_status == + TrackGeometryStatus::provisional); + } + + assert_failure_preserves_active(replace_once(valid_json, "\"schema_version\": 4", "\"schema_version\": 1"), TrackLoadResult::unsupported_version); assert_failure_preserves_active( @@ -153,18 +168,6 @@ int main(const int argc, char** argv) "\"lat_deg\": 51.99995,\n \"lon_deg\": -0.99998", "\"lat_deg\": 52.00005,\n \"lon_deg\": -1.00002"), TrackLoadResult::degenerate_finish_gate, TrackDefinitionField::gates_finish); - assert_failure_preserves_active( - replace_once_after(valid_json, "\"pit_entry\"", - "\"lat_deg\": 52.00005,\n \"lon_deg\": -1.0", - "\"lat_deg\": 52.00015,\n \"lon_deg\": -1.00004"), - TrackLoadResult::degenerate_pit_entry_gate, - TrackDefinitionField::gates_pit_entry); - assert_failure_preserves_active( - replace_once_after(valid_json, "\"pit_exit\"", - "\"lat_deg\": 51.99985,\n \"lon_deg\": -0.99996", - "\"lat_deg\": 51.99995,\n \"lon_deg\": -1.0"), - TrackLoadResult::degenerate_pit_exit_gate, - TrackDefinitionField::gates_pit_exit); assert_failure_preserves_active( replace_once(valid_json, "\"minimum_crossing_speed_mps\": 2.0", "\"minimum_crossing_speed_mps\": 0.0"), @@ -177,9 +180,6 @@ int main(const int argc, char** argv) replace_once_after(valid_json, "\"start\"", "\"left\": {", "\"left\": {\"unknown_geometry\": 1,"), TrackLoadResult::invalid_json); - assert_failure_preserves_active( - replace_once(valid_json, "\"pit_exit\": {", "\"pit_entry\": {"), - TrackLoadResult::invalid_json); const std::string with_unknown = replace_once( valid_json, "\"country\": \"XX\",", @@ -248,8 +248,6 @@ int main(const int argc, char** argv) TrackLoadResult::capacity_exceeded, TrackLoadResult::degenerate_start_gate, TrackLoadResult::degenerate_finish_gate, - TrackLoadResult::degenerate_pit_entry_gate, - TrackLoadResult::degenerate_pit_exit_gate, TrackLoadResult::degenerate_sector_gate, TrackLoadResult::duplicate_sector_id, TrackLoadResult::geometry_outside_geofence}) { @@ -266,8 +264,6 @@ int main(const int argc, char** argv) TrackDefinitionField::geofence, TrackDefinitionField::gates_start, TrackDefinitionField::gates_finish, - TrackDefinitionField::gates_pit_entry, - TrackDefinitionField::gates_pit_exit, TrackDefinitionField::timing, TrackDefinitionField::sectors}) { assert(track_definition_field_name(field)[0] != '\0'); diff --git a/tests/test_test_track_pack.py b/tests/test_test_track_pack.py index 720e6ba..d55455c 100644 --- a/tests/test_test_track_pack.py +++ b/tests/test_test_track_pack.py @@ -171,20 +171,6 @@ def test_the_start_line_is_clear_of_the_outbound_leg(self): on_return = min(_distance_to_segment(local, loop[i], loop[i + 1]) for i in range(11, 13)) self.assertLess(on_return, 5.0) - def test_the_pit_exit_faces_the_way_the_car_leaves_the_pit_lane(self): - """Both pit gates are on Harbour Road, so pit_exit is passed twice per stop. - - Once north-bound on the way in, once south-bound on the way out. Only the heading - separates them, so a reversed heading would register every exit on the way in and - miss it on the way out. - """ - _, definition = self._build() - heading = definition["gates"]["pit_exit"]["direction_heading_deg"] - self.assertAlmostEqual(heading, 147.3, places=1) - tolerance = definition["gates"]["pit_exit"]["heading_tolerance_deg"] - # The two passes are 180 degrees apart, so the tolerance must not span both. - self.assertLess(tolerance, 90.0) - def test_the_pack_cannot_quietly_become_a_uk_circuit_pack(self): """The UK pack's rules are its own claims, and this pack must not inherit them. diff --git a/tests/test_track_schema.py b/tests/test_track_schema.py index 04ef4b3..f79f6bd 100644 --- a/tests/test_track_schema.py +++ b/tests/test_track_schema.py @@ -31,7 +31,7 @@ def test_wrong_track_id_type_is_rejected(self): self.assertTrue(errors) def test_all_four_gates_are_required(self): - for gate_name in ("start", "finish", "pit_entry", "pit_exit"): + for gate_name in ("start", "finish"): with self.subTest(gate=gate_name): invalid = deepcopy(self.example) del invalid["gates"][gate_name] @@ -92,7 +92,7 @@ def test_sector_fields_and_unknown_properties_are_strict(self): def test_invalid_gate_limits_are_rejected(self): invalid = deepcopy(self.example) - invalid["gates"]["pit_exit"]["minimum_crossing_speed_mps"] = 0 + invalid["gates"]["finish"]["minimum_crossing_speed_mps"] = 0 errors = list(Draft202012Validator(self.schema).iter_errors(invalid)) self.assertTrue(errors) @@ -104,12 +104,12 @@ def test_start_and_finish_may_share_geometry(self): def test_degenerate_gate_is_rejected_semantically(self): invalid = deepcopy(self.example) - invalid["gates"]["pit_entry"]["right"] = deepcopy( - invalid["gates"]["pit_entry"]["left"] + invalid["gates"]["finish"]["right"] = deepcopy( + invalid["gates"]["finish"]["left"] ) self.assertEqual( validate_track_geometry(invalid), - ["gates.pit_entry: directed gate is shorter than 1 metre"], + ["gates.finish: directed gate is shorter than 1 metre"], ) def test_duplicate_sector_identifier_names_exact_field(self): diff --git a/tests/test_track_workbench.py b/tests/test_track_workbench.py index e0bb8b8..96578d1 100644 --- a/tests/test_track_workbench.py +++ b/tests/test_track_workbench.py @@ -20,8 +20,6 @@ def gate_specs(self): return { "start": (52.0, -1.0, 90.0, 20.0), "finish": (52.0, -1.0, 90.0, 20.0), - "pit_entry": (52.0002, -1.0, 90.0, 15.0), - "pit_exit": (51.9998, -1.0, 90.0, 15.0), } def definition(self): @@ -105,12 +103,12 @@ def test_invalid_geometry_never_imports(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) candidate = self.definition() - candidate["gates"]["pit_entry"]["right"] = candidate["gates"]["pit_entry"][ + candidate["gates"]["finish"]["right"] = candidate["gates"]["finish"][ "left" ] source = root / "invalid.json" source.write_text(json.dumps(candidate), encoding="utf-8") - with self.assertRaisesRegex(TrackWorkbenchError, "gates.pit_entry"): + with self.assertRaisesRegex(TrackWorkbenchError, "gates.finish"): import_definition(source, root / "database") self.assertFalse((root / "database" / "workbench_test.json").exists()) diff --git a/tests/test_uk_track_pack.py b/tests/test_uk_track_pack.py index 52ee256..4d72498 100644 --- a/tests/test_uk_track_pack.py +++ b/tests/test_uk_track_pack.py @@ -7,7 +7,6 @@ import zipfile from tools.build_track_pack import ( - MINIMUM_START_PIT_SEPARATION_M, PROMOTED_STATUSES, TrackPackError, build_pack, @@ -161,44 +160,15 @@ def build_with(validation): result = build_with(None) self.assertEqual(result["timing_ready_count"], 0) - def test_a_start_gate_copied_from_a_pit_gate_is_refused(self): - """How two gates in this pack came to sit on a corner instead of a straight. + def test_the_shipped_manifest_validates(self): + """What survives of the start-line check now that the pit gates are gone. - Silverstone's national profile and Llandow both carried a start line whose - coordinates were byte-identical to their pit entry. That is not a close call on a - tight circuit, it is the mark of a gate derived from the pit lane rather than from - the start/finish straight - and it put both lines at the junction where the - preceding corner meets the pit straight, where heading changes fastest and the - direction check is least reliable. + The original defect was a start line copied from a pit gate's position, caught by + a minimum separation between the two. With no pit gates to be coincident with, + that check and its threshold are gone. The manifest still has to validate, which + is what would fail if a profile lost or malformed a gate. """ - manifest = json.loads(UK_MANIFEST.read_text(encoding="utf-8")) - - # Nothing in the shipped pack may regress to that state. - validate_manifest(manifest) - - def validate_with_start(start): - candidate = deepcopy(manifest) - venue = next(v for v in candidate["venues"] if v["venue_id"] == "llandow") - venue["gate_profiles"]["full"]["start"] = start - validate_manifest(candidate) - - llandow = next(v for v in manifest["venues"] if v["venue_id"] == "llandow") - pit_entry = llandow["gate_profiles"]["full"]["pit_entry"] - - # The exact failure that shipped: the pit gate's position, reused. - with self.assertRaisesRegex(TrackPackError, "coincident with pit_entry"): - validate_with_start([pit_entry[0], pit_entry[1], 89.5, 20]) - - # And near-coincidence, since a copy nudged by a rounding error is still a copy. - # One degree of latitude is about 111 km, so this offset is well under a metre. - with self.assertRaisesRegex(TrackPackError, "coincident with pit_entry"): - validate_with_start([pit_entry[0] + 0.000005, pit_entry[1], 89.5, 20]) - - # A genuine separation is accepted. 0.001 degrees of latitude is about 111 m, - # comfortably clear of the threshold without asserting anything about where a real - # start line sits relative to its pit entry. - self.assertLess(MINIMUM_START_PIT_SEPARATION_M, 111.0) - validate_with_start([pit_entry[0] + 0.001, pit_entry[1], 89.5, 20]) + validate_manifest(json.loads(UK_MANIFEST.read_text(encoding="utf-8"))) def test_osm_derivation_never_claims_timing_readiness(self): snapshot = { diff --git a/tools/build_track_pack.py b/tools/build_track_pack.py index 34f7ace..001cea8 100644 --- a/tools/build_track_pack.py +++ b/tools/build_track_pack.py @@ -27,14 +27,12 @@ TRACK_ID_PATTERN = re.compile(r"^[A-Za-z0-9._-]{1,47}$") PACK_ID_PATTERN = re.compile(r"^[a-z0-9][a-z0-9-]{1,46}$") COUNTRY_PATTERN = re.compile(r"^[A-Z]{2,3}$") -GATE_NAMES = ("start", "finish", "pit_entry", "pit_exit") +GATE_NAMES = ("start", "finish") # A start line and a pit gate can sit close together on a real circuit, but they are # never the same point. Identical coordinates mean the start gate was copied from the # pit gate instead of being derived from the start/finish straight, which is how two # gates in this pack came to sit on the corner before the pit straight. The threshold -# is below GNSS noise, so it cannot reject a position that was genuinely surveyed. -MINIMUM_START_PIT_SEPARATION_M = 5.0 class TrackPackError(ValueError): @@ -144,14 +142,6 @@ def validate_manifest(manifest: dict) -> None: and 1 <= gate[3] <= 1_000, f"{path}.gate_profiles.{profile_name}.{gate_name} is invalid", ) - for pit_gate in ("pit_entry", "pit_exit"): - _require( - _separation_m(profile["start"], profile[pit_gate]) - >= MINIMUM_START_PIT_SEPARATION_M, - f"{path}.gate_profiles.{profile_name}.start is coincident with " - f"{pit_gate}, so it was taken from the pit gate rather than from the " - "start/finish straight", - ) layouts = venue.get("layouts") _require(isinstance(layouts, list) and layouts, f"{path}.layouts missing") diff --git a/tools/derive_osm_gates.py b/tools/derive_osm_gates.py index 1292a31..3473150 100644 --- a/tools/derive_osm_gates.py +++ b/tools/derive_osm_gates.py @@ -89,20 +89,12 @@ def derive(snapshot: dict, pit_way_id: int) -> dict: main_geometry = main_way["geometry"] main_heading = tangent_heading(main_geometry, main_index) start_finish = gate_candidate(main_geometry[main_index], main_heading, 20.0) - pit_entry_heading = bearing_deg( - pit_geometry[0], pit_geometry[min(2, len(pit_geometry) - 1)] - ) - pit_exit_heading = bearing_deg( - pit_geometry[max(0, len(pit_geometry) - 3)], pit_geometry[-1] - ) return { "geometry_status": "provisional", "source_way_ids": {"main": main_way_id, "pit": pit_way_id}, "gates": { "start": start_finish, "finish": start_finish, - "pit_entry": gate_candidate(pit_geometry[0], pit_entry_heading, 12.0), - "pit_exit": gate_candidate(pit_geometry[-1], pit_exit_heading, 12.0), }, } diff --git a/tools/track_workbench.py b/tools/track_workbench.py index 5dc126f..28de929 100644 --- a/tools/track_workbench.py +++ b/tools/track_workbench.py @@ -21,7 +21,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1] DEFAULT_SCHEMA_PATH = REPO_ROOT / "data" / "tracks" / "schema.json" -GATE_NAMES = ("start", "finish", "pit_entry", "pit_exit") +GATE_NAMES = ("start", "finish") GEOMETRY_STATUSES = ( "provisional", "device_captured", @@ -191,17 +191,16 @@ def create_definition( gates = {} for gate_name in GATE_NAMES: latitude, longitude, heading, width = gate_specs[gate_name] - is_pit_gate = gate_name in ("pit_entry", "pit_exit") gates[gate_name] = derive_gate( latitude, longitude, heading, width, - minimum_crossing_speed_mps=1.0 if is_pit_gate else 2.0, - rearm_corridor_m=10.0 if is_pit_gate else 15.0, + minimum_crossing_speed_mps=2.0, + rearm_corridor_m=15.0, ) instance = { - "schema_version": 3, + "schema_version": 4, "revision": 1, "track_id": track_id, "name": name, diff --git a/tools/validate_tracks.py b/tools/validate_tracks.py index a4d10af..23aa9bc 100644 --- a/tools/validate_tracks.py +++ b/tools/validate_tracks.py @@ -11,7 +11,7 @@ EARTH_RADIUS_M = 6_371_000.0 -REQUIRED_GATES = ("start", "finish", "pit_entry", "pit_exit") +REQUIRED_GATES = ("start", "finish") MINIMUM_GATE_LENGTH_M = 1.0 MAXIMUM_GATE_LENGTH_M = 1_000.0 MINIMUM_CROSSING_ANGLE_DEG = 10.0