mirror of
https://github.com/infiniteCable2/openpilot.git
synced 2026-07-26 20:02:11 +08:00
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:
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6b45f88128430fb50ef51c8e08b8e2a1c8fbe0b5c3a08de9f5d9d59bc1edc82e
|
||||
size 4545
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user