Files
StarPilot/openpilot/starpilot/ui/home_state.py
T
2026-10-21 13:10:49 -04:00

153 lines
4.5 KiB
Python

"""Immutable Home presentation data and explicit input actions."""
from dataclasses import dataclass
from enum import StrEnum
from collections.abc import Callable
from openpilot.starpilot.ui.presentation import Profile
class HomeMode(StrEnum):
CHILL = "chill"
EXPERIMENTAL = "experimental"
CONDITIONAL_EXPERIMENTAL = "conditional_experimental"
CONDITIONAL_CHILL = "conditional_chill"
class HomeActionKind(StrEnum):
OPEN_SETTINGS = "open_settings"
OPEN_TOGGLES = "open_toggles"
OPEN_PAIRING = "open_pairing"
SET_EXPERIMENTAL = "set_experimental"
@dataclass(frozen=True)
class DriveSummary:
drives: int = 0
distance: float = 0.0
hours: float = 0.0
unit: str = "miles"
@dataclass(frozen=True)
class DailyDistance:
label: str
distance: float = 0.0
is_today: bool = False
is_future: bool = False
@dataclass(frozen=True)
class PersonalRecord:
title: str
value: str
detail: str
@dataclass(frozen=True)
class DriveStatsData:
all_time: DriveSummary
past_week: DriveSummary
this_week: DriveSummary
daily_distance: tuple[DailyDistance, ...]
records: tuple[PersonalRecord, ...]
@dataclass(frozen=True)
class HomeState:
version: str
commit: str
commit_date: str
model_label: str
description: str
mode: HomeMode
experimental_enabled: bool
experimental_available: bool
stats: DriveStatsData | None
paired: bool = True
network: str = "none"
network_strength: int = 0
temperature_c: float | None = None
vehicle_online: bool = True
connection: str = "OFFLINE"
bluetooth: str = ""
gpu_present: bool = False
gpu_active: bool = False
recording_audio: bool = False
branch: str = ""
gpu_state: str = "disconnected"
metric: bool = False
temperature_high: bool = False
jetlink: str = "off"
@dataclass(frozen=True)
class HomeAction:
kind: HomeActionKind
value: bool | None = None
class HomeInput:
"""Dispatch touch gestures without writing settings or accessing services."""
def __init__(self, profile: Profile, emit: Callable[[HomeAction], None]):
self.profile = profile
self.emit = emit
self._press: tuple[float, float, float] | None = None
self._long_press_handled = False
self._hold_started: float | None = None
self._inside = False
def press(self, x: float, y: float, timestamp: float) -> None:
width, height = self.profile.size
self._press = (x, y, timestamp) if 0 <= x < width and 0 <= y < height else None
self._long_press_handled = False
self._inside = self._press is not None
self._hold_started = timestamp if self._inside else None
def move(self, x: float, y: float, timestamp: float) -> None:
width, height = self.profile.size
inside = 0 <= x < width and 0 <= y < height
if self._press is not None and self.profile == Profile.COMPACT:
if not inside:
self._hold_started = None
self._long_press_handled = False
elif not self._inside:
self._hold_started = timestamp
self._inside = inside
def tick(self, timestamp: float, state: HomeState) -> None:
if self.profile == Profile.COMPACT and self._hold_started is not None and not self._long_press_handled:
if timestamp - self._hold_started > 0.5:
self._long_press_handled = True
if state.experimental_available:
self.emit(HomeAction(HomeActionKind.SET_EXPERIMENTAL, not state.experimental_enabled))
def release(self, x: float, y: float, timestamp: float, state: HomeState) -> None:
if self._press is None:
return
self.move(x, y, timestamp)
self.tick(timestamp, state)
px, py, _ = self._press
self._press = None
self._hold_started = None
if self._long_press_handled:
return
if self.profile == Profile.COMPACT:
if 0 <= x < 536 and 0 <= y < 240:
self.emit(HomeAction(HomeActionKind.OPEN_SETTINGS))
elif px < 300 and 50 <= x <= 250 and 35 <= y <= 152:
self.emit(HomeAction(HomeActionKind.OPEN_SETTINGS))
elif px < 300 and state.recording_audio and 170 <= x <= 245 and 245 <= y <= 285:
self.emit(HomeAction(HomeActionKind.OPEN_TOGGLES))
elif 1370 <= px <= 2120 and 145 <= py <= 270 and 1370 <= x <= 2120 and 145 <= y <= 270:
self.emit(HomeAction(HomeActionKind.OPEN_TOGGLES))
elif not state.paired and 1370 <= px <= 2120 and 300 <= py <= 430 and 1370 <= x <= 2120 and 300 <= y <= 430:
self.emit(HomeAction(HomeActionKind.OPEN_PAIRING))
def cancel(self) -> None:
self._press = None
self._long_press_handled = False
self._hold_started = None
self._inside = False