MotorSignal-App/docs/CODE_MAP.md

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)

  1. Acquisition (lib/services/location_service.dart)

    • Geolocator.getPositionStream() initializes the Android Fused Location Provider.
    • Settings: LocationAccuracy.bestForNavigation at a requested 100ms interval.
    • Triggers _handleRawPosition() upon receiving an OS-level GPS fix.
  2. 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.
  3. 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.90 when delta > 0.4 m/s (preserves instant responsiveness when accelerating/braking).
      • alpha = 0.75 for cruising.
    • The smoothed speed (in m/s) is added to _controller.stream.
  4. 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 a TelemetrySample.
  5. 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 MPH gate to prevent kickstand or walking artifacts from registering as personal records.
  6. UI Display (lib/features/dashboard/widgets/speed_readout.dart)

    • Rebuilding the entire dashboard at 20Hz would cause severe UI lag.
    • ResponsiveDashboardLayout isolates updates using the SpeedReadout leaf widget.
    • SpeedReadout watches telemetryStreamProvider.select((s) => s.value?.speed ?? 0.0).
    • 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.speed in LocationService (m/s directly from OS, unfiltered).
  • Filtered Speed: _emaSpeed in LocationService (m/s after EMA and deadband).
  • Telemetry Speed: sample.speed in TelemetrySample (converted to MPH, sampled at 20Hz).
  • Display Speed: Clamped to >= 0.0 inside SpeedReadout before integer rounding.
  • Top Speed (Current implementation): state.topSpeedMph managed by RideStateMachine, rejecting single spikes > 220 MPH.
  • Average Speed: Derived in RideStateMachine ((tripDistanceMiles / newMovingTime) * 3600) — relies heavily on the 1.8 MPH threshold determining movingTime.

D. Boundaries (High-Risk Concepts)

The following behaviors are load-bearing and architecturally high-risk:

  1. The 20Hz / 50ms Clock: TelemetryService._samplingTimer is the primary telemetry sampling clock. The UI does not poll for location or dictate sampling frequency.
  2. Stationary Deadband (< 0.6 m/s): Enforced in LocationService to prevent the speedometer from bouncing while idling.
  3. Dynamic Metrics Gate (< 3.0 MPH): RideStateMachine ignores lean angles and heavy G-forces when rolling slowly.
  4. Auto-Start Gate (>= 1.5 MPH): RideStateMachine uses this threshold to transition from ready to active recording.
  5. 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.
  6. 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 '--' on SpeedReadout rather than freezing on the last measured speed or fabricating 0 MPH.
  7. 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.topSpeedMph updates only when sample.isSpeedFresh && sample.speed != null && sample.speed! <= 220.0.
    • Moving Time & Stopped Time: Sub-second continuous time accumulation (_movingTimeAccumulator / _stoppedTimeAccumulator) requires sample.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 (launched or countdown) abort with PerformanceAbortReason.gpsLost when GPS becomes stale. ready state reverts to idle on stale GPS, preventing false movement detection or high-speed launch anomalies upon recovery.
    • Performance Timer Service: Active runs in timing state abort when GPS becomes stale, preventing false 0-60/0-100 completions across outages.
  8. Heading & Location Freshness Invariant:
    • Heading Selection: TelemetryService._emitSample() selects GPS course (_latestPosition.heading) only when GPS is fresh (isGpsFresh), _latestPosition.heading >= 0, and speedMph > 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: DashboardMapPanel consumes s.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 subtle LAST KNOWN LOCATION pill badge is displayed. When fresh GPS recovers, the badge disappears and the marker restores full CyberCyan.
    • GPS Status Indicator: GPSStatusIndicator evaluates isGpsFresh. When GPS is stale, it truthfully indicates NO GPS with Icons.gps_off rather than falsely reporting GPS 3D FIX from cached accuracy.

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.
  • 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 to recordingActive.
  • test/truthful_speed_presentation_test.dart:
    • Validates speed freshness semantics on TelemetrySample and SpeedReadout presentation across normal moving GPS, 10 Hz / 20 Hz duplication, GNSS outages, non-zero placeholder behavior ('--'), genuine stopped motorcycle (0 MPH), automatic recovery, and ride-independent operation.
  • 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.md
  • docs/DEBUGGING_MAP.md

G. Change Scope

  • lib/features/rides/ride_state_machine.dart: Added _lastSeenGpsTimestamp tracking so GPS evidence freshness advances only on genuinely newer GNSS fixes. Aligned lifecycle reset in _resetHealthEvidence() and reset(). Gated newTopSpeed, isDynamicallyMoving, continuous moving/stopped time accumulation, distance accumulation, elevation accumulation, and 'High Speed' event detection on sample.isSpeedFresh.
  • lib/models/telemetry_sample.dart: Added isSpeedFresh, freshSpeed, and isGpsFresh getters reusing RecordingHealthContract.evidenceStaleAfterMs (4000 ms).
  • lib/features/dashboard/widgets/speed_readout.dart: Updated SpeedReadout to present freshSpeed and render '--' when stale.
  • lib/features/performance/services/performance_launch_state_machine.dart: Added stale GPS check in _onTelemetryUpdate to abort active runs (launched, countdown) with PerformanceAbortReason.gpsLost and revert ready to idle.
  • lib/features/performance/performance_timer_service.dart: Added stale GPS check in _onTelemetryUpdate to abort timing state on stale GPS.
  • lib/services/telemetry_service.dart: Gated GPS heading selection on isGpsFresh using RecordingHealthContract.evidenceStaleAfterMs (4000 ms).
  • lib/features/dashboard/widgets/dashboard_map_panel.dart: Dimmed vehicle marker and added LAST KNOWN LOCATION pill badge when !isGpsFresh.
  • lib/features/dashboard/widgets/gps_status_indicator.dart: Required isGpsFresh for 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.