diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py index 8afe43aed..1d545b52b 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -21,6 +21,7 @@ from openpilot.selfdrive.ui.mici.onroad.starpilot_status import ( ) from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView from openpilot.selfdrive.ui.onroad.starpilot.pip_sidecam import PipSideCamera +from openpilot.selfdrive.ui.onroad.starpilot.starpilot_border import get_traffic_border_colors from openpilot.selfdrive.ui.lib.starpilot_visuals import get_border_width from openpilot.starpilot.common.favorite_slots import is_favorite_action_key, load_favorite_slots, toggle_favorite_slot from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent @@ -825,6 +826,17 @@ class AugmentedRoadView(CameraView): int(self._content_rect.height), ) rl.draw_rectangle_rounded_lines_ex(border_rect, 0.12, 16, border_size, get_border_color(ui_state)) + + if (colors := get_traffic_border_colors()) is not None: + for x, w, color in ( + (border_rect.x, border_rect.width / 2, colors[0]), + (border_rect.x + border_rect.width / 2, border_rect.width - border_rect.width / 2, colors[1]), + ): + if color.a > 0: + rl.begin_scissor_mode(int(x), int(border_rect.y), int(w), int(border_rect.height)) + rl.draw_rectangle_rounded_lines_ex(border_rect, 0.12, 16, border_size, color) + rl.end_scissor_mode() + rl.end_scissor_mode() def _get_border_width(self) -> int: diff --git a/selfdrive/ui/onroad/starpilot/starpilot_border.py b/selfdrive/ui/onroad/starpilot/starpilot_border.py index b2a6f4dee..495a8d5e1 100644 --- a/selfdrive/ui/onroad/starpilot/starpilot_border.py +++ b/selfdrive/ui/onroad/starpilot/starpilot_border.py @@ -170,55 +170,65 @@ def _render_csc_glow(border_rect: rl.Rectangle, border_width: float = UI_BORDER_ _smoothed_steer = 0.0 +def get_traffic_border_colors() -> tuple[rl.Color, rl.Color] | None: + sm = ui_state.sm + car_state = sm["carState"] if sm.valid.get("carState", False) else None + if car_state is None: + return None + params = ui_state.ui_params + show_signal = params.get_bool("SignalMetrics") + show_blindspot = params.get_bool("BlindSpotMetrics") + if not (show_signal or show_blindspot): + return None + + left_blindspot = car_state.leftBlindspot + right_blindspot = car_state.rightBlindspot + if ui_state.starpilot_toggles.get("v_asm_enabled", False): + vasm_left, vasm_right = get_fresh_vasm_state(ui_state.params_memory) + left_blindspot = left_blindspot or vasm_left + right_blindspot = right_blindspot or vasm_right + left_blinker = car_state.leftBlinker + right_blinker = car_state.rightBlinker + + if not ((show_signal and (left_blinker or right_blinker)) or (show_blindspot and (left_blindspot or right_blindspot))): + return None + + interval = 250 if show_blindspot and (left_blindspot or right_blindspot) else 500 + flicker_active = (int(rl.get_time() * 1000) % (interval * 2)) < interval + + def get_half_border_color(blindspot, turn_signal): + if turn_signal and show_signal: + if blindspot: + return TRAFFIC_COLOR if flicker_active else CEM_OVERRIDE_COLOR + else: + return CEM_OVERRIDE_COLOR if flicker_active else rl.Color(0, 0, 0, 0) + elif blindspot and show_blindspot: + return TRAFFIC_COLOR + else: + return rl.Color(0, 0, 0, 0) + + left_color = get_half_border_color(left_blindspot, left_blinker) + right_color = get_half_border_color(right_blindspot, right_blinker) + + return left_color, right_color + + def render_background_effects(rect: rl.Rectangle, border_width: float): global _smoothed_steer sm = ui_state.sm # 1. Turn Signal and Blind Spot indicators - car_state = sm["carState"] if sm.valid.get("carState", False) else None - if car_state: - params = ui_state.ui_params - show_signal = params.get_bool("SignalMetrics") - show_blindspot = params.get_bool("BlindSpotMetrics") - if show_signal or show_blindspot: - left_blindspot = car_state.leftBlindspot - right_blindspot = car_state.rightBlindspot - if ui_state.starpilot_toggles.get("v_asm_enabled", False): - vasm_left, vasm_right = get_fresh_vasm_state(ui_state.params_memory) - left_blindspot = left_blindspot or vasm_left - right_blindspot = right_blindspot or vasm_right - left_blinker = car_state.leftBlinker - right_blinker = car_state.rightBlinker - - if (show_signal and (left_blinker or right_blinker)) or (show_blindspot and (left_blindspot or right_blindspot)): - interval = 250 if show_blindspot and (left_blindspot or right_blindspot) else 500 - flicker_active = (int(rl.get_time() * 1000) % (interval * 2)) < interval - - def get_half_border_color(blindspot, turn_signal): - if turn_signal and show_signal: - if blindspot: - return TRAFFIC_COLOR if flicker_active else CEM_OVERRIDE_COLOR - else: - return CEM_OVERRIDE_COLOR if flicker_active else rl.Color(0, 0, 0, 0) - elif blindspot and show_blindspot: - return TRAFFIC_COLOR - else: - return rl.Color(0, 0, 0, 0) - - left_color = get_half_border_color(left_blindspot, left_blinker) - right_color = get_half_border_color(right_blindspot, right_blinker) - - # Draw left side borders - if left_color.a > 0: - rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width // 2), int(rect.height)) - rl.draw_rectangle_rounded(rect, 0.12, 10, left_color) - rl.end_scissor_mode() - - # Draw right side borders - if right_color.a > 0: - rl.begin_scissor_mode(int(rect.x + rect.width // 2), int(rect.y), int(rect.width // 2), int(rect.height)) - rl.draw_rectangle_rounded(rect, 0.12, 10, right_color) - rl.end_scissor_mode() + colors = get_traffic_border_colors() + if colors is not None: + left_color, right_color = colors + if left_color.a > 0: + rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width // 2), int(rect.height)) + rl.draw_rectangle_rounded(rect, 0.12, 10, left_color) + rl.end_scissor_mode() + if right_color.a > 0: + rl.begin_scissor_mode(int(rect.x + rect.width // 2), int(rect.y), int(rect.width // 2), int(rect.height)) + rl.draw_rectangle_rounded(rect, 0.12, 10, right_color) + rl.end_scissor_mode() # 2. Steering Torque Border car_control = sm["carControl"] if sm.valid.get("carControl", False) else None diff --git a/selfdrive/ui/tests/test_traffic_border.py b/selfdrive/ui/tests/test_traffic_border.py new file mode 100644 index 000000000..86e6b85f2 --- /dev/null +++ b/selfdrive/ui/tests/test_traffic_border.py @@ -0,0 +1,149 @@ +from types import SimpleNamespace + +from openpilot.selfdrive.ui.lib.starpilot_status import TRAFFIC_COLOR, CEM_OVERRIDE_COLOR +from openpilot.selfdrive.ui.onroad.starpilot import starpilot_border +from openpilot.selfdrive.ui.ui_state import ui_state + +TRANSPARENT = (0, 0, 0, 0) + + +def _rgba(color): + return color.r, color.g, color.b, color.a + + +def _car_state(left_blindspot=False, right_blindspot=False, left_blinker=False, right_blinker=False): + return SimpleNamespace( + leftBlindspot=left_blindspot, + rightBlindspot=right_blindspot, + leftBlinker=left_blinker, + rightBlinker=right_blinker, + ) + + +def _setup(monkeypatch, *, car_state, signal=True, blindspot=True, v_asm_enabled=False, v_asm=(False, False), time=0.0): + class FakeSM(dict): + valid = {"carState": True} + + monkeypatch.setattr(ui_state, "sm", FakeSM(carState=car_state)) + monkeypatch.setattr( + ui_state, + "ui_params", + SimpleNamespace(get_bool=lambda key: {"SignalMetrics": signal, "BlindSpotMetrics": blindspot}[key]), + ) + monkeypatch.setattr(ui_state, "starpilot_toggles", {"v_asm_enabled": v_asm_enabled}) + monkeypatch.setattr(ui_state, "params_memory", object()) + monkeypatch.setattr(starpilot_border, "get_fresh_vasm_state", lambda _memory: v_asm) + monkeypatch.setattr(starpilot_border.rl, "get_time", lambda: time) + + +def test_traffic_border_inactive_when_metrics_disabled(monkeypatch): + _setup(monkeypatch, car_state=_car_state(left_blinker=True), signal=False, blindspot=False) + + assert starpilot_border.get_traffic_border_colors() is None + + +def test_traffic_border_inactive_when_nothing_active(monkeypatch): + _setup(monkeypatch, car_state=_car_state()) + + assert starpilot_border.get_traffic_border_colors() is None + + +def test_traffic_border_left_blindspot_is_red(monkeypatch): + _setup(monkeypatch, car_state=_car_state(left_blindspot=True)) + + left, right = starpilot_border.get_traffic_border_colors() + + assert _rgba(left) == _rgba(TRAFFIC_COLOR) + assert _rgba(right) == TRANSPARENT + + +def test_traffic_border_right_blindspot_is_red(monkeypatch): + _setup(monkeypatch, car_state=_car_state(right_blindspot=True)) + + left, right = starpilot_border.get_traffic_border_colors() + + assert _rgba(left) == TRANSPARENT + assert _rgba(right) == _rgba(TRAFFIC_COLOR) + + +def test_traffic_border_blinker_alone_flickers_amber(monkeypatch): + _setup(monkeypatch, car_state=_car_state(left_blinker=True), blindspot=False, time=0.1) + + left, right = starpilot_border.get_traffic_border_colors() + assert _rgba(left) == _rgba(CEM_OVERRIDE_COLOR) + assert _rgba(right) == TRANSPARENT + + _setup(monkeypatch, car_state=_car_state(left_blinker=True), blindspot=False, time=0.6) + left, right = starpilot_border.get_traffic_border_colors() + assert _rgba(left) == TRANSPARENT + assert _rgba(right) == TRANSPARENT + + +def test_traffic_border_blinker_with_blindspot_flickers_red_and_amber(monkeypatch): + _setup(monkeypatch, car_state=_car_state(left_blinker=True, left_blindspot=True), time=0.1) + + left, _ = starpilot_border.get_traffic_border_colors() + assert _rgba(left) == _rgba(TRAFFIC_COLOR) + + _setup(monkeypatch, car_state=_car_state(left_blinker=True, left_blindspot=True), time=0.3) + left, _ = starpilot_border.get_traffic_border_colors() + assert _rgba(left) == _rgba(CEM_OVERRIDE_COLOR) + + +def test_traffic_border_v_asm_blindspot_is_red(monkeypatch): + _setup(monkeypatch, car_state=_car_state(), v_asm_enabled=True, v_asm=(True, False)) + + left, right = starpilot_border.get_traffic_border_colors() + assert _rgba(left) == _rgba(TRAFFIC_COLOR) + assert _rgba(right) == TRANSPARENT + + +def test_c4_draw_border_paints_traffic_color_on_active_half(monkeypatch): + import pyray as rl + from openpilot.selfdrive.ui.mici.onroad import augmented_road_view as mici_view + + view = object.__new__(mici_view.AugmentedRoadView) + view._content_rect = rl.Rectangle(10, 20, 200, 100) + view._get_border_width = lambda: 8 + view._closed = True + + base_color = rl.Color(0, 0, 0, 255) + calls = [] + monkeypatch.setattr(mici_view, "get_border_color", lambda _state: base_color) + monkeypatch.setattr(mici_view, "get_traffic_border_colors", lambda: (TRAFFIC_COLOR, rl.Color(0, 0, 0, 0))) + monkeypatch.setattr(mici_view.rl, "begin_scissor_mode", lambda *args: calls.append(("begin_scissor", args))) + monkeypatch.setattr(mici_view.rl, "end_scissor_mode", lambda: calls.append(("end_scissor",))) + monkeypatch.setattr(mici_view.rl, "draw_rectangle_rounded_lines_ex", lambda *args: calls.append(("line", args))) + + view._draw_border() + + lines = [c for c in calls if c[0] == "line"] + assert len(lines) == 2 + assert _rgba(lines[0][1][4]) == _rgba(base_color) + assert _rgba(lines[1][1][4]) == _rgba(TRAFFIC_COLOR) + + scissor = [c for c in calls if c[0] == "begin_scissor"] + assert scissor[0][1] == (10, 20, 200, 100) + assert scissor[1][1] == (14, 24, 96, 92) + + +def test_c4_draw_border_skips_traffic_colors_when_inactive(monkeypatch): + import pyray as rl + from openpilot.selfdrive.ui.mici.onroad import augmented_road_view as mici_view + + view = object.__new__(mici_view.AugmentedRoadView) + view._content_rect = rl.Rectangle(10, 20, 200, 100) + view._get_border_width = lambda: 8 + view._closed = True + + calls = [] + monkeypatch.setattr(mici_view, "get_border_color", lambda _state: rl.Color(0, 0, 0, 255)) + monkeypatch.setattr(mici_view, "get_traffic_border_colors", lambda: None) + monkeypatch.setattr(mici_view.rl, "begin_scissor_mode", lambda *args: calls.append(("begin_scissor", args))) + monkeypatch.setattr(mici_view.rl, "end_scissor_mode", lambda: calls.append(("end_scissor",))) + monkeypatch.setattr(mici_view.rl, "draw_rectangle_rounded_lines_ex", lambda *args: calls.append(("line", args))) + + view._draw_border() + + assert len([c for c in calls if c[0] == "line"]) == 1 + assert len([c for c in calls if c[0] == "begin_scissor"]) == 1