mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-05 20:13:47 +08:00
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:
@@ -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 & 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'))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user