Drag widgets in Arrange widgets to place them. Readings are samples.
+Add Stopped timer to preview its minutes and seconds. Device preview renders the same widget as the driving screen; choose Stopped timer for a sample at rest.
The steering wheel can be resized in each layout{{ projection ? ', and the map overlay takes any size and opacity' : '' }}. Existing display preferences and driving state still control when widgets appear.
Small speed-limit signs can overlap confirmation actions. Live confirmations hide an overlapping sign until the decision ends. Select either widget in the list to edit it.
Favorite tap areas apply to enabled slots. Speed-limit controls and the steering wheel take priority when the drawer is closed.
diff --git a/openpilot/starpilot/system/android_auto/projection_layout.py b/openpilot/starpilot/system/android_auto/projection_layout.py index fcbcf69cfe..46c598508b 100644 --- a/openpilot/starpilot/system/android_auto/projection_layout.py +++ b/openpilot/starpilot/system/android_auto/projection_layout.py @@ -6,8 +6,9 @@ from pathlib import Path from openpilot.starpilot.system.android_auto.identity import DATA_DIR from openpilot.starpilot.system.android_auto.display_profile import screen_geometry -from openpilot.starpilot.ui.onroad_customization import (CLOCK_WIDGET, MODE_WIDGET, WHEEL_SIZES, +from openpilot.starpilot.ui.onroad_customization import (CLOCK_WIDGET, MODE_WIDGET, STOPPED_TIMER_WIDGET, WHEEL_SIZES, customization_metadata, validate_widget_order) +from openpilot.starpilot.ui.onroad_stopped_timer import timer_size from openpilot.starpilot.ui.onroad_torque_geometry import maximum_footprint MAX_BYTES = 16384 @@ -26,7 +27,7 @@ ACTION_WIDGETS = (*DISPLAY_WIDGETS, CAR_EXIT) FAVORITE_SIZE = (320, 110) FAVORITE_ICON_SIZE = 110 CAR_EXIT_SIZE = (96, 96) -PROJECTION_WIDGETS = (NAV_CARD, NAV_MAP, *DISPLAY_WIDGETS, CAR_EXIT) +PROJECTION_WIDGETS = (NAV_CARD, NAV_MAP, *DISPLAY_WIDGETS, STOPPED_TIMER_WIDGET, CAR_EXIT) NAV_CARD_SIZE = (560, 195) MAP_MIN_SIZE = (280, 200) MAP_OPACITY = (15, 100, 50) # percent: min, max, default @@ -69,6 +70,11 @@ def layout_metadata_for_viewport(viewport): x, y, w, h = maximum_footprint(30, 30, width - 60, height - 60, width) widgets['torque_bar'].update(width=w, height=h) widgets['torque_bar']['default'].update(x=x, y=y) + timer_w, timer_h = timer_size('large') + widgets[STOPPED_TIMER_WIDGET] = {'label': 'Stopped timer', 'kind': STOPPED_TIMER_WIDGET, + 'width': timer_w, 'height': timer_h, 'colors': {}, + 'note': 'Counts how long you have been stopped, after the first minute of the drive', + 'default': {'x': (width - timer_w) / 2, 'y': 520, 'enabled': False}} # The turn card keeps the comma's large-UI spot, following the right edge. card_w, card_h = NAV_CARD_SIZE widgets[NAV_CARD] = {'label': 'Turn-by-turn', 'kind': NAV_CARD, 'width': card_w, 'height': card_h, 'colors': {}, @@ -106,7 +112,7 @@ def layout_metadata_for_viewport(viewport): for key in ACTION_WIDGETS: widgets[key].update(opacity={'min': 0, 'max': 100, 'default': 100}, backgroundToggle=True) widgets[key]['default'].update(opacity=100, background=True) - profile["widgetOrder"] = [NAV_MAP, *profile["widgetOrder"], NAV_CARD, *DISPLAY_WIDGETS, CAR_EXIT] + profile["widgetOrder"] = [NAV_MAP, *profile["widgetOrder"], STOPPED_TIMER_WIDGET, NAV_CARD, *DISPLAY_WIDGETS, CAR_EXIT] return profile diff --git a/openpilot/starpilot/system/android_auto/projection_onroad.py b/openpilot/starpilot/system/android_auto/projection_onroad.py index 192067e5a1..da9c5922a6 100644 --- a/openpilot/starpilot/system/android_auto/projection_onroad.py +++ b/openpilot/starpilot/system/android_auto/projection_onroad.py @@ -295,7 +295,8 @@ class ProjectionOnroad: self.camera_stream = None if ui.started: now_ns = time.monotonic_ns() - state = self.adapter.build(self.native.shell_mode.ONROAD, now_ns=now_ns).onroad + timer_enabled = bool((self.customization or {}).get('widgets', {}).get('stopped_timer', {}).get('enabled', False)) + state = self.adapter.build(self.native.shell_mode.ONROAD, now_ns=now_ns, stopped_timer_enabled=timer_enabled).onroad if self.customization is not None: from openpilot.starpilot.system.android_auto.projection_layout import projection_customization if state.customization is not self._base_customization: diff --git a/openpilot/starpilot/system/android_auto/tests/test_projection_layout.py b/openpilot/starpilot/system/android_auto/tests/test_projection_layout.py index 2a7341f650..2e26b20058 100644 --- a/openpilot/starpilot/system/android_auto/tests/test_projection_layout.py +++ b/openpilot/starpilot/system/android_auto/tests/test_projection_layout.py @@ -10,7 +10,7 @@ from openpilot.starpilot.system.android_auto.projection_layout import ( default_layout, validate_layout, decode_layout, layout_metadata, projection_customization, ProjectionLayoutSource, PROJECTION_WIDGETS, NAV_CARD, NAV_MAP, FAVORITE_WIDGETS, BOOKMARK_WIDGET, CAR_EXIT, ACTION_WIDGETS, placement_size, ) -from openpilot.starpilot.ui.onroad_customization import CLOCK_WIDGET, MODE_WIDGET, default_document, customization_metadata +from openpilot.starpilot.ui.onroad_customization import CLOCK_WIDGET, MODE_WIDGET, STOPPED_TIMER_WIDGET, default_document, customization_metadata from openpilot.starpilot.saved_document import commit_exact SCREEN = {'version': 1, 'width': 1280, 'height': 720, 'margin_width': 0, @@ -18,6 +18,29 @@ SCREEN = {'version': 1, 'width': 1280, 'height': 720, 'margin_width': 0, class TestProjectionLayout(unittest.TestCase): + def test_stopped_timer_migrates_and_keeps_independent_placement_and_order(self): + document = default_layout(SCREEN) + metadata = layout_metadata(SCREEN) + expected = document['widgets'].pop(STOPPED_TIMER_WIDGET) + self.assertFalse(expected['enabled']) + self.assertEqual((metadata['widgets'][STOPPED_TIMER_WIDGET]['width'], + metadata['widgets'][STOPPED_TIMER_WIDGET]['height']), (660, 168)) + document['widgetOrder'] = [key for key in metadata['widgetOrder'] if key != STOPPED_TIMER_WIDGET] + document['widgets']['current_speed']['x'] += 20 + before = copy.deepcopy(document) + migrated = validate_layout(document, SCREEN) + self.assertEqual(document, before) + self.assertEqual(migrated['widgets'][STOPPED_TIMER_WIDGET], expected) + self.assertEqual(migrated['widgets']['current_speed'], document['widgets']['current_speed']) + self.assertEqual(migrated['widgetOrder'], [*document['widgetOrder'][:-1], STOPPED_TIMER_WIDGET, CAR_EXIT]) + migrated['widgets'][STOPPED_TIMER_WIDGET].update(enabled=True, x=450, y=600) + decoded = decode_layout(json.dumps(migrated).encode(), SCREEN) + base = default_document() + converted = projection_customization(decoded, base) + self.assertEqual(converted['layouts']['large'][STOPPED_TIMER_WIDGET], {'enabled': True, 'x': 450, 'y': 600}) + self.assertEqual(converted['layouts']['compact'], base['layouts']['compact']) + self.assertFalse(base['layouts']['compact'][STOPPED_TIMER_WIDGET]['enabled']) + def test_bookmark_is_opt_in_and_upgrades_existing_layouts_without_changing_them(self): document = default_layout(SCREEN) metadata = layout_metadata(SCREEN) diff --git a/openpilot/starpilot/system/android_auto/tests/test_projection_layout_runtime.py b/openpilot/starpilot/system/android_auto/tests/test_projection_layout_runtime.py index 835eaf45c5..1b8f4b029f 100644 --- a/openpilot/starpilot/system/android_auto/tests/test_projection_layout_runtime.py +++ b/openpilot/starpilot/system/android_auto/tests/test_projection_layout_runtime.py @@ -21,6 +21,28 @@ SCREEN = {'version': 1, 'width': 1280, 'height': 720, 'margin_width': 0, class TestProjectionLayoutRuntime(unittest.TestCase): + def test_projection_passes_its_own_timer_visibility_to_runtime(self): + from unittest.mock import Mock + from openpilot.starpilot.ui.onroad_state import OnroadState, SpeedLimitObservation + for native_enabled, projection_enabled in ((False, True), (True, False)): + with self.subTest(native_enabled=native_enabled, projection_enabled=projection_enabled): + native, _ = boundary.TestProjectionOnroad().dependencies() + native.ui_state.started = True + base = default_document() + base['layouts']['compact']['stopped_timer']['enabled'] = native_enabled + state = OnroadState(False, False, None, None, SpeedLimitObservation(), customization=base) + build = Mock(return_value=SimpleNamespace(onroad=state)) + native.adapter = lambda *_, build=build: SimpleNamespace(build=build) + layout = default_layout_for_viewport((2880, 1080)) + layout['widgets']['stopped_timer']['enabled'] = projection_enabled + view = projection.ProjectionOnroad(dependencies=native, viewport=(2880, 1080), customization=layout) + try: + view.render() + self.assertEqual(build.call_args.kwargs['stopped_timer_enabled'], projection_enabled) + self.assertEqual(base['layouts']['compact']['stopped_timer']['enabled'], native_enabled) + finally: + view.close() + def test_saved_gamma_trial_reaches_only_projection_camera(self): for enabled in (False, True): with self.subTest(enabled=enabled): diff --git a/openpilot/starpilot/ui/layout_preview_renderer.py b/openpilot/starpilot/ui/layout_preview_renderer.py index 07cdc76f2f..a014e844cc 100644 --- a/openpilot/starpilot/ui/layout_preview_renderer.py +++ b/openpilot/starpilot/ui/layout_preview_renderer.py @@ -18,6 +18,7 @@ from openpilot.starpilot.ui.onroad_widget_style import draw_widget_frame from openpilot.starpilot.ui.onroad_state import AlertSize, ObservationKind, OnroadAlert, OnroadState, SourceReading, SpeedLimitObservation from openpilot.starpilot.ui.appearance_preferences import CameraViewChoice, OnroadAppearance from openpilot.starpilot.ui.onroad_dm import monitor_visible +from openpilot.starpilot.ui.onroad_stopped_timer import PREVIEW_DURATION_S from openpilot.starpilot.ui.layout_preview_sidebar import SIDEBAR_WIDTH, render_sidebar from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole, Profile, default_font_directory from openpilot.starpilot.ui.wheel_feedback import WheelFeedback @@ -62,11 +63,12 @@ def render_sample_road(rect: rl.Rectangle, state: OnroadState, profile: Profile) def sample_state(scene: str, document: dict) -> OnroadState: if scene not in SCENES: raise ValueError("Unknown layout preview scene") - lateral = scene in ("engaged", "aol", "experimental", "braking", "slc_pending") or scene in CEM_SCENE_REASONS - longitudinal = scene in ("engaged", "long_only", "experimental", "braking", "slc_pending") or scene in CEM_SCENE_REASONS + lateral = scene in ("engaged", "aol", "experimental", "braking", "stopped", "slc_pending") or scene in CEM_SCENE_REASONS + longitudinal = scene in ("engaged", "long_only", "experimental", "braking", "stopped", "slc_pending") or scene in CEM_SCENE_REASONS longitudinal = longitudinal or scene in ('slc_max', 'slc_equal', 'slc_pedal', 'slc_retained') state = OnroadState(engaged=lateral or longitudinal, camera_available=False, - speed_mps=0.0 if scene == "cem_lead" else 22.0, cruise_kph=88.0, + speed_mps=0.0 if scene in ("cem_lead", "stopped") else 22.0, cruise_kph=88.0, + stopped_duration_s=PREVIEW_DURATION_S, speed_limit=SpeedLimitObservation(kind=ObservationKind.VALID, source="dashboard", speed_limit_mps=20.0, accepted_speed_limit_mps=20.0, accepted_source='dashboard', accepted_adjusted_limit_mps=22.0, effective_cluster_target_mps=22.0, diff --git a/openpilot/starpilot/ui/layout_preview_transport.py b/openpilot/starpilot/ui/layout_preview_transport.py index f0fc6ee85a..af53fbe96c 100644 --- a/openpilot/starpilot/ui/layout_preview_transport.py +++ b/openpilot/starpilot/ui/layout_preview_transport.py @@ -23,7 +23,7 @@ RENDER_DEADLINE = 2.0 MIN_INTERVAL = 0.5 ERROR_LOG_INTERVAL = 10.0 PNG_SIGNATURE = b'\x89PNG\r\n\x1a\n' -SCENES = frozenset(('engaged', 'aol', 'long_only', 'experimental', 'braking', +SCENES = frozenset(('engaged', 'aol', 'long_only', 'experimental', 'braking', 'stopped', 'cem_stop_light', 'cem_lead', 'cem_curve', 'slc_pending', 'slc_max', 'slc_equal', 'slc_pedal', 'slc_retained', 'slc_disengaged', 'slc_unavailable', 'slc_display')) PROFILES = frozenset(('large', 'compact')) diff --git a/openpilot/starpilot/ui/onroad.py b/openpilot/starpilot/ui/onroad.py index 5af4bfc34c..fa330762da 100644 --- a/openpilot/starpilot/ui/onroad.py +++ b/openpilot/starpilot/ui/onroad.py @@ -30,7 +30,7 @@ from openpilot.starpilot.ui.onroad_corner import CornerHintCache, render_corner_ from openpilot.starpilot.ui.onroad_large_widgets import CurrentSpeedHud, UnifiedSpeedWidget, SteeringWheelWidget from openpilot.starpilot.ui.live_developer_sidebar import render_sidebar as render_live_sidebar from openpilot.starpilot.ui.onroad_state import AlertSize, OnroadState, slc_controls -from openpilot.starpilot.ui.onroad_stopped_timer import duration_color, duration_text +from openpilot.starpilot.ui.onroad_stopped_timer import duration_color, duration_text, timer_size from openpilot.starpilot.ui.onroad_torque import TorqueBarWidget from openpilot.starpilot.ui.onroad_navigation import NavigationCard from openpilot.starpilot.ui.onroad_navigation_favorites import NavigationFavorites @@ -303,6 +303,8 @@ class OnroadView: submit("steering_wheel", lambda: self.steering_wheel.render(content, state)) if placement(state.customization, profile, "torque_bar")["enabled"]: submit("torque_bar", lambda: self.torque_bar.render(content, state, width if self.projection_viewport else 2160)) + if state.stopped_duration_s is not None: + submit(STOPPED_TIMER_WIDGET, lambda: self._stopped_timer(state, state.stopped_duration_s)) else: if state.alert.size == AlertSize.NONE and not driver_camera: submit("speed_limit", lambda: self.compact_hud._speed_limit_sign(state)) @@ -369,6 +371,8 @@ class OnroadView: self.steering_wheel.render(content, state) if placement(state.customization, "large", "torque_bar")["enabled"]: self.torque_bar.render(content, state, width if self.projection_viewport else 2160) + if state.stopped_duration_s is not None: + self._stopped_timer(state, state.stopped_duration_s) if self.extra_overlays is not None: self.extra_overlays(content, state) if monitor_layer := getattr(self, "driver_monitor_layer", None): @@ -507,8 +511,12 @@ class OnroadView: render_compact_half_borders(state, rl.get_time()) def _stopped_timer(self, state: OnroadState, seconds: int) -> None: - position = placement(state.customization, "compact", STOPPED_TIMER_WIDGET) - width, _ = widget_size(state.customization, "compact", STOPPED_TIMER_WIDGET) + profile = str(self.fonts.profile) + position = state.customization["layouts"][profile].get(STOPPED_TIMER_WIDGET) + if position is None or not position["enabled"]: + return + width, height = timer_size(profile) + scale = height / 112 minutes, remainder = duration_text(seconds) color = rl.Color(*duration_color(seconds), 255) # Line centers sit 53 and 101 px into the widget box. @@ -516,12 +524,12 @@ class OnroadView: (minutes, FontRole.BOLD, 78, 28, 53, color), (remainder, FontRole.MEDIUM, 35, 16, 101, rl.Color(255, 255, 255, 242)), ): - size = initial_size - while size > minimum_size and self.fonts.measure(text, role, size).width > width: + size = round(initial_size * scale) + while size > round(minimum_size * scale) and self.fonts.measure(text, role, size).width > width: size -= 2 measured = self.fonts.measure(text, role, size) x = position["x"] + (width - measured.width) / 2 - y = position["y"] + center_y - measured.height / 2 + y = position["y"] + center_y * scale - measured.height / 2 self.fonts.draw(text, role, size, x + 2, y + 2, rl.Color(0, 0, 0, 170)) self.fonts.draw(text, role, size, x, y, tint) diff --git a/openpilot/starpilot/ui/onroad_stopped_timer.py b/openpilot/starpilot/ui/onroad_stopped_timer.py index aa1374b8ab..8266be5f3e 100644 --- a/openpilot/starpilot/ui/onroad_stopped_timer.py +++ b/openpilot/starpilot/ui/onroad_stopped_timer.py @@ -1,4 +1,4 @@ -"""Monotonic, drive-scoped C4 standstill duration for the saved Visuals choice.""" +"""Monotonic, drive-scoped standstill duration for native and Android Auto widgets.""" from dataclasses import dataclass @@ -8,6 +8,11 @@ SECOND_NS = 1_000_000_000 ENGAGED = (22, 127, 64) EXPERIMENTAL = (218, 111, 37) TRAFFIC = (201, 34, 49) +PREVIEW_DURATION_S = 134 + + +def timer_size(profile: str) -> tuple[int, int]: + return (660, 168) if profile == "large" else (440, 112) @dataclass diff --git a/openpilot/starpilot/ui/runtime_snapshot.py b/openpilot/starpilot/ui/runtime_snapshot.py index 0e115acdb5..6f80d0bd47 100644 --- a/openpilot/starpilot/ui/runtime_snapshot.py +++ b/openpilot/starpilot/ui/runtime_snapshot.py @@ -358,7 +358,7 @@ class RuntimeSnapshotAdapter: def build(self, mode: ShellMode, selected: Destination = Destination.STAR, *, compact_y: float = 0, compact_scroll_x: float = 0, sidebar_expanded: bool = True, now_ns: int | None = None, - menu_only: bool = False) -> ShellSnapshot: + menu_only: bool = False, stopped_timer_enabled: bool | None = None) -> ShellSnapshot: replay = getattr(self.ui_state, "replay_sample", None) if getattr(self.ui_state, "replay_clock", None) is not None: if replay is not None and replay.epoch != self._replay_epoch: @@ -459,7 +459,8 @@ class RuntimeSnapshotAdapter: now_ns=now_ns, drive_key=drive_key, car_fresh=timer_car_fresh, standstill=timer_standstill, reverse=timer_reverse, - enabled=placement(self._customization_value, "compact", STOPPED_TIMER_WIDGET)["enabled"], + enabled=(placement(self._customization_value, "compact", STOPPED_TIMER_WIDGET)["enabled"] + if stopped_timer_enabled is None else stopped_timer_enabled), ) border_signals = (BorderSignals(bool(getattr(display_car, 'leftBlinker', False)), bool(getattr(display_car, 'rightBlinker', False)), diff --git a/openpilot/starpilot/ui/tests/test_layout_preview_renderer.py b/openpilot/starpilot/ui/tests/test_layout_preview_renderer.py index 1fa5cd7dca..110051dfcc 100644 --- a/openpilot/starpilot/ui/tests/test_layout_preview_renderer.py +++ b/openpilot/starpilot/ui/tests/test_layout_preview_renderer.py @@ -15,6 +15,32 @@ import pyray as rl class LayoutPreviewRendererTests(unittest.TestCase): + def test_native_and_projection_previews_supply_timer_sample(self): + from openpilot.starpilot.system.android_auto.projection_layout import default_layout_for_viewport + from openpilot.starpilot.ui.onroad_customization import STOPPED_TIMER_WIDGET + native = default_document() + native['layouts']['compact'][STOPPED_TIMER_WIDGET]['enabled'] = True + projection = default_layout_for_viewport((2880, 1080)) + projection['widgets'][STOPPED_TIMER_WIDGET].update(enabled=True, x=450, y=600) + payloads = [('compact', native), ('projection', {'base': default_document(), 'layout': projection})] + with patch('openpilot.starpilot.ui.layout_preview_renderer.rl.is_window_ready', return_value=True), \ + patch('openpilot.starpilot.ui.layout_preview_renderer._Canvas') as canvas_type: + for profile, document in payloads: + renderer = LayoutPreviewRenderer() + renderer({'profile': profile, 'document': document, 'scene': 'stopped'}) + state, scene = canvas_type.return_value.render.call_args.args + self.assertEqual(scene, 'stopped') + self.assertEqual(state.speed_mps, 0) + self.assertEqual(state.stopped_duration_s, 134) + layout_profile = 'large' if profile == 'projection' else profile + self.assertTrue(state.customization['layouts'][layout_profile][STOPPED_TIMER_WIDGET]['enabled']) + if profile == 'projection': + self.assertEqual(canvas_type.call_args.args, (Profile.LARGE, (2880, 1080))) + self.assertEqual(state.customization['layouts']['large'][STOPPED_TIMER_WIDGET]['x'], 450) + renderer.close() + # Ordinary layout scenes also show enabled widgets, even without live standstill data. + self.assertEqual(sample_state('engaged', native).stopped_duration_s, 134) + def test_fixed_scenes_have_no_camera_or_live_input(self): document = default_document() expected = { diff --git a/openpilot/starpilot/ui/tests/test_onroad_customization.py b/openpilot/starpilot/ui/tests/test_onroad_customization.py index edbe61785d..3e4a16465e 100644 --- a/openpilot/starpilot/ui/tests/test_onroad_customization.py +++ b/openpilot/starpilot/ui/tests/test_onroad_customization.py @@ -745,12 +745,36 @@ def test_stopped_timer_draws_inside_its_moved_widget(): document = default_document() view = onroad.OnroadView.__new__(onroad.OnroadView) view.fonts = Mock(spec=BitmapFonts) + view.fonts.profile = Profile.COMPACT view.fonts.measure.return_value = NS(width=100, height=20) state = NS(customization=document) view._stopped_timer(state, 75) + view.fonts.draw.assert_not_called() + document["layouts"]["compact"][STOPPED_TIMER_WIDGET]["enabled"] = True + view._stopped_timer(state, 75) default = [call.args[3:5] for call in view.fonts.draw.call_args_list[1::2]] assert default == [((476 - 100) / 2, 101 - 10), ((476 - 100) / 2, 149 - 10)] document["layouts"]["compact"][STOPPED_TIMER_WIDGET].update(x=0, y=0) view.fonts.draw.reset_mock() view._stopped_timer(state, 75) assert [call.args[3:5] for call in view.fonts.draw.call_args_list[1::2]] == [(170, 53 - 10), (170, 101 - 10)] + + +def test_projection_timer_renders_at_saved_coordinates_without_native_timer_enabled(): + from openpilot.starpilot.ui import onroad + from openpilot.starpilot.system.android_auto.projection_layout import default_layout_for_viewport, projection_customization + layout = default_layout_for_viewport((2880, 1080)) + layout['widgets'][STOPPED_TIMER_WIDGET].update(enabled=True, x=450, y=600) + document = projection_customization(layout, default_document()) + view = onroad.OnroadView.__new__(onroad.OnroadView) + view.fonts = Mock(spec=BitmapFonts) + view.fonts.profile = Profile.LARGE + view.fonts.measure.return_value = NS(width=100, height=20) + view._stopped_timer(NS(customization=document), 134) + text_calls = view.fonts.draw.call_args_list[1::2] + assert [call.args[0] for call in text_calls] == ['2 minutes', '14 seconds'] + assert [call.args[3:5] for call in text_calls] == [(730, 669.5), (730, 741.5)] + view.fonts.draw.reset_mock() + document['layouts']['large'][STOPPED_TIMER_WIDGET]['enabled'] = False + view._stopped_timer(NS(customization=document), 134) + view.fonts.draw.assert_not_called() diff --git a/openpilot/starpilot/ui/tests/test_onroad_stopped_timer.py b/openpilot/starpilot/ui/tests/test_onroad_stopped_timer.py index 7bc45e4fe1..ee1ecf1e0d 100644 --- a/openpilot/starpilot/ui/tests/test_onroad_stopped_timer.py +++ b/openpilot/starpilot/ui/tests/test_onroad_stopped_timer.py @@ -17,6 +17,25 @@ SECOND = 1_000_000_000 class StoppedTimerTests(unittest.TestCase): + def test_projection_timer_enablement_overrides_native_choice(self): + for native_enabled, projection_enabled in ((False, True), (True, False)): + with self.subTest(native_enabled=native_enabled, projection_enabled=projection_enabled): + ui = ui_fake() + car = ui.sm.messages['carState'] + car.canValid, car.canTimeout, car.standstill = True, False, True + car.gearShifter = car_schema.CarState.GearShifter.drive + adapter = RuntimeSnapshotAdapter(ui) + document = default_document() + document['layouts']['compact'][STOPPED_TIMER_WIDGET]['enabled'] = native_enabled + with patch('openpilot.starpilot.ui.runtime_snapshot.read_customization', return_value=document): + for second in (0, 61): + now = NOW + second * SECOND + for name in ('deviceState', 'pandaStates', 'carState'): + ui.sm.logMonoTime[name] = now + state = adapter.build(ShellMode.ONROAD, now_ns=now, stopped_timer_enabled=projection_enabled).onroad + self.assertEqual(state.stopped_duration_s, 61 if projection_enabled else None) + self.assertEqual(document['layouts']['compact'][STOPPED_TIMER_WIDGET]['enabled'], native_enabled) + def test_first_drive_minute_then_elapsed_and_color_transitions(self): timer = StoppedTimer() def step(second): diff --git a/openpilot/starpilot/ui/tests/test_widget_layers.py b/openpilot/starpilot/ui/tests/test_widget_layers.py index a5fe5342b9..8a6c1f3a84 100644 --- a/openpilot/starpilot/ui/tests/test_widget_layers.py +++ b/openpilot/starpilot/ui/tests/test_widget_layers.py @@ -61,7 +61,8 @@ def test_legacy_projection_order_adds_upstream_mode_widget(): projection['widgetOrder'] = [key for key in DEFAULT_WIDGET_ORDER['large'] if key != MODE_WIDGET] migrated = validate_layout_for_viewport(projection, (2880, 1080)) assert migrated['widgets'][MODE_WIDGET]['enabled'] is False - assert migrated['widgetOrder'] == [*projection['widgetOrder'], MODE_WIDGET, 'nav_card', 'nav_map', 'nav_home', 'nav_work', 'bookmark', CAR_EXIT] + assert migrated['widgetOrder'] == [*projection['widgetOrder'], MODE_WIDGET, 'nav_card', 'nav_map', 'nav_home', 'nav_work', + 'bookmark', 'stopped_timer', CAR_EXIT] def test_old_documents_keep_their_original_shape(): @@ -153,6 +154,7 @@ def test_renderer_dispatches_each_widget_in_saved_order(profile, viewport): assert 'steering_wheel' not in seen assert 'speed_limit_actions' not in seen assert 'torque_bar' not in seen + assert STOPPED_TIMER_WIDGET not in seen if profile == 'compact': seen.clear() state.alert.size = 'none'