repo: archive legacy pico dashboard
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user