From 849c1f7cc947238f95b86a985933b80d1bb937ff Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Thu, 11 Jun 2026 17:41:51 -0400 Subject: [PATCH 01/13] BluePilot: add AlertsPill to custom home layout (home_bp.py) The BluePilot custom home layout (`home_bp.py`) was missing the `AlertsPill` used to display and access offroad alerts. Updated the layout to match the stock Mici layout's `set_callbacks` signature, now accepting `on_alerts`, `alert_count_callback`, and `max_severity_callback`. Instantiated the `AlertsPill` component and positioned it just below the settings gear icon so offroad alerts remain reachable. Added hit-testing (`_point_in_alerts`) and click routing to ensure tapping the pill successfully opens the alerts pane. --- selfdrive/ui/bp/mici/layouts/home_bp.py | 28 ++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/bp/mici/layouts/home_bp.py b/selfdrive/ui/bp/mici/layouts/home_bp.py index 03eef600b0..0e00228ff9 100644 --- a/selfdrive/ui/bp/mici/layouts/home_bp.py +++ b/selfdrive/ui/bp/mici/layouts/home_bp.py @@ -5,7 +5,7 @@ place; a top progress strip fills as you hold. Tap on body opens settings. Public API mirrors MiciHomeLayout so main.py wiring works unchanged: -- set_callbacks(on_settings: Callable | None = None) +- set_callbacks(on_settings, on_alerts, alert_count_callback, max_severity_callback) """ import time import pyray as rl @@ -17,7 +17,7 @@ from openpilot.system.ui.widgets.icon_widget import IconWidget from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.selfdrive.ui.mici.layouts.home import NetworkIcon, NETWORK_TYPES +from openpilot.selfdrive.ui.mici.layouts.home import AlertsPill, NetworkIcon, NETWORK_TYPES from openpilot.selfdrive.ui.bp.mici.widgets.bg_radial import BPRadialBackground from openpilot.selfdrive.ui.bp.mici.widgets.aurora_wordmark import AuroraWordmark from openpilot.selfdrive.ui.bp.mici.widgets.state_pill import StatePill @@ -37,6 +37,8 @@ class MiciHomeLayoutBP(Widget): def __init__(self): super().__init__() self._on_settings_click: Callable | None = None + self._on_alerts_click: Callable | None = None + self._alert_count_callback: Callable[[], int] | None = None # Long-press state self._mouse_down_t: float | None = None @@ -52,6 +54,7 @@ def __init__(self): self._bg = self._child(BPRadialBackground()) self._aurora = self._child(AuroraWordmark(get_mode=self._mode)) self._pill = self._child(StatePill(get_state=self._pill_state)) + self._alerts_pill = self._child(AlertsPill()) self._long_press_bar = self._child(LongPressBar()) # Gear (top-right) @@ -70,8 +73,13 @@ def __init__(self): ) # ---- public API mirroring MiciHomeLayout ---- - def set_callbacks(self, on_settings: Callable | None = None): + def set_callbacks(self, on_settings: Callable | None = None, on_alerts: Callable | None = None, + alert_count_callback: Callable[[], int] | None = None, + max_severity_callback: Callable[[], int | None] | None = None): self._on_settings_click = on_settings + self._on_alerts_click = on_alerts + self._alert_count_callback = alert_count_callback + self._alerts_pill.set_alert_count_callback(alert_count_callback, max_severity_callback) # ---- mode helpers ---- def _mode(self) -> str: @@ -128,6 +136,11 @@ def _handle_mouse_release(self, mouse_pos: MousePos): if self._did_long_press: self._did_long_press = False return + # Tap on alerts pill → alerts pane. + if self._point_in_alerts(mouse_pos): + if self._on_alerts_click: + self._on_alerts_click() + return # Tap on gear → settings. if self._point_in_gear(mouse_pos): if self._on_settings_click: @@ -147,6 +160,10 @@ def _gear_rect(self) -> rl.Rectangle: def _point_in_gear(self, p: MousePos) -> bool: return rl.check_collision_point_rec(p, self._gear_rect()) + def _point_in_alerts(self, p: MousePos) -> bool: + has_alerts = self._alert_count_callback and self._alert_count_callback() > 0 + return bool(has_alerts and rl.check_collision_point_rec(p, self._alerts_pill.rect)) + def _gear_pressed(self) -> bool: """Is the user currently pressing inside the gear hit rect?""" rect = self._gear_rect() @@ -188,6 +205,11 @@ def _render(self, _): visible_y = g.y + (g.height - GEAR_VISIBLE) / 2 self._gear_icon.render(rl.Rectangle(visible_x, visible_y, GEAR_VISIBLE, GEAR_VISIBLE)) + # Alerts pill below the gear so offroad alerts remain reachable. + alert_rect = self._alerts_pill.rect + self._alerts_pill.set_position(r.x + r.width - alert_rect.width - 10, g.y + g.height + 8) + self._alerts_pill.render() + # Meta strip (bottom-left): network icon + label meta_y = r.y + r.height - META_FONT_SIZE - 12 icon_rect = rl.Rectangle(r.x + HOME_PADDING_X, meta_y - 4, 54, 30) From 891b0faf2251a6d52c40eaa713f57e84985bd442 Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Thu, 11 Jun 2026 17:47:35 -0400 Subject: [PATCH 02/13] draw_circle_gradient crash --- selfdrive/ui/bp/mici/widgets/aurora_wordmark.py | 2 +- selfdrive/ui/bp/mici/widgets/bg_radial.py | 6 +++--- selfdrive/ui/bp/mici/widgets/icons.py | 2 +- selfdrive/ui/bp/mici/widgets/keyboard_bp.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/selfdrive/ui/bp/mici/widgets/aurora_wordmark.py b/selfdrive/ui/bp/mici/widgets/aurora_wordmark.py index ea48c7e5e0..ea03752750 100644 --- a/selfdrive/ui/bp/mici/widgets/aurora_wordmark.py +++ b/selfdrive/ui/bp/mici/widgets/aurora_wordmark.py @@ -60,7 +60,7 @@ def _render(self, _): # Three radial passes give a soft falloff with fixed-cost circle draws. for r_mul, alpha_mul in ((1.00, 1.00), (0.62, 0.85), (0.35, 0.70)): col = rl.Color(inner.r, inner.g, inner.b, int(inner.a * alpha_mul)) - rl.draw_circle_gradient(int(cx), int(cy), radius * r_mul, col, P.AURORA_OUTER) + rl.draw_circle_gradient(rl.Vector2(int(cx), int(cy)), radius * r_mul, col, P.AURORA_OUTER) # ---- Wordmark ---- # UnifiedLabel doesn't expose center alignment cleanly with wrap_text=False, diff --git a/selfdrive/ui/bp/mici/widgets/bg_radial.py b/selfdrive/ui/bp/mici/widgets/bg_radial.py index e833ce08af..5e2ba64666 100644 --- a/selfdrive/ui/bp/mici/widgets/bg_radial.py +++ b/selfdrive/ui/bp/mici/widgets/bg_radial.py @@ -36,16 +36,16 @@ def _render(self, _): # Outer fade (62% stop) — barely-perceptible navy lift at the edges of # the gradient extent. - rl.draw_circle_gradient(cx, cy, diag * 1.2, + rl.draw_circle_gradient(rl.Vector2(cx, cy), diag * 1.2, rl.Color(0x14, 0x28, 0x5A, int(0.50 * 255)), rl.Color(0, 0, 0, 0)) # Mid stop (28%) — navy tint covering most of the upper-left half. - rl.draw_circle_gradient(cx, cy, diag * 0.85, + rl.draw_circle_gradient(rl.Vector2(cx, cy), diag * 0.85, rl.Color(0x14, 0x28, 0x5A, int(0.65 * 255)), rl.Color(0, 0, 0, 0)) # Inner glow (0%) — bright blue near the corner. - rl.draw_circle_gradient(cx, cy, diag * 0.55, + rl.draw_circle_gradient(rl.Vector2(cx, cy), diag * 0.55, rl.Color(0x4A, 0x8C, 0xFF, int(0.42 * 255)), rl.Color(0, 0, 0, 0)) diff --git a/selfdrive/ui/bp/mici/widgets/icons.py b/selfdrive/ui/bp/mici/widgets/icons.py index 87441cbc0b..571a80099f 100644 --- a/selfdrive/ui/bp/mici/widgets/icons.py +++ b/selfdrive/ui/bp/mici/widgets/icons.py @@ -15,7 +15,7 @@ def _halo(cx: float, cy: float, size: float, color: rl.Color) -> None: """Soft blue halo behind an icon. Three concentric circles fading outward.""" for r_mul, alpha in ((1.6, 0.06), (1.1, 0.10), (0.7, 0.12)): c = rl.Color(color.r, color.g, color.b, int(255 * alpha)) - rl.draw_circle_gradient(int(cx), int(cy), size * r_mul, c, rl.Color(0, 0, 0, 0)) + rl.draw_circle_gradient(rl.Vector2(int(cx), int(cy)), size * r_mul, c, rl.Color(0, 0, 0, 0)) def _draw_with_halo(draw_fn, cx: float, cy: float, size: float, diff --git a/selfdrive/ui/bp/mici/widgets/keyboard_bp.py b/selfdrive/ui/bp/mici/widgets/keyboard_bp.py index bfa3ba753d..c5a2fd8cba 100644 --- a/selfdrive/ui/bp/mici/widgets/keyboard_bp.py +++ b/selfdrive/ui/bp/mici/widgets/keyboard_bp.py @@ -345,7 +345,7 @@ def _lay_out_keys(self, bg_x, bg_y, keys: list[list[Key]]): # draw black circle behind selected key circle_alpha = int(self._selected_key_filter.x * 225) - rl.draw_circle_gradient(int(key_x + key.rect.width / 2), int(key_y + key.rect.height / 2), + rl.draw_circle_gradient(rl.Vector2(int(key_x + key.rect.width / 2), int(key_y + key.rect.height / 2)), SELECTED_CHAR_FONT_SIZE, rl.Color(0, 0, 0, circle_alpha), rl.BLANK) else: # move other keys away from selected key a bit @@ -393,4 +393,4 @@ def _render(self, _): self._lay_out_keys(bg_x, bg_y, self._current_keys) for row in self._current_keys: for key in row: - key.render() \ No newline at end of file + key.render() From 93d470f342a9499691783388bd15edb5eb60b569 Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Thu, 11 Jun 2026 18:31:47 -0400 Subject: [PATCH 03/13] Use stock keyboard --- selfdrive/ui/bp/mici/widgets/keyboard_bp.py | 67 ++++++++++++--------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/selfdrive/ui/bp/mici/widgets/keyboard_bp.py b/selfdrive/ui/bp/mici/widgets/keyboard_bp.py index c5a2fd8cba..bc3041aa3c 100644 --- a/selfdrive/ui/bp/mici/widgets/keyboard_bp.py +++ b/selfdrive/ui/bp/mici/widgets/keyboard_bp.py @@ -38,10 +38,10 @@ def fast_euclidean_distance(dx, dy): class Key(Widget): - def __init__(self, char: str): + def __init__(self, char: str, font_weight: FontWeight = FontWeight.SEMI_BOLD): super().__init__() self.char = char - self._font = gui_app.font(FontWeight.SEMI_BOLD) + self._font = gui_app.font(font_weight) self._x_filter = BounceFilter(0.0, 0.1 * ANIMATION_SCALE, 1 / gui_app.target_fps) self._y_filter = BounceFilter(0.0, 0.1 * ANIMATION_SCALE, 1 / gui_app.target_fps) self._size_filter = BounceFilter(CHAR_FONT_SIZE, 0.1 * ANIMATION_SCALE, 1 / gui_app.target_fps) @@ -53,20 +53,23 @@ def __init__(self, char: str): self.original_position = rl.Vector2(0, 0) def set_position(self, x: float, y: float, smooth: bool = True): - # TODO: swipe up from NavWidget has the keys lag behind other elements a bit + # Smooth keys within parent rect + base_y = self._parent_rect.y if self._parent_rect else 0.0 + local_y = y - base_y + if not self._position_initialized: self._x_filter.x = x - self._y_filter.x = y + self._y_filter.x = local_y # keep track of original position so dragging around feels consistent. also move touch area down a bit - self.original_position = rl.Vector2(x, y + KEY_TOUCH_AREA_OFFSET) + self.original_position = rl.Vector2(x, local_y + KEY_TOUCH_AREA_OFFSET) self._position_initialized = True if not smooth: self._x_filter.x = x - self._y_filter.x = y + self._y_filter.x = local_y self._rect.x = self._x_filter.update(x) - self._rect.y = self._y_filter.update(y) + self._rect.y = base_y + self._y_filter.update(local_y) def set_alpha(self, alpha: float): self._alpha_filter.update(alpha) @@ -92,12 +95,12 @@ def set_font_size(self, size: float): self._size_filter.update(size) def _get_font_size(self) -> int: - return int(round(self._size_filter.x)) + return round(self._size_filter.x) class SmallKey(Key): def __init__(self, chars: str): - super().__init__(chars) + super().__init__(chars, FontWeight.BOLD) self._size_filter.x = NUMBER_LAYER_SWITCH_FONT_SIZE def set_font_size(self, size: float): @@ -105,13 +108,15 @@ def set_font_size(self, size: float): class IconKey(Key): - def __init__(self, icon: str, vertical_align: str = "center", char: str = ""): + def __init__(self, icon: str, vertical_align: str = "center", char: str = "", icon_size: tuple[int, int] = (38, 38)): super().__init__(char) - self._icon = gui_app.texture(icon, 38, 38) + self._icon_size = icon_size + self._icon = gui_app.texture(icon, *icon_size) self._vertical_align = vertical_align - def set_icon(self, icon: str): - self._icon = gui_app.texture(icon, 38, 38) + def set_icon(self, icon: str, icon_size: tuple[int, int] | None = None): + size = icon_size if icon_size is not None else self._icon_size + self._icon = gui_app.texture(icon, *size) def _render(self, _): scale = np.interp(self._size_filter.x, [CHAR_FONT_SIZE, CHAR_NEAR_FONT_SIZE], [1, 1.5]) @@ -140,9 +145,10 @@ class CapsState(IntEnum): LOCK = 2 -class MiciKeyboardBP(Widget): - def __init__(self, show_special_keys: bool = False): +class MiciKeyboard(Widget): + def __init__(self, auto_return_to_letters: str = ""): super().__init__() + self._auto_return_to_letters = auto_return_to_letters lower_chars = [ "qwertyuiop", @@ -167,8 +173,8 @@ def __init__(self, show_special_keys: bool = False): self._super_special_keys = [[Key(char) for char in row] for row in super_special_chars] # control keys - self._space_key = IconKey("icons_mici/settings/keyboard/space.png", char=" ", vertical_align="bottom") - self._caps_key = IconKey("icons_mici/settings/keyboard/caps_lower.png") + self._space_key = IconKey("icons_mici/settings/keyboard/space.png", char=" ", vertical_align="bottom", icon_size=(43, 14)) + self._caps_key = IconKey("icons_mici/settings/keyboard/caps_lower.png", icon_size=(38, 33)) # these two are in different places on some layouts self._123_key, self._123_key2 = SmallKey("123"), SmallKey("123") self._abc_key = SmallKey("abc") @@ -190,12 +196,7 @@ def __init__(self, show_special_keys: bool = False): # set initial keys self._current_keys: list[list[Key]] = [] - - if show_special_keys: - self._set_keys(self._special_keys) - else: - self._set_keys(self._lower_keys) - + self._set_keys(self._lower_keys) self._caps_state = CapsState.LOWER self._initialized = False @@ -227,6 +228,8 @@ def _set_keys(self, keys: list[list[Key]]): for current_row, row in zip(self._current_keys, keys, strict=False): # not all layouts have the same number of keys for current_key, key in zip_repeat(current_row, row): + # reset parent rect for new keys + key.set_parent_rect(self._rect) current_pos = current_key.get_position() key.set_position(current_pos[0], current_pos[1], smooth=False) @@ -264,7 +267,8 @@ def _get_closest_key(self) -> tuple[Key | None, float]: for key in row: mouse_pos = gui_app.last_mouse_event.pos # approximate distance for comparison is accurate enough - dist = abs(key.original_position.x - mouse_pos.x) + abs(key.original_position.y - mouse_pos.y) + # use local y coords so parent widget offset (e.g. during NavWidget animate-in) doesn't affect hit testing + dist = abs(key.original_position.x - mouse_pos.x) + abs(key.original_position.y - (mouse_pos.y - self._rect.y)) if dist < closest_key[1]: if self._closest_key[0] is None or key is self._closest_key[0] or dist < self._closest_key[1] - KEY_DRAG_HYSTERESIS: closest_key = (key, dist) @@ -274,14 +278,14 @@ def _set_uppercase(self, cycle: bool): self._set_keys(self._upper_keys if cycle else self._lower_keys) if not cycle: self._caps_state = CapsState.LOWER - self._caps_key.set_icon("icons_mici/settings/keyboard/caps_lower.png") + self._caps_key.set_icon("icons_mici/settings/keyboard/caps_lower.png", icon_size=(38, 33)) else: if self._caps_state == CapsState.LOWER: self._caps_state = CapsState.UPPER - self._caps_key.set_icon("icons_mici/settings/keyboard/caps_upper.png") + self._caps_key.set_icon("icons_mici/settings/keyboard/caps_upper.png", icon_size=(38, 33)) elif self._caps_state == CapsState.UPPER: self._caps_state = CapsState.LOCK - self._caps_key.set_icon("icons_mici/settings/keyboard/caps_lock.png") + self._caps_key.set_icon("icons_mici/settings/keyboard/caps_lock.png", icon_size=(39, 38)) else: self._set_uppercase(False) @@ -302,6 +306,10 @@ def _handle_mouse_release(self, mouse_pos: MousePos): if self._caps_state == CapsState.UPPER: self._set_uppercase(False) + # Switch back to letters after common URL delimiters + if self._closest_key[0].char in self._auto_return_to_letters and self._current_keys in (self._special_keys, self._super_special_keys): + self._set_uppercase(False) + # ensure minimum selected animation time key_selected_dt = rl.get_time() - (self._selected_key_t or 0) cur_t = rl.get_time() @@ -319,7 +327,7 @@ def _update_state(self): self._selected_key_filter.update(self._closest_key[0] is not None) # unselect key after animation plays - if self._unselect_key_t is not None and rl.get_time() > self._unselect_key_t: + if (self._unselect_key_t is not None and rl.get_time() > self._unselect_key_t) or not self.enabled: self._closest_key = (None, float('inf')) self._unselect_key_t = None self._selected_key_t = None @@ -345,7 +353,7 @@ def _lay_out_keys(self, bg_x, bg_y, keys: list[list[Key]]): # draw black circle behind selected key circle_alpha = int(self._selected_key_filter.x * 225) - rl.draw_circle_gradient(rl.Vector2(int(key_x + key.rect.width / 2), int(key_y + key.rect.height / 2)), + rl.draw_circle_gradient(rl.Vector2(key_x + key.rect.width / 2, key_y + key.rect.height / 2), SELECTED_CHAR_FONT_SIZE, rl.Color(0, 0, 0, circle_alpha), rl.BLANK) else: # move other keys away from selected key a bit @@ -370,6 +378,7 @@ def _lay_out_keys(self, bg_x, bg_y, keys: list[list[Key]]): key.set_font_size(font_size) # TODO: I like the push amount, so we should clip the pos inside the keyboard rect + key.set_parent_rect(self._rect) key.set_position(key_x, key_y) def _render(self, _): From fe3322d801b7986c898b8badae1edba7e65a2891 Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Thu, 11 Jun 2026 18:35:36 -0400 Subject: [PATCH 04/13] Use stock keyboard, but called `MiciKeyboardBP` --- selfdrive/ui/bp/mici/widgets/keyboard_bp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/bp/mici/widgets/keyboard_bp.py b/selfdrive/ui/bp/mici/widgets/keyboard_bp.py index bc3041aa3c..293efd19e9 100644 --- a/selfdrive/ui/bp/mici/widgets/keyboard_bp.py +++ b/selfdrive/ui/bp/mici/widgets/keyboard_bp.py @@ -145,7 +145,7 @@ class CapsState(IntEnum): LOCK = 2 -class MiciKeyboard(Widget): +class MiciKeyboardBP(Widget): def __init__(self, auto_return_to_letters: str = ""): super().__init__() self._auto_return_to_letters = auto_return_to_letters From 372e199e174efc0247df3201dc14a707541bcf4a Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Thu, 11 Jun 2026 18:48:10 -0400 Subject: [PATCH 05/13] Hopefully fix dialogs --- .../ui/bp/mici/widgets/big_input_dialog_bp.py | 49 ++++++++++++------- selfdrive/ui/bp/mici/widgets/bp_dialogs.py | 21 ++++++-- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/selfdrive/ui/bp/mici/widgets/big_input_dialog_bp.py b/selfdrive/ui/bp/mici/widgets/big_input_dialog_bp.py index 2e89ddddb4..fdb226ec53 100644 --- a/selfdrive/ui/bp/mici/widgets/big_input_dialog_bp.py +++ b/selfdrive/ui/bp/mici/widgets/big_input_dialog_bp.py @@ -41,6 +41,8 @@ def __init__(self, # rects for top buttons self._top_left_button_rect = rl.Rectangle(0, 0, 0, 0) self._top_right_button_rect = rl.Rectangle(0, 0, 0, 0) + self._enter_pressed = False + self._backspace_pressed = False self._ret = None # Only set to CONFIRM when user taps enter; prevents AttributeError in _render before first confirm def confirm_callback_wrapper(): @@ -53,7 +55,7 @@ def _update_state(self): super()._update_state() last_mouse_event = gui_app.last_mouse_event - if last_mouse_event.left_down and rl.check_collision_point_rec(last_mouse_event.pos, self._top_right_button_rect) and self._backspace_img_alpha.x > 1: + if last_mouse_event.left_down and self._backspace_pressed and rl.check_collision_point_rec(last_mouse_event.pos, self._top_right_button_rect): if self._backspace_held_time is None: self._backspace_held_time = rl.get_time() @@ -116,26 +118,33 @@ def _render(self, _): # draw backspace icon with nice fade self._backspace_img_alpha.update(255 * bool(text)) + backspace_x = int(self._rect.width - self._backspace_img.width - 27) + backspace_y = int(text_field_rect.y) if self._backspace_img_alpha.x > 1: color = rl.Color(255, 255, 255, int(self._backspace_img_alpha.x)) - rl.draw_texture(self._backspace_img, int(self._rect.width - self._backspace_img.width - 27), int(text_field_rect.y), color) + rl.draw_texture(self._backspace_img, backspace_x, backspace_y, color) if not text and self._hint_label.text and not candidate_char: # draw description if no text entered yet and not drawing candidate char self._hint_label.render(text_field_rect) - # TODO: move to update state - # make rect take up entire area so it's easier to click - self._top_left_button_rect = rl.Rectangle(self._rect.x, self._rect.y, text_field_rect.x, self._rect.height - self._keyboard.get_keyboard_height()) - self._top_right_button_rect = rl.Rectangle(text_field_rect.x + text_field_rect.width, self._rect.y, - self._rect.width - (text_field_rect.x + text_field_rect.width), self._top_left_button_rect.height) - # draw enter button (enabled + disabled states, same as stock BigInputDialog) self._enter_img_alpha.update(255 if (len(text) >= self._minimum_length) else 0) + enter_x = int(self._rect.x + 15) + enter_y = int(text_field_rect.y) color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x)) - rl.draw_texture(self._enter_img, int(self._rect.x + 15), int(text_field_rect.y), color) + rl.draw_texture(self._enter_img, enter_x, enter_y, color) color = rl.Color(255, 255, 255, 255 - int(self._enter_img_alpha.x)) - rl.draw_texture(self._enter_disabled_img, int(self._rect.x + 15), int(text_field_rect.y), color) + rl.draw_texture(self._enter_disabled_img, enter_x, enter_y, color) + + # Use the visible icon bounds with a small padding instead of full header halves. + hit_pad = 14 + self._top_left_button_rect = rl.Rectangle( + enter_x - hit_pad, enter_y - hit_pad, + self._enter_img.width + hit_pad * 2, self._enter_img.height + hit_pad * 2) + self._top_right_button_rect = rl.Rectangle( + backspace_x - hit_pad, backspace_y - hit_pad, + self._backspace_img.width + hit_pad * 2, self._backspace_img.height + hit_pad * 2) # keyboard goes over everything self._keyboard.render(self._rect) @@ -151,11 +160,15 @@ def _render(self, _): def _handle_mouse_press(self, mouse_pos: MousePos): super()._handle_mouse_press(mouse_pos) - # TODO: need to track where press was so enter and back can activate on release rather than press - # or turn into icon widgets :eyes_open: - # handle backspace icon click - if rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect) and self._backspace_img_alpha.x > 254: - self._keyboard.backspace() - elif rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect) and self._enter_img_alpha.x > 254: - # handle enter icon click - self._confirm_callback() \ No newline at end of file + self._backspace_pressed = rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect) + self._enter_pressed = rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect) + + def _handle_mouse_release(self, mouse_pos: MousePos): + if self._backspace_pressed and rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect) and self._keyboard.text(): + if self._backspace_held_time is None: + self._keyboard.backspace() + self._backspace_held_time = None + elif self._enter_pressed and rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect) and len(self._keyboard.text()) >= self._minimum_length: + self._confirm_callback() + self._enter_pressed = False + self._backspace_pressed = False diff --git a/selfdrive/ui/bp/mici/widgets/bp_dialogs.py b/selfdrive/ui/bp/mici/widgets/bp_dialogs.py index 846ed5a132..2a3a58701d 100644 --- a/selfdrive/ui/bp/mici/widgets/bp_dialogs.py +++ b/selfdrive/ui/bp/mici/widgets/bp_dialogs.py @@ -166,6 +166,8 @@ def __init__(self, self._top_left_button_rect = rl.Rectangle(0, 0, 0, 0) self._top_right_button_rect = rl.Rectangle(0, 0, 0, 0) + self._enter_pressed = False + self._backspace_pressed = False def _do_confirm(): text = self._keyboard.text() @@ -173,33 +175,42 @@ def _do_confirm(): self._do_confirm = _do_confirm # ---- input ---- + def _handle_mouse_press(self, mouse_pos: MousePos): + super()._handle_mouse_press(mouse_pos) + self._backspace_pressed = rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect) + self._enter_pressed = rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect) + def _handle_mouse_release(self, mouse_pos: MousePos): - if rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect): + if self._backspace_pressed and rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect): # Backspace if self._backspace_held_time is None: self._keyboard.backspace() self._backspace_held_time = None - return - if rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect): + elif self._enter_pressed and rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect): # Enter if len(self._keyboard.text()) >= self._minimum_length: self._do_confirm() - return + self._enter_pressed = False + self._backspace_pressed = False def _update_state(self): super()._update_state() if self.is_dismissing: + self._enter_pressed = False + self._backspace_pressed = False self._backspace_held_time = None return # Held backspace repeat (mirrors stock BigInputDialog behavior) last = gui_app.last_mouse_event - if last.left_down and rl.check_collision_point_rec(last.pos, self._top_right_button_rect): + if last.left_down and self._backspace_pressed and rl.check_collision_point_rec(last.pos, self._top_right_button_rect): if self._backspace_held_time is None: self._backspace_held_time = rl.get_time() if rl.get_time() - self._backspace_held_time > 0.5: if gui_app.frame % round(gui_app.target_fps / self.BACKSPACE_RATE) == 0: self._keyboard.backspace() + else: + self._backspace_held_time = None def _render(self, _): r = self._rect From 1cdc88a213f7986e499eeac1459473e5e22a9fa2 Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Thu, 11 Jun 2026 19:25:07 -0400 Subject: [PATCH 06/13] Hopefully fix dialogs take 2 --- selfdrive/ui/bp/mici/widgets/big_input_dialog_bp.py | 8 +++++++- selfdrive/ui/bp/mici/widgets/bp_dialogs.py | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/bp/mici/widgets/big_input_dialog_bp.py b/selfdrive/ui/bp/mici/widgets/big_input_dialog_bp.py index fdb226ec53..d52b70c58f 100644 --- a/selfdrive/ui/bp/mici/widgets/big_input_dialog_bp.py +++ b/selfdrive/ui/bp/mici/widgets/big_input_dialog_bp.py @@ -30,6 +30,7 @@ def __init__(self, self._minimum_length = minimum_length self._backspace_held_time: float | None = None + self._backspace_repeated = False self._backspace_img = gui_app.texture("icons_mici/settings/keyboard/backspace.png", 42, 36) self._backspace_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) @@ -62,9 +63,11 @@ def _update_state(self): if rl.get_time() - self._backspace_held_time > 0.5: if gui_app.frame % round(gui_app.target_fps / self.BACKSPACE_RATE) == 0: self._keyboard.backspace() + self._backspace_repeated = True else: self._backspace_held_time = None + self._backspace_repeated = False def _render(self, _): text_input_size = 35 @@ -162,12 +165,15 @@ def _handle_mouse_press(self, mouse_pos: MousePos): super()._handle_mouse_press(mouse_pos) self._backspace_pressed = rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect) self._enter_pressed = rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect) + if self._backspace_pressed: + self._backspace_repeated = False def _handle_mouse_release(self, mouse_pos: MousePos): if self._backspace_pressed and rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect) and self._keyboard.text(): - if self._backspace_held_time is None: + if not self._backspace_repeated: self._keyboard.backspace() self._backspace_held_time = None + self._backspace_repeated = False elif self._enter_pressed and rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect) and len(self._keyboard.text()) >= self._minimum_length: self._confirm_callback() self._enter_pressed = False diff --git a/selfdrive/ui/bp/mici/widgets/bp_dialogs.py b/selfdrive/ui/bp/mici/widgets/bp_dialogs.py index 2a3a58701d..43ef87c85e 100644 --- a/selfdrive/ui/bp/mici/widgets/bp_dialogs.py +++ b/selfdrive/ui/bp/mici/widgets/bp_dialogs.py @@ -160,6 +160,7 @@ def __init__(self, text_color=P.TEXT, max_width=520, wrap_text=False) self._backspace_held_time: float | None = None + self._backspace_repeated = False self._backspace_img = gui_app.texture("icons_mici/settings/keyboard/backspace.png", 36, 30) self._enter_img = gui_app.texture("icons_mici/settings/keyboard/enter.png", 56, 46) self._enter_disabled_img = gui_app.texture("icons_mici/settings/keyboard/enter_disabled.png", 56, 46) @@ -179,13 +180,16 @@ def _handle_mouse_press(self, mouse_pos: MousePos): super()._handle_mouse_press(mouse_pos) self._backspace_pressed = rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect) self._enter_pressed = rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect) + if self._backspace_pressed: + self._backspace_repeated = False def _handle_mouse_release(self, mouse_pos: MousePos): if self._backspace_pressed and rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect): # Backspace - if self._backspace_held_time is None: + if not self._backspace_repeated: self._keyboard.backspace() self._backspace_held_time = None + self._backspace_repeated = False elif self._enter_pressed and rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect): # Enter if len(self._keyboard.text()) >= self._minimum_length: @@ -209,8 +213,10 @@ def _update_state(self): if rl.get_time() - self._backspace_held_time > 0.5: if gui_app.frame % round(gui_app.target_fps / self.BACKSPACE_RATE) == 0: self._keyboard.backspace() + self._backspace_repeated = True else: self._backspace_held_time = None + self._backspace_repeated = False def _render(self, _): r = self._rect From 129edb844933c8221f1b3f888760b78d7c207fc7 Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Fri, 12 Jun 2026 06:04:21 -0400 Subject: [PATCH 07/13] Add sunnylink settings ui src for generated `settings_ui.json` --- .../settings_ui_src/pages/bluepilot.yaml | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 sunnypilot/sunnylink/settings_ui_src/pages/bluepilot.yaml diff --git a/sunnypilot/sunnylink/settings_ui_src/pages/bluepilot.yaml b/sunnypilot/sunnylink/settings_ui_src/pages/bluepilot.yaml new file mode 100644 index 0000000000..621e0cd823 --- /dev/null +++ b/sunnypilot/sunnylink/settings_ui_src/pages/bluepilot.yaml @@ -0,0 +1,245 @@ +# Page: bluepilot +# Edit this file. Run compile_settings_ui.py to emit settings_ui.json. +id: bluepilot +label: BluePilot +icon: toggles +order: 8 +remote_configurable: true +description: 'BluePilot: Ford-specific features, gauges, and lateral/longitudinal tuning.' +sections: +- id: bp_system + title: System + description: '' + items: + - key: EnableWebRoutesServer + widget: toggle + title: Enable Web Routes Server + description: Run the BluePilot web portal to view routes and videos over WiFi. + - key: BPUIDebugLog + widget: toggle + title: UI Debug Logging + description: Log UI state transitions for diagnosing rendering issues on device. +- id: bp_vehicle + title: Vehicle + description: '' + items: + - key: send_hands_free_cluster_msg + widget: toggle + title: Show BlueCruise UI on Cluster + description: Display BlueCruise UI on the cluster for supported vehicles. + - key: vbatt_pause_charging + widget: option + title: 12V Battery Limit + description: Set the 12V battery charging pause limit (11.0-14.0V). + min: 11.0 + max: 14.0 + step: 0.1 + unit: V +- id: bp_visuals + title: Visuals + description: '' + items: + - key: BPHideOnroadBorder + widget: toggle + title: Hide Onroad Border (C3X) + description: Hide the colored status border around the driving view. + - key: mici_hide_onroad_border + widget: toggle + title: Hide Screen Border (C4) + description: Hide the colored status border around the driving view (comma 4). + - key: mici_hide_onroad_fade + widget: toggle + title: Hide Onroad Fade (C4) + description: Hide the onroad fade overlay (comma 4). + - key: BPDisableLaneLineStatusColor + widget: toggle + title: Disable Lane Line Status Color + description: Keep lane lines grey instead of changing to green when engaged. + - key: ShowBlindspotOverlay + widget: toggle + title: Show Blindspot Overlay + description: Display red overlay when a vehicle is detected in the blindspot. + - key: ShowBrakeStatus + widget: toggle + title: Show Brake Status + description: Display the speed setpoint in red when the vehicle is braking. + - key: BPShowConfidenceBall + widget: toggle + title: Show Confidence Ball (C3X) + description: Display the confidence ball on the left side of the driving view. + - key: BPAnimateSteeringWheel + widget: toggle + title: Animate Steering Wheel + description: Rotate the steering wheel icon to match the current steering angle. + - key: mici_complication + widget: multiple_button + title: Lower Right Display (C4) + description: Choose what is shown in the lower-right corner of the driving view (comma 4). + options: + - value: 0 + label: 'Off' + - value: 1 + label: Lead Car Speed + - value: 2 + label: Speed + - value: 3 + label: Lead Car Distance + - value: 4 + label: Time to Lead Car + - key: FordPrefShowRadarLeadOverlay + widget: toggle + title: Show Radar Lead Overlay (Ford ACC) + description: Display a chevron with lead vehicle info when using Ford stock ACC. + - key: FordPrefRadarOverlaySize + widget: multiple_button + title: Radar Overlay Size + description: Set the size of the radar lead overlay chevron and info boxes. + options: + - value: 0 + label: Small + - value: 1 + label: Medium + - value: 2 + label: Large + enablement: + - type: param + key: FordPrefShowRadarLeadOverlay + equals: true + - key: FordPrefHybridBatteryStatus + widget: toggle + title: Show Hybrid/EV Battery Status + description: Display the hybrid battery gauge with SOC, voltage, and amps. + - key: FordPrefHybridPowerFlow + widget: toggle + title: Show Hybrid/EV Power Flow + description: Display the power flow gauge showing throttle demand and regenerative braking. + - key: FordPrefHybridDriveGaugeSize + widget: multiple_button + title: Hybrid/EV Gauge Size (C3X) + description: Set the size of the battery and power flow gauges. + options: + - value: 1 + label: Small + - value: 2 + label: Large + enablement: + - type: param + key: FordPrefHybridPowerFlow + equals: true + - key: FordPrefHybridGaugeStyle + widget: multiple_button + title: Hybrid Gauge Style (C3X) + description: 'Flat: horizontal bar in a shared container. Arched: arch above the torque bar.' + options: + - value: flat + label: Flat + - value: arched + label: Arched + enablement: + - type: param + key: FordPrefHybridPowerFlow + equals: true + - key: FordPrefHybridPowerFlowAlternate + widget: toggle + title: Hybrid/EV Gauge Style (C4) + description: Use the alternate hybrid gauge style (comma 4). +- id: bp_longitudinal + title: Longitudinal Tuning + description: '' + items: + - key: disable_BP_long_UI + widget: toggle + title: Bypass BP Longitudinal Control + description: Use stock longitudinal logic instead of BluePilot TTC/coasting tuning. + - key: disable_downhill_comp_UI + widget: toggle + title: Disable Downhill Compensation + description: Disable pitch-based brake/gas compensation when going downhill. +- id: bp_lateral + title: Lateral Tuning + description: '' + items: + - key: disable_BP_lat_UI + widget: toggle + title: Disable BP Lateral Control + description: Disable BluePilot 4-signal Ford lateral control. + - key: enable_human_turn_detection + widget: toggle + title: Enable Human Turn Detection + description: Detect human-initiated turns and briefly latch out of lateral control. + - key: BlinkerPauseLaneChange + widget: toggle + title: Disable Lane Change Under Speed + description: Pause lateral control when the blinker is on and below the minimum speed. + - key: lane_change_factor_high + widget: option + title: Lane Change Factor High + description: Adjust the high-speed lane change factor (0.5-1.0). + min: 0.5 + max: 1.0 + step: 0.05 + - key: enable_lane_positioning + widget: toggle + title: Enable Lane Positioning + description: Enable custom in-lane positioning controls. + - key: custom_path_offset + widget: option + title: In-Lane Offset + description: Adjust the in-lane offset (-0.5 to 0.5 m). + min: -0.5 + max: 0.5 + step: 0.05 + enablement: + - type: param + key: enable_lane_positioning + equals: true + - key: enable_lane_full_mode + widget: toggle + title: Enable Lanefull Mode + description: Enable lanefull mode for lane positioning. + enablement: + - type: param + key: enable_lane_positioning + equals: true + - key: custom_profile + widget: toggle + title: Use Custom Tuning Profile + description: Enable custom lateral tuning profile settings. + - key: pc_blend_ratio_high_C_UI + widget: option + title: Predicted Curvature Blend Ratio High + description: Adjust the high curvature blend ratio (0.0-1.0). + min: 0.0 + max: 1.0 + step: 0.05 + enablement: + - type: param + key: custom_profile + equals: true + - key: pc_blend_ratio_low_C_UI + widget: option + title: Predicted Curvature Blend Ratio Low + description: Adjust the low curvature blend ratio (0.0-1.0). + min: 0.0 + max: 1.0 + step: 0.05 + enablement: + - type: param + key: custom_profile + equals: true + - key: LC_PID_gain_UI + widget: option + title: Low Curvature PID Gain + description: Adjust the low curvature PID gain (0.0-5.0). + min: 0.0 + max: 5.0 + step: 0.1 + enablement: + - type: all + conditions: + - type: param + key: enable_lane_positioning + equals: true + - type: param + key: custom_profile + equals: true From b1e6e9f91b57b06565078375568c2f796bd29543 Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Fri, 12 Jun 2026 06:05:35 -0400 Subject: [PATCH 08/13] Disable BP customizations for settings on mici --- selfdrive/ui/mici/layouts/main.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index 014ee5d1f6..316196e71d 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -3,13 +3,13 @@ from openpilot.common.bluepilot import is_bluepilot from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout # BluePilot: replace home with paged-design home (mockups/mici_home.html) -if is_bluepilot(): - from openpilot.selfdrive.ui.bp.mici.layouts.home_bp import MiciHomeLayoutBP as MiciHomeLayout +# if is_bluepilot(): +# from openpilot.selfdrive.ui.bp.mici.layouts.home_bp import MiciHomeLayoutBP as MiciHomeLayout from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts # BluePilot: paged-carousel offroad alerts on radial backdrop -if is_bluepilot(): - from openpilot.selfdrive.ui.bp.mici.layouts.offroad_alerts_bp import MiciOffroadAlertsBP as MiciOffroadAlerts +# if is_bluepilot(): +# from openpilot.selfdrive.ui.bp.mici.layouts.offroad_alerts_bp import MiciOffroadAlertsBP as MiciOffroadAlerts from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView # BluePilot: override onroad view with blindspot, complication, brake coloring, powerflow if is_bluepilot(): @@ -25,8 +25,8 @@ from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout # BluePilot: BP settings landing wins over the SP variant. Keep this AFTER the # sunnypilot block so the rebind sticks. -if is_bluepilot(): - from openpilot.selfdrive.ui.bp.mici.layouts.settings_bp import SettingsLayoutBP as SettingsLayout +# if is_bluepilot(): +# from openpilot.selfdrive.ui.bp.mici.layouts.settings_bp import SettingsLayoutBP as SettingsLayout ONROAD_DELAY = 2.5 # seconds From 6ecaa7efa58d328b2d70e8c50e22729115f300bb Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Fri, 12 Jun 2026 06:39:14 -0400 Subject: [PATCH 09/13] Add BluePilot and Vehicle buttons to stock settings. --- selfdrive/ui/mici/layouts/main.py | 7 - .../ui/mici/layouts/settings/bluepilot.py | 403 ++++++++++++++++++ .../ui/mici/layouts/settings/settings.py | 25 +- selfdrive/ui/mici/layouts/settings/vehicle.py | 141 ++++++ 4 files changed, 567 insertions(+), 9 deletions(-) create mode 100644 selfdrive/ui/mici/layouts/settings/bluepilot.py create mode 100644 selfdrive/ui/mici/layouts/settings/vehicle.py diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index 316196e71d..3b76e32204 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -21,13 +21,6 @@ from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.lib.application import gui_app -if gui_app.sunnypilot_ui(): - from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout -# BluePilot: BP settings landing wins over the SP variant. Keep this AFTER the -# sunnypilot block so the rebind sticks. -# if is_bluepilot(): -# from openpilot.selfdrive.ui.bp.mici.layouts.settings_bp import SettingsLayoutBP as SettingsLayout - ONROAD_DELAY = 2.5 # seconds diff --git a/selfdrive/ui/mici/layouts/settings/bluepilot.py b/selfdrive/ui/mici/layouts/settings/bluepilot.py new file mode 100644 index 0000000000..5fce4e98a1 --- /dev/null +++ b/selfdrive/ui/mici/layouts/settings/bluepilot.py @@ -0,0 +1,403 @@ +from __future__ import annotations + +import pyray as rl + +from collections.abc import Callable + +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiParamToggle, BigMultiToggle, BigParamControl +from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog +from openpilot.selfdrive.ui.bp.mici.widgets.web_server_qr_dialog import WebServerQRDialog +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app, MousePos +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.lib.wifi_manager import WifiManager, Network +from openpilot.system.ui.widgets.scroller import NavScroller + +CONTENT_MARGIN = 20 +LINE_L = 40 +LINE_W = 8 + + +class BigBoolParamToggle(BigMultiToggle): + def __init__(self, text: str, param: str, options: list[str]): + super().__init__(text, options) + self._param = param + self._params = Params() + self._load_value() + + def _load_value(self): + self.set_value(self._options[1] if self._params.get_bool(self._param) else self._options[0]) + + def _handle_mouse_release(self, mouse_pos: MousePos): + super()._handle_mouse_release(mouse_pos) + self._params.put_bool(self._param, self.value == self._options[1], block=True) + + +class BigNumericParamControl(BigButton): + def __init__(self, text: str, param: str, *, min_value: float | int | None = None, + max_value: float | int | None = None, step: float | int = 1, + formatter: Callable[[float | int], str], parser: Callable[[str], float | int], + default_value: float | int = 0, is_active: Callable[[], bool] | None = None): + super().__init__(text, "") + self._param = param + self._params = Params() + self._min_value = min_value + self._max_value = max_value + self._step = step + self._formatter = formatter + self._parser = parser + self._default_value = default_value + self._active_fn = is_active + + self._sub_label.set_font_size(22) + self._margin = self._rect.width * 0.1 + self._hit_rect_size = LINE_L + 2 * CONTENT_MARGIN + self.minus_hit_rect = rl.Rectangle(0, 0, 0, 0) + self.plus_hit_rect = rl.Rectangle(0, 0, 0, 0) + + self.set_click_callback(self._on_click) + self._refresh_value() + + def _is_active(self) -> bool: + return self._active_fn() if self._active_fn is not None else True + + def _clamp(self, value: float | int) -> float | int: + if self._min_value is not None and value < self._min_value: + value = self._min_value + if self._max_value is not None and value > self._max_value: + value = self._max_value + return value + + def _get_param(self) -> float | int: + raw = self._params.get(self._param, return_default=True) + if raw in (None, b"", ""): + return self._default_value + try: + return self._parser(raw.decode("utf-8") if isinstance(raw, bytes) else str(raw)) + except (TypeError, ValueError): + return self._default_value + + def _set_param(self, value: float | int): + value = self._clamp(value) + self._params.put(self._param, value) + self.set_value(self._formatter(value)) + + def _refresh_value(self): + self.set_value(self._formatter(self._get_param())) + + def _on_click(self): + if not self._is_active(): + return + + hint = f"({self._min_value}-{self._max_value})" if self._min_value is not None or self._max_value is not None else tr("enter a value...") + + def confirm_callback(text: str): + if text: + try: + self._set_param(self._parser(text)) + except ValueError: + pass + else: + self._params.remove(self._param) + self._refresh_value() + + gui_app.push_widget(BigInputDialog(hint, str(self._get_param()), minimum_length=0, confirm_callback=confirm_callback)) + + def _get_label_font_size(self): + return super()._get_label_font_size() - 10 + + def _draw_content(self, btn_y: float): + offset = self._hit_rect_size / 3 + self.rect.height -= offset + super()._draw_content(btn_y + offset) + self.rect.height += offset + + def _render(self, _): + super()._render(_) + + left = self._rect.x + self._margin + right = self._rect.x + self._rect.width - self._margin + top = self._rect.y + self._margin + color = rl.WHITE if self.enabled and self._is_active() else rl.Color(255, 255, 255, int(255 * 0.35)) + + self.minus_hit_rect = rl.Rectangle(left - CONTENT_MARGIN, top - self._hit_rect_size / 2, self._hit_rect_size, self._hit_rect_size) + self.plus_hit_rect = rl.Rectangle(right - self._hit_rect_size / 2 - CONTENT_MARGIN, top - self._hit_rect_size / 2, self._hit_rect_size, self._hit_rect_size) + + rl.draw_line_ex((left, top), (left + LINE_L, top), LINE_W, color) + rl.draw_line_ex((right - LINE_L, top), (right, top), LINE_W, color) + mid = right - LINE_L / 2 + rl.draw_line_ex((mid, top - LINE_L / 2), (mid, top + LINE_L / 2), LINE_W, color) + + def _handle_mouse_release(self, mouse_pos: MousePos): + if not self._is_active(): + return + if rl.check_collision_point_rec(mouse_pos, self.minus_hit_rect): + self._set_param(self._get_param() - self._step) + return + if rl.check_collision_point_rec(mouse_pos, self.plus_hit_rect): + self._set_param(self._get_param() + self._step) + return + super()._handle_mouse_release(mouse_pos) + + +class BigFloatParamControl(BigNumericParamControl): + def __init__(self, text: str, param: str, *, min_value: float | None = None, + max_value: float | None = None, step: float = 0.05, + default_value: float = 0.0, is_active: Callable[[], bool] | None = None): + super().__init__(text, param, min_value=min_value, max_value=max_value, step=step, + formatter=lambda value: f"{round(float(value), 4)}", + parser=float, default_value=default_value, is_active=is_active) + + +class BigIntParamControl(BigNumericParamControl): + def __init__(self, text: str, param: str, *, min_value: int | None = None, + max_value: int | None = None, step: int = 1, default_value: int = 0, + is_active: Callable[[], bool] | None = None): + super().__init__(text, param, min_value=min_value, max_value=max_value, step=step, + formatter=lambda value: str(int(value)), + parser=lambda raw: int(float(raw)), + default_value=default_value, is_active=is_active) + + def _set_param(self, value: float | int): + step_value = int(value) + step_value -= step_value % int(self._step) + super()._set_param(step_value) + + +class PreferredNetworkLayoutMici(NavScroller): + def __init__(self, wifi_manager: WifiManager, saved_networks: list[Network], on_select: Callable[[], None]): + super().__init__() + self._params = Params() + self._wifi_manager = wifi_manager + self._saved_networks = saved_networks + self._on_select = on_select + self._build_buttons() + + def _build_buttons(self): + items = [self._make_button(tr("none"), "", lambda: self._select(""))] + for network in self._saved_networks: + label = network.ssid.strip() or tr("hidden network") + items.append(self._make_button(label, "", lambda ssid=network.ssid: self._select(ssid))) + + self._scroller._items.clear() + for item in items: + self._scroller.add_widget(item) + + @staticmethod + def _make_button(text: str, value: str, callback: Callable[[], None]) -> BigButton: + button = BigButton(text, value, gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 76, 56), scroll=True) + button.set_click_callback(callback) + return button + + def _select(self, ssid: str): + self._params.put("WifiFavoriteSSID", ssid) + if ssid: + cloudlog.info(f"Set preferred network: {ssid}") + else: + cloudlog.info("Cleared preferred network") + self.dismiss(self._on_select) + + +class BluePilotLayoutMici(NavScroller): + def __init__(self): + super().__init__() + self._params = Params() + self._wifi_manager = WifiManager() + self._wifi_manager.set_active(False) + self._saved_networks: list[Network] = [] + self._wifi_manager.add_callbacks(networks_updated=self._on_network_updated) + + self._preferred_network_btn = BigButton(tr("preferred WiFi network"), "", gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 76, 56), scroll=True) + self._preferred_network_btn.set_click_callback(self._select_preferred_network) + + self._enable_web_routes = BigParamControl("enable web routes server", "EnableWebRoutesServer") + self._show_web_routes_qr = BigButton("show QR code", "", gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 76, 56)) + self._show_web_routes_qr.set_click_callback(self._show_qr_dialog) + self._show_hands_free_ui = BigParamControl("show BlueCruise UI on Cluster", "send_hands_free_cluster_msg") + self._show_lead_vehicle = BigMultiParamToggle("Lower Right Display", "mici_complication", ["off", "lead car speed", "speed", "lead car distance", "time to lead car"]) + self._show_brake_status = BigParamControl("show brake status", "ShowBrakeStatus") + self._show_blindspot_ui = BigParamControl("show blindspot overlay", "ShowBlindspotOverlay") + self._show_hybrid_power_flow = BigParamControl("show hybrid/EV power flow", "FordPrefHybridPowerFlow") + self._hybrid_power_flow_style = BigBoolParamToggle("hybrid/EV power flow style", "FordPrefHybridPowerFlowAlternate", ["flat", "round"]) + self._rainbow_mode = BigParamControl("rainbow mode", "RainbowMode") + self._enable_human_turn_detection = BigParamControl("enable human turn detection", "enable_human_turn_detection") + self._lane_change_factor_high = BigFloatParamControl("lane change factor high", "lane_change_factor_high", min_value=0.5, max_value=1.0, step=0.05) + self._disable_lane_change_under_speed = BigParamControl("disable auto lane change under speed", "BlinkerPauseLaneChange") + self._blinker_min_speed = BigIntParamControl("blinker min lane change speed", "BlinkerMinLateralControlSpeed", min_value=5, max_value=50, step=5) + self._enable_lane_positioning = BigParamControl("enable lane positioning", "enable_lane_positioning") + self._custom_path_offset = BigFloatParamControl("in-lane offset", "custom_path_offset", min_value=-0.5, max_value=0.5, step=0.05, + is_active=lambda: self._params.get_bool("enable_lane_positioning")) + self._enable_lane_full_mode = BigParamControl("enable lanefull mode", "enable_lane_full_mode") + self._custom_profile = BigParamControl("use custom tuning profile", "custom_profile") + self._pc_blend_ratio_high = BigFloatParamControl("predicted curvature blend ratio high", "pc_blend_ratio_high_C_UI", min_value=0.0, max_value=1.0, step=0.05, + is_active=lambda: self._params.get_bool("custom_profile")) + self._pc_blend_ratio_low = BigFloatParamControl("predicted curvature blend ratio low", "pc_blend_ratio_low_C_UI", min_value=0.0, max_value=1.0, step=0.05, + is_active=lambda: self._params.get_bool("custom_profile")) + self._lc_pid_gain = BigFloatParamControl("low curvature PID gain", "LC_PID_gain_UI", min_value=0.0, max_value=5.0, step=0.05, + is_active=lambda: self._params.get_bool("enable_lane_positioning") and self._params.get_bool("custom_profile")) + self._animate_steering_wheel = BigParamControl("animate steering wheel", "BPAnimateSteeringWheel") + self._hide_fade = BigParamControl("hide onroad fade", "mici_hide_onroad_fade") + self._hide_border = BigParamControl("hide screen border", "BPHideOnroadBorder") + self._disable_bp_lat = BigParamControl("disable BP lateral control", "disable_BP_lat_UI") + self._disable_bp_long = BigParamControl("bypass BP longitudinal control", "disable_BP_long_UI") + self._disable_downhill_comp = BigParamControl("disable downhill compensation", "disable_downhill_comp_UI") + self._clear_model_cache = BigButton("clear crashed model", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70)) + self._clear_model_cache.set_click_callback(self._clear_model_cache_and_reboot) + self._ui_debug_log = BigParamControl("ui debug logging", "BPUIDebugLog") + self._vbatt_pause_charging = BigFloatParamControl("12V battery limit", "vbatt_pause_charging", min_value=11.0, max_value=14.0, step=0.1) + + self._scroller.add_widgets([ + self._enable_web_routes, + self._show_web_routes_qr, + self._preferred_network_btn, + self._show_hands_free_ui, + self._show_lead_vehicle, + self._show_brake_status, + self._show_blindspot_ui, + self._show_hybrid_power_flow, + self._hybrid_power_flow_style, + self._rainbow_mode, + self._enable_human_turn_detection, + self._lane_change_factor_high, + self._disable_lane_change_under_speed, + self._blinker_min_speed, + self._enable_lane_positioning, + self._custom_path_offset, + self._enable_lane_full_mode, + self._custom_profile, + self._pc_blend_ratio_high, + self._pc_blend_ratio_low, + self._lc_pid_gain, + self._animate_steering_wheel, + self._hide_fade, + self._hide_border, + self._vbatt_pause_charging, + self._disable_bp_lat, + self._disable_bp_long, + self._disable_downhill_comp, + self._clear_model_cache, + self._ui_debug_log, + ]) + + self._refresh_toggles = ( + ("EnableWebRoutesServer", self._enable_web_routes), + ("send_hands_free_cluster_msg", self._show_hands_free_ui), + ("FordPrefHybridPowerFlow", self._show_hybrid_power_flow), + ("ShowBrakeStatus", self._show_brake_status), + ("ShowBlindspotOverlay", self._show_blindspot_ui), + ("RainbowMode", self._rainbow_mode), + ("enable_human_turn_detection", self._enable_human_turn_detection), + ("BlinkerPauseLaneChange", self._disable_lane_change_under_speed), + ("enable_lane_positioning", self._enable_lane_positioning), + ("enable_lane_full_mode", self._enable_lane_full_mode), + ("custom_profile", self._custom_profile), + ("disable_BP_lat_UI", self._disable_bp_lat), + ("disable_BP_long_UI", self._disable_bp_long), + ("disable_downhill_comp_UI", self._disable_downhill_comp), + ("BPAnimateSteeringWheel", self._animate_steering_wheel), + ("BPUIDebugLog", self._ui_debug_log), + ("mici_hide_onroad_fade", self._hide_fade), + ("BPHideOnroadBorder", self._hide_border), + ) + + ui_state.add_offroad_transition_callback(self._update_controls) + + def show_event(self): + super().show_event() + self._wifi_manager.set_active(True) + self._update_controls() + + def hide_event(self): + super().hide_event() + self._wifi_manager.set_active(False) + + def _update_state(self): + super()._update_state() + self._show_lead_vehicle._load_value() + self._hybrid_power_flow_style._load_value() + self._update_numeric_labels() + self._update_buttons() + + def _update_numeric_labels(self): + for control in ( + self._lane_change_factor_high, + self._blinker_min_speed, + self._custom_path_offset, + self._pc_blend_ratio_high, + self._pc_blend_ratio_low, + self._lc_pid_gain, + self._vbatt_pause_charging, + ): + control._refresh_value() + + def _update_controls(self): + ui_state.update_params() + for key, item in self._refresh_toggles: + item.set_checked(ui_state.params.get_bool(key)) + self._update_buttons() + + def _update_buttons(self): + server_enabled = self._params.get_bool("EnableWebRoutesServer") + power_flow_enabled = self._params.get_bool("FordPrefHybridPowerFlow") + lane_positioning_enabled = self._params.get_bool("enable_lane_positioning") + custom_profile_enabled = self._params.get_bool("custom_profile") + + self._show_web_routes_qr.set_enabled(server_enabled) + self._hybrid_power_flow_style.set_enabled(power_flow_enabled) + self._custom_path_offset.set_enabled(lane_positioning_enabled) + self._enable_lane_full_mode.set_enabled(lane_positioning_enabled) + self._pc_blend_ratio_high.set_enabled(custom_profile_enabled) + self._pc_blend_ratio_low.set_enabled(custom_profile_enabled) + self._lc_pid_gain.set_enabled(lane_positioning_enabled and custom_profile_enabled) + self._preferred_network_btn.set_enabled(len(self._saved_networks) > 0) + self._preferred_network_btn.set_value(self._get_preferred_network_display()) + + def _show_qr_dialog(self): + if self._params.get_bool("EnableWebRoutesServer"): + gui_app.push_widget(WebServerQRDialog(back_callback=gui_app.pop_widget)) + + def _clear_model_cache_and_reboot(self): + def confirm_callback(): + try: + self._params.remove("ModelRunnerTypeCache") + except Exception: + pass + try: + self._params.remove("ModelManager_ActiveBundle") + except Exception: + pass + self._params.put_bool("DoReboot", True) + cloudlog.info("BluePilot: cleared model cache and requested reboot") + + gui_app.push_widget( + BigInputDialog( + tr("type REBOOT to clear model cache"), + "", + minimum_length=0, + confirm_callback=lambda text: confirm_callback() if text.strip().upper() == "REBOOT" else None, + ) + ) + + def _on_network_updated(self, networks: list[Network]): + self._saved_networks = [network for network in networks if self._wifi_manager.is_connection_saved(network.ssid)] + self._update_buttons() + + favorite_value = self._params.get("WifiFavoriteSSID") + current_favorite = favorite_value.decode("utf-8", errors="replace").strip("\x00") if isinstance(favorite_value, bytes) else str(favorite_value or "").strip("\x00") + if current_favorite and current_favorite not in self._wifi_manager._connections: + self._params.put("WifiFavoriteSSID", "") + cloudlog.info(f"Cleared preferred network '{current_favorite}' - network no longer saved") + + def _get_preferred_network_display(self) -> str: + raw = self._params.get("WifiFavoriteSSID") + value = raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else str(raw or "") + value = value.strip("\x00").strip() + return value or tr("none") + + def _select_preferred_network(self): + if len(self._saved_networks) == 0: + return + panel = PreferredNetworkLayoutMici(self._wifi_manager, self._saved_networks, on_select=self._update_buttons) + gui_app.push_widget(panel) diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index 4ccc5ba139..4287199174 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -1,4 +1,5 @@ from openpilot.common.params import Params +from openpilot.common.bluepilot import is_bluepilot from openpilot.system.ui.widgets.scroller import NavScroller from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMici @@ -7,6 +8,9 @@ from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout from openpilot.system.ui.lib.application import gui_app, FontWeight +if is_bluepilot(): + from openpilot.selfdrive.ui.mici.layouts.settings.bluepilot import BluePilotLayoutMici + from openpilot.selfdrive.ui.mici.layouts.settings.vehicle import VehicleLayoutMici class SettingsBigButton(BigButton): @@ -39,7 +43,19 @@ def __init__(self): firehose_btn = SettingsBigButton("firehose", "", gui_app.texture("icons_mici/settings/firehose.png", 52, 62)) firehose_btn.set_click_callback(lambda: gui_app.push_widget(firehose_panel)) - self._scroller.add_widgets([ + # BluePilot: add MICI-local Vehicle and BluePilot settings panels without + # depending on the sunnypilot SettingsLayoutSP wrapper. + if is_bluepilot(): + vehicle_panel = VehicleLayoutMici() + vehicle_btn = SettingsBigButton("vehicle", "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png", 70, 70)) + vehicle_btn.set_click_callback(lambda: gui_app.push_widget(vehicle_panel)) + + bluepilot_panel = BluePilotLayoutMici() + bluepilot_btn = SettingsBigButton("bluepilot", "", gui_app.texture("icons_mici/settings/car_icon.png", 70, 70)) + bluepilot_btn.set_click_callback(lambda: gui_app.push_widget(bluepilot_panel)) + # End BluePilot + + items = [ toggles_btn, network_btn, device_btn, @@ -47,6 +63,11 @@ def __init__(self): #BigDialogButton("manual", "", "icons_mici/settings/manual_icon.png", "Check out the mici user\nmanual at comma.ai/setup"), firehose_btn, developer_btn, - ]) + ] + if is_bluepilot(): + items.insert(3, vehicle_btn) + items.insert(4, bluepilot_btn) + + self._scroller.add_widgets(items) self._font_medium = gui_app.font(FontWeight.MEDIUM) diff --git a/selfdrive/ui/mici/layouts/settings/vehicle.py b/selfdrive/ui/mici/layouts/settings/vehicle.py new file mode 100644 index 0000000000..f22e92755c --- /dev/null +++ b/selfdrive/ui/mici/layouts/settings/vehicle.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +import json +import os + +from openpilot.common.basedir import BASEDIR +from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.ui.mici.widgets.button import BigButton +from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.widgets.scroller import NavScroller + +CAR_LIST_JSON = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "car", "car_list.json") +VEHICLE_ICON = "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png" + + +def load_car_platforms() -> dict: + with open(CAR_LIST_JSON) as f: + return json.load(f) + + +def _truncate(text: str, max_len: int = 36) -> str: + text = text.strip() + return text if len(text) <= max_len else text[:max_len - 3] + "..." + + +def _current_platform_name() -> str: + bundle = ui_state.params.get("CarPlatformBundle") + if not bundle: + return "" + name = bundle.get("name", "") if isinstance(bundle, dict) else "" + if isinstance(name, bytes): + name = name.decode("utf-8", errors="replace") + return str(name).strip() + + +def _vehicle_status() -> tuple[str, str]: + if bundle := ui_state.params.get("CarPlatformBundle"): + name = bundle.get("name", "") if isinstance(bundle, dict) else "" + if isinstance(name, bytes): + name = name.decode("utf-8", errors="replace") + return tr("manual selection"), _truncate(str(name)) + if ui_state.CP is not None and ui_state.CP.carFingerprint != "MOCK": + return tr("auto fingerprint"), _truncate(str(ui_state.CP.carFingerprint)) + return tr("vehicle"), tr("unrecognized") + + +class VehicleModelLayoutMici(NavScroller): + def __init__(self, platforms: dict, platform_names: list[str]): + super().__init__() + self._platforms = platforms + + items = [] + for platform_name in platform_names: + button = BigButton(platform_name, "", gui_app.texture(VEHICLE_ICON, 70, 70), scroll=True) + button.set_click_callback(lambda name=platform_name: self._confirm_selection(name)) + items.append(button) + + self._scroller.add_widgets(items) + + def _confirm_selection(self, platform_name: str): + title = tr("slide to\napply now") if ui_state.is_offroad() else tr("slide to\napply when offroad") + + def confirm_callback(): + data = self._platforms.get(platform_name) + if not data: + cloudlog.error(f"Missing car_list entry for {platform_name}") + return + ui_state.params.put("CarPlatformBundle", {**data, "name": platform_name}) + cloudlog.info(f"MICI vehicle: set CarPlatformBundle to {platform_name}") + gui_app.pop_widget() + gui_app.pop_widget() + + gui_app.push_widget(BigConfirmationDialog(title, gui_app.texture("icons_mici/settings/car_icon.png", 64, 64), confirm_callback)) + + +class VehicleMakeLayoutMici(NavScroller): + def __init__(self, platforms: dict): + super().__init__() + self._platforms = platforms + + makes = sorted({data.get("make") for data in platforms.values() if data.get("make")}) + items = [] + for make in makes: + button = BigButton(make, "", gui_app.texture(VEHICLE_ICON, 70, 70), scroll=True) + button.set_click_callback(lambda make_name=make: self._open_models(make_name)) + items.append(button) + + self._scroller.add_widgets(items) + + def _open_models(self, make: str): + platform_names = sorted(name for name, data in self._platforms.items() if data.get("make") == make) + if platform_names: + gui_app.push_widget(VehicleModelLayoutMici(self._platforms, platform_names)) + + +class VehicleLayoutMici(NavScroller): + def __init__(self): + super().__init__() + try: + self._platforms = load_car_platforms() + except OSError as e: + self._platforms = {} + cloudlog.error(f"MICI vehicle: could not load {CAR_LIST_JSON}: {e}") + + self._btn_current = BigButton(tr("current vehicle"), "", gui_app.texture(VEHICLE_ICON, 70, 70), scroll=True) + self._btn_clear = BigButton(tr("clear vehicle"), "", gui_app.texture(VEHICLE_ICON, 70, 70)) + self._btn_select = BigButton(tr("select vehicle"), "", gui_app.texture(VEHICLE_ICON, 70, 70)) + + self._btn_current.set_enabled(False) + self._btn_clear.set_click_callback(self._on_clear) + self._btn_select.set_click_callback(self._on_select) + + self._scroller.add_widgets([self._btn_current, self._btn_clear, self._btn_select]) + + def show_event(self): + super().show_event() + ui_state.update_params() + self._refresh_display() + + def _update_state(self): + super()._update_state() + self._refresh_display() + + def _refresh_display(self): + title, value = _vehicle_status() + self._btn_current.set_text(title) + self._btn_current.set_value(value) + self._btn_clear.set_enabled(bool(ui_state.params.get("CarPlatformBundle"))) + self._btn_select.set_enabled(len(self._platforms) > 0) + + def _on_clear(self): + if ui_state.params.get("CarPlatformBundle"): + ui_state.params.remove("CarPlatformBundle") + self._refresh_display() + + def _on_select(self): + if self._platforms: + gui_app.push_widget(VehicleMakeLayoutMici(self._platforms)) From c166e29f8285b1308b2c1ad63060360cab26265d Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Fri, 12 Jun 2026 06:47:10 -0400 Subject: [PATCH 10/13] Fix preferred wifi network --- selfdrive/ui/mici/layouts/settings/bluepilot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/ui/mici/layouts/settings/bluepilot.py b/selfdrive/ui/mici/layouts/settings/bluepilot.py index 5fce4e98a1..1d1a72be85 100644 --- a/selfdrive/ui/mici/layouts/settings/bluepilot.py +++ b/selfdrive/ui/mici/layouts/settings/bluepilot.py @@ -315,6 +315,7 @@ def hide_event(self): def _update_state(self): super()._update_state() + self._wifi_manager.process_callbacks() self._show_lead_vehicle._load_value() self._hybrid_power_flow_style._load_value() self._update_numeric_labels() From 57e94a79776ffce06c87751618a4f33002ba505c Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Fri, 12 Jun 2026 06:58:11 -0400 Subject: [PATCH 11/13] Remove unused code --- selfdrive/ui/bp/mici/layouts/bluepilot_bp.py | 155 ----- selfdrive/ui/bp/mici/layouts/home_bp.py | 222 ------ .../ui/bp/mici/layouts/offroad_alerts_bp.py | 184 ----- .../ui/bp/mici/layouts/panels/developer_bp.py | 76 -- .../ui/bp/mici/layouts/panels/device_bp.py | 112 --- .../ui/bp/mici/layouts/panels/firehose_bp.py | 42 -- .../ui/bp/mici/layouts/panels/network_bp.py | 106 --- .../ui/bp/mici/layouts/panels/toggles_bp.py | 56 -- .../ui/bp/mici/layouts/settings/bluepilot.py | 316 --------- .../bp/mici/layouts/settings/vehicle_mici.py | 103 --- selfdrive/ui/bp/mici/layouts/settings_bp.py | 92 --- selfdrive/ui/bp/mici/layouts/vehicle_bp.py | 62 -- selfdrive/ui/bp/mici/layouts/wifi_ui_bp.py | 126 ---- .../ui/bp/mici/widgets/aurora_wordmark.py | 75 -- selfdrive/ui/bp/mici/widgets/bg_radial.py | 51 -- .../ui/bp/mici/widgets/big_input_dialog_bp.py | 180 ----- selfdrive/ui/bp/mici/widgets/bp_dialogs.py | 271 -------- selfdrive/ui/bp/mici/widgets/bp_palette.py | 50 -- selfdrive/ui/bp/mici/widgets/bp_topbar.py | 103 --- selfdrive/ui/bp/mici/widgets/button_bp.py | 149 ---- selfdrive/ui/bp/mici/widgets/cards.py | 650 ------------------ selfdrive/ui/bp/mici/widgets/floatbutton.py | 242 ------- selfdrive/ui/bp/mici/widgets/icons.py | 171 ----- selfdrive/ui/bp/mici/widgets/keyboard_bp.py | 405 ----------- .../ui/bp/mici/widgets/long_press_progress.py | 30 - selfdrive/ui/bp/mici/widgets/page_dots.py | 62 -- .../ui/bp/mici/widgets/paged_scroller.py | 81 --- .../mici/widgets/preferred_network_select.py | 84 --- selfdrive/ui/bp/mici/widgets/select_panel.py | 65 -- selfdrive/ui/bp/mici/widgets/state_pill.py | 77 --- selfdrive/ui/bp/mici/widgets/sub_panel.py | 79 --- .../ui/bp/mici/widgets/vehicle_select_mici.py | 161 ----- .../mici/widgets/web_server_qr_dialog_bp.py | 130 ---- .../ui/mici/layouts/settings/bluepilot.py | 2 +- .../layouts/settings}/web_server_qr_dialog.py | 97 +-- .../ui/sunnypilot/mici/layouts/settings.py | 19 - 36 files changed, 31 insertions(+), 4855 deletions(-) delete mode 100644 selfdrive/ui/bp/mici/layouts/bluepilot_bp.py delete mode 100644 selfdrive/ui/bp/mici/layouts/home_bp.py delete mode 100644 selfdrive/ui/bp/mici/layouts/offroad_alerts_bp.py delete mode 100644 selfdrive/ui/bp/mici/layouts/panels/developer_bp.py delete mode 100644 selfdrive/ui/bp/mici/layouts/panels/device_bp.py delete mode 100644 selfdrive/ui/bp/mici/layouts/panels/firehose_bp.py delete mode 100644 selfdrive/ui/bp/mici/layouts/panels/network_bp.py delete mode 100644 selfdrive/ui/bp/mici/layouts/panels/toggles_bp.py delete mode 100644 selfdrive/ui/bp/mici/layouts/settings/bluepilot.py delete mode 100644 selfdrive/ui/bp/mici/layouts/settings/vehicle_mici.py delete mode 100644 selfdrive/ui/bp/mici/layouts/settings_bp.py delete mode 100644 selfdrive/ui/bp/mici/layouts/vehicle_bp.py delete mode 100644 selfdrive/ui/bp/mici/layouts/wifi_ui_bp.py delete mode 100644 selfdrive/ui/bp/mici/widgets/aurora_wordmark.py delete mode 100644 selfdrive/ui/bp/mici/widgets/bg_radial.py delete mode 100644 selfdrive/ui/bp/mici/widgets/big_input_dialog_bp.py delete mode 100644 selfdrive/ui/bp/mici/widgets/bp_dialogs.py delete mode 100644 selfdrive/ui/bp/mici/widgets/bp_palette.py delete mode 100644 selfdrive/ui/bp/mici/widgets/bp_topbar.py delete mode 100644 selfdrive/ui/bp/mici/widgets/button_bp.py delete mode 100644 selfdrive/ui/bp/mici/widgets/cards.py delete mode 100644 selfdrive/ui/bp/mici/widgets/floatbutton.py delete mode 100644 selfdrive/ui/bp/mici/widgets/icons.py delete mode 100644 selfdrive/ui/bp/mici/widgets/keyboard_bp.py delete mode 100644 selfdrive/ui/bp/mici/widgets/long_press_progress.py delete mode 100644 selfdrive/ui/bp/mici/widgets/page_dots.py delete mode 100644 selfdrive/ui/bp/mici/widgets/paged_scroller.py delete mode 100644 selfdrive/ui/bp/mici/widgets/preferred_network_select.py delete mode 100644 selfdrive/ui/bp/mici/widgets/select_panel.py delete mode 100644 selfdrive/ui/bp/mici/widgets/state_pill.py delete mode 100644 selfdrive/ui/bp/mici/widgets/sub_panel.py delete mode 100644 selfdrive/ui/bp/mici/widgets/vehicle_select_mici.py delete mode 100644 selfdrive/ui/bp/mici/widgets/web_server_qr_dialog_bp.py rename selfdrive/ui/{bp/mici/widgets => mici/layouts/settings}/web_server_qr_dialog.py (69%) diff --git a/selfdrive/ui/bp/mici/layouts/bluepilot_bp.py b/selfdrive/ui/bp/mici/layouts/bluepilot_bp.py deleted file mode 100644 index 2926c1fc04..0000000000 --- a/selfdrive/ui/bp/mici/layouts/bluepilot_bp.py +++ /dev/null @@ -1,155 +0,0 @@ -"""BP-styled BluePilot settings panel — port of selfdrive/ui/bp/mici/layouts/settings/bluepilot.py. - -Renders the existing BluePilot toggle/button list as a paged carousel of BP -cards. Float controls (predicted-curvature blend, lane-change factor, etc.) -are deferred to a future BP slider widget — for v1 they're shown as read-only -StatCards so the user can at least see current values. Toggles and buttons -keep full functionality and the same Params keys. -""" -from collections.abc import Callable - -from openpilot.common.params import Params -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.wifi_manager import WifiManager, Network -from openpilot.selfdrive.ui.ui_state import ui_state - -from openpilot.selfdrive.ui.bp.mici.widgets.sub_panel import BPSubPanel -from openpilot.selfdrive.ui.bp.mici.widgets.cards import ( - BigToggleCard, BigMultiToggleCard, BigButtonCard, StatCard, -) -from openpilot.selfdrive.ui.bp.mici.widgets.bp_dialogs import BPInfoDialog -from openpilot.selfdrive.ui.bp.mici.widgets.web_server_qr_dialog_bp import BPWebServerQRDialog -from openpilot.selfdrive.ui.bp.mici.widgets.preferred_network_select import PreferredNetworkSelectMici - - -def _read_param_text(key: str, default: str = "—") -> str: - v = Params().get(key) - if v is None: - return default - if isinstance(v, bytes): - v = v.decode("utf-8", errors="replace").strip("\x00") - return str(v).strip() or default - - -def _read_param_float(key: str) -> str: - try: - return f"{float(_read_param_text(key, '0')):.2f}" - except (ValueError, TypeError): - return "—" - - -class BluePilotLayoutBP(BPSubPanel): - TITLE = "BluePilot" - - def __init__(self, back_callback: Callable | None = None): - self._params = Params() - self._wifi_manager = WifiManager() - self._wifi_manager.set_active(False) - self._saved_networks: list[Network] = [] - self._wifi_manager.add_callbacks(networks_updated=self._on_networks_updated) - super().__init__(back_callback=back_callback) - - # ---- lifecycle ---- - def show_event(self): - super().show_event() - self._wifi_manager.set_active(True) - - def hide_event(self): - super().hide_event() - self._wifi_manager.set_active(False) - - def _on_networks_updated(self, networks: list[Network]): - self._saved_networks = [n for n in networks if self._wifi_manager.is_connection_saved(n.ssid)] - - # ---- callbacks ---- - def _open_qr(self): - if not self._params.get_bool("EnableWebRoutesServer"): - gui_app.push_widget(BPInfoDialog("Web routes off", - "Enable web routes server first.")) - return - try: - gui_app.push_widget(BPWebServerQRDialog(back_callback=gui_app.pop_widget)) - except Exception: - pass - - def _open_preferred_network(self): - if not self._saved_networks: - gui_app.push_widget(BPInfoDialog("No saved networks", - "Connect to a Wi-Fi network first.")) - return - gui_app.push_widget(PreferredNetworkSelectMici( - self._wifi_manager, self._saved_networks, on_dismiss=lambda: None, - )) - - def _clear_model_cache(self): - self._params.remove("ModelRunnerTypeCache") - self._params.remove("ModelManager_ActiveBundle") - self._params.put_bool("DoReboot", True) - - # ---- pages ---- - def _build_pages(self): - return [ - # ---- Web routes / preferred network ---- - BigToggleCard("Web Routes Server", - "Run the on-device web server for diagnostics.", - param_key="EnableWebRoutesServer"), - BigButtonCard("Show QR Code", on_click=self._open_qr), - BigButtonCard("Preferred Wi-Fi", on_click=self._open_preferred_network), - - # ---- Onroad UI ---- - BigToggleCard("Hands-free Cluster UI", - "Show BlueCruise UI on the instrument cluster.", - param_key="send_hands_free_cluster_msg"), - BigMultiToggleCard("Lower Right Display", "What to show in the lower-right corner onroad.", - options=["off", "lead car speed", "speed", "lead car distance", "time to lead"], - param_key="mici_complication"), - BigToggleCard("Show Brake Status", "Color the path when braking.", - param_key="ShowBrakeStatus"), - BigToggleCard("Show Blindspot Overlay", "Highlight blindspot vehicles.", - param_key="ShowBlindspotOverlay"), - BigToggleCard("Hybrid/EV Power Flow", "Show energy flow diagram for hybrids/EVs.", - param_key="FordPrefHybridPowerFlow"), - BigMultiToggleCard("Power Flow Style", "Visual style for the power flow widget.", - options=["flat", "round"], - param_key="FordPrefHybridPowerFlowAlternate"), - BigToggleCard("Rainbow Mode", "Cycle path colors over time.", - param_key="RainbowMode"), - BigToggleCard("Animate Steering Wheel", "Rotate the steering wheel icon onroad.", - param_key="BPAnimateSteeringWheel"), - BigToggleCard("Hide Onroad Fade", "Skip the fade-in transition onroad.", - param_key="mici_hide_onroad_fade"), - BigToggleCard("Hide Screen Border", "Remove the dark border around the onroad view.", - param_key="mici_hide_onroad_border"), - - # ---- Lateral / lane positioning ---- - BigToggleCard("Human Turn Detection", "Pause control when the driver makes a turn.", - param_key="enable_human_turn_detection"), - BigToggleCard("Disable ALC Under Speed", "Block auto lane change below a min speed.", - param_key="BlinkerPauseLaneChange"), - BigToggleCard("Lane Positioning", "Bias the path within the lane based on conditions.", - param_key="enable_lane_positioning"), - BigToggleCard("Lanefull Mode", "Stay near the lane center even with weak lines.", - param_key="enable_lane_full_mode"), - BigToggleCard("Custom Tuning Profile", "Use a hand-tuned curvature blend.", - param_key="custom_profile"), - StatCard("Lane Change Factor (high)", lambda: _read_param_float("lane_change_factor_high")), - StatCard("In-Lane Offset", lambda: _read_param_float("custom_path_offset")), - StatCard("Blend Ratio (high curve)", lambda: _read_param_float("pc_blend_ratio_high_C_UI")), - StatCard("Blend Ratio (low curve)", lambda: _read_param_float("pc_blend_ratio_low_C_UI")), - StatCard("Low-Curve PID Gain", lambda: _read_param_float("LC_PID_gain_UI")), - StatCard("Min Lane Change Speed", lambda: _read_param_text("BlinkerMinLateralControlSpeed", "—")), - - # ---- BP overrides ---- - BigToggleCard("Disable BP Lateral", "Bypass BP lateral control.", - param_key="disable_BP_lat_UI"), - BigToggleCard("Disable BP Long", "Bypass BP longitudinal control.", - param_key="disable_BP_long_UI"), - BigToggleCard("Disable Downhill Comp", "Disable downhill speed compensation.", - param_key="disable_downhill_comp_UI"), - BigToggleCard("UI Debug Logging", "Verbose BP UI logging.", - param_key="BPUIDebugLog"), - StatCard("12V Battery Limit", lambda: _read_param_float("vbatt_pause_charging")), - - # ---- Maintenance ---- - BigButtonCard("Clear Crashed Model", on_click=self._clear_model_cache, danger=True), - ] diff --git a/selfdrive/ui/bp/mici/layouts/home_bp.py b/selfdrive/ui/bp/mici/layouts/home_bp.py deleted file mode 100644 index 0e00228ff9..0000000000 --- a/selfdrive/ui/bp/mici/layouts/home_bp.py +++ /dev/null @@ -1,222 +0,0 @@ -"""BluePilot MICI home — paged carousel-friendly redesign of MiciHomeLayout. - -Renders the wordmark + breathing aurora + state pill on a blue/black radial -backdrop. Long-press anywhere on the body (>= 0.5s) toggles ExperimentalMode in -place; a top progress strip fills as you hold. Tap on body opens settings. - -Public API mirrors MiciHomeLayout so main.py wiring works unchanged: -- set_callbacks(on_settings, on_alerts, alert_count_callback, max_severity_callback) -""" -import time -import pyray as rl -from cereal import log -from collections.abc import Callable - -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.widgets.icon_widget import IconWidget -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.selfdrive.ui.mici.layouts.home import AlertsPill, NetworkIcon, NETWORK_TYPES -from openpilot.selfdrive.ui.bp.mici.widgets.bg_radial import BPRadialBackground -from openpilot.selfdrive.ui.bp.mici.widgets.aurora_wordmark import AuroraWordmark -from openpilot.selfdrive.ui.bp.mici.widgets.state_pill import StatePill -from openpilot.selfdrive.ui.bp.mici.widgets.long_press_progress import LongPressBar -from openpilot.selfdrive.ui.bp.mici.widgets import bp_palette as P - -NetworkType = log.DeviceState.NetworkType - -LONG_PRESS_DURATION = 0.5 -GEAR_HIT_SIZE = 64 # Touch target — bigger than visible -GEAR_VISIBLE = 36 -HOME_PADDING_X = 18 -META_FONT_SIZE = P.FS_META - - -class MiciHomeLayoutBP(Widget): - def __init__(self): - super().__init__() - self._on_settings_click: Callable | None = None - self._on_alerts_click: Callable | None = None - self._alert_count_callback: Callable[[], int] | None = None - - # Long-press state - self._mouse_down_t: float | None = None - self._did_long_press = False - self._is_pressed_prev = False - - # Cached for state-pill / wordmark callbacks. Refreshed every frame in - # _update_state so external param changes (e.g. params web UI) are visible - # immediately. ExperimentalMode is a single bool — cheap. - self._experimental_mode = False - - # Children - self._bg = self._child(BPRadialBackground()) - self._aurora = self._child(AuroraWordmark(get_mode=self._mode)) - self._pill = self._child(StatePill(get_state=self._pill_state)) - self._alerts_pill = self._child(AlertsPill()) - self._long_press_bar = self._child(LongPressBar()) - - # Gear (top-right) - self._gear_icon = self._child(IconWidget( - "icons_mici/settings.png", (GEAR_VISIBLE, GEAR_VISIBLE), opacity=0.95)) - - # Meta strip (bottom-left): network icon + label + branch - self._network_icon = self._child(NetworkIcon()) - self._meta_label = UnifiedLabel( - "", - font_size=META_FONT_SIZE, - font_weight=FontWeight.MEDIUM, - text_color=P.MUTED, - max_width=520, - wrap_text=False, - ) - - # ---- public API mirroring MiciHomeLayout ---- - def set_callbacks(self, on_settings: Callable | None = None, on_alerts: Callable | None = None, - alert_count_callback: Callable[[], int] | None = None, - max_severity_callback: Callable[[], int | None] | None = None): - self._on_settings_click = on_settings - self._on_alerts_click = on_alerts - self._alert_count_callback = alert_count_callback - self._alerts_pill.set_alert_count_callback(alert_count_callback, max_severity_callback) - - # ---- mode helpers ---- - def _mode(self) -> str: - """Return 'ready' / 'experimental' / 'offline' based on current state.""" - net = ui_state.sm["deviceState"].networkType - if net == NetworkType.none: - return "offline" - return "experimental" if self._experimental_mode else "ready" - - def _pill_state(self) -> tuple[str, str]: - m = self._mode() - label = { - "experimental": "EXPERIMENTAL ARMED", - "offline": "OFFLINE", - }.get(m, "STANDARD READY") - return m, label - - # ---- lifecycle ---- - def show_event(self): - super().show_event() - self._update_params() - - def _update_params(self): - self._experimental_mode = ui_state.params.get_bool("ExperimentalMode") - - def _update_state(self): - self._update_params() - - # ---- long-press tracking (mirrors stock MiciHomeLayout) ---- - pressed = self.is_pressed and not self._gear_pressed() - if pressed and not self._is_pressed_prev: - self._mouse_down_t = time.monotonic() - elif not pressed and self._is_pressed_prev: - self._mouse_down_t = None - self._did_long_press = False - self._is_pressed_prev = pressed - - if self._mouse_down_t is not None: - held = time.monotonic() - self._mouse_down_t - self._long_press_bar.set_progress(held / LONG_PRESS_DURATION) - if held > LONG_PRESS_DURATION: - # Mirror stock behavior: only toggle if longitudinal is available and - # we are NOT onroad (onroad would change behavior mid-drive). - if ui_state.has_longitudinal_control and not ui_state.started: - self._experimental_mode = not self._experimental_mode - ui_state.params.put("ExperimentalMode", self._experimental_mode) - self._mouse_down_t = None - self._did_long_press = True - else: - self._long_press_bar.set_progress(0.0) - - def _handle_mouse_release(self, mouse_pos: MousePos): - # Long-press fired? Don't also fire the tap. - if self._did_long_press: - self._did_long_press = False - return - # Tap on alerts pill → alerts pane. - if self._point_in_alerts(mouse_pos): - if self._on_alerts_click: - self._on_alerts_click() - return - # Tap on gear → settings. - if self._point_in_gear(mouse_pos): - if self._on_settings_click: - self._on_settings_click() - return - # Tap on body (anywhere else) → also opens settings, matching stock UX. - if self._on_settings_click: - self._on_settings_click() - - # ---- helpers ---- - def _gear_rect(self) -> rl.Rectangle: - """Touch-sized gear hit rect, anchored top-right of self._rect.""" - s = GEAR_HIT_SIZE - return rl.Rectangle(self._rect.x + self._rect.width - s - 4, - self._rect.y + 4, s, s) - - def _point_in_gear(self, p: MousePos) -> bool: - return rl.check_collision_point_rec(p, self._gear_rect()) - - def _point_in_alerts(self, p: MousePos) -> bool: - has_alerts = self._alert_count_callback and self._alert_count_callback() > 0 - return bool(has_alerts and rl.check_collision_point_rec(p, self._alerts_pill.rect)) - - def _gear_pressed(self) -> bool: - """Is the user currently pressing inside the gear hit rect?""" - rect = self._gear_rect() - for ev in gui_app.mouse_events: - if ev.left_down and rl.check_collision_point_rec(ev.pos, rect): - return True - return False - - def _format_meta(self) -> str: - ds = ui_state.sm["deviceState"] - net_label = NETWORK_TYPES.get(ds.networkType, "Offline") - branch = ui_state.params.get("GitBranch") or "" - if not branch: - return net_label - return f"{net_label} {branch}" - - # ---- render ---- - def _render(self, _): - r = self._rect - self._bg.render(r) - - # Aurora + wordmark fill the upper-most region (vertical-center weighted). - # Carve out ~60% top for wordmark, leaving room for pill + meta below. - aurora_rect = rl.Rectangle(r.x, r.y, r.width, r.height * 0.62) - self._aurora.render(aurora_rect) - - # State pill, centered horizontally just below the wordmark. - # The pill auto-sizes itself in _update_state(); we read last frame's width - # to center it. First frame may be 1px off-center; corrected next frame. - pill_w = max(self._pill.rect.width, 1) - px = r.x + (r.width - pill_w) / 2 - py = r.y + r.height * 0.62 + 2 - self._pill.set_position(px, py) - self._pill.render() - - # Gear (top right) - g = self._gear_rect() - visible_x = g.x + (g.width - GEAR_VISIBLE) / 2 - visible_y = g.y + (g.height - GEAR_VISIBLE) / 2 - self._gear_icon.render(rl.Rectangle(visible_x, visible_y, GEAR_VISIBLE, GEAR_VISIBLE)) - - # Alerts pill below the gear so offroad alerts remain reachable. - alert_rect = self._alerts_pill.rect - self._alerts_pill.set_position(r.x + r.width - alert_rect.width - 10, g.y + g.height + 8) - self._alerts_pill.render() - - # Meta strip (bottom-left): network icon + label - meta_y = r.y + r.height - META_FONT_SIZE - 12 - icon_rect = rl.Rectangle(r.x + HOME_PADDING_X, meta_y - 4, 54, 30) - self._network_icon.render(icon_rect) - self._meta_label.set_text(self._format_meta()) - self._meta_label.set_position(icon_rect.x + icon_rect.width + 8, meta_y) - self._meta_label.render() - - # Long-press progress strip (top edge) - self._long_press_bar.render(rl.Rectangle(r.x, r.y, r.width, 4)) diff --git a/selfdrive/ui/bp/mici/layouts/offroad_alerts_bp.py b/selfdrive/ui/bp/mici/layouts/offroad_alerts_bp.py deleted file mode 100644 index dc3fdd2c12..0000000000 --- a/selfdrive/ui/bp/mici/layouts/offroad_alerts_bp.py +++ /dev/null @@ -1,184 +0,0 @@ -"""BP-styled offroad alerts pane. - -Replaces the vertical-scroll list in selfdrive/ui/mici/layouts/offroad_alerts.py -with a paged horizontal carousel of alert cards on the BP radial backdrop. -Empty state shows a big centered "NO ALERTS" message. - -Public API parity with MiciOffroadAlerts so MainLayout's wiring is unchanged: -- active_alerts() -> int -- scrolling() -> bool (always False for our pager — alerts swipe horizontally) -""" -import time -import pyray as rl -from collections.abc import Callable - -from openpilot.common.params import Params -from openpilot.system.hardware import HARDWARE -from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import UnifiedLabel - -from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import ( - OFFROAD_ALERTS, AlertData, REFRESH_INTERVAL, -) -from openpilot.selfdrive.ui.bp.mici.widgets.bg_radial import BPRadialBackground -from openpilot.selfdrive.ui.bp.mici.widgets.paged_scroller import PagedScroller -from openpilot.selfdrive.ui.bp.mici.widgets.page_dots import PageDots -from openpilot.selfdrive.ui.bp.mici.widgets import bp_palette as P - - -SEVERITY_COLOR = { - -1: P.ACCENT, # update available - 0: P.OFFLINE, - 1: P.READY, - 2: P.WARN, - 3: P.DANGER, -} - - -class _AlertCard(Widget): - """One alert per page.""" - def __init__(self, alert_data: AlertData, on_click: Callable | None = None): - super().__init__() - self.alert_data = alert_data - self._on_click = on_click - self._title = UnifiedLabel("", font_size=P.FS_NAME, font_weight=FontWeight.BOLD, - text_color=P.TEXT, max_width=520, wrap_text=True, - line_height=1.05) - - def _handle_mouse_release(self, _): - if self._on_click: - self._on_click() - - def _render(self, _): - r = self._rect - pad = 6 - bg = rl.Rectangle(r.x + pad, r.y + pad, r.width - pad * 2, r.height - pad * 2) - color = SEVERITY_COLOR.get(self.alert_data.severity, P.WARN) - - # Card background — tinted by severity color - card_bg = rl.Color(color.r, color.g, color.b, int(0.10 * 255)) - border = rl.Color(color.r, color.g, color.b, int(0.50 * 255)) - rl.draw_rectangle_rounded(bg, 0.10, 16, card_bg) - rl.draw_rectangle_rounded_lines_ex(bg, 0.10, 16, 1, border) - - # Severity bar on the left - rl.draw_rectangle(int(bg.x), int(bg.y) + 12, 6, int(bg.height) - 24, color) - - # Title - text_x = bg.x + 22 - text_max_w = bg.width - 32 - self._title.set_text(self.alert_data.text) - self._title.set_max_width(int(text_max_w)) - h = self._title.get_content_height(int(text_max_w)) + P.FS_NAME * 0.4 - self._title.set_rect(rl.Rectangle(text_x, bg.y + 14, text_max_w, h)) - self._title.render() - - -class MiciOffroadAlertsBP(Widget): - def __init__(self): - super().__init__() - self._params = Params() - self._sorted_alerts: list[AlertData] = [] - self._cards: list[_AlertCard] = [] - self._last_refresh = 0.0 - - self._bg = self._child(BPRadialBackground()) - self._pager = self._child(PagedScroller([])) - self._dots = self._child(PageDots( - get_current_page=lambda: self._pager.current_page, - get_page_count=lambda: self._pager.page_count, - )) - - self._empty = UnifiedLabel( - tr("no alerts"), - font_size=72, font_weight=FontWeight.DISPLAY, text_color=P.MUTED, - max_width=520, wrap_text=False, - ) - - self._build() - - # ---- public API parity ---- - def active_alerts(self) -> int: - return sum(1 for a in self._sorted_alerts if a.visible) - - def max_severity(self) -> int | None: - return max((a.severity for a in self._sorted_alerts if a.visible), default=None) - - def scrolling(self) -> bool: - # Pager handles its own touch; report False so MainLayout doesn't gate. - return False - - # ---- build / refresh ---- - def _build(self): - self._sorted_alerts = [] - self._cards = [] - - update = AlertData(key="UpdateAvailable", text="", severity=-1) - self._sorted_alerts.append(update) - self._cards.append(_AlertCard(update, on_click=lambda: HARDWARE.reboot())) - - for key, cfg in sorted(OFFROAD_ALERTS.items(), - key=lambda x: x[1].get("severity", 0), reverse=True): - a = AlertData(key=key, text="", severity=cfg.get("severity", 0)) - self._sorted_alerts.append(a) - self._cards.append(_AlertCard(a)) - - def _refresh(self): - update_available = self._params.get_bool("UpdateAvailable") - - for a in self._sorted_alerts: - if a.key == "UpdateAvailable": - if update_available: - new_desc = self._params.get("UpdaterNewDescription") or "" - version = "" - if new_desc: - parts = new_desc.split(" / ") - if len(parts) > 3: - version = f" — {parts[0]} ({parts[3]})" - a.text = f"Update available{version}. Tap to install." - a.visible = True - else: - a.text = "" - a.visible = False - else: - text = "" - blob = self._params.get(a.key) - if blob: - text = blob.get("text", "").replace("%1", blob.get("extra", "")) - a.text = text - a.visible = bool(text) - - visible_cards = [c for c in self._cards if c.alert_data.visible] - self._pager._items = visible_cards - self._pager._scroller._items = [] - for c in visible_cards: - self._pager._scroller.add_widget(c) - - # ---- lifecycle ---- - def show_event(self): - super().show_event() - self._last_refresh = time.monotonic() - self._refresh() - - def _update_state(self): - if time.monotonic() - self._last_refresh >= REFRESH_INTERVAL: - self._refresh() - self._last_refresh = time.monotonic() - - # ---- render ---- - def _render(self, _): - r = self._rect - self._bg.render(r) - - if self.active_alerts() == 0: - tw = max(self._empty.text_width, 1) - self._empty.set_position(r.x + (r.width - tw) / 2, r.y + (r.height - 72) / 2) - self._empty.render() - return - - # Pager + dots - DOT = 20 - self._pager.render(rl.Rectangle(r.x, r.y, r.width, r.height - DOT)) - self._dots.render(rl.Rectangle(r.x, r.y + r.height - DOT, r.width, DOT)) diff --git a/selfdrive/ui/bp/mici/layouts/panels/developer_bp.py b/selfdrive/ui/bp/mici/layouts/panels/developer_bp.py deleted file mode 100644 index e1a748beed..0000000000 --- a/selfdrive/ui/bp/mici/layouts/panels/developer_bp.py +++ /dev/null @@ -1,76 +0,0 @@ -"""BP Developer panel — mirrors selfdrive/ui/mici/layouts/settings/developer.py. - -Same param keys; same SSH-keys flow (delegates to existing SshKeyFetcher and -the BigInputDialog). One-per-page layout with mono ID stat cards on top. -""" -from openpilot.common.time_helpers import system_time_valid -from openpilot.system.ui.lib.application import gui_app -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.selfdrive.ui.bp.mici.widgets.bp_dialogs import BPInfoDialog, BPInputDialog -from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyFetcher - -from openpilot.selfdrive.ui.bp.mici.widgets.sub_panel import BPSubPanel -from openpilot.selfdrive.ui.bp.mici.widgets.cards import ( - StatCard, BigToggleCard, BigButtonCard, -) - - -def _dongle_id() -> str: - return (ui_state.params.get("DongleId") or "-").strip() - - -def _serial() -> str: - return (ui_state.params.get("HardwareSerial") or "-").strip() - - -class DeveloperLayoutBP(BPSubPanel): - TITLE = "Developer" - - def __init__(self, back_callback=None): - self._ssh_fetcher = SshKeyFetcher(ui_state.params) - super().__init__(back_callback=back_callback) - - def _open_ssh_keys(self): - if not system_time_valid(): - gui_app.push_widget(BPInfoDialog("Wi-Fi required", "Connect to Wi-Fi to fetch your key.")) - return - - current = ui_state.params.get("GithubUsername") or "" - - def on_username(username: str): - if not username: - self._ssh_fetcher.clear() - return - - def on_response(error): - if error is not None: - gui_app.push_widget(BPInfoDialog("SSH key error", error)) - - self._ssh_fetcher.fetch(username, on_response) - - gui_app.push_widget(BPInputDialog( - "enter GitHub username...", current, minimum_length=0, confirm_callback=on_username, - )) - - def _on_debug_toggle(self, on: bool): - gui_app.set_show_touches(on) - gui_app.set_show_fps(on) - - def _build_pages(self): - return [ - StatCard("Dongle ID", _dongle_id, variant="mono"), - StatCard("Serial", _serial, variant="mono"), - BigToggleCard("Enable ADB", "Remote shell over USB.", param_key="AdbEnabled"), - BigToggleCard("Enable SSH", "Allow SSH connections to the device.", param_key="SshEnabled"), - BigButtonCard("SSH Keys", on_click=self._open_ssh_keys), - BigToggleCard("Joystick Debug Mode", "Manually drive the actuators with a joystick.", - param_key="JoystickDebugMode"), - BigToggleCard("Longitudinal Maneuver Mode", "Run scripted long maneuvers offroad.", - param_key="LongitudinalManeuverMode"), - BigToggleCard("Lateral Maneuver Mode", "Run scripted lat maneuvers offroad.", - param_key="LateralManeuverMode"), - BigToggleCard("Alpha Longitudinal", "Enable in-development longitudinal control.", - param_key="AlphaLongitudinalEnabled"), - BigToggleCard("UI Debug Mode", "Show touch points and FPS counter.", - param_key="ShowDebugInfo", on_change=self._on_debug_toggle), - ] diff --git a/selfdrive/ui/bp/mici/layouts/panels/device_bp.py b/selfdrive/ui/bp/mici/layouts/panels/device_bp.py deleted file mode 100644 index a0c5f58d30..0000000000 --- a/selfdrive/ui/bp/mici/layouts/panels/device_bp.py +++ /dev/null @@ -1,112 +0,0 @@ -"""BP Device panel — mirrors selfdrive/ui/mici/layouts/settings/device.py. - -Reuses the existing dialogs (PairingDialog, DriverCameraDialog, -ReviewTrainingGuide, ReviewTermsPage, MiciFccModal, BigConfirmationDialog) so -the actions stay in lock-step with stock behavior. Param flips for the -destructive actions match stock. -""" -import os -import pyray as rl -from openpilot.common.basedir import BASEDIR -from openpilot.system.ui.lib.application import gui_app -from openpilot.selfdrive.ui.ui_state import ui_state, device - -from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog -from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog -from openpilot.selfdrive.ui.bp.mici.widgets.bp_dialogs import BPConfirmDialog, BPInfoDialog -from openpilot.selfdrive.ui.mici.layouts.settings.device import ( - ReviewTrainingGuide, ReviewTermsPage, MiciFccModal, -) -from openpilot.system.ui.widgets.html_render import HtmlModal - -from openpilot.selfdrive.ui.bp.mici.widgets.sub_panel import BPSubPanel -from openpilot.selfdrive.ui.bp.mici.widgets.cards import StatCard, BigButtonCard - - -def _calibration_text() -> str: - blob = ui_state.params.get("CalibrationParams") - if not blob: - return "-" - # Stock writes a structured value; we just show "complete" when present. - return "Complete" - - -def _paired_text() -> str: - return "comma.ai" if ui_state.prime_state.is_paired() else "Not paired" - - -def _confirm(action_text: str, callback, icon_path: str, exit_on_confirm: bool = True, red: bool = False): - """Stock-style: only fire if not engaged; show a slide-to-confirm dialog.""" - if ui_state.engaged: - gui_app.push_widget(BPInfoDialog("Disengage first", f"Disengage to {action_text}.")) - return - - def on_confirm(): - if not ui_state.engaged: - callback() - - gui_app.push_widget(BPConfirmDialog( - f"slide to\n{action_text.lower()}", - gui_app.texture(icon_path, 64, 64), - on_confirm, - exit_on_confirm=exit_on_confirm, red=red, - )) - - -class DeviceLayoutBP(BPSubPanel): - TITLE = "Device" - - def __init__(self, back_callback=None): - self._fcc_dialog: HtmlModal | None = None - super().__init__(back_callback=back_callback) - - # ---- callbacks ---- - def _open_pair(self): gui_app.push_widget(PairingDialog()) - def _open_driver_cam(self): gui_app.push_widget(DriverCameraDialog()) - def _open_terms(self): gui_app.push_widget(ReviewTermsPage()) - def _open_training(self): - gui_app.push_widget(ReviewTrainingGuide(completed_callback=lambda: gui_app.pop_widgets_to(self))) - - def _open_regulatory(self): - if not self._fcc_dialog: - self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/mici_fcc.html")) - gui_app.push_widget(self._fcc_dialog) - - def _reset_calibration(self): - def go(): - params = ui_state.params - params.remove("CalibrationParams") - params.remove("LiveTorqueParameters") - params.remove("LiveParameters") - params.remove("LiveParametersV2") - params.remove("LiveDelay") - params.put_bool("OnroadCycleRequested", True) - _confirm("reset calibration", go, "icons_mici/settings/device/lkas.png") - - def _reboot(self): - _confirm("reboot", lambda: ui_state.params.put_bool("DoReboot", True), - "icons_mici/settings/device/reboot.png", exit_on_confirm=False) - - def _power_off(self): - _confirm("power off", lambda: ui_state.params.put_bool("DoShutdown", True), - "icons_mici/settings/device/power.png", exit_on_confirm=False, red=True) - - def _uninstall(self): - _confirm("uninstall", lambda: ui_state.params.put_bool("DoUninstall", True), - "icons_mici/settings/device/uninstall.png", exit_on_confirm=False, red=True) - - # ---- pages ---- - def _build_pages(self): - return [ - StatCard("Paired", _paired_text, variant="small"), - StatCard("Calibration", _calibration_text, variant="small"), - BigButtonCard("Pair Device", icon="icons_mici/settings/comma_icon.png", on_click=self._open_pair), - BigButtonCard("Driver Camera", icon="icons_mici/settings/device/cameras.png", on_click=self._open_driver_cam), - BigButtonCard("Training Guide", icon="icons_mici/settings/device/info.png", on_click=self._open_training), - BigButtonCard("Terms", icon="icons_mici/settings/device/info.png", on_click=self._open_terms), - BigButtonCard("Regulatory Info", icon="icons_mici/settings/device/info.png", on_click=self._open_regulatory), - BigButtonCard("Reset Calibration", icon="icons_mici/settings/device/lkas.png", on_click=self._reset_calibration, danger=True), - BigButtonCard("Uninstall", icon="icons_mici/settings/device/uninstall.png", on_click=self._uninstall, danger=True), - BigButtonCard("Reboot", icon="icons_mici/settings/device/reboot.png", on_click=self._reboot), - BigButtonCard("Power Off", icon="icons_mici/settings/device/power.png", on_click=self._power_off, danger=True), - ] diff --git a/selfdrive/ui/bp/mici/layouts/panels/firehose_bp.py b/selfdrive/ui/bp/mici/layouts/panels/firehose_bp.py deleted file mode 100644 index b95d9270d5..0000000000 --- a/selfdrive/ui/bp/mici/layouts/panels/firehose_bp.py +++ /dev/null @@ -1,42 +0,0 @@ -"""BP Firehose panel — short summary of training data uploads. - -The stock FirehoseLayout is a long HTML scroll panel (Title + description + -FAQ). For MICI's small screen, we expose a couple of paged stat cards plus a -"learn more" jump back to the stock layout for users who want the full FAQ. -""" -from openpilot.common.params import Params -from openpilot.system.ui.lib.application import gui_app -from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout - -from openpilot.selfdrive.ui.bp.mici.widgets.sub_panel import BPSubPanel -from openpilot.selfdrive.ui.bp.mici.widgets.cards import StatCard, BigButtonCard - - -def _segment_count() -> str: - blob = Params().get("ApiCache_FirehoseStats") - if not blob: - return "0" - try: - return f"{int(blob.get('firehose', 0)):,}" - except Exception: - return "0" - - -class FirehoseLayoutBP(BPSubPanel): - TITLE = "Firehose" - - def __init__(self, back_callback=None): - self._stock_panel: FirehoseLayout | None = None - super().__init__(back_callback=back_callback) - - def _open_full(self): - if self._stock_panel is None: - self._stock_panel = FirehoseLayout() - gui_app.push_widget(self._stock_panel) - - def _build_pages(self): - return [ - StatCard("Segments banked", _segment_count), - StatCard("Status", "Active", variant="small"), - BigButtonCard("Learn more", on_click=self._open_full), - ] diff --git a/selfdrive/ui/bp/mici/layouts/panels/network_bp.py b/selfdrive/ui/bp/mici/layouts/panels/network_bp.py deleted file mode 100644 index 192f3248df..0000000000 --- a/selfdrive/ui/bp/mici/layouts/panels/network_bp.py +++ /dev/null @@ -1,106 +0,0 @@ -"""BP Network panel — mirrors selfdrive/ui/mici/layouts/settings/network/network_layout.py. - -The actual Wi-Fi list / connect flow is delegated to the existing WifiUIMici so -we don't reimplement the wifi UI; the BP panel just provides the entry point -plus tethering / metered / roaming / APN. -""" -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.wifi_manager import WifiManager, MeteredType -from openpilot.selfdrive.ui.bp.mici.layouts.wifi_ui_bp import BPWifiUI -from openpilot.selfdrive.ui.bp.mici.widgets.bp_dialogs import BPInputDialog -from openpilot.selfdrive.ui.ui_state import ui_state - -from openpilot.selfdrive.ui.bp.mici.widgets.sub_panel import BPSubPanel -from openpilot.selfdrive.ui.bp.mici.widgets.cards import ( - BigToggleCard, BigMultiToggleCard, BigButtonCard, -) - - -class NetworkLayoutBP(BPSubPanel): - TITLE = "Network" - - def __init__(self, back_callback=None): - self._wifi_manager = WifiManager() - self._wifi_manager.set_active(False) - self._wifi_ui = BPWifiUI(self._wifi_manager) - super().__init__(back_callback=back_callback) - - def _open_wifi(self): - gui_app.push_widget(self._wifi_ui) - - def _open_tethering_password(self): - current = self._wifi_manager.tethering_password or "" - - def on_confirm(password: str): - if password: - self._wifi_manager.set_tethering_password(password) - - gui_app.push_widget(BPInputDialog( - "enter password...", current, minimum_length=8, confirm_callback=on_confirm, - )) - - def _open_apn(self): - current = ui_state.params.get("GsmApn") or "" - - def on_confirm(apn: str): - apn = apn.strip() - if apn == "": - ui_state.params.remove("GsmApn") - else: - ui_state.params.put("GsmApn", apn) - - gui_app.push_widget(BPInputDialog( - "enter APN...", current, minimum_length=0, confirm_callback=on_confirm, - )) - - def _network_metered_change(self, idx: int): - metered = [MeteredType.UNKNOWN, MeteredType.YES, MeteredType.NO][idx] - self._wifi_manager.set_current_network_metered(metered) - - def _build_pages(self): - return [ - BigButtonCard( - "Wi-Fi networks", icon="icons_mici/settings/network/wifi_strength_full.png", - on_click=self._open_wifi, - ), - BigMultiToggleCard( - "Network Usage", - "Tag this Wi-Fi as default, metered, or unmetered.", - options=["default", "metered", "unmetered"], - on_change=self._network_metered_change, - ), - BigToggleCard( - "Enable Tethering", - "Share this device's connection over Wi-Fi.", - # Stock uses set_tethering_active() rather than a Params bool — for - # simplicity in the carousel we expose the WifiManager state. - ), - BigButtonCard( - "Tethering Password", icon="icons_mici/settings/network/tethering.png", - on_click=self._open_tethering_password, - ), - BigToggleCard( - "Enable Roaming", - "Allow data on roaming carriers.", - param_key="GsmRoaming", - ), - BigButtonCard( - "APN Settings", on_click=self._open_apn, - ), - BigToggleCard( - "Cellular Metered", - "Treat the SIM as a metered connection.", - param_key="GsmMetered", - ), - ] - - # Lifecycle parity with stock NetworkLayoutMici. - def show_event(self): - super().show_event() - self._wifi_manager.set_active(True) - gui_app.add_nav_stack_tick(self._wifi_manager.process_callbacks) - - def hide_event(self): - super().hide_event() - self._wifi_manager.set_active(False) - gui_app.remove_nav_stack_tick(self._wifi_manager.process_callbacks) diff --git a/selfdrive/ui/bp/mici/layouts/panels/toggles_bp.py b/selfdrive/ui/bp/mici/layouts/panels/toggles_bp.py deleted file mode 100644 index d1ec8f6ecc..0000000000 --- a/selfdrive/ui/bp/mici/layouts/panels/toggles_bp.py +++ /dev/null @@ -1,56 +0,0 @@ -"""BP-styled Toggles panel — mirrors selfdrive/ui/mici/layouts/settings/toggles.py. - -Same param keys, same enable/disable gates, same toggles in the same order — but -rendered as a one-item-per-page paged carousel. -""" -from openpilot.selfdrive.ui.bp.mici.widgets.sub_panel import BPSubPanel -from openpilot.selfdrive.ui.bp.mici.widgets.cards import BigToggleCard, BigMultiToggleCard - - -class TogglesLayoutBP(BPSubPanel): - TITLE = "Toggles" - - def _build_pages(self): - return [ - BigMultiToggleCard( - "Driving Personality", - "Pick how aggressive bluepilot follows the car ahead.", - options=["aggressive", "standard", "relaxed"], - param_key="LongitudinalPersonality", - ), - BigToggleCard( - "Experimental Mode", - "End-to-end longitudinal control. Behavior may change without warning.", - param_key="ExperimentalMode", - ), - BigToggleCard( - "Use Metric Units", - "Display speed in km/h instead of mph.", - param_key="IsMetric", - ), - BigToggleCard( - "Lane Departure Warnings", - "Visual + audible alert when you drift out of your lane unintentionally.", - param_key="IsLdwEnabled", - ), - BigToggleCard( - "Always-on Driver Monitor", - "Track driver attention even while bluepilot is disengaged.", - param_key="AlwaysOnDM", - ), - BigToggleCard( - "Record & Upload Driver Camera", - "Send driver-facing footage to comma after each drive.", - param_key="RecordFront", - ), - BigToggleCard( - "Record & Upload Mic Audio", - "Save in-cabin microphone audio with each drive.", - param_key="RecordAudio", - ), - BigToggleCard( - "Enable bluepilot", - "Master gate. When off, bluepilot will not engage.", - param_key="OpenpilotEnabledToggle", - ), - ] diff --git a/selfdrive/ui/bp/mici/layouts/settings/bluepilot.py b/selfdrive/ui/bp/mici/layouts/settings/bluepilot.py deleted file mode 100644 index 341e57655c..0000000000 --- a/selfdrive/ui/bp/mici/layouts/settings/bluepilot.py +++ /dev/null @@ -1,316 +0,0 @@ -import pyray as rl -from collections.abc import Callable - -from openpilot.system.ui.widgets.scroller import Scroller -from openpilot.selfdrive.ui.bp.mici.widgets.button_bp import BigButtonBP, BigParamControlBP, BigMultiToggleBP, BigMultiParamToggleBP, BigMultiParamToggleBoolBP -from openpilot.selfdrive.ui.bp.mici.widgets.floatbutton import BigParamFloatControl, BigParamIntControl -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.widgets.nav_widget import NavWidget -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.common.params import Params -from openpilot.common.swaglog import cloudlog -from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.lib.wifi_manager import WifiManager, Network -from openpilot.system.ui.widgets import DialogResult -from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog -from openpilot.selfdrive.ui.bp.mici.widgets.preferred_network_select import PreferredNetworkSelectMici - -class BluePilotLayoutMici(NavWidget): - def __init__(self, back_callback: Callable): - super().__init__() - self.set_back_callback(back_callback) - self._params = Params() - self.lane_change_factor_high = float(self._params.get("lane_change_factor_high", return_default=True)) - - # WifiManager for preferred network selector (same pattern as TICI BluePilotLayout) - self._wifi_manager = WifiManager() - self._wifi_manager.set_active(False) # Don't scan unless menu is shown - self._saved_networks: list[Network] = [] - self._wifi_manager.add_callbacks(networks_updated=self._on_network_updated) - - # Preferred WiFi network selector (same as TICI - list of saved networks) - self.preferred_network_btn = BigButtonBP( - tr("Preferred WiFi Network"), - "", - "icons_mici/settings/network/wifi_strength_full.png" - ) - self.preferred_network_btn.set_click_callback(self._select_preferred_network) - - # ******** Main Scroller ******** - self.enable_web_routes = BigParamControlBP("enable web routes server", "EnableWebRoutesServer") - self.show_web_routes_qr = BigButtonBP("show QR code", "", "icons_mici/settings/network/wifi_strength_full.png") - self.show_web_routes_qr.set_click_callback(self._show_qr_dialog) - self.show_hands_free_ui = BigParamControlBP("show BlueCruise UI on Cluster", "send_hands_free_cluster_msg") - self.show_lead_vehicle = BigMultiParamToggleBP("Lower Right Display", "mici_complication", ["off", "lead car speed", "speed", "lead car distance", "time to lead car"]) - self.show_brake_status = BigParamControlBP("show brake status", "ShowBrakeStatus") - self.show_blindspot_ui = BigParamControlBP("show blindspot overlay", "ShowBlindspotOverlay") - self.rainbow_mode = BigParamControlBP("rainbow mode", "RainbowMode") - self.enable_human_turn_detection = BigParamControlBP("enable human turn detection", "enable_human_turn_detection") - self.lane_change_factor_high = BigParamFloatControl("lane change factor high", "lane_change_factor_high", min=0.5, max=1.0) - self.enable_lane_positioning = BigParamControlBP("enable lane positioning", "enable_lane_positioning", tint=rl.GREEN) - self.custom_path_offset = BigParamFloatControl("in-lane offset", "custom_path_offset", is_active_param="enable_lane_positioning", min=-0.5, max=0.5, tint=rl.GREEN) - self.enable_lane_full_mode = BigParamControlBP("enable lanefull mode", "enable_lane_full_mode", is_active_param="enable_lane_positioning", tint=rl.GREEN) - self.custom_profile = BigParamControlBP("use custom tuning profile", "custom_profile", tint=rl.BLUE) - self.pc_blend_ratio_high_C = BigParamFloatControl("predicted curvature blend ratio high", "pc_blend_ratio_high_C_UI", is_active_param="custom_profile", min=0.0, max=1.0, tint=rl.BLUE) - self.pc_blend_ratio_low_C = BigParamFloatControl("predicted curvature blend ratio low", "pc_blend_ratio_low_C_UI", is_active_param="custom_profile", min=0.0, max=1.0, tint=rl.BLUE) - self.LC_PID_gain = BigParamFloatControl( - "low curvature PID gain", - "LC_PID_gain_UI", - is_active=lambda: self._params.get_bool("enable_lane_positioning") and self._params.get_bool("custom_profile"), - min=0.0, - max=5.0, - tint=rl.BLUE, - ) - self.disable_lane_change_under_speed = BigParamControlBP("disable auto lane change under speed", "BlinkerPauseLaneChange") - self.blinker_min_speed = BigParamIntControl("blinker min lane change speed", "BlinkerMinLateralControlSpeed", min=5, max=50, step=5.0) - self.animate_steering_wheel = BigParamControlBP("animate steering wheel", "BPAnimateSteeringWheel") - self.hide_fade = BigParamControlBP("hide onroad fade", "mici_hide_onroad_fade") - self.hide_border = BigParamControlBP("hide screen border", "mici_hide_onroad_border") - self.disable_BP_lat = BigParamControlBP("disable BP lateral control", "disable_BP_lat_UI") - self.disable_BP_long = BigParamControlBP("bypass BP longitudinal control", "disable_BP_long_UI") - self.disable_dowhill_comp = BigParamControlBP("disable downhill compensation", "disable_downhill_comp_UI") - self.clear_model_cache = BigButtonBP("clear crashed model", "", "icons_mici/settings/device/reboot.png") - self.clear_model_cache.set_click_callback(self._clear_model_cache) - self.ui_debug_log = BigParamControlBP("ui debug logging", "BPUIDebugLog") - self.vbatt_pause_charging = BigParamFloatControl("12V battery limit", "vbatt_pause_charging", min=11.0, max=14.0, step=0.1) - - # Hybrid/EV power flow: enable toggle (like C3X) + style dropdown Flat/Round (C4), same pattern as Lower Right Display - self.show_hybrid_power_flow = BigParamControlBP("show hybrid/EV power flow", "FordPrefHybridPowerFlow") - - self.hybrid_power_flow_style = BigMultiParamToggleBoolBP( - "hybrid/EV power flow style", - "FordPrefHybridPowerFlowAlternate", - ["flat", "round"] - ) - - #self.charging_btn = BigButton("charging", "", "icons_mici/settings/charge_icon.png") - #self.charging_btn.set_click_callback(lambda: self._show_charging_view()) - - self._scroller = Scroller(snap_items=False) - self._scroller._scroller.add_widgets([ - self.enable_web_routes, - self.show_web_routes_qr, - self.preferred_network_btn, - self.show_hands_free_ui, - self.show_lead_vehicle, - self.show_brake_status, - self.show_blindspot_ui, - self.show_hybrid_power_flow, - self.hybrid_power_flow_style, - self.rainbow_mode, - self.enable_human_turn_detection, - self.lane_change_factor_high, - self.disable_lane_change_under_speed, - self.blinker_min_speed, - self.enable_lane_positioning, - self.custom_path_offset, - self.enable_lane_full_mode, - self.custom_profile, - self.pc_blend_ratio_high_C, - self.pc_blend_ratio_low_C, - self.LC_PID_gain, - self.animate_steering_wheel, - self.hide_fade, - self.hide_border, - self.vbatt_pause_charging, - self.disable_BP_lat, - self.disable_BP_long, - self.disable_dowhill_comp, - self.clear_model_cache, - self.ui_debug_log, - ]) - - # Toggle lists - self._refresh_toggles = ( - ("EnableWebRoutesServer", self.enable_web_routes), - ("send_hands_free_cluster_msg", self.show_hands_free_ui), - ("FordPrefHybridPowerFlow", self.show_hybrid_power_flow), - ("ShowBrakeStatus", self.show_brake_status), - ("ShowBlindspotOverlay", self.show_blindspot_ui), - ("RainbowMode", self.rainbow_mode), - ("enable_human_turn_detection", self.enable_human_turn_detection), - ("BlinkerPauseLaneChange", self.disable_lane_change_under_speed), - ("enable_lane_positioning", self.enable_lane_positioning), - ("enable_lane_full_mode", self.enable_lane_full_mode), - ("custom_profile", self.custom_profile), - ("disable_BP_lat_UI", self.disable_BP_lat), - ("disable_BP_long_UI", self.disable_BP_long), - ("disable_downhill_comp_UI", self.disable_dowhill_comp), - ("BPAnimateSteeringWheel", self.animate_steering_wheel), - ("BPUIDebugLog", self.ui_debug_log), - ("mici_hide_onroad_fade", self.hide_fade), - ("BPHideOnroadBorder", self.hide_border), - ) - - ui_state.add_offroad_transition_callback(self._update_toggles) - - # def _show_charging_view(self): - # dlg = BigChargingDialog() - # gui_app.set_modal_overlay(dlg) - - def show_event(self): - super().show_event() - self._scroller.show_event() - self._update_toggles() - self._update_buttons() - # Enable WiFi scanning when BluePilot menu is shown - self._wifi_manager.set_active(True) - self.preferred_network_btn.set_value(self._get_preferred_network_display()) - - def hide_event(self): - super().hide_event() - # Disable WiFi scanning when BluePilot menu is hidden - self._wifi_manager.set_active(False) - - def _render(self, rect: rl.Rectangle): - self._wifi_manager.process_callbacks() - self._scroller.render(rect) - - def _clear_model_cache(self): - """Clear ModelRunnerTypeCache and ModelManager_ActiveBundle, then reboot.""" - - def handle_confirm(result: DialogResult): - if result == DialogResult.CONFIRM: - try: - self._params.remove("ModelRunnerTypeCache") - except Exception: - pass - try: - self._params.remove("ModelManager_ActiveBundle") - except Exception: - pass - self._params.put_bool("DoReboot", True) - cloudlog.info("BluePilot: Cleared model cache (ModelRunnerTypeCache, ModelManager_ActiveBundle), triggered reboot") - - dialog = ConfirmDialog( - tr("Clear crashed model runner cache and reboot? This fixes 'Communication Issue' when modeld fails to start."), - tr("Clear & Reboot"), - callback=handle_confirm - ) - gui_app.push_widget(dialog) - - def _show_qr_dialog(self): - """Show QR code dialog for webserver access. MICI uses push_widget/pop_widget (no set_modal_overlay).""" - if not self._params.get_bool("EnableWebRoutesServer"): - return - try: - qr_dialog = WebServerQRDialog(back_callback=gui_app.pop_widget) - gui_app.push_widget(qr_dialog) - except Exception as e: - from openpilot.common.swaglog import cloudlog - cloudlog.warning(f"Failed to show QR dialog: {e}") - - def _update_state(self): - super()._update_state() - self.show_lead_vehicle._load_value() - self.hybrid_power_flow_style._load_value() - # Refresh dependent control enabled state (e.g. after toggling enable_lane_positioning) - self._update_buttons() - - def _update_buttons(self): - """Update button enabled state based on server status and parameter dependencies (see MICI_MENU.csv).""" - ui_state.update_params() - p = self._params - - # Web routes QR: only when server enabled - server_enabled = ui_state.params.get_bool("EnableWebRoutesServer") - self.show_web_routes_qr.set_enabled(server_enabled) - - # Hybrid/EV power flow style (flat/round): only when power flow is enabled - power_flow_enabled = p.get_bool("FordPrefHybridPowerFlow") - self.hybrid_power_flow_style.set_enabled(power_flow_enabled) - - # Lane positioning–dependent controls (prereq: Enable Advanced Lane Positioning) - lane_positioning_enabled = p.get_bool("enable_lane_positioning") - self.custom_path_offset.set_enabled(lane_positioning_enabled) - self.enable_lane_full_mode.set_enabled(lane_positioning_enabled) - - # Custom profile–dependent controls (prereq: Use Custom Tuning Profile) - custom_profile_enabled = p.get_bool("custom_profile") - self.pc_blend_ratio_high_C.set_enabled(custom_profile_enabled) - self.pc_blend_ratio_low_C.set_enabled(custom_profile_enabled) - - # Low Curvature PID Gain: requires BOTH lane positioning AND custom profile - self.LC_PID_gain.set_enabled(lane_positioning_enabled and custom_profile_enabled) - - # Preferred WiFi Network: enable when saved networks exist, refresh display value - self.preferred_network_btn.set_enabled(len(self._saved_networks) > 0) - self.preferred_network_btn.set_value(self._get_preferred_network_display()) - - def _on_network_updated(self, networks: list[Network]): - """Update saved networks list when WiFi networks are updated (callback from WifiManager).""" - self._saved_networks = [n for n in networks if self._wifi_manager.is_connection_saved(n.ssid)] - self.preferred_network_btn.set_enabled(len(self._saved_networks) > 0) - self.preferred_network_btn.set_value(self._get_preferred_network_display()) - - # Clear preferred if network was forgotten in NetworkManager - try: - favorite_value = self._params.get("WifiFavoriteSSID") - current_favorite = "" - if favorite_value: - if isinstance(favorite_value, bytes): - current_favorite = favorite_value.decode("utf-8", errors="replace").strip("\x00") - else: - current_favorite = str(favorite_value).strip("\x00") - if current_favorite: - saved_connections = self._wifi_manager._connections - if current_favorite not in saved_connections: - self._params.put("WifiFavoriteSSID", "") - cloudlog.info(f"Cleared preferred network '{current_favorite}' - network no longer saved in NetworkManager") - except Exception as e: - cloudlog.debug(f"Error checking preferred network: {e}") - - def _get_preferred_network_display(self) -> str: - """Get the display text for preferred network.""" - try: - favorite_value = self._params.get("WifiFavoriteSSID") - if favorite_value: - if isinstance(favorite_value, bytes): - favorite_ssid = favorite_value.decode("utf-8", errors="replace").strip("\x00") - else: - favorite_ssid = str(favorite_value).strip("\x00") - if favorite_ssid: - if len(favorite_ssid) > 20: - return favorite_ssid[:17] + "..." - return favorite_ssid - except Exception: - pass - return tr("None") - - def _select_preferred_network(self): - """Open horizontal-scroll panel to select preferred network (same pattern as WiFi network panel).""" - if len(self._saved_networks) == 0: - return - - panel = PreferredNetworkSelectMici( - self._wifi_manager, - self._saved_networks, - on_dismiss=lambda: self.preferred_network_btn.set_value(self._get_preferred_network_display()) - ) - gui_app.push_widget(panel) - - def _update_toggles(self): - ui_state.update_params() - - # Refresh toggles from params to mirror external changes - for key, item in self._refresh_toggles: - item.set_checked(ui_state.params.get_bool(key)) - - # Also update button state - self._update_buttons() - -# class BigChargingDialog(BigDialogBase): -# def __init__(self): -# super().__init__(None, None) - -# self._watt_label = MiciLabel("120kW", font_size=90) -# self._watt_label.set_position(150,75) - -# def _render(self, _): -# self._watt_label.render() -# return self._ret - -# def _update_state(self): -# super()._update_state() -# if self._swiping_away: -# self._ret = DialogResult.CANCEL diff --git a/selfdrive/ui/bp/mici/layouts/settings/vehicle_mici.py b/selfdrive/ui/bp/mici/layouts/settings/vehicle_mici.py deleted file mode 100644 index 3c0257b6d0..0000000000 --- a/selfdrive/ui/bp/mici/layouts/settings/vehicle_mici.py +++ /dev/null @@ -1,103 +0,0 @@ -""" -BluePilot: MICI vehicle menu — current vehicle, clear manual fingerprint, select vehicle. - -Uses horizontal NavScroller (same pattern as WiFi / preferred network). -""" - -from __future__ import annotations - -import os -from collections.abc import Callable - -from openpilot.common.basedir import BASEDIR -from openpilot.selfdrive.ui.bp.mici.widgets.button_bp import BigButtonBP -from openpilot.selfdrive.ui.bp.mici.widgets.vehicle_select_mici import ( - VehicleMakeSelectMici, - load_car_platforms, -) -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.widgets.scroller import NavScroller - -CAR_LIST_JSON = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "car", "car_list.json") - - -def _truncate(s: str, max_len: int = 36) -> str: - s = s.strip() - if len(s) <= max_len: - return s - return s[: max_len - 3] + "..." - - -def get_vehicle_status_text() -> tuple[str, str]: - """ - Returns (title_line, value_line) for the current vehicle display. - Mirrors TICI PlatformSelector.refresh() semantics. - """ - if bundle := ui_state.params.get("CarPlatformBundle"): - name = bundle.get("name", "") - if isinstance(name, bytes): - name = name.decode("utf-8", errors="replace") - return (tr("manual selection"), _truncate(str(name))) - if ui_state.CP is not None and ui_state.CP.carFingerprint != "MOCK": - fp = str(ui_state.CP.carFingerprint) - return (tr("auto fingerprint"), _truncate(fp)) - return (tr("vehicle"), tr("unrecognized")) - - -class VehicleLayoutMici(NavScroller): - """Three-button horizontal strip: current vehicle | clear | select.""" - - def __init__(self, back_callback: Callable[[], None]): - super().__init__() - self.set_back_callback(back_callback) - self._platforms: dict = {} - try: - self._platforms = load_car_platforms() - except OSError as e: - from openpilot.common.swaglog import cloudlog - - cloudlog.error(f"MICI vehicle: could not load {CAR_LIST_JSON}: {e}") - - self._btn_current = BigButtonBP(tr("current vehicle"), "", "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png") - self._btn_clear = BigButtonBP(tr("clear vehicle"), "", "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png") - self._btn_select = BigButtonBP(tr("select vehicle"), "", "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png") - - self._btn_current.set_enabled(False) - self._btn_clear.set_click_callback(self._on_clear) - self._btn_select.set_click_callback(self._on_select) - - self._scroller.add_widgets([self._btn_current, self._btn_clear, self._btn_select]) - - def show_event(self): - super().show_event() - ui_state.update_params() - self._refresh_display() - - def _update_state(self): - super()._update_state() - ui_state.update_params() - self._refresh_display() - - def _refresh_display(self): - t, v = get_vehicle_status_text() - self._btn_current.set_text(t) - self._btn_current.set_value(v) - has_manual = bool(ui_state.params.get("CarPlatformBundle")) - self._btn_clear.set_enabled(has_manual) - self._btn_select.set_enabled(len(self._platforms) > 0) - - def _on_clear(self): - if ui_state.params.get("CarPlatformBundle"): - ui_state.params.remove("CarPlatformBundle") - self._refresh_display() - - def _on_select(self): - if not self._platforms: - return - - def on_complete(): - self._refresh_display() - - gui_app.push_widget(VehicleMakeSelectMici(self._platforms, on_stack_done=on_complete)) diff --git a/selfdrive/ui/bp/mici/layouts/settings_bp.py b/selfdrive/ui/bp/mici/layouts/settings_bp.py deleted file mode 100644 index 1d4f28d727..0000000000 --- a/selfdrive/ui/bp/mici/layouts/settings_bp.py +++ /dev/null @@ -1,92 +0,0 @@ -"""BluePilot MICI settings landing — horizontal row of category tiles. - -Replaces stock SettingsLayout (and sunnypilot's SettingsLayoutSP) with the -mockup-faithful layout: a "Settings" topbar at top, then a row of frosted -category tiles (5 visible at once on a 536px screen) drawn with feather-style -procedural icons. - -The two existing BP-only panels (Vehicle and BluePilot) are kept as the 6th -and 7th tiles so functionality stays at parity — they scroll into view. -""" -import pyray as rl -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.widgets.nav_widget import NavWidget -from openpilot.system.ui.widgets.scroller import _Scroller - -from openpilot.selfdrive.ui.bp.mici.widgets.cards import BigCategoryTile -from openpilot.selfdrive.ui.bp.mici.widgets.bg_radial import BPRadialBackground -from openpilot.selfdrive.ui.bp.mici.widgets.bp_topbar import BPTopbar, TOPBAR_HEIGHT - - -# --- new BP-styled panels (mirror stock content / replace legacy BP panels) --- -from openpilot.selfdrive.ui.bp.mici.layouts.panels.toggles_bp import TogglesLayoutBP -from openpilot.selfdrive.ui.bp.mici.layouts.panels.network_bp import NetworkLayoutBP -from openpilot.selfdrive.ui.bp.mici.layouts.panels.device_bp import DeviceLayoutBP -from openpilot.selfdrive.ui.bp.mici.layouts.panels.firehose_bp import FirehoseLayoutBP -from openpilot.selfdrive.ui.bp.mici.layouts.panels.developer_bp import DeveloperLayoutBP -from openpilot.selfdrive.ui.bp.mici.layouts.bluepilot_bp import BluePilotLayoutBP -from openpilot.selfdrive.ui.bp.mici.layouts.vehicle_bp import VehicleLayoutBP - - -# Each tile: (panel key, display label, icon PNG path). -_TILES: list[tuple[str, str, str]] = [ - ("toggles", "Toggles", "icons_mici/settings.png"), - ("network", "Network", "icons_mici/settings/network/wifi_strength_full.png"), - ("device", "Device", "icons_mici/settings/device_icon.png"), - ("firehose", "Firehose", "icons_mici/settings/firehose.png"), - ("developer", "Developer", "icons_mici/settings/developer_icon.png"), - ("vehicle", "Vehicle", "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"), - ("bluepilot", "BluePilot", "icons_mici/settings/car_icon.png"), -] - - -class SettingsLayoutBP(NavWidget): - def __init__(self): - super().__init__() - self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - - self._bg = self._child(BPRadialBackground()) - self._topbar = self._child(BPTopbar(title="Settings", on_back=self._on_back)) - - # Lazy panel instantiation so opening Settings doesn't pay WifiManager - # init costs unless the user actually drills into a panel. - panels: dict[str, object] = {} - - def _ensure(name): - if name not in panels: - panels[name] = { - "toggles": TogglesLayoutBP, - "network": NetworkLayoutBP, - "device": DeviceLayoutBP, - "firehose": FirehoseLayoutBP, - "developer": DeveloperLayoutBP, - "vehicle": lambda: VehicleLayoutBP(back_callback=gui_app.pop_widget), - "bluepilot": lambda: BluePilotLayoutBP(back_callback=gui_app.pop_widget), - }[name]() - return panels[name] - - tiles = [] - for key, label, icon_path in _TILES: - tile = BigCategoryTile(label=label, icon=icon_path) - tile.set_click_callback(lambda n=key: gui_app.push_widget(_ensure(n))) - tiles.append(tile) - - # Horizontal scroller below the topbar. Scroll indicator stays on so the - # user knows there's more to scroll into; edge shadows are off because the - # frosted-tile borders already define the row visually and the dark - # vignette they draw reads as a "black section" next to the tiles. - self._scroller = self._child(_Scroller( - tiles, horizontal=True, snap_items=False, - spacing=6, pad=10, - scroll_indicator=True, edge_shadows=False, - )) - - def _on_back(self): - gui_app.pop_widget() - - def _render(self, _): - r = self._rect - self._bg.render(r) - self._topbar.render(rl.Rectangle(r.x, r.y, r.width, TOPBAR_HEIGHT)) - body = rl.Rectangle(r.x, r.y + TOPBAR_HEIGHT, r.width, r.height - TOPBAR_HEIGHT) - self._scroller.render(body) diff --git a/selfdrive/ui/bp/mici/layouts/vehicle_bp.py b/selfdrive/ui/bp/mici/layouts/vehicle_bp.py deleted file mode 100644 index e0bff32d4c..0000000000 --- a/selfdrive/ui/bp/mici/layouts/vehicle_bp.py +++ /dev/null @@ -1,62 +0,0 @@ -"""BP-styled vehicle panel — port of selfdrive/ui/bp/mici/layouts/settings/vehicle_mici.py. - -Same backend logic and dialogs; renders as a paged carousel matching the rest -of the BP UI. -""" -from collections.abc import Callable - -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.multilang import tr -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.selfdrive.ui.bp.mici.layouts.settings.vehicle_mici import CAR_LIST_JSON -from openpilot.selfdrive.ui.bp.mici.widgets.vehicle_select_mici import ( - VehicleMakeSelectMici, load_car_platforms, -) - - -def _vehicle_value_text() -> str: - """Full (non-truncated) current-vehicle display string. The card auto-shrinks - to fit, so we don't pre-elide here.""" - if bundle := ui_state.params.get("CarPlatformBundle"): - name = bundle.get("name", "") - if isinstance(name, bytes): - name = name.decode("utf-8", errors="replace") - return str(name).strip() or tr("unrecognized") - if ui_state.CP is not None and ui_state.CP.carFingerprint != "MOCK": - return str(ui_state.CP.carFingerprint) - return tr("unrecognized") - -from openpilot.selfdrive.ui.bp.mici.widgets.sub_panel import BPSubPanel -from openpilot.selfdrive.ui.bp.mici.widgets.cards import StatCard, BigButtonCard - - -class VehicleLayoutBP(BPSubPanel): - TITLE = "Vehicle" - - def __init__(self, back_callback: Callable | None = None): - self._platforms: dict = {} - try: - self._platforms = load_car_platforms() - except OSError as e: - from openpilot.common.swaglog import cloudlog - cloudlog.error(f"BP vehicle: could not load {CAR_LIST_JSON}: {e}") - super().__init__(back_callback=back_callback) - - def _on_clear(self): - ui_state.params.remove("CarPlatformBundle") - - def _on_select(self): - if not self._platforms: - return - gui_app.push_widget(VehicleMakeSelectMici(self._platforms)) - - def _build_pages(self): - return [ - StatCard( - tr("Current Vehicle"), - _vehicle_value_text, - variant="small", - ), - BigButtonCard(tr("Clear Vehicle"), on_click=self._on_clear), - BigButtonCard(tr("Select Vehicle"), on_click=self._on_select), - ] diff --git a/selfdrive/ui/bp/mici/layouts/wifi_ui_bp.py b/selfdrive/ui/bp/mici/layouts/wifi_ui_bp.py deleted file mode 100644 index 6bc50fc7ee..0000000000 --- a/selfdrive/ui/bp/mici/layouts/wifi_ui_bp.py +++ /dev/null @@ -1,126 +0,0 @@ -"""BP Wi-Fi UI — paged carousel of SSIDs. - -Drop-in replacement for selfdrive/ui/mici/layouts/settings/network/wifi_ui.py -that renders the SSID list as one full-screen card per network. Tap a card to -connect (or to push a BPInputDialog if a password is needed). Currently does -not expose a "forget" affordance; that can be added with a long-press in a -future pass. -""" -import pyray as rl -from openpilot.common.swaglog import cloudlog -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType -from openpilot.system.ui.widgets.nav_widget import NavWidget - -from openpilot.selfdrive.ui.bp.mici.widgets.bg_radial import BPRadialBackground -from openpilot.selfdrive.ui.bp.mici.widgets.bp_topbar import BPTopbar, TOPBAR_HEIGHT -from openpilot.selfdrive.ui.bp.mici.widgets.paged_scroller import PagedScroller -from openpilot.selfdrive.ui.bp.mici.widgets.page_dots import PageDots -from openpilot.selfdrive.ui.bp.mici.widgets.cards import SsidRowCard -from openpilot.selfdrive.ui.bp.mici.widgets.bp_dialogs import BPInputDialog - - -DOTS_HEIGHT = 20 - - -class BPWifiUI(NavWidget): - TITLE = "Wi-Fi" - - def __init__(self, wifi_manager: WifiManager): - super().__init__() - self._wifi_manager = wifi_manager - self._networks: list[Network] = [] - self._cards: list[SsidRowCard] = [] - - self._bg = self._child(BPRadialBackground()) - # Pager + dots are rebuilt whenever the network list changes. - self._pager = self._child(PagedScroller([])) - self._topbar = self._child(BPTopbar( - title=self.TITLE, - on_back=self._on_back, - get_page_meta=lambda: (self._pager.current_page, self._pager.page_count), - )) - self._dots = self._child(PageDots( - get_current_page=lambda: self._pager.current_page, - get_page_count=lambda: self._pager.page_count, - )) - - self._wifi_manager.add_callbacks( - need_auth=self._on_need_auth, - networks_updated=self._on_networks_updated, - ) - - # ---- public ---- - def show_event(self): - super().show_event() - self._wifi_manager.set_active(True) - self._rebuild_pages(list(self._wifi_manager.networks)) - - def hide_event(self): - super().hide_event() - self._wifi_manager.set_active(False) - - def _on_back(self): - gui_app.pop_widget() - - def _on_networks_updated(self, networks: list[Network]): - self._rebuild_pages(networks) - - def _rebuild_pages(self, networks: list[Network]): - """Build one SsidRowCard per network and push them into the pager.""" - self._networks = networks - self._cards = [] - for net in networks: - strength_bars = max(0, min(4, int(net.strength * 4 / 100))) - connected = (self._wifi_manager.connected_ssid == net.ssid) - secured = (net.security_type != SecurityType.OPEN) - card = SsidRowCard( - ssid=net.ssid, - secured=secured, - strength=strength_bars, - connected=connected, - on_click=lambda ssid=net.ssid: self._connect_to(ssid), - ) - self._cards.append(card) - - # Replace the pager's items in-place. - self._pager._items = self._cards - # Re-add to underlying scroller (clear + re-add). - self._pager._scroller._items = [] - for c in self._cards: - self._pager._scroller.add_widget(c) - - # ---- connect flow ---- - def _connect_to(self, ssid: str): - network = next((n for n in self._networks if n.ssid == ssid), None) - if network is None: - cloudlog.warning(f"BPWifiUI: unknown SSID {ssid!r}") - return - - if self._wifi_manager.is_connection_saved(network.ssid): - self._wifi_manager.activate_connection(network.ssid) - elif network.security_type == SecurityType.OPEN: - self._wifi_manager.connect_to_network(network.ssid, "") - else: - self._on_need_auth(network.ssid, incorrect_password=False) - - def _on_need_auth(self, ssid: str, incorrect_password: bool = True): - hint = "wrong password — re-enter" if incorrect_password else "enter password..." - gui_app.push_widget(BPInputDialog( - hint, "", minimum_length=8, - confirm_callback=lambda pw: self._wifi_manager.connect_to_network(ssid, pw), - )) - - # ---- render ---- - def _render(self, _): - r = self._rect - self._bg.render(r) - - topbar_rect = rl.Rectangle(r.x, r.y, r.width, TOPBAR_HEIGHT) - self._topbar.render(topbar_rect) - - pager_y = r.y + TOPBAR_HEIGHT - pager_h = r.height - TOPBAR_HEIGHT - DOTS_HEIGHT - self._pager.render(rl.Rectangle(r.x, pager_y, r.width, pager_h)) - - self._dots.render(rl.Rectangle(r.x, r.y + r.height - DOTS_HEIGHT, r.width, DOTS_HEIGHT)) diff --git a/selfdrive/ui/bp/mici/widgets/aurora_wordmark.py b/selfdrive/ui/bp/mici/widgets/aurora_wordmark.py deleted file mode 100644 index ea03752750..0000000000 --- a/selfdrive/ui/bp/mici/widgets/aurora_wordmark.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Aurora wordmark — "bluepilot" with a breathing radial glow behind it. - -Mode is driven by a callable returning 'ready' / 'experimental' / 'offline'. -The aurora is drawn as concentric `draw_circle_gradient` passes for a soft -radial bloom; it scales 0.96 -> 1.05 over a 3.6s sine cycle so the home feels -alive without clutter. -""" -import math -import pyray as rl -from collections.abc import Callable - -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.selfdrive.ui.bp.mici.widgets import bp_palette as P - - -class AuroraWordmark(Widget): - AURORA_BASE_RADIUS = 220 # px before breathing scale - BREATHE_PERIOD = 3.6 # seconds - - def __init__(self, get_mode: Callable[[], str]): - super().__init__() - self._get_mode = get_mode - self._mode = "ready" - - self._label = UnifiedLabel( - "bluepilot", - font_size=P.FS_WORDMARK, - font_weight=FontWeight.DISPLAY, - text_color=P.WORDMARK_BLUE, - max_width=520, - wrap_text=False, - letter_spacing=-0.045, - ) - - def _update_state(self): - self._mode = self._get_mode() - color = { - "experimental": P.WORDMARK_AMBER, - "offline": P.WORDMARK_GREY, - }.get(self._mode, P.WORDMARK_BLUE) - self._label.set_text_color(color) - - def _render(self, _): - r = self._rect - cx = r.x + r.width / 2 - cy = r.y + r.height / 2 - - # ---- Aurora glow (skipped offline) ---- - if self._mode != "offline": - t = rl.get_time() - # 0..1 sine wave at the breathe period - phase = (math.sin(t * (2 * math.pi / self.BREATHE_PERIOD)) + 1) * 0.5 - scale = 0.96 + phase * 0.09 - radius = self.AURORA_BASE_RADIUS * scale - inner = { - "experimental": P.AURORA_AMBER_INNER, - }.get(self._mode, P.AURORA_BLUE_INNER) - # Three radial passes give a soft falloff with fixed-cost circle draws. - for r_mul, alpha_mul in ((1.00, 1.00), (0.62, 0.85), (0.35, 0.70)): - col = rl.Color(inner.r, inner.g, inner.b, int(inner.a * alpha_mul)) - rl.draw_circle_gradient(rl.Vector2(int(cx), int(cy)), radius * r_mul, col, P.AURORA_OUTER) - - # ---- Wordmark ---- - # UnifiedLabel doesn't expose center alignment cleanly with wrap_text=False, - # so we measure once and position manually. - text_w = self._label.text_width - if text_w <= 0: - # First render: text_width isn't cached yet. Place at left=0 so the label - # renders + caches; next frame we'll center. - self._label.set_position(cx - 240, cy - P.FS_WORDMARK / 2) - else: - self._label.set_position(cx - text_w / 2, cy - P.FS_WORDMARK / 2) - self._label.render() diff --git a/selfdrive/ui/bp/mici/widgets/bg_radial.py b/selfdrive/ui/bp/mici/widgets/bg_radial.py deleted file mode 100644 index 5e2ba64666..0000000000 --- a/selfdrive/ui/bp/mici/widgets/bg_radial.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Blue/black radial gradient — paints the BP screen backdrop. - -Mirrors the mockup CSS: - radial-gradient(120% 140% at 0% 0%, - rgba(74,140,255,0.42) 0%, - rgba(20,40,90,0.28) 28%, - rgba(2,6,15,0.95) 62%, - #02060f 100%); - -raylib has no rectangle radial gradient, so we paint a deep-navy base then -overlay three concentric soft circles centered at the top-left corner. The -result is visually very close to the CSS radial when viewed on the 536x240 -canvas. -""" -import pyray as rl - -from openpilot.system.ui.widgets import Widget -from openpilot.selfdrive.ui.bp.mici.widgets import bp_palette as P - - -class BPRadialBackground(Widget): - def __init__(self): - super().__init__() - - def _render(self, _): - r = self._rect - - # Base: solid deep navy / near-black. - rl.draw_rectangle_rec(r, P.BG_DEEP) - - # Concentric "light" stops centered at the top-left corner. Each call - # draws inner_color at center fading to fully transparent at radius. - # Larger radii first so the brighter inner stops paint on top. - cx, cy = int(r.x), int(r.y) - diag = (r.width ** 2 + r.height ** 2) ** 0.5 - - # Outer fade (62% stop) — barely-perceptible navy lift at the edges of - # the gradient extent. - rl.draw_circle_gradient(rl.Vector2(cx, cy), diag * 1.2, - rl.Color(0x14, 0x28, 0x5A, int(0.50 * 255)), - rl.Color(0, 0, 0, 0)) - - # Mid stop (28%) — navy tint covering most of the upper-left half. - rl.draw_circle_gradient(rl.Vector2(cx, cy), diag * 0.85, - rl.Color(0x14, 0x28, 0x5A, int(0.65 * 255)), - rl.Color(0, 0, 0, 0)) - - # Inner glow (0%) — bright blue near the corner. - rl.draw_circle_gradient(rl.Vector2(cx, cy), diag * 0.55, - rl.Color(0x4A, 0x8C, 0xFF, int(0.42 * 255)), - rl.Color(0, 0, 0, 0)) diff --git a/selfdrive/ui/bp/mici/widgets/big_input_dialog_bp.py b/selfdrive/ui/bp/mici/widgets/big_input_dialog_bp.py deleted file mode 100644 index d52b70c58f..0000000000 --- a/selfdrive/ui/bp/mici/widgets/big_input_dialog_bp.py +++ /dev/null @@ -1,180 +0,0 @@ -import math -import pyray as rl -from collections.abc import Callable -from openpilot.system.ui.widgets import DialogResult -from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.selfdrive.ui.bp.mici.widgets.keyboard_bp import MiciKeyboardBP -from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos -from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialogBase - -DEBUG = False -PADDING = 20 - -class BigInputDialogBP(BigDialogBase): - BACK_TOUCH_AREA_PERCENTAGE = 0.2 - BACKSPACE_RATE = 25 # hz - - def __init__(self, - hint: str, - default_text: str = "", - minimum_length: int = 1, - confirm_callback: Callable[[str], None] = None, - show_special_keys: bool = False): - super().__init__() - self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)), - font_weight=FontWeight.MEDIUM) - self._keyboard = MiciKeyboardBP(show_special_keys=show_special_keys) - self._keyboard.set_text(default_text) - self._minimum_length = minimum_length - - self._backspace_held_time: float | None = None - self._backspace_repeated = False - - self._backspace_img = gui_app.texture("icons_mici/settings/keyboard/backspace.png", 42, 36) - self._backspace_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) - - self._enter_img = gui_app.texture("icons_mici/settings/keyboard/enter.png", 76, 62) - self._enter_disabled_img = gui_app.texture("icons_mici/settings/keyboard/enter_disabled.png", 76, 62) - self._enter_img_alpha = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) - - # rects for top buttons - self._top_left_button_rect = rl.Rectangle(0, 0, 0, 0) - self._top_right_button_rect = rl.Rectangle(0, 0, 0, 0) - self._enter_pressed = False - self._backspace_pressed = False - - self._ret = None # Only set to CONFIRM when user taps enter; prevents AttributeError in _render before first confirm - def confirm_callback_wrapper(): - self._ret = DialogResult.CONFIRM - if confirm_callback: - confirm_callback(self._keyboard.text()) - self._confirm_callback = confirm_callback_wrapper - - def _update_state(self): - super()._update_state() - - last_mouse_event = gui_app.last_mouse_event - if last_mouse_event.left_down and self._backspace_pressed and rl.check_collision_point_rec(last_mouse_event.pos, self._top_right_button_rect): - if self._backspace_held_time is None: - self._backspace_held_time = rl.get_time() - - if rl.get_time() - self._backspace_held_time > 0.5: - if gui_app.frame % round(gui_app.target_fps / self.BACKSPACE_RATE) == 0: - self._keyboard.backspace() - self._backspace_repeated = True - - else: - self._backspace_held_time = None - self._backspace_repeated = False - - def _render(self, _): - text_input_size = 35 - - # draw current text so far below everything. text floats left but always stays in view - text = self._keyboard.text() - candidate_char = self._keyboard.get_candidate_character() - text_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), text + candidate_char or self._hint_label.text, text_input_size) - text_x = PADDING * 2 + self._enter_img.width - - # text needs to move left if we're at the end where right button is - text_rect = rl.Rectangle(text_x, - int(self._rect.y + PADDING), - # clip width to right button when in view - int(self._rect.width - text_x - PADDING * 2 - self._enter_img.width + 5), # TODO: why 5? - int(text_size.y)) - - # draw rounded background for text input - bg_block_margin = 5 - text_field_rect = rl.Rectangle(text_rect.x - bg_block_margin, text_rect.y - bg_block_margin, - text_rect.width + bg_block_margin * 2, text_input_size + bg_block_margin * 2) - - # draw text input - # push text left with a gradient on left side if too long - if text_size.x > text_rect.width: - text_x -= text_size.x - text_rect.width - - rl.begin_scissor_mode(int(text_rect.x), int(text_rect.y), int(text_rect.width), int(text_rect.height)) - rl.draw_text_ex(gui_app.font(FontWeight.ROMAN), text, rl.Vector2(text_x, text_rect.y), text_input_size, 0, rl.WHITE) - - # draw grayed out character user is hovering over - if candidate_char: - candidate_char_size = measure_text_cached(gui_app.font(FontWeight.ROMAN), candidate_char, text_input_size) - rl.draw_text_ex(gui_app.font(FontWeight.ROMAN), candidate_char, - rl.Vector2(min(text_x + text_size.x, text_rect.x + text_rect.width) - candidate_char_size.x, text_rect.y), - text_input_size, 0, rl.Color(255, 255, 255, 128)) - - rl.end_scissor_mode() - - # draw gradient on left side to indicate more text - if text_size.x > text_rect.width: - rl.draw_rectangle_gradient_h(int(text_rect.x), int(text_rect.y), 80, int(text_rect.height), - rl.BLACK, rl.BLANK) - - # draw cursor - if text: - blink_alpha = (math.sin(rl.get_time() * 6) + 1) / 2 - cursor_x = min(text_x + text_size.x + 3, text_rect.x + text_rect.width) - rl.draw_rectangle_rounded(rl.Rectangle(int(cursor_x), int(text_rect.y), 4, int(text_size.y)), - 1, 4, rl.Color(255, 255, 255, int(255 * blink_alpha))) - - # draw backspace icon with nice fade - self._backspace_img_alpha.update(255 * bool(text)) - backspace_x = int(self._rect.width - self._backspace_img.width - 27) - backspace_y = int(text_field_rect.y) - if self._backspace_img_alpha.x > 1: - color = rl.Color(255, 255, 255, int(self._backspace_img_alpha.x)) - rl.draw_texture(self._backspace_img, backspace_x, backspace_y, color) - - if not text and self._hint_label.text and not candidate_char: - # draw description if no text entered yet and not drawing candidate char - self._hint_label.render(text_field_rect) - - # draw enter button (enabled + disabled states, same as stock BigInputDialog) - self._enter_img_alpha.update(255 if (len(text) >= self._minimum_length) else 0) - enter_x = int(self._rect.x + 15) - enter_y = int(text_field_rect.y) - color = rl.Color(255, 255, 255, int(self._enter_img_alpha.x)) - rl.draw_texture(self._enter_img, enter_x, enter_y, color) - color = rl.Color(255, 255, 255, 255 - int(self._enter_img_alpha.x)) - rl.draw_texture(self._enter_disabled_img, enter_x, enter_y, color) - - # Use the visible icon bounds with a small padding instead of full header halves. - hit_pad = 14 - self._top_left_button_rect = rl.Rectangle( - enter_x - hit_pad, enter_y - hit_pad, - self._enter_img.width + hit_pad * 2, self._enter_img.height + hit_pad * 2) - self._top_right_button_rect = rl.Rectangle( - backspace_x - hit_pad, backspace_y - hit_pad, - self._backspace_img.width + hit_pad * 2, self._backspace_img.height + hit_pad * 2) - - # keyboard goes over everything - self._keyboard.render(self._rect) - - # draw debugging rect bounds - if DEBUG: - rl.draw_rectangle_lines_ex(text_field_rect, 1, rl.Color(100, 100, 100, 255)) - rl.draw_rectangle_lines_ex(text_rect, 1, rl.Color(0, 255, 0, 255)) - rl.draw_rectangle_lines_ex(self._top_right_button_rect, 1, rl.Color(0, 255, 0, 255)) - rl.draw_rectangle_lines_ex(self._top_left_button_rect, 1, rl.Color(0, 255, 0, 255)) - - return self._ret - - def _handle_mouse_press(self, mouse_pos: MousePos): - super()._handle_mouse_press(mouse_pos) - self._backspace_pressed = rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect) - self._enter_pressed = rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect) - if self._backspace_pressed: - self._backspace_repeated = False - - def _handle_mouse_release(self, mouse_pos: MousePos): - if self._backspace_pressed and rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect) and self._keyboard.text(): - if not self._backspace_repeated: - self._keyboard.backspace() - self._backspace_held_time = None - self._backspace_repeated = False - elif self._enter_pressed and rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect) and len(self._keyboard.text()) >= self._minimum_length: - self._confirm_callback() - self._enter_pressed = False - self._backspace_pressed = False diff --git a/selfdrive/ui/bp/mici/widgets/bp_dialogs.py b/selfdrive/ui/bp/mici/widgets/bp_dialogs.py deleted file mode 100644 index 43ef87c85e..0000000000 --- a/selfdrive/ui/bp/mici/widgets/bp_dialogs.py +++ /dev/null @@ -1,271 +0,0 @@ -"""BP-styled modal dialogs. - -Drop-in replacements for selfdrive/ui/mici/widgets/dialog.py: -- BPInfoDialog <- BigDialog (info / error toast — tap anywhere to dismiss) -- BPConfirmDialog <- BigConfirmationDialog (slide-to-confirm; reuses BigSlider/RedBigSlider internals) -- BPInputDialog <- BigInputDialog (text input; reuses MiciKeyboard inside a BP frame) - -All sit on the BP radial backdrop and use BP typography. -""" -import pyray as rl -from typing import Union -from collections.abc import Callable - -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos -from openpilot.system.ui.widgets.nav_widget import NavWidget -from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.widgets.slider import BigSlider, RedBigSlider -from openpilot.system.ui.widgets.mici_keyboard import MiciKeyboard -from openpilot.common.filter_simple import FirstOrderFilter - -from openpilot.selfdrive.ui.bp.mici.widgets.bg_radial import BPRadialBackground -from openpilot.selfdrive.ui.bp.mici.widgets import bp_palette as P - - -from openpilot.system.ui.widgets import Widget - -class _BPDialogBase(Widget): - """Common: BP backdrop + full-screen rect. - - Plain Widget (not NavWidget) — avoids the slide-in animation and the - full-screen black overlay that NavWidget._layout draws. We handle dismiss - manually via tap-to-close in _handle_mouse_release. - """ - def __init__(self): - super().__init__() - self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - self._bg = self._child(BPRadialBackground()) - - # NavWidget API shims so callers can still call .dismiss() / .is_dismissing - @property - def is_dismissing(self) -> bool: - return False - - def dismiss(self, callback=None): - gui_app.pop_widget() - if callback: - callback() - - -# ----------------------------------------------------------------- -# Info / error dialog -# ----------------------------------------------------------------- -class BPInfoDialog(_BPDialogBase): - """Tap anywhere outside the card (or on it) to dismiss.""" - def __init__(self, title: str, description: str = "", icon: Union[rl.Texture, None] = None): - super().__init__() - self._icon = icon - self._title = UnifiedLabel(title, font_size=P.FS_NAME, font_weight=FontWeight.BOLD, - text_color=P.TEXT, max_width=480, wrap_text=True, line_height=1.05) - self._desc = UnifiedLabel(description, font_size=P.FS_SUB, font_weight=FontWeight.MEDIUM, - text_color=P.MUTED, max_width=480, wrap_text=True, line_height=1.15) - - def _handle_mouse_release(self, mouse_pos: MousePos): - self.dismiss() - - def _render(self, _): - r = self._rect - self._bg.render(r) - - # Card - pad = 18 - card_w = min(int(r.width * 0.85), 480) - card_h = int(r.height * 0.78) - card_x = r.x + (r.width - card_w) / 2 - card_y = r.y + (r.height - card_h) / 2 - card = rl.Rectangle(card_x, card_y, card_w, card_h) - rl.draw_rectangle_rounded(card, 0.10, 16, P.PANEL_BG) - rl.draw_rectangle_rounded_lines_ex(card, 0.10, 16, 1, P.PANEL_BORDER) - - text_x = card_x + pad - text_max_w = card_w - pad * 2 - - # Title (top) - self._title.set_max_width(int(text_max_w)) - title_h = self._title.get_content_height(int(text_max_w)) + P.FS_NAME * 0.3 - self._title.set_rect(rl.Rectangle(text_x, card_y + pad, text_max_w, title_h)) - self._title.render() - - # Description (below title) - if self._desc.text: - self._desc.set_max_width(int(text_max_w)) - desc_y = card_y + pad + title_h + 4 - desc_h = self._desc.get_content_height(int(text_max_w)) + P.FS_SUB * 0.5 - self._desc.set_rect(rl.Rectangle(text_x, desc_y, text_max_w, desc_h)) - self._desc.render() - - -# ----------------------------------------------------------------- -# Confirm dialog (slide-to-confirm) -# ----------------------------------------------------------------- -class BPConfirmDialog(_BPDialogBase): - def __init__(self, title: str, icon: rl.Texture, confirm_callback: Callable[[], None], - exit_on_confirm: bool = True, red: bool = False): - super().__init__() - self._confirm_callback = confirm_callback - self._exit_on_confirm = exit_on_confirm - - Slider = RedBigSlider if red else BigSlider - self._slider = self._child(Slider(title, icon, confirm_callback=self._on_confirm)) - self._slider.set_enabled(lambda: self.enabled and not self.is_dismissing) - - def _on_confirm(self): - if self._exit_on_confirm: - self.dismiss(self._confirm_callback) - elif self._confirm_callback: - self._confirm_callback() - - def _update_state(self): - super()._update_state() - if self.is_dismissing and not self._slider.confirmed: - self._slider.reset() - - def _render(self, _): - r = self._rect - self._bg.render(r) - # The slider widget is fixed-size (520x180); center it. - sw = self._slider.rect.width - sh = self._slider.rect.height - sx = r.x + (r.width - sw) / 2 - sy = r.y + (r.height - sh) / 2 - self._slider.render(rl.Rectangle(sx, sy, sw, sh)) - - -# ----------------------------------------------------------------- -# Input dialog (BP frame around the existing MICI keyboard) -# ----------------------------------------------------------------- -class BPInputDialog(_BPDialogBase): - BACK_TOUCH_AREA_PERCENTAGE = 0.2 # smaller — most of screen is the keyboard - BACKSPACE_RATE = 25 - - TEXT_BAR_H = 56 - - def __init__(self, - hint: str, - default_text: str = "", - minimum_length: int = 1, - confirm_callback: Callable[[str], None] | None = None, - auto_return_to_letters: str = ""): - super().__init__() - self._hint = hint - self._minimum_length = minimum_length - - self._keyboard = self._child(MiciKeyboard(auto_return_to_letters=auto_return_to_letters)) - self._keyboard.set_text(default_text) - self._keyboard.set_enabled(lambda: self.enabled and not self.is_dismissing) - - self._hint_label = UnifiedLabel(hint, font_size=P.FS_SUB, font_weight=FontWeight.MEDIUM, - text_color=P.MUTED, max_width=520, wrap_text=False) - self._text_label = UnifiedLabel("", font_size=32, font_weight=FontWeight.BOLD, - text_color=P.TEXT, max_width=520, wrap_text=False) - - self._backspace_held_time: float | None = None - self._backspace_repeated = False - self._backspace_img = gui_app.texture("icons_mici/settings/keyboard/backspace.png", 36, 30) - self._enter_img = gui_app.texture("icons_mici/settings/keyboard/enter.png", 56, 46) - self._enter_disabled_img = gui_app.texture("icons_mici/settings/keyboard/enter_disabled.png", 56, 46) - - self._top_left_button_rect = rl.Rectangle(0, 0, 0, 0) - self._top_right_button_rect = rl.Rectangle(0, 0, 0, 0) - self._enter_pressed = False - self._backspace_pressed = False - - def _do_confirm(): - text = self._keyboard.text() - self.dismiss((lambda: confirm_callback(text)) if confirm_callback else None) - self._do_confirm = _do_confirm - - # ---- input ---- - def _handle_mouse_press(self, mouse_pos: MousePos): - super()._handle_mouse_press(mouse_pos) - self._backspace_pressed = rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect) - self._enter_pressed = rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect) - if self._backspace_pressed: - self._backspace_repeated = False - - def _handle_mouse_release(self, mouse_pos: MousePos): - if self._backspace_pressed and rl.check_collision_point_rec(mouse_pos, self._top_right_button_rect): - # Backspace - if not self._backspace_repeated: - self._keyboard.backspace() - self._backspace_held_time = None - self._backspace_repeated = False - elif self._enter_pressed and rl.check_collision_point_rec(mouse_pos, self._top_left_button_rect): - # Enter - if len(self._keyboard.text()) >= self._minimum_length: - self._do_confirm() - self._enter_pressed = False - self._backspace_pressed = False - - def _update_state(self): - super()._update_state() - if self.is_dismissing: - self._enter_pressed = False - self._backspace_pressed = False - self._backspace_held_time = None - return - - # Held backspace repeat (mirrors stock BigInputDialog behavior) - last = gui_app.last_mouse_event - if last.left_down and self._backspace_pressed and rl.check_collision_point_rec(last.pos, self._top_right_button_rect): - if self._backspace_held_time is None: - self._backspace_held_time = rl.get_time() - if rl.get_time() - self._backspace_held_time > 0.5: - if gui_app.frame % round(gui_app.target_fps / self.BACKSPACE_RATE) == 0: - self._keyboard.backspace() - self._backspace_repeated = True - else: - self._backspace_held_time = None - self._backspace_repeated = False - - def _render(self, _): - r = self._rect - self._bg.render(r) - - # ---- Header bar: hint (left), text, backspace (right) ---- - bar_y = r.y + 8 - bar_h = self.TEXT_BAR_H - pad_x = 14 - - # Backspace button (top-right) - bs_size = 56 - self._top_right_button_rect = rl.Rectangle( - r.x + r.width - bs_size - pad_x, bar_y, bs_size, bar_h) - rl.draw_rectangle_rounded(self._top_right_button_rect, 0.30, 8, - rl.Color(255, 255, 255, int(0.06 * 255))) - bs_x = self._top_right_button_rect.x + (bs_size - self._backspace_img.width) / 2 - bs_y = self._top_right_button_rect.y + (bar_h - self._backspace_img.height) / 2 - rl.draw_texture(self._backspace_img, int(bs_x), int(bs_y), P.TEXT) - - # Enter button (top-right just left of backspace) — only when input meets min length - text = self._keyboard.text() - enter_enabled = len(text) >= self._minimum_length - enter_size = 56 - self._top_left_button_rect = rl.Rectangle( - self._top_right_button_rect.x - enter_size - 8, bar_y, enter_size, bar_h) - rl.draw_rectangle_rounded(self._top_left_button_rect, 0.30, 8, - rl.Color(P.ACCENT.r, P.ACCENT.g, P.ACCENT.b, - int((0.45 if enter_enabled else 0.10) * 255))) - en_img = self._enter_img if enter_enabled else self._enter_disabled_img - en_x = self._top_left_button_rect.x + (enter_size - en_img.width) / 2 - en_y = self._top_left_button_rect.y + (bar_h - en_img.height) / 2 - rl.draw_texture(en_img, int(en_x), int(en_y), P.TEXT) - - # Text input area (left of the buttons) - text_box_w = self._top_left_button_rect.x - r.x - pad_x * 2 - text_box = rl.Rectangle(r.x + pad_x, bar_y, text_box_w, bar_h) - rl.draw_rectangle_rounded(text_box, 0.30, 8, rl.Color(255, 255, 255, int(0.04 * 255))) - - if not text: - self._hint_label.set_text(self._hint) - self._hint_label.set_position(text_box.x + 12, text_box.y + (bar_h - P.FS_SUB) / 2) - self._hint_label.render() - else: - self._text_label.set_text(text if len(text) < 30 else text[-30:]) - self._text_label.set_position(text_box.x + 12, text_box.y + (bar_h - 32) / 2) - self._text_label.render() - - # ---- Keyboard fills the rest ---- - kb_y = bar_y + bar_h + 6 - kb_h = r.y + r.height - kb_y - 6 - self._keyboard.render(rl.Rectangle(r.x + 4, kb_y, r.width - 8, kb_h)) diff --git a/selfdrive/ui/bp/mici/widgets/bp_palette.py b/selfdrive/ui/bp/mici/widgets/bp_palette.py deleted file mode 100644 index f134f901ea..0000000000 --- a/selfdrive/ui/bp/mici/widgets/bp_palette.py +++ /dev/null @@ -1,50 +0,0 @@ -"""BluePilot MICI palette — single source of truth for the new home + settings UI. - -All colors mirror mockups/mici_home.html so a tweak here propagates everywhere. -""" -import pyray as rl - -# Accent (blue → cyan) -ACCENT = rl.Color(0x4A, 0x8C, 0xFF, 0xFF) # --accent -ACCENT2 = rl.Color(0x7E, 0xE0, 0xFF, 0xFF) # --accent2 -HALO = rl.Color(0x4A, 0x8C, 0xFF, int(0.45 * 255)) - -# Text -TEXT = rl.Color(0xF4, 0xF8, 0xFF, 0xFF) -MUTED = rl.Color(0xF4, 0xF8, 0xFF, int(0.55 * 255)) -DIM = rl.Color(0xF4, 0xF8, 0xFF, int(0.30 * 255)) - -# Backgrounds -BG_DEEP = rl.Color(0x02, 0x06, 0x0F, 0xFF) -BG_TOPLEFT = rl.Color(0x14, 0x32, 0x6E, 0xFF) # blue glow corner -PANEL_BG = rl.Color(0xFF, 0xFF, 0xFF, int(0.04 * 255)) -PANEL_BORDER = rl.Color(0xFF, 0xFF, 0xFF, int(0.07 * 255)) - -# Wordmark gradient targets (we draw with a single solid tint per state until -# we ship pre-baked gradient PNGs) -WORDMARK_BLUE = rl.Color(0xCF, 0xEA, 0xFF, 0xFF) -WORDMARK_AMBER = rl.Color(0xFF, 0xD8, 0x7A, 0xFF) -WORDMARK_GREY = rl.Color(0x99, 0xA3, 0xB4, 0xFF) - -# Status -READY = rl.Color(0x4A, 0xDE, 0x80, 0xFF) -WARN = rl.Color(0xFB, 0xBF, 0x24, 0xFF) -OFFLINE = rl.Color(0x6B, 0x72, 0x80, 0xFF) -DANGER = rl.Color(0xF8, 0x71, 0x71, 0xFF) - -# Aurora glow color tints (used inner color for radial circle gradient) -AURORA_BLUE_INNER = rl.Color(0x4A, 0x8C, 0xFF, int(0.32 * 255)) -AURORA_AMBER_INNER = rl.Color(0xFB, 0xBF, 0x24, int(0.36 * 255)) -AURORA_GREY_INNER = rl.Color(0x78, 0x82, 0x96, int(0.18 * 255)) -AURORA_OUTER = rl.Color(0x00, 0x00, 0x00, 0x00) - -# Font sizes (post FONT_SCALE: framework multiplies by 1.16 internally for MICI) -FS_WORDMARK = 96 -FS_PILL = 26 -FS_META = 22 -FS_NAME = 40 # one-per-page card title -FS_SUB = 24 # one-per-page card description -FS_STAT_KEY = 22 -FS_STAT_VAL = 80 -FS_BTN_LBL = 44 -FS_PAGE_CTR = 18 diff --git a/selfdrive/ui/bp/mici/widgets/bp_topbar.py b/selfdrive/ui/bp/mici/widgets/bp_topbar.py deleted file mode 100644 index b850999cb6..0000000000 --- a/selfdrive/ui/bp/mici/widgets/bp_topbar.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Topbar for BP sub-panels: chevron back + title + (optional) "n/N" page counter. - -Sized at 50px tall to match the mockup. The back chevron is a normal Widget so -it gets the framework's pressed/hover states for free. -""" -import pyray as rl -from collections.abc import Callable - -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.selfdrive.ui.bp.mici.widgets import bp_palette as P - - -TOPBAR_HEIGHT = 50 -BACK_HIT_SIZE = 56 -BACK_VISIBLE = 28 -TITLE_FONT = 30 - - -class _BackChevron(Widget): - """Procedural left-chevron with a touch hit rect bigger than the visible glyph.""" - def __init__(self, on_back: Callable | None = None): - super().__init__() - self._on_back = on_back - - def _handle_mouse_release(self, _): - if self._on_back: - self._on_back() - - def _render(self, _): - r = self._rect - color = rl.Color(P.TEXT.r, P.TEXT.g, P.TEXT.b, 240) if not self.is_pressed else P.ACCENT2 - cx = r.x + r.width / 2 - cy = r.y + r.height / 2 - s = BACK_VISIBLE / 2 - # "<" — two line segments. - rl.draw_line_ex(rl.Vector2(cx + s * 0.5, cy - s), - rl.Vector2(cx - s * 0.5, cy), thickness := 4, color) - rl.draw_line_ex(rl.Vector2(cx - s * 0.5, cy), - rl.Vector2(cx + s * 0.5, cy + s), thickness, color) - - -class BPTopbar(Widget): - def __init__(self, title: str, on_back: Callable | None = None, - get_page_meta: Callable[[], tuple[int, int]] | None = None): - """ - title: shown at left next to back chevron. - on_back: tap callback for the back chevron. - get_page_meta: optional callable returning (current_page_zero_indexed, count) - shown as "n/N" at right. - """ - super().__init__() - self._on_back = on_back - self._get_page_meta = get_page_meta - - self._back = self._child(_BackChevron(on_back=on_back)) - - self._title = UnifiedLabel( - title, - font_size=TITLE_FONT, - font_weight=FontWeight.BOLD, - text_color=P.TEXT, - max_width=400, - wrap_text=False, - ) - self._counter = UnifiedLabel( - "", - font_size=P.FS_PAGE_CTR, - font_weight=FontWeight.MEDIUM, - text_color=P.MUTED, - max_width=120, - wrap_text=False, - ) - - def _render(self, _): - r = self._rect - # Subtle gradient background so scrolled body content visually clips - rl.draw_rectangle_gradient_v(int(r.x), int(r.y), int(r.width), int(r.height), - rl.Color(0x02, 0x06, 0x0F, int(0.55 * 255)), - rl.Color(0x02, 0x06, 0x0F, 0)) - - # Back chevron — touch hit rect is BACK_HIT_SIZE square - back_x = r.x + 8 - back_y = r.y + (r.height - BACK_HIT_SIZE) / 2 - self._back.render(rl.Rectangle(back_x, back_y, BACK_HIT_SIZE, BACK_HIT_SIZE)) - - # Title — to the right of the chevron - title_x = back_x + BACK_HIT_SIZE + 4 - title_y = r.y + (r.height - TITLE_FONT) / 2 - 2 - self._title.set_position(title_x, title_y) - self._title.render() - - # Counter — right edge - if self._get_page_meta is not None: - cur, n = self._get_page_meta() - if n > 1: - self._counter.set_text(f"{cur + 1}/{n}") - cw = max(self._counter.text_width, 1) - cx = r.x + r.width - cw - 16 - cy = r.y + (r.height - P.FS_PAGE_CTR) / 2 - self._counter.set_position(cx, cy) - self._counter.render() diff --git a/selfdrive/ui/bp/mici/widgets/button_bp.py b/selfdrive/ui/bp/mici/widgets/button_bp.py deleted file mode 100644 index b860a6ec10..0000000000 --- a/selfdrive/ui/bp/mici/widgets/button_bp.py +++ /dev/null @@ -1,149 +0,0 @@ -import pyray as rl -from typing import Union -from collections.abc import Callable -from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigParamControl, BigToggle, BigMultiToggle, BigMultiParamToggle -from openpilot.system.ui import text -from openpilot.system.ui.widgets.scroller import DO_ZOOM -from openpilot.system.ui.lib.application import gui_app - -SCROLLING_SPEED_PX_S = 50 -COMPLICATION_SIZE = 36 -LABEL_COLOR = rl.WHITE -LABEL_HORIZONTAL_PADDING = 40 -COMPLICATION_GREY = rl.Color(0xAA, 0xAA, 0xAA, 255) -PRESSED_SCALE = 1.15 if DO_ZOOM else 1.07 - -class BigButtonBP(BigButton): - def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture, None] = None, - scroll: bool = False, tint: rl.Color = rl.WHITE, is_active: Callable[[], bool] = None, - value_size: int = COMPLICATION_SIZE): - # BluePilot: Convert string icon paths to pre-loaded textures (upstream removed string support) - if isinstance(icon, str) and icon: - icon = gui_app.texture(icon) - elif isinstance(icon, str): - icon = None - BigButton.__init__(self, text, value, icon, scroll) - self.tint = tint - self.get_is_active = is_active - - self._sub_label.set_font_size(value_size) - - def set_checked(self, checked: bool): - self._checked = checked - - def _load_images(self): - BigButton._load_images(self) - self._is_active = gui_app.texture("icons_mici/buttons/toggle_pill_enabled.png", 120, 66, keep_aspect_ratio=False) - self._is_non_active = gui_app.texture("icons_mici/buttons/toggle_pill_disabled.png", 120, 66, keep_aspect_ratio=False) - - def _render(self, _): - # draw _txt_default_bg - txt_bg = self._txt_default_bg - if not self.enabled: - txt_bg = self._txt_disabled_bg - elif self.is_pressed: - txt_bg = self._txt_pressed_bg - - scale = self._scale_filter.update(PRESSED_SCALE if self.is_pressed else 1.0) - btn_x = self._rect.x + (self._rect.width * (1 - scale)) / 2 - btn_y = self._rect.y + (self._rect.height * (1 - scale)) / 2 - rl.draw_texture_ex(txt_bg, (btn_x, btn_y), 0, scale, self.tint) - - self._draw_content(btn_y) - - self._draw_active_indicator() - - def _draw_active_indicator(self): - if self.get_is_active is not None: - x = self._rect.x + self._rect.width / 2 - self._is_active.width / 2 - y = self._rect.y - - active = self.get_is_active() - if active: - rl.draw_texture(self._is_active, int(x), int(y), rl.GREEN) - else: - rl.draw_texture(self._is_non_active, int(x), int(y), rl.WHITE) - -class BigToggleBP(BigButtonBP, BigToggle): - def __init__(self, text: str, value: str = "", initial_state: bool = False, toggle_callback: Callable = None, - tint: rl.Color = rl.WHITE, is_active: Callable[[], bool] = None): - BigButtonBP.__init__(self, text, value, None, tint=tint, is_active=is_active) - BigToggle.__init__(self, text, value, initial_state=initial_state, toggle_callback=toggle_callback) - - def _load_images(self): - BigButtonBP._load_images(self) - BigToggle._load_images(self) - -class BigMultiToggleBP(BigToggleBP, BigMultiToggle): - def __init__(self, text: str, options: list[str], toggle_callback: Callable = None, - select_callback: Callable = None, is_active: Callable[[], bool] = None): - BigToggleBP.__init__(self, text, "", toggle_callback=toggle_callback, is_active=is_active) - BigMultiToggle.__init__(self, text, options, toggle_callback=toggle_callback, select_callback=select_callback) - - def _load_images(self): - BigToggleBP._load_images(self) - BigMultiToggle._load_images(self) - - def _get_label_font_size(self): - font_size = BigMultiToggle._get_label_font_size(self) - return font_size - 10 - - def _draw_content(self, btn_y: float): - # don't draw pill from BigToggle - BigToggleBP._draw_content(self, btn_y) - - checked_idx = self._options.index(self.value) - - x = self._rect.x + self._rect.width - self._txt_enabled_toggle.width - y = self._rect.y - - num_options = len(self._options) - for i in range(num_options): - dist = 35 - if num_options > 4: - dist = self._rect.height / int(num_options + 1) - self._draw_pill(x, y, checked_idx == i) - y += dist - -class BigMultiParamToggleBP(BigMultiToggleBP, BigMultiParamToggle): - def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable = None, - select_callback: Callable = None, value_size: int = 30, is_active: Callable[[], bool] = None): - BigMultiToggleBP.__init__(self, text, options, toggle_callback, select_callback, is_active=is_active) - BigMultiParamToggle.__init__(self, text, param, options, toggle_callback, select_callback) - self._sub_label.set_font_size(value_size) - - - def _load_images(self): - BigMultiToggleBP._load_images(self) - BigMultiParamToggle._load_images(self) if hasattr(BigMultiParamToggle, '_load_images') else None - - def _load_value(self): - self.set_value(self._options[self._params.get(self._param) or 0]) - - -class BigMultiParamToggleBoolBP(BigMultiParamToggleBP): - """Like BigMultiParamToggleBP but for a BOOL param: index 0 = False, index 1 = True.""" - - def _load_value(self): - idx = 1 if self._params.get_bool(self._param) else 0 - self.set_value(self._options[idx]) - - def _handle_mouse_release(self, mouse_pos): - # Advance option and update display (BigMultiToggle), but do NOT call BigMultiParamToggle's - # put_nonblocking(self._param, new_idx) — param is BOOL, so we must use put_bool_nonblocking. - BigMultiToggle._handle_mouse_release(self, mouse_pos) - new_idx = self._options.index(self.value) - self._params.put_bool(self._param, bool(new_idx)) - - -class BigParamControlBP(BigToggleBP, BigParamControl): - def __init__(self, text: str, param: str, is_active_param: str = None, toggle_callback: Callable = None, - tint: rl.Color = rl.WHITE): - BigToggleBP.__init__(self, text, "", toggle_callback=toggle_callback, tint=tint, - is_active=(lambda: self.params.get_bool(is_active_param)) if is_active_param is not None else None) - BigParamControl.__init__(self, text, param, toggle_callback=toggle_callback) - self.set_checked(self.params.get_bool(self.param, False)) - - def _load_images(self): - BigToggleBP._load_images(self) - BigParamControl._load_images(self) if hasattr(BigParamControl, '_load_images') else None diff --git a/selfdrive/ui/bp/mici/widgets/cards.py b/selfdrive/ui/bp/mici/widgets/cards.py deleted file mode 100644 index adc4c9d82f..0000000000 --- a/selfdrive/ui/bp/mici/widgets/cards.py +++ /dev/null @@ -1,650 +0,0 @@ -"""Full-page cards used by the BP paged sub-panels. - -Each card is a Widget sized to fill an entire page (PagedScroller resizes them -on show). Layouts: - -- BigToggleCard — name + (wrapped) sub on the left, big iOS-style switch on - the right. Bound to a Params bool key. -- BigMultiToggleCard — name + sub + a label-cycling button on the right - (replaces a BigMultiParamToggle for one-per-page layout). -- StatCard — caps key on top, huge value below. -- BigButtonCard — single big card: icon (left) + label (right). danger=True - flips colors red. -- SsidRowCard — Wi-Fi network row: lock icon, SSID, status; bars on the right. -- BigCategoryTile — settings landing tile (icon + label). - -Description wrapping: the sub label uses wrap_text=True with max_width set to -the available column. If a sub overflows even after wrap, it is rendered with -a smaller font (24 → 22 → 20 px) chosen at render time via measure_text_cached. -""" -import pyray as rl -from collections.abc import Callable -from typing import Union - -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.widgets.icon_widget import IconWidget -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos -from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.common.params import Params - -from openpilot.selfdrive.ui.bp.mici.widgets import bp_palette as P - - -def _fit_single_line(label: UnifiedLabel, target_size: int, max_width: float, - min_size: int = 20, step: int = 2) -> int: - """Shrink `label`'s font from target_size down to min_size until the text - fits in max_width on a single line. Returns the chosen size and leaves the - label set to that size. - """ - text = label.text - font = gui_app.font(label._font_weight) - letter_spacing = label._letter_spacing - # Walk down in `step`-sized chunks; stop the first time the text fits. - for sz in range(target_size, min_size - 1, -step): - spacing_px = sz * letter_spacing - w = measure_text_cached(font, text, sz, spacing_px).x - if w <= max_width: - label.set_font_size(sz) - return sz - label.set_font_size(min_size) - return min_size - - -CARD_PADDING_X = 24 -CARD_PADDING_Y = 14 - -# Horizontal margin between the page edge and the card frame. Vertical margin -# stays small so cards still fill most of the page height. -CARD_MARGIN_X = 22 -CARD_MARGIN_Y = 6 -SWITCH_W = 110 -SWITCH_H = 64 -SWITCH_KNOB = 56 - - -def _draw_panel_bg(r: rl.Rectangle, color: rl.Color = P.PANEL_BG, border: rl.Color = P.PANEL_BORDER) -> None: - rl.draw_rectangle_rounded(r, 0.10, 16, color) - rl.draw_rectangle_rounded_lines_ex(r, 0.10, 16, 1, border) - - -def _draw_frosted_card(r, tint=None) -> None: - """Subtle frosted-glass card: translucent fill + soft top highlight + thin - border. Sits on top of the BP radial gradient so the underlying color - bleeds through, giving the depth the mockup has via backdrop-filter. - - `tint`: when given, used as a faint accent overlay (e.g. red for danger, - blue for active). Otherwise the card is the neutral white-on-dark frost. - """ - # Base fill — slightly more visible than P.PANEL_BG to read as glass over - # the radial gradient. - fill = rl.Color(0xFF, 0xFF, 0xFF, int(0.06 * 255)) - rl.draw_rectangle_rounded(r, 0.14, 14, fill) - - # Top-edge highlight: brighter strip at the top fading to transparent, - # mimicking the glass-bevel look of frosted UI cards. - hl_h = max(2, min(int(r.height * 0.35), 28)) - hl = rl.Rectangle(r.x + 1, r.y + 1, r.width - 2, hl_h) - rl.draw_rectangle_gradient_v(int(hl.x), int(hl.y), int(hl.width), int(hl.height), - rl.Color(0xFF, 0xFF, 0xFF, int(0.06 * 255)), - rl.Color(0xFF, 0xFF, 0xFF, 0)) - - # Optional accent tint (danger / active state). - if tint is not None: - overlay = rl.Color(tint.r, tint.g, tint.b, int(0.10 * 255)) - rl.draw_rectangle_rounded(r, 0.14, 14, overlay) - - # Visible frosted-glass border — 2px @ 18% alpha reads cleanly against the - # radial gradient without looking heavy. Matches the mockup card outline. - border = rl.Color(0xFF, 0xFF, 0xFF, int(0.18 * 255)) - rl.draw_rectangle_rounded_lines_ex(r, 0.14, 14, 2, border) - - -# ------------------------------------------------------------ -# Toggle (boolean) -# ------------------------------------------------------------ -class BigToggleCard(Widget): - def __init__(self, name: str, sub: str, param_key: str | None = None, - initial: bool = False, on_change: Callable[[bool], None] | None = None): - super().__init__() - self._name_text = name - self._sub_text = sub - self._param_key = param_key - self._on_change = on_change - self._params = Params() if param_key else None - - self._on = bool(self._params.get_bool(param_key)) if param_key and self._params else initial - - self._name_label = UnifiedLabel(name, font_size=P.FS_NAME, font_weight=FontWeight.BOLD, - text_color=P.TEXT, max_width=520, wrap_text=False, elide=False) - self._sub_label = UnifiedLabel(sub, font_size=P.FS_SUB, font_weight=FontWeight.MEDIUM, - text_color=P.MUTED, max_width=400, wrap_text=True, - line_height=1.15) - - # ---- public ---- - @property - def is_on(self) -> bool: - return self._on - - def set_on(self, v: bool, persist: bool = True) -> None: - self._on = v - if persist and self._params and self._param_key: - self._params.put_bool(self._param_key, v) - if self._on_change: - self._on_change(v) - - # ---- input ---- - def _handle_mouse_release(self, mouse_pos: MousePos): - self.set_on(not self._on, persist=True) - - def _update_state(self): - # Pick up external param changes (e.g. from another panel or a script) - if self._params and self._param_key: - external = bool(self._params.get_bool(self._param_key)) - if external != self._on: - self._on = external - - # ---- render ---- - def _render(self, _): - r = self._rect - # Mockup: toggle rows have NO card frame — just text + switch on gradient. - bg_rect = rl.Rectangle(r.x + CARD_MARGIN_X, r.y + CARD_MARGIN_Y, - r.width - CARD_MARGIN_X * 2, r.height - CARD_MARGIN_Y * 2) - - # Switch on right - sw_y = bg_rect.y + (bg_rect.height - SWITCH_H) / 2 - sw_x = bg_rect.x + bg_rect.width - SWITCH_W - CARD_PADDING_X - self._draw_switch(sw_x, sw_y) - - # Text column on left - text_x = bg_rect.x + CARD_PADDING_X - text_max_w = sw_x - text_x - 16 - - # Name label — auto-shrink to fit horizontally - name_y = bg_rect.y + CARD_PADDING_Y - _fit_single_line(self._name_label, P.FS_NAME, text_max_w) - self._name_label.set_max_width(int(text_max_w)) - self._name_label.set_position(text_x, name_y) - self._name_label.render() - - # Sub label — wrap to text_max_w, step down font if too tall - self._sub_label.set_max_width(int(text_max_w)) - sub_y = name_y + P.FS_NAME * 1.05 + 4 - - # Auto-shrink: try 24 → 22 → 20 if sub would overflow available height. - # Then set rect.height explicitly so all wrapped lines render. - available_h = bg_rect.y + bg_rect.height - sub_y - CARD_PADDING_Y - for sz in (P.FS_SUB, 22, 20): - self._sub_label.set_font_size(sz) - h = self._sub_label.get_content_height(int(text_max_w)) - if h <= available_h: - break - # Buffer rect.height: UnifiedLabel.get_content_height omits the line_height - # multiplier on line 0, but _render applies it to every line — so total - # measured by get_content_height is ~one factor short. Add a generous fudge - # so all wrapped lines actually render. - h = self._sub_label.get_content_height(int(text_max_w)) + self._sub_label.font_size * 0.5 - self._sub_label.set_rect(rl.Rectangle(text_x, sub_y, text_max_w, h)) - self._sub_label.render() - - def _draw_switch(self, x: float, y: float) -> None: - track = rl.Rectangle(x, y, SWITCH_W, SWITCH_H) - track_color = P.ACCENT if self._on else rl.Color(0xFF, 0xFF, 0xFF, int(0.10 * 255)) - rl.draw_rectangle_rounded(track, 1.0, 16, track_color) - if self._on: - # Soft halo - glow = rl.Color(P.ACCENT.r, P.ACCENT.g, P.ACCENT.b, int(0.45 * 255)) - glow_rect = rl.Rectangle(x - 2, y - 2, SWITCH_W + 4, SWITCH_H + 4) - rl.draw_rectangle_rounded_lines_ex(glow_rect, 1.0, 16, 2, glow) - # Knob - knob_x = x + (SWITCH_W - SWITCH_KNOB - 4) if self._on else x + 4 - knob_y = y + 4 - rl.draw_circle(int(knob_x + SWITCH_KNOB / 2), int(knob_y + SWITCH_KNOB / 2), - SWITCH_KNOB / 2, rl.WHITE) - - -# ------------------------------------------------------------ -# 3+ state toggle (e.g. driving personality) -# ------------------------------------------------------------ -class BigMultiToggleCard(Widget): - def __init__(self, name: str, sub: str, options: list[str], - param_key: str | None = None, on_change: Callable[[int], None] | None = None, - initial_idx: int = 0): - super().__init__() - self._name = name - self._options = options - self._param_key = param_key - self._on_change = on_change - self._params = Params() if param_key else None - self._idx = self._read_idx(initial_idx) - - self._name_label = UnifiedLabel(name, font_size=P.FS_NAME, font_weight=FontWeight.BOLD, - text_color=P.TEXT, max_width=520, wrap_text=False, elide=False) - self._sub_label = UnifiedLabel(sub, font_size=P.FS_SUB, font_weight=FontWeight.MEDIUM, - text_color=P.MUTED, max_width=400, wrap_text=True, - line_height=1.15) - # Value pill text — wide max_width so we never elide; pill bounding box - # is computed from measured text in _render and capped to card width. - self._value_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.BOLD, - text_color=P.ACCENT2, max_width=520, wrap_text=False, - letter_spacing=0.04, elide=False) - - def _read_idx(self, default: int) -> int: - if not self._params or not self._param_key: - return default - val = self._params.get(self._param_key) - if val is None: - return default - # Params returns int for INT keys, bool/None for BOOL, bytes/str otherwise. - if isinstance(val, bool): - return 1 if val else 0 - if isinstance(val, int): - return max(0, min(len(self._options) - 1, val)) - if isinstance(val, (bytes, str)): - try: - return int(val) - except (TypeError, ValueError): - return default - return default - - def _persist(self): - if self._params and self._param_key: - # Use put_nonblocking with the right type. We don't know the param type - # at construction time, so try int first (covers LongitudinalPersonality, - # mici_complication, etc.), then fall back to bool for 2-option keys. - try: - self._params.put(self._param_key, int(self._idx)) - except TypeError: - try: - self._params.put_bool(self._param_key, bool(self._idx)) - except TypeError: - self._params.put(self._param_key, str(self._idx)) - if self._on_change: - self._on_change(self._idx) - - def _handle_mouse_release(self, _): - self._idx = (self._idx + 1) % len(self._options) - self._persist() - - def _render(self, _): - r = self._rect - # Mockup: multi-toggle has NO frame around content. Only the value pill - # (drawn below) has accent fill. - bg_rect = rl.Rectangle(r.x + CARD_MARGIN_X, r.y + CARD_MARGIN_Y, - r.width - CARD_MARGIN_X * 2, r.height - CARD_MARGIN_Y * 2) - - text_x = bg_rect.x + CARD_PADDING_X - text_max_w = bg_rect.width - CARD_PADDING_X * 2 - - # ---- Title (full width, top) — auto-shrink to fit - name_y = bg_rect.y + CARD_PADDING_Y - _fit_single_line(self._name_label, P.FS_NAME, text_max_w) - self._name_label.set_max_width(int(text_max_w)) - self._name_label.set_position(text_x, name_y) - self._name_label.render() - - # ---- Big centered value pill (bottom half) ---- - # Pill width follows the text. We try the target font (36px) first; if - # the resulting pill would exceed available card width, shrink the font - # rather than the text. - value_text = self._options[self._idx].upper() - self._value_label.set_text(value_text) - - PILL_PAD_X = 36 - PILL_TARGET = 36 - PILL_MIN = 22 - pill_max_inner_w = bg_rect.width - CARD_PADDING_X * 2 - PILL_PAD_X * 2 - - chosen_size = _fit_single_line(self._value_label, PILL_TARGET, pill_max_inner_w, min_size=PILL_MIN) - - # Now measure at the chosen size so vw is exact for this frame. - font = gui_app.font(self._value_label._font_weight) - spacing_px = chosen_size * self._value_label._letter_spacing - vw = measure_text_cached(font, value_text, chosen_size, spacing_px).x - - pill_w = vw + PILL_PAD_X * 2 - pill_h = 70 - px = bg_rect.x + (bg_rect.width - pill_w) / 2 - bottom_band_y = bg_rect.y + bg_rect.height * 0.45 - bottom_band_h = bg_rect.height * 0.55 - CARD_PADDING_Y - py = bottom_band_y + (bottom_band_h - pill_h) / 2 - - pill = rl.Rectangle(px, py, pill_w, pill_h) - rl.draw_rectangle_rounded(pill, 1.0, 16, rl.Color(P.ACCENT.r, P.ACCENT.g, P.ACCENT.b, int(0.20 * 255))) - rl.draw_rectangle_rounded_lines_ex(pill, 1.0, 16, 1, - rl.Color(P.ACCENT.r, P.ACCENT.g, P.ACCENT.b, int(0.55 * 255))) - # Vertically center the (possibly shrunk) text in the pill. - self._value_label.set_position(px + (pill_w - vw) / 2, - py + (pill_h - chosen_size) / 2 - 2) - self._value_label.render() - - -# ------------------------------------------------------------ -# Stat card -# ------------------------------------------------------------ -class StatCard(Widget): - def __init__(self, key: str, value: Union[str, Callable[[], str]], - variant: str = "default"): - """variant: 'default' (huge bold), 'small' (60px), 'mono' (Inter Bold mono-ish).""" - super().__init__() - self._key = key - self._value = value - self._variant = variant - - val_size = {"small": 60, "mono": 56}.get(variant, P.FS_STAT_VAL) - self._target_val_size = val_size - self._key_label = UnifiedLabel(key.upper(), font_size=P.FS_STAT_KEY, font_weight=FontWeight.BOLD, - text_color=P.MUTED, letter_spacing=0.10, - max_width=520, wrap_text=False, elide=False) - self._val_label = UnifiedLabel("", font_size=val_size, - font_weight=FontWeight.BOLD, - text_color=P.TEXT, letter_spacing=-0.02, - max_width=520, wrap_text=False, elide=False) - - def _resolve_value(self) -> str: - return self._value() if callable(self._value) else str(self._value) - - def _render(self, _): - r = self._rect - bg_rect = rl.Rectangle(r.x + CARD_MARGIN_X, r.y + CARD_MARGIN_Y, r.width - CARD_MARGIN_X * 2, r.height - CARD_MARGIN_Y * 2) - _draw_frosted_card(bg_rect) - - self._val_label.set_text(self._resolve_value()) - - text_x = bg_rect.x + CARD_PADDING_X - text_max_w = bg_rect.width - CARD_PADDING_X * 2 - - key_y = bg_rect.y + bg_rect.height * 0.30 - self._key_label.set_position(text_x, key_y) - self._key_label.render() - - # Auto-shrink value to fit horizontally — long values like "Ford F-150 - # Hybrid Lariat" should size down rather than elide. - chosen = _fit_single_line(self._val_label, self._target_val_size, text_max_w, min_size=24) - val_y = bg_rect.y + bg_rect.height * 0.42 - self._val_label.set_max_width(int(text_max_w)) - self._val_label.set_position(text_x, val_y) - self._val_label.render() - - -# ------------------------------------------------------------ -# Big button card (Pair, Reboot, Power Off, ...) -# ------------------------------------------------------------ -class BigButtonCard(Widget): - ICON_BOX = 60 # bounding box for the icon — drawn aspect-preserving inside - - def __init__(self, label: str, icon: Union[str, rl.Texture, None] = None, - on_click: Callable | None = None, danger: bool = False): - super().__init__() - self._danger = danger - if isinstance(icon, str): - # Force a small bounding-box load so we never blow up to 256+ px for - # icons authored at large native sizes. - icon = gui_app.texture(icon, self.ICON_BOX, self.ICON_BOX) - self._icon = icon - self._label = UnifiedLabel(label, font_size=P.FS_BTN_LBL, font_weight=FontWeight.BOLD, - text_color=(P.DANGER if danger else P.TEXT), - max_width=520, wrap_text=False, elide=False) - if on_click is not None: - self.set_click_callback(on_click) - # No _handle_mouse_release override — use the framework's default which - # fires self._click_callback. This way both ctor on_click= and external - # set_click_callback() work. - - def _render(self, _): - r = self._rect - bg_rect = rl.Rectangle(r.x + CARD_MARGIN_X, r.y + CARD_MARGIN_Y, r.width - CARD_MARGIN_X * 2, r.height - CARD_MARGIN_Y * 2) - - tint = None - if self._danger: - tint = P.DANGER - elif self.is_pressed: - tint = P.ACCENT - _draw_frosted_card(bg_rect, tint=tint) - # Add a slightly stronger danger border so destructive buttons read - # immediately, not only when reading the label. - if self._danger: - border = rl.Color(P.DANGER.r, P.DANGER.g, P.DANGER.b, int(0.55 * 255)) - rl.draw_rectangle_rounded_lines_ex(bg_rect, 0.14, 14, 1, border) - - icon_col_w = (self.ICON_BOX + 28) if self._icon is not None else 0 - label_x = bg_rect.x + CARD_PADDING_X + icon_col_w - label_max_w = int(bg_rect.x + bg_rect.width - CARD_PADDING_X - label_x) - - # Icon — centered vertically in its own column on the left - if self._icon is not None: - icon_x = bg_rect.x + CARD_PADDING_X - icon_y = bg_rect.y + (bg_rect.height - self._icon.height) / 2 - tint = P.DANGER if self._danger else P.TEXT - rl.draw_texture(self._icon, int(icon_x), int(icon_y), tint) - - # Label — auto-shrink so the full text always fits the remaining width. - chosen = _fit_single_line(self._label, P.FS_BTN_LBL, label_max_w) - self._label.set_max_width(label_max_w) - lbl_y = bg_rect.y + (bg_rect.height - chosen) / 2 - 2 - self._label.set_position(label_x, lbl_y) - self._label.render() - - -# ------------------------------------------------------------ -# Wi-Fi SSID row card -# ------------------------------------------------------------ -class SsidRowCard(Widget): - def __init__(self, ssid: str, secured: bool, strength: int, connected: bool = False, - on_click: Callable | None = None): - """strength: 0..4.""" - super().__init__() - self._ssid_text = ssid - self._secured = secured - self._strength = max(0, min(4, strength)) - self._connected = connected - - self._ssid_label = UnifiedLabel(ssid, font_size=P.FS_NAME, font_weight=FontWeight.BOLD, - text_color=P.TEXT, max_width=400, wrap_text=False, elide=False) - self._sub_label = UnifiedLabel("", font_size=P.FS_SUB, font_weight=FontWeight.BOLD, - text_color=P.MUTED, max_width=400, wrap_text=False, - letter_spacing=0.04) - if on_click is not None: - self.set_click_callback(on_click) - - def _render(self, _): - r = self._rect - bg_rect = rl.Rectangle(r.x + CARD_MARGIN_X, r.y + CARD_MARGIN_Y, r.width - CARD_MARGIN_X * 2, r.height - CARD_MARGIN_Y * 2) - - if self._connected: - tint = P.ACCENT - sub_color = P.ACCENT2 - bar_color = P.ACCENT2 - else: - tint = None - sub_color = P.MUTED - bar_color = P.TEXT - - _draw_frosted_card(bg_rect, tint=tint) - if self._connected: - border = rl.Color(P.ACCENT.r, P.ACCENT.g, P.ACCENT.b, int(0.45 * 255)) - rl.draw_rectangle_rounded_lines_ex(bg_rect, 0.14, 14, 1, border) - - # Lock icon (procedural — small rounded rect over a hollow shackle). - lock_x = bg_rect.x + 18 - lock_y = bg_rect.y + (bg_rect.height - 36) / 2 - if self._secured: - _draw_lock(lock_x, lock_y, 28, P.MUTED) - - # SSID + sub — auto-shrink so long SSIDs don't get truncated - text_x = lock_x + 40 - bars_x = bg_rect.x + bg_rect.width - 70 - ssid_max_w = bars_x - text_x - 12 - _fit_single_line(self._ssid_label, P.FS_NAME, ssid_max_w) - self._ssid_label.set_position(text_x, bg_rect.y + 18) - self._ssid_label.render() - - self._sub_label.set_text("CONNECTED" if self._connected else ("WPA2" if self._secured else "OPEN")) - self._sub_label.set_text_color(sub_color) - self._sub_label.set_position(text_x, bg_rect.y + 18 + P.FS_NAME + 4) - self._sub_label.render() - - # Bars on the right - self._draw_bars(bg_rect.x + bg_rect.width - 70, bg_rect.y + bg_rect.height / 2 - 20, - self._strength, bar_color) - - def _draw_bars(self, x: float, y: float, lit: int, color: rl.Color) -> None: - HEIGHTS = (12, 22, 32, 42) - bar_w = 8 - gap = 5 - for i, h in enumerate(HEIGHTS): - cx = x + i * (bar_w + gap) - cy = y + (HEIGHTS[-1] - h) - c = color if i < lit else rl.Color(0xFF, 0xFF, 0xFF, int(0.25 * 255)) - rl.draw_rectangle(int(cx), int(cy), bar_w, int(h), c) - - -def _draw_lock(x: float, y: float, size: int, color: rl.Color) -> None: - """Procedural padlock glyph at (x, y), height = size.""" - body_h = size * 0.55 - body_w = size * 0.85 - body_x = x + (size - body_w) / 2 - body_y = y + size - body_h - rl.draw_rectangle_rounded(rl.Rectangle(body_x, body_y, body_w, body_h), 0.20, 8, color) - # Shackle: arc above - shackle_r = size * 0.30 - cx = x + size / 2 - cy = body_y - shackle_r * 0.10 - rl.draw_ring(rl.Vector2(cx, cy), shackle_r * 0.65, shackle_r, 180, 360, 16, color) - - -# ------------------------------------------------------------ -# Settings landing tile -# ------------------------------------------------------------ -class BigCategoryTile(Widget): - # Wider tiles — ~3 visible on the 536-wide MICI screen so each tile reads - # as a substantial card rather than a thin column. Extra tiles scroll - # horizontally; the framework's scroll-indicator pill below hints at this. - TILE_WIDTH = 150 - TILE_HEIGHT = 180 - - def __init__(self, label: str, icon: Union[str, rl.Texture, None] = None, - on_click: Callable | None = None): - super().__init__() - if isinstance(icon, str): - icon = gui_app.texture(icon, 64, 64) - self._icon = icon - self._label = UnifiedLabel(label, font_size=22, font_weight=FontWeight.BOLD, - text_color=P.TEXT, max_width=180, wrap_text=False, elide=False) - # Default natural size; parent (Scroller) reads rect.width to lay out. - self.set_rect(rl.Rectangle(0, 0, self.TILE_WIDTH, self.TILE_HEIGHT)) - if on_click is not None: - self.set_click_callback(on_click) - - def _render(self, _): - r = self._rect - # Slightly inset so the frosted-card border has room to breathe between - # adjacent tiles in the menu strip. - pad = 6 - bg_rect = rl.Rectangle(r.x + pad, r.y + pad, r.width - pad * 2, r.height - pad * 2) - - _draw_frosted_card(bg_rect, tint=P.ACCENT if self.is_pressed else None) - - # PNG icon centered slightly above middle - if self._icon is not None: - ix = bg_rect.x + (bg_rect.width - self._icon.width) / 2 - iy = bg_rect.y + bg_rect.height * 0.32 - self._icon.height / 2 - rl.draw_texture(self._icon, int(ix), int(iy), P.TEXT) - - # Label below icon — auto-shrink so longer labels still fit - label_max = bg_rect.width - 12 - _fit_single_line(self._label, 22, label_max, min_size=14) - lw = max(self._label.text_width, 1) - lx = bg_rect.x + (bg_rect.width - lw) / 2 - ly = bg_rect.y + bg_rect.height * 0.78 - self._label.set_position(lx, ly) - self._label.render() - - -# ------------------------------------------------------------ -# Select-list tile (horizontal scroller item: vehicle make/model, saved networks, ...) -# -# Horizontal scroll matches the rest of the BP UI (settings landing, sub-panels) -# and avoids the swipe-down vs. scroll-back-to-top conflict that vertical -# NavWidget children hit (see NavScroller._back_enabled). -# ------------------------------------------------------------ -class BPSelectTile(Widget): - """Frosted tile used by BPSelectPanel. - - Layout: icon top-center, label bottom (auto-shrunk to fit). `selected=True` - paints an accent tint + accent border for the currently-chosen entry. - """ - TILE_WIDTH = 170 - TILE_HEIGHT = 180 - ICON_BOX = 64 - LABEL_FS = 22 - - def __init__(self, label: str, icon: Union[str, rl.Texture, None] = None, - selected: bool = False, on_click: Callable | None = None, - width: int | None = None, height: int | None = None, - wrap_label: bool = False): - super().__init__() - self._selected = selected - self._wrap_label = wrap_label - if isinstance(icon, str): - icon = gui_app.texture(icon, self.ICON_BOX, self.ICON_BOX) - self._icon = icon - self._label = UnifiedLabel(label, font_size=self.LABEL_FS, font_weight=FontWeight.BOLD, - text_color=P.TEXT, max_width=240, - wrap_text=wrap_label, line_height=1.10, elide=False) - w = width if width is not None else self.TILE_WIDTH - h = height if height is not None else self.TILE_HEIGHT - self.set_rect(rl.Rectangle(0, 0, w, h)) - if on_click is not None: - self.set_click_callback(on_click) - - def _render(self, _): - r = self._rect - pad = 6 - bg_rect = rl.Rectangle(r.x + pad, r.y + pad, r.width - pad * 2, r.height - pad * 2) - - if self._selected: - tint = P.ACCENT - elif self.is_pressed: - tint = P.ACCENT - else: - tint = None - _draw_frosted_card(bg_rect, tint=tint) - if self._selected: - border = rl.Color(P.ACCENT.r, P.ACCENT.g, P.ACCENT.b, int(0.55 * 255)) - rl.draw_rectangle_rounded_lines_ex(bg_rect, 0.14, 14, 2, border) - - # Icon centered slightly above middle. - if self._icon is not None: - ix = bg_rect.x + (bg_rect.width - self._icon.width) / 2 - iy = bg_rect.y + bg_rect.height * 0.30 - self._icon.height / 2 - tint_color = P.ACCENT2 if self._selected else P.TEXT - rl.draw_texture(self._icon, int(ix), int(iy), tint_color) - - # Label below icon - label_max = int(bg_rect.width - 12) - if self._wrap_label: - # Multi-line: try font sizes 22 → 18 → 16 until 3 wrapped lines fit - # in the lower 65% of the tile. - max_lbl_h = bg_rect.height * 0.62 - chosen = self.LABEL_FS - for sz in (self.LABEL_FS, 20, 18, 16): - self._label.set_font_size(sz) - self._label.set_max_width(label_max) - h = self._label.get_content_height(label_max) - chosen = sz - if h <= max_lbl_h: - break - h = self._label.get_content_height(label_max) + chosen * 0.5 - lbl_y = bg_rect.y + bg_rect.height * 0.55 - self._label.set_rect(rl.Rectangle(bg_rect.x + 6, lbl_y, - label_max, h)) - self._label.render() - else: - # Single-line: auto-shrink horizontally to fit. - chosen = _fit_single_line(self._label, self.LABEL_FS, label_max, min_size=14) - lw = max(self._label.text_width, 1) - lx = bg_rect.x + (bg_rect.width - lw) / 2 - ly = bg_rect.y + bg_rect.height * 0.74 - self._label.set_position(lx, ly) - self._label.render() diff --git a/selfdrive/ui/bp/mici/widgets/floatbutton.py b/selfdrive/ui/bp/mici/widgets/floatbutton.py deleted file mode 100644 index 22908187e4..0000000000 --- a/selfdrive/ui/bp/mici/widgets/floatbutton.py +++ /dev/null @@ -1,242 +0,0 @@ - -import pyray as rl -from collections.abc import Callable -from openpilot.selfdrive.ui.bp.mici.widgets.button_bp import BigButtonBP -from openpilot.selfdrive.ui.bp.mici.widgets.big_input_dialog_bp import BigInputDialogBP -from openpilot.common.params import Params -from openpilot.system.ui.lib.application import gui_app, MousePos - -CONTENT_MARGIN = 20 -LINE_L = 40 -LINE_W = 8 -LABEL_HORIZONTAL_PADDING = 40 - -class BigParamFloatControl(BigButtonBP): - def __init__(self, text: str, param: str, is_active_param: str = None, is_active: Callable[[], bool] = None, - min: float = None, max: float = None, step: float = 0.05, tint: rl.Color = rl.WHITE): - active_fn = is_active - if active_fn is None and is_active_param is not None: - active_fn = lambda: Params().get_bool(is_active_param) - super().__init__(text, "", tint=tint, is_active=active_fn) - self.min = min - self.max = max - self.step = step - - self._sub_label.set_font_size(22) - - self.margin = self._rect.width * 0.1 - self.rect_size = LINE_L + 2 * CONTENT_MARGIN - - self.param = param - self.params = Params() - self.set_click_callback(self._on_click) - self.update_label() - - def _on_click(self): - if self.min is not None or self.max is not None: - message = f"({self.min}-{self.max})" - else: - message = "enter a numberic value..." - - def _wrapped_callback(val): - self._callback(val) - gui_app.pop_widget() - - dlg = BigInputDialogBP(message, str(self.get_param()), - confirm_callback=_wrapped_callback, show_special_keys=True, minimum_length=0) - gui_app.push_widget(dlg) - - def _callback(self, password: str): - if password: - try: - float_value = float(password) - self.set_param(float_value) - except ValueError: - pass - else: - #revert to default - self.params.remove(self.param) - self.update_label() - - def get_param(self) -> float: - try: - return float(self.params.get(self.param, return_default=True)) - except (TypeError, ValueError): - return 0.0 - - def set_param(self, value: float): - if self.min is not None and value < self.min: - value = self.min - elif self.max is not None and value > self.max: - value = self.max - - self.params.put(self.param, value) - self.update_label(value) - - def update_label(self, value: float = None): - if value is None: - value = self.get_param() - self.set_value(f"{round(value,4)}") - - def _get_label_font_size(self): - font_size = super()._get_label_font_size() - return font_size - 10 - - def _draw_content(self, btn_y: float): - offset = self.rect_size / 3 - self.rect.height -= offset - super()._draw_content(btn_y + offset) - self.rect.height += offset - - def _render(self, _): - super()._render(_) - - self.left = self._rect.x + self.margin - self.right = self._rect.x + self._rect.width - self.margin - self.top = self._rect.y + self.margin - - self.minus_hit_rect = rl.Rectangle( - self.left - CONTENT_MARGIN, self.top - self.rect_size / 2, self.rect_size, self.rect_size - ) - self.plus_hit_rect = rl.Rectangle( - self.right - self.rect_size / 2 - CONTENT_MARGIN, self.top - self.rect_size / 2, self.rect_size, self.rect_size - ) - - #rl.draw_rectangle_lines_ex(self.minus_hit_rect, 1, rl.RED) - #rl.draw_rectangle_lines_ex(self.plus_hit_rect, 1, rl.GREEN) - - rl.draw_line_ex((self.left,self.top), (self.left+LINE_L, self.top), LINE_W, rl.WHITE) - - rl.draw_line_ex((self.right-LINE_L,self.top), (self.right, self.top), LINE_W, rl.WHITE) - m = self.right - LINE_L/2 - rl.draw_line_ex((m,self.top-LINE_L/2), (m, self.top+LINE_L/2), LINE_W, rl.WHITE) - - def minus_clicked(self): - self.set_param(self.get_param() - self.step) - - def plus_clicked(self): - self.set_param(self.get_param() + self.step) - - def _handle_mouse_release(self, mouse_pos: MousePos): - if rl.check_collision_point_rec(mouse_pos, self.minus_hit_rect): - self.minus_clicked() - elif rl.check_collision_point_rec(mouse_pos, self.plus_hit_rect): - self.plus_clicked() - else: - super()._handle_mouse_release(mouse_pos) - - -class BigParamIntControl(BigButtonBP): - def __init__(self, text: str, param: str, is_active_param: str = None, min: int = None, max: int = None, step: int = 1, tint: rl.Color = rl.WHITE): - super().__init__(text, "", tint=tint, is_active=(lambda: Params().get_bool(is_active_param)) if is_active_param is not None else None) - self.min = min - self.max = max - self.step = step - - self._sub_label.set_font_size(22) - - self.margin = self._rect.width * 0.1 - self.rect_size = LINE_L + 2 * CONTENT_MARGIN - - self.param = param - self.params = Params() - self.set_click_callback(self._on_click) - self.update_label() - - def _on_click(self): - if self.min is not None or self.max is not None: - message = f"({self.min}-{self.max})" - else: - message = "enter a numberic value..." - - def _wrapped_callback(val): - self._callback(val) - gui_app.pop_widget() - - dlg = BigInputDialogBP(message, str(self.get_param()), - confirm_callback=_wrapped_callback, show_special_keys=True, minimum_length=0) - gui_app.push_widget(dlg) - - def _callback(self, password: str): - if password: - try: - int_value = int(password) - self.set_param(int_value) - except ValueError: - pass - else: - #revert to default - self.params.remove(self.param) - self.update_label() - - def get_param(self) -> int: - try: - return int(self.params.get(self.param, return_default=True)) - except (TypeError, ValueError): - return 0 - - def set_param(self, value: int): - value=int(value) - if self.min is not None and value < self.min: - value = self.min - elif self.max is not None and value > self.max: - value = self.max - - self.params.put(self.param, value) - self.update_label(value) - - def update_label(self, value: int = None): - if value is None: - value = self.get_param() - self.set_value(f"{value}") - - def _get_label_font_size(self): - font_size = super()._get_label_font_size() - return font_size - 10 - - def _draw_content(self, btn_y: float): - offset = self.rect_size / 3 - self.rect.height -= offset - super()._draw_content(btn_y + offset) - self.rect.height += offset - - def _render(self, _): - super()._render(_) - - self.left = self._rect.x + self.margin - self.right = self._rect.x + self._rect.width - self.margin - self.top = self._rect.y + self.margin - - self.minus_hit_rect = rl.Rectangle( - self.left - CONTENT_MARGIN, self.top - self.rect_size / 2, self.rect_size, self.rect_size - ) - self.plus_hit_rect = rl.Rectangle( - self.right - self.rect_size / 2 - CONTENT_MARGIN, self.top - self.rect_size / 2, self.rect_size, self.rect_size - ) - - #rl.draw_rectangle_lines_ex(self.minus_hit_rect, 1, rl.RED) - #rl.draw_rectangle_lines_ex(self.plus_hit_rect, 1, rl.GREEN) - - rl.draw_line_ex((self.left,self.top), (self.left+LINE_L, self.top), LINE_W, rl.WHITE) - - rl.draw_line_ex((self.right-LINE_L,self.top), (self.right, self.top), LINE_W, rl.WHITE) - m = self.right - LINE_L/2 - rl.draw_line_ex((m,self.top-LINE_L/2), (m, self.top+LINE_L/2), LINE_W, rl.WHITE) - - def set_step(self, value: int): - value -= value % self.step - self.set_param(value) - - def minus_clicked(self): - self.set_step(self.get_param() - self.step) - - def plus_clicked(self): - self.set_step(self.get_param() + self.step) - - def _handle_mouse_release(self, mouse_pos: MousePos): - if rl.check_collision_point_rec(mouse_pos, self.minus_hit_rect): - self.minus_clicked() - elif rl.check_collision_point_rec(mouse_pos, self.plus_hit_rect): - self.plus_clicked() - else: - super()._handle_mouse_release(mouse_pos) diff --git a/selfdrive/ui/bp/mici/widgets/icons.py b/selfdrive/ui/bp/mici/widgets/icons.py deleted file mode 100644 index 571a80099f..0000000000 --- a/selfdrive/ui/bp/mici/widgets/icons.py +++ /dev/null @@ -1,171 +0,0 @@ -"""Procedural feather-style line icons for BP MICI category tiles. - -These mirror the inline SVG icons used in the mockup (mockups/mici_home.html): -sliders / wifi / cpu / upload-cloud / terminal / vehicle / car. Drawn with -raylib lines/circles/rings so we don't ship new asset files. Each is -center-anchored at (cx, cy) and scaled by `size`. -""" -import math -import pyray as rl - -from openpilot.selfdrive.ui.bp.mici.widgets import bp_palette as P - - -def _halo(cx: float, cy: float, size: float, color: rl.Color) -> None: - """Soft blue halo behind an icon. Three concentric circles fading outward.""" - for r_mul, alpha in ((1.6, 0.06), (1.1, 0.10), (0.7, 0.12)): - c = rl.Color(color.r, color.g, color.b, int(255 * alpha)) - rl.draw_circle_gradient(rl.Vector2(int(cx), int(cy)), size * r_mul, c, rl.Color(0, 0, 0, 0)) - - -def _draw_with_halo(draw_fn, cx: float, cy: float, size: float, - icon_color: rl.Color = P.TEXT, halo_color: rl.Color = None) -> None: - if halo_color is None: - halo_color = P.ACCENT - _halo(cx, cy, size, halo_color) - draw_fn(cx, cy, size, icon_color) - - -# ---------------------------------------------------------------- -# Individual icon glyphs — center at (cx, cy), bbox roughly 2*size square. -# ---------------------------------------------------------------- - -def _sliders(cx: float, cy: float, size: float, color: rl.Color) -> None: - th = max(3, size * 0.10) - half = size * 0.85 - # Two horizontal bars with a circular knob at one position each. - for sign, knob_at in ((-1, 0.65), (+1, 0.35)): - y = cy + sign * size * 0.40 - rl.draw_line_ex(rl.Vector2(cx - half, y), rl.Vector2(cx + half, y), th, color) - knob_x = cx - half + knob_at * (half * 2) - rl.draw_circle(int(knob_x), int(y), th * 1.7, color) - # Knob inner ring (for depth) - rl.draw_circle(int(knob_x), int(y), th * 0.7, P.BG_DEEP) - - -def _wifi(cx: float, cy: float, size: float, color: rl.Color) -> None: - th = max(3, size * 0.10) - # Three concentric arcs above a dot — feather wifi shape. - for r_mul in (1.0, 0.65, 0.32): - rl.draw_ring(rl.Vector2(cx, cy + size * 0.35), - size * r_mul - th * 0.6, size * r_mul + th * 0.6, - 200, 340, 32, color) - rl.draw_circle(int(cx), int(cy + size * 0.45), th * 0.9, color) - - -def _cpu(cx: float, cy: float, size: float, color: rl.Color) -> None: - th = max(3, size * 0.10) - s = size * 0.80 - # Outer rounded square - outer = rl.Rectangle(cx - s, cy - s, s * 2, s * 2) - rl.draw_rectangle_rounded_lines_ex(outer, 0.10, 8, th, color) - # Inner filled square - inner = rl.Rectangle(cx - s * 0.45, cy - s * 0.45, s * 0.90, s * 0.90) - rl.draw_rectangle_rounded_lines_ex(inner, 0.05, 6, th, color) - # Pins on each side (3 per side) - pin_len = s * 0.30 - for i in range(-1, 2): - o = i * s * 0.5 - # top - rl.draw_line_ex(rl.Vector2(cx + o, cy - s), rl.Vector2(cx + o, cy - s - pin_len), th * 0.8, color) - # bottom - rl.draw_line_ex(rl.Vector2(cx + o, cy + s), rl.Vector2(cx + o, cy + s + pin_len), th * 0.8, color) - # left - rl.draw_line_ex(rl.Vector2(cx - s, cy + o), rl.Vector2(cx - s - pin_len, cy + o), th * 0.8, color) - # right - rl.draw_line_ex(rl.Vector2(cx + s, cy + o), rl.Vector2(cx + s + pin_len, cy + o), th * 0.8, color) - - -def _upload_cloud(cx: float, cy: float, size: float, color: rl.Color) -> None: - th = max(3, size * 0.10) - # Cloud body — three circles + a base line. - rl.draw_circle(int(cx - size * 0.45), int(cy - size * 0.05), size * 0.35, color) - rl.draw_circle(int(cx + size * 0.10), int(cy - size * 0.30), size * 0.45, color) - rl.draw_circle(int(cx + size * 0.55), int(cy + size * 0.05), size * 0.35, color) - rl.draw_rectangle(int(cx - size * 0.55), int(cy - size * 0.05), - int(size * 1.1), int(size * 0.40), color) - # Carve inner shape so the cloud reads as outline (subtract a slightly - # smaller copy filled with BG_DEEP). - inner_color = P.BG_DEEP - rl.draw_circle(int(cx - size * 0.45), int(cy - size * 0.05), size * 0.35 - th, inner_color) - rl.draw_circle(int(cx + size * 0.10), int(cy - size * 0.30), size * 0.45 - th, inner_color) - rl.draw_circle(int(cx + size * 0.55), int(cy + size * 0.05), size * 0.35 - th, inner_color) - rl.draw_rectangle(int(cx - size * 0.55) + int(th * 0.7), int(cy - size * 0.05), - int(size * 1.1) - int(th * 1.4), int(size * 0.40) - int(th * 0.7), inner_color) - # Up arrow inside the cloud - arrow_w = size * 0.32 - arrow_h = size * 0.55 - rl.draw_line_ex(rl.Vector2(cx, cy - arrow_h * 0.30), - rl.Vector2(cx, cy + arrow_h * 0.45), th, color) - rl.draw_line_ex(rl.Vector2(cx, cy - arrow_h * 0.30), - rl.Vector2(cx - arrow_w * 0.5, cy - arrow_h * 0.05), th, color) - rl.draw_line_ex(rl.Vector2(cx, cy - arrow_h * 0.30), - rl.Vector2(cx + arrow_w * 0.5, cy - arrow_h * 0.05), th, color) - - -def _terminal(cx: float, cy: float, size: float, color: rl.Color) -> None: - th = max(3, size * 0.12) - # ">" chevron on the left - rl.draw_line_ex(rl.Vector2(cx - size * 0.7, cy - size * 0.35), - rl.Vector2(cx - size * 0.05, cy + size * 0.05), th, color) - rl.draw_line_ex(rl.Vector2(cx - size * 0.05, cy + size * 0.05), - rl.Vector2(cx - size * 0.7, cy + size * 0.45), th, color) - # "_" underscore on the right - rl.draw_line_ex(rl.Vector2(cx + size * 0.05, cy + size * 0.45), - rl.Vector2(cx + size * 0.75, cy + size * 0.45), th, color) - - -def _vehicle(cx: float, cy: float, size: float, color: rl.Color) -> None: - th = max(3, size * 0.10) - # Simple car silhouette: body rect + roof bump + 2 wheels. - body = rl.Rectangle(cx - size * 0.9, cy - size * 0.05, size * 1.8, size * 0.55) - rl.draw_rectangle_rounded_lines_ex(body, 0.30, 6, th, color) - # Roof - roof_x1 = cx - size * 0.55 - roof_x2 = cx + size * 0.55 - roof_y = cy - size * 0.45 - rl.draw_line_ex(rl.Vector2(cx - size * 0.65, cy - size * 0.05), - rl.Vector2(roof_x1, roof_y), th, color) - rl.draw_line_ex(rl.Vector2(roof_x1, roof_y), rl.Vector2(roof_x2, roof_y), th, color) - rl.draw_line_ex(rl.Vector2(roof_x2, roof_y), - rl.Vector2(cx + size * 0.65, cy - size * 0.05), th, color) - # Wheels - rl.draw_circle(int(cx - size * 0.55), int(cy + size * 0.55), th * 1.6, color) - rl.draw_circle(int(cx + size * 0.55), int(cy + size * 0.55), th * 1.6, color) - rl.draw_circle(int(cx - size * 0.55), int(cy + size * 0.55), th * 0.7, P.BG_DEEP) - rl.draw_circle(int(cx + size * 0.55), int(cy + size * 0.55), th * 0.7, P.BG_DEEP) - - -def _bluepilot(cx: float, cy: float, size: float, color: rl.Color) -> None: - th = max(3, size * 0.10) - # Steering wheel: outer ring + inner hub + 3 spokes. - rl.draw_ring(rl.Vector2(cx, cy), size * 0.78, size * 0.92, 0, 360, 64, color) - rl.draw_circle(int(cx), int(cy), size * 0.18, color) - for ang in (90, 210, 330): - a = math.radians(ang) - rl.draw_line_ex(rl.Vector2(cx, cy), - rl.Vector2(cx + math.cos(a) * size * 0.78, - cy + math.sin(a) * size * 0.78), - th, color) - - -# ---------------------------------------------------------------- -# Public lookup -# ---------------------------------------------------------------- -ICON_DRAWERS = { - "toggles": _sliders, - "network": _wifi, - "device": _cpu, - "firehose": _upload_cloud, - "developer": _terminal, - "vehicle": _vehicle, - "bluepilot": _bluepilot, -} - - -def draw_category_icon(name: str, cx: float, cy: float, size: float = 36, - color: rl.Color = P.TEXT, halo_color: rl.Color = None) -> None: - fn = ICON_DRAWERS.get(name) - if fn is None: - return - _draw_with_halo(fn, cx, cy, size, color, halo_color) diff --git a/selfdrive/ui/bp/mici/widgets/keyboard_bp.py b/selfdrive/ui/bp/mici/widgets/keyboard_bp.py deleted file mode 100644 index 293efd19e9..0000000000 --- a/selfdrive/ui/bp/mici/widgets/keyboard_bp.py +++ /dev/null @@ -1,405 +0,0 @@ -from enum import IntEnum -import pyray as rl -import numpy as np -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, MouseEvent -from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.widgets import Widget -from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter - -CHAR_FONT_SIZE = 42 -CHAR_NEAR_FONT_SIZE = CHAR_FONT_SIZE * 2 -SELECTED_CHAR_FONT_SIZE = 128 -CHAR_CAPS_FONT_SIZE = 38 # TODO: implement this -NUMBER_LAYER_SWITCH_FONT_SIZE = 24 -KEYBOARD_COLUMN_PADDING = 33 -KEYBOARD_ROW_PADDING = {0: 44, 1: 33, 2: 44} # TODO: 2 should be 116 with extra control keys added in - -KEY_TOUCH_AREA_OFFSET = 10 # px -KEY_DRAG_HYSTERESIS = 5 # px -KEY_MIN_ANIMATION_TIME = 0.075 # s - -DEBUG = False -ANIMATION_SCALE = 0.65 - - -def zip_repeat(a, b): - la, lb = len(a), len(b) - for i in range(max(la, lb)): - yield (a[i] if i < la else a[-1], - b[i] if i < lb else b[-1]) - - -def fast_euclidean_distance(dx, dy): - # https://en.wikibooks.org/wiki/Algorithms/Distance_approximations - max_d, min_d = abs(dx), abs(dy) - if max_d < min_d: - max_d, min_d = min_d, max_d - return 0.941246 * max_d + 0.41 * min_d - - -class Key(Widget): - def __init__(self, char: str, font_weight: FontWeight = FontWeight.SEMI_BOLD): - super().__init__() - self.char = char - self._font = gui_app.font(font_weight) - self._x_filter = BounceFilter(0.0, 0.1 * ANIMATION_SCALE, 1 / gui_app.target_fps) - self._y_filter = BounceFilter(0.0, 0.1 * ANIMATION_SCALE, 1 / gui_app.target_fps) - self._size_filter = BounceFilter(CHAR_FONT_SIZE, 0.1 * ANIMATION_SCALE, 1 / gui_app.target_fps) - self._alpha_filter = BounceFilter(1.0, 0.075 * ANIMATION_SCALE, 1 / gui_app.target_fps) - - self._color = rl.Color(255, 255, 255, 255) - - self._position_initialized = False - self.original_position = rl.Vector2(0, 0) - - def set_position(self, x: float, y: float, smooth: bool = True): - # Smooth keys within parent rect - base_y = self._parent_rect.y if self._parent_rect else 0.0 - local_y = y - base_y - - if not self._position_initialized: - self._x_filter.x = x - self._y_filter.x = local_y - # keep track of original position so dragging around feels consistent. also move touch area down a bit - self.original_position = rl.Vector2(x, local_y + KEY_TOUCH_AREA_OFFSET) - self._position_initialized = True - - if not smooth: - self._x_filter.x = x - self._y_filter.x = local_y - - self._rect.x = self._x_filter.update(x) - self._rect.y = base_y + self._y_filter.update(local_y) - - def set_alpha(self, alpha: float): - self._alpha_filter.update(alpha) - - def get_position(self) -> tuple[float, float]: - return self._rect.x, self._rect.y - - def _update_state(self): - self._color.a = min(int(255 * self._alpha_filter.x), 255) - - def _render(self, _): - # center char at rect position - text_size = measure_text_cached(self._font, self.char, self._get_font_size()) - x = self._rect.x + self._rect.width / 2 - text_size.x / 2 - y = self._rect.y + self._rect.height / 2 - text_size.y / 2 - rl.draw_text_ex(self._font, self.char, (x, y), self._get_font_size(), 0, self._color) - - if DEBUG: - rl.draw_circle(int(self._rect.x), int(self._rect.y), 5, rl.RED) # Debug: draw circle around key - rl.draw_rectangle_lines_ex(self._rect, 2, rl.RED) - - def set_font_size(self, size: float): - self._size_filter.update(size) - - def _get_font_size(self) -> int: - return round(self._size_filter.x) - - -class SmallKey(Key): - def __init__(self, chars: str): - super().__init__(chars, FontWeight.BOLD) - self._size_filter.x = NUMBER_LAYER_SWITCH_FONT_SIZE - - def set_font_size(self, size: float): - self._size_filter.update(size * (NUMBER_LAYER_SWITCH_FONT_SIZE / CHAR_FONT_SIZE)) - - -class IconKey(Key): - def __init__(self, icon: str, vertical_align: str = "center", char: str = "", icon_size: tuple[int, int] = (38, 38)): - super().__init__(char) - self._icon_size = icon_size - self._icon = gui_app.texture(icon, *icon_size) - self._vertical_align = vertical_align - - def set_icon(self, icon: str, icon_size: tuple[int, int] | None = None): - size = icon_size if icon_size is not None else self._icon_size - self._icon = gui_app.texture(icon, *size) - - def _render(self, _): - scale = np.interp(self._size_filter.x, [CHAR_FONT_SIZE, CHAR_NEAR_FONT_SIZE], [1, 1.5]) - - if self._vertical_align == "center": - dest_rec = rl.Rectangle(self._rect.x + (self._rect.width - self._icon.width * scale) / 2, - self._rect.y + (self._rect.height - self._icon.height * scale) / 2, - self._icon.width * scale, self._icon.height * scale) - src_rec = rl.Rectangle(0, 0, self._icon.width, self._icon.height) - rl.draw_texture_pro(self._icon, src_rec, dest_rec, rl.Vector2(0, 0), 0, self._color) - - elif self._vertical_align == "bottom": - dest_rec = rl.Rectangle(self._rect.x + (self._rect.width - self._icon.width * scale) / 2, self._rect.y, - self._icon.width * scale, self._icon.height * scale) - src_rec = rl.Rectangle(0, 0, self._icon.width, self._icon.height) - rl.draw_texture_pro(self._icon, src_rec, dest_rec, rl.Vector2(0, 0), 0, self._color) - - if DEBUG: - rl.draw_circle(int(self._rect.x), int(self._rect.y), 5, rl.RED) # Debug: draw circle around key - rl.draw_rectangle_lines_ex(self._rect, 2, rl.RED) - - -class CapsState(IntEnum): - LOWER = 0 - UPPER = 1 - LOCK = 2 - - -class MiciKeyboardBP(Widget): - def __init__(self, auto_return_to_letters: str = ""): - super().__init__() - self._auto_return_to_letters = auto_return_to_letters - - lower_chars = [ - "qwertyuiop", - "asdfghjkl", - "zxcvbnm", - ] - upper_chars = ["".join([char.upper() for char in row]) for row in lower_chars] - special_chars = [ - "1234567890", - "-/:;()$&@\"", - "~.,?!'#%", - ] - super_special_chars = [ - "1234567890", - "`[]{}^*+=_", - "\\|<>¥€£•", - ] - - self._lower_keys = [[Key(char) for char in row] for row in lower_chars] - self._upper_keys = [[Key(char) for char in row] for row in upper_chars] - self._special_keys = [[Key(char) for char in row] for row in special_chars] - self._super_special_keys = [[Key(char) for char in row] for row in super_special_chars] - - # control keys - self._space_key = IconKey("icons_mici/settings/keyboard/space.png", char=" ", vertical_align="bottom", icon_size=(43, 14)) - self._caps_key = IconKey("icons_mici/settings/keyboard/caps_lower.png", icon_size=(38, 33)) - # these two are in different places on some layouts - self._123_key, self._123_key2 = SmallKey("123"), SmallKey("123") - self._abc_key = SmallKey("abc") - self._super_special_key = SmallKey("#+=") - - # insert control keys - for keys in (self._lower_keys, self._upper_keys): - keys[2].insert(0, self._caps_key) - keys[2].append(self._123_key) - - for keys in (self._lower_keys, self._upper_keys, self._special_keys, self._super_special_keys): - keys[1].append(self._space_key) - - for keys in (self._special_keys, self._super_special_keys): - keys[2].append(self._abc_key) - - self._special_keys[2].insert(0, self._super_special_key) - self._super_special_keys[2].insert(0, self._123_key2) - - # set initial keys - self._current_keys: list[list[Key]] = [] - self._set_keys(self._lower_keys) - self._caps_state = CapsState.LOWER - self._initialized = False - - self._load_images() - - self._closest_key: tuple[Key | None, float] = None, float('inf') - self._selected_key_t: float | None = None # time key was initially selected - self._unselect_key_t: float | None = None # time to unselect key after release - self._dragging_on_keyboard = False - - self._text: str = "" - - self._bg_scale_filter = BounceFilter(1.0, 0.1 * ANIMATION_SCALE, 1 / gui_app.target_fps) - self._selected_key_filter = FirstOrderFilter(0.0, 0.075 * ANIMATION_SCALE, 1 / gui_app.target_fps) - - def get_candidate_character(self) -> str: - # return str of character about to be added to text - key = self._closest_key[0] - return key.char if key is not None and key.__class__ is Key and self._dragging_on_keyboard else "" - - def get_keyboard_height(self) -> int: - return int(self._txt_bg.height) - - def _load_images(self): - self._txt_bg = gui_app.texture("icons_mici/settings/keyboard/keyboard_background.png", 520, 170, keep_aspect_ratio=False) - - def _set_keys(self, keys: list[list[Key]]): - # inherit previous keys' positions to fix switching animation - for current_row, row in zip(self._current_keys, keys, strict=False): - # not all layouts have the same number of keys - for current_key, key in zip_repeat(current_row, row): - # reset parent rect for new keys - key.set_parent_rect(self._rect) - current_pos = current_key.get_position() - key.set_position(current_pos[0], current_pos[1], smooth=False) - - self._current_keys = keys - - def set_text(self, text: str): - self._text = text - - def text(self) -> str: - return self._text - - def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: - keyboard_pos_y = self._rect.y + self._rect.height - self._txt_bg.height - if mouse_event.left_pressed: - if mouse_event.pos.y > keyboard_pos_y: - self._dragging_on_keyboard = True - elif mouse_event.left_released: - self._dragging_on_keyboard = False - - if mouse_event.left_down and self._dragging_on_keyboard: - self._closest_key = self._get_closest_key() - if self._selected_key_t is None: - self._selected_key_t = rl.get_time() - - # unselect key temporarily if mouse goes above keyboard - if mouse_event.pos.y <= keyboard_pos_y: - self._closest_key = (None, float('inf')) - - if DEBUG: - print('HANDLE MOUSE EVENT', mouse_event, self._closest_key[0].char if self._closest_key[0] else 'None') - - def _get_closest_key(self) -> tuple[Key | None, float]: - closest_key: tuple[Key | None, float] = (None, float('inf')) - for row in self._current_keys: - for key in row: - mouse_pos = gui_app.last_mouse_event.pos - # approximate distance for comparison is accurate enough - # use local y coords so parent widget offset (e.g. during NavWidget animate-in) doesn't affect hit testing - dist = abs(key.original_position.x - mouse_pos.x) + abs(key.original_position.y - (mouse_pos.y - self._rect.y)) - if dist < closest_key[1]: - if self._closest_key[0] is None or key is self._closest_key[0] or dist < self._closest_key[1] - KEY_DRAG_HYSTERESIS: - closest_key = (key, dist) - return closest_key - - def _set_uppercase(self, cycle: bool): - self._set_keys(self._upper_keys if cycle else self._lower_keys) - if not cycle: - self._caps_state = CapsState.LOWER - self._caps_key.set_icon("icons_mici/settings/keyboard/caps_lower.png", icon_size=(38, 33)) - else: - if self._caps_state == CapsState.LOWER: - self._caps_state = CapsState.UPPER - self._caps_key.set_icon("icons_mici/settings/keyboard/caps_upper.png", icon_size=(38, 33)) - elif self._caps_state == CapsState.UPPER: - self._caps_state = CapsState.LOCK - self._caps_key.set_icon("icons_mici/settings/keyboard/caps_lock.png", icon_size=(39, 38)) - else: - self._set_uppercase(False) - - def _handle_mouse_release(self, mouse_pos: MousePos): - if self._closest_key[0] is not None: - if self._closest_key[0] == self._caps_key: - self._set_uppercase(True) - elif self._closest_key[0] in (self._123_key, self._123_key2): - self._set_keys(self._special_keys) - elif self._closest_key[0] == self._abc_key: - self._set_uppercase(False) - elif self._closest_key[0] == self._super_special_key: - self._set_keys(self._super_special_keys) - else: - self._text += self._closest_key[0].char - - # Reset caps state - if self._caps_state == CapsState.UPPER: - self._set_uppercase(False) - - # Switch back to letters after common URL delimiters - if self._closest_key[0].char in self._auto_return_to_letters and self._current_keys in (self._special_keys, self._super_special_keys): - self._set_uppercase(False) - - # ensure minimum selected animation time - key_selected_dt = rl.get_time() - (self._selected_key_t or 0) - cur_t = rl.get_time() - self._unselect_key_t = cur_t + KEY_MIN_ANIMATION_TIME if (key_selected_dt < KEY_MIN_ANIMATION_TIME) else cur_t - - def backspace(self): - if self._text: - self._text = self._text[:-1] - - def space(self): - self._text += ' ' - - def _update_state(self): - # update selected key filter - self._selected_key_filter.update(self._closest_key[0] is not None) - - # unselect key after animation plays - if (self._unselect_key_t is not None and rl.get_time() > self._unselect_key_t) or not self.enabled: - self._closest_key = (None, float('inf')) - self._unselect_key_t = None - self._selected_key_t = None - - def _lay_out_keys(self, bg_x, bg_y, keys: list[list[Key]]): - key_rect = rl.Rectangle(bg_x, bg_y, self._txt_bg.width, self._txt_bg.height) - for row_idx, row in enumerate(keys): - padding = KEYBOARD_ROW_PADDING[row_idx] - step_y = (key_rect.height - 2 * KEYBOARD_COLUMN_PADDING) / (len(keys) - 1) - for key_idx, key in enumerate(row): - key_x = key_rect.x + padding + key_idx * ((key_rect.width - 2 * padding) / (len(row) - 1)) - key_y = key_rect.y + KEYBOARD_COLUMN_PADDING + row_idx * step_y - - if self._closest_key[0] is None: - key.set_alpha(1.0) - key.set_font_size(CHAR_FONT_SIZE) - elif key == self._closest_key[0]: - # push key up with a max and inward so user can see key easier - key_y = max(key_y - 120, 40) - key_x += np.interp(key_x, [self._rect.x, self._rect.x + self._rect.width], [100, -100]) - key.set_alpha(1.0) - key.set_font_size(SELECTED_CHAR_FONT_SIZE) - - # draw black circle behind selected key - circle_alpha = int(self._selected_key_filter.x * 225) - rl.draw_circle_gradient(rl.Vector2(key_x + key.rect.width / 2, key_y + key.rect.height / 2), - SELECTED_CHAR_FONT_SIZE, rl.Color(0, 0, 0, circle_alpha), rl.BLANK) - else: - # move other keys away from selected key a bit - dx = key.original_position.x - self._closest_key[0].original_position.x - dy = key.original_position.y - self._closest_key[0].original_position.y - distance_from_selected_key = fast_euclidean_distance(dx, dy) - - inv = 1 / (distance_from_selected_key or 1.0) - ux = dx * inv - uy = dy * inv - - # NOTE: hardcode to 20 to get entire keyboard to move - push_pixels = np.interp(distance_from_selected_key, [0, 250], [20, 0]) - key_x += ux * push_pixels - key_y += uy * push_pixels - - # TODO: slow enough to use an approximation or nah? also caching might work - font_size = np.interp(distance_from_selected_key, [0, 150], [CHAR_NEAR_FONT_SIZE, CHAR_FONT_SIZE]) - - key_alpha = np.interp(distance_from_selected_key, [0, 100], [1.0, 0.35]) - key.set_alpha(key_alpha) - key.set_font_size(font_size) - - # TODO: I like the push amount, so we should clip the pos inside the keyboard rect - key.set_parent_rect(self._rect) - key.set_position(key_x, key_y) - - def _render(self, _): - # draw bg - bg_x = self._rect.x + (self._rect.width - self._txt_bg.width) / 2 - bg_y = self._rect.y + self._rect.height - self._txt_bg.height - - scale = self._bg_scale_filter.update(1.0307692307692307 if self._closest_key[0] is not None else 1.0) - src_rec = rl.Rectangle(0, 0, self._txt_bg.width, self._txt_bg.height) - dest_rec = rl.Rectangle(self._rect.x + self._rect.width / 2 - self._txt_bg.width * scale / 2, bg_y, - self._txt_bg.width * scale, self._txt_bg.height) - - rl.draw_texture_pro(self._txt_bg, src_rec, dest_rec, rl.Vector2(0, 0), 0.0, rl.WHITE) - - # draw keys - if not self._initialized: - for keys in (self._lower_keys, self._upper_keys, self._special_keys, self._super_special_keys): - self._lay_out_keys(bg_x, bg_y, keys) - self._initialized = True - - self._lay_out_keys(bg_x, bg_y, self._current_keys) - for row in self._current_keys: - for key in row: - key.render() diff --git a/selfdrive/ui/bp/mici/widgets/long_press_progress.py b/selfdrive/ui/bp/mici/widgets/long_press_progress.py deleted file mode 100644 index 4a64c683a3..0000000000 --- a/selfdrive/ui/bp/mici/widgets/long_press_progress.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Top-edge progress strip for long-press feedback on the BP home. - -The home owns the long-press timer (mirrors stock MiciHomeLayout). It calls -update(progress) each frame with 0..1; this widget paints a 4px cyan strip -whose width = progress * rect_width. Glows out via box drop-shadow. -""" -import pyray as rl -from openpilot.system.ui.widgets import Widget -from openpilot.selfdrive.ui.bp.mici.widgets import bp_palette as P - - -class LongPressBar(Widget): - HEIGHT = 4 - - def __init__(self): - super().__init__() - self._progress: float = 0.0 # 0..1 - - def set_progress(self, p: float) -> None: - self._progress = max(0.0, min(1.0, p)) - - def _render(self, _): - if self._progress <= 0.0: - return - r = self._rect - w = r.width * self._progress - rl.draw_rectangle(int(r.x), int(r.y), int(w), self.HEIGHT, P.ACCENT2) - # Subtle halo: a slightly translucent overlay one row below - halo = rl.Color(P.ACCENT2.r, P.ACCENT2.g, P.ACCENT2.b, 90) - rl.draw_rectangle(int(r.x), int(r.y) + self.HEIGHT, int(w), 2, halo) diff --git a/selfdrive/ui/bp/mici/widgets/page_dots.py b/selfdrive/ui/bp/mici/widgets/page_dots.py deleted file mode 100644 index aea80498e9..0000000000 --- a/selfdrive/ui/bp/mici/widgets/page_dots.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Page-indicator dots row for the BP paged scroller. - -Renders N dots with the active one elongated cyan (mockup style). Reads -current page + count from a callable so we don't have to reset on changes. -""" -import pyray as rl -from collections.abc import Callable - -from openpilot.system.ui.widgets import Widget -from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.system.ui.lib.application import gui_app -from openpilot.selfdrive.ui.bp.mici.widgets import bp_palette as P - - -DOT_SIZE = 7 -DOT_ACTIVE_W = 22 -GAP = 6 -ANIMATION_RC = 0.05 - - -class PageDots(Widget): - def __init__(self, get_current_page: Callable[[], int], get_page_count: Callable[[], int]): - super().__init__() - self._get_current = get_current_page - self._get_count = get_page_count - self._widths: list[FirstOrderFilter] = [] - - def _ensure_filters(self, n: int) -> None: - while len(self._widths) < n: - self._widths.append(FirstOrderFilter(DOT_SIZE, ANIMATION_RC, 1 / gui_app.target_fps)) - while len(self._widths) > n: - self._widths.pop() - - def _render(self, _): - count = self._get_count() - if count <= 1: - return # nothing to indicate - cur = self._get_current() - self._ensure_filters(count) - - # Animate widths (active → DOT_ACTIVE_W, others → DOT_SIZE) - for i, f in enumerate(self._widths): - target = DOT_ACTIVE_W if i == cur else DOT_SIZE - f.update(target) - - total_w = sum(f.x for f in self._widths) + GAP * (count - 1) - r = self._rect - x = r.x + (r.width - total_w) / 2 - y = r.y + (r.height - DOT_SIZE) / 2 - - for i, f in enumerate(self._widths): - w = f.x - if i == cur: - # Active: cyan rounded pill - dot_rect = rl.Rectangle(int(x), int(y), w, DOT_SIZE) - rl.draw_rectangle_rounded(dot_rect, 1.0, 8, P.ACCENT2) - else: - # Inactive: dim circle - cx = int(x + DOT_SIZE / 2) - cy = int(y + DOT_SIZE / 2) - rl.draw_circle(cx, cy, DOT_SIZE / 2, rl.Color(0xFF, 0xFF, 0xFF, int(0.25 * 255))) - x += w + GAP diff --git a/selfdrive/ui/bp/mici/widgets/paged_scroller.py b/selfdrive/ui/bp/mici/widgets/paged_scroller.py deleted file mode 100644 index 3bea5d6a17..0000000000 --- a/selfdrive/ui/bp/mici/widgets/paged_scroller.py +++ /dev/null @@ -1,81 +0,0 @@ -"""One-item-per-page horizontal carousel for BP MICI sub-panels. - -Wraps the existing _Scroller (snap-to-item) by forcing every child to occupy a -full page width. The user-facing view is a horizontal pager with edge resistance -and momentum from GuiScrollPanel2. - -Public API: - - PagedScroller(items=[card,...]) — items are full-page widgets sized to - (page_width, page_height); they are auto-resized in show_event() and - whenever the parent rect changes. - - current_page — int, the page closest to center. - - page_count — int. - - set_page(idx) — smooth scroll to page idx. - -The included tap-vs-swipe arbitration is automatic: _Scroller's touch_valid -callback gates child clicks on `scroll_panel.is_touch_valid()`, which goes False -during a drag. So tap on a switch only fires if the carousel never entered -DRAGGING — the raylib equivalent of the mockup's setPointerCapture + -elementFromPoint dance. -""" -import pyray as rl - -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.scroller import _Scroller - - -class PagedScroller(Widget): - def __init__(self, items: list[Widget]): - super().__init__() - # spacing=0, pad=0 means each item takes exactly 1 page; snap_items=True - # snaps to the centered item ⇒ snaps to whole pages. - self._scroller = self._child(_Scroller( - [], horizontal=True, snap_items=True, - spacing=0, pad=0, - scroll_indicator=False, edge_shadows=False, - )) - self._items = items - for item in items: - self._scroller.add_widget(item) - - # ---- public API ---- - @property - def page_count(self) -> int: - return len(self._items) - - @property - def current_page(self) -> int: - """Page index closest to the viewport center.""" - if self.page_count == 0 or self._rect.width <= 0: - return 0 - page_w = self._rect.width - # scroll_offset is negative as we scroll right. 0 = page 0. - raw = -self._scroller._scroll_offset / page_w - return max(0, min(self.page_count - 1, round(raw))) - - def set_page(self, idx: int, smooth: bool = True) -> None: - """Move to page `idx`. Sets the underlying scroll offset directly so - snap math doesn't fight us — Scroller.scroll_to() expects screen-space - coordinates which we don't always have here. - """ - idx = max(0, min(self.page_count - 1, idx)) - target_offset = -idx * self._rect.width - self._scroller.scroll_panel.set_offset(target_offset) - if not smooth: - # Cancel any in-flight snap interp so the new offset sticks. - self._scroller._scrolling_to = (None, False) - self._scroller._scroll_snap_filter.x = 0.0 - - # ---- lifecycle ---- - def _update_layout_rects(self) -> None: - # Resize every child to a full page so snap-to-item == snap-to-page. - for item in self._items: - item.set_rect(rl.Rectangle(0, 0, self._rect.width, self._rect.height)) - - def show_event(self): - super().show_event() - # Make sure children are sized correctly for first render - self._update_layout_rects() - - def _render(self, _): - self._scroller.render(self._rect) diff --git a/selfdrive/ui/bp/mici/widgets/preferred_network_select.py b/selfdrive/ui/bp/mici/widgets/preferred_network_select.py deleted file mode 100644 index 9bdee920e6..0000000000 --- a/selfdrive/ui/bp/mici/widgets/preferred_network_select.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -BluePilot: MICI preferred WiFi network selector. - -BP-styled: vertical scrolling list of saved networks rendered as frosted -row cards on the BP radial backdrop. Tap a network to set it as preferred, -or "None" to clear. -""" - -from collections.abc import Callable - -from openpilot.common.params import Params -from openpilot.common.swaglog import cloudlog -from openpilot.selfdrive.ui.bp.mici.widgets.cards import BPSelectTile -from openpilot.selfdrive.ui.bp.mici.widgets.select_panel import BPSelectPanel -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.lib.wifi_manager import WifiManager, Network - - -WIFI_ICON = "icons_mici/settings/network/wifi_strength_full.png" - - -def _display_ssid(ssid: str) -> str: - """Normalize SSID for display; fallback for empty.""" - s = (ssid or "").strip() - if not s: - return "Hidden Network" - return "".join(c for c in s if c.isprintable() or c in " \t") or "Hidden Network" - - -class PreferredNetworkSelectMici(BPSelectPanel): - """Tap a saved network to set it as preferred; tap "None" to clear.""" - TITLE = "Preferred Network" - - def __init__(self, wifi_manager: WifiManager, saved_networks: list[Network], - on_dismiss: Callable[[], None] | None = None): - self._params = Params() - self._wifi_manager = wifi_manager - self._saved_networks = saved_networks - self._on_dismiss = on_dismiss - self._current = "" - val = self._params.get("WifiFavoriteSSID") - if isinstance(val, bytes): - val = val.decode("utf-8", errors="replace") - self._current = (val or "").strip() - super().__init__() - if on_dismiss is not None: - self.set_back_callback(on_dismiss) - - def _build_rows(self) -> list[BPSelectTile]: - rows = [BPSelectTile( - label=tr("None"), - icon=WIFI_ICON, - selected=(self._current == ""), - on_click=lambda: self._select(""), - )] - for network in self._saved_networks: - ssid = network.ssid - rows.append(BPSelectTile( - label=_display_ssid(ssid), - icon=WIFI_ICON, - selected=(ssid == self._current), - on_click=lambda s=ssid: self._select(s), - )) - return rows - - def _select(self, ssid: str): - """Save selection and dismiss.""" - self._params.put("WifiFavoriteSSID", ssid) - if ssid: - cloudlog.info(f"Set preferred network: {ssid}") - else: - cloudlog.info("Cleared preferred network") - gui_app.pop_widget() - if self._on_dismiss: - self._on_dismiss() - - def show_event(self): - super().show_event() - self._wifi_manager.set_active(True) - - def hide_event(self): - super().hide_event() - self._wifi_manager.set_active(False) diff --git a/selfdrive/ui/bp/mici/widgets/select_panel.py b/selfdrive/ui/bp/mici/widgets/select_panel.py deleted file mode 100644 index 9e86c909e8..0000000000 --- a/selfdrive/ui/bp/mici/widgets/select_panel.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Base class for BP MICI "select from a list" screens. - -Same look as the BP sub-panels (radial backdrop, BP topbar, frosted tiles). -Body is a horizontal scroller of `BPSelectTile`s — matches the rest of the BP -UI (settings landing, sub-panels) and avoids the swipe-down-to-dismiss vs. -scroll-back-to-top conflict that bites NavWidget children using vertical -scrollers. -""" -import pyray as rl -from collections.abc import Callable - -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.widgets.nav_widget import NavWidget -from openpilot.system.ui.widgets.scroller import _Scroller - -from openpilot.selfdrive.ui.bp.mici.widgets.bg_radial import BPRadialBackground -from openpilot.selfdrive.ui.bp.mici.widgets.bp_topbar import BPTopbar, TOPBAR_HEIGHT -from openpilot.selfdrive.ui.bp.mici.widgets.cards import BPSelectTile - - -class BPSelectPanel(NavWidget): - """NavWidget composing topbar + horizontal scroller of `BPSelectTile`s.""" - TITLE: str = "" - - def __init__(self, back_callback: Callable | None = None): - super().__init__() - if back_callback is not None: - self.set_back_callback(back_callback) - - self._bg = self._child(BPRadialBackground()) - self._topbar = self._child(BPTopbar(title=self.TITLE, on_back=self._on_back)) - - tiles = self._build_rows() - self._scroller = self._child(_Scroller( - tiles, horizontal=True, snap_items=False, - spacing=6, pad=10, - scroll_indicator=True, edge_shadows=False, - )) - - def _on_back(self): - gui_app.pop_widget() - if self._back_callback is not None: - self._back_callback() - - # ---- subclass hook ---- - def _build_rows(self) -> list[BPSelectTile]: - raise NotImplementedError - - # ---- helper for subclasses to rebuild on data change ---- - def _replace_rows(self, tiles: list[BPSelectTile]): - self._scroller._items = [] - for tile in tiles: - self._scroller.add_widget(tile) - - # ---- render ---- - def _render(self, _): - r = self._rect - self._bg.render(r) - - topbar_rect = rl.Rectangle(r.x, r.y, r.width, TOPBAR_HEIGHT) - self._topbar.render(topbar_rect) - - body_y = r.y + TOPBAR_HEIGHT - body_h = r.height - TOPBAR_HEIGHT - self._scroller.render(rl.Rectangle(r.x, body_y, r.width, body_h)) diff --git a/selfdrive/ui/bp/mici/widgets/state_pill.py b/selfdrive/ui/bp/mici/widgets/state_pill.py deleted file mode 100644 index a233fd72cf..0000000000 --- a/selfdrive/ui/bp/mici/widgets/state_pill.py +++ /dev/null @@ -1,77 +0,0 @@ -"""State pill — rounded pill with pulsing dot + caps label. - -Three modes: STANDARD READY (green dot) / EXPERIMENTAL ARMED (amber dot) / -OFFLINE (grey dot, no pulse). -""" -import math -import pyray as rl -from collections.abc import Callable - -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.selfdrive.ui.bp.mici.widgets import bp_palette as P - - -class StatePill(Widget): - """Pill: [colored dot] LABEL ; auto-sized to label width. - - Pass a callable that returns ('ready'|'experimental'|'offline', text). The - pill recolors and resizes itself to fit the text per render. - """ - PADDING_X = 18 - PADDING_Y = 8 - DOT_SIZE = 16 - GAP = 12 - - def __init__(self, get_state: Callable[[], tuple[str, str]]): - super().__init__() - self._get_state = get_state - self._label = UnifiedLabel( - "", - font_size=P.FS_PILL, - font_weight=FontWeight.BOLD, - text_color=P.TEXT, - max_width=480, - wrap_text=False, - ) - self._mode = "ready" - - def _update_state(self): - self._mode, text = self._get_state() - self._label.set_text(text) - - # Resize self to fit content - text_w = self._label.text_width - pill_w = int(self.PADDING_X * 2 + self.DOT_SIZE + self.GAP + text_w) - pill_h = int(self.PADDING_Y * 2 + max(self.DOT_SIZE, P.FS_PILL)) - self._rect = rl.Rectangle(self._rect.x, self._rect.y, pill_w, pill_h) - - def _dot_color(self) -> rl.Color: - return {"experimental": P.WARN, "offline": P.OFFLINE}.get(self._mode, P.READY) - - def _render(self, _): - r = self._rect - - # Pill bg - rl.draw_rectangle_rounded(r, 1.0, 16, P.PANEL_BG) - rl.draw_rectangle_rounded_lines_ex(r, 1.0, 16, 1, P.PANEL_BORDER) - - # Pulsing dot (no pulse when offline) - cx = r.x + self.PADDING_X + self.DOT_SIZE / 2 - cy = r.y + r.height / 2 - color = self._dot_color() - if self._mode != "offline": - # Soft 1.8s pulse via sine: scale 0.85 → 1.0 - t = rl.get_time() - pulse = 0.85 + 0.15 * (math.sin(t * (2 * math.pi / 1.8)) + 1) * 0.5 - glow = rl.Color(color.r, color.g, color.b, int(color.a * 0.5)) - rl.draw_circle(int(cx), int(cy), self.DOT_SIZE * 0.9 * pulse, glow) - rl.draw_circle(int(cx), int(cy), self.DOT_SIZE / 2, color) - - # Label - text_x = r.x + self.PADDING_X + self.DOT_SIZE + self.GAP - # Vertical-center the label inside the pill (font baseline math via font_size) - text_y = r.y + (r.height - P.FS_PILL) / 2 - self._label.set_position(text_x, text_y) - self._label.render() diff --git a/selfdrive/ui/bp/mici/widgets/sub_panel.py b/selfdrive/ui/bp/mici/widgets/sub_panel.py deleted file mode 100644 index 69e2208e41..0000000000 --- a/selfdrive/ui/bp/mici/widgets/sub_panel.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Base class for BP MICI sub-panels. - -Composes a topbar, a paged-carousel of single-item-per-page cards, and a row -of page-indicator dots on top of the BP radial backdrop. Subclasses just -provide a list of card widgets. -""" -import pyray as rl -from collections.abc import Callable - -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.nav_widget import NavWidget - -from openpilot.selfdrive.ui.bp.mici.widgets.bg_radial import BPRadialBackground -from openpilot.selfdrive.ui.bp.mici.widgets.bp_topbar import BPTopbar, TOPBAR_HEIGHT -from openpilot.selfdrive.ui.bp.mici.widgets.paged_scroller import PagedScroller -from openpilot.selfdrive.ui.bp.mici.widgets.page_dots import PageDots - - -DOTS_HEIGHT = 20 - - -class BPSubPanel(NavWidget): - """A NavWidget containing topbar + paged carousel + dot indicator. - - Override `_build_pages()` (returns list[Widget]) in subclasses. - Title is provided via `title` parameter; back_callback is optional (NavWidget - also supports swipe-down to dismiss). - """ - TITLE: str = "" - - def __init__(self, back_callback: Callable | None = None): - super().__init__() - if back_callback is not None: - self.set_back_callback(back_callback) - - self._bg = self._child(BPRadialBackground()) - - pages = self._build_pages() - self._pager = self._child(PagedScroller(pages)) - - self._topbar = self._child(BPTopbar( - title=self.TITLE, - on_back=self._on_back, - get_page_meta=lambda: (self._pager.current_page, self._pager.page_count), - )) - - self._dots = self._child(PageDots( - get_current_page=lambda: self._pager.current_page, - get_page_count=lambda: self._pager.page_count, - )) - - def _on_back(self): - # Tapping the back chevron same as a programmatic dismiss. - from openpilot.system.ui.lib.application import gui_app as _ga - _ga.pop_widget() - if self._back_callback is not None: - self._back_callback() - - # ---- subclass hook ---- - def _build_pages(self) -> list[Widget]: - """Return the cards (one per page).""" - raise NotImplementedError - - # ---- render ---- - def _render(self, _): - r = self._rect - self._bg.render(r) - - # Topbar (50px) at the top - topbar_rect = rl.Rectangle(r.x, r.y, r.width, TOPBAR_HEIGHT) - self._topbar.render(topbar_rect) - - # Pager fills middle, leaving room for dots at bottom - pager_y = r.y + TOPBAR_HEIGHT - pager_h = r.height - TOPBAR_HEIGHT - DOTS_HEIGHT - self._pager.render(rl.Rectangle(r.x, pager_y, r.width, pager_h)) - - # Dots row pinned to bottom - self._dots.render(rl.Rectangle(r.x, r.y + r.height - DOTS_HEIGHT, r.width, DOTS_HEIGHT)) diff --git a/selfdrive/ui/bp/mici/widgets/vehicle_select_mici.py b/selfdrive/ui/bp/mici/widgets/vehicle_select_mici.py deleted file mode 100644 index 1c68a85be0..0000000000 --- a/selfdrive/ui/bp/mici/widgets/vehicle_select_mici.py +++ /dev/null @@ -1,161 +0,0 @@ -""" -BluePilot: MICI vehicle fingerprint selector (make → model). - -BP-styled: uses BPSelectPanel (BP topbar + radial backdrop + vertical -scrolling frosted row cards) to match the rest of the BP UI. Car data from -sunnypilot selfdrive/car/car_list.json (same as TICI PlatformSelector). -""" - -from __future__ import annotations - -import json -import os -from collections.abc import Callable - -from openpilot.common.basedir import BASEDIR -from openpilot.common.swaglog import cloudlog -from openpilot.selfdrive.ui.bp.mici.widgets.bp_dialogs import BPConfirmDialog -from openpilot.selfdrive.ui.bp.mici.widgets.cards import BPSelectTile -from openpilot.selfdrive.ui.bp.mici.widgets.select_panel import BPSelectPanel -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.multilang import tr - -CAR_LIST_JSON = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "car", "car_list.json") -VEHICLE_ICON = "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png" - - -def load_car_platforms() -> dict: - with open(CAR_LIST_JSON) as f: - return json.load(f) - - -def platform_names_for_make(platforms: dict, make: str) -> list[str]: - names = [p for p, d in platforms.items() if d.get("make") == make] - return sorted(names) - - -def makes_available(platforms: dict) -> list[tuple[str, str]]: - """Every unique ``make`` from car_list.json, sorted.""" - makes = sorted({d.get("make") for d in platforms.values() if d.get("make")}) - return [(m, m) for m in makes] - - -def _current_platform_name() -> str: - bundle = ui_state.params.get("CarPlatformBundle") - if not bundle: - return "" - name = bundle.get("name", "") if isinstance(bundle, dict) else "" - if isinstance(name, bytes): - name = name.decode("utf-8", errors="replace") - return str(name).strip() - - -class VehicleMakeSelectMici(BPSelectPanel): - """Vertical list of make rows; tapping a make pushes the model selector.""" - TITLE = "Select Make" - - def __init__( - self, - platforms: dict, - on_stack_done: Callable[[], None] | None = None, - ): - self._platforms = platforms - self._on_stack_done = on_stack_done - self._current_make = "" - cur = _current_platform_name() - if cur: - data = platforms.get(cur) - if data: - self._current_make = data.get("make", "") - super().__init__() - # Wire swipe-down + chevron back to fire on_stack_done so the parent - # panel (Vehicle settings) refreshes its current-vehicle text. - if on_stack_done is not None: - self.set_back_callback(on_stack_done) - - def _build_rows(self) -> list[BPSelectTile]: - rows: list[BPSelectTile] = [] - for display_label, make_key in makes_available(self._platforms): - rows.append(BPSelectTile( - label=display_label, - icon=VEHICLE_ICON, - selected=(make_key == self._current_make), - on_click=lambda mk=make_key: self._open_models(mk), - )) - return rows - - def _open_models(self, make_key: str): - names = platform_names_for_make(self._platforms, make_key) - if not names: - cloudlog.warning(f"No platforms for make {make_key}") - return - panel = VehicleModelSelectMici( - self._platforms, - names, - on_vehicle_set=self._after_vehicle_set, - ) - gui_app.push_widget(panel) - - def _after_vehicle_set(self): - """User confirmed a vehicle: pop make panel, refresh root screen.""" - gui_app.pop_widget() - if self._on_stack_done: - self._on_stack_done() - - -class VehicleModelSelectMici(BPSelectPanel): - """Vertical list of platform names for the chosen make; tap to confirm + apply.""" - TITLE = "Select Model" - - def __init__( - self, - platforms: dict, - platform_names: list[str], - on_vehicle_set: Callable[[], None] | None = None, - ): - self._platforms = platforms - self._platform_names = platform_names - self._on_vehicle_set = on_vehicle_set - self._current = _current_platform_name() - super().__init__() - - def _build_rows(self) -> list[BPSelectTile]: - # Models can be 50+ chars (e.g. "Hyundai Ioniq 5 (Southeast Asia ...)") — - # widen each tile so ~2 fit per page and let the label wrap to 2-3 lines. - rows: list[BPSelectTile] = [] - for name in self._platform_names: - rows.append(BPSelectTile( - label=name, - icon=VEHICLE_ICON, - selected=(name == self._current), - on_click=lambda n=name: self._ask_confirm(n), - width=260, - wrap_label=True, - )) - return rows - - def _ask_confirm(self, platform_name: str): - title = ( - tr("slide to\napply now") - if ui_state.is_offroad - else tr("slide to\napply when offroad") - ) - dlg = BPConfirmDialog( - title, - gui_app.texture("icons_mici/settings/car_icon.png", 64, 64), - red=False, - confirm_callback=lambda: self._apply_vehicle(platform_name), - ) - gui_app.push_widget(dlg) - - def _apply_vehicle(self, platform_name: str): - data = self._platforms.get(platform_name) - if not data: - cloudlog.error(f"Missing car_list entry for {platform_name}") - return - ui_state.params.put("CarPlatformBundle", {**data, "name": platform_name}) - cloudlog.info(f"MICI vehicle: set CarPlatformBundle to {platform_name}") - gui_app.pop_widget() - if self._on_vehicle_set: - self._on_vehicle_set() diff --git a/selfdrive/ui/bp/mici/widgets/web_server_qr_dialog_bp.py b/selfdrive/ui/bp/mici/widgets/web_server_qr_dialog_bp.py deleted file mode 100644 index 65b72d8c85..0000000000 --- a/selfdrive/ui/bp/mici/widgets/web_server_qr_dialog_bp.py +++ /dev/null @@ -1,130 +0,0 @@ -"""BP-styled QR dialog for the BluePilot web server. - -Replaces selfdrive/ui/bp/mici/widgets/web_server_qr_dialog.py with a layout -that matches the new BP design language: radial backdrop, BP topbar with -chevron back, QR centered on the left, URL + caption + a BP toggle on the -right. The QR generation, IP detection, and toggle behavior are reused -verbatim from the existing dialog. -""" -import pyray as rl -from collections.abc import Callable - -from openpilot.common.params import Params -from openpilot.common.swaglog import cloudlog -from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.widgets.nav_widget import NavWidget - -from openpilot.selfdrive.ui.bp.mici.widgets.bg_radial import BPRadialBackground -from openpilot.selfdrive.ui.bp.mici.widgets.bp_topbar import BPTopbar, TOPBAR_HEIGHT -from openpilot.selfdrive.ui.bp.mici.widgets.cards import BigToggleCard, _fit_single_line -from openpilot.selfdrive.ui.bp.mici.widgets import bp_palette as P - -# Reuse the URL / IP / QR-image helpers from the existing dialog so we don't -# fork that logic. Only the rendering layer is reimplemented here. -from openpilot.selfdrive.ui.bp.mici.widgets.web_server_qr_dialog import ( - WebServerQRDialog as _LegacyQRDialog, -) - - -class BPWebServerQRDialog(NavWidget): - """BP-styled QR display + disable toggle.""" - - PADDING = 14 - - def __init__(self, back_callback: Callable | None = None): - super().__init__() - if back_callback is not None: - self.set_back_callback(back_callback) - self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - self._params = Params() - - # Borrow the legacy QR generation logic by composition. - self._gen = _LegacyQRDialog(back_callback=back_callback or (lambda: None)) - # We don't render the legacy widget — only call its helpers — so detach - # its callbacks to avoid surprising side effects. - self._gen._handle_toggle = lambda *_a, **_k: None - - self._bg = self._child(BPRadialBackground()) - self._topbar = self._child(BPTopbar(title="Web Server", on_back=self._on_back)) - - self._url_label = UnifiedLabel( - "", font_size=22, font_weight=FontWeight.BOLD, text_color=P.ACCENT2, - letter_spacing=0.02, max_width=380, wrap_text=False, elide=False, - ) - self._scan_label = UnifiedLabel( - "scan to open", font_size=20, font_weight=FontWeight.MEDIUM, - text_color=P.MUTED, letter_spacing=0.04, - max_width=380, wrap_text=False, - ) - self._error_label = UnifiedLabel( - "no Wi-Fi connection", font_size=24, font_weight=FontWeight.BOLD, - text_color=P.DANGER, max_width=380, wrap_text=False, - ) - - # BP toggle for the disable action — same Params key as the legacy widget. - self._disable_toggle = self._child(BigToggleCard( - "Web Server", "Tap to disable.", param_key="EnableWebRoutesServer", - on_change=self._on_toggle_change, - )) - - # ---- handlers ---- - def _on_back(self): - gui_app.pop_widget() - - def _on_toggle_change(self, enabled: bool): - if not enabled: - self._on_back() - - # ---- render ---- - def _render(self, _): - r = self._rect - self._bg.render(r) - - # Topbar - self._topbar.render(rl.Rectangle(r.x, r.y, r.width, TOPBAR_HEIGHT)) - - # Body area - body_y = r.y + TOPBAR_HEIGHT - body_h = r.height - TOPBAR_HEIGHT - 8 - - # ---- Left column: QR + URL/caption ---- - try: - self._gen._generate_qr_code() - url = self._gen._get_server_url() - except Exception as e: - cloudlog.warning(f"BPQRDialog: gen failed: {e}") - url = "" - - # QR sizing — fit a square inside the body height with caption underneath. - caption_h = 22 + 4 + 20 + 8 # url + gap + scan caption + bottom margin - qr_max = body_h - caption_h - 8 - qr_size = max(60, min(qr_max, 160)) - qr_x = r.x + self.PADDING - qr_y = body_y + (body_h - (qr_size + caption_h)) / 2 - - if self._gen._qr_texture is not None and self._gen._qr_texture.id != 0: - scale = qr_size / max(1, self._gen._qr_texture.height) - rl.draw_texture_ex(self._gen._qr_texture, rl.Vector2(qr_x, qr_y), 0.0, scale, rl.WHITE) - else: - # No connection — show error in QR area - err_x = qr_x + (qr_size - 200) / 2 - err_y = qr_y + (qr_size - 24) / 2 - self._error_label.set_position(err_x, err_y) - self._error_label.render() - - # URL + caption under the QR - if url: - _fit_single_line(self._url_label, 22, qr_size + 60, min_size=14) - self._url_label.set_text(url) - self._url_label.set_position(qr_x, qr_y + qr_size + 6) - self._url_label.render() - - self._scan_label.set_position(qr_x, qr_y + qr_size + 30) - self._scan_label.render() - - # ---- Right column: BP-styled disable toggle ---- - right_x = qr_x + qr_size + 20 - right_w = r.width - right_x - self.PADDING - self._disable_toggle.render(rl.Rectangle(right_x, body_y + 6, right_w, body_h - 12)) diff --git a/selfdrive/ui/mici/layouts/settings/bluepilot.py b/selfdrive/ui/mici/layouts/settings/bluepilot.py index 1d1a72be85..9d0418ae32 100644 --- a/selfdrive/ui/mici/layouts/settings/bluepilot.py +++ b/selfdrive/ui/mici/layouts/settings/bluepilot.py @@ -8,7 +8,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiParamToggle, BigMultiToggle, BigParamControl from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog -from openpilot.selfdrive.ui.bp.mici.widgets.web_server_qr_dialog import WebServerQRDialog +from openpilot.selfdrive.ui.mici.layouts.settings.web_server_qr_dialog import WebServerQRDialog from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app, MousePos from openpilot.system.ui.lib.multilang import tr diff --git a/selfdrive/ui/bp/mici/widgets/web_server_qr_dialog.py b/selfdrive/ui/mici/layouts/settings/web_server_qr_dialog.py similarity index 69% rename from selfdrive/ui/bp/mici/widgets/web_server_qr_dialog.py rename to selfdrive/ui/mici/layouts/settings/web_server_qr_dialog.py index a845a2b7ab..20b91243e9 100644 --- a/selfdrive/ui/bp/mici/widgets/web_server_qr_dialog.py +++ b/selfdrive/ui/mici/layouts/settings/web_server_qr_dialog.py @@ -1,20 +1,19 @@ +from collections.abc import Callable +import subprocess + +import numpy as np import pyray as rl import qrcode -import numpy as np -import subprocess -from typing import Callable -from openpilot.common.swaglog import cloudlog from openpilot.common.params import Params -from openpilot.system.ui.widgets.nav_widget import NavWidget +from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.ui.mici.widgets.button import BigParamControl from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.selfdrive.ui.mici.widgets.button import BigParamControl +from openpilot.system.ui.widgets.nav_widget import NavWidget class WebServerQRDialog(NavWidget): - """Dialog showing QR code for webserver access and toggle to disable.""" - def __init__(self, back_callback: Callable): super().__init__() self.set_back_callback(back_callback) @@ -22,14 +21,11 @@ def __init__(self, back_callback: Callable): self._params = Params() self._qr_texture: rl.Texture | None = None self._last_url = "" - - # Toggle to disable server (initially enabled since dialog shows when server is on) + self._disable_toggle = BigParamControl("web routes server", "EnableWebRoutesServer", toggle_callback=self._handle_toggle) - # Ensure toggle reflects current state self._disable_toggle.refresh() - - # Labels (BluePilot: migrated from MiciLabel to UnifiedLabel after upstream removal) + self._title_label = UnifiedLabel("web routes server", font_size=56, font_weight=FontWeight.BOLD, text_color=rl.Color(255, 255, 255, int(255 * 0.9))) self._url_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.MEDIUM, @@ -38,7 +34,6 @@ def __init__(self, back_callback: Callable): text_color=rl.Color(150, 150, 150, int(255 * 0.7))) def _get_wifi_ip(self) -> str: - """Get WiFi interface IP address.""" def _parse_ip_from_line(line: str) -> str | None: try: if 'inet ' in line: @@ -52,8 +47,7 @@ def _parse_ip_from_line(line: str) -> str | None: return None try: - result = subprocess.run(['ip', 'addr', 'show', 'wlan0'], - capture_output=True, text=True, timeout=2) + result = subprocess.run(['ip', 'addr', 'show', 'wlan0'], capture_output=True, text=True, timeout=2) for line in (result.stdout or '').split('\n'): ip = _parse_ip_from_line(line) if ip: @@ -63,8 +57,7 @@ def _parse_ip_from_line(line: str) -> str | None: try: for iface in ['wlan1', 'wlan2']: - result = subprocess.run(['ip', 'addr', 'show', iface], - capture_output=True, text=True, timeout=2) + result = subprocess.run(['ip', 'addr', 'show', iface], capture_output=True, text=True, timeout=2) for line in (result.stdout or '').split('\n'): ip = _parse_ip_from_line(line) if ip: @@ -75,7 +68,6 @@ def _parse_ip_from_line(line: str) -> str | None: return "" def _get_server_url(self) -> str: - """Get the full server URL for QR code.""" wifi_ip = self._get_wifi_ip() if not wifi_ip: return "" @@ -88,18 +80,16 @@ def _get_server_url(self) -> str: return f"http://{wifi_ip}:{port}" def _generate_qr_code(self) -> None: - """Generate QR code texture from server URL.""" url = self._get_server_url() if not url: self._qr_texture = None return - - # Only regenerate if URL changed + if url == self._last_url and self._qr_texture: return - + self._last_url = url - + try: qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=0) qr.add_data(url) @@ -124,13 +114,10 @@ def _generate_qr_code(self) -> None: self._qr_texture = None def _handle_toggle(self, checked: bool): - """Handle toggle click - if disabled, close dialog.""" - if not checked: - # Server was disabled, close the dialog - if self._back_callback: - self._back_callback() + if not checked and self._back_callback: + self._back_callback() - def _render(self, rect: rl.Rectangle) -> int: + def _render(self, _): try: self._generate_qr_code() url = self._get_server_url() @@ -138,77 +125,53 @@ def _render(self, rect: rl.Rectangle) -> int: cloudlog.warning(f"QR dialog state error: {e}") url = "" - # Layout: QR code on left, controls on right padding = 24 qr_size = max(80, min(self._rect.height - (padding * 2), (self._rect.width - padding * 3) // 2)) qr_x = self._rect.x + padding qr_y = self._rect.y + padding - right_x = qr_x + qr_size + padding right_width = max(100, self._rect.width - right_x - padding) - # Render QR code on left (or error message when no IP) self._render_qr_code(rl.Rectangle(qr_x, qr_y, qr_size, qr_size), has_url=bool(url)) - # Render URL label below QR code if url: self._url_label.set_text(url) self._url_label.set_max_width(int(qr_size)) self._url_label.set_position(qr_x, qr_y + qr_size + 16) self._url_label.render() - - # Scan label + self._scan_label.set_max_width(int(qr_size)) - self._scan_label.set_position(qr_x, qr_y + qr_size + 16 + 40) + self._scan_label.set_position(qr_x, qr_y + qr_size + 56) self._scan_label.render() - else: - # No IP - message shown in _render_qr_code - pass - - # Render title and toggle on right + title_y = self._rect.y + padding self._title_label.set_max_width(int(right_width)) self._title_label.set_position(right_x, title_y) self._title_label.render() - - # Toggle below title - toggle_y = title_y + 80 - toggle_rect = rl.Rectangle(right_x, toggle_y, right_width, 60) + + toggle_rect = rl.Rectangle(right_x, title_y + 80, right_width, 60) self._disable_toggle.set_rect(toggle_rect) self._disable_toggle.render() - - return -1 def _render_qr_code(self, rect: rl.Rectangle, has_url: bool = True) -> None: - """Render QR code texture or error message when no IP/generation failed.""" if not self._qr_texture: msg = "No WiFi connection" if not has_url else "QR Code Error" - try: - error_font = gui_app.font(FontWeight.BOLD) - msg_y = rect.y + max(0, rect.height // 2 - 15) - rl.draw_text_ex(error_font, msg, rl.Vector2(rect.x + 20, msg_y), 28, 0.0, rl.RED) - except Exception as e: - cloudlog.warning(f"QR dialog draw error: {e}") + error_font = gui_app.font(FontWeight.BOLD) + msg_y = rect.y + max(0, rect.height // 2 - 15) + rl.draw_text_ex(error_font, msg, rl.Vector2(rect.x + 20, msg_y), 28, 0.0, rl.RED) return if rect.height <= 0 or self._qr_texture.height <= 0: return scale = rect.height / self._qr_texture.height - pos = rl.Vector2(rect.x, rect.y) - rl.draw_texture_ex(self._qr_texture, pos, 0.0, scale, rl.WHITE) + rl.draw_texture_ex(self._qr_texture, rl.Vector2(rect.x, rect.y), 0.0, scale, rl.WHITE) def _handle_mouse_release(self, mouse_pos): - """Handle mouse clicks.""" - # Let the toggle handle its own clicks toggle_rect = self._disable_toggle._rect - if toggle_rect: - if (toggle_rect.x <= mouse_pos.x <= toggle_rect.x + toggle_rect.width and - toggle_rect.y <= mouse_pos.y <= toggle_rect.y + toggle_rect.height): - self._disable_toggle._handle_mouse_release(mouse_pos) - # Refresh toggle state after click - self._disable_toggle.refresh() - return - + if toggle_rect and rl.check_collision_point_rec(mouse_pos, toggle_rect): + self._disable_toggle._handle_mouse_release(mouse_pos) + self._disable_toggle.refresh() + return super()._handle_mouse_release(mouse_pos) def __del__(self): diff --git a/selfdrive/ui/sunnypilot/mici/layouts/settings.py b/selfdrive/ui/sunnypilot/mici/layouts/settings.py index 3f1932a791..f0cdc144dc 100644 --- a/selfdrive/ui/sunnypilot/mici/layouts/settings.py +++ b/selfdrive/ui/sunnypilot/mici/layouts/settings.py @@ -15,12 +15,6 @@ from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr import pyray as rl -# BluePilot: vehicle selector, BP settings panel, and BigButtonBP override -from openpilot.common.bluepilot import is_bluepilot -if is_bluepilot(): - from openpilot.selfdrive.ui.bp.mici.widgets.button_bp import BigButtonBP as BigButton - from openpilot.selfdrive.ui.bp.mici.layouts.settings.bluepilot import BluePilotLayoutMici - from openpilot.selfdrive.ui.bp.mici.layouts.settings.vehicle_mici import VehicleLayoutMici ICON_SIZE = 70 BIG_ICON_SIZE = 110 @@ -66,19 +60,6 @@ def __init__(self): items.insert(1, sunnylink_btn) items.insert(2, models_btn) - # BluePilot: insert vehicle fingerprint selector and BP settings buttons (before front slots so indices land after models) - if is_bluepilot(): - vehicle_panel = VehicleLayoutMici(back_callback=gui_app.pop_widget) - vehicle_btn = BigButton("vehicle", "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png", ICON_SIZE, ICON_SIZE)) - vehicle_btn.set_click_callback(lambda: gui_app.push_widget(vehicle_panel)) - - bp_panel = BluePilotLayoutMici(back_callback=gui_app.pop_widget) - bluepilot_btn = BigButton("bluepilot", "", gui_app.texture("icons_mici/settings/car_icon.png", ICON_SIZE, ICON_SIZE)) - bluepilot_btn.set_click_callback(lambda: gui_app.push_widget(bp_panel)) - - items.insert(3, vehicle_btn) - items.insert(4, bluepilot_btn) - # front slots (only one ever visible at a time): exit-always-offroad, then enable-onroad items.insert(0, self._enable_offroad_btn_onroad) items.insert(0, self._disable_offroad_btn) From a4959a5aeaf4750058d9e1c119f5fc2989a873b9 Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Fri, 12 Jun 2026 07:27:20 -0400 Subject: [PATCH 12/13] Cleanup onroad UI --- selfdrive/ui/bp/mici/__init__.py | 0 selfdrive/ui/bp/mici/layouts/__init__.py | 0 .../ui/bp/mici/layouts/panels/__init__.py | 0 .../ui/bp/mici/layouts/settings/__init__.py | 0 selfdrive/ui/bp/mici/onroad/__init__.py | 0 .../bp/mici/onroad/augmented_road_view_bp.py | 125 ------------ .../ui/bp/mici/onroad/confidence_ball_bp.py | 127 ------------ .../ui/bp/mici/onroad/hud_renderer_bp.py | 97 --------- .../ui/bp/mici/onroad/model_renderer_bp.py | 26 --- selfdrive/ui/bp/mici/onroad/torque_bar_bp.py | 33 --- selfdrive/ui/bp/mici/widgets/__init__.py | 0 .../ui/bp/onroad/augmented_road_view_bp.py | 2 +- selfdrive/ui/mici/layouts/main.py | 4 - .../ui/mici/onroad/augmented_road_view.py | 77 ++++--- .../ui/{bp => }/mici/onroad/complication.py | 81 ++++---- selfdrive/ui/mici/onroad/confidence_ball.py | 84 +++++--- selfdrive/ui/mici/onroad/hud_renderer.py | 36 +++- selfdrive/ui/mici/onroad/model_renderer.py | 6 +- .../{bp => }/mici/onroad/powerflow_gauge.py | 193 +++++------------- selfdrive/ui/mici/onroad/torque_bar.py | 14 ++ 20 files changed, 253 insertions(+), 652 deletions(-) delete mode 100644 selfdrive/ui/bp/mici/__init__.py delete mode 100644 selfdrive/ui/bp/mici/layouts/__init__.py delete mode 100644 selfdrive/ui/bp/mici/layouts/panels/__init__.py delete mode 100644 selfdrive/ui/bp/mici/layouts/settings/__init__.py delete mode 100644 selfdrive/ui/bp/mici/onroad/__init__.py delete mode 100644 selfdrive/ui/bp/mici/onroad/augmented_road_view_bp.py delete mode 100644 selfdrive/ui/bp/mici/onroad/confidence_ball_bp.py delete mode 100644 selfdrive/ui/bp/mici/onroad/hud_renderer_bp.py delete mode 100644 selfdrive/ui/bp/mici/onroad/model_renderer_bp.py delete mode 100644 selfdrive/ui/bp/mici/onroad/torque_bar_bp.py delete mode 100644 selfdrive/ui/bp/mici/widgets/__init__.py rename selfdrive/ui/{bp => }/mici/onroad/complication.py (82%) rename selfdrive/ui/{bp => }/mici/onroad/powerflow_gauge.py (51%) diff --git a/selfdrive/ui/bp/mici/__init__.py b/selfdrive/ui/bp/mici/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/selfdrive/ui/bp/mici/layouts/__init__.py b/selfdrive/ui/bp/mici/layouts/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/selfdrive/ui/bp/mici/layouts/panels/__init__.py b/selfdrive/ui/bp/mici/layouts/panels/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/selfdrive/ui/bp/mici/layouts/settings/__init__.py b/selfdrive/ui/bp/mici/layouts/settings/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/selfdrive/ui/bp/mici/onroad/__init__.py b/selfdrive/ui/bp/mici/onroad/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/selfdrive/ui/bp/mici/onroad/augmented_road_view_bp.py b/selfdrive/ui/bp/mici/onroad/augmented_road_view_bp.py deleted file mode 100644 index 6136219f67..0000000000 --- a/selfdrive/ui/bp/mici/onroad/augmented_road_view_bp.py +++ /dev/null @@ -1,125 +0,0 @@ -import time -import pyray as rl -from cereal import messaging, car -from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.common.params import Params -from openpilot.system.ui.lib.application import gui_app -from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH -from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView -from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView -from openpilot.selfdrive.ui.bp.mici.onroad.model_renderer_bp import ModelRendererBP -from openpilot.selfdrive.ui.bp.onroad.blindspot_renderer import BlindspotRendererMixin -from openpilot.selfdrive.ui.bp.mici.onroad.hud_renderer_bp import MiciHudRendererBP -from openpilot.selfdrive.ui.bp.mici.onroad.complication import MiciComplication -from openpilot.selfdrive.ui.bp.mici.onroad.confidence_ball_bp import ConfidenceBallMiciBP -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus -from openpilot.selfdrive.ui.bp.lib.ui_debug_logger import bp_ui_log - -# BluePilot: Margin to keep confidence ball inside the MICI rounded border -MICI_BALL_BORDER_MARGIN = 25 # half of 50px MICI border thickness - - -class MiciAugmentedRoadViewBP(AugmentedRoadView, BlindspotRendererMixin): - """BluePilot MICI AugmentedRoadView with blindspot indicators, BP HUD, and complication.""" - - BLIND_SPOT_WIDTH = 125 # Narrower for MICI's smaller screen - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._init_blindspot() - self._bp_params = Params() - - # BluePilot: Replace HUD renderer with BP version (brake coloring + powerflow) - self._hud_renderer = MiciHudRendererBP() - - # BluePilot: Replace confidence ball with BP version on the left (MADS beam + enhanced coloring) - self._confidence_ball = ConfidenceBallMiciBP() - - # BluePilot: Add lead car complication widget - self._complication = MiciComplication() - - self._model_renderer = ModelRendererBP() - - # BluePilot: TICI uses AugmentedRoadViewSP for this; upstream MICI no longer does — BP _render still fades the overlay. - self._fade_alpha_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) - - def _render(self, _): - """Override render to place confidence ball on left, offset driver state, and conditionally hide border.""" - self._switch_stream_if_needed(ui_state.sm) - self._update_calibration() - - # Create inner content area (camera view, excluding side panel) - self._content_rect = rl.Rectangle( - self.rect.x, - self.rect.y, - self.rect.width - SIDE_PANEL_WIDTH, - self.rect.height, - ) - - bp_ui_log.scissor("MiciAugRoadView", "begin", - x=int(self._content_rect.x), y=int(self._content_rect.y), - w=int(self._content_rect.width), h=int(self._content_rect.height)) - rl.begin_scissor_mode( - int(self._content_rect.x), - int(self._content_rect.y), - int(self._content_rect.width), - int(self._content_rect.height) - ) - - # Render the base camera view - CameraView._render(self, self._content_rect) - - # Model overlays - self._model_renderer.render(self._content_rect) - - # Fade out bottom overlay (only when engaged) - fade_alpha = self._fade_alpha_filter.update(ui_state.status != UIStatus.DISENGAGED) - if fade_alpha > 1e-2: - rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0, - rl.Color(255, 255, 255, int(255 * fade_alpha))) - - alert_to_render, not_animating_out = self._alert_renderer.will_render() - - # BluePilot: Driver monitor pushed right by ball width - should_draw_dmoji = (not self._hud_renderer.drawing_top_icons() and ui_state.is_onroad() and - (ui_state.status != UIStatus.DISENGAGED or ui_state.always_on_dm)) - self._driver_state_renderer.set_should_draw(should_draw_dmoji) - self._driver_state_renderer.set_position(self._rect.x + 16, self._rect.y + 10) - self._driver_state_renderer.render() - - # HUD and alerts - self._hud_renderer.set_can_draw_top_icons(alert_to_render is None) - self._hud_renderer.set_wheel_critical_icon(alert_to_render is not None and not not_animating_out and - alert_to_render.visual_alert == car.CarControl.HUDControl.VisualAlert.steerRequired) - if ui_state.started: - self._alert_renderer.render(self._content_rect) - self._hud_renderer.render(self._content_rect) - - bp_ui_log.scissor("MiciAugRoadView", "end") - rl.end_scissor_mode() - - # BluePilot: Conditionally draw MICI rounded border - if not self._bp_params.get_bool("BPHideOnroadBorder"): - rl.draw_rectangle_rounded_lines_ex(self._content_rect, 0.2 * 1.02, 10, 50, rl.BLACK) - - # BluePilot: Blindspot indicators (outside scissor, on screen edges) - self._draw_blindspot_screen_edges(self.rect, self.BLIND_SPOT_WIDTH) - - # BluePilot: Lead car complication widget - self._complication.render(self._content_rect) - - ball_rect = rl.Rectangle( - self._rect.x + self._rect.width - SIDE_PANEL_WIDTH, - self._content_rect.y, - SIDE_PANEL_WIDTH, - self._content_rect.height, - ) - self._confidence_ball.render(ball_rect) - - # Bookmark icon - self._bookmark_icon.render(self.rect) - - # Offroad label - if not ui_state.started: - rl.draw_rectangle(int(self.rect.x), int(self.rect.y), int(self.rect.width), int(self.rect.height), rl.Color(0, 0, 0, 175)) - self._offroad_label.render(self._content_rect) diff --git a/selfdrive/ui/bp/mici/onroad/confidence_ball_bp.py b/selfdrive/ui/bp/mici/onroad/confidence_ball_bp.py deleted file mode 100644 index 1a369350ad..0000000000 --- a/selfdrive/ui/bp/mici/onroad/confidence_ball_bp.py +++ /dev/null @@ -1,127 +0,0 @@ -import math -import pyray as rl -from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall, draw_circle_gradient -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus -# BluePilot: GPU circle shader moved to BP module after upstream removal -from openpilot.bluepilot.ui.lib.bp_shaders import draw_shader_circle_gradient -from openpilot.selfdrive.ui.bp.lib.ui_debug_logger import bp_ui_log - -class ConfidenceBallBP(ConfidenceBall): - def __init__(self, demo: bool = False, radius: float=24, width: float = 60, align_right: bool = True): - ConfidenceBall.__init__(self, demo=demo) - self._align_right = align_right - self._width = width - self._status_dot_radius = radius - - def draw_mads_beam(self, x: int, y: int, width: int, height: int, color: rl.Color): - transparent = rl.Color(color.r, color.g, color.b, 0) - segments = 3 - seg_width = width // segments - - # Center segment: solid color - rl.draw_rectangle( - x + seg_width, y, seg_width, height, - color - ) - - # Left segment: fade from transparent -> solid - rl.draw_rectangle_gradient_h( - x, y, seg_width, height, - transparent, # bottom-left - color # top-right - ) - - # Right segment: fade from solid -> transparent - rl.draw_rectangle_gradient_h( - x + seg_width * (segments-1), y, width - seg_width, height, - color, # bottom-left - transparent # top-right - ) - - def _render(self, _): - bar_width = self._width - x = self.rect.x if not self._align_right else self.rect.x + self.rect.width - bar_width - content_rect = rl.Rectangle( - x, - self.rect.y, - bar_width, - self.rect.height, - ) - - bottom_position = content_rect.height - top_position = 0.0 - range_height = bottom_position - top_position - - # Map confidence filter to new range - # Original: (1 - self._confidence_filter.x) maps -0.5->1.5 (top) and 1.0->0.0 (bottom) - # We want to preserve this mapping but constrain to new range - # Normalize filter.x from [-0.5, ~1.0] to [0, 1] where 0 = bottom, 1 = top - filter_min = -0.5 - filter_max = 1.0 - normalized = (self._confidence_filter.x - filter_min) / (filter_max - filter_min) - normalized = max(0.0, min(1.0, normalized)) # Clamp to [0, 1] - - # Map normalized [0, 1] to [bottom_position, top_position] - # When normalized=0 (low confidence), ball at bottom_position - # When normalized=1 (high confidence), ball at top_position - dot_height = bottom_position - (normalized * range_height) + self._status_dot_radius - dot_height = content_rect.y + dot_height - - # confidence zones - if ui_state.status in (UIStatus.LAT_ONLY, UIStatus.LONG_ONLY, UIStatus.ENGAGED) or self._demo: - if self._confidence_filter.x > 0.5: - top_dot_color = rl.Color(0, 255, 204, 255) - bottom_dot_color = rl.Color(0, 255, 38, 255) - elif self._confidence_filter.x > 0.2: - top_dot_color = rl.Color(255, 200, 0, 255) - bottom_dot_color = rl.Color(255, 115, 0, 255) - else: - top_dot_color = rl.Color(255, 0, 21, 255) - bottom_dot_color = rl.Color(255, 0, 89, 255) - - elif ui_state.status == UIStatus.OVERRIDE: - top_dot_color = rl.Color(255, 255, 255, 255) - bottom_dot_color = rl.Color(82, 82, 82, 255) - - else: - top_dot_color = rl.Color(50, 50, 50, 255) - bottom_dot_color = rl.Color(13, 13, 13, 255) - - if content_rect.width < 2 * self._status_dot_radius: - # Bar is narrower than ball diameter - position so left edge of ball is at bar left edge - ball_center_x = content_rect.x + self._status_dot_radius - else: - # Bar is wide enough - position ball aligned to right edge of bar (original behavior) - ball_center_x = content_rect.x + content_rect.width - self._status_dot_radius - - # MADS beam (teal bar) only when LAT_ONLY or LONG_ONLY; no bar when ENGAGED - if ui_state.status in (UIStatus.LAT_ONLY, UIStatus.LONG_ONLY): - color = self.get_lat_long_dot_color() - color = rl.Color(color.r, color.g, color.b, 150) # Set alpha for faded background - self.draw_mads_beam(int(content_rect.x), - int(content_rect.y), - int(content_rect.width), - int(content_rect.height), - color) - - self._draw_circle(ball_center_x, dot_height, self._status_dot_radius, - top_dot_color, bottom_dot_color) - - def _draw_circle(self, cx: float, cy: float, radius: float, top: rl.Color, bottom: rl.Color): - """Use GPU shader for smooth anti-aliased circle on TICI's larger display.""" - draw_shader_circle_gradient(cx, cy, radius, top, bottom) - - -class ConfidenceBallMiciBP(ConfidenceBallBP): - BALL_WIDTH = 60 - def __init__(self, demo: bool = False): - ConfidenceBallBP.__init__(self, demo=demo, radius=24, width=self.BALL_WIDTH, align_right=False) - -TICI_CONFIDENCE_BALL_R = 50 -TICI_CONFIDENCE_BALL_MARGIN = 5 -TICI_CONFIDENCE_BALL_W = TICI_CONFIDENCE_BALL_R * 2 + TICI_CONFIDENCE_BALL_MARGIN - -class ConfidenceBallTiciBP(ConfidenceBallBP): - BALL_WIDTH = TICI_CONFIDENCE_BALL_W - def __init__(self, demo: bool = False): - ConfidenceBallBP.__init__(self, demo=demo, radius=TICI_CONFIDENCE_BALL_R, width=self.BALL_WIDTH, align_right=False) diff --git a/selfdrive/ui/bp/mici/onroad/hud_renderer_bp.py b/selfdrive/ui/bp/mici/onroad/hud_renderer_bp.py deleted file mode 100644 index 0b5901190e..0000000000 --- a/selfdrive/ui/bp/mici/onroad/hud_renderer_bp.py +++ /dev/null @@ -1,97 +0,0 @@ -import pyray as rl -from openpilot.common.params import Params -from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer -from openpilot.selfdrive.ui.bp.mici.onroad.powerflow_gauge import MiciPowerflowGauge -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus -from openpilot.selfdrive.ui.bp.lib.ui_debug_logger import bp_ui_log - - -class MiciHudRendererBP(HudRenderer): - """BluePilot MICI HudRenderer with brake status coloring and powerflow gauge.""" - - def __init__(self): - super().__init__() - self._bp_params = Params() - self._brakes_on = False - self._power_flow = MiciPowerflowGauge() - - def _update_state(self) -> None: - super()._update_state() - - if self._bp_params.get_bool("ShowBrakeStatus"): - sm = ui_state.sm - try: - car_state_bp = sm['carStateBP'] - brake_light_status = car_state_bp.brakeLightStatus - self._brakes_on = brake_light_status.dataAvailable and brake_light_status.brakeLightsOn - except (KeyError, AttributeError): - self._brakes_on = False - else: - self._brakes_on = False - - bp_ui_log.state("MiciHudRenderer", "brakes_on", self._brakes_on) - - def _render(self, rect: rl.Rectangle) -> None: - """Render HUD elements to the screen.""" - self._torque_bar.render(rect) - - if self.is_cruise_set: - self._draw_set_speed(rect) - - self._draw_steering_wheel(rect) - - def _draw_steering_wheel(self, rect: rl.Rectangle) -> None: - """Override to add brake status coloring to wheel icon and powerflow gauge.""" - wheel_txt = self._txt_wheel_critical if self._show_wheel_critical else self._txt_wheel - - bsm_detected = self._has_blind_spot_detected() if hasattr(self, '_has_blind_spot_detected') else False - - if self._show_wheel_critical: - self._wheel_alpha_filter.update(255) - self._wheel_y_filter.update(0) - else: - if ui_state.status == UIStatus.DISENGAGED or bsm_detected: - self._wheel_alpha_filter.update(0) - self._wheel_y_filter.update(wheel_txt.height / 2) - else: - self._wheel_alpha_filter.update(255 * 0.9) - self._wheel_y_filter.update(0) - - pos_x = int(rect.x + 21 + wheel_txt.width / 2) - pos_y = int(rect.y + rect.height - 14 - wheel_txt.height / 2 + self._wheel_y_filter.x) - rotation = -ui_state.sm['carState'].steeringAngleDeg - - turn_intent_margin = 25 - self._turn_intent.render(rl.Rectangle( - pos_x - wheel_txt.width / 2 - turn_intent_margin, - pos_y - wheel_txt.height / 2 - turn_intent_margin, - wheel_txt.width + turn_intent_margin * 2, - wheel_txt.height + turn_intent_margin * 2, - )) - - src_rect = rl.Rectangle(0, 0, wheel_txt.width, wheel_txt.height) - dest_rect = rl.Rectangle(pos_x, pos_y, wheel_txt.width, wheel_txt.height) - origin = (wheel_txt.width / 2, wheel_txt.height / 2) - - # BluePilot: Red color when braking - if self._brakes_on: - color = rl.Color(255, 60, 60, int(self._wheel_alpha_filter.x)) - else: - color = rl.Color(255, 255, 255, int(self._wheel_alpha_filter.x)) - rl.draw_texture_pro(wheel_txt, src_rect, dest_rect, origin, rotation, color) - - if self._show_wheel_critical: - EXCLAMATION_POINT_SPACING = 10 - exclamation_pos_x = pos_x - self._txt_exclamation_point.width / 2 + wheel_txt.width / 2 + EXCLAMATION_POINT_SPACING - exclamation_pos_y = pos_y - self._txt_exclamation_point.height / 2 - rl.draw_texture(self._txt_exclamation_point, int(exclamation_pos_x), int(exclamation_pos_y), rl.WHITE) - - # BluePilot: Render powerflow gauge around steering wheel - power_flow_radius = self._power_flow.RADIUS - power_rect = rl.Rectangle( - int(rect.x + 21) - power_flow_radius, - int(rect.y + rect.height - wheel_txt.height - 14) - power_flow_radius, - wheel_txt.width + power_flow_radius * 2, - wheel_txt.height + power_flow_radius * 2) - self._power_flow.set_wheel_rect(power_rect) - self._power_flow.render(rect) diff --git a/selfdrive/ui/bp/mici/onroad/model_renderer_bp.py b/selfdrive/ui/bp/mici/onroad/model_renderer_bp.py deleted file mode 100644 index b7be3d16ba..0000000000 --- a/selfdrive/ui/bp/mici/onroad/model_renderer_bp.py +++ /dev/null @@ -1,26 +0,0 @@ -import numpy as np -import pyray as rl -from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer, THROTTLE_COLORS, NO_THROTTLE_COLORS -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus -from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient -# BluePilot: Rainbow shader moved to BP module after upstream removal -from openpilot.bluepilot.ui.lib.bp_shaders import draw_rainbow_polygon - -class ModelRendererBP(ModelRenderer): - def __init__(self): - super().__init__() - self._rainbow_v = 20 - - def _update_state(self): - super()._update_state() - sm = ui_state.sm - - if ui_state.rainbow_path: - v= sm['carState'].vEgo - self._rainbow_v = np.clip(v, 2.5, 35) / 30 - - def _draw_path(self, sm): - if ui_state.rainbow_path: - draw_rainbow_polygon(self._rect, self._path.projected_points, rainbow_v=self._rainbow_v) - else: - super()._draw_path(sm) \ No newline at end of file diff --git a/selfdrive/ui/bp/mici/onroad/torque_bar_bp.py b/selfdrive/ui/bp/mici/onroad/torque_bar_bp.py deleted file mode 100644 index 45ef050ee7..0000000000 --- a/selfdrive/ui/bp/mici/onroad/torque_bar_bp.py +++ /dev/null @@ -1,33 +0,0 @@ -import pyray as rl -from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar -from openpilot.selfdrive.ui.ui_state import ui_state - -class TorqueBarBP(TorqueBar): - """BluePilot TorqueBar with lateral uncertainty from controllerStateBP. - - On angleState vehicles (e.g. Tesla), uses lateralUncertainty from - controllerStateBP instead of the acceleration-based calculation. - """ - - def _update_state(self): - if self._demo: - return - - # BluePilot: Use lateral uncertainty from controllerStateBP when available on angleState - if ui_state.sm['controlsState'].lateralControlState.which() == 'angleState': - if ui_state.sm.valid.get("controllerStateBP", False): - try: - lateral_uncertainty = ui_state.sm['controllerStateBP'].lateralUncertainty - # lateralUncertainty is already normalized to [-1, 1] range - self._torque_filter.update(min(max(lateral_uncertainty, -1), 1)) - return - except (KeyError, AttributeError): - pass - - # Fall back to stock behavior for non-angleState or when controllerStateBP unavailable - super()._update_state() - - def _render(self, rect: rl.Rectangle) -> None: - if ui_state.sm['controlsState'].lateralControlState.which() == 'angleState' and not ui_state.sm.valid.get("controllerStateBP", False): - return - super()._render(rect) diff --git a/selfdrive/ui/bp/mici/widgets/__init__.py b/selfdrive/ui/bp/mici/widgets/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/selfdrive/ui/bp/onroad/augmented_road_view_bp.py b/selfdrive/ui/bp/onroad/augmented_road_view_bp.py index 09643b1b41..0d6011a583 100644 --- a/selfdrive/ui/bp/onroad/augmented_road_view_bp.py +++ b/selfdrive/ui/bp/onroad/augmented_road_view_bp.py @@ -14,7 +14,7 @@ from openpilot.selfdrive.ui.bp.onroad.power_flow_gauge import PowerFlowGauge from openpilot.selfdrive.ui.bp.onroad.powerflow_gauge_arched import PowerflowGaugeArched, POWERFLOW_ANGLE_SPAN from openpilot.selfdrive.ui.bp.onroad.torque_bar_renderer_bp import TorqueBarRendererBP -from openpilot.selfdrive.ui.bp.mici.onroad.confidence_ball_bp import ConfidenceBallTiciBP +from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBallTiciBP from openpilot.selfdrive.ui.onroad.driver_state import BTN_SIZE from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiState, get_bottom_dev_ui_offset from openpilot.selfdrive.ui.ui_state import ui_state diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index 3b76e32204..8908367435 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -1,6 +1,5 @@ import pyray as rl import cereal.messaging as messaging -from openpilot.common.bluepilot import is_bluepilot from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout # BluePilot: replace home with paged-design home (mockups/mici_home.html) # if is_bluepilot(): @@ -11,9 +10,6 @@ # if is_bluepilot(): # from openpilot.selfdrive.ui.bp.mici.layouts.offroad_alerts_bp import MiciOffroadAlertsBP as MiciOffroadAlerts from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView -# BluePilot: override onroad view with blindspot, complication, brake coloring, powerflow -if is_bluepilot(): - from openpilot.selfdrive.ui.bp.mici.onroad.augmented_road_view_bp import MiciAugmentedRoadViewBP as AugmentedRoadView from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.selfdrive.ui.mici.layouts.onboarding import OnboardingWindow from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py index 8dafc4df57..2937abe002 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -1,19 +1,23 @@ import numpy as np import pyray as rl from cereal import car, log +from openpilot.common.params import Params from msgq.visionipc import VisionStreamType from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH from openpilot.selfdrive.ui.mici.onroad.alert_renderer import AlertRenderer +from openpilot.selfdrive.ui.mici.onroad.complication import MiciComplication +from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBallMiciBP from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer -from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView +from openpilot.selfdrive.ui.bp.lib.ui_debug_logger import bp_ui_log +from openpilot.selfdrive.ui.bp.onroad.blindspot_renderer import BlindspotRendererMixin from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets import Widget -from openpilot.common.filter_simple import BounceFilter +from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame from openpilot.common.transformations.orientation import rot_from_euler from enum import IntEnum @@ -38,6 +42,7 @@ class BookmarkState(IntEnum): ROAD_CAM_MIN_SPEED = 10 # m/s (25 mph) CAM_Y_OFFSET = 20 +MICI_BLIND_SPOT_WIDTH = 125 class BookmarkIcon(Widget): @@ -132,11 +137,14 @@ def _render(self, _): rl.draw_texture_ex(self._icon, rl.Vector2(icon_x, icon_y), 0.0, 1.0, rl.WHITE) -class AugmentedRoadView(CameraView): +class AugmentedRoadView(CameraView, BlindspotRendererMixin): def __init__(self, bookmark_callback=None, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_ROAD): super().__init__("camerad", stream_type) self._bookmark_callback = bookmark_callback self._set_placeholder_color(rl.BLACK) + # BluePilot: migrated MICI blindspot and border settings + self._init_blindspot() + self._bp_params = Params() self.device_camera: DeviceCameraConfig | None = None self.view_from_calib = view_frame_from_device_frame.copy() @@ -154,13 +162,17 @@ def __init__(self, bookmark_callback=None, stream_type: VisionStreamType = Visio self._hud_renderer = HudRenderer() self._alert_renderer = AlertRenderer() self._driver_state_renderer = DriverStateRenderer() - self._confidence_ball = ConfidenceBall() + # BluePilot: left-side confidence rail and lead/speed complication + self._confidence_ball = ConfidenceBallMiciBP() + self._complication = MiciComplication() self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png") + # BluePilot: fade the bottom overlay only when engaged + self._fade_alpha_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) def is_swiping_left(self) -> bool: """Check if currently swiping left (for scroller to disable).""" @@ -183,18 +195,9 @@ def _handle_mouse_release(self, mouse_pos: MousePos): super()._handle_mouse_release(mouse_pos) def _render(self, _): - # Draw text if not onroad - if not ui_state.started: - rl.draw_rectangle_rec(self.rect, rl.BLACK) - self._offroad_label.render(self._rect) - return - self._switch_stream_if_needed(ui_state.sm) - - # Update calibration before rendering self._update_calibration() - # Create inner content area with border padding self._content_rect = rl.Rectangle( self.rect.x, self.rect.y, @@ -202,8 +205,9 @@ def _render(self, _): self.rect.height, ) - # Enable scissor mode to clip all rendering within content rectangle boundaries - # This creates a rendering viewport that prevents graphics from drawing outside the border + bp_ui_log.scissor("MiciAugRoadView", "begin", + x=int(self._content_rect.x), y=int(self._content_rect.y), + w=int(self._content_rect.width), h=int(self._content_rect.height)) rl.begin_scissor_mode( int(self._content_rect.x), int(self._content_rect.y), @@ -211,19 +215,17 @@ def _render(self, _): int(self._content_rect.height) ) - # Render the base camera view super()._render(self._content_rect) - - # Draw all UI overlays self._model_renderer.render(self._content_rect) - # Fade out bottom of overlays for looks - rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0, rl.WHITE) + fade_alpha = self._fade_alpha_filter.update(ui_state.status != UIStatus.DISENGAGED) + if fade_alpha > 1e-2: + rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0, + rl.Color(255, 255, 255, int(255 * fade_alpha))) alert_to_render, not_animating_out = self._alert_renderer.will_render() - # Hide DMoji when disengaged unless AlwaysOnDM is enabled - should_draw_dmoji = (not self._hud_renderer.drawing_top_icons() and + should_draw_dmoji = (not self._hud_renderer.drawing_top_icons() and ui_state.is_onroad() and (ui_state.status != UIStatus.DISENGAGED or ui_state.always_on_dm)) self._driver_state_renderer.set_should_draw(should_draw_dmoji) self._driver_state_renderer.set_position(self._rect.x + 16, self._rect.y + 10) @@ -232,21 +234,36 @@ def _render(self, _): self._hud_renderer.set_can_draw_top_icons(alert_to_render is None) self._hud_renderer.set_wheel_critical_icon(alert_to_render is not None and not not_animating_out and alert_to_render.visual_alert == car.CarControl.HUDControl.VisualAlert.steerRequired) - self._alert_renderer.render(self._content_rect) + if ui_state.started: + self._alert_renderer.render(self._content_rect) self._hud_renderer.render(self._content_rect) - # Draw fake rounded border - rl.draw_rectangle_rounded_lines_ex(self._content_rect, 0.2 * 1.02, 10, 50, rl.BLACK) - - # End clipping region + bp_ui_log.scissor("MiciAugRoadView", "end") rl.end_scissor_mode() - # Custom UI extension point - add custom overlays here - # Use self._content_rect for positioning within camera bounds - self._confidence_ball.render(self.rect) + # BluePilot: optionally hide the MICI rounded border + if not self._bp_params.get_bool("BPHideOnroadBorder"): + rl.draw_rectangle_rounded_lines_ex(self._content_rect, 0.2 * 1.02, 10, 50, rl.BLACK) + + # BluePilot: blindspot indicators live outside the scissor + self._draw_blindspot_screen_edges(self.rect, MICI_BLIND_SPOT_WIDTH) + + self._complication.render(self._content_rect) + + ball_rect = rl.Rectangle( + self._rect.x + self._rect.width - SIDE_PANEL_WIDTH, + self._content_rect.y, + SIDE_PANEL_WIDTH, + self._content_rect.height, + ) + self._confidence_ball.render(ball_rect) self._bookmark_icon.render(self.rect) + if not ui_state.started: + rl.draw_rectangle(int(self.rect.x), int(self.rect.y), int(self.rect.width), int(self.rect.height), rl.Color(0, 0, 0, 175)) + self._offroad_label.render(self._content_rect) + def _switch_stream_if_needed(self, sm): if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams: v_ego = sm['carState'].vEgo diff --git a/selfdrive/ui/bp/mici/onroad/complication.py b/selfdrive/ui/mici/onroad/complication.py similarity index 82% rename from selfdrive/ui/bp/mici/onroad/complication.py rename to selfdrive/ui/mici/onroad/complication.py index 95ea200aaf..69dcd90a48 100644 --- a/selfdrive/ui/bp/mici/onroad/complication.py +++ b/selfdrive/ui/mici/onroad/complication.py @@ -1,25 +1,28 @@ -import pyray as rl -import numpy as np import time + +import numpy as np +import pyray as rl + from openpilot.common.constants import CV +from openpilot.common.params import Params +from openpilot.selfdrive.ui.bp.lib.ui_debug_logger import bp_ui_log from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget -from openpilot.common.params import Params -from opendbc.car import structs from openpilot.sunnypilot import IntEnumBase -from openpilot.selfdrive.ui.bp.lib.ui_debug_logger import bp_ui_log +from opendbc.car import structs FONT_SIZE = 68 DIST_FONT_SIZE = 55 TIME_FONT_SIZE = 50 UNIT_FONT_SIZE = 24 WIDTH = 80 -COLOR_DELTA_MS = 4.5 # ~ 10MPH +COLOR_DELTA_MS = 4.5 SHADOW_DEPTH = 3 -DELAY = 3.0 #seconds to remove last lead car speed +DELAY = 3.0 + class ComplicationType(IntEnumBase): off = 0 @@ -28,6 +31,7 @@ class ComplicationType(IntEnumBase): lead_car_dist = 3 lead_car_time = 4 + class MiciComplication(Widget): def __init__(self): super().__init__() @@ -35,21 +39,19 @@ def __init__(self): self.vRel: float = 0.0 self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD) self._color = np.array([255, 255, 255], dtype=float) - self._slower_color = np.array([255, 160, 0], dtype=float) #orangish - self._faster_color = np.array([0, 255, 0], dtype=float) #green + self._slower_color = np.array([255, 160, 0], dtype=float) + self._faster_color = np.array([0, 255, 0], dtype=float) self._font_color: rl.Color = rl.Color(255, 255, 255, 180) self._car_state = None self._render_type = 1 self._last_active_time = 0.0 - self.params = Params() def _update_state(self): - self._render_type = self.params.get("mici_complication") - bp_ui_log.state("MiciComplication", "render_type", self._render_type) + self._render_type = self.params.get("mici_complication") + bp_ui_log.state("MiciComplication", "render_type", self._render_type) def _render(self, rect: rl.Rectangle) -> None: - """Draw the first lead vehicle speed and unit.""" if self._render_type == ComplicationType.off: return @@ -63,21 +65,17 @@ def _render(self, rect: rl.Rectangle) -> None: has_lead_one = self._lead_one.status if self._lead_one else False self._render_lead_indicator = self._radar_state is not None and has_lead_one and in_gear - match self._render_type: - case ComplicationType.lead_car_speed: - self._render_lead_speed(rect) - case ComplicationType.speed: - # BluePilot: Respect "Speedometer: Hide from Onroad Screen" (HideVEgoUI) from Visuals. - # Read param directly for immediate response (ui_state.hide_v_ego_ui refreshes every 5s). - if not self.params.get_bool("HideVEgoUI"): - self._render_current_speed(rect) - case ComplicationType.lead_car_dist: - self._render_lead_dist(rect) - case ComplicationType.lead_car_time: - self._render_lead_time(rect) - - - def _render_lead_speed(self,rect: rl.Rectangle): + if self._render_type == ComplicationType.lead_car_speed: + self._render_lead_speed(rect) + elif self._render_type == ComplicationType.speed: + if not self.params.get_bool("HideVEgoUI"): + self._render_current_speed(rect) + elif self._render_type == ComplicationType.lead_car_dist: + self._render_lead_dist(rect) + elif self._render_type == ComplicationType.lead_car_time: + self._render_lead_time(rect) + + def _render_lead_speed(self, rect: rl.Rectangle): if self._render_lead_indicator: self._last_active_time = time.monotonic() speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH @@ -89,16 +87,15 @@ def _render_lead_speed(self,rect: rl.Rectangle): delay_time = time.monotonic() - self._last_active_time if delay_time > DELAY: return - else: - fade_ratio = 1.0 - (delay_time / DELAY) + fade_ratio = 1.0 - (delay_time / DELAY) v_delta = np.clip(self.vRel, -COLOR_DELTA_MS, COLOR_DELTA_MS) if v_delta <= 0: - t = (v_delta + COLOR_DELTA_MS) / COLOR_DELTA_MS - result = (1 - t) * self._slower_color + t * self._color + t = (v_delta + COLOR_DELTA_MS) / COLOR_DELTA_MS + result = (1 - t) * self._slower_color + t * self._color else: - t = v_delta / COLOR_DELTA_MS - result = (1 - t) * self._color + t * self._faster_color + t = v_delta / COLOR_DELTA_MS + result = (1 - t) * self._color + t * self._faster_color color = result.astype(int) self._font_color = rl.Color(color[0], color[1], color[2], int(220 * fade_ratio)) @@ -128,9 +125,9 @@ def _render_lead_speed(self,rect: rl.Rectangle): rl.draw_triangle_fan(chevron, len(chevron), rl.Color(201, 34, 49, int(150 * fade_ratio))) def _render_current_speed(self, rect: rl.Rectangle) -> None: - # BluePilot: Respect "Speedometer: Hide from Onroad Screen" (HideVEgoUI) from Visuals if ui_state.hide_v_ego_ui: return + self._font_color = rl.Color(255, 255, 255, 220) shadow_color = rl.Color(0, 0, 0, 180) @@ -152,7 +149,7 @@ def _render_current_speed(self, rect: rl.Rectangle) -> None: unit_pos.y -= SHADOW_DEPTH rl.draw_text_ex(self._font_bold, unit_text, unit_pos, UNIT_FONT_SIZE, 0, self._font_color) - def _render_lead_dist(self,rect: rl.Rectangle): + def _render_lead_dist(self, rect: rl.Rectangle): if self._render_lead_indicator: self._last_active_time = time.monotonic() self.dist = self._lead_one.dRel @@ -163,8 +160,7 @@ def _render_lead_dist(self,rect: rl.Rectangle): delay_time = time.monotonic() - self._last_active_time if delay_time > DELAY: return - else: - fade_ratio = 1.0 - (delay_time / DELAY) + fade_ratio = 1.0 - (delay_time / DELAY) self._font_color = rl.Color(255, 255, 255, int(220 * fade_ratio)) shadow_color = rl.Color(0, 0, 0, int(180 * fade_ratio)) @@ -186,7 +182,7 @@ def _render_lead_dist(self,rect: rl.Rectangle): unit_pos.y -= SHADOW_DEPTH rl.draw_text_ex(self._font_bold, unit_text, unit_pos, UNIT_FONT_SIZE, 0, self._font_color) - def _render_lead_time(self,rect: rl.Rectangle): + def _render_lead_time(self, rect: rl.Rectangle): if self._render_lead_indicator and self._lead_one.vRel > 0: self._last_active_time = time.monotonic() self.dist = self._lead_one.dRel @@ -196,8 +192,7 @@ def _render_lead_time(self,rect: rl.Rectangle): delay_time = time.monotonic() - self._last_active_time if delay_time > DELAY: return - else: - fade_ratio = 1.0 - (delay_time / DELAY) + fade_ratio = 1.0 - (delay_time / DELAY) self._font_color = rl.Color(255, 255, 255, int(220 * fade_ratio)) shadow_color = rl.Color(0, 0, 0, int(180 * fade_ratio)) @@ -217,4 +212,4 @@ def _render_lead_time(self,rect: rl.Rectangle): rl.draw_text_ex(self._font_bold, unit_text, unit_pos, UNIT_FONT_SIZE, 0, shadow_color) unit_pos.x -= SHADOW_DEPTH unit_pos.y -= SHADOW_DEPTH - rl.draw_text_ex(self._font_bold, unit_text, unit_pos, UNIT_FONT_SIZE, 0, self._font_color) \ No newline at end of file + rl.draw_text_ex(self._font_bold, unit_text, unit_pos, UNIT_FONT_SIZE, 0, self._font_color) diff --git a/selfdrive/ui/mici/onroad/confidence_ball.py b/selfdrive/ui/mici/onroad/confidence_ball.py index 54699eab54..8f7d12b587 100644 --- a/selfdrive/ui/mici/onroad/confidence_ball.py +++ b/selfdrive/ui/mici/onroad/confidence_ball.py @@ -1,22 +1,21 @@ import math + import pyray as rl -from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.lib.application import gui_app -from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.bluepilot.ui.lib.bp_shaders import draw_shader_circle_gradient from openpilot.selfdrive.ui.sunnypilot.mici.onroad.confidence_ball import ConfidenceBallSP +from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.widgets import Widget def draw_circle_gradient(center_x: float, center_y: float, radius: int, top: rl.Color, bottom: rl.Color) -> None: - # Draw a square with the gradient rl.draw_rectangle_gradient_v(int(center_x - radius), int(center_y - radius), radius * 2, radius * 2, top, bottom) - # Paint over square with a ring outer_radius = math.ceil(radius * math.sqrt(2)) + 1 rl.draw_ring(rl.Vector2(int(center_x), int(center_y)), radius, outer_radius, 0.0, 360.0, @@ -24,10 +23,12 @@ def draw_circle_gradient(center_x: float, center_y: float, radius: int, class ConfidenceBall(Widget, ConfidenceBallSP): - def __init__(self, demo: bool = False): + def __init__(self, demo: bool = False, radius: float = 24, width: float = 60): Widget.__init__(self) ConfidenceBallSP.__init__(self) self._demo = demo + self._status_dot_radius = radius + self._width = width self._confidence_filter = FirstOrderFilter(-0.5, 0.5, 1 / gui_app.target_fps) def update_filter(self, value: float): @@ -37,29 +38,30 @@ def _update_state(self): if self._demo: return - # animate status dot in from bottom if ui_state.status == UIStatus.DISENGAGED: self._confidence_filter.update(-0.5) elif ui_state.status in (UIStatus.LAT_ONLY, UIStatus.LONG_ONLY): self._confidence_filter.update(1 - max(self.get_animate_status_probs() or [1])) else: self._confidence_filter.update((1 - max(ui_state.sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs or [1])) * - (1 - max(ui_state.sm['modelV2'].meta.disengagePredictions.steerOverrideProbs or [1]))) + (1 - max(ui_state.sm['modelV2'].meta.disengagePredictions.steerOverrideProbs or [1]))) def _render(self, _): content_rect = rl.Rectangle( - self.rect.x + self.rect.width - SIDE_PANEL_WIDTH, + self.rect.x, self.rect.y, - SIDE_PANEL_WIDTH, + self._width, self.rect.height, ) - status_dot_radius = 24 - dot_height = (1 - self._confidence_filter.x) * (content_rect.height - 2 * status_dot_radius) + status_dot_radius - dot_height = self._rect.y + dot_height + filter_min = -0.5 + filter_max = 1.0 + normalized = (self._confidence_filter.x - filter_min) / (filter_max - filter_min) + normalized = max(0.0, min(1.0, normalized)) + dot_height = content_rect.height - (normalized * content_rect.height) + self._status_dot_radius + dot_height = content_rect.y + dot_height - # confidence zones - if ui_state.status == UIStatus.ENGAGED or self._demo: + if ui_state.status in (UIStatus.LAT_ONLY, UIStatus.LONG_ONLY, UIStatus.ENGAGED) or self._demo: if self._confidence_filter.x > 0.5: top_dot_color = rl.Color(0, 255, 204, 255) bottom_dot_color = rl.Color(0, 255, 38, 255) @@ -69,18 +71,50 @@ def _render(self, _): else: top_dot_color = rl.Color(255, 0, 21, 255) bottom_dot_color = rl.Color(255, 0, 89, 255) - - elif ui_state.status in (UIStatus.LAT_ONLY, UIStatus.LONG_ONLY): - top_dot_color = bottom_dot_color = self.get_lat_long_dot_color() - elif ui_state.status == UIStatus.OVERRIDE: top_dot_color = rl.Color(255, 255, 255, 255) bottom_dot_color = rl.Color(82, 82, 82, 255) - else: top_dot_color = rl.Color(50, 50, 50, 255) bottom_dot_color = rl.Color(13, 13, 13, 255) - draw_circle_gradient(content_rect.x + content_rect.width - status_dot_radius, - dot_height, status_dot_radius, - top_dot_color, bottom_dot_color) + if ui_state.status in (UIStatus.LAT_ONLY, UIStatus.LONG_ONLY): + color = self.get_lat_long_dot_color() + self._draw_mads_beam(int(content_rect.x), int(content_rect.y), int(content_rect.width), int(content_rect.height), + rl.Color(color.r, color.g, color.b, 150)) + + self._draw_circle(content_rect.x + self._status_dot_radius, dot_height, self._status_dot_radius, + top_dot_color, bottom_dot_color) + + @staticmethod + def _draw_mads_beam(x: int, y: int, width: int, height: int, color: rl.Color): + transparent = rl.Color(color.r, color.g, color.b, 0) + segments = 3 + seg_width = width // segments + + rl.draw_rectangle(x + seg_width, y, seg_width, height, color) + rl.draw_rectangle_gradient_h(x, y, seg_width, height, transparent, color) + rl.draw_rectangle_gradient_h(x + seg_width * (segments - 1), y, width - seg_width, height, color, transparent) + + @staticmethod + def _draw_circle(cx: float, cy: float, radius: float, top: rl.Color, bottom: rl.Color): + draw_shader_circle_gradient(cx, cy, radius, top, bottom) + + +class ConfidenceBallMiciBP(ConfidenceBall): + BALL_WIDTH = 60 + + def __init__(self, demo: bool = False): + super().__init__(demo=demo, radius=24, width=self.BALL_WIDTH) + + +TICI_CONFIDENCE_BALL_R = 50 +TICI_CONFIDENCE_BALL_MARGIN = 5 +TICI_CONFIDENCE_BALL_W = TICI_CONFIDENCE_BALL_R * 2 + TICI_CONFIDENCE_BALL_MARGIN + + +class ConfidenceBallTiciBP(ConfidenceBall): + BALL_WIDTH = TICI_CONFIDENCE_BALL_W + + def __init__(self, demo: bool = False): + super().__init__(demo=demo, radius=TICI_CONFIDENCE_BALL_R, width=self.BALL_WIDTH) diff --git a/selfdrive/ui/mici/onroad/hud_renderer.py b/selfdrive/ui/mici/onroad/hud_renderer.py index c74d3101b0..c1f6b99cca 100644 --- a/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/selfdrive/ui/mici/onroad/hud_renderer.py @@ -1,7 +1,10 @@ import pyray as rl from dataclasses import dataclass from openpilot.common.constants import CV +from openpilot.common.params import Params +from openpilot.selfdrive.ui.mici.onroad.powerflow_gauge import MiciPowerflowGauge from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar +from openpilot.selfdrive.ui.bp.lib.ui_debug_logger import bp_ui_log from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr @@ -117,6 +120,10 @@ def __init__(self): self._turn_intent = TurnIntent() self._torque_bar = TorqueBar() + # BluePilot: brake status coloring and hybrid powerflow on MICI + self._bp_params = Params() + self._brakes_on = False + self._power_flow = MiciPowerflowGauge() self._txt_wheel: rl.Texture = gui_app.texture('icons_mici/wheel.png', 50, 50) self._txt_wheel_critical: rl.Texture = gui_app.texture('icons_mici/wheel_critical.png', 50, 50) @@ -169,6 +176,19 @@ def _update_state(self) -> None: speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH self.speed = max(0.0, v_ego * speed_conversion) + # BluePilot: brake status coloring from carStateBP + if self._bp_params.get_bool("ShowBrakeStatus"): + try: + car_state_bp = sm['carStateBP'] + brake_light_status = car_state_bp.brakeLightStatus + self._brakes_on = brake_light_status.dataAvailable and brake_light_status.brakeLightsOn + except (KeyError, AttributeError): + self._brakes_on = False + else: + self._brakes_on = False + + bp_ui_log.state("MiciHudRenderer", "brakes_on", self._brakes_on) + def _render(self, rect: rl.Rectangle) -> None: """Render HUD elements to the screen.""" @@ -213,7 +233,11 @@ def _draw_steering_wheel(self, rect: rl.Rectangle) -> None: origin = (wheel_txt.width / 2, wheel_txt.height / 2) # color and draw - color = rl.Color(255, 255, 255, int(self._wheel_alpha_filter.x)) + # BluePilot: wheel turns red while brake lights are active + if self._brakes_on: + color = rl.Color(255, 60, 60, int(self._wheel_alpha_filter.x)) + else: + color = rl.Color(255, 255, 255, int(self._wheel_alpha_filter.x)) rl.draw_texture_pro(wheel_txt, src_rect, dest_rect, origin, rotation, color) if self._show_wheel_critical: @@ -223,6 +247,16 @@ def _draw_steering_wheel(self, rect: rl.Rectangle) -> None: exclamation_pos_y = pos_y - self._txt_exclamation_point.height / 2 rl.draw_texture_ex(self._txt_exclamation_point, rl.Vector2(exclamation_pos_x, exclamation_pos_y), 0.0, 1.0, rl.WHITE) + # BluePilot: render powerflow gauge around steering wheel + power_flow_radius = self._power_flow.RADIUS + power_rect = rl.Rectangle( + int(rect.x + 21) - power_flow_radius, + int(rect.y + rect.height - wheel_txt.height - 14) - power_flow_radius, + wheel_txt.width + power_flow_radius * 2, + wheel_txt.height + power_flow_radius * 2) + self._power_flow.set_wheel_rect(power_rect) + self._power_flow.render(rect) + def _draw_set_speed(self, rect: rl.Rectangle) -> None: """Draw the MAX speed indicator box.""" alpha = self._set_speed_alpha_filter.update(0 < rl.get_time() - self._set_speed_changed_time < SET_SPEED_PERSISTENCE and diff --git a/selfdrive/ui/mici/onroad/model_renderer.py b/selfdrive/ui/mici/onroad/model_renderer.py index e88d320a3f..fae14805d0 100644 --- a/selfdrive/ui/mici/onroad/model_renderer.py +++ b/selfdrive/ui/mici/onroad/model_renderer.py @@ -8,6 +8,7 @@ from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.mici.onroad import blend_colors +from openpilot.bluepilot.ui.lib.bp_shaders import draw_rainbow_polygon from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient from openpilot.system.ui.widgets import Widget @@ -344,7 +345,10 @@ def _draw_path(self, sm): self._blend_filter.update(int(allow_throttle)) if ui_state.rainbow_path: - self.rainbow_path.draw_rainbow_path(self._rect, self._path) + # BluePilot: use the migrated rainbow shader path for MICI + v_ego = sm['carState'].vEgo + rainbow_v = np.clip(v_ego, 2.5, 35) / 30 + draw_rainbow_polygon(self._rect, self._path.projected_points, rainbow_v=rainbow_v) return path_pts = self._path.projected_points + np.array([self._rect.x, self._rect.y], dtype=np.float32) diff --git a/selfdrive/ui/bp/mici/onroad/powerflow_gauge.py b/selfdrive/ui/mici/onroad/powerflow_gauge.py similarity index 51% rename from selfdrive/ui/bp/mici/onroad/powerflow_gauge.py rename to selfdrive/ui/mici/onroad/powerflow_gauge.py index 08173c17e4..a1af4d4bc8 100644 --- a/selfdrive/ui/bp/mici/onroad/powerflow_gauge.py +++ b/selfdrive/ui/mici/onroad/powerflow_gauge.py @@ -1,46 +1,40 @@ +import math + import numpy as np import pyray as rl -import math + +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.selfdrive.ui.bp.lib.ui_debug_logger import bp_ui_log from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import Widget -from openpilot.selfdrive.ui.mici.onroad import blend_colors -from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient -from opendbc.sunnypilot.car.ford.carstate_ext import get_hev_power_flow_text, get_hev_engine_on_reason_text -from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.selfdrive.ui.bp.lib.ui_debug_logger import bp_ui_log SEGMENTS = 50 LINES = 20 -SMOOTHING = 0.12 BAR_W = 20 DEMO = False -# Angles (radians) BOTTOM = math.radians(90) TOP = math.radians(-90) POWERFLOW_REGEN_COLOR = rl.Color(100, 255, 100, 255) POWERFLOW_DEMAND_COLOR = rl.Color(100, 150, 255, 255) + class MiciPowerflowGauge(Widget): - """Widget to display powerflow gauge as an arch above the torque bar""" + """Widget to display powerflow gauge as an arch above the torque bar.""" RADIUS = 20 def __init__(self): super().__init__() - self._value = 0 - self._inc = 0.01 self._powerflow_filter = FirstOrderFilter(0.0, 0.0, 1.0 / gui_app.target_fps * 10) self._power_flow_mode_value = 0 - self._engine_on_reason_value = 0 self._top_angle = -90 if DEMO: - self._demo_value = 0.0 - self._demo_inc = 0.01 + self._demo_value = 0.0 + self._demo_inc = 0.01 def _update_state(self): - """Update power flow state and animate changes""" if not self._should_render(): return @@ -48,20 +42,13 @@ def _update_state(self): try: car_state_bp = sm['carStateBP'] throttle_demand = car_state_bp.hybridDrive.throttleDemandPercent - # Clamp to expected range [-102.2, 102.4] and normalize to [-1, 1] for easier calculation - # Positive = throttle demand (power out, should be blue) - # Negative = regenerative braking (power in, should be green) normalized_value = np.clip(throttle_demand / 102.0, -1.0, 1.0) self._powerflow_filter.update(normalized_value) - - # Store current power flow mode and engine on reason for text display self._power_flow_mode_value = car_state_bp.hybridDrive.powerFlowModeValue except (KeyError, AttributeError, TypeError): self._power_flow_mode_value = 0 def _should_render(self) -> bool: - """Check if powerflow gauge should be rendered""" - # Only render if hybrid power flow is enabled from openpilot.common.params import Params params = Params() power_flow_enabled = params.get_bool("FordPrefHybridPowerFlow") @@ -71,11 +58,10 @@ def _should_render(self) -> bool: return False if DEMO: - return True + return True sm = ui_state.sm try: - # Check if message exists and is recent enough if "carStateBP" not in sm.recv_frame: bp_ui_log.visibility("MiciPowerflow", False, reason="no_recv_frame") return False @@ -94,10 +80,9 @@ def _should_render(self) -> bool: return False def set_wheel_rect(self, rect: rl.Rectangle): - self._wheel_rect = rect + self._wheel_rect = rect def _render(self, rect: rl.Rectangle) -> None: - """Render the powerflow gauge arch""" if not self._should_render(): return @@ -111,7 +96,6 @@ def _render(self, rect: rl.Rectangle) -> None: self.draw_circular_gauge(self._demo_value) else: self.draw_circular_gauge(self._powerflow_filter.x) - else: if DEMO: self.draw_vertical_gauge(rect, self._demo_value) @@ -124,128 +108,59 @@ def draw_arc_segment(self, angle, color): x2 = self._center.x + math.cos(angle) * self._outer_radius y2 = self._center.y + math.sin(angle) * self._outer_radius - rl.draw_line_ex( - rl.Vector2(x1, y1), - rl.Vector2(x2, y2), - 6, - color - ) + rl.draw_line_ex(rl.Vector2(x1, y1), rl.Vector2(x2, y2), 6, color) def draw_circular_gauge(self, value): self._center = rl.Vector2(self._wheel_rect.x + self._wheel_rect.width // 2, self._wheel_rect.y + self._wheel_rect.height // 2) self._outer_radius = self._wheel_rect.width // 2 self._inner_radius = self._outer_radius - self.RADIUS * 1.1 - # --- Regen (left side) --- if value < 0: - active = abs(value) - for i in range(SEGMENTS): - t = i / (SEGMENTS - 1) - if t >= active: - break + active = abs(value) + for i in range(SEGMENTS): + t = i / (SEGMENTS - 1) + if t >= active: + break - angle = -TOP + t * (BOTTOM - TOP) - self.draw_arc_segment(angle, POWERFLOW_REGEN_COLOR) + angle = -TOP + t * (BOTTOM - TOP) + self.draw_arc_segment(angle, POWERFLOW_REGEN_COLOR) - # --- Throttle (right side) --- if value > 0: - active = value - for i in range(SEGMENTS): - t = i / (SEGMENTS - 1) - if t >= active: - break - - angle = BOTTOM + t * (TOP - BOTTOM) - self.draw_arc_segment(angle, POWERFLOW_DEMAND_COLOR) + active = value + for i in range(SEGMENTS): + t = i / (SEGMENTS - 1) + if t >= active: + break - def lerp(a, b, t): - return a + (b - a) * t + angle = BOTTOM + t * (TOP - BOTTOM) + self.draw_arc_segment(angle, POWERFLOW_DEMAND_COLOR) def draw_vertical_gauge(self, rect: rl.Rectangle, value): - bar_x = int(rect.x) - bar_y = int(rect.y) - bar_h = int(rect.height) - - segment_h = bar_h / LINES - mid = bar_h * 0.6 - - rl.draw_rectangle( - bar_x, - bar_y, - BAR_W, - bar_h, - rl.Color(0,0,0,100) - ) - - demand_h = mid * abs(value) - regen_h = (bar_h - mid) * abs(value) - # --- Throttle side (above center) --- - if value > 0: - rl.draw_rectangle( - bar_x, - int(mid - demand_h), - BAR_W, - int(demand_h), - POWERFLOW_DEMAND_COLOR - ) - - # --- Regen side (below center) --- - elif value < 0: - rl.draw_rectangle( - bar_x, - int(mid), - BAR_W, - int(regen_h), - POWERFLOW_REGEN_COLOR - ) - - line_color = rl.Color(255,255,255,150) - line_shadow = rl.Color(0,0,0,150) - - for i in range(LINES): - y = int(segment_h * i) - rl.draw_line( - bar_x, - y, - bar_x + 5, - y, - line_color - ) - rl.draw_line( - bar_x, - int(y-1), - bar_x + 5, - int(y-1), - line_shadow - ) - - rl.draw_line( - bar_x + BAR_W - 5, - y, - bar_x + BAR_W, - y, - line_color - ) - rl.draw_line( - bar_x + BAR_W - 5, - int(y-1), - bar_x + BAR_W, - int(y-1), - line_shadow - ) - - rl.draw_rectangle( - bar_x, - int(mid), - BAR_W, - 2, - rl.WHITE - ) - - rl.draw_line( - bar_x, - int(mid + 2), - bar_x + BAR_W, - int(mid + 2), - line_shadow - ) \ No newline at end of file + bar_x = int(rect.x) + bar_y = int(rect.y) + bar_h = int(rect.height) + + segment_h = bar_h / LINES + mid = bar_h * 0.6 + + rl.draw_rectangle(bar_x, bar_y, BAR_W, bar_h, rl.Color(0, 0, 0, 100)) + + demand_h = mid * abs(value) + regen_h = (bar_h - mid) * abs(value) + if value > 0: + rl.draw_rectangle(bar_x, int(mid - demand_h), BAR_W, int(demand_h), POWERFLOW_DEMAND_COLOR) + elif value < 0: + rl.draw_rectangle(bar_x, int(mid), BAR_W, int(regen_h), POWERFLOW_REGEN_COLOR) + + line_color = rl.Color(255, 255, 255, 150) + line_shadow = rl.Color(0, 0, 0, 150) + + for i in range(LINES): + y = int(segment_h * i) + rl.draw_line(bar_x, y, bar_x + 5, y, line_color) + rl.draw_line(bar_x, int(y - 1), bar_x + 5, int(y - 1), line_shadow) + rl.draw_line(bar_x + BAR_W - 5, y, bar_x + BAR_W, y, line_color) + rl.draw_line(bar_x + BAR_W - 5, int(y - 1), bar_x + BAR_W, int(y - 1), line_shadow) + + rl.draw_rectangle(bar_x, int(mid), BAR_W, 2, rl.WHITE) + rl.draw_line(bar_x, int(mid + 2), bar_x + BAR_W, int(mid + 2), line_shadow) diff --git a/selfdrive/ui/mici/onroad/torque_bar.py b/selfdrive/ui/mici/onroad/torque_bar.py index 861bfe493e..8e004770cf 100644 --- a/selfdrive/ui/mici/onroad/torque_bar.py +++ b/selfdrive/ui/mici/onroad/torque_bar.py @@ -164,6 +164,16 @@ def _update_state(self): if self._demo: return + # BluePilot: Use lateral uncertainty from controllerStateBP when available on angleState + if ui_state.sm['controlsState'].lateralControlState.which() == 'angleState': + if ui_state.sm.valid.get("controllerStateBP", False): + try: + lateral_uncertainty = ui_state.sm['controllerStateBP'].lateralUncertainty + self._torque_filter.update(min(max(lateral_uncertainty, -1), 1)) + return + except (KeyError, AttributeError): + pass + # torque line if ui_state.sm['controlsState'].lateralControlState.which() == 'angleState': controls_state = ui_state.sm['controlsState'] @@ -190,6 +200,10 @@ def _update_state(self): self._torque_filter.update(-ui_state.sm['carOutput'].actuatorsOutput.torque) def _render(self, rect: rl.Rectangle) -> None: + # BluePilot: hide angle-state torque bar when controllerStateBP is absent + if ui_state.sm['controlsState'].lateralControlState.which() == 'angleState' and not ui_state.sm.valid.get("controllerStateBP", False): + return + # adjust y pos with torque torque_line_offset = np.interp(abs(self._torque_filter.x), [0.5, 1], [22 * self._scale, 26 * self._scale]) torque_line_height = np.interp(abs(self._torque_filter.x), [0.5, 1], [14 * self._scale, 56 * self._scale]) From cccf552a5911d41ad04346984b08a44117746131 Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Fri, 12 Jun 2026 07:54:58 -0400 Subject: [PATCH 13/13] Better follow the `_bp` file and `BP` class suffix. --- selfdrive/car/card.py | 4 +++ selfdrive/car/helpers.py | 4 ++- selfdrive/ui/layouts/main.py | 9 ++++- selfdrive/ui/mici/layouts/main.py | 6 ---- .../ui/mici/onroad/augmented_road_view.py | 34 ++++++++++++++++--- .../{complication.py => complication_bp.py} | 2 +- selfdrive/ui/mici/onroad/confidence_ball.py | 15 +++++++- selfdrive/ui/mici/onroad/hud_renderer.py | 15 +++++--- selfdrive/ui/mici/onroad/model_renderer.py | 8 +++-- ...werflow_gauge.py => powerflow_gauge_bp.py} | 2 +- selfdrive/ui/mici/onroad/torque_bar.py | 2 ++ selfdrive/ui/sunnypilot/ui_state.py | 5 ++- selfdrive/ui/ui_state.py | 1 - system/manager/process_config.py | 1 + system/ui/lib/application.py | 1 + system/ui/lib/wifi_manager.py | 9 +++-- 16 files changed, 92 insertions(+), 26 deletions(-) rename selfdrive/ui/mici/onroad/{complication.py => complication_bp.py} (99%) rename selfdrive/ui/mici/onroad/{powerflow_gauge.py => powerflow_gauge_bp.py} (99%) diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index 5d6278bbd1..b85f845c14 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -27,6 +27,7 @@ from openpilot.common.bluepilot import is_bluepilot if is_bluepilot(): from openpilot.bluepilot.selfdrive.car.bp_card_publisher import publish_controller_state_bp, publish_car_state_bp +# End BluePilot REPLAY = "REPLAY" in os.environ @@ -77,6 +78,7 @@ def __init__(self, CI=None, RI=None) -> None: self.sm = messaging.SubMaster(['pandaStates', 'carControl', 'onroadEvents'] + ['carControlSP', 'longitudinalPlanSP']) # BluePilot: added controllerStateBP, carStateBP to PubMaster self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput', 'liveTracks'] + ['carParamsSP', 'carStateSP', 'controllerStateBP', 'carStateBP']) + # End BluePilot self.can_rcv_cum_timeout_counter = 0 @@ -274,6 +276,7 @@ def state_publish(self, CS: car.CarState, CS_SP: custom.CarStateSP, RD: structs. # BluePilot: publish hybrid drive gauge data (carStateBP) if is_bluepilot(): publish_car_state_bp(self.CI, self.pm, CS.canValid) + # End BluePilot def controls_update(self, CS: car.CarState, CC: car.CarControl, CC_SP: custom.CarControlSP): """control update loop, driven by carControl""" @@ -296,6 +299,7 @@ def controls_update(self, CS: car.CarState, CC: car.CarControl, CC_SP: custom.Ca # BluePilot: publish lateral uncertainty for angleState vehicles (controllerStateBP) if is_bluepilot(): publish_controller_state_bp(self.CI, self.pm) + # End BluePilot def step(self): diff --git a/selfdrive/car/helpers.py b/selfdrive/car/helpers.py index 642aa49ab6..136c638f81 100644 --- a/selfdrive/car/helpers.py +++ b/selfdrive/car/helpers.py @@ -42,8 +42,10 @@ def convert_to_capnp(struct: structs.CarParamsSP | structs.CarStateSP | structs. struct_capnp = custom.CarParamsSP.new_message(**struct_dict) elif isinstance(struct, structs.CarStateSP): struct_capnp = custom.CarStateSP.new_message(**struct_dict) - elif isinstance(struct, structs.ControllerStateBP): # BluePilot: controllerStateBP (lateral uncertainty) + # BluePilot: convert controllerStateBP custom message + elif isinstance(struct, structs.ControllerStateBP): struct_capnp = custom.ControllerStateBP.new_message(**struct_dict) + # End BluePilot else: raise ValueError(f"Unsupported struct type: {type(struct)}") diff --git a/selfdrive/ui/layouts/main.py b/selfdrive/ui/layouts/main.py index 3b57b51795..62a0422da9 100644 --- a/selfdrive/ui/layouts/main.py +++ b/selfdrive/ui/layouts/main.py @@ -20,6 +20,7 @@ from bluepilot.ui.layouts.home_bp import HomeLayoutBP as HomeLayout from openpilot.selfdrive.ui.bp.onroad.augmented_road_view_bp import AugmentedRoadViewBP as AugmentedRoadView from bluepilot.ui.widgets.debug import ControlsDebugPanel +# End BluePilot if gui_app.sunnypilot_ui(): from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout @@ -55,6 +56,7 @@ def __init__(self): if is_bluepilot(): self._debug_panel = ControlsDebugPanel() self._debug_toggled_this_frame = False + # End BluePilot # Set callbacks self._setup_callbacks() @@ -73,17 +75,19 @@ def _render(self, _): self._render_main_content() def _setup_callbacks(self): + # BluePilot: sidebar debug and network buttons self._sidebar.set_callbacks(on_settings=self._on_settings_clicked, on_flag=self._on_bookmark_clicked, - # BluePilot: sidebar debug and network buttons **({"on_debug": self._on_debug_clicked, "on_network": lambda: self.open_settings(PanelType.NETWORK)} if is_bluepilot() else {}), open_settings=lambda: self.open_settings(PanelType.TOGGLES)) + # End BluePilot self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE)) self._layouts[MainState.HOME].set_settings_callback(lambda: self.open_settings(PanelType.TOGGLES)) # BluePilot: model info click opens Models settings panel if is_bluepilot() and hasattr(self._layouts[MainState.HOME], 'set_model_settings_callback'): self._layouts[MainState.HOME].set_model_settings_callback(lambda: self.open_settings(PanelType.MODELS)) + # End BluePilot self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state) for layout in (self._layouts[MainState.ONROAD], self._home_body_layout): @@ -143,6 +147,7 @@ def _on_onroad_clicked(self): # BluePilot: suppress onroad clicks when debug panel is visible if is_bluepilot() and (self._debug_toggled_this_frame or self._debug_panel.is_panel_visible): return + # End BluePilot self._sidebar.set_visible(not self._sidebar.is_visible) def _on_body_changed(self): @@ -152,6 +157,7 @@ def _on_body_changed(self): def _on_debug_clicked(self): self._debug_panel.toggle_visibility() self._debug_toggled_this_frame = True + # End BluePilot def _render_main_content(self): # Render sidebar @@ -164,3 +170,4 @@ def _render_main_content(self): # BluePilot: render debug panel overlay on top of onroad view if is_bluepilot() and self._current_mode == MainState.ONROAD and self._debug_panel.is_panel_visible: self._debug_panel.render(content_rect) + # End BluePilot diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index 8908367435..315a1171da 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -1,14 +1,8 @@ import pyray as rl import cereal.messaging as messaging from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout -# BluePilot: replace home with paged-design home (mockups/mici_home.html) -# if is_bluepilot(): -# from openpilot.selfdrive.ui.bp.mici.layouts.home_bp import MiciHomeLayoutBP as MiciHomeLayout from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts -# BluePilot: paged-carousel offroad alerts on radial backdrop -# if is_bluepilot(): -# from openpilot.selfdrive.ui.bp.mici.layouts.offroad_alerts_bp import MiciOffroadAlertsBP as MiciOffroadAlerts from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.selfdrive.ui.mici.layouts.onboarding import OnboardingWindow diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py index 2937abe002..0a6d977b22 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -1,19 +1,15 @@ import numpy as np import pyray as rl from cereal import car, log -from openpilot.common.params import Params from msgq.visionipc import VisionStreamType from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH from openpilot.selfdrive.ui.mici.onroad.alert_renderer import AlertRenderer -from openpilot.selfdrive.ui.mici.onroad.complication import MiciComplication from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBallMiciBP from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView -from openpilot.selfdrive.ui.bp.lib.ui_debug_logger import bp_ui_log -from openpilot.selfdrive.ui.bp.onroad.blindspot_renderer import BlindspotRendererMixin from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets import Widget @@ -22,6 +18,13 @@ from openpilot.common.transformations.orientation import rot_from_euler from enum import IntEnum +# BluePilot: import MICI BP onroad helpers +from openpilot.common.params import Params +from openpilot.selfdrive.ui.bp.lib.ui_debug_logger import bp_ui_log +from openpilot.selfdrive.ui.bp.onroad.blindspot_renderer import BlindspotRendererMixin +from openpilot.selfdrive.ui.mici.onroad.complication_bp import MiciComplicationBP +# End BluePilot + if gui_app.sunnypilot_ui(): from openpilot.selfdrive.ui.sunnypilot.mici.onroad.hud_renderer import HudRendererSP as HudRenderer from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus @@ -42,7 +45,9 @@ class BookmarkState(IntEnum): ROAD_CAM_MIN_SPEED = 10 # m/s (25 mph) CAM_Y_OFFSET = 20 +# BluePilot: use narrower blindspot edge indicators on MICI MICI_BLIND_SPOT_WIDTH = 125 +# End BluePilot class BookmarkIcon(Widget): @@ -145,6 +150,7 @@ def __init__(self, bookmark_callback=None, stream_type: VisionStreamType = Visio # BluePilot: migrated MICI blindspot and border settings self._init_blindspot() self._bp_params = Params() + # End BluePilot self.device_camera: DeviceCameraConfig | None = None self.view_from_calib = view_frame_from_device_frame.copy() @@ -164,7 +170,8 @@ def __init__(self, bookmark_callback=None, stream_type: VisionStreamType = Visio self._driver_state_renderer = DriverStateRenderer() # BluePilot: left-side confidence rail and lead/speed complication self._confidence_ball = ConfidenceBallMiciBP() - self._complication = MiciComplication() + self._complication = MiciComplicationBP() + # End BluePilot self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, @@ -173,6 +180,7 @@ def __init__(self, bookmark_callback=None, stream_type: VisionStreamType = Visio self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png") # BluePilot: fade the bottom overlay only when engaged self._fade_alpha_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) + # End BluePilot def is_swiping_left(self) -> bool: """Check if currently swiping left (for scroller to disable).""" @@ -205,9 +213,11 @@ def _render(self, _): self.rect.height, ) + # BluePilot: log scissor bounds for MICI BP onroad debugging bp_ui_log.scissor("MiciAugRoadView", "begin", x=int(self._content_rect.x), y=int(self._content_rect.y), w=int(self._content_rect.width), h=int(self._content_rect.height)) + # End BluePilot rl.begin_scissor_mode( int(self._content_rect.x), int(self._content_rect.y), @@ -218,15 +228,19 @@ def _render(self, _): super()._render(self._content_rect) self._model_renderer.render(self._content_rect) + # BluePilot: fade bottom overlay only while engaged fade_alpha = self._fade_alpha_filter.update(ui_state.status != UIStatus.DISENGAGED) if fade_alpha > 1e-2: rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0, rl.Color(255, 255, 255, int(255 * fade_alpha))) + # End BluePilot alert_to_render, not_animating_out = self._alert_renderer.will_render() + # BluePilot: hide DMoji while offroad behind overlays should_draw_dmoji = (not self._hud_renderer.drawing_top_icons() and ui_state.is_onroad() and (ui_state.status != UIStatus.DISENGAGED or ui_state.always_on_dm)) + # End BluePilot self._driver_state_renderer.set_should_draw(should_draw_dmoji) self._driver_state_renderer.set_position(self._rect.x + 16, self._rect.y + 10) self._driver_state_renderer.render() @@ -234,20 +248,27 @@ def _render(self, _): self._hud_renderer.set_can_draw_top_icons(alert_to_render is None) self._hud_renderer.set_wheel_critical_icon(alert_to_render is not None and not not_animating_out and alert_to_render.visual_alert == car.CarControl.HUDControl.VisualAlert.steerRequired) + # BluePilot: only render MICI alerts while onroad if ui_state.started: self._alert_renderer.render(self._content_rect) + # End BluePilot self._hud_renderer.render(self._content_rect) + # BluePilot: log end of MICI onroad scissor region bp_ui_log.scissor("MiciAugRoadView", "end") + # End BluePilot rl.end_scissor_mode() # BluePilot: optionally hide the MICI rounded border if not self._bp_params.get_bool("BPHideOnroadBorder"): rl.draw_rectangle_rounded_lines_ex(self._content_rect, 0.2 * 1.02, 10, 50, rl.BLACK) + # End BluePilot # BluePilot: blindspot indicators live outside the scissor self._draw_blindspot_screen_edges(self.rect, MICI_BLIND_SPOT_WIDTH) + # End BluePilot + # BluePilot: render MICI BP complication and confidence rail self._complication.render(self._content_rect) ball_rect = rl.Rectangle( @@ -257,12 +278,15 @@ def _render(self, _): self._content_rect.height, ) self._confidence_ball.render(ball_rect) + # End BluePilot self._bookmark_icon.render(self.rect) + # BluePilot: dim offroad camera content under label overlay if not ui_state.started: rl.draw_rectangle(int(self.rect.x), int(self.rect.y), int(self.rect.width), int(self.rect.height), rl.Color(0, 0, 0, 175)) self._offroad_label.render(self._content_rect) + # End BluePilot def _switch_stream_if_needed(self, sm): if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams: diff --git a/selfdrive/ui/mici/onroad/complication.py b/selfdrive/ui/mici/onroad/complication_bp.py similarity index 99% rename from selfdrive/ui/mici/onroad/complication.py rename to selfdrive/ui/mici/onroad/complication_bp.py index 69dcd90a48..33f8493089 100644 --- a/selfdrive/ui/mici/onroad/complication.py +++ b/selfdrive/ui/mici/onroad/complication_bp.py @@ -32,7 +32,7 @@ class ComplicationType(IntEnumBase): lead_car_time = 4 -class MiciComplication(Widget): +class MiciComplicationBP(Widget): def __init__(self): super().__init__() self.speed: float = 0.0 diff --git a/selfdrive/ui/mici/onroad/confidence_ball.py b/selfdrive/ui/mici/onroad/confidence_ball.py index 8f7d12b587..f44d4b77b2 100644 --- a/selfdrive/ui/mici/onroad/confidence_ball.py +++ b/selfdrive/ui/mici/onroad/confidence_ball.py @@ -3,12 +3,15 @@ import pyray as rl from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.bluepilot.ui.lib.bp_shaders import draw_shader_circle_gradient from openpilot.selfdrive.ui.sunnypilot.mici.onroad.confidence_ball import ConfidenceBallSP from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import Widget +# BluePilot: import shader-backed confidence ball helper +from openpilot.bluepilot.ui.lib.bp_shaders import draw_shader_circle_gradient +# End BluePilot + def draw_circle_gradient(center_x: float, center_y: float, radius: int, top: rl.Color, bottom: rl.Color) -> None: @@ -27,8 +30,10 @@ def __init__(self, demo: bool = False, radius: float = 24, width: float = 60): Widget.__init__(self) ConfidenceBallSP.__init__(self) self._demo = demo + # BluePilot: parameterize confidence rail geometry for MICI and TICI variants self._status_dot_radius = radius self._width = width + # End BluePilot self._confidence_filter = FirstOrderFilter(-0.5, 0.5, 1 / gui_app.target_fps) def update_filter(self, value: float): @@ -54,12 +59,14 @@ def _render(self, _): self.rect.height, ) + # BluePilot: remap confidence across the full vertical rail filter_min = -0.5 filter_max = 1.0 normalized = (self._confidence_filter.x - filter_min) / (filter_max - filter_min) normalized = max(0.0, min(1.0, normalized)) dot_height = content_rect.height - (normalized * content_rect.height) + self._status_dot_radius dot_height = content_rect.y + dot_height + # End BluePilot if ui_state.status in (UIStatus.LAT_ONLY, UIStatus.LONG_ONLY, UIStatus.ENGAGED) or self._demo: if self._confidence_filter.x > 0.5: @@ -78,10 +85,12 @@ def _render(self, _): top_dot_color = rl.Color(50, 50, 50, 255) bottom_dot_color = rl.Color(13, 13, 13, 255) + # BluePilot: render MADS beam when partially engaged if ui_state.status in (UIStatus.LAT_ONLY, UIStatus.LONG_ONLY): color = self.get_lat_long_dot_color() self._draw_mads_beam(int(content_rect.x), int(content_rect.y), int(content_rect.width), int(content_rect.height), rl.Color(color.r, color.g, color.b, 150)) + # End BluePilot self._draw_circle(content_rect.x + self._status_dot_radius, dot_height, self._status_dot_radius, top_dot_color, bottom_dot_color) @@ -101,13 +110,16 @@ def _draw_circle(cx: float, cy: float, radius: float, top: rl.Color, bottom: rl. draw_shader_circle_gradient(cx, cy, radius, top, bottom) +# BluePilot: MICI confidence rail BP variant class ConfidenceBallMiciBP(ConfidenceBall): BALL_WIDTH = 60 def __init__(self, demo: bool = False): super().__init__(demo=demo, radius=24, width=self.BALL_WIDTH) +# End BluePilot +# BluePilot: TICI confidence rail BP variant TICI_CONFIDENCE_BALL_R = 50 TICI_CONFIDENCE_BALL_MARGIN = 5 TICI_CONFIDENCE_BALL_W = TICI_CONFIDENCE_BALL_R * 2 + TICI_CONFIDENCE_BALL_MARGIN @@ -118,3 +130,4 @@ class ConfidenceBallTiciBP(ConfidenceBall): def __init__(self, demo: bool = False): super().__init__(demo=demo, radius=TICI_CONFIDENCE_BALL_R, width=self.BALL_WIDTH) +# End BluePilot diff --git a/selfdrive/ui/mici/onroad/hud_renderer.py b/selfdrive/ui/mici/onroad/hud_renderer.py index c1f6b99cca..a782957a7b 100644 --- a/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/selfdrive/ui/mici/onroad/hud_renderer.py @@ -1,10 +1,7 @@ import pyray as rl from dataclasses import dataclass from openpilot.common.constants import CV -from openpilot.common.params import Params -from openpilot.selfdrive.ui.mici.onroad.powerflow_gauge import MiciPowerflowGauge from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar -from openpilot.selfdrive.ui.bp.lib.ui_debug_logger import bp_ui_log from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr @@ -13,6 +10,12 @@ from openpilot.common.filter_simple import FirstOrderFilter from cereal import log +# BluePilot: import MICI BP HUD helpers +from openpilot.common.params import Params +from openpilot.selfdrive.ui.bp.lib.ui_debug_logger import bp_ui_log +from openpilot.selfdrive.ui.mici.onroad.powerflow_gauge_bp import MiciPowerflowGaugeBP +# End BluePilot + EventName = log.OnroadEvent.EventName # Constants @@ -123,7 +126,8 @@ def __init__(self): # BluePilot: brake status coloring and hybrid powerflow on MICI self._bp_params = Params() self._brakes_on = False - self._power_flow = MiciPowerflowGauge() + self._power_flow = MiciPowerflowGaugeBP() + # End BluePilot self._txt_wheel: rl.Texture = gui_app.texture('icons_mici/wheel.png', 50, 50) self._txt_wheel_critical: rl.Texture = gui_app.texture('icons_mici/wheel_critical.png', 50, 50) @@ -188,6 +192,7 @@ def _update_state(self) -> None: self._brakes_on = False bp_ui_log.state("MiciHudRenderer", "brakes_on", self._brakes_on) + # End BluePilot def _render(self, rect: rl.Rectangle) -> None: """Render HUD elements to the screen.""" @@ -238,6 +243,7 @@ def _draw_steering_wheel(self, rect: rl.Rectangle) -> None: color = rl.Color(255, 60, 60, int(self._wheel_alpha_filter.x)) else: color = rl.Color(255, 255, 255, int(self._wheel_alpha_filter.x)) + # End BluePilot rl.draw_texture_pro(wheel_txt, src_rect, dest_rect, origin, rotation, color) if self._show_wheel_critical: @@ -256,6 +262,7 @@ def _draw_steering_wheel(self, rect: rl.Rectangle) -> None: wheel_txt.height + power_flow_radius * 2) self._power_flow.set_wheel_rect(power_rect) self._power_flow.render(rect) + # End BluePilot def _draw_set_speed(self, rect: rl.Rectangle) -> None: """Draw the MAX speed indicator box.""" diff --git a/selfdrive/ui/mici/onroad/model_renderer.py b/selfdrive/ui/mici/onroad/model_renderer.py index fae14805d0..642b58b0e0 100644 --- a/selfdrive/ui/mici/onroad/model_renderer.py +++ b/selfdrive/ui/mici/onroad/model_renderer.py @@ -8,11 +8,14 @@ from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.mici.onroad import blend_colors -from openpilot.bluepilot.ui.lib.bp_shaders import draw_rainbow_polygon from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient from openpilot.system.ui.widgets import Widget +# BluePilot: import rainbow MICI path shader +from openpilot.bluepilot.ui.lib.bp_shaders import draw_rainbow_polygon +# End BluePilot + from openpilot.selfdrive.ui.sunnypilot.mici.onroad.model_renderer import LANE_LINE_COLORS_SP, ModelRendererSP CLIP_MARGIN = 500 @@ -344,12 +347,13 @@ def _draw_path(self, sm): allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control self._blend_filter.update(int(allow_throttle)) + # BluePilot: use the migrated rainbow shader path for MICI if ui_state.rainbow_path: - # BluePilot: use the migrated rainbow shader path for MICI v_ego = sm['carState'].vEgo rainbow_v = np.clip(v_ego, 2.5, 35) / 30 draw_rainbow_polygon(self._rect, self._path.projected_points, rainbow_v=rainbow_v) return + # End BluePilot path_pts = self._path.projected_points + np.array([self._rect.x, self._rect.y], dtype=np.float32) diff --git a/selfdrive/ui/mici/onroad/powerflow_gauge.py b/selfdrive/ui/mici/onroad/powerflow_gauge_bp.py similarity index 99% rename from selfdrive/ui/mici/onroad/powerflow_gauge.py rename to selfdrive/ui/mici/onroad/powerflow_gauge_bp.py index a1af4d4bc8..c5cbf29947 100644 --- a/selfdrive/ui/mici/onroad/powerflow_gauge.py +++ b/selfdrive/ui/mici/onroad/powerflow_gauge_bp.py @@ -21,7 +21,7 @@ POWERFLOW_DEMAND_COLOR = rl.Color(100, 150, 255, 255) -class MiciPowerflowGauge(Widget): +class MiciPowerflowGaugeBP(Widget): """Widget to display powerflow gauge as an arch above the torque bar.""" RADIUS = 20 diff --git a/selfdrive/ui/mici/onroad/torque_bar.py b/selfdrive/ui/mici/onroad/torque_bar.py index 8e004770cf..e0c60e192a 100644 --- a/selfdrive/ui/mici/onroad/torque_bar.py +++ b/selfdrive/ui/mici/onroad/torque_bar.py @@ -173,6 +173,7 @@ def _update_state(self): return except (KeyError, AttributeError): pass + # End BluePilot # torque line if ui_state.sm['controlsState'].lateralControlState.which() == 'angleState': @@ -203,6 +204,7 @@ def _render(self, rect: rl.Rectangle) -> None: # BluePilot: hide angle-state torque bar when controllerStateBP is absent if ui_state.sm['controlsState'].lateralControlState.which() == 'angleState' and not ui_state.sm.valid.get("controllerStateBP", False): return + # End BluePilot # adjust y pos with torque torque_line_offset = np.interp(abs(self._torque_filter.x), [0.5, 1], [22 * self._scale, 26 * self._scale]) diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index 5a47d9238a..5fe4095294 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -33,7 +33,10 @@ def __init__(self): self.sm_services_ext = [ "modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP", "gpsLocation", "liveTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "liveDelay", - "controllerStateBP", # BluePilot: lateral uncertainty for torque bar + # BluePilot: lateral uncertainty for torque bar + hybrid battery and drive data + "controllerStateBP", + "carStateBP", + # End BluePilot ] self.sunnylink_state = SunnylinkState() diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index 956b03eb0e..aa0edd6128 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -65,7 +65,6 @@ def _initialize(self): "testJoystick", "rawAudioData", ] + self.sm_services_ext - + (["carStateBP"] if is_bluepilot() else []) # BluePilot: hybrid battery and drive data ) self.prime_state = PrimeState() diff --git a/system/manager/process_config.py b/system/manager/process_config.py index db233660d0..f63e613a5b 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -195,6 +195,7 @@ def _bp_route_preprocessor_enabled(started, params, CP): PythonProcess("bp_portal", "bluepilot.backend.bp_portal", _bp_portal_enabled), PythonProcess("bp_route_preprocessor", "bluepilot.backend.routes.preprocessor", _bp_route_preprocessor_enabled), ] +# End BluePilot if os.path.exists("./github_runner.sh"): procs += [NativeProcess("github_runner_start", "system/manager", ["./github_runner.sh", "start"], and_(only_offroad, use_github_runner), sigkill=False)] diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index d4b54b81ef..c9e8b2901d 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -489,6 +489,7 @@ def _load_image_from_path(self, image_path: str, width: int | None = None, heigh cloudlog.error("Invalid image dimensions from %r: %dx%d", image_path, image.width, image.height) rl.unload_image(image) image = rl.gen_image_color(1, 1, rl.Color(0, 0, 0, 0)) + # End BluePilot if alpha_premultiply: rl.image_alpha_premultiply(image) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 903ac4ebca..aa903bf623 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -84,6 +84,7 @@ def get_security_type(flags: int, wpa_flags: int, rsn_flags: int) -> SecurityTyp if is_bluepilot(): from openpilot.bluepilot.system.ui.lib.bp_wifi import SUPPORTS_WPA_EXTENDED supports_wpa = SUPPORTS_WPA_EXTENDED + # End BluePilot if (flags == NM_802_11_AP_FLAGS_NONE) or ((flags & NM_802_11_AP_FLAGS_WPS) and not (wpa_props & supports_wpa)): return SecurityType.OPEN @@ -210,6 +211,7 @@ def __init__(self): if is_bluepilot(): from openpilot.selfdrive.ui.bp.lib.wifi_favorite import WifiFavoriteManager self._favorite_manager = WifiFavoriteManager(self) + # End BluePilot self._initialize() atexit.register(self.stop) @@ -230,6 +232,7 @@ def worker(): # BluePilot: start favorite network background scanner if is_bluepilot(): self._favorite_manager.start() + # End BluePilot self._tethering_password = self._get_tethering_password() cloudlog.debug("WifiManager initialized") @@ -857,8 +860,9 @@ def _request_scan(self): if reply.header.message_type == MessageType.error: cloudlog.warning(f"Failed to request scan: {reply}") - def _update_networks(self, block: bool = True, - force: bool = False): # BluePilot: force=True refreshes AP list when settings UI is hidden + # BluePilot: force=True refreshes AP list when settings UI is hidden + def _update_networks(self, block: bool = True, force: bool = False): + # End BluePilot if not self._active and not force: return @@ -956,6 +960,7 @@ def stop(self): # BluePilot: stop favorite network auto-connect if is_bluepilot() and hasattr(self, '_favorite_manager'): self._favorite_manager.stop() + # End BluePilot if self._router_main is not None: self._router_main.close()