Add StarPilot logo and purple theme to AA standby screen

This commit is contained in:
dirwin31
2026-10-13 21:37:09 -07:00
parent abf3eb99b6
commit 88e85ed46e
2 changed files with 82 additions and 6 deletions
@@ -12,6 +12,7 @@ from openpilot.starpilot.system.android_auto.projection_geometry import FALLBACK
CERTIFICATE_NOTICE_NS = 10_000_000_000 # how long the expiry heads-up stays at the start of a drive
DRIVER_MONITOR_HOLD_NS = 350_000_000
STANDBY_LOGO = Path(__file__).parents[3] / 'selfdrive/assets/images/starpilot_logo.png'
BUBBLE_REDUNDANT_ALERTS = frozenset(('preLaneChangeLeft', 'preLaneChangeRight', 'laneChange',
'laneChangeBlocked', 'laneChangeBlockedLoud'))
@@ -80,6 +81,23 @@ def native_dependencies():
rl.set_shader_value(self.shader, self._engaged_loc, self._engaged_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT)
rl.set_shader_value(self.shader, self._road_camera_loc, self._road_camera_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT)
def standby_logo(height):
"""The StarPilot crescent, shrunk once on the CPU to its standby size; None if it cannot be loaded."""
image = rl.load_image(str(STANDBY_LOGO))
try:
if image.data == rl.ffi.NULL or image.width <= 0 or image.height <= 0:
return None
if image.height > height:
rl.image_resize(image, round(image.width * height / image.height), height)
texture = rl.load_texture_from_image(image)
finally:
if image.data != rl.ffi.NULL:
rl.unload_image(image)
if texture.id == 0:
return None
rl.set_texture_filter(texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
return texture
return SimpleNamespace(rl=rl, ui_state=ui_state, camera=ProjectionRoadCamera,
onroad=OnroadView, monitor=DriverMonitorLayer, fonts=BitmapFonts,
font_role=FontRole, profile=Profile, font_directory=default_font_directory,
@@ -87,7 +105,7 @@ def native_dependencies():
shell_mode=ShellMode, pip_renderer=PiPRenderer, read_pip=read_pip, pip_signals=Signals,
pip_rect=Rect, pip_widgets=CAMERA_WIDGETS, placement=placement, widget_size=widget_size,
alert=OnroadAlert, alert_size=AlertSize, map_overlay=MapOverlay, map_feed=MapFeed,
favorites=ProjectionFavorites)
favorites=ProjectionFavorites, standby_logo=standby_logo)
class ProjectionOnroad:
@@ -110,6 +128,8 @@ class ProjectionOnroad:
self.camera_stream = None # the camera stream the last frame drew, or None
self.native = dependencies or native_dependencies()
native = self.native
self._standby_logo = None
self._standby_logo_tried = False
if native.ui_state.projection_read_only is not True:
raise RuntimeError('Projection UIState must be read-only')
self._resources = ExitStack()
@@ -325,11 +345,39 @@ class ProjectionOnroad:
def _standby(self):
rl = self.native.rl
rl.draw_rectangle(0, 0, int(self.width), int(self.height), rl.Color(12, 26, 34, 255))
rl.draw_rectangle_rounded(rl.Rectangle(120, 140, self.width - 240, self.height - 280), 0.06, 16, rl.Color(18, 40, 57, 255))
rl.draw_rectangle_rounded_lines_ex(rl.Rectangle(120, 140, self.width - 240, self.height - 280), 0.06, 16, 4, rl.Color(50, 126, 144, 255))
self.fonts.draw('StarPilot', self.native.font_role.BRAND, 112, 220, 325)
self.fonts.draw('Ready when driving', self.native.font_role.MEDIUM, 55, 225, 475)
purple = rl.Color(188, 132, 255, 255) # StarPilot purple, as on the comma's moon
rl.draw_rectangle(0, 0, int(self.width), int(self.height), rl.Color(10, 10, 22, 255))
panel_height = self.height - 280
panel = rl.Rectangle(120, 140, self.width - 240, panel_height)
rl.draw_rectangle_rounded(panel, 0.06, 16, rl.Color(26, 16, 46, 255))
rl.draw_rectangle_rounded_lines_ex(panel, 0.06, 16, 4, purple)
logo = self._standby_texture(int(panel_height * 0.6))
text_x = 220
if logo is not None:
rl.draw_texture(logo, 200, int((self.height - logo.height) / 2), rl.WHITE)
text_x = 200 + logo.width + 70
middle = self.height / 2
self.fonts.draw('StarPilot', self.native.font_role.BRAND, 112, text_x, middle - 130, purple)
self.fonts.draw('Ready when driving', self.native.font_role.MEDIUM, 55, text_x + 5, middle + 20, rl.Color(222, 204, 255, 255))
def _standby_texture(self, height):
"""Load the logo the first time standby shows; a missing logo leaves the text-only screen."""
if not self._standby_logo_tried:
self._standby_logo_tried = True
load = getattr(self.native, 'standby_logo', None)
if load is not None:
try:
self._standby_logo = load(height)
except Exception:
self._standby_logo = None
if self._standby_logo is not None:
self._resources.callback(self._unload_standby_logo)
return self._standby_logo
def _unload_standby_logo(self):
if self._standby_logo is not None:
self.native.rl.unload_texture(self._standby_logo)
self._standby_logo = None
def render(self):
ui = self.native.ui_state
@@ -21,6 +21,7 @@ spec.loader.exec_module(projection)
class FakeGraphics:
BLACK = object()
WHITE = object()
@staticmethod
def Color(*values): return values
@@ -37,6 +38,9 @@ class FakeGraphics:
@staticmethod
def draw_rectangle_rounded_lines_ex(*_): pass
@staticmethod
def draw_texture(*_): pass
@staticmethod
def unload_texture(*_): pass
@@ -150,6 +154,30 @@ class TestProjectionOnroad(unittest.TestCase):
self.assertEqual(events[-3:], ['view_closed', 'camera_closed', 'fonts_closed'])
self.assertEqual(native.ui_state._offroad_transition_callbacks, [])
def test_standby_logo_loads_once_and_is_freed_on_close(self):
native, _ = self.dependencies()
logo = SimpleNamespace(width=300, height=250)
heights, drawn, unloaded = [], [], []
native.standby_logo = lambda height: heights.append(height) or logo
native.rl = SimpleNamespace(**{name: getattr(FakeGraphics, name) for name in dir(FakeGraphics) if not name.startswith('_')})
native.rl.draw_texture = lambda texture, *_: drawn.append(texture)
native.rl.unload_texture = unloaded.append
view = projection.ProjectionOnroad(dependencies=native)
view.render()
view.render()
self.assertEqual(heights, [576])
self.assertEqual(drawn, [logo, logo])
view.close()
self.assertEqual(unloaded, [logo])
def test_standby_without_logo_still_draws_text(self):
native, events = self.dependencies()
native.standby_logo = lambda _height: None
view = projection.ProjectionOnroad(dependencies=native)
self.addCleanup(view.close)
view.render()
self.assertEqual(events.count('draw'), 2)
def test_side_camera_layer_uses_blinkers_saved_preview_and_projection_placements(self):
native, events = self.dependencies()
calls = []