mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-08-22 00:23:48 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc6e823976 | |||
| cee6a6bdac | |||
| 8d9f3971b0 | |||
| 931ebf1f5a | |||
| bdda9006fd |
@@ -103,25 +103,20 @@ jobs:
|
||||
- run: |
|
||||
cd ${{ github.workspace }}/openpilot/openpilot
|
||||
if [ "${{ inputs.target_hardware }}" != "usbgpu" ]; then
|
||||
git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/dmonitoring_*.onnx"
|
||||
git lfs pull -X "selfdrive/modeld/models/big_*.onnx" -X "selfdrive/modeld/models/dmonitoring_*.onnx"
|
||||
rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx
|
||||
else
|
||||
git lfs pull -I "**/selfdrive/modeld/models/big_*.onnx" -X ""
|
||||
git lfs pull -I "selfdrive/modeld/models/big_*.onnx"
|
||||
find selfdrive/modeld/models -name "*.onnx" ! -name "big_*.onnx" -delete
|
||||
fi
|
||||
if grep -lIF "version https://git-lfs.github.com/spec/v1" selfdrive/modeld/models/*.onnx; then
|
||||
echo "::error::the ONNX files above are still LFS pointers, not real models"
|
||||
exit 1
|
||||
fi
|
||||
- name: 'Upload Artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
|
||||
path: ${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx
|
||||
if-no-files-found: error
|
||||
|
||||
build_model:
|
||||
runs-on: [self-hosted, usbgpu]
|
||||
runs-on: [self-hosted, tici]
|
||||
needs: get_model
|
||||
env:
|
||||
MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }})
|
||||
@@ -132,6 +127,7 @@ jobs:
|
||||
fetch-depth: 1
|
||||
submodules: recursive
|
||||
|
||||
- run: git lfs pull
|
||||
|
||||
- name: Set environment variables
|
||||
id: set-env
|
||||
@@ -164,7 +160,7 @@ jobs:
|
||||
fi
|
||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable
|
||||
rm -rf ${{ env.MODELS_DIR }}/*.onnx*
|
||||
rm -rf ${{ env.MODELS_DIR }}/*.onnx
|
||||
|
||||
- name: Download model artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -184,7 +180,6 @@ jobs:
|
||||
MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
|
||||
CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')")
|
||||
|
||||
TG_FLAGS_QCOM="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
||||
if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then
|
||||
echo "USBGPU build"
|
||||
export USBGPU=1
|
||||
@@ -192,40 +187,27 @@ jobs:
|
||||
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
|
||||
else
|
||||
echo "QCOM build"
|
||||
TG_FLAGS="$TG_FLAGS_QCOM"
|
||||
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
||||
OUTPUT_PKL="${{ env.MODELS_DIR }}/driving_tinygrad.pkl"
|
||||
fi
|
||||
|
||||
# Generate metadata for all ONNX files
|
||||
find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do
|
||||
echo "Generating metadata: $onnx_file"
|
||||
env ${TG_FLAGS_QCOM} python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
|
||||
env ${TG_FLAGS} python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
|
||||
done
|
||||
|
||||
# Detect model type and build compile args
|
||||
VISION_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_vision.onnx" "${{ env.MODELS_DIR }}/big_driving_vision.onnx"; do
|
||||
[ -f "$f" ] && VISION_ONNX="$f" && break
|
||||
done
|
||||
|
||||
POLICY_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_policy.onnx"; do
|
||||
[ -f "$f" ] && POLICY_ONNX="$f" && break
|
||||
done
|
||||
|
||||
OFF_POLICY_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_off_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_off_policy.onnx"; do
|
||||
[ -f "$f" ] && OFF_POLICY_ONNX="$f" && break
|
||||
done
|
||||
|
||||
ON_POLICY_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/driving_on_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_on_policy.onnx"; do
|
||||
[ -f "$f" ] && ON_POLICY_ONNX="$f" && break
|
||||
done
|
||||
|
||||
VISION_ONNX="${{ env.MODELS_DIR }}/driving_vision.onnx"
|
||||
POLICY_ONNX="${{ env.MODELS_DIR }}/driving_policy.onnx"
|
||||
OFF_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_off_policy.onnx"
|
||||
ON_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_on_policy.onnx"
|
||||
SUPERCOMBO_ONNX=""
|
||||
for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx" "${{ env.MODELS_DIR }}/big_supercombo.onnx" "${{ env.MODELS_DIR }}/big_driving_supercombo.onnx"; do
|
||||
[ -f "$f" ] && SUPERCOMBO_ONNX="$f" && break
|
||||
for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx"; do
|
||||
if [ -f "$f" ]; then
|
||||
SUPERCOMBO_ONNX="$f"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME=""
|
||||
|
||||
@@ -14,7 +14,6 @@ from openpilot.system.ui.lib.application import gui_app
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.home import MiciHomeLayoutSP as MiciHomeLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onroad import OnroadViewContainerSP as AugmentedRoadView
|
||||
|
||||
ONROAD_DELAY = 2.5 # seconds
|
||||
|
||||
@@ -73,9 +72,6 @@ class MiciMainLayout(Scroller):
|
||||
# For scroll_to
|
||||
return self._body_onroad_layout if ui_state.is_body else self._car_onroad_layout
|
||||
|
||||
def _should_auto_scroll_to_onroad(self) -> bool:
|
||||
return True
|
||||
|
||||
def _setup_callbacks(self):
|
||||
self._home_layout.set_callbacks(
|
||||
on_settings=lambda: gui_app.push_widget(self._settings_layout),
|
||||
@@ -126,15 +122,13 @@ class MiciMainLayout(Scroller):
|
||||
|
||||
# FIXME: these two pops can interrupt user interacting in the settings
|
||||
if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY:
|
||||
if not gui_app.sunnypilot_ui() or self._should_auto_scroll_to_onroad():
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
self._onroad_time_delay = None
|
||||
|
||||
# When car leaves standstill, pop nav stack and scroll to onroad
|
||||
CS = ui_state.sm["carState"]
|
||||
if not CS.standstill and self._prev_standstill:
|
||||
if not gui_app.sunnypilot_ui() or self._should_auto_scroll_to_onroad():
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
self._prev_standstill = CS.standstill
|
||||
|
||||
def _on_interactive_timeout(self):
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
"""
|
||||
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 openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroll_panel_sp import GuiScrollPanel2SP
|
||||
|
||||
|
||||
class MiciMainLayoutSP(MiciMainLayout):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
scroller = self._scroller
|
||||
scroller.scroll_panel = GuiScrollPanel2SP(scroller._horizontal, handle_out_of_bounds=not scroller._snap_items)
|
||||
|
||||
def _should_auto_scroll_to_onroad(self) -> bool:
|
||||
return not self._onroad_layout.is_on_info_panel()
|
||||
@@ -4,11 +4,13 @@ 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.cereal import custom
|
||||
from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
@@ -17,6 +19,24 @@ from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
|
||||
def _build_folders() -> dict[str, list]:
|
||||
manager = ui_state.sm["modelManagerSP"]
|
||||
bundles = manager.availableBundles
|
||||
folders = {}
|
||||
for bundle in bundles:
|
||||
folder = next((override.value for override in bundle.overrides if override.key == "folder"), "")
|
||||
folders.setdefault(folder, []).append(bundle)
|
||||
|
||||
favs = ui_state.params.get("ModelManager_Favs")
|
||||
favorites = set(favs.split(';')) if favs else set()
|
||||
|
||||
if favorites:
|
||||
for fav_bundle in [bundle for bundle in bundles if bundle.ref in favorites]:
|
||||
folders.setdefault("favorites", []).append(fav_bundle)
|
||||
|
||||
return folders
|
||||
|
||||
|
||||
class CurrentModelInfo(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -46,6 +66,41 @@ class CurrentModelInfo(Widget):
|
||||
self.info_text.set_position(self._rect.x + 20, self._rect.y + 161 - 25)
|
||||
self.info_text.render()
|
||||
|
||||
|
||||
class FolderSelectionMici(NavScroller):
|
||||
|
||||
def __init__(self, folder_name: str | None = None,
|
||||
select_default_callback: Callable | None = None,
|
||||
select_folder_callback: Callable | None = None,
|
||||
select_model_callback: Callable | None = None):
|
||||
super().__init__()
|
||||
|
||||
folders = _build_folders()
|
||||
|
||||
btns = []
|
||||
if folder_name is None:
|
||||
assert select_default_callback is not None and select_folder_callback is not None
|
||||
default_btn = BigButton(f"{DEFAULT_MODEL} (Default)".lower())
|
||||
default_btn.set_click_callback(select_default_callback)
|
||||
btns.append(default_btn)
|
||||
|
||||
for folder in sorted(folders.keys(), key=lambda f: max((bundle.index for bundle in folders[f]), default=-1), reverse=True):
|
||||
btn = BigButton(folder.lower())
|
||||
btn.set_click_callback(lambda f=folder: select_folder_callback(f))
|
||||
if folder.lower() == "favorites":
|
||||
btns.insert(0, btn)
|
||||
else:
|
||||
btns.append(btn)
|
||||
else:
|
||||
assert select_model_callback is not None
|
||||
for bundle in sorted(folders.get(folder_name, []), key=lambda b: b.index, reverse=True):
|
||||
btn = BigButton(bundle.displayName.lower())
|
||||
btn.set_click_callback(lambda b=bundle: select_model_callback(b))
|
||||
btns.append(btn)
|
||||
|
||||
self._scroller.add_widgets(btns)
|
||||
|
||||
|
||||
class ModelsLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -59,81 +114,47 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.select_model_btn = BigButton(tr("select model"))
|
||||
self.select_model_btn.set_click_callback(self._show_folders)
|
||||
|
||||
self.clear_cache_btn = BigButton(tr("clear cache"), "")
|
||||
self.clear_cache_btn.set_click_callback(self._clear_cache)
|
||||
|
||||
self.cancel_download_btn = BigButton(tr("cancel download"))
|
||||
self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadIndex"))
|
||||
|
||||
self.main_items = [self.current_model_info, self.select_model_btn, self.cancel_download_btn]
|
||||
self.main_items = [self.current_model_info, self.select_model_btn, self.clear_cache_btn, self.cancel_download_btn]
|
||||
self._scroller.add_widgets(self.main_items)
|
||||
|
||||
@property
|
||||
def model_manager(self):
|
||||
return ui_state.sm["modelManagerSP"]
|
||||
|
||||
def _get_grouped_bundles(self, favorites = None):
|
||||
bundles = self.model_manager.availableBundles
|
||||
folders = {}
|
||||
for bundle in bundles:
|
||||
folder = next((override.value for override in bundle.overrides if override.key == "folder"), "")
|
||||
folders.setdefault(folder, []).append(bundle)
|
||||
|
||||
if favorites:
|
||||
for fav_bundle in [bundle for bundle in bundles if bundle.ref in favorites]:
|
||||
folders.setdefault("favorites", []).append(fav_bundle)
|
||||
|
||||
return folders
|
||||
|
||||
def _push_selection_view(self, items):
|
||||
scroller = NavScroller()
|
||||
scroller._scroller.add_widgets(items)
|
||||
gui_app.push_widget(scroller)
|
||||
|
||||
def _show_folders(self):
|
||||
self.focused_widget = self.select_model_btn
|
||||
|
||||
favs = ui_state.params.get("ModelManager_Favs")
|
||||
favorites = set(favs.split(';')) if favs else set()
|
||||
def select_default():
|
||||
ui_state.params.remove("ModelManager_ActiveBundle")
|
||||
gui_app.pop_widgets_to(self, instant=True)
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
self._scroller.scroll_to(0)
|
||||
|
||||
folders = self._get_grouped_bundles(favorites)
|
||||
folder_buttons = []
|
||||
default_btn = BigButton(f"{DEFAULT_MODEL} (Default)".lower())
|
||||
default_btn.set_click_callback(self._select_default)
|
||||
folder_buttons.append(default_btn)
|
||||
def select_model(bundle):
|
||||
ui_state.params.put("ModelManager_DownloadIndex", bundle.index)
|
||||
gui_app.pop_widgets_to(self, instant=True)
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
self._scroller.scroll_to(0)
|
||||
|
||||
for folder in sorted(folders.keys(), key=lambda f: max((bundle.index for bundle in folders[f]), default=-1), reverse=True):
|
||||
if folder.lower() in ["release models", "master models", "favorites"]:
|
||||
btn = BigButton(folder.lower())
|
||||
btn.set_click_callback(lambda f=folder: self._select_folder(f))
|
||||
if folder.lower() == "favorites":
|
||||
folder_buttons.insert(0, btn)
|
||||
else:
|
||||
folder_buttons.append(btn)
|
||||
self._push_selection_view(folder_buttons)
|
||||
def select_folder(folder_name):
|
||||
gui_app.push_widget(FolderSelectionMici(folder_name, select_model_callback=select_model))
|
||||
|
||||
def _pop_to_main(self):
|
||||
gui_app.pop_widgets_to(self)
|
||||
gui_app.push_widget(FolderSelectionMici(select_default_callback=select_default, select_folder_callback=select_folder))
|
||||
|
||||
def _select_model(self, bundle):
|
||||
ui_state.params.put("ModelManager_DownloadIndex", bundle.index)
|
||||
self._pop_to_main()
|
||||
def _clear_cache(self):
|
||||
def confirm_callback():
|
||||
ui_state.params.put_bool("ModelManager_ClearCache", True)
|
||||
|
||||
def _select_default(self):
|
||||
ui_state.params.remove("ModelManager_ActiveBundle")
|
||||
self._pop_to_main()
|
||||
|
||||
def _select_folder(self, folder_name):
|
||||
favs = ui_state.params.get("ModelManager_Favs")
|
||||
favorites = set(favs.split(';')) if favs else set()
|
||||
|
||||
folders = self._get_grouped_bundles(favorites)
|
||||
bundles = sorted(folders.get(folder_name, []), key=lambda b: b.index, reverse=True)
|
||||
|
||||
btns = []
|
||||
for bundle in bundles:
|
||||
txt = bundle.displayName.lower()
|
||||
btn = BigButton(txt)
|
||||
btn.set_click_callback(lambda b=bundle: self._select_model(b))
|
||||
btns.append(btn)
|
||||
self._push_selection_view(btns)
|
||||
lbl = tr("slide to clear cache")
|
||||
icon = gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64)
|
||||
dlg = BigConfirmationDialog(lbl, icon, confirm_callback=confirm_callback, red=True)
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
@@ -145,6 +166,7 @@ class ModelsLayoutMici(NavScroller):
|
||||
super()._update_state()
|
||||
|
||||
self.select_model_btn.set_enabled(ui_state.is_offroad())
|
||||
self.clear_cache_btn.set_enabled(ui_state.is_offroad())
|
||||
self.cancel_download_btn.set_visible(False)
|
||||
self.current_model_info.current_model_header._shimmer = False
|
||||
self.current_model_info.info_header._shimmer = False
|
||||
@@ -191,4 +213,3 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.current_model_info.info_header.set_text(tr("progress") + self._download_progress)
|
||||
self.current_model_info.info_header._shimmer = True
|
||||
self.current_model_info.info_text.set_text(f"{progress/count:.2f}%")
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
"""
|
||||
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 gui_app
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroller_sp import ScrollerSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.augmented_road_view import AugmentedRoadViewSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onroad_info_panel import OnroadInfoPanel
|
||||
|
||||
CONFIDENCE_BALL_VISIBLE_RATIO = 0.4
|
||||
HORIZONTAL_SETTLE_PX = 5
|
||||
HORIZONTAL_RESET_RATIO = 0.5
|
||||
|
||||
|
||||
class OnroadViewContainerSP(ScrollerSP):
|
||||
def __init__(self, bookmark_callback=None):
|
||||
super().__init__(horizontal=False, snap_items=True, spacing=0, pad=0, scroll_indicator=False, edge_shadows=False)
|
||||
self.road_view = AugmentedRoadViewSP(bookmark_callback=bookmark_callback)
|
||||
self.onroad_info_panel = OnroadInfoPanel(bookmark_callback=bookmark_callback)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
self.road_view,
|
||||
self.onroad_info_panel,
|
||||
])
|
||||
self._scroller.set_reset_scroll_at_show(False)
|
||||
self._scroller.set_scrolling_enabled(lambda: abs(self.rect.x) < HORIZONTAL_SETTLE_PX)
|
||||
|
||||
for child in (self.road_view, self.onroad_info_panel):
|
||||
inner_touch_valid = child._touch_valid_callback
|
||||
child.set_touch_valid_callback(
|
||||
lambda inner=inner_touch_valid: self._touch_valid() and (inner() if inner else True)
|
||||
)
|
||||
|
||||
def set_rect(self, rect: rl.Rectangle):
|
||||
super().set_rect(rect)
|
||||
self.road_view.set_rect(rect)
|
||||
self.onroad_info_panel.set_rect(rect)
|
||||
return self
|
||||
|
||||
def is_swiping_left(self) -> bool:
|
||||
return self.road_view.is_swiping_left() or self.onroad_info_panel.is_swiping_left()
|
||||
|
||||
def set_click_callback(self, click_callback: Callable[[], None] | None) -> None:
|
||||
self.road_view.set_click_callback(click_callback)
|
||||
self.onroad_info_panel.set_click_callback(click_callback)
|
||||
|
||||
def is_on_info_panel(self) -> bool:
|
||||
"""True when scrolled past halfway toward onroad_info_panel (used by main layout
|
||||
to skip auto-pop-back-to-camera while user is reading the info panel)."""
|
||||
return abs(self._scroller.scroll_panel.get_offset()) > self._rect.height / 2
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if abs(self.rect.x) > gui_app.width * HORIZONTAL_RESET_RATIO:
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
|
||||
vertical_offset = self._scroller.scroll_panel.get_offset()
|
||||
show_ball = abs(vertical_offset) < rect.height * CONFIDENCE_BALL_VISIBLE_RATIO
|
||||
self.road_view.set_show_confidence_ball(show_ball)
|
||||
|
||||
super()._render(rect)
|
||||
@@ -1,403 +0,0 @@
|
||||
"""
|
||||
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 pyray as rl
|
||||
from dataclasses import dataclass, field
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.mici.onroad.alert_renderer import AlertRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import BookmarkIcon
|
||||
|
||||
METER_TO_KM = 0.001
|
||||
METER_TO_MILE = 0.000621371
|
||||
|
||||
CONTENT_MARGIN = 16
|
||||
SPEED_LIMIT_SIGN_WIDTH = 146
|
||||
VIENNA_SIGN_SIZE = 146
|
||||
MUTCD_SIGN_HEIGHT = 178
|
||||
OFFSET_BADGE_SIZE = 50
|
||||
OFFSET_BADGE_PANEL_PADDING = 4
|
||||
MUTCD_OFFSET_SIGN_Y_SHIFT = 6
|
||||
VIENNA_BADGE_X_RATIO = 0.80
|
||||
VIENNA_BADGE_UPCOMING_X_RATIO = 0.70
|
||||
VIENNA_BADGE_Y_RATIO = -0.82
|
||||
UPCOMING_SIGN_SIZE_RATIO = 0.76
|
||||
UPCOMING_SIGN_OVERLAP_RATIO = 0.05
|
||||
UNIT_FONT_SIZE = 40
|
||||
SPEED_FONT_SIZE = 114
|
||||
ROAD_FONT_SIZE = 32
|
||||
SCC_TAG_WIDTH = 78
|
||||
SCC_TAG_HEIGHT = 30
|
||||
SCC_TAG_GAP = 5
|
||||
COLUMN_GAP = 12
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OnroadInfoPanelColors:
|
||||
white: rl.Color = rl.WHITE
|
||||
black: rl.Color = rl.BLACK
|
||||
red: rl.Color = field(default_factory=lambda: rl.Color(255, 0, 0, 255))
|
||||
green: rl.Color = field(default_factory=lambda: rl.Color(0, 255, 0, 255))
|
||||
grey: rl.Color = field(default_factory=lambda: rl.Color(190, 195, 190, 255))
|
||||
light_grey: rl.Color = field(default_factory=lambda: rl.Color(200, 200, 200, 255))
|
||||
dark_grey: rl.Color = field(default_factory=lambda: rl.Color(100, 100, 100, 255))
|
||||
bg_dark: rl.Color = field(default_factory=lambda: rl.Color(0, 0, 0, 255))
|
||||
card_bg: rl.Color = field(default_factory=lambda: rl.Color(50, 50, 50, 200))
|
||||
badge_bg: rl.Color = field(default_factory=lambda: rl.Color(60, 60, 60, 255))
|
||||
|
||||
|
||||
COLORS = OnroadInfoPanelColors()
|
||||
|
||||
|
||||
class OnroadInfoPanel(Widget):
|
||||
def __init__(self, bookmark_callback=None):
|
||||
super().__init__()
|
||||
self.speed_limit: float = 0.0
|
||||
self.speed_limit_valid: bool = False
|
||||
self.speed_limit_offset: float = 0.0
|
||||
self.next_speed_limit: float = 0.0
|
||||
self.next_speed_limit_distance: float = 0.0
|
||||
self.road_name: str = ""
|
||||
self.current_speed: float = 0.0
|
||||
self.set_speed: float = 0.0
|
||||
self.cruise_enabled: bool = False
|
||||
|
||||
self._sign_slide: float = 0.0
|
||||
|
||||
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
self._marquee_offset: float = 0.0
|
||||
self._marquee_direction: int = 1
|
||||
self._marquee_pause_timer: float = 0.0
|
||||
self._marquee_speed: float = 40.0
|
||||
self._marquee_pause_duration: float = 1.5
|
||||
|
||||
self._alert_renderer = AlertRenderer()
|
||||
self._alert_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
|
||||
|
||||
self._bookmark_icon = BookmarkIcon(bookmark_callback)
|
||||
|
||||
def is_swiping_left(self) -> bool:
|
||||
return self._bookmark_icon.is_swiping_left()
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos) -> None:
|
||||
# Mirror stock AugmentedRoadView: suppress click while bookmark gesture active
|
||||
if not self._bookmark_icon.interacting():
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
def _update_state(self) -> None:
|
||||
sm = ui_state.sm
|
||||
speed_conv = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
|
||||
if sm.valid["longitudinalPlanSP"]:
|
||||
lp_sp = sm["longitudinalPlanSP"]
|
||||
resolver = lp_sp.speedLimit.resolver
|
||||
self.speed_limit = resolver.speedLimit * speed_conv
|
||||
self.speed_limit_valid = resolver.speedLimitValid
|
||||
self.speed_limit_offset = resolver.speedLimitOffset * speed_conv
|
||||
|
||||
if sm.valid["liveMapDataSP"]:
|
||||
lmd = sm["liveMapDataSP"]
|
||||
self.next_speed_limit = lmd.speedLimitAhead * speed_conv
|
||||
self.next_speed_limit_distance = lmd.speedLimitAheadDistance
|
||||
self.road_name = lmd.roadName
|
||||
|
||||
if sm.updated["carState"]:
|
||||
self.current_speed = sm["carState"].vEgo * speed_conv
|
||||
|
||||
if sm.valid["carState"] and sm.valid["controlsState"]:
|
||||
self.cruise_enabled = sm["carState"].cruiseState.enabled
|
||||
v_cruise_cluster = sm["carState"].vCruiseCluster
|
||||
set_speed_kph = sm["controlsState"].vCruiseDEPRECATED if v_cruise_cluster == 0.0 else v_cruise_cluster
|
||||
self.set_speed = set_speed_kph * (METER_TO_MILE / METER_TO_KM) if not ui_state.is_metric else set_speed_kph
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
self._update_state()
|
||||
|
||||
rl.draw_rectangle(int(rect.x), int(rect.y), int(rect.width), int(rect.height), COLORS.bg_dark)
|
||||
|
||||
left_x = rect.x + CONTENT_MARGIN
|
||||
|
||||
if self.cruise_enabled:
|
||||
unit = tr("MAX")
|
||||
display_speed = self.set_speed
|
||||
else:
|
||||
unit = tr("km/h") if ui_state.is_metric else tr("MPH")
|
||||
display_speed = self.current_speed
|
||||
|
||||
display_speed_text = str(round(display_speed))
|
||||
if self.speed_limit_valid and display_speed > self.speed_limit:
|
||||
speed_color = COLORS.red
|
||||
else:
|
||||
speed_color = COLORS.white
|
||||
|
||||
sign_width = min(SPEED_LIMIT_SIGN_WIDTH, rect.width * 0.30)
|
||||
sign_height = VIENNA_SIGN_SIZE if ui_state.is_metric else MUTCD_SIGN_HEIGHT
|
||||
|
||||
has_upcoming_limit = self.next_speed_limit > 0 and self.next_speed_limit != self.speed_limit
|
||||
target_sign_slide = 1.0 if has_upcoming_limit else 0.0
|
||||
slide_speed = 3.0 * rl.get_frame_time()
|
||||
if self._sign_slide < target_sign_slide:
|
||||
self._sign_slide = min(self._sign_slide + slide_speed, target_sign_slide)
|
||||
elif self._sign_slide > target_sign_slide:
|
||||
self._sign_slide = max(self._sign_slide - slide_speed, target_sign_slide)
|
||||
|
||||
upcoming_width = int(sign_width * UPCOMING_SIGN_SIZE_RATIO)
|
||||
upcoming_height = int(sign_height * UPCOMING_SIGN_SIZE_RATIO)
|
||||
upcoming_reserved_width = int(upcoming_width * 0.85) + 5
|
||||
sign_x_without_upcoming = rect.x + rect.width - sign_width - CONTENT_MARGIN
|
||||
sign_x_with_upcoming = rect.x + rect.width - sign_width - CONTENT_MARGIN - upcoming_reserved_width
|
||||
sign_x = sign_x_without_upcoming + (sign_x_with_upcoming - sign_x_without_upcoming) * self._sign_slide
|
||||
sign_y = rect.y + (rect.height - sign_height) / 2
|
||||
if not ui_state.is_metric and self.speed_limit_offset != 0 and self.speed_limit_valid:
|
||||
sign_y += MUTCD_OFFSET_SIGN_Y_SHIFT
|
||||
|
||||
readout_right = sign_x - COLUMN_GAP
|
||||
readout_width = max(1, readout_right - left_x)
|
||||
road_y = rect.y + rect.height - 44
|
||||
|
||||
unit_font_size = self._fit_font_size(self._font_semi_bold, unit, readout_width, 46, UNIT_FONT_SIZE, 28)
|
||||
speed_font_size = self._fit_font_size(self._font_bold, display_speed_text, readout_width, road_y - (rect.y + 54) - 8,
|
||||
SPEED_FONT_SIZE, 76)
|
||||
speed_size = measure_text_cached(self._font_bold, display_speed_text, speed_font_size)
|
||||
speed_y = min(rect.y + 54, road_y - speed_size.y - 8)
|
||||
unit_y = max(rect.y + 14, speed_y - unit_font_size - 6)
|
||||
|
||||
rl.draw_text_ex(self._font_semi_bold, unit, rl.Vector2(left_x, unit_y), unit_font_size, 0, COLORS.grey)
|
||||
rl.draw_text_ex(self._font_bold, display_speed_text, rl.Vector2(left_x, speed_y), speed_font_size, 0, speed_color)
|
||||
self._draw_road_name(left_x, road_y, readout_width)
|
||||
|
||||
if has_upcoming_limit and self._sign_slide > 0.01:
|
||||
upcoming_speed_text = str(round(self.next_speed_limit))
|
||||
distance_text = self._format_distance(self.next_speed_limit_distance)
|
||||
upcoming_x = sign_x + sign_width - int(upcoming_width * UPCOMING_SIGN_OVERLAP_RATIO)
|
||||
upcoming_y = sign_y + (sign_height - upcoming_height) / 2
|
||||
|
||||
upcoming_speed_color = COLORS.black
|
||||
if ui_state.is_metric:
|
||||
self._draw_vienna_sign(upcoming_x, upcoming_y, upcoming_width, upcoming_height, upcoming_speed_text, upcoming_speed_color, is_upcoming=True)
|
||||
else:
|
||||
self._draw_mutcd_sign(upcoming_x, upcoming_y, upcoming_width, upcoming_height, upcoming_speed_text, upcoming_speed_color, is_upcoming=True)
|
||||
|
||||
distance_font_size = self._fit_font_size(self._font_medium, distance_text, upcoming_width, 30, 24, 16)
|
||||
distance_size = measure_text_cached(self._font_medium, distance_text, distance_font_size)
|
||||
rl.draw_text_ex(self._font_medium, distance_text, rl.Vector2(upcoming_x + upcoming_width / 2 - distance_size.x / 2, upcoming_y + upcoming_height),
|
||||
distance_font_size, 0, COLORS.grey)
|
||||
|
||||
self._draw_speed_limit_sign(sign_x, sign_y, sign_width, sign_height)
|
||||
|
||||
if self.speed_limit_offset != 0 and self.speed_limit_valid:
|
||||
offset_text = str(abs(round(self.speed_limit_offset)))
|
||||
badge_size = OFFSET_BADGE_SIZE
|
||||
badge_rect = self._offset_badge_rect(rect, sign_x, sign_y, sign_width, sign_height, badge_size, has_upcoming_limit)
|
||||
|
||||
if ui_state.is_metric:
|
||||
badge_radius = badge_size / 2
|
||||
badge_center_x = badge_rect.x + badge_radius
|
||||
badge_center_y = badge_rect.y + badge_radius
|
||||
rl.draw_circle(int(badge_center_x), int(badge_center_y), badge_radius + 2, COLORS.dark_grey)
|
||||
rl.draw_circle(int(badge_center_x), int(badge_center_y), badge_radius, COLORS.badge_bg)
|
||||
self._draw_text_centered_fit(self._font_bold, offset_text, 32, rl.Vector2(badge_center_x, badge_center_y), COLORS.white,
|
||||
badge_size - 10, badge_size - 8, min_size=24)
|
||||
else:
|
||||
rl.draw_rectangle_rounded(badge_rect, 0.25, 10, COLORS.badge_bg)
|
||||
rl.draw_rectangle_rounded_lines_ex(badge_rect, 0.25, 10, 2, COLORS.dark_grey)
|
||||
self._draw_text_centered_fit(self._font_bold, offset_text, 32, rl.Vector2(badge_rect.x + badge_size / 2, badge_rect.y + badge_size / 2),
|
||||
COLORS.white, badge_size - 10, badge_size - 8, min_size=24)
|
||||
|
||||
scc_tag_x = min(left_x + speed_size.x + COLUMN_GAP, readout_right - SCC_TAG_WIDTH)
|
||||
scc_tag_y = speed_y + (speed_size.y - (SCC_TAG_HEIGHT * 2 + SCC_TAG_GAP)) / 2
|
||||
if scc_tag_x >= left_x + speed_size.x + 8:
|
||||
self._draw_scc_icons(scc_tag_x, scc_tag_y, readout_right)
|
||||
|
||||
self._bookmark_icon.render(rect)
|
||||
|
||||
if ui_state.started:
|
||||
alert_obj, no_alert = self._alert_renderer.will_render()
|
||||
self._alert_alpha_filter.update(0 if no_alert else 1)
|
||||
alpha = self._alert_alpha_filter.x
|
||||
if alpha > 0.01:
|
||||
rl.draw_rectangle(int(rect.x), int(rect.y), int(rect.width), int(rect.height), rl.Color(0, 0, 0, int(150 * alpha)))
|
||||
self._alert_renderer.render(rect)
|
||||
|
||||
def _draw_scc_icons(self, x: float, y: float, right_limit: float) -> None:
|
||||
sm = ui_state.sm
|
||||
if not sm.valid["longitudinalPlanSP"]:
|
||||
return
|
||||
scc = sm["longitudinalPlanSP"].smartCruiseControl
|
||||
|
||||
drawn = 0
|
||||
|
||||
for label, active in [("SCC-V", scc.vision.active), ("SCC-M", scc.map.active)]:
|
||||
if not active:
|
||||
continue
|
||||
tag_x = x
|
||||
if tag_x + SCC_TAG_WIDTH > right_limit:
|
||||
return
|
||||
tag_y = y + drawn * (SCC_TAG_HEIGHT + SCC_TAG_GAP)
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(tag_x, tag_y, SCC_TAG_WIDTH, SCC_TAG_HEIGHT), 0.3, 10, COLORS.green)
|
||||
self._draw_text_centered_fit(self._font_bold, label, 18, rl.Vector2(tag_x + SCC_TAG_WIDTH / 2, tag_y + SCC_TAG_HEIGHT / 2), COLORS.black,
|
||||
SCC_TAG_WIDTH - 10, SCC_TAG_HEIGHT - 4, min_size=14)
|
||||
drawn += 1
|
||||
|
||||
def _draw_speed_limit_sign(self, x: float, y: float, sign_width: float, sign_height: float) -> None:
|
||||
speed_str = str(round(self.speed_limit)) if self.speed_limit_valid and self.speed_limit > 0 else "--"
|
||||
speed_color = COLORS.black if not self.speed_limit_valid or self.current_speed <= self.speed_limit else COLORS.red
|
||||
|
||||
if ui_state.is_metric:
|
||||
self._draw_vienna_sign(x, y, sign_width, sign_height, speed_str, speed_color, is_upcoming=False)
|
||||
else:
|
||||
self._draw_mutcd_sign(x, y, sign_width, sign_height, speed_str, speed_color, is_upcoming=False)
|
||||
|
||||
def _draw_road_name(self, x: float, y: float, width: float) -> None:
|
||||
if width <= 0:
|
||||
return
|
||||
|
||||
road_display = self.road_name if self.road_name else "--"
|
||||
font_size = self._fit_font_size(self._font_semi_bold, road_display, width, 38, ROAD_FONT_SIZE, 28)
|
||||
road_size = measure_text_cached(self._font_semi_bold, road_display, font_size)
|
||||
text_width = road_size.x
|
||||
|
||||
if text_width <= width:
|
||||
self._marquee_offset = 0.0
|
||||
self._marquee_direction = 1
|
||||
self._marquee_pause_timer = 0.0
|
||||
rl.draw_text_ex(self._font_semi_bold, road_display, rl.Vector2(x, y), font_size, 0, COLORS.white)
|
||||
else:
|
||||
overflow = text_width - width
|
||||
dt = rl.get_frame_time()
|
||||
|
||||
if self._marquee_pause_timer > 0:
|
||||
self._marquee_pause_timer -= dt
|
||||
else:
|
||||
self._marquee_offset += self._marquee_direction * self._marquee_speed * dt
|
||||
|
||||
if self._marquee_offset >= overflow:
|
||||
self._marquee_offset = overflow
|
||||
self._marquee_direction = -1
|
||||
self._marquee_pause_timer = self._marquee_pause_duration
|
||||
elif self._marquee_offset <= 0:
|
||||
self._marquee_offset = 0
|
||||
self._marquee_direction = 1
|
||||
self._marquee_pause_timer = self._marquee_pause_duration
|
||||
|
||||
rl.begin_scissor_mode(int(x), int(y), int(width), int(road_size.y + 4))
|
||||
text_pos = rl.Vector2(x - self._marquee_offset, y)
|
||||
rl.draw_text_ex(self._font_semi_bold, road_display, text_pos, font_size, 0, COLORS.white)
|
||||
rl.end_scissor_mode()
|
||||
|
||||
def _draw_vienna_sign(self, x: float, y: float, width: float, height: float, speed_str: str, speed_color: rl.Color, is_upcoming: bool = False) -> None:
|
||||
center = rl.Vector2(x + width / 2, y + height / 2)
|
||||
outer_radius = min(width, height) / 2
|
||||
|
||||
rl.draw_circle_v(center, outer_radius, COLORS.white)
|
||||
ring_width = outer_radius * 0.18
|
||||
rl.draw_ring(center, outer_radius - ring_width, outer_radius, 0, 360, 36, COLORS.red)
|
||||
|
||||
font_size = outer_radius * (0.7 if len(speed_str) >= 3 else 0.9)
|
||||
self._draw_text_centered_fit(self._font_bold, speed_str, int(font_size), center, speed_color, width * 0.72, height * 0.50, min_size=24)
|
||||
|
||||
def _draw_mutcd_sign(self, x: float, y: float, width: float, height: float, speed_str: str, speed_color: rl.Color, is_upcoming: bool = False) -> None:
|
||||
sign_rect = rl.Rectangle(x, y, width, height)
|
||||
rl.draw_rectangle_rounded(sign_rect, 0.35, 10, COLORS.white)
|
||||
|
||||
inset = max(4, width * 0.05)
|
||||
inner_rect = rl.Rectangle(x + inset, y + inset, width - inset * 2, height - inset * 2)
|
||||
outer_radius = 0.35 * width / 2.0
|
||||
inner_radius = outer_radius - inset
|
||||
inner_roundness = inner_radius / (inner_rect.width / 2.0)
|
||||
rl.draw_rectangle_rounded_lines_ex(inner_rect, inner_roundness, 10, 3, COLORS.black)
|
||||
|
||||
mid_x = x + width / 2
|
||||
label_size = max(18, int(width * 0.26))
|
||||
if is_upcoming:
|
||||
self._draw_text_centered_fit(self._font_bold, tr("AHEAD"), int(width * 0.34), rl.Vector2(mid_x, y + height * 0.28), COLORS.black,
|
||||
width * 0.94, height * 0.32, min_size=20)
|
||||
else:
|
||||
self._draw_text_centered_fit(self._font_bold, tr("SPEED"), label_size, rl.Vector2(mid_x, y + height * 0.20), COLORS.black,
|
||||
width * 0.84, height * 0.24, min_size=16)
|
||||
self._draw_text_centered_fit(self._font_bold, tr("LIMIT"), label_size, rl.Vector2(mid_x, y + height * 0.40), COLORS.black,
|
||||
width * 0.84, height * 0.24, min_size=16)
|
||||
|
||||
speed_font_size = int(width * 0.60) if len(speed_str) >= 3 else int(width * 0.72)
|
||||
self._draw_text_centered_fit(self._font_bold, speed_str, speed_font_size, rl.Vector2(mid_x, y + height * 0.72), speed_color,
|
||||
width * 0.90, height * 0.52, min_size=32)
|
||||
|
||||
def _draw_text_centered(self, font, text, size, pos_center, color):
|
||||
sz = measure_text_cached(font, text, size)
|
||||
rl.draw_text_ex(font, text, rl.Vector2(pos_center.x - sz.x / 2, pos_center.y - sz.y / 2), size, 0, color)
|
||||
|
||||
def _draw_text_centered_fit(self, font, text, size, pos_center, color, max_width: float, max_height: float, min_size: int = 10):
|
||||
size = self._fit_font_size(font, text, max_width, max_height, size, min_size)
|
||||
self._draw_text_centered(font, text, size, pos_center, color)
|
||||
|
||||
def _fit_font_size(self, font, text: str, max_width: float, max_height: float, max_size: int | float, min_size: int) -> int:
|
||||
size = int(max_size)
|
||||
while size > min_size:
|
||||
text_size = measure_text_cached(font, text, size)
|
||||
if text_size.x <= max_width and text_size.y <= max_height:
|
||||
return size
|
||||
size -= 2
|
||||
return min_size
|
||||
|
||||
def _offset_badge_rect(self, panel_rect: rl.Rectangle, sign_x: float, sign_y: float, sign_width: float, sign_height: float,
|
||||
badge_size: float, has_upcoming_limit: bool) -> rl.Rectangle:
|
||||
if ui_state.is_metric:
|
||||
radius = min(sign_width, sign_height) / 2
|
||||
center_x = sign_x + sign_width / 2
|
||||
center_y = sign_y + sign_height / 2
|
||||
badge_x_ratio = VIENNA_BADGE_UPCOMING_X_RATIO if has_upcoming_limit else VIENNA_BADGE_X_RATIO
|
||||
badge_center_x = center_x + radius * badge_x_ratio
|
||||
badge_center_y = center_y + radius * VIENNA_BADGE_Y_RATIO
|
||||
badge_x = badge_center_x - badge_size / 2
|
||||
badge_y = badge_center_y - badge_size / 2
|
||||
else:
|
||||
badge_x = sign_x + sign_width - badge_size * 0.45
|
||||
badge_y = sign_y - badge_size * 0.75
|
||||
|
||||
return rl.Rectangle(
|
||||
self._clamp(
|
||||
badge_x,
|
||||
panel_rect.x + OFFSET_BADGE_PANEL_PADDING,
|
||||
panel_rect.x + panel_rect.width - badge_size - OFFSET_BADGE_PANEL_PADDING,
|
||||
),
|
||||
self._clamp(
|
||||
badge_y,
|
||||
panel_rect.y + OFFSET_BADGE_PANEL_PADDING,
|
||||
panel_rect.y + panel_rect.height - badge_size - OFFSET_BADGE_PANEL_PADDING,
|
||||
),
|
||||
badge_size,
|
||||
badge_size,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _clamp(value: float, min_value: float, max_value: float) -> float:
|
||||
return max(min_value, min(max_value, value))
|
||||
|
||||
def _format_distance(self, distance: float) -> str:
|
||||
if ui_state.is_metric:
|
||||
if distance < 50:
|
||||
return tr("Near")
|
||||
if distance >= 1000:
|
||||
return f"{distance * METER_TO_KM:.1f}" + tr("km")
|
||||
if distance < 200:
|
||||
rounded = max(10, int(distance / 10) * 10)
|
||||
else:
|
||||
rounded = int(distance / 100) * 100
|
||||
return str(rounded) + tr("m")
|
||||
else:
|
||||
distance_mi = distance * METER_TO_MILE
|
||||
if distance_mi < 0.1:
|
||||
return tr("Near")
|
||||
return f"{distance_mi:.1f}" + tr("mi")
|
||||
@@ -1,29 +0,0 @@
|
||||
"""
|
||||
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 openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView
|
||||
|
||||
|
||||
class _SuppressedConfidenceBall:
|
||||
def render(self, *_):
|
||||
pass
|
||||
|
||||
|
||||
class AugmentedRoadViewSP(AugmentedRoadView):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._show_confidence_ball: bool = True
|
||||
self._real_confidence_ball = self._confidence_ball
|
||||
self._confidence_ball = _SuppressedConfidenceBall()
|
||||
|
||||
def set_show_confidence_ball(self, show: bool) -> None:
|
||||
self._show_confidence_ball = show
|
||||
|
||||
def _render(self, _) -> None:
|
||||
super()._render(_)
|
||||
if self._show_confidence_ball:
|
||||
self._real_confidence_ball.render(self.rect)
|
||||
@@ -1,83 +0,0 @@
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import MouseEvent, MousePos, gui_app
|
||||
from openpilot.system.ui.lib.scroll_panel2 import ScrollState
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets import scroller as scroller_mod
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroll_panel_sp import GuiScrollPanel2SP
|
||||
|
||||
|
||||
class DummyScrollIndicator:
|
||||
def update(self, *_) -> None:
|
||||
pass
|
||||
|
||||
def render(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class DummyWidget(Widget):
|
||||
def __init__(self, rect: rl.Rectangle):
|
||||
super().__init__()
|
||||
self.set_rect(rect)
|
||||
|
||||
def _render(self, _) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _mouse_event(x: float, y: float, *, pressed: bool = False, released: bool = False,
|
||||
down: bool = True, t: float = 0.0) -> MouseEvent:
|
||||
return MouseEvent(MousePos(x, y), 0, pressed, released, down, t)
|
||||
|
||||
|
||||
def test_vertical_snap_items_are_supported(monkeypatch):
|
||||
monkeypatch.setattr(scroller_mod, "ScrollIndicator", DummyScrollIndicator)
|
||||
|
||||
scroller = scroller_mod._Scroller([], horizontal=False, snap_items=True, scroll_indicator=False)
|
||||
scroller.set_rect(rl.Rectangle(0, 0, 100, 100))
|
||||
scroller.scroll_panel.set_offset(-60)
|
||||
|
||||
captured_snap_target = None
|
||||
|
||||
def update(_, __, snap_target=None):
|
||||
nonlocal captured_snap_target
|
||||
captured_snap_target = snap_target
|
||||
return scroller.scroll_panel.get_offset()
|
||||
|
||||
monkeypatch.setattr(scroller.scroll_panel, "update", update)
|
||||
|
||||
visible_items: list[Widget] = [
|
||||
DummyWidget(rl.Rectangle(0, -60, 100, 100)),
|
||||
DummyWidget(rl.Rectangle(0, 40, 100, 100)),
|
||||
]
|
||||
scroller._get_scroll(visible_items, 200)
|
||||
|
||||
assert captured_snap_target == -100
|
||||
|
||||
|
||||
def test_scroll_panel_sp_rejects_orthogonal_drags(monkeypatch):
|
||||
panel = GuiScrollPanel2SP(horizontal=True)
|
||||
bounds = rl.Rectangle(0, 0, 100, 100)
|
||||
|
||||
monkeypatch.setattr(gui_app, "_mouse_events", [_mouse_event(10, 10, pressed=True, t=1.0)])
|
||||
panel.update(bounds, 200)
|
||||
assert panel.state == ScrollState.PRESSED
|
||||
|
||||
monkeypatch.setattr(gui_app, "_mouse_events", [_mouse_event(23, 60, t=1.1)])
|
||||
panel.update(bounds, 200)
|
||||
|
||||
assert panel.state == ScrollState.STEADY
|
||||
assert panel.get_offset() == 0
|
||||
|
||||
|
||||
def test_scroll_panel_sp_can_disable_out_of_bounds_handling(monkeypatch):
|
||||
panel = GuiScrollPanel2SP(horizontal=False, handle_out_of_bounds=False)
|
||||
bounds = rl.Rectangle(0, 0, 100, 100)
|
||||
monkeypatch.setattr(gui_app, "_mouse_events", [])
|
||||
|
||||
panel.set_offset(20)
|
||||
panel.update(bounds, 200)
|
||||
assert panel.get_offset() == 0
|
||||
|
||||
panel.set_offset(-150)
|
||||
panel.update(bounds, 200)
|
||||
assert panel.get_offset() == -100
|
||||
@@ -1,33 +0,0 @@
|
||||
"""
|
||||
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 pyray as rl
|
||||
from openpilot.system.ui.lib.application import MouseEvent
|
||||
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2, ScrollState
|
||||
|
||||
|
||||
class GuiScrollPanel2SP(GuiScrollPanel2):
|
||||
"""Scroll panel behavior for nested Mici pagers."""
|
||||
|
||||
def __init__(self, horizontal: bool = True, handle_out_of_bounds: bool = True) -> None:
|
||||
super().__init__(horizontal, handle_out_of_bounds=handle_out_of_bounds)
|
||||
|
||||
def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, bounds_size: float,
|
||||
content_size: float) -> None:
|
||||
state_before_update = self._state
|
||||
super()._handle_mouse_event(mouse_event, bounds, bounds_size, content_size)
|
||||
|
||||
if self._state == ScrollState.MANUAL_SCROLL and state_before_update == ScrollState.PRESSED and \
|
||||
self._initial_click_event is not None:
|
||||
drag_x = abs(mouse_event.pos.x - self._initial_click_event.pos.x)
|
||||
drag_y = abs(mouse_event.pos.y - self._initial_click_event.pos.y)
|
||||
primary_drag = drag_x if self._horizontal else drag_y
|
||||
cross_drag = drag_y if self._horizontal else drag_x
|
||||
if cross_drag > primary_drag:
|
||||
self._state = ScrollState.STEADY
|
||||
self._velocity = 0.0
|
||||
self._velocity_buffer.clear()
|
||||
@@ -1,16 +0,0 @@
|
||||
"""
|
||||
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 openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroll_panel_sp import GuiScrollPanel2SP
|
||||
|
||||
|
||||
class ScrollerSP(Scroller):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
inner = self._scroller
|
||||
inner.scroll_panel = GuiScrollPanel2SP(inner._horizontal, handle_out_of_bounds=not inner._snap_items)
|
||||
@@ -10,9 +10,6 @@ from openpilot.selfdrive.ui.layouts.main import MainLayout
|
||||
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.main import MiciMainLayoutSP as MiciMainLayout
|
||||
|
||||
BIG_UI = gui_app.big_ui()
|
||||
|
||||
|
||||
|
||||
@@ -272,17 +272,18 @@ def _parse_size(size_str: str) -> tuple[int, int]:
|
||||
return int(width), int(height)
|
||||
|
||||
|
||||
def read_file_chunked_to_disk(path):
|
||||
def read_file_chunked_to_shm(path):
|
||||
if not path:
|
||||
return None
|
||||
import atexit
|
||||
import shutil
|
||||
from openpilot.common.file_chunker import open_file_chunked
|
||||
tmp_path = f'{path}.unchunked'
|
||||
with open(tmp_path, 'wb') as f, open_file_chunked(path) as src:
|
||||
shutil.copyfileobj(src, f)
|
||||
atexit.register(lambda: os.path.exists(tmp_path) and os.remove(tmp_path))
|
||||
return tmp_path
|
||||
from openpilot.common.hardware.hw import Paths
|
||||
shm_path = os.path.join(Paths.shm_path(), os.path.basename(path))
|
||||
atexit.register(lambda: os.path.exists(shm_path) and os.remove(shm_path))
|
||||
with open(shm_path, 'wb') as dst, open_file_chunked(path) as src:
|
||||
shutil.copyfileobj(src, dst)
|
||||
return shm_path
|
||||
|
||||
|
||||
def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]:
|
||||
@@ -326,11 +327,11 @@ if __name__ == "__main__":
|
||||
model_w, model_h = args.model_size
|
||||
output_data = {}
|
||||
|
||||
args.vision_onnx = read_file_chunked_to_disk(args.vision_onnx)
|
||||
args.policy_onnx = read_file_chunked_to_disk(args.policy_onnx)
|
||||
args.off_policy_onnx = read_file_chunked_to_disk(args.off_policy_onnx)
|
||||
args.on_policy_onnx = read_file_chunked_to_disk(args.on_policy_onnx)
|
||||
args.supercombo_onnx = read_file_chunked_to_disk(args.supercombo_onnx)
|
||||
args.vision_onnx = read_file_chunked_to_shm(args.vision_onnx)
|
||||
args.policy_onnx = read_file_chunked_to_shm(args.policy_onnx)
|
||||
args.off_policy_onnx = read_file_chunked_to_shm(args.off_policy_onnx)
|
||||
args.on_policy_onnx = read_file_chunked_to_shm(args.on_policy_onnx)
|
||||
args.supercombo_onnx = read_file_chunked_to_shm(args.supercombo_onnx)
|
||||
|
||||
vision_runner = OnnxRunner(args.vision_onnx) if args.vision_onnx else None
|
||||
|
||||
|
||||
@@ -5,15 +5,10 @@ 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 os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from openpilot.common.parameterized import parameterized
|
||||
|
||||
from openpilot.common.file_chunker import chunk_file, get_chunk_targets
|
||||
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, _detect_desire_key, read_file_chunked_to_disk
|
||||
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, _detect_desire_key
|
||||
from openpilot.common.test import OpenpilotTestCase
|
||||
|
||||
|
||||
@@ -165,33 +160,3 @@ class TestOutputSlicePreservation(OpenpilotTestCase):
|
||||
policy_slices = {'plan': slice(0, 495), 'meta': slice(495, 550)}
|
||||
assert set(vision_slices.keys()) & set(policy_slices.keys()) == set(), \
|
||||
"vision and policy slices should not overlap in keys"
|
||||
|
||||
|
||||
class TestReadFileChunkedToDisk(OpenpilotTestCase):
|
||||
def test_none_passthrough(self):
|
||||
assert read_file_chunked_to_disk(None) is None
|
||||
|
||||
def test_unchunked_source_staged_on_disk(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
src = Path(d) / "driving_supercombo.onnx"
|
||||
payload = os.urandom(1024)
|
||||
src.write_bytes(payload)
|
||||
|
||||
out = Path(read_file_chunked_to_disk(str(src)))
|
||||
|
||||
assert out.parent == Path(d)
|
||||
assert out.name == "driving_supercombo.onnx.unchunked"
|
||||
assert out.read_bytes() == payload
|
||||
|
||||
def test_chunked_source_reassembled_on_disk(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
src = Path(d) / "driving_supercombo.onnx"
|
||||
payload = os.urandom(4096)
|
||||
src.write_bytes(payload)
|
||||
chunk_file(str(src), get_chunk_targets(str(src), len(payload)))
|
||||
assert not src.exists()
|
||||
|
||||
out = Path(read_file_chunked_to_disk(str(src)))
|
||||
|
||||
assert out.parent == Path(d)
|
||||
assert out.read_bytes() == payload
|
||||
|
||||
@@ -4,13 +4,7 @@ import hashlib
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.sunnypilot import get_file_hash
|
||||
from openpilot.selfdrive.modeld.helpers import usbgpu_present
|
||||
from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MODEL
|
||||
|
||||
|
||||
def get_default_model() -> str:
|
||||
return DEFAULT_BIG_MODEL if usbgpu_present() else DEFAULT_MODEL
|
||||
|
||||
from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL
|
||||
|
||||
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "model_name.py")
|
||||
MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "model_hash")
|
||||
@@ -19,6 +13,7 @@ SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld",
|
||||
|
||||
def update_model_hash():
|
||||
supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH)
|
||||
|
||||
combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest()
|
||||
|
||||
with open(MODEL_HASH_PATH, "w") as f:
|
||||
@@ -27,28 +22,40 @@ def update_model_hash():
|
||||
print(f"Generated and updated new combined model hash to {MODEL_HASH_PATH}")
|
||||
|
||||
|
||||
def update_default_model_names(default_model_name: str, default_big_model_name: str):
|
||||
print("[CHANGE DEFAULT MODEL NAMES]")
|
||||
with open(DEFAULT_MODEL_NAME_PATH, "w") as f:
|
||||
f.write(f'DEFAULT_MODEL = "{default_model_name}"\n')
|
||||
f.write(f'DEFAULT_BIG_MODEL = "{default_big_model_name}"\n')
|
||||
def get_current_default_model_name():
|
||||
print("[GET DEFAULT MODEL NAME]")
|
||||
name = DEFAULT_MODEL
|
||||
print(f'Current default model name: "{name}"')
|
||||
|
||||
print(f'New default small model name: "{default_model_name}"')
|
||||
print(f'New default big model name: "{default_big_model_name}"')
|
||||
return name
|
||||
|
||||
|
||||
def update_default_model_name(name: str):
|
||||
print("[CHANGE DEFAULT MODEL NAME]")
|
||||
with open(DEFAULT_MODEL_NAME_PATH, "w") as f:
|
||||
f.write(f'DEFAULT_MODEL = "{name}"\n')
|
||||
print(f'New default model name: "{name}"')
|
||||
print("[DONE]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Update default model names and hash")
|
||||
parser.add_argument("--new_small_model_name", type=str, help="New default small model name")
|
||||
parser.add_argument("--new_big_model_name", type=str, help="New default big model name")
|
||||
parser = argparse.ArgumentParser(description="Update default model name and hash")
|
||||
parser.add_argument("--new_name", type=str, help="New default model name")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.new_small_model_name is None and args.new_big_model_name is None:
|
||||
new_name = input(f'Enter new default small model name (current: "{DEFAULT_MODEL}", leave empty to keep): ').strip()
|
||||
new_big_model_name = input(f'Enter new default big model name (current: "{DEFAULT_BIG_MODEL}", leave empty to keep): ').strip()
|
||||
else:
|
||||
new_name, new_big_model_name = args.new_small_model_name, args.new_big_model_name
|
||||
if not args.new_name:
|
||||
print("Warning: No new default model name provided. Use --new_name to specify")
|
||||
print("Default model name and hash will not be updated! (aborted)")
|
||||
exit(0)
|
||||
|
||||
update_default_model_names(new_name or DEFAULT_MODEL, new_big_model_name or DEFAULT_BIG_MODEL)
|
||||
current_name = get_current_default_model_name()
|
||||
new_name = args.new_name
|
||||
if current_name == new_name:
|
||||
print(f'Proposed default model name: "{new_name}"')
|
||||
confirm = input("Proposed default model name is the same as the current default model name. Confirm? (y/n): ").upper().strip()
|
||||
if confirm != "Y":
|
||||
print("Default model name and hash will not be updated! (aborted)")
|
||||
exit(0)
|
||||
|
||||
update_default_model_name(new_name)
|
||||
update_model_hash()
|
||||
|
||||
@@ -141,7 +141,7 @@ 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_v20.json"
|
||||
MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v21.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
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
DEFAULT_MODEL = "CD210"
|
||||
DEFAULT_BIG_MODEL = "Lebowski"
|
||||
|
||||
@@ -20,4 +20,4 @@ class TestDefaultModel(OpenpilotTestCase):
|
||||
with open(MODEL_HASH_PATH) as f:
|
||||
current_hash = f.read().strip()
|
||||
|
||||
assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash"
|
||||
assert combined_hash == current_hash, "Run sunnypilot/models/default_model.py to update the default model name and hash"
|
||||
|
||||
@@ -28,7 +28,7 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce
|
||||
create_connection, WebSocketConnectionClosedException)
|
||||
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.sunnypilot.models.default_model import get_default_model
|
||||
from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL
|
||||
from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params import update_car_list_param
|
||||
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi
|
||||
from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string
|
||||
@@ -181,7 +181,7 @@ def getParamsMetadata() -> str:
|
||||
schema = generate_schema()
|
||||
schema["capabilities"] = generate_capabilities()
|
||||
schema["capability_labels"] = CAPABILITY_LABELS
|
||||
schema["default_model"] = get_default_model()
|
||||
schema["default_model"] = DEFAULT_MODEL
|
||||
raw = json.dumps(schema, separators=(",", ":")).encode("utf-8")
|
||||
return base64.b64encode(gzip.compress(raw)).decode("utf-8")
|
||||
except Exception:
|
||||
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Define the service name
|
||||
SERVICE_NAME="actions.runner.sunnypilot.$(uname -n)"
|
||||
|
||||
# Function to control the service
|
||||
control_service() {
|
||||
local action=$1 # Store the function argument in a local variable
|
||||
sudo systemctl $action ${SERVICE_NAME}
|
||||
}
|
||||
|
||||
service_exists_and_is_loaded() {
|
||||
sudo systemctl status ${SERVICE_NAME} &>/dev/null
|
||||
if [[ $? -ne 4 ]]; then
|
||||
return 0 # Service is known to systemd (i.e., loaded)
|
||||
else
|
||||
return 1 # Service is unknown to systemd (i.e., not loaded)
|
||||
fi
|
||||
}
|
||||
|
||||
# Check for required argument
|
||||
if [[ -z $1 ]] || { [[ $1 != "start" ]] && [[ $1 != "stop" ]]; }; then
|
||||
echo "Usage: $0 {start|stop}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Store the script argument in a descriptive variable
|
||||
ACTION=$1
|
||||
|
||||
# Trap EXIT signal (Ctrl+C) and stop the service
|
||||
trap 'control_service stop ; exit' SIGINT SIGKILL EXIT
|
||||
|
||||
# Enter the main loop
|
||||
while true; do
|
||||
# Check if the service is actually present on the system
|
||||
if service_exists_and_is_loaded; then
|
||||
control_service $ACTION # Call the function with the specified action
|
||||
fi
|
||||
sleep 1 # Pause before the next iteration
|
||||
done
|
||||
@@ -68,6 +68,10 @@ def only_offroad(started: bool, params: Params, CP: car.CarParams) -> bool:
|
||||
def livestream(started: bool, params: Params, CP: car.CarParams) -> bool:
|
||||
return params.get_bool("IsLiveStreaming")
|
||||
|
||||
def use_github_runner(started, params, CP: car.CarParams) -> bool:
|
||||
return not PC and params.get_bool("EnableGithubRunner") and (
|
||||
not params.get_bool("NetworkMetered") and not params.get_bool("GithubRunnerSufficientVoltage"))
|
||||
|
||||
def use_copyparty(started, params, CP: car.CarParams) -> bool:
|
||||
return bool(params.get_bool("EnableCopyparty"))
|
||||
|
||||
@@ -185,6 +189,10 @@ procs += [
|
||||
NativeProcess("locationd_llk", "openpilot/sunnypilot/selfdrive/locationd", ["./locationd"], only_onroad),
|
||||
]
|
||||
|
||||
if os.path.exists("./github_runner.sh"):
|
||||
procs += [NativeProcess("github_runner_start", "openpilot/system/manager",
|
||||
["./github_runner.sh", "start"], and_(only_offroad, use_github_runner), sigkill=False)]
|
||||
|
||||
if os.path.exists("../../sunnypilot/sunnylink/uploader.py"):
|
||||
procs += [PythonProcess("sunnylink_uploader", "openpilot.sunnypilot.sunnylink.uploader", use_sunnylink_uploader_shim)]
|
||||
|
||||
|
||||
@@ -45,9 +45,8 @@ class ScrollState(Enum):
|
||||
|
||||
|
||||
class GuiScrollPanel2:
|
||||
def __init__(self, horizontal: bool = True, handle_out_of_bounds: bool = True) -> None:
|
||||
def __init__(self, horizontal: bool = True) -> None:
|
||||
self._horizontal = horizontal
|
||||
self._handle_out_of_bounds = handle_out_of_bounds
|
||||
self._state = ScrollState.STEADY
|
||||
self._offset: rl.Vector2 = rl.Vector2(0, 0)
|
||||
self._initial_click_event: MouseEvent | None = None
|
||||
@@ -86,20 +85,6 @@ class GuiScrollPanel2:
|
||||
"""Returns (max_offset, min_offset) for the given bounds and content size."""
|
||||
return 0.0, min(0.0, bounds_size - content_size)
|
||||
|
||||
def _clamp_offset(self, bounds_size: float, content_size: float) -> None:
|
||||
if self._handle_out_of_bounds:
|
||||
return
|
||||
|
||||
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
|
||||
offset = self.get_offset()
|
||||
clamped_offset = max(min_offset, min(max_offset, offset))
|
||||
if clamped_offset == offset:
|
||||
return
|
||||
|
||||
self.set_offset(clamped_offset)
|
||||
if (clamped_offset == max_offset and self._velocity > 0) or (clamped_offset == min_offset and self._velocity < 0):
|
||||
self._velocity = 0.0
|
||||
|
||||
def _update_state(self, bounds_size: float, content_size: float, snap_target: float | None) -> None:
|
||||
"""Runs per render frame, independent of mouse events. Updates auto-scrolling state and velocity."""
|
||||
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
|
||||
@@ -153,8 +138,6 @@ class GuiScrollPanel2:
|
||||
factor = 1.0 - math.exp(-SNAP_RATE * dt)
|
||||
self.set_offset(self.get_offset() + dist * factor)
|
||||
|
||||
self._clamp_offset(bounds_size, content_size)
|
||||
|
||||
def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, bounds_size: float,
|
||||
content_size: float) -> None:
|
||||
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
|
||||
|
||||
@@ -75,6 +75,7 @@ class _Scroller(Widget):
|
||||
self._items: list[Widget] = []
|
||||
self._horizontal = horizontal
|
||||
self._snap_items = snap_items
|
||||
assert not self._snap_items or self._horizontal, "Snapping is only supported for horizontal scrolling"
|
||||
self._spacing = spacing
|
||||
self._pad = pad
|
||||
|
||||
@@ -190,20 +191,12 @@ class _Scroller(Widget):
|
||||
snap_target: float | None = None
|
||||
if self._snap_items and visible_items and self._scrolling_to[0] is None:
|
||||
# TODO: this doesn't handle two small buttons at the edges well
|
||||
center_pos = (self._rect.x + self._rect.width / 2) if self._horizontal else (self._rect.y + self._rect.height / 2)
|
||||
closest_delta_pos = min(
|
||||
(self._item_center_pos(item) - center_pos for item in visible_items),
|
||||
key=abs,
|
||||
)
|
||||
center_pos = self._rect.x + self._rect.width / 2
|
||||
closest_delta_pos = min((((item.rect.x + item.rect.width / 2) - center_pos) for item in visible_items), key=abs)
|
||||
snap_target = self.scroll_panel.get_offset() - closest_delta_pos
|
||||
|
||||
return self.scroll_panel.update(self._rect, content_size, snap_target=snap_target)
|
||||
|
||||
def _item_center_pos(self, item: Widget) -> float:
|
||||
if self._horizontal:
|
||||
return item.rect.x + item.rect.width / 2
|
||||
return item.rect.y + item.rect.height / 2
|
||||
|
||||
@property
|
||||
def moving_items(self) -> bool:
|
||||
return len(self._move_animations) > 0 or len(self._move_lift) > 0
|
||||
|
||||
Executable
+260
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# Default values
|
||||
DEFAULT_REPO_URL="https://github.com/sunnypilot"
|
||||
START_AT_BOOT=false
|
||||
RESTORE_MODE=false
|
||||
RUNNER_VERSION="2.325.0"
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--start-at-boot)
|
||||
START_AT_BOOT=true
|
||||
shift
|
||||
;;
|
||||
--token)
|
||||
GITHUB_TOKEN="$2"
|
||||
shift 2
|
||||
;;
|
||||
--repo)
|
||||
REPO_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--restore)
|
||||
RESTORE_MODE=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
if [ -z "$GITHUB_TOKEN" ]; then
|
||||
GITHUB_TOKEN="$1"
|
||||
elif [ -z "$REPO_URL" ]; then
|
||||
REPO_URL="$1"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Determine BASE_DIR based on mount point
|
||||
if mountpoint -q /data/media; then
|
||||
BASE_DIR="/data/media/0/github"
|
||||
else
|
||||
BASE_DIR="/data/github"
|
||||
fi
|
||||
|
||||
# Constants
|
||||
RUNNER_USER="github-runner"
|
||||
USER_GROUPS="comma,gpu,gpio,sudo"
|
||||
RUNNER_DIR="${BASE_DIR}/runner"
|
||||
BUILDS_DIR="${BASE_DIR}/builds"
|
||||
LOGS_DIR="${BASE_DIR}/logs"
|
||||
CACHE_DIR="${BASE_DIR}/cache"
|
||||
OPENPILOT_DIR="${BASE_DIR}/openpilot"
|
||||
|
||||
# Basic utility functions (no dependencies)
|
||||
remount_rw() {
|
||||
sudo mount -o remount,rw /
|
||||
}
|
||||
|
||||
remount_ro() {
|
||||
sync || true # Try to sync but continue even if it fails
|
||||
sudo mount -o remount,ro / # Always try to remount as read-only
|
||||
}
|
||||
|
||||
# Always ensure we try to remount as read-only on exit
|
||||
trap remount_ro EXIT
|
||||
|
||||
setup_runner_user() {
|
||||
sudo useradd --comment 'GitHub Runner' --create-home --home-dir ${BASE_DIR} ${RUNNER_USER} --shell /bin/bash -G ${USER_GROUPS} || sudo usermod -aG ${USER_GROUPS} ${RUNNER_USER}
|
||||
}
|
||||
|
||||
create_sudoers_entry() {
|
||||
sudo grep -qxF "${RUNNER_USER} ALL=(ALL) NOPASSWD: ALL" /etc/sudoers || echo "${RUNNER_USER} ALL=(ALL) NOPASSWD: ALL" | sudo tee -a /etc/sudoers
|
||||
}
|
||||
|
||||
set_directory_permissions() {
|
||||
sudo chown -R ${RUNNER_USER}:comma "$BASE_DIR"
|
||||
sudo chmod -R g+rwx "$BASE_DIR"
|
||||
sudo find "$BASE_DIR" -type d -exec chmod g+s {} +
|
||||
}
|
||||
|
||||
setup_directories() {
|
||||
echo "Creating necessary directories..."
|
||||
sudo mkdir -p "$RUNNER_DIR" "$BUILDS_DIR" "$LOGS_DIR" "$CACHE_DIR" "$OPENPILOT_DIR"
|
||||
mkdir -p "/data/openpilot"
|
||||
sudo chown -R comma:comma "/data/openpilot"
|
||||
sync
|
||||
}
|
||||
|
||||
wipe_bash_logout() {
|
||||
export BASE_DIR
|
||||
sudo -u ${RUNNER_USER} bash -c "touch ${BASE_DIR}/.bash_logout"
|
||||
sudo -u ${RUNNER_USER} bash -c "truncate -s 0 '${BASE_DIR}/.bash_logout'"
|
||||
}
|
||||
|
||||
# System configuration functions (depends on basic utility functions)
|
||||
setup_system_configs() {
|
||||
echo "Setting up system configurations..."
|
||||
remount_rw
|
||||
setup_runner_user
|
||||
create_sudoers_entry
|
||||
remount_ro
|
||||
set_directory_permissions
|
||||
wipe_bash_logout
|
||||
}
|
||||
|
||||
# Runner setup functions
|
||||
install_runner() {
|
||||
echo "Downloading and setting up runner..."
|
||||
cd "$RUNNER_DIR"
|
||||
curl -o actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz -L https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz
|
||||
sudo -u ${RUNNER_USER} tar -xzf ./actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz
|
||||
sudo rm ./actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz
|
||||
sudo chmod +x ./config.sh
|
||||
}
|
||||
|
||||
configure_runner() {
|
||||
remount_rw
|
||||
echo "Configuring runner..."
|
||||
cd "$RUNNER_DIR"
|
||||
sudo -u ${RUNNER_USER} ./config.sh --url "$REPO_URL" --token "$GITHUB_TOKEN" --name $(hostname) --runnergroup "tici-tizi" --labels "tici" --work "$BUILDS_DIR" --unattended
|
||||
remount_ro
|
||||
}
|
||||
|
||||
create_service_template() {
|
||||
echo "Creating service template..."
|
||||
cat <<EOL > "$RUNNER_DIR/bin/actions.runner.service.template"
|
||||
[Unit]
|
||||
Description={{Description}}
|
||||
After=network-online.target nss-lookup.target time-sync.target
|
||||
Wants=network-online.target nss-lookup.target time-sync.target
|
||||
StartLimitInterval=5
|
||||
StartLimitBurst=10
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStart=/usr/bin/unshare -m -- /bin/bash -c 'mount --bind ${OPENPILOT_DIR} /data/openpilot && setpriv --reuid={{User}} --regid={{User}} --init-groups env HOME=${BASE_DIR} USER={{User}} LOGNAME={{User}} MAIL=/var/mail/{{User}} {{RunnerRoot}}/runsvc.sh'
|
||||
WorkingDirectory={{RunnerRoot}}
|
||||
KillMode=process
|
||||
KillSignal=SIGTERM
|
||||
TimeoutStopSec=5min
|
||||
Restart=always
|
||||
RestartSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOL
|
||||
}
|
||||
|
||||
install_service() {
|
||||
local service_name
|
||||
if [ -f "${RUNNER_DIR}/.service" ]; then
|
||||
service_name=$(cat "${RUNNER_DIR}/.service")
|
||||
else
|
||||
service_name="actions.runner.sunnypilot.$(uname -n)"
|
||||
fi
|
||||
|
||||
create_service_template
|
||||
remount_rw
|
||||
local service_path="/etc/systemd/system/${service_name}"
|
||||
echo "Installing systemd service..."
|
||||
if [ -f "${service_path}" ]; then
|
||||
echo "Service ${service_path} found in systemd, we will delete it"
|
||||
sudo rm -f "${service_path}"
|
||||
fi
|
||||
|
||||
cd "$RUNNER_DIR"
|
||||
sudo ./svc.sh install $RUNNER_USER
|
||||
|
||||
if [ "$START_AT_BOOT" = false ]; then
|
||||
sudo systemctl disable "${service_name}"
|
||||
fi
|
||||
remount_ro
|
||||
}
|
||||
|
||||
check_restore_prerequisites() {
|
||||
local can_restore=false
|
||||
local service_name=""
|
||||
|
||||
# Check if base runner directory exists
|
||||
if [ ! -d "${RUNNER_DIR}" ]; then
|
||||
echo "ERROR: Runner directory ${RUNNER_DIR} does not exist"
|
||||
echo "This directory is required for restore operations"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# First check if we have the required files for restoration
|
||||
if [ -f "${RUNNER_DIR}/.credentials" ] && [ -f "${RUNNER_DIR}/.service" ]; then
|
||||
can_restore=true
|
||||
service_name=$(cat "${RUNNER_DIR}/.service")
|
||||
echo "Found required runner configuration files"
|
||||
else
|
||||
echo "Missing required runner configuration files"
|
||||
echo "Required: .credentials and .service files in ${RUNNER_DIR}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! id "${RUNNER_USER}" &>/dev/null; then
|
||||
echo "User ${RUNNER_USER} does not exist"
|
||||
fi
|
||||
|
||||
# Only proceed if we can restore AND need to restore
|
||||
if [ "$can_restore" = true ]; then
|
||||
echo "Restoration is possible"
|
||||
return 0
|
||||
else
|
||||
echo "No restoration possible"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
perform_restore() {
|
||||
echo "Starting runner restoration..."
|
||||
setup_directories
|
||||
setup_system_configs
|
||||
install_service
|
||||
echo "Runner restoration completed successfully"
|
||||
}
|
||||
|
||||
perform_install() {
|
||||
echo "Starting fresh installation..."
|
||||
setup_directories
|
||||
setup_system_configs
|
||||
install_runner
|
||||
set_directory_permissions
|
||||
configure_runner
|
||||
install_service
|
||||
echo "Installation completed successfully"
|
||||
}
|
||||
|
||||
main() {
|
||||
if [ "$RESTORE_MODE" = true ]; then
|
||||
echo "Running in restore mode - will only restore system configurations..."
|
||||
check_restore_prerequisites
|
||||
perform_restore
|
||||
else
|
||||
# Check required arguments for normal installation
|
||||
if [ -z "$GITHUB_TOKEN" ]; then
|
||||
echo "Usage: $0 [--start-at-boot] [--token <github_token>] [--repo <repository_url>] [--restore]"
|
||||
echo "Required argument (except for --restore): github_token"
|
||||
echo "Optional arguments:"
|
||||
echo " --start-at-boot Enable auto-start at boot (default: false)"
|
||||
echo " --repo Repository URL (default: ${DEFAULT_REPO_URL})"
|
||||
echo " --restore Restore existing runner configuration"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set repository URL if not provided
|
||||
REPO_URL="${REPO_URL:-$DEFAULT_REPO_URL}"
|
||||
perform_install
|
||||
fi
|
||||
|
||||
echo "Starting runner service..."
|
||||
cd "$RUNNER_DIR"
|
||||
sudo ./svc.sh start
|
||||
}
|
||||
|
||||
main
|
||||
@@ -53,28 +53,24 @@ def create_pkl_name(full_name: str) -> str:
|
||||
return pkl
|
||||
|
||||
|
||||
def _hash_pkl(pkl_path: Path) -> str:
|
||||
def _read_pkl_bytes(pkl_path: Path) -> bytes:
|
||||
manifest = Path(f"{pkl_path}.chunkmanifest")
|
||||
if manifest.exists():
|
||||
num_chunks = int(manifest.read_text().strip())
|
||||
paths = [Path(f"{pkl_path}.chunk{i + 1:02d}of{num_chunks:02d}") for i in range(num_chunks)]
|
||||
else:
|
||||
paths = [pkl_path]
|
||||
|
||||
digest = hashlib.sha256()
|
||||
for path in paths:
|
||||
with path.open('rb') as f:
|
||||
while block := f.read(1024 * 1024):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
parts = []
|
||||
for i in range(num_chunks):
|
||||
chunk = Path(f"{pkl_path}.chunk{i + 1:02d}of{num_chunks:02d}")
|
||||
parts.append(chunk.read_bytes())
|
||||
return b''.join(parts)
|
||||
return pkl_path.read_bytes()
|
||||
|
||||
|
||||
def _find_driving_pkl(output_path: Path) -> Path | None:
|
||||
for pattern in ('*driving_tinygrad.pkl', '*driving_*_tinygrad.pkl'):
|
||||
for pattern in ('driving_tinygrad.pkl', 'driving_*_tinygrad.pkl'):
|
||||
matches = sorted(output_path.glob(pattern))
|
||||
if matches:
|
||||
return matches[0]
|
||||
for pattern in ('*driving_tinygrad.pkl.chunkmanifest', '*driving_*_tinygrad.pkl.chunkmanifest'):
|
||||
for pattern in ('driving_tinygrad.pkl.chunkmanifest', 'driving_*_tinygrad.pkl.chunkmanifest'):
|
||||
matches = sorted(output_path.glob(pattern))
|
||||
if matches:
|
||||
return Path(str(matches[0]).removesuffix('.chunkmanifest'))
|
||||
@@ -91,7 +87,7 @@ def _rename_pkl_with_chunks(old_pkl: Path, new_pkl: Path) -> Path:
|
||||
|
||||
|
||||
def generate_chunked_model(driving_pkl: Path) -> dict:
|
||||
tinygrad_hash = _hash_pkl(driving_pkl)
|
||||
tinygrad_hash = hashlib.sha256(_read_pkl_bytes(driving_pkl)).hexdigest()
|
||||
|
||||
chunks_config = []
|
||||
manifest_file = Path(f"{driving_pkl}.chunkmanifest")
|
||||
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
# Determine BASE_DIR based on mount point
|
||||
if mountpoint -q /data/media; then
|
||||
GITHUB_BASE_DIR="/data/media/0/github"
|
||||
else
|
||||
GITHUB_BASE_DIR="/data/github"
|
||||
fi
|
||||
|
||||
# Define directories and user
|
||||
BIN_DIR="$GITHUB_BASE_DIR/bin"
|
||||
BUILDS_DIR="$GITHUB_BASE_DIR/builds"
|
||||
OPENPILOT_DIR="$GITHUB_BASE_DIR/openpilot"
|
||||
LOGS_DIR="$GITHUB_BASE_DIR/logs"
|
||||
CACHE_DIR="$GITHUB_BASE_DIR/cache"
|
||||
RUNNER_USERNAME="github-runner"
|
||||
# Define the systemd service name
|
||||
SERVICE_NAME="github-runner"
|
||||
USER_GROUPS="comma,gpu,gpio,sudo"
|
||||
|
||||
# Function to stop and disable the systemd service
|
||||
stop_and_uninstall_service() {
|
||||
cd $GITHUB_BASE_DIR/runner
|
||||
sudo ./svc.sh stop
|
||||
sudo ./svc.sh uninstall
|
||||
}
|
||||
|
||||
# Function to remove the systemd service file
|
||||
remove_runner() {
|
||||
cd $GITHUB_BASE_DIR/runner
|
||||
sudo rm .runner
|
||||
sudo su -c './config.sh remove' github-runner
|
||||
}
|
||||
|
||||
# Function to delete the Github Runner directories
|
||||
delete_directories() {
|
||||
sudo rm -rf "$BIN_DIR/github-runner"
|
||||
sudo rm -rf "$GITHUB_BASE_DIR" "$BIN_DIR" "$BUILDS_DIR" "$LOGS_DIR" "$CACHE_DIR" "$OPENPILOT_DIR"
|
||||
}
|
||||
|
||||
# Function to remove the Github Runner user
|
||||
delete_user() {
|
||||
for group in ${USER_GROUPS//,/ }
|
||||
do
|
||||
sudo gpasswd -d ${RUNNER_USERNAME} ${group}
|
||||
done
|
||||
sudo userdel -r ${RUNNER_USERNAME}
|
||||
}
|
||||
|
||||
# Function to remove sudoers entry
|
||||
remove_sudoers_entry() {
|
||||
sudo sed -i.bak "/${RUNNER_USERNAME} ALL=(ALL) NOPASSWD: ALL/d" /etc/sudoers
|
||||
}
|
||||
|
||||
# Make filesystem writable
|
||||
sudo mount -o remount rw /
|
||||
|
||||
# Ensure filesystem is remounted as read-only on script exit
|
||||
trap "sudo mount -o remount ro /" EXIT
|
||||
|
||||
# Call functions
|
||||
stop_and_uninstall_service
|
||||
remove_runner
|
||||
delete_directories
|
||||
delete_user
|
||||
remove_sudoers_entry
|
||||
# End of uninstall script
|
||||
Reference in New Issue
Block a user