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/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/opendbc_repo b/opendbc_repo
index 86d3b68483..e7a6defadd 160000
--- a/opendbc_repo
+++ b/opendbc_repo
@@ -1 +1 @@
-Subproject commit 86d3b68483698c13e4c28832ddf062c22a293141
+Subproject commit e7a6defadd9be1cfb317aac9c213a2d85013ef68
diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h
index 279d36abfd..f0921b6723 100644
--- a/openpilot/common/params_keys.h
+++ b/openpilot/common/params_keys.h
@@ -273,6 +273,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/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/ui_state.py b/openpilot/selfdrive/ui/sunnypilot/ui_state.py
index c2729bbd91..4828f37103 100644
--- a/openpilot/selfdrive/ui/sunnypilot/ui_state.py
+++ b/openpilot/selfdrive/ui/sunnypilot/ui_state.py
@@ -184,10 +184,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 +205,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
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..1e6422ac84 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",
@@ -2037,6 +2063,11 @@
"type": "param",
"key": "EnforceTorqueControl",
"equals": false
+ },
+ {
+ "type": "param",
+ "key": "LateralJerkTorqueController",
+ "equals": false
}
]
}
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/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"