From d1beeffbc29659822ab85c9ffdd881a7933240f3 Mon Sep 17 00:00:00 2001 From: dirwin31 <83434411+dirwin31@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:21:25 -0700 Subject: [PATCH] SLC: Keep nav directions and sources up, hide redundant Use limit --- .../starpilot/ui/onroad_large_widgets.py | 9 ++++---- openpilot/starpilot/ui/onroad_navigation.py | 22 +++++++++++++++++-- openpilot/starpilot/ui/onroad_state.py | 11 +++++++++- .../ui/tests/test_onroad_navigation.py | 12 ++++++++-- .../starpilot/ui/tests/test_slc_ui_actions.py | 14 +++++++++--- 5 files changed, 56 insertions(+), 12 deletions(-) diff --git a/openpilot/starpilot/ui/onroad_large_widgets.py b/openpilot/starpilot/ui/onroad_large_widgets.py index 45330e5c0c..0988490926 100644 --- a/openpilot/starpilot/ui/onroad_large_widgets.py +++ b/openpilot/starpilot/ui/onroad_large_widgets.py @@ -12,7 +12,7 @@ import json import pyray as rl from openpilot.starpilot.ui.onroad_customization import offset, rgba, widget_size -from openpilot.starpilot.ui.onroad_state import ObservationKind, OnroadState +from openpilot.starpilot.ui.onroad_state import OnroadState from openpilot.starpilot.ui.onroad_widget_style import draw_control_card from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole from openpilot.starpilot.ui.wheel_feedback import WheelFeedbackFade @@ -40,7 +40,8 @@ class UnifiedSpeedWidget: from openpilot.starpilot.ui.large_speed_geometry import PADDING rows = [row for row in state.speed_limit.source_readings if row.enabled] if not rows: - _center(self.fonts, 'No sources enabled', FontRole.NORMAL, 26, rect, rect.y + 22) + text = 'No sources enabled' if state.speed_limit.source_readings else 'No speed limit' + _center(self.fonts, text, FontRole.NORMAL, 26, rect, rect.y + 22) return active = state.speed_limit.accepted_source labels = {'dashboard': 'Dash', 'map': 'Map', 'vision': 'Vision', 'online': 'Online'} @@ -65,8 +66,8 @@ class UnifiedSpeedWidget: height = card_height(shown.mode) rect = rl.Rectangle(saved['x'], saved['y'], WIDTH, height) # Sources follow the Galaxy layout setting: a row in this card, or a small card of its own right below. - sources = (state.customization.get('speedSources') is True and shown.mode != 'max_only' and - state.speed_limit.kind == ObservationKind.VALID and bool(state.speed_limit.session_id)) + # Kept up whenever enabled so the card does not pop in and out as limits come and go. + sources = state.customization.get('speedSources') is True separate = state.customization.get('speedSourcesCard') is True if sources and not separate: rect.height += SOURCE_ROW_HEIGHT diff --git a/openpilot/starpilot/ui/onroad_navigation.py b/openpilot/starpilot/ui/onroad_navigation.py index d5b384244f..52af83b05c 100644 --- a/openpilot/starpilot/ui/onroad_navigation.py +++ b/openpilot/starpilot/ui/onroad_navigation.py @@ -23,6 +23,18 @@ def icon_name(maneuver_type: str, modifier: str) -> str: return candidate if candidate in MANIFEST else 'direction_turn_straight.png' +def _overlaps_slc_actions(rect, profile, state) -> bool: + from openpilot.starpilot.ui.large_speed_geometry import ACTION_HEADER_HEIGHT + from openpilot.starpilot.ui.onroad_state import slc_controls + for control in slc_controls(profile, state): + # Include the decision header drawn above the buttons. + left, top, right, bottom = control.bounds + top -= ACTION_HEADER_HEIGHT + if left < rect.x + rect.width and rect.x < right and top < rect.y + rect.height and rect.y < bottom: + return True + return False + + def arrival_time(remaining_seconds: float, now: datetime) -> str: arrival = now + timedelta(seconds=max(0, remaining_seconds)) return arrival.strftime('%I:%M %p').lstrip('0') @@ -40,11 +52,17 @@ class NavigationCard: def bounds(self, state): if (state.navigation is None or state.alert.size != 'none' or state.reverse_driver_camera or - state.appearance.camera_view in (CameraViewChoice.DRIVER, CameraViewChoice.NONE) or - (state.speed_limit.action_enabled and state.longitudinal_active)): + state.appearance.camera_view in (CameraViewChoice.DRIVER, CameraViewChoice.NONE)): return None if state.navigation.key != self._key: self._key, self.collapsed = state.navigation.key, False + rect = self._placed(state) + # A live SLC decision keeps its touch priority only where its buttons actually sit on the card. + if rect is not None and state.longitudinal_active and _overlaps_slc_actions(rect, self.fonts.profile, state): + return None + return rect + + def _placed(self, state): if self.fonts.profile == Profile.COMPACT: return rl.Rectangle(244, 18, 72, 72) if self.collapsed else rl.Rectangle(96, 16, 368, 142) # Android Auto layouts place the card; the comma keeps its fixed spot. diff --git a/openpilot/starpilot/ui/onroad_state.py b/openpilot/starpilot/ui/onroad_state.py index e75a813610..c9757fd3d9 100644 --- a/openpilot/starpilot/ui/onroad_state.py +++ b/openpilot/starpilot/ui/onroad_state.py @@ -269,6 +269,14 @@ def _default_slc_controls(profile: Profile, state: OnroadState) -> tuple[SlcCont observation.presentation_id is not None and observation.presentation_id > 0): if profile == Profile.COMPACT: return () + # Once this limit is the one in use, adopting it again only helps to clear an override + # or to raise a set speed that sits below it. + accepted = observation.accepted_speed_limit_mps + adjusted = observation.presentation_adjusted_limit_mps or observation.speed_limit_mps + raises_set = state.cruise_active and state.cruise_kph is not None and state.cruise_kph / 3.6 < adjusted - 0.1 + if (accepted is not None and abs(accepted - observation.speed_limit_mps) <= 0.02 and + not observation.driver_override_active and not raises_set): + return () request = SlcUiRequest(SlcActionKind.ADOPT, observation.session_id, observation.decision_id or 0, observation.presentation_id, observation.speed_limit_mps) from openpilot.starpilot.ui.large_speed_geometry import WIDTH, ACTION_HEIGHT, ACTION_X, ACTION_Y, ACTION_HEADER_HEIGHT @@ -423,7 +431,8 @@ def speed_limit_from_message(message: SlcStateMessage | None) -> SpeedLimitObser except (ValueError, AttributeError): return SpeedLimitObservation(kind=ObservationKind.UNKNOWN, status="Invalid SLC observation kind") if kind != ObservationKind.VALID: - return SpeedLimitObservation(kind=kind, source=message.source, status=message.status) + return SpeedLimitObservation(kind=kind, source=message.source, status=message.status, + source_readings=source_readings_from_message(message)) try: return SpeedLimitObservation(kind=kind, source=message.source, speed_limit_mps=message.speedLimit, source_readings=source_readings_from_message(message), diff --git a/openpilot/starpilot/ui/tests/test_onroad_navigation.py b/openpilot/starpilot/ui/tests/test_onroad_navigation.py index 3374e3bd84..4eaf144b3a 100644 --- a/openpilot/starpilot/ui/tests/test_onroad_navigation.py +++ b/openpilot/starpilot/ui/tests/test_onroad_navigation.py @@ -99,9 +99,17 @@ def test_native_alert_owns_space(size): assert not card.press(120, 40, replace(state(), alert=OnroadAlert(size, 'warning'))) -def test_slc_decision_keeps_its_touch_priority(): +def test_slc_decision_keeps_its_touch_priority_only_where_it_overlaps(): + from openpilot.starpilot.ui.onroad_state import ObservationKind card = NavigationCard(Mock(profile=Profile.COMPACT)) - assert card.bounds(replace(state(), longitudinal_active=True, speed_limit=SpeedLimitObservation(action_enabled=True))) is None + pending = SpeedLimitObservation(kind=ObservationKind.VALID, source='map', speed_limit_mps=22., pending_speed_limit_mps=22., + session_id='s', decision_id=1, presentation_id=1, action_enabled=True) + # Compact buttons (y 180-234, header from 136) cover the card's lower edge. + assert card.bounds(replace(state(), longitudinal_active=True, speed_limit=pending)) is None + # SLC being on, with no decision on screen, leaves the directions up. + assert card.bounds(replace(state(), longitudinal_active=True, speed_limit=SpeedLimitObservation(action_enabled=True))) is not None + large = NavigationCard(Mock(profile=Profile.LARGE)) + assert large.bounds(replace(state(), longitudinal_active=True, speed_limit=pending)) is not None @pytest.mark.parametrize('profile', [Profile.COMPACT, Profile.LARGE]) diff --git a/openpilot/starpilot/ui/tests/test_slc_ui_actions.py b/openpilot/starpilot/ui/tests/test_slc_ui_actions.py index b3d11dbecb..046a4b51d3 100644 --- a/openpilot/starpilot/ui/tests/test_slc_ui_actions.py +++ b/openpilot/starpilot/ui/tests/test_slc_ui_actions.py @@ -68,7 +68,17 @@ class TestSlcTouch(unittest.TestCase): touch.release(400, 630, state(presentation=10)) self.assertEqual(emitted, []) - def test_adopt_same_accepted_limit_and_rejected_limit_remain_visible(self): + def test_adopt_hidden_once_limit_is_in_use(self): + adoptable = state(pending=False) + in_use = replace(adoptable, speed_limit=replace(adoptable.speed_limit, speed_limit_mps=18.0)) + self.assertEqual(slc_controls(Profile.LARGE, in_use), ()) + # Still offered when it would clear an override or raise a lower set speed. + overridden = replace(in_use, speed_limit=replace(in_use.speed_limit, driver_override_active=True)) + self.assertEqual(slc_controls(Profile.LARGE, overridden)[0].label, "USE LIMIT") + self.assertEqual(slc_controls(Profile.LARGE, replace(in_use, cruise_kph=50.0))[0].label, "USE LIMIT") + self.assertEqual(slc_controls(Profile.LARGE, replace(in_use, cruise_kph=50.0, longitudinal_active=False)), ()) + + def test_adopt_new_limit_and_rejected_limit_remain_visible(self): emitted: list[OnroadRequest | SlcUiRequest] = [] adoptable = state(pending=False) for profile, xy in ((Profile.LARGE, (150, 590)),): @@ -77,9 +87,7 @@ class TestSlcTouch(unittest.TestCase): touch.release(*xy, adoptable) self.assertEqual(emitted[-1].kind, SlcActionKind.ADOPT) self.assertEqual(slc_controls(Profile.COMPACT, adoptable), ()) - same_accepted = replace(adoptable, speed_limit=replace(adoptable.speed_limit, speed_limit_mps=18.0)) previously_rejected = replace(adoptable, speed_limit=replace(adoptable.speed_limit, accepted_speed_limit_mps=None)) - self.assertEqual(slc_controls(Profile.LARGE, same_accepted)[0].label, "USE LIMIT") self.assertEqual(slc_controls(Profile.LARGE, previously_rejected)[0].label, "USE LIMIT") # Use-limit is offered without longitudinal control; it is recorded without commanding a speed. self.assertEqual(slc_controls(Profile.LARGE, replace(adoptable, longitudinal_active=False))[0].label, "USE LIMIT")