Files
StarPilot/openpilot/starpilot/ui/tests/test_onroad_axes.py
T
2026-10-26 21:55:36 -04:00

399 lines
23 KiB
Python

"""Fresh control axes drive the existing large and compact onroad indicators."""
from dataclasses import replace
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace as NS
import unittest
from unittest.mock import Mock, patch
import pyray as rl
from openpilot.starpilot.ui import onroad
from openpilot.starpilot.ui.onroad import axis_status_color, clock_text, driving_mode_description
from openpilot.starpilot.ui.onroad_customization import MODE_WIDGET
from openpilot.starpilot.ui.onroad_compact_widgets import CompactHudRenderer
from openpilot.starpilot.ui.onroad_large_widgets import UnifiedSpeedWidget
from openpilot.starpilot.ui.onroad_state import AlertSize, OnroadAlert, OnroadInput
from openpilot.starpilot.ui.onroad_torque import TorqueBarWidget
from openpilot.starpilot.ui.presentation import Profile
from openpilot.starpilot.ui.preview_shell import reference_onroad
from openpilot.starpilot.ui.runtime_snapshot import RuntimeSnapshotAdapter
from openpilot.starpilot.ui.shell import ShellMode
from openpilot.starpilot.ui.tests.test_runtime_snapshot import NOW, ui_fake
from openpilot.starpilot.ui.tests.test_onroad_torque_draw import record_gpu
def rgba(color: rl.Color) -> tuple[int, int, int, int]:
return color.r, color.g, color.b, color.a
class TestOnroadAxes(unittest.TestCase):
def setUp(self):
for name in ('draw_rectangle_rounded', 'draw_rectangle_rounded_lines_ex', 'draw_circle', 'draw_ring'):
self.enterContext(patch('openpilot.starpilot.ui.speed_limit_widget.rl.' + name, new_callable=Mock))
for name in ('circle', 'arrow', 'outline', 'confirmation'):
self.enterContext(patch('openpilot.starpilot.ui.speed_limit_shapes.SpeedLimitShapes.' + name, new_callable=Mock))
self.ui = ui_fake()
self.ui.CP = NS(carFingerprint='', openpilotLongitudinalControl=True, pcmCruise=False)
car = self.ui.sm["carState"]
car.canValid = True
car.canTimeout = False
car.cruiseState = NS(enabled=False)
self.ui.sm["selfdriveState"].enabled = True
self.ui.sm.put("controlsState", NS(longControlState="pid"))
self.adapter = RuntimeSnapshotAdapter(self.ui)
def state(self, lateral: bool, longitudinal: bool):
control = self.ui.sm["carControl"]
control.latActive = lateral
control.longActive = longitudinal
return self.adapter.build(ShellMode.ONROAD, now_ns=NOW).onroad
def test_four_axis_modes_drive_border_max_and_compact_wheel(self):
colors = {(False, False): (18, 40, 57, 255), (True, False): (10, 186, 181, 255),
(False, True): (255, 105, 180, 255), (True, True): (22, 127, 64, 255)}
for (lateral, longitudinal), expected in colors.items():
with self.subTest(lateral=lateral, longitudinal=longitudinal):
state = self.state(lateral, longitudinal)
self.assertEqual((state.lateral_active, state.longitudinal_active), (lateral, longitudinal))
self.assertEqual(rgba(axis_status_color(state)), expected)
fonts = Mock()
fonts.measure.return_value = NS(width=20, height=10)
fonts.vertical_ink.return_value = (0, 10)
fonts.horizontal_ink.return_value = (0, 20)
with patch("openpilot.starpilot.ui.onroad_large_widgets.draw_control_card"), patch.object(rl, "draw_line_ex"):
UnifiedSpeedWidget(fonts).render(rl.Rectangle(30, 30, 1800, 1020), state)
from openpilot.starpilot.ui.tests.test_unified_speed_native import foreground_calls
label_color = foreground_calls(fonts.draw)[0].args[-1]
self.assertEqual(rgba(label_color), (190, 208, 202, 255))
compact = CompactHudRenderer(fonts, Path("/unused"))
with patch.object(compact, "prepare"), patch("openpilot.starpilot.ui.onroad_compact_widgets.rl.draw_texture_pro"), \
patch("openpilot.starpilot.ui.onroad_compact_widgets.rl.draw_circle_gradient"):
compact.render(state)
compact.render(replace(state, observed_ns=state.observed_ns + 20_000_000))
self.assertEqual(compact._wheel_alpha.x > 0, lateral)
self.assertEqual(compact._set_speed_alpha.x > 0, longitudinal)
def test_pedal_override_keeps_cruise_display_and_uses_gray_border(self):
self.ui.sm['carState'].gasPressed = True
self.ui.sm['selfdriveState'].state = 'overriding'
state = self.state(True, False)
self.assertTrue(state.longitudinal_overridden)
self.assertTrue(state.driver_override_active)
self.assertTrue(self.state(False, False).driver_override_active)
self.assertTrue(state.cruise_active)
self.assertTrue(state.lateral_active)
self.assertEqual(rgba(axis_status_color(state)), (145, 155, 149, 255))
self.ui.sm['carState'].gasPressed = False
self.assertFalse(self.state(True, False).longitudinal_overridden)
self.ui.sm['carState'].gasPressed = True
self.ui.sm['selfdriveState'].enabled = False
self.assertFalse(self.state(True, False).longitudinal_overridden)
self.ui.sm['selfdriveState'].enabled = True
self.ui.sm.logMonoTime['selfdriveState'] = NOW - 300_000_000
self.assertFalse(self.state(True, False).longitudinal_overridden)
def test_driver_inputs_only_gray_their_active_axis_and_clear_on_release(self):
car = self.ui.sm['carState']
gray, blue, cyan, pink = (145, 155, 149, 255), (18, 40, 57, 255), (10, 186, 181, 255), (255, 105, 180, 255)
for lateral, longitudinal, colors in ((False, False, (blue, blue, blue)), (True, False, (cyan, gray, gray)),
(False, True, (gray, pink, gray)), (True, True, (gray, gray, gray))):
self.ui.sm['selfdriveState'].enabled = longitudinal
for (gas, steering), color in zip(((True, False), (False, True), (True, True)), colors, strict=True):
with self.subTest(lateral=lateral, longitudinal=longitudinal, gas=gas, steering=steering):
car.gasPressed, car.steeringPressed = gas, steering
state = self.state(lateral, longitudinal)
self.assertEqual(state.driver_override_active, color == gray)
self.assertEqual((state.lateral_active, state.longitudinal_active), (lateral, longitudinal))
self.assertFalse(state.longitudinal_overridden)
self.assertEqual(state.cruise_active, longitudinal)
self.assertEqual(rgba(axis_status_color(state)), color)
car.gasPressed = car.steeringPressed = False
released = self.state(lateral, longitudinal)
self.assertFalse(released.driver_override_active)
self.assertNotEqual(rgba(axis_status_color(released)), (145, 155, 149, 255))
def test_driver_override_requires_valid_current_sources(self):
car = self.ui.sm['carState']
self.ui.sm['selfdriveState'].enabled = False
for field in ('gasPressed', 'steeringPressed'):
with self.subTest(field=field):
longitudinal = field == 'gasPressed'
setattr(car, field, True)
self.assertTrue(self.state(True, longitudinal).driver_override_active)
for service in ('carState', 'carControl'):
self.ui.sm.logMonoTime[service] = NOW - 300_000_000
self.assertFalse(self.state(True, longitudinal).driver_override_active)
self.ui.sm.logMonoTime[service] = NOW
for valid, timeout in ((False, False), (True, True)):
car.canValid, car.canTimeout = valid, timeout
self.assertFalse(self.state(True, longitudinal).driver_override_active)
car.canValid, car.canTimeout = True, False
setattr(car, field, False)
def test_independent_long_request_distinguishes_pause_from_pedal_override(self):
self.ui.aol_runtime = True
self.ui.sm['selfdriveState'].state = 'overriding'
axis = NS(qualified=True, sessionId='ui-axis', observedMonoTime=NOW, validUntilMonoTime=NOW + 30_000_000,
nativeAcknowledged=True, desiredLongitudinal=False)
self.ui.sm.put('aolAxisState', axis)
car = self.ui.sm['carState']
for requested, gas, steering, gray, pedal in (
(False, False, False, False, False), (False, True, False, False, False),
(False, False, True, True, False), (False, True, True, True, False),
(True, False, False, False, False), (True, True, False, True, True),
(True, False, True, True, False), (True, True, True, True, True)):
with self.subTest(requested=requested, gas=gas, steering=steering):
axis.desiredLongitudinal = requested
car.gasPressed, car.steeringPressed = gas, steering
state = self.state(True, False)
self.assertEqual(state.longitudinal_overridden, pedal)
self.assertEqual(state.driver_override_active, gray)
self.assertEqual(state.cruise_active, pedal)
self.assertEqual(driving_mode_description(state), 'ACC - Override' if pedal else 'Always On Lateral')
self.assertEqual(rgba(axis_status_color(state)), (145, 155, 149, 255) if gray else (10, 186, 181, 255))
def test_independent_pedal_override_requires_current_axis_request(self):
self.ui.aol_runtime = True
self.ui.sm['selfdriveState'].state = 'overriding'
self.ui.sm['carState'].gasPressed = True
self.assertFalse(self.state(True, False).longitudinal_overridden)
axis = NS(qualified=True, sessionId='ui-axis', observedMonoTime=NOW, validUntilMonoTime=NOW + 30_000_000,
nativeAcknowledged=True, desiredLongitudinal=True)
self.ui.sm.put('aolAxisState', axis)
self.assertTrue(self.state(True, False).longitudinal_overridden)
for field, value in (('qualified', False), ('sessionId', ''), ('observedMonoTime', NOW + 1),
('validUntilMonoTime', NOW - 1), ('nativeAcknowledged', False)):
with self.subTest(field=field):
original = getattr(axis, field)
setattr(axis, field, value)
state = self.state(True, False)
self.assertFalse(state.longitudinal_overridden or state.driver_override_active or state.cruise_active)
setattr(axis, field, original)
for stamp in (NOW + 1, NOW - 31_000_000):
self.ui.sm.logMonoTime['aolAxisState'] = stamp
self.assertFalse(self.state(True, False).longitudinal_overridden)
self.ui.sm.logMonoTime['aolAxisState'] = NOW
self.ui.sm.recv_frame['aolAxisState'] = self.ui.started_frame
self.assertFalse(self.state(True, False).longitudinal_overridden)
self.ui.sm.recv_frame['aolAxisState'] = self.ui.started_frame + 1
for flags in (self.ui.sm.valid, self.ui.sm.alive, self.ui.sm.seen):
flags['aolAxisState'] = False
self.assertFalse(self.state(True, False).longitudinal_overridden)
flags['aolAxisState'] = True
self.assertTrue(self.state(True, False).longitudinal_overridden)
def test_traffic_returns_after_conditional_stop_without_changing_intent(self):
from openpilot.starpilot.conditional_mode.policy import ModeChoice
from openpilot.starpilot.ui.conditional_status import ConditionalDisplay
traffic = replace(self.state(True, True), traffic_mode=True)
stopping = replace(traffic, conditional_effective=ConditionalDisplay(ModeChoice.CEM, True, 'cem_stop', 8, 'test', 1))
self.assertEqual(rgba(axis_status_color(traffic)), (201, 34, 49, 255))
for _ in range(3):
self.assertEqual(rgba(axis_status_color(stopping)), (218, 111, 37, 255))
self.assertTrue(stopping.traffic_mode)
self.assertEqual(rgba(axis_status_color(traffic)), (201, 34, 49, 255))
self.assertTrue(traffic.traffic_mode)
self.assertEqual(rgba(axis_status_color(replace(stopping, longitudinal_overridden=True))), (145, 155, 149, 255))
def test_experimental_border_requires_active_combined_axes(self):
combined = replace(self.state(True, True), experimental_enabled=True)
self.assertEqual(rgba(axis_status_color(combined)), (218, 111, 37, 255))
self.assertEqual(rgba(axis_status_color(replace(combined, longitudinal_active=False))), (10, 186, 181, 255))
self.assertEqual(rgba(axis_status_color(replace(combined, lateral_active=False))), (255, 105, 180, 255))
self.assertEqual(rgba(axis_status_color(replace(combined, lateral_active=False, longitudinal_active=False))), (18, 40, 57, 255))
def test_both_profile_compositions_use_axis_border_and_fixtures_remain_combined(self):
engaged_fixture = reference_onroad("onroad_engaged_no_camera")
self.assertTrue(engaged_fixture.lateral_active and engaged_fixture.longitudinal_active)
disengaged_fixture = reference_onroad("onroad_disengaged_no_camera")
self.assertFalse(disengaged_fixture.lateral_active or disengaged_fixture.longitudinal_active)
gray, blue = (145, 155, 149, 255), (18, 40, 57, 255)
cases = [(False, False, False, OnroadAlert(), blue),
(True, False, False, OnroadAlert(), (10, 186, 181, 255)),
(False, True, False, OnroadAlert(), (255, 105, 180, 255)),
(True, True, False, OnroadAlert(), (22, 127, 64, 255)),
(True, False, False, OnroadAlert(size=AlertSize.SMALL), blue)]
cases += [(True, longitudinal, True, alert, color) for longitudinal in (False, True) for alert, color in (
(OnroadAlert(), gray), (OnroadAlert(size=AlertSize.SMALL), gray), (OnroadAlert(size=AlertSize.MID), gray),
(OnroadAlert(size=AlertSize.FULL), blue), (OnroadAlert(size=AlertSize.SMALL, critical=True), blue))]
for lateral, longitudinal, override, alert, compact_color in cases:
state = replace(self.state(lateral, longitudinal), driver_override_active=override, alert=alert)
for layout in state.customization['layouts'].values():
layout[MODE_WIDGET]['enabled'] = True
for profile in (Profile.LARGE, Profile.COMPACT):
with self.subTest(profile=profile, lateral=lateral, longitudinal=longitudinal, override=override, alert=alert):
view = onroad.OnroadView.__new__(onroad.OnroadView)
object.__setattr__(view, "fonts", NS(profile=profile, draw=Mock(), measure=Mock(return_value=NS(width=100, height=30)),
vertical_ink=Mock(return_value=(0, 20)), horizontal_ink=Mock(return_value=(0, 100))))
view.camera_layer = Mock()
view.extra_overlays = None
view.alert = Mock()
view.torque_bar = Mock()
view.unified_speed = Mock()
view.navigation = Mock()
view.navigation_favorites = Mock()
view.background_layer = None
view.projection_viewport = None
view._corner_cache = Mock()
view.current_speed = Mock()
view.steering_wheel = Mock()
view.compact_hud = Mock()
view.compact_sidebar = Mock()
view._fade = None
with patch.object(onroad.clip, "begin_scissor_mode"), patch.object(onroad.clip, "end_scissor_mode"), \
patch.object(onroad.rl, "draw_rectangle_rec"), patch.object(onroad.rl, "draw_rectangle_gradient_v"), \
patch.object(onroad.rl, "draw_rectangle_lines_ex"), patch.object(onroad.rl, "draw_rectangle"), \
patch.object(onroad.rl, "draw_texture_ex"), patch.object(onroad.rl, "draw_rectangle_rounded_lines_ex") as border, \
patch.object(onroad, "render_corner_hint"), patch.object(view, "_prepare_compact_fade"), \
patch.object(view, "_slc_actions"):
view.render(state)
expected = compact_color if profile == Profile.COMPACT else rgba(axis_status_color(state))
self.assertEqual(rgba(border.call_args.args[-1]), expected)
if alert.size == AlertSize.NONE:
self.assertIn(driving_mode_description(state), [call.args[0] for call in view.fonts.draw.call_args_list])
def test_driving_mode_text_uses_active_axes_and_effective_mode(self):
from openpilot.starpilot.conditional_mode.policy import ModeChoice
from openpilot.starpilot.ui.conditional_status import ConditionalDisplay
base = self.state(True, True)
for changes, expected in [
({}, 'ACC - Chill'),
({'experimental_enabled': True}, 'ACC - Experimental'),
({'longitudinal_active': False}, 'Always On Lateral'),
({'lateral_active': False}, 'ACC - Chill'),
({'switchback_mode': True, 'traffic_mode': True}, 'Switchback Mode'),
({'traffic_mode': True, 'experimental_enabled': True}, 'Traffic Mode'),
({'longitudinal_overridden': True}, 'ACC - Override'),
({'lateral_active': False, 'longitudinal_active': False, 'traffic_mode': True,
'switchback_mode': True, 'experimental_enabled': True}, 'Disengaged'),
({'lateral_active': False, 'longitudinal_active': False, 'stock_cruise_active': True}, 'Stock ACC'),
({'longitudinal_active': False, 'stock_cruise_active': True}, 'Always On Lateral'),
]:
with self.subTest(changes=changes):
self.assertEqual(driving_mode_description(replace(base, **changes)), expected)
stop = ConditionalDisplay(ModeChoice.CEM, True, 'cem_stop', 8, 'test', 1)
chill = replace(stop, effective_experimental=False, reason='manual_chill')
self.assertEqual(driving_mode_description(replace(base, conditional_effective=stop)), 'ACC - Experimental')
self.assertEqual(driving_mode_description(replace(base, experimental_enabled=True, conditional_effective=chill)), 'ACC - Chill')
self.assertEqual(driving_mode_description(replace(base, traffic_mode=True, conditional_effective=stop)), 'Traffic Mode')
def test_mode_widget_is_optional_and_yields_to_alerts(self):
from openpilot.starpilot.ui.onroad_state import AlertSize, OnroadAlert
state = self.state(True, True)
for profile in (Profile.LARGE, Profile.COMPACT):
view = onroad.OnroadView.__new__(onroad.OnroadView)
self.enterContext(patch.object(view, 'fonts', NS(profile=profile, draw=Mock()), create=True))
view._driving_mode(state)
view.fonts.draw.assert_not_called()
state.customization['layouts'][profile][MODE_WIDGET]['enabled'] = True
for size in (AlertSize.SMALL, AlertSize.MID, AlertSize.FULL):
view._driving_mode(replace(state, alert=OnroadAlert(size=size)))
view.fonts.draw.assert_not_called()
def test_stock_cruise_is_distinct_from_system_long_and_lateral(self):
self.ui.CP = NS(carFingerprint='', openpilotLongitudinalControl=False, pcmCruise=True)
self.ui.sm["carState"].cruiseState.enabled = True
state = self.state(False, False)
self.assertTrue(state.stock_cruise_active)
self.assertTrue(state.cruise_active)
self.assertFalse(state.longitudinal_active)
self.assertEqual(rgba(axis_status_color(state)), (18, 40, 57, 255))
fonts = Mock()
fonts.measure.return_value = NS(width=20, height=10)
fonts.vertical_ink.return_value = (0, 10)
fonts.horizontal_ink.return_value = (0, 20)
with patch("openpilot.starpilot.ui.onroad_large_widgets.draw_control_card"), patch.object(rl, "draw_line_ex"):
UnifiedSpeedWidget(fonts).render(rl.Rectangle(30, 30, 1800, 1020), state)
from openpilot.starpilot.ui.tests.test_unified_speed_native import foreground_calls
self.assertEqual(rgba(foreground_calls(fonts.draw)[0].args[-1]), (190, 208, 202, 255))
compact = CompactHudRenderer(fonts, Path("/unused"))
with patch("openpilot.starpilot.ui.onroad_compact_widgets.rl.draw_circle_gradient"):
compact.render(state)
compact.render(replace(state, observed_ns=state.observed_ns + 20_000_000))
self.assertGreater(compact._set_speed_alpha.x, 0)
self.assertEqual(compact._wheel_alpha.x, 0)
self.ui.sm["carState"].canValid = False
invalid = self.state(False, False)
self.assertFalse(invalid.cruise_active)
self.assertIsNone(invalid.cruise_kph)
def test_lost_control_clears_steering_and_cruise_immediately_and_cancels_touch(self):
self.ui.sm.put("controlsState", NS(longControlState="pid", lateralControlState=NS(which=lambda: "torqueState")))
self.ui.sm.put("carOutput", NS(actuatorsOutput=NS(torque=0.4)))
active = self.state(True, True)
self.assertTrue(active.torque_source_available)
self.assertAlmostEqual(active.torque_utilization, -0.4)
requests = []
touch = OnroadInput(requests.append, Profile.LARGE)
touch.press(120, 530, active)
self.ui.sm.logMonoTime["carControl"] = NOW - 300_000_000
stale = self.adapter.build(ShellMode.ONROAD, now_ns=NOW).onroad
self.assertFalse(stale.lateral_active or stale.longitudinal_active or stale.cruise_active)
self.assertEqual(rgba(axis_status_color(stale)), (18, 40, 57, 255))
touch.release(120, 530, stale)
self.assertEqual(requests, [])
fonts = Mock()
fonts.measure.return_value = NS(width=20, height=10)
fonts.vertical_ink.return_value = (0, 10)
compact = CompactHudRenderer(fonts, Path("/unused"))
fonts.horizontal_ink.return_value = (0, 20)
with patch.object(compact, "prepare"), patch("openpilot.starpilot.ui.onroad_compact_widgets.rl.draw_texture_pro"), \
patch("openpilot.starpilot.ui.onroad_compact_widgets.rl.draw_circle_gradient"):
compact.render(active)
compact.render(replace(active, observed_ns=active.observed_ns + 20_000_000))
self.assertGreater(compact._wheel_alpha.x, 0)
self.assertGreater(compact._set_speed_alpha.x, 0)
compact.render(replace(stale, observed_ns=active.observed_ns + 40_000_000))
self.assertEqual(compact._wheel_alpha.x, 0)
assert compact._last_cruise_ns is not None
self.assertEqual(compact._set_speed_opacity(stale, compact._last_cruise_ns + 1), 0)
self.assertEqual(compact._set_speed_opacity(stale, compact._last_cruise_ns + 150_000_001), 0)
self.assertEqual(compact._set_speed_alpha.x, 0)
torque = TorqueBarWidget()
with record_gpu() as gpu:
torque.render(rl.Rectangle(0, 0, 476, 240), active, 536)
self.assertGreater(torque._alpha_filter.x, 0)
gpu.frames.clear()
torque.render(rl.Rectangle(0, 0, 476, 240), stale, 536)
self.assertFalse(gpu.frames)
self.assertEqual(torque._alpha_filter.x, 0)
def test_missing_actual_torque_output_stays_hidden_with_active_axes(self):
self.ui.sm.put("controlsState", NS(longControlState="pid", lateralControlState=NS(which=lambda: "torqueState")))
state = self.state(True, True)
self.assertTrue(state.lateral_active)
self.assertFalse(state.torque_source_available)
torque = TorqueBarWidget()
with record_gpu() as gpu:
torque.render(rl.Rectangle(0, 0, 476, 240), state, 536)
self.assertFalse(gpu.frames or gpu.loads)
self.assertEqual(torque._alpha_filter.x, 0)
def test_pressed_control_cancels_on_axis_transition_even_with_fresh_transport(self):
active = self.state(True, True)
requests = []
touch = OnroadInput(requests.append, Profile.LARGE)
touch.press(120, 530, active)
touch.release(120, 530, self.state(True, False))
self.assertEqual(requests, [])
self.assertFalse(replace(active, lateral_active=False, longitudinal_active=False).cruise_active)
def test_clock_text_supports_12_and_24_hour_formats(self):
evening = datetime(2026, 10, 7, 18, 0)
self.assertEqual(clock_text(evening), "6:00 PM")
self.assertEqual(clock_text(evening, use_24_hour=True), "18:00")
midnight = datetime(2026, 10, 8, 0, 0)
self.assertEqual(clock_text(midnight), "12:00 AM")
self.assertEqual(clock_text(midnight, use_24_hour=True), "00:00")
if __name__ == "__main__":
unittest.main()