// The Orin at a glance, the way a phone's status bar shows the phone: the link on the // left, then its parts, links out and power on the right, and the dark/bright switch. // Drawn by the shell above every view (DASH-NEXT.md item 1). // // Colours are palette roles: foreground when fine, good for charging, caution when // degraded or low, alarm when down, subdued when unknown. The parts (CAN, OBD, IMU, CAM) // are lamps by one rule; WiFi and LTE show signal in four steps and Android's traffic // arrows; the battery's fill shows its level in its colour. Temperature appears only // when hot, LTE only with a modem. import 'package:flutter/material.dart'; import '../link/boundary.dart'; import '../link/reading.dart'; import '../theme/palette.dart'; import 'status_icons.dart'; /// A part's state, for its lamp. enum _Part { up, degraded, down, unknown } 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; /// Traffic arrows light above this rate (kbit/s): over background chatter. static const trafficKbps = 2.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')), _lamp(p, 'CAN', _can(r('can_up'), r('can_bus_state'))), _lamp(p, 'OBD', _flag(r('elm_connected'))), _lamp(p, 'IMU', _fresh(b, 'imu_roll_deg')), _lamp(p, 'CAM', _cam(r('cam_up'), r('vision_up'))), const SizedBox(width: 4), ..._gnss(p, r('gnss_fix'), r('gnss_satellites')), ..._lte(p, r), ..._wifi(p, r), ..._ups(p, r('ups_state'), r('ups_battery_pct')), ], _VariantButton(themes: themes), ], ), ), ), ); } /// One indicator: its pieces, and the gap after it. static List _item(List pieces) => [ ...pieces, const SizedBox(width: 14), ]; static Widget _lamp(Palette p, String label, _Part s) => Padding( padding: const EdgeInsets.only(right: 10), child: Text( label, style: TextStyle( color: switch (s) { _Part.up => p.foreground, _Part.degraded => p.caution, _Part.down => p.alarm, _Part.unknown => p.subdued, }, ), ), ); static _Part _flag(Reading on) => on.value == null ? _Part.unknown : on.dim ? _Part.unknown : on.on ? _Part.up : _Part.down; static _Part _can(Reading up, Reading state) => switch (_flag(up)) { _Part.up => switch (state.value) { 'BUS-OFF' || 'STOPPED' => _Part.down, 'ERROR-WARNING' || 'ERROR-PASSIVE' => _Part.degraded, _ => _Part.up, }, final s => s, }; /// A part known only by its values arriving: fresh is up, stale is down. static _Part _fresh(Boundary b, String name) { final e = b.signals[name]; if (e is! Map) return _Part.unknown; return (e['quality'] as String? ?? 'ok') == 'ok' ? _Part.up : _Part.down; } /// The camera: present and served is up; present but not served, degraded. static _Part _cam(Reading cam, Reading vision) => switch (_flag(cam)) { _Part.up => _flag(vision) == _Part.down ? _Part.degraded : _Part.up, final s => s, }; /// 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 []; final color = c >= 90 ? p.alarm : p.caution; return _item([ Icon(Icons.thermostat, color: color), Text('${c.round()}°', style: TextStyle(color: color)), ]); } 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), }; final c = fix.dim ? p.subdued : color; return _item([ Icon(icon, color: c), if (n != null) ...[ const SizedBox(width: 2), Text('$n', style: TextStyle(color: c)), ], ]); } static Widget _arrows(Reading Function(String) r, String net, Color c) => TrafficArrows( up: (r('net_${net}_tx_kbps').number ?? 0) > trafficKbps, down: (r('net_${net}_rx_kbps').number ?? 0) > trafficKbps, color: c, ); /// The modem, when there is one: signal, traffic, and the technology. static List _lte(Palette p, Reading Function(String) r) { final state = r('net_lte_state'); final s = state.value as String?; if (s == null) return []; final q = r('net_lte_signal_pct').number ?? 0; final t = (r('net_lte_tech').value as String? ?? '').toUpperCase(); final color = state.dim ? p.subdued : switch (s) { 'connected' => p.foreground, 'registered' || 'searching' || 'connecting' || 'enabling' => p.caution, _ => p.subdued, }; final connected = s == 'connected'; final bars = q >= 80 ? 4 : q >= 60 ? 3 : q >= 40 ? 2 : q >= 20 ? 1 : 0; return _item([ if (t.isNotEmpty) ...[ Text(t, style: TextStyle(color: color, fontSize: 11)), const SizedBox(width: 2), ], SignalIcon.cellular( bars: s == 'disabled' || s == 'failed' ? null : bars, color: color, ), if (connected) ...[const SizedBox(width: 1), _arrows(r, 'lte', color)], ]); } static List _wifi(Palette p, Reading Function(String) r) { final up = r('net_wifi_up'); if (!up.on) { return _item([SignalIcon.wifi(bars: null, color: p.subdued)]); } final d = r('net_wifi_signal_dbm').number ?? -100; final bars = d >= -55 ? 4 : d >= -65 ? 3 : d >= -75 ? 2 : d >= -85 ? 1 : 0; final color = up.dim ? p.subdued : p.foreground; return _item([ SignalIcon.wifi(bars: bars, color: color), const SizedBox(width: 1), _arrows(r, 'wifi', color), ]); } /// The UPS: the fill in good while charging, caution under 40 %, alarm under 20 %. static List _ups(Palette p, Reading state, Reading pct) { final n = pct.number; final fill = n == null || pct.dim ? p.subdued : state.value == 'charging' ? p.good : n < 20 ? p.alarm : n < 40 ? p.caution : p.foreground; return _item([ BatteryIcon( level: n == null ? null : n / 100, fill: fill, outline: pct.dim ? p.subdued : p.foreground, ), if (n != null) ...[ const SizedBox(width: 4), Text('${n.round()}%', style: TextStyle(color: fill)), ], ]); } } /// 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, ), ), ); } }