sunnypilot v2026.002.000
version: sunnypilot v2026.002.000 (staging) date: 2026-07-21T07:44:02 master commit: 2c334ede443d7391d27575af8c854a095ba702a8
This commit is contained in:
commit
de77c89cf6
3995 files changed
+1260688
No files matched your search
Whitespace-only changes.
@@ -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
|
||||
Whitespace-only changes.
@@ -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)
|
||||
Reference in new issue
Block a user