Files
app-flutter/lib/widgets/status_bar.dart
T

233 lines
7.4 KiB
Dart
Raw Normal View History

// 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<Widget> _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<Widget> _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<Widget> _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<Widget> _elm(Palette p, Reading c) => c.on
? _item(
c.dim ? p.subdued : p.foreground,
icon: Icons.bluetooth_connected,
text: 'OBD',
)
: [];
static List<Widget> _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<Widget> _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<Widget> _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<Widget> _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,
),
),
);
}
}