Add stopped timer to Android Auto and layout previews

Render the stopped timer in projected driving views and preview scenes, with layout migration and runtime coverage. Include the matching prebuilt receipt.
This commit is contained in:
Danny
2026-10-10 18:32:29 -07:00
committed by dirwin31
parent fbb21f702a
commit 6cfbf1deb8
16 changed files with 163 additions and 22 deletions
@@ -19,7 +19,7 @@ function fixture() {
return { feed, requests, updates, timers, revoked, tick, reply, get unauthorized() { return unauthorized } }
}
assert.deepEqual(PREVIEW_SCENES.map(({ id }) => id), ["engaged", "aol", "long_only", "experimental", "braking",
assert.deepEqual(PREVIEW_SCENES.map(({ id }) => id), ["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"])
assert.equal(PREVIEW_SCENES.find(({ id }) => id === "experimental").label, "Manual experimental")
@@ -4,6 +4,7 @@ export const PREVIEW_SCENES = [
{ id: "long_only", label: "Longitudinal only" },
{ id: "experimental", label: "Manual experimental" },
{ id: "braking", label: "Braking" },
{ id: "stopped", label: "Stopped timer" },
{ id: "cem_stop_light", label: "CEM · Traffic light" },
{ id: "cem_lead", label: "CEM · Stopped lead" },
{ id: "cem_curve", label: "CEM · Curve" },
@@ -339,9 +339,9 @@ export const LayoutWidgetPreview = {
</text>
</template>
<template v-else-if="widget.kind === 'stopped_timer'">
<text :x="widget.width / 2" y="53" text-anchor="middle" dominant-baseline="middle" font-size="64" font-weight="700" fill="#33c2a0"
<text :x="widget.width / 2" :y="widget.height * 53 / 112" text-anchor="middle" dominant-baseline="middle" :font-size="widget.height * 64 / 112" font-weight="700" fill="#b67129"
style="filter: drop-shadow(2px 2px 1px #000000aa)">2 minutes</text>
<text :x="widget.width / 2" y="101" text-anchor="middle" dominant-baseline="middle" font-size="30" font-weight="500"
<text :x="widget.width / 2" :y="widget.height * 101 / 112" text-anchor="middle" dominant-baseline="middle" :font-size="widget.height * 30 / 112" font-weight="500" fill="#fffffff2"
style="filter: drop-shadow(2px 2px 1px #000000aa)">14 seconds</text>
</template>
<template v-else-if="widget.kind === 'clock'">
@@ -1244,6 +1244,7 @@ export const OnroadLayoutPage = {
</section>
<details class="gx-layout__help"><summary>Preview tips &amp; display behavior</summary><div>
<p class="gx-note">Drag widgets in Arrange widgets to place them. Readings are samples.</p>
<p class="gx-note">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.</p>
<p class="gx-note">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.</p>
<p v-if="!projection" class="gx-note">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.</p>
<p v-if="!projection && state.showFavoriteZones" class="gx-note">Favorite tap areas apply to enabled slots. Speed-limit controls and the steering wheel take priority when the drawer is closed.</p>
@@ -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
@@ -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:
@@ -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)
@@ -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):
@@ -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,
@@ -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'))
+14 -6
View File
@@ -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)
@@ -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
+3 -2
View File
@@ -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)),
@@ -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 = {
@@ -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()
@@ -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):
@@ -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'