Files
StarPilot/openpilot/starpilot/ui/speed_limit_pulse.py
T
2026-10-26 20:38:11 -04:00

81 lines
3.0 KiB
Python

"""Speed-limit change highlight shared by both native layouts."""
import math
import pyray as rl
from openpilot.starpilot.ui.onroad_state import ObservationKind, OnroadState
from openpilot.starpilot.ui.unified_speed_presentation import displayed_limit_mps
CONFIRMATION_PULSES = ((0.0, .7), (.9, .7))
CONFIRMATION_CYCLE_SECONDS = 3.2
class SpeedLimitPulse:
def __init__(self):
self.last_limit = 0.0
self.start_time = -math.inf
self.manual_pending = False
self.display_pulsing = False
self._started_frame: int | None = None
def update(self, state: OnroadState, now: float, *, visible: bool = True) -> None:
if state.drive_frame != self._started_frame:
self.last_limit = 0.0
self.display_pulsing = False
self.start_time = -math.inf
self._started_frame = state.drive_frame
observation = state.speed_limit
limit = displayed_limit_mps(observation)
pending = (observation.kind == ObservationKind.VALID and observation.pending_speed_limit_mps is not None and
observation.pending_speed_limit_mps > 0)
if not visible or limit is None:
self.start_time = -math.inf
self.manual_pending = self.display_pulsing = False
if limit is not None:
self.last_limit = limit
return
if not pending:
if (not observation.action_enabled and observation.kind == ObservationKind.VALID and
limit != self.last_limit):
self.start_time = now
self.display_pulsing = True
elif not self.display_pulsing or observation.action_enabled:
self.start_time = -math.inf
self.display_pulsing = False
self.manual_pending = False
self.last_limit = limit
return
self.display_pulsing = False
manual = not observation.automatic_adoption_pending
if observation.pending_since_ns > 0:
self.start_time = observation.pending_since_ns / 1e9
elif limit != self.last_limit or manual != self.manual_pending or not math.isfinite(self.start_time):
self.start_time = now
self.manual_pending = manual
self.last_limit = limit
def confirmation_strength(self, now: float) -> float:
elapsed = now - self.start_time
if not self.manual_pending or not 0 <= elapsed < 10:
return 0.0
phase = elapsed % CONFIRMATION_CYCLE_SECONDS
for start, duration in CONFIRMATION_PULSES:
if start <= phase < start + duration:
return math.sin(math.pi * (phase - start) / duration) ** 2
return 0.0
def color(self, base: rl.Color, now: float) -> rl.Color:
if isinstance(base, (tuple, list)):
base = rl.Color(*base)
elapsed = now - self.start_time
if self.manual_pending or not math.isfinite(elapsed) or elapsed < 0:
return base
if elapsed >= 3.0:
return base
fraction = (1 - math.cos(2 * math.pi * elapsed)) / 2
target = (188, 132, 255)
return rl.Color(*(round(value + (target - value) * fraction)
for value, target in zip((base.r, base.g, base.b), target, strict=True)), base.a)