mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
698 lines
30 KiB
Python
698 lines
30 KiB
Python
import numpy as np
|
|
import os
|
|
import time
|
|
import threading
|
|
import capnp
|
|
from collections.abc import Callable
|
|
from enum import Enum
|
|
from dataclasses import replace
|
|
from openpilot.cereal import messaging, log
|
|
from openpilot.cereal.services import SERVICE_LIST
|
|
from opendbc.car.structs import car
|
|
from openpilot.common.filter_simple import FirstOrderFilter
|
|
from openpilot.common.params import Params
|
|
from openpilot.starpilot.schema_cache import get_cache, prewarm_cache_contracts
|
|
from openpilot.starpilot.feature_runtime import enabled as feature_enabled
|
|
from openpilot.starpilot.ui.display_preferences import AUTO, DisplayPreferences, read_preferences
|
|
from openpilot.common.realtime import drop_realtime
|
|
from openpilot.common.swaglog import cloudlog
|
|
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
|
|
from openpilot.selfdrive.selfdrived.alertmanager import OFFROAD_ALERTS
|
|
from openpilot.system.ui.lib.application import gui_app
|
|
from openpilot.common.hardware import HARDWARE, PC
|
|
from openpilot.common.hardware.usb import cable_connected, get_usb_state, is_chestnut_usb_id
|
|
from openpilot.starpilot.models.manager import ModelManager
|
|
from openpilot.starpilot.models.jetlink_status import JetlinkStatusFeed
|
|
from openpilot.tools.replay.display_clock import BOOT_TIME_SERVICES, DisplayClockReader
|
|
|
|
BACKLIGHT_OFFROAD = 65 if HARDWARE.get_device_type() == "mici" else 50
|
|
PARAM_UPDATE_TIME = 1 / 5.0
|
|
|
|
|
|
class UIStatus(Enum):
|
|
DISENGAGED = "disengaged"
|
|
ENGAGED = "engaged"
|
|
OVERRIDE = "override"
|
|
|
|
|
|
class ChestnutState(Enum):
|
|
DISCONNECTED = "disconnected"
|
|
UNCOMPILED = "uncompiled"
|
|
READY = "ready"
|
|
LOADING = "loading"
|
|
ACTIVE = "active"
|
|
FAILED = "failed"
|
|
|
|
|
|
class ProjectionParams:
|
|
"""Only the read interface needed by the projection display."""
|
|
|
|
def __init__(self, source: Params):
|
|
self._source = source
|
|
|
|
def get(self, *args, **kwargs):
|
|
return self._source.get(*args, **kwargs)
|
|
|
|
def get_bool(self, *args, **kwargs):
|
|
return self._source.get_bool(*args, **kwargs)
|
|
|
|
def get_int(self, *args, **kwargs):
|
|
return self._source.get_int(*args, **kwargs)
|
|
|
|
def get_float(self, *args, **kwargs):
|
|
return self._source.get_float(*args, **kwargs)
|
|
|
|
def get_param_path(self, *args, **kwargs):
|
|
return self._source.get_param_path(*args, **kwargs)
|
|
|
|
|
|
class ProjectionPrimeState:
|
|
"""Saved pairing label only; projection never starts the API or alert owner."""
|
|
|
|
def __init__(self, params: ProjectionParams | Params):
|
|
self.params = params
|
|
|
|
def is_paired(self) -> bool:
|
|
try:
|
|
return int(self.params.get("PrimeType") or -1) >= 0
|
|
except (TypeError, ValueError):
|
|
return False
|
|
|
|
def start(self) -> None:
|
|
pass
|
|
|
|
|
|
class UIState:
|
|
_instance: 'UIState | None' = None
|
|
|
|
def __new__(cls):
|
|
if cls._instance is None:
|
|
cls._instance = super().__new__(cls)
|
|
cls._instance._initialize()
|
|
return cls._instance
|
|
|
|
def _initialize(self):
|
|
prewarm_cache_contracts()
|
|
self.replay_clock = DisplayClockReader() if DisplayClockReader.enabled() else None
|
|
self.replay_sample = None
|
|
self._replay_state_epoch = None
|
|
self.replay_needs_reprime = False
|
|
self._replay_reprime_services: set[str] = set()
|
|
self.projection_read_only = os.environ.get("STARPILOT_PROJECTION_READ_ONLY") == "1"
|
|
params = Params()
|
|
self.params = ProjectionParams(params) if self.projection_read_only else params
|
|
self.sm = messaging.SubMaster(
|
|
[
|
|
"modelV2",
|
|
"controlsState",
|
|
"lateralDelay",
|
|
"lateralTorqueParameters",
|
|
"onroadEvents",
|
|
"extrinsicsCalibration",
|
|
"radarState",
|
|
"deviceState",
|
|
"pandaStates",
|
|
"carParams",
|
|
"driverMonitoringState",
|
|
"carState",
|
|
"starpilotCarState",
|
|
"driverStateV2",
|
|
"narrowRoadCameraState",
|
|
"wideRoadCameraState",
|
|
"managerState",
|
|
"selfdriveState",
|
|
"starpilotSelfdriveState",
|
|
"starpilotLateralState",
|
|
"starpilotNavigation",
|
|
"longitudinalPlan",
|
|
"gpsLocationExternal",
|
|
"gpsLocation",
|
|
"navInstruction",
|
|
"navRoute",
|
|
"carOutput",
|
|
"carControl",
|
|
"aolAxisState",
|
|
"slcState",
|
|
"vehicleParameters",
|
|
"testJoystick",
|
|
"rawAudioData",
|
|
*(["customReserved6", "cabinCameraState", "narrowRoadEncodeIdx", "wideRoadEncodeIdx", "cabinEncodeIdx"]
|
|
if self.replay_clock is not None else []),
|
|
],
|
|
ignore_alive=["starpilotSelfdriveState", "starpilotLateralState", "aolAxisState", "starpilotCarState"],
|
|
ignore_valid=["starpilotSelfdriveState", "starpilotLateralState", "aolAxisState", "starpilotCarState"],
|
|
ignore_avg_freq=["starpilotSelfdriveState", "starpilotLateralState", "aolAxisState", "starpilotCarState"],
|
|
)
|
|
|
|
self.prime_state = ProjectionPrimeState(self.params) if self.replay_clock is not None or isinstance(self.params, ProjectionParams) else PrimeState()
|
|
|
|
# UI Status tracking
|
|
self.status: UIStatus = UIStatus.DISENGAGED
|
|
self.started_frame: int = 0
|
|
self.started_time: float = 0.0
|
|
self._engaged_prev: bool = False
|
|
self._started_prev: bool = False
|
|
|
|
# Core state variables
|
|
self.is_metric: bool = self.params.get_bool("IsMetric")
|
|
self.initial_cruise_speed: int = self.params.get("InitialCruiseSpeedKph", return_default=True)
|
|
self.is_release = self.params.get_bool("IsReleaseBranch")
|
|
self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM")
|
|
self.experimental_mode: bool = self.params.get_bool("ExperimentalMode")
|
|
self.navigation_enabled = self.params.get_bool("NavigationEnabled")
|
|
self.nav_destination_set = self.params.get("NavDestination") is not None
|
|
self.jetlink_status_feed = None if self.replay_clock is not None else JetlinkStatusFeed()
|
|
if self.jetlink_status_feed is not None:
|
|
self.jetlink_status_feed.set_enabled(True)
|
|
self.chestnut_present: bool = False
|
|
self._gpu_artifacts = None if self.replay_clock is not None else ModelManager()
|
|
self._gpu_artifacts_at: float | None = None
|
|
self.chestnut_compiled: bool = False
|
|
self.chestnut_checking: bool = False
|
|
self.chestnut_active: bool | None = None
|
|
self.chestnut_output_seen: bool = False
|
|
self.chestnut_alert: bool = False
|
|
self.chestnut_loading: bool = False
|
|
self.usb_connected: bool = False
|
|
self.usb_connected_ts: float | None = None
|
|
self.usb_disconnected_ts: float | None = None
|
|
self.usb_unknown: bool = False
|
|
self.chestnut_state = ChestnutState.DISCONNECTED
|
|
self.started: bool = False
|
|
self.ignition: bool = False
|
|
self.recording_audio: bool = False
|
|
self.panda_type: log.PandaState.PandaType = log.PandaState.PandaType.unknown
|
|
self.personality: log.LongitudinalPersonality = log.LongitudinalPersonality.standard
|
|
self.has_longitudinal_control: bool = False
|
|
self.aol_runtime: bool = False
|
|
self.is_body: bool | None = False
|
|
self.CP: car.CarParams | None = None
|
|
self.light_sensor: float = -1.0
|
|
|
|
self._params_thread: threading.Thread | None = None
|
|
self._projection_params_at: float | None = None
|
|
|
|
# Callbacks
|
|
self._offroad_transition_callbacks: list[Callable[[], None]] = []
|
|
self._engaged_transition_callbacks: list[Callable[[], None]] = []
|
|
self._on_body_changed_callbacks: list[Callable[[], None]] = []
|
|
|
|
def add_offroad_transition_callback(self, callback: Callable[[], None]):
|
|
self._offroad_transition_callbacks.append(callback)
|
|
|
|
def add_engaged_transition_callback(self, callback: Callable[[], None]):
|
|
self._engaged_transition_callbacks.append(callback)
|
|
|
|
def add_on_body_changed_callbacks(self, callback: Callable[[], None]):
|
|
self._on_body_changed_callbacks.append(callback)
|
|
|
|
@property
|
|
def engaged(self) -> bool:
|
|
return self.started and self.sm["selfdriveState"].enabled
|
|
|
|
def is_onroad(self) -> bool:
|
|
return self.started
|
|
|
|
def is_offroad(self) -> bool:
|
|
return not self.started
|
|
|
|
def update(self) -> None:
|
|
if getattr(self, "replay_clock", None) is not None:
|
|
before = self.replay_clock.sample()
|
|
cache_epoch = getattr(self, "_replay_state_epoch", self.replay_sample.epoch if self.replay_sample is not None else None)
|
|
if before.epoch is not None and before.epoch != cache_epoch:
|
|
self._reset_replay_state()
|
|
self._replay_state_epoch = before.epoch
|
|
self.replay_needs_reprime = False
|
|
self._replay_reprime_services = set()
|
|
self.sm.update(0)
|
|
sample = self.replay_clock.sample()
|
|
if sample.epoch is not None and sample.epoch != before.epoch:
|
|
# A seek straddled this receive. Keep only its eligible new primer, and
|
|
# withhold this mixed frame until the next coherent clock sample.
|
|
self._replay_reprime_services = {name for name in self.sm.sock if self.sm.updated[name]}
|
|
self._reset_replay_state(retain_sample=sample)
|
|
self.replay_needs_reprime = not sample.valid
|
|
self._replay_state_epoch = sample.epoch
|
|
sample = replace(sample, now_ns=None, boot_ns=None, valid=False)
|
|
if sample.valid and getattr(self, "replay_needs_reprime", False):
|
|
# A later receive can restore the primer without another decoder seek.
|
|
for name in self._replay_reprime_services.copy():
|
|
target = sample.boot_ns if name in BOOT_TIME_SERVICES else sample.now_ns
|
|
stamp = self.sm.logMonoTime[name]
|
|
if self.sm.seen[name] and self.sm.valid[name] and type(target) is int and type(stamp) is int and 0 < stamp <= target:
|
|
self._replay_reprime_services.remove(name)
|
|
self.replay_needs_reprime = bool(self._replay_reprime_services)
|
|
self.replay_sample = sample
|
|
self.sm.replay_sample = sample
|
|
self._update_replay_alive(sample)
|
|
self._update_state()
|
|
self._update_status()
|
|
now = time.monotonic()
|
|
if self._projection_params_at is None or now - self._projection_params_at >= 1.0:
|
|
self.update_params()
|
|
self._projection_params_at = now
|
|
self._update_chestnut_state()
|
|
return
|
|
if self.projection_read_only:
|
|
self.sm.update(0)
|
|
self._update_state()
|
|
self._update_status()
|
|
now = time.monotonic()
|
|
if self._projection_params_at is None or now < self._projection_params_at or now - self._projection_params_at >= 1.0:
|
|
self.update_params()
|
|
self._projection_params_at = now
|
|
return
|
|
self.prime_state.start() # start thread after manager forks ui
|
|
if self._params_thread is None:
|
|
self._params_thread = threading.Thread(target=self._params_refresh_worker, daemon=True)
|
|
self._params_thread.start()
|
|
|
|
self.sm.update(0)
|
|
self._update_state()
|
|
self._update_status()
|
|
self._update_chestnut_state()
|
|
device.update()
|
|
|
|
def display_time_ns(self) -> int:
|
|
if getattr(self, "replay_clock", None) is not None:
|
|
sample = self.replay_sample
|
|
return sample.now_ns if sample is not None and sample.valid and sample.now_ns is not None else 0
|
|
return time.monotonic_ns()
|
|
|
|
def _update_replay_alive(self, sample) -> None:
|
|
# Receipt timestamps stay on the host clock. Logical freshness follows the
|
|
# recorded clock, including a paused primer and Panda's boot-time domain.
|
|
for service in self.sm.static_freq_services:
|
|
target = sample.boot_ns if service in BOOT_TIME_SERVICES else sample.now_ns
|
|
stamp = self.sm.logMonoTime[service]
|
|
spec = SERVICE_LIST[service]
|
|
max_age_ns = 10e9 / spec.frequency
|
|
if spec.decimation:
|
|
# Qlogs sample high-rate messages sparsely. Keep replay transport alive
|
|
# through the display window; current_message still enforces its own
|
|
# stricter age limit for actions, independently of this UI-only flag.
|
|
max_age_ns = max(max_age_ns, min(1e9, 2e9 * spec.decimation / spec.frequency))
|
|
self.sm.alive[service] = bool(sample.valid and self.sm.seen[service] and
|
|
type(target) is int and type(stamp) is int and
|
|
0 < stamp <= target and target - stamp < max_age_ns)
|
|
|
|
def _reset_replay_state(self, retain_sample=None) -> None:
|
|
for service in self.sm.sock:
|
|
target = None if retain_sample is None else retain_sample.boot_ns if service in BOOT_TIME_SERVICES else retain_sample.now_ns
|
|
stamp = self.sm.logMonoTime[service]
|
|
if (retain_sample is not None and retain_sample.valid and self.sm.updated[service] and
|
|
type(target) is int and type(stamp) is int and 0 < stamp <= target):
|
|
continue
|
|
# Do not drain sockets: the new epoch's paused primer may already be queued.
|
|
for field in ("seen", "updated", "alive", "valid"):
|
|
getattr(self.sm, field)[service] = False
|
|
for field in ("recv_frame", "recv_time", "logMonoTime"):
|
|
getattr(self.sm, field)[service] = 0
|
|
try:
|
|
empty = messaging.new_message(service)
|
|
except capnp.lib.capnp.KjException:
|
|
empty = messaging.new_message(service, 0)
|
|
self.sm.data[service] = getattr(empty.as_reader(), service)
|
|
self.started = self._started_prev = self._engaged_prev = False
|
|
self.started_frame = max(0, self.sm.frame - 1)
|
|
self.CP = None
|
|
self.has_longitudinal_control = False
|
|
self.aol_runtime = False
|
|
self.ignition = False
|
|
self.light_sensor = -1.0
|
|
self._projection_params_at = None
|
|
self.chestnut_output_seen = False
|
|
self.chestnut_present = False
|
|
self.status = UIStatus.DISENGAGED
|
|
|
|
def _params_refresh_worker(self):
|
|
drop_realtime()
|
|
while True:
|
|
self.update_params()
|
|
time.sleep(PARAM_UPDATE_TIME)
|
|
|
|
def _update_state(self) -> None:
|
|
# Handle panda states updates
|
|
if self.sm.updated["pandaStates"]:
|
|
panda_states = self.sm["pandaStates"]
|
|
|
|
if len(panda_states) > 0:
|
|
# Get panda type from first panda
|
|
self.panda_type = panda_states[0].pandaType
|
|
# Check ignition status across all pandas
|
|
if self.panda_type != log.PandaState.PandaType.unknown:
|
|
self.ignition = any(state.ignitionLine or state.ignitionCan for state in panda_states)
|
|
elif not self.sm.alive["pandaStates"]:
|
|
self.panda_type = log.PandaState.PandaType.unknown
|
|
|
|
# Handle wide road camera state updates
|
|
if self.sm.updated["wideRoadCameraState"]:
|
|
cam_state = self.sm["wideRoadCameraState"]
|
|
self.light_sensor = max(100.0 - cam_state.exposureValPercent, 0.0)
|
|
elif not self.sm.alive["wideRoadCameraState"] or not self.sm.valid["wideRoadCameraState"]:
|
|
self.light_sensor = -1
|
|
|
|
# hardwared owns the pipeline transition; raw ignition can arrive separately.
|
|
self.started = self.sm["deviceState"].started
|
|
|
|
# Update body state
|
|
if self.CP is not None and self.is_body != self.CP.notCar:
|
|
self.is_body = self.CP.notCar
|
|
for callback in self._on_body_changed_callbacks:
|
|
callback()
|
|
|
|
def _update_status(self) -> None:
|
|
if self.started and self.sm.updated["selfdriveState"]:
|
|
ss = self.sm["selfdriveState"]
|
|
state = ss.state
|
|
|
|
if state in (log.SelfdriveState.OpenpilotState.preEnabled, log.SelfdriveState.OpenpilotState.overriding):
|
|
self.status = UIStatus.OVERRIDE
|
|
else:
|
|
self.status = UIStatus.ENGAGED if ss.enabled else UIStatus.DISENGAGED
|
|
|
|
# Check for engagement state changes
|
|
if self.engaged != self._engaged_prev:
|
|
for callback in self._engaged_transition_callbacks:
|
|
gui_app.measure_frame_phase("transition", callback)
|
|
self._engaged_prev = self.engaged
|
|
|
|
# Handle onroad/offroad transition
|
|
if self.started != self._started_prev or self.sm.frame == 1:
|
|
if self.started:
|
|
replay = getattr(self, "replay_clock", None) is not None
|
|
if not replay:
|
|
self.status = UIStatus.DISENGAGED
|
|
self.chestnut_output_seen = False
|
|
# A paused replay publishes one atomic primer; its same-frame receipts
|
|
# are eligible. Live transitions retain their stricter receive boundary.
|
|
self.started_frame = max(0, self.sm.frame - 1) if replay else self.sm.frame
|
|
self.started_time = time.monotonic()
|
|
self.chestnut_present = self.sm["deviceState"].chestnutPresent or (self.chestnut_compiled and self.usb_connected)
|
|
|
|
for callback in self._offroad_transition_callbacks:
|
|
gui_app.measure_frame_phase("transition", callback)
|
|
|
|
self._started_prev = self.started
|
|
|
|
if (self.started and self.sm.updated["selfdriveState"] and self.sm.valid["selfdriveState"] and
|
|
self.sm.alive["selfdriveState"] and self.sm.recv_frame["selfdriveState"] > self.started_frame):
|
|
stamp = self.sm.logMonoTime["selfdriveState"]
|
|
if 0 < stamp <= UIState.display_time_ns(self) <= stamp + 250_000_000:
|
|
self.personality = log.LongitudinalPersonality.schema.enumerants[self.sm["selfdriveState"].personality]
|
|
|
|
def _update_chestnut_state(self) -> None:
|
|
detected = self.sm["deviceState"].chestnutPresent
|
|
feed = getattr(self, "jetlink_status_feed", None)
|
|
jetlink = feed.snapshot() if feed is not None and getattr(self, "replay_clock", None) is None and not detected else None
|
|
if jetlink is not None and jetlink.state != "off" and not self.started:
|
|
self.chestnut_present = jetlink.state not in ("disconnected", "unavailable")
|
|
self.chestnut_state = (ChestnutState.DISCONNECTED if jetlink.state == "disconnected" else
|
|
ChestnutState.FAILED if jetlink.state == "unavailable" else
|
|
ChestnutState.READY if jetlink.state in ("ready", "active") else ChestnutState.LOADING)
|
|
return
|
|
if not self.started:
|
|
self.chestnut_present = detected
|
|
self.chestnut_state = (ChestnutState.FAILED if self.chestnut_alert else
|
|
ChestnutState.DISCONNECTED if not detected else
|
|
ChestnutState.LOADING if self.chestnut_checking else
|
|
ChestnutState.READY if self.chestnut_compiled else ChestnutState.UNCOMPILED)
|
|
return
|
|
|
|
model_seen = self.sm.recv_frame["modelV2"] > self.started_frame
|
|
model_stamp = self.sm.logMonoTime["modelV2"]
|
|
model_fresh = (model_seen and self.sm.valid["modelV2"] and self.sm.alive["modelV2"] and
|
|
0 < model_stamp <= UIState.display_time_ns(self) <= model_stamp + 250_000_000)
|
|
if jetlink is not None and jetlink.state != "off":
|
|
self.chestnut_present = jetlink.state not in ("disconnected", "unavailable")
|
|
if jetlink.state == "active" and model_fresh and self.sm["modelV2"].big:
|
|
self.chestnut_output_seen = True
|
|
self.chestnut_state = ChestnutState.ACTIVE
|
|
elif jetlink.state == "disconnected":
|
|
self.chestnut_state = ChestnutState.DISCONNECTED
|
|
elif jetlink.state == "unavailable" or self.chestnut_output_seen:
|
|
self.chestnut_state = ChestnutState.FAILED
|
|
else:
|
|
self.chestnut_state = ChestnutState.LOADING
|
|
return
|
|
if getattr(self, "replay_clock", None) is not None:
|
|
self.chestnut_present = detected
|
|
if model_fresh and self.sm["modelV2"].big:
|
|
self.chestnut_output_seen = True
|
|
if not self.chestnut_present:
|
|
self.chestnut_state = ChestnutState.DISCONNECTED
|
|
elif detected and model_fresh and self.sm["modelV2"].big:
|
|
self.chestnut_state = ChestnutState.ACTIVE
|
|
elif model_fresh and not self.sm["modelV2"].big:
|
|
self.chestnut_state = ChestnutState.FAILED
|
|
elif not self.chestnut_output_seen and self.chestnut_loading:
|
|
self.chestnut_state = ChestnutState.LOADING
|
|
elif not detected:
|
|
self.chestnut_state = ChestnutState.FAILED
|
|
elif not self.chestnut_output_seen and (self.chestnut_loading or not model_seen or self.chestnut_active is not False):
|
|
self.chestnut_state = ChestnutState.LOADING
|
|
else:
|
|
self.chestnut_state = ChestnutState.FAILED
|
|
|
|
def update_params(self) -> None:
|
|
# For slower operations
|
|
# Update longitudinal control state
|
|
CP_bytes = get_cache(self.params, "CarParamsPersistent")
|
|
if (getattr(self, "replay_clock", None) is not None and
|
|
self.sm.seen["carParams"] and self.sm.valid["carParams"]):
|
|
self.CP = self.sm["carParams"]
|
|
self.has_longitudinal_control = self.CP.openpilotLongitudinalControl
|
|
elif CP_bytes is not None:
|
|
self.CP = messaging.log_from_bytes(CP_bytes, car.CarParams)
|
|
# The saved Alpha Long choice can survive an unavailable configuration.
|
|
self.has_longitudinal_control = self.CP.openpilotLongitudinalControl
|
|
else:
|
|
self.CP = None
|
|
self.has_longitudinal_control = False
|
|
|
|
self.aol_runtime = bool(self.CP is not None and feature_enabled(self.params, self.CP, "aol", os.environ))
|
|
self.recording_audio = self.params.get_bool("RecordAudio") and self.started
|
|
self.is_metric = self.params.get_bool("IsMetric")
|
|
self.initial_cruise_speed = self.params.get("InitialCruiseSpeedKph", return_default=True)
|
|
self.always_on_dm = self.params.get_bool("AlwaysOnDM")
|
|
self.experimental_mode = self.params.get_bool("ExperimentalMode")
|
|
self.navigation_enabled = self.params.get_bool("NavigationEnabled")
|
|
self.nav_destination_set = self.params.get("NavDestination") is not None
|
|
raw_chestnut = self.params.get("ChestnutActive")
|
|
self.chestnut_active = raw_chestnut if type(raw_chestnut) is bool else (
|
|
True if raw_chestnut in (b"1", "1") else False if raw_chestnut in (b"0", "0") else None)
|
|
self.chestnut_loading = self.params.get_bool("ChestnutLoading")
|
|
self.chestnut_alert = any(self.params.get(k) is not None for k, a in OFFROAD_ALERTS.items()
|
|
if k.startswith("Offroad_Chestnut") and a["severity"] >= 0)
|
|
if getattr(self, "replay_clock", None) is not None:
|
|
self.chestnut_compiled = self.chestnut_active is True or self.chestnut_output_seen
|
|
return
|
|
now = time.monotonic()
|
|
if not self.started and (self._gpu_artifacts_at is None or now < self._gpu_artifacts_at or now - self._gpu_artifacts_at >= 1.):
|
|
self.chestnut_compiled, self.chestnut_checking = self._gpu_artifacts.selected_gpu_status()
|
|
self._gpu_artifacts_at = now
|
|
if cable_connected():
|
|
self.usb_disconnected_ts = None
|
|
if not self.usb_connected:
|
|
self.usb_connected = True
|
|
self.usb_connected_ts = now
|
|
self.usb_unknown = False
|
|
elif self.usb_connected_ts is not None and now - self.usb_connected_ts > 10.:
|
|
self.usb_unknown = not any(is_chestnut_usb_id(d["vendorId"], d["productId"], True) for d in get_usb_state())
|
|
self.usb_connected_ts = None
|
|
elif self.usb_connected:
|
|
if self.usb_disconnected_ts is None:
|
|
self.usb_disconnected_ts = now
|
|
elif now - self.usb_disconnected_ts > PARAM_UPDATE_TIME:
|
|
self.usb_connected = False
|
|
self.usb_connected_ts = None
|
|
self.usb_unknown = False
|
|
|
|
|
|
class Device:
|
|
def __init__(self):
|
|
self._ignition = False
|
|
self._interaction_time: float = -1
|
|
self._override_interactive_timeout: int | None = None
|
|
self._interactive_timeout_callbacks: list[Callable] = []
|
|
self._prev_timed_out = False
|
|
self._awake: bool = True
|
|
|
|
self._offroad_brightness: int = BACKLIGHT_OFFROAD
|
|
self._last_brightness: int = 0
|
|
self._brightness_filter = FirstOrderFilter(BACKLIGHT_OFFROAD, 10.00, 1 / gui_app.target_fps)
|
|
self._brightness_thread: threading.Thread | None = None
|
|
self._brightness_event = threading.Event()
|
|
self._brightness_target: int = 0
|
|
self._display_preferences = DisplayPreferences(driving_timeout=10 if gui_app.big_ui() else 5)
|
|
self._display_refresh_time: float | None = None
|
|
self._display_started = False
|
|
self._screen_off = False
|
|
self._screen_off_started = False
|
|
self._wake_touch_active = False
|
|
|
|
def invalidate_display_preferences(self) -> None:
|
|
self._display_refresh_time = None
|
|
|
|
def _refresh_display_preferences(self) -> None:
|
|
now = time.monotonic()
|
|
if self._display_refresh_time is not None and 0 <= now - self._display_refresh_time < 1.0:
|
|
return
|
|
previous = self._display_preferences
|
|
self._display_preferences = read_preferences(ui_state.params, large=gui_app.big_ui())
|
|
self._display_refresh_time = now
|
|
if previous != self._display_preferences and self._interaction_time > 0:
|
|
self._reset_interactive_timeout()
|
|
|
|
@property
|
|
def awake(self) -> bool:
|
|
return self._awake
|
|
|
|
def toggle_screen_off(self) -> bool:
|
|
if ui_state.projection_read_only or not ui_state.started:
|
|
return False
|
|
self._screen_off = self._awake
|
|
self._screen_off_started = ui_state.started
|
|
self._reset_interactive_timeout()
|
|
self._set_awake(not self._screen_off)
|
|
return True
|
|
|
|
def set_override_interactive_timeout(self, timeout: int | None) -> None:
|
|
# Override the interactive timeout duration temporarily
|
|
self._override_interactive_timeout = timeout
|
|
self._reset_interactive_timeout()
|
|
|
|
@property
|
|
def interactive_timeout(self) -> int:
|
|
if self._override_interactive_timeout is not None:
|
|
return self._override_interactive_timeout
|
|
|
|
if self._display_preferences.enabled:
|
|
return self._display_preferences.driving_timeout if ui_state.started else self._display_preferences.parked_timeout
|
|
|
|
ignition_timeout = 10 if gui_app.big_ui() else 5
|
|
return ignition_timeout if ui_state.started else 30
|
|
|
|
def _reset_interactive_timeout(self) -> None:
|
|
self._interaction_time = time.monotonic() + self.interactive_timeout
|
|
|
|
def add_interactive_timeout_callback(self, callback: Callable):
|
|
self._interactive_timeout_callbacks.append(callback)
|
|
|
|
def update(self):
|
|
if ui_state.projection_read_only:
|
|
return
|
|
self._start_brightness_thread() # start thread after manager forks ui
|
|
started_changed = self._display_started != bool(ui_state.started)
|
|
if started_changed:
|
|
self.invalidate_display_preferences()
|
|
self._refresh_display_preferences()
|
|
|
|
if started_changed:
|
|
self._reset_interactive_timeout()
|
|
self._display_started = bool(ui_state.started)
|
|
|
|
# do initial reset
|
|
if self._interaction_time <= 0:
|
|
self._reset_interactive_timeout()
|
|
|
|
self._update_wakefulness()
|
|
self._update_brightness()
|
|
|
|
def _start_brightness_thread(self):
|
|
if self._brightness_thread is None or not self._brightness_thread.is_alive():
|
|
self._brightness_thread = threading.Thread(target=self._brightness_worker, daemon=True)
|
|
self._brightness_thread.start()
|
|
|
|
def _brightness_worker(self):
|
|
drop_realtime()
|
|
while True:
|
|
self._brightness_event.wait()
|
|
self._brightness_event.clear()
|
|
HARDWARE.set_screen_brightness(self._brightness_target)
|
|
|
|
def set_offroad_brightness(self, brightness: int | None):
|
|
if brightness is None:
|
|
brightness = BACKLIGHT_OFFROAD
|
|
self._offroad_brightness = min(max(brightness, 0), 100)
|
|
|
|
def _update_brightness(self):
|
|
clipped_brightness = self._offroad_brightness
|
|
|
|
if ui_state.started and ui_state.light_sensor >= 0:
|
|
clipped_brightness = ui_state.light_sensor
|
|
|
|
# CIE 1931 - https://www.photonstophotos.net/GeneralTopics/Exposure/Psychometric_Lightness_and_Gamma.htm
|
|
if clipped_brightness <= 8:
|
|
clipped_brightness = clipped_brightness / 903.3
|
|
else:
|
|
clipped_brightness = ((clipped_brightness + 16.0) / 116.0) ** 3.0
|
|
|
|
clipped_brightness = float(np.interp(clipped_brightness, [0, 1], [30, 100]))
|
|
|
|
if self._display_preferences.enabled:
|
|
saved = (self._display_preferences.driving_brightness if ui_state.started else
|
|
self._display_preferences.parked_brightness)
|
|
if saved != AUTO:
|
|
clipped_brightness = saved
|
|
brightness = round(self._brightness_filter.update(clipped_brightness))
|
|
if not self._awake:
|
|
brightness = 0
|
|
|
|
if brightness != self._last_brightness:
|
|
self._brightness_target = brightness
|
|
self._brightness_event.set()
|
|
self._last_brightness = brightness
|
|
|
|
def _update_wakefulness(self):
|
|
# Handle interactive timeout
|
|
ignition_just_turned_off = not ui_state.ignition and self._ignition
|
|
ignition_changed = self._ignition != ui_state.ignition
|
|
self._ignition = ui_state.ignition
|
|
touched = any(ev.left_down for ev in gui_app.mouse_events)
|
|
if not self._awake and touched:
|
|
self._wake_touch_active = True
|
|
|
|
if (ignition_just_turned_off or (self._display_preferences.enabled and ignition_changed) or
|
|
touched):
|
|
self._reset_interactive_timeout()
|
|
|
|
road_changed = ui_state.started != self._screen_off_started
|
|
self._screen_off_started = ui_state.started
|
|
critical = (ui_state.sm.valid.get('selfdriveState', False) and ui_state.sm.alive.get('selfdriveState', False) and
|
|
ui_state.sm['selfdriveState'].alertStatus == log.SelfdriveState.AlertStatus.critical)
|
|
if not ui_state.started or road_changed or touched or critical:
|
|
self._screen_off = False
|
|
if critical:
|
|
self._reset_interactive_timeout()
|
|
gpu_loading = (ui_state.started and self._display_preferences.keep_screen_on_gpu_loading and ui_state.chestnut_loading)
|
|
if gpu_loading:
|
|
self._reset_interactive_timeout()
|
|
interaction_timeout = time.monotonic() > self._interaction_time
|
|
if interaction_timeout and not self._prev_timed_out:
|
|
for callback in self._interactive_timeout_callbacks:
|
|
callback()
|
|
self._prev_timed_out = interaction_timeout
|
|
|
|
auto_screen_off = ui_state.started and self._display_preferences.turn_off_screen and interaction_timeout and not critical and not gpu_loading
|
|
self._set_awake(not self._screen_off and not auto_screen_off and (ui_state.started or not interaction_timeout or PC))
|
|
# The entire wake gesture belongs to the display, including its release.
|
|
# Widgets were not rendered while asleep, so their touch state can be stale.
|
|
if self._wake_touch_active:
|
|
if any(ev.left_released for ev in gui_app.mouse_events):
|
|
self._wake_touch_active = False
|
|
gui_app.mouse_events.clear()
|
|
|
|
def _set_awake(self, on: bool):
|
|
if on != self._awake:
|
|
self._awake = on
|
|
cloudlog.debug(f"setting display power {int(on)}")
|
|
HARDWARE.set_display_power(on)
|
|
gui_app.set_should_render(on)
|
|
|
|
|
|
# Global instance
|
|
ui_state = UIState()
|
|
device = Device()
|