Compare commits

..

12 Commits

Author SHA1 Message Date
Jason Wen 5ac160222e Revert "camerad: remove AR0231 (#36070)"
This reverts commit 1d8dc8a6
2025-12-18 21:02:47 -05:00
Jason Wen a3797eb7a4 more tici 2025-12-18 15:05:27 -05:00
Jason Wen 930c010cf2 another more 2025-12-18 14:44:28 -05:00
Jason Wen 0e67486182 more 2025-12-18 14:42:33 -05:00
Jason Wen c7f49b43fa bump 2025-12-18 14:37:03 -05:00
Jason Wen 7c7c6319f8 bump 2025-12-18 14:35:31 -05:00
Jason Wen b9adcb7a19 bump 2025-12-18 13:29:45 -05:00
Jason Wen a829e4d171 wrong bump 2025-12-18 12:52:58 -05:00
Jason Wen 3ef5e531e6 Revert "remove tici-specific code (commaai/openpilot#36078)"
This reverts commits 3e2549f2b8.
2025-12-18 12:42:00 -05:00
Jason Wen 33c93ab413 bump 2025-12-18 12:21:30 -05:00
Jason Wen e6b2befed1 lock it in 2025-12-18 12:21:14 -05:00
Jason Wen a429b6c096 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into sync-20251218-tici 2025-12-18 05:27:46 -05:00
93 changed files with 1078 additions and 2434 deletions
@@ -174,24 +174,6 @@ 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:
+1 -1
View File
@@ -107,8 +107,8 @@ jobs:
build_mac:
name: build macOS
if: false # tmp disable due to brew install not working
runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }}
if: false # There'll be one day that this works. That day is not today.
steps:
- uses: actions/checkout@v4
with:
+58 -2
View File
@@ -11,10 +11,66 @@ 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
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).
* 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).
## Installation
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).
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>
## 🎆 Pull Requests
We welcome both pull requests and issues on GitHub. Bug fixes are encouraged.
+1 -5
View File
@@ -95,6 +95,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"Offroad_NeosUpdate", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_NoFirmware", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_Recalibration", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_StorageMissing", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_TemperatureTooHigh", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_UnregisteredHardware", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_UpdateFailed", {CLEAR_ON_MANAGER_START, JSON}},
@@ -145,7 +146,6 @@ 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"}},
@@ -155,7 +155,6 @@ 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"}},
@@ -215,19 +214,16 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
{"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}},
{"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
View File
@@ -39,7 +39,7 @@ All of these are examples of good PRs:
### First contribution
[Projects / openpilot bounties](https://github.com/orgs/commaai/projects/26/views/1?pane=info) is the best place to get started and goes in-depth on what's expected when working on a bounty.
There's lot of bounties that don't require a comma 3X or a car.
There's lot of bounties that don't require a comma 3/3X or a car.
## Pull Requests
+4 -4
View File
@@ -1,11 +1,11 @@
# connect to a comma 3X
# connect to a comma 3/3X
A comma 3X is a normal [Linux](https://github.com/commaai/agnos-builder) computer that exposes [SSH](https://wiki.archlinux.org/title/Secure_Shell) and a [serial console](https://wiki.archlinux.org/title/Working_with_the_serial_console).
A comma 3/3X is a normal [Linux](https://github.com/commaai/agnos-builder) computer that exposes [SSH](https://wiki.archlinux.org/title/Secure_Shell) and a [serial console](https://wiki.archlinux.org/title/Working_with_the_serial_console).
## Serial Console
On both the comma three and 3X, the serial console is accessible from the main OBD-C port.
Connect the comma 3X to your computer with a normal USB C cable, or use a [comma serial](https://comma.ai/shop/comma-serial) for steady 12V power.
Connect the comma 3/3X to your computer with a normal USB C cable, or use a [comma serial](https://comma.ai/shop/comma-serial) for steady 12V power.
On the comma three, the serial console is exposed through a UART-to-USB chip, and `tools/scripts/serial.sh` can be used to connect.
@@ -45,7 +45,7 @@ In order to use ADB on your device, you'll need to perform the following steps u
* Here's an example command for connecting to your device using its tethered connection: `adb connect 192.168.43.1:5555`
> [!NOTE]
> The default port for ADB is 5555 on the comma 3X.
> The default port for ADB is 5555 on the comma 3/3X.
For more info on ADB, see the [Android Debug Bridge (ADB) documentation](https://developer.android.com/tools/adb).
+1 -1
View File
@@ -8,7 +8,7 @@ Replaying is a critical tool for openpilot development and debugging.
Just run `tools/replay/replay --demo`.
## Replaying CAN data
*Hardware required: jungle and comma 3X*
*Hardware required: jungle and comma 3/3X*
1. Connect your PC to a jungle.
2.
+1 -1
View File
@@ -3,7 +3,7 @@
In 30 minutes, we'll get an openpilot development environment set up on your computer and make some changes to openpilot's UI.
And if you have a comma 3X, we'll deploy the change to your device for testing.
And if you have a comma 3/3X, we'll deploy the change to your device for testing.
## 1. Set up your development environment
-17
View File
@@ -1,20 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# On any failure, run the fallback launcher
trap 'exec ./launch_chffrplus.sh' ERR
C3_LAUNCH_SH="./sunnypilot/system/hardware/c3/launch_chffrplus.sh"
MODEL="$(tr -d '\0' < "/sys/firmware/devicetree/base/model")"
export MODEL
if [ "$MODEL" = "comma tici" ]; then
# Force a failure if the launcher doesn't exist
[ -x "$C3_LAUNCH_SH" ] || false
# If it exists, run it
exec "$C3_LAUNCH_SH"
fi
exec ./launch_chffrplus.sh
+1 -1
View File
@@ -21,7 +21,7 @@ nav:
- What is openpilot?: getting-started/what-is-openpilot.md
- How-to:
- Turn the speed blue: how-to/turn-the-speed-blue.md
- Connect to a comma 3X: how-to/connect-to-comma.md
- Connect to a comma 3/3X: how-to/connect-to-comma.md
# - Make your first pull request: how-to/make-first-pr.md
#- Replay a drive: how-to/replay-a-drive.md
- Concepts:
+1 -1
Submodule panda updated: 5f3c09c910...378f4abcbd
+1 -1
View File
@@ -449,7 +449,7 @@ class DriverMonitoring:
rpyCalib = [0., 0., 0.]
else:
highway_speed = sm['carState'].vEgo
enabled = sm['selfdriveState'].enabled or sm['carControl'].latActive
enabled = sm['selfdriveState'].enabled
wrong_gear = sm['carState'].gearShifter not in (car.CarState.GearShifter.drive, car.CarState.GearShifter.low)
standstill = sm['carState'].standstill
driver_engaged = sm['carState'].steeringPressed or sm['carState'].gasPressed
+1 -65
View File
@@ -1,7 +1,6 @@
import numpy as np
import pytest
from cereal import log, car
from cereal import log
from openpilot.common.realtime import DT_DMON
from openpilot.selfdrive.monitoring.helpers import DriverMonitoring, DRIVER_MONITOR_SETTINGS
from openpilot.system.hardware import HARDWARE
@@ -205,66 +204,3 @@ 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}"
+5
View File
@@ -99,6 +99,11 @@ def main() -> None:
cloudlog.event("pandad.flash_and_connect", count=count)
params.remove("PandaSignatures")
# TODO: remove this in the next AGNOS
# wait until USB is up before counting
if time.monotonic() < 60.:
no_internal_panda_count = 0
# Handle missing internal panda
if no_internal_panda_count > 0:
if no_internal_panda_count == 3:
Binary file not shown.
+9 -5
View File
@@ -21,6 +21,8 @@ class TestPandad:
if len(Panda.list()) == 0:
self._run_test(60)
self.spi = HARDWARE.get_device_type() != 'tici'
def teardown_method(self):
managed_processes['pandad'].stop()
@@ -92,12 +94,14 @@ class TestPandad:
# - 0.2s pandad -> pandad
# - plus some buffer
print("startup times", ts, sum(ts) / len(ts))
assert 0.1 < (sum(ts)/len(ts)) < 0.7
assert 0.1 < (sum(ts)/len(ts)) < (0.7 if self.spi else 5.0)
def test_old_spi_protocol(self):
# flash firmware with old SPI protocol
self._flash_bootstub(os.path.join(HERE, "bootstub.panda_h7_spiv0.bin"))
self._run_test(45)
def test_protocol_version_check(self):
if not self.spi:
pytest.skip("SPI test")
# flash old fw
fn = os.path.join(HERE, "bootstub.panda_h7_spiv0.bin")
self._flash_bootstub_and_test(fn, expect_mismatch=True)
def test_release_to_devel_bootstub(self):
self._flash_bootstub(None)
@@ -6,6 +6,7 @@ import random
import cereal.messaging as messaging
from cereal.services import SERVICE_LIST
from openpilot.system.hardware import HARDWARE
from openpilot.selfdrive.test.helpers import with_processes
from openpilot.selfdrive.pandad.tests.test_pandad_loopback import setup_pandad, send_random_can_messages
@@ -15,6 +16,8 @@ JUNGLE_SPAM = "JUNGLE_SPAM" in os.environ
class TestBoarddSpi:
@classmethod
def setup_class(cls):
if HARDWARE.get_device_type() == 'tici':
pytest.skip("only for spi pandas")
os.environ['STARTED'] = '1'
os.environ['SPI_ERR_PROB'] = '0.001'
if not JUNGLE_SPAM:
+4
View File
@@ -29,6 +29,10 @@
"text": "Failed to register with comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support.",
"severity": 1
},
"Offroad_StorageMissing": {
"text": "NVMe drive not mounted.",
"severity": 1
},
"Offroad_CarUnrecognized": {
"text": "sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai.",
"severity": 0
+9 -1
View File
@@ -21,6 +21,7 @@ from openpilot.selfdrive.selfdrived.helpers import ExcessiveActuationCheck
from openpilot.selfdrive.selfdrived.state import StateMachine
from openpilot.selfdrive.selfdrived.alertmanager import AlertManager, set_offroad_alert
from openpilot.system.hardware import HARDWARE
from openpilot.system.version import get_build_metadata
from openpilot.system.hardware import HARDWARE
@@ -143,7 +144,14 @@ class SelfdriveD(CruiseHelper):
self.state_machine = StateMachine()
self.rk = Ratekeeper(100, print_delay_threshold=None)
self.ignored_processes = {'mapd', }
# some comma three with NVMe experience NVMe dropouts mid-drive that
# cause loggerd to crash on write, so ignore it only on that platform
self.ignored_processes = set()
nvme_expected = os.path.exists('/dev/nvme0n1') or (not os.path.isfile("/persist/comma/living-in-the-moment"))
if HARDWARE.get_device_type() == 'tici' and nvme_expected:
self.ignored_processes = {'loggerd', }
self.ignored_processes.update({'mapd'})
# Determine startup event
is_remote = build_metadata.openpilot.comma_remote or build_metadata.openpilot.sunnypilot_remote
+9 -38
View File
@@ -11,9 +11,7 @@ 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, terms_version_sp
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkOnboarding
from openpilot.system.version import terms_version, training_version
DEBUG = False
@@ -35,7 +33,6 @@ class OnboardingState(IntEnum):
TERMS = 0
ONBOARDING = 1
DECLINE = 2
SUNNYLINK_CONSENT = 3
class TrainingGuide(Widget):
@@ -113,14 +110,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 of Service to use sunnypilot. Read the latest terms at https://sunnypilot.ai/terms before continuing."),
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."),
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 + 95
welcome_x = self._rect.x + 165
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)
@@ -146,7 +143,7 @@ class TermsPage(Widget):
class DeclinePage(Widget):
def __init__(self, back_callback=None):
super().__init__()
self._text = Label(tr("You must accept the Terms of Service in order to use sunnypilot."),
self._text = Label(tr("You must accept the Terms and Conditions 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,
@@ -183,21 +180,9 @@ 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._sunnylink.completed and self._training_done
return self._accepted_terms and self._training_done
def _on_terms_declined(self):
self._state = OnboardingState.DECLINE
@@ -207,12 +192,8 @@ class OnboardingWindow(Widget):
def _on_terms_accepted(self):
ui_state.params.put("HasAcceptedTerms", terms_version)
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._state = OnboardingState.ONBOARDING
if self._training_done:
gui_app.set_modal_overlay(None)
def _on_completed_training(self):
@@ -225,18 +206,8 @@ class OnboardingWindow(Widget):
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:
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)
if self._state == OnboardingState.ONBOARDING:
self._training_guide.render(self._rect)
elif self._state == OnboardingState.DECLINE:
self._decline_page.render(self._rect)
return -1
+2 -13
View File
@@ -9,8 +9,6 @@ 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
@@ -64,10 +62,9 @@ class MetricData:
self.color = color
class Sidebar(Widget, SidebarSP):
class Sidebar(Widget):
def __init__(self):
Widget.__init__(self)
SidebarSP.__init__(self)
super().__init__()
self._net_type = NETWORK_TYPES.get(NetworkType.none)
self._net_strength = 0
@@ -115,7 +112,6 @@ class Sidebar(Widget, SidebarSP):
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"))
@@ -204,13 +200,6 @@ class Sidebar(Widget, SidebarSP):
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:
+1 -1
View File
@@ -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=90, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.AUDIOWIDE)
self._openpilot_label = MiciLabel("sunnypilot", font_size=96, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY)
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)
-3
View File
@@ -11,9 +11,6 @@ from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.lib.application import gui_app
if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
ONROAD_DELAY = 2.5 # seconds
+7 -38
View File
@@ -17,16 +17,13 @@ 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, terms_version_sp
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkOnboarding
from openpilot.system.version import terms_version, training_version
class OnboardingState(IntEnum):
TERMS = 0
ONBOARDING = 1
DECLINE = 2
SUNNYLINK_CONSENT = 3
class DriverCameraSetupDialog(DriverCameraDialog):
@@ -415,10 +412,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 of service", info_txt)
self._title_header = TermsHeader("terms & conditions", info_txt)
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,
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,
FontWeight.ROMAN)
@property
@@ -452,18 +449,6 @@ 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)
@@ -474,7 +459,7 @@ class OnboardingWindow(Widget):
@property
def completed(self) -> bool:
return self._accepted_terms and self._sunnylink.completed and self._training_done
return self._accepted_terms and self._training_done
def _on_terms_declined(self):
self._state = OnboardingState.DECLINE
@@ -488,13 +473,7 @@ class OnboardingWindow(Widget):
def _on_terms_accepted(self):
ui_state.params.put("HasAcceptedTerms", terms_version)
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()
self._state = OnboardingState.ONBOARDING
def _on_completed_training(self):
ui_state.params.put("CompletedTrainingVersion", training_version)
@@ -503,18 +482,8 @@ 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:
if not self._training_done:
self._training_guide.render(self._rect)
else:
self.close()
self._training_guide.render(self._rect)
elif self._state == OnboardingState.DECLINE:
self._decline_page.render(self._rect)
return -1
+2 -10
View File
@@ -6,8 +6,6 @@ from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.lib.application import gui_app
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.confidence_ball import ConfidenceBallSP
def draw_circle_gradient(center_x: float, center_y: float, radius: int,
top: rl.Color, bottom: rl.Color) -> None:
@@ -23,10 +21,9 @@ def draw_circle_gradient(center_x: float, center_y: float, radius: int,
20, rl.BLACK)
class ConfidenceBall(Widget, ConfidenceBallSP):
class ConfidenceBall(Widget):
def __init__(self, demo: bool = False):
Widget.__init__(self)
ConfidenceBallSP.__init__(self)
super().__init__()
self._demo = demo
self._confidence_filter = FirstOrderFilter(-0.5, 0.5, 1 / gui_app.target_fps)
@@ -40,8 +37,6 @@ class ConfidenceBall(Widget, ConfidenceBallSP):
# animate status dot in from bottom
if ui_state.status == UIStatus.DISENGAGED:
self._confidence_filter.update(-0.5)
elif ui_state.status in (UIStatus.LAT_ONLY, UIStatus.LONG_ONLY):
self._confidence_filter.update(1 - max(self.get_animate_status_probs() or [1]))
else:
self._confidence_filter.update((1 - max(ui_state.sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs or [1])) *
(1 - max(ui_state.sm['modelV2'].meta.disengagePredictions.steerOverrideProbs or [1])))
@@ -70,9 +65,6 @@ class ConfidenceBall(Widget, ConfidenceBallSP):
top_dot_color = rl.Color(255, 0, 21, 255)
bottom_dot_color = rl.Color(255, 0, 89, 255)
elif ui_state.status in (UIStatus.LAT_ONLY, UIStatus.LONG_ONLY):
top_dot_color = bottom_dot_color = self.get_lat_long_dot_color()
elif ui_state.status == UIStatus.OVERRIDE:
top_dot_color = rl.Color(255, 255, 255, 255)
bottom_dot_color = rl.Color(82, 82, 82, 255)
+4 -14
View File
@@ -12,8 +12,6 @@ from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.model_renderer import LANE_LINE_COLORS_SP
CLIP_MARGIN = 500
MIN_DRAW_DISTANCE = 10.0
MAX_DRAW_DISTANCE = 100.0
@@ -34,7 +32,6 @@ LANE_LINE_COLORS = {
UIStatus.DISENGAGED: rl.Color(200, 200, 200, 255),
UIStatus.OVERRIDE: rl.Color(255, 255, 255, 255),
UIStatus.ENGAGED: rl.Color(0, 255, 64, 255),
**LANE_LINE_COLORS_SP,
}
@@ -80,9 +77,6 @@ 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
@@ -102,10 +96,6 @@ 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
@@ -157,13 +147,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, np.array(model.position.y) + self._camera_offset, model.position.z], dtype=np.float32).T
self._path.raw_points = np.array([model.position.x, model.position.y, 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, np.array(lane_line.y) + self._camera_offset, lane_line.z], dtype=np.float32).T
self._lane_lines[i].raw_points = np.array([lane_line.x, lane_line.y, 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, np.array(road_edge.y) + self._camera_offset, road_edge.z], dtype=np.float32).T
self._road_edges[i].raw_points = np.array([road_edge.x, road_edge.y, 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)
@@ -181,7 +171,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 + self._camera_offset, z + self._path_offset_z)
point = self._map_to_screen(d_rel, -y_rel, z + self._path_offset_z)
if point:
self._lead_vehicles[i] = self._update_lead_vehicle(d_rel, v_rel, point, self._rect)
+3 -3
View File
@@ -185,13 +185,13 @@ class TorqueBar(Widget):
# animate alpha and angle span
if not self._demo:
self._torque_line_alpha_filter.update(ui_state.status not in (UIStatus.DISENGAGED, UIStatus.LONG_ONLY))
self._torque_line_alpha_filter.update(ui_state.status != UIStatus.DISENGAGED)
else:
self._torque_line_alpha_filter.update(1.0)
torque_line_bg_alpha = np.interp(abs(self._torque_filter.x), [0.5, 1.0], [0.25, 0.5])
torque_line_bg_color = rl.Color(255, 255, 255, int(255 * torque_line_bg_alpha * self._torque_line_alpha_filter.x))
if ui_state.status not in (UIStatus.ENGAGED, UIStatus.LAT_ONLY) and not self._demo:
if ui_state.status != UIStatus.ENGAGED and not self._demo:
torque_line_bg_color = rl.Color(255, 255, 255, int(255 * 0.15 * self._torque_line_alpha_filter.x))
# draw curved line polygon torque bar
@@ -234,7 +234,7 @@ class TorqueBar(Widget):
max(0, abs(self._torque_filter.x) - 0.75) * 4,
)
if ui_state.status not in (UIStatus.ENGAGED, UIStatus.LAT_ONLY) and not self._demo:
if ui_state.status != UIStatus.ENGAGED and not self._demo:
start_color = end_color = rl.Color(255, 255, 255, int(255 * 0.35 * self._torque_line_alpha_filter.x))
gradient = Gradient(
@@ -16,9 +16,6 @@ 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
OpState = log.SelfdriveState.OpenpilotState
CALIBRATED = log.LiveCalibrationData.Status.calibrated
@@ -30,7 +27,6 @@ BORDER_COLORS = {
UIStatus.DISENGAGED: rl.Color(0x12, 0x28, 0x39, 0xFF), # Blue for disengaged state
UIStatus.OVERRIDE: rl.Color(0x89, 0x92, 0x8D, 0xFF), # Gray for override state
UIStatus.ENGAGED: rl.Color(0x16, 0x7F, 0x40, 0xFF), # Green for engaged state
**BORDER_COLORS_SP,
}
WIDE_CAM_MAX_SPEED = 10.0 # m/s (22 mph)
+7 -21
View File
@@ -11,8 +11,6 @@ from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.sunnypilot.onroad.model_renderer import ChevronMetrics, ModelRendererSP
CLIP_MARGIN = 500
MIN_DRAW_DISTANCE = 10.0
MAX_DRAW_DISTANCE = 100.0
@@ -43,11 +41,9 @@ class LeadVehicle:
fill_alpha: int = 0
class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
class ModelRenderer(Widget):
def __init__(self):
Widget.__init__(self)
ChevronMetrics.__init__(self)
ModelRendererSP.__init__(self)
super().__init__()
self._longitudinal_control = False
self._experimental_mode = False
self._blend_filter = FirstOrderFilter(1.0, 0.25, 1 / gui_app.target_fps)
@@ -56,8 +52,7 @@ 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)]
@@ -104,10 +99,6 @@ 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
@@ -137,17 +128,16 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
if render_lead_indicator and radar_state:
self._draw_lead_indicator()
self.chevron_metrics.draw_lead_status(sm, radar_state, self._rect, self._lead_vehicles)
def _update_raw_points(self, model):
"""Update raw 3D points from model data"""
self._path.raw_points = np.array([model.position.x, np.array(model.position.y) + self._camera_offset, model.position.z], dtype=np.float32).T
self._path.raw_points = np.array([model.position.x, model.position.y, 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, np.array(lane_line.y) + self._camera_offset, lane_line.z], dtype=np.float32).T
self._lane_lines[i].raw_points = np.array([lane_line.x, lane_line.y, 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, np.array(road_edge.y) + self._camera_offset, road_edge.z], dtype=np.float32).T
self._road_edges[i].raw_points = np.array([road_edge.x, road_edge.y, 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)
@@ -165,7 +155,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 + self._camera_offset, z + self._path_offset_z)
point = self._map_to_screen(d_rel, -y_rel, z + self._path_offset_z)
if point:
self._lead_vehicles[i] = self._update_lead_vehicle(d_rel, v_rel, point, self._rect)
@@ -291,10 +281,6 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control
self._blend_filter.update(int(allow_throttle))
if ui_state.rainbow_path:
self.rainbow_path.draw_rainbow_path(self._rect, self._path)
return
if self._experimental_mode:
# Draw with acceleration coloring
if len(self._exp_gradient.colors) > 1:
+2 -2
View File
@@ -27,7 +27,7 @@ AMBIENT_DB = 30 # DB where MIN_VOLUME is applied
DB_SCALE = 30 # AMBIENT_DB + DB_SCALE is where MAX_VOLUME is applied
VOLUME_BASE = 20
if HARDWARE.get_device_type() == "tizi":
if HARDWARE.get_device_type() in ("tizi", "tici"):
VOLUME_BASE = 10
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
@@ -55,7 +55,7 @@ sound_list: dict[int, tuple[str, int | None, float]] = {
**sound_list_sp,
}
if HARDWARE.get_device_type() == "tizi":
if HARDWARE.get_device_type() in ("tizi", "tici"):
sound_list.update({
AudibleAlert.engage: ("engage_tizi.wav", 1, MAX_VOLUME),
AudibleAlert.disengage: ("disengage_tizi.wav", 1, MAX_VOLUME),
@@ -1,116 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.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,216 +5,8 @@ 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.layouts.settings.firehose import FirehoseLayout
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
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.osm import OSMLayout
from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
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.visuals import VisualsLayout
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.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.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.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.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 dual_button_item
from openpilot.system.ui.widgets.list_view import button_item, dual_button_item
from openpilot.system.ui.widgets.scroller_tici import Scroller, LineSeparator
from openpilot.system.version import sunnylink_consent_version
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
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_sp(
self._sponsor_btn = button_item(
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_sp(
self._pair_btn = button_item(
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:
return ui_state.params.get("SunnylinkDongleId") or tr("N/A")
def _get_sunnylink_dongle_id() -> str | None:
return str(ui_state.params.get("SunnylinkDongleId") or (lambda: tr("N/A")))
def _handle_pair_btn(self, sponsor_pairing: bool = False):
sunnylink_dongle_id = self._get_sunnylink_dongle_id()
@@ -302,22 +302,6 @@ 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!")
@@ -23,7 +23,7 @@ class VehicleLayout(Widget):
self._current_brand = None
self._platform_selector = PlatformSelector(self._update_brand_settings)
self._vehicle_item = ListItemSP(title=self._platform_selector.text, action_item=ButtonAction(text=tr("SELECT")),
self._vehicle_item = ListItemSP(title=self._platform_selector.text, action_item=ButtonAction(text=tr("Select")),
callback=self._platform_selector._on_clicked)
self._vehicle_item.title_color = self._platform_selector.color
self._legend_widget = LegendWidget(self._platform_selector)
@@ -42,7 +42,7 @@ class VehicleLayout(Widget):
def _update_brand_settings(self):
self._vehicle_item._title = self._platform_selector.text
self._vehicle_item.title_color = self._platform_selector.color
vehicle_text = tr("REMOVE") if ui_state.params.get("CarPlatformBundle") else tr("SELECT")
vehicle_text = tr("Remove") if ui_state.params.get("CarPlatformBundle") else tr("Select")
self._vehicle_item.action_item.set_text(vehicle_text)
brand = self.get_brand()
@@ -5,55 +5,11 @@ 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.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
DESCRIPTIONS = {
'enforce_stock_longitudinal': tr_noop(
'sunnypilot will not take over control of gas and brakes. Factory Toyota longitudinal control will be used.'
),
}
class ToyotaSettings(BrandSettings):
def __init__(self):
super().__init__()
self.enforce_stock_longitudinal = toggle_item_sp(
lambda: tr("Enforce Factory Longitudinal Control"),
description=lambda: tr(DESCRIPTIONS["enforce_stock_longitudinal"]),
initial_state=ui_state.params.get_bool("ToyotaEnforceStockLongitudinal"),
callback=self._on_enable_enforce_stock_longitudinal,
enabled=lambda: not ui_state.engaged,
)
self.items = [self.enforce_stock_longitudinal, ]
def _on_enable_enforce_stock_longitudinal(self, state: bool):
if state:
def confirm_callback(result: int):
if result == DialogResult.CONFIRM:
ui_state.params.put_bool("ToyotaEnforceStockLongitudinal", True)
if ui_state.params.get_bool("AlphaLongitudinalEnabled"):
ui_state.params.put_bool("AlphaLongitudinalEnabled", False)
ui_state.params.put_bool("OnroadCycleRequested", True)
else:
self.enforce_stock_longitudinal.action_item.set_state(False)
content = (f"<h1>{self.enforce_stock_longitudinal.title}</h1><br>" +
f"<p>{self.enforce_stock_longitudinal.description}</p>")
dlg = ConfirmDialog(content, tr("Enable"), rich=True)
gui_app.set_modal_overlay(dlg, callback=confirm_callback)
else:
ui_state.params.put_bool("ToyotaEnforceStockLongitudinal", False)
ui_state.params.put_bool("OnroadCycleRequested", True)
def update_settings(self):
pass
@@ -1,87 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
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
@@ -1,97 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.system.ui.lib.application import 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)
@@ -1,39 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from enum import IntEnum
from openpilot.selfdrive.ui.mici.layouts.settings import settings as OP
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici
ICON_SIZE = 70
OP.PanelType = IntEnum( # type: ignore
"PanelType",
[es.name for es in OP.PanelType] + [
"SUNNYLINK",
],
start=0,
)
class SettingsLayoutSP(OP.SettingsLayout):
def __init__(self):
OP.SettingsLayout.__init__(self)
sunnylink_btn = BigButton("sunnylink", "", "icons_mici/settings/developer/ssh.png")
sunnylink_btn.set_click_callback(lambda: self._set_current_panel(OP.PanelType.SUNNYLINK))
self._panels.update({
OP.PanelType.SUNNYLINK: OP.PanelInfo("sunnylink", SunnylinkLayoutMici(back_callback=lambda: self._set_current_panel(None))),
})
items = self._scroller._items.copy()
items.insert(1, sunnylink_btn)
self._scroller._items.clear()
for item in items:
self._scroller.add_widget(item)
@@ -1,211 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from collections.abc import Callable
import pyray as rl
from cereal import custom
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle
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):
def __init__(self, back_callback: Callable):
super().__init__()
self.set_back_callback(back_callback)
self._restore_in_progress = False
self._backup_in_progress = False
self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled")
self._sunnylink_toggle = BigToggle(text=tr("enable sunnylink"),
initial_state=self._sunnylink_enabled,
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"), "", "")
self._backup_btn.set_click_callback(lambda: self._handle_backup_restore_btn(restore=False))
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=self._sunnylink_uploader_callback)
self._scroller = Scroller([
self._sunnylink_toggle,
self._sunnylink_sponsor_button,
self._sunnylink_pair_button,
self._backup_btn,
self._restore_btn,
self._sunnylink_uploader_toggle
], snap_items=False)
def _update_state(self):
super()._update_state()
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)
self._restore_btn.set_visible(self._sunnylink_enabled)
self._sunnylink_uploader_toggle.set_visible(self._sunnylink_enabled)
self.handle_backup_restore_progress()
if ui_state.sunnylink_state.is_sponsor():
self._sunnylink_sponsor_button.set_text(tr("thanks"))
self._sunnylink_sponsor_button.set_value(ui_state.sunnylink_state.get_sponsor_tier().name.lower())
self._sunnylink_sponsor_button.set_enabled(False)
else:
self._sunnylink_sponsor_button.set_text(tr("sponsor"))
self._sunnylink_sponsor_button.set_value("")
if ui_state.sunnylink_state.is_paired():
self._sunnylink_pair_button.set_text(tr("paired"))
else:
self._sunnylink_pair_button.set_text(tr("pair"))
def show_event(self):
super().show_event()
self._scroller.show_event()
ui_state.update_params()
def _render(self, rect: rl.Rectangle):
self._scroller.render(rect)
@staticmethod
def _sunnylink_toggle_callback(state: bool):
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
def _sunnylink_uploader_callback(state: bool):
ui_state.params.put_bool("EnableSunnylinkUploader", state)
def _handle_backup_restore_btn(self, restore: bool = False):
lbl = tr("slide to restore") if restore else tr("slide to backup")
icon = "icons_mici/settings/device/update.png"
dlg = BigConfirmationDialogV2(lbl, icon, confirm_callback=self._restore_handler if restore else self._backup_handler)
gui_app.set_modal_overlay(dlg)
def _backup_handler(self):
self._backup_in_progress = True
self._backup_btn.set_enabled(False)
ui_state.params.put_bool("BackupManager_CreateBackup", True)
def _restore_handler(self):
self._restore_in_progress = True
self._restore_btn.set_enabled(False)
ui_state.params.put("BackupManager_RestoreVersion", "latest")
def handle_backup_restore_progress(self):
sunnylink_backup_manager = ui_state.sm["backupManagerSP"]
backup_status = sunnylink_backup_manager.backupStatus
restore_status = sunnylink_backup_manager.restoreStatus
backup_progress = sunnylink_backup_manager.backupProgress
restore_progress = sunnylink_backup_manager.restoreProgress
if self._backup_in_progress:
self._restore_btn.set_enabled(False)
self._backup_btn.set_enabled(False)
if backup_status == custom.BackupManagerSP.Status.inProgress:
self._backup_in_progress = True
self._backup_btn.set_text(tr("backing up"))
text = tr(f"{backup_progress}%")
self._backup_btn.set_value(text)
elif backup_status == custom.BackupManagerSP.Status.failed:
self._backup_in_progress = False
self._backup_btn.set_enabled(not ui_state.is_onroad())
self._backup_btn.set_text(tr("backup"))
self._backup_btn.set_value(tr("failed"))
elif (backup_status == custom.BackupManagerSP.Status.completed or
(backup_status == custom.BackupManagerSP.Status.idle and backup_progress == 100.0)):
self._backup_in_progress = False
gui_app.set_modal_overlay(BigDialog(title=tr("settings backed up"), description=""))
self._backup_btn.set_enabled(not ui_state.is_onroad())
elif self._restore_in_progress:
self._restore_btn.set_enabled(False)
self._backup_btn.set_enabled(False)
if restore_status == custom.BackupManagerSP.Status.inProgress:
self._restore_in_progress = True
self._restore_btn.set_text(tr("restoring"))
text = tr(f"{restore_progress}%")
self._restore_btn.set_value(text)
elif restore_status == custom.BackupManagerSP.Status.failed:
self._restore_in_progress = False
self._restore_btn.set_enabled(not ui_state.is_onroad())
self._restore_btn.set_text(tr("restore"))
self._restore_btn.set_value(tr("failed"))
gui_app.set_modal_overlay(BigDialog(title=tr("unable to restore"), description="try again later."))
elif (restore_status == custom.BackupManagerSP.Status.completed or
(restore_status == custom.BackupManagerSP.Status.idle and restore_progress == 100.0)):
self._restore_in_progress = False
gui_app.set_modal_overlay(BigConfirmationDialogV2(
title="slide to restart", icon="icons_mici/settings/device/reboot.png",
confirm_callback=lambda: gui_app.request_close()))
else:
can_enable = self._sunnylink_enabled and not ui_state.is_onroad()
self._backup_btn.set_enabled(can_enable)
self._backup_btn.set_text(tr("backup settings"))
self._backup_btn.set_value("")
self._restore_btn.set_enabled(can_enable)
self._restore_btn.set_text(tr("restore settings"))
self._restore_btn.set_value("")
class SunnylinkPairBigButton(BigButton):
def __init__(self, sponsor_pairing: bool = False):
self.sponsor_pairing = sponsor_pairing
super().__init__("", "", "")
def _update_state(self):
super()._update_state()
def _handle_mouse_release(self, mouse_pos: MousePos):
super()._handle_mouse_release(mouse_pos)
dlg: BigDialog | SunnylinkPairingDialog | None = None
if UNREGISTERED_SUNNYLINK_DONGLE_ID == (ui_state.params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID):
dlg = BigDialog(tr("sunnylink Dongle ID not found. Please reboot & try again."), "")
elif self.sponsor_pairing:
dlg = SunnylinkPairingDialog(sponsor_pairing=True)
elif not self.sponsor_pairing:
dlg = SunnylinkPairingDialog(sponsor_pairing=False)
if dlg:
gui_app.set_modal_overlay(dlg)
@@ -1,26 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.onroad.augmented_road_view import BORDER_COLORS
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
class ConfidenceBallSP:
@staticmethod
def get_animate_status_probs():
if ui_state.status == UIStatus.LAT_ONLY:
return ui_state.sm['modelV2'].meta.disengagePredictions.steerOverrideProbs
# UIStatus.LONG_ONLY
return ui_state.sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs
@staticmethod
def get_lat_long_dot_color():
if ui_state.status == UIStatus.LAT_ONLY:
return BORDER_COLORS[UIStatus.LAT_ONLY]
# UIStatus.LONG_ONLY
return BORDER_COLORS[UIStatus.LONG_ONLY]
@@ -1,13 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.selfdrive.ui.ui_state import UIStatus
LANE_LINE_COLORS_SP = {
UIStatus.LAT_ONLY: rl.Color(0, 255, 64, 255),
UIStatus.LONG_ONLY: rl.Color(0, 255, 64, 255),
}
@@ -1,57 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import base64
import pyray as rl
from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYLINK_DONGLE_ID, API_HOST
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import NavWidget
from openpilot.system.ui.widgets.label import MiciLabel
class SunnylinkPairingDialog(PairingDialog):
"""Dialog for device pairing with QR code."""
def __init__(self, sponsor_pairing: bool = False):
PairingDialog.__init__(self)
self._sponsor_pairing = sponsor_pairing
label_text = tr("pair with sunnylink") if sponsor_pairing else tr("become a sunnypilot sponsor")
self._pair_label = MiciLabel(label_text, 48, font_weight=FontWeight.BOLD,
color=rl.Color(255, 255, 255, int(255 * 0.9)), line_height=40, wrap_text=True)
def _get_pairing_url(self) -> str:
qr_string = "https://github.com/sponsors/sunnyhaibin"
if self._sponsor_pairing:
try:
sl_dongle_id = self._params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID
token = SunnylinkApi(sl_dongle_id).get_token()
inner_string = f"1|{sl_dongle_id}|{token}"
payload_bytes = base64.b64encode(inner_string.encode('utf-8')).decode('utf-8')
qr_string = f"{API_HOST}/sso?state={payload_bytes}"
except Exception:
cloudlog.exception("Failed to get pairing token")
return qr_string
def _update_state(self):
NavWidget._update_state(self)
if __name__ == "__main__":
gui_app.init_window("pairing device")
pairing = SunnylinkPairingDialog(sponsor_pairing=True)
try:
for _ in gui_app.render():
result = pairing.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
if result != -1:
break
finally:
del pairing
@@ -1,13 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.selfdrive.ui.ui_state import UIStatus
BORDER_COLORS_SP = {
UIStatus.LAT_ONLY: rl.Color(0x00, 0xC8, 0xC8, 0xFF), # Cyan for lateral-only state
UIStatus.LONG_ONLY: rl.Color(0x96, 0x1C, 0xA8, 0xFF), # Purple for longitudinal-only state
}
@@ -1,147 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import numpy as np
import pyray as rl
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.text_measure import measure_text_cached
class ChevronOptions:
OFF = 0
DISTANCE_ONLY = 1
SPEED_ONLY = 2
TTC_ONLY = 3
ALL = 4
class ChevronMetrics:
def __init__(self):
self._lead_status_alpha: float = 0.0
self._font = gui_app.font(FontWeight.SEMI_BOLD)
def update_alpha(self, has_lead: bool):
"""Update the alpha value for fade in/out animation"""
if not has_lead:
self._lead_status_alpha = max(0.0, self._lead_status_alpha - 0.05)
else:
self._lead_status_alpha = min(1.0, self._lead_status_alpha + 0.1)
def should_render(self) -> bool:
"""Check if dev UI should be rendered"""
return ui_state.chevron_metrics != ChevronOptions.OFF and self._lead_status_alpha > 0.0
def _draw_lead(self, lead_data, lead_vehicle, v_ego: float, rect: rl.Rectangle):
"""Draw lead vehicle status information (distance, speed, TTC)"""
if not self.should_render():
return
d_rel = lead_data.dRel
v_rel = lead_data.vRel
if not lead_vehicle.chevron or len(lead_vehicle.chevron) < 2:
return
chevron_x = lead_vehicle.chevron[1][0]
chevron_y = lead_vehicle.chevron[1][1]
sz = np.clip((25 * 30) / (d_rel / 3 + 30), 15.0, 30.0) * 2.35
text_lines = self._build_text_lines(d_rel, v_rel, v_ego)
if not text_lines:
return
self._render_text_lines(text_lines, chevron_x, chevron_y, sz, rect)
@staticmethod
def _build_text_lines(d_rel: float, v_rel: float, v_ego: float) -> list[str]:
"""Build text lines based on chevron info setting"""
text_lines = []
# Distance
if ui_state.chevron_metrics == ChevronOptions.DISTANCE_ONLY or ui_state.chevron_metrics == ChevronOptions.ALL:
val = max(0.0, d_rel)
unit = "m" if ui_state.is_metric else "ft"
if not ui_state.is_metric:
val *= 3.28084
text_lines.append(f"{val:.0f} {unit}")
# Speed
if ui_state.chevron_metrics == ChevronOptions.SPEED_ONLY or ui_state.chevron_metrics == ChevronOptions.ALL:
multiplier = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
val = max(0.0, (v_rel + v_ego) * multiplier)
unit = "km/h" if ui_state.is_metric else "mph"
text_lines.append(f"{val:.0f} {unit}")
# Time to collision
if ui_state.chevron_metrics == ChevronOptions.TTC_ONLY or ui_state.chevron_metrics == ChevronOptions.ALL:
val = (d_rel / v_ego) if (d_rel > 0 and v_ego > 0) else 0.0
ttc_text = f"{val:.1f} s" if (0 < val < 200) else "---"
text_lines.append(ttc_text)
return text_lines
def _render_text_lines(self, text_lines: list[str], chevron_x: float, chevron_y: float,
sz: float, rect: rl.Rectangle):
"""Render text lines with proper centering and positioning"""
font_size = 40
line_height = 50
margin = 20
text_y = chevron_y + sz + 15
total_height = len(text_lines) * line_height
# Adjust Y position if text would go off screen
if text_y + total_height > rect.height - margin:
y_max = min(chevron_y, rect.height - margin)
text_y = y_max - 15 - total_height
text_y = max(margin, text_y)
alpha = int(255 * self._lead_status_alpha)
text_color = rl.Color(255, 255, 255, alpha)
shadow_color = rl.Color(0, 0, 0, int(200 * self._lead_status_alpha))
for i, line in enumerate(text_lines):
y = int(text_y + (i * line_height))
if y + line_height > rect.height - margin:
break
# Measure actual text width for proper centering
text_size = measure_text_cached(self._font, line, font_size, 0)
text_width = text_size.x
# Center the text horizontally on the chevron
x = int(chevron_x - text_width / 2)
x = int(np.clip(x, margin, rect.width - text_width - margin))
# Draw shadow
rl.draw_text_ex(self._font, line, rl.Vector2(x + 2, y + 2), font_size, 0, shadow_color)
# Draw text
rl.draw_text_ex(self._font, line, rl.Vector2(x, y), font_size, 0, text_color)
def draw_lead_status(self, sm, radar_state, rect, lead_vehicles):
lead_one = radar_state.leadOne
lead_two = radar_state.leadTwo
has_lead_one = lead_one.status if lead_one else False
has_lead_two = lead_two.status if lead_two else False
self.update_alpha(has_lead_one or has_lead_two)
if not self.should_render():
return
v_ego = sm['carState'].vEgo
if has_lead_one and lead_vehicles[0].chevron:
self._draw_lead(lead_one, lead_vehicles[0], v_ego, rect)
if has_lead_two and lead_vehicles[1].chevron:
d_rel_diff = abs(lead_one.dRel - lead_two.dRel) if has_lead_one else float('inf')
if d_rel_diff > 3.0:
self._draw_lead(lead_two, lead_vehicles[1], v_ego, rect)
@@ -22,7 +22,6 @@ class DeveloperUiRenderer(Widget):
DEV_UI_RIGHT = 1
DEV_UI_BOTTOM = 2
DEV_UI_BOTH = 3
BOTTOM_BAR_HEIGHT = 61
def __init__(self):
super().__init__()
@@ -44,12 +43,6 @@ 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
@@ -85,11 +78,10 @@ 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)
@@ -113,7 +105,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
units_x = x + container_width - 10
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)
@@ -151,35 +143,22 @@ class DeveloperUiRenderer(Widget):
if sm.valid['gpsLocationExternal'] or sm.valid['gpsLocation']:
elements.append(self.altitude_elem.update(sm, ui_state.is_metric))
if not elements:
return
font_size = 38
element_widths = []
for element in elements:
element.measure(self._font_bold, font_size)
element_widths.append(element.total_width)
total_element_width = sum(element_widths)
num_gaps = len(elements) + 1
available_width = rect.width
gap_width = (available_width - total_element_width) / num_gaps
current_x = int(rect.x + 90)
center_y = y + bar_height // 2
current_x = rect.x + gap_width
for element in elements:
current_x += self._draw_bottom_dev_ui_element(current_x, center_y, element)
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:
def _draw_bottom_dev_ui_element(self, x: int, y: int, element: UiElement) -> int:
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)
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)
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)
if element.unit:
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)
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
@@ -10,33 +10,12 @@ 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:
@@ -1,49 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import 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
)
@@ -1,14 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.onroad.chevron_metrics import ChevronMetrics
from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath
class ModelRendererSP:
def __init__(self):
self.rainbow_path = RainbowPath()
self.chevron_metrics = ChevronMetrics()
@@ -1,78 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import time
import colorsys
import pyray as rl
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
class RainbowPath:
DEFAULT_NUM_SEGMENTS = 8
DEFAULT_SPEED = 50.0 # degrees per second
DEFAULT_SATURATION = 0.9
DEFAULT_LIGHTNESS = 0.6
BASE_ALPHA = 0.8
ALPHA_FADE = 0.3 # Alpha reduction from bottom to top
def __init__(self, num_segments: int = None, speed: float = None, saturation: float = None, lightness: float = None):
self.num_segments = num_segments if num_segments is not None else self.DEFAULT_NUM_SEGMENTS
self.speed = speed if speed is not None else self.DEFAULT_SPEED
self.saturation = saturation if saturation is not None else self.DEFAULT_SATURATION
self.lightness = lightness if lightness is not None else self.DEFAULT_LIGHTNESS
def set_speed(self, speed: float):
self.speed = speed
def set_num_segments(self, num_segments: int):
self.num_segments = num_segments
def set_saturation(self, saturation: float):
self.saturation = max(0.0, min(1.0, saturation))
def set_lightness(self, lightness: float):
self.lightness = max(0.0, min(1.0, lightness))
def get_gradient(self) -> Gradient:
time_offset = time.monotonic()
hue_offset = (time_offset * self.speed) % 360.0
segment_colors = []
gradient_stops = []
for i in range(self.num_segments):
position = i / (self.num_segments - 1)
hue = (hue_offset + position * 360.0) % 360.0
alpha = self.BASE_ALPHA * (1.0 - position * self.ALPHA_FADE)
color = self._hsla_to_color(
hue / 360.0,
self.saturation,
self.lightness,
alpha
)
gradient_stops.append(position)
segment_colors.append(color)
return Gradient(
start=(0.0, 1.0), # Bottom of path
end=(0.0, 0.0), # Top of path
colors=segment_colors,
stops=gradient_stops,
)
@staticmethod
def _hsla_to_color(h: float, s: float, l: float, a: float) -> rl.Color:
rgb = colorsys.hls_to_rgb(h, l, s)
return rl.Color(
int(rgb[0] * 255),
int(rgb[1] * 255),
int(rgb[2] * 255),
int(a * 255)
)
def draw_rainbow_path(self, rect, path):
gradient = self.get_gradient()
draw_polygon(rect, path.projected_points, gradient=gradient)
+2 -60
View File
@@ -4,13 +4,10 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from cereal import messaging, log, custom
from cereal import messaging, custom
from openpilot.common.params import Params
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
OpenpilotState = log.SelfdriveState.OpenpilotState
MADSState = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState
class UIStateSP:
def __init__(self):
@@ -22,50 +19,8 @@ class UIStateSP:
self.sunnylink_state = SunnylinkState()
self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True)
def update(self) -> None:
if self.sunnylink_enabled:
self.sunnylink_state.start()
else:
self.sunnylink_state.stop()
@staticmethod
def update_status(ss, ss_sp, onroad_evt) -> str:
state = ss.state
mads = ss_sp.mads
mads_state = mads.state
if state == OpenpilotState.preEnabled:
return "override"
if state == OpenpilotState.overriding:
if not mads.available:
return "override"
if any(e.overrideLongitudinal for e in onroad_evt):
return "override"
if mads_state in (MADSState.paused, MADSState.overriding):
return "override"
# MADS specific statuses
if not mads.available:
return "engaged" if ss.enabled else "disengaged"
if not mads.enabled and not ss.enabled:
return "disengaged"
if mads.enabled and ss.enabled:
return "engaged"
if mads.enabled:
return "lat_only"
if ss.enabled:
return "long_only"
return "disengaged"
self.sunnylink_state.start()
def update_params(self) -> None:
CP_SP_bytes = self.params.get("CarParamsSPPersistent")
@@ -73,16 +28,3 @@ class UIStateSP:
self.CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP)
self.sunnylink_enabled = self.params.get_bool("SunnylinkEnabled")
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)
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)
+1 -3
View File
@@ -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, terms_version_sp, sunnylink_consent_version
from openpilot.system.version import terms_version, training_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,8 +45,6 @@ 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, terms_version_sp, sunnylink_consent_version
from openpilot.system.version import terms_version, training_version
AlertSize = log.SelfdriveState.AlertSize
AlertStatus = log.SelfdriveState.AlertStatus
@@ -378,8 +378,6 @@ 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
+310
View File
@@ -0,0 +1,310 @@
#!/usr/bin/env python3
import capnp
import pathlib
import shutil
import sys
import os
import pywinctl
import pyautogui
import pickle
import time
from collections import namedtuple
from cereal import car, log
from msgq.visionipc import VisionIpcServer, VisionStreamType
from cereal.messaging import PubMaster, log_from_bytes, sub_sock
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.common.transformations.camera import CameraConfig, DEVICE_CAMERAS
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.selfdrive.test.helpers import with_processes
from openpilot.selfdrive.test.process_replay.migration import migrate, migrate_controlsState, migrate_carState
from openpilot.tools.lib.logreader import LogReader
from openpilot.tools.lib.framereader import FrameReader
from openpilot.tools.lib.route import Route
from openpilot.tools.lib.cache import DEFAULT_CACHE_DIR
UI_DELAY = 0.1 # may be slower on CI?
TEST_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19"
STREAMS: list[tuple[VisionStreamType, CameraConfig, bytes]] = []
OFFROAD_ALERTS = ['Offroad_StorageMissing', 'Offroad_IsTakingSnapshot']
DATA: dict[str, capnp.lib.capnp._DynamicStructBuilder] = dict.fromkeys(
["carParams", "deviceState", "pandaStates", "controlsState", "selfdriveState",
"liveCalibration", "modelV2", "radarState", "driverMonitoringState", "carState",
"driverStateV2", "roadCameraState", "wideRoadCameraState", "driverCameraState"], None)
def setup_homescreen(click, pm: PubMaster):
pass
def setup_settings_device(click, pm: PubMaster):
click(100, 100)
def setup_settings_toggles(click, pm: PubMaster):
setup_settings_device(click, pm)
click(278, 600)
time.sleep(UI_DELAY)
def setup_settings_software(click, pm: PubMaster):
setup_settings_device(click, pm)
click(278, 720)
time.sleep(UI_DELAY)
def setup_settings_firehose(click, pm: PubMaster):
click(1780, 730)
def setup_settings_developer(click, pm: PubMaster):
CP = car.CarParams()
CP.alphaLongitudinalAvailable = True
Params().put("CarParamsPersistent", CP.to_bytes())
setup_settings_device(click, pm)
click(278, 970)
time.sleep(UI_DELAY)
def setup_onroad(click, pm: PubMaster):
vipc_server = VisionIpcServer("camerad")
for stream_type, cam, _ in STREAMS:
vipc_server.create_buffers(stream_type, 5, cam.width, cam.height)
vipc_server.start_listener()
uidebug_received_cnt = 0
packet_id = 0
uidebug_sock = sub_sock('uiDebug')
# Condition check for uiDebug processing
check_uidebug = DATA['deviceState'].deviceState.started and not DATA['carParams'].carParams.notCar
# Loop until 20 'uiDebug' messages are received
while uidebug_received_cnt <= 20:
for service, data in DATA.items():
if data:
data.clear_write_flag()
pm.send(service, data)
for stream_type, _, image in STREAMS:
vipc_server.send(stream_type, image, packet_id, packet_id, packet_id)
if check_uidebug:
while uidebug_sock.receive(non_blocking=True):
uidebug_received_cnt += 1
else:
uidebug_received_cnt += 1
packet_id += 1
time.sleep(0.05)
def setup_onroad_disengaged(click, pm: PubMaster):
DATA['selfdriveState'].selfdriveState.enabled = False
setup_onroad(click, pm)
DATA['selfdriveState'].selfdriveState.enabled = True
def setup_onroad_override(click, pm: PubMaster):
DATA['selfdriveState'].selfdriveState.state = log.SelfdriveState.OpenpilotState.overriding
setup_onroad(click, pm)
DATA['selfdriveState'].selfdriveState.state = log.SelfdriveState.OpenpilotState.enabled
def setup_onroad_wide(click, pm: PubMaster):
DATA['selfdriveState'].selfdriveState.experimentalMode = True
DATA["carState"].carState.vEgo = 1
setup_onroad(click, pm)
def setup_onroad_sidebar(click, pm: PubMaster):
setup_onroad(click, pm)
click(500, 500)
setup_onroad(click, pm)
def setup_onroad_wide_sidebar(click, pm: PubMaster):
setup_onroad_wide(click, pm)
click(500, 500)
setup_onroad_wide(click, pm)
def setup_body(click, pm: PubMaster):
DATA['carParams'].carParams.brand = "body"
DATA['carParams'].carParams.notCar = True
DATA['carState'].carState.charging = True
DATA['carState'].carState.fuelGauge = 50.0
setup_onroad(click, pm)
def setup_keyboard(click, pm: PubMaster):
setup_settings_device(click, pm)
click(250, 965)
click(1930, 420)
def setup_keyboard_uppercase(click, pm: PubMaster):
setup_keyboard(click, pm)
click(200, 800)
def setup_driver_camera(click, pm: PubMaster):
setup_settings_device(click, pm)
click(1950, 435)
DATA['deviceState'].deviceState.started = False
setup_onroad(click, pm)
DATA['deviceState'].deviceState.started = True
def setup_onroad_alert(click, pm: PubMaster, text1, text2, size, status=log.SelfdriveState.AlertStatus.normal):
print(f'setup onroad alert, size: {size}')
state = DATA['selfdriveState']
origin_state_bytes = state.to_bytes()
cs = state.selfdriveState
cs.alertText1 = text1
cs.alertText2 = text2
cs.alertSize = size
cs.alertStatus = status
cs.alertType = "test_onroad_alert"
setup_onroad(click, pm)
DATA['selfdriveState'] = log_from_bytes(origin_state_bytes).as_builder()
def setup_onroad_alert_small(click, pm: PubMaster):
setup_onroad_alert(click, pm, 'This is a small alert message', '', log.SelfdriveState.AlertSize.small)
def setup_onroad_alert_mid(click, pm: PubMaster):
setup_onroad_alert(click, pm, 'Medium Alert', 'This is a medium alert message', log.SelfdriveState.AlertSize.mid)
def setup_onroad_alert_full(click, pm: PubMaster):
setup_onroad_alert(click, pm, 'Full Alert', 'This is a full alert message', log.SelfdriveState.AlertSize.full)
def setup_offroad_alert(click, pm: PubMaster):
for alert in OFFROAD_ALERTS:
set_offroad_alert(alert, True)
# Toggle between settings and home to refresh the offroad alert widget
setup_settings_device(click, pm)
click(240, 216)
def setup_update_available(click, pm: PubMaster):
Params().put_bool("UpdateAvailable", True)
release_notes_path = os.path.join(BASEDIR, "RELEASES.md")
with open(release_notes_path) as file:
release_notes = file.read().split('\n\n', 1)[0]
Params().put("UpdaterNewReleaseNotes", release_notes + "\n")
setup_settings_device(click, pm)
click(240, 216)
def setup_pair_device(click, pm: PubMaster):
click(1950, 435)
click(1800, 900)
CASES = {
"homescreen": setup_homescreen,
"prime": setup_homescreen,
"pair_device": setup_pair_device,
"settings_device": setup_settings_device,
"settings_toggles": setup_settings_toggles,
"settings_software": setup_settings_software,
"settings_firehose": setup_settings_firehose,
"settings_developer": setup_settings_developer,
"onroad": setup_onroad,
"onroad_disengaged": setup_onroad_disengaged,
"onroad_override": setup_onroad_override,
"onroad_sidebar": setup_onroad_sidebar,
"onroad_alert_small": setup_onroad_alert_small,
"onroad_alert_mid": setup_onroad_alert_mid,
"onroad_alert_full": setup_onroad_alert_full,
"onroad_wide": setup_onroad_wide,
"onroad_wide_sidebar": setup_onroad_wide_sidebar,
"driver_camera": setup_driver_camera,
"body": setup_body,
"offroad_alert": setup_offroad_alert,
"update_available": setup_update_available,
"keyboard": setup_keyboard,
"keyboard_uppercase": setup_keyboard_uppercase
}
TEST_DIR = pathlib.Path(__file__).parent
TEST_OUTPUT_DIR = TEST_DIR / "report_1"
SCREENSHOTS_DIR = TEST_OUTPUT_DIR / "screenshots"
class TestUI:
def __init__(self):
os.environ["SCALE"] = "1"
sys.modules["mouseinfo"] = False
def setup(self):
self.pm = PubMaster(list(DATA.keys()))
DATA['deviceState'].deviceState.networkType = log.DeviceState.NetworkType.wifi
DATA['deviceState'].deviceState.lastAthenaPingTime = 0
for _ in range(10):
self.pm.send('deviceState', DATA['deviceState'])
DATA['deviceState'].clear_write_flag()
time.sleep(0.05)
try:
self.ui = pywinctl.getWindowsWithTitle("ui")[0]
except Exception as e:
print(f"failed to find ui window, assuming that it's in the top left (for Xvfb) {e}")
self.ui = namedtuple("bb", ["left", "top", "width", "height"])(0,0,2160,1080)
def screenshot(self, name):
im = pyautogui.screenshot(SCREENSHOTS_DIR / f"{name}.png", region=(self.ui.left, self.ui.top, self.ui.width, self.ui.height))
assert im.width == 2160
assert im.height == 1080
def click(self, x, y, *args, **kwargs):
pyautogui.click(self.ui.left + x, self.ui.top + y, *args, **kwargs)
time.sleep(UI_DELAY) # give enough time for the UI to react
@with_processes(["ui"])
def test_ui(self, name, setup_case):
self.setup()
setup_case(self.click, self.pm)
self.screenshot(name)
def create_screenshots():
if TEST_OUTPUT_DIR.exists():
shutil.rmtree(TEST_OUTPUT_DIR)
SCREENSHOTS_DIR.mkdir(parents=True)
route = Route(TEST_ROUTE)
segnum = 2
lr = LogReader(route.qlog_paths()[segnum])
DATA['carParams'] = next((event.as_builder() for event in lr if event.which() == 'carParams'), None)
for event in migrate(lr, [migrate_controlsState, migrate_carState]):
if event.which() in DATA:
DATA[event.which()] = event.as_builder()
if all(DATA.values()):
break
cam = DEVICE_CAMERAS[("tici", "ar0231")]
frames_cache = f'{DEFAULT_CACHE_DIR}/ui_frames'
if os.path.isfile(frames_cache):
with open(frames_cache, 'rb') as f:
frames = pickle.load(f)
road_img = frames[0]
wide_road_img = frames[1]
driver_img = frames[2]
else:
with open(frames_cache, 'wb') as f:
road_img = FrameReader(route.camera_paths()[segnum], pix_fmt="nv12").get(0)
wide_road_img = FrameReader(route.ecamera_paths()[segnum], pix_fmt="nv12").get(0)
driver_img = FrameReader(route.dcamera_paths()[segnum], pix_fmt="nv12").get(0)
pickle.dump([road_img, wide_road_img, driver_img], f)
STREAMS.append((VisionStreamType.VISION_STREAM_ROAD, cam.fcam, road_img.flatten().tobytes()))
STREAMS.append((VisionStreamType.VISION_STREAM_WIDE_ROAD, cam.ecam, wide_road_img.flatten().tobytes()))
STREAMS.append((VisionStreamType.VISION_STREAM_DRIVER, cam.dcam, driver_img.flatten().tobytes()))
t = TestUI()
for name, setup in CASES.items():
with OpenpilotPrefix():
params = Params()
params.put("DongleId", "123456789012345")
if name == 'prime':
params.put('PrimeType', 1)
elif name == 'pair_device':
params.put('ApiCache_Device', {"is_paired":0, "prime_type":-1})
t.test_ui(name, setup)
if __name__ == "__main__":
print("creating test screenshots")
create_screenshots()
+7 -13
View File
@@ -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, DeviceSP
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP
BACKLIGHT_OFFROAD = 65 if HARDWARE.get_device_type() == "mici" else 50
@@ -21,8 +21,6 @@ class UIStatus(Enum):
DISENGAGED = "disengaged"
ENGAGED = "engaged"
OVERRIDE = "override"
LAT_ONLY = "lat_only"
LONG_ONLY = "long_only"
class UIState(UIStateSP):
@@ -100,7 +98,7 @@ class UIState(UIStateSP):
@property
def engaged(self) -> bool:
return self.started and (self.sm["selfdriveState"].enabled or self.sm["selfdriveStateSP"].mads.enabled)
return self.started and self.sm["selfdriveState"].enabled
def is_onroad(self) -> bool:
return self.started
@@ -135,7 +133,10 @@ class UIState(UIStateSP):
# Handle wide road camera state updates
if self.sm.updated["wideRoadCameraState"]:
cam_state = self.sm["wideRoadCameraState"]
self.light_sensor = max(100.0 - cam_state.exposureValPercent, 0.0)
# Scale factor based on sensor type
scale = 6.0 if cam_state.sensor == 'ar0231' else 1.0
self.light_sensor = max(100.0 - scale * cam_state.exposureValPercent, 0.0)
elif not self.sm.alive["wideRoadCameraState"] or not self.sm.valid["wideRoadCameraState"]:
self.light_sensor = -1
@@ -158,8 +159,6 @@ class UIState(UIStateSP):
else:
self.status = UIStatus.ENGAGED if ss.enabled else UIStatus.DISENGAGED
self.status = UIStatus(UIStateSP.update_status(ss, self.sm["selfdriveStateSP"], self.sm["onroadEvents"]))
# Check for engagement state changes
if self.engaged != self._engaged_prev:
for callback in self._engaged_transition_callbacks:
@@ -192,9 +191,8 @@ class UIState(UIStateSP):
self._param_update_time = time.monotonic()
class Device(DeviceSP):
class Device:
def __init__(self):
DeviceSP.__init__(self)
self._ignition = False
self._interaction_time: float = -1
self._override_interactive_timeout: int | None = None
@@ -221,9 +219,6 @@ class Device(DeviceSP):
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
@@ -288,7 +283,6 @@ class Device(DeviceSP):
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)
-4
View File
@@ -24,7 +24,6 @@ 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
@@ -196,7 +195,6 @@ def main(demo=False):
buf_main, buf_extra = None, None
meta_main = FrameMeta()
meta_extra = FrameMeta()
camera_offset_helper = CameraOffsetHelper()
if demo:
@@ -252,14 +250,12 @@ 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)
-36
View File
@@ -1,4 +1,3 @@
import os
import glob
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc', 'transformations')
@@ -29,38 +28,3 @@ 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)
@@ -1,39 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import 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
-89
View File
@@ -1,89 +0,0 @@
#!/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])
+4 -9
View File
@@ -21,7 +21,6 @@ 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
@@ -29,6 +28,7 @@ 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,7 +63,6 @@ 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}
@@ -159,8 +158,7 @@ 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:
recovery_power = self.PLANPLUS_CONTROL * (0.75 if v_ego > 20.0 else 1.0)
plan = plan + recovery_power * model_output['planplus'][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)
@@ -231,7 +229,6 @@ def main(demo=False):
buf_main, buf_extra = None, None
meta_main = FrameMeta()
meta_extra = FrameMeta()
camera_offset_helper = CameraOffsetHelper()
if demo:
@@ -286,15 +283,13 @@ 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)
@@ -1,84 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import 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
+102
View File
@@ -0,0 +1,102 @@
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']
@@ -1,61 +0,0 @@
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)
+9 -1
View File
@@ -2,17 +2,25 @@ 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 TinygradRunner, choosing TinygradSplitRunner if separate vision/policy
Selects between ONNXRunner (for non-TICI platforms) and TinygradRunner
(for TICI platforms), 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}
-3
View File
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:66b3aefa108dd0c7f64205a11e424430c318e6fd06de31b5550d0b9d05616e6a
size 19035
+1 -6
View File
@@ -114,7 +114,7 @@ def initialize_params(params) -> list[dict[str, Any]]:
# hyundai
keys.extend([
"HyundaiLongitudinalTuning",
"HyundaiLongitudinalTuning"
])
# subaru
@@ -128,9 +128,4 @@ def initialize_params(params) -> list[dict[str, Any]]:
"TeslaCoopSteering",
])
# toyota
keys.extend([
"ToyotaEnforceStockLongitudinal",
])
return [{k: params.get(k, return_default=True)} for k in keys]
+1 -16
View File
@@ -42,16 +42,6 @@ METADATA_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__f
params = Params()
# Parameters that should never be remotely modified
BLOCKED_PARAMS = {
"CompletedSunnylinkConsentVersion",
"CompletedTrainingVersion",
"GithubUsername", # Could grant SSH access
"GithubSshKeys", # Direct SSH key injection
"HasAcceptedTerms",
"HasAcceptedTermsSP",
}
def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None:
cloudlog.info("sunnylinkd.handle_long_poll started")
@@ -66,7 +56,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}')
@@ -258,11 +248,6 @@ def getParams(params_keys: list[str], compression: bool = False) -> str | dict[s
@dispatcher.add_method
def saveParams(params_to_update: dict[str, str], compression: bool = False) -> None:
for key, value in params_to_update.items():
# disallow modifications to blocked parameters
if key in BLOCKED_PARAMS:
cloudlog.warning(f"sunnylinkd.saveParams.blocked: Attempted to modify blocked parameter '{key}'")
continue
try:
save_param_from_base64_encoded_string(key, value, compression)
except Exception as e:
@@ -1,59 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.sunnypilot.sunnylink.athena import sunnylinkd
class TestSunnylinkdMethods:
def setup_method(self):
self.saved_params = []
self.original_save = sunnylinkd.save_param_from_base64_encoded_string
def mock_save_param(key, value, compression=False):
self.saved_params.append((key, value, compression))
sunnylinkd.save_param_from_base64_encoded_string = mock_save_param
def teardown_method(self):
sunnylinkd.save_param_from_base64_encoded_string = self.original_save
def test_saveParams_blocked(self):
blocked_params = {
"GithubUsername": "attacker",
"GithubSshKeys": "ssh-rsa attacker_key",
}
sunnylinkd.saveParams(blocked_params)
assert len(self.saved_params) == 0
def test_saveParams_allowed(self):
allowed_params = {
"SpeedLimitOffset": "5",
"MyCustomParam": "123"
}
sunnylinkd.saveParams(allowed_params)
# verify content
assert len(self.saved_params) == 2
keys_saved = [p[0] for p in self.saved_params]
assert "SpeedLimitOffset" in keys_saved
assert "MyCustomParam" in keys_saved
def test_saveParams_mixed(self):
mixed_params = {
"GithubUsername": "attacker",
"SpeedLimitOffset": "10"
}
sunnylinkd.saveParams(mixed_params)
# should save allowed one
assert len(self.saved_params) == 1
assert self.saved_params[0][0] == "SpeedLimitOffset"
assert self.saved_params[0][1] == "10"
+2 -2
View File
@@ -19,7 +19,7 @@ from openpilot.system.version import get_version
from cereal import messaging, custom
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi
from openpilot.sunnypilot.sunnylink.backups.utils import decrypt_compressed_data, encrypt_compressed_data, SnakeCaseEncoder
from openpilot.sunnypilot.sunnylink.backups.utils import decrypt_compressed_data, encrypt_compress_data, SnakeCaseEncoder
from openpilot.sunnypilot.sunnylink.utils import get_param_as_byte, save_param_from_base64_encoded_string
@@ -95,7 +95,7 @@ class BackupManagerSP:
# Serialize and encrypt config data
config_json = json.dumps(config_data)
encrypted_config = encrypt_compressed_data(config_json, use_aes_256=True)
encrypted_config = encrypt_compress_data(config_json, use_aes_256=True)
self._update_progress(50.0, OperationType.BACKUP)
backup_info = custom.BackupManagerSP.BackupInfo()
+31 -49
View File
@@ -4,9 +4,9 @@ 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 base64
import hashlib
import os
import zlib
import re
import json
@@ -14,9 +14,8 @@ from pathlib import Path
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa, ec
from cryptography.hazmat.primitives.asymmetric import rsa
from openpilot.common.api.base import KEYS
from openpilot.sunnypilot.sunnylink.backups.AESCipher import AESCipher
from openpilot.system.hardware.hw import Paths
@@ -28,43 +27,37 @@ class KeyDerivation:
return f.read()
@staticmethod
def derive_aes_key_iv(key_path: str, use_aes_256: bool) -> tuple[bytes, bytes]:
key_pem: bytes = KeyDerivation._load_key(key_path)
key_plain = key_pem.decode(errors="ignore")
def derive_aes_key_iv_from_rsa(key_path: str, use_aes_256: bool) -> tuple[bytes, bytes]:
rsa_key_pem: bytes = KeyDerivation._load_key(key_path)
key_plain = rsa_key_pem.decode(errors="ignore")
if "private" in key_plain.lower():
private_key = serialization.load_pem_private_key(key_pem, password=None, backend=default_backend())
if isinstance(private_key, (rsa.RSAPrivateKey, ec.EllipticCurvePrivateKey)):
public_key = private_key.public_key()
else:
raise ValueError("Invalid key format: Unable to determine if key is public or private.")
private_key = serialization.load_pem_private_key(rsa_key_pem, password=None, backend=default_backend())
if not isinstance(private_key, rsa.RSAPrivateKey):
raise ValueError("Invalid RSA key format: Unable to determine if key is public or private.")
der_data = private_key.private_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
)
elif "public" in key_plain.lower():
public_key = serialization.load_pem_public_key(key_pem, backend=default_backend()) # type: ignore[assignment]
if not isinstance(public_key, (rsa.RSAPublicKey, ec.EllipticCurvePublicKey)):
raise ValueError("Invalid key format: Unable to determine if key is public or private.")
else:
raise ValueError("Invalid key format: Unable to determine if key is public or private.")
public_key = serialization.load_pem_public_key(rsa_key_pem, backend=default_backend())
if not isinstance(public_key, rsa.RSAPublicKey):
raise ValueError("Invalid RSA key format: Unable to determine if key is public or private.")
if isinstance(public_key, rsa.RSAPublicKey):
der_data = public_key.public_bytes(encoding=serialization.Encoding.DER, format=serialization.PublicFormat.PKCS1)
elif isinstance(public_key, ec.EllipticCurvePublicKey):
der_data = public_key.public_bytes(encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo)
else:
raise ValueError("Unsupported key type.")
raise ValueError("Unknown key format: Unable to determine if key is public or private.")
if use_aes_256:
# AES-256-CBC
key = hashlib.sha256(der_data).digest()
iv = hashlib.md5(der_data).digest()
else:
# AES-128-CBC
key = hashlib.md5(der_data).digest()
iv = hashlib.md5(der_data).digest() # Insecure IV reuse, kept for compatibility
sha256_hash = hashlib.sha256(der_data).digest()
aes_key = sha256_hash[:32] if use_aes_256 else sha256_hash[:16]
aes_iv = sha256_hash[16:32]
return key, iv
return aes_key, aes_iv
def uncompress_dat(data):
def qUncompress(data):
"""
Decompress data using zlib.
@@ -78,7 +71,7 @@ def uncompress_dat(data):
return zlib.decompress(data_stripped_4)
def compress_dat(data):
def qCompress(data):
"""
Compress data using zlib.
@@ -92,19 +85,6 @@ def compress_dat(data):
return b"ZLIB" + compressed_data
def get_key_path(use_aes_256=False) -> str:
key_path = ""
for key in KEYS:
if os.path.isfile(Paths.persist_root() + f'/comma/{key}') and os.path.isfile(Paths.persist_root() + f'/comma/{key}.pub'):
key_path = str(Path(Paths.persist_root() + f'/comma/{key}') if use_aes_256 else Path(Paths.persist_root() + f'/comma/{key}.pub'))
break
if not key_path:
raise FileNotFoundError("No valid key pair found in persist storage.")
return key_path
def decrypt_compressed_data(encrypted_base64, use_aes_256=False):
"""
Decrypt and decompress data from base64 string.
@@ -116,17 +96,18 @@ def decrypt_compressed_data(encrypted_base64, use_aes_256=False):
Returns:
str: Decrypted and decompressed string
"""
key_path = Path(f"{Paths.persist_root()}/comma/id_rsa") if use_aes_256 else Path(f"{Paths.persist_root()}/comma/id_rsa.pub")
try:
# Decode base64
encrypted_data = base64.b64decode(encrypted_base64)
# Decrypt
key, iv = KeyDerivation.derive_aes_key_iv(get_key_path(use_aes_256), use_aes_256)
key, iv = KeyDerivation.derive_aes_key_iv_from_rsa(str(key_path), use_aes_256)
cipher = AESCipher(key, iv)
decrypted_data = cipher.decrypt(encrypted_data)
# Decompress
decompressed_data = uncompress_dat(decrypted_data)
decompressed_data = qUncompress(decrypted_data)
# Decode UTF-8
result = decompressed_data.decode('utf-8')
@@ -136,7 +117,7 @@ def decrypt_compressed_data(encrypted_base64, use_aes_256=False):
return ""
def encrypt_compressed_data(text, use_aes_256=True):
def encrypt_compress_data(text, use_aes_256=True):
"""
Compress and encrypt string data to base64.
@@ -147,15 +128,16 @@ def encrypt_compressed_data(text, use_aes_256=True):
Returns:
str: Base64 encoded encrypted data
"""
key_path = Path(f"{Paths.persist_root()}/comma/id_rsa") if use_aes_256 else Path(f"{Paths.persist_root()}/comma/id_rsa.pub")
try:
# Encode to UTF-8
text_bytes = text.encode('utf-8')
# Compress
compressed_data = compress_dat(text_bytes)
compressed_data = qCompress(text_bytes)
# Encrypt
key, iv = KeyDerivation.derive_aes_key_iv(get_key_path(use_aes_256), use_aes_256)
key, iv = KeyDerivation.derive_aes_key_iv_from_rsa(str(key_path), use_aes_256)
cipher = AESCipher(key, iv)
encrypted_data = cipher.encrypt(compressed_data)
+78 -221
View File
@@ -5,17 +5,15 @@
},
"AdbEnabled": {
"title": "Enable ADB",
"description": "Allow ADB connections to the device.",
"long_description": "ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info."
"description": ""
},
"AlphaLongitudinalEnabled": {
"title": "sunnypilot Longitudinal Control (Alpha)",
"description": "Enable sunnypilot longitudinal control alpha for this car (disables AEB).",
"long_description": "<b>WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).</b><br><br>On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. Enable this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. Changing this setting will restart sunnypilot if the car is powered on."
"title": "Alpha Longitudinal",
"description": ""
},
"AlwaysOnDM": {
"title": "Always-On Driver Monitoring",
"description": "Enable driver monitoring even when sunnypilot is not engaged."
"title": "Always-on Driver Monitor",
"description": ""
},
"ApiCache_Device": {
"title": "Api Cache Device",
@@ -84,11 +82,11 @@
]
},
"BackupManager_CreateBackup": {
"title": "Backup Settings",
"title": "Create Backup",
"description": ""
},
"BackupManager_RestoreVersion": {
"title": "Restore Settings",
"title": "Restore Version",
"description": ""
},
"BlindSpot": {
@@ -123,18 +121,9 @@
"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": "Battery Size",
"unit": "kWh"
"description": ""
},
"CarList": {
"title": "Supported Car List",
@@ -176,10 +165,6 @@
"title": "Chevron Info",
"description": ""
},
"CompletedSunnylinkConsentVersion": {
"title": "Completed sunnylink Consent Version",
"description": ""
},
"CompletedTrainingVersion": {
"title": "Completed Training Version",
"description": ""
@@ -197,7 +182,7 @@
"description": ""
},
"CustomAccIncrementsEnabled": {
"title": "Custom ACC Increments",
"title": "Custom ACC Increments Enabled",
"description": ""
},
"CustomAccLongPressIncrement": {
@@ -215,25 +200,24 @@
"step": 1
},
"CustomTorqueParams": {
"title": "Enable Custom Torque Tuning",
"description": "Enables custom tuning for Torque lateral control"
"title": "Custom Torque Params",
"description": ""
},
"DevUIInfo": {
"title": "Developer UI Info",
"description": ""
},
"DeviceBootMode": {
"title": "Wake Up Behavior",
"description": "Choose device behavior after boot/sleep.",
"long_description": "Controls state of the device after boot/sleep.\n\nDefault: Device will boot/wake-up normally & will be ready to engage.\nOffroad: Device will be in Always Offroad mode after boot/wake-up.",
"title": "Device Boot Mode",
"description": "",
"options": [
{
"value": 0,
"label": "Default"
"label": "Standard"
},
{
"value": 1,
"label": "Offroad"
"label": "Always Offroad"
}
]
},
@@ -250,8 +234,8 @@
"description": ""
},
"DisengageOnAccelerator": {
"title": "Disengage on Accelerator Pedal",
"description": "When enabled, pressing the accelerator pedal will disengage sunnypilot."
"title": "Disengage On Accelerator",
"description": ""
},
"DoReboot": {
"title": "Reboot",
@@ -266,7 +250,7 @@
"description": ""
},
"DongleId": {
"title": "Dongle ID",
"title": "Device ID",
"description": ""
},
"DriverTooDistracted": {
@@ -286,18 +270,16 @@
"description": ""
},
"EnableSunnylinkUploader": {
"title": "Enable sunnylink uploader (infrastructure test)",
"description": "Upload driving data to sunnypilot servers (tier-gated).",
"long_description": "Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (Only for highest tiers, and does NOT bring ANY benefit to you yet. We are just testing data volume.)"
"title": "Enable sunnylink Uploader",
"description": ""
},
"EnforceTorqueControl": {
"title": "Enforce Torque Control",
"description": "Enable this to enforce sunnypilot to steer with Torque lateral control."
"description": ""
},
"ExperimentalMode": {
"title": "Experimental Mode",
"description": "Enable alpha experimental features and new driving visualization.",
"long_description": "sunnypilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. Experimental features are listed below:<br><h4>End-to-End Longitudinal Control</h4><br>Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected.<br><h4>New Driving Visualization</h4><br>The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner."
"description": ""
},
"ExperimentalModeConfirmed": {
"title": "Experimental Mode Confirmed",
@@ -360,17 +342,13 @@
"description": ""
},
"HardwareSerial": {
"title": "Serial",
"title": "Serial Number",
"description": ""
},
"HasAcceptedTerms": {
"title": "Has Accepted Terms",
"description": ""
},
"HasAcceptedTermsSP": {
"title": "Has Accepted sunnypilot Terms",
"description": ""
},
"HideVEgoUI": {
"title": "Hide vEgo UI",
"description": ""
@@ -403,8 +381,7 @@
},
"InteractivityTimeout": {
"title": "Interactivity Timeout",
"description": "",
"unit": "seconds"
"description": ""
},
"IsDevelopmentBranch": {
"title": "Is Development Branch",
@@ -419,13 +396,12 @@
"description": ""
},
"IsLdwEnabled": {
"title": "Enable Lane Departure Warnings",
"description": "Alert when drifting over lane lines without signaling.",
"long_description": "Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h)."
"title": "Lane Departure Warnings",
"description": ""
},
"IsMetric": {
"title": "Use Metric System",
"description": "Display speed in km/h instead of mph."
"title": "Use Metric Units",
"description": ""
},
"IsOffroad": {
"title": "Is Offroad",
@@ -460,31 +436,25 @@
"description": ""
},
"LagdToggle": {
"title": "Live Learning Steer Delay",
"description": "Let the car learn and adapt its steering response time.",
"long_description": "Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience."
"title": "LaGD Toggle",
"description": ""
},
"LagdToggleDelay": {
"title": "Adjust Software Delay",
"description": "Adjust the software delay when Live Learning Steer Delay is toggled off. The default software delay value is 0.2",
"min": 0.05,
"max": 0.5,
"step": 0.01,
"unit": "seconds"
"title": "LaGD Toggle Delay",
"description": ""
},
"LagdValueCache": {
"title": "LaGD Value Cache",
"description": ""
},
"LaneTurnDesire": {
"title": "Use Lane Turn Desires",
"description": "Plan a turn at low speeds when signaling.",
"long_description": "If you're driving at 20 mph (32 km/h) or below and have your blinker on, the car will plan a turn in that direction at the nearest drivable path. This prevents situations (like at red lights) where the car might plan the wrong turn direction."
"title": "Lane Turn Desire",
"description": ""
},
"LaneTurnValue": {
"title": "Adjust Lane Turn Speed",
"description": "Set the maximum speed for lane turn desires. Default is 19 mph.",
"min": 5,
"title": "Lane Turn Value",
"description": "",
"min": 0,
"max": 20,
"step": 1
},
@@ -561,12 +531,12 @@
"description": ""
},
"LiveTorqueParamsRelaxedToggle": {
"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."
"title": "Live Torque Params Relaxed Toggle",
"description": ""
},
"LiveTorqueParamsToggle": {
"title": "Self-Tune",
"description": "Enables self-tune for Torque lateral control"
"title": "Live Torque Params Toggle",
"description": ""
},
"LocationFilterInitialState": {
"title": "Location Filter Initial State",
@@ -578,8 +548,7 @@
},
"LongitudinalPersonality": {
"title": "Driving Personality",
"description": "Choose relaxed, standard, or aggressive following behavior.",
"long_description": "Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button.",
"description": "",
"options": [
{
"value": 0,
@@ -643,68 +612,18 @@
},
"MaxTimeOffroad": {
"title": "Max Time Offroad",
"description": "Device will automatically shutdown after set time once the engine is turned off.\n(30h is the default)",
"options": [
{
"value": 0,
"label": "Always On"
},
{
"value": 5,
"label": "5m"
},
{
"value": 10,
"label": "10m"
},
{
"value": 15,
"label": "15m"
},
{
"value": 30,
"label": "30m"
},
{
"value": 60,
"label": "1h"
},
{
"value": 120,
"label": "2h"
},
{
"value": 180,
"label": "3h"
},
{
"value": 300,
"label": "5h"
},
{
"value": 600,
"label": "10h"
},
{
"value": 1440,
"label": "24h"
},
{
"value": 1800,
"label": "30h (Default)"
}
]
"description": ""
},
"ModelManager_ActiveBundle": {
"title": "Current Model",
"title": "Model Manager Active Bundle",
"description": ""
},
"ModelManager_ClearCache": {
"title": "Clear Model Cache",
"title": "Model Manager Clear Cache",
"description": ""
},
"ModelManager_DownloadIndex": {
"title": "Cancel Download",
"title": "Model Manager Download Index",
"description": ""
},
"ModelManager_Favs": {
@@ -712,7 +631,7 @@
"description": ""
},
"ModelManager_LastSyncTime": {
"title": "Refresh Model List",
"title": "Model Manager Last Sync Time",
"description": ""
},
"ModelManager_ModelsCache": {
@@ -770,7 +689,7 @@
"description": ""
},
"OffroadMode": {
"title": "Always Offroad",
"title": "Offroad Mode",
"description": ""
},
"Offroad_CarUnrecognized": {
@@ -834,69 +753,22 @@
"description": ""
},
"OnroadScreenOffBrightness": {
"title": "Onroad Brightness",
"title": "Onroad Screen Off Brightness",
"description": "",
"min": 0,
"max": 100,
"step": 5
},
"OnroadScreenOffControl": {
"title": "Onroad Screen: Reduced Brightness",
"description": "Turn off device screen or reduce brightness after driving starts"
"title": "Onroad Screen Off Control",
"description": ""
},
"OnroadScreenOffTimer": {
"title": "Onroad Brightness Delay",
"title": "Onroad Screen Off Timer",
"description": "",
"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"
}
]
"min": 0,
"max": 60,
"step": 1
},
"OnroadUploads": {
"title": "Onroad Uploads",
@@ -904,8 +776,7 @@
},
"OpenpilotEnabledToggle": {
"title": "Enable sunnypilot",
"description": "Enable sunnypilot adaptive cruise and lane keep.",
"long_description": "Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature."
"description": ""
},
"OsmDbUpdatesCheck": {
"title": "OSM DB Updates Check",
@@ -955,14 +826,6 @@
"title": "Panda Som Reset Triggered",
"description": ""
},
"PlanplusControl": {
"title": "Plan Plus Controls",
"description": "Adjust planplus model recentering strength.",
"long_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": ""
@@ -980,16 +843,16 @@
"description": ""
},
"RecordAudio": {
"title": "Record and Upload Microphone Audio",
"description": "Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."
"title": "Record & Upload Mic Audio",
"description": ""
},
"RecordAudioFeedback": {
"title": "Record Audio Feedback",
"description": ""
},
"RecordFront": {
"title": "Record and Upload Driver Camera",
"description": "Upload data from the driver facing camera and help improve the driver monitoring algorithm."
"title": "Record & Upload Driver Camera",
"description": ""
},
"RecordFrontLock": {
"title": "Record Front Lock",
@@ -1000,7 +863,7 @@
"description": ""
},
"RoadNameToggle": {
"title": "Display Road Name",
"title": "Road Name Toggle",
"description": ""
},
"RouteCount": {
@@ -1013,7 +876,7 @@
},
"ShowAdvancedControls": {
"title": "Show Advanced Controls",
"description": "Enable to show advanced controls on device"
"description": ""
},
"ShowDebugInfo": {
"title": "UI Debug Mode",
@@ -1024,11 +887,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": {
@@ -1036,7 +899,7 @@
"description": ""
},
"SpeedLimitMode": {
"title": "Speed Limit Assist Mode",
"title": "Speed Limit Mode",
"description": "",
"options": [
{
@@ -1076,7 +939,7 @@
]
},
"SpeedLimitPolicy": {
"title": "Speed Limit Source",
"title": "Speed Limit Policy",
"description": "",
"options": [
{
@@ -1102,7 +965,7 @@
]
},
"SpeedLimitValueOffset": {
"title": "Speed Limit Offset Value",
"title": "Speed Limit Value Offset",
"description": "",
"min": -30,
"max": 30,
@@ -1117,13 +980,12 @@
"description": ""
},
"SubaruStopAndGo": {
"title": "Stop and Go (Beta)",
"description": "Experimental feature to enable auto-resume during stop-and-go for certain supported Subaru platforms."
"title": "Subaru Stop and Go",
"description": ""
},
"SubaruStopAndGoManualParkingBrake": {
"title": "Stop and Go for Manual Parking Brake (Beta)",
"description": "Enable stop-and-go for Subaru Global models with manual handbrake.",
"long_description": "Experimental feature to enable stop and go for Subaru Global models with manual handbrake. Models with electric parking brake should keep this disabled. Thanks to martinl for this implementation!"
"title": "Subaru Stop and Go Manual Parking Brake",
"description": ""
},
"SunnylinkCache_Roles": {
"title": "sunnylink Cache Roles",
@@ -1134,12 +996,12 @@
"description": ""
},
"SunnylinkDongleId": {
"title": "Dongle ID",
"title": "sunnylink Dongle ID",
"description": ""
},
"SunnylinkEnabled": {
"title": "Enable sunnylink",
"description": "This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that."
"title": "sunnylink Enabled",
"description": ""
},
"SunnylinkTempFault": {
"title": "sunnylink Temp Fault",
@@ -1158,27 +1020,22 @@
"description": ""
},
"TorqueParamsOverrideEnabled": {
"title": "Manual Real-Time Tuning",
"title": "Torque Params Override Enabled",
"description": ""
},
"TorqueParamsOverrideFriction": {
"title": "Manual Tune - Friction",
"title": "Torque Params Override Friction",
"description": "",
"min": 0.0,
"max": 1.0,
"step": 0.01
},
"TorqueParamsOverrideLatAccelFactor": {
"title": "Manual Tune - Lateral Acceleration Factor",
"title": "Torque Params Override Lat Accel Factor",
"description": "",
"min": 0.1,
"max": 5.0,
"step": 0.1,
"unit": "m/s²"
},
"ToyotaEnforceStockLongitudinal": {
"title": "Enforce Factory Longitudinal Control",
"description": "sunnypilot will not take over control of gas and brakes. Factory Toyota longitudinal control will be used."
"step": 0.1
},
"TrainingVersion": {
"title": "Training Version",
+4 -5
View File
@@ -136,11 +136,10 @@ 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:
roles = response.text
self._params.put("SunnylinkCache_Roles", roles)
self._roles = _parse_roles(response.text)
self._params.put("SunnylinkCache_Roles", response.text)
sponsor_tier = self._get_highest_tier()
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}")
@@ -158,7 +157,7 @@ class SunnylinkState:
users = response.text
self._params.put("SunnylinkCache_Users", users)
with self._lock:
self._users = _parse_users(users)
_parse_users(users)
except Exception as e:
cloudlog.exception(f"Failed to fetch sunnylink users: {e} for dongle id {self.sunnylink_dongle_id}")
+1 -1
View File
@@ -4,7 +4,7 @@ libs = [common, 'OpenCL', messaging, visionipc]
if arch != "Darwin":
camera_obj = env.Object(['cameras/camera_qcom2.cc', 'cameras/camera_common.cc', 'cameras/spectra.cc',
'cameras/cdm.cc', 'sensors/ox03c10.cc', 'sensors/os04c10.cc'])
'cameras/cdm.cc', 'sensors/ar0231.cc', 'sensors/ox03c10.cc', 'sensors/os04c10.cc'])
env.Program('camerad', ['main.cc', camera_obj], LIBS=libs)
if GetOption("extras") and arch == "x86_64":
+1 -1
View File
@@ -13,7 +13,7 @@ typedef enum {
ISP_BPS_PROCESSED, // fully processed image through the BPS
} SpectraOutputType;
// For the comma 3X three camera platform
// For the comma 3/3X three camera platform
struct CameraConfig {
int camera_num;
+3 -2
View File
@@ -1004,8 +1004,9 @@ bool SpectraCamera::openSensor() {
};
// Figure out which sensor we have
if (!init_sensor_lambda(new OS04C10) &&
!init_sensor_lambda(new OX03C10)) {
if (!init_sensor_lambda(new AR0231) &&
!init_sensor_lambda(new OX03C10) &&
!init_sensor_lambda(new OS04C10)) {
LOGE("** sensor %d FAILED bringup, disabling", cc.camera_num);
enabled = false;
return false;
+136
View File
@@ -0,0 +1,136 @@
#include <cassert>
#include <cmath>
#include "system/camerad/sensors/sensor.h"
namespace {
const size_t AR0231_REGISTERS_HEIGHT = 2;
// TODO: this extra height is universal and doesn't apply per camera
const size_t AR0231_STATS_HEIGHT = 2 + 8;
const float sensor_analog_gains_AR0231[] = {
1.0 / 8.0, 2.0 / 8.0, 2.0 / 7.0, 3.0 / 7.0, // 0, 1, 2, 3
3.0 / 6.0, 4.0 / 6.0, 4.0 / 5.0, 5.0 / 5.0, // 4, 5, 6, 7
5.0 / 4.0, 6.0 / 4.0, 6.0 / 3.0, 7.0 / 3.0, // 8, 9, 10, 11
7.0 / 2.0, 8.0 / 2.0, 8.0 / 1.0}; // 12, 13, 14, 15 = bypass
} // namespace
AR0231::AR0231() {
image_sensor = cereal::FrameData::ImageSensor::AR0231;
bayer_pattern = CAM_ISP_PATTERN_BAYER_GRGRGR;
pixel_size_mm = 0.003;
data_word = true;
frame_width = 1928;
frame_height = 1208;
frame_stride = (frame_width * 12 / 8) + 4;
extra_height = AR0231_REGISTERS_HEIGHT + AR0231_STATS_HEIGHT;
registers_offset = 0;
frame_offset = AR0231_REGISTERS_HEIGHT;
stats_offset = AR0231_REGISTERS_HEIGHT + frame_height;
start_reg_array.assign(std::begin(start_reg_array_ar0231), std::end(start_reg_array_ar0231));
init_reg_array.assign(std::begin(init_array_ar0231), std::end(init_array_ar0231));
probe_reg_addr = 0x3000;
probe_expected_data = 0x354;
bits_per_pixel = 12;
mipi_format = CAM_FORMAT_MIPI_RAW_12;
frame_data_type = 0x12; // Changing stats to 0x2C doesn't work, so change pixels to 0x12 instead
mclk_frequency = 19200000; //Hz
readout_time_ns = 22850000;
dc_gain_factor = 2.5;
dc_gain_min_weight = 0;
dc_gain_max_weight = 1;
dc_gain_on_grey = 0.2;
dc_gain_off_grey = 0.3;
exposure_time_min = 2; // with HDR, fastest ss
exposure_time_max = 0x0855; // with HDR, slowest ss, 40ms
analog_gain_min_idx = 0x1; // 0.25x
analog_gain_rec_idx = 0x6; // 0.8x
analog_gain_max_idx = 0xD; // 4.0x
analog_gain_cost_delta = 0;
analog_gain_cost_low = 0.1;
analog_gain_cost_high = 5.0;
for (int i = 0; i <= analog_gain_max_idx; i++) {
sensor_analog_gains[i] = sensor_analog_gains_AR0231[i];
}
min_ev = exposure_time_min * sensor_analog_gains[analog_gain_min_idx];
max_ev = exposure_time_max * dc_gain_factor * sensor_analog_gains[analog_gain_max_idx];
target_grey_factor = 1.0;
black_level = 168;
color_correct_matrix = {
0x000000af, 0x00000ff9, 0x00000fd8,
0x00000fbc, 0x000000bb, 0x00000009,
0x00000fb6, 0x00000fe0, 0x000000ea,
};
for (int i = 0; i < 65; i++) {
float fx = i / 64.0;
const float gamma_k = 0.75;
const float gamma_b = 0.125;
const float mp = 0.01; // ideally midpoint should be adaptive
const float rk = 9 - 100*mp;
// poly approximation for s curve
fx = (fx > mp) ?
((rk * (fx-mp) * (1-(gamma_k*mp+gamma_b)) * (1+1/(rk*(1-mp))) / (1+rk*(fx-mp))) + gamma_k*mp + gamma_b) :
((rk * (fx-mp) * (gamma_k*mp+gamma_b) * (1+1/(rk*mp)) / (1-rk*(fx-mp))) + gamma_k*mp + gamma_b);
gamma_lut_rgb.push_back((uint32_t)(fx*1023.0 + 0.5));
}
prepare_gamma_lut();
linearization_lut = {
0x02000000, 0x02000000, 0x02000000, 0x02000000,
0x020007ff, 0x020007ff, 0x020007ff, 0x020007ff,
0x02000bff, 0x02000bff, 0x02000bff, 0x02000bff,
0x020017ff, 0x020017ff, 0x020017ff, 0x020017ff,
0x02001bff, 0x02001bff, 0x02001bff, 0x02001bff,
0x020023ff, 0x020023ff, 0x020023ff, 0x020023ff,
0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff,
0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff,
0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff,
};
linearization_pts = {0x07ff0bff, 0x17ff1bff, 0x23ff3fff, 0x3fff3fff};
vignetting_lut = {
0x00eaa755, 0x00cf2679, 0x00bc05e0, 0x00acc566, 0x00a1450a, 0x009984cc, 0x0095a4ad, 0x009584ac, 0x009944ca, 0x00a0c506, 0x00ac0560, 0x00bb25d9, 0x00ce2671, 0x00e90748, 0x01112889, 0x014a2a51, 0x01984cc2,
0x00db06d8, 0x00c30618, 0x00afe57f, 0x00a0a505, 0x009524a9, 0x008d646b, 0x0089844c, 0x0089644b, 0x008d2469, 0x0094a4a5, 0x009fe4ff, 0x00af0578, 0x00c20610, 0x00d986cc, 0x00fda7ed, 0x01320990, 0x017aebd7,
0x00d1868c, 0x00baa5d5, 0x00a7853c, 0x009844c2, 0x008cc466, 0x0085a42d, 0x0083641b, 0x0083641b, 0x0085842c, 0x008c4462, 0x0097a4bd, 0x00a6c536, 0x00b9a5cd, 0x00d06683, 0x00f1678b, 0x01226913, 0x0167ab3d,
0x00cd0668, 0x00b625b1, 0x00a30518, 0x0093c49e, 0x00884442, 0x00830418, 0x0080e407, 0x0080c406, 0x0082e417, 0x0087c43e, 0x00932499, 0x00a22511, 0x00b525a9, 0x00cbe65f, 0x00eb0758, 0x011a68d3, 0x015daaed,
0x00cc4662, 0x00b565ab, 0x00a24512, 0x00930498, 0x0087843c, 0x0082a415, 0x00806403, 0x00806403, 0x00828414, 0x00870438, 0x00926493, 0x00a1850c, 0x00b465a3, 0x00cb2659, 0x00ea2751, 0x011928c9, 0x015c2ae1,
0x00cf667b, 0x00b885c4, 0x00a5652b, 0x009624b1, 0x008aa455, 0x00846423, 0x00822411, 0x00822411, 0x00844422, 0x008a2451, 0x009564ab, 0x00a48524, 0x00b785bc, 0x00ce4672, 0x00ee6773, 0x011e88f4, 0x0162eb17,
0x00d6c6b6, 0x00bf65fb, 0x00ac4562, 0x009d04e8, 0x0091848c, 0x0089c44e, 0x00862431, 0x00860430, 0x0089844c, 0x00910488, 0x009c64e3, 0x00ab655b, 0x00be65f3, 0x00d566ab, 0x00f847c2, 0x012b2959, 0x01726b93,
0x00e3e71f, 0x00ca0650, 0x00b705b8, 0x00a7a53d, 0x009c24e1, 0x009484a4, 0x00908484, 0x00908484, 0x009424a1, 0x009bc4de, 0x00a70538, 0x00b625b1, 0x00c90648, 0x00e26713, 0x0108e847, 0x013fe9ff, 0x018bcc5e,
0x00f807c0, 0x00d966cb, 0x00c5862c, 0x00b625b1, 0x00aaa555, 0x00a30518, 0x009f04f8, 0x009f04f8, 0x00a2a515, 0x00aa2551, 0x00b585ac, 0x00c4a625, 0x00d846c2, 0x00f647b2, 0x0121a90d, 0x015e4af2, 0x01b8cdc6,
0x011548aa, 0x00f1678b, 0x00d886c4, 0x00c86643, 0x00bce5e7, 0x00b545aa, 0x00b1658b, 0x00b1458a, 0x00b505a8, 0x00bc85e4, 0x00c7c63e, 0x00d786bc, 0x00efe77f, 0x0113489a, 0x0144ea27, 0x01888c44, 0x01fdcfee,
0x013e49f2, 0x0113e89f, 0x00f5a7ad, 0x00e0c706, 0x00d30698, 0x00cb665b, 0x00c7663b, 0x00c7663b, 0x00cb0658, 0x00d2a695, 0x00dfe6ff, 0x00f467a3, 0x01122891, 0x013be9df, 0x01750ba8, 0x01cfae7d, 0x025912c8,
0x01766bb3, 0x01446a23, 0x011fc8fe, 0x0105e82f, 0x00f467a3, 0x00e9874c, 0x00e46723, 0x00e44722, 0x00e92749, 0x00f3a79d, 0x0104c826, 0x011e48f2, 0x01424a12, 0x01738b9c, 0x01bf6dfb, 0x023611b0, 0x02ced676,
0x01cf8e7c, 0x01866c33, 0x015aaad5, 0x013ae9d7, 0x01250928, 0x011768bb, 0x0110a885, 0x01108884, 0x0116e8b7, 0x01242921, 0x0139a9cd, 0x0158eac7, 0x01840c20, 0x01cb0e58, 0x0233719b, 0x02b9d5ce, 0x03645b22,
};
}
std::vector<i2c_random_wr_payload> AR0231::getExposureRegisters(int exposure_time, int new_exp_g, bool dc_gain_enabled) const {
uint16_t analog_gain_reg = 0xFF00 | (new_exp_g << 4) | new_exp_g;
return {
{0x3366, analog_gain_reg},
{0x3362, (uint16_t)(dc_gain_enabled ? 0x1 : 0x0)},
{0x3012, (uint16_t)exposure_time},
};
}
int AR0231::getSlaveAddress(int port) const {
assert(port >= 0 && port <= 2);
return (int[]){0x20, 0x30, 0x20}[port];
}
float AR0231::getExposureScore(float desired_ev, int exp_t, int exp_g_idx, float exp_gain, int gain_idx) const {
// Cost of ev diff
float score = std::abs(desired_ev - (exp_t * exp_gain)) * 10;
// Cost of absolute gain
float m = exp_g_idx > analog_gain_rec_idx ? analog_gain_cost_high : analog_gain_cost_low;
score += std::abs(exp_g_idx - (int)analog_gain_rec_idx) * m;
// Cost of changing gain
score += std::abs(exp_g_idx - gain_idx) * (score + 1.0) / 10.0;
return score;
}
+121
View File
@@ -0,0 +1,121 @@
#pragma once
const struct i2c_random_wr_payload start_reg_array_ar0231[] = {{0x301A, 0x91C}};
const struct i2c_random_wr_payload stop_reg_array_ar0231[] = {{0x301A, 0x918}};
const struct i2c_random_wr_payload init_array_ar0231[] = {
{0x301A, 0x0018}, // RESET_REGISTER
// **NOTE**: if this is changed, readout_time_ns must be updated in the Sensor config
// CLOCK Settings
// input clock is 19.2 / 2 * 0x37 = 528 MHz
// pixclk is 528 / 6 = 88 MHz
// full roll time is 1000/(PIXCLK/(LINE_LENGTH_PCK*FRAME_LENGTH_LINES)) = 39.99 ms
// img roll time is 1000/(PIXCLK/(LINE_LENGTH_PCK*Y_OUTPUT_CONTROL)) = 22.85 ms
{0x302A, 0x0006}, // VT_PIX_CLK_DIV
{0x302C, 0x0001}, // VT_SYS_CLK_DIV
{0x302E, 0x0002}, // PRE_PLL_CLK_DIV
{0x3030, 0x0037}, // PLL_MULTIPLIER
{0x3036, 0x000C}, // OP_PIX_CLK_DIV
{0x3038, 0x0001}, // OP_SYS_CLK_DIV
// FORMAT
{0x3040, 0xC000}, // READ_MODE
{0x3004, 0x0000}, // X_ADDR_START_
{0x3008, 0x0787}, // X_ADDR_END_
{0x3002, 0x0000}, // Y_ADDR_START_
{0x3006, 0x04B7}, // Y_ADDR_END_
{0x3032, 0x0000}, // SCALING_MODE
{0x30A2, 0x0001}, // X_ODD_INC_
{0x30A6, 0x0001}, // Y_ODD_INC_
{0x3402, 0x0788}, // X_OUTPUT_CONTROL
{0x3404, 0x04B8}, // Y_OUTPUT_CONTROL
{0x3064, 0x1982}, // SMIA_TEST
{0x30BA, 0x11F2}, // DIGITAL_CTRL
// Enable external trigger and disable GPIO outputs
{0x30CE, 0x0120}, // SLAVE_SH_SYNC_MODE | FRAME_START_MODE
{0x340A, 0xE0}, // GPIO3_INPUT_DISABLE | GPIO2_INPUT_DISABLE | GPIO1_INPUT_DISABLE
{0x340C, 0x802}, // GPIO_HIDRV_EN | GPIO0_ISEL=2
// Readout timing
{0x300C, 0x0672}, // LINE_LENGTH_PCK (valid for 3-exposure HDR)
{0x300A, 0x0855}, // FRAME_LENGTH_LINES
{0x3042, 0x0000}, // EXTRA_DELAY
// Readout Settings
{0x31AE, 0x0204}, // SERIAL_FORMAT, 4-lane MIPI
{0x31AC, 0x0C0C}, // DATA_FORMAT_BITS, 12 -> 12
{0x3342, 0x1212}, // MIPI_F1_PDT_EDT
{0x3346, 0x1212}, // MIPI_F2_PDT_EDT
{0x334A, 0x1212}, // MIPI_F3_PDT_EDT
{0x334E, 0x1212}, // MIPI_F4_PDT_EDT
{0x3344, 0x0011}, // MIPI_F1_VDT_VC
{0x3348, 0x0111}, // MIPI_F2_VDT_VC
{0x334C, 0x0211}, // MIPI_F3_VDT_VC
{0x3350, 0x0311}, // MIPI_F4_VDT_VC
{0x31B0, 0x0053}, // FRAME_PREAMBLE
{0x31B2, 0x003B}, // LINE_PREAMBLE
{0x301A, 0x001C}, // RESET_REGISTER
// Noise Corrections
{0x3092, 0x0C24}, // ROW_NOISE_CONTROL
{0x337A, 0x0C80}, // DBLC_SCALE0
{0x3370, 0x03B1}, // DBLC
{0x3044, 0x0400}, // DARK_CONTROL
// Enable temperature sensor
{0x30B4, 0x0007}, // TEMPSENS0_CTRL_REG
{0x30B8, 0x0007}, // TEMPSENS1_CTRL_REG
// Enable dead pixel correction using
// the 1D line correction scheme
{0x31E0, 0x0003},
// HDR Settings
{0x3082, 0x0004}, // OPERATION_MODE_CTRL
{0x3238, 0x0444}, // EXPOSURE_RATIO
{0x1008, 0x0361}, // FINE_INTEGRATION_TIME_MIN
{0x100C, 0x0589}, // FINE_INTEGRATION_TIME2_MIN
{0x100E, 0x07B1}, // FINE_INTEGRATION_TIME3_MIN
{0x1010, 0x0139}, // FINE_INTEGRATION_TIME4_MIN
// TODO: do these have to be lower than LINE_LENGTH_PCK?
{0x3014, 0x08CB}, // FINE_INTEGRATION_TIME_
{0x321E, 0x0894}, // FINE_INTEGRATION_TIME2
{0x31D0, 0x0000}, // COMPANDING, no good in 10 bit?
{0x33DA, 0x0000}, // COMPANDING
{0x318E, 0x0200}, // PRE_HDR_GAIN_EN
// DLO Settings
{0x3100, 0x4000}, // DLO_CONTROL0
{0x3280, 0x0CCC}, // T1 G1
{0x3282, 0x0CCC}, // T1 R
{0x3284, 0x0CCC}, // T1 B
{0x3286, 0x0CCC}, // T1 G2
{0x3288, 0x0FA0}, // T2 G1
{0x328A, 0x0FA0}, // T2 R
{0x328C, 0x0FA0}, // T2 B
{0x328E, 0x0FA0}, // T2 G2
// Initial Gains
{0x3022, 0x0001}, // GROUPED_PARAMETER_HOLD_
{0x3366, 0xFF77}, // ANALOG_GAIN (1x)
{0x3060, 0x3333}, // ANALOG_COLOR_GAIN
{0x3362, 0x0000}, // DC GAIN
{0x305A, 0x00F8}, // red gain
{0x3058, 0x0122}, // blue gain
{0x3056, 0x009A}, // g1 gain
{0x305C, 0x009A}, // g2 gain
{0x3022, 0x0000}, // GROUPED_PARAMETER_HOLD_
// Initial Integration Time
{0x3012, 0x0005},
};
+12
View File
@@ -10,6 +10,7 @@
#include "media/cam_sensor.h"
#include "cereal/gen/cpp/log.capnp.h"
#include "system/camerad/sensors/ar0231_registers.h"
#include "system/camerad/sensors/ox03c10_registers.h"
#include "system/camerad/sensors/os04c10_registers.h"
@@ -87,6 +88,17 @@ public:
};
};
class AR0231 : public SensorInfo {
public:
AR0231();
std::vector<i2c_random_wr_payload> getExposureRegisters(int exposure_time, int new_exp_g, bool dc_gain_enabled) const override;
float getExposureScore(float desired_ev, int exp_t, int exp_g_idx, float exp_gain, int gain_idx) const override;
int getSlaveAddress(int port) const override;
private:
mutable std::map<uint16_t, std::pair<int, int>> ar0231_register_lut;
};
class OX03C10 : public SensorInfo {
public:
OX03C10();
+8 -2
View File
@@ -6,6 +6,7 @@ import struct
import threading
import time
from collections import OrderedDict, namedtuple
from pathlib import Path
import psutil
@@ -23,7 +24,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, terms_version_sp
from openpilot.system.version import terms_version, training_version, get_build_metadata
ThermalStatus = log.DeviceState.ThermalStatus
NetworkType = log.DeviceState.NetworkType
@@ -310,7 +311,6 @@ 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
@@ -345,6 +345,12 @@ def hardware_thread(end_event, hw_queue) -> None:
show_alert = (not onroad_conditions["device_temp_good"] or not startup_conditions["device_temp_engageable"]) and onroad_conditions["ignition"]
set_offroad_alert_if_changed("Offroad_TemperatureTooHigh", show_alert, extra_text=extra_text)
# TODO: this should move to TICI.initialize_hardware, but we currently can't import params there
if TICI and HARDWARE.get_device_type() == "tici":
if not os.path.isfile("/persist/comma/living-in-the-moment"):
if not Path("/data/media").is_mount():
set_offroad_alert_if_changed("Offroad_StorageMissing", True)
# Handle offroad/onroad transition
should_start = all(onroad_conditions.values())
if started_ts is None:
+10 -10
View File
@@ -23,14 +23,14 @@
},
{
"name": "abl",
"url": "https://commadist.azureedge.net/agnosupdate/abl-556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee.img.xz",
"hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee",
"hash_raw": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee",
"url": "https://commadist.azureedge.net/agnosupdate/abl-32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6.img.xz",
"hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
"hash_raw": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
"size": 274432,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee"
"ondevice_hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6"
},
{
"name": "aop",
@@ -56,14 +56,14 @@
},
{
"name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-90bd687e9e407834d4ee1b07f3d05527dfae0ff09c0cacd64cfd6097f6b10e2c.img.xz",
"hash": "90bd687e9e407834d4ee1b07f3d05527dfae0ff09c0cacd64cfd6097f6b10e2c",
"hash_raw": "90bd687e9e407834d4ee1b07f3d05527dfae0ff09c0cacd64cfd6097f6b10e2c",
"size": 17496064,
"url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz",
"hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
"hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
"size": 18515968,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "35014c39b55010ac955c10f808b088e74259147c7a8cbf989b3dff7d95a1e8ae"
"ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0"
},
{
"name": "system",
@@ -81,4 +81,4 @@
"size": 4718592000
}
}
]
]
+12
View File
@@ -61,6 +61,18 @@ BASE_CONFIG = [
]
CONFIGS = {
"tici": [
AmpConfig("Right speaker output from right DAC", 0b1, 0x2C, 0, 0b11111111),
AmpConfig("Right Speaker Mixer Gain", 0b00, 0x2D, 2, 0b00001100),
AmpConfig("Right speaker output volume", 0x1c, 0x3E, 0, 0b00011111),
AmpConfig("DAI2 EQ enable", 0b1, 0x49, 1, 0b00000010),
*configs_from_eq_params(0x84, EQParams(0x274F, 0xC0FF, 0x3BF9, 0x0B3C, 0x1656)),
*configs_from_eq_params(0x8E, EQParams(0x1009, 0xC6BF, 0x2952, 0x1C97, 0x30DF)),
*configs_from_eq_params(0x98, EQParams(0x0F75, 0xCBE5, 0x0ED2, 0x2528, 0x3E42)),
*configs_from_eq_params(0xA2, EQParams(0x091F, 0x3D4C, 0xCE11, 0x1266, 0x2807)),
*configs_from_eq_params(0xAC, EQParams(0x0A9E, 0x3F20, 0xE573, 0x0A8B, 0x3A3B)),
],
"tizi": [
AmpConfig("Left speaker output from left DAC", 0b1, 0x2B, 0, 0b11111111),
AmpConfig("Right speaker output from right DAC", 0b1, 0x2C, 0, 0b11111111),
+10 -5
View File
@@ -446,6 +446,9 @@ class Tici(HardwareBase):
# pandad core
affine_irq(3, "spi_geni") # SPI
if "tici" in self.get_device_type():
affine_irq(3, "xhci-hcd:usb3") # aux panda USB (or potentially anything else on USB)
affine_irq(3, "xhci-hcd:usb1") # internal panda USB (also modem)
try:
pid = subprocess.check_output(["pgrep", "-f", "spi0"], encoding='utf8').strip()
subprocess.call(["sudo", "chrt", "-f", "-p", "1", pid])
@@ -464,20 +467,22 @@ class Tici(HardwareBase):
cmds = []
if self.get_device_type() in ("tizi", ):
if self.get_device_type() in ("tici", "tizi"):
# clear out old blue prime initial APN
os.system('mmcli -m any --3gpp-set-initial-eps-bearer-settings="apn="')
cmds += [
# SIM hot swap
'AT+QSIMDET=1,0',
'AT+QSIMSTAT=1',
# configure modem as data-centric
'AT+QNVW=5280,0,"0102000000000000"',
'AT+QNVFW="/nv/item_files/ims/IMS_enable",00',
'AT+QNVFW="/nv/item_files/modem/mmode/ue_usage_setting",01',
]
if self.get_device_type() == "tizi":
# SIM hot swap, not routed on tici
cmds += [
'AT+QSIMDET=1,0',
'AT+QSIMSTAT=1',
]
elif manufacturer == 'Cavli Inc.':
cmds += [
'AT^SIMSWAP=1', # use SIM slot, instead of internal eSIM
+5
View File
@@ -1,3 +1,5 @@
# TODO: these are also defined in a header
# GPIO pin definitions
class GPIO:
# both GPIO_STM_RST_N and GPIO_LTE_RST_N are misnamed, they are high to reset
@@ -24,4 +26,7 @@ class GPIO:
CAM2_RSTN = 12
# Sensor interrupts
BMX055_ACCEL_INT = 21
BMX055_GYRO_INT = 23
BMX055_MAGN_INT = 87
LSM_INT = 84
+1 -1
View File
@@ -97,7 +97,7 @@ def main() -> None:
(LSM6DS3_Gyro(I2C_BUS_IMU), "gyroscope", True),
(LSM6DS3_Temp(I2C_BUS_IMU), "temperatureSensor", False),
]
if HARDWARE.get_device_type() == "tizi":
if HARDWARE.get_device_type() in ("tizi", "tici"):
sensors_cfg.append(
(MMC5603NJ_Magn(I2C_BUS_IMU), "magnetometer", False),
)
+1 -1
View File
@@ -23,7 +23,7 @@ from openpilot.common.realtime import Ratekeeper
from openpilot.system.ui.sunnypilot.lib.application import GuiApplicationExt
_DEFAULT_FPS = int(os.getenv("FPS", {'tizi': 20}.get(HARDWARE.get_device_type(), 60)))
_DEFAULT_FPS = int(os.getenv("FPS", {'tizi': 20, "tici": 20}.get(HARDWARE.get_device_type(), 60)))
FPS_LOG_INTERVAL = 5 # Seconds between logging FPS drops
FPS_DROP_THRESHOLD = 0.9 # FPS drop threshold for triggering a warning
FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions
+17 -73
View File
@@ -11,29 +11,14 @@ 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, DualButtonAction
from openpilot.system.ui.widgets.scroller_tici import LineSeparator, LINE_COLOR, LINE_PADDING
_resolve_value, BUTTON_WIDTH, BUTTON_HEIGHT, TEXT_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):
@@ -98,33 +83,6 @@ 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):
@@ -221,8 +179,13 @@ class ListItemSP(ListItem):
return rl.Rectangle(0, 0, 0, 0)
if not self.inline:
text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE)
action_y = item_rect.y + text_size.y + style.ITEM_PADDING * 3
has_description = bool(self.description) and self.description_visible
if has_description:
action_y = item_rect.y + self._text_size.y + style.ITEM_PADDING * 3
else:
action_y = item_rect.y + item_rect.height - style.BUTTON_HEIGHT - style.ITEM_PADDING * 1.5
return rl.Rectangle(item_rect.x + style.ITEM_PADDING, action_y, item_rect.width - (style.ITEM_PADDING * 2), style.BUTTON_HEIGHT)
right_width = self.action_item.get_width_hint()
@@ -292,13 +255,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:
@@ -337,34 +300,15 @@ 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, inline: bool = False) -> ListItemSP:
use_float_scaling: bool = False, label_callback: Callable[[int], str] | None = None) -> 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, inline=inline)
return ListItemSP(title=title, description=description, action_item=action, icon=icon)
def button_item_sp(title: str | Callable[[], str], button_text: str | Callable[[], str], description: str | Callable[[], str] | None = None,
callback: Callable | None = None, enabled: bool | Callable[[], bool] = True) -> ListItemSP:
action = ButtonActionSP(text=button_text, enabled=enabled)
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__()
self._rect = rl.Rectangle(0, 0, 0, height)
def _render(self, _):
line_y = int(self._rect.y + self._rect.height // 2)
rl.draw_line(int(self._rect.x) + LINE_PADDING, line_y,
int(self._rect.x + self._rect.width) - LINE_PADDING, line_y,
LINE_COLOR)
+5
View File
@@ -13,6 +13,7 @@ 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, gui_text_box
NVME = "/dev/nvme0n1"
USERDATA = "/dev/disk/by-partlabel/userdata"
TIMEOUT = 3*60
@@ -48,6 +49,10 @@ class Reset(Widget):
if PC:
return
# Best effort to wipe NVME
os.system(f"sudo umount {NVME}")
os.system(f"yes | sudo mkfs.ext4 {NVME}")
# Removing data and formatting
rm = os.system("sudo rm -rf /data/*")
os.system(f"sudo umount {USERDATA}")
-3
View File
@@ -30,9 +30,6 @@ 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: