mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-23 20:52:06 +08:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 58d57f7e1a | |||
| 9cbc91d129 | |||
| 764ff0db6a | |||
| 8faedaa062 | |||
| 4cc43ae6f5 | |||
| 578e80855a | |||
| 66c335113e | |||
| a0c10be1ff | |||
| a58db66a98 | |||
| e5e56614c9 | |||
| 1eb82fcc85 | |||
| 987f53e69a | |||
| 9a04a5eaae | |||
| 50b8ae9e09 | |||
| a30fc9bcd2 | |||
| 19a7d1d5d7 | |||
| fb8f46cba9 | |||
| 70386c6b00 | |||
| edeede5e82 | |||
| 6df313b974 | |||
| 9442bc9aec | |||
| 763049f068 | |||
| c6c644a3a6 | |||
| c876a83a31 | |||
| a04a5b4284 | |||
| 373894a81f | |||
| 34ce746869 | |||
| e96b0da9d7 | |||
| 6d7910ed74 | |||
| e54ddf30b8 | |||
| 3093bb0b66 |
@@ -174,6 +174,24 @@ jobs:
|
||||
echo ' pushurl = ${{ env.LFS_PUSH_URL }}' >> .lfsconfig
|
||||
echo ' locksverify = false' >> .lfsconfig
|
||||
|
||||
- name: Restore workflows from source
|
||||
run: |
|
||||
TARGET_BRANCH="${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}"
|
||||
SOURCE_BRANCH="${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }}"
|
||||
|
||||
# Ensure we are on the target branch
|
||||
git checkout $TARGET_BRANCH
|
||||
|
||||
echo "Restoring .github/workflows from $SOURCE_BRANCH"
|
||||
git checkout origin/$SOURCE_BRANCH -- .github/workflows
|
||||
|
||||
if ! git diff --cached --quiet; then
|
||||
echo "Workflows differ. Committing restoration."
|
||||
git commit -m "chore: restore .github/workflows from $SOURCE_BRANCH"
|
||||
else
|
||||
echo "Workflows match $SOURCE_BRANCH."
|
||||
fi
|
||||
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: ci-token
|
||||
with:
|
||||
|
||||
@@ -11,66 +11,10 @@ Join the official sunnypilot community forum to stay up to date with all the lat
|
||||
https://docs.sunnypilot.ai/ is your one stop shop for everything from features to installation to FAQ about the sunnypilot
|
||||
|
||||
## 🚘 Running on a dedicated device in a car
|
||||
* A supported device to run this software
|
||||
* a [comma three](https://comma.ai/shop/products/three) or a [C3X](https://comma.ai/shop/comma-3x)
|
||||
* This software
|
||||
* One of [the 325+ supported cars](https://github.com/sunnypilot/sunnypilot/blob/master/docs/CARS.md). We support Honda, Toyota, Hyundai, Nissan, Kia, Chrysler, Lexus, Acura, Audi, VW, Ford, and more. If your car is not supported but has adaptive cruise control and lane-keeping assist, it's likely able to run sunnypilot.
|
||||
* A [car harness](https://comma.ai/shop/products/car-harness) to connect to your car
|
||||
|
||||
Detailed instructions for [how to mount the device in a car](https://comma.ai/setup).
|
||||
First, check out this list of items you'll need to [get started](https://community.sunnypilot.ai/t/getting-started-using-sunnypilot-in-your-supported-car/251).
|
||||
|
||||
## Installation
|
||||
Please refer to [Recommended Branches](#recommended-branches) to find your preferred/supported branch. This guide will assume you want to install the latest `staging` branch.
|
||||
|
||||
### If you want to use our newest branches (our rewrite)
|
||||
> [!TIP]
|
||||
>You can see the rewrite state on our [rewrite project board](https://github.com/orgs/sunnypilot/projects/2), and to install the new branches, you can use the following links
|
||||
|
||||
* sunnypilot not installed or you installed a version before 0.8.17?
|
||||
1. [Factory reset/uninstall](https://github.com/commaai/openpilot/wiki/FAQ#how-can-i-reset-the-device) the previous software if you have another software/fork installed.
|
||||
2. After factory reset/uninstall and upon reboot, select `Custom Software` when given the option.
|
||||
3. Input the installation URL per [Recommended Branches](#recommended-branches). Example: ```https://staging.sunnypilot.ai```.
|
||||
4. Complete the rest of the installation following the onscreen instructions.
|
||||
|
||||
* sunnypilot already installed and you installed a version after 0.8.17?
|
||||
1. On the comma three/3X, go to `Settings` ▶️ `Software`.
|
||||
2. At the `Download` option, press `CHECK`. This will fetch the list of latest branches from sunnypilot.
|
||||
3. At the `Target Branch` option, press `SELECT` to open the Target Branch selector.
|
||||
4. Scroll to select the desired branch per Recommended Branches (see below). Example: `staging`
|
||||
|
||||
### Recommended Branches
|
||||
| Branch | Installation URL |
|
||||
|:---------------:|:---------------------------------------------:|
|
||||
| `release` | `https://release.sunnypilot.ai` |
|
||||
| `staging` | `https://staging.sunnypilot.ai` |
|
||||
| `dev` | `https://dev.sunnypilot.ai` |
|
||||
| `custom-branch` | `https://install.sunnypilot.ai/{branch_name}` |
|
||||
|
||||
> [!TIP]
|
||||
> You can use sunnypilot/targetbranch as an install URL. Example: 'sunnypilot/staging'.
|
||||
|
||||
> [!NOTE]
|
||||
> Do you require further assistance with software installation? Join the [sunnypilot community forum](https://community.sunnypilot.ai/new-topic?category=general/qa) and create a topic in the General/Q&A Category channel.
|
||||
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Older legacy branches</summary>
|
||||
|
||||
### If you want to use our older legacy branches (*not recommended*)
|
||||
|
||||
> [**IMPORTANT**]
|
||||
> It is recommended to [re-flash AGNOS](https://flash.comma.ai/) if you intend to downgrade from the new branches.
|
||||
> You can still restore the latest sunnylink backup made on the old branches.
|
||||
|
||||
| Branch | Installation URL |
|
||||
|:------------:|:--------------------------------:|
|
||||
| `release-c3` | https://release-c3.sunnypilot.ai |
|
||||
| `staging-c3` | https://staging-c3.sunnypilot.ai |
|
||||
| `dev-c3` | https://dev-c3.sunnypilot.ai |
|
||||
|
||||
</details>
|
||||
|
||||
Next, refer to the sunnypilot community forum for [installation instructions](https://community.sunnypilot.ai/t/read-before-installing-sunnypilot/254), as well as a complete list of [Recommended Branch Installations](https://community.sunnypilot.ai/t/recommended-branch-installations/235).
|
||||
|
||||
## 🎆 Pull Requests
|
||||
We welcome both pull requests and issues on GitHub. Bug fixes are encouraged.
|
||||
|
||||
@@ -145,6 +145,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"CarParamsSPPersistent", {PERSISTENT, BYTES}},
|
||||
{"CarPlatformBundle", {PERSISTENT | BACKUP, JSON}},
|
||||
{"ChevronInfo", {PERSISTENT | BACKUP, INT, "4"}},
|
||||
{"CompletedSunnylinkConsentVersion", {PERSISTENT, STRING, "0"}},
|
||||
{"CustomAccIncrementsEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"CustomAccLongPressIncrement", {PERSISTENT | BACKUP, INT, "5"}},
|
||||
{"CustomAccShortPressIncrement", {PERSISTENT | BACKUP, INT, "1"}},
|
||||
@@ -154,6 +155,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
|
||||
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
|
||||
{"HideVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"IntelligentCruiseButtonManagement", {PERSISTENT | BACKUP , BOOL}},
|
||||
{"InteractivityTimeout", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
@@ -219,11 +221,13 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// sunnypilot model params
|
||||
{"CameraOffset", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
|
||||
{"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
{"LagdToggleDelay", {PERSISTENT | BACKUP, FLOAT, "0.2"}},
|
||||
{"LagdValueCache", {PERSISTENT, FLOAT, "0.2"}},
|
||||
{"LaneTurnDesire", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}},
|
||||
{"PlanplusControl", {PERSISTENT | BACKUP, FLOAT, "1.0"}},
|
||||
|
||||
// mapd
|
||||
{"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}},
|
||||
|
||||
+1
-1
Submodule opendbc_repo updated: 74ac678501...e03fbf9be8
@@ -449,7 +449,6 @@ class DriverMonitoring:
|
||||
rpyCalib = [0., 0., 0.]
|
||||
else:
|
||||
highway_speed = sm['carState'].vEgo
|
||||
# TODO-SP: unit test to assert both control checks are always present
|
||||
enabled = sm['selfdriveState'].enabled or sm['carControl'].latActive
|
||||
wrong_gear = sm['carState'].gearShifter not in (car.CarState.GearShifter.drive, car.CarState.GearShifter.low)
|
||||
standstill = sm['carState'].standstill
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from cereal import log
|
||||
from cereal import log, car
|
||||
from openpilot.common.realtime import DT_DMON
|
||||
from openpilot.selfdrive.monitoring.helpers import DriverMonitoring, DRIVER_MONITOR_SETTINGS
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
@@ -204,3 +205,66 @@ class TestMonitoring:
|
||||
assert EventName.driverUnresponsive in \
|
||||
events[int((INVISIBLE_SECONDS_TO_RED-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled_state, lat_active_state, expected", [
|
||||
(False, False, False), # Both Disabled
|
||||
(True, False, True), # OP Enabled, Lat Inactive
|
||||
(False, True, True), # OP Disabled, Lat Active (e.g. MADS)
|
||||
(True, True, True) # Both Active
|
||||
])
|
||||
def test_enabled_states(enabled_state, lat_active_state, expected):
|
||||
"""
|
||||
Test DriverMonitoring.run_step with all 4 combinations of:
|
||||
- selfdriveState.enabled (True/False)
|
||||
- carControl.latActive (True/False)
|
||||
"""
|
||||
cs = car.CarState.new_message()
|
||||
cs.vEgo = 30.0
|
||||
cs.gearShifter = car.CarState.GearShifter.drive
|
||||
cs.standstill = False
|
||||
cs.steeringPressed = False
|
||||
cs.gasPressed = False
|
||||
|
||||
ss = log.SelfdriveState.new_message()
|
||||
ss.enabled = enabled_state
|
||||
|
||||
cc = car.CarControl.new_message()
|
||||
cc.latActive = lat_active_state
|
||||
|
||||
mv2 = log.ModelDataV2.new_message()
|
||||
mv2.meta.disengagePredictions.brakeDisengageProbs = [0.0]
|
||||
|
||||
lc = log.LiveCalibrationData.new_message()
|
||||
lc.rpyCalib = [0.0, 0.0, 0.0]
|
||||
|
||||
ds = make_msg(False)
|
||||
|
||||
sm = {
|
||||
'carState': cs,
|
||||
'selfdriveState': ss,
|
||||
'carControl': cc,
|
||||
'modelV2': mv2,
|
||||
'liveCalibration': lc,
|
||||
'driverStateV2': ds
|
||||
}
|
||||
|
||||
driver_monitoring = DriverMonitoring()
|
||||
|
||||
# run_test doesn't assign enabled to a variable, so we need to spy on _update_events to see its value
|
||||
captured_args = []
|
||||
original_update_events = driver_monitoring._update_events
|
||||
|
||||
def spy_update_events(driver_engaged, op_engaged, standstill, wrong_gear, car_speed):
|
||||
captured_args.append(op_engaged)
|
||||
return original_update_events(driver_engaged, op_engaged, standstill, wrong_gear, car_speed)
|
||||
|
||||
driver_monitoring._update_events = spy_update_events
|
||||
|
||||
driver_monitoring.run_step(sm, demo=False)
|
||||
|
||||
# Assertion
|
||||
assert len(captured_args) == 1, "Expected _update_events to be called exactly once"
|
||||
actual_enabled = captured_args[0]
|
||||
|
||||
assert actual_enabled == expected, f"Expected op_engaged={expected}, but got {actual_enabled}"
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
from openpilot.system.ui.widgets.label import Label
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.version import terms_version, training_version
|
||||
from openpilot.system.version import terms_version, training_version, terms_version_sp
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkOnboarding
|
||||
|
||||
DEBUG = False
|
||||
|
||||
@@ -33,6 +35,7 @@ class OnboardingState(IntEnum):
|
||||
TERMS = 0
|
||||
ONBOARDING = 1
|
||||
DECLINE = 2
|
||||
SUNNYLINK_CONSENT = 3
|
||||
|
||||
|
||||
class TrainingGuide(Widget):
|
||||
@@ -110,14 +113,14 @@ class TermsPage(Widget):
|
||||
self._on_decline = on_decline
|
||||
|
||||
self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
self._desc = Label(tr("You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at https://comma.ai/terms before continuing."),
|
||||
self._desc = Label(tr("You must accept the Terms of Service to use sunnypilot. Read the latest terms at https://sunnypilot.ai/terms before continuing."),
|
||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
|
||||
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
|
||||
self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
|
||||
|
||||
def _render(self, _):
|
||||
welcome_x = self._rect.x + 165
|
||||
welcome_x = self._rect.x + 95
|
||||
welcome_y = self._rect.y + 165
|
||||
welcome_rect = rl.Rectangle(welcome_x, welcome_y, self._rect.width - welcome_x, 90)
|
||||
self._title.render(welcome_rect)
|
||||
@@ -143,7 +146,7 @@ class TermsPage(Widget):
|
||||
class DeclinePage(Widget):
|
||||
def __init__(self, back_callback=None):
|
||||
super().__init__()
|
||||
self._text = Label(tr("You must accept the Terms and Conditions in order to use sunnypilot."),
|
||||
self._text = Label(tr("You must accept the Terms of Service in order to use sunnypilot."),
|
||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
self._back_btn = Button(tr("Back"), click_callback=back_callback)
|
||||
self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER,
|
||||
@@ -180,9 +183,21 @@ class OnboardingWindow(Widget):
|
||||
self._training_guide: TrainingGuide | None = None
|
||||
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
|
||||
|
||||
# sunnylink consent pages
|
||||
self._accepted_terms = self._accepted_terms and ui_state.params.get("HasAcceptedTermsSP") == terms_version_sp
|
||||
self._sunnylink = SunnylinkOnboarding()
|
||||
if not self._accepted_terms:
|
||||
self._state = OnboardingState.TERMS
|
||||
elif not self._sunnylink.completed:
|
||||
self._state = OnboardingState.SUNNYLINK_CONSENT
|
||||
elif not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
else:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
|
||||
@property
|
||||
def completed(self) -> bool:
|
||||
return self._accepted_terms and self._training_done
|
||||
return self._accepted_terms and self._sunnylink.completed and self._training_done
|
||||
|
||||
def _on_terms_declined(self):
|
||||
self._state = OnboardingState.DECLINE
|
||||
@@ -192,8 +207,12 @@ class OnboardingWindow(Widget):
|
||||
|
||||
def _on_terms_accepted(self):
|
||||
ui_state.params.put("HasAcceptedTerms", terms_version)
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
if self._training_done:
|
||||
ui_state.params.put("HasAcceptedTermsSP", terms_version_sp)
|
||||
if not self._sunnylink.completed:
|
||||
self._state = OnboardingState.SUNNYLINK_CONSENT
|
||||
elif not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
else:
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
def _on_completed_training(self):
|
||||
@@ -206,8 +225,18 @@ class OnboardingWindow(Widget):
|
||||
|
||||
if self._state == OnboardingState.TERMS:
|
||||
self._terms.render(self._rect)
|
||||
if self._state == OnboardingState.ONBOARDING:
|
||||
self._training_guide.render(self._rect)
|
||||
elif self._state == OnboardingState.SUNNYLINK_CONSENT:
|
||||
self._sunnylink.render(self._rect)
|
||||
if self._sunnylink.completed:
|
||||
if not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
else:
|
||||
gui_app.set_modal_overlay(None)
|
||||
elif self._state == OnboardingState.ONBOARDING:
|
||||
if not self._training_done:
|
||||
self._training_guide.render(self._rect)
|
||||
else:
|
||||
gui_app.set_modal_overlay(None)
|
||||
elif self._state == OnboardingState.DECLINE:
|
||||
self._decline_page.render(self._rect)
|
||||
return -1
|
||||
|
||||
@@ -9,6 +9,8 @@ from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.sidebar import SidebarSP
|
||||
|
||||
SIDEBAR_WIDTH = 300
|
||||
METRIC_HEIGHT = 126
|
||||
METRIC_WIDTH = 240
|
||||
@@ -62,9 +64,10 @@ class MetricData:
|
||||
self.color = color
|
||||
|
||||
|
||||
class Sidebar(Widget):
|
||||
class Sidebar(Widget, SidebarSP):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
Widget.__init__(self)
|
||||
SidebarSP.__init__(self)
|
||||
self._net_type = NETWORK_TYPES.get(NetworkType.none)
|
||||
self._net_strength = 0
|
||||
|
||||
@@ -112,6 +115,7 @@ class Sidebar(Widget):
|
||||
self._update_temperature_status(device_state)
|
||||
self._update_connection_status(device_state)
|
||||
self._update_panda_status()
|
||||
SidebarSP._update_sunnylink_status(self)
|
||||
|
||||
def _update_network_status(self, device_state):
|
||||
self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, tr_noop("Unknown"))
|
||||
@@ -200,6 +204,13 @@ class Sidebar(Widget):
|
||||
rl.draw_text_ex(self._font_regular, tr(self._net_type), text_pos, FONT_SIZE, 0, Colors.WHITE)
|
||||
|
||||
def _draw_metrics(self, rect: rl.Rectangle):
|
||||
if gui_app.sunnypilot_ui():
|
||||
metrics, start_y, spacing = SidebarSP._draw_metrics_w_sunnylink(self, rect, self._temp_status, self._panda_status, self._connect_status)
|
||||
for idx, metric in enumerate(metrics):
|
||||
self._draw_metric(rect, metric, start_y + idx * spacing)
|
||||
|
||||
return
|
||||
|
||||
metrics = [(self._temp_status, 338), (self._panda_status, 496), (self._connect_status, 654)]
|
||||
|
||||
for metric, y_offset in metrics:
|
||||
|
||||
@@ -109,7 +109,7 @@ class MiciHomeLayout(Widget):
|
||||
self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 55, 35)
|
||||
self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 55, 35)
|
||||
|
||||
self._openpilot_label = MiciLabel("sunnypilot", font_size=96, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY)
|
||||
self._openpilot_label = MiciLabel("sunnypilot", font_size=90, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.AUDIOWIDE)
|
||||
self._version_label = MiciLabel("", font_size=36, font_weight=FontWeight.ROMAN)
|
||||
self._large_version_label = MiciLabel("", font_size=64, color=rl.GRAY, font_weight=FontWeight.ROMAN)
|
||||
self._date_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
|
||||
|
||||
@@ -17,13 +17,16 @@ from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.version import terms_version, training_version
|
||||
from openpilot.system.version import terms_version, training_version, terms_version_sp
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkOnboarding
|
||||
|
||||
|
||||
class OnboardingState(IntEnum):
|
||||
TERMS = 0
|
||||
ONBOARDING = 1
|
||||
DECLINE = 2
|
||||
SUNNYLINK_CONSENT = 3
|
||||
|
||||
|
||||
class DriverCameraSetupDialog(DriverCameraDialog):
|
||||
@@ -412,10 +415,10 @@ class TermsPage(SetupTermsPage):
|
||||
super().__init__(on_accept, on_decline, "decline")
|
||||
|
||||
info_txt = gui_app.texture("icons_mici/setup/green_info.png", 60, 60)
|
||||
self._title_header = TermsHeader("terms & conditions", info_txt)
|
||||
self._title_header = TermsHeader("terms of service", info_txt)
|
||||
|
||||
self._terms_label = UnifiedLabel("You must accept the Terms and Conditions to use sunnypilot. " +
|
||||
"Read the latest terms at https://comma.ai/terms before continuing.", 36,
|
||||
self._terms_label = UnifiedLabel("You must accept the Terms of Service to use sunnypilot. " +
|
||||
"Read the latest terms at https://sunnypilot.ai/terms before continuing.", 36,
|
||||
FontWeight.ROMAN)
|
||||
|
||||
@property
|
||||
@@ -449,6 +452,18 @@ class OnboardingWindow(Widget):
|
||||
self._training_guide = TrainingGuide(completed_callback=self._on_completed_training)
|
||||
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
|
||||
|
||||
# sunnylink consent pages
|
||||
self._accepted_terms = self._accepted_terms and ui_state.params.get("HasAcceptedTermsSP") == terms_version_sp
|
||||
self._sunnylink = SunnylinkOnboarding()
|
||||
if not self._accepted_terms:
|
||||
self._state = OnboardingState.TERMS
|
||||
elif not self._sunnylink.completed:
|
||||
self._state = OnboardingState.SUNNYLINK_CONSENT
|
||||
elif not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
else:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
device.set_override_interactive_timeout(300)
|
||||
@@ -459,7 +474,7 @@ class OnboardingWindow(Widget):
|
||||
|
||||
@property
|
||||
def completed(self) -> bool:
|
||||
return self._accepted_terms and self._training_done
|
||||
return self._accepted_terms and self._sunnylink.completed and self._training_done
|
||||
|
||||
def _on_terms_declined(self):
|
||||
self._state = OnboardingState.DECLINE
|
||||
@@ -473,7 +488,13 @@ class OnboardingWindow(Widget):
|
||||
|
||||
def _on_terms_accepted(self):
|
||||
ui_state.params.put("HasAcceptedTerms", terms_version)
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
ui_state.params.put("HasAcceptedTermsSP", terms_version_sp)
|
||||
if not self._sunnylink.completed:
|
||||
self._state = OnboardingState.SUNNYLINK_CONSENT
|
||||
elif not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
else:
|
||||
self.close()
|
||||
|
||||
def _on_completed_training(self):
|
||||
ui_state.params.put("CompletedTrainingVersion", training_version)
|
||||
@@ -482,8 +503,18 @@ class OnboardingWindow(Widget):
|
||||
def _render(self, _):
|
||||
if self._state == OnboardingState.TERMS:
|
||||
self._terms.render(self._rect)
|
||||
elif self._state == OnboardingState.SUNNYLINK_CONSENT:
|
||||
self._sunnylink.render(self._rect)
|
||||
if self._sunnylink.completed:
|
||||
if not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
else:
|
||||
self.close()
|
||||
elif self._state == OnboardingState.ONBOARDING:
|
||||
self._training_guide.render(self._rect)
|
||||
if not self._training_done:
|
||||
self._training_guide.render(self._rect)
|
||||
else:
|
||||
self.close()
|
||||
elif self._state == OnboardingState.DECLINE:
|
||||
self._decline_page.render(self._rect)
|
||||
return -1
|
||||
|
||||
@@ -80,6 +80,9 @@ class ModelRenderer(Widget):
|
||||
self._transform_dirty = True
|
||||
self._clip_region = None
|
||||
|
||||
self._counter = -1
|
||||
self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0
|
||||
|
||||
self._exp_gradient = Gradient(
|
||||
start=(0.0, 1.0), # Bottom of path
|
||||
end=(0.0, 0.0), # Top of path
|
||||
@@ -99,6 +102,10 @@ class ModelRenderer(Widget):
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
sm = ui_state.sm
|
||||
|
||||
if self._counter % 180 == 0: # This runs at 60fps, so we query every 3 seconds
|
||||
self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0
|
||||
self._counter += 1
|
||||
|
||||
self._torque_filter.update(-ui_state.sm['carOutput'].actuatorsOutput.torque)
|
||||
|
||||
# Check if data is up-to-date
|
||||
@@ -150,13 +157,13 @@ class ModelRenderer(Widget):
|
||||
|
||||
def _update_raw_points(self, model):
|
||||
"""Update raw 3D points from model data"""
|
||||
self._path.raw_points = np.array([model.position.x, model.position.y, model.position.z], dtype=np.float32).T
|
||||
self._path.raw_points = np.array([model.position.x, np.array(model.position.y) + self._camera_offset, model.position.z], dtype=np.float32).T
|
||||
|
||||
for i, lane_line in enumerate(model.laneLines):
|
||||
self._lane_lines[i].raw_points = np.array([lane_line.x, lane_line.y, lane_line.z], dtype=np.float32).T
|
||||
self._lane_lines[i].raw_points = np.array([lane_line.x, np.array(lane_line.y) + self._camera_offset, lane_line.z], dtype=np.float32).T
|
||||
|
||||
for i, road_edge in enumerate(model.roadEdges):
|
||||
self._road_edges[i].raw_points = np.array([road_edge.x, road_edge.y, road_edge.z], dtype=np.float32).T
|
||||
self._road_edges[i].raw_points = np.array([road_edge.x, np.array(road_edge.y) + self._camera_offset, road_edge.z], dtype=np.float32).T
|
||||
|
||||
self._lane_line_probs = np.array(model.laneLineProbs, dtype=np.float32)
|
||||
self._road_edge_stds = np.array(model.roadEdgeStds, dtype=np.float32)
|
||||
@@ -174,7 +181,7 @@ class ModelRenderer(Widget):
|
||||
|
||||
# Get z-coordinate from path at the lead vehicle position
|
||||
z = self._path.raw_points[idx, 2] if idx < len(self._path.raw_points) else 0.0
|
||||
point = self._map_to_screen(d_rel, -y_rel, z + self._path_offset_z)
|
||||
point = self._map_to_screen(d_rel, -y_rel + self._camera_offset, z + self._path_offset_z)
|
||||
if point:
|
||||
self._lead_vehicles[i] = self._update_lead_vehicle(d_rel, v_rel, point, self._rect)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from openpilot.common.transformations.orientation import rot_from_euler
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.hud_renderer import HudRendererSP as HudRenderer
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.driver_state import DriverStateRendererSP as DriverStateRenderer
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.augmented_road_view import BORDER_COLORS_SP
|
||||
|
||||
|
||||
@@ -56,7 +56,8 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
|
||||
self._road_edge_stds = np.zeros(2, dtype=np.float32)
|
||||
self._lead_vehicles = [LeadVehicle(), LeadVehicle()]
|
||||
self._path_offset_z = HEIGHT_INIT[0]
|
||||
|
||||
self._counter = -1
|
||||
self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0
|
||||
# Initialize ModelPoints objects
|
||||
self._path = ModelPoints()
|
||||
self._lane_lines = [ModelPoints() for _ in range(4)]
|
||||
@@ -103,6 +104,10 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
|
||||
live_calib = sm['liveCalibration']
|
||||
self._path_offset_z = live_calib.height[0] if live_calib.height else HEIGHT_INIT[0]
|
||||
|
||||
if self._counter % 60 == 0:
|
||||
self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0
|
||||
self._counter += 1
|
||||
|
||||
if sm.updated['carParams']:
|
||||
self._longitudinal_control = sm['carParams'].openpilotLongitudinalControl
|
||||
|
||||
@@ -136,13 +141,13 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
|
||||
|
||||
def _update_raw_points(self, model):
|
||||
"""Update raw 3D points from model data"""
|
||||
self._path.raw_points = np.array([model.position.x, model.position.y, model.position.z], dtype=np.float32).T
|
||||
self._path.raw_points = np.array([model.position.x, np.array(model.position.y) + self._camera_offset, model.position.z], dtype=np.float32).T
|
||||
|
||||
for i, lane_line in enumerate(model.laneLines):
|
||||
self._lane_lines[i].raw_points = np.array([lane_line.x, lane_line.y, lane_line.z], dtype=np.float32).T
|
||||
self._lane_lines[i].raw_points = np.array([lane_line.x, np.array(lane_line.y) + self._camera_offset, lane_line.z], dtype=np.float32).T
|
||||
|
||||
for i, road_edge in enumerate(model.roadEdges):
|
||||
self._road_edges[i].raw_points = np.array([road_edge.x, road_edge.y, road_edge.z], dtype=np.float32).T
|
||||
self._road_edges[i].raw_points = np.array([road_edge.x, np.array(road_edge.y) + self._camera_offset, road_edge.z], dtype=np.float32).T
|
||||
|
||||
self._lane_line_probs = np.array(model.laneLineProbs, dtype=np.float32)
|
||||
self._road_edge_stds = np.array(model.roadEdgeStds, dtype=np.float32)
|
||||
@@ -160,7 +165,7 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
|
||||
|
||||
# Get z-coordinate from path at the lead vehicle position
|
||||
z = self._path.raw_points[idx, 2] if idx < len(self._path.raw_points) else 0.0
|
||||
point = self._map_to_screen(d_rel, -y_rel, z + self._path_offset_z)
|
||||
point = self._map_to_screen(d_rel, -y_rel + self._camera_offset, z + self._path_offset_z)
|
||||
if point:
|
||||
self._lead_vehicles[i] = self._update_lead_vehicle(d_rel, v_rel, point, self._rect)
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
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.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
from openpilot.system.ui.widgets.label import Label
|
||||
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
|
||||
|
||||
|
||||
class SunnylinkConsentPage(Widget):
|
||||
def __init__(self, done_callback=None):
|
||||
super().__init__()
|
||||
self._done_callback = done_callback
|
||||
self._step = 0
|
||||
|
||||
self._title = Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
|
||||
self._content = [
|
||||
{
|
||||
"text": tr("sunnylink enables secured remote access to your comma device from anywhere, " +
|
||||
"including settings management, remote monitoring, real-time dashboard, etc."),
|
||||
"primary_btn": tr("Enable"),
|
||||
"secondary_btn": tr("Disable"),
|
||||
"highlight_primary": True
|
||||
},
|
||||
{
|
||||
"text": tr("sunnylink is designed to be enabled as part of sunnypilot's core functionality. " +
|
||||
"If sunnylink is disabled, features such as settings management, remote monitoring, " +
|
||||
"real-time dashboards will be unavailable."),
|
||||
"secondary_btn": tr("Back"),
|
||||
"danger_btn": tr("Disable"),
|
||||
"highlight_primary": True
|
||||
}
|
||||
]
|
||||
|
||||
self._primary_btn = Button("", button_style=ButtonStyle.PRIMARY, click_callback=lambda: self._handle_choice("enable"))
|
||||
self._secondary_btn = Button("", button_style=ButtonStyle.NORMAL, click_callback=lambda: self._handle_choice("secondary"))
|
||||
self._danger_btn = Button("", button_style=ButtonStyle.DANGER, click_callback=lambda: self._handle_choice("disable"))
|
||||
|
||||
def _handle_choice(self, choice):
|
||||
if choice == "enable":
|
||||
ui_state.params.put_bool("SunnylinkEnabled", True)
|
||||
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
|
||||
if self._done_callback:
|
||||
self._done_callback()
|
||||
elif choice == "secondary":
|
||||
if self._step == 0:
|
||||
self._step = 1
|
||||
elif self._step == 1:
|
||||
self._step = 0
|
||||
elif choice == "disable":
|
||||
ui_state.params.put_bool("SunnylinkEnabled", False)
|
||||
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_declined)
|
||||
if self._done_callback:
|
||||
self._done_callback()
|
||||
|
||||
def _render(self, _):
|
||||
step_data = self._content[self._step]
|
||||
|
||||
welcome_x = self._rect.x + 95
|
||||
welcome_y = self._rect.y + 165
|
||||
welcome_rect = rl.Rectangle(welcome_x, welcome_y, self._rect.width - welcome_x, 90)
|
||||
self._title.render(welcome_rect)
|
||||
|
||||
desc_x = welcome_x
|
||||
desc_y = welcome_y + 120
|
||||
desc_rect = rl.Rectangle(desc_x, desc_y, self._rect.width - desc_x, self._rect.height - desc_y - 250)
|
||||
|
||||
desc_label = Label(step_data["text"], font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
desc_label.render(desc_rect)
|
||||
|
||||
btn_y = self._rect.y + self._rect.height - 160 - 45
|
||||
|
||||
if "danger_btn" in step_data:
|
||||
btn_width = (self._rect.width - 45 * 3) / 2
|
||||
|
||||
self._secondary_btn.set_text(step_data["secondary_btn"])
|
||||
self._secondary_btn.render(rl.Rectangle(self._rect.x + 45, btn_y, btn_width, 160))
|
||||
|
||||
self._danger_btn.set_text(step_data["danger_btn"])
|
||||
self._danger_btn.render(rl.Rectangle(self._rect.x + 45 * 2 + btn_width, btn_y, btn_width, 160))
|
||||
|
||||
else:
|
||||
btn_width = (self._rect.width - 45 * 3) / 2
|
||||
|
||||
self._secondary_btn.set_text(step_data["secondary_btn"])
|
||||
self._secondary_btn.render(rl.Rectangle(self._rect.x + 45, btn_y, btn_width, 160))
|
||||
|
||||
self._primary_btn.set_text(step_data["primary_btn"])
|
||||
self._primary_btn.render(rl.Rectangle(self._rect.x + 45 * 2 + btn_width, btn_y, btn_width, 160))
|
||||
|
||||
return -1
|
||||
|
||||
|
||||
class SunnylinkOnboarding:
|
||||
def __init__(self):
|
||||
self.consent_page = SunnylinkConsentPage(done_callback=self._on_done)
|
||||
self.consent_done: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") in {sunnylink_consent_version, sunnylink_consent_declined}
|
||||
|
||||
@property
|
||||
def completed(self) -> bool:
|
||||
return self.consent_done
|
||||
|
||||
def _on_done(self):
|
||||
self.consent_done = True
|
||||
|
||||
def render(self, rect):
|
||||
if not self.consent_done:
|
||||
self.consent_page.render(rect)
|
||||
@@ -5,8 +5,216 @@ 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.layouts.settings.device import DeviceLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp, multiple_button_item_sp, button_item_sp, \
|
||||
dual_button_item_sp, Spacer
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
from openpilot.system.ui.widgets.button import ButtonStyle
|
||||
from openpilot.system.ui.widgets.confirm_dialog import alert_dialog, ConfirmDialog
|
||||
from openpilot.system.ui.widgets.list_view import text_item
|
||||
from openpilot.system.ui.widgets.scroller_tici import LineSeparator
|
||||
|
||||
offroad_time_options = {
|
||||
0: 0,
|
||||
1: 5,
|
||||
2: 10,
|
||||
3: 15,
|
||||
4: 30,
|
||||
5: 60,
|
||||
6: 120,
|
||||
7: 180,
|
||||
8: 300,
|
||||
9: 600,
|
||||
10: 1440,
|
||||
11: 1800,
|
||||
}
|
||||
|
||||
|
||||
class DeviceLayoutSP(DeviceLayout):
|
||||
def __init__(self):
|
||||
DeviceLayout.__init__(self)
|
||||
self._scroller._line_separator = None
|
||||
|
||||
def _initialize_items(self):
|
||||
DeviceLayout._initialize_items(self)
|
||||
|
||||
# Using dual button with no right button for better alignment
|
||||
self._always_offroad_btn = dual_button_item_sp(
|
||||
left_text=lambda: tr("Enable Always Offroad"),
|
||||
left_callback=self._handle_always_offroad,
|
||||
right_text="",
|
||||
right_callback=None,
|
||||
)
|
||||
self._always_offroad_btn.action_item.right_button.set_visible(False)
|
||||
|
||||
self._max_time_offroad = option_item_sp(
|
||||
title=lambda: tr("Max Time Offroad"),
|
||||
description=lambda: tr("Device will automatically shutdown after set time once the engine is turned off.\n(30h is the default)"),
|
||||
param="MaxTimeOffroad",
|
||||
min_value=0,
|
||||
max_value=11,
|
||||
value_change_step=1,
|
||||
on_value_changed=None,
|
||||
enabled=True,
|
||||
icon="",
|
||||
value_map=offroad_time_options,
|
||||
label_width=360,
|
||||
use_float_scaling=False,
|
||||
inline=True,
|
||||
label_callback=self._update_max_time_offroad_label
|
||||
)
|
||||
|
||||
self._device_wake_mode = multiple_button_item_sp(
|
||||
title=lambda: tr("Wake Up Behavior"),
|
||||
description=self.wake_mode_description,
|
||||
param="DeviceBootMode",
|
||||
buttons=[lambda: tr("Default"), lambda: tr("Offroad")],
|
||||
button_width=364,
|
||||
callback=None,
|
||||
inline=True,
|
||||
)
|
||||
|
||||
self._quiet_mode_and_dcam = dual_button_item_sp(
|
||||
left_text=lambda: tr("Quiet Mode"),
|
||||
right_text=lambda: tr("Driver Camera Preview"),
|
||||
left_callback=lambda: ui_state.params.put_bool("QuietMode", not ui_state.params.get_bool("QuietMode")),
|
||||
right_callback=self._show_driver_camera
|
||||
)
|
||||
self._quiet_mode_and_dcam.action_item.right_button.set_button_style(ButtonStyle.NORMAL)
|
||||
|
||||
self._reg_and_training = dual_button_item_sp(
|
||||
left_text=lambda: tr("Regulatory"),
|
||||
left_callback=self._on_regulatory,
|
||||
right_text=lambda: tr("Training Guide"),
|
||||
right_callback=self._on_review_training_guide
|
||||
)
|
||||
self._reg_and_training.action_item.right_button.set_button_style(ButtonStyle.NORMAL)
|
||||
|
||||
self._onroad_uploads_and_reset_settings = dual_button_item_sp(
|
||||
left_text=lambda: tr("Onroad Uploads"),
|
||||
left_callback=lambda: ui_state.params.put_bool("OnroadUploads", not ui_state.params.get_bool("OnroadUploads")),
|
||||
right_text=lambda: tr("Reset Settings"),
|
||||
right_callback=self._reset_settings
|
||||
)
|
||||
|
||||
self._power_buttons = dual_button_item_sp(
|
||||
left_text=lambda: tr("Reboot"),
|
||||
right_text=lambda: tr("Power Off"),
|
||||
left_callback=self._reboot_prompt,
|
||||
right_callback=self._power_off_prompt
|
||||
)
|
||||
|
||||
items = [
|
||||
text_item(lambda: tr("Dongle ID"), self._params.get("DongleId") or (lambda: tr("N/A"))),
|
||||
LineSeparator(),
|
||||
text_item(lambda: tr("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))),
|
||||
LineSeparator(),
|
||||
self._pair_device_btn,
|
||||
LineSeparator(),
|
||||
self._reset_calib_btn,
|
||||
LineSeparator(),
|
||||
button_item_sp(lambda: tr("Change Language"), lambda: tr("CHANGE"), callback=self._show_language_dialog),
|
||||
LineSeparator(),
|
||||
self._device_wake_mode,
|
||||
LineSeparator(),
|
||||
self._max_time_offroad,
|
||||
LineSeparator(height=10),
|
||||
self._quiet_mode_and_dcam,
|
||||
self._reg_and_training,
|
||||
self._onroad_uploads_and_reset_settings,
|
||||
Spacer(10),
|
||||
LineSeparator(height=10),
|
||||
self._power_buttons,
|
||||
]
|
||||
|
||||
return items
|
||||
|
||||
def _offroad_transition(self):
|
||||
self._power_buttons.action_item.right_button.set_visible(ui_state.is_offroad())
|
||||
|
||||
@staticmethod
|
||||
def wake_mode_description() -> str:
|
||||
def_str = tr("Default: Device will boot/wake-up normally & will be ready to engage.")
|
||||
offrd_str = tr("Offroad: Device will be in Always Offroad mode after boot/wake-up.")
|
||||
header = tr("Controls state of the device after boot/sleep.")
|
||||
|
||||
return f"{header}\n\n{def_str}\n{offrd_str}"
|
||||
|
||||
@staticmethod
|
||||
def _reset_settings():
|
||||
def _do_reset(result: int):
|
||||
if result == DialogResult.CONFIRM:
|
||||
for _key in ui_state.params.all_keys():
|
||||
ui_state.params.remove(_key)
|
||||
HARDWARE.reboot()
|
||||
|
||||
def _second_confirm(result: int):
|
||||
if result == DialogResult.CONFIRM:
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
text=tr("The reset cannot be undone. You have been warned."),
|
||||
confirm_text=tr("Confirm")
|
||||
), callback=_do_reset)
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
text=tr("Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back."),
|
||||
confirm_text=tr("Reset")
|
||||
), callback=_second_confirm)
|
||||
|
||||
@staticmethod
|
||||
def _handle_always_offroad():
|
||||
if ui_state.engaged:
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Enter Always Offroad Mode")))
|
||||
return
|
||||
|
||||
_offroad_mode_state = ui_state.params.get_bool("OffroadMode")
|
||||
_offroad_mode_str = tr("Are you sure you want to exit Always Offroad mode?") if _offroad_mode_state else \
|
||||
tr("Are you sure you want to enter Always Offroad mode?")
|
||||
|
||||
def _set_always_offroad(result: int):
|
||||
if result == DialogResult.CONFIRM and not ui_state.engaged:
|
||||
ui_state.params.put_bool("OffroadMode", not _offroad_mode_state)
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(_offroad_mode_str, tr("Confirm")), callback=lambda result: _set_always_offroad(result))
|
||||
|
||||
@staticmethod
|
||||
def _update_max_time_offroad_label(value: int) -> str:
|
||||
label = tr("Always On") if value == 0 else f"{value}" + tr("m") if value < 60 else f"{value // 60}" + tr("h")
|
||||
label += tr(" (Default)") if value == 1800 else ""
|
||||
return label
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
# Handle Always Offroad button
|
||||
always_offroad = ui_state.params.get_bool("OffroadMode")
|
||||
|
||||
# Text & Color
|
||||
offroad_mode_btn_text = tr("Exit Always Offroad") if always_offroad else tr("Enable Always Offroad")
|
||||
offroad_mode_btn_style = ButtonStyle.NORMAL if always_offroad else ButtonStyle.DANGER
|
||||
self._always_offroad_btn.action_item.left_button.set_text(offroad_mode_btn_text)
|
||||
self._always_offroad_btn.action_item.left_button.set_button_style(offroad_mode_btn_style)
|
||||
|
||||
# Position
|
||||
if self._scroller._items.__contains__(self._always_offroad_btn):
|
||||
self._scroller._items.remove(self._always_offroad_btn)
|
||||
if ui_state.is_offroad() and not always_offroad:
|
||||
self._scroller._items.insert(len(self._scroller._items) - 1, self._always_offroad_btn)
|
||||
else:
|
||||
self._scroller._items.insert(0, self._always_offroad_btn)
|
||||
|
||||
# Quiet Mode button
|
||||
self._quiet_mode_and_dcam.action_item.left_button.set_button_style(ButtonStyle.PRIMARY if ui_state.params.get_bool("QuietMode") else ButtonStyle.NORMAL)
|
||||
|
||||
# Onroad Uploads
|
||||
self._onroad_uploads_and_reset_settings.action_item.left_button.set_button_style(
|
||||
ButtonStyle.PRIMARY if ui_state.params.get_bool("OnroadUploads") else ButtonStyle.NORMAL
|
||||
)
|
||||
|
||||
# Offroad only buttons
|
||||
self._quiet_mode_and_dcam.action_item.right_button.set_enabled(ui_state.is_offroad())
|
||||
self._reg_and_training.action_item.left_button.set_enabled(ui_state.is_offroad())
|
||||
self._reg_and_training.action_item.right_button.set_enabled(ui_state.is_offroad())
|
||||
self._onroad_uploads_and_reset_settings.action_item.right_button.set_enabled(ui_state.is_offroad())
|
||||
|
||||
@@ -9,28 +9,28 @@ from enum import IntEnum
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.layouts.settings import settings as OP
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayoutSP
|
||||
from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.software import SoftwareLayoutSP
|
||||
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr_noop
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise import CruiseLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.developer import DeveloperLayoutSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayoutSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import DisplayLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.network import NetworkUISP
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.sunnylink import SunnylinkLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.osm import OSMLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.software import SoftwareLayoutSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering import SteeringLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.sunnylink import SunnylinkLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.trips import TripsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle import VehicleLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering import SteeringLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise import CruiseLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.visuals import VisualsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import DisplayLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.developer import DeveloperLayoutSP
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr_noop
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
# from openpilot.selfdrive.ui.sunnypilot.layouts.settings.navigation import NavigationLayout
|
||||
|
||||
|
||||
@@ -4,23 +4,23 @@ 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 cereal import custom
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkConsentPage
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
from openpilot.system.ui.sunnypilot.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.button import ButtonStyle, Button
|
||||
from openpilot.system.ui.widgets.confirm_dialog import alert_dialog, ConfirmDialog
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.list_view import button_item, dual_button_item
|
||||
from openpilot.system.ui.widgets.list_view import dual_button_item
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller, LineSeparator
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
import pyray as rl
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp as button_item
|
||||
from openpilot.system.version import sunnylink_consent_version
|
||||
|
||||
|
||||
class SunnylinkHeader(Widget):
|
||||
@@ -160,14 +160,14 @@ class SunnylinkLayout(Widget):
|
||||
self._sunnylink_description = SunnylinkDescriptionItem()
|
||||
self._sunnylink_description.set_visible(False)
|
||||
|
||||
self._sponsor_btn = button_item(
|
||||
self._sponsor_btn = button_item_sp(
|
||||
title=tr("Sponsor Status"),
|
||||
button_text=tr("SPONSOR"),
|
||||
description=tr(
|
||||
"Become a sponsor of sunnypilot to get early access to sunnylink features when they become available."),
|
||||
callback=lambda: self._handle_pair_btn(False)
|
||||
)
|
||||
self._pair_btn = button_item(
|
||||
self._pair_btn = button_item_sp(
|
||||
title=tr("Pair GitHub Account"),
|
||||
button_text=tr("Not Paired"),
|
||||
description=tr(
|
||||
@@ -209,8 +209,8 @@ class SunnylinkLayout(Widget):
|
||||
return items
|
||||
|
||||
@staticmethod
|
||||
def _get_sunnylink_dongle_id() -> str | None:
|
||||
return str(ui_state.params.get("SunnylinkDongleId") or (lambda: tr("N/A")))
|
||||
def _get_sunnylink_dongle_id() -> str:
|
||||
return ui_state.params.get("SunnylinkDongleId") or tr("N/A")
|
||||
|
||||
def _handle_pair_btn(self, sponsor_pairing: bool = False):
|
||||
sunnylink_dongle_id = self._get_sunnylink_dongle_id()
|
||||
@@ -302,6 +302,22 @@ class SunnylinkLayout(Widget):
|
||||
self._restore_btn.set_text(tr("Restore Settings"))
|
||||
|
||||
def _sunnylink_toggle_callback(self, state: bool):
|
||||
sl_consent: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") == sunnylink_consent_version
|
||||
sl_enabled: bool = ui_state.params.get_bool("SunnylinkEnabled")
|
||||
|
||||
if state and not sl_consent and not sl_enabled:
|
||||
def on_consent_done():
|
||||
enabled = ui_state.params.get_bool("SunnylinkEnabled")
|
||||
self._update_description(enabled)
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
sl_terms_dlg = SunnylinkConsentPage(done_callback=on_consent_done)
|
||||
gui_app.set_modal_overlay(sl_terms_dlg)
|
||||
else:
|
||||
ui_state.params.put_bool("SunnylinkEnabled", state)
|
||||
self._update_description(state)
|
||||
|
||||
def _update_description(self, state: bool):
|
||||
if state:
|
||||
description = tr(
|
||||
"Welcome back!! We're excited to see you've enabled sunnylink again!")
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
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
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||
from openpilot.system.ui.lib.multilang import tr_noop
|
||||
|
||||
|
||||
PING_TIMEOUT_NS = 80_000_000_000 # 80 seconds in nanoseconds
|
||||
METRIC_HEIGHT = 126
|
||||
METRIC_MARGIN = 30
|
||||
METRIC_START_Y = 300
|
||||
HOME_BTN = rl.Rectangle(60, 860, 180, 180)
|
||||
|
||||
|
||||
# Color scheme
|
||||
class Colors:
|
||||
WHITE = rl.WHITE
|
||||
WHITE_DIM = rl.Color(255, 255, 255, 85)
|
||||
GRAY = rl.Color(84, 84, 84, 255)
|
||||
|
||||
# Status colors
|
||||
GOOD = rl.WHITE
|
||||
WARNING = rl.Color(218, 202, 37, 255)
|
||||
DANGER = rl.Color(201, 34, 49, 255)
|
||||
PROGRESS = rl.Color(0, 134, 233, 255)
|
||||
DISABLED = rl.Color(128, 128, 128, 255)
|
||||
|
||||
# UI elements
|
||||
METRIC_BORDER = rl.Color(255, 255, 255, 85)
|
||||
BUTTON_NORMAL = rl.WHITE
|
||||
BUTTON_PRESSED = rl.Color(255, 255, 255, 166)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class MetricData:
|
||||
label: str
|
||||
value: str
|
||||
color: rl.Color
|
||||
|
||||
def update(self, label: str, value: str, color: rl.Color):
|
||||
self.label = label
|
||||
self.value = value
|
||||
self.color = color
|
||||
|
||||
|
||||
class SidebarSP:
|
||||
def __init__(self):
|
||||
self._sunnylink_status = MetricData(tr_noop("SUNNYLINK"), tr_noop("OFFLINE"), Colors.WARNING)
|
||||
|
||||
def _update_sunnylink_status(self):
|
||||
if not ui_state.params.get_bool("SunnylinkEnabled"):
|
||||
self._sunnylink_status.update(tr_noop("SUNNYLINK"), tr_noop("DISABLED"), Colors.DISABLED)
|
||||
return
|
||||
|
||||
last_ping = ui_state.params.get("LastSunnylinkPingTime") or 0
|
||||
dongle_id = ui_state.params.get("SunnylinkDongleId")
|
||||
|
||||
is_online = last_ping and (time.monotonic_ns() - last_ping) < PING_TIMEOUT_NS
|
||||
is_temp_fault = ui_state.params.get_bool("SunnylinkTempFault")
|
||||
is_registering = not is_temp_fault and dongle_id in (None, "", UNREGISTERED_SUNNYLINK_DONGLE_ID)
|
||||
|
||||
# Determine status/color pair based on priority
|
||||
if last_ping:
|
||||
status, color = (tr_noop("ONLINE"), Colors.GOOD) if is_online else (tr_noop("ERROR"), Colors.DANGER)
|
||||
elif is_temp_fault:
|
||||
status, color = (tr_noop("FAULT"), Colors.WARNING)
|
||||
elif is_registering:
|
||||
status, color = (tr_noop("REGIST..."), Colors.PROGRESS)
|
||||
else:
|
||||
status, color = (tr_noop("OFFLINE"), Colors.DANGER)
|
||||
|
||||
self._sunnylink_status.update(tr_noop("SUNNYLINK"), status, color)
|
||||
|
||||
def _draw_metrics_w_sunnylink(self, rect: rl.Rectangle, _temp, _panda, _connect):
|
||||
metrics = [_temp, _panda, _connect, self._sunnylink_status]
|
||||
start_y = int(rect.y) + METRIC_START_Y
|
||||
available_height = max(0, int(HOME_BTN.y) - METRIC_MARGIN - METRIC_HEIGHT - start_y)
|
||||
spacing = available_height / max(1, len(metrics) - 1)
|
||||
|
||||
return metrics, start_y, spacing
|
||||
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
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 FontWeight, gui_app
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.slider import SmallSlider
|
||||
from openpilot.system.ui.mici_setup import TermsHeader, TermsPage as SetupTermsPage
|
||||
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
|
||||
class SunnylinkConsentPage(SetupTermsPage):
|
||||
def __init__(self, on_accept=None, on_decline=None, left_text: str = "disable", right_text: str = "enable"):
|
||||
super().__init__(on_accept, on_decline, left_text, continue_text=right_text)
|
||||
|
||||
self._title_header = TermsHeader("sunnylink",
|
||||
gui_app.texture("../../sunnypilot/selfdrive/assets/logo.png", 66, 60))
|
||||
|
||||
self._terms_label = UnifiedLabel("sunnylink enables secured remote access to your comma device from anywhere, " +
|
||||
"including settings management, remote monitoring, real-time dashboard, etc.",
|
||||
36, FontWeight.ROMAN)
|
||||
|
||||
@property
|
||||
def _content_height(self):
|
||||
return self._terms_label.rect.y + self._terms_label.rect.height - self._scroll_panel.get_offset()
|
||||
|
||||
def _render(self, _):
|
||||
super()._render(_)
|
||||
return -1
|
||||
|
||||
def _render_content(self, scroll_offset):
|
||||
self._title_header.set_position(self._rect.x + 16, self._rect.y + 12 + scroll_offset)
|
||||
self._title_header.render()
|
||||
|
||||
self._terms_label.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._title_header.rect.y + self._title_header.rect.height + self.ITEM_SPACING,
|
||||
self._rect.width - 100,
|
||||
self._terms_label.get_content_height(int(self._rect.width - 100)),
|
||||
))
|
||||
|
||||
|
||||
class SunnylinkConsentDisableConfirmPage(SunnylinkConsentPage):
|
||||
def __init__(self, on_accept=None, on_decline=None):
|
||||
super().__init__(on_accept=on_decline, on_decline=on_accept, left_text="enable", right_text="disable")
|
||||
|
||||
# we flip the continue & disable buttons to use slider for disable
|
||||
self._continue_slider = True
|
||||
self._continue_button = SmallSlider("disable", confirm_callback=on_decline)
|
||||
self._scroll_panel.set_enabled(lambda: not self._continue_button.is_pressed)
|
||||
|
||||
self._title_header = TermsHeader("disable sunnylink?",
|
||||
gui_app.texture("icons_mici/setup/red_warning.png", 66, 60))
|
||||
|
||||
self._terms_label = UnifiedLabel("sunnylink is designed to be enabled as part of sunnypilot's core functionality. " +
|
||||
"If sunnylink is disabled, features such as settings management, " +
|
||||
"remote monitoring, real-time dashboards will be unavailable.",
|
||||
36, FontWeight.ROMAN)
|
||||
|
||||
|
||||
class SunnylinkOnboarding:
|
||||
def __init__(self):
|
||||
self.consent_done: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") in {sunnylink_consent_version, sunnylink_consent_declined}
|
||||
self.disable_confirm = False
|
||||
|
||||
self.consent_page = SunnylinkConsentPage(on_decline=self._on_decline, on_accept=self._on_accept)
|
||||
self.confirm_page = SunnylinkConsentDisableConfirmPage(on_decline=self._on_confirm_decline, on_accept=self._on_accept)
|
||||
|
||||
@property
|
||||
def completed(self) -> bool:
|
||||
return self.consent_done
|
||||
|
||||
def _on_accept(self):
|
||||
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
|
||||
ui_state.params.put_bool("SunnylinkEnabled", True)
|
||||
self.consent_done = True
|
||||
|
||||
def _on_decline(self):
|
||||
self.disable_confirm = True
|
||||
|
||||
def _on_confirm_decline(self):
|
||||
ui_state.params.put_bool("SunnylinkEnabled", False)
|
||||
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_declined)
|
||||
self.consent_done = True
|
||||
|
||||
def render(self, rect):
|
||||
if self.consent_done:
|
||||
return
|
||||
|
||||
if self.disable_confirm:
|
||||
self.confirm_page.render(rect)
|
||||
else:
|
||||
self.consent_page.render(rect)
|
||||
@@ -8,16 +8,17 @@ from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
from cereal import custom
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
|
||||
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos
|
||||
from openpilot.system.ui.widgets import NavWidget
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkConsentPage
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import NavWidget
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
|
||||
|
||||
|
||||
class SunnylinkLayoutMici(NavWidget):
|
||||
@@ -28,9 +29,9 @@ class SunnylinkLayoutMici(NavWidget):
|
||||
self._backup_in_progress = False
|
||||
self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled")
|
||||
|
||||
self._sunnylink_toggle = BigToggle(text="",
|
||||
self._sunnylink_toggle = BigToggle(text=tr("enable sunnylink"),
|
||||
initial_state=self._sunnylink_enabled,
|
||||
toggle_callback=SunnylinkLayoutMici._sunnylink_toggle_callback)
|
||||
toggle_callback=self._sunnylink_toggle_callback)
|
||||
self._sunnylink_sponsor_button = SunnylinkPairBigButton(sponsor_pairing=False)
|
||||
self._sunnylink_pair_button = SunnylinkPairBigButton(sponsor_pairing=True)
|
||||
self._backup_btn = BigButton(tr("backup settings"), "", "")
|
||||
@@ -38,7 +39,7 @@ class SunnylinkLayoutMici(NavWidget):
|
||||
self._restore_btn = BigButton(tr("restore settings"), "", "")
|
||||
self._restore_btn.set_click_callback(lambda: self._handle_backup_restore_btn(restore=True))
|
||||
self._sunnylink_uploader_toggle = BigToggle(text=tr("sunnylink uploader"), initial_state=False,
|
||||
toggle_callback=SunnylinkLayoutMici._sunnylink_uploader_callback)
|
||||
toggle_callback=self._sunnylink_uploader_callback)
|
||||
|
||||
self._scroller = Scroller([
|
||||
self._sunnylink_toggle,
|
||||
@@ -51,8 +52,8 @@ class SunnylinkLayoutMici(NavWidget):
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
self._sunnylink_enabled = ui_state.sunnylink_enabled
|
||||
self._sunnylink_toggle.set_text(tr("enable sunnylink"))
|
||||
self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled")
|
||||
self._sunnylink_toggle.set_checked(self._sunnylink_enabled)
|
||||
self._sunnylink_pair_button.set_visible(self._sunnylink_enabled)
|
||||
self._sunnylink_sponsor_button.set_visible(self._sunnylink_enabled)
|
||||
self._backup_btn.set_visible(self._sunnylink_enabled)
|
||||
@@ -83,7 +84,25 @@ class SunnylinkLayoutMici(NavWidget):
|
||||
|
||||
@staticmethod
|
||||
def _sunnylink_toggle_callback(state: bool):
|
||||
ui_state.params.put_bool("SunnylinkEnabled", state)
|
||||
sl_consent: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") == sunnylink_consent_version
|
||||
sl_enabled: bool = ui_state.params.get("SunnylinkEnabled")
|
||||
|
||||
def sl_terms_accepted():
|
||||
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
|
||||
ui_state.params.put_bool("SunnylinkEnabled", True)
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
def sl_terms_declined():
|
||||
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_declined)
|
||||
ui_state.params.put_bool("SunnylinkEnabled", False)
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
if state and not sl_consent and not sl_enabled:
|
||||
sl_terms_dlg = SunnylinkConsentPage(on_accept=sl_terms_accepted, on_decline=sl_terms_declined)
|
||||
gui_app.set_modal_overlay(sl_terms_dlg)
|
||||
else:
|
||||
ui_state.params.put_bool("SunnylinkEnabled", state)
|
||||
|
||||
ui_state.update_params()
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -22,6 +22,7 @@ class DeveloperUiRenderer(Widget):
|
||||
DEV_UI_RIGHT = 1
|
||||
DEV_UI_BOTTOM = 2
|
||||
DEV_UI_BOTH = 3
|
||||
BOTTOM_BAR_HEIGHT = 61
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -43,6 +44,12 @@ class DeveloperUiRenderer(Widget):
|
||||
self.bearing_elem = BearingDegElement()
|
||||
self.altitude_elem = AltitudeElement()
|
||||
|
||||
@staticmethod
|
||||
def get_bottom_dev_ui_offset():
|
||||
if ui_state.developer_ui in (DeveloperUiRenderer.DEV_UI_BOTTOM, DeveloperUiRenderer.DEV_UI_BOTH):
|
||||
return DeveloperUiRenderer.BOTTOM_BAR_HEIGHT
|
||||
return 0
|
||||
|
||||
def _update_state(self) -> None:
|
||||
self.dev_ui_mode = ui_state.developer_ui
|
||||
|
||||
@@ -78,10 +85,11 @@ class DeveloperUiRenderer(Widget):
|
||||
]
|
||||
if controls_state.lateralControlState.which() == 'torqueState':
|
||||
elements.append(self.desired_lat_accel_elem.update(sm, ui_state.is_metric))
|
||||
elements.append(self.actual_lat_accel_elem.update(sm, ui_state.is_metric))
|
||||
else:
|
||||
elements.append(self.desired_steer_elem.update(sm, ui_state.is_metric))
|
||||
|
||||
elements.append(self.actual_lat_accel_elem.update(sm, ui_state.is_metric))
|
||||
|
||||
current_y = y
|
||||
for element in elements:
|
||||
current_y += self._draw_right_dev_ui_element(x, current_y, element)
|
||||
@@ -105,7 +113,7 @@ class DeveloperUiRenderer(Widget):
|
||||
if element.unit:
|
||||
units_height = measure_text_cached(self._font_bold, element.unit, unit_size, 0).x
|
||||
|
||||
units_x = x + container_width - 10
|
||||
units_x = x + container_width
|
||||
units_y = y + (value_size / 2) + (units_height / 2)
|
||||
|
||||
rl.draw_text_pro(self._font_bold, element.unit, rl.Vector2(units_x, units_y), rl.Vector2(0, 0), -90.0, unit_size, 0, rl.WHITE)
|
||||
@@ -143,22 +151,35 @@ class DeveloperUiRenderer(Widget):
|
||||
if sm.valid['gpsLocationExternal'] or sm.valid['gpsLocation']:
|
||||
elements.append(self.altitude_elem.update(sm, ui_state.is_metric))
|
||||
|
||||
current_x = int(rect.x + 90)
|
||||
center_y = y + bar_height // 2
|
||||
for element in elements:
|
||||
current_x += self._draw_bottom_dev_ui_element(current_x, center_y, element)
|
||||
if not elements:
|
||||
return
|
||||
|
||||
def _draw_bottom_dev_ui_element(self, x: int, y: int, element: UiElement) -> int:
|
||||
font_size = 38
|
||||
element_widths = []
|
||||
for element in elements:
|
||||
element.measure(self._font_bold, font_size)
|
||||
element_widths.append(element.total_width)
|
||||
|
||||
label_text = f"{element.label} "
|
||||
label_width = measure_text_cached(self._font_bold, label_text, font_size, 0).x
|
||||
rl.draw_text_ex(self._font_bold, label_text, rl.Vector2(x, y - font_size // 2), font_size, 0, rl.WHITE)
|
||||
total_element_width = sum(element_widths)
|
||||
num_gaps = len(elements) + 1
|
||||
available_width = rect.width
|
||||
gap_width = (available_width - total_element_width) / num_gaps
|
||||
|
||||
value_width = measure_text_cached(self._font_bold, element.value, font_size, 0).x
|
||||
rl.draw_text_ex(self._font_bold, element.value, rl.Vector2(x + label_width + 10, y - font_size // 2), font_size, 0, element.color)
|
||||
center_y = y + bar_height // 2
|
||||
current_x = rect.x + gap_width
|
||||
|
||||
for i, element in enumerate(elements):
|
||||
element_center_x = int(current_x + element_widths[i] / 2)
|
||||
self._draw_bottom_dev_ui_element(element_center_x, center_y, element)
|
||||
current_x += element_widths[i] + gap_width
|
||||
|
||||
def _draw_bottom_dev_ui_element(self, center_x: int, y: int, element: UiElement) -> None:
|
||||
font_size = 38
|
||||
start_x = center_x - element.total_width / 2
|
||||
|
||||
rl.draw_text_ex(self._font_bold, element.label_text, rl.Vector2(start_x, y - font_size // 2), font_size, 0, rl.WHITE)
|
||||
rl.draw_text_ex(self._font_bold, element.val_text, rl.Vector2(start_x + element.label_width, y - font_size // 2), font_size, 0, element.color)
|
||||
|
||||
if element.unit:
|
||||
rl.draw_text_ex(self._font_bold, element.unit, rl.Vector2(x + label_width + value_width + 20, y - font_size // 2), font_size, 0, rl.WHITE)
|
||||
|
||||
return 400
|
||||
rl.draw_text_ex(self._font_bold, element.unit_text, rl.Vector2(start_x + element.label_width + element.val_width, y - font_size // 2),
|
||||
font_size, 0, rl.WHITE)
|
||||
|
||||
@@ -10,12 +10,33 @@ from dataclasses import dataclass
|
||||
from openpilot.common.constants import CV
|
||||
|
||||
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
|
||||
|
||||
@dataclass
|
||||
class UiElement:
|
||||
value: str
|
||||
label: str
|
||||
unit: str
|
||||
color: rl.Color
|
||||
val_text: str = ""
|
||||
label_text: str = ""
|
||||
unit_text: str = ""
|
||||
val_width: float = 0.0
|
||||
label_width: float = 0.0
|
||||
unit_width: float = 0.0
|
||||
total_width: float = 0.0
|
||||
|
||||
def measure(self, font, font_size: int):
|
||||
self.label_text = f"{self.label} "
|
||||
self.val_text = self.value
|
||||
self.unit_text = f" {self.unit}" if self.unit else ""
|
||||
|
||||
self.label_width = measure_text_cached(font, self.label_text, font_size, 0).x
|
||||
self.val_width = measure_text_cached(font, self.val_text, font_size, 0).x
|
||||
self.unit_width = measure_text_cached(font, self.unit_text, font_size, 0).x if self.unit else 0
|
||||
|
||||
self.total_width = self.label_width + self.val_width + self.unit_width
|
||||
|
||||
|
||||
class LeadInfoElement:
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
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 numpy as np
|
||||
|
||||
from openpilot.selfdrive.ui import UI_BORDER_SIZE
|
||||
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer, BTN_SIZE, ARC_LENGTH
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer
|
||||
|
||||
|
||||
class DriverStateRendererSP(DriverStateRenderer):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.dev_ui_offset = DeveloperUiRenderer.get_bottom_dev_ui_offset()
|
||||
|
||||
def _pre_calculate_drawing_elements(self):
|
||||
"""Pre-calculate all drawing elements based on the current rectangle"""
|
||||
# Calculate icon position (bottom-left or bottom-right)
|
||||
width, height = self._rect.width, self._rect.height
|
||||
offset = UI_BORDER_SIZE + BTN_SIZE // 2
|
||||
self.position_x = self._rect.x + (width - offset if self.is_rhd else offset)
|
||||
self.position_y = self._rect.y + height - offset - self.dev_ui_offset
|
||||
|
||||
# Pre-calculate the face lines positions
|
||||
positioned_keypoints = self.face_keypoints_transformed + np.array([self.position_x, self.position_y])
|
||||
for i in range(len(positioned_keypoints)):
|
||||
self.face_lines[i].x = positioned_keypoints[i][0]
|
||||
self.face_lines[i].y = positioned_keypoints[i][1]
|
||||
|
||||
# Calculate arc dimensions based on head rotation
|
||||
delta_x = -self.driver_pose_sins[1] * ARC_LENGTH / 2.0 # Horizontal movement
|
||||
delta_y = -self.driver_pose_sins[0] * ARC_LENGTH / 2.0 # Vertical movement
|
||||
|
||||
# Horizontal arc
|
||||
h_width = abs(delta_x)
|
||||
self.h_arc_data = self._calculate_arc_data(
|
||||
delta_x, h_width, self.position_x, self.position_y - ARC_LENGTH / 2,
|
||||
self.driver_pose_sins[1], self.driver_pose_diff[1], is_horizontal=True
|
||||
)
|
||||
|
||||
# Vertical arc
|
||||
v_height = abs(delta_y)
|
||||
self.v_arc_data = self._calculate_arc_data(
|
||||
delta_y, v_height, self.position_x - ARC_LENGTH / 2, self.position_y,
|
||||
self.driver_pose_sins[0], self.driver_pose_diff[0], is_horizontal=False
|
||||
)
|
||||
@@ -22,8 +22,14 @@ class UIStateSP:
|
||||
|
||||
self.sunnylink_state = SunnylinkState()
|
||||
|
||||
self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True)
|
||||
self.global_brightness_override: int = self.params.get("Brightness", return_default=True)
|
||||
|
||||
def update(self) -> None:
|
||||
self.sunnylink_state.start()
|
||||
if self.sunnylink_enabled:
|
||||
self.sunnylink_state.start()
|
||||
else:
|
||||
self.sunnylink_state.stop()
|
||||
|
||||
@staticmethod
|
||||
def update_status(ss, ss_sp, onroad_evt) -> str:
|
||||
@@ -70,3 +76,34 @@ class UIStateSP:
|
||||
self.developer_ui = self.params.get("DevUIInfo")
|
||||
self.rainbow_path = self.params.get_bool("RainbowMode")
|
||||
self.chevron_metrics = self.params.get("ChevronInfo")
|
||||
self.active_bundle = self.params.get("ModelManager_ActiveBundle")
|
||||
self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True)
|
||||
self.global_brightness_override = self.params.get("Brightness", return_default=True)
|
||||
|
||||
|
||||
class DeviceSP:
|
||||
def __init__(self):
|
||||
self._params = Params()
|
||||
|
||||
def _set_awake(self, on: bool):
|
||||
if on and self._params.get("DeviceBootMode", return_default=True) == 1:
|
||||
self._params.put_bool("OffroadMode", True)
|
||||
|
||||
@staticmethod
|
||||
def update_max_global_brightness(brightness_override: int) -> float:
|
||||
"""
|
||||
Updates the max global brightness by constraining the value to a predefined range.
|
||||
|
||||
The method takes an integer `brightness` value, adjusts it to ensure it is within the
|
||||
range of 30 to 100, inclusive, and returns the adjusted value as a float.
|
||||
|
||||
This method only runs if 0 (Auto) is not selected.
|
||||
|
||||
:param brightness_override: The desired brightness level. It is constrained between
|
||||
a minimum of 30 and a maximum of 100.
|
||||
:type brightness_override: int
|
||||
:return: The brightness value adjusted to fit within the allowable range,
|
||||
converted to a float.
|
||||
:rtype: float
|
||||
"""
|
||||
return float(min(max(brightness_override, 30), 100))
|
||||
|
||||
@@ -13,7 +13,7 @@ if "RECORD_OUTPUT" not in os.environ:
|
||||
os.environ["RECORD_OUTPUT"] = os.path.join(DIFF_OUT_DIR, os.environ["RECORD_OUTPUT"])
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.version import terms_version, training_version
|
||||
from openpilot.system.version import terms_version, training_version, terms_version_sp, sunnylink_consent_version
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
||||
@@ -45,6 +45,8 @@ def setup_state():
|
||||
params.put("CompletedTrainingVersion", training_version)
|
||||
params.put("DongleId", "test123456789")
|
||||
params.put("UpdaterCurrentDescription", "0.10.1 / test-branch / abc1234 / Nov 30")
|
||||
params.put("HasAcceptedTermsSP", terms_version_sp)
|
||||
params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.selfdrive.test.helpers import with_processes
|
||||
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
||||
from openpilot.system.updated.updated import parse_release_notes
|
||||
from openpilot.system.version import terms_version, training_version
|
||||
from openpilot.system.version import terms_version, training_version, terms_version_sp, sunnylink_consent_version
|
||||
|
||||
AlertSize = log.SelfdriveState.AlertSize
|
||||
AlertStatus = log.SelfdriveState.AlertStatus
|
||||
@@ -378,6 +378,8 @@ def create_screenshots():
|
||||
# Set terms and training version (to skip onboarding)
|
||||
params.put("HasAcceptedTerms", terms_version)
|
||||
params.put("CompletedTrainingVersion", training_version)
|
||||
params.put("HasAcceptedTermsSP", terms_version_sp)
|
||||
params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
|
||||
|
||||
if name == "homescreen_paired":
|
||||
params.put("PrimeType", 0) # NONE
|
||||
|
||||
@@ -12,7 +12,7 @@ from openpilot.selfdrive.ui.lib.prime_state import PrimeState
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.hardware import HARDWARE, PC
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP
|
||||
|
||||
BACKLIGHT_OFFROAD = 65 if HARDWARE.get_device_type() == "mici" else 50
|
||||
|
||||
@@ -192,8 +192,9 @@ class UIState(UIStateSP):
|
||||
self._param_update_time = time.monotonic()
|
||||
|
||||
|
||||
class Device:
|
||||
class Device(DeviceSP):
|
||||
def __init__(self):
|
||||
DeviceSP.__init__(self)
|
||||
self._ignition = False
|
||||
self._interaction_time: float = -1
|
||||
self._override_interactive_timeout: int | None = None
|
||||
@@ -220,6 +221,9 @@ class Device:
|
||||
if self._override_interactive_timeout is not None:
|
||||
return self._override_interactive_timeout
|
||||
|
||||
if gui_app.sunnypilot_ui() and ui_state.custom_interactive_timeout != 0:
|
||||
return ui_state.custom_interactive_timeout
|
||||
|
||||
ignition_timeout = 10 if gui_app.big_ui() else 5
|
||||
return ignition_timeout if ui_state.ignition else 30
|
||||
|
||||
@@ -257,6 +261,10 @@ class Device:
|
||||
clipped_brightness = float(np.interp(clipped_brightness, [0, 1], [30, 100]))
|
||||
|
||||
brightness = round(self._brightness_filter.update(clipped_brightness))
|
||||
|
||||
if gui_app.sunnypilot_ui() and ui_state.global_brightness_override != 0:
|
||||
brightness = DeviceSP.update_max_global_brightness(ui_state.global_brightness_override)
|
||||
|
||||
if not self._awake:
|
||||
brightness = 0
|
||||
|
||||
@@ -284,6 +292,7 @@ class Device:
|
||||
|
||||
def _set_awake(self, on: bool):
|
||||
if on != self._awake:
|
||||
DeviceSP._set_awake(self, on)
|
||||
self._awake = on
|
||||
cloudlog.debug(f"setting display power {int(on)}")
|
||||
HARDWARE.set_display_power(on)
|
||||
|
||||
@@ -24,6 +24,7 @@ from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld.runners import ModelRunner, Runtime
|
||||
from openpilot.sunnypilot.modeld.parse_model_outputs import Parser
|
||||
from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
|
||||
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
|
||||
from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan
|
||||
from openpilot.sunnypilot.models.helpers import get_active_bundle, get_model_path, load_metadata, prepare_inputs, load_meta_constants
|
||||
from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext
|
||||
@@ -195,6 +196,7 @@ def main(demo=False):
|
||||
buf_main, buf_extra = None, None
|
||||
meta_main = FrameMeta()
|
||||
meta_extra = FrameMeta()
|
||||
camera_offset_helper = CameraOffsetHelper()
|
||||
|
||||
|
||||
if demo:
|
||||
@@ -250,12 +252,14 @@ def main(demo=False):
|
||||
frame_id = sm["roadCameraState"].frameId
|
||||
if sm.frame % 60 == 0:
|
||||
model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay)
|
||||
camera_offset_helper.set_offset(params.get("CameraOffset", return_default=True))
|
||||
lat_delay = model.lat_delay + model.LAT_SMOOTH_SECONDS
|
||||
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
|
||||
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
|
||||
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
|
||||
model_transform_main = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics, False).astype(np.float32)
|
||||
model_transform_extra = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics, True).astype(np.float32)
|
||||
model_transform_main, model_transform_extra = camera_offset_helper.update(model_transform_main, model_transform_extra, sm, main_wide_camera)
|
||||
live_calib_seen = True
|
||||
|
||||
traffic_convention = np.zeros(2)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import glob
|
||||
|
||||
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc', 'transformations')
|
||||
@@ -28,3 +29,38 @@ for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transfor
|
||||
cython_libs = envCython["LIBS"] + libs
|
||||
commonmodel_lib = lenv.Library('commonmodel', common_src)
|
||||
lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks)
|
||||
tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath) if 'pycache' not in x]
|
||||
|
||||
# Get model metadata
|
||||
PC = not os.path.isfile('/TICI')
|
||||
if PC:
|
||||
inputs = tinygrad_files + [File(Dir("#sunnypilot/modeld_v2").File("install_models_pc.py").abspath)]
|
||||
outputs = []
|
||||
model_dir = Dir("models").abspath
|
||||
cmd = f'python3 {Dir("#sunnypilot/modeld_v2").abspath}/install_models_pc.py {model_dir}'
|
||||
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_policy']:
|
||||
if File(f"models/{model_name}.onnx").exists():
|
||||
inputs.append(File(f"models/{model_name}.onnx"))
|
||||
inputs.append(File(f"models/{model_name}_tinygrad.pkl"))
|
||||
outputs.append(File(f"models/{model_name}_metadata.pkl"))
|
||||
if outputs:
|
||||
lenv.Command(outputs, inputs, cmd)
|
||||
|
||||
def tg_compile(flags, model_name):
|
||||
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"'
|
||||
fn = File(f"models/{model_name}").abspath
|
||||
return lenv.Command(
|
||||
fn + "_tinygrad.pkl",
|
||||
[fn + ".onnx"] + tinygrad_files,
|
||||
f'{pythonpath_string} {flags} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {fn}_tinygrad.pkl'
|
||||
)
|
||||
|
||||
# Compile small models
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_policy']:
|
||||
if File(f"models/{model_name}.onnx").exists():
|
||||
flags = {
|
||||
'larch64': 'DEV=QCOM',
|
||||
'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env
|
||||
}.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0')
|
||||
tg_compile(flags, model_name)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
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 numpy as np
|
||||
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
|
||||
|
||||
class CameraOffsetHelper:
|
||||
def __init__(self):
|
||||
self.camera_offset = 0.0
|
||||
self.actual_camera_offset = 0.0
|
||||
|
||||
@staticmethod
|
||||
def apply_camera_offset(model_transform, intrinsics, height, offset_param):
|
||||
cy = intrinsics[1, 2]
|
||||
shear = np.eye(3, dtype=np.float32)
|
||||
shear[0, 1] = offset_param / height
|
||||
shear[0, 2] = -offset_param / height * cy
|
||||
model_transform = (shear @ model_transform).astype(np.float32)
|
||||
return model_transform
|
||||
|
||||
def set_offset(self, offset):
|
||||
self.camera_offset = offset
|
||||
|
||||
def update(self, model_transform_main, model_transform_extra, sm, main_wide_camera):
|
||||
self.actual_camera_offset = (0.9 * self.actual_camera_offset) + (0.1 * self.camera_offset)
|
||||
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
|
||||
height = sm["liveCalibration"].height[0] if sm['liveCalibration'].height else 1.22
|
||||
|
||||
intrinsics_main = dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics
|
||||
model_transform_main = self.apply_camera_offset(model_transform_main, intrinsics_main, height, self.actual_camera_offset)
|
||||
|
||||
intrinsics_extra = dc.ecam.intrinsics
|
||||
model_transform_extra = self.apply_camera_offset(model_transform_extra, intrinsics_extra, height, self.actual_camera_offset)
|
||||
return model_transform_main, model_transform_extra
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import shutil
|
||||
import pickle
|
||||
import codecs
|
||||
import onnx
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
|
||||
|
||||
def get_name_and_shape(value_info):
|
||||
shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim])
|
||||
return value_info.name, shape
|
||||
|
||||
|
||||
def get_metadata_value_by_name(model, name):
|
||||
for prop in model.metadata_props:
|
||||
if prop.key == name:
|
||||
return prop.value
|
||||
return None
|
||||
|
||||
|
||||
def generate_metadata_pkl(model_path, output_path):
|
||||
try:
|
||||
model = onnx.load(str(model_path))
|
||||
output_slices = get_metadata_value_by_name(model, 'output_slices')
|
||||
|
||||
if output_slices:
|
||||
metadata = {
|
||||
'model_checkpoint': get_metadata_value_by_name(model, 'model_checkpoint'),
|
||||
'output_slices': pickle.loads(codecs.decode(output_slices.encode(), "base64")),
|
||||
'input_shapes': dict([get_name_and_shape(x) for x in model.graph.input]),
|
||||
'output_shapes': dict([get_name_and_shape(x) for x in model.graph.output])
|
||||
}
|
||||
with open(output_path, 'wb') as f:
|
||||
pickle.dump(metadata, f)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def install_models(model_dir):
|
||||
model_dir = Path(model_dir)
|
||||
models = ["driving_policy", "driving_vision"]
|
||||
found_models = []
|
||||
|
||||
for model in models:
|
||||
if (model_dir / f"{model}.onnx").exists():
|
||||
found_models.append(model)
|
||||
|
||||
if not found_models:
|
||||
return
|
||||
|
||||
try:
|
||||
custom_name = input(f"Found models ({', '.join(found_models)}). Enter model short name (e.g. wmiv4): ").strip()
|
||||
except EOFError:
|
||||
return
|
||||
|
||||
if not custom_name:
|
||||
print("No name provided, skipping installation.")
|
||||
return
|
||||
|
||||
dest_dir = Path(Paths.model_root())
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for model in found_models:
|
||||
onnx_path = model_dir / f"{model}.onnx"
|
||||
tinygrad_pkl = model_dir / f"{model}_tinygrad.pkl"
|
||||
metadata_pkl = model_dir / f"{model}_metadata.pkl"
|
||||
|
||||
if not metadata_pkl.exists():
|
||||
generate_metadata_pkl(onnx_path, metadata_pkl)
|
||||
|
||||
dest_tinygrad = dest_dir / f"{model}_{custom_name}_tinygrad.pkl"
|
||||
dest_metadata = dest_dir / f"{model}_{custom_name}_metadata.pkl"
|
||||
|
||||
if tinygrad_pkl.exists():
|
||||
shutil.move(str(tinygrad_pkl), str(dest_tinygrad))
|
||||
if metadata_pkl.exists():
|
||||
shutil.move(str(metadata_pkl), str(dest_metadata))
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: install_models_pc.py <model_dir>")
|
||||
sys.exit(1)
|
||||
install_models(sys.argv[1])
|
||||
@@ -21,6 +21,7 @@ from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_p
|
||||
from openpilot.sunnypilot.modeld_v2.constants import Plan
|
||||
from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLContext
|
||||
from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants
|
||||
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
|
||||
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase
|
||||
@@ -28,7 +29,6 @@ from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||
from openpilot.sunnypilot.models.runners.helpers import get_model_runner
|
||||
|
||||
PROCESS_NAME = "selfdrive.modeld.modeld_tinygrad"
|
||||
RECOVERY_POWER = 1.0 # The higher this number the more aggressively the model will recover to lanecenter, too high and it will ping-pong
|
||||
|
||||
|
||||
class FrameMeta:
|
||||
@@ -63,6 +63,7 @@ class ModelState(ModelStateBase):
|
||||
self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".0"))
|
||||
self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0"))
|
||||
self.MIN_LAT_CONTROL_SPEED = 0.3
|
||||
self.PLANPLUS_CONTROL: float = 1.0
|
||||
|
||||
buffer_length = 5 if self.model_runner.is_20hz else 2
|
||||
self.frames = {name: DrivingModelFrame(context, buffer_length) for name in self.model_runner.vision_input_names}
|
||||
@@ -158,7 +159,8 @@ class ModelState(ModelStateBase):
|
||||
lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action:
|
||||
plan = model_output['plan'][0]
|
||||
if 'planplus' in model_output:
|
||||
plan = plan + RECOVERY_POWER*model_output['planplus'][0]
|
||||
recovery_power = self.PLANPLUS_CONTROL * (0.75 if v_ego > 20.0 else 1.0)
|
||||
plan = plan + recovery_power * model_output['planplus'][0]
|
||||
desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS,
|
||||
action_t=long_action_t)
|
||||
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
|
||||
@@ -229,6 +231,7 @@ def main(demo=False):
|
||||
buf_main, buf_extra = None, None
|
||||
meta_main = FrameMeta()
|
||||
meta_extra = FrameMeta()
|
||||
camera_offset_helper = CameraOffsetHelper()
|
||||
|
||||
|
||||
if demo:
|
||||
@@ -283,13 +286,15 @@ def main(demo=False):
|
||||
v_ego = max(sm["carState"].vEgo, 0.)
|
||||
if sm.frame % 60 == 0:
|
||||
model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay)
|
||||
model.PLANPLUS_CONTROL = params.get("PlanplusControl", return_default=True)
|
||||
camera_offset_helper.set_offset(params.get("CameraOffset", return_default=True))
|
||||
lat_delay = model.lat_delay + model.LAT_SMOOTH_SECONDS
|
||||
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
|
||||
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
|
||||
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
|
||||
model_transform_main = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics,
|
||||
False).astype(np.float32)
|
||||
model_transform_main = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics, False).astype(np.float32)
|
||||
model_transform_extra = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics, True).astype(np.float32)
|
||||
model_transform_main, model_transform_extra = camera_offset_helper.update(model_transform_main, model_transform_extra, sm, main_wide_camera)
|
||||
live_calib_seen = True
|
||||
|
||||
traffic_convention = np.zeros(2)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
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 numpy as np
|
||||
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
from openpilot.common.transformations.model import get_warp_matrix
|
||||
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
|
||||
|
||||
|
||||
class MockStruct:
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
|
||||
|
||||
class TestCameraOffset:
|
||||
def setup_method(self):
|
||||
self.camera_offset = CameraOffsetHelper()
|
||||
self.dc = DEVICE_CAMERAS[('mici', 'os04c10')]
|
||||
|
||||
def test_smoothing(self):
|
||||
self.camera_offset.set_offset(0.2)
|
||||
|
||||
sm = MockStruct(
|
||||
deviceState=MockStruct(deviceType='mici'),
|
||||
roadCameraState=MockStruct(sensor='os04c10'),
|
||||
liveCalibration=MockStruct(rpyCalib=[0.0, 0.0, 0.0], height=[1.22])
|
||||
)
|
||||
|
||||
intrinsics_main = self.dc.fcam.intrinsics
|
||||
intrinsics_extra = self.dc.ecam.intrinsics
|
||||
device_from_calib_euler = np.array([0.0, 0.0, 0.0], dtype=np.float32)
|
||||
main_transform = get_warp_matrix(device_from_calib_euler, intrinsics_main, False).astype(np.float32)
|
||||
extra_transform = get_warp_matrix(device_from_calib_euler, intrinsics_extra, True).astype(np.float32)
|
||||
|
||||
self.camera_offset.update(main_transform, extra_transform, sm, False)
|
||||
np.testing.assert_almost_equal(self.camera_offset.actual_camera_offset, 0.02)
|
||||
self.camera_offset.update(main_transform, extra_transform, sm, False)
|
||||
np.testing.assert_almost_equal(self.camera_offset.actual_camera_offset, 0.038)
|
||||
|
||||
def test_camera_offset_(self):
|
||||
intrinsics = self.dc.fcam.intrinsics
|
||||
transform = np.eye(3, dtype=np.float32)
|
||||
height = 1.22
|
||||
offset = 0.1
|
||||
|
||||
cy = intrinsics[1, 2]
|
||||
expected_shear = np.eye(3, dtype=np.float32)
|
||||
expected_shear[0, 1] = offset / height
|
||||
expected_shear[0, 2] = -offset / height * cy
|
||||
|
||||
result = CameraOffsetHelper.apply_camera_offset(transform, intrinsics, height, offset)
|
||||
np.testing.assert_array_almost_equal(result, expected_shear)
|
||||
|
||||
def test_update(self):
|
||||
sm = MockStruct(
|
||||
deviceState=MockStruct(deviceType='mici'),
|
||||
roadCameraState=MockStruct(sensor='os04c10'),
|
||||
liveCalibration=MockStruct(rpyCalib=[0.0, 0.0, 0.0], height=[1.22])
|
||||
)
|
||||
intrinsics_main = self.dc.fcam.intrinsics
|
||||
intrinsics_extra = self.dc.ecam.intrinsics
|
||||
device_from_calib_euler = np.array([0.0, 0.0, 0.0], dtype=np.float32)
|
||||
main_transform = get_warp_matrix(device_from_calib_euler, intrinsics_main, False).astype(np.float32)
|
||||
extra_transform = get_warp_matrix(device_from_calib_euler, intrinsics_extra, True).astype(np.float32)
|
||||
|
||||
self.camera_offset.set_offset(0.0) # test default offset doesn't change transformation
|
||||
main_out, extra_out = self.camera_offset.update(main_transform, extra_transform, sm, False)
|
||||
np.testing.assert_array_equal(main_out, main_transform)
|
||||
np.testing.assert_array_equal(extra_out, extra_transform)
|
||||
|
||||
self.camera_offset.set_offset(0.2) # test valid offset changes transformation
|
||||
main_out, extra_out = self.camera_offset.update(main_transform, extra_transform, sm, False)
|
||||
assert not np.array_equal(main_out, main_transform)
|
||||
assert not np.array_equal(extra_out, extra_transform)
|
||||
assert main_out[0, 1] != 0.0
|
||||
assert main_out[0, 2] != 0.0
|
||||
@@ -1,102 +0,0 @@
|
||||
import numpy as np
|
||||
import random
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from msgq.visionipc import VisionIpcServer, VisionStreamType
|
||||
from opendbc.car.car_helpers import get_demo_car_params
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.system.manager.process_config import managed_processes
|
||||
from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera_state
|
||||
|
||||
CAM = DEVICE_CAMERAS[("tici", "ar0231")].fcam
|
||||
IMG = np.zeros(int(CAM.width*CAM.height*(3/2)), dtype=np.uint8)
|
||||
IMG_BYTES = IMG.flatten().tobytes()
|
||||
|
||||
|
||||
class TestModeld:
|
||||
|
||||
def setup_method(self):
|
||||
self.vipc_server = VisionIpcServer("camerad")
|
||||
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 40, CAM.width, CAM.height)
|
||||
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_DRIVER, 40, CAM.width, CAM.height)
|
||||
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 40, CAM.width, CAM.height)
|
||||
self.vipc_server.start_listener()
|
||||
Params().put("CarParams", get_demo_car_params().to_bytes())
|
||||
|
||||
self.sm = messaging.SubMaster(['modelV2', 'cameraOdometry'])
|
||||
self.pm = messaging.PubMaster(['roadCameraState', 'wideRoadCameraState', 'liveCalibration'])
|
||||
|
||||
managed_processes['modeld'].start()
|
||||
self.pm.wait_for_readers_to_update("roadCameraState", 10)
|
||||
|
||||
def teardown_method(self):
|
||||
managed_processes['modeld'].stop()
|
||||
del self.vipc_server
|
||||
|
||||
def _send_frames(self, frame_id, cams=None):
|
||||
if cams is None:
|
||||
cams = ('roadCameraState', 'wideRoadCameraState')
|
||||
|
||||
cs = None
|
||||
for cam in cams:
|
||||
msg = messaging.new_message(cam)
|
||||
cs = getattr(msg, cam)
|
||||
cs.frameId = frame_id
|
||||
cs.timestampSof = int((frame_id * DT_MDL) * 1e9)
|
||||
cs.timestampEof = int(cs.timestampSof + (DT_MDL * 1e9))
|
||||
cam_meta = meta_from_camera_state(cam)
|
||||
|
||||
self.pm.send(msg.which(), msg)
|
||||
self.vipc_server.send(cam_meta.stream, IMG_BYTES, cs.frameId,
|
||||
cs.timestampSof, cs.timestampEof)
|
||||
return cs
|
||||
|
||||
def _wait(self):
|
||||
self.sm.update(5000)
|
||||
if self.sm['modelV2'].frameId != self.sm['cameraOdometry'].frameId:
|
||||
self.sm.update(1000)
|
||||
|
||||
def test_modeld(self):
|
||||
for n in range(1, 500):
|
||||
cs = self._send_frames(n)
|
||||
self._wait()
|
||||
|
||||
mdl = self.sm['modelV2']
|
||||
assert mdl.frameId == n
|
||||
assert mdl.frameIdExtra == n
|
||||
assert mdl.timestampEof == cs.timestampEof
|
||||
assert mdl.frameAge == 0
|
||||
assert mdl.frameDropPerc == 0
|
||||
|
||||
odo = self.sm['cameraOdometry']
|
||||
assert odo.frameId == n
|
||||
assert odo.timestampEof == cs.timestampEof
|
||||
|
||||
def test_dropped_frames(self):
|
||||
"""
|
||||
modeld should only run on consecutive road frames
|
||||
"""
|
||||
frame_id = -1
|
||||
road_frames = list()
|
||||
for n in range(1, 50):
|
||||
if (random.random() < 0.1) and n > 3:
|
||||
cams = random.choice([(), ('wideRoadCameraState', )])
|
||||
self._send_frames(n, cams)
|
||||
else:
|
||||
self._send_frames(n)
|
||||
road_frames.append(n)
|
||||
self._wait()
|
||||
|
||||
if len(road_frames) < 3 or road_frames[-1] - road_frames[-2] == 1:
|
||||
frame_id = road_frames[-1]
|
||||
|
||||
mdl = self.sm['modelV2']
|
||||
odo = self.sm['cameraOdometry']
|
||||
assert mdl.frameId == frame_id
|
||||
assert mdl.frameIdExtra == frame_id
|
||||
assert odo.frameId == frame_id
|
||||
if n != frame_id:
|
||||
assert not self.sm.updated['modelV2']
|
||||
assert not self.sm.updated['cameraOdometry']
|
||||
@@ -0,0 +1,61 @@
|
||||
import numpy as np
|
||||
|
||||
from cereal import log
|
||||
|
||||
from openpilot.sunnypilot.modeld_v2.constants import Plan
|
||||
from openpilot.sunnypilot.modeld_v2.modeld import ModelState
|
||||
import openpilot.sunnypilot.modeld_v2.modeld as modeld
|
||||
|
||||
|
||||
class MockStruct:
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
|
||||
def test_recovery_power_scaling():
|
||||
state = MockStruct(
|
||||
PLANPLUS_CONTROL=1.0,
|
||||
LONG_SMOOTH_SECONDS=0.3,
|
||||
LAT_SMOOTH_SECONDS=0.1,
|
||||
MIN_LAT_CONTROL_SPEED=0.3,
|
||||
mlsim=True,
|
||||
generation=12,
|
||||
constants=MockStruct(T_IDXS=np.arange(100), DESIRE_LEN=8)
|
||||
)
|
||||
prev_action = log.ModelDataV2.Action()
|
||||
recorded_vel: list = []
|
||||
|
||||
def mock_accel(plan_vel, plan_accel, t_idxs, action_t=0.0):
|
||||
recorded_vel.append(plan_vel.copy())
|
||||
return 0.0, False
|
||||
|
||||
modeld.get_accel_from_plan = mock_accel
|
||||
modeld.get_curvature_from_output = lambda *args: 0.0
|
||||
plan = np.random.rand(1, 100, 15).astype(np.float32)
|
||||
planplus = np.random.rand(1, 100, 15).astype(np.float32)
|
||||
|
||||
model_output: dict = {
|
||||
'plan': plan.copy(),
|
||||
'planplus': planplus.copy()
|
||||
}
|
||||
|
||||
test_cases: list = [
|
||||
# (control, v_ego, expected_factor)
|
||||
(0.55, 20.0, 1.0),
|
||||
(1.0, 25.0, .75),
|
||||
(1.5, 25.1, 0.75),
|
||||
(2.0, 20.0, 1.0),
|
||||
(0.75, 19.0, 1.0),
|
||||
(0.8, 25.1, 0.75),
|
||||
]
|
||||
|
||||
for control, v_ego, factor in test_cases:
|
||||
state.PLANPLUS_CONTROL = control
|
||||
recorded_vel.clear()
|
||||
ModelState.get_action_from_model(state, model_output, prev_action, 0.0, 0.0, v_ego)
|
||||
|
||||
expected_recovery_power = control * factor
|
||||
expected_plan_vel = plan[0, :, Plan.VELOCITY][:, 0] + expected_recovery_power * planplus[0, :, Plan.VELOCITY][:, 0]
|
||||
|
||||
np.testing.assert_allclose(recorded_vel[0], expected_plan_vel, rtol=1e-5, atol=1e-6)
|
||||
@@ -2,25 +2,17 @@ from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||
from openpilot.sunnypilot.models.runners.model_runner import ModelRunner
|
||||
from openpilot.sunnypilot.models.runners.tinygrad.tinygrad_runner import TinygradRunner, TinygradSplitRunner
|
||||
from openpilot.sunnypilot.models.runners.constants import ModelType
|
||||
from openpilot.system.hardware import TICI
|
||||
|
||||
if not TICI:
|
||||
from openpilot.sunnypilot.models.runners.onnx.onnx_runner import ONNXRunner
|
||||
|
||||
def get_model_runner() -> ModelRunner:
|
||||
"""
|
||||
Factory function to create and return the appropriate ModelRunner instance.
|
||||
|
||||
Selects between ONNXRunner (for non-TICI platforms) and TinygradRunner
|
||||
(for TICI platforms), choosing TinygradSplitRunner if separate vision/policy
|
||||
Selects TinygradRunner, choosing TinygradSplitRunner if separate vision/policy
|
||||
models are detected in the active bundle.
|
||||
|
||||
:return: An instance of a ModelRunner subclass (ONNXRunner, TinygradRunner, or TinygradSplitRunner).
|
||||
"""
|
||||
if not TICI:
|
||||
return ONNXRunner()
|
||||
|
||||
# On TICI platforms, use Tinygrad runners
|
||||
bundle = get_active_bundle()
|
||||
if bundle and bundle.models:
|
||||
model_types = {m.type.raw for m in bundle.models}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:66b3aefa108dd0c7f64205a11e424430c318e6fd06de31b5550d0b9d05616e6a
|
||||
size 19035
|
||||
@@ -42,10 +42,14 @@ METADATA_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__f
|
||||
|
||||
params = Params()
|
||||
|
||||
# Parameters that should never be remotely modified for security reasons
|
||||
# Parameters that should never be remotely modified
|
||||
BLOCKED_PARAMS = {
|
||||
"CompletedSunnylinkConsentVersion",
|
||||
"CompletedTrainingVersion",
|
||||
"GithubUsername", # Could grant SSH access
|
||||
"GithubSshKeys", # Direct SSH key injection
|
||||
"HasAcceptedTerms",
|
||||
"HasAcceptedTermsSP",
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +66,7 @@ def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None:
|
||||
threading.Thread(target=ws_ping, args=(ws, end_event), name='ws_ping'),
|
||||
threading.Thread(target=ws_queue, args=(end_event,), name='ws_queue'),
|
||||
threading.Thread(target=upload_handler, args=(end_event,), name='upload_handler'),
|
||||
# threading.Thread(target=sunny_log_handler, args=(end_event, comma_prime_cellular_end_event), name='log_handler'),
|
||||
threading.Thread(target=sunny_log_handler, args=(end_event, comma_prime_cellular_end_event), name='log_handler'),
|
||||
threading.Thread(target=stat_handler, args=(end_event, Paths.stats_sp_root(), True), name='stat_handler'),
|
||||
] + [
|
||||
threading.Thread(target=jsonrpc_handler, args=(end_event, partial(startLocalProxy, end_event),), name=f'worker_{x}')
|
||||
|
||||
@@ -121,9 +121,18 @@
|
||||
"title": "Camera Debug Exp Time",
|
||||
"description": ""
|
||||
},
|
||||
"CameraOffset": {
|
||||
"title": "Adjust Camera Offset",
|
||||
"description": "Virtually shift camera's perspective to move model's center to Left(+ values) or Right (- values)",
|
||||
"min": -0.35,
|
||||
"max": 0.35,
|
||||
"step": 0.01,
|
||||
"unit": "meters"
|
||||
},
|
||||
"CarBatteryCapacity": {
|
||||
"title": "Car Battery Capacity",
|
||||
"description": ""
|
||||
"description": "Battery Size",
|
||||
"unit": "kWh"
|
||||
},
|
||||
"CarList": {
|
||||
"title": "Supported Car List",
|
||||
@@ -165,6 +174,10 @@
|
||||
"title": "Chevron Info",
|
||||
"description": ""
|
||||
},
|
||||
"CompletedSunnylinkConsentVersion": {
|
||||
"title": "Completed sunnylink Consent Version",
|
||||
"description": ""
|
||||
},
|
||||
"CompletedTrainingVersion": {
|
||||
"title": "Completed Training Version",
|
||||
"description": ""
|
||||
@@ -182,7 +195,7 @@
|
||||
"description": ""
|
||||
},
|
||||
"CustomAccIncrementsEnabled": {
|
||||
"title": "Custom ACC Increments Enabled",
|
||||
"title": "Custom ACC Increments",
|
||||
"description": ""
|
||||
},
|
||||
"CustomAccLongPressIncrement": {
|
||||
@@ -200,8 +213,8 @@
|
||||
"step": 1
|
||||
},
|
||||
"CustomTorqueParams": {
|
||||
"title": "Custom Torque Params",
|
||||
"description": ""
|
||||
"title": "Enable Custom Torque Tuning",
|
||||
"description": "Enables custom tuning for Torque lateral control"
|
||||
},
|
||||
"DevUIInfo": {
|
||||
"title": "Developer UI Info",
|
||||
@@ -275,7 +288,7 @@
|
||||
},
|
||||
"EnforceTorqueControl": {
|
||||
"title": "Enforce Torque Control",
|
||||
"description": ""
|
||||
"description": "Enable this to enforce sunnypilot to steer with Torque lateral control."
|
||||
},
|
||||
"ExperimentalMode": {
|
||||
"title": "Experimental Mode",
|
||||
@@ -349,6 +362,10 @@
|
||||
"title": "Has Accepted Terms",
|
||||
"description": ""
|
||||
},
|
||||
"HasAcceptedTermsSP": {
|
||||
"title": "Has Accepted sunnypilot Terms",
|
||||
"description": ""
|
||||
},
|
||||
"HideVEgoUI": {
|
||||
"title": "Hide vEgo UI",
|
||||
"description": ""
|
||||
@@ -381,7 +398,8 @@
|
||||
},
|
||||
"InteractivityTimeout": {
|
||||
"title": "Interactivity Timeout",
|
||||
"description": ""
|
||||
"description": "",
|
||||
"unit": "seconds"
|
||||
},
|
||||
"IsDevelopmentBranch": {
|
||||
"title": "Is Development Branch",
|
||||
@@ -436,12 +454,15 @@
|
||||
"description": ""
|
||||
},
|
||||
"LagdToggle": {
|
||||
"title": "LaGD Toggle",
|
||||
"description": ""
|
||||
"title": "Live Learning Steer Delay",
|
||||
"description": "Allow device to learn and adapt car's steering response time"
|
||||
},
|
||||
"LagdToggleDelay": {
|
||||
"title": "LaGD Toggle Delay",
|
||||
"description": ""
|
||||
"title": "Manual Software Delay",
|
||||
"description": "Software delay to use when Live Learning Steer Delay is toggled off",
|
||||
"min": 0.05,
|
||||
"max": 0.5,
|
||||
"step": 0.01
|
||||
},
|
||||
"LagdValueCache": {
|
||||
"title": "LaGD Value Cache",
|
||||
@@ -449,11 +470,11 @@
|
||||
},
|
||||
"LaneTurnDesire": {
|
||||
"title": "Lane Turn Desire",
|
||||
"description": ""
|
||||
"description": "Force model to plan an intent to turn based on blinker"
|
||||
},
|
||||
"LaneTurnValue": {
|
||||
"title": "Lane Turn Value",
|
||||
"description": "",
|
||||
"title": "Lane Turn Speed",
|
||||
"description": "Maximum speed for lane turn desire",
|
||||
"min": 0,
|
||||
"max": 20,
|
||||
"step": 1
|
||||
@@ -531,12 +552,12 @@
|
||||
"description": ""
|
||||
},
|
||||
"LiveTorqueParamsRelaxedToggle": {
|
||||
"title": "Live Torque Params Relaxed Toggle",
|
||||
"description": ""
|
||||
"title": "Less Restrict Settings for Self-Tune (Beta)",
|
||||
"description": "Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values."
|
||||
},
|
||||
"LiveTorqueParamsToggle": {
|
||||
"title": "Live Torque Params Toggle",
|
||||
"description": ""
|
||||
"title": "Self-Tune",
|
||||
"description": "Enables self-tune for Torque lateral control"
|
||||
},
|
||||
"LocationFilterInitialState": {
|
||||
"title": "Location Filter Initial State",
|
||||
@@ -612,7 +633,8 @@
|
||||
},
|
||||
"MaxTimeOffroad": {
|
||||
"title": "Max Time Offroad",
|
||||
"description": ""
|
||||
"description": "",
|
||||
"unit": "seconds"
|
||||
},
|
||||
"ModelManager_ActiveBundle": {
|
||||
"title": "Model Manager Active Bundle",
|
||||
@@ -689,7 +711,7 @@
|
||||
"description": ""
|
||||
},
|
||||
"OffroadMode": {
|
||||
"title": "Offroad Mode",
|
||||
"title": "Force Offroad Mode",
|
||||
"description": ""
|
||||
},
|
||||
"Offroad_CarUnrecognized": {
|
||||
@@ -753,22 +775,69 @@
|
||||
"description": ""
|
||||
},
|
||||
"OnroadScreenOffBrightness": {
|
||||
"title": "Onroad Screen Off Brightness",
|
||||
"title": "Onroad Brightness",
|
||||
"description": "",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 5
|
||||
},
|
||||
"OnroadScreenOffControl": {
|
||||
"title": "Onroad Screen Off Control",
|
||||
"description": ""
|
||||
"title": "Onroad Screen: Reduced Brightness",
|
||||
"description": "Turn off device screen or reduce brightness after driving starts"
|
||||
},
|
||||
"OnroadScreenOffTimer": {
|
||||
"title": "Onroad Screen Off Timer",
|
||||
"title": "Onroad Brightness Delay",
|
||||
"description": "",
|
||||
"min": 0,
|
||||
"max": 60,
|
||||
"step": 1
|
||||
"options": [
|
||||
{
|
||||
"value": 15,
|
||||
"label": "15s"
|
||||
},
|
||||
{
|
||||
"value": 30,
|
||||
"label": "30s"
|
||||
},
|
||||
{
|
||||
"value": 60,
|
||||
"label": "1m"
|
||||
},
|
||||
{
|
||||
"value": 120,
|
||||
"label": "2m"
|
||||
},
|
||||
{
|
||||
"value": 180,
|
||||
"label": "3m"
|
||||
},
|
||||
{
|
||||
"value": 240,
|
||||
"label": "4m"
|
||||
},
|
||||
{
|
||||
"value": 300,
|
||||
"label": "5m"
|
||||
},
|
||||
{
|
||||
"value": 360,
|
||||
"label": "6m"
|
||||
},
|
||||
{
|
||||
"value": 420,
|
||||
"label": "7m"
|
||||
},
|
||||
{
|
||||
"value": 480,
|
||||
"label": "8m"
|
||||
},
|
||||
{
|
||||
"value": 540,
|
||||
"label": "9m"
|
||||
},
|
||||
{
|
||||
"value": 600,
|
||||
"label": "10m"
|
||||
}
|
||||
]
|
||||
},
|
||||
"OnroadUploads": {
|
||||
"title": "Onroad Uploads",
|
||||
@@ -826,6 +895,13 @@
|
||||
"title": "Panda Som Reset Triggered",
|
||||
"description": ""
|
||||
},
|
||||
"PlanplusControl": {
|
||||
"title": "Plan Plus Controls",
|
||||
"description": "Adjust planplus model recentering strength. The higher this number the more aggressively the model will recover to lanecenter, too high and it will ping-pong",
|
||||
"min": 0.0,
|
||||
"max": 2.0,
|
||||
"step": 0.1
|
||||
},
|
||||
"PrimeType": {
|
||||
"title": "Prime Type",
|
||||
"description": ""
|
||||
@@ -863,7 +939,7 @@
|
||||
"description": ""
|
||||
},
|
||||
"RoadNameToggle": {
|
||||
"title": "Road Name Toggle",
|
||||
"title": "Display Road Name",
|
||||
"description": ""
|
||||
},
|
||||
"RouteCount": {
|
||||
@@ -876,7 +952,7 @@
|
||||
},
|
||||
"ShowAdvancedControls": {
|
||||
"title": "Show Advanced Controls",
|
||||
"description": ""
|
||||
"description": "Enable to show advanced controls on device"
|
||||
},
|
||||
"ShowDebugInfo": {
|
||||
"title": "UI Debug Mode",
|
||||
@@ -887,11 +963,11 @@
|
||||
"description": ""
|
||||
},
|
||||
"SmartCruiseControlMap": {
|
||||
"title": "Smart Cruise Control Map",
|
||||
"title": "Smart Cruise Control - Map",
|
||||
"description": ""
|
||||
},
|
||||
"SmartCruiseControlVision": {
|
||||
"title": "Smart Cruise Control Vision",
|
||||
"title": "Smart Cruise Control - Vision",
|
||||
"description": ""
|
||||
},
|
||||
"SnoozeUpdate": {
|
||||
@@ -899,7 +975,7 @@
|
||||
"description": ""
|
||||
},
|
||||
"SpeedLimitMode": {
|
||||
"title": "Speed Limit Mode",
|
||||
"title": "Speed Limit Assist Mode",
|
||||
"description": "",
|
||||
"options": [
|
||||
{
|
||||
@@ -939,7 +1015,7 @@
|
||||
]
|
||||
},
|
||||
"SpeedLimitPolicy": {
|
||||
"title": "Speed Limit Policy",
|
||||
"title": "Speed Limit Source",
|
||||
"description": "",
|
||||
"options": [
|
||||
{
|
||||
@@ -965,7 +1041,7 @@
|
||||
]
|
||||
},
|
||||
"SpeedLimitValueOffset": {
|
||||
"title": "Speed Limit Value Offset",
|
||||
"title": "Speed Limit Offset Value",
|
||||
"description": "",
|
||||
"min": -30,
|
||||
"max": 30,
|
||||
@@ -1020,22 +1096,23 @@
|
||||
"description": ""
|
||||
},
|
||||
"TorqueParamsOverrideEnabled": {
|
||||
"title": "Torque Params Override Enabled",
|
||||
"title": "Manual Real-Time Tuning",
|
||||
"description": ""
|
||||
},
|
||||
"TorqueParamsOverrideFriction": {
|
||||
"title": "Torque Params Override Friction",
|
||||
"title": "Manual Tune - Friction",
|
||||
"description": "",
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
},
|
||||
"TorqueParamsOverrideLatAccelFactor": {
|
||||
"title": "Torque Params Override Lat Accel Factor",
|
||||
"title": "Manual Tune - Lateral Acceleration Factor",
|
||||
"description": "",
|
||||
"min": 0.1,
|
||||
"max": 5.0,
|
||||
"step": 0.1
|
||||
"step": 0.1,
|
||||
"unit": "m/s²"
|
||||
},
|
||||
"ToyotaEnforceStockLongitudinal": {
|
||||
"title": "Toyota: Enforce Factory Longitudinal Control",
|
||||
|
||||
@@ -136,10 +136,11 @@ class SunnylinkState:
|
||||
token = self._api.get_token()
|
||||
response = self._api.api_get(f"device/{self.sunnylink_dongle_id}/roles", method='GET', access_token=token)
|
||||
if response.status_code == 200:
|
||||
self._roles = _parse_roles(response.text)
|
||||
self._params.put("SunnylinkCache_Roles", response.text)
|
||||
sponsor_tier = self._get_highest_tier()
|
||||
roles = response.text
|
||||
self._params.put("SunnylinkCache_Roles", roles)
|
||||
with self._lock:
|
||||
self._roles = _parse_roles(roles)
|
||||
sponsor_tier = self._get_highest_tier()
|
||||
if sponsor_tier != self.sponsor_tier:
|
||||
self.sponsor_tier = sponsor_tier
|
||||
cloudlog.info(f"Sunnylink sponsor tier updated to {sponsor_tier.name}")
|
||||
@@ -157,7 +158,7 @@ class SunnylinkState:
|
||||
users = response.text
|
||||
self._params.put("SunnylinkCache_Users", users)
|
||||
with self._lock:
|
||||
_parse_users(users)
|
||||
self._users = _parse_users(users)
|
||||
except Exception as e:
|
||||
cloudlog.exception(f"Failed to fetch sunnylink users: {e} for dongle id {self.sunnylink_dongle_id}")
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from openpilot.system.statsd import statlog
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.hardware.power_monitoring import PowerMonitoring
|
||||
from openpilot.system.hardware.fan_controller import TiciFanController
|
||||
from openpilot.system.version import terms_version, training_version, get_build_metadata
|
||||
from openpilot.system.version import terms_version, training_version, get_build_metadata, terms_version_sp
|
||||
|
||||
ThermalStatus = log.DeviceState.ThermalStatus
|
||||
NetworkType = log.DeviceState.NetworkType
|
||||
@@ -310,6 +310,7 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
startup_conditions["no_excessive_actuation"] = params.get("Offroad_ExcessiveActuation") is None
|
||||
startup_conditions["not_uninstalling"] = not params.get_bool("DoUninstall")
|
||||
startup_conditions["accepted_terms"] = params.get("HasAcceptedTerms") == terms_version
|
||||
startup_conditions["accepted_terms_sp"] = params.get("HasAcceptedTermsSP") == terms_version_sp
|
||||
|
||||
# with 2% left, we killall, otherwise the phone will take a long time to boot
|
||||
startup_conditions["free_space"] = msg.deviceState.freeSpacePercent > 2
|
||||
|
||||
@@ -11,15 +11,29 @@ from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.sunnypilot.widgets.toggle import ToggleSP
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, ItemAction, MultipleButtonAction, ButtonAction, \
|
||||
_resolve_value, BUTTON_WIDTH, BUTTON_HEIGHT, TEXT_PADDING
|
||||
_resolve_value, BUTTON_WIDTH, BUTTON_HEIGHT, TEXT_PADDING, DualButtonAction
|
||||
from openpilot.system.ui.widgets.scroller_tici import LineSeparator, LINE_COLOR, LINE_PADDING
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.sunnypilot.widgets.option_control import OptionControlSP, LABEL_WIDTH
|
||||
|
||||
|
||||
class Spacer(Widget):
|
||||
def __init__(self, height: int = 1):
|
||||
super().__init__()
|
||||
self._rect = rl.Rectangle(0, 0, 0, height)
|
||||
|
||||
def set_parent_rect(self, parent_rect: rl.Rectangle) -> None:
|
||||
super().set_parent_rect(parent_rect)
|
||||
self._rect.width = parent_rect.width
|
||||
|
||||
def _render(self, _):
|
||||
rl.draw_rectangle(int(self._rect.x), int(self._rect.y), int(self._rect.x + self._rect.width), int(self._rect.y), rl.Color(0,0,0,0))
|
||||
|
||||
|
||||
class ToggleActionSP(ToggleAction):
|
||||
def __init__(self, initial_state: bool = False, width: int = style.TOGGLE_WIDTH, enabled: bool | Callable[[], bool] = True,
|
||||
callback: Callable[[bool], None] | None = None, param: str | None = None):
|
||||
@@ -84,6 +98,33 @@ class ButtonActionSP(ButtonAction):
|
||||
return pressed
|
||||
|
||||
|
||||
class DualButtonActionSP(DualButtonAction):
|
||||
def __init__(self, left_text: str | Callable[[], str], right_text: str | Callable[[], str], left_callback: Callable = None,
|
||||
right_callback: Callable = None, enabled: bool | Callable[[], bool] = True, border_radius: int = 15):
|
||||
DualButtonAction.__init__(self, left_text, right_text, left_callback, right_callback, enabled)
|
||||
self.left_button._border_radius = self.right_button._border_radius = border_radius
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
button_spacing = 20
|
||||
button_height = 150
|
||||
button_width = (rect.width - button_spacing) / 2
|
||||
button_y = rect.y + (rect.height - button_height) / 2
|
||||
|
||||
left_rect = rl.Rectangle(rect.x, button_y, button_width, button_height)
|
||||
right_rect = rl.Rectangle(rect.x + button_width + button_spacing, button_y, button_width, button_height)
|
||||
|
||||
# expand one to full width if other is not visible
|
||||
if not self.left_button.is_visible:
|
||||
right_rect.x = rect.x
|
||||
right_rect.width = rect.width
|
||||
elif not self.right_button.is_visible:
|
||||
left_rect.width = rect.width
|
||||
|
||||
# Render buttons
|
||||
self.left_button.render(left_rect)
|
||||
self.right_button.render(right_rect)
|
||||
|
||||
|
||||
class MultipleButtonActionSP(MultipleButtonAction):
|
||||
def __init__(self, buttons: list[str | Callable[[], str]], button_width: int, selected_index: int = 0, callback: Callable = None,
|
||||
param: str | None = None):
|
||||
@@ -251,13 +292,13 @@ class ListItemSP(ListItem):
|
||||
item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - self._text_size.y) // 2 if self.inline else self._rect.y + style.ITEM_PADDING * 1.5
|
||||
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, self.title_color)
|
||||
|
||||
# Draw right item if present
|
||||
if self.action_item:
|
||||
right_rect = self.get_right_item_rect(self._rect)
|
||||
if self.action_item.render(right_rect) and self.action_item.enabled:
|
||||
# Right item was clicked/activated
|
||||
if self.callback:
|
||||
self.callback()
|
||||
# Draw right item if present
|
||||
if self.action_item:
|
||||
right_rect = self.get_right_item_rect(self._rect)
|
||||
if self.action_item.render(right_rect) and self.action_item.enabled:
|
||||
# Right item was clicked/activated
|
||||
if self.callback:
|
||||
self.callback()
|
||||
|
||||
# Draw description if visible
|
||||
if self.description_visible:
|
||||
@@ -296,12 +337,12 @@ def option_item_sp(title: str | Callable[[], str], param: str,
|
||||
value_change_step: int = 1, on_value_changed: Callable[[int], None] | None = None,
|
||||
enabled: bool | Callable[[], bool] = True,
|
||||
icon: str = "", label_width: int = LABEL_WIDTH, value_map: dict[int, int] | None = None,
|
||||
use_float_scaling: bool = False, label_callback: Callable[[int], str] | None = None) -> ListItemSP:
|
||||
use_float_scaling: bool = False, label_callback: Callable[[int], str] | None = None, inline: bool = False) -> ListItemSP:
|
||||
action = OptionControlSP(
|
||||
param, min_value, max_value, value_change_step,
|
||||
enabled, on_value_changed, value_map, label_width, use_float_scaling, label_callback
|
||||
)
|
||||
return ListItemSP(title=title, description=description, action_item=action, icon=icon)
|
||||
return ListItemSP(title=title, description=description, action_item=action, icon=icon, inline=inline)
|
||||
|
||||
|
||||
def button_item_sp(title: str | Callable[[], str], button_text: str | Callable[[], str], description: str | Callable[[], str] | None = None,
|
||||
@@ -310,6 +351,13 @@ def button_item_sp(title: str | Callable[[], str], button_text: str | Callable[[
|
||||
return ListItemSP(title=title, description=description, action_item=action, callback=callback)
|
||||
|
||||
|
||||
def dual_button_item_sp(left_text: str | Callable[[], str], right_text: str | Callable[[], str], left_callback: Callable = None,
|
||||
right_callback: Callable = None, description: str | Callable[[], str] | None = None,
|
||||
enabled: bool | Callable[[], bool] = True, border_radius: int = 15) -> ListItemSP:
|
||||
action = DualButtonActionSP(left_text, right_text, left_callback, right_callback, enabled, border_radius)
|
||||
return ListItemSP(title="", description=description, action_item=action)
|
||||
|
||||
|
||||
class LineSeparatorSP(LineSeparator):
|
||||
def __init__(self, height: int = 1):
|
||||
super().__init__()
|
||||
|
||||
@@ -30,6 +30,9 @@ BUILD_METADATA_FILENAME = "build.json"
|
||||
|
||||
training_version: str = "0.2.0"
|
||||
terms_version: str = "2"
|
||||
terms_version_sp: str = "1.0"
|
||||
sunnylink_consent_version: str = "1.0"
|
||||
sunnylink_consent_declined: str = "-1"
|
||||
|
||||
|
||||
def get_version(path: str = BASEDIR) -> str:
|
||||
|
||||
Reference in New Issue
Block a user