From 76d50df4665ec83e1dc6d9fc6502fbf90d5d8d94 Mon Sep 17 00:00:00 2001 From: Kumar <36933347+rav4kumar@users.noreply.github.com> Date: Fri, 23 Jan 2026 22:46:00 -0700 Subject: [PATCH] [TIZI/TICI] ui: MICI style turn signals (#1504) * mici turn signal for c3x * sp dir * decouple * more * ty * refactor and slim down * bigger --------- Co-authored-by: Jason Wen --- .../ui/sunnypilot/onroad/hud_renderer.py | 9 ++ selfdrive/ui/sunnypilot/onroad/turn_signal.py | 125 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 selfdrive/ui/sunnypilot/onroad/turn_signal.py diff --git a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py index 9a29a97b9f..0155e38a6e 100644 --- a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py +++ b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py @@ -10,6 +10,7 @@ from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.rocket_fuel import RocketFuel +from openpilot.selfdrive.ui.sunnypilot.onroad.turn_signal import TurnSignalController class HudRendererSP(HudRenderer): @@ -17,9 +18,17 @@ class HudRendererSP(HudRenderer): super().__init__() self.developer_ui = DeveloperUiRenderer() self.rocket_fuel = RocketFuel() + self.turn_signal_controller = TurnSignalController() + + def _update_state(self) -> None: + super()._update_state() + self.turn_signal_controller.update() def _render(self, rect: rl.Rectangle) -> None: super()._render(rect) self.developer_ui.render(rect) + + self.turn_signal_controller.render() + if ui_state.rocket_fuel: self.rocket_fuel.render(rect, ui_state.sm) diff --git a/selfdrive/ui/sunnypilot/onroad/turn_signal.py b/selfdrive/ui/sunnypilot/onroad/turn_signal.py new file mode 100644 index 0000000000..ad14e72f5a --- /dev/null +++ b/selfdrive/ui/sunnypilot/onroad/turn_signal.py @@ -0,0 +1,125 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import pyray as rl +import time +from dataclasses import dataclass + +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.mici.onroad.alert_renderer import IconSide, TURN_SIGNAL_BLINK_PERIOD +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.widgets import Widget +from openpilot.common.filter_simple import FirstOrderFilter + + +@dataclass(frozen=True) +class TurnSignalConfig: + left_x: int = 870 + left_y: int = 220 + right_x: int = 1140 + right_y: int = 220 + size: int = 150 + + +class TurnSignalWidget(Widget): + def __init__(self, direction: IconSide): + super().__init__() + self._direction = direction + self._active = False + + self._turn_signal_timer = 0.0 + self._turn_signal_alpha_filter = FirstOrderFilter(0.0, 0.3, 1 / gui_app.target_fps) + + texture_path = f'icons_mici/onroad/turn_signal_{direction}.png' + self._texture = gui_app.texture(texture_path, 120, 109) + + def _render(self, _): + if not self._active: + return + + if time.monotonic() - self._turn_signal_timer > TURN_SIGNAL_BLINK_PERIOD: + self._turn_signal_timer = time.monotonic() + self._turn_signal_alpha_filter.x = 255 * 2 + else: + self._turn_signal_alpha_filter.update(255 * 0.2) + + icon_alpha = int(min(self._turn_signal_alpha_filter.x, 255)) + + if self._texture: + pos_x = int(self._rect.x + (self._rect.width - self._texture.width) / 2) + pos_y = int(self._rect.y + (self._rect.height - self._texture.height) / 2) + color = rl.Color(255, 255, 255, icon_alpha) + rl.draw_texture(self._texture, pos_x, pos_y, color) + + def activate(self): + if not self._active: + self._turn_signal_timer = 0.0 + self._active = True + + def deactivate(self): + self._active = False + self._turn_signal_timer = 0.0 + + +class TurnSignalController: + def __init__(self, config: TurnSignalConfig | None = None): + self._config = config or TurnSignalConfig() + self._left_signal = TurnSignalWidget(direction=IconSide.left) + self._right_signal = TurnSignalWidget(direction=IconSide.right) + self._last_icon_side = None + + def update(self): + sm = ui_state.sm + ss = sm['selfdriveState'] + + event_name = ss.alertType.split('/')[0] if ss.alertType else '' + + if event_name == 'preLaneChangeLeft': + self._last_icon_side = IconSide.left + self._left_signal.activate() + self._right_signal.deactivate() + + elif event_name == 'preLaneChangeRight': + self._last_icon_side = IconSide.right + self._right_signal.activate() + self._left_signal.deactivate() + + elif event_name == 'laneChange': + if self._last_icon_side == IconSide.left: + self._left_signal.activate() + self._right_signal.deactivate() + elif self._last_icon_side == IconSide.right: + self._right_signal.activate() + self._left_signal.deactivate() + + else: + self._last_icon_side = None + self._left_signal.deactivate() + self._right_signal.deactivate() + + def render(self): + if self._last_icon_side == IconSide.left: + self._left_signal.render(rl.Rectangle( + self._config.left_x, + self._config.left_y, + self._config.size, + self._config.size + )) + elif self._last_icon_side == IconSide.right: + self._right_signal.render(rl.Rectangle( + self._config.right_x, + self._config.right_y, + self._config.size, + self._config.size + )) + + @property + def config(self) -> TurnSignalConfig: + return self._config + + @config.setter + def config(self, new_config: TurnSignalConfig): + self._config = new_config