Files
StarPilot/openpilot/starpilot/ui/onroad_large_widgets.py

172 lines
8.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Adapted large StarPilot onroad widgets with supplied observations.
The speed group uses the reviewed device geometry; the current speed and wheel
retain their native anchors. See the adjacent UI LICENSE for retained source.
"""
from pathlib import Path
import hashlib
import json
import pyray as rl
from openpilot.starpilot.ui.onroad_customization import offset, rgba, widget_size
from openpilot.starpilot.ui.onroad_state import OnroadState, speed_limit_bounds
from openpilot.starpilot.ui.onroad_widget_style import draw_control_card
from openpilot.starpilot.ui.presentation import BitmapFonts, FontRole, Profile
from openpilot.starpilot.ui.wheel_feedback import WheelFeedbackFade
from openpilot.starpilot.ui.unified_speed_presentation import resolve_unified_speed
from openpilot.starpilot.ui.speed_limit_widget import SpeedLimitWidget, draw_caption, draw_ink, draw_text
ENGAGED = rl.Color(128, 216, 166, 255)
DISENGAGED = rl.Color(145, 155, 149, 255)
def _center(fonts: BitmapFonts, text: str, role: FontRole, size: int, rect: rl.Rectangle, y: float,
color: rl.Color = rl.WHITE) -> None:
measured = fonts.measure(text, role, size)
fonts.draw(text, role, size, rect.x + (rect.width - measured.width) / 2, y, color)
class UnifiedSpeedWidget:
"""The set speed with the latest limit beside it."""
def __init__(self, fonts: BitmapFonts):
self.fonts = fonts
self.sign = SpeedLimitWidget(fonts, Profile.LARGE)
def close(self):
self.sign.close()
def _draw_sources(self, rect, state):
"""Each enabled source as a small label over its reading; the selected source is highlighted."""
from openpilot.starpilot.ui.large_speed_geometry import PADDING
rows = [row for row in state.speed_limit.source_readings if row.enabled]
if not rows:
text = 'No sources enabled' if state.speed_limit.source_readings else 'No speed limit'
width = self.fonts.measure(text, FontRole.NORMAL, 26).width
draw_text(self.fonts, text, FontRole.NORMAL, 26, rect.x + (rect.width - width) / 2, rect.y + 22, rl.WHITE)
return
active = state.speed_limit.source if str(state.speed_limit.kind) == 'valid' else state.speed_limit.accepted_source
labels = {'dashboard': 'Dash', 'map': 'Map', 'vision': 'Vision', 'online': 'Online'}
cell = (rect.width - 2 * PADDING) / len(rows)
for index, row in enumerate(rows):
color = rl.WHITE if row.source == active else rl.Color(170, 179, 174, 255)
value = str(round(row.speed_mps * (3.6 if state.metric else 2.2369362921))) if row.kind == 'valid' and row.speed_mps is not None else '–'
box = rl.Rectangle(rect.x + PADDING + index * cell, rect.y, cell, rect.height)
size = 22
while size > 12 and self.fonts.measure(labels[row.source], FontRole.NORMAL, size).width > cell - 4:
size -= 1
for text, role, font_size, y in ((labels[row.source], FontRole.NORMAL, size, rect.y + 10),
(value, FontRole.MEDIUM if row.source == active else FontRole.NORMAL, 30, rect.y + 36)):
width = self.fonts.measure(text, role, font_size).width
draw_text(self.fonts, text, role, font_size, box.x + (box.width - width) / 2, y, color)
def render(self, content: rl.Rectangle, state: OnroadState) -> None:
from openpilot.starpilot.ui.large_speed_geometry import GAP, WIDTH, PADDING, SOURCE_ROW_HEIGHT, card_height
from openpilot.starpilot.ui.onroad_customization import placement
shown = resolve_unified_speed(state)
if shown.mode == 'hidden':
return
saved = placement(state.customization, 'large', 'cruise_limits')
height = card_height(shown.mode)
rect = rl.Rectangle(saved['x'], saved['y'], WIDTH, height)
sources = state.customization.get('speedSources') is True
separate = state.customization.get('speedSourcesCard') is True
fill = rl.Color(*rgba(state.customization, 'cardFill', 'large', 'cruise_limits'))
border = rl.Color(*rgba(state.customization, 'cardBorder', 'large', 'cruise_limits'))
text = rl.Color(*rgba(state.customization, 'text', 'large', 'cruise_limits'))
if shown.mode != 'limit_only':
x = rect.x + PADDING
draw_caption(self.fonts, 'SET', x, rect.y + 19, unit=shown.unit)
size = 120
while size > 72 and self.fonts.measure(shown.max_text, FontRole.SET_SPEED, size).width > 148:
size -= 1
draw_ink(self.fonts, shown.max_text, FontRole.SET_SPEED, size, x, rect.y + 51, text)
self.sign.render(state)
if shown.mode == 'limit_only' and (bounds := speed_limit_bounds(Profile.LARGE, state)) is not None:
draw_ink(self.fonts, shown.unit, FontRole.NORMAL, 24, bounds[2], bounds[3] - 32)
if sources:
row = rl.Rectangle(rect.x, rect.y + height + (GAP if separate else 0), WIDTH, SOURCE_ROW_HEIGHT)
if separate:
draw_control_card(row, fill=fill, border=border, border_width=1, roundness=.2)
else:
rl.draw_line_ex(rl.Vector2(rect.x + 1, row.y), rl.Vector2(rect.x + WIDTH - 1, row.y), 1, border)
self._draw_sources(row, state)
class CurrentSpeedHud:
def __init__(self, fonts: BitmapFonts):
self.fonts = fonts
def render(self, content: rl.Rectangle, state: OnroadState) -> None:
if state.speed_mps is None:
return
speed = str(round(state.speed_mps * (3.6 if state.metric else 2.2369362921)))
dx, dy = offset(state.customization, "large", "current_speed")
rect = rl.Rectangle(content.x + 610 + dx, content.y + dy, 580, 300)
number_top, _ = self.fonts.vertical_ink(speed, FontRole.SPEED, 176)
old_top, old_bottom = self.fonts.vertical_ink(speed, FontRole.BOLD, 176)
_center(self.fonts, speed, FontRole.SPEED, 176, rect, content.y + 42 + dy + old_top - number_top,
rl.Color(*rgba(state.customization, 'text', 'large', 'current_speed')))
unit = 'km/h' if state.metric else 'mph'
unit_top, _ = self.fonts.vertical_ink(unit, FontRole.NORMAL, 44)
red, green, blue, alpha = rgba(state.customization, 'text', 'large', 'current_speed')
_center(self.fonts, unit, FontRole.NORMAL, 44, rect, content.y + 42 + dy + old_bottom + 20 - unit_top,
rl.Color(red, green, blue, int(alpha * 200 / 255)))
class SteeringWheelWidget:
"""Frozen ExpButton shape; clicks are handled by the separate request input."""
def __init__(self, asset_directory: Path):
self.asset_directory = asset_directory
self._texture: rl.Texture | None = None
self._feedback = WheelFeedbackFade()
def prepare(self) -> None:
if self._texture is not None:
return
path = self.asset_directory / "icons/chffr_wheel.png"
entry = json.loads(Path(__file__).with_name("onroad-assets.json").read_text())["files"][0]
data = path.read_bytes()
if len(data) != entry["bytes"] or hashlib.sha256(data).hexdigest() != entry["sha256"]:
raise ValueError("Unreviewed steering wheel art")
image = rl.load_image(str(path))
try:
if image.data == rl.ffi.NULL:
raise RuntimeError("Unable to load steering wheel art")
rl.image_resize(image, 144, 144)
texture = rl.load_texture_from_image(image)
if not texture.id:
raise RuntimeError("Unable to create steering wheel texture")
self._texture = texture
finally:
if image.data != rl.ffi.NULL:
rl.unload_image(image)
def render(self, content: rl.Rectangle, state: OnroadState) -> None:
if self._texture is None:
self.prepare()
dx, dy = offset(state.customization, "large", "steering_wheel")
x = content.x + content.width - 146 - 96 + dx
y = content.y + 45 + dy
size, _ = widget_size(state.customization, "large", "steering_wheel")
radius = size / 2
rl.draw_circle(int(x + radius), int(y + radius), radius, rl.Color(*rgba(state.customization, "cardFill", "large", "steering_wheel")))
border = rl.Color(*rgba(state.customization, "cardBorder", "large", "steering_wheel"))
if border.a:
rl.draw_ring(rl.Vector2(x + radius, y + radius), radius - 3, radius, 0, 360, 64, border)
feedback = self._feedback.update(state.wheel_acceleration, state.appearance.wheel_pedal_feedback, state.observed_ns, state.drive_frame)
color = rl.Color(*feedback, 255)
rl.draw_texture_pro(self._texture, rl.Rectangle(0, 0, 144, 144),
rl.Rectangle(x + size / 8, y + size / 8, size * .75, size * .75),
rl.Vector2(0, 0), 0, color)
def close(self) -> None:
if self._texture is not None and rl.is_window_ready():
rl.unload_texture(self._texture)
self._texture = None