diff --git a/openpilot/starpilot/system/android_auto/projection_onroad.py b/openpilot/starpilot/system/android_auto/projection_onroad.py index 926bad69e3..72f825c5f0 100644 --- a/openpilot/starpilot/system/android_auto/projection_onroad.py +++ b/openpilot/starpilot/system/android_auto/projection_onroad.py @@ -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 diff --git a/openpilot/starpilot/system/android_auto/tests/test_projection_onroad_boundary.py b/openpilot/starpilot/system/android_auto/tests/test_projection_onroad_boundary.py index 422eb6cdbb..1e7c39602d 100644 --- a/openpilot/starpilot/system/android_auto/tests/test_projection_onroad_boundary.py +++ b/openpilot/starpilot/system/android_auto/tests/test_projection_onroad_boundary.py @@ -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 = []