From 936a50e35d88b8b7cfb18b5eacec78afe296da1e Mon Sep 17 00:00:00 2001 From: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Date: Tue, 14 Apr 2026 03:21:25 -0400 Subject: [PATCH] BigUI WIP: Some Sounds --- .../layouts/settings/starpilot/aethergrid.py | 122 ++++++++++ .../ui/layouts/settings/starpilot/panel.py | 18 +- .../ui/layouts/settings/starpilot/sounds.py | 228 +++++++----------- 3 files changed, 231 insertions(+), 137 deletions(-) diff --git a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py index c7289b6df..4a40948b4 100644 --- a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py +++ b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py @@ -521,6 +521,128 @@ class ValueTile(AetherTile): self._draw_text_fit(self._font_desc, self.desc, rl.Vector2(face.x + content_pad, ty + 28 + SPACING.line_gap + 34), max_w, 18, align_center=True) +class SliderTile(AetherTile): + def __init__( + self, + title: str, + get_value: Callable[[], float], + set_value: Callable[[float], None], + min_val: float, + max_val: float, + step: float, + icon_path: str | None = None, + bg_color: rl.Color | str | None = None, + is_enabled: Callable[[], bool] | None = None, + desc: str = "", + unit: str = "", + labels: dict[float, str] | None = None, + ): + super().__init__(surface_color=bg_color) + self.title = title + self.desc = desc + self.get_value = get_value + self.set_value = set_value + self.min_val = min_val + self.max_val = max_val + self.step = step + self.unit = unit + self.labels = labels or {} + self.set_enabled(is_enabled or (lambda: True)) + self._icon = starpilot_texture(icon_path, 80, 80) if icon_path else None + self._font = gui_app.font(FontWeight.BOLD) + self._font_desc = gui_app.font(FontWeight.NORMAL) + self._active_color = self.surface_color + self._disabled_color = rl.Color(120, 120, 120, 255) + + self._is_dragging = False + self._last_mouse_x = 0.0 + self._velocity = 0.0 + self._smooth_value = get_value() + + def _handle_mouse_press(self, mouse_pos: MousePos): + if rl.check_collision_point_rec(mouse_pos, self._hit_rect) and self.enabled: + self._is_pressed = True + self._is_dragging = True + self._last_mouse_x = mouse_pos.x + self._velocity = 0.0 + + def _handle_mouse_release(self, mouse_pos: MousePos): + if self._is_dragging: + self._is_dragging = False + self._is_pressed = False + + def _handle_mouse_event(self, mouse_event): + if not rl.check_collision_point_rec(mouse_event.pos, self._hit_rect): + if not self._is_dragging: + self._plate_target = 0.0 + + if self._is_dragging: + dt = rl.get_frame_time() + current_val = self.get_value() + mouse_pos = mouse_event.pos + dx = mouse_pos.x - self._last_mouse_x + self._velocity = dx / max(dt, 0.001) + self._last_mouse_x = mouse_pos.x + + rect_w = self._rect.width + if rect_w > 0: + val_range = self.max_val - self.min_val + val_dx = (dx / rect_w) * val_range + new_val = current_val + val_dx + + abs_vel = abs(self._velocity) + snap_threshold = 800 + coarse_step = 10 if val_range >= 100 else self.step * 5 + dynamic_step = coarse_step if abs_vel > snap_threshold else self.step + + snapped = round(new_val / dynamic_step) * dynamic_step + snapped = max(self.min_val, min(self.max_val, snapped)) + + if abs(snapped - current_val) >= self.step: + self.set_value(float(snapped)) + + def _render(self, rect: rl.Rectangle): + enabled = self.enabled + current_val = self.get_value() + dt = rl.get_frame_time() + + self._smooth_value += (current_val - self._smooth_value) * (1 - math.exp(-dt / 0.1)) + + self.surface_color = self._active_color if enabled else self._disabled_color + if not enabled: + self._plate_offset = 0.0 + self._plate_target = 0.0 + + face = self._render_layers(rect) + + frac = (self._smooth_value - self.min_val) / (self.max_val - self.min_val) + fill_w = face.width * frac + if fill_w > 1: + fill_rect = rl.Rectangle(face.x, face.y, fill_w, face.height) + fill_color = rl.Color( + min(self.surface_color.r + 30, 255), + min(self.surface_color.g + 30, 255), + min(self.surface_color.b + 30, 255), + 140 + ) + rl.draw_rectangle_rounded(fill_rect, TILE_RADIUS, 10, fill_color) + edge_x = face.x + fill_w + rl.draw_rectangle_rec(rl.Rectangle(edge_x - 3, face.y, 3, face.height), rl.Color(255, 255, 255, 80)) + + line_heights = [28, 28] + _, ty = self._centered_content(face, self._icon, 0.75, 28, len(line_heights), line_heights) + content_pad = SPACING.tile_content + max_w = face.width - (content_pad * 2) + + self._draw_text_fit(self._font, self.title, rl.Vector2(face.x + content_pad, ty), max_w, 28, align_center=True, uppercase=True) + + val_str = self.labels.get(current_val, f"{int(current_val)}{self.unit}") + self._draw_text_fit(self._font, val_str, rl.Vector2(face.x + content_pad, ty + 28 + SPACING.line_gap), max_w, 28, align_center=True, uppercase=True) + + if self.desc: + self._draw_text_fit(self._font_desc, self.desc, rl.Vector2(face.x + content_pad, ty + 28 + SPACING.line_gap + 34), max_w, 18, align_center=True) + + class AetherSlider(Widget): def __init__( self, diff --git a/selfdrive/ui/layouts/settings/starpilot/panel.py b/selfdrive/ui/layouts/settings/starpilot/panel.py index c7cd62ba1..0c98e1d72 100644 --- a/selfdrive/ui/layouts/settings/starpilot/panel.py +++ b/selfdrive/ui/layouts/settings/starpilot/panel.py @@ -8,7 +8,7 @@ import pyray as rl from openpilot.common.params import Params from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget -from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, HubTile, ToggleTile, ValueTile, SPACING +from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, HubTile, ToggleTile, ValueTile, SliderTile, SPACING from openpilot.selfdrive.ui.layouts.settings.starpilot.sectioned_panel import SectionedTileLayout, TileSection @@ -93,6 +93,22 @@ class StarPilotPanel(Widget): if tile_type == "value": return ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], icon_path=cat.get("icon"), bg_color=cat.get("color"), is_enabled=cat.get("is_enabled"), desc=tr(cat.get("desc", ""))) + if tile_type == "slider": + return SliderTile( + title=tr(cat["title"]), + get_value=cat["get_value"], + set_value=cat["set_value"], + min_val=cat["min_val"], + max_val=cat["max_val"], + step=cat["step"], + unit=cat.get("unit", ""), + labels=cat.get("labels", {}), + icon_path=cat.get("icon"), + bg_color=cat.get("color"), + is_enabled=cat.get("is_enabled"), + desc=tr(cat.get("desc", "")) + ) + return None def _build_tile_grid(self, categories: list[dict], columns: int | None = None, padding: int | None = None, uniform_width: bool = False) -> TileGrid: diff --git a/selfdrive/ui/layouts/settings/starpilot/sounds.py b/selfdrive/ui/layouts/settings/starpilot/sounds.py index 8765be1e8..50ab27ec6 100644 --- a/selfdrive/ui/layouts/settings/starpilot/sounds.py +++ b/selfdrive/ui/layouts/settings/starpilot/sounds.py @@ -1,5 +1,6 @@ from __future__ import annotations import subprocess +import time from pathlib import Path import pyray as rl @@ -12,7 +13,8 @@ from openpilot.system.ui.widgets import DialogResult from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel -from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, ToggleTile, AetherSliderDialog, RadioTileGroup, SPACING +from openpilot.selfdrive.ui.layouts.settings.starpilot.tabbed_panel import TabSectionSpec, TabbedSectionHost +from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, ToggleTile, SPACING class StarPilotSoundsLayout(StarPilotPanel): COOLDOWN_KEY = "SwitchbackModeCooldown" @@ -37,79 +39,28 @@ class StarPilotSoundsLayout(StarPilotPanel): def __init__(self): super().__init__() - self._section_names = ["volume_control", "custom_alerts"] - self._active_section = self._section_names[0] - self._sub_panels = { - "volume_control": StarPilotVolumeControlLayout(), - "custom_alerts": StarPilotCustomAlertsLayout(), - } - self._section_tabs = RadioTileGroup( - "", - [tr("Volumes"), tr("Alerts")], - 0, - self._on_section_change, - ) + self._section_tabs = TabbedSectionHost([ + TabSectionSpec("volume_control", "Volumes", StarPilotVolumeControlLayout()), + TabSectionSpec("custom_alerts", "Alerts", StarPilotCustomAlertsLayout()), + ]) - for name, panel in self._sub_panels.items(): - if hasattr(panel, 'set_navigate_callback'): - panel.set_navigate_callback(lambda sub_panel, section_name=name: self._navigate_to_child(section_name, sub_panel)) - if hasattr(panel, 'set_back_callback'): - panel.set_back_callback(self._go_back) + def set_navigate_callback(self, callback): + self._section_tabs.set_navigate_callback(callback) - def _on_section_change(self, index: int): - if 0 <= index < len(self._section_names): - previous_panel = self._sub_panels[self._active_section] - if hasattr(previous_panel, 'set_current_sub_panel'): - previous_panel.set_current_sub_panel("") - self._current_sub_panel = "" - self._set_active_section(self._section_names[index], "") - if self._navigate_callback: - self._navigate_callback("") - - def _set_active_section(self, section_name: str, child_panel: str = ""): - if section_name not in self._sub_panels: - return - - if section_name != self._active_section: - self._sub_panels[self._active_section].hide_event() - self._active_section = section_name - self._sub_panels[self._active_section].show_event() - - self._section_tabs.set_index(self._section_names.index(section_name)) - panel = self._sub_panels[section_name] - if hasattr(panel, 'set_current_sub_panel'): - panel.set_current_sub_panel(child_panel) - - def _navigate_to_child(self, section_name: str, child_panel: str): - self._set_active_section(section_name, child_panel) - if self._navigate_callback: - self._navigate_callback(f"{section_name}:{child_panel}") - - def set_current_sub_panel(self, sub_panel: str): - super().set_current_sub_panel(sub_panel) - if not sub_panel: - self._set_active_section(self._active_section, "") - return - - if ":" in sub_panel: - section_name, child_panel = sub_panel.split(":", 1) - self._set_active_section(section_name, child_panel) - elif sub_panel in self._section_names: - self._set_active_section(sub_panel) + def set_back_callback(self, callback): + self._section_tabs.set_back_callback(callback) def _render(self, rect): - tab_rect = rl.Rectangle(rect.x, rect.y, rect.width, SPACING.tab_height) - panel_rect = rl.Rectangle(rect.x, rect.y + SPACING.tab_height + SPACING.tab_panel_gap, rect.width, rect.height - SPACING.tab_height - SPACING.tab_panel_gap) - self._section_tabs.render(tab_rect) - self._sub_panels[self._active_section].render(panel_rect) + self._section_tabs.render(rect) + + def set_current_sub_panel(self, sub_panel: str): + self._section_tabs.set_current_sub_panel(sub_panel) def show_event(self): - super().show_event() - self._sub_panels[self._active_section].show_event() + self._section_tabs.show_event() def hide_event(self): - super().hide_event() - self._sub_panels[self._active_section].hide_event() + self._section_tabs.hide_event() class StarPilotVolumeControlLayout(StarPilotPanel): COOLDOWN_INFO = {"title": tr_noop("Switchback Mode Cooldown"), "icon": "toggle_icons/icon_mute.png", "min": 0, "max": 30} @@ -129,82 +80,82 @@ class StarPilotVolumeControlLayout(StarPilotPanel): def __init__(self): super().__init__() self._init_sound_player() - self._tile_grid = TileGrid(columns=2, padding=SPACING.tile_gap, uniform_width=True) - - self.CATEGORIES = [] - for key in StarPilotSoundsLayout.VOLUME_KEYS: - info = self.VOLUME_INFO[key] - - def get_val(k=key): - v = self._params.get_int(k, return_default=True, default=100) - if v == 0: return tr("Muted") - if v == 101: return tr("Auto") - return f"{v}%" - - def on_click(k=key, i=info): - self._show_volume_selector(k, i) - - self.CATEGORIES.append({ - "title": info["title"], - "type": "value", - "get_value": get_val, - "on_click": on_click, - "icon": info["icon"], - "color": "#E63956" - }) - - def get_cooldown_val(): - v = self._params.get_int(StarPilotSoundsLayout.COOLDOWN_KEY, return_default=True, default=0) - if v == 0: - return tr("Off") - if v == 1: - return tr("1 minute") - return f"{v} {tr('minutes')}" - - self.CATEGORIES.append({ - "title": self.COOLDOWN_INFO["title"], - "type": "value", - "get_value": get_cooldown_val, - "on_click": self._show_cooldown_selector, - "icon": self.COOLDOWN_INFO["icon"], - "color": "#E63956" - }) + self._pending_sound_test = None + self._last_sound_change_time = 0.0 + self.SECTIONS = [ + { + "title": tr_noop("Volume Levels"), + "categories": self._build_volume_categories(), + }, + { + "title": tr_noop("Safety & Cooldown"), + "categories": self._build_safety_categories(), + } + ] self._rebuild_grid() - def _show_volume_selector(self, key: str, info: dict): - current_v = self._params.get_int(key, return_default=True, default=100) - - def on_close(res, val): - if res == DialogResult.CONFIRM: + def _update_state(self): + super()._update_state() + if self._pending_sound_test and (time.monotonic() - self._last_sound_change_time) > 0.8: + self._test_sound(self._pending_sound_test) + self._pending_sound_test = None + + def _build_volume_categories(self): + cats = [] + for key in StarPilotSoundsLayout.VOLUME_KEYS: + info = self.VOLUME_INFO[key] + + def get_val(k=key): + return float(self._params.get_int(k, return_default=True, default=100)) + + def set_val(val, k=key): new_v = int(val) - if new_v != 101 and new_v < info["min"]: - new_v = info["min"] - self._params.put_int(key, new_v) - self._test_sound(key) - self._rebuild_grid() + if new_v != 101 and new_v < self.VOLUME_INFO[k]["min"]: + new_v = self.VOLUME_INFO[k]["min"] + self._params.put_int(k, new_v) + self._pending_sound_test = k + self._last_sound_change_time = time.monotonic() - gui_app.set_modal_overlay(AetherSliderDialog( - tr(info["title"]), 0, 101, 1, current_v, on_close, - unit="%", labels={0: tr("Muted"), 101: tr("Auto")}, color="#E63956" - )) + cats.append({ + "title": info["title"], + "type": "slider", + "get_value": get_val, + "set_value": set_val, + "min_val": 0, + "max_val": 101, + "step": 1, + "unit": "%", + "labels": {0: tr("Muted"), 101: tr("Auto")}, + "icon": info["icon"], + "color": "#3B82F6" + }) + return cats - def _show_cooldown_selector(self): - current_v = self._params.get_int(StarPilotSoundsLayout.COOLDOWN_KEY, return_default=True, default=0) + def _build_safety_categories(self): + def get_cooldown_val(): + return float(self._params.get_int(StarPilotSoundsLayout.COOLDOWN_KEY, return_default=True, default=0)) - def on_close(res, val): - if res == DialogResult.CONFIRM: - self._params.put_int(StarPilotSoundsLayout.COOLDOWN_KEY, int(val)) - self._rebuild_grid() + def set_cooldown_val(val): + self._params.put_int(StarPilotSoundsLayout.COOLDOWN_KEY, int(val)) - gui_app.set_modal_overlay(AetherSliderDialog( - tr(self.COOLDOWN_INFO["title"]), 0, self.COOLDOWN_INFO["max"], 1, current_v, on_close, - unit=" min", labels={0: tr("Off")}, color="#E63956" - )) + return [{ + "title": self.COOLDOWN_INFO["title"], + "type": "slider", + "get_value": get_cooldown_val, + "set_value": set_cooldown_val, + "min_val": 0, + "max_val": float(self.COOLDOWN_INFO["max"]), + "step": 1, + "unit": " " + tr("min"), + "labels": {0: tr("Off"), 1: tr("1 minute")}, + "icon": self.COOLDOWN_INFO["icon"], + "color": "#3B82F6" + }] @classmethod def _init_sound_player(cls): - if cls._sound_player_process is not None: return + if cls._sound_player_process is not None and cls._sound_player_process.poll() is None: return program = """ import numpy as np import sounddevice as sd @@ -215,11 +166,13 @@ while True: line = sys.stdin.readline() if not line: break path, volume = line.strip().split('|') - sound_file = wave.open(path, 'rb') - audio = np.frombuffer(sound_file.readframes(sound_file.getnframes()), dtype=np.int16).astype(np.float32) / 32768.0 - sd.play(audio * float(volume), sound_file.getframerate()) + with wave.open(path, 'rb') as sound_file: + audio = np.frombuffer(sound_file.readframes(sound_file.getnframes()), dtype=np.int16).astype(np.float32) / 32768.0 + sd.play(audio * float(volume), sound_file.getframerate()) sd.wait() - except: pass + except Exception: + sd._terminate() + sd._initialize() """ cls._sound_player_process = subprocess.Popen(["python3", "-u", "-c", program], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) @@ -240,6 +193,9 @@ while True: sound_path = theme_path if theme_path.exists() else stock_path if not sound_path.exists(): return volume = self._params.get_int(key, return_default=True, default=100) / 100.0 + if self._sound_player_process.poll() is not None: + self._sound_player_process = None + self._init_sound_player() try: self._sound_player_process.stdin.write(f"{sound_path}|{volume}\n".encode()) self._sound_player_process.stdin.flush() @@ -267,7 +223,7 @@ class StarPilotCustomAlertsLayout(StarPilotPanel): "get_state": lambda k=key: self._params.get_bool(k), "set_state": lambda s, k=key: self._params.put_bool(k, s), "icon": info["icon"], - "color": "#E63956", + "color": "#3B82F6", "key": key # Store for visibility check }) self._rebuild_grid()