mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-08-19 22:23:44 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba29a38507 | |||
| ed35a82129 | |||
| 8edce0da44 | |||
| 3c90b66b65 | |||
| 08c83149b0 | |||
| 03711a13b0 | |||
| 9f1709a7e1 | |||
| 2b576c5fce | |||
| 20ba774eaa | |||
| 59833c500a | |||
| 3d09a47a47 | |||
| 2f4744d39b | |||
| b7657f6553 | |||
| b8e14d85fb |
@@ -25,7 +25,8 @@ env:
|
||||
|
||||
jobs:
|
||||
preview:
|
||||
if: github.repository == 'sunnypilot/sunnypilot'
|
||||
if: false # tmp disable due to GH API rate limiting flakiness
|
||||
#if: github.repository == 'sunnypilot/sunnypilot'
|
||||
name: preview
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
@@ -49,9 +49,8 @@ def get_cruise_accel(e2e, v_cruise, v_ego, a_cruise_prev, angle_steers, CP, dt,
|
||||
max_accel = min(max_accel, coast_limit)
|
||||
|
||||
target_accel = np.clip(v_cruise - v_ego, A_CRUISE_MIN, max_accel)
|
||||
if not e2e:
|
||||
j_cruise = np.interp(v_ego, A_CRUISE_MAX_BP, J_CRUISE_VALS)
|
||||
target_accel = float(np.clip(target_accel, a_cruise_prev - j_cruise * dt, a_cruise_prev + j_cruise * dt))
|
||||
j_cruise = np.interp(v_ego, A_CRUISE_MAX_BP, J_CRUISE_VALS)
|
||||
target_accel = float(np.clip(target_accel, a_cruise_prev - j_cruise * dt, a_cruise_prev + j_cruise * dt))
|
||||
|
||||
return target_accel
|
||||
|
||||
@@ -65,10 +64,9 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
self.dt = dt
|
||||
self.allow_throttle = True
|
||||
|
||||
self.a_desired = init_a
|
||||
self.v_desired_filter = FirstOrderFilter(init_v, 2.0, self.dt)
|
||||
self.a_cruise = 0.0
|
||||
self.output_a_target = 0.0
|
||||
self.a_cruise = init_a
|
||||
self.output_a_target = init_a
|
||||
self.output_should_stop = False
|
||||
|
||||
self.v_desired_trajectory = np.zeros(CONTROL_N)
|
||||
@@ -105,7 +103,8 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
|
||||
if reset_state:
|
||||
self.v_desired_filter.x = v_ego
|
||||
self.a_desired = np.clip(sm['carState'].aEgo, ACCEL_MIN, ACCEL_MAX)
|
||||
self.output_a_target = np.clip(sm['carState'].aEgo, ACCEL_MIN, ACCEL_MAX)
|
||||
self.a_cruise = self.output_a_target
|
||||
|
||||
# Prevent divergence, smooth in current v_ego
|
||||
self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego))
|
||||
@@ -113,11 +112,11 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
# No change cost when user is controlling the speed, or when standstill
|
||||
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
||||
|
||||
# Get new v_cruise and a_desired from Smart Cruise Control and Speed Limit Assist
|
||||
v_cruise, self.a_desired = LongitudinalPlannerSP.update_targets(self, sm, self.v_desired_filter.x, self.a_desired, v_cruise)
|
||||
# Get new v_cruise and a_target from Smart Cruise Control and Speed Limit Assist
|
||||
v_cruise, self.output_a_target = LongitudinalPlannerSP.update_targets(self, sm, self.v_desired_filter.x, self.output_a_target, v_cruise)
|
||||
|
||||
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
|
||||
self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired)
|
||||
self.mpc.set_cur_state(self.v_desired_filter.x, self.output_a_target)
|
||||
self.mpc.update(sm['radarState'], personality=sm['selfdriveState'].personality)
|
||||
|
||||
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
|
||||
@@ -130,7 +129,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
cloudlog.info("FCW triggered")
|
||||
|
||||
# Save starting point for next iteration
|
||||
a_prev = self.a_desired
|
||||
a_prev = self.output_a_target
|
||||
|
||||
action_t = self.CP.longitudinalActuatorDelay + DT_MDL
|
||||
output_a_target_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX,
|
||||
@@ -155,7 +154,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
self.output_should_stop = any(should_stop for _, _, should_stop in candidates)
|
||||
self.output_a_target = np.clip(output_a_target, ACCEL_MIN, ACCEL_MAX)
|
||||
|
||||
self.a_desired = float(self.output_a_target)
|
||||
self.v_desired_filter.x = self.v_desired_filter.x + self.dt * (self.output_a_target + a_prev) / 2.0
|
||||
|
||||
def publish(self, sm, pm):
|
||||
|
||||
@@ -22,9 +22,9 @@ from openpilot.system.ui.widgets.toggle import ON_COLOR
|
||||
|
||||
from openpilot.sunnypilot.models.runners.constants import CUSTOM_MODEL_PATH
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction
|
||||
from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction, ScrollingButtonAction
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP, toggle_item_sp, option_item_sp
|
||||
from openpilot.system.ui.sunnypilot.widgets.progress_bar import progress_item
|
||||
from openpilot.system.ui.sunnypilot.widgets.download_status import download_status_item
|
||||
from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeNode, TreeFolder
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
@@ -35,9 +35,8 @@ class ModelsLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.model_manager = None
|
||||
self.download_status = None
|
||||
self.prev_download_status = None
|
||||
self.model_dialog = None
|
||||
self._downloading = False
|
||||
self.last_cache_calc_time = 0
|
||||
|
||||
self._initialize_items()
|
||||
@@ -52,15 +51,11 @@ class ModelsLayout(Widget):
|
||||
self.current_model_item = ListItemSP(
|
||||
title=tr("Current Model"),
|
||||
description="",
|
||||
action_item=NoElideButtonAction(tr("SELECT")),
|
||||
action_item=ScrollingButtonAction(tr("SELECT")),
|
||||
callback=self._handle_current_model_clicked
|
||||
)
|
||||
|
||||
self.supercombo_label = progress_item(tr("Driving Model"))
|
||||
self.vision_label = progress_item(tr("Vision Model"))
|
||||
self.policy_label = progress_item(tr("Policy Model"))
|
||||
self.off_policy_label = progress_item(tr("Off-Policy Model"))
|
||||
self.on_policy_label = progress_item(tr("On-Policy Model"))
|
||||
self.download_item = download_status_item(lambda: tr("Download") if self._downloading else tr("Model Status"))
|
||||
|
||||
self.refresh_item = button_item(tr("Refresh Model List"), tr("REFRESH"), "",
|
||||
lambda: (ui_state.params.put("ModelManager_LastSyncTime", 0),
|
||||
@@ -98,8 +93,7 @@ class ModelsLayout(Widget):
|
||||
1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True,
|
||||
lambda v: f"{v / 100:.2f} m")
|
||||
|
||||
self.items = [self.current_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label,
|
||||
self.policy_label, self.off_policy_label, self.on_policy_label, self.refresh_item, self.clear_cache_item,
|
||||
self.items = [self.current_model_item, self.cancel_download_item, self.download_item, self.refresh_item, self.clear_cache_item,
|
||||
self.lane_turn_desire_toggle, self.lane_turn_value_control, self.lagd_toggle, self.delay_control, self.camera_offset]
|
||||
|
||||
def _update_lagd_description(self, lagd_toggle: bool):
|
||||
@@ -135,14 +129,9 @@ class ModelsLayout(Widget):
|
||||
gui_app.push_widget(dialog)
|
||||
|
||||
def _handle_bundle_download_progress(self):
|
||||
labels = {custom.ModelManagerSP.Model.Type.supercombo: self.supercombo_label,
|
||||
custom.ModelManagerSP.Model.Type.vision: self.vision_label,
|
||||
custom.ModelManagerSP.Model.Type.policy: self.policy_label,
|
||||
custom.ModelManagerSP.Model.Type.offPolicy: self.off_policy_label,
|
||||
custom.ModelManagerSP.Model.Type.onPolicy: self.on_policy_label}
|
||||
for label in labels.values():
|
||||
label.set_visible(False)
|
||||
self.download_item.set_visible(False)
|
||||
self.cancel_download_item.set_visible(False)
|
||||
self._downloading = False
|
||||
|
||||
if not self.model_manager or (not self.model_manager.selectedBundle and not self.model_manager.activeBundle):
|
||||
return
|
||||
@@ -153,32 +142,41 @@ class ModelsLayout(Widget):
|
||||
if not bundle:
|
||||
return
|
||||
|
||||
self.download_status = bundle.status
|
||||
status_changed = self.prev_download_status != self.download_status
|
||||
self.prev_download_status = self.download_status
|
||||
|
||||
self.cancel_download_item.set_visible(bool(self.model_manager.selectedBundle) and ui_state.params.get("ModelManager_DownloadIndex") is not None)
|
||||
|
||||
if (current_time := time.monotonic()) - self.last_cache_calc_time > 0.5:
|
||||
self.last_cache_calc_time = current_time
|
||||
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
|
||||
|
||||
if self.download_status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
if bundle.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
device._reset_interactive_timeout()
|
||||
|
||||
for model in bundle.models:
|
||||
if label := labels.get(getattr(model.type, 'raw', model.type)):
|
||||
label.set_visible(True)
|
||||
p = model.artifact.downloadProgress
|
||||
text, show, color = f"pending - {bundle.displayName}", False, rl.GRAY
|
||||
if p.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
text, show = f"{int(p.progress)}% - {bundle.displayName}", True
|
||||
elif p.status in (custom.ModelManagerSP.DownloadStatus.downloaded, custom.ModelManagerSP.DownloadStatus.cached):
|
||||
status_text = tr("from cache" if p.status == custom.ModelManagerSP.DownloadStatus.cached else "downloaded")
|
||||
text, color = f"{bundle.displayName} - {status_text if status_changed else tr('ready')}", ON_COLOR
|
||||
elif p.status == custom.ModelManagerSP.DownloadStatus.failed:
|
||||
text, color = f"download failed - {bundle.displayName}", rl.RED
|
||||
label.action_item.update(p.progress, text, show, color)
|
||||
# every bundle is a single chunked artifact now
|
||||
progresses = [model.artifact.downloadProgress for model in bundle.models if model.artifact.fileName]
|
||||
if not progresses:
|
||||
return
|
||||
|
||||
self.download_item.set_visible(True)
|
||||
self.download_item.action_item.update(**self._download_row_state(progresses, bundle.internalName))
|
||||
self._downloading = self.download_item.action_item.downloading
|
||||
|
||||
@staticmethod
|
||||
def _download_row_state(progresses, name: str) -> dict:
|
||||
"""Maps a bundle's artifact progress to DownloadStatusAction.update kwargs."""
|
||||
# .raw: _DynamicEnum equals its int but does not hash like it
|
||||
statuses = {getattr(p.status, 'raw', p.status) for p in progresses}
|
||||
progress = sum(p.progress for p in progresses) / len(progresses)
|
||||
ds = custom.ModelManagerSP.DownloadStatus
|
||||
|
||||
if ds.failed in statuses:
|
||||
# close.png is authored black and a tint cannot lift it, hence close2
|
||||
return {"name": name, "status_text": tr("download failed"), "text_color": rl.RED, "icon": "icons/close2.png"}
|
||||
if ds.downloading in statuses:
|
||||
return {"name": name, "downloading": True, "progress": progress}
|
||||
if statuses <= {ds.downloaded, ds.cached}:
|
||||
return {"name": name, "text_color": ON_COLOR, "icon": "icons/checkmark.png"}
|
||||
# circled_slash is authored grey; tinting it again only darkens it
|
||||
return {"name": name, "text_color": rl.GRAY, "icon": "icons/circled_slash.png", "icon_color": rl.WHITE}
|
||||
|
||||
@staticmethod
|
||||
def _show_reset_params_dialog():
|
||||
@@ -251,7 +249,7 @@ class ModelsLayout(Widget):
|
||||
self._update_lagd_description(live_delay)
|
||||
self.model_manager = ui_state.sm["modelManagerSP"]
|
||||
self._handle_bundle_download_progress()
|
||||
active_name = self.model_manager.activeBundle.internalName if self.model_manager and self.model_manager.activeBundle.ref else f"{DEFAULT_MODEL} (Default)"
|
||||
active_name = self.model_manager.activeBundle.displayName if self.model_manager and self.model_manager.activeBundle.ref else f"{DEFAULT_MODEL} (Default)"
|
||||
self.current_model_item.action_item.set_value(active_name)
|
||||
|
||||
if not ui_state.is_offroad():
|
||||
|
||||
@@ -117,7 +117,8 @@ def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = D
|
||||
}
|
||||
|
||||
if features_buffer:
|
||||
queues['feat_q'] = Tensor(np.zeros((frame_skip * (features_buffer[1] - 1) + 1, features_buffer[0], features_buffer[2]),
|
||||
feat_q_len = frame_skip * features_buffer[1] if is_supercombo else frame_skip * (features_buffer[1] - 1) + 1
|
||||
queues['feat_q'] = Tensor(np.zeros((feat_q_len, features_buffer[0], features_buffer[2]),
|
||||
dtype=np.float32), device=device).contiguous().realize()
|
||||
|
||||
queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items() if key in ('tfm', 'big_tfm')})
|
||||
|
||||
@@ -140,8 +140,8 @@ class ModelCache:
|
||||
|
||||
class ModelFetcher:
|
||||
"""Handles fetching and caching of model data from remote source"""
|
||||
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v19.json"
|
||||
MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v19.json"
|
||||
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v20.json"
|
||||
MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v20.json"
|
||||
|
||||
def __init__(self, params: Params):
|
||||
self.params = params
|
||||
|
||||
@@ -16,6 +16,7 @@ from openpilot.common.utils import strip_deprecated_keys
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_HW
|
||||
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, usbgpu_compiled
|
||||
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
||||
from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
@@ -238,8 +239,7 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
|
||||
fan_controller = FanController(int(1./DT_HW))
|
||||
chestnut = Chestnut()
|
||||
big_model_available = os.path.isfile(os.path.join(BASEDIR, "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx")) or \
|
||||
os.path.isfile(os.path.join(BASEDIR, "openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl.chunkmanifest"))
|
||||
big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or usbgpu_compiled()
|
||||
|
||||
while not end_event.is_set():
|
||||
sm.update(PANDA_STATES_TIMEOUT)
|
||||
|
||||
@@ -4,7 +4,15 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import FontWeight
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.list_view import BUTTON_WIDTH, BUTTON_HEIGHT, TEXT_PADDING, _resolve_value
|
||||
|
||||
|
||||
class NoElideButtonAction(ButtonActionSP):
|
||||
@@ -12,6 +20,38 @@ class NoElideButtonAction(ButtonActionSP):
|
||||
return super().get_width_hint() + 1
|
||||
|
||||
|
||||
class ScrollingButtonAction(ButtonActionSP):
|
||||
"""ButtonActionSP whose value scrolls instead of eliding when it doesn't fit."""
|
||||
|
||||
def __init__(self, text: str | Callable[[], str], width: int = style.BUTTON_ACTION_WIDTH,
|
||||
enabled: bool | Callable[[], bool] = True):
|
||||
super().__init__(text=text, width=width, enabled=enabled)
|
||||
self._value_label = UnifiedLabel("", font_size=style.ITEM_TEXT_FONT_SIZE, font_weight=FontWeight.NORMAL,
|
||||
text_color=self._value_color, scroll=True,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
|
||||
def set_value(self, value: str | Callable[[], str], color: rl.Color = style.ITEM_TEXT_VALUE_COLOR):
|
||||
if self.value != _resolve_value(value, ""):
|
||||
self._value_label.reset_scroll()
|
||||
super().set_value(value, color)
|
||||
self._value_label.set_text(value)
|
||||
self._value_label.set_text_color(color)
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> bool:
|
||||
"""Duplicate of ButtonActionSP._render, with the value drawn by a scrolling label"""
|
||||
self._button.set_text(self.text)
|
||||
self._button.set_enabled(_resolve_value(self.enabled))
|
||||
button_rect = rl.Rectangle(rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT)
|
||||
self._button.render(button_rect)
|
||||
|
||||
if self.value:
|
||||
self._value_label.render(rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height))
|
||||
|
||||
pressed = self._pressed
|
||||
self._pressed = False
|
||||
return pressed
|
||||
|
||||
|
||||
class AlertFadeAnimator:
|
||||
def __init__(self, target_fps: int, duration_on: float = 0.75, rc: float = 0.05):
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.list_view import ItemAction
|
||||
|
||||
FONT_SIZE = style.ITEM_TEXT_FONT_SIZE
|
||||
ICON_SIZE = 56
|
||||
ICON_PADDING = 12
|
||||
|
||||
BAR_WIDTH = 1100
|
||||
BAR_HEIGHT = 20
|
||||
BAR_GAP = 16
|
||||
BAR_RADIUS = BAR_HEIGHT / 2
|
||||
CAPSULE_POINTS = 24
|
||||
|
||||
RAIL_COLOR = rl.Color(60, 60, 60, 255)
|
||||
FILL_COLOR = rl.Color(30, 121, 232, 255)
|
||||
# rl.WHITE is a tuple; the shimmer path reads .a off the color
|
||||
TEXT_COLOR = rl.Color(255, 255, 255, 255)
|
||||
|
||||
SWEEP_SPEED = 550.0 # px/s
|
||||
SWEEP_BAND = 240.0 # highlight half-width, px
|
||||
SWEEP_DIM = 0.65
|
||||
|
||||
|
||||
class DownloadStatusAction(ItemAction):
|
||||
"""Model download row: a name + percent over a progress rail while downloading, a name + icon otherwise."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(width=BAR_WIDTH)
|
||||
self.name = ""
|
||||
self.status_text = ""
|
||||
self.downloading = False
|
||||
self.text_color = rl.GRAY
|
||||
self.icon: str | None = None
|
||||
self.icon_color: rl.Color | None = None
|
||||
self._font = gui_app.font(FontWeight.NORMAL)
|
||||
# raw progress arrives in steps, one per 128KB chunk the manager publishes
|
||||
self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps)
|
||||
# integrated per frame; (t * speed) % span jumps whenever the fill width changes
|
||||
self._sweep = 0.0
|
||||
|
||||
self._name_label = UnifiedLabel("", font_size=FONT_SIZE, font_weight=FontWeight.NORMAL, text_color=TEXT_COLOR,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
self._percent_label = UnifiedLabel("", font_size=FONT_SIZE, font_weight=FontWeight.NORMAL, text_color=TEXT_COLOR,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
|
||||
def update(self, name, downloading=False, progress=0.0, status_text="", text_color=rl.GRAY, icon=None, icon_color=None):
|
||||
if downloading and not self.downloading:
|
||||
self._name_label.reset_shimmer()
|
||||
self._progress.x = progress
|
||||
self._sweep = 0.0
|
||||
self.name = name
|
||||
self.downloading = downloading
|
||||
self.status_text = status_text
|
||||
self.text_color = text_color
|
||||
self.icon = icon
|
||||
self.icon_color = icon_color
|
||||
self._name_label._shimmer = downloading
|
||||
if downloading:
|
||||
self._progress.update(progress)
|
||||
self._sweep += SWEEP_SPEED / gui_app.target_fps
|
||||
|
||||
@property
|
||||
def _idle_text(self) -> str:
|
||||
return f"{self.name} - {self.status_text}" if self.status_text else self.name
|
||||
|
||||
def get_width_hint(self) -> float:
|
||||
if self.downloading:
|
||||
return BAR_WIDTH
|
||||
width = measure_text_cached(self._font, self._idle_text, FONT_SIZE).x
|
||||
if self.icon:
|
||||
width += ICON_SIZE + ICON_PADDING
|
||||
return width
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if self.downloading:
|
||||
self._render_downloading(rect)
|
||||
else:
|
||||
self._render_idle(rect)
|
||||
|
||||
def _sweep_gradient(self, width: float) -> Gradient:
|
||||
# clearance at both ends keeps the wrap offscreen
|
||||
center = (self._sweep % (width + 2 * SWEEP_BAND)) - SWEEP_BAND
|
||||
|
||||
def band(x: float) -> float:
|
||||
return max(0.0, 1.0 - abs(x - center) / SWEEP_BAND)
|
||||
|
||||
# sampling the corners is exact for a piecewise linear band
|
||||
xs = sorted({0.0, width} | {min(max(center + o, 0.0), width) for o in (-SWEEP_BAND, 0.0, SWEEP_BAND)}, reverse=True)
|
||||
# the gradient axis runs right-to-left in screen space
|
||||
stops = [1.0 - x / width for x in xs]
|
||||
# alpha here is the lift over the SWEEP_DIM base, not the final opacity
|
||||
colors = [rl.Color(FILL_COLOR.r, FILL_COLOR.g, FILL_COLOR.b, int(255 * band(x))) for x in xs]
|
||||
return Gradient(start=(0.0, 0.0), end=(1.0, 0.0), colors=colors, stops=stops)
|
||||
|
||||
@staticmethod
|
||||
def _capsule(rect: rl.Rectangle) -> np.ndarray:
|
||||
"""Rounded-end ribbon so the gradient covers the caps."""
|
||||
r = rect.height / 2
|
||||
cy = rect.y + r
|
||||
top, bottom = [], []
|
||||
for i in range(CAPSULE_POINTS):
|
||||
x = rect.x + rect.width * i / (CAPSULE_POINTS - 1)
|
||||
d = min(x - rect.x, rect.x + rect.width - x, r)
|
||||
h = math.sqrt(max(r * r - (r - d) ** 2, 0.0))
|
||||
top.append((x, cy - h))
|
||||
bottom.append((x, cy + h))
|
||||
return np.array(top + bottom[::-1], dtype=np.float32)
|
||||
|
||||
def _draw_fill(self, rail: rl.Rectangle, fill_width: float):
|
||||
if fill_width <= 0:
|
||||
return
|
||||
fill = rl.Rectangle(rail.x, rail.y, fill_width, rail.height)
|
||||
rl.draw_rectangle_rounded(fill, 1.0, 10, rl.Color(FILL_COLOR.r, FILL_COLOR.g, FILL_COLOR.b, int(255 * SWEEP_DIM)))
|
||||
draw_polygon(fill, self._capsule(fill), gradient=self._sweep_gradient(fill_width))
|
||||
|
||||
def _render_downloading(self, rect: rl.Rectangle):
|
||||
percent = f"{int(self._progress.x)}%"
|
||||
text_height = measure_text_cached(self._font, percent, FONT_SIZE).y
|
||||
top = rect.y + (rect.height - (text_height + BAR_GAP + BAR_HEIGHT)) / 2
|
||||
|
||||
text_rect = rl.Rectangle(rect.x, top, rect.width, text_height)
|
||||
self._name_label.set_text(self.name)
|
||||
self._name_label.render(text_rect)
|
||||
self._percent_label.set_text(percent)
|
||||
self._percent_label.render(text_rect)
|
||||
|
||||
rail = rl.Rectangle(rect.x, top + text_height + BAR_GAP, rect.width, BAR_HEIGHT)
|
||||
rl.draw_rectangle_rounded(rail, 1.0, 10, RAIL_COLOR)
|
||||
self._draw_fill(rail, max(0.0, min(rect.width, rect.width * (self._progress.x / 100.0))))
|
||||
|
||||
def _render_idle(self, rect: rl.Rectangle):
|
||||
text = self._idle_text
|
||||
text_size = measure_text_cached(self._font, text, FONT_SIZE)
|
||||
right = rect.x + rect.width
|
||||
|
||||
if self.icon:
|
||||
texture = gui_app.texture(self.icon, ICON_SIZE, ICON_SIZE, keep_aspect_ratio=True)
|
||||
rl.draw_texture_v(texture, rl.Vector2(right - texture.width, rect.y + (rect.height - texture.height) / 2),
|
||||
self.icon_color or self.text_color)
|
||||
right -= texture.width + ICON_PADDING
|
||||
|
||||
rl.draw_text_ex(self._font, text, rl.Vector2(right - text_size.x, rect.y + (rect.height - text_size.y) / 2),
|
||||
FONT_SIZE, 0, self.text_color)
|
||||
|
||||
|
||||
def download_status_item(title):
|
||||
return ListItemSP(title=title, action_item=DownloadStatusAction(), title_color=style.ITEM_TEXT_COLOR)
|
||||
@@ -7,7 +7,7 @@ from openpilot.common.test import OpenpilotTestCase
|
||||
from openpilot.cereal import messaging, log
|
||||
from teleoprtc.tracks import VIDEO_CLOCK_RATE
|
||||
|
||||
from openpilot.system.webrtc.webrtcd import CerealOutgoingMessageProxy, CerealIncomingMessageProxy
|
||||
from openpilot.system.webrtc.webrtcd import CerealOutgoingMessageProxy, CerealIncomingMessageProxy, ServerState, handle_get_stream
|
||||
from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack
|
||||
|
||||
|
||||
@@ -80,3 +80,8 @@ class TestStreamSession(OpenpilotTestCase):
|
||||
start_pts = packet.pts
|
||||
assert abs(i + packet.pts - (start_pts + (((time.monotonic_ns() - start_ns) * VIDEO_CLOCK_RATE) // 1_000_000_000))) < 450 #5ms
|
||||
assert bytes(packet) == b""
|
||||
|
||||
def test_stream_rejects_non_json_content_type(self):
|
||||
response = self.loop.run_until_complete(handle_get_stream(ServerState(), b"{}", "text/plain"))
|
||||
|
||||
assert response == (415, b'{"error": "unsupported media type"}', "application/json; charset=utf-8")
|
||||
|
||||
@@ -395,7 +395,10 @@ def _text_response(text: str, status: int = 200) -> tuple[int, bytes, str]:
|
||||
return (status, text.encode(), "text/plain; charset=utf-8")
|
||||
|
||||
|
||||
async def handle_get_stream(state: ServerState, raw_body: bytes) -> tuple[int, bytes, str]:
|
||||
async def handle_get_stream(state: ServerState, raw_body: bytes, content_type: str) -> tuple[int, bytes, str]:
|
||||
if content_type != "application/json":
|
||||
return _json_response({"error": "unsupported media type"}, status=415)
|
||||
|
||||
stream_dict = state.streams
|
||||
body = StreamRequestBody(**json.loads(raw_body))
|
||||
|
||||
@@ -508,7 +511,7 @@ class WebrtcdHandler(BaseHTTPRequestHandler):
|
||||
services = parse_qs(parsed.query).get("services", [""])[0]
|
||||
result = self._run(handle_get_schema(self.server.state, services))
|
||||
elif parsed.path == "/stream":
|
||||
result = self._run(handle_get_stream(self.server.state, self._read_body()))
|
||||
result = self._run(handle_get_stream(self.server.state, self._read_body(), self.headers.get_content_type()))
|
||||
else: # /notify
|
||||
try:
|
||||
payload = json.loads(self._read_body())
|
||||
@@ -611,7 +614,7 @@ def webrtcd_thread(host: str, port: int):
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="WebRTC daemon")
|
||||
parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to listen on")
|
||||
parser.add_argument("--host", type=str, default="127.0.0.1", help="Host to listen on")
|
||||
parser.add_argument("--port", type=int, default=5001, help="Port to listen on")
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ dependencies = [
|
||||
"tqdm", # cars (fw_versions.py) on start + many one-off uses
|
||||
|
||||
# core
|
||||
"scons",
|
||||
"scons==4.10.1", # 4.11 removed the qt3 tool still used to build Cabana
|
||||
"pycapnp==2.1.0", # 2.2 introduces a memory leak due to cyclic references
|
||||
"numpy >=2.0",
|
||||
|
||||
|
||||
@@ -847,7 +847,7 @@ requires-dist = [
|
||||
{ name = "rednose", marker = "extra == 'submodules'", editable = "rednose_repo" },
|
||||
{ name = "requests" },
|
||||
{ name = "ruff", marker = "extra == 'testing'" },
|
||||
{ name = "scons" },
|
||||
{ name = "scons", specifier = "==4.10.1" },
|
||||
{ name = "sentry-sdk" },
|
||||
{ name = "setproctitle" },
|
||||
{ name = "sounddevice" },
|
||||
@@ -1136,11 +1136,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "scons"
|
||||
version = "4.11.0"
|
||||
version = "4.10.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dd/82/3c4e089ac8df2eaee8a7f14e489b2a76f94f4c1d8defa4e46c8ad15cae86/scons-4.11.0.tar.gz", hash = "sha256:5ba48f9e2eb6b9178cabdc9893792418e6970c84f43f4b027e4468e20616a89c", size = 3269126, upload-time = "2026-08-11T04:29:45.62Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/c9/2f430bb39e4eccba32ce8008df4a3206df651276422204e177a09e12b30b/scons-4.10.1.tar.gz", hash = "sha256:99c0e94a42a2c1182fa6859b0be697953db07ba936ecc9817ae0d218ced20b15", size = 3258403, upload-time = "2025-11-16T22:43:39.258Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/ac/a4445bbbd58a5fa6a5c8b3b0458ffbee04e4acaff87677058eab9c6af682/scons-4.11.0-py3-none-any.whl", hash = "sha256:2edc077aaeafc43377ba46ce1fa3e7b40edea59c62db9ef7e39e07dc88b754fa", size = 4123742, upload-time = "2026-08-11T04:29:42.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/bf/931fb9fbb87234c32b8b1b1c15fba23472a10777c12043336675633809a7/scons-4.10.1-py3-none-any.whl", hash = "sha256:bd9d1c52f908d874eba92a8c0c0a8dcf2ed9f3b88ab956d0fce1da479c4e7126", size = 4136069, upload-time = "2025-11-16T22:43:35.933Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1285,7 +1285,6 @@ requires-dist = [
|
||||
{ name = "pylint", marker = "extra == 'linting'" },
|
||||
{ name = "pytest", marker = "extra == 'testing-minimal'" },
|
||||
{ name = "pytest-split", marker = "extra == 'testing-minimal'" },
|
||||
{ name = "pytest-timeout", marker = "extra == 'testing-minimal'" },
|
||||
{ name = "pytest-xdist", marker = "extra == 'testing-minimal'" },
|
||||
{ name = "ruff", marker = "extra == 'linting'", specifier = "==0.14.10" },
|
||||
{ name = "safetensors", marker = "extra == 'testing-unit'" },
|
||||
@@ -1294,6 +1293,7 @@ requires-dist = [
|
||||
{ name = "tiktoken", marker = "extra == 'testing'" },
|
||||
{ name = "tinygrad", extras = ["testing-minimal"], marker = "extra == 'testing-unit'" },
|
||||
{ name = "tinygrad", extras = ["testing-unit"], marker = "extra == 'testing'" },
|
||||
{ name = "tinymesa", marker = "extra == 'mesa'", specifier = "==25.2.7.2" },
|
||||
{ name = "torch", marker = "extra == 'testing-minimal'", specifier = "==2.9.1" },
|
||||
{ name = "tqdm", marker = "extra == 'testing-unit'" },
|
||||
{ name = "transformers", marker = "extra == 'testing'" },
|
||||
@@ -1301,7 +1301,7 @@ requires-dist = [
|
||||
{ name = "typing-extensions", marker = "extra == 'linting'" },
|
||||
{ name = "z3-solver", marker = "extra == 'testing-minimal'", specifier = "<4.15.4" },
|
||||
]
|
||||
provides-extras = ["linting", "testing-minimal", "testing-unit", "testing", "docs"]
|
||||
provides-extras = ["linting", "testing-minimal", "testing-unit", "testing", "docs", "mesa"]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
|
||||
Reference in New Issue
Block a user