body ui c3 & c4 (#37794)

* c4 body ui

* clean up diff

* clean up

* default bodyview debug with True

* remove battery indicator and fix close settings bug

* organize debug file

* clean

* clean up frame index

* remove unneccessary is body check

* update bodyview

* remove joystick_debug_mode based events

* remove debug script

* apply suggestions

* clean diff

* clean diff

* move ignition message

* save a line

* remove visibility set and fix tici body face when sidebar open

* remove explicit textColor offroad message

* remove unused imports

* revert pairing dialog

* apply suggestions

* add body specific icon

* add body icon for homescreen

* set mode for state on tici after on body changed

* tiny

* tweak

* v

* tweaks

* icon ratio was wrong!

* same order

* rm

* apply suggestions

* remove commented lines in animation

* onroad click callback was on home bug and fix setup widget settings call back hack

* fix body changed

* one liner

* clean up

* formatting

* if false

* revert to master + reimplement

* close sidebar on body home tici

* make ignition message bigger on c3

* flip eye direction when turning

---------

Co-authored-by: Nick <nickorie@gmail.com>
Co-authored-by: Shane Smiskol <shane@smiskol.com>
This commit is contained in:
stef
2026-04-18 13:00:05 -07:00
committed by GitHub
parent b9986cae06
commit 5d7155fdda
9 changed files with 434 additions and 13 deletions
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6b45f88128430fb50ef51c8e08b8e2a1c8fbe0b5c3a08de9f5d9d59bc1edc82e
size 4545
View File
+278
View File
@@ -0,0 +1,278 @@
from dataclasses import dataclass
from enum import Enum
import time
class AnimationMode(Enum):
ONCE_FORWARD = 1
ONCE_FORWARD_BACKWARD = 2
REPEAT_FORWARD = 3
REPEAT_FORWARD_BACKWARD = 4
@dataclass
class Animation:
frames: list[list[tuple[int, int]]]
starting_frames: list[list[tuple[int, int]]] | None = None # played once before the main loop
frame_duration: float = 0.15 # seconds each frame is shown
mode: AnimationMode = AnimationMode.REPEAT_FORWARD_BACKWARD
repeat_interval: float = 5.0 # seconds between animation restarts (only for REPEAT modes)
hold_end: float = 0.0 # seconds to hold the last frame before playing backward (only for *_BACKWARD modes)
left_turn_remove: list[tuple[int, int]] | None = None # dots to remove from frame when turning left
right_turn_remove: list[tuple[int, int]] | None = None # dots to remove from frame when turning right
# --- Animation Helper Functions ---
def _mirror(dots: list[tuple[int, int]]) -> list[tuple[int, int]]:
"""Mirror a component from the left side of the face to the right"""
return [(r, 15 - c) for r, c in dots]
def _mirror_no_flip(dots: list[tuple[int, int]]) -> list[tuple[int, int]]:
"""Move a component to the mirrored position on the right half without flipping its shape."""
min_c = min(c for _, c in dots)
max_c = max(c for _, c in dots)
return [(r, 15 - max_c - min_c + c) for r, c in dots]
def _shift(dots: list[tuple[int, int]], rc: tuple[int, int]) -> list[tuple[int, int]]:
dr, dc = rc
return [(r + dr, c + dc) for r, c in dots]
def _make_frame(left_eye: list[tuple[int, int]], right_eye: list[tuple[int, int]],
left_brow: list[tuple[int, int]], right_brow: list[tuple[int, int]],
mouth: list[tuple[int, int]]) -> list[tuple[int, int]]:
return left_eye + left_brow + right_eye + right_brow + mouth
# --- Animation Helper Components ---
# Eyes (left side)
EYE_OPEN = [
(2, 2), (2, 3),
(3, 1), (3, 2), (3, 3), (3, 4),
(4, 1), (4, 2), (4, 3), (4, 4),
(5, 2), (5, 3)
]
EYE_HALF = [
(4, 1), (4, 2), (4, 3), (4, 4),
(5, 2), (5, 3)
]
EYE_CLOSED = [
(4, 1), (4, 4),
(5, 2), (5, 3),
]
EYE_LEFT_LOOK = [
(2, 2), (2, 3),
(3, 1), (3, 2),
(4, 1), (4, 2),
(5, 2), (5, 3),
]
EYE_RIGHT_LOOK = [
(2, 2), (2, 3),
(3, 3), (3, 4),
(4, 3), (4, 4),
(5, 2), (5, 3),
]
# Eyebrows (left side)
BROW_HIGH = [
(0, 1), (0, 2),
(1, 0),
]
BROW_LOWERED = [
(1, 1), (1, 2),
(2, 0)
]
BROW_STRAIGHT = [(1, 0), (1, 1), (1, 2)]
BROW_DOWN = [
(0, 1), (0, 2),
(1, 3)
]
# Mouths (centered, not mirrored)
MOUTH_SMILE = [
(6, 6), (6, 9),
(7, 7), (7, 8),
]
MOUTH_NORMAL = [(7, 7), (7, 8)]
MOUTH_SAD = [
(6, 7), (6, 8),
(7, 6), (7, 9)
]
# --- Animations ---
NORMAL = Animation(
frames=[
_make_frame(EYE_OPEN, _mirror(EYE_OPEN), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(EYE_HALF, _mirror(EYE_HALF), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(EYE_CLOSED, _mirror(EYE_CLOSED), BROW_LOWERED, _mirror(BROW_LOWERED), MOUTH_SMILE),
],
left_turn_remove=[
(3, 3), (3, 4),
(4, 3), (4, 4),
] + _mirror_no_flip([
(3, 1), (3, 2),
(4, 1), (4, 2),
]),
right_turn_remove=[
(3, 1), (3, 2),
(4, 1), (4, 2),
] + _mirror_no_flip([
(3, 3), (3, 4),
(4, 3), (4, 4),
])
)
ASLEEP = Animation(
frames=[
_make_frame(EYE_CLOSED, _mirror(EYE_CLOSED), [], [], MOUTH_NORMAL),
],
)
SLEEPY = Animation(
frames=[
_make_frame(EYE_CLOSED, _mirror(EYE_CLOSED), _shift(BROW_STRAIGHT, (1, 0)), [], MOUTH_NORMAL),
_make_frame(EYE_HALF, _mirror(EYE_CLOSED), BROW_LOWERED, [], MOUTH_NORMAL),
_make_frame(EYE_OPEN, _mirror(EYE_CLOSED), BROW_HIGH, [], MOUTH_NORMAL)
],
frame_duration=0.25,
mode=AnimationMode.ONCE_FORWARD_BACKWARD,
repeat_interval=10,
hold_end=1.5,
)
INQUISITIVE = Animation(
frames=[
_make_frame(EYE_OPEN, _mirror(EYE_OPEN), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(EYE_LEFT_LOOK, _mirror(EYE_RIGHT_LOOK), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(_shift(EYE_LEFT_LOOK, (0, -1)), _shift(_mirror(EYE_RIGHT_LOOK), (0, -1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(_shift(EYE_LEFT_LOOK, (0, -1)), _shift(_mirror(EYE_RIGHT_LOOK), (0, -1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(_shift(EYE_LEFT_LOOK, (0, -1)), _shift(_mirror(EYE_RIGHT_LOOK), (0, -1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(EYE_LEFT_LOOK, _mirror(EYE_RIGHT_LOOK), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(EYE_RIGHT_LOOK, _mirror(EYE_LEFT_LOOK), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(_shift(EYE_RIGHT_LOOK, (0, 1)), _shift(_mirror(EYE_LEFT_LOOK), (0, 1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(_shift(EYE_RIGHT_LOOK, (0, 1)), _shift(_mirror(EYE_LEFT_LOOK), (0, 1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(_shift(EYE_RIGHT_LOOK, (0, 1)), _shift(_mirror(EYE_LEFT_LOOK), (0, 1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(EYE_RIGHT_LOOK, _mirror(EYE_LEFT_LOOK), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(EYE_OPEN, _mirror(EYE_OPEN), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
],
mode=AnimationMode.REPEAT_FORWARD,
frame_duration=0.15,
repeat_interval=10
)
WINK = Animation(
frames=[
_make_frame(EYE_OPEN, _mirror(EYE_OPEN), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
_make_frame(EYE_OPEN, _mirror(EYE_CLOSED), BROW_HIGH, _mirror(_shift(BROW_DOWN, (0, 2))), MOUTH_SMILE),
],
mode=AnimationMode.ONCE_FORWARD_BACKWARD,
frame_duration=0.75,
)
# --- Face Animator Class ---
class FaceAnimator:
def __init__(self, animation: Animation):
self._animation = animation
self._next: Animation | None = None
self._start_time = time.monotonic()
self._rewinding = False
self._rewind_start: float = 0.0
self._rewind_from: int = 0
self._seen_nonzero = False
def set_animation(self, animation: Animation):
if animation is not self._animation:
self._next = animation
def get_dots(self) -> list[tuple[int, int]]:
now = time.monotonic()
elapsed = now - self._start_time
# Handle rewind for forward-only animations
if self._rewinding:
rewind_elapsed = now - self._rewind_start
frames_back = round(rewind_elapsed / self._animation.frame_duration)
frame_index = self._rewind_from - frames_back
if frame_index <= 0:
return self._switch_to_next(now)
return self._animation.frames[frame_index]
# Play starting frames first (once)
starting = self._animation.starting_frames or []
starting_duration = len(starting) * self._animation.frame_duration
if starting and elapsed < starting_duration:
frame_index = min(int(elapsed / self._animation.frame_duration), len(starting) - 1)
return starting[frame_index]
# Main loop
loop_elapsed = elapsed - starting_duration if starting else elapsed
frame_index = _get_frame_index(self._animation, loop_elapsed, gap_first=bool(starting))
if frame_index != 0:
self._seen_nonzero = True
if self._next is not None:
if frame_index == 0 and (len(self._animation.frames) == 1 or self._seen_nonzero):
return self._switch_to_next(now)
# No natural return to frame 0 — start rewinding
if self._animation.mode in (AnimationMode.ONCE_FORWARD, AnimationMode.REPEAT_FORWARD):
self._rewinding = True
self._rewind_start = now
self._rewind_from = frame_index
return self._animation.frames[frame_index]
def _switch_to_next(self, now: float) -> list[tuple[int, int]]:
self._animation = self._next
self._next = None
self._rewinding = False
self._seen_nonzero = False
self._start_time = now
return self._animation.frames[0]
def _get_frame_index(animation: Animation, elapsed: float, gap_first: bool = False) -> int:
"""Get the current frame index given elapsed time and animation mode."""
num_frames = len(animation.frames)
if num_frames == 1:
return 0
fd = animation.frame_duration
has_backward = animation.mode in (AnimationMode.ONCE_FORWARD_BACKWARD, AnimationMode.REPEAT_FORWARD_BACKWARD)
repeats = animation.mode in (AnimationMode.REPEAT_FORWARD, AnimationMode.REPEAT_FORWARD_BACKWARD)
forward_duration = num_frames * fd
backward_frames = max(num_frames - 2, 0) if has_backward else 0
hold = animation.hold_end if has_backward else 0.0
cycle_duration = forward_duration + hold + backward_frames * fd
if not repeats:
t = min(elapsed, cycle_duration)
else:
t = (elapsed + cycle_duration if gap_first else elapsed) % animation.repeat_interval
# Forward phase
if t < forward_duration:
return min(int(t / fd), num_frames - 1)
t -= forward_duration
# Hold at last frame
if t < hold:
return num_frames - 1
t -= hold
# Backward phase
if backward_frames and t < backward_frames * fd:
return num_frames - 2 - min(int(t / fd), backward_frames - 1)
return 0 if has_backward else num_frames - 1
+93
View File
@@ -0,0 +1,93 @@
import time
import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.body.animations import FaceAnimator, ASLEEP, INQUISITIVE, NORMAL, SLEEPY
GRID_COLS = 16
GRID_ROWS = 8
DOT_RADIUS = 50 if gui_app.big_ui() else 10
IDLE_TIMEOUT = 30.0 # seconds of no joystick input before playing INQUISITIVE
IDLE_STEER_THRESH = 0.5 # degrees — below this counts as no input
IDLE_SPEED_THRESH = 0.01 # m/s — below this counts as no input
# This class is used both in BIG (tizi) and small (mici) UIs
class BodyLayout(Widget):
def __init__(self):
super().__init__()
self._animator = FaceAnimator(ASLEEP)
self._turning_left = False
self._turning_right = False
self._last_input_time = time.monotonic()
self._was_active = False
self._offroad_label = UnifiedLabel("turn on ignition to use", 95 if gui_app.big_ui() else 45, FontWeight.DISPLAY,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
def draw_dot_grid(self, rect: rl.Rectangle, dots: list[tuple[int, int]], color: rl.Color):
spacing = min(rect.height / GRID_ROWS, rect.width / GRID_COLS)
grid_w = (GRID_COLS - 1) * spacing
grid_h = (GRID_ROWS - 1) * spacing
offset_x = rect.x + (rect.width - grid_w) / 2
offset_y = rect.y + (rect.height - grid_h) / 2
for row, col in dots:
x = int(offset_x + col * spacing)
y = int(offset_y + row * spacing)
rl.draw_circle(x, y, DOT_RADIUS, color)
def _update_state(self):
super()._update_state()
sm = ui_state.sm
if ui_state.is_onroad():
if not self._was_active:
self._last_input_time = time.monotonic()
self._was_active = True
cs = sm['carState']
has_input = abs(cs.steeringAngleDeg) > IDLE_STEER_THRESH or abs(cs.vEgo) > IDLE_SPEED_THRESH
if has_input:
self._last_input_time = time.monotonic()
if time.monotonic() - self._last_input_time > IDLE_TIMEOUT:
self._animator.set_animation(INQUISITIVE)
else:
self._animator.set_animation(NORMAL)
else:
self._was_active = False
self._animator.set_animation(ASLEEP)
steer = sm['testJoystick'].axes[1] if len(sm['testJoystick'].axes) > 1 else 0
self._turning_left = steer <= -0.05
self._turning_right = steer >= 0.05
# play animation on screen tap
def _handle_mouse_release(self, mouse_pos):
super()._handle_mouse_release(mouse_pos)
if not self._was_active:
self._animator.set_animation(SLEEPY)
def _render(self, rect: rl.Rectangle):
dots = self._animator.get_dots()
animation = self._animator._animation
if self._turning_left and animation.left_turn_remove:
remove_set = set(animation.left_turn_remove)
dots = [d for d in dots if d not in remove_set]
elif self._turning_right and animation.right_turn_remove:
remove_set = set(animation.right_turn_remove)
dots = [d for d in dots if d not in remove_set]
self.draw_dot_grid(rect, dots, rl.WHITE)
if ui_state.is_offroad():
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))
upper_half = rl.Rectangle(rect.x, rect.y, rect.width, rect.height / 2)
self._offroad_label.render(upper_half)
+21 -4
View File
@@ -2,13 +2,14 @@ import pyray as rl
from enum import IntEnum
import cereal.messaging as messaging
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH
from openpilot.selfdrive.ui.layouts.home import HomeLayout
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow
from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout
class MainState(IntEnum):
@@ -28,7 +29,9 @@ class MainLayout(Widget):
self._prev_onroad = False
# Initialize layouts
self._layouts = {MainState.HOME: HomeLayout(), MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()}
self._home_layout = HomeLayout()
self._home_body_layout = BodyLayout()
self._layouts = {MainState.HOME: self._home_layout, MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()}
self._sidebar_rect = rl.Rectangle(0, 0, 0, 0)
self._content_rect = rl.Rectangle(0, 0, 0, 0)
@@ -54,14 +57,18 @@ class MainLayout(Widget):
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))
self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
self._layouts[MainState.ONROAD].set_click_callback(self._on_onroad_clicked)
for layout in (self._layouts[MainState.ONROAD], self._home_body_layout):
layout.set_click_callback(self._on_onroad_clicked)
device.add_interactive_timeout_callback(self._set_mode_for_state)
ui_state.add_on_body_changed_callbacks(self._on_body_changed)
def _update_layout_rects(self):
self._sidebar_rect = rl.Rectangle(self._rect.x, self._rect.y, SIDEBAR_WIDTH, self._rect.height)
x_offset = SIDEBAR_WIDTH if self._sidebar.is_visible else 0
self._content_rect = rl.Rectangle(self._rect.y + x_offset, self._rect.y, self._rect.width - x_offset, self._rect.height)
self._content_rect = rl.Rectangle(self._rect.x + x_offset, self._rect.y, self._rect.width - x_offset, self._rect.height)
def _handle_onroad_transition(self):
if ui_state.started != self._prev_onroad:
@@ -70,6 +77,12 @@ class MainLayout(Widget):
self._set_mode_for_state()
def _set_mode_for_state(self):
# Don't go onroad if body, home is onroad
if ui_state.is_body:
self._set_current_layout(MainState.HOME)
self._sidebar.set_visible(not ui_state.ignition)
return
if ui_state.started:
# Don't hide sidebar from interactive timeout
if self._current_mode != MainState.ONROAD:
@@ -101,6 +114,10 @@ class MainLayout(Widget):
def _on_onroad_clicked(self):
self._sidebar.set_visible(not self._sidebar.is_visible)
def _on_body_changed(self):
self._layouts[MainState.HOME] = self._home_body_layout if ui_state.is_body else self._home_layout
self._set_mode_for_state()
def _render_main_content(self):
# Render sidebar
if self._sidebar.is_visible:
+3
View File
@@ -130,6 +130,7 @@ class MiciHomeLayout(Widget):
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
self._body_icon = IconWidget("icons_mici/body.png", (54, 37))
self._alerts_pill = AlertsPill()
@@ -137,6 +138,7 @@ class MiciHomeLayout(Widget):
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
NetworkIcon(),
self._experimental_icon,
self._body_icon,
self._mic_icon,
], spacing=18)
@@ -247,6 +249,7 @@ class MiciHomeLayout(Widget):
# ***** Center-aligned bottom section icons *****
self._experimental_icon.set_visible(self._experimental_mode)
self._mic_icon.set_visible(ui_state.recording_audio)
self._body_icon.set_visible(ui_state.is_body)
footer_rect = rl.Rectangle(self.rect.x + HOME_PADDING, self.rect.y + self.rect.height - 48, self.rect.width - HOME_PADDING, 48)
self._status_bar_layout.render(footer_rect)
+21 -5
View File
@@ -6,6 +6,7 @@ from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import 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
from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.lib.application import gui_app
@@ -29,22 +30,25 @@ class MiciMainLayout(Scroller):
self._home_layout = MiciHomeLayout()
self._alerts_layout = MiciOffroadAlerts()
self._settings_layout = SettingsLayout()
self._onroad_layout = AugmentedRoadView(bookmark_callback=self._on_bookmark_clicked)
self._car_onroad_layout = AugmentedRoadView(bookmark_callback=self._on_bookmark_clicked)
self._body_onroad_layout = BodyLayout()
# Initialize widget rects
for widget in (self._home_layout, self._settings_layout, self._alerts_layout, self._onroad_layout):
for widget in (self._home_layout, self._alerts_layout, self._settings_layout,
self._car_onroad_layout, self._body_onroad_layout):
# TODO: set parent rect and use it if never passed rect from render (like in Scroller)
widget.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
self._scroller.add_widgets([
self._alerts_layout,
self._home_layout,
self._onroad_layout,
self._car_onroad_layout,
self._body_onroad_layout,
])
self._scroller.set_reset_scroll_at_show(False)
# Disable scrolling when onroad is interacting with bookmark
self._scroller.set_scrolling_enabled(lambda: not self._onroad_layout.is_swiping_left())
self._scroller.set_scrolling_enabled(lambda: not self._car_onroad_layout.is_swiping_left())
# Set callbacks
self._setup_callbacks()
@@ -57,14 +61,22 @@ class MiciMainLayout(Scroller):
if not self._onboarding_window.completed:
gui_app.push_widget(self._onboarding_window)
@property
def _onroad_layout(self) -> Widget:
# For scroll_to
return self._body_onroad_layout if ui_state.is_body else self._car_onroad_layout
def _setup_callbacks(self):
self._home_layout.set_callbacks(
on_settings=lambda: gui_app.push_widget(self._settings_layout),
on_alerts=lambda: self._scroll_to(self._alerts_layout),
alert_count_callback=self._alerts_layout.active_alerts,
)
self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
for layout in (self._car_onroad_layout, self._body_onroad_layout):
layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
device.add_interactive_timeout_callback(self._on_interactive_timeout)
ui_state.add_on_body_changed_callbacks(self._on_body_changed)
def _scroll_to(self, layout: Widget):
layout_x = int(layout.rect.x)
@@ -130,3 +142,7 @@ class MiciMainLayout(Scroller):
user_bookmark = messaging.new_message('bookmarkButton')
user_bookmark.valid = True
self._pm.send('bookmarkButton', user_bookmark)
def _on_body_changed(self):
self._car_onroad_layout.set_visible(not ui_state.is_body)
self._body_onroad_layout.set_visible(ui_state.is_body)
+15 -4
View File
@@ -13,6 +13,7 @@ from openpilot.system.ui.lib.application import gui_app
from openpilot.system.hardware import HARDWARE, PC
BACKLIGHT_OFFROAD = 65 if HARDWARE.get_device_type() == "mici" else 50
PARAM_UPDATE_TIME = 5.0
class UIStatus(Enum):
@@ -54,6 +55,7 @@ class UIState:
"carOutput",
"carControl",
"liveParameters",
"testJoystick",
"rawAudioData",
]
)
@@ -77,15 +79,15 @@ class UIState:
self.panda_type: log.PandaState.PandaType = log.PandaState.PandaType.unknown
self.personality: log.LongitudinalPersonality = log.LongitudinalPersonality.standard
self.has_longitudinal_control: bool = False
self.is_body: bool | None = None
self.CP: car.CarParams | None = None
self.light_sensor: float = -1.0
self._param_update_time: float = 0.0
self._param_update_time: float = -PARAM_UPDATE_TIME
# Callbacks
self._offroad_transition_callbacks: list[Callable[[], None]] = []
self._engaged_transition_callbacks: list[Callable[[], None]] = []
self.update_params()
self._on_body_changed_callbacks: list[Callable[[], None]] = []
def add_offroad_transition_callback(self, callback: Callable[[], None]):
self._offroad_transition_callbacks.append(callback)
@@ -93,6 +95,9 @@ class UIState:
def add_engaged_transition_callback(self, callback: Callable[[], None]):
self._engaged_transition_callbacks.append(callback)
def add_on_body_changed_callbacks(self, callback: Callable[[], None]):
self._on_body_changed_callbacks.append(callback)
@property
def engaged(self) -> bool:
return self.started and self.sm["selfdriveState"].enabled
@@ -108,7 +113,7 @@ class UIState:
self.sm.update(0)
self._update_state()
self._update_status()
if time.monotonic() - self._param_update_time > 5.0:
if time.monotonic() - self._param_update_time >= PARAM_UPDATE_TIME:
self.update_params()
device.update()
@@ -180,6 +185,12 @@ class UIState:
self.has_longitudinal_control = self.params.get_bool("AlphaLongitudinalEnabled")
else:
self.has_longitudinal_control = self.CP.openpilotLongitudinalControl
if self.is_body != self.CP.notCar:
self.is_body = self.CP.notCar
for callback in self._on_body_changed_callbacks:
callback()
self._param_update_time = time.monotonic()