SLC: Keep nav directions and sources up, hide redundant Use limit

This commit is contained in:
dirwin31
2026-10-15 09:21:25 -07:00
committed by Prabhaav Pillai
parent f413e0f089
commit d1beeffbc2
5 changed files with 56 additions and 12 deletions
@@ -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
+20 -2
View File
@@ -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.
+10 -1
View File
@@ -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),
@@ -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])
@@ -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")