12 KiB
12 KiB
MOTORSIGNAL DEVELOPER OWNERSHIP MAP — PHASE 1
A. Source State
- Known Baseline Branch:
master - Checkpoint Commit:
d3caf37bb2ff0031bec5982791b77127dd341cbd(Merge pull request #3) - No tracked or staged modifications exist in production files.
B. Data Flow (Sensor to Pixels)
-
Acquisition (
lib/services/location_service.dart)Geolocator.getPositionStream()initializes the Android Fused Location Provider.- Settings:
LocationAccuracy.bestForNavigationat a requested 100ms interval. - Triggers
_handleRawPosition()upon receiving an OS-level GPS fix.
-
Validation (
LocationService._isValidPosition)- Validates the fix before applying logic.
- Drops fixes with > 200m accuracy (bootstrapping) or > 80m-100m accuracy (moving).
- Drops impossible speeds (> 100.0 m/s or ~223 MPH).
- Drops impossible acceleration (> 30.0 m/s²) and detects GPS teleportation over 50 meters.
-
Filtering & Smoothing (
LocationService._applyEmaFilter)- Stationary Jitter Deadband: Speeds under 0.6 m/s (~1.3 MPH) are forced to 0.0 to prevent drifting at stoplights.
- Fast Dynamic Exponential Moving Average (EMA):
alpha = 0.90when delta > 0.4 m/s (preserves instant responsiveness when accelerating/braking).alpha = 0.75for cruising.
- The smoothed speed (in m/s) is added to
_controller.stream.
-
Sampling & The Primary Clock (
lib/services/telemetry_service.dart)- Subscribes to the location stream but decouples acquisition from reporting.
- Uses a
Timer.periodic(const Duration(milliseconds: 50))to establish a requested 20Hz monotonic clock. _emitSample()reads the latest filtered position, converts speed from m/s to MPH (speed * 2.23694), and emits aTelemetrySample.
-
State Aggregation (
lib/features/rides/ride_state_machine.dart)- Consumes
TelemetrySamples at 20Hz via_onTelemetryUpdate. - Modifies dynamic metric tracking (
_processStats) and performs auto-starts (gate:>= 1.5 MPH). - Isolates motorcycle dynamics (Top Speed, Lean Angle, G-Force) behind a
>= 3.0 MPHgate to prevent kickstand or walking artifacts from registering as personal records.
- Consumes
-
UI Display (
lib/features/dashboard/widgets/speed_readout.dart)- Rebuilding the entire dashboard at 20Hz would cause severe UI lag.
ResponsiveDashboardLayoutisolates updates using theSpeedReadoutleaf widget.SpeedReadoutselects freshness-awaresample.freshSpeedfromtelemetryStreamProvider; unavailable/stale speed renders as--.- Clamps the value to
>= 0.0, converts to an integer string (toStringAsFixed(0)), and renders.
C. Discoveries (Current implementation speeds)
MotorSignal manages speed in several distinct concepts:
- Raw GNSS Speed:
position.speedinLocationService(m/s directly from OS, unfiltered). - Filtered Speed:
_emaSpeedinLocationService(m/s after EMA and deadband). - Telemetry Speed:
sample.speedinTelemetrySample(converted to MPH, sampled at 20Hz). - Display Speed: Clamped to
>= 0.0insideSpeedReadoutbefore integer rounding. - Top Speed (Current implementation):
state.topSpeedMphmanaged byRideStateMachine, rejecting single spikes > 220 MPH. - Average Speed: Derived in
RideStateMachine((tripDistanceMiles / newMovingTime) * 3600) — relies heavily on the1.8 MPHthreshold determiningmovingTime.
D. Boundaries (High-Risk Concepts)
The following behaviors are load-bearing and architecturally high-risk:
- The 20Hz / 50ms Clock:
TelemetryService._samplingTimeris the primary telemetry sampling clock. The UI does not poll for location or dictate sampling frequency. - Stationary Deadband (< 0.6 m/s): Enforced in
LocationServiceto prevent the speedometer from bouncing while idling. - Dynamic Metrics Gate (< 3.0 MPH):
RideStateMachineignores lean angles and heavy G-forces when rolling slowly. - Auto-Start Gate (>= 1.5 MPH):
RideStateMachineuses this threshold to transition fromreadyto active recording. - GPS Freshness Invariant: GPS freshness advances from genuinely newer underlying GNSS evidence, not merely from creation of a new telemetry wrapper. Repeated or cached GNSS fixes repackaged into 20 Hz
TelemetrySamples do not refresh_lastGpsEvidenceMonotonicMs. - Speed Freshness Invariant: Fresh telemetry wrapper does not imply fresh GNSS-derived speed. Speed freshness (
TelemetrySample.isSpeedFresh/TelemetrySample.freshSpeed) is valid only while the underlying GNSS fix age is within the canonical 4000 ms staleness bound. Stale GNSS fixes render as'--'onSpeedReadoutrather than freezing on the last measured speed or fabricating 0 MPH. - GNSS-Derived Metrics Integrity Invariant: Stale GNSS evidence may remain preserved in raw telemetry (
sample.speed), but it must not create new GNSS-derived ride facts:- Top Speed:
state.topSpeedMphupdates only whensample.isSpeedFresh && sample.speed != null && sample.speed! <= 220.0. - Moving Time & Stopped Time: Sub-second continuous time accumulation (
_movingTimeAccumulator/_stoppedTimeAccumulator) requiressample.isSpeedFresh && _lastSample!.isSpeedFresh. Stale samples do not accumulate moving or stopped time. - Dynamic Motorcycle Metrics Gate: Dynamic metrics (max lean, max accel/braking G) require
sample.isSpeedFresh && sample.speed != null && sample.speed! >= 3.0, preventing stationary/kickstand artifacts during a GPS outage from polluting records. - Trip Distance & Elevation: Requires
hasFreshGps = sample.isSpeedFresh && (_lastSample?.isSpeedFresh ?? false). Distance and elevation do not accumulate across an outage, preventing artificial catch-up jumps upon recovery. - High Speed Event Detection: Logging 'High Speed' requires
sample.isSpeedFresh && sample.speed != null && sample.speed! > 100.0, preventing repeated events during an outage. - Performance Launch State Machine: Active runs (
launchedorcountdown) abort withPerformanceAbortReason.gpsLostwhen GPS becomes stale.readystate reverts toidleon stale GPS, preventing false movement detection or high-speed launch anomalies upon recovery. - Performance Timer Service: Active runs in
timingstate abort when GPS becomes stale, preventing false 0-60/0-100 completions across outages.
- Top Speed:
- Heading & Location Freshness Invariant:
- Heading Selection:
TelemetryService._emitSample()selects GPS course (_latestPosition.heading) only when GPS is fresh (isGpsFresh),_latestPosition.heading >= 0, andspeedMph > 3.0. When GPS becomes stale (> 4000 ms), heading selection immediately falls back to live hardware compass azimuth (_latestCompassHeading), preventing stale course angles from overriding the rider's actual orientation. When fresh GPS resumes and speed exceeds 3.0 MPH, GPS heading automatically recovers. Normal 10 Hz / 20 Hz sample repetitions within 4000 ms maintain heading stability without flicker. - Location & Map Presentation:
DashboardMapPanelconsumess.value?.isGpsFresh. When GPS coordinates are fresh, the vehicle marker renders in active CyberCyan without warning overlays. When GPS becomes stale (> 4000 ms), the last-known coordinate is retained without artificial dead-reckoning or fake drift, the vehicle marker is dimmed (alpha 0.38), and a subtleLAST KNOWN LOCATIONpill badge is displayed. When fresh GPS recovers, the badge disappears and the marker restores full CyberCyan. - GPS Status Indicator:
GPSStatusIndicatorevaluatesisGpsFresh. When GPS is stale, it truthfully indicatesNO GPSwithIcons.gps_offrather than falsely reportingGPS 3D FIXfrom cached accuracy.
- Heading Selection:
E. Tests
test/location_service_test.dart:- Protects physical/GPS thresholds (0.6 m/s deadband == 1.34 MPH, 100 m/s limit == 223 MPH, 3.0s 0-60 == 0.91g).
test/telemetry_validation_test.dart:- Validates
TelemetryValidator's sanity checks, such as rejecting > 220 MPH jumps and flagging poor GPS confidence.
- Validates
test/stale_gps_regression_test.dart:- Validates GPS evidence freshness semantics: duplicate fix timestamps do not refresh GPS freshness, genuinely newer fixes refresh evidence, older out-of-order fixes are ignored, null fixes do not refresh evidence, normal 10 Hz / 20 Hz sampling remains healthy, GPS outages degrade health to
recordingDegraded, and fresh fixes recover torecordingActive.
- Validates GPS evidence freshness semantics: duplicate fix timestamps do not refresh GPS freshness, genuinely newer fixes refresh evidence, older out-of-order fixes are ignored, null fixes do not refresh evidence, normal 10 Hz / 20 Hz sampling remains healthy, GPS outages degrade health to
test/truthful_speed_presentation_test.dart:- Validates speed freshness semantics on
TelemetrySampleandSpeedReadoutpresentation across normal moving GPS, 10 Hz / 20 Hz duplication, GNSS outages, non-zero placeholder behavior ('--'), genuine stopped motorcycle (0MPH), automatic recovery, and ride-independent operation.
- Validates speed freshness semantics on
test/gnss_metrics_integrity_test.dart:- Validates GNSS-derived metrics integrity during stale GPS: moving time cessation, distance cessation, top speed isolation, boundary conditions at 3999ms / 4000ms / 4001ms, recovery without jump distance or time integration across outage, dynamic motorcycle metrics isolation, High Speed event deduplication, PerformanceLaunchStateMachine abort on stale GPS in launched/countdown and reversion in ready, and PerformanceTimerService abort in timing.
test/truthful_heading_location_test.dart:- Validates heading selection and map/location presentation truthfulness during stale GPS: GPS course priority when moving fresh, normal 10 Hz / 20 Hz fix stability, fallback to live compass when GPS is stale (> 4000ms), automatic heading recovery on fresh GPS, 3999/4000/4001ms boundaries, map marker dimming, LAST KNOWN LOCATION pill badge, absence of fake drift / dead reckoning, automatic map recovery, ride independence, and raw forensic data preservation.
F. Documents Created
docs/CODE_MAP.mddocs/DEBUGGING_MAP.md
G. Change Scope
lib/features/rides/ride_state_machine.dart: Added_lastSeenGpsTimestamptracking so GPS evidence freshness advances only on genuinely newer GNSS fixes. Aligned lifecycle reset in_resetHealthEvidence()andreset(). GatednewTopSpeed,isDynamicallyMoving, continuous moving/stopped time accumulation, distance accumulation, elevation accumulation, and 'High Speed' event detection onsample.isSpeedFresh.lib/models/telemetry_sample.dart: AddedisSpeedFresh,freshSpeed, andisGpsFreshgetters reusingRecordingHealthContract.evidenceStaleAfterMs(4000 ms).lib/features/dashboard/widgets/speed_readout.dart: UpdatedSpeedReadoutto presentfreshSpeedand render'--'when stale.lib/features/performance/services/performance_launch_state_machine.dart: Added stale GPS check in_onTelemetryUpdateto abort active runs (launched,countdown) withPerformanceAbortReason.gpsLostand revertreadytoidle.lib/features/performance/performance_timer_service.dart: Added stale GPS check in_onTelemetryUpdateto aborttimingstate on stale GPS.lib/services/telemetry_service.dart: Gated GPS heading selection onisGpsFreshusingRecordingHealthContract.evidenceStaleAfterMs(4000 ms).lib/features/dashboard/widgets/dashboard_map_panel.dart: Dimmed vehicle marker and addedLAST KNOWN LOCATIONpill badge when!isGpsFresh.lib/features/dashboard/widgets/gps_status_indicator.dart: RequiredisGpsFreshfor reporting active GPS fix status.test/stale_gps_regression_test.dart: Added comprehensive behavioral test suite for stale GPS, duplicate fixes, outage degradation, and recovery.test/truthful_speed_presentation_test.dart: Added comprehensive behavioral and widget test suite for truthful speed presentation.test/gnss_metrics_integrity_test.dart: Added comprehensive behavioral test suite for GNSS-derived metrics integrity during stale GPS.test/truthful_heading_location_test.dart: Added comprehensive behavioral and widget test suite for truthful heading selection and location presentation.
H. Remaining Issues / Future Considerations
- All primary GNSS-derived subsystems (recording health, speedometer presentation, ride statistics, performance launch timers, heading selection, map/location presentation, and GPS status indicator) now operate under a single, unified canonical freshness contract (4000 ms bound). No further unaddressed stale GNSS corruption vectors remain in core telemetry paths.