diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index b5f941fc76..621aa123c5 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -132,7 +132,6 @@ jobs:
process_replay:
name: process replay
- if: false # disable process_replay for forks
runs-on: ${{
(github.repository == 'commaai/openpilot') &&
((github.event_name != 'pull_request') ||
@@ -169,14 +168,14 @@ jobs:
name: diff_report_${{ github.event.number }}
path: openpilot/selfdrive/test/process_replay/diff_report.txt
- name: Checkout ci-artifacts
- if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master'
+ if: github.repository == 'sunnypilot/sunnypilot' && github.ref == 'refs/heads/master'
uses: actions/checkout@v7
with:
- repository: commaai/ci-artifacts
+ repository: sunnypilot/ci-artifacts
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
path: ${{ github.workspace }}/ci-artifacts
- name: Prepare refs
- if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master'
+ if: github.repository == 'sunnypilot/sunnypilot' && github.ref == 'refs/heads/master'
working-directory: ${{ github.workspace }}/ci-artifacts
run: |
git config user.name "GitHub Actions Bot"
@@ -188,7 +187,7 @@ jobs:
git add .
git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit"
- name: Push refs
- if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master'
+ if: github.repository == 'sunnypilot/sunnypilot' && github.ref == 'refs/heads/master'
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60
with:
timeout_minutes: 2
diff --git a/docs/AI_POLICY.md b/docs/AI_POLICY.md
new file mode 100644
index 0000000000..a32eb96bac
--- /dev/null
+++ b/docs/AI_POLICY.md
@@ -0,0 +1,44 @@
+# AI policy
+
+## Why this exists
+
+We use AI tools ourselves, so this isn't an anti-AI stance. The problem is people submitting code, issues, or comments they don't actually understand. AI makes that very easy to do, and it creates real work for reviewers who have to figure out what you meant when you can't explain it yourself.
+
+If you're not going to put effort into understanding and verifying your submission, we're not going to put effort into reviewing it.
+
+## The rule
+
+You are responsible for everything you submit: code, PR descriptions, issues, bug reports, comments.
+
+1. Understand what you submit. If a reviewer asks why you did something, you answer from your own understanding, not by re-prompting. If you can't do that, don't submit it.
+
+2. Test your change. AI gets things wrong all the time. Run it, break it, confirm it actually works.
+
+3. Driving fixes need real evidence. Attach a dongle ID, upload logs, and include segments that show the fix working. A route hash by itself proves nothing.
+
+4. No AI-generated media (images, diagrams, videos) in issues or PRs.
+
+## Disclosure
+
+If AI tools helped you write something, say so. Add an `Assisted-by:` line in your commit message:
+
+```
+Assisted-by: GitHub Copilot
+Assisted-by: Claude
+```
+
+Disclosing won't count against your PR. It helps reviewers know where to look. Hiding it and getting caught will.
+
+## How we review
+
+Reviewers are looking at whether you understand your own change. Can you explain it? Can you respond to feedback without re-prompting? Does your PR description say why you made the change, not just list what changed?
+
+Good code from someone who used AI and understands what they wrote is fine. How you got there doesn't matter as long as you can stand behind it.
+
+## What happens
+
+Submissions that don't meet this bar get closed. If it keeps happening, you get blocked.
+
+## Maintainers
+
+Maintainers use AI at their discretion. They've earned that through sustained contribution and they know the codebase.
diff --git a/docs/CARS.md b/docs/CARS.md
index c52707f4f3..22c35b9a12 100644
--- a/docs/CARS.md
+++ b/docs/CARS.md
@@ -1,10 +1,10 @@
-
+
# Supported Cars
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
-# 341 Supported Cars
+# 342 Supported Cars
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|
Hardware Needed
|Video|Setup Video|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
@@ -78,8 +78,8 @@ A supported vehicle is one that just works when you install a comma device. All
|Honda|Accord 2018-22|All|openpilot available[1,5](#footnotes)|0 mph|3 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Honda|Accord 2023-25|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Honda|Accord Hybrid 2018-22|All|openpilot available[1,5](#footnotes)|0 mph|3 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Honda|Accord Hybrid 2023-25|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Honda|City (Brazil only) 2023|All|openpilot available[1,5](#footnotes)|0 mph|14 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
+|Honda|Accord Hybrid 2023-26|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
+|Honda|City (Brazil only) 2023-25|All|openpilot available[1,5](#footnotes)|0 mph|14 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Honda|Civic 2019-21|All|openpilot available[1,5](#footnotes)|0 mph|2 mph[4](#footnotes)|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Honda|Civic 2022-24|All|openpilot available[1,5](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
@@ -187,7 +187,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Kia|Niro Plug-in Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai F connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|Optima 2017|Advanced Smart Cruise Control|Stock|0 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai G connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
+|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|Seltos 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
@@ -230,14 +230,15 @@ A supported vehicle is one that just works when you install a comma device. All
|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[](##)|[](##)|Parts
- 1 Mazda connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Nissan[6](#footnotes)|Altima 2019-24|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Nissan B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Nissan[6](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
+|Nissan[6](#footnotes)|Leaf IC 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
|Nissan[6](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Nissan[6](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|32 mph|1 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Ram|2500 2020-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Ram|3500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
+|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
|
|
|Rivian|R1S 2025|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Rivian B connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
-|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
+|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
|
|
|Rivian|R1T 2025|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Rivian B connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|SEAT[12](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|SEAT[12](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index cbeb5f6d3a..0c011ee22b 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -1,3 +1,5 @@
+> sunnypilot follows [commaai/openpilot](https://github.com/commaai/openpilot)'s contributing guidelines. The following applies to all contributions here.
+
# How to contribute
Our software is open source so you can solve your own problems without needing help from others. And if you solve a problem and are so kind, you can upstream it for the rest of the world to use. Check out our [post about externalization](https://blog.comma.ai/a-2020-theme-externalization/).
@@ -35,6 +37,7 @@ All of these are examples of good PRs:
* **UI design**: we do not have a good review process for this yet
* **New features**: We believe openpilot is mostly feature-complete, and the rest is a matter of refinement and fixing bugs. As a result of this, most feature PRs will be immediately closed, however the beauty of open source is that forks can and do offer features that upstream openpilot doesn't.
* **Negative expected value**: This is a class of PRs that makes an improvement, but the risk or validation costs more than the improvement. The risk can be mitigated by first getting a failing test merged.
+* **AI-generated contributions**: see our [AI policy](AI_POLICY.md)
### First contribution
diff --git a/opendbc_repo b/opendbc_repo
index 4c64e8a95b..ae445c9b5e 160000
--- a/opendbc_repo
+++ b/opendbc_repo
@@ -1 +1 @@
-Subproject commit 4c64e8a95b4eadca6a9e631d06a854f52bb9ebfa
+Subproject commit ae445c9b5ea18cc66ce3b6d53584db238555dd17
diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h
index c8527b974e..1459c536d7 100644
--- a/openpilot/common/params_keys.h
+++ b/openpilot/common/params_keys.h
@@ -180,6 +180,8 @@ inline static std::unordered_map keys = {
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RocketFuel", {PERSISTENT | BACKUP, BOOL, "0"}},
+ {"ScreenSaverEnabled", {PERSISTENT | BACKUP, BOOL, "1"}},
+ {"ScreenSaverTimeout", {PERSISTENT | BACKUP, INT, "300"}},
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -275,6 +277,7 @@ inline static std::unordered_map keys = {
// Torque lateral control custom params
{"CustomTorqueParams", {PERSISTENT | BACKUP , BOOL}},
{"EnforceTorqueControl", {PERSISTENT | BACKUP, BOOL}},
+ {"LateralJerkTorqueController", {PERSISTENT | BACKUP, BOOL, "0"}},
{"LiveTorqueParamsToggle", {PERSISTENT | BACKUP , BOOL}},
{"LiveTorqueParamsRelaxedToggle", {PERSISTENT | BACKUP , BOOL}},
{"TorqueControlTune", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
diff --git a/openpilot/selfdrive/car/car_specific.py b/openpilot/selfdrive/car/car_specific.py
index 244a8e3b07..a7fdbd0b44 100644
--- a/openpilot/selfdrive/car/car_specific.py
+++ b/openpilot/selfdrive/car/car_specific.py
@@ -56,6 +56,9 @@ class CarSpecificEvents:
if self.CP.minEnableSpeed > 0 and CS.vEgo < 0.001:
events.add(EventName.manualRestart)
+ if CS.brakeHoldActive and CS.blockPcmEnable: # set by Nidec Hybrid which cannot resume from brakehold
+ events.add(EventName.belowEngageSpeed)
+
elif self.CP.brand == 'toyota':
# TODO: when we check for unexpected disengagement, check gear not S1, S2, S3
if self.CP.openpilotLongitudinalControl:
diff --git a/openpilot/selfdrive/car/tests/test_models.py b/openpilot/selfdrive/car/tests/test_models.py
index b98890838f..3dc7dd8a7b 100644
--- a/openpilot/selfdrive/car/tests/test_models.py
+++ b/openpilot/selfdrive/car/tests/test_models.py
@@ -25,6 +25,8 @@ from openpilot.tools.lib.logreader import LogReader, LogsUnavailable, openpilotc
from openpilot.tools.lib.file_sources import Source
from openpilot.tools.lib.route import SegmentName
+from openpilot.sunnypilot.tools.lib.sunnypilot_car_segments import sunnypilot_car_segments_source
+
SafetyModel = car.CarParams.SafetyModel
SteerControlType = structs.CarParams.SteerControlType
@@ -132,7 +134,7 @@ class TestCarModelBase(unittest.TestCase):
segment_range = f"{cls.test_route.route}/{seg}"
try:
- sources: list[Source] = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source]
+ sources: list[Source] = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source, sunnypilot_car_segments_source]
lr = LogReader(segment_range, sources=sources, sort_by_time=True)
return cls.get_testing_data_from_logreader(lr)
except (LogsUnavailable, AssertionError):
diff --git a/openpilot/selfdrive/controls/plannerd.py b/openpilot/selfdrive/controls/plannerd.py
index d80b69ad19..0af341b121 100755
--- a/openpilot/selfdrive/controls/plannerd.py
+++ b/openpilot/selfdrive/controls/plannerd.py
@@ -23,7 +23,7 @@ def main():
cloudlog.info("plannerd got CarParamsSP")
gps_location_service = get_gps_location_service(params)
- ignore_services = ["liveMapDataSP", gps_location_service]
+ ignore_services = ["liveMapDataSP", "carStateSP", "selfdriveStateSP", gps_location_service]
ldw = LaneDepartureWarning()
longitudinal_planner = LongitudinalPlanner(CP, CP_SP)
diff --git a/openpilot/selfdrive/selfdrived/alerts_offroad.json b/openpilot/selfdrive/selfdrived/alerts_offroad.json
index c90497e8c1..8a77bd29a4 100644
--- a/openpilot/selfdrive/selfdrived/alerts_offroad.json
+++ b/openpilot/selfdrive/selfdrived/alerts_offroad.json
@@ -26,7 +26,7 @@
"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.",
+ "text": "sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please select your vehicle manually at https://www.sunnylink.ai/. Need help? Visit https://community.sunnypilot.ai/",
"severity": 0
},
"Offroad_Recalibration": {
@@ -42,7 +42,7 @@
"severity": 0
},
"Offroad_ExcessiveActuation": {
- "text": "Excessive %1 actuation detected on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting.",
+ "text": "Excessive %1 actuation detected on your last drive. Please visit https://community.sunnypilot.ai/ and share your device's Dongle ID for troubleshooting.",
"severity": 1,
"_comment": "Set extra field to lateral or longitudinal."
},
diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py
index cfad1433db..2bc0574e81 100755
--- a/openpilot/selfdrive/selfdrived/selfdrived.py
+++ b/openpilot/selfdrive/selfdrived/selfdrived.py
@@ -93,7 +93,7 @@ class SelfdriveD(CruiseHelper):
# TODO: de-couple selfdrived with card/conflate on carState without introducing controls mismatches
self.car_state_sock = messaging.sub_sock('carState', timeout=20)
- ignore = self.sensor_packets + self.gps_packets + ['alertDebug', 'lateralManeuverPlan'] + ['modelDataV2SP']
+ ignore = self.sensor_packets + self.gps_packets + ['alertDebug', 'lateralManeuverPlan'] + ['modelDataV2SP', 'longitudinalPlanSP']
if SIMULATION:
ignore += ['driverCameraState', 'managerState']
if REPLAY:
diff --git a/openpilot/selfdrive/test/process_replay/process_replay.py b/openpilot/selfdrive/test/process_replay/process_replay.py
index fc3463b376..4834cc44e6 100755
--- a/openpilot/selfdrive/test/process_replay/process_replay.py
+++ b/openpilot/selfdrive/test/process_replay/process_replay.py
@@ -500,7 +500,7 @@ CONFIGS = [
),
ProcessConfig(
proc_name="dmonitoringd",
- pubs=["driverStateV2", "liveCalibration", "carState", "modelV2", "selfdriveState"],
+ pubs=["driverStateV2", "liveCalibration", "carState", "modelV2", "selfdriveState", "carControl"],
subs=["driverMonitoringState"],
ignore=["logMonoTime"],
should_recv_callback=MessageBasedRcvCallback("driverStateV2"),
@@ -511,7 +511,7 @@ CONFIGS = [
pubs=[
"cameraOdometry", "accelerometer", "gyroscope", "liveCalibration", "carState"
],
- subs=["liveLocationKalman", "livePose"],
+ subs=["livePose"],
ignore=["logMonoTime"],
should_recv_callback=MessageBasedRcvCallback("cameraOdometry"),
tolerance=NUMPY_TOLERANCE,
diff --git a/openpilot/selfdrive/test/process_replay/test_processes.py b/openpilot/selfdrive/test/process_replay/test_processes.py
index d9d827add5..1627ab0658 100755
--- a/openpilot/selfdrive/test/process_replay/test_processes.py
+++ b/openpilot/selfdrive/test/process_replay/test_processes.py
@@ -66,7 +66,7 @@ segments = [
# dashcamOnly makes don't need to be tested until a full port is done
excluded_interfaces = ["mock", "body", "psa"]
-BASE_URL = "https://raw.githubusercontent.com/commaai/ci-artifacts/refs/heads/process-replay/"
+BASE_URL = "https://raw.githubusercontent.com/sunnypilot/ci-artifacts/refs/heads/process-replay/"
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")
EXCLUDED_PROCS = {"modeld", "dmonitoringmodeld"}
diff --git a/openpilot/selfdrive/ui/layouts/main.py b/openpilot/selfdrive/ui/layouts/main.py
index 2b8bac22c2..69536912ce 100644
--- a/openpilot/selfdrive/ui/layouts/main.py
+++ b/openpilot/selfdrive/ui/layouts/main.py
@@ -13,6 +13,7 @@ from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout
if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout
+ from openpilot.selfdrive.ui.sunnypilot.layouts.home import HomeLayoutSP as HomeLayout
class MainState(IntEnum):
diff --git a/openpilot/selfdrive/ui/mici/layouts/main.py b/openpilot/selfdrive/ui/mici/layouts/main.py
index 6356a2bc9d..0ddd2decd1 100644
--- a/openpilot/selfdrive/ui/mici/layouts/main.py
+++ b/openpilot/selfdrive/ui/mici/layouts/main.py
@@ -13,6 +13,7 @@ from openpilot.system.ui.lib.application import gui_app
if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
+ from openpilot.selfdrive.ui.sunnypilot.mici.layouts.home import MiciHomeLayoutSP as MiciHomeLayout
ONROAD_DELAY = 2.5 # seconds
diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/home.py b/openpilot/selfdrive/ui/sunnypilot/layouts/home.py
new file mode 100644
index 0000000000..a8c0790d3f
--- /dev/null
+++ b/openpilot/selfdrive/ui/sunnypilot/layouts/home.py
@@ -0,0 +1,68 @@
+"""
+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.layouts.home import HomeLayout, HomeLayoutState, HEAD_BUTTON_FONT_SIZE, SPACING
+from openpilot.system.ui.lib.application import gui_app, FontWeight
+from openpilot.system.ui.lib.text_measure import measure_text_cached
+from openpilot.system.ui.lib.multilang import tr, trn
+from openpilot.system.ui.widgets.label import gui_label
+
+BRAND_FONT_SIZE = 48
+BRAND_DESC_SPACING = 12
+
+
+class HomeLayoutSP(HomeLayout):
+ def _render_header(self):
+ font = gui_app.font(FontWeight.MEDIUM)
+
+ version_text_width = self.header_rect.width
+
+ if self.update_available:
+ version_text_width -= self.update_notif_rect.width
+
+ highlight_color = rl.Color(75, 95, 255, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(54, 77, 239, 255)
+ rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color)
+
+ text = tr("UPDATE")
+ text_size = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE)
+ text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_size.x) // 2
+ text_y = self.update_notif_rect.y + (self.update_notif_rect.height - text_size.y) // 2
+ rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
+
+ if self.alert_count > 0:
+ version_text_width -= self.alert_notif_rect.width
+
+ highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255)
+ rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color)
+
+ alert_text = trn("{} ALERT", "{} ALERTS", self.alert_count).format(self.alert_count)
+ text_size = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE)
+ text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_size.x) // 2
+ text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - text_size.y) // 2
+ rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
+
+ if self.update_available or self.alert_count > 0:
+ version_text_width -= SPACING * 1.5
+
+ version_right = self.header_rect.x + self.header_rect.width
+ version_left = version_right - version_text_width
+
+ brand = "sunnypilot"
+ description = self.params.get("UpdaterCurrentDescription") or ""
+
+ desc_width = 0
+ if description:
+ desc_size = measure_text_cached(gui_app.font(FontWeight.NORMAL), description, BRAND_FONT_SIZE)
+ desc_width = desc_size.x
+ desc_rect = rl.Rectangle(version_right - desc_width, self.header_rect.y, desc_width, self.header_rect.height)
+ gui_label(desc_rect, description, BRAND_FONT_SIZE, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
+
+ brand_size = measure_text_cached(gui_app.font(FontWeight.AUDIOWIDE), brand, BRAND_FONT_SIZE)
+ spacing = BRAND_DESC_SPACING if description else 0
+ brand_x = version_right - desc_width - spacing - brand_size.x
+ brand_rect = rl.Rectangle(max(version_left, brand_x), self.header_rect.y, brand_size.x, self.header_rect.height)
+ gui_label(brand_rect, brand, BRAND_FONT_SIZE, rl.WHITE, font_weight=FontWeight.AUDIOWIDE)
diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/onboarding.py b/openpilot/selfdrive/ui/sunnypilot/layouts/onboarding.py
index a86677a7b1..ee4b479afe 100644
--- a/openpilot/selfdrive/ui/sunnypilot/layouts/onboarding.py
+++ b/openpilot/selfdrive/ui/sunnypilot/layouts/onboarding.py
@@ -20,7 +20,7 @@ class SunnylinkConsentPage(Widget):
self._done_callback = done_callback
self._step = 0
- self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
+ self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.AUDIOWIDE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
self._content = [
{
diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py
index 8ba5663662..897d34085a 100644
--- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py
+++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py
@@ -9,7 +9,7 @@ from enum import IntEnum
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets.scroller_tici import Scroller
-from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp
+from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp
from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES
@@ -61,10 +61,26 @@ class DisplayLayout(Widget):
f"{value} s" if value < 60 else f"{int(value/60)} m"),
inline=True
)
+ self._screensaver_toggle = toggle_item_sp(
+ param="ScreenSaverEnabled",
+ title=lambda: tr("Screen Saver"),
+ description=lambda: tr("Show a screen saver when the device is offroad and idle, instead of turning the screen off."),
+ )
+ self._screensaver_timeout = option_item_sp(
+ param="ScreenSaverTimeout",
+ title=lambda: tr("Screen Saver Duration"),
+ description=lambda: tr("How long the screen saver runs before the screen turns off."),
+ min_value=60,
+ max_value=600,
+ value_change_step=60,
+ label_callback=lambda value: f"{int(value/60)} m"
+ )
items = [
self._onroad_brightness,
self._onroad_brightness_timer,
self._interactivity_timeout,
+ self._screensaver_toggle,
+ self._screensaver_timeout,
]
return items
@@ -87,6 +103,8 @@ class DisplayLayout(Widget):
brightness_val = self._onroad_brightness.action_item.current_value
self._onroad_brightness_timer.action_item.set_enabled(brightness_val not in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK))
+ self._screensaver_timeout.set_visible(self._screensaver_toggle.action_item.get_state())
+
def _render(self, rect):
self._scroller.render(rect)
diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering.py
index 15cb6a15e0..28d9236361 100644
--- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering.py
+++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering.py
@@ -139,7 +139,8 @@ class SteeringLayout(Widget):
self._nnlc_toggle.action_item.set_state(False)
enforce_torque_enabled = False
nnlc_enabled = False
- self._nnlc_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not enforce_torque_enabled)
+ jerk_aware_enabled = ui_state.params.get_bool("LateralJerkTorqueController")
+ self._nnlc_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not enforce_torque_enabled and not jerk_aware_enabled)
self._torque_control_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not nnlc_enabled)
self._torque_customization_button.action_item.set_enabled(self._torque_control_toggle.action_item.get_state())
diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py
index f3c4419e45..6dae8308cd 100644
--- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py
+++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py
@@ -40,6 +40,13 @@ class TorqueSettingsLayout(Widget):
self.cached_torque_versions = json.load(f)
def _initialize_items(self):
+ self._jerk_aware_toggle = toggle_item_sp(
+ param="LateralJerkTorqueController",
+ title=lambda: tr("Lateral Jerk Torque Controller"),
+ description=lambda: tr("Looks ahead at planned steering to reduce sudden corrections, so the wheel moves " +
+ "more smoothly through turns. Works with Self-Tune and custom tuning. " +
+ "Thanks to @twilsonco for the implementation."),
+ )
self._torque_control_versions = ListItemSP(
title=tr("Torque Control Tune Version"),
description="Select the version of Torque Control Tune to use.",
@@ -95,6 +102,7 @@ class TorqueSettingsLayout(Widget):
)
items = [
+ self._jerk_aware_toggle,
self._torque_control_versions,
self._self_tune_toggle,
self._relaxed_tune_toggle,
@@ -107,6 +115,8 @@ class TorqueSettingsLayout(Widget):
def _update_state(self):
super()._update_state()
+ nnlc_enabled = ui_state.params.get_bool("NeuralNetworkLateralControl")
+ self._jerk_aware_toggle.action_item.set_enabled(ui_state.is_offroad() and not nnlc_enabled)
if not ui_state.params.get_bool("LiveTorqueParamsToggle"):
ui_state.params.remove("LiveTorqueParamsRelaxedToggle")
self._relaxed_tune_toggle.action_item.set_state(False)
diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py
new file mode 100644
index 0000000000..d29e579c52
--- /dev/null
+++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py
@@ -0,0 +1,15 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
+from openpilot.system.ui.lib.application import FontWeight
+from openpilot.system.ui.widgets.label import UnifiedLabel
+
+
+class MiciHomeLayoutSP(MiciHomeLayout):
+ def __init__(self):
+ super().__init__()
+ self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False)
diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py
index 8999281430..af8f348f72 100644
--- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py
+++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py
@@ -4,7 +4,6 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
-from collections.abc import Callable
import pyray as rl
from openpilot.cereal import custom
@@ -48,10 +47,8 @@ class CurrentModelInfo(Widget):
self.info_text.render()
class ModelsLayoutMici(NavScroller):
- def __init__(self, back_callback: Callable):
+ def __init__(self):
super().__init__()
- self.set_back_callback(back_callback)
- self.original_back_callback = back_callback
self.focused_widget = None
self.current_model_info = CurrentModelInfo()
@@ -85,12 +82,10 @@ class ModelsLayoutMici(NavScroller):
return folders
- def _show_selection_view(self, items, back_callback: Callable):
- self._scroller._items = items
- for item in items:
- item.set_touch_valid_callback(lambda: self._scroller.scroll_panel.is_touch_valid() and self._scroller.enabled)
- self._scroller.scroll_panel.set_offset(0)
- self.set_back_callback(back_callback)
+ def _push_selection_view(self, items):
+ scroller = NavScroller()
+ scroller._scroller.add_widgets(items)
+ gui_app.push_widget(scroller)
def _show_folders(self):
self.focused_widget = self.select_model_btn
@@ -112,15 +107,18 @@ class ModelsLayoutMici(NavScroller):
folder_buttons.insert(0, btn)
else:
folder_buttons.append(btn)
- self._show_selection_view(folder_buttons, self._reset_main_view)
+ self._push_selection_view(folder_buttons)
+
+ def _pop_to_main(self):
+ gui_app.pop_widgets_to(self)
def _select_model(self, bundle):
ui_state.params.put("ModelManager_DownloadIndex", bundle.index)
- self._reset_main_view()
+ self._pop_to_main()
def _select_default(self):
ui_state.params.remove("ModelManager_ActiveBundle")
- self._reset_main_view()
+ self._pop_to_main()
def _select_folder(self, folder_name):
favs = ui_state.params.get("ModelManager_Favs")
@@ -135,13 +133,7 @@ class ModelsLayoutMici(NavScroller):
btn = BigButton(txt)
btn.set_click_callback(lambda b=bundle: self._select_model(b))
btns.append(btn)
- self._show_selection_view(btns, self._show_folders)
-
- def _reset_main_view(self):
- self._scroller._items = self.main_items # type: ignore[assignment] # ty: ignore[invalid-assignment]
- self.set_back_callback(self.original_back_callback)
- self._scroller.scroll_panel.set_offset(0)
- self._scroller.scroll_to(0)
+ self._push_selection_view(btns)
def hide_event(self):
super().hide_event()
diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/settings.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/settings.py
index 96a4c789c1..f14efe51a3 100644
--- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/settings.py
+++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/settings.py
@@ -32,11 +32,11 @@ class SettingsLayoutSP(OP.SettingsLayout):
BIG_ICON_SIZE)
self.icon_offroad_slider = gui_app.texture("icons_mici/settings/device/lkas.png", BIG_ICON_SIZE, BIG_ICON_SIZE)
- sunnylink_panel = SunnylinkLayoutMici(back_callback=gui_app.pop_widget)
+ sunnylink_panel = SunnylinkLayoutMici()
sunnylink_btn = SettingsBigButton(tr("sunnylink"), "", gui_app.texture("icons_mici/settings/developer/ssh.png", 55, 55))
sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel))
- models_panel = ModelsLayoutMici(back_callback=gui_app.pop_widget)
+ models_panel = ModelsLayoutMici()
models_btn = SettingsBigButton(tr("models"), "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE))
models_btn.set_click_callback(lambda: gui_app.push_widget(models_panel))
diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py
index e804c78035..7c42f99f83 100644
--- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py
+++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py
@@ -6,7 +6,6 @@ See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
-from collections.abc import Callable
from openpilot.cereal import custom
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle
@@ -54,9 +53,8 @@ class SunnylinkInfo(Widget):
self.sponsor_text.render()
class SunnylinkLayoutMici(NavScroller):
- def __init__(self, back_callback: Callable):
+ def __init__(self):
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")
diff --git a/openpilot/selfdrive/ui/sunnypilot/ui_state.py b/openpilot/selfdrive/ui/sunnypilot/ui_state.py
index c2729bbd91..3f2889de9a 100644
--- a/openpilot/selfdrive/ui/sunnypilot/ui_state.py
+++ b/openpilot/selfdrive/ui/sunnypilot/ui_state.py
@@ -12,6 +12,7 @@ from openpilot.common.params import Params
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
from openpilot.system.ui.lib.application import gui_app
+from openpilot.system.ui.sunnypilot.widgets.screen_saver import ScreenSaverSP
OpenpilotState = log.SelfdriveState.OpenpilotState
MADSState = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState
@@ -38,6 +39,9 @@ class UIStateSP:
self.sunnylink_state = SunnylinkState()
+ self.screensaver = ScreenSaverSP(params=self.params)
+ self.screensaver_enabled: bool = False
+
self.active_bundle = None
self.blindspot: bool = False
self.chevron_metrics = None
@@ -170,6 +174,7 @@ class UIStateSP:
self.turn_signals = self.params.get_bool("ShowTurnSignals")
self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True)
self.always_offroad = self.params.get_bool("OffroadMode")
+ self.screensaver_enabled = self.params.get_bool("ScreenSaverEnabled")
if not self._sp_initialized:
self._sp_initialized = True
@@ -184,10 +189,15 @@ class UIStateSP:
self.params.put_bool("EnforceTorqueControl", False, block=True)
self.params.put_bool("NeuralNetworkLateralControl", False, block=True)
+ if self.params.get_bool("LateralJerkTorqueController") and self.params.get_bool("NeuralNetworkLateralControl"):
+ self.params.put_bool("LateralJerkTorqueController", False, block=True)
+ self.params.put_bool("NeuralNetworkLateralControl", False, block=True)
+
# Angle steering: no torque-based lateral controls
if CP.steerControlType == car.CarParams.SteerControlType.angle:
self.params.remove("EnforceTorqueControl")
self.params.remove("NeuralNetworkLateralControl")
+ self.params.remove("LateralJerkTorqueController")
# Alpha longitudinal: clear if not available
if not CP.alphaLongitudinalAvailable:
@@ -200,6 +210,7 @@ class UIStateSP:
# No CarParams: clear all car-dependent params as safety default
self.params.remove("EnforceTorqueControl")
self.params.remove("NeuralNetworkLateralControl")
+ self.params.remove("LateralJerkTorqueController")
self.params.remove("AlphaLongitudinalEnabled")
# No longitudinal control: no experimental mode or DEC
@@ -224,10 +235,26 @@ class UIStateSP:
class DeviceSP:
+ def __init__(self):
+ self._blocked_by_screensaver: bool = False
+
def _set_awake(self, on: bool, _ui_state=None):
+ self._blocked_by_screensaver = False
+
if _ui_state.boot_offroad_mode == 1 and not on:
_ui_state.params.put_bool("OffroadMode", True)
+ if not on and _ui_state.screensaver_enabled:
+ if _ui_state.screensaver.was_dismissed:
+ if gui_app.get_active_widget() == _ui_state.screensaver:
+ gui_app.pop_widget()
+ elif _ui_state.screensaver.is_active:
+ self._blocked_by_screensaver = True
+ else:
+ _ui_state.screensaver.initialize()
+ gui_app.push_widget(_ui_state.screensaver)
+ self._blocked_by_screensaver = True
+
@staticmethod
def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float:
if not awake or not _ui_state.started:
diff --git a/openpilot/selfdrive/ui/tests/diff/replay_script.py b/openpilot/selfdrive/ui/tests/diff/replay_script.py
index 109f32e47a..18f3141fa1 100644
--- a/openpilot/selfdrive/ui/tests/diff/replay_script.py
+++ b/openpilot/selfdrive/ui/tests/diff/replay_script.py
@@ -338,8 +338,11 @@ def build_mici_script(pm: PubMaster, main_layout, script: Script) -> None:
settings_cases: Cases = [
lambda: scroll_through_cases(toggle_cases),
+ None, # sunnylink (just open and close)
+ None, # models (just open and close)
lambda: scroll_through_cases(network_cases),
lambda: scroll_through_cases(device_cases),
+ lambda: script.wait(WAIT_SHORT), # software
lambda: script.wait(WAIT_SHORT), # pairing
lambda: run_actions(lambda: swipe_up(height * 3), lambda: swipe_down(height * 3)), # firehose (scroll down and back up)
lambda: scroll_through_cases(developer_cases),
diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py
index 59742edfed..67845bdc87 100644
--- a/openpilot/selfdrive/ui/ui_state.py
+++ b/openpilot/selfdrive/ui/ui_state.py
@@ -340,6 +340,8 @@ class Device(DeviceSP):
def _set_awake(self, on: bool, _ui_state=None):
if on != self._awake:
super()._set_awake(on, _ui_state or ui_state)
+ if self._blocked_by_screensaver:
+ return
self._awake = on
cloudlog.debug(f"setting display power {int(on)}")
HARDWARE.set_display_power(on)
diff --git a/openpilot/sunnypilot/selfdrive/car/interfaces.py b/openpilot/sunnypilot/selfdrive/car/interfaces.py
index ed5b71d4b1..ecabc53b57 100644
--- a/openpilot/sunnypilot/selfdrive/car/interfaces.py
+++ b/openpilot/sunnypilot/selfdrive/car/interfaces.py
@@ -73,10 +73,16 @@ def _cleanup_unsupported_params(CP: structs.CarParams, CP_SP: structs.CarParamsS
if params is None:
params = Params()
+ if params.get_bool("LateralJerkTorqueController") and params.get_bool("NeuralNetworkLateralControl"):
+ cloudlog.warning("LateralJerkTorqueController and NeuralNetworkLateralControl both enabled, disabling both")
+ params.put_bool("LateralJerkTorqueController", False, block=True)
+ params.put_bool("NeuralNetworkLateralControl", False, block=True)
+
if CP.steerControlType == structs.CarParams.SteerControlType.angle:
cloudlog.warning("SteerControlType is angle, cleaning up params")
params.remove("NeuralNetworkLateralControl")
params.remove("EnforceTorqueControl")
+ params.remove("LateralJerkTorqueController")
if not CP_SP.intelligentCruiseButtonManagementAvailable or CP.openpilotLongitudinalControl:
cloudlog.warning("ICBM not available or openpilot Longitudinal Control enabled, cleaning up params")
diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py
index 39525b3b8e..50add19cd2 100644
--- a/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py
+++ b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py
@@ -33,6 +33,7 @@ class LatControlTorqueExt(NeuralNetworkLateralControl, LatControlTorqueExtOverri
self._output_torque = output_torque
self.update_calculations(CS, VM, desired_lateral_accel)
+ self.update_jerk_aware_torque_control(CS, roll_compensation, gravity_adjusted_lateral_accel)
self.update_neural_network_feedforward(CS, params, calibrated_pose)
return self._pid_log, self._output_torque
diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py
index df773889a9..31ac615db8 100644
--- a/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py
+++ b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py
@@ -132,3 +132,10 @@ class LatControlTorqueExtBase:
self.lat_accel_friction_factor = 1.0
self.lateral_jerk_setpoint = self.lat_jerk_friction_factor * self.lookahead_lateral_jerk
self.lateral_jerk_measurement = self.lat_jerk_friction_factor * self.actual_lateral_jerk
+
+ def update_output_torque(self, CS):
+ freeze_integrator = self._steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
+ self._output_torque = self._pid.update(self._pid_log.error,
+ feedforward=self._ff,
+ speed=CS.vEgo,
+ freeze_integrator=freeze_integrator)
diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_jerk_aware.py b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_jerk_aware.py
new file mode 100644
index 0000000000..8d780ed4cc
--- /dev/null
+++ b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_jerk_aware.py
@@ -0,0 +1,45 @@
+"""
+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 opendbc.car.lateral import FRICTION_THRESHOLD
+from opendbc.sunnypilot.car.interfaces import LatControlInputs
+from opendbc.sunnypilot.car.lateral_ext import get_friction as get_friction_in_torque_space
+from openpilot.common.params import Params
+
+from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import LatControlTorqueExtBase
+
+
+class LatControlTorqueJerkAware(LatControlTorqueExtBase):
+ def __init__(self, lac_torque, CP, CP_SP, CI):
+ super().__init__(lac_torque, CP, CP_SP, CI)
+ self.params = Params()
+ self._jerk_aware_enabled = self.params.get_bool("LateralJerkTorqueController")
+
+ def update_limits(self):
+ if not self._jerk_aware_enabled:
+ return
+ self._pid.set_limits(self.lac_torque.steer_max, -self.lac_torque.steer_max)
+
+ def update_jerk_aware_torque_control(self, CS, roll_compensation, gravity_adjusted_lateral_accel):
+ if not self._jerk_aware_enabled:
+ return
+
+ torque_from_setpoint = self.torque_from_lateral_accel_in_torque_space(
+ LatControlInputs(self._setpoint, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=False
+ )
+ torque_from_measurement = self.torque_from_lateral_accel_in_torque_space(
+ LatControlInputs(self._measurement, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=False
+ )
+
+ self._pid_log.error = float(torque_from_setpoint - torque_from_measurement) # ty: ignore[invalid-assignment]
+ self._ff = self.torque_from_lateral_accel_in_torque_space(
+ LatControlInputs(gravity_adjusted_lateral_accel, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=True
+ )
+
+ friction_input = self.update_friction_input(self._desired_lateral_accel, self._actual_lateral_accel)
+ self._ff += get_friction_in_torque_space(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
+
+ self.update_output_torque(CS)
diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py
index 4d9e4492f9..6ddfaea231 100644
--- a/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py
+++ b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py
@@ -82,7 +82,7 @@ class LatControlTorque(LatControl):
future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2
self.lat_accel_request_buffer.append(future_desired_lateral_accel)
gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation
- desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / lat_delay
+ desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / max(lat_delay, self.dt)
measurement = measured_curvature * CS.vEgo ** 2
measurement_rate = self.measurement_rate_filter.update((measurement - self.previous_measurement) / self.dt)
diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py
index e66072f86f..9684c86688 100644
--- a/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py
+++ b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py
@@ -14,7 +14,8 @@ from opendbc.sunnypilot.car.lateral_ext import get_friction as get_friction_in_t
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params
from openpilot.selfdrive.modeld.constants import ModelConstants
-from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import LatControlTorqueExtBase, sign
+from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import sign
+from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_jerk_aware import LatControlTorqueJerkAware
from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.helpers import MOCK_MODEL_PATH
from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.model import NNTorqueModel
@@ -31,17 +32,18 @@ def roll_pitch_adjust(roll, pitch):
return roll * math.cos(pitch)
-class NeuralNetworkLateralControl(LatControlTorqueExtBase):
+class NeuralNetworkLateralControl(LatControlTorqueJerkAware):
def __init__(self, lac_torque, CP, CP_SP, CI):
super().__init__(lac_torque, CP, CP_SP, CI)
self.params = Params()
self.enabled = self.params.get_bool("NeuralNetworkLateralControl")
- self.has_nn_model = CP_SP.neuralNetworkLateralControl.model.path != MOCK_MODEL_PATH
+ model_path = CP_SP.neuralNetworkLateralControl.model.path
+ self.has_nn_model = model_path not in (MOCK_MODEL_PATH, '')
# NN model takes current v_ego, lateral_accel, lat accel/jerk error, roll, and past/future/planned data
# of lat accel and roll
# Past value is computed using previous desired lat accel and observed roll
- self.model = NNTorqueModel(CP_SP.neuralNetworkLateralControl.model.path)
+ self.model = NNTorqueModel(model_path) if self.has_nn_model else None
self.pitch = FirstOrderFilter(0.0, 0.5, 0.01)
self.pitch_last = 0.0
@@ -64,6 +66,7 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
return self.enabled and self.model_valid and self.has_nn_model
def update_limits(self):
+ super().update_limits()
if not self._nnlc_enabled:
return
@@ -84,13 +87,6 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
self._ff += get_friction_in_torque_space(self._desired_lateral_accel - self._actual_lateral_accel, self._lateral_accel_deadzone,
FRICTION_THRESHOLD, self.torque_params)
- def update_output_torque(self, CS):
- freeze_integrator = self._steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
- self._output_torque = self._pid.update(self._pid_log.error,
- feedforward=self._ff,
- speed=CS.vEgo,
- freeze_integrator=freeze_integrator)
-
def update_neural_network_feedforward(self, CS, params, calibrated_pose) -> None:
if not self._nnlc_enabled:
return
diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_latcontrol_torque_ext.py b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_latcontrol_torque_ext.py
new file mode 100644
index 0000000000..2b47977514
--- /dev/null
+++ b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_latcontrol_torque_ext.py
@@ -0,0 +1,108 @@
+"""
+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.cereal import log, messaging
+from opendbc.car.structs import car
+from opendbc.car.car_helpers import interfaces
+from opendbc.car.honda.values import CAR as HONDA
+from opendbc.car.vehicle_model import VehicleModel
+from openpilot.common.params import Params
+from openpilot.common.realtime import DT_CTRL
+from openpilot.selfdrive.car.helpers import convert_to_capnp
+from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque
+from openpilot.selfdrive.locationd.helpers import Pose
+from openpilot.common.mock.generators import generate_livePose
+from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces
+from openpilot.selfdrive.modeld.constants import ModelConstants
+
+
+def _make_controller(enhanced=False, nnlc=False):
+ params = Params()
+ params.put_bool("EnforceTorqueControl", True, block=True)
+ params.put_bool("LateralJerkTorqueController", enhanced, block=True)
+ params.put_bool("NeuralNetworkLateralControl", nnlc, block=True)
+
+ car_name = HONDA.HONDA_CIVIC
+ CarInterface = interfaces[car_name]
+ CP = CarInterface.get_non_essential_params(car_name)
+ CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name)
+ CI = CarInterface(CP, CP_SP)
+ sunnypilot_interfaces.setup_interfaces(CI, params)
+ CP_SP = convert_to_capnp(CP_SP)
+ VM = VehicleModel(CP)
+ controller = LatControlTorque(CP.as_reader(), CP_SP.as_reader(), CI, DT_CTRL)
+ return controller, VM, CP
+
+
+def _make_model_v2():
+ model = messaging.new_message('modelV2')
+ position = log.XYZTData.new_message()
+ position.x = [float(x) for x in 30.0 * np.array(ModelConstants.T_IDXS)]
+ model.modelV2.position = position
+ orientation = log.XYZTData.new_message()
+ orientation.x = [0.0 for _ in ModelConstants.T_IDXS]
+ orientation.y = [0.0 for _ in ModelConstants.T_IDXS]
+ model.modelV2.orientation = orientation
+ velocity = log.XYZTData.new_message()
+ velocity.x = [30.0 for _ in ModelConstants.T_IDXS]
+ model.modelV2.velocity = velocity
+ acceleration = log.XYZTData.new_message()
+ acceleration.x = [0.0 for _ in ModelConstants.T_IDXS]
+ acceleration.y = [0.0 for _ in ModelConstants.T_IDXS]
+ model.modelV2.acceleration = acceleration
+ return model
+
+
+def _run_update(controller, VM):
+ CS = car.CarState.new_message()
+ CS.vEgo = 30
+ CS.steeringPressed = False
+ lp = generate_livePose()
+ pose = Pose.from_live_pose(lp.livePose)
+ params = log.LiveParametersData.new_message()
+ model_v2 = _make_model_v2().modelV2
+ controller.extension.update_model_v2(model_v2)
+ controller.extension.update_lateral_lag(0.2)
+ return controller.update(True, CS, VM, params, False, 0.5, pose, False, 0.2)
+
+
+class TestLatControlTorqueExt:
+ def test_init_enhanced_only(self):
+ controller, VM, _ = _make_controller(enhanced=True, nnlc=False)
+ assert controller.extension._jerk_aware_enabled
+ assert not controller.extension.enabled # NNLC disabled
+
+ def test_init_nnlc_only(self):
+ controller, VM, _ = _make_controller(enhanced=False, nnlc=True)
+ assert not controller.extension._jerk_aware_enabled
+ assert controller.extension.enabled
+
+ def test_init_neither(self):
+ controller, VM, _ = _make_controller(enhanced=False, nnlc=False)
+ assert not controller.extension._jerk_aware_enabled
+ assert not controller.extension.enabled
+
+ def test_init_both_no_crash(self):
+ controller, VM, _ = _make_controller(enhanced=True, nnlc=True)
+ assert not controller.extension._jerk_aware_enabled
+ assert not controller.extension.enabled
+
+ def test_update_enhanced_only(self):
+ controller, VM, _ = _make_controller(enhanced=True, nnlc=False)
+ output_torque, _, pid_log = _run_update(controller, VM)
+ assert pid_log.active
+
+ def test_update_neither(self):
+ controller, VM, _ = _make_controller(enhanced=False, nnlc=False)
+ output_torque, _, pid_log = _run_update(controller, VM)
+ assert pid_log.active
+
+ def test_update_both_no_crash(self):
+ controller, VM, _ = _make_controller(enhanced=True, nnlc=True)
+ output_torque, _, pid_log = _run_update(controller, VM)
+ assert pid_log.active
diff --git a/openpilot/sunnypilot/sunnylink/settings_ui.json b/openpilot/sunnypilot/sunnylink/settings_ui.json
index cd5f0b118f..dd972c2957 100644
--- a/openpilot/sunnypilot/sunnylink/settings_ui.json
+++ b/openpilot/sunnypilot/sunnylink/settings_ui.json
@@ -323,6 +323,32 @@
"equals": true
},
"items": [
+ {
+ "key": "LateralJerkTorqueController",
+ "widget": "toggle",
+ "title": "Lateral Jerk Torque Controller",
+ "description": "Looks ahead at planned steering to reduce sudden corrections, so the wheel moves more smoothly through turns. Works with Self-Tune and custom tuning. Thanks to @twilsonco for the implementation.",
+ "visibility": [
+ {
+ "type": "not",
+ "condition": {
+ "type": "capability",
+ "field": "steer_control_type",
+ "equals": "angle"
+ }
+ }
+ ],
+ "enablement": [
+ {
+ "type": "offroad_only"
+ },
+ {
+ "type": "param",
+ "key": "NeuralNetworkLateralControl",
+ "equals": false
+ }
+ ]
+ },
{
"key": "LiveTorqueParamsToggle",
"widget": "toggle",
@@ -1260,6 +1286,67 @@
"label": "2 m"
}
]
+ },
+ {
+ "key": "ScreenSaverEnabled",
+ "widget": "toggle",
+ "title": "Screen Saver",
+ "description": "Show a screen saver when the device is offroad and idle, instead of turning the screen off."
+ },
+ {
+ "key": "ScreenSaverTimeout",
+ "widget": "multiple_button",
+ "title": "Screen Saver Duration",
+ "description": "How long the screen saver runs before the screen turns off.",
+ "options": [
+ {
+ "value": 60,
+ "label": "1 m"
+ },
+ {
+ "value": 120,
+ "label": "2 m"
+ },
+ {
+ "value": 180,
+ "label": "3 m"
+ },
+ {
+ "value": 240,
+ "label": "4 m"
+ },
+ {
+ "value": 300,
+ "label": "5 m"
+ },
+ {
+ "value": 360,
+ "label": "6 m"
+ },
+ {
+ "value": 420,
+ "label": "7 m"
+ },
+ {
+ "value": 480,
+ "label": "8 m"
+ },
+ {
+ "value": 540,
+ "label": "9 m"
+ },
+ {
+ "value": 600,
+ "label": "10 m"
+ }
+ ],
+ "enablement": [
+ {
+ "type": "param",
+ "key": "ScreenSaverEnabled",
+ "equals": true
+ }
+ ]
}
]
}
@@ -2037,6 +2124,11 @@
"type": "param",
"key": "EnforceTorqueControl",
"equals": false
+ },
+ {
+ "type": "param",
+ "key": "LateralJerkTorqueController",
+ "equals": false
}
]
}
diff --git a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/display.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/display.yaml
index 39a8cbaf80..3e3b16c374 100644
--- a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/display.yaml
+++ b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/display.yaml
@@ -128,3 +128,36 @@ sections:
label: 1 m
- value: 120
label: 2 m
+ - key: ScreenSaverEnabled
+ widget: toggle
+ title: Screen Saver
+ description: Show a screen saver when the device is offroad and idle, instead of turning the screen off.
+ - key: ScreenSaverTimeout
+ widget: multiple_button
+ title: Screen Saver Duration
+ description: How long the screen saver runs before the screen turns off.
+ options:
+ - value: 60
+ label: 1 m
+ - value: 120
+ label: 2 m
+ - value: 180
+ label: 3 m
+ - value: 240
+ label: 4 m
+ - value: 300
+ label: 5 m
+ - value: 360
+ label: 6 m
+ - value: 420
+ label: 7 m
+ - value: 480
+ label: 8 m
+ - value: 540
+ label: 9 m
+ - value: 600
+ label: 10 m
+ enablement:
+ - type: param
+ key: ScreenSaverEnabled
+ equals: true
diff --git a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/models.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/models.yaml
index 4ae1fca88b..bcb8b895b9 100644
--- a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/models.yaml
+++ b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/models.yaml
@@ -73,6 +73,9 @@ sections:
- type: param
key: EnforceTorqueControl
equals: false
+ - type: param
+ key: LateralJerkTorqueController
+ equals: false
- id: camera
title: Camera
description: Camera position and calibration
diff --git a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/steering.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/steering.yaml
index 697c5f4f21..a09796ab7a 100644
--- a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/steering.yaml
+++ b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/steering.yaml
@@ -127,6 +127,21 @@ sections:
key: EnforceTorqueControl
equals: true
items:
+ - key: LateralJerkTorqueController
+ widget: toggle
+ title: Lateral Jerk Torque Controller
+ description: Looks ahead at planned steering to reduce sudden corrections, so the wheel moves more smoothly through turns. Works with Self-Tune and custom tuning. Thanks to @twilsonco for the implementation.
+ visibility:
+ - type: not
+ condition:
+ type: capability
+ field: steer_control_type
+ equals: angle
+ enablement:
+ - $ref: '#/macros/offroad'
+ - type: param
+ key: NeuralNetworkLateralControl
+ equals: false
- key: LiveTorqueParamsToggle
widget: toggle
title: Self-Tune
diff --git a/openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py b/openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py
index 61cc0131cf..579d72b60b 100644
--- a/openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py
+++ b/openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py
@@ -257,20 +257,26 @@ class TestKnownPanels:
assert "mads_settings" in sub_ids
def test_mutual_exclusion_torque_nnlc(self, schema):
- """EnforceTorqueControl and NNLC must reference each other in enablement."""
- torque = nnlc = None
+ """EnforceTorqueControl, EnhancedLatAccel, and NNLC must reference each other in enablement."""
+ torque = nnlc = enhanced = None
for panel in schema["panels"]:
for item in _iter_panel_items(panel):
if item["key"] == "EnforceTorqueControl":
torque = item
elif item["key"] == "NeuralNetworkLateralControl":
nnlc = item
+ elif item["key"] == "LateralJerkTorqueController":
+ enhanced = item
assert torque is not None, "EnforceTorqueControl item missing"
assert nnlc is not None, "NeuralNetworkLateralControl item missing"
+ assert enhanced is not None, "LateralJerkTorqueController item missing"
torque_enable_keys = {r.get("key") for r in torque.get("enablement", []) if r.get("type") == "param"}
assert "NeuralNetworkLateralControl" in torque_enable_keys
nnlc_enable_keys = {r.get("key") for r in nnlc.get("enablement", []) if r.get("type") == "param"}
assert "EnforceTorqueControl" in nnlc_enable_keys
+ assert "LateralJerkTorqueController" in nnlc_enable_keys
+ enhanced_enable_keys = {r.get("key") for r in enhanced.get("enablement", []) if r.get("type") == "param"}
+ assert "NeuralNetworkLateralControl" in enhanced_enable_keys
class TestKnownVehicleSettings:
diff --git a/openpilot/sunnypilot/tools/lib/__init__.py b/openpilot/sunnypilot/tools/lib/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/openpilot/sunnypilot/tools/lib/sunnypilot_car_segments.py b/openpilot/sunnypilot/tools/lib/sunnypilot_car_segments.py
new file mode 100644
index 0000000000..a3a0e576cd
--- /dev/null
+++ b/openpilot/sunnypilot/tools/lib/sunnypilot_car_segments.py
@@ -0,0 +1,21 @@
+"""
+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 os
+
+SUNNYPILOT_CAR_SEGMENTS_REPO = os.environ.get("SUNNYPILOT_CAR_SEGMENTS_REPO",
+ "https://huggingface.co/datasets/sunnypilot/sunnypilotCarSegments")
+SUNNYPILOT_CAR_SEGMENTS_BRANCH = os.environ.get("SUNNYPILOT_CAR_SEGMENTS_BRANCH", "main")
+
+
+def get_url(route, segment, file="rlog.zst"):
+ return f"{SUNNYPILOT_CAR_SEGMENTS_REPO}/resolve/{SUNNYPILOT_CAR_SEGMENTS_BRANCH}/segments/{route.replace('|', '/')}/{segment}/{file}"
+
+
+def sunnypilot_car_segments_source(sr, seg_idxs, fns, /):
+ from openpilot.tools.lib.file_sources import eval_source
+ return eval_source({seg: [get_url(sr.route_name, seg, fn) for fn in fns] for seg in seg_idxs})
diff --git a/openpilot/sunnypilot/tools/upload_ci_routes.py b/openpilot/sunnypilot/tools/upload_ci_routes.py
new file mode 100755
index 0000000000..3f511a6a72
--- /dev/null
+++ b/openpilot/sunnypilot/tools/upload_ci_routes.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+"""
+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 argparse
+import os
+import tempfile
+
+import requests
+from huggingface_hub import HfApi
+from tqdm import tqdm
+
+from openpilot.tools.lib.route import Route
+
+REPO_ID = os.environ.get("SUNNYPILOT_CAR_SEGMENTS_REPO_ID", "sunnypilot/sunnypilotCarSegments")
+
+
+def upload_route(route_name: str, dry_run: bool = False) -> None:
+ route = Route(route_name)
+ log_paths = route.log_paths()
+ valid_segments = [(i, url) for i, url in enumerate(log_paths) if url is not None]
+
+ print(f"Route: {route_name}")
+ print(f"Segments: {len(valid_segments)}/{len(log_paths)}")
+
+ if not valid_segments:
+ print("No segments found.")
+ return
+
+ api = HfApi()
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ for seg_idx, url in tqdm(valid_segments, desc="Uploading"):
+ filename = url.split("?")[0].rsplit("/", 1)[-1]
+ local_path = os.path.join(tmpdir, f"{seg_idx}_{filename}")
+ resp = requests.get(url, stream=True)
+ resp.raise_for_status()
+ with open(local_path, "wb") as f:
+ for chunk in resp.iter_content(chunk_size=8192):
+ f.write(chunk)
+
+ repo_path = f"segments/{route_name.replace('|', '/')}/{seg_idx}/{filename}"
+
+ if dry_run:
+ size_mb = os.path.getsize(local_path) / 1024 / 1024
+ print(f" [{seg_idx}] {size_mb:.1f} MB -> {repo_path}")
+ else:
+ api.upload_file(
+ path_or_fileobj=local_path,
+ path_in_repo=repo_path,
+ repo_id=REPO_ID,
+ repo_type="dataset",
+ )
+
+ print("Done.")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Upload route rlogs to sunnypilot HuggingFace dataset")
+ parser.add_argument("route", help="Route ID (e.g. 5beb9b58bd12b691/0000010a--a51155e496)")
+ parser.add_argument("--dry-run", action="store_true", help="Download and show sizes without uploading")
+ args = parser.parse_args()
+
+ upload_route(args.route, dry_run=args.dry_run)
diff --git a/openpilot/system/ui/sunnypilot/widgets/screen_saver.py b/openpilot/system/ui/sunnypilot/widgets/screen_saver.py
new file mode 100644
index 0000000000..bf218306d8
--- /dev/null
+++ b/openpilot/system/ui/sunnypilot/widgets/screen_saver.py
@@ -0,0 +1,118 @@
+"""
+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 os
+import time
+
+import pyray as rl
+
+from openpilot.common.hardware import HARDWARE
+from openpilot.common.params import Params
+from openpilot.system.ui.lib.application import gui_app, FontWeight
+from openpilot.system.ui.lib.text_measure import measure_text_cached
+from openpilot.system.ui.widgets import Widget
+
+
+class ScreenSaverSP(Widget):
+ def __init__(self, params: Params | None = None):
+ super().__init__()
+ self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
+ self._params = params or Params()
+ self._is_mici = HARDWARE.get_device_type() == 'mici' or (HARDWARE.get_device_type() == "pc" and os.getenv("BIG") != "1")
+
+ self.x = 0.0
+ self.y = 100.0
+ self.vx = 120.0 if self._is_mici else 300.0
+ self.vy = 70.0 if self._is_mici else 200.0
+ self._hue = 150
+ self.color = rl.color_from_hsv(self._hue, 1, 1)
+
+ self.text = "sunnypilot"
+ self.font_size = 50 if self._is_mici else 200
+ self._start_time = None
+ self._dismiss = False
+ self._screensaver_timeout = 300
+ self._hit_last_frame = False
+
+ @property
+ def is_active(self) -> bool:
+ return self._start_time is not None and not self._dismiss
+
+ @property
+ def was_dismissed(self) -> bool:
+ return self._dismiss
+
+ def initialize(self):
+ self._screensaver_timeout = self._params.get("ScreenSaverTimeout", return_default=True)
+ if self._start_time is None:
+ self._start_time = time.monotonic()
+ self._dismiss = False
+
+ def hide_event(self):
+ super().hide_event()
+ self._dismiss = False
+ self._start_time = None
+
+ def _handle_mouse_release(self, mouse_pos):
+ self._dismiss = True
+ self._start_time = None
+ gui_app.pop_widget()
+ return super()._handle_mouse_release(mouse_pos)
+
+ def _update_state(self):
+ super()._update_state()
+
+ self.font = gui_app.font(FontWeight.AUDIOWIDE)
+ text_size = measure_text_cached(self.font, self.text, self.font_size, 0)
+ self.logo_width = text_size.x
+ self.logo_height = text_size.y
+
+ if self._start_time and time.monotonic() - self._start_time > self._screensaver_timeout:
+ self._dismiss = True
+ self._start_time = None
+
+ dt = rl.get_frame_time()
+
+ self.x += self.vx * dt
+ self.y += self.vy * dt
+
+ hit_x = hit_y = False
+ if self.x + self.logo_width > self.rect.width:
+ self.vx *= -1
+ self.x = self.rect.width - self.logo_width
+ hit_x = True
+ elif self.x < 0:
+ self.vx *= -1
+ self.x = 0
+ hit_x = True
+
+ if self.y + self.logo_height > self.rect.height:
+ self.vy *= -1
+ self.y = self.rect.height - self.logo_height
+ hit_y = True
+ elif self.y < 0:
+ self.vy *= -1
+ self.y = 0
+ hit_y = True
+
+ hit = hit_x or hit_y
+ if hit and not self._hit_last_frame:
+ while self._hue_dist((new_hue := rl.get_random_value(0, 360)), self._hue) < 120:
+ pass
+ self._hue = new_hue
+ self.color = rl.color_from_hsv(self._hue, 1, 1)
+ self._hit_last_frame = hit
+
+ @staticmethod
+ def _hue_dist(a, b):
+ d = abs(a - b)
+ return min(d, 360 - d)
+
+ def _render(self, rect: rl.Rectangle):
+ self.set_rect(rect)
+ rl.clear_background(rl.BLACK)
+ rl.draw_text_ex(self.font, self.text, rl.Vector2(int(self.x), int(self.y)), self.font_size, 0, self.color)
+ return -1
diff --git a/openpilot/tools/lib/logreader.py b/openpilot/tools/lib/logreader.py
index 805e411b53..fbfb28dbe0 100755
--- a/openpilot/tools/lib/logreader.py
+++ b/openpilot/tools/lib/logreader.py
@@ -22,6 +22,8 @@ from openpilot.tools.lib.file_sources import comma_api_source, internal_source,
from openpilot.tools.lib.route import SegmentRange, FileName
from openpilot.tools.lib.log_time_series import msgs_to_time_series
+from openpilot.sunnypilot.tools.lib.sunnypilot_car_segments import sunnypilot_car_segments_source
+
LogMessage = type[capnp._DynamicStructReader]
LogIterable = Iterable[LogMessage]
RawLogIterable = Iterable[bytes]
@@ -246,7 +248,7 @@ class LogReader:
def __init__(self, identifier: str | list[str], default_mode: ReadMode = ReadMode.RLOG,
sources: list[Source] | None = None, sort_by_time=False, only_union_types=False):
if sources is None:
- sources = [internal_source, comma_api_source, openpilotci_source, comma_car_segments_source]
+ sources = [internal_source, comma_api_source, openpilotci_source, comma_car_segments_source, sunnypilot_car_segments_source]
self.default_mode = default_mode
self.sources = sources
diff --git a/pyproject.toml b/pyproject.toml
index d400ff5678..051eb5ffc6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -75,6 +75,7 @@ testing = [
]
dev = [
+ "huggingface_hub",
"matplotlib",
]
diff --git a/uv.lock b/uv.lock
index a0a951d33d..b88ba23d7c 100644
--- a/uv.lock
+++ b/uv.lock
@@ -5,6 +5,19 @@ requires-python = ">=3.12.3, <3.13"
[manifest]
overrides = [{ name = "opendbc", editable = "opendbc_repo" }]
+[[package]]
+name = "anyio"
+version = "4.14.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
+]
+
[[package]]
name = "attrs"
version = "26.1.0"
@@ -395,6 +408,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
]
+[[package]]
+name = "filelock"
+version = "3.32.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172, upload-time = "2026-07-29T22:46:04.895Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" },
+]
+
[[package]]
name = "fonttools"
version = "4.63.0"
@@ -412,6 +434,88 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" },
]
+[[package]]
+name = "fsspec"
+version = "2026.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/00/78/f34251dadb8f3921264a1d9b8946f5e542014ee2614b285261b4e40e6775/fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88", size = 317040, upload-time = "2026-07-28T16:34:51.052Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "hf-xet"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" },
+ { url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538, upload-time = "2026-08-03T22:33:05.287Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520, upload-time = "2026-08-03T22:33:06.81Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937, upload-time = "2026-08-03T22:33:08.616Z" },
+ { url = "https://files.pythonhosted.org/packages/98/b7/8c59a66d15205024662f1d66968136f13893f96df1ddc5087e2e281fc95f/hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b", size = 4033128, upload-time = "2026-08-03T22:33:10.171Z" },
+ { url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359, upload-time = "2026-08-03T22:33:11.725Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "huggingface-hub"
+version = "1.27.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
+ { name = "httpx" },
+ { name = "packaging" },
+ { name = "pyyaml" },
+ { name = "tqdm" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3e/9b/ddf3d02a8681f1b9ce52fda03d755dad6b74c4f8172304c4c8d2975450f9/huggingface_hub-1.27.0.tar.gz", hash = "sha256:c1fed40ea82a6b41b477f5243546549b792ae0a93abcea608cff66089bf8f8df", size = 942668, upload-time = "2026-08-07T12:48:05.161Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/d8/95b735e183957c1f26d94c52977f09d466d55119cbbc1558ea4975e4c216/huggingface_hub-1.27.0-py3-none-any.whl", hash = "sha256:7df6827c2f956c60fbaa64646e979e566db76f619dd0a9729dfb8c5a3eb4f68d", size = 784926, upload-time = "2026-08-07T12:48:02.905Z" },
+]
+
[[package]]
name = "hypothesis"
version = "6.47.5"
@@ -730,6 +834,7 @@ dependencies = [
[package.optional-dependencies]
dev = [
+ { name = "huggingface-hub" },
{ name = "matplotlib" },
]
docs = [
@@ -788,6 +893,7 @@ requires-dist = [
{ name = "comma-deps-zstd" },
{ name = "coverage", marker = "extra == 'testing'" },
{ name = "cython" },
+ { name = "huggingface-hub", marker = "extra == 'dev'" },
{ name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" },
{ name = "inputs" },
{ name = "jeepney" },
@@ -1302,7 +1408,7 @@ provides-extras = ["dev"]
[[package]]
name = "tinygrad"
-version = "0.12.0"
+version = "0.13.0"
source = { editable = "tinygrad_repo" }
[package.metadata]
@@ -1413,6 +1519,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/fc/6a183e71edde90d0c35c2303f23f7a45b6891d1a2c45daf7b8f869831e19/ty-0.0.56-py3-none-win_arm64.whl", hash = "sha256:57538f273d444a5f1293fa7860e967178afe3917611fc5eff16b64e1204fe0d6", size = 11538780, upload-time = "2026-07-01T16:44:53.8Z" },
]
+[[package]]
+name = "typing-extensions"
+version = "4.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
+]
+
[[package]]
name = "urllib3"
version = "2.7.0"