// The software dash: smart-serow's HUD layout and widgets, fed from the boundary. // A grid of boxes (lib/widgets/stat_box.dart): the instruments and the big numbers // in the main row, smaller readouts and telltales below. Every value names its // signal from contracts' signals.json and nothing else (app guideline 1). import 'package:flutter/material.dart'; import '../link/boundary.dart'; import '../shell_scope.dart'; import '../link/reading.dart'; import '../theme/palette.dart'; import '../widgets/accel_graph.dart'; import '../widgets/gps_compass.dart'; import '../widgets/stat_box.dart'; import '../widgets/telltale.dart'; import '../widgets/whiskey_mark.dart'; class DashPage extends StatelessWidget { const DashPage({super.key, required this.boundary, required this.themes}); final Boundary boundary; final Themes themes; @override Widget build(BuildContext context) { return ListenableBuilder( listenable: Listenable.merge([boundary, themes]), builder: (context, _) { final p = AppTheme.of(context); final b = boundary; Reading r(String n) => Reading.of(b, n); final rpm = r('engine_rpm'); final speed = r('gnss_speed_kph'); final pedal = r('accel_pedal_pct'); final coolant = r('obd_coolant_temp_c'); final lat = r('imu_accel_lat_g'); final lon = r('imu_accel_lon_g'); // The compass's states, from gnss_fix: none → acquiring, 2d/3d → fix, missing → none known. final fix = r('gnss_fix'); final gpsState = fix.value == null ? null : fix.value == 'none' ? 'acquiring' : 'fix'; return Scaffold( backgroundColor: p.background, body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _DashBar(boundary: b, themes: themes), Expanded( child: Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: Column( children: [ // Main area: instruments and the big numbers. Expanded( flex: 7, child: Row( children: [ Expanded( child: WhiskeyMark( roll: r('imu_roll_deg').number, pitch: r('imu_pitch_deg').number, ), ), Expanded( child: AccelGraph( ax: lat.number, ay: lon.number, maxG: 0.8, ghostTrackPeriod: const Duration(seconds: 4), ), ), Expanded( child: Column( children: [ StatBoxMain( value: rpm.fixed(), label: 'RPM', dim: rpm.dim, isWarning: () => (rpm.number ?? 0) > 6000, ), StatBoxMain( value: speed.fixed(), unit: 'km/h', label: 'GNSS SPEED', dim: speed.dim, ), ], ), ), ], ), ), // Bottom row: smaller readouts and telltales. Expanded( flex: 2, child: Row( children: [ StatBox( value: pedal.fixed(), unit: '%', label: 'PEDAL', dim: pedal.dim, ), StatBox( value: coolant.fixed(), unit: '°C', label: 'COOLANT', dim: coolant.dim, isWarning: () => (coolant.number ?? 0) > 105, ), Expanded( child: GpsCompass( heading: r('gnss_heading_deg').number, gpsState: gpsState, ), ), Telltale( label: 'LEFT', icon: Icons.arrow_left, reading: r('indicator_left'), ), Telltale( label: 'RIGHT', icon: Icons.arrow_right, reading: r('indicator_right'), ), Telltale( label: 'BRAKE', reading: r('brake_pedal'), ), Telltale( label: 'PARK', reading: r('handbrake'), warn: true, ), Telltale( label: 'LIGHTS', reading: r('side_lights'), ), Telltale( label: 'DOOR', reading: r('door_open'), warn: true, ), ], ), ), ], ), ), ), ], ), ), ); }, ); } } /// The dash's own thin top line: the menu, link state, and the dark/bright switch. class _DashBar extends StatelessWidget { const _DashBar({required this.boundary, required this.themes}); final Boundary boundary; final Themes themes; @override Widget build(BuildContext context) { final p = AppTheme.of(context); final (text, color) = switch (boundary.state) { LinkState.up => ('LINK', p.subdued), LinkState.connecting => ('CONNECTING', p.caution), LinkState.down => ('NO LINK', p.alarm), }; return SizedBox( height: 40, child: Row( children: [ MenuButton(color: p.subdued), Text(text, style: TextStyle(color: color, letterSpacing: 2)), const Spacer(), IconButton( tooltip: themes.isDark ? 'Bright' : 'Dark', onPressed: themes.toggle, icon: Icon( themes.isDark ? Icons.light_mode : Icons.dark_mode, color: p.subdued, ), ), ], ), ); } }