// The Orin at a glance, the way a phone's status bar shows the phone: the link on the // left, then its links out and its power on the right, and the dark/bright switch. // Drawn by the shell above every view (DASH-NEXT.md item 1). // // An indicator shows its state in palette roles: foreground when fine, caution when // degraded or on battery, alarm when down, subdued when stale or unknown. Things // that are only news when wrong (temperature) appear only then; ones that are only // news when present (the ELM, a modem) appear only then. import 'package:flutter/material.dart'; import '../link/boundary.dart'; import '../link/reading.dart'; import '../theme/palette.dart'; class StatusBar extends StatelessWidget { const StatusBar({ super.key, required this.boundary, required this.themes, this.leading, }); final Boundary boundary; final Themes themes; /// Before the link state: the menu button, on a view with no app bar of its own. final Widget? leading; static const height = 32.0; @override Widget build(BuildContext context) { final p = AppTheme.of(context); final b = boundary; Reading r(String n) => Reading.of(b, n); final up = b.state == LinkState.up; final (text, color) = switch (b.state) { LinkState.up => ('LINK', p.subdued), LinkState.connecting => ('CONNECTING', p.caution), LinkState.down => ('NO LINK', p.alarm), }; return SizedBox( height: height, child: IconTheme( data: IconThemeData(size: 18, color: p.foreground), child: DefaultTextStyle( style: TextStyle( fontSize: 14, color: p.foreground, fontFeatures: const [FontFeature.tabularFigures()], ), child: Row( children: [ if (leading != null) leading! else const SizedBox(width: 16), Text(text, style: TextStyle(color: color, letterSpacing: 2)), const Spacer(), // Without the link, every Orin value is old news. if (up) ...[ ..._temperature(p, r('orin_tj_temp_c')), ..._can(p, r('can_up'), r('can_bus_state')), ..._elm(p, r('elm_connected')), ..._gnss(p, r('gnss_fix'), r('gnss_satellites')), ..._lte( p, r('net_lte_state'), r('net_lte_tech'), r('net_lte_signal_pct'), ), ..._wifi(p, r('net_wifi_up'), r('net_wifi_signal_dbm')), ..._ups(p, r('ups_state'), r('ups_battery_pct')), ], _VariantButton(themes: themes), ], ), ), ), ); } /// One indicator: an icon, maybe a short text, and the gap after it. static List _item(Color c, {IconData? icon, String? text}) => [ if (icon != null) Icon(icon, color: c), if (icon != null && text != null) const SizedBox(width: 2), if (text != null) Text(text, style: TextStyle(color: c)), const SizedBox(width: 14), ]; /// Only when hot: caution from 80 °C, alarm from 90 (the Orin throttles near 99). static List _temperature(Palette p, Reading t) { final c = t.number; if (t.dim || c == null || c < 80) return []; return _item( c >= 90 ? p.alarm : p.caution, icon: Icons.thermostat, text: '${c.round()}°', ); } static List _can(Palette p, Reading up, Reading state) { if (up.value == null) return _item(p.subdued, text: 'CAN'); final color = switch ((up.on, state.value)) { (false, _) || (_, 'BUS-OFF') || (_, 'STOPPED') => p.alarm, (_, 'ERROR-WARNING') || (_, 'ERROR-PASSIVE') => p.caution, _ => p.foreground, }; return _item(up.dim ? p.subdued : color, text: 'CAN'); } /// The OBD-II adapter, only while connected (it is for calibration, not always on). static List _elm(Palette p, Reading c) => c.on ? _item( c.dim ? p.subdued : p.foreground, icon: Icons.bluetooth_connected, text: 'OBD', ) : []; static List _gnss(Palette p, Reading fix, Reading sats) { final n = sats.number?.round(); final (icon, color) = switch (fix.value) { '3d' => (Icons.gps_fixed, p.foreground), '2d' => (Icons.gps_fixed, p.caution), 'none' => (Icons.gps_not_fixed, p.caution), _ => (Icons.gps_off, p.subdued), }; return _item( fix.dim ? p.subdued : color, icon: icon, text: n == null ? null : '$n', ); } /// The modem, when there is one: bars from its signal, and the technology. static List _lte( Palette p, Reading state, Reading tech, Reading pct, ) { final s = state.value as String?; if (s == null) return []; final q = pct.number ?? 0; final t = (tech.value as String? ?? '').toUpperCase(); final (icon, color) = switch (s) { 'connected' => ( q >= 60 ? Icons.signal_cellular_alt : q >= 30 ? Icons.signal_cellular_alt_2_bar : Icons.signal_cellular_alt_1_bar, p.foreground, ), 'registered' || 'searching' || 'connecting' || 'enabling' => (Icons.signal_cellular_alt_1_bar, p.caution), _ => (Icons.signal_cellular_off, p.subdued), }; return _item( state.dim ? p.subdued : color, icon: icon, text: t.isEmpty ? null : t, ); } static List _wifi(Palette p, Reading up, Reading dbm) { if (!up.on) return _item(p.subdued, icon: Icons.wifi_off); final d = dbm.number ?? -100; final icon = d >= -60 ? Icons.wifi : d >= -70 ? Icons.wifi_2_bar : Icons.wifi_1_bar; return _item(up.dim ? p.subdued : p.foreground, icon: icon); } /// The UPS: caution on battery (the car is off), alarm under 20 %. static List _ups(Palette p, Reading state, Reading pct) { final n = pct.number; if (n == null) return _item(p.subdued, icon: Icons.battery_unknown); const bars = [ Icons.battery_0_bar, Icons.battery_1_bar, Icons.battery_2_bar, Icons.battery_3_bar, Icons.battery_4_bar, Icons.battery_5_bar, Icons.battery_6_bar, Icons.battery_full, ]; final charging = state.value == 'charging'; final icon = charging ? Icons.battery_charging_full : bars[(n / 100 * 7).round().clamp(0, 7)]; final color = n < 20 ? p.alarm : state.value == 'discharging' ? p.caution : p.foreground; return _item( pct.dim ? p.subdued : color, icon: icon, text: '${n.round()}%', ); } } /// Tap: hold the other variant. Long press: back to auto (Settings has all three). class _VariantButton extends StatelessWidget { const _VariantButton({required this.themes}); final Themes themes; @override Widget build(BuildContext context) { final p = AppTheme.of(context); return GestureDetector( onLongPress: () => themes.mode = VariantMode.auto, child: IconButton( tooltip: themes.isDark ? 'Bright' : 'Dark', onPressed: themes.toggle, iconSize: 20, visualDensity: VisualDensity.compact, icon: Icon( themes.mode == VariantMode.auto ? Icons.brightness_auto : (themes.isDark ? Icons.light_mode : Icons.dark_mode), color: p.subdued, ), ), ); } }