118 lines
3.5 KiB
Dart
118 lines
3.5 KiB
Dart
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,
|
|
shadows: isDark
|
|
? [
|
|
Shadow(
|
|
color: AppColors.cyberCyan.withValues(alpha: 0.4),
|
|
blurRadius: 16,
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
unit,
|
|
style: TextStyle(
|
|
fontFamily: AppTypography.fontTelemetry,
|
|
fontSize: unitSize,
|
|
fontWeight: FontWeight.w800,
|
|
fontStyle: FontStyle.italic,
|
|
letterSpacing: 2.5,
|
|
color: unitColor,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|