import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_typography.dart'; import '../../../services/telemetry_service.dart'; /// Structured telemetry observation model for speedometer pipeline. /// /// Designed so future forensic instrumentation can observe raw GNSS speed, /// validated speed, filtered speed, and displayed speed without rewriting UI. class SpeedTelemetryObservation { final double rawGnssSpeedMph; final double validatedSpeedMph; final double filteredSpeedMph; final double displayedSpeedMph; const SpeedTelemetryObservation({ required this.rawGnssSpeedMph, required this.validatedSpeedMph, required this.filteredSpeedMph, required this.displayedSpeedMph, }); } enum SpeedReadoutSize { compact, regular, hero, } /// Authoritative isolated leaf widget for current speed readout. /// /// Subscribes strictly to speed changes via [telemetryStreamProvider] /// to prevent outer dashboard reconstructions at 20 Hz. class SpeedReadout extends ConsumerWidget { final SpeedReadoutSize size; final String unit; final void Function(SpeedTelemetryObservation observation)? onObservation; const SpeedReadout({ super.key, this.size = SpeedReadoutSize.regular, this.unit = 'MPH', this.onObservation, }); @override Widget build(BuildContext context, WidgetRef ref) { final speed = ref.watch( telemetryStreamProvider.select((s) => s.value?.speed ?? 0.0), ); final clampedSpeed = speed < 0.0 ? 0.0 : speed; if (onObservation != null) { onObservation!(SpeedTelemetryObservation( rawGnssSpeedMph: clampedSpeed, validatedSpeedMph: clampedSpeed, filteredSpeedMph: clampedSpeed, displayedSpeedMph: clampedSpeed, )); } final isDark = Theme.of(context).brightness == Brightness.dark; final speedColor = isDark ? Colors.white : AppColors.lightOnSurface; final unitColor = isDark ? AppColors.cyberCyan : AppColors.lightPrimary; final (double fontSize, double unitSize) = switch (size) { SpeedReadoutSize.compact => (54.0, 14.0), SpeedReadoutSize.regular => (96.0, 18.0), SpeedReadoutSize.hero => (140.0, 24.0), }; return RepaintBoundary( child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ FittedBox( fit: BoxFit.scaleDown, child: Text( clampedSpeed.toStringAsFixed(0), style: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: fontSize, fontWeight: FontWeight.w900, fontStyle: FontStyle.italic, height: 0.95, color: speedColor, ), ), ), const SizedBox(height: 2), Text( unit, style: TextStyle( fontFamily: AppTypography.fontTelemetry, fontSize: unitSize, fontWeight: FontWeight.w800, fontStyle: FontStyle.italic, letterSpacing: 2.5, color: unitColor, ), ), ], ), ); } }