repo: archive legacy pico dashboard

This commit is contained in:
2026-06-09 00:52:00 -06:00
parent 9cb06b86ff
commit f5e2c5fd24
38 changed files with 107 additions and 20 deletions
@@ -0,0 +1,74 @@
from alarms.alarm_definitions import (
BATTERY_SOC_LOW,
BATTERY_VOLTAGE_LOW,
FRIDGE_ZONE_1_HIGH,
FRIDGE_ZONE_2_HIGH,
SENSOR_FAILURE,
COMMUNICATION_LOST,
)
ALARM_LABELS = {
BATTERY_SOC_LOW: "Battery SOC Low",
BATTERY_VOLTAGE_LOW: "Battery Voltage Low",
FRIDGE_ZONE_1_HIGH: "Fridge Zone 1 High Temp",
FRIDGE_ZONE_2_HIGH: "Fridge Zone 2 High Temp",
COMMUNICATION_LOST: "ESP32 Communication Lost",
}
CRITICAL_ALARMS = {
BATTERY_SOC_LOW,
BATTERY_VOLTAGE_LOW,
FRIDGE_ZONE_1_HIGH,
FRIDGE_ZONE_2_HIGH,
COMMUNICATION_LOST,
}
class AlarmViewModel:
def __init__(self, alarms):
self.alarms = alarms or []
def count(self):
return len(self.alarms)
def has_alarms(self):
return self.count() > 0
def has_critical(self):
return any(self._base_alarm(alarm) in CRITICAL_ALARMS for alarm in self.alarms)
def should_buzz(self):
return self.has_critical()
def primary_alarm(self):
if not self.alarms:
return None
return self.format_alarm(self.alarms[0])
def as_list(self):
return [self.format_alarm(alarm) for alarm in self.alarms]
def format_alarm(self, alarm):
base = self._base_alarm(alarm)
if base == SENSOR_FAILURE:
sensor = alarm.split(":", 1)[1] if ":" in alarm else "unknown"
return {
"code": alarm,
"severity": "warning",
"title": "Sensor Failure",
"detail": sensor.replace("_", " ").title(),
}
return {
"code": alarm,
"severity": "critical" if base in CRITICAL_ALARMS else "info",
"title": ALARM_LABELS.get(base, alarm),
"detail": None,
}
def _base_alarm(self, alarm):
return alarm.split(":", 1)[0]
@@ -0,0 +1,59 @@
def format_value(value, suffix="", decimals=1, missing="--"):
if value is None:
return missing
if isinstance(value, int):
return f"{value}{suffix}"
if isinstance(value, float):
return f"{value:.{decimals}f}{suffix}"
return f"{value}{suffix}"
def format_bool(value, true_text="ON", false_text="OFF"):
return true_text if value else false_text
class DashboardViewModel:
def __init__(self, state, alarms=None):
self.state = state
self.alarms = alarms or []
def top_bar(self):
soc = format_value(self.state.battery.get("soc"), "%", decimals=0)
uart = "UART OK" if self.state.network.get("uart_connected") else "UART LOST"
alarm_count = len(self.alarms)
return {
"soc": soc,
"comms": uart,
"alarms": alarm_count,
}
def battery_summary(self):
return {
"voltage": format_value(self.state.battery.get("voltage"), "V", decimals=1),
"current": format_value(self.state.battery.get("current"), "A", decimals=1),
"runtime": format_value(self.state.battery.get("runtime_hours"), " hr", decimals=1),
}
def fridge_summary(self):
return {
"zone_1": format_value(self.state.temps.get("fridge_zone_1"), "°F", decimals=1),
"zone_2": format_value(self.state.temps.get("fridge_zone_2"), "°F", decimals=1),
}
def power_summary(self):
return {
"starlink": format_bool(self.state.relays.get("starlink")),
"fridge": format_bool(self.state.relays.get("fridge")),
}
def as_dict(self):
return {
"top_bar": self.top_bar(),
"battery": self.battery_summary(),
"fridge": self.fridge_summary(),
"power": self.power_summary(),
}
@@ -0,0 +1,74 @@
from .dashboard_view_model import format_value, format_bool
class BatteryViewModel:
def __init__(self, state):
self.state = state
def as_dict(self):
battery = self.state.battery
return {
"soc": format_value(battery.get("soc"), "%", decimals=0),
"voltage": format_value(battery.get("voltage"), "V", decimals=1),
"current": format_value(battery.get("current"), "A", decimals=1),
"remaining_ah": format_value(battery.get("remaining_ah"), "Ah", decimals=1),
"runtime_hours": format_value(battery.get("runtime_hours"), " hr", decimals=1),
"temperature_f": format_value(battery.get("temperature_f"), "°F", decimals=1),
}
class TempsViewModel:
SENSOR_LABELS = {
"fridge_zone_1": "Fridge Zone 1",
"fridge_zone_2": "Fridge Zone 2",
"rear_seat": "Rear Seat",
"outside": "Outside Air",
}
def __init__(self, state):
self.state = state
def as_list(self):
rows = []
for key, label in self.SENSOR_LABELS.items():
online = self.state.sensor_health.get(key, False)
temp = self.state.temps.get(key)
rows.append({
"key": key,
"label": label,
"temperature": format_value(temp, "°F", decimals=1),
"status": "OK" if online else "FAULT",
})
return rows
class PowerViewModel:
def __init__(self, state):
self.state = state
def as_dict(self):
return {
"starlink": format_bool(self.state.relays.get("starlink")),
"fridge": format_bool(self.state.relays.get("fridge")),
}
class SystemViewModel:
def __init__(self, state):
self.state = state
def as_dict(self):
sensor_count = sum(1 for online in self.state.sensor_health.values() if online)
total_sensors = len(self.state.sensor_health)
return {
"esp32": "Online" if self.state.network.get("uart_connected") else "Unknown",
"uart": "Connected" if self.state.network.get("uart_connected") else "Disconnected",
"wifi_api": "Available" if self.state.network.get("wifi_enabled") else "Unavailable",
"sensors": f"{sensor_count} / {total_sensors} OK",
"ignition": "On" if self.state.vehicle.get("ignition_on") else "Off",
}
@@ -0,0 +1,51 @@
class DashboardRenderer:
def __init__(self, display):
self.display = display
def render(self, view_model):
vm = view_model.as_dict()
self.display.clear()
top = vm["top_bar"]
self.display.text(0, 0, f'SOC {top["soc"]}')
self.display.text(120, 0, top["comms"])
self.display.text(260, 0, f'ALM {top["alarms"]}')
battery = vm["battery"]
self.display.text(0, 48, "Battery")
self.display.text(
0,
72,
f'{battery["voltage"]} {battery["current"]} {battery["runtime"]}',
)
fridge = vm["fridge"]
self.display.text(0, 128, "Fridge")
self.display.text(0, 152, f'Zone 1: {fridge["zone_1"]}')
self.display.text(0, 176, f'Zone 2: {fridge["zone_2"]}')
power = vm["power"]
self.display.text(0, 232, "Power")
self.display.text(
0,
256,
f'Starlink {power["starlink"]} Fridge {power["fridge"]}',
)
self.render_nav("dashboard")
def render_nav(self, active_screen):
labels = [
("dashboard", "Dash"),
("battery", "Bat"),
("temps", "Temps"),
("power", "Power"),
("system", "System"),
]
x = 0
for screen, label in labels:
text = f"[{label}]" if screen == active_screen else label
self.display.text(x, 440, text)
x += 64
@@ -0,0 +1,21 @@
class ScreenManager:
VALID_SCREENS = ("dashboard", "battery", "temps", "power", "system")
def __init__(self):
self.current_screen = "dashboard"
def go_to(self, screen_name):
if screen_name not in self.VALID_SCREENS:
raise ValueError(f"Invalid screen: {screen_name}")
self.current_screen = screen_name
def next_screen(self):
current_index = self.VALID_SCREENS.index(self.current_screen)
next_index = (current_index + 1) % len(self.VALID_SCREENS)
self.current_screen = self.VALID_SCREENS[next_index]
def previous_screen(self):
current_index = self.VALID_SCREENS.index(self.current_screen)
previous_index = (current_index - 1) % len(self.VALID_SCREENS)
self.current_screen = self.VALID_SCREENS[previous_index]
@@ -0,0 +1,38 @@
NAV_ITEMS = [
("dashboard", 0, 64),
("battery", 64, 128),
("temps", 128, 192),
("power", 192, 256),
("system", 256, 320),
]
class TouchRouter:
def __init__(self, screen_manager, width=320, height=480, nav_height=56):
self.screen_manager = screen_manager
self.width = width
self.height = height
self.nav_height = nav_height
def handle_touch(self, event):
if not event or not event.pressed:
return False
if self._is_bottom_nav(event.y):
screen = self._screen_for_nav_x(event.x)
if screen:
self.screen_manager.go_to(screen)
return True
return False
def _is_bottom_nav(self, y):
return y >= self.height - self.nav_height
def _screen_for_nav_x(self, x):
for screen, start_x, end_x in NAV_ITEMS:
if start_x <= x < end_x:
return screen
return None