diff --git a/openpilot/starpilot/ui/onroad_personality.py b/openpilot/starpilot/ui/onroad_personality.py index 30f29c9e7e..e161d6d9dc 100644 --- a/openpilot/starpilot/ui/onroad_personality.py +++ b/openpilot/starpilot/ui/onroad_personality.py @@ -5,7 +5,7 @@ import pyray as rl from openpilot.common.filter_simple import FirstOrderFilter from openpilot.starpilot.ui.onroad_state import AlertSize, OnroadState, is_lane_change_notice, personality_bounds from openpilot.starpilot.ui.onroad_widget_style import draw_widget_frame -from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole, Profile +from openpilot.starpilot.ui.presentation import BitmapFonts, Profile from openpilot.system.ui.lib.application import gui_app HIGHLIGHT_TIME = 2.5 @@ -47,19 +47,8 @@ class PersonalityWidget: width, height = right - left, bottom - top rect = rl.Rectangle(left, top, width, height) draw_widget_frame(rect, state.customization, profile, 'following_distance') - scale = width / 60 - label = ({'active': 'TRF', 'paused': 'PAUSE', 'off': 'OFF'}.get(state.traffic_display.state) - if state.traffic_display is not None else None) - icon = rl.Rectangle(rect.x, rect.y, width, height - 14 * scale if label else height) - self.draw(icon, personality=state.personality, has_lead=state.lead_detected, longitudinal_active=state.cruise_active, - traffic_mode=state.traffic_mode, gas_override=state.gas_pressed_override, visible=routine) - if label and self.fonts is not None and state.alert.size != AlertSize.FULL: - size = 11 * scale * Profile.COMPACT.font_scale / profile.font_scale - measured = self.fonts.measure(label, FontRole.SEMI_BOLD, size) - color = ((255, 155, 63, 255) if state.traffic_display.state == 'paused' else - (200, 32, 48, 255) if state.traffic_mode else (112, 192, 216, 255)) - self.fonts.draw(label, FontRole.SEMI_BOLD, size, rect.x + (width - measured.width) / 2, - rect.y + height - 14 * scale, rl.Color(*color)) + self.draw(rect, personality=state.personality, has_lead=state.lead_detected, longitudinal_active=state.cruise_active, + traffic_mode=state.traffic_selected or state.traffic_mode, gas_override=state.gas_pressed_override, visible=routine) def draw(self, rect: rl.Rectangle, *, personality: int, has_lead: bool, longitudinal_active: bool, traffic_mode: bool = False, gas_override: bool = False, visible: bool = True, fit: bool = True) -> None: @@ -82,7 +71,7 @@ class PersonalityWidget: if not visible and now - self._personality_changed_time < HIGHLIGHT_TIME: self._personality_changed_time = now highlight = now - self._personality_changed_time < HIGHLIGHT_TIME - blink = 0.35 / 0.9 if gas_override and longitudinal_active and now % 0.375 > 0.1875 else 1.0 + blink = 0.35 / 0.9 if gas_override and longitudinal_active and not traffic_mode and now % 0.375 > 0.1875 else 1.0 count = 1 if traffic_mode else personality + 1 for i, ((white, green), y, (active_f, green_f)) in enumerate(zip(self._distance, (122, 136, 152), self._distance_filters, strict=True)): active = active_f.update(float(i < count)) diff --git a/openpilot/starpilot/ui/onroad_state.py b/openpilot/starpilot/ui/onroad_state.py index f65aa2ac6c..a966460a28 100644 --- a/openpilot/starpilot/ui/onroad_state.py +++ b/openpilot/starpilot/ui/onroad_state.py @@ -195,6 +195,7 @@ class OnroadState: right_hand_drive: bool = False switchback_mode: bool = False traffic_mode: bool = False + traffic_selected: bool = False traffic_display: TrafficDisplay | None = None aol_latched: bool | None = None lateral_active: bool = False diff --git a/openpilot/starpilot/ui/runtime_app.py b/openpilot/starpilot/ui/runtime_app.py index a2200dc1af..d9f35462ea 100644 --- a/openpilot/starpilot/ui/runtime_app.py +++ b/openpilot/starpilot/ui/runtime_app.py @@ -452,7 +452,7 @@ class StarShellSession: invoke=lambda: guarded(device.toggle_screen_off)) if not hasattr(self, 'mode_actions'): self.mode_actions = ModeActionPublisher() - for key, label, mode, requested in ((TRAFFIC, "Traffic Mode", 'traffic', state.traffic_mode), + for key, label, mode, requested in ((TRAFFIC, "Traffic Mode", 'traffic', state.traffic_selected or state.traffic_mode), (SWITCHBACK, "Switchback Mode", 'switchback', getattr(state, 'switchback_mode', False))): now_ns = time.monotonic_ns() allowed = (self._favorite_authority() and not ui_state.params.get_bool("SafeMode") and diff --git a/openpilot/starpilot/ui/runtime_snapshot.py b/openpilot/starpilot/ui/runtime_snapshot.py index 869b33373e..de57f26f2e 100644 --- a/openpilot/starpilot/ui/runtime_snapshot.py +++ b/openpilot/starpilot/ui/runtime_snapshot.py @@ -693,6 +693,7 @@ class RuntimeSnapshotAdapter: self._switchback.sample_state(slc, drive_id=drive_id, now_ns=now_ns, event_ns=int(sm.logMonoTime['slcState']))), traffic_mode=bool(traffic is not None and traffic.state == 'active'), + traffic_selected=bool(started and self._traffic.drive_id == drive_id and self._traffic.requested), traffic_display=traffic, torque_utilization=torque.utilization if torque.utilization is not None else 0.0, torque_source_available=torque.utilization is not None, diff --git a/openpilot/starpilot/ui/tests/test_long_indicator.py b/openpilot/starpilot/ui/tests/test_long_indicator.py index c72987e2cf..4151937e5d 100644 --- a/openpilot/starpilot/ui/tests/test_long_indicator.py +++ b/openpilot/starpilot/ui/tests/test_long_indicator.py @@ -99,18 +99,25 @@ def test_gas_override_blinks_only_selected_bars_and_clears_on_disengagement(disp @pytest.mark.parametrize('profile', list(Profile)) -def test_traffic_label_and_accent_restore_selected_personality(display, profile): +def test_traffic_accent_is_stable_without_labels_and_restores_personality(display, profile): display.fonts.profile = profile state = road(personality=2, traffic_mode=True, traffic_display=TrafficDisplay('active', 'TRAFFIC')) draws = settle(display, state) assert draws[2].args[4] == (200, 32, 48) assert draws[4].args[3] == pytest.approx(.35, abs=.001) assert all(draws[i].args[3] < .001 for i in (3, 5, 7)) - assert display.fonts.draw.call_args.args[0] == 'TRF' - for status, label in (('paused', 'PAUSE'), ('off', 'OFF')): - draws = settle(display, replace(state, traffic_mode=False, traffic_display=TrafficDisplay(status, ''))) - assert all(draws[i].args[3] == pytest.approx(.9, abs=.001) for i in (2, 4, 6)) - assert display.fonts.draw.call_args.args[0] == label + original_rect = draws[2].args[1] + for status in ('paused', 'unavailable_source', 'unavailable_profile', None): + draws = settle(display, replace(state, traffic_mode=False, traffic_selected=True, + gas_pressed_override=True, + traffic_display=TrafficDisplay(status, '') if status else None)) + assert draws[2].args[4] == (200, 32, 48) + assert draws[2].args[3] == pytest.approx(.9, abs=.001) + rect = draws[2].args[1] + assert (rect.x, rect.y, rect.width, rect.height) == (original_rect.x, original_rect.y, original_rect.width, original_rect.height) + draws = settle(display, replace(state, traffic_mode=False, traffic_selected=False, traffic_display=TrafficDisplay('off', 'OFF'))) + assert all(draws[i].args[3] == pytest.approx(.9, abs=.001) for i in (2, 4, 6)) + display.fonts.draw.assert_not_called() def test_large_default_sits_left_of_driver_monitor_and_custom_positions_stay_put(display): diff --git a/openpilot/starpilot/ui/tests/test_traffic_status.py b/openpilot/starpilot/ui/tests/test_traffic_status.py index 88f52a371f..d299917f02 100644 --- a/openpilot/starpilot/ui/tests/test_traffic_status.py +++ b/openpilot/starpilot/ui/tests/test_traffic_status.py @@ -74,15 +74,19 @@ class TestTrafficStatus(unittest.TestCase): def test_actual_wire_states_and_clearing(self): active = receipt(self.settings_hash, self.map_hash) self.assertEqual(self.project(active).state, 'active') + self.assertTrue(self.reader.requested) self.assertEqual(self.project(active).state, 'active') # Same frame remains fresh, not a new source. self.assertIsNone(self.project(active, now_mono_ns=NOW + 101_000_000)) + self.assertTrue(self.reader.requested) self.assertIsNone(self.project(active, now_boot_ns=BOOT + 400_000_000)) paused = receipt(self.settings_hash, self.map_hash, sequence=2, observed=NOW + 10_000_000, reason='authority_unavailable', effective=False) self.assertEqual(self.project(paused, now_mono_ns=NOW + 10_000_000, long_active=False).state, 'paused') + self.assertTrue(self.reader.requested) off = receipt(self.settings_hash, self.map_hash, sequence=3, observed=NOW + 20_000_000, reason='off', accepted=False, effective=False) self.assertEqual(self.project(off, now_mono_ns=NOW + 20_000_000).state, 'off') + self.assertFalse(self.reader.requested) unavailable = receipt(self.settings_hash, self.map_hash, sequence=4, observed=NOW + 30_000_000, ready=False) self.assertEqual(self.project(unavailable, now_mono_ns=NOW + 30_000_000).state, 'unavailable_profile') self.assertIsNone(self.project(active)) # Old sequence cannot resurrect active status. @@ -133,7 +137,7 @@ class TestTrafficStatus(unittest.TestCase): rail = MiciSidebarWidgets(fonts) with mock.patch.object(rail.personality, 'draw'): rail._personality(rl.Rectangle(476, 160, 60, 80), shown) - self.assertEqual(fonts.draw.call_args.args[:3], ('TRF', FontRole.SEMI_BOLD, 11)) + fonts.draw.assert_not_called() large = OnroadView.__new__(OnroadView) large.personality = mock.Mock() large.fonts = fonts @@ -143,6 +147,7 @@ class TestTrafficStatus(unittest.TestCase): large._traffic_badge(replace(shown, viewport_width=width), 101) self.assertEqual(fonts.draw.call_args.args[:5], ('TRAFFIC', FontRole.SEMI_BOLD, 25, expected_x, 101)) self.assertFalse(adapter.build(ShellMode.ONROAD, now_ns=NOW + 101_000_000).onroad.traffic_mode) + self.assertTrue(adapter.build(ShellMode.ONROAD, now_ns=NOW + 101_000_000).onroad.traffic_selected) self.assertIsNone(adapter.build(ShellMode.ONROAD, now_ns=NOW + 101_000_000).onroad.traffic_display)