import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/responsive/viewport_size.dart'; import '../../core/theme/theme_extensions.dart'; import '../../services/telemetry_service.dart'; /// Canonical responsive diagnostics screen displaying sensor health, GNSS fidelity, and raw IMU readings. class DiagnosticsScreen extends ConsumerWidget { const DiagnosticsScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final snapshot = context.responsive; final isWide = snapshot.isExpanded || (snapshot.isMedium && snapshot.isLandscape); return Scaffold( appBar: AppBar( title: const Text( 'Sensor Diagnostics', style: TextStyle(fontWeight: FontWeight.w800, letterSpacing: 0.5), ), ), body: SafeArea( child: isWide ? _buildWideLayout(context) : _buildCompactLayout(context), ), ); } Widget _buildCompactLayout(BuildContext context) { return const SingleChildScrollView( padding: EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _TimestampHeaderCard(), SizedBox(height: 12), _SensorHealthCard(), SizedBox(height: 12), _GpsDataCard(), SizedBox(height: 12), _ImuGForceCard(), SizedBox(height: 16), ], ), ); } Widget _buildWideLayout(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 1200), child: const Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _TimestampHeaderCard(), SizedBox(height: 16), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( children: [ _SensorHealthCard(), SizedBox(height: 16), _GpsDataCard(), ], ), ), SizedBox(width: 16), Expanded( child: _ImuGForceCard(), ), ], ), ], ), ), ), ); } } /// Header card displaying live sample timestamp isolated to prevent full-screen redraws. class _TimestampHeaderCard extends ConsumerWidget { const _TimestampHeaderCard(); @override Widget build(BuildContext context, WidgetRef ref) { final colors = context.motorColors; final timestamp = ref.watch( telemetryStreamProvider.select((async) => async.value?.timestamp), ); return Material( color: colors.surfaceContainer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 10.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Row( children: [ Icon(Icons.access_time_rounded, color: colors.cyberCyan, size: 16), const SizedBox(width: 6), Flexible( child: Text( 'SAMPLE TIME (UTC)', overflow: TextOverflow.ellipsis, style: TextStyle( fontFamily: 'Sora', fontSize: 11, fontWeight: FontWeight.w800, letterSpacing: 0.5, color: colors.cyberCyan, ), ), ), ], ), ), const SizedBox(width: 8), Text( timestamp != null ? timestamp.toUtc().toIso8601String().substring(11, 23) : 'Awaiting samples...', style: TextStyle( fontFamily: 'monospace', fontSize: 11, fontWeight: FontWeight.w700, color: timestamp != null ? Theme.of(context).colorScheme.onSurface : Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ), ), ); } } /// Diagnostic health indicator card checking GPS, IMU, and heading signals truthfully. class _SensorHealthCard extends ConsumerWidget { const _SensorHealthCard(); @override Widget build(BuildContext context, WidgetRef ref) { final colors = context.motorColors; final telemetry = ref.watch(telemetryStreamProvider).value; final gpsAccuracy = telemetry?.gpsAccuracy; String gpsHealthText = 'No Fix (Unavailable)'; Color gpsHealthColor = colors.raceRed; IconData gpsIcon = Icons.cancel_outlined; if (gpsAccuracy != null) { if (gpsAccuracy <= 15.0) { gpsHealthText = 'Healthy (±${gpsAccuracy.toStringAsFixed(1)}m)'; gpsHealthColor = colors.launchGreen; gpsIcon = Icons.check_circle_outline; } else if (gpsAccuracy <= 35.0) { gpsHealthText = 'Degraded (±${gpsAccuracy.toStringAsFixed(1)}m)'; gpsHealthColor = colors.warningAmber; gpsIcon = Icons.warning_amber_rounded; } else { gpsHealthText = 'Poor (±${gpsAccuracy.toStringAsFixed(1)}m)'; gpsHealthColor = colors.raceRed; gpsIcon = Icons.error_outline; } } final hasImu = telemetry?.accelX != null || telemetry?.accelY != null || telemetry?.accelZ != null; final hasHeading = telemetry?.heading != null; return Material( color: colors.surfaceContainer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(14.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.monitor_heart, color: colors.cyberCyan, size: 18), const SizedBox(width: 8), Expanded( child: Text( 'SENSOR HEALTH & SIGNAL', style: TextStyle( fontFamily: 'Sora', fontSize: 12, fontWeight: FontWeight.w800, letterSpacing: 0.8, color: colors.cyberCyan, ), ), ), ], ), const SizedBox(height: 12), _buildHealthItem( context, label: 'GPS Constellation Fix', statusText: gpsHealthText, statusColor: gpsHealthColor, icon: gpsIcon, ), const Divider(height: 14), _buildHealthItem( context, label: 'IMU / Accel (~50Hz)', statusText: hasImu ? 'Active & Streaming' : 'No Data / Unavailable', statusColor: hasImu ? colors.launchGreen : colors.outlineVariant, icon: hasImu ? Icons.check_circle_outline : Icons.remove_circle_outline, ), const Divider(height: 14), _buildHealthItem( context, label: 'Compass / Gyro Fusion', statusText: hasHeading ? 'Active & Calibrated' : 'No Data / Unavailable', statusColor: hasHeading ? colors.launchGreen : colors.outlineVariant, icon: hasHeading ? Icons.check_circle_outline : Icons.remove_circle_outline, ), ], ), ), ); } Widget _buildHealthItem( BuildContext context, { required String label, required String statusText, required Color statusColor, required IconData icon, }) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface, ), ), ), const SizedBox(width: 6), Flexible( child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, children: [ Icon(icon, color: statusColor, size: 14), const SizedBox(width: 4), Flexible( child: Text( statusText, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w700, color: statusColor, ), ), ), ], ), ), ], ); } } /// GPS Position & Navigation metrics card. class _GpsDataCard extends ConsumerWidget { const _GpsDataCard(); @override Widget build(BuildContext context, WidgetRef ref) { final colors = context.motorColors; final telemetry = ref.watch(telemetryStreamProvider).value; return Material( color: colors.surfaceContainer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(14.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.gps_fixed, color: colors.cyberCyan, size: 18), const SizedBox(width: 8), Expanded( child: Text( 'GPS & NAVIGATION METRICS', style: TextStyle( fontFamily: 'Sora', fontSize: 12, fontWeight: FontWeight.w800, letterSpacing: 0.8, color: colors.cyberCyan, ), ), ), ], ), const SizedBox(height: 12), _buildMetricRow( context, label: 'GPS Accuracy', value: telemetry?.gpsAccuracy != null ? '±${telemetry!.gpsAccuracy!.toStringAsFixed(1)} m' : 'N/A', ), _buildMetricRow( context, label: 'Latitude', value: telemetry?.latitude?.toStringAsFixed(6) ?? 'N/A', ), _buildMetricRow( context, label: 'Longitude', value: telemetry?.longitude?.toStringAsFixed(6) ?? 'N/A', ), _buildMetricRow( context, label: 'Altitude (MSL)', value: telemetry?.altitude != null ? '${telemetry!.altitude!.toStringAsFixed(1)} m' : 'N/A', ), _buildMetricRow( context, label: 'Ground Speed', value: telemetry?.speed != null ? '${telemetry!.speed!.toStringAsFixed(1)} MPH' : 'N/A', ), _buildMetricRow( context, label: 'Heading', value: telemetry?.heading != null ? '${telemetry!.heading!.toStringAsFixed(1)}°' : 'N/A', ), ], ), ), ); } Widget _buildMetricRow(BuildContext context, {required String label, required String value}) { return Padding( padding: const EdgeInsets.symmetric(vertical: 3.5), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( label, style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), const SizedBox(width: 8), Flexible( child: Text( value, overflow: TextOverflow.ellipsis, style: TextStyle( fontFamily: 'monospace', fontSize: 12, fontWeight: FontWeight.w700, color: Theme.of(context).colorScheme.onSurface, ), ), ), ], ), ); } } /// IMU, G-Force, and Derived Orientation card. class _ImuGForceCard extends ConsumerWidget { const _ImuGForceCard(); @override Widget build(BuildContext context, WidgetRef ref) { final colors = context.motorColors; final telemetry = ref.watch(telemetryStreamProvider).value; return Material( color: colors.surfaceContainer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: colors.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(14.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.screen_rotation, color: colors.cyberCyan, size: 18), const SizedBox(width: 8), Expanded( child: Text( 'IMU, G-FORCE & ORIENTATION', style: TextStyle( fontFamily: 'Sora', fontSize: 12, fontWeight: FontWeight.w800, letterSpacing: 0.8, color: colors.cyberCyan, ), ), ), ], ), const SizedBox(height: 12), () { final accelX = telemetry?.accelX; final accelY = telemetry?.accelY; final accelZ = telemetry?.accelZ; final longG = telemetry?.longitudinalG; final latG = telemetry?.lateralG; final lean = telemetry?.leanAngle; final pitch = telemetry?.pitchAngle; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildMetricRow( context, label: 'Linear Accel X (Lat)', value: accelX != null ? '${accelX.toStringAsFixed(2)} m/s²' : 'N/A', ), _buildMetricRow( context, label: 'Linear Accel Y (Long)', value: accelY != null ? '${accelY.toStringAsFixed(2)} m/s²' : 'N/A', ), _buildMetricRow( context, label: 'Linear Accel Z (Vert)', value: accelZ != null ? '${accelZ.toStringAsFixed(2)} m/s²' : 'N/A', ), const Divider(height: 12), _buildMetricRow( context, label: 'Longitudinal G-Force', value: longG != null ? '${longG.toStringAsFixed(2)} G' : 'N/A', ), _buildMetricRow( context, label: 'Lateral G-Force', value: latG != null ? '${latG.toStringAsFixed(2)} G' : 'N/A', ), const Divider(height: 12), _buildMetricRow( context, label: 'Derived Lean Angle', value: lean != null ? '${lean.abs().toStringAsFixed(1)}° (${lean < 0 ? "L" : (lean > 0 ? "R" : "Center")})' : 'N/A', ), _buildMetricRow( context, label: 'Derived Pitch Angle', value: pitch != null ? '${pitch.toStringAsFixed(1)}°' : 'N/A', ), ], ); }(), ], ), ), ); } Widget _buildMetricRow(BuildContext context, {required String label, required String value}) { return Padding( padding: const EdgeInsets.symmetric(vertical: 3.5), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( label, style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), const SizedBox(width: 8), Flexible( child: Text( value, overflow: TextOverflow.ellipsis, style: TextStyle( fontFamily: 'monospace', fontSize: 12, fontWeight: FontWeight.w700, color: Theme.of(context).colorScheme.onSurface, ), ), ), ], ), ); } }