diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index b5f941fc7..621aa123c 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 000000000..a32eb96ba
--- /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 c52707f4f..22c35b9a1 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 cbeb5f6d3..0c011ee22 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 a26fd1f55..9d1fc7af6 160000
--- a/opendbc_repo
+++ b/opendbc_repo
@@ -1 +1 @@
-Subproject commit a26fd1f55a88f742ae1e51ae17528abeb7efd775
+Subproject commit 9d1fc7af686b4d5e71ac9978b951b2b2564358c3
diff --git a/openpilot/cereal/custom.capnp b/openpilot/cereal/custom.capnp
index 3a7abe474..5e6b8c99c 100644
--- a/openpilot/cereal/custom.capnp
+++ b/openpilot/cereal/custom.capnp
@@ -351,6 +351,7 @@ struct OnroadEventSP @0xda96579883444c35 {
speedLimitChanged @21;
speedLimitPending @22;
e2eChime @23;
+ laneChangeRoadEdge @24;
}
}
@@ -457,6 +458,8 @@ struct LiveMapDataSP @0xf416ec09499d9d19 {
struct ModelDataV2SP @0xa1680744031fdb2d {
laneTurnDirection @0 :TurnDirection;
+ leftLaneChangeEdgeBlock @1 :Bool;
+ rightLaneChangeEdgeBlock @2 :Bool;
enum TurnDirection {
none @0;
diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h
index 58cadfdec..bc5a99af5 100644
--- a/openpilot/common/params_keys.h
+++ b/openpilot/common/params_keys.h
@@ -201,7 +201,10 @@ inline static std::unordered_map keys = {
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
+ {"RoadEdgeLaneChangeEnabled", {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"}},
@@ -295,6 +298,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 7abbc7468..c16978b69 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 50ae58403..49adce9c8 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/lib/desire_helper.py b/openpilot/selfdrive/controls/lib/desire_helper.py
index df4e5c56a..334b360a6 100644
--- a/openpilot/selfdrive/controls/lib/desire_helper.py
+++ b/openpilot/selfdrive/controls/lib/desire_helper.py
@@ -33,7 +33,7 @@ class DesireHelper:
def get_lane_change_direction(CS):
return LaneChangeDirection.left if CS.leftBlinker else LaneChangeDirection.right
- def update(self, carstate, lateral_active, lane_change_prob):
+ def update(self, carstate, lateral_active, lane_change_prob, left_edge_detected=False, right_edge_detected=False):
self.alc.update_params()
self.lane_turn_controller.update_params()
v_ego = carstate.vEgo
@@ -64,8 +64,8 @@ class DesireHelper:
((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or
(carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right))
- blindspot_detected = ((carstate.leftBlindspot and self.lane_change_direction == LaneChangeDirection.left) or
- (carstate.rightBlindspot and self.lane_change_direction == LaneChangeDirection.right))
+ blindspot_detected = (((carstate.leftBlindspot or left_edge_detected) and self.lane_change_direction == LaneChangeDirection.left) or
+ ((carstate.rightBlindspot or right_edge_detected) and self.lane_change_direction == LaneChangeDirection.right))
self.alc.update_lane_change(blindspot_detected, carstate.brakePressed)
diff --git a/openpilot/selfdrive/controls/plannerd.py b/openpilot/selfdrive/controls/plannerd.py
index 45d724cc2..cfcce56eb 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/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py
index 1ca1a71f3..5dc871211 100755
--- a/openpilot/selfdrive/modeld/modeld.py
+++ b/openpilot/selfdrive/modeld/modeld.py
@@ -29,6 +29,7 @@ from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
+from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
PROCESS_NAME = "openpilot.selfdrive.modeld.modeld"
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
@@ -213,6 +214,7 @@ def main(demo=False):
prev_action = log.ModelDataV2.Action()
DH = DesireHelper()
+ RELC = RoadEdgeLaneChangeController()
while True:
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
@@ -313,7 +315,8 @@ def main(demo=False):
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
lane_change_prob = l_lane_change_prob + r_lane_change_prob
- DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
+ left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
+ DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
diff --git a/openpilot/selfdrive/selfdrived/alerts_offroad.json b/openpilot/selfdrive/selfdrived/alerts_offroad.json
index c90497e8c..8a77bd29a 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 7734db581..6b0b9d9a5 100644
--- a/openpilot/selfdrive/selfdrived/selfdrived.py
+++ b/openpilot/selfdrive/selfdrived/selfdrived.py
@@ -101,7 +101,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 not Params().get_bool("EnableCurvatureD"):
ignore += ['liveCurvatureParameters']
if SIMULATION:
@@ -345,9 +345,16 @@ class SelfdriveD(CruiseHelper):
# Handle lane change
if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange:
direction = self.sm['modelV2'].meta.laneChangeDirection
+ mdv2sp = self.sm['modelDataV2SP']
+
if (CS.leftBlindspot and direction == LaneChangeDirection.left) or \
(CS.rightBlindspot and direction == LaneChangeDirection.right):
self.events.add(EventName.laneChangeBlocked)
+
+ elif (mdv2sp.leftLaneChangeEdgeBlock and direction == LaneChangeDirection.left) or \
+ (mdv2sp.rightLaneChangeEdgeBlock and direction == LaneChangeDirection.right):
+ self.events_sp.add(custom.OnroadEventSP.EventName.laneChangeRoadEdge)
+
else:
if direction == LaneChangeDirection.left:
self.events.add(EventName.preLaneChangeLeft)
diff --git a/openpilot/selfdrive/test/process_replay/process_replay.py b/openpilot/selfdrive/test/process_replay/process_replay.py
index d18aceb25..d536b76ef 100755
--- a/openpilot/selfdrive/test/process_replay/process_replay.py
+++ b/openpilot/selfdrive/test/process_replay/process_replay.py
@@ -503,7 +503,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"),
@@ -514,7 +514,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 d9d827add..1627ab065 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/sunnypilot/layouts/settings/display.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py
index 8ba566366..897d34085 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 796196962..69959a959 100644
--- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering.py
+++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering.py
@@ -141,7 +141,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/lane_change_settings.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py
index fbb9ce7cf..82419a556 100644
--- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py
+++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py
@@ -51,11 +51,18 @@ class LaneChangeSettingsLayout(Widget):
description=lambda: tr("Toggle to enable a delay timer for seamless lane changes when blind spot monitoring " +
"(BSM) detects a obstructing vehicle, ensuring safe maneuvering."),
)
+ self._road_edge_block = toggle_item_sp(
+ param="RoadEdgeLaneChangeEnabled",
+ title=lambda: tr("Block Lane Change: Road Edge Detection"),
+ description=lambda: tr("Blocks the lane change if the model sees a road edge on your signaled side."),
+ )
items = [
self._lane_change_timer,
LineSeparatorSP(40),
self._bsm_delay,
+ LineSeparatorSP(40),
+ self._road_edge_block,
]
return items
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 f3c4419e4..6dae8308c 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/ui_state.py b/openpilot/selfdrive/ui/sunnypilot/ui_state.py
index 1777cac06..ca00fde98 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,11 +189,16 @@ 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 or
CP.steerControlType == car.CarParams.SteerControlType.curvature):
self.params.remove("EnforceTorqueControl")
self.params.remove("NeuralNetworkLateralControl")
+ self.params.remove("LateralJerkTorqueController")
# Alpha longitudinal: clear if not available
if not CP.alphaLongitudinalAvailable:
@@ -201,6 +211,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
@@ -225,10 +236,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/ui_state.py b/openpilot/selfdrive/ui/ui_state.py
index 5221af953..3c0b0e2cb 100644
--- a/openpilot/selfdrive/ui/ui_state.py
+++ b/openpilot/selfdrive/ui/ui_state.py
@@ -371,6 +371,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/modeld_v2/modeld.py b/openpilot/sunnypilot/modeld_v2/modeld.py
index b53ab18c7..9f3a75a66 100755
--- a/openpilot/sunnypilot/modeld_v2/modeld.py
+++ b/openpilot/sunnypilot/modeld_v2/modeld.py
@@ -49,6 +49,7 @@ from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, mak
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
from openpilot.sunnypilot.models.helpers import get_active_bundle
+from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
PROCESS_NAME = "openpilot.selfdrive.modeld.modeld_tinygrad"
@@ -367,6 +368,7 @@ def main(demo=False):
DH = DesireHelper()
meta_constants = load_meta_constants()
+ RELC = RoadEdgeLaneChangeController()
while True:
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
@@ -479,7 +481,8 @@ def main(demo=False):
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
lane_change_prob = l_lane_change_prob + r_lane_change_prob
- DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
+ left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
+ DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
diff --git a/openpilot/sunnypilot/selfdrive/car/interfaces.py b/openpilot/sunnypilot/selfdrive/car/interfaces.py
index ed5b71d4b..ecabc53b5 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/dec/dec.py b/openpilot/sunnypilot/selfdrive/controls/lib/dec/dec.py
index 1ba5ab061..fb854edae 100644
--- a/openpilot/sunnypilot/selfdrive/controls/lib/dec/dec.py
+++ b/openpilot/sunnypilot/selfdrive/controls/lib/dec/dec.py
@@ -1,5 +1,5 @@
"""
-Copyright (c) 2021-, rav4kumar, Haibin Wen, sunnypilot, and a number of other contributors.
+Copyright (c) 2021-, rav4kumar, 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.
diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py
index 39525b3b8..50add19cd 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 df773889a..31ac615db 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 000000000..8d780ed4c
--- /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 6f6515063..4650d154e 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 e66072f86..9684c8668 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/relc.py b/openpilot/sunnypilot/selfdrive/controls/lib/relc.py
new file mode 100644
index 000000000..031e751b4
--- /dev/null
+++ b/openpilot/sunnypilot/selfdrive/controls/lib/relc.py
@@ -0,0 +1,98 @@
+"""
+Copyright (c) 2021-, rav4kumar, 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.constants import CV
+from openpilot.common.realtime import DT_MDL
+from openpilot.common.params import Params
+
+NEARSIDE_PROB = 0.2
+EDGE_PROB = 0.35
+EDGE_REACTION_TIME = 1.0
+EDGE_CLEAR_TIME = 0.3
+MIN_SPEED = 20 * CV.MPH_TO_MS
+VEHICLE_EDGE_MARGIN = 1.08
+EDGE_CLEARANCE = 3.7
+
+
+class RoadEdgeLaneChangeController:
+ def __init__(self):
+ self.params = Params()
+ self.enabled = self.params.get_bool("RoadEdgeLaneChangeEnabled")
+ self.param_read_counter = 0
+ self.left_edge_detected = False
+ self.right_edge_detected = False
+ self.left_edge_timer = 0.0
+ self.right_edge_timer = 0.0
+ self.left_clear_timer = 0.0
+ self.right_clear_timer = 0.0
+
+ def read_params(self) -> None:
+ self.enabled = self.params.get_bool("RoadEdgeLaneChangeEnabled")
+
+ def update_params(self) -> None:
+ if self.param_read_counter % 50 == 0:
+ self.read_params()
+ self.param_read_counter += 1
+
+ def reset(self) -> None:
+ self.left_edge_detected = False
+ self.right_edge_detected = False
+ self.left_edge_timer = 0.0
+ self.right_edge_timer = 0.0
+ self.left_clear_timer = 0.0
+ self.right_clear_timer = 0.0
+
+ def update(self, road_edge_stds, lane_line_probs, v_ego: float, road_edges=None) -> None:
+ self.update_params()
+
+ if not self.enabled or v_ego < MIN_SPEED:
+ self.reset()
+ return
+
+ left_edge_prob = np.clip(1.0 - road_edge_stds[0], 0.0, 1.0)
+ right_edge_prob = np.clip(1.0 - road_edge_stds[1], 0.0, 1.0)
+ left_lane_prob = lane_line_probs[0]
+ right_lane_prob = lane_line_probs[3]
+
+ if road_edges is not None and len(road_edges) == 2 and len(road_edges[0].y) > 0 and len(road_edges[1].y) > 0:
+ left_clearance = abs(road_edges[0].y[0]) - VEHICLE_EDGE_MARGIN
+ right_clearance = abs(road_edges[1].y[0]) - VEHICLE_EDGE_MARGIN
+ else:
+ left_clearance = 0.0
+ right_clearance = 0.0
+
+ left_cond = left_edge_prob > EDGE_PROB and left_lane_prob < NEARSIDE_PROB and left_clearance < EDGE_CLEARANCE
+ right_cond = right_edge_prob > EDGE_PROB and right_lane_prob < NEARSIDE_PROB and right_clearance < EDGE_CLEARANCE
+
+ if left_cond:
+ self.left_edge_timer = min(self.left_edge_timer + DT_MDL, EDGE_REACTION_TIME + EDGE_CLEAR_TIME)
+ self.left_clear_timer = 0.0
+ if self.left_edge_timer > EDGE_REACTION_TIME:
+ self.left_edge_detected = True
+ else:
+ self.left_clear_timer += DT_MDL
+ if self.left_clear_timer > EDGE_CLEAR_TIME:
+ self.left_edge_timer = 0.0
+ self.left_edge_detected = False
+
+ if right_cond:
+ self.right_edge_timer = min(self.right_edge_timer + DT_MDL, EDGE_REACTION_TIME + EDGE_CLEAR_TIME)
+ self.right_clear_timer = 0.0
+ if self.right_edge_timer > EDGE_REACTION_TIME:
+ self.right_edge_detected = True
+ else:
+ self.right_clear_timer += DT_MDL
+ if self.right_clear_timer > EDGE_CLEAR_TIME:
+ self.right_edge_timer = 0.0
+ self.right_edge_detected = False
+
+ def update_and_fill(self, modelv2, mdv2sp, v_ego):
+ self.update(modelv2.roadEdgeStds, modelv2.laneLineProbs, v_ego, modelv2.roadEdges)
+ mdv2sp.leftLaneChangeEdgeBlock = self.left_edge_detected
+ mdv2sp.rightLaneChangeEdgeBlock = self.right_edge_detected
+ return self.left_edge_detected, self.right_edge_detected
diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py
index c3e96fd77..434b12110 100644
--- a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py
+++ b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py
@@ -2,10 +2,11 @@ import pytest
from openpilot.cereal import log, custom
from openpilot.common.params import Params
-from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
+from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper, LaneChangeState, LaneChangeDirection
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, LANE_CHANGE_SPEED_MIN
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeMode
+
TurnDirection = custom.ModelDataV2SP.TurnDirection
@@ -109,5 +110,17 @@ def test_desire_helper_integration(carstate, lateral_active, lane_change_prob, e
dh = DesireHelper()
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
for _ in range(10):
- dh.update(carstate, lateral_active, lane_change_prob)
- assert dh.desire == expected_desire # The first four tests were unit tests to test the controller, where this tests the integration in desire helpers
+ dh.update(carstate, lateral_active, lane_change_prob,
+ left_edge_detected=False, right_edge_detected=False)
+ assert dh.desire == expected_desire
+
+
+def test_edge_blocks_lane_change(set_lane_turn_params):
+ dh = DesireHelper()
+ dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
+ carstate = DummyCarState(vEgo=15, leftBlinker=True, steeringPressed=True, steeringTorque=1)
+ for _ in range(10):
+ dh.update(carstate, True, 1.0, left_edge_detected=True, right_edge_detected=False)
+ assert dh.lane_change_state == LaneChangeState.preLaneChange
+ assert dh.lane_change_direction == LaneChangeDirection.left
+ assert dh.desire == log.Desire.none
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 000000000..2b4797751
--- /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/selfdrive/controls/lib/tests/test_relc.py b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_relc.py
new file mode 100644
index 000000000..71b153c8b
--- /dev/null
+++ b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_relc.py
@@ -0,0 +1,169 @@
+"""
+Copyright (c) 2021-, rav4kumar, 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 pytest
+
+from openpilot.common.realtime import DT_MDL
+from openpilot.sunnypilot.selfdrive.controls.lib.relc import (
+ RoadEdgeLaneChangeController, EDGE_REACTION_TIME, EDGE_CLEAR_TIME, MIN_SPEED,
+ VEHICLE_EDGE_MARGIN, EDGE_CLEARANCE,
+)
+
+V_HIGH = MIN_SPEED + 2.0
+V_LOW = MIN_SPEED - 1.0
+
+
+class MockEdge:
+ def __init__(self, y_val):
+ self.y = [y_val] * 33
+
+
+def edges(left_y, right_y):
+ return [MockEdge(left_y), MockEdge(right_y)]
+
+
+CLOSE_EDGES = edges(-2.0, 1.5)
+FAR_EDGES = edges(-10.0, 10.0)
+
+
+@pytest.fixture
+def relc(mocker):
+ mocker.patch("openpilot.sunnypilot.selfdrive.controls.lib.relc.Params")
+ controller = RoadEdgeLaneChangeController()
+ controller.enabled = True
+ return controller
+
+
+def drive(controller, road_edge_stds, lane_line_probs, seconds, v_ego=V_HIGH, road_edges=CLOSE_EDGES):
+ for _ in range(int(seconds / DT_MDL) + 1):
+ controller.update(road_edge_stds, lane_line_probs, v_ego, road_edges)
+
+
+@pytest.mark.parametrize("road_edge_stds,lane_line_probs,attr", [
+ ([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], "left_edge_detected"),
+ ([0.9, 0.0], [0.8, 0.8, 0.8, 0.0], "right_edge_detected"),
+])
+def test_edge_detection(relc, road_edge_stds, lane_line_probs, attr):
+ drive(relc, road_edge_stds, lane_line_probs, EDGE_REACTION_TIME + 0.1)
+ assert getattr(relc, attr)
+
+
+def test_edge_detection_requires_time(relc):
+ drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME - 0.05)
+ assert not relc.left_edge_detected
+
+
+def test_both_edges_detected(relc):
+ drive(relc, [0.0, 0.0], [0.0, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1)
+ assert relc.left_edge_detected
+ assert relc.right_edge_detected
+
+
+def test_noise_doesnt_clear(relc):
+ edge = ([0.0, 0.9], [0.0, 0.8, 0.8, 0.8])
+ clear = ([0.9, 0.9], [0.8, 0.8, 0.8, 0.8])
+
+ drive(relc, *edge, EDGE_REACTION_TIME + 0.1)
+ assert relc.left_edge_detected
+
+ relc.update(*clear, V_HIGH, CLOSE_EDGES)
+ relc.update(*edge, V_HIGH, CLOSE_EDGES)
+ assert relc.left_edge_detected
+
+
+def test_clears_after_window(relc):
+ edge = ([0.0, 0.9], [0.0, 0.8, 0.8, 0.8])
+ clear = ([0.9, 0.9], [0.8, 0.8, 0.8, 0.8])
+
+ drive(relc, *edge, EDGE_REACTION_TIME + 0.1)
+ assert relc.left_edge_detected
+
+ drive(relc, *clear, EDGE_CLEAR_TIME + 0.05)
+ assert not relc.left_edge_detected
+ assert relc.left_edge_timer == 0.0
+
+
+def test_low_speed_skips(relc):
+ drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1, v_ego=V_LOW)
+ assert not relc.left_edge_detected
+ assert relc.left_edge_timer == 0.0
+
+
+def test_speed_drop_resets(relc):
+ drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1)
+ assert relc.left_edge_detected
+
+ relc.update([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], V_LOW, CLOSE_EDGES)
+ assert not relc.left_edge_detected
+
+
+def test_param_off_resets(relc):
+ drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1)
+ assert relc.left_edge_detected
+
+ relc.params.get_bool.return_value = False
+ relc.read_params()
+ relc.update([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], V_HIGH, CLOSE_EDGES)
+ assert not relc.left_edge_detected
+ assert not relc.right_edge_detected
+
+
+def test_lane_line_prevents_detection(relc):
+ drive(relc, [0.0, 0.9], [0.8, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1)
+ assert not relc.left_edge_detected
+
+
+def test_one_side_blocks_other_allows(relc):
+ drive(relc, [0.9, 0.0], [0.8, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1)
+ assert relc.right_edge_detected
+ assert not relc.left_edge_detected
+
+
+def test_disabled_no_detection(relc):
+ relc.enabled = False
+ relc.params.get_bool.return_value = False
+ drive(relc, [0.0, 0.0], [0.0, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1)
+ assert not relc.left_edge_detected
+ assert not relc.right_edge_detected
+
+
+def test_far_edge_no_block(relc):
+ drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1, road_edges=FAR_EDGES)
+ assert not relc.left_edge_detected
+
+
+def test_close_edge_blocks(relc):
+ drive(relc, [0.9, 0.0], [0.05, 0.8, 0.8, 0.05], EDGE_REACTION_TIME + 0.1,
+ road_edges=edges(-8.0, 1.5))
+ assert relc.right_edge_detected
+ assert not relc.left_edge_detected
+
+
+def test_wide_road_no_lines_no_block(relc):
+ drive(relc, [0.0, 0.0], [0.05, 0.4, 0.4, 0.05], EDGE_REACTION_TIME + 0.1,
+ road_edges=edges(-8.0, 8.0))
+ assert not relc.left_edge_detected
+ assert not relc.right_edge_detected
+
+
+def test_narrow_road_both_block(relc):
+ drive(relc, [0.0, 0.0], [0.02, 0.4, 0.4, 0.02], EDGE_REACTION_TIME + 0.1,
+ road_edges=edges(-2.5, 2.5))
+ assert relc.left_edge_detected
+ assert relc.right_edge_detected
+
+
+def test_clearance_boundary(relc):
+ boundary = VEHICLE_EDGE_MARGIN + EDGE_CLEARANCE # 4.78m
+ drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1,
+ road_edges=edges(-(boundary - 0.1), 10.0))
+ assert relc.left_edge_detected
+
+ relc.reset()
+
+ drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1,
+ road_edges=edges(-(boundary + 0.1), 10.0))
+ assert not relc.left_edge_detected
diff --git a/openpilot/sunnypilot/selfdrive/selfdrived/events.py b/openpilot/sunnypilot/selfdrive/selfdrived/events.py
index 2001d0dbe..3c010cc77 100644
--- a/openpilot/sunnypilot/selfdrive/selfdrived/events.py
+++ b/openpilot/sunnypilot/selfdrive/selfdrived/events.py
@@ -244,4 +244,12 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
AlertStatus.normal, AlertSize.none,
Priority.MID, VisualAlert.none, AudibleAlert.prompt, 3.),
},
+
+ EventNameSP.laneChangeRoadEdge: {
+ ET.WARNING: Alert(
+ "Lane Change Unavailable: Road Edge",
+ "",
+ AlertStatus.userPrompt, AlertSize.small,
+ Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.1),
+ },
}
diff --git a/openpilot/sunnypilot/sunnylink/settings_ui.json b/openpilot/sunnypilot/sunnylink/settings_ui.json
index cd5f0b118..63bf342fc 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",
@@ -519,6 +545,12 @@
}
]
},
+ {
+ "key": "RoadEdgeLaneChangeEnabled",
+ "widget": "toggle",
+ "title": "Block Lane Change: Road Edge Detection",
+ "description": "Blocks lane change when the model sees a road edge on the side you signal."
+ },
{
"key": "AutoLaneChangeBsmDelay",
"widget": "toggle",
@@ -1260,6 +1292,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 +2130,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 39a8cbaf8..3e3b16c37 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 4ae1fca88..bcb8b895b 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 697c5f4f2..73d46681a 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
@@ -242,6 +257,10 @@ sections:
label: 2 seconds
- value: 5
label: 3 seconds
+ - key: RoadEdgeLaneChangeEnabled
+ widget: toggle
+ title: 'Block Lane Change: Road Edge Detection'
+ description: Blocks lane change when the model sees a road edge on the side you signal.
- key: AutoLaneChangeBsmDelay
widget: toggle
title: 'Auto Lane Change: Delay with Blind Spot'
diff --git a/openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py b/openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py
index 61cc0131c..579d72b60 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 000000000..e69de29bb
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 000000000..a3a0e576c
--- /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 000000000..3f511a6a7
--- /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/manager/process_config.py b/openpilot/system/manager/process_config.py
index 816b0d171..92441e518 100644
--- a/openpilot/system/manager/process_config.py
+++ b/openpilot/system/manager/process_config.py
@@ -91,7 +91,7 @@ def use_sunnylink_uploader_shim(started, params, CP: car.CarParams) -> bool:
return use_sunnylink_uploader(params)
def is_tinygrad_model(started, params, CP: car.CarParams) -> bool:
- """Check if the active model runner is SNPE."""
+ """Check if the active model runner is tinygrad."""
return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.tinygrad)
def is_stock_model(started, params, CP: car.CarParams) -> bool:
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 000000000..bf218306d
--- /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 805e411b5..fbfb28dbe 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 d400ff567..051eb5ffc 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 a0a951d33..b88ba23d7 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"