Compare commits

...

24 Commits

Author SHA1 Message Date
firestar5683 4486dc2f14 steer errors 2026-08-28 23:12:10 -05:00
firestar5683 f546a3e07a PIKA 2026-08-28 22:19:02 -05:00
Prabhaav Pillai 3acff1e3d7 Enhance vehicle annotation tool with side labels and mirrored preview adjustments consistient with pip-cam 2026-08-28 23:03:02 -04:00
firestar5683 f198977141 build 2026-08-28 21:52:37 -05:00
firestar5683 dc62d0e27a Sokka John 2026-08-28 21:50:54 -05:00
firestar5683 6b1ad03acf butt 2026-08-28 15:22:24 -05:00
firestar5683 ebb096a956 estate sale 2026-08-28 13:45:11 -05:00
firestar5683 11b2987c50 DRV ECU 2026-08-28 10:41:17 -05:00
firestarsdog ec0ab9d300 Big UI GPU Widget 2026-08-28 01:02:49 -04:00
firestar5683 f501a4de37 nighty night 2026-08-28 00:01:11 -05:00
firestar5683 63f8828c01 2018-2022 Accord radar 2026-08-27 23:20:58 -05:00
firestar5683 aa1d691304 The Last Dragon 2026-08-27 21:33:02 -05:00
firestar5683 ac95896551 moar 2026-08-27 21:33:02 -05:00
firestar5683 443c35228d iPod stuck on replay
f
2026-08-27 21:32:50 -05:00
firestar5683 b2602bc4e7 v16 2026-08-27 15:11:41 -05:00
firestar5683 3e176f3884 bop 2026-08-27 14:49:17 -05:00
firestar5683 f79ab05fa5 wat 2026-08-27 14:43:40 -05:00
firestar5683 5b47ff584a build 2026-08-27 14:26:00 -05:00
firestar5683 af2e153e52 GUM 2026-08-27 14:24:43 -05:00
firestar5683 227b5cf82a gpu 2026-08-27 12:25:23 -05:00
firestar5683 adb5058cb8 build 2026-08-27 11:59:02 -05:00
firestar5683 3c655e79a9 the morrow 2026-08-27 11:57:40 -05:00
Prabhaav Pillai 032f085500 ui: add PiP side-camera orientation toggle 2026-08-27 11:36:58 -05:00
whoisdomi 5882bd23ba Force Stop: Fast Approach
Force Stop is now better at faster approach speeds. It has more authority on braking in general.

Force Stop Distance Offset: still the lever that moves the ending point sooner or later.

Radar bug fixed
2026-08-27 08:22:54 -05:00
226 changed files with 8399 additions and 1754 deletions
+5
View File
@@ -221,6 +221,11 @@ struct StarPilotPlan @0xf98d843bfd7004a3 {
trackingLead @36 :Bool;
stopSignConfirmed @37 :Bool;
pulseGlideCoasting @38 :Bool; # developer-only P&G phase for on-road status UI
# Curve Speed Controller diagnostics, for tuning and rollout validation
cscOverridden @39 :Bool; # driver cancelled this curve with RES+
cscLearnedLatAccel @40 :Float32; # learned comfort at the current curvature, before margin
cscBindingDistance @41 :Float32; # distance to the horizon point setting the target, m
approachStopLength @42 :Float32; # pre-commit distance to a detected stop, m; 0 when off
}
struct StarPilotRadarState @0xb86e6369214c01c8 {
Binary file not shown.
Binary file not shown.
+2
View File
@@ -522,6 +522,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"PIPPreviewMask", {PERSISTENT, JSON, "{\"width\":1928,\"height\":1208,\"center_left\":[315,548],\"center_right\":[1571,539],\"crop_size\":580}", "{\"width\":1928,\"height\":1208,\"center_left\":[315,548],\"center_right\":[1571,539],\"crop_size\":580}", 2}},
{"PIPPreviewShowOnBlinker", {PERSISTENT, BOOL, "0", "0", 1}},
{"PIPPreviewShowOnBSM", {PERSISTENT, BOOL, "0", "0", 1}},
{"PIPPreviewInvert", {PERSISTENT, BOOL, "0", "0", 1}},
{"GalaxyPaired", {PERSISTENT, BOOL, "0", "0", 0}},
{"GalaxyUploadPending", {PERSISTENT, BOOL, "0", "0", 0}},
{"PreferredSchedule", {PERSISTENT, INT, "2", "0", 0}},
@@ -664,6 +665,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"SwitchbackModeEnabled", {CLEAR_ON_OFFROAD_TRANSITION, BOOL, "0", "0"}},
{"SubaruSNG", {PERSISTENT, BOOL, "1", "0", 2, SETTINGS_SIMPLE}},
{"SubaruSNGManualParkingBrake", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"SubaruStopStartOff", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"TacoTune", {PERSISTENT, BOOL, "0", "0", 2}},
{"TeslaCoopSteering", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
{"TestAlert", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
Binary file not shown.
+2 -2
View File
@@ -76,7 +76,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Chevrolet|Volt 2019|Adaptive Cruise Control (ACC) & LKAS|Stock|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM SDGM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Volt 2019">Buy Here</a></sub></details>|||
|Chevrolet|Volt ASCM Harness 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Volt ASCM Harness 2017-18">Buy Here</a></sub></details>|<a href="https://youtu.be/QeMCN_4TFfQ" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Chevrolet|Volt Camera Harness 2017-18|Flashed camera-forward integration with ACC|openpilot available[<sup>1</sup>](#footnotes)|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Volt Camera Harness 2017-18">Buy Here</a></sub></details>|||
|Chevrolet|Volt No-ACC 2017-18|Adaptive Cruise Control (ACC)|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Volt No-ACC 2017-18">Buy Here</a></sub></details>|||
|Chevrolet|Volt No-ACC 2016-18 (OBD-C / L&P Harness)|Redneck ACC|openpilot|0 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-II connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Volt No-ACC 2016-18 (OBD-C / L&P Harness)">Buy Here</a></sub></details>|||
|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica 2017-18">Buy Here</a></sub></details>|||
|Chrysler|Pacifica 2019-20|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica 2019-20">Buy Here</a></sub></details>|||
|Chrysler|Pacifica 2021-23|All|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica 2021-23">Buy Here</a></sub></details>|||
@@ -545,4 +545,4 @@ openpilot does not yet support these Toyota models due to a new message authenti
* Toyota Camry 2025+
* Lexus NX 2022+
* Toyota bZ4x 2023+
* Subaru Solterra 2023+
* Subaru Solterra 2023+
+1 -1
View File
@@ -21,7 +21,7 @@ fi
export QCOM_PRIORITY=12
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="19.6.10"
export AGNOS_VERSION="19.6.12"
fi
if [ -z "$AGNOS_ACCEPTED_VERSIONS" ]; then
+2 -2
View File
@@ -85,7 +85,7 @@
|Chevrolet|Volt 2019|Adaptive Cruise Control (ACC) & LKAS|[Upstream](#upstream)|
|Chevrolet|Volt ASCM Harness 2017-18|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|Chevrolet|Volt Camera Harness 2017-18|Flashed camera-forward integration with ACC|[Upstream](#upstream)|
|Chevrolet|Volt No-ACC 2017-18|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|Chevrolet|Volt No-ACC 2016-18 (OBD-C / L&P Harness)|Redneck ACC|[Upstream](#upstream)|
|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|Chrysler|Pacifica 2019-20|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|Chrysler|Pacifica 2021-23|All|[Upstream](#upstream)|
@@ -578,4 +578,4 @@ Toyota, and the GM Global B platform.
All the cars that openpilot supports use a [CAN bus](https://en.wikipedia.org/wiki/CAN_bus) for communication between all the car's computers, however a
CAN bus isn't the only way that the computers in your car can communicate. Most, if not all, vehicles from the following
manufacturers use [FlexRay](https://en.wikipedia.org/wiki/FlexRay) instead of a CAN bus: **BMW, Mercedes, Audi, Land Rover, and some Volvo**. These cars
may one day be supported, but we have no immediate plans to support FlexRay.
may one day be supported, but we have no immediate plans to support FlexRay.
+2
View File
@@ -644,6 +644,8 @@ struct CarParams {
fcaGiorgio @32;
rivian @33;
volkswagenMeb @34;
teslaPreAP @35;
volvo @36;
}
enum SteerControlType {
+74 -2
View File
@@ -209,7 +209,79 @@ class CarState(CarStateBase):
def get_can_parsers(CP):
gps_config = get_car_gps_config(CP)
gps_messages = [(name, 0) for name in gps_config.messages] if gps_config is not None else []
pt_messages = [
("BrakeSysFeatures", 50),
("Yaw_Data_FD1", 100),
("DesiredTorqBrk", 50),
("EngVehicleSpThrottle", 100),
("EngVehicleSpThrottle2", 50),
("BrakeSnData_4", 50),
("EngBrakeData", 10),
("EPAS_INFO", 50),
("Cluster_Info1_FD1", 10),
("Steering_Data_FD1", 10),
("BodyInfo_3_FD1", 2),
("RCMStatusMessage2_FD1", 10),
("BCM_Lamp_Stat_FD1", 0),
*gps_messages,
]
if CP.flags & FordFlags.ALT_STEER_ANGLE:
pt_messages += [
("SteeringPinion_Data_Alt", 100),
("ParkAid_Data", 50),
]
else:
pt_messages += [("SteeringPinion_Data", 100)]
if CP.flags & FordFlags.CANFD:
pt_messages += [
("Lane_Assist_Data3_FD1", 33),
("Cluster_Info_3_FD1", 10),
]
else:
pt_messages += [("INSTRUMENT_PANEL", 1)]
if CP.transmissionType == TransmissionType.automatic:
if CP.flags & FordFlags.CANFD:
pt_messages += [("Gear_Shift_by_Wire_FD1", 10)]
elif CP.flags & FordFlags.ALT_STEER_ANGLE:
pt_messages += [("TransGearData", 10)]
else:
pt_messages += [("PowertrainData_10", 10)]
if CP.enableBsm and not (CP.flags & FordFlags.CANFD):
pt_messages += [
("Side_Detect_L_Stat", 5),
("Side_Detect_R_Stat", 5),
]
cam_messages = [
("ACCDATA", 50),
("ACCDATA_2", 50),
("ACCDATA_3", 5),
("IPMA_Data", 1),
]
if CP.flags & FordFlags.CANFD:
cam_messages += [
("Traffic_RecognitnData", 1),
("IPMA_Data2", 1),
]
else:
cam_messages += [("Traffic_RecognitnData", 0)]
if CP.enableBsm and CP.flags & FordFlags.CANFD:
cam_messages += [
("Side_Detect_L_Stat", 5),
("Side_Detect_R_Stat", 5),
]
if CP.flags & FordFlags.LKA_STEERING:
cam_messages += [("LateralMotionControl", 20)]
return {
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], gps_messages, CanBus(CP).main),
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], CanBus(CP).camera),
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, CanBus(CP).main),
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, CanBus(CP).camera),
}
@@ -4,6 +4,7 @@ from types import SimpleNamespace
from hypothesis import settings, given, strategies as st
from parameterized import parameterized
import pytest
from opendbc.car import Bus, gen_empty_fingerprint
from opendbc.can import CANPacker
@@ -274,6 +275,23 @@ def test_mach_e_can_gps_messages_are_optional_main_bus_inputs():
assert all(parser.message_states[address].ignore_alive for address in (0x462, 0x463, 0x464))
def test_lightning_low_rate_camera_messages_use_declared_frequencies():
cp = CarInterface.get_params(CAR.FORD_F_150_LIGHTNING_MK1, gen_empty_fingerprint(), [], True, False, False, None)
cp.enableBsm = True
parser = CarInterface.CarState.get_can_parsers(cp)[Bus.cam]
expected_frequencies = {
"IPMA_Data": 1,
"Traffic_RecognitnData": 1,
"Side_Detect_L_Stat": 5,
"Side_Detect_R_Stat": 5,
}
for message, frequency in expected_frequencies.items():
state = parser.message_states[parser.dbc.name_to_msg[message].address]
assert state.frequency == frequency
assert state.timeout_threshold == pytest.approx(10e9 / frequency)
def test_hands_free_cluster_status_is_opt_in():
packer = CANPacker("ford_lincoln_base_pt")
CAN = SimpleNamespace(main=0)
+14 -15
View File
@@ -30,20 +30,20 @@ FINGERPRINTS = {
CAR.BUICK_LACROSSE: [{
190: 6, 193: 8, 197: 8, 199: 4, 201: 8, 209: 7, 211: 2, 241: 6, 249: 8, 288: 5, 298: 8, 304: 1, 309: 8, 311: 8, 313: 8, 320: 3, 322: 7, 328: 1, 352: 5, 353: 3, 381: 6, 386: 8, 388: 8, 393: 7, 398: 8, 407: 7, 413: 8, 417: 7, 419: 1, 422: 4, 426: 7, 431: 8, 442: 8, 451: 8, 452: 8, 453: 6, 455: 7, 456: 8, 463: 3, 479: 3, 481: 7, 485: 8, 487: 8, 489: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 8, 503: 1, 508: 8, 510: 8, 528: 5, 532: 6, 534: 2, 554: 3, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 567: 5, 573: 1, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 5, 707: 8, 753: 5, 761: 7, 801: 8, 804: 3, 810: 8, 840: 5, 842: 5, 844: 8, 866: 4, 872: 1, 882: 8, 890: 1, 892: 2, 893: 1, 894: 1, 961: 8, 967: 4, 969: 8, 977: 8, 979: 8, 985: 5, 1001: 8, 1005: 6, 1009: 8, 1011: 6, 1013: 3, 1017: 8, 1019: 2, 1020: 8, 1022: 1, 1105: 6, 1217: 8, 1221: 5, 1223: 2, 1225: 7, 1233: 8, 1243: 3, 1249: 8, 1257: 6, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 1, 1280: 4, 1300: 8, 1322: 6, 1328: 4, 1417: 8, 1609: 8, 1613: 8, 1649: 8, 1792: 8, 1798: 8, 1824: 8, 1825: 8, 1840: 8, 1842: 8, 1858: 8, 1860: 8, 1863: 8, 1872: 8, 1875: 8, 1882: 8, 1888: 8, 1889: 8, 1892: 8, 1904: 7, 1906: 7, 1907: 7, 1912: 7, 1913: 7, 1914: 7, 1916: 7, 1918: 7, 1919: 7, 1937: 8, 1953: 8, 1968: 8, 2001: 8, 2017: 8, 2018: 8, 2020: 8, 2026: 8
}],
# CAR.CHEVROLET_VOLT_CC: [
# FIXME: Need a message to distinguish flashed from non-flashed
# Volt Premier w/o acc 2016
# {
# 170: 8, 171: 8, 189: 7, 190: 6, 192: 5, 193: 8, 197: 8, 199: 4, 201: 6, 209: 7, 211: 2, 241: 6, 288: 5, 289: 1, 290: 1, 298: 2, 304: 8, 308: 4, 309: 8, 311: 8, 313: 8, 320: 8, 328: 1, 352: 5, 368: 8, 381: 6, 384: 8, 386: 5, 388: 8, 389: 2, 390: 7, 417: 7, 419: 1, 426: 7, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 458: 8, 479: 3, 481: 7, 485: 8, 489: 5, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 3, 508: 8, 512: 3, 528: 4, 530: 8, 532: 6, 537: 4, 539: 8, 542: 7, 546: 7, 550: 8, 554: 3, 558: 8, 560: 6, 562: 8, 563: 5, 564: 5, 565: 8, 566: 5, 567: 3, 568: 1, 577: 8, 578: 8, 594: 8, 647: 3, 707: 8, 711: 6, 717: 5, 761: 7, 800: 6, 810: 8, 821: 4, 823: 7, 832: 8, 840: 5, 842: 6, 844: 8, 866: 4, 869: 4, 961: 8, 969: 8, 977: 8, 979: 7, 988: 6, 989: 8, 995: 7, 1001: 5, 1003: 5, 1005: 6, 1009: 8, 1017: 8, 1019: 2, 1020: 8, 1033: 7, 1034: 7, 1105: 6, 1187: 4, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1249: 8, 1257: 6, 1265: 8, 1267: 1, 1273: 3, 1275: 3, 1280: 4, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 1417: 8, 1601: 8, 1602: 8, 1618: 8, 1906: 7, 1907: 7, 1910: 7, 1912: 7, 1922: 7, 1927: 7, 1928: 7, 1930: 7, 2016: 8, 2017: 8, 2018: 8, 2019: 8, 2020: 8, 2024: 8, 2025: 8, 2028: 8
# },
# {
# 201: 8, 493: 8, 495: 4, 193: 8, 197: 8, 209: 7, 171: 8, 456: 8, 199: 4, 489: 8, 211: 2, 499: 3, 390: 7, 532: 6, 568: 1, 761: 7, 381: 6, 485: 8, 189: 7, 479: 3, 711: 6, 501: 8, 241: 6, 717: 5, 869: 4, 389: 2, 454: 8, 170: 8, 190: 6, 497: 8, 417: 7, 419: 1, 426: 7, 451: 8, 452: 8, 453: 6, 500: 6, 508: 8, 528: 4, 647: 3, 1105: 6, 1005: 6, 481: 7, 844: 8, 866: 4, 564: 5, 969: 8, 388: 8, 352: 5, 562: 8, 961: 8, 386: 8, 707: 8, 977: 8, 979: 7, 298: 8, 840: 5, 842: 5, 988: 6, 1001: 8, 560: 8, 546: 7, 558: 8, 309: 8, 995: 7, 311: 8, 566: 5, 567:3, 989: 8, 384: 4, 800: 6, 1033: 7, 1034: 7, 313: 8, 554: 3, 810: 8, 1017: 8, 1019: 2, 1020: 8, 1217: 8, 1223: 3, 1233: 8, 1227: 4, 1417: 8, 1009: 8, 1221: 5, 1275: 3, 1225: 7, 289: 8, 550: 8, 1273: 3, 1928: 7, 1187: 4, 1265: 8, 1927: 7, 1267: 1, 1906: 7, 288: 5, 304: 1, 328: 1, 1912: 7, 320: 3, 1910: 7, 563: 5, 1249: 8, 1930: 7, 1257: 6, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 565: 5, 1280: 4, 1907: 7
# },
# # Volt Premier w/o ACC 2018 + Pedal
# {
# 189: 7, 193: 8, 197: 8, 201: 8, 209: 7, 211: 2, 241: 6, 288: 5, 298: 8, 304: 1, 308: 4, 309: 8, 311: 8, 313: 8, 320: 3, 328: 1, 352: 5, 381: 6, 384: 4, 386: 8, 388: 8, 451: 8, 452: 8, 453: 6, 479: 3, 481: 7, 485: 8, 489: 8, 493: 8, 497: 8, 500: 6, 501: 8, 513: 6, 528: 4, 532: 6, 560: 8, 562: 8, 563: 5, 565: 5, 566: 5, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 707: 8, 717: 5, 761: 7, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 869: 4, 977: 8, 1001: 8, 1017: 8, 1020: 8, 1033: 7, 1034: 7, 1217: 8, 1221: 5, 1233: 8, 1249: 8, 1265: 8, 1267: 1, 1280: 4, 1300: 8, 1922: 7, 1930: 7
# }
# ],
CAR.CHEVROLET_VOLT_CC: [
# Captured no-ACC Volt fingerprints for OBD-C/L&P harness installations
# Volt Premier w/o ACC 2016
{
170: 8, 171: 8, 189: 7, 190: 6, 192: 5, 193: 8, 197: 8, 199: 4, 201: 6, 209: 7, 211: 2, 241: 6, 288: 5, 289: 1, 290: 1, 298: 2, 304: 8, 308: 4, 309: 8, 311: 8, 313: 8, 320: 8, 328: 1, 352: 5, 368: 8, 381: 6, 384: 8, 386: 5, 388: 8, 389: 2, 390: 7, 417: 7, 419: 1, 426: 7, 451: 8, 452: 8, 453: 6, 454: 8, 456: 8, 458: 8, 479: 3, 481: 7, 485: 8, 489: 5, 493: 8, 495: 4, 497: 8, 499: 3, 500: 6, 501: 3, 508: 8, 512: 3, 528: 4, 530: 8, 532: 6, 537: 4, 539: 8, 542: 7, 546: 7, 550: 8, 554: 3, 558: 8, 560: 6, 562: 8, 563: 5, 564: 5, 565: 8, 566: 5, 567: 3, 568: 1, 577: 8, 578: 8, 594: 8, 647: 3, 707: 8, 711: 6, 717: 5, 761: 7, 800: 6, 810: 8, 821: 4, 823: 7, 832: 8, 840: 5, 842: 6, 844: 8, 866: 4, 869: 4, 961: 8, 969: 8, 977: 8, 979: 7, 988: 6, 989: 8, 995: 7, 1001: 5, 1003: 5, 1005: 6, 1009: 8, 1017: 8, 1019: 2, 1020: 8, 1033: 7, 1034: 7, 1105: 6, 1187: 4, 1217: 8, 1221: 5, 1223: 3, 1225: 7, 1227: 4, 1233: 8, 1249: 8, 1257: 6, 1265: 8, 1267: 1, 1273: 3, 1275: 3, 1280: 4, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 1417: 8, 1601: 8, 1602: 8, 1618: 8, 1906: 7, 1907: 7, 1910: 7, 1912: 7, 1922: 7, 1927: 7, 1928: 7, 1930: 7, 2016: 8, 2017: 8, 2018: 8, 2019: 8, 2020: 8, 2024: 8, 2025: 8, 2028: 8
},
{
201: 8, 493: 8, 495: 4, 193: 8, 197: 8, 209: 7, 171: 8, 456: 8, 199: 4, 489: 8, 211: 2, 499: 3, 390: 7, 532: 6, 568: 1, 761: 7, 381: 6, 485: 8, 189: 7, 479: 3, 711: 6, 501: 8, 241: 6, 717: 5, 869: 4, 389: 2, 454: 8, 170: 8, 190: 6, 497: 8, 417: 7, 419: 1, 426: 7, 451: 8, 452: 8, 453: 6, 500: 6, 508: 8, 528: 4, 647: 3, 1105: 6, 1005: 6, 481: 7, 844: 8, 866: 4, 564: 5, 969: 8, 388: 8, 352: 5, 562: 8, 961: 8, 386: 8, 707: 8, 977: 8, 979: 7, 298: 8, 840: 5, 842: 5, 988: 6, 1001: 8, 560: 8, 546: 7, 558: 8, 309: 8, 995: 7, 311: 8, 566: 5, 567:3, 989: 8, 384: 4, 800: 6, 1033: 7, 1034: 7, 313: 8, 554: 3, 810: 8, 1017: 8, 1019: 2, 1020: 8, 1217: 8, 1223: 3, 1233: 8, 1227: 4, 1417: 8, 1009: 8, 1221: 5, 1275: 3, 1225: 7, 289: 8, 550: 8, 1273: 3, 1928: 7, 1187: 4, 1265: 8, 1927: 7, 1267: 1, 1906: 7, 288: 5, 304: 1, 328: 1, 1912: 7, 320: 3, 1910: 7, 563: 5, 1249: 8, 1930: 7, 1257: 6, 1300: 8, 1322: 6, 1323: 4, 1328: 4, 565: 5, 1280: 4, 1907: 7
},
# Volt Premier w/o ACC 2018 + Pedal
{
189: 7, 193: 8, 197: 8, 201: 8, 209: 7, 211: 2, 241: 6, 288: 5, 298: 8, 304: 1, 308: 4, 309: 8, 311: 8, 313: 8, 320: 3, 328: 1, 352: 5, 381: 6, 384: 4, 386: 8, 388: 8, 451: 8, 452: 8, 453: 6, 479: 3, 481: 7, 485: 8, 489: 8, 493: 8, 497: 8, 500: 6, 501: 8, 513: 6, 528: 4, 532: 6, 560: 8, 562: 8, 563: 5, 565: 5, 566: 5, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 707: 8, 717: 5, 761: 7, 800: 6, 810: 8, 840: 5, 842: 5, 844: 8, 869: 4, 977: 8, 1001: 8, 1017: 8, 1020: 8, 1033: 7, 1034: 7, 1217: 8, 1221: 5, 1233: 8, 1249: 8, 1265: 8, 1267: 1, 1280: 4, 1300: 8, 1922: 7, 1930: 7
}
],
CAR.BUICK_REGAL: [{
190: 8, 193: 8, 197: 8, 199: 4, 201: 8, 209: 7, 211: 8, 241: 6, 249: 8, 288: 5, 298: 8, 304: 1, 309: 8, 311: 8, 313: 8, 320: 3, 322: 7, 328: 1, 352: 5, 381: 6, 384: 4, 386: 8, 388: 8, 393: 7, 398: 8, 407: 7, 413: 8, 417: 8, 419: 8, 422: 4, 426: 8, 431: 8, 442: 8, 451: 8, 452: 8, 453: 8, 455: 7, 456: 8, 463: 3, 479: 8, 481: 7, 485: 8, 487: 8, 489: 8, 495: 8, 497: 8, 499: 3, 500: 8, 501: 8, 508: 8, 528: 5, 532: 6, 554: 3, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 567: 5, 569: 3, 573: 1, 577: 8, 578: 8, 579: 8, 587: 8, 608: 8, 609: 6, 610: 6, 611: 6, 612: 8, 613: 8, 647: 3, 707: 8, 715: 8, 717: 5, 753: 5, 761: 7, 810: 8, 840: 5, 842: 5, 844: 8, 866: 4, 869: 4, 880: 6, 882: 8, 884: 8, 890: 1, 892: 2, 893: 2, 894: 1, 961: 8, 967: 8, 969: 8, 977: 8, 979: 8, 985: 8, 1001: 8, 1005: 6, 1009: 8, 1011: 8, 1013: 3, 1017: 8, 1020: 8, 1024: 8, 1025: 8, 1026: 8, 1027: 8, 1028: 8, 1029: 8, 1030: 8, 1031: 8, 1032: 2, 1033: 7, 1034: 7, 1105: 6, 1217: 8, 1221: 5, 1223: 8, 1225: 7, 1233: 8, 1249: 8, 1257: 6, 1259: 8, 1261: 8, 1263: 8, 1265: 8, 1267: 8, 1271: 8, 1280: 4, 1296: 4, 1300: 8, 1322: 6, 1328: 4, 1417: 8, 1601: 8, 1602: 8, 1603: 7, 1611: 8, 1618: 8, 1906: 8, 1907: 7, 1912: 7, 1914: 7, 1916: 7, 1919: 7, 1930: 7, 2016: 8, 2018: 8, 2019: 8, 2024: 8, 2026: 8
}],
@@ -207,7 +207,6 @@ FINGERPRINTS = {
FINGERPRINTS.update({
CAR.CHEVROLET_VOLT_ASCM: FINGERPRINTS[CAR.CHEVROLET_VOLT],
CAR.CHEVROLET_VOLT_CAMERA: [{**fp, CAMERA_DIAGNOSTIC_ADDRESS: 8, CAMERA_DIAGNOSTIC_RX_ADDRESS: 8} for fp in FINGERPRINTS[CAR.CHEVROLET_VOLT]],
CAR.CHEVROLET_VOLT_CC: FINGERPRINTS[CAR.CHEVROLET_VOLT],
CAR.GMC_ACADIA_ASCM: FINGERPRINTS[CAR.GMC_ACADIA],
CAR.CHEVROLET_MALIBU_ASCM: FINGERPRINTS[CAR.CHEVROLET_MALIBU],
CAR.CADILLAC_ESCALADE_ASCM: FINGERPRINTS[CAR.CADILLAC_ESCALADE],
+6 -4
View File
@@ -1,4 +1,4 @@
from opendbc.car import DT_CTRL
from opendbc.car import DT_CTRL, structs
from opendbc.car.can_definitions import CanData
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.gm.values import CAR, CanBus, CruiseButtons, GMFlags
@@ -420,9 +420,11 @@ def create_gm_cc_spam_command(packer, controller, CS, actuators, starpilot_toggl
idx = (CS.buttons_counter + 1) % 4 # Need to predict the next idx for '22-23 EUV
msgs = [create_buttons(packer, CanBus.POWERTRAIN, idx, cruise_btn)]
# Flashed camera-forward Volt CC installs also need the button spoof on the
# camera side. Removed-camera installs set NO_CAMERA and keep this PT-only.
if CS.CP.carFingerprint == CAR.CHEVROLET_VOLT_CC and not (CS.CP.flags & GMFlags.NO_CAMERA.value):
# A camera-forward Volt CC install needs the button spoof on both sides.
# The OBD-C/L&P gateway variant has no camera bus and remains PT-only.
if (CS.CP.carFingerprint == CAR.CHEVROLET_VOLT_CC and
getattr(CS.CP, "networkLocation", None) == structs.CarParams.NetworkLocation.fwdCamera and
not (CS.CP.flags & GMFlags.NO_CAMERA.value)):
msgs.append(create_buttons(packer, CanBus.CAMERA, idx, cruise_btn))
return msgs
else:
+3 -2
View File
@@ -273,7 +273,6 @@ class CarInterface(CarInterfaceBase):
kaofui_camera_cars = {
CAR.CHEVROLET_VOLT_CAMERA,
CAR.CHEVROLET_VOLT_CC,
CAR.CHEVROLET_MALIBU_HYBRID_CC,
}
bolt_cc_camera_cars = {
@@ -685,6 +684,8 @@ class CarInterface(CarInterfaceBase):
if candidate in CC_ONLY_CAR:
ret.safetyConfigs[0].safetyParam |= GMSafetyFlags.FLAG_GM_NO_ACC.value
if candidate == CAR.CHEVROLET_VOLT_CC and ret.networkLocation == NetworkLocation.gateway:
ret.safetyConfigs[0].safetyParam |= GMSafetyFlags.FLAG_GM_VOLT_CC_GATEWAY.value
if candidate in SDGM_CAR and ACCELERATOR_POS_MSG not in fingerprint[CanBus.POWERTRAIN]:
ret.flags |= GMFlags.FORCE_BRAKE_C9.value
@@ -698,7 +699,7 @@ class CarInterface(CarInterfaceBase):
if ACCELERATOR_POS_MSG not in fingerprint[CanBus.POWERTRAIN]:
ret.flags |= GMFlags.NO_ACCELERATOR_POS_MSG.value
if candidate == CAR.CHEVROLET_VOLT and ret.networkLocation == NetworkLocation.gateway:
if candidate in (CAR.CHEVROLET_VOLT, CAR.CHEVROLET_VOLT_CC) and ret.networkLocation == NetworkLocation.gateway:
# Reuse the no-camera safety bit as an ASCM Volt selector for the alternate EBCM brake path.
ret.safetyConfigs[0].safetyParam |= GMSafetyFlags.FLAG_GM_NO_CAMERA.value
@@ -201,6 +201,45 @@ class TestGMInterface:
assert car_params.flags & GMFlags.NO_CAMERA.value
assert car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_NO_CAMERA.value
def test_volt_cc_obd_gateway_uses_cc_long_no_camera_path(self):
CarInterface = interfaces[CAR.CHEVROLET_VOLT_CC]
car_params = CarInterface.get_params(
CAR.CHEVROLET_VOLT_CC,
_empty_fingerprint(),
[],
alpha_long=False,
is_release=False,
docs=False,
starpilot_toggles=_test_starpilot_toggles(),
)
assert car_params.networkLocation == structs.CarParams.NetworkLocation.gateway
assert car_params.flags & GMFlags.CC_LONG.value
assert car_params.flags & GMFlags.NO_CAMERA.value
assert not (car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.HW_CAM.value)
assert car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_CC_LONG.value
assert car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_NO_ACC.value
assert car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_NO_CAMERA.value
parsers = CarInterface.CarState.get_can_parsers(car_params)
assert "ECMCruiseControl" in parsers[Bus.pt].vl
assert not parsers[Bus.cam].vl
def test_other_cc_only_gateway_does_not_use_volt_cc_safety_path(self):
CarInterface = interfaces[CAR.CHEVROLET_SILVERADO_CC]
car_params = CarInterface.get_params(
CAR.CHEVROLET_SILVERADO_CC,
_empty_fingerprint(),
[],
alpha_long=False,
is_release=False,
docs=False,
starpilot_toggles=_test_starpilot_toggles(),
)
assert car_params.networkLocation == structs.CarParams.NetworkLocation.gateway
assert not (car_params.safetyConfigs[0].safetyParam & GMSafetyFlags.FLAG_GM_VOLT_CC_GATEWAY.value)
def test_volt_ascm_sparse_fingerprint_without_camera_does_not_set_no_camera(self):
CarInterface = interfaces[CAR.CHEVROLET_VOLT_ASCM]
fingerprint = {
@@ -533,6 +572,7 @@ class TestGMCarController:
CP=SimpleNamespace(
carFingerprint=CAR.CHEVROLET_VOLT_CC,
flags=0,
networkLocation=structs.CarParams.NetworkLocation.fwdCamera,
minEnableSpeed=24 * CV.MPH_TO_MS,
),
buttons_counter=2,
@@ -554,6 +594,7 @@ class TestGMCarController:
CP=SimpleNamespace(
carFingerprint=CAR.CHEVROLET_VOLT_CC,
flags=GMFlags.NO_CAMERA.value,
networkLocation=structs.CarParams.NetworkLocation.gateway,
minEnableSpeed=24 * CV.MPH_TO_MS,
),
buttons_counter=2,
+2 -1
View File
@@ -175,6 +175,7 @@ class GMSafetyFlags(IntFlag):
FLAG_GM_REMOTE_START_BOOTS_COMMA = 8192
FLAG_GM_PANDA_3D1_SCHED = 16384
FLAG_GM_PANDA_PADDLE_SCHED = 32768
FLAG_GM_VOLT_CC_GATEWAY = 16384
class Footnote(Enum):
@@ -247,7 +248,7 @@ class CAR(Platforms):
dbc_dict=CHEVROLET_VOLT.dbc_dict,
)
CHEVROLET_VOLT_CC = GMPlatformConfig(
[GMCarDocs("Chevrolet Volt No-ACC 2017-18", min_enable_speed=0)],
[GMCarDocs("Chevrolet Volt No-ACC 2016-18 (OBD-C / L&P Harness)", "Redneck ACC", min_enable_speed=0)],
CHEVROLET_VOLT.specs,
dbc_dict=CHEVROLET_VOLT.dbc_dict,
)
@@ -1107,6 +1107,14 @@ def test_crv_5g_bosch_a_radar_dbc_wired_for_parser_unit_tests():
assert ri.rcp.bus == CanBus(cp).camera
def test_accord_bosch_a_radar_stays_disabled_until_validated():
cp = CarInterface.get_non_essential_params(CAR.HONDA_ACCORD)
assert cp.radarUnavailable is True
ri = CarInterface.RadarInterface(cp)
assert ri.bosch_a_radar is False
assert ri.rcp is None
def test_civic_bosch_object_feed_uses_camera_side_acc_can():
ri = make_radar_interface()
can = CanBus(CP)
@@ -1180,5 +1188,6 @@ def test_bosch_a_toggle_defaults_on_but_allowlist_still_gates_platforms():
Params().remove("HondaBoschARadar")
assert CarInterface.get_non_essential_params(CAR.HONDA_CIVIC_BOSCH).radarUnavailable is False
assert CarInterface.get_non_essential_params(CAR.HONDA_ACCORD).radarUnavailable is True
assert CarInterface.get_non_essential_params(CAR.HONDA_ACCORD_11G).radarUnavailable is True
finally:
Params().put_bool("HondaBoschARadar", original)
-3
View File
@@ -533,9 +533,6 @@ HONDA_BOSCH_ALT_RADAR = CAR.with_flags(HondaFlags.BOSCH_ALT_RADAR)
# HondaBoschARadar. This describes hardware compatibility only; it is deliberately separate from the
# verified set below so a newly supported model cannot start using unvalidated radar data by accident.
HONDA_BOSCH_A = HONDA_BOSCH - HONDA_BOSCH_RADARLESS - HONDA_BOSCH_CANFD - HONDA_BOSCH_ALT_RADAR
# Add individual CAR entries only after the exact platform has a real capture and decoder replay
# validation. The Civic and CR-V 5G captures both exercise the plain Bosch-A object bank; every
# other Bosch-A variant remains disabled until it gets the same verification.
HONDA_BOSCH_A_RADAR_VERIFIED = frozenset({CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CRV_5G})
HONDA_BOSCH_TJA_CONTROL = CAR.with_flags(HondaFlags.BOSCH_TJA_CONTROL)
HONDA_CAMERA_MESSAGE_CARS = {
@@ -431,6 +431,12 @@ def suppress_redundant_gv70_brake_cancel(CP, brake_pressed: bool, lat_active: bo
)
def clear_ioniq_6_torque_when_request_inactive(CP, apply_torque: int, apply_steer_req: bool) -> int:
if CP.carFingerprint == CAR.HYUNDAI_IONIQ_6 and not apply_steer_req:
return 0
return apply_torque
class CarController(CarControllerBase):
def __init__(self, dbc_names, CP):
super().__init__(dbc_names, CP)
@@ -613,6 +619,8 @@ class CarController(CarControllerBase):
if not CC.latActive:
apply_torque = 0
apply_torque = clear_ioniq_6_torque_when_request_inactive(self.CP, apply_torque, apply_steer_req)
# Hold torque with induced temporary fault when cutting the actuation bit
# FIXME: we don't use this with CAN FD?
torque_fault = CC.latActive and not apply_steer_req
@@ -20,7 +20,8 @@ from opendbc.car.hyundai.carcontroller import CarController, Ioniq6LongitudinalT
should_track_stop_accel_directly_for_car, \
preserve_stock_canfd_lfa_status, \
preserve_stock_canfd_lkas_status, \
suppress_redundant_gv70_brake_cancel
suppress_redundant_gv70_brake_cancel, \
clear_ioniq_6_torque_when_request_inactive
from opendbc.car.hyundai.carstate import CarState, decode_canfd_camera_lead, decode_ioniq_6_blindspot_radar_state, \
get_canfd_cruise_available
from opendbc.car.hyundai.interface import CarInterface, KIA_EV9_ACCEL_MAX
@@ -554,6 +555,14 @@ class TestHyundaiFingerprint:
assert not (CP.flags & HyundaiFlags.CANFD_LKA_STEERING)
assert bool(CP.flags & HyundaiFlags.CANFD_CAMERA_SCC)
def test_ioniq_6_clears_torque_with_inactive_safety_request(self):
ioniq_6_cp = SimpleNamespace(carFingerprint=CAR.HYUNDAI_IONIQ_6)
other_cp = SimpleNamespace(carFingerprint=CAR.KIA_EV6)
assert clear_ioniq_6_torque_when_request_inactive(ioniq_6_cp, -409, False) == 0
assert clear_ioniq_6_torque_when_request_inactive(ioniq_6_cp, -409, True) == -409
assert clear_ioniq_6_torque_when_request_inactive(other_cp, -409, False) == -409
palisade_2023 = CarInterface.get_params(CAR.HYUNDAI_PALISADE_2023, gen_empty_fingerprint(), [], True, False, False, None)
assert palisade_2023.flags & HyundaiFlags.CAN_CANFD_BLENDED
assert DBC[palisade_2023.carFingerprint][Bus.pt] == "hyundai_palisade_2023_generated"
@@ -111,6 +111,7 @@ class HyundaiSafetyFlags(IntFlag):
class HyundaiStarPilotSafetyFlags(IntFlag):
AOL_MAIN_LKAS_ON_ENGAGE = 128
AOL_MAIN_LKAS_SYNC = 32
HAS_LDA_BUTTON = 1024
AOL_LKAS_ON_ENGAGE = 2048
+5
View File
@@ -257,6 +257,11 @@ class CarInterfaceBase(ABC):
if getattr(starpilot_toggles, "always_on_lateral_lkas", False):
fp_ret.safetyConfigs[-1].safetyParam |= HyundaiStarPilotSafetyFlags.AOL_LKAS_ON_ENGAGE.value
if candidate in (HYUNDAI.HYUNDAI_ELANTRA_HEV_2024, HYUNDAI.HYUNDAI_SONATA_HYBRID) and \
getattr(starpilot_toggles, "always_on_lateral_main", False):
fp_ret.safetyConfigs[-1].safetyParam |= HyundaiStarPilotSafetyFlags.AOL_LKAS_ON_ENGAGE.value
fp_ret.safetyConfigs[-1].safetyParam |= HyundaiStarPilotSafetyFlags.AOL_MAIN_LKAS_ON_ENGAGE.value
# LKASButtonControl == 9 means BUTTON_FUNCTIONS["AOL_TOGGLE"] in starpilot_variables.
if params.get_bool("AlwaysOnLateral") and params.get_int("LKASButtonControl") == 9:
fp_ret.safetyConfigs[-1].safetyParam |= HyundaiStarPilotSafetyFlags.AOL_LKAS_ON_ENGAGE.value
+112 -15
View File
@@ -4,7 +4,7 @@ from opendbc.car import Bus, DT_CTRL, make_tester_present_msg, structs
from opendbc.car.lateral import apply_driver_steer_torque_limits, apply_std_steer_angle_limits, apply_steer_angle_limits_vm, common_fault_avoidance
from opendbc.car.interfaces import CarControllerBase
from opendbc.car.subaru import subarucan
from opendbc.car.subaru.values import CAR, DBC, GLOBAL_ES_ADDR, CanBus, CarControllerParams, SubaruFlags
from opendbc.car.subaru.values import CAR, DBC, GLOBAL_ES_ADDR, SUBARU_STOP_START_CARS, CanBus, CarControllerParams, SubaruFlags
from opendbc.car.vehicle_model import VehicleModel
# FIXME: These limits aren't exact. The real limit is more than likely over a larger time period and
@@ -22,6 +22,7 @@ _LEGACY_2025_REENGAGE_MAX_STEER_RATE = 2.0
_LEGACY_2025_REENGAGE_MAX_ANGLE_DELTA = 1.0
_LEGACY_2025_RECLAIM_FRAMES = 36
_LEGACY_2025_RECLAIM_EXPONENT = 2.5
_ANGLE_OVERRIDE_CONFIRM_FRAMES = 2
_ANGLE_OVERRIDE_HOLD_FRAMES = 10
_ANGLE_REENGAGE_SETTLE_FRAMES = 8
_ANGLE_REENGAGE_MAX_STEER_RATE = 2.0
@@ -30,6 +31,10 @@ _ANGLE_RECLAIM_FRAMES = 36
_ANGLE_RECLAIM_EXPONENT = 2.5
_ANGLE_MADS_MIN_SPEED = 0.44704
_ANGLE_MADS_MAX_STEER_ANGLE = 120.0
_STOP_START_STARTUP_DELAY_FRAMES = 100
_STOP_START_STARTUP_DEADLINE_FRAMES = 300
_STOP_START_PULSE_FRAMES = 30
_STOP_START_PULSE_PERIOD_FRAMES = 5
def get_safety_CP():
@@ -43,6 +48,7 @@ class CarController(CarControllerBase):
self.apply_torque_last = 0
self.apply_steer_last = 0
self.driver_override = False
self.angle_override_confirm_frames = 0
self.legacy_2025_lkas_active = False
self.legacy_2025_handoff_active = False
self.legacy_2025_override_hold_frames = 0
@@ -73,8 +79,70 @@ class CarController(CarControllerBase):
self.prev_close_distance = 0
self.epb_resume_frames_remaining = -1
self.last_standstill_frame = 0
self.stop_start_attempted = False
self.stop_start_request_started = False
self.stop_start_request_frame = 0
self.stop_start_initial_state = None
self.stop_start_counter = 0
self.stop_start_acknowledged = False
def _stop_start_off_request(self, CC, CS, starpilot_toggles):
"""Send one bounded Subaru Stop/Start OFF request after ignition.
This is intentionally opt-in and limited to a stationary vehicle in
Park/Neutral. A single ignition session gets at most one attempt.
"""
if self.CP.carFingerprint not in SUBARU_STOP_START_CARS or \
not getattr(starpilot_toggles, "subaru_stop_start_off", False) or self.stop_start_attempted:
return None
if self.frame > _STOP_START_STARTUP_DEADLINE_FRAMES or getattr(CC, "enabled", False):
self.stop_start_attempted = True
return None
if self.frame < _STOP_START_STARTUP_DELAY_FRAMES or not getattr(getattr(CS, "out", None), "canValid", True):
return None
out = CS.out
if not getattr(out, "standstill", False) or out.gearShifter not in (
structs.CarState.GearShifter.park,
structs.CarState.GearShifter.neutral,
):
return None
dashlights_msg = getattr(CS, "dashlights_msg", None)
if not dashlights_msg:
return None
if not self.stop_start_request_started:
self.stop_start_request_started = True
self.stop_start_request_frame = self.frame
self.stop_start_initial_state = getattr(CS, "stop_start_state", None)
self.stop_start_counter = (int(dashlights_msg.get("COUNTER", 0)) + 1) % 0x10
current_state = getattr(CS, "stop_start_state", None)
if self.stop_start_initial_state is not None and self.stop_start_initial_state != 3 and current_state == 3:
self.stop_start_attempted = True
self.stop_start_acknowledged = True
return None
elapsed = self.frame - self.stop_start_request_frame
if elapsed >= _STOP_START_PULSE_FRAMES:
self.stop_start_attempted = True
return None
if elapsed % _STOP_START_PULSE_PERIOD_FRAMES != 0:
return None
msg = subarucan.create_stop_start_control(
self.packer, dashlights_msg, counter=self.stop_start_counter, bus=self.main_bus,
)
self.stop_start_counter = (self.stop_start_counter + 1) % 0x10
return msg
def _reset_legacy_2025_handoff(self):
self.driver_override = False
self.angle_override_confirm_frames = 0
self.legacy_2025_handoff_active = False
self.legacy_2025_override_hold_frames = 0
self.legacy_2025_reengage_settle_frames = 0
@@ -87,7 +155,8 @@ class CarController(CarControllerBase):
self._reset_legacy_2025_handoff()
return False
if CS.out.steeringPressed:
driver_override = self._update_angle_driver_override(CS)
if driver_override:
self.legacy_2025_handoff_active = True
self.legacy_2025_override_hold_frames = _LEGACY_2025_OVERRIDE_HOLD_FRAMES
self.legacy_2025_reengage_settle_frames = 0
@@ -96,7 +165,7 @@ class CarController(CarControllerBase):
return True
if not self.legacy_2025_handoff_active and not self.legacy_2025_lkas_active and \
abs(CS.out.steeringRateDeg) > _LEGACY_2025_REENGAGE_MAX_STEER_RATE:
abs(getattr(CS.out, "steeringRateDeg", 0.0)) > _LEGACY_2025_REENGAGE_MAX_STEER_RATE:
self.legacy_2025_handoff_active = True
self.legacy_2025_reengage_reference_angle = CS.out.steeringAngleDeg
@@ -109,7 +178,7 @@ class CarController(CarControllerBase):
self.legacy_2025_reengage_reference_angle = CS.out.steeringAngleDeg
return True
wheel_stable = abs(CS.out.steeringRateDeg) <= _LEGACY_2025_REENGAGE_MAX_STEER_RATE and \
wheel_stable = abs(getattr(CS.out, "steeringRateDeg", 0.0)) <= _LEGACY_2025_REENGAGE_MAX_STEER_RATE and \
abs(CS.out.steeringAngleDeg - self.legacy_2025_reengage_reference_angle) <= _LEGACY_2025_REENGAGE_MAX_ANGLE_DELTA
if wheel_stable:
self.legacy_2025_reengage_settle_frames += 1
@@ -138,6 +207,8 @@ class CarController(CarControllerBase):
return target_angle
def _reset_angle_handoff(self):
self.driver_override = False
self.angle_override_confirm_frames = 0
self.angle_handoff_active = False
self.angle_override_hold_frames = 0
self.angle_reengage_settle_frames = 0
@@ -150,7 +221,8 @@ class CarController(CarControllerBase):
self._reset_angle_handoff()
return False
if CS.out.steeringPressed:
driver_override = self._update_angle_driver_override(CS)
if driver_override:
self.angle_handoff_active = True
self.angle_override_hold_frames = _ANGLE_OVERRIDE_HOLD_FRAMES
self.angle_reengage_settle_frames = 0
@@ -159,7 +231,7 @@ class CarController(CarControllerBase):
return True
if not self.angle_handoff_active and not self.angle_lkas_active and \
abs(CS.out.steeringRateDeg) > _ANGLE_REENGAGE_MAX_STEER_RATE:
abs(getattr(CS.out, "steeringRateDeg", 0.0)) > _ANGLE_REENGAGE_MAX_STEER_RATE:
self.angle_handoff_active = True
self.angle_reengage_reference_angle = CS.out.steeringAngleDeg
@@ -172,7 +244,7 @@ class CarController(CarControllerBase):
self.angle_reengage_reference_angle = CS.out.steeringAngleDeg
return True
wheel_stable = abs(CS.out.steeringRateDeg) <= _ANGLE_REENGAGE_MAX_STEER_RATE and \
wheel_stable = abs(getattr(CS.out, "steeringRateDeg", 0.0)) <= _ANGLE_REENGAGE_MAX_STEER_RATE and \
abs(CS.out.steeringAngleDeg - self.angle_reengage_reference_angle) <= _ANGLE_REENGAGE_MAX_ANGLE_DELTA
if wheel_stable:
self.angle_reengage_settle_frames += 1
@@ -189,6 +261,22 @@ class CarController(CarControllerBase):
self.angle_reclaim_start_angle = CS.out.steeringAngleDeg
return True
def _update_angle_driver_override(self, CS):
"""Debounce the higher-confidence raw torque override signal for angle cars."""
abs_torque = abs(getattr(CS.out, "steeringTorque", 0.0))
if self.driver_override:
if abs_torque < self.p.STEER_OVERRIDE_TORQUE_LOW:
self.driver_override = False
elif abs_torque > self.p.STEER_OVERRIDE_TORQUE_HIGH:
self.angle_override_confirm_frames += 1
if self.angle_override_confirm_frames >= _ANGLE_OVERRIDE_CONFIRM_FRAMES:
self.driver_override = True
self.angle_override_confirm_frames = 0
else:
self.angle_override_confirm_frames = 0
return self.driver_override
def _angle_reclaim_target(self, target_angle):
if self.angle_reclaim_frames <= 0:
return target_angle
@@ -261,15 +349,19 @@ class CarController(CarControllerBase):
self.angle_lkas_active = lkas_active
return subarucan.create_steering_control_angle(self.packer, apply_steer, lkas_active, self.angle_bus)
abs_torque = abs(CS.out.steeringTorque)
if abs_torque > self.p.STEER_OVERRIDE_TORQUE_HIGH:
self.driver_override = True
elif abs_torque < self.p.STEER_OVERRIDE_TORQUE_LOW:
self.driver_override = False
lat_active = CC.latActive and not self.driver_override
mads_only = CC.latActive and not getattr(CC, "enabled", False)
mads_only_ok = CS.out.vEgoRaw > _ANGLE_MADS_MIN_SPEED and \
abs(CS.out.steeringAngleDeg) < _ANGLE_MADS_MAX_STEER_ANGLE
lkas_available = CC.latActive and (not mads_only or mads_only_ok) and \
getattr(CS.out, "gearShifter", structs.CarState.GearShifter.drive) == structs.CarState.GearShifter.drive and \
not getattr(CS.out, "standstill", False)
manual_handoff = self._angle_manual_handoff(CS, lkas_available)
lat_active = lkas_available and not self.driver_override and not manual_handoff
if lat_active and not self.angle_lkas_active:
self.apply_steer_last = CS.out.steeringAngleDeg
steer_target = self._angle_reclaim_target(CC.actuators.steeringAngleDeg) if lat_active else CC.actuators.steeringAngleDeg
apply_steer = apply_steer_angle_limits_vm(
CC.actuators.steeringAngleDeg,
steer_target,
self.apply_steer_last,
CS.out.vEgoRaw,
CS.out.steeringAngleDeg,
@@ -282,6 +374,7 @@ class CarController(CarControllerBase):
apply_steer = CS.out.steeringAngleDeg
self.apply_steer_last = apply_steer
self.angle_lkas_active = lat_active
return subarucan.create_steering_control_angle(self.packer, apply_steer, lat_active, self.angle_bus)
def lateral_torque(self, CC, CS):
@@ -316,6 +409,10 @@ class CarController(CarControllerBase):
can_sends = []
stop_start_msg = self._stop_start_off_request(CC, CS, starpilot_toggles)
if stop_start_msg is not None:
can_sends.append(stop_start_msg)
# *** steering ***
if (self.frame % self.p.STEER_STEP) == 0:
if self.CP.flags & SubaruFlags.LKAS_ANGLE:
+7 -1
View File
@@ -4,7 +4,7 @@ from opendbc.can import CANDefine, CANParser
from opendbc.car import Bus, structs
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.interfaces import CarStateBase
from opendbc.car.subaru.values import DBC, CanBus, SubaruFlags
from opendbc.car.subaru.values import CAR, DBC, CanBus, SubaruFlags
from opendbc.car import CanSignalRateCalculator
@@ -15,6 +15,8 @@ class CarState(CarStateBase):
self.shifter_values = can_define.dv["Transmission"]["Gear"]
self.angle_rate_calulator = CanSignalRateCalculator(50)
self.dashlights_msg = {}
self.stop_start_state = 0
def update(self, can_parsers, starpilot_toggles) -> structs.CarState:
cp = can_parsers[Bus.pt]
@@ -24,6 +26,10 @@ class CarState(CarStateBase):
cp_angle = cp_main if self.CP.flags & SubaruFlags.D_PLATFORM else cp
ret = structs.CarState()
if self.CP.carFingerprint == CAR.SUBARU_OUTBACK_2023:
self.dashlights_msg = copy.copy(cp.vl["Dashlights"])
self.stop_start_state = cp.vl["Engine_Stop_Start"]["STOP_START_STATE"]
throttle_msg = cp.vl["Throttle"] if not (self.CP.flags & SubaruFlags.HYBRID) else cp_alt.vl["Throttle_Hybrid"]
ret.gasPressed = throttle_msg["Throttle_Pedal"] > 1e-5
if self.CP.flags & SubaruFlags.PREGLOBAL:
+3 -1
View File
@@ -3,7 +3,7 @@ from opendbc.car.disable_ecu import disable_ecu
from opendbc.car.interfaces import CarInterfaceBase
from opendbc.car.subaru.carcontroller import CarController
from opendbc.car.subaru.carstate import CarState
from opendbc.car.subaru.values import CAR, CanBus, GLOBAL_ES_ADDR, SubaruFlags, SubaruSafetyFlags
from opendbc.car.subaru.values import CAR, CanBus, GLOBAL_ES_ADDR, SUBARU_STOP_START_CARS, SubaruFlags, SubaruSafetyFlags
class CarInterface(CarInterfaceBase):
@@ -40,6 +40,8 @@ class CarInterface(CarInterfaceBase):
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.D_PLATFORM.value
if ret.flags & SubaruFlags.D_PLATFORM_CAMERA:
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.D_PLATFORM_CAMERA.value
if candidate in SUBARU_STOP_START_CARS:
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.STOP_START_BUTTON.value
if candidate in (CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023):
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.FIXED_ANGLE_LIMITS.value
@@ -182,6 +182,20 @@ def create_es_dashstatus(packer, frame, dashstatus_msg, enabled, long_enabled, l
return packer.make_can_msg("ES_DashStatus", bus, values)
def create_stop_start_control(packer, dashlights_msg, counter=None, bus=CanBus.alt):
"""Create the Outback 2023-24 momentary Stop/Start button request.
Dashlights is a stock periodic message, so preserve the live frame and only
change the event bit. CANPacker calculates the Subaru checksum for us.
"""
values = dict(dashlights_msg)
if counter is None:
counter = (int(values.get("COUNTER", 0)) + 1) % 0x10
values["COUNTER"] = counter % 0x10
values["STOP_START"] = 1
return packer.make_can_msg("Dashlights", bus, values)
def create_es_brake(packer, frame, es_brake_msg, long_enabled, long_active, brake_value, bus=CanBus.main):
values = {s: es_brake_msg[s] for s in [
"CHECKSUM",
@@ -193,6 +193,7 @@ def test_outback_2023_uses_d_platform_bus_layout():
assert CP.flags & SubaruFlags.D_PLATFORM
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.D_PLATFORM
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.STOP_START_BUTTON
assert not (CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.LEGACY_2025_ANGLE_LIMITS)
assert CanBus.main_for_cp(CP) == CanBus.alt
assert CanBus.angle_for_cp(CP) == CanBus.main
@@ -205,6 +206,56 @@ def test_outback_2023_uses_d_platform_bus_layout():
assert CP.lateralSmoothSeconds == pytest.approx(0.4)
@pytest.mark.parametrize("platform, expected_bus", [
(CAR.SUBARU_OUTBACK_2023, CanBus.alt),
(CAR.SUBARU_LEGACY_2025, CanBus.main),
])
def test_stop_start_request_is_bounded_and_uses_live_dashlights(platform, expected_bus):
CP = CarInterface.get_non_essential_params(platform)
controller = CarController({}, CP)
controller.frame = 101
class TestActuators:
steeringAngleDeg = 0.0
def as_builder(self):
return SimpleNamespace(steeringAngleDeg=self.steeringAngleDeg)
CC = SimpleNamespace(
enabled=False,
latActive=False,
longActive=False,
actuators=TestActuators(),
hudControl=SimpleNamespace(leadVisible=False),
cruiseControl=SimpleNamespace(cancel=False),
)
CS = SimpleNamespace(
canValid=True,
dashlights_msg={"COUNTER": 6, "STOP_START": 0},
stop_start_state=0,
out=SimpleNamespace(
standstill=True,
gearShifter=structs.CarState.GearShifter.park,
),
)
toggles = SimpleNamespace(subaru_stop_start_off=True, subaru_sng=False)
_, can_sends = controller.update(CC, CS, 0, toggles)
stop_start_msgs = [msg for msg in can_sends if msg[0] == 0x390]
assert len(stop_start_msgs) == 1
assert stop_start_msgs[0][2] == expected_bus
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("Dashlights", 0)], expected_bus)
parser.update([(expected_bus, [stop_start_msgs[0]])])
assert parser.vl["Dashlights"]["STOP_START"] == 1
assert parser.vl["Dashlights"]["COUNTER"] == 7
controller.frame = 103
CS.stop_start_state = 3
_, can_sends = controller.update(CC, CS, 0, toggles)
assert not any(msg[0] == 0x390 for msg in can_sends)
assert controller.stop_start_acknowledged
def test_legacy_2025_uses_gen2_angle_bus_layout():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_LEGACY_2025)
parsers = CarState.get_can_parsers(CP)
@@ -215,6 +266,7 @@ def test_legacy_2025_uses_gen2_angle_bus_layout():
assert not (CP.flags & SubaruFlags.D_PLATFORM_CAMERA)
assert not (CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.D_PLATFORM_CAMERA)
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.FIXED_ANGLE_LIMITS
assert CP.safetyConfigs[0].safetyParam & SubaruSafetyFlags.STOP_START_BUTTON
assert CanBus.main_for_cp(CP) == CanBus.main
assert CanBus.angle_for_cp(CP) == CanBus.main
assert parsers[Bus.pt].bus == CanBus.main
@@ -304,6 +356,7 @@ def test_legacy_2025_waits_for_manual_steering_to_settle_before_reengaging():
vEgoRaw=6.2,
steeringAngleDeg=-121.55,
steeringRateDeg=350.0,
steeringTorque=250.0,
steeringPressed=True,
gearShifter=structs.CarState.GearShifter.drive,
standstill=False,
@@ -312,10 +365,14 @@ def test_legacy_2025_waits_for_manual_steering_to_settle_before_reengaging():
msg = controller.lateral_angle(CC, CS)
parser.update([(1, [msg])])
msg = controller.lateral_angle(CC, CS)
parser.update([(2, [msg])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
CS.out.steeringPressed = False
CS.out.steeringTorque = 0.0
CS.out.steeringAngleDeg = -113.78
msg = controller.lateral_angle(CC, CS)
parser.update([(2, [msg])])
@@ -364,6 +421,7 @@ def test_legacy_2025_manual_handoff_reclaim_is_gradual():
vEgoRaw=3.7,
steeringAngleDeg=2.5,
steeringRateDeg=-45.0,
steeringTorque=250.0,
steeringPressed=True,
gearShifter=structs.CarState.GearShifter.drive,
standstill=False,
@@ -372,9 +430,13 @@ def test_legacy_2025_manual_handoff_reclaim_is_gradual():
msg = controller.lateral_angle(CC, CS)
parser.update([(1, [msg])])
msg = controller.lateral_angle(CC, CS)
parser.update([(2, [msg])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
CS.out.steeringPressed = False
CS.out.steeringTorque = 0.0
CS.out.steeringRateDeg = 0.0
for i in range(19):
msg = controller.lateral_angle(CC, CS)
@@ -433,6 +495,10 @@ def test_angle_controller_tracks_driver_override():
msg = controller.lateral_angle(CC, CS)
assert not controller.driver_override
msg = controller.lateral_angle(CC, CS)
assert controller.driver_override
assert controller.p.STEER_OVERRIDE_TORQUE_HIGH == 150
assert controller.p.STEER_OVERRIDE_TORQUE_LOW == 100
@@ -448,6 +514,44 @@ def test_angle_controller_tracks_driver_override():
assert not controller.driver_override
def test_angle_controller_blocks_low_speed_mads_engagement():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_CROSSTREK_2025)
controller = CarController({}, CP)
CC = SimpleNamespace(
enabled=False,
latActive=True,
actuators=SimpleNamespace(steeringAngleDeg=15.0),
)
CS = SimpleNamespace(out=SimpleNamespace(
vEgoRaw=0.3,
steeringAngleDeg=80.0,
steeringTorque=0.0,
gearShifter=structs.CarState.GearShifter.drive,
standstill=False,
))
parser = CANParser(DBC[CP.carFingerprint][Bus.pt], [("ES_LKAS_ANGLE", 0)], CanBus.main)
msg = controller.lateral_angle(CC, CS)
parser.update([(1, [msg])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
CS.out.vEgoRaw = 1.0
CS.out.steeringAngleDeg = 130.0
msg = controller.lateral_angle(CC, CS)
parser.update([(2, [msg])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
CS.out.steeringAngleDeg = 0.0
msg = controller.lateral_angle(CC, CS)
parser.update([(3, [msg])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 1
def test_ascent_angle_controller_uses_fixed_angle_rate_limits():
CP = CarInterface.get_non_essential_params(CAR.SUBARU_ASCENT_2023)
controller = CarController({}, CP)
@@ -456,7 +560,7 @@ def test_ascent_angle_controller_uses_fixed_angle_rate_limits():
vEgoRaw=21.66,
steeringAngleDeg=-25.77,
steeringRateDeg=0.0,
steeringTorque=-149.0,
steeringTorque=-250.0,
steeringPressed=False,
gearShifter=structs.CarState.GearShifter.drive,
standstill=False,
@@ -478,7 +582,7 @@ def test_angle_controller_yields_until_manual_steering_settles(platform):
vEgoRaw=21.66,
steeringAngleDeg=-25.06,
steeringRateDeg=35.0,
steeringTorque=-149.0,
steeringTorque=-250.0,
steeringPressed=True,
gearShifter=structs.CarState.GearShifter.drive,
standstill=False,
@@ -487,10 +591,14 @@ def test_angle_controller_yields_until_manual_steering_settles(platform):
msg = controller.lateral_angle(CC, CS)
parser.update([(1, [msg])])
msg = controller.lateral_angle(CC, CS)
parser.update([(2, [msg])])
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Request"] == 0
assert parser.vl["ES_LKAS_ANGLE"]["LKAS_Output"] == pytest.approx(CS.out.steeringAngleDeg)
CS.out.steeringPressed = False
CS.out.steeringTorque = 0.0
CS.out.steeringAngleDeg = -17.91
CS.out.steeringRateDeg = 0.0
for i in range(18):
@@ -88,6 +88,7 @@ class SubaruSafetyFlags(IntFlag):
D_PLATFORM = 32
D_PLATFORM_CAMERA = 64
FIXED_ANGLE_LIMITS = 128
STOP_START_BUTTON = 256
LEGACY_2025_ANGLE_LIMITS = FIXED_ANGLE_LIMITS
@@ -269,6 +270,12 @@ class CAR(Platforms):
)
SUBARU_STOP_START_CARS = (
CAR.SUBARU_OUTBACK_2023,
CAR.SUBARU_LEGACY_2025,
)
SUBARU_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
p16(uds.DATA_IDENTIFIER_TYPE.APPLICATION_DATA_IDENTIFICATION)
SUBARU_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
+6
View File
@@ -14,6 +14,7 @@ from opendbc.car.tesla.values import CAR as TESLA
from opendbc.car.toyota.values import CAR as TOYOTA
from opendbc.car.values import Platform
from opendbc.car.volkswagen.values import CAR as VOLKSWAGEN
from opendbc.car.volvo.values import CAR as VOLVO
from opendbc.car.body.values import CAR as COMMA
from opendbc.car.psa.values import CAR as PSA
@@ -105,6 +106,11 @@ non_tested_cars = [
TOYOTA.TOYOTA_COROLLA,
TOYOTA.TOYOTA_RAV4H,
# No recorded routes yet
VOLVO.VOLVO_XC40_RECHARGE,
VOLVO.VOLVO_S60_RECHARGE,
VOLVO.POLESTAR_2,
]
non_tested_cars.extend(CC_ONLY_CAR)
@@ -300,6 +300,64 @@ class TestCarInterfaces:
)
assert fp_car_params.safetyConfigs[-1].safetyParam & HyundaiStarPilotSafetyFlags.AOL_LKAS_ON_ENGAGE.value
def test_hyundai_elantra_hev_auto_aol_sets_lkas_on_engage_flag(self):
toggles = get_test_starpilot_toggles()
toggles.always_on_lateral_main = True
fingerprint = {bus: {} for bus in range(8)}
car_params = HyundaiCarInterface.get_params(
HYUNDAI_CAR.HYUNDAI_ELANTRA_HEV_2024,
fingerprint,
[],
alpha_long=True,
is_release=False,
docs=False,
starpilot_toggles=toggles,
)
fp_car_params = HyundaiCarInterface.get_starpilot_params(
HYUNDAI_CAR.HYUNDAI_ELANTRA_HEV_2024,
fingerprint,
[],
car_params,
toggles,
)
assert fp_car_params.safetyConfigs[-1].safetyParam & HyundaiStarPilotSafetyFlags.AOL_LKAS_ON_ENGAGE.value
assert fp_car_params.safetyConfigs[-1].safetyParam & HyundaiStarPilotSafetyFlags.AOL_MAIN_LKAS_ON_ENGAGE.value
@pytest.mark.parametrize(
("candidate", "sets_main_aol_flag"),
(
(HYUNDAI_CAR.HYUNDAI_SONATA_HYBRID, True),
(HYUNDAI_CAR.HYUNDAI_SONATA, False),
),
)
def test_hyundai_main_aol_engage_flag_is_scoped_to_hybrid(self, candidate, sets_main_aol_flag):
toggles = get_test_starpilot_toggles()
toggles.always_on_lateral_main = True
fingerprint = {bus: {} for bus in range(8)}
car_params = HyundaiCarInterface.get_params(
candidate,
fingerprint,
[],
alpha_long=True,
is_release=False,
docs=False,
starpilot_toggles=toggles,
)
fp_car_params = HyundaiCarInterface.get_starpilot_params(
candidate,
fingerprint,
[],
car_params,
toggles,
)
has_main_aol_flag = bool(fp_car_params.safetyConfigs[-1].safetyParam &
HyundaiStarPilotSafetyFlags.AOL_MAIN_LKAS_ON_ENGAGE.value)
assert has_main_aol_flag is sets_main_aol_flag
def test_toyota_disable_openpilot_long_sets_stock_long_safety_flag(self):
CarInterface = interfaces[TOYOTA_CAR.TOYOTA_PRIUS_TSS2]
fingerprint = {bus: {} for bus in range(8)}
@@ -283,6 +283,7 @@ class TestFwFingerprintTiming:
'tesla': 0.1,
'toyota': 0.7,
'volkswagen': 0.65,
'volvo': 0.0,
'rivian': 0.3,
'psa': 0.1,
},
@@ -142,6 +142,8 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
"HONDA_NBOX_2G" = [1.2, 1.2, 0.2]
"ACURA_TLX_2G" = [1.2, 1.2, 0.15]
"PORSCHE_MACAN_MK1" = [2.0, 2.0, 0.2]
"VOLVO_XC40_RECHARGE" = [1.5, 1.5, 0.1]
"VOLVO_S60_RECHARGE" = [1.5, 1.5, 0.1]
# Dashcam or fallback configured as ideal car
"MOCK" = [10.0, 10, 0.0]
@@ -136,3 +136,5 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
"CHEVROLET_SILVERADO_CC" = "CHEVROLET_SILVERADO"
"CADILLAC_XT4_CC" = "CADILLAC_XT4"
"CADILLAC_XT6" = "GMC_ACADIA"
"POLESTAR_2" = "VOLVO_XC40_RECHARGE"
@@ -345,8 +345,10 @@ class CarController(CarControllerBase):
apply_torque = apply_meas_steer_torque_limits(new_torque, self.last_torque, CS.out.steeringTorqueEps, self.params)
# >100 degree/sec steering fault prevention
self.steer_rate_counter, apply_steer_req = common_fault_avoidance(abs(CS.out.steeringRateDeg) >= MAX_STEER_RATE, lat_active,
self.steer_rate_counter, MAX_STEER_RATE_FRAMES)
self.steer_rate_counter, apply_steer_req = common_fault_avoidance(
abs(CS.out.steeringRateDeg) >= MAX_STEER_RATE, lat_active,
self.steer_rate_counter, MAX_STEER_RATE_FRAMES,
)
if not lat_active:
apply_torque = 0
+12 -1
View File
@@ -245,6 +245,11 @@ class CarState(CarStateBase):
buttonEvents += create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
if self.CP.carFingerprint in LEGACY_PRIUS_CAR and not self.has_SDSU:
prev_distance_button = self.distance_button
self.distance_button = cp_acc.vl["ACC_CONTROL"]["DISTANCE"]
buttonEvents += create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
if self.CP.carFingerprint in DISTANCE_BUTTON_CAR:
prev_distance_button = self.distance_button
self.distance_button = cp.vl["PCM_CRUISE_4"]["DISTANCE"]
@@ -294,14 +299,20 @@ class CarState(CarStateBase):
pt_messages = [
("BLINKERS_STATE", float('nan')),
]
cam_messages = []
if CP.enableGasInterceptorDEPRECATED:
pt_messages.append(("GAS_SENSOR", 50))
if CP.carFingerprint in LEGACY_PRIUS_CAR:
pt_messages.append(("ACC_CONTROL", float('nan')))
if CP.flags & ToyotaFlags.DSU_BYPASS.value:
cam_messages.append(("ACC_CONTROL", float('nan')))
if CP.carFingerprint in DISTANCE_BUTTON_CAR:
pt_messages.append(("PCM_CRUISE_4", 1))
return {
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, 0),
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 2),
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], cam_messages, 2),
}
@@ -1038,6 +1038,36 @@ class TestToyotaCarController:
class TestToyotaCarState:
@pytest.mark.parametrize("candidate", [CAR.TOYOTA_PRIUS, CAR.TOYOTA_PRIUS_RETROFIT])
def test_legacy_prius_distance_button_generates_events(self, candidate):
params = CarInterface.get_params(
candidate,
{bus: {} for bus in range(8)},
[],
alpha_long=False,
is_release=False,
docs=False,
starpilot_toggles=SimpleNamespace(force_torque_controller=False, nnff=False, nnff_lite=False),
)
starpilot_params = CarInterface.get_starpilot_params(candidate, {bus: {} for bus in range(8)}, [], params, SimpleNamespace())
car_state = CarState(params, starpilot_params)
can_parsers = car_state.get_can_parsers(params)
assert "ACC_CONTROL" in can_parsers[Bus.pt].vl
assert ("ACC_CONTROL" in can_parsers[Bus.cam].vl) == bool(params.flags & ToyotaFlags.DSU_BYPASS.value)
can_parsers[Bus.pt].vl["ACC_CONTROL"]["DISTANCE"] = 1
ret, _ = car_state.update(can_parsers, SimpleNamespace(cluster_offset=1.0))
assert [(event.type, event.pressed) for event in ret.buttonEvents] == [
(structs.CarState.ButtonEvent.Type.gapAdjustCruise, True),
]
can_parsers[Bus.pt].vl["ACC_CONTROL"]["DISTANCE"] = 0
ret, _ = car_state.update(can_parsers, SimpleNamespace(cluster_offset=1.0))
assert [(event.type, event.pressed) for event in ret.buttonEvents] == [
(structs.CarState.ButtonEvent.Type.gapAdjustCruise, False),
]
def test_lkas_button_platforms(self):
assert CAR.TOYOTA_PRIUS in LKAS_BUTTON_CAR
assert TSS2_CAR <= LKAS_BUTTON_CAR
+2 -1
View File
@@ -14,8 +14,9 @@ from opendbc.car.subaru.values import CAR as SUBARU
from opendbc.car.tesla.values import CAR as TESLA
from opendbc.car.toyota.values import CAR as TOYOTA
from opendbc.car.volkswagen.values import CAR as VOLKSWAGEN
from opendbc.car.volvo.values import CAR as VOLVO
Platform = BODY | CHRYSLER | FORD | GM | HONDA | HYUNDAI | MAZDA | MOCK | NISSAN | PSA | RIVIAN | SUBARU | TESLA | TOYOTA | VOLKSWAGEN
Platform = BODY | CHRYSLER | FORD | GM | HONDA | HYUNDAI | MAZDA | MOCK | NISSAN | PSA | RIVIAN | SUBARU | TESLA | TOYOTA | VOLKSWAGEN | VOLVO
BRANDS = get_args(Platform)
PLATFORMS: dict[str, Platform] = {str(platform): platform for brand in BRANDS for platform in brand}
@@ -0,0 +1 @@
# Volvo CMA platform support for openpilot
@@ -0,0 +1,287 @@
import numpy as np
from opendbc.can.packer import CANPacker
from opendbc.car import Bus
from opendbc.car.interfaces import CarControllerBase
from opendbc.car.lateral import apply_std_steer_angle_limits
from opendbc.car.volvo.helpers import LCA3CounterSync
from opendbc.car.volvo.volvocan import (create_lca_message, create_pscm_message, create_lca_3_message, create_lca_2_message, create_lca_4_message,
create_lca_5_message, create_lca_6_message, create_lca_7_message, create_pscm_related_message)
from opendbc.car.volvo.values import CarControllerParams
class CarController(CarControllerBase):
def __init__(self, dbc_names, CP):
super().__init__(dbc_names, CP)
self.packer = CANPacker(dbc_names[Bus.party])
self.apply_angle_last = 0.0 # Track last applied steering angle
self.gear_acc = 60
self.lca_4_acc = 0 # Bresenham accumulator for 29 Hz
# Counter management for LCA_2
self.lca_2_counter_1 = None # Will grab initial value from CarState
self.lca_2_counter_2 = None
# Counter management for PSCM_RELATED
self.pscm_related_counter = None # Will grab initial value from CarState
# Counter management for LCA_3 (pattern-based)
self.lca_3_counter_sync = LCA3CounterSync()
# Counter management for LCA_5 (formerly SPEED_1)
self.lca_5_counter = None # Will grab initial value from CarState
self.last_lat_active = False # Track state
self.lca_7_acc = 0 # Bresenham accumulator for 29 Hz
self.lca_7_last_steer = 0 # used to calculate change in steer from last update
# LCA torque-authority envelope state. Both arms are persistent across frames.
# See CarControllerParams.LCA_AUTH_* and route_analysis/lca_override_mechanism.md.
# When lat_active goes True they ramp up from 0 to ±MAX at REBUILD_RATE; on
# driver override they collapse at COLLAPSE_RATE (symmetric until SPLIT, then
# asymmetric: yielding arm → 0, counter arm holds at ±PLATEAU).
self.lca_auth_pos = 0.0
self.lca_auth_neg = 0.0
# "Light contact" rising-edge detector for haptic-ack on resting hands.
# Per-frame |drv| derivative; LIGHT_HOLD_FRAMES counter ticks down while
# the brief-yield window is active and does not re-arm during that window.
self.lca_auth_drv_prev = 0.0
self.lca_auth_light_frames = 0
# Frames since real_override was last active — used to gate light_collapse
# re-arming during active co-steering (must be > LIGHT_COOLDOWN_FRAMES).
self.lca_auth_real_off_frames = 1000 # large initial → light can fire immediately
# Override-mode latch with hysteresis (enter at ENTER, exit at EXIT).
# Prevents threshold flapping when driver torque hovers near the boundary,
# which caused ~10 Hz EPS-torque ripple felt during lane-change overrides.
self.lca_auth_override_active = False
# LP-filtered |drv| for yield-arm magnitude calculation. Suppresses 1-2
# unit driver-torque jitter that would otherwise propagate (~10x amplified
# via YIELD_SLOPE) into envelope ripple felt at the wheel.
self.lca_auth_drv_mag_filt = 0.0
def update(self, CC, CS, now_nanos, starpilot_toggles):
can_sends = []
actuators = CC.actuators
# Detect disengagement
if not CC.latActive and self.last_lat_active:
#self.lca_commands.reset() # Clear state ← IMPORTANT!
pass
lat_active = CC.latActive
# lateral control - angle-based steering
# NOTE: LCA message is sent every frame (even when inactive) to replace stock LCA
# Stock LCA is permanently blocked by panda safety, so we must always send
if self.frame % CarControllerParams.STEER_STEP == 0: # 100 Hz
# Get desired steering angle from controlsd (LatControlAngle)
apply_angle = actuators.steeringAngleDeg # degrees
# Clamp commanded angle to actual ± ANGLE_ERROR. Without this, a driver override
# lets the model's plan drift far from the wheel's actual position; on release,
# the EPS slams back toward that stale command and overshoots. Stock Volvo Pilot
# Assist keeps this gap inside ~2° even under sustained override.
apply_angle = float(np.clip(
apply_angle,
CS.out.steeringAngleDeg - CarControllerParams.ANGLE_ERROR,
CS.out.steeringAngleDeg + CarControllerParams.ANGLE_ERROR,
))
# Rate limit + inactive passthrough (apply_angle = steering angle when not lat_active)
apply_angle = apply_std_steer_angle_limits(apply_angle, self.apply_angle_last, CS.out.vEgoRaw,
CS.out.steeringAngleDeg, lat_active, CarControllerParams.ANGLE_LIMITS)
# Update LCA torque-authority envelope (replicates stock Pilot Assist's
# easy-override and bounce-free release). Stock holds both arms at ±614
# in steady state; on override the arms collapse to a shifted plateau
# (counter arm deeper than yielding arm); rebuilds at +230 c/s.
P = CarControllerParams
DT = 0.01 # 100 Hz
# Override trigger uses an explicit |steeringTorque| threshold rather than
# CS.steeringPressed, which is a very-sensitive DM-fallback floor (raw>2)
# — fires from resting hands alone and is not an override-intent signal.
drv_mag = abs(CS.out.steeringTorque)
drv_rate = drv_mag - self.lca_auth_drv_prev
self.lca_auth_drv_prev = drv_mag
# LP filter on |drv| used for yield-arm magnitude — absorbs 1-2 unit
# driver-torque jitter that would otherwise propagate into ~10 unit
# envelope ripple via the YIELD_SLOPE multiplier.
self.lca_auth_drv_mag_filt = ((1.0 - P.LCA_AUTH_YIELD_LP_ALPHA) * self.lca_auth_drv_mag_filt
+ P.LCA_AUTH_YIELD_LP_ALPHA * drv_mag)
# Hysteretic override latch — enter at ENTER, hold until drv drops below
# EXIT. Eliminates ~10 Hz envelope flapping when |drv| hovers near a
# single threshold during sustained co-steering.
if not self.lca_auth_override_active and drv_mag > P.LCA_AUTH_OVERRIDE_ENTER:
self.lca_auth_override_active = True
elif self.lca_auth_override_active and drv_mag < P.LCA_AUTH_OVERRIDE_EXIT:
self.lca_auth_override_active = False
real_override = self.lca_auth_override_active
# Track frames since real_override was last active. Used to gate light
# contact re-firing — light_collapse must NOT trigger while the driver
# is actively co-steering (real_override repeatedly entering/exiting).
if real_override:
self.lca_auth_real_off_frames = 0
else:
self.lca_auth_real_off_frames += 1
# Per-frame rising edge into the "light contact" zone arms a brief-yield
# window for haptic acknowledgment of hand-on-wheel. Suppressed while
# the window is already active OR real_override has been off less than
# LIGHT_COOLDOWN_FRAMES (i.e., user is actively co-steering).
if (drv_mag > P.LCA_AUTH_LIGHT_THRESH and
drv_rate > P.LCA_AUTH_LIGHT_RISE_DELTA and
self.lca_auth_light_frames == 0 and
self.lca_auth_real_off_frames > P.LCA_AUTH_LIGHT_COOLDOWN_FRAMES):
self.lca_auth_light_frames = P.LCA_AUTH_LIGHT_HOLD_FRAMES
else:
self.lca_auth_light_frames = max(0, self.lca_auth_light_frames - 1)
light_collapse = self.lca_auth_light_frames > 0
overriding = real_override or light_collapse
# Collapse rate scales with driver torque so a sharp pothole jolt drops the
# envelope faster than a soft sustained press. Floor at base rate so light
# contact still produces a perceptible (but small) dip.
collapse_rate = P.LCA_AUTH_COLLAPSE_RATE * max(1.0, drv_mag / float(P.LCA_AUTH_OVERRIDE_ENTER))
step = collapse_rate * DT
if not lat_active:
self.lca_auth_pos = 0.0
self.lca_auth_neg = 0.0
self.lca_auth_drv_prev = 0.0
self.lca_auth_light_frames = 0
self.lca_auth_override_active = False
self.lca_auth_drv_mag_filt = 0.0
self.lca_auth_real_off_frames = 1000
elif overriding:
if self.lca_auth_pos > P.LCA_AUTH_SPLIT or -self.lca_auth_neg > P.LCA_AUTH_SPLIT:
# Symmetric collapse phase: both arms shrink toward ±SPLIT
self.lca_auth_pos = max(float(P.LCA_AUTH_SPLIT), self.lca_auth_pos - step)
self.lca_auth_neg = min(-float(P.LCA_AUTH_SPLIT), self.lca_auth_neg + step)
else:
# Asymmetric plateau phase. CS.out.steeringTorque > 0 in openpilot
# convention = driver pushing right → yields right authority
# (LOOSELY/+ arm), retains left (INV/- arm).
# Yield arm scales with |drv| above OVERRIDE_THRESH — strong presses
# (potholes, hard corrections) cross past zero so EPS hands the wheel
# to the driver in their direction.
excess = max(0.0, self.lca_auth_drv_mag_filt - float(P.LCA_AUTH_OVERRIDE_ENTER))
yield_signed = float(P.LCA_AUTH_YIELD_BASE) - P.LCA_AUTH_YIELD_SLOPE * excess
yield_signed = max(float(P.LCA_AUTH_YIELD_MIN), min(yield_signed, float(P.LCA_AUTH_YIELD_BASE)))
if CS.out.steeringTorque > 0: # driver pushing right
target_pos = yield_signed # yield arm (+ side)
target_neg = -float(P.LCA_AUTH_PLATEAU_COUNTER) # counter arm
else: # driver pushing left (or zero — default to symmetric collapse direction)
target_pos = float(P.LCA_AUTH_PLATEAU_COUNTER) # counter arm
target_neg = -yield_signed # yield arm ( side)
# Drive each arm toward its plateau target at COLLAPSE_RATE
self.lca_auth_pos = max(target_pos, self.lca_auth_pos - step) \
if self.lca_auth_pos > target_pos \
else min(target_pos, self.lca_auth_pos + step)
self.lca_auth_neg = min(target_neg, self.lca_auth_neg + step) \
if self.lca_auth_neg < target_neg \
else max(target_neg, self.lca_auth_neg - step)
else:
# No override → rebuild both arms toward saturation
rebuild_step = P.LCA_AUTH_REBUILD_RATE * DT
self.lca_auth_pos = min(float(P.LCA_AUTH_MAX), self.lca_auth_pos + rebuild_step)
self.lca_auth_neg = max(-float(P.LCA_AUTH_MAX), self.lca_auth_neg - rebuild_step)
# LCA - 0x58 - 100 Hz (angle-based)
can_sends.append(create_lca_message(self.packer, lat_active, apply_angle, CS.msg_lca,
authority_pos=int(round(self.lca_auth_pos)),
authority_neg=int(round(self.lca_auth_neg))))
self.apply_angle_last = apply_angle
# PSCM (bus 2 -> 0) - 0x16 - 100 Hz
can_sends.append(create_pscm_message(self.packer, lat_active, CS.msg_pscm, self.frame))
# EGSM - 0x45 - 100 Hz
#can_sends.append(create_egsm_message(self.packer, CS.msg_egsm))
# PSCM_RELATED (bus 2 -> 0) - 0x17 - 100 Hz
# Initialize counter from CarState on first run
if self.pscm_related_counter is None:
self.pscm_related_counter = CS.msg_pscm_related['SIG1_BYTE_1_HI_NIBBLE']
# Increment counter by +1, wrap from 14 → 0 (modulo 15)
self.pscm_related_counter = (self.pscm_related_counter + 1) % 15
can_sends.append(create_pscm_related_message(self.packer, lat_active, CS.pilot_assist_engaged,
CS.msg_pscm_related, self.pscm_related_counter))
# LCA_3 - 0x57 - avg 66.66 Hz
#if (self.frame * 67) % 100 < 67: # if (self.frame % 3) < 2:
# 0x57 at ~66.67 Hz: send on 2 out of every 3 frames
# Pattern: send on frame % 3 == 0 or 2, skip when frame % 3 == 1
if self.frame % 3 != 1: # → 2/3 * 100 Hz = 66.67 Hz
# Update counter with observed value, get counter to send
counter, is_synced = self.lca_3_counter_sync.update(CS.msg_lca_3['COUNTER_1'])
can_sends.append(create_lca_3_message(self.packer, lat_active, apply_angle, CS.msg_lca_3, counter))
#can_sends.append(create_0x1a_message(self.packer, CS.msg_0x1a))
# SPEED messages - 0x60, 0x68 - 50 Hz
if self.frame % 2 == 0: # 50 Hz
#can_sends.append(create_speed_message(self.packer, CS.msg_speed))
#can_sends.append(create_speed_2_message(self.packer, CS.msg_speed_2))
pass
# LCA_2 - 0x69 - 50 Hz
# Spoof PILOT_ASSIST_ENGAGED to keep PSCM accepting LCA commands
if self.frame % 2 == 0: # 50 Hz
# Initialize counters from CarState on first run
if self.lca_2_counter_1 is None:
self.lca_2_counter_1 = CS.msg_lca_2['COUNTER_1']
self.lca_2_counter_2 = CS.msg_lca_2['COUNTER_2']
# Increment counters (COUNTER_1 by +2, COUNTER_2 by +4, both modulo 16)
self.lca_2_counter_1 = (self.lca_2_counter_1 + 2) % 16
self.lca_2_counter_2 = (self.lca_2_counter_2 + 4) % 16
can_sends.append(create_lca_2_message(self.packer, lat_active, CS.msg_lca_2,
self.lca_2_counter_1, self.lca_2_counter_2))
# LCA_5 (formerly SPEED_1) - 0x67 - 50 Hz
# Contains wheel speeds + LCA signals (LCA_TURN_BITS, LCA_5_STEER)
if self.frame % 2 == 0: # 50 Hz
# Initialize counter from CarState on first run
if self.lca_5_counter is None:
self.lca_5_counter = CS.msg_lca_5['COUNTER']
# Increment counter by +4, wrap at 15 (0xF never used)
self.lca_5_counter = (self.lca_5_counter + 4) % 15
can_sends.append(create_lca_5_message(self.packer, lat_active, apply_angle,
CS.msg_lca_5, self.lca_5_counter))
# LCA_4 - 0x90 - 29 Hz
# Spoof LCA_ENABLE bits to maintain PA ON state when openpilot is active
# Using Bresenham-style accumulator for precise 29 Hz
self.lca_4_acc += 29
if self.lca_4_acc >= 100:
self.lca_4_acc -= 100
can_sends.append(create_lca_4_message(self.packer, lat_active, CS.msg_lca_4, apply_angle))
# LCA_6 - 0X97 - 25 Hz
if self.frame % 4 == 0: # 25 Hz
can_sends.append(create_lca_6_message(self.packer, lat_active, CS.msg_lca_6, apply_angle))
# LCA_7 - 0x92 - 29 Hz
# Using Bresenham-style accumulator for precise 29 Hz
self.lca_7_acc += 29
if self.lca_7_acc >= 100:
self.lca_7_acc -= 100
delta_steer = apply_angle - self.lca_7_last_steer
can_sends.append(create_lca_7_message(self.packer, lat_active, CS.msg_lca_7, apply_angle, delta_steer))
self.lca_7_last_steer = apply_angle
# GEAR_POSITION - 0x80 - 40 Hz
#self.gear_acc += 40 # Bresenham-style approach
#if self.gear_acc >= 100:
# self.gear_acc -= 100
if self.frame % 5 == 0 or self.frame % 5 == 2: # 2/5 * 100 Hz = 40 Hz # openpilot forward delay causes DTC in EGSM, but fixes DTC in PSCM
#can_sends.append(create_gear_position_message(self.packer, CS.msg_gear_position))
pass
new_actuators = actuators.as_builder()
new_actuators.steeringAngleDeg = self.apply_angle_last
self.frame += 1
self.last_lat_active = CC.latActive
return new_actuators, can_sends
+145
View File
@@ -0,0 +1,145 @@
from cereal import custom
from opendbc.car import structs, Bus
from opendbc.can.parser import CANParser
from opendbc.car.volvo.values import DBC, VolvoSPAPlatformConfig, CAR
from opendbc.car.interfaces import CarStateBase
GearShifter = structs.CarState.GearShifter
TransmissionType = structs.CarParams.TransmissionType
# main-bus SPEED (0x60) is raw counts in the DBC; measured against GPS ground speed.
# Must match VOLVO_SPEED_TO_MS in opendbc/safety/modes/volvo.h.
SPEED_TO_MS = 0.003977
class CarState(CarStateBase):
def __init__(self, CP, FPCP):
super().__init__(CP, FPCP)
self.is_spa = isinstance(CAR(CP.carFingerprint).config, VolvoSPAPlatformConfig)
self.gas_pressed_prev = False
self.dispatch_lca_2_msg = False
self.msg_pscm = {}
self.msg_lca = {}
self.msg_lca_2 = {}
self.msg_lca_3 = {}
self.msg_gear_position = {}
self.pilot_assist_engaged = False
self.msg_lca_5 = {} # Formerly msg_speed_1
self.msg_speed = {}
self.msg_speed_2 = {}
self.msg_0x1a = {}
self.msg_egsm = {}
self.msg_pscm_related = {}
self.msg_lca_4 = {}
self.msg_lca_6 = {}
self.msg_lca_7 = {}
def update(self, can_parsers, starpilot_toggles) -> structs.CarState:
cp_main = can_parsers[Bus.main]
cp_pt = can_parsers[Bus.pt]
cp_party = can_parsers[Bus.party]
ret = structs.CarState()
# car speed
# SPEED on the main bus, not BUS1_SPEED on the PT bus: the main bus is identical
# across harnesses, while which car bus lands on PT (bus 1) is not, and the PT DBC
# in use depends on the fingerprint. Regressed against GPS ground speed over two
# routes on different harnesses: r=0.99989 both, residual sd 0.35-0.40 km/h.
ret.vEgoRaw = cp_main.vl["SPEED"]["SPEED"] * SPEED_TO_MS
ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
ret.standstill = ret.vEgoRaw <= 0.1 # 0.1 m/s
# gas
# CMA ECM_1.ACCELERATOR_PEDAL_POS is raw 0-255 (DBC factor 1, idle ~20).
# SPA ECM_1.ACCELERATOR_PEDAL_POS is DBC-scaled to percent (factor 0.00390625, idle ~0).
# Thresholds must match volvo.h (see opendbc/safety/modes/volvo.h GAS_PRESSED_THRESHOLD_*)
# and opendbc/safety/tests/test_volvo.py::test_gas_threshold_self_consistent.
if self.is_spa:
ret.gasPressed = cp_pt.vl["ECM_1"]["ACCELERATOR_PEDAL_POS"] > 1.0 # percent
else:
ret.gasPressed = cp_pt.vl["ECM_1"]["ACCELERATOR_PEDAL_POS"] > 20+1 # raw counts, 20 baseline + 1 tolerance
# brake
#ret.brakePressed = bool(cp_main.vl["LCA_2"]["BRAKE_PEDAL_PRESSED_A"] or cp_main.vl["LCA_2"]["BRAKE_PEDAL_PRESSED_B"])
# BRAKE_PEDAL_PRESSED_A goes active when user starts pressing brake pedal, but no brake light is on yet due to tolerance
# BRAKE_PEDAL_PRESSED_B goes active when when the brake pedal is pressed above minimum threshold, brake light is on
ret.brakePressed = cp_main.vl["LCA_2"]["BRAKE_PEDAL_PRESSED_B"] == 1
ret.parkingBrake = False # TODO: add parking brake
# stability control - becomes true when ESC intervenes (e.g., aquaplaning)
ret.espActive = cp_main.vl["LCA_2"]["ESC_ACTUATING"] == 1 and cp_main.vl["LCA_2"]["ESC_ELIGIBLE"] == 1
# steering wheel
ret.steeringAngleDeg = cp_party.vl['PSCM']['PSCM_ANGLE_SENSOR'] # openpilot expects a negative value for a right turn
#ret.steeringAngleDeg = cp_party.vl['SAS']['SAS_ANGLE_SENSOR']
# Driver steering torque feedback (used for driver override detection)
ret.steeringTorque = -cp_party.vl['DRIVER_INPUT']['STEERING_DRIVER_INPUT'] # Car right turn is negative, openpilot right turn is positive
ret.steeringPressed = abs(cp_party.vl['DRIVER_INPUT']['STEERING_DRIVER_INPUT']) > 2
# EPS status - placeholder until actual signal is found
self.eps_active = True # Assume EPS is active for now
if self.is_spa:
# SPA: byte 0 bit 1, inverted (0 = cruise on, 1 = cruise off)
cruise_raw = cp_pt.vl["BUS1_CRUISE_CONTROL"]["CRUISE_CONTROL_SPA_ENABLED"] == 1
else:
# CMA: two separate boolean signals
cruise_raw = cp_pt.vl["BUS1_CRUISE_CONTROL"]["CRUISE_CONTROL_ENABLED"] == 1 or cp_pt.vl["BUS1_CRUISE_CONTROL"]["CRUISE_CONTROL_ENABLED_IDLE_TRAFFIC"] == 1
ret.cruiseState.enabled = cruise_raw
self.gas_pressed_prev = ret.gasPressed
ret.cruiseState.available = True # TODO: Determine actual availability
ret.cruiseState.speed = 0 # TODO: Find cruise set speed (not required for lateral control)
ret.cruiseState.nonAdaptive = False
ret.cruiseState.standstill = ret.standstill # False # Todo: Find cruise control standstill signal
# gear
gearPosition = cp_main.vl['GEAR_POSITION']['GEAR_POSITION'] # 0: P; 1: R; 2: N; 3: D; 4: B;
if gearPosition == 0:
ret.gearShifter = GearShifter.park
elif gearPosition == 1:
ret.gearShifter = GearShifter.reverse
elif gearPosition == 2:
ret.gearShifter = GearShifter.neutral
elif gearPosition == 3:
ret.gearShifter = GearShifter.drive
elif gearPosition == 4:
ret.gearShifter = GearShifter.drive
# blinkers TODO FlexRay
ret.leftBlinker = False
ret.rightBlinker = False
# lock info TODO FlexRay
ret.doorOpen = False # TODO: add door open
ret.seatbeltUnlatched = False # TODO: add seatbelt unlatched
# Store entire message dictionaries
self.msg_pscm = cp_party.vl['PSCM']
self.msg_lca = cp_main.vl['LCA']
self.msg_lca_2 = cp_main.vl['LCA_2']
self.msg_lca_3 = cp_main.vl['LCA_3']
self.msg_lca_4 = cp_main.vl['LCA_4']
self.msg_lca_5 = cp_main.vl['LCA_5']
self.msg_lca_6 = cp_main.vl['LCA_6']
self.msg_lca_7 = cp_main.vl['LCA_7']
self.msg_speed = cp_main.vl['SPEED']
self.msg_speed_2 = cp_main.vl['SPEED_2']
self.msg_gear_position = cp_main.vl['GEAR_POSITION']
self.msg_egsm = cp_party.vl['EGSM']
self.msg_pscm_related = cp_party.vl['PSCM_RELATED']
self.pilot_assist_engaged = cp_main.vl['LCA_2']['PILOT_ASSIST_ENGAGED'] == 1
fp_ret = custom.StarPilotCarState.new_message()
return ret, fp_ret
@staticmethod
def get_can_parsers(CP):
return {
Bus.main: CANParser(DBC[CP.carFingerprint][Bus.main], [], 0),
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 1),
Bus.party: CANParser(DBC[CP.carFingerprint][Bus.party], [], 2),
}
@@ -0,0 +1,18 @@
# ruff: noqa: E501
""" AUTO-FORMATTED USING opendbc/car/debug/format_fingerprints.py, EDIT STRUCTURE THERE."""
from opendbc.car.volvo.values import CAR
FINGERPRINTS = {
CAR.VOLVO_XC40_RECHARGE: [{
21: 8, 22: 8, 23: 8, 26: 8, 69: 8, 85: 8, 87: 8, 88: 8, 96: 8, 103: 8, 104: 8, 105: 8, 128: 8, 144: 8, 146: 8, 147: 8, 151: 8, 304: 8, 336: 8, 339: 8, 341: 8, 394: 8, 395: 8, 640: 8, 656: 8, 666: 8, 773: 8, 778: 8, 1072: 8, 1120: 8, 1302: 8, 1336: 8, 1407: 8, 1422: 8, 1423: 8, 1424: 8, 1425: 8, 1426: 8
}],
CAR.VOLVO_S60_RECHARGE: [{
21: 8, 22: 8, 23: 8, 26: 8, 69: 8, 85: 8, 87: 8, 88: 8, 96: 8, 103: 8, 104: 8, 105: 8, 128: 8, 144: 8, 146: 8, 147: 8, 151: 8, 336: 8, 339: 8, 341: 8, 395: 8, 587: 8, 640: 8, 656: 8, 666: 8, 773: 8, 778: 8, 1072: 8, 1120: 8, 1298: 8, 1302: 8, 1319: 8, 1336: 8, 1407: 8, 1422: 8, 1423: 8, 1424: 8, 1425: 8, 1426: 8, 1554: 8, 1587: 8, 1718: 8, 1792: 8, 1793: 8, 1798: 8, 1799: 8, 1810: 8, 1813: 8, 1824: 8, 1825: 8, 1840: 8, 1842: 8, 1843: 8, 1856: 8, 1858: 8, 1859: 8, 1860: 8, 1862: 8, 1863: 8, 1871: 8, 1872: 8, 1875: 8, 1879: 8, 1882: 8, 1888: 8, 1889: 8, 1892: 8, 1920: 8, 1927: 8, 1937: 8, 1943: 8, 1953: 8, 1954: 8, 1955: 8, 1968: 8, 1969: 8, 1971: 8, 1975: 8, 1988: 8, 1990: 8, 2000: 8, 2001: 8, 2002: 8, 2004: 8, 2017: 8, 2018: 8, 2020: 8
}],
CAR.POLESTAR_2: [{
7: 4, 21: 8, 22: 8, 23: 8, 26: 8, 35: 8, 37: 8, 53: 8, 58: 8, 67: 8, 69: 8, 70: 8, 85: 8, 87: 8, 88: 8, 96: 8, 103: 8, 104: 8, 105: 8, 112: 8, 117: 8, 128: 8, 133: 8, 138: 8, 144: 8, 146: 8, 147: 8, 151: 8, 256: 8, 277: 8, 278: 8, 284: 8, 293: 8, 309: 8, 320: 8, 325: 8, 336: 8, 339: 8, 341: 8, 348: 8, 349: 8, 352: 8, 368: 8, 370: 8, 373: 8, 375: 8, 376: 8, 389: 8, 395: 8, 400: 8, 408: 8, 411: 8, 417: 8, 420: 8, 426: 8, 429: 8, 435: 8, 440: 8, 464: 8, 556: 8, 640: 8, 656: 8, 666: 8, 773: 8, 778: 8, 789: 8, 791: 8, 800: 8, 805: 8, 807: 8, 816: 8, 821: 8, 832: 8, 837: 8, 841: 8, 848: 8, 853: 8, 854: 8, 858: 8, 860: 8, 873: 8, 882: 8, 889: 8, 890: 8, 891: 8, 892: 8, 893: 8, 896: 8, 899: 8, 901: 8, 917: 8, 919: 8, 1043: 8, 1045: 8, 1061: 8, 1072: 8, 1077: 8, 1088: 8, 1093: 8, 1120: 8, 1127: 8, 1160: 8, 1168: 8, 1171: 8, 1174: 8, 1175: 8, 1177: 8, 1296: 8, 1302: 8, 1320: 8, 1334: 8, 1336: 8, 1407: 8, 1422: 8, 1423: 8, 1424: 8, 1425: 8, 1426: 8, 1428: 8, 1429: 8, 1430: 8, 1431: 8, 1432: 8, 1554: 8, 1584: 8, 1587: 8, 2022: 8
}],
}
FW_VERSIONS: dict[str, dict[tuple, list[bytes]]] = {
}
+397
View File
@@ -0,0 +1,397 @@
def checksum_lca_2_message(b0: int, b5: int) -> int:
"""
Compute checksum for VCU1 CAN ID 0x69 from bytes b0 and b5.
b0: first data byte (MSB) of the frame (usually 0x18 in your logs)
b5: sixth data byte of the frame (what you called Byte5)
Returns: checksum byte (0..255) that goes into byte index 6.
"""
if b0 == 0 and b5 == 128: # Hotfix openpilot test (don't know where this alleged test message comes from)
return 0
# Masks per checksum bit (bit 0..7) for b0 and b5
M0 = [0x08, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00]
M5 = [0x83, 0x86, 0xCF, 0xCD, 0x09, 0x02, 0x44, 0x89]
def parity8(x: int) -> int:
# 1 if x has an odd number of bits set, else 0
x ^= x >> 4
x ^= x >> 2
x ^= x >> 1
return x & 1
b0 &= 0xFF
b5 &= 0xFF
c = 0
for bit in range(8):
p = 0
if M0[bit]:
p ^= parity8(b0 & M0[bit])
if M5[bit]:
p ^= parity8(b5 & M5[bit])
c |= (p << bit)
return c & 0xFF
def checksum_2_0x69_message(b0: int, b1: int, b3: int = 0, b4: int = 0) -> int:
"""
Compute checksum byte (b2) for CAN ID 0x69 (LCA_2 message).
The checksum depends on bytes 0, 1, 3, and 4. During normal driving (BYTE_1_MSBS_3=0),
bytes 3-4 (NEW_SIGNAL_2) are always 0, so only b0 and b1 matter. During stability
control events (aquaplaning, etc.), BYTE_1_MSBS_3 becomes non-zero and bytes 3-4
contain non-zero values that affect the checksum.
Args:
b0: Byte 0 (usually 0x18)
b1: Byte 1 ([7:5] BYTE_1_MSBS_3 | [4] PILOT_ASSIST_ENGAGED | [3:0] COUNTER_1)
b3: Byte 3 (NEW_SIGNAL_2 high byte, default 0)
b4: Byte 4 (NEW_SIGNAL_2 low byte, default 0)
Returns:
Checksum byte (0-255) for position 2
"""
b0 &= 0xFF
b1 &= 0xFF
b3 &= 0xFF
b4 &= 0xFF
def bit(byte, pos):
return (byte >> pos) & 1
c = 0
# Bit 0
c |= (bit(b0, 0) ^ bit(b1, 2) ^ bit(b1, 3) ^ bit(b1, 4) ^ bit(b1, 5) ^ bit(b4, 0) ^ bit(b4, 1)) << 0
# Bit 1
c |= (bit(b0, 1) ^ bit(b0, 3) ^ bit(b1, 0) ^ bit(b1, 3) ^ bit(b1, 5) ^ bit(b1, 6) ^
bit(b3, 1) ^ bit(b4, 0) ^ bit(b4, 1) ^ bit(b4, 2)) << 1
# Bit 2
c |= (bit(b0, 0) ^ bit(b0, 3) ^ bit(b1, 1) ^ bit(b1, 2) ^ bit(b1, 3) ^ bit(b1, 4) ^ bit(b1, 5) ^ bit(b1, 6) ^
bit(b3, 0) ^ bit(b3, 1) ^ bit(b4, 0) ^ bit(b4, 2) ^ bit(b4, 3)) << 2
# Bit 3
c |= (bit(b0, 0) ^ bit(b0, 1) ^ bit(b1, 0) ^ bit(b1, 4) ^ bit(b1, 6) ^
bit(b3, 0) ^ bit(b3, 1) ^ bit(b4, 0) ^ bit(b4, 3) ^ bit(b4, 4)) << 3
# Bit 4
c |= (bit(b0, 0) ^ bit(b0, 1) ^ bit(b0, 3) ^ bit(b1, 1) ^ bit(b1, 2) ^ bit(b1, 3) ^ bit(b1, 4) ^
bit(b4, 4) ^ bit(b4, 5)) << 4
# Bit 5
c |= (bit(b0, 1) ^ bit(b1, 0) ^ bit(b1, 2) ^ bit(b1, 3) ^ bit(b1, 5) ^
bit(b3, 1) ^ bit(b4, 5) ^ bit(b4, 6)) << 5
# Bit 6
c |= (bit(b0, 3) ^ bit(b1, 0) ^ bit(b1, 1) ^ bit(b1, 3) ^ bit(b1, 6) ^
bit(b3, 0) ^ bit(b4, 6) ^ bit(b4, 7)) << 6
# Bit 7
c |= (bit(b0, 3) ^ bit(b1, 1) ^ bit(b1, 2) ^ bit(b1, 4) ^ bit(b4, 0) ^ bit(b4, 7)) << 7
return c & 0xFF
def checksum_1_pscm_related_message(b1, b2):
"""
Computes checksum #1 (goes in byte[0]) for PSCM-related 0x17 message.
Depends only on (byte[1], byte[2]).
b1 = SIG1 counter (high nibble) | LCA_ENABLED_ECHO (low nibble)
b2 = 0x80 | SIG1 counter replica (low nibble)
Linear over GF(2), same shape as checksum_lca_2_message: each output bit is
the parity of a fixed mask over b1 and b2. Solved from 137,965 logged PSCM
frames (60 distinct (b1,b2) keys, leave-one-out cross-validated 60/60).
This replaces a 45-entry lookup table that covered only LCA_ENABLED_ECHO in
{0, 1, 4} and returned 0 on a miss. During an ESC intervention the rack
reports ECHO=6, so openpilot transmitted 128 consecutive frames with an
invalid checksum (0x00) before this fix.
Note: bit 3 of b1 (LCA_ENABLED_ECHO >= 8) has never been observed on the bus,
so its contribution is unconstrained by the data and is taken to be zero.
"""
# Masks per checksum bit (bit 0..7) for b1 and b2
M1 = [0x41, 0x82, 0x55, 0xF3, 0xA7, 0x46, 0x94, 0x20]
M2 = [0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x80]
def parity8(x: int) -> int:
# 1 if x has an odd number of bits set, else 0
x ^= x >> 4
x ^= x >> 2
x ^= x >> 1
return x & 1
b1 &= 0xFF
b2 &= 0xFF
c = 0
for bit in range(8):
p = 0
if M1[bit]:
p ^= parity8(b1 & M1[bit])
if M2[bit]:
p ^= parity8(b2 & M2[bit])
c |= (p << bit)
return c & 0xFF
def checksum_2_pscm_related_message(b2):
"""
Computes checksum #2 (goes in byte[3]) for PSCM-related 0x17 message.
Depends only on byte[2].
"""
lut = {
0x80: 0xBF,
0x81: 0xF3,
0x82: 0x27,
0x83: 0x6B,
0x84: 0x92,
0x85: 0xDE,
0x86: 0x0A,
0x87: 0x46,
0x88: 0xE5,
0x89: 0xA9,
0x8A: 0x7D,
0x8B: 0x31,
0x8C: 0xC8,
0x8D: 0x84,
0x8E: 0x50,
}
return lut.get(b2, 0)
def checksum_lca_4_message(*args) -> int:
"""
Placeholder for LCA_4 (0x90) checksum calculation.
TODO: Implementation will be provided after checksum analysis is complete.
For now, returns 0 as a placeholder.
Args:
*args: Byte values needed for checksum calculation (TBD)
Returns:
Checksum byte (0-255)
"""
# Placeholder - will be replaced with actual checksum algorithm
return 0
class LCA3CounterSync:
"""
Best-effort pattern synchronization for LCA_3 COUNTER_1.
The counter follows a 20-element pattern that cycles based on transmission count.
We track recent observed counter values and match them against the pattern to
determine the current index. While not synchronized, we pass through stock values.
Once synchronized, we permanently use the pattern.
"""
PATTERN = [2, 2, 1, 2, 2, 2, 1, 2, 2, 3, 0, 2, 3, 2, 0, 2, 3, 2, 0, 3]
PATTERN_LEN = 20
WINDOW_SIZE = 5 # Track last 5 values for matching
MIN_CONFIDENCE = 4 # Need 4 consecutive matches to sync
def __init__(self):
self.pattern_index = None # Current index in pattern (None = not synced)
self.observed_window = [] # Circular buffer of last N observed values
self.confidence = 0 # Number of consecutive successful matches
def update(self, observed_counter: int) -> tuple:
"""
Update with newly observed counter value from stock message.
Args:
observed_counter: Counter value from CS.msg_lca_3['COUNTER_1']
Returns:
Tuple of (counter_to_send, is_synchronized)
"""
# If already synchronized, ignore stock and use our pattern permanently
if self.pattern_index is not None:
counter_to_send = self.PATTERN[self.pattern_index]
self.pattern_index = (self.pattern_index + 1) % self.PATTERN_LEN
return counter_to_send, True
# Not synchronized yet - try to find pattern index
self.observed_window.append(observed_counter)
if len(self.observed_window) > self.WINDOW_SIZE:
self.observed_window.pop(0)
# Attempt to sync if we have enough samples
if len(self.observed_window) >= 3:
self._attempt_sync()
# While not synced, pass through stock counter
return observed_counter, False
def _attempt_sync(self):
"""Try to find current pattern index based on observed window."""
# Try to match observation window against all positions in pattern
best_match_idx = None
best_match_len = 0
for start_idx in range(self.PATTERN_LEN):
match_len = self._count_match(start_idx)
if match_len > best_match_len:
best_match_len = match_len
best_match_idx = start_idx
# Require MIN_CONFIDENCE matching values to declare sync
if best_match_len >= self.MIN_CONFIDENCE:
# The match tells us where we WERE in the pattern
# We need to set index to NEXT position for next transmission
self.pattern_index = (best_match_idx + len(self.observed_window)) % self.PATTERN_LEN
self.confidence = best_match_len
def _count_match(self, pattern_start_idx: int) -> int:
"""
Count how many values in observed_window match pattern starting at pattern_start_idx.
Returns:
Number of consecutive matching values from start
"""
match_count = 0
for i, observed in enumerate(self.observed_window):
pattern_idx = (pattern_start_idx + i) % self.PATTERN_LEN
if observed == self.PATTERN[pattern_idx]:
match_count += 1
else:
break # Stop at first mismatch
return match_count
def is_synchronized(self) -> bool:
"""Returns True if we have synchronized to the pattern."""
return self.pattern_index is not None
def checksum_lca_5_message(byte0: int, byte1: int, byte3: int, byte4: int, byte5: int) -> int:
"""
Calculate checksum for LCA_5 message 0x67 (byte 2)
Args:
byte0: Byte 0 (0-255)
byte1: Byte 1 (0-255)
byte3: Byte 3 (0-255)
byte4: Byte 4 (0-255)
byte5: Byte 5 (0-255)
Returns:
int: Checksum value (0-255) for byte 2
Example:
>>> checksum = checksum_lca_5_message(0x80, 0x00, 0x4F, 0x00, 0x00)
>>> print(f"0x{checksum:02X}")
0x32
"""
# Helper function to extract a bit (LSB = bit 0)
def bit(byte_val, pos):
return (byte_val >> pos) & 1
checksum = 0
# Bit 0: XOR of 13 bits
checksum |= (
bit(byte0, 2) ^ bit(byte0, 3) ^ bit(byte0, 5) ^ bit(byte0, 7) ^
bit(byte1, 2) ^
bit(byte3, 4) ^ bit(byte3, 6) ^
bit(byte4, 0) ^ bit(byte4, 1) ^ bit(byte4, 5) ^ bit(byte4, 6) ^
bit(byte5, 3) ^ bit(byte5, 6)
) << 0
# Bit 1: XOR of 12 bits
checksum |= (
bit(byte0, 0) ^ bit(byte0, 3) ^ bit(byte0, 4) ^ bit(byte0, 7) ^
bit(byte1, 0) ^ bit(byte1, 3) ^
bit(byte3, 5) ^ bit(byte3, 7) ^
bit(byte4, 2) ^ bit(byte4, 6) ^
bit(byte5, 4) ^ bit(byte5, 7)
) << 1
# Bit 2: XOR of 17 bits
checksum |= (
bit(byte0, 1) ^ bit(byte0, 2) ^ bit(byte0, 3) ^ bit(byte0, 4) ^
bit(byte1, 0) ^ bit(byte1, 1) ^ bit(byte1, 2) ^ bit(byte1, 4) ^
bit(byte3, 4) ^
bit(byte4, 1) ^ bit(byte4, 3) ^ bit(byte4, 5) ^ bit(byte4, 6) ^
bit(byte5, 1) ^ bit(byte5, 3) ^ bit(byte5, 5) ^ bit(byte5, 6)
) << 2
# Bit 3: XOR of 19 bits
checksum |= (
bit(byte0, 0) ^ bit(byte0, 4) ^ bit(byte0, 7) ^
bit(byte1, 1) ^ bit(byte1, 3) ^ bit(byte1, 5) ^
bit(byte3, 4) ^ bit(byte3, 5) ^ bit(byte3, 6) ^
bit(byte4, 0) ^ bit(byte4, 1) ^ bit(byte4, 2) ^ bit(byte4, 4) ^ bit(byte4, 5) ^
bit(byte5, 1) ^ bit(byte5, 2) ^ bit(byte5, 3) ^ bit(byte5, 4) ^ bit(byte5, 7)
) << 3
# Bit 4: XOR of 16 bits
checksum |= (
bit(byte0, 1) ^ bit(byte0, 2) ^ bit(byte0, 3) ^ bit(byte0, 7) ^
bit(byte1, 4) ^ bit(byte1, 6) ^
bit(byte3, 4) ^ bit(byte3, 5) ^ bit(byte3, 7) ^
bit(byte4, 1) ^ bit(byte4, 2) ^ bit(byte4, 3) ^
bit(byte5, 2) ^ bit(byte5, 4) ^ bit(byte5, 5) ^ bit(byte5, 6)
) << 4
# Bit 5: XOR of 15 bits
checksum |= (
bit(byte0, 0) ^ bit(byte0, 2) ^ bit(byte0, 3) ^ bit(byte0, 4) ^
bit(byte1, 5) ^ bit(byte1, 7) ^
bit(byte3, 5) ^ bit(byte3, 6) ^
bit(byte4, 2) ^ bit(byte4, 3) ^ bit(byte4, 4) ^
bit(byte5, 3) ^ bit(byte5, 5) ^ bit(byte5, 6) ^ bit(byte5, 7)
) << 5
# Bit 6: XOR of 19 bits
checksum |= (
bit(byte0, 0) ^ bit(byte0, 1) ^ bit(byte0, 3) ^ bit(byte0, 4) ^ bit(byte0, 5) ^ bit(byte0, 7) ^
bit(byte1, 0) ^ bit(byte1, 6) ^
bit(byte3, 4) ^ bit(byte3, 6) ^ bit(byte3, 7) ^
bit(byte4, 0) ^ bit(byte4, 3) ^ bit(byte4, 4) ^ bit(byte4, 5) ^
bit(byte5, 1) ^ bit(byte5, 4) ^ bit(byte5, 6) ^ bit(byte5, 7)
) << 6
# Bit 7: XOR of 15 bits
checksum |= (
bit(byte0, 1) ^ bit(byte0, 2) ^ bit(byte0, 4) ^ bit(byte0, 5) ^
bit(byte1, 1) ^ bit(byte1, 7) ^
bit(byte3, 5) ^ bit(byte3, 7) ^
bit(byte4, 0) ^ bit(byte4, 4) ^ bit(byte4, 5) ^ bit(byte4, 6) ^
bit(byte5, 2) ^ bit(byte5, 5) ^ bit(byte5, 7)
) << 7
return checksum
# Test examples
if __name__ == "__main__":
print("CAN 0x67 Checksum Calculator")
print("=" * 60)
# Test cases
tests = [
([0x80, 0x00, 0x4F, 0x00, 0x00, 0xBA, 0x00], 0x32),
([0x80, 0x00, 0x8F, 0x00, 0x00, 0xBA, 0x00], 0x89),
([0x80, 0x00, 0xCF, 0x00, 0x00, 0xBA, 0x00], 0xE0),
([0x80, 0x00, 0x1F, 0x00, 0x00, 0xBA, 0x00], 0x06),
]
all_passed = True
for i, (bytes_list, expected) in enumerate(tests, 1):
calculated = checksum_lca_5_message(*bytes_list[:5])
status = "" if calculated == expected else ""
print(f"\nTest {i}: {status}")
print(f" Bytes: {' '.join(f'{b:02X}' for b in bytes_list)}")
print(f" Expected: 0x{expected:02X}")
print(f" Calculated: 0x{calculated:02X}")
if calculated != expected:
all_passed = False
print("\n" + "=" * 60)
if all_passed:
print("All tests passed! ✓")
else:
print("Some tests failed! ✗")
@@ -0,0 +1,42 @@
from opendbc.car import structs, get_safety_config
from opendbc.car.interfaces import CarInterfaceBase
from opendbc.car.volvo.carcontroller import CarController
from opendbc.car.volvo.carstate import CarState
from opendbc.car.volvo.values import VolvoSPAPlatformConfig, CAR
TransmissionType = structs.CarParams.TransmissionType
VOLVO_FLAG_SPA = 1
SAFETY_VOLVO = structs.CarParams.SafetyModel.volvo
class CarInterface(CarInterfaceBase):
CarState = CarState
CarController = CarController
@staticmethod
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
ret.brand = 'volvo'
safety_param = 0
if isinstance(CAR(candidate).config, VolvoSPAPlatformConfig):
safety_param = VOLVO_FLAG_SPA
ret.safetyConfigs = [get_safety_config(SAFETY_VOLVO, safety_param)]
#ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.noOutput)]
ret.dashcamOnly = False
ret.steerActuatorDelay = 0.3
ret.steerLimitTimer = 0.1
ret.steerAtStandstill = True
# Use angle-based steering control for Volvo CMA platform
ret.steerControlType = structs.CarParams.SteerControlType.angle
# Note: No lateral tuning configuration needed for basic angle control
ret.radarUnavailable = True
ret.alphaLongitudinalAvailable = False
ret.pcmCruise = True
return ret
@@ -0,0 +1 @@
@@ -0,0 +1,59 @@
import unittest
from opendbc.car.volvo.helpers import (
checksum_1_pscm_related_message,
checksum_2_pscm_related_message,
)
# (b1, b2) -> byte[0], observed on the bus.
# b1 = SIG1 counter (high nibble) | LCA_ENABLED_ECHO (low nibble), b2 = 0x80 | counter.
# ECHO 0/1/4 are normal driving; ECHO 6 only appears while ESC is intervening and was
# the case that used to fall through to a 0x00 checksum.
PSCM_RELATED_CHECKSUM_1 = {
(0x00, 0x80): 0xD4, (0x01, 0x80): 0xC9, (0x04, 0x80): 0xA0, (0x06, 0x80): 0x9A,
(0x10, 0x81): 0x98, (0x11, 0x81): 0x85, (0x14, 0x81): 0xEC, (0x16, 0x81): 0xD6,
(0x20, 0x82): 0x4C, (0x21, 0x82): 0x51, (0x24, 0x82): 0x38, (0x26, 0x82): 0x02,
(0x30, 0x83): 0x00, (0x31, 0x83): 0x1D, (0x34, 0x83): 0x74, (0x36, 0x83): 0x4E,
(0x40, 0x84): 0xF9, (0x41, 0x84): 0xE4, (0x44, 0x84): 0x8D, (0x46, 0x84): 0xB7,
(0x50, 0x85): 0xB5, (0x51, 0x85): 0xA8, (0x54, 0x85): 0xC1, (0x56, 0x85): 0xFB,
(0x60, 0x86): 0x61, (0x61, 0x86): 0x7C, (0x64, 0x86): 0x15, (0x66, 0x86): 0x2F,
(0x70, 0x87): 0x2D, (0x71, 0x87): 0x30, (0x74, 0x87): 0x59, (0x76, 0x87): 0x63,
(0x80, 0x88): 0x8E, (0x81, 0x88): 0x93, (0x84, 0x88): 0xFA, (0x86, 0x88): 0xC0,
(0x90, 0x89): 0xC2, (0x91, 0x89): 0xDF, (0x94, 0x89): 0xB6, (0x96, 0x89): 0x8C,
(0xA0, 0x8A): 0x16, (0xA1, 0x8A): 0x0B, (0xA4, 0x8A): 0x62, (0xA6, 0x8A): 0x58,
(0xB0, 0x8B): 0x5A, (0xB1, 0x8B): 0x47, (0xB4, 0x8B): 0x2E, (0xB6, 0x8B): 0x14,
(0xC0, 0x8C): 0xA3, (0xC1, 0x8C): 0xBE, (0xC4, 0x8C): 0xD7, (0xC6, 0x8C): 0xED,
(0xD0, 0x8D): 0xEF, (0xD1, 0x8D): 0xF2, (0xD4, 0x8D): 0x9B, (0xD6, 0x8D): 0xA1,
(0xE0, 0x8E): 0x3B, (0xE1, 0x8E): 0x26, (0xE4, 0x8E): 0x4F, (0xE6, 0x8E): 0x75,
}
# b2 -> byte[3], same source
PSCM_RELATED_CHECKSUM_2 = {
0x80: 0xBF, 0x81: 0xF3, 0x82: 0x27, 0x83: 0x6B, 0x84: 0x92,
0x85: 0xDE, 0x86: 0x0A, 0x87: 0x46, 0x88: 0xE5, 0x89: 0xA9,
0x8A: 0x7D, 0x8B: 0x31, 0x8C: 0xC8, 0x8D: 0x84, 0x8E: 0x50,
}
class TestPscmRelatedChecksums(unittest.TestCase):
def test_checksum_1_matches_the_car(self):
for (b1, b2), expected in PSCM_RELATED_CHECKSUM_1.items():
with self.subTest(b1=hex(b1), b2=hex(b2)):
assert checksum_1_pscm_related_message(b1, b2) == expected
def test_checksum_1_covers_esc_echo(self):
# regression: ECHO=6 used to miss the lookup table and return 0x00, which the
# receiving ECU logged as a checksum fault for as long as ESC was active
for counter in range(15):
b1, b2 = (counter << 4) | 6, 0x80 | counter
with self.subTest(counter=counter):
assert checksum_1_pscm_related_message(b1, b2) == PSCM_RELATED_CHECKSUM_1[(b1, b2)]
def test_checksum_2_matches_the_car(self):
for b2, expected in PSCM_RELATED_CHECKSUM_2.items():
with self.subTest(b2=hex(b2)):
assert checksum_2_pscm_related_message(b2) == expected
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,69 @@
from collections import defaultdict
from types import SimpleNamespace
from opendbc.car.volvo.carcontroller import CarController
from opendbc.car.volvo.helpers import checksum_lca_5_message
from opendbc.car.volvo.interface import CarInterface
from opendbc.car.volvo.values import DBC
def _zero_message():
return defaultdict(int)
def _state():
return SimpleNamespace(
out=SimpleNamespace(steeringAngleDeg=0.0, vEgoRaw=12.0, steeringTorque=0.0),
msg_lca=_zero_message(),
msg_pscm=_zero_message(),
msg_pscm_related=_zero_message(),
msg_lca_3=_zero_message(),
msg_lca_2=_zero_message(),
msg_lca_5=_zero_message(),
msg_lca_4=_zero_message(),
msg_lca_6=_zero_message(),
msg_lca_7=_zero_message(),
pilot_assist_engaged=False,
)
class _Actuators:
steeringAngleDeg = 30.0
def as_builder(self):
return SimpleNamespace(steeringAngleDeg=self.steeringAngleDeg)
def test_controller_emits_valid_eight_byte_messages_and_lca5_checksum():
cp = CarInterface.get_non_essential_params("POLESTAR_2")
controller = CarController(DBC[cp.carFingerprint], cp)
cs = _state()
cc = SimpleNamespace(latActive=True, actuators=_Actuators())
actuators, can_sends = controller.update(cc, cs, 0, None)
assert can_sends
assert {msg[2] for msg in can_sends} == {0, 2}
assert all(len(msg[1]) == 8 for msg in can_sends)
assert 0.0 < actuators.steeringAngleDeg < 540.0
lca5 = next(msg for msg in can_sends if msg[0] == 0x67)
data = lca5[1]
assert data[2] == checksum_lca_5_message(data[0], data[1], data[3], data[4], data[5])
def test_controller_relays_stock_lca5_angle_when_inactive():
cp = CarInterface.get_non_essential_params("VOLVO_XC40_RECHARGE")
controller = CarController(DBC[cp.carFingerprint], cp)
cs = _state()
cs.msg_lca_5["LCA_5_STEER"] = 12.0
cc = SimpleNamespace(latActive=False, actuators=_Actuators())
_, can_sends = controller.update(cc, cs, 0, None)
lca5 = next(msg for msg in can_sends if msg[0] == 0x67)
# The inactive path must not manufacture a new angle command.
raw = ((lca5[1][6] & 0x7F) << 8) | lca5[1][7]
if raw & (1 << 14):
raw -= 1 << 15
assert abs(raw * 0.05596 - 12.0) < 0.1
+154
View File
@@ -0,0 +1,154 @@
from dataclasses import dataclass, field
from opendbc.car.structs import CarParams
from opendbc.car import Bus, CarSpecs, DbcDict, PlatformConfig, Platforms
from opendbc.car.lateral import AngleSteeringLimits
from opendbc.car.docs_definitions import CarDocs, CarHarness, CarParts
from opendbc.car.fw_query_definitions import FwQueryConfig
Ecu = CarParams.Ecu
class CarControllerParams:
STEER_STEP = 1 # 100 Hz LCA command frequency (controlsd runs at 100 Hz)
# Max commanded-vs-actual steering angle error (deg). Stock Volvo Pilot Assist holds
# commanded within ~1.7° of actual even under sustained driver override; bounding the
# command to actual ± this error prevents the stale-command snap-back that causes
# aggressive post-release overcorrection.
ANGLE_ERROR = 3.0
# LCA torque-authority envelope, modeled after stock Pilot Assist behavior.
# LCA_STEER_LOOSELY (positive arm) and LCA_STEER_LOOSELY_INV (negative arm)
# form a directional envelope that PSCM applies to its EPS torque. Stock PA:
# - holds both arms at saturation (±LCA_AUTH_MAX) when no driver torque
# - on driver override, collapses both arms symmetrically at COLLAPSE_RATE
# until envelope reaches ~±LCA_AUTH_SPLIT, then splits asymmetrically:
# the arm matching driver direction (yielding) settles at ±PLATEAU_YIELD,
# the counter arm holds at ±PLATEAU_COUNTER (yield is shallower than counter)
# - rebuilds at REBUILD_RATE after release (~3 s back to saturation)
# See route_analysis/lca_override_mechanism.md for the data behind these.
LCA_AUTH_MAX = 614 # signal saturation
LCA_AUTH_PLATEAU_COUNTER = 130 # counter-arm magnitude during sustained override
# Override trigger thresholds on |CS.out.steeringTorque| (op-convention raw
# units, mirror of DRIVER_INPUT). Must be ABOVE the resting-hand noise floor
# (CS.steeringPressed uses |raw|>2 as a sensitive DM-fallback floor and does
# NOT indicate override intent — don't use it for envelope triggering).
# Hysteresis: enter override at ENTER, exit at EXIT (< ENTER) to prevent the
# envelope flapping between collapse and rebuild when driver torque hovers
# near a single threshold (was causing ~10 Hz EPS-torque ripple in lane
# changes when driver applied 6-8 raw to "ride along" with op).
LCA_AUTH_OVERRIDE_ENTER = 5
LCA_AUTH_OVERRIDE_EXIT = 3
# "Light contact" / haptic-acknowledgment region. When |drv| crosses into
# [LIGHT_THRESH, OVERRIDE_THRESH] from below, briefly collapse the envelope
# for LIGHT_HOLD_FRAMES (a haptic confirmation of hand-on-wheel detection),
# then rebuild even while the contact persists. Prevents the driver from
# needing to sustain force just to feel that the system noticed them — helps
# with hand-fatigue / RSI.
# Rising edge detected via per-frame derivative; the brief-yield window does
# NOT re-arm while still active, so a steady elevated torque only triggers
# one yield and then the envelope rebuilds.
# Cooldown: light_collapse only fires when real_override has been off for
# LIGHT_COOLDOWN_FRAMES — suppresses repeated firings during active
# co-steering (lane changes), where |drv| oscillates and would otherwise
# re-arm the haptic-ack window each time, causing felt ripple.
LCA_AUTH_LIGHT_THRESH = 3 # min |drv| to consider as contact
LCA_AUTH_LIGHT_RISE_DELTA = 1.0 # min per-frame increase in |drv| to count as rising contact
LCA_AUTH_LIGHT_HOLD_FRAMES = 15 # ~150 ms of yield on fresh light contact
LCA_AUTH_LIGHT_COOLDOWN_FRAMES = 30 # ~300 ms quiet-time on real_override before light contact re-arms
# Yield-arm plateau scales with driver-torque magnitude so brief strong presses
# (potholes, lane corrections) get full yield while light sustained pressure
# only gets a soft yield. yield_signed = YIELD_BASE YIELD_SLOPE *
# max(0, drv_mag_filt OVERRIDE_ENTER), clamped to [YIELD_MIN, YIELD_BASE].
# At |drv|=7 (just over threshold): yield = +60 (light resistance).
# At |drv|=14: yield ≈ -4 (crosses past zero — EPS hands wheel to driver).
# drv_mag_filt is a low-pass of |drv| (alpha=0.04, ~250 ms time constant) —
# without it, 1-2 unit driver-torque jitter became ~10 unit yield-arm jitter
# which PSCM converted to felt ripple at sustained co-steering pressure.
LCA_AUTH_YIELD_BASE = 60 # yield-arm magnitude at the override threshold
LCA_AUTH_YIELD_SLOPE = 8 # counts of yield reduction per unit |drv torque| above threshold
LCA_AUTH_YIELD_MIN = -30 # cap how far past zero the yield arm can go (full hand-over)
LCA_AUTH_YIELD_LP_ALPHA = 0.04 # LP-filter coefficient on |drv| for yield calc (~250 ms tau)
LCA_AUTH_SPLIT = 200 # symmetric → asymmetric handover
LCA_AUTH_REBUILD_RATE = 230 # counts/s (≈ 2.7 s rebuild from 0 to 614)
LCA_AUTH_COLLAPSE_RATE = 2500 # counts/s base (scales with |drv|/THRESH for sharper pothole jolts)
# Angle limits for rate limiting
ANGLE_LIMITS: AngleSteeringLimits = AngleSteeringLimits(
540, # deg - 1.5 turns to lock
([0., 5., 25.], [2.5, 1.5, .2]), # rate up limits at different speeds
([0., 5., 25.], [5., 2., .3]), # rate down limits at different speeds
)
@dataclass
class VolvoCarDocs(CarDocs):
package: str = "Pilot Assist & Adaptive Cruise Control"
car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.custom]))
@dataclass
class VolvoCMAPlatformConfig(PlatformConfig):
dbc_dict: DbcDict = field(default_factory=lambda: {
Bus.main: 'volvo_mid_1',
Bus.party: 'volvo_mid_1',
Bus.pt: 'volvo_front_1_cma',
})
@dataclass
class VolvoSPAPlatformConfig(PlatformConfig):
dbc_dict: DbcDict = field(default_factory=lambda: {
Bus.main: 'volvo_mid_1',
Bus.party: 'volvo_mid_1',
Bus.pt: 'volvo_front_1_spa',
})
class CAR(Platforms):
VOLVO_XC40_RECHARGE = VolvoCMAPlatformConfig(
[VolvoCarDocs("Volvo XC40 Recharge 2021-23")],
CarSpecs(
mass=2170,
wheelbase=2.702,
steerRatio=15.8,
centerToFrontRatio=0.52,
),
)
VOLVO_S60_RECHARGE = VolvoSPAPlatformConfig(
[VolvoCarDocs("Volvo S60 Recharge 2024")],
CarSpecs(
mass=2020,
wheelbase=2.872,
steerRatio=16.2,
centerToFrontRatio=0.516,
),
)
# Polestar 2 is technically CMA, but appears to use SPA DBC for CAN 1 bus
POLESTAR_2 = VolvoSPAPlatformConfig(
[VolvoCarDocs("Polestar 2 2020-25")],
CarSpecs(
mass=2123,
wheelbase=2.735,
steerRatio=15.8,
centerToFrontRatio=0.52,
),
)
# FW Query configuration for Volvo CMA platform
# FW_QUERY_CONFIG = FwQueryConfig(
# requests=[
# Request(
# [StdQueries.TESTER_PRESENT_REQUEST, StdQueries.UDS_VERSION_REQUEST],
# [StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.UDS_VERSION_RESPONSE],
# bus=0,
# ),
# ],
# )
FW_QUERY_CONFIG = FwQueryConfig(
requests=[]
)
DBC = CAR.create_dbc_map()
+464
View File
@@ -0,0 +1,464 @@
from opendbc.car.volvo.helpers import (checksum_lca_2_message, checksum_2_0x69_message, checksum_1_pscm_related_message,
checksum_2_pscm_related_message, checksum_lca_5_message)
from opendbc.car.carlog import carlog
def create_lca_message(packer, lat_active: bool, apply_angle: float, msg_lca: dict,
authority_pos: int = 614, authority_neg: int = -614,
overrides: dict | None = None):
"""
Create LCA (Lane Centering Assist) steering command for Volvo CMA platform.
Uses angle-based control via the LCA_STEER signal.
NOTE: This message must be sent continuously (even when inactive) because
stock LCA is permanently blocked by panda safety. When lat_active=False,
we send a safe/inactive LCA message to maintain PSCM communication.
Args:
packer: CAN packer instance
lat_active: Whether lateral control is active
apply_angle: Steering angle in degrees (positive = left, negative = right)
msg_lca: Dictionary containing LCA message values
authority_pos: LCA_STEER_LOOSELY value [0..614] right-pull torque-authority
envelope. Saturated (614) for stock-equivalent stiff feel; the
carcontroller envelope tracker collapses this on driver override
and rebuilds slowly to reproduce stock PA's easy-override feel.
authority_neg: LCA_STEER_LOOSELY_INV value [-614..0] left-pull authority.
overrides: Optional dict of signal overrides (keys are UPPERCASE DBC signal names)
"""
if not lat_active:
return packer.make_can_msg('LCA', 2, msg_lca)
# In openpilot, a positive angle corresponds to a LEFT turn.
# In Volvo, a positive LCA_STEER value corresponds to a LEFT turn.
values = {
'NEW_SIGNAL_1': 3,
'LCA_ENABLE_INV': 0 if lat_active else 1,
'LANE_KEEP_ACTIVE_INV': 3,
'LCA_STEER_LOOSELY': int(authority_pos) if lat_active else 0,
'NEW_SIGNAL_7': 7,
'LCA_STEER_LOOSELY_INV': int(authority_neg) if lat_active else 0,
# Steering rate - Stock LCA increased from 35 to 39 steppedly when steering request was overridden by openpilot that couldn't steer enough
'LCA_RATE_OF_CHANGE': 80 if lat_active else 251,
'LCA_STEER': msg_lca['LCA_STEER'],
'NEW_SIGNAL_6': 15,
}
# Apply any overrides from live testing config
if overrides:
for key, val in overrides.items():
values[key] = val
return packer.make_can_msg('LCA', 2, values)
def create_pscm_message(packer, lat_active: bool, msg_pscm: dict, frame: int):
values = {
'PSCM_ANGLE_SENSOR': msg_pscm['PSCM_ANGLE_SENSOR'],
'BIT_0': msg_pscm['BIT_0'],
'HANDS_ON_STEERING_WHEEL_A': msg_pscm['HANDS_ON_STEERING_WHEEL_A'],
'HANDS_ON_STEERING_WHEEL_B': msg_pscm['HANDS_ON_STEERING_WHEEL_B'],
'BYTE_4': msg_pscm['BYTE_4'],
'DRIVER_INPUT_DEVIATION': msg_pscm['DRIVER_INPUT_DEVIATION'],
'BYTE_6': msg_pscm['BYTE_6'],
'BYTE_7': msg_pscm['BYTE_7'],
}
# Spoof hands on wheel while openpilot is actively steering, so the stock EPS
# doesn't fault/nag on torque that didn't come from a human.
if lat_active:
values['HANDS_ON_STEERING_WHEEL_B'] = 186 if frame % 2 == 0 else 154 # msg_pscm['HANDS_ON_STEERING_WHEEL_B']
values['HANDS_ON_STEERING_WHEEL_A'] = 195 if frame % 2 == 0 else 249 # msg_pscm['HANDS_ON_STEERING_WHEEL_A']
return packer.make_can_msg('PSCM', 0, values)
def create_lca_3_message(packer, lat_active: bool, apply_angle: float, msg_lca_3: dict, counter_value: int):
"""
Create LCA_3 message for Volvo CMA platform.
This message enables PSCM to accept LCA commands.
Args:
packer: CAN packer instance
lat_active: Whether lateral control is active
apply_angle: Steering angle in degrees (used for direction indicator)
msg_lca_3: Dictionary containing LCA_3 message values
counter_value: Counter value to use (from pattern or stock)
"""
values = {
'NEW_SIGNAL_3': 0 if lat_active else msg_lca_3['NEW_SIGNAL_3'],
'LCA_ACCEPT_COMMANDS_RELATED': 15 if lat_active else msg_lca_3['LCA_ACCEPT_COMMANDS_RELATED'],
'NEW_SIGNAL_2': 0 if lat_active else msg_lca_3['NEW_SIGNAL_2'],
'NEW_SIGNAL_5': 30 if lat_active else msg_lca_3['NEW_SIGNAL_5'],
'LCA_ACCEPT_COMMANDS_INV': 0 if lat_active else msg_lca_3['LCA_ACCEPT_COMMANDS_INV'],
'NEW_SIGNAL_4': 3 if lat_active else msg_lca_3['NEW_SIGNAL_4'],
'SPEED_A': msg_lca_3['SPEED_A'],
'SPEED_B': msg_lca_3['SPEED_B'],
'NEW_SIGNAL_8': 1 if lat_active else msg_lca_3['NEW_SIGNAL_8'],
'NEW_SIGNAL_7': 3 if lat_active else msg_lca_3['NEW_SIGNAL_7'],
'NEW_SIGNAL_9': msg_lca_3['NEW_SIGNAL_9'],
'COUNTER_1': counter_value,
}
return packer.make_can_msg('LCA_3', 2, values)
def diff_dicts(a, b):
only_in_a = a.keys() - b.keys()
only_in_b = b.keys() - a.keys()
in_both = a.keys() & b.keys()
changed = {k: (a[k], b[k]) for k in in_both if a[k] != b[k]}
return {
"only_in_a": {k: a[k] for k in only_in_a},
"only_in_b": {k: b[k] for k in only_in_b},
"changed": changed,
}
def create_lca_2_message(packer, lat_active: bool, msg_lca_2: dict, counter_1: int, counter_2: int):
"""
Create LCA_2 message to spoof PILOT_ASSIST_ENGAGED when openpilot is active.
When lat_active=True, we set PILOT_ASSIST_ENGAGED=1 to make PSCM accept LCA commands,
even if the driver has disabled stock Pilot Assist.
Args:
packer: CAN packer instance
lat_active: Whether lateral control is active
msg_lca_2: Dictionary containing LCA_2 message values from car
counter_1: Managed COUNTER_1 value (increments by +2 mod 16)
counter_2: Managed COUNTER_2 value (increments by +4 mod 16)
"""
#return packer.make_can_msg('LCA_2', 2, msg_lca_2)
#if not lat_active:
# return packer.make_can_msg('LCA_2', 2, msg_lca_2)
#values = dict(msg_lca_2)
values = {
'BYTE_0': 24 if lat_active else msg_lca_2['BYTE_0'], # 24 always
'COUNTER_1': msg_lca_2['COUNTER_1'], # Byte 1 Low Nibble [5:8] - 4-bit counter that increments by +2 (modulo 16)
'PILOT_ASSIST_ENGAGED': 1 if lat_active else msg_lca_2['PILOT_ASSIST_ENGAGED'], # Byte 1 [4]
'BYTE_1_BITFIELD_0': msg_lca_2['BYTE_1_BITFIELD_0'],
'ESC_ACTUATING': msg_lca_2['ESC_ACTUATING'],
'ESC_ELIGIBLE': msg_lca_2['ESC_ELIGIBLE'],
'CHECKSUM_2': msg_lca_2['CHECKSUM_2'], # Checksum on bytes 0 and 1
'NEW_SIGNAL_2': 0 if lat_active else msg_lca_2['NEW_SIGNAL_2'],
'COUNTER_2': msg_lca_2['COUNTER_2'], # Byte 5 Low Nibble - 4-bit counter that increments by +4 (modulo 16)
'NEW_SIGNAL_3': 3 if lat_active else msg_lca_2['NEW_SIGNAL_3'],
'BRAKE_PEDAL_PRESSED_B': msg_lca_2['BRAKE_PEDAL_PRESSED_B'],
'BRAKE_PEDAL_PRESSED_A': msg_lca_2['BRAKE_PEDAL_PRESSED_A'],
'CHECKSUM_1': msg_lca_2['CHECKSUM_1'], # Byte 6 is a checksum based on Bytes 1, 2, and 5 only
'BYTE_7': 0 if lat_active else msg_lca_2['BYTE_7'],
}
dat = packer.make_can_msg('LCA_2', 2, values)
built_bytes = dat[1] # dat is (addr, bytes, bus) tuple - extract bytes
b0 = built_bytes[0]
b1 = built_bytes[1]
b2 = built_bytes[2]
b3 = built_bytes[3]
b4 = built_bytes[4]
b5 = built_bytes[5]
values['CHECKSUM_1'] = checksum_lca_2_message(b0, b5)
# Only validate when not active and message is valid (BYTE_0 should be 24, not 0)
if not lat_active:
#assert values['CHECKSUM_1'] == msg_lca_2['CHECKSUM_1']
if values['CHECKSUM_1'] != msg_lca_2['CHECKSUM_1']:
carlog.warning("[volvocan.py] LCA_2 CHECKSUM mismatch")
print(f"b0={b0}, b1={b1}, b2={b2}, b5={b5}, calculated={values['CHECKSUM_1']}, expected={msg_lca_2['CHECKSUM_1']}")
#assert False
# Checksum 2 - depends on bytes 0, 1, 3, and 4
checksum_2 = checksum_2_0x69_message(b0, b1, b3, b4)
values['CHECKSUM_2'] = checksum_2
if not lat_active:
if values['CHECKSUM_2'] != msg_lca_2['CHECKSUM_2']:
carlog.warning("[volvocan.py] LCA_2 CHECKSUM_2 mismatch")
print(f"b0={b0}, b1={b1}, b3={b3}, b4={b4}, calculated={values['CHECKSUM_2']}, expected={msg_lca_2['CHECKSUM_2']}")
#assert False
values['COUNTER_1'] = counter_1
values['COUNTER_2'] = counter_2
# Re-pack with updated counters to get correct bytes for checksum calculation
dat = packer.make_can_msg('LCA_2', 2, values)
built_bytes = dat[1] # dat is (addr, bytes, bus) tuple - extract bytes
b0 = built_bytes[0]
b1 = built_bytes[1]
b3 = built_bytes[3]
b4 = built_bytes[4]
b5 = built_bytes[5]
values['CHECKSUM_1'] = checksum_lca_2_message(b0, b5)
values['CHECKSUM_2'] = checksum_2_0x69_message(b0, b1, b3, b4)
return packer.make_can_msg('LCA_2', 2, values)
def create_lca_5_message(packer, lat_active: bool, target_angle_deg: float, msg_lca_5: dict, counter: int,
overrides: dict | None = None):
"""
Create LCA_5 message (0x67) with angle-based steering control.
Args:
packer: CAN packer instance
lat_active: Whether lateral control is active
target_angle_deg: Target steering angle in degrees (positive = left, negative = right)
msg_lca_5: Stock LCA_5 values from car
counter: Counter value (0-15, increments by 4)
overrides: Optional dict of signal overrides (keys are UPPERCASE DBC signal names)
Returns:
CAN message for LCA_5 on bus 2
"""
# DBC defines LCA_5_STEER as 15-bit signed with scale 0.05596 deg/count
# Packer handles the encoding automatically - just pass the angle in degrees
# Build values dictionary (wheel speeds and counter unchanged)
values = {
'WHEEL_SPEED_1': msg_lca_5['WHEEL_SPEED_1'],
'NEW_SIGNAL_4': msg_lca_5['NEW_SIGNAL_4'],
'NEW_SIGNAL_1': msg_lca_5['NEW_SIGNAL_1'],
'WHEEL_SPEED_2': msg_lca_5['WHEEL_SPEED_2'],
'NEW_SIGNAL_5': msg_lca_5['NEW_SIGNAL_5'],
'NEW_SIGNAL_2': msg_lca_5['NEW_SIGNAL_2'],
'LCA_5_STEER': target_angle_deg if lat_active else msg_lca_5['LCA_5_STEER'],
'COUNTER': counter,
}
# Apply any overrides from live testing config
if overrides:
for key, val in overrides.items():
values[key] = val
dat = packer.make_can_msg('LCA_5', 2, values)
built_bytes = dat[1] # dat is (addr, bytes, bus) tuple - extract bytes
values['CHECKSUM'] = checksum_lca_5_message(built_bytes[0], built_bytes[1], built_bytes[3], built_bytes[4], built_bytes[5])
return packer.make_can_msg('LCA_5', 2, values)
def create_speed_message(packer, msg_speed: dict):
"""
Forward SPEED message (0x60) by copying all bytes.
Args:
packer: CAN packer instance
msg_speed: Dictionary containing SPEED message values from car
"""
values = {
'SPEED': msg_speed['SPEED'],
'NEW_SIGNAL_1': msg_speed['NEW_SIGNAL_1'],
'NEW_SIGNAL_2': msg_speed['NEW_SIGNAL_2'],
'NEW_SIGNAL_3': msg_speed['NEW_SIGNAL_3'],
'NEW_SIGNAL_4': msg_speed['NEW_SIGNAL_4'],
'NEW_SIGNAL_5': msg_speed['NEW_SIGNAL_5'],
'NEW_SIGNAL_6': msg_speed['NEW_SIGNAL_6'],
}
return packer.make_can_msg('SPEED', 2, values)
def create_speed_2_message(packer, msg_speed_2: dict):
"""
Forward SPEED_2 message (0x68) by copying all bytes.
Args:
packer: CAN packer instance
msg_speed_2: Dictionary containing SPEED_2 message values from car
"""
values = {
'WHEEL_SPEED_LEFT': msg_speed_2['WHEEL_SPEED_LEFT'],
'WHEEL_SPEED_RIGHT': msg_speed_2['WHEEL_SPEED_RIGHT'],
'NEW_SIGNAL_1': msg_speed_2['NEW_SIGNAL_1'],
'NEW_SIGNAL_2': msg_speed_2['NEW_SIGNAL_2'],
'NEW_SIGNAL_3': msg_speed_2['NEW_SIGNAL_3'],
'NEW_SIGNAL_4': msg_speed_2['NEW_SIGNAL_4'],
'COUNTER_1': msg_speed_2['COUNTER_1'],
'COUNTER_2': msg_speed_2['COUNTER_2'],
}
return packer.make_can_msg('SPEED_2', 2, values)
def create_speed_3_message(packer, msg_speed_3: dict):
"""
Forward SPEED_3 message (0x60) by copying all bytes.
Args:
packer: CAN packer instance
msg_speed_3: Dictionary containing SPEED_3 message values from car
"""
values = {
'ALL_BYTES': msg_speed_3['ALL_BYTES'],
}
return packer.make_can_msg('SPEED_3', 2, values)
def create_0x1a_message(packer, msg_0x1a: dict):
"""
Forward 0x1A message by copying all bytes.
Args:
packer: CAN packer instance
msg_0x1a: Dictionary containing 0x1A message values from car
"""
values = {
'ALL_BYTES': msg_0x1a['ALL_BYTES'],
}
return packer.make_can_msg('NEW_MSG_1A', 2, values)
def create_gear_position_message(packer, msg_gear_position: dict):
"""
Forward GEAR_POSITION message by copying all bytes.
Args:
packer: CAN packer instance
msg_gear_position: Dictionary containing GEAR_POSITION message values from car
"""
values = dict(msg_gear_position)
values['GEAR_POSITION'] = msg_gear_position['GEAR_POSITION'] # 3
return packer.make_can_msg('GEAR_POSITION', 2, values)
def create_egsm_message(packer, msg_egsm: dict):
"""
Forward EGSM message by copying all bytes.
Args:
packer: CAN packer instance
msg_egsm: Dictionary containing EGSM message values from car
"""
values = {
'ALL_BYTES': msg_egsm['ALL_BYTES'],
}
return packer.make_can_msg('EGSM', 0, values)
def create_pscm_related_message(packer, lat_active: bool, stock_lca_engaged: bool, msg_pscm_related: dict, sig1_counter: int):
# BO_ 23 PSCM_RELATED: 8 XXX
# SG_ CHECKSUM : 7|8@0+ (1,0) [0|255] "" XXX
# SG_ LCA_ENABLED_ECHO : 11|4@0+ (1,0) [0|15] "" XXX
# SG_ SIG1_BYTE_1_HI_NIBBLE : 15|4@0+ (1,0) [0|15] "" XXX
# SG_ SIG1_REPLICA_BYTE_2_LO_NIBLE : 19|4@0+ (1,0) [0|15] "" XXX
# SG_ NEW_SIGNAL_2 : 23|4@0+ (1,0) [0|15] "" XXX
# SG_ BYTE_3 : 31|8@0+ (1,0) [0|255] "" XXX
# SG_ BYTE_4_5 : 39|16@0+ (1,0) [0|65535] "" XXX
# SG_ BYTE_6 : 55|8@0+ (1,0) [0|255] "" XXX
# SG_ BYTE_7 : 63|8@0+ (1,0) [0|255] "" XXX
values = dict(msg_pscm_related)
# Update SIG1 counter (same value in both locations for redundancy)
values['SIG1_BYTE_1_HI_NIBBLE'] = sig1_counter
values['SIG1_REPLICA_BYTE_2_LO_NIBLE'] = sig1_counter
dat = packer.make_can_msg('PSCM_RELATED', 0, values)
built_bytes = dat[1] # dat is (addr, bytes, bus) tuple - extract bytes
b1 = built_bytes[1]
b2 = built_bytes[2]
values['CHECKSUM_1'] = checksum_1_pscm_related_message(b1, b2)
values['CHECKSUM_2'] = checksum_2_pscm_related_message(b2)
#assert values['CHECKSUM_1'] == msg_pscm_related['CHECKSUM_1']
#assert values['CHECKSUM_2'] == msg_pscm_related['CHECKSUM_2']
if lat_active and not stock_lca_engaged:
values['LCA_ENABLED_ECHO'] = 0
b1 = packer.make_can_msg('PSCM_RELATED', 0, values)[1][1]
values['CHECKSUM_1'] = checksum_1_pscm_related_message(b1, b2)
return packer.make_can_msg('PSCM_RELATED', 0, values)
def create_lca_4_message(packer, lat_active: bool, msg_lca_4: dict, lca_4_steer: int,
overrides: dict | None = None):
"""
Create LCA_4 (0x90) message to maintain Pilot Assist state when openpilot is active.
Critical: LCA_ENABLE (byte 1 bits 0-1) must be held at 3 (both bits=1) when lat_active.
When PA turns off, these bits start varying (become counters). We need to keep them
stable at 3 to fool PSCM into thinking PA is still on, allowing LCA commands to be accepted.
Based on analysis from route_analysis/pilot_assist_off/BASELINE_FILTERED_FINDINGS.md:
- Message 0x090 byte 1 bits 0-1 are PA state signals
- During PA ON: bits are stable at 3 (binary 11)
- During PA OFF: bits start varying (counters)
- PSCM uses this to determine whether to accept LCA steering commands
Args:
packer: CAN packer instance
lat_active: Whether lateral control is active
msg_lca_4: Dictionary containing LCA_4 message values from car
lca_4_steer: Pre-computed signed angle with hysteresis applied
overrides: Optional dict of signal overrides (keys are UPPERCASE DBC signal names)
Returns:
CAN message for LCA_4 on bus 2
"""
if not lat_active:
# When not active, just relay stock message unchanged
return packer.make_can_msg('LCA_4', 2, msg_lca_4)
# When lat_active, force LCA_ENABLE to 3 (PA ON state)
values = {
'BYTE_0': msg_lca_4['BYTE_0'],
'LCA_ENABLE': 3, # Force bits 0-1 to 1 (value=3 means both bits set)
'BYTE_1_FLAGS': msg_lca_4['BYTE_1_FLAGS'],
'BYTE_1_NIBBLE_HI': msg_lca_4['BYTE_1_NIBBLE_HI'],
'BYTE_2_3': msg_lca_4['BYTE_2_3'],
'YAW_RATE': msg_lca_4['YAW_RATE'],
'BYTE_6': msg_lca_4['BYTE_6'],
'BYTE_7_NIBBLE_LO': msg_lca_4['BYTE_7_NIBBLE_LO'],
'BYTE_7_NIBBLE_HI': msg_lca_4['BYTE_7_NIBBLE_HI'],
}
# Apply any overrides from live testing config
if overrides:
for key, val in overrides.items():
values[key] = val
# TODO: Add checksum calculation when checksum function is implemented
# If message has a checksum signal, it would be calculated here like:
# values['CHECKSUM'] = checksum_lca_4_message(...)
# TODO: Add checksum validation when not active (once checksum is known)
# if not lat_active and 'CHECKSUM' in msg_lca_4:
# if values['CHECKSUM'] != msg_lca_4['CHECKSUM']:
# carlog.warning("[volvocan.py] LCA_4 CHECKSUM mismatch")
return packer.make_can_msg('LCA_4', 2, values)
def create_lca_6_message(packer, lat_active: bool, msg_lca_6: dict, lca_6_steer: int,
overrides: dict | None = None):
if not lat_active:
# When not active, just relay stock message unchanged
return packer.make_can_msg('LCA_6', 2, msg_lca_6)
values = {
'LCA_6_STEER': msg_lca_6['LCA_6_STEER'],
'LCA_6_STEER_2': msg_lca_6['LCA_6_STEER_2'],
'NEW_SIGNAL_1': msg_lca_6['NEW_SIGNAL_1'],
'NEW_SIGNAL_2': msg_lca_6['NEW_SIGNAL_2'],
'NEW_SIGNAL_3': msg_lca_6['NEW_SIGNAL_3'],
'NEW_SIGNAL_4': msg_lca_6['NEW_SIGNAL_4'],
'NEW_SIGNAL_5': msg_lca_6['NEW_SIGNAL_5'],
}
# Apply any overrides from live testing config
if overrides:
for key, val in overrides.items():
values[key] = val
return packer.make_can_msg('LCA_6', 2, values)
def create_lca_7_message(packer, lat_active: bool, msg_lca_7: dict, lca_7_steer: int, lca_7_delta_steer: int,
overrides: dict | None = None, steer_active: bool = False):
if not lat_active:
# When not active, just relay stock message unchanged
return packer.make_can_msg('LCA_7', 2, msg_lca_7)
values = {
'LCA_7_STEER': msg_lca_7['LCA_7_STEER'],
'LCA_7_DELTA_STEER': msg_lca_7['LCA_7_DELTA_STEER'],
'NEW_SIGNAL_1': msg_lca_7['NEW_SIGNAL_1'],
'NEW_SIGNAL_2': msg_lca_7['NEW_SIGNAL_2'],
'NEW_SIGNAL_3': msg_lca_7['NEW_SIGNAL_3'],
'NEW_SIGNAL_4': msg_lca_7['NEW_SIGNAL_4'],
}
# Apply any overrides from live testing config
if overrides:
for key, val in overrides.items():
values[key] = val
return packer.make_can_msg('LCA_7', 2, values)
+245
View File
@@ -0,0 +1,245 @@
BO_ 21 DRIVER_INPUT: 8 XXX
SG_ BYTE_0 : 7|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_1 : 15|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_2 : 16|8@1+ (1,0) [0|255] "" XXX
SG_ STEERING_DRIVER_RATE_OF_CHANGE : 31|8@0- (1,0) [0|255] "" XXX
SG_ BYTE_4 : 39|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_5 : 40|8@1+ (1,0) [0|255] "" XXX
SG_ STEERING_DRIVER_INPUT : 55|8@0- (1,1) [0|255] "" XXX
SG_ BYTE_7 : 63|8@0+ (1,0) [0|255] "" XXX
BO_ 22 PSCM: 8 XXX
SG_ PSCM_ANGLE_SENSOR : 6|15@0- (0.05596,0) [-916|916] "º" XXX
SG_ BIT_0 : 7|1@0+ (1,0) [0|1] "" XXX
SG_ HANDS_ON_STEERING_WHEEL_A : 23|8@0+ (1,0) [0|255] "" XXX
SG_ HANDS_ON_STEERING_WHEEL_B : 31|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_4 : 39|8@0+ (1,0) [0|255] "" XXX
SG_ DRIVER_INPUT_DEVIATION : 47|8@0- (1,0) [-128|127] "" XXX
SG_ BYTE_6 : 55|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_7 : 63|8@0+ (1,0) [0|255] "" XXX
BO_ 23 PSCM_RELATED: 8 XXX
SG_ CHECKSUM_1 : 7|8@0+ (1,0) [0|255] "" XXX
SG_ LCA_ENABLED_ECHO : 11|4@0+ (1,0) [0|15] "" XXX
SG_ SIG1_BYTE_1_HI_NIBBLE : 15|4@0+ (1,0) [0|15] "" XXX
SG_ SIG1_REPLICA_BYTE_2_LO_NIBLE : 19|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_2 : 23|4@0+ (1,0) [0|15] "" XXX
SG_ CHECKSUM_2 : 31|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_4 : 39|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_5 : 47|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_6 : 55|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_7 : 63|8@0+ (1,0) [0|255] "" XXX
BO_ 26 NEW_MSG_1A: 8 XXX
SG_ NEW_SIGNAL_2 : 5|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_1 : 7|1@0+ (1,0) [0|1] "" XXX
BO_ 38 NEW_MSG_26: 8 XXX
SG_ NEW_SIGNAL_1 : 5|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_2 : 15|8@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_4 : 35|12@0+ (1,0) [0|4095] "" XXX
SG_ NEW_SIGNAL_3 : 39|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_6 : 51|12@0+ (1,0) [0|4095] "" XXX
SG_ NEW_SIGNAL_5 : 55|1@0+ (1,0) [0|1] "" XXX
BO_ 55 NEW_MSG_37: 8 XXX
SG_ ACCELERATOR_PEDAL_RATE_OF_CHANGE : 6|15@0+ (1,0) [0|32767] "" XXX
BO_ 69 EGSM: 8 XXX
SG_ COUNTER_1 : 3|12@0+ (1,0) [0|4095] "" XXX
SG_ GEAR_LEVER_DBC_INCOHERENT : 5|2@0+ (1,0) [0|3] "" XXX
SG_ COUNTER_2 : 19|12@0+ (1,0) [0|4095] "" XXX
SG_ GEAR_1ST_RATCH : 20|1@0+ (1,0) [0|1] "" XXX
SG_ GEAR_RATCHED_UP : 21|1@0+ (1,0) [0|1] "" XXX
SG_ GEAR_RATCHED_DOWN : 22|1@0+ (1,0) [0|1] "" XXX
SG_ GEAR_PRESSED : 36|2@0+ (1,0) [0|3] "" XXX
SG_ PARKING_BRAKE_BUTTON : 38|1@0+ (1,0) [0|1] "" XXX
SG_ COUNTER_3 : 51|12@0+ (1,0) [0|4095] "" XXX
BO_ 85 SAS: 8 XXX
SG_ SAS_ANGLE_SENSOR : 6|15@0- (0.05596,0) [0|32767] "º" XXX
SG_ NEW_SIGNAL_1 : 7|1@0+ (1,0) [0|1] "" XXX
SG_ SAS_INPUT_ACTIVITY : 21|6@0+ (1,0) [0|16383] "" XXX
SG_ NEW_SIGNAL_2 : 23|2@0+ (1,0) [0|3] "" XXX
SG_ SAS_RATE_OF_CHANGE : 31|8@0- (1,0) [0|255] "" XXX
SG_ SAS_CHECKSUM : 39|8@0+ (1,0) [0|4095] "" XXX
SG_ NEW_SIGNAL_4 : 43|20@0+ (1,0) [0|1048575] "" XXX
SG_ SAS_COUNTER : 44|4@1+ (1,0) [0|15] "" XXX
BO_ 87 LCA_3: 8 XXX
SG_ NEW_SIGNAL_3 : 0|1@0+ (1,0) [0|1] "" XXX
SG_ LCA_ACCEPT_COMMANDS_RELATED : 4|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_2 : 7|3@0+ (1,0) [0|7] "" XXX
SG_ NEW_SIGNAL_5 : 12|5@0+ (1,0) [0|31] "" XXX
SG_ LCA_ACCEPT_COMMANDS_INV : 13|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_4 : 15|2@0+ (1,0) [0|3] "" XXX
SG_ SPEED_A : 23|16@0+ (1,0) [0|65535] "" XXX
SG_ SPEED_B : 39|16@0+ (1,0) [0|65535] "" XXX
SG_ NEW_SIGNAL_8 : 51|4@0+ (1,0) [0|15] "" XXX
SG_ COUNTER_1 : 53|2@0+ (1,0) [0|3] "" XXX
SG_ NEW_SIGNAL_7 : 55|2@0+ (1,0) [0|3] "" XXX
SG_ NEW_SIGNAL_9 : 63|8@0+ (1,0) [0|255] "" XXX
BO_ 88 LCA: 8 XXX
SG_ NEW_SIGNAL_3 : 1|2@0+ (1,0) [0|3] "" XXX
SG_ NEW_SIGNAL_8 : 2|1@0+ (1,0) [0|1] "" XXX
SG_ LCA_ENABLE_INV : 3|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_2 : 5|2@0+ (1,0) [0|3] "" XXX
SG_ NEW_SIGNAL_1 : 7|2@0+ (1,0) [0|3] "" XXX
SG_ LCA_STEER_LOOSELY_1 : 15|8@0+ (1,0) [0|255] "" XXX
SG_ LCA_STEER_ACTIVE_INCOHERENT : 16|1@0+ (1,0) [0|1] "" XXX
SG_ LCA_STEER_ACTIVE : 18|2@0+ (1,0) [0|3] "" XXX
SG_ NEW_SIGNAL_7 : 21|3@0+ (1,0) [0|7] "" XXX
SG_ NEW_SIGNAL_9 : 23|2@0+ (1,0) [0|3] "" XXX
SG_ LCA_STEER_LOOSELY_2 : 31|8@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_4 : 39|8@0+ (1,0) [0|255] "" XXX
SG_ CURVE_RIGHT : 45|6@0+ (1,0) [0|63] "" XXX
SG_ NEW_SIGNAL_5 : 46|2@1+ (1,0) [0|3] "" XXX
SG_ LCA_STEER : 55|8@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_10 : 59|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_6 : 60|4@1+ (1,0) [0|15] "" XXX
BO_ 96 SPEED_3: 8 XXX
SG_ NEW_SIGNAL_1 : 0|8@1+ (1,0) [0|255] "" XXX
SG_ SPEED_COUNTER : 15|8@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_3 : 23|8@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_5 : 24|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_2 : 27|3@0+ (1,0) [0|7] "" XXX
SG_ NEW_SIGNAL_4 : 31|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_8 : 34|3@0+ (1,0) [0|7] "" XXX
SG_ NEW_SIGNAL_6 : 35|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_7 : 39|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_9 : 40|8@1+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_10 : 55|8@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_11 : 56|8@1+ (1,0) [0|255] "" XXX
BO_ 103 LCA_5: 8 XXX
SG_ WHEEL_SPEED_1 : 6|15@0+ (0.1,0) [0|255] "rpm" XXX
SG_ NEW_SIGNAL_4 : 7|1@0+ (1,0) [0|1] "" XXX
SG_ CHECKSUM : 23|8@0+ (1,0) [0|4095] "" XXX
SG_ NEW_SIGNAL_1 : 24|4@1+ (1,0) [0|15] "" XXX
SG_ COUNTER : 28|4@1+ (1,0) [0|15] "" XXX
SG_ WHEEL_SPEED_2 : 39|15@0+ (0.1,0) [0|32767] "rpm" XXX
SG_ NEW_SIGNAL_5 : 40|1@0+ (1,0) [0|1] "" XXX
SG_ LCA_TURN_BITS : 55|8@0+ (1,0) [0|255] "" XXX
SG_ LCA_5_STEER : 63|8@0+ (1,0) [0|255] "" XXX
BO_ 104 SPEED_2: 8 XXX
SG_ WHEEL_SPEED_3 : 6|15@0+ (0.1,0) [0|32767] "rpm" XXX
SG_ WHEEL_SPEED_4 : 39|15@0+ (0.1,0) [0|32767] "rpm" XXX
SG_ NEW_SIGNAL_2 : 51|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_3 : 54|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_1 : 56|8@1+ (1,0) [0|255] "" XXX
BO_ 105 LCA_2: 8 XXX
SG_ BYTE_0 : 7|8@0+ (1,0) [0|2047] "" XXX
SG_ COUNTER_1 : 11|4@0+ (1,0) [0|4095] "" XXX
SG_ PILOT_ASSIST_ENGAGED : 12|1@0+ (1,0) [0|1] "" XXX
SG_ BYTE_1_MSBS_3 : 15|3@0+ (1,0) [0|7] "" XXX
SG_ CHECKSUM_2 : 23|8@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_2 : 31|16@0+ (1,0) [0|65535] "" XXX
SG_ COUNTER_2 : 43|4@0+ (1,0) [0|4095] "" XXX
SG_ NEW_SIGNAL_3 : 45|2@0+ (1,0) [0|3] "" XXX
SG_ BRAKE_PEDAL_PRESSED_B : 46|1@0+ (1,0) [0|1] "" XXX
SG_ BRAKE_PEDAL_PRESSED_A : 47|1@0+ (-1,1) [0|1] "" XXX
SG_ CHECKSUM_1 : 55|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_7 : 63|8@0+ (1,0) [0|255] "" XXX
BO_ 112 BUS1_SPEED: 8 XXX
SG_ BUS1_SPEED : 23|16@0+ (0.01886,0) [0|65535] "m/s" XXX
BO_ 128 GEAR_POSITION: 8 XXX
SG_ NEW_SIGNAL_7 : 3|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_6 : 7|3@0+ (1,0) [0|7] "" XXX
SG_ NEW_SIGNAL_8 : 12|4@1+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_1 : 43|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_3 : 47|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_2 : 48|4@1+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_4 : 55|1@0+ (1,0) [0|1] "" XXX
SG_ GEAR_POSITION : 57|2@0+ (1,0) [0|3] "" XXX
SG_ NEW_SIGNAL_5 : 63|1@0+ (1,0) [0|1] "" XXX
BO_ 144 LCA_4: 8 XXX
SG_ BYTE_0 : 7|8@0+ (1,0) [0|16383] "" XXX
SG_ LCA_ENABLE : 9|2@0+ (1,0) [0|3] "" XXX
SG_ BYTE_1_FLAGS : 11|2@0+ (1,0) [0|3] "" XXX
SG_ BYTE_1_NIBBLE_HI : 15|4@0+ (1,0) [0|63] "" XXX
SG_ BYTE_2_3 : 23|16@0- (1,0) [0|65535] "" XXX
SG_ YAW_RATE : 39|16@0- (1,0) [0|65535] "" XXX
SG_ BYTE_6 : 55|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_7_NIBBLE_LO : 59|4@0+ (1,0) [0|15] "" XXX
SG_ BYTE_7_NIBBLE_HI : 60|4@1+ (1,0) [0|15] "" XXX
BO_ 146 NEW_MSG_92: 8 XXX
SG_ NEW_SIGNAL_1 : 7|8@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_2 : 12|4@1+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_3 : 23|8@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_4 : 25|7@1+ (1,0) [0|127] "" XXX
SG_ NEW_SIGNAL_5 : 38|7@0+ (1,0) [0|127] "" XXX
SG_ NEW_SIGNAL_6 : 40|7@1+ (1,0) [0|127] "" XXX
SG_ NEW_SIGNAL_7 : 48|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_8 : 49|3@1+ (1,0) [0|7] "" XXX
SG_ NEW_SIGNAL_9 : 57|7@1+ (1,0) [0|127] "" XXX
BO_ 147 NEW_MSG_93: 8 XXX
SG_ NEW_SIGNAL_1 : 38|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_2 : 47|8@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_4 : 52|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_3 : 55|1@0+ (1,0) [0|1] "" XXX
BO_ 151 LCA_SUSPECT: 8 XXX
SG_ NEW_SIGNAL_1 : 7|8@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_2 : 8|8@1+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_3 : 23|8@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_6 : 25|2@0+ (1,0) [0|3] "" XXX
SG_ NEW_SIGNAL_5 : 26|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_4 : 27|5@1+ (1,0) [0|31] "" XXX
SG_ NEW_SIGNAL_7 : 39|8@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_9 : 43|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_8 : 44|4@1+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_10 : 48|8@1+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_12 : 56|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_11 : 63|7@0+ (1,0) [0|127] "" XXX
BO_ 336 NEW_MSG_150: 8 XXX
SG_ IGN_DRAFT : 5|2@0+ (1,0) [0|3] "" XXX
BO_ 341 NEW_MSG_155: 8 XXX
SG_ NEW_SIGNAL_1 : 23|8@0+ (1,0) [0|255] "" XXX
BO_ 592 ECM_1: 8 XXX
SG_ ACCELERATOR_PEDAL_POS : 31|8@0+ (1,0) [0|255] "" XXX
BO_ 773 NEW_MSG_305: 8 XXX
SG_ NEW_SIGNAL_1 : 23|8@0+ (1,0) [0|255] "" XXX
BO_ 778 NEW_MSG_30A: 8 XXX
SG_ NEW_SIGNAL_1 : 15|8@0+ (1,0) [0|255] "" XXX
BO_ 832 BUS1_CRUISE_CONTROL: 8 XXX
SG_ CRUISE_CONTROL_ENABLED : 56|1@0+ (1,0) [0|1] "" XXX
SG_ CRUISE_CONTROL_ENABLED_IDLE_TRAFFIC : 57|1@0+ (1,0) [0|1] "" XXX
BO_ 896 NEW_MSG_380: 8 XXX
SG_ NEW_SIGNAL_1 : 23|8@0+ (1,0) [0|255] "" XXX
BO_ 1336 NEW_MSG_538: 8 XXX
SG_ SWM_MULTIMEDIA_ACTIVITY : 22|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_1 : 23|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_3 : 24|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_2 : 29|1@0+ (1,0) [0|1] "" XXX
CM_ BO_ 23 "Might be related to PSCM 0x16";
CM_ SG_ 87 LCA_ACCEPT_COMMANDS_RELATED "Goes to 15 when actively operating LCA";
CM_ SG_ 87 LCA_ACCEPT_COMMANDS_INV "Goes to 0 when PSCM allowed to receive LCA commands";
CM_ SG_ 87 NEW_SIGNAL_9 "NEW_SIGNAL_9 appears to be similar to LCA_5_STEER, but different scale, and zero-point is at 128. I haven't seen what happens once LCA_TURN_BITS wrap";
CM_ SG_ 88 LCA_ENABLE_INV "Master enable, active-low";
CM_ SG_ 88 CURVE_RIGHT "Only appears to be HIGH on curve right, LOW curve left";
CM_ SG_ 88 LCA_STEER "Seems torque-based, signed, follows the road curvature";
CM_ SG_ 103 WHEEL_SPEED_1 "Possible Front Left (FL)";
CM_ SG_ 103 WHEEL_SPEED_2 "Possible Front Right (FR)";
CM_ SG_ 103 LCA_TURN_BITS "Two-byte torque encoding (high byte). Left: 128->134 (increments), Right: 255->249 (decrements), Neutral: 186";
CM_ SG_ 103 LCA_5_STEER "Two-byte torque encoding (low byte). Left: 0->255 (wraps at boundary), Right: 255->0 (wraps at boundary)";
CM_ SG_ 104 WHEEL_SPEED_3 "Possible Rear Left (RR)";
CM_ SG_ 104 WHEEL_SPEED_4 "Possibe Rear Right (RR)";
CM_ SG_ 592 ACCELERATOR_PEDAL_POS "Full gas ~200; Idle ~20";
@@ -0,0 +1,14 @@
BO_ 55 NEW_MSG_37: 8 XXX
SG_ ACCELERATOR_PEDAL_RATE_OF_CHANGE : 6|15@0+ (1,0) [0|32767] "" XXX
BO_ 112 BUS1_SPEED: 8 XXX
SG_ BUS1_SPEED : 23|16@0+ (0.01886,0) [0|65535] "m/s" XXX
BO_ 592 ECM_1: 8 XXX
SG_ ACCELERATOR_PEDAL_POS : 31|8@0+ (1,0) [0|255] "" XXX
BO_ 832 BUS1_CRUISE_CONTROL: 8 XXX
SG_ CRUISE_CONTROL_ENABLED : 56|1@0+ (1,0) [0|1] "" XXX
SG_ CRUISE_CONTROL_ENABLED_IDLE_TRAFFIC : 57|1@0+ (1,0) [0|1] "" XXX
CM_ SG_ 592 ACCELERATOR_PEDAL_POS "Full gas ~200; Idle ~20";
@@ -0,0 +1,10 @@
BO_ 37 ECM_1: 8 XXX
SG_ ACCELERATOR_PEDAL_POS : 6|15@0+ (0.00390625,0) [0|32767] "%" XXX
BO_ 117 BUS1_SPEED: 8 XXX
SG_ BUS1_SPEED : 6|15@0+ (0.0044704,0) [0|32767] "" XXX
BO_ 841 BUS1_CRUISE_CONTROL: 8 XXX
SG_ CRUISE_CONTROL_SPA_ENABLED : 1|1@0+ (-1,1) [0|1] "" XXX
CM_ SG_ 117 BUS1_SPEED "m/s";
+188
View File
@@ -0,0 +1,188 @@
BO_ 21 DRIVER_INPUT: 8 XXX
SG_ BYTE_0 : 7|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_1 : 15|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_2 : 16|8@1+ (1,0) [0|255] "" XXX
SG_ STEERING_DRIVER_RATE_OF_CHANGE : 31|8@0- (1,0) [0|255] "" XXX
SG_ BYTE_4 : 39|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_5 : 40|8@1+ (1,0) [0|255] "" XXX
SG_ STEERING_DRIVER_INPUT : 55|8@0- (1,1) [0|255] "" XXX
SG_ BYTE_7 : 63|8@0+ (1,0) [0|255] "" XXX
BO_ 22 PSCM: 8 XXX
SG_ PSCM_ANGLE_SENSOR : 6|15@0- (0.05596,0) [-916|916] "º" XXX
SG_ BIT_0 : 7|1@0+ (1,0) [0|1] "" XXX
SG_ HANDS_ON_STEERING_WHEEL_A : 23|8@0+ (1,0) [0|255] "" XXX
SG_ HANDS_ON_STEERING_WHEEL_B : 31|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_4 : 39|8@0+ (1,0) [0|255] "" XXX
SG_ DRIVER_INPUT_DEVIATION : 47|8@0- (1,0) [-128|127] "" XXX
SG_ BYTE_6 : 55|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_7 : 63|8@0+ (1,0) [0|255] "" XXX
BO_ 23 PSCM_RELATED: 8 XXX
SG_ CHECKSUM_1 : 7|8@0+ (1,0) [0|255] "" XXX
SG_ LCA_ENABLED_ECHO : 11|4@0+ (1,0) [0|15] "" XXX
SG_ SIG1_BYTE_1_HI_NIBBLE : 15|4@0+ (1,0) [0|15] "" XXX
SG_ SIG1_REPLICA_BYTE_2_LO_NIBLE : 19|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_2 : 23|4@0+ (1,0) [0|15] "" XXX
SG_ CHECKSUM_2 : 31|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_4_5 : 39|16@0+ (1,0) [0|65535] "" XXX
SG_ BYTE_6 : 55|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_7 : 63|8@0+ (1,0) [0|255] "" XXX
BO_ 69 EGSM: 8 XXX
SG_ COUNTER_1 : 3|12@0+ (1,0) [0|4095] "" XXX
SG_ GEAR_LEVER_DBC_INCOHERENT : 5|2@0+ (1,0) [0|3] "" XXX
SG_ COUNTER_2 : 19|12@0+ (1,0) [0|4095] "" XXX
SG_ GEAR_1ST_RATCH : 20|1@0+ (1,0) [0|1] "" XXX
SG_ GEAR_RATCHED_UP : 21|1@0+ (1,0) [0|1] "" XXX
SG_ GEAR_RATCHED_DOWN : 22|1@0+ (1,0) [0|1] "" XXX
SG_ GEAR_PRESSED : 36|2@0+ (1,0) [0|3] "" XXX
SG_ PARKING_BRAKE_BUTTON : 38|1@0+ (1,0) [0|1] "" XXX
SG_ COUNTER_3 : 51|12@0+ (1,0) [0|4095] "" XXX
BO_ 85 SAS: 8 XXX
SG_ SAS_ANGLE_SENSOR : 6|15@0- (0.05596,0) [0|32767] "º" XXX
SG_ SAS_RATE_OF_CHANGE : 21|14@0- (1,0) [0|16383] "" XXX
SG_ SAS_CHECKSUM : 39|8@0+ (1,0) [0|4095] "" XXX
SG_ SAS_COUNTER : 44|4@1+ (1,0) [0|15] "" XXX
BO_ 87 LCA_3: 8 XXX
SG_ NEW_SIGNAL_3 : 0|1@0+ (1,0) [0|1] "" XXX
SG_ LCA_ACCEPT_COMMANDS_RELATED : 4|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_2 : 7|3@0+ (1,0) [0|7] "" XXX
SG_ NEW_SIGNAL_5 : 12|5@0+ (1,0) [0|31] "" XXX
SG_ LCA_ACCEPT_COMMANDS_INV : 13|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_4 : 15|2@0+ (1,0) [0|3] "" XXX
SG_ SPEED_A : 23|16@0+ (1,0) [0|65535] "" XXX
SG_ SPEED_B : 39|16@0+ (1,0) [0|65535] "" XXX
SG_ NEW_SIGNAL_8 : 51|4@0+ (1,0) [0|15] "" XXX
SG_ COUNTER_1 : 53|2@0+ (1,0) [0|3] "" XXX
SG_ NEW_SIGNAL_7 : 55|2@0+ (1,0) [0|3] "" XXX
SG_ NEW_SIGNAL_9 : 63|8@0+ (1,0) [0|255] "" XXX
BO_ 88 LCA: 8 XXX
SG_ LCA_STEER_LOOSELY : 2|11@0- (1,0) [0|2047] "" XXX
SG_ LCA_ENABLE_INV : 3|1@0+ (1,0) [0|1] "" XXX
SG_ LANE_KEEP_ACTIVE_INV : 7|2@0+ (1,0) [0|3] "" XXX
SG_ LCA_STEER_LOOSELY_INV : 18|11@0- (1,0) [0|2047] "" XXX
SG_ NEW_SIGNAL_7 : 21|3@0+ (1,0) [0|7] "" XXX
SG_ LCA_RATE_OF_CHANGE : 39|8@0+ (1,0) [0|255] "" XXX
SG_ LCA_STEER : 45|14@0- (1,0) [0|16383] "" XXX
SG_ NEW_SIGNAL_1 : 47|2@0+ (1,0) [0|3] "" XXX
SG_ NEW_SIGNAL_6 : 60|4@1+ (1,0) [0|15] "" XXX
BO_ 96 SPEED: 8 XXX
SG_ SPEED : 6|15@0+ (1,0) [0|32767] "" XXX
SG_ NEW_SIGNAL_1 : 7|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_2 : 23|12@0+ (1,0) [0|4095] "" XXX
SG_ NEW_SIGNAL_4 : 24|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_3 : 27|3@0+ (1,0) [0|7] "" XXX
SG_ NEW_SIGNAL_6 : 35|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_5 : 36|1@0+ (1,0) [0|1] "" XXX
BO_ 103 LCA_5: 8 XXX
SG_ WHEEL_SPEED_1 : 6|15@0+ (0.1,0) [0|255] "rpm" XXX
SG_ NEW_SIGNAL_4 : 7|1@0+ (1,0) [0|1] "" XXX
SG_ CHECKSUM : 23|8@0+ (1,0) [0|4095] "" XXX
SG_ NEW_SIGNAL_1 : 24|4@1+ (1,0) [0|15] "" XXX
SG_ COUNTER : 28|4@1+ (1,0) [0|15] "" XXX
SG_ WHEEL_SPEED_2 : 39|15@0+ (0.1,0) [0|32767] "rpm" XXX
SG_ NEW_SIGNAL_5 : 40|1@0+ (1,0) [0|1] "" XXX
SG_ LCA_5_STEER : 54|15@0- (0.05596,0) [0|32767] "deg" XXX
SG_ NEW_SIGNAL_2 : 55|1@0+ (1,0) [0|1] "" XXX
BO_ 104 SPEED_2: 8 XXX
SG_ WHEEL_SPEED_LEFT : 6|15@0+ (1,0) [0|32767] "" XXX
SG_ NEW_SIGNAL_1 : 7|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_2 : 23|12@0+ (1,0) [0|4095] "" XXX
SG_ NEW_SIGNAL_3 : 27|4@0+ (1,0) [0|15] "" XXX
SG_ WHEEL_SPEED_RIGHT : 39|15@0+ (1,0) [0|32767] "" XXX
SG_ COUNTER_1 : 51|12@0+ (1,0) [0|4095] "" XXX
SG_ NEW_SIGNAL_4 : 53|1@0+ (1,0) [0|1] "" XXX
SG_ COUNTER_2 : 54|1@0+ (1,0) [0|1] "" XXX
BO_ 105 LCA_2: 8 XXX
SG_ BYTE_0 : 7|8@0+ (1,0) [0|2047] "" XXX
SG_ COUNTER_1 : 11|4@0+ (1,0) [0|4095] "" XXX
SG_ PILOT_ASSIST_ENGAGED : 12|1@0+ (1,0) [0|1] "" XXX
SG_ ESC_ACTUATING : 13|1@0+ (1,0) [0|1] "" XXX
SG_ ESC_ELIGIBLE : 14|1@0+ (1,0) [0|1] "" XXX
SG_ BYTE_1_BITFIELD_0 : 15|1@0+ (1,0) [0|7] "" XXX
SG_ CHECKSUM_2 : 23|8@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_2 : 31|16@0+ (1,0) [0|65535] "" XXX
SG_ COUNTER_2 : 43|4@0+ (1,0) [0|4095] "" XXX
SG_ NEW_SIGNAL_3 : 45|2@0+ (1,0) [0|3] "" XXX
SG_ BRAKE_PEDAL_PRESSED_B : 46|1@0+ (1,0) [0|1] "" XXX
SG_ BRAKE_PEDAL_PRESSED_A : 47|1@0+ (-1,1) [0|1] "" XXX
SG_ CHECKSUM_1 : 55|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_7 : 63|8@0+ (1,0) [0|255] "" XXX
BO_ 128 GEAR_POSITION: 8 XXX
SG_ NEW_SIGNAL_3 : 3|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_2 : 4|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_1 : 7|3@0+ (1,0) [0|7] "" XXX
SG_ NEW_SIGNAL_5 : 11|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_4 : 15|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_17 : 17|2@0+ (1,0) [0|3] "" XXX
SG_ AEB_A : 18|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_18 : 19|1@0+ (1,0) [0|1] "" XXX
SG_ AEB_B : 20|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_6 : 23|3@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_7 : 24|8@1+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_8 : 39|8@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_13 : 43|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_10 : 46|3@0+ (1,0) [0|7] "" XXX
SG_ NEW_SIGNAL_9 : 47|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_14 : 51|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_12 : 54|3@0+ (1,0) [0|7] "" XXX
SG_ NEW_SIGNAL_11 : 55|1@0+ (1,0) [0|1] "" XXX
SG_ GEAR_POSITION : 58|3@0+ (1,0) [0|7] "" XXX
SG_ NEW_SIGNAL_16 : 62|4@0+ (1,0) [0|15] "" XXX
SG_ NEW_SIGNAL_15 : 63|1@0+ (1,0) [0|1] "" XXX
BO_ 144 LCA_4: 8 XXX
SG_ BYTE_0 : 7|8@0+ (1,0) [0|16383] "" XXX
SG_ LCA_ENABLE : 9|2@0+ (1,0) [0|3] "" XXX
SG_ BYTE_1_FLAGS : 11|2@0+ (1,0) [0|3] "" XXX
SG_ BYTE_1_NIBBLE_HI : 15|4@0+ (1,0) [0|63] "" XXX
SG_ BYTE_2_3 : 23|16@0- (1,0) [0|65535] "" XXX
SG_ YAW_RATE : 39|16@0- (1,0) [0|65535] "" XXX
SG_ BYTE_6 : 55|8@0+ (1,0) [0|255] "" XXX
SG_ BYTE_7_NIBBLE_LO : 59|4@0+ (1,0) [0|15] "" XXX
SG_ BYTE_7_NIBBLE_HI : 60|4@1+ (1,0) [0|15] "" XXX
BO_ 146 LCA_7: 8 XXX
SG_ NEW_SIGNAL_1 : 7|12@0+ (1,0) [0|4095] "" XXX
SG_ NEW_SIGNAL_2 : 11|4@0+ (1,0) [0|15] "" XXX
SG_ LCA_7_STEER : 23|15@0- (1,0) [0|32767] "" XXX
SG_ NEW_SIGNAL_3 : 24|2@0+ (1,0) [0|3] "" XXX
SG_ LCA_7_DELTA_STEER : 38|15@0- (1,0) [0|32767] "" XXX
SG_ NEW_SIGNAL_4 : 55|16@0+ (1,0) [0|65535] "" XXX
BO_ 151 LCA_6: 8 XXX
SG_ LCA_6_STEER : 7|16@0- (1,0) [0|65535] "" XXX
SG_ NEW_SIGNAL_1 : 23|14@0- (1,0) [0|16383] "" XXX
SG_ NEW_SIGNAL_3 : 25|2@0+ (1,0) [0|3] "" XXX
SG_ NEW_SIGNAL_2 : 39|12@0+ (1,0) [0|4095] "" XXX
SG_ NEW_SIGNAL_4 : 43|4@0+ (1,0) [0|15] "" XXX
SG_ LCA_6_STEER_2 : 55|15@0- (1,0) [0|32767] "" XXX
SG_ NEW_SIGNAL_5 : 56|1@0+ (1,0) [0|1] "" XXX
BO_ 336 NEW_MSG_150: 8 XXX
SG_ IGN_DRAFT : 5|2@0+ (1,0) [0|3] "" XXX
BO_ 1336 NEW_MSG_538: 8 XXX
SG_ SWM_MULTIMEDIA_ACTIVITY : 22|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_1 : 23|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_3 : 24|1@0+ (1,0) [0|1] "" XXX
SG_ NEW_SIGNAL_2 : 29|1@0+ (1,0) [0|1] "" XXX
CM_ BO_ 23 "Might be related to PSCM 0x16";
CM_ SG_ 87 LCA_ACCEPT_COMMANDS_RELATED "Goes to 15 when actively operating LCA";
CM_ SG_ 87 LCA_ACCEPT_COMMANDS_INV "Goes to 0 when PSCM allowed to receive LCA commands";
CM_ SG_ 88 LCA_ENABLE_INV "Master enable, active-low";
CM_ SG_ 103 WHEEL_SPEED_1 "Possible Front Left (FL)";
CM_ SG_ 103 WHEEL_SPEED_2 "Possible Front Right (FR)";
CM_ SG_ 105 ESC_ACTUATING "Goes to 1 in combination with ESC_ELIGIBLE when ESC is actively actuating";
CM_ SG_ 105 ESC_ELIGIBLE "Goes to 1 on e.g. speed bump, but doesn't trigger any intervention";
VAL_ 128 GEAR_POSITION 0 "Park" 1 "Reverse" 2 "Neutral" 3 "Drive" 4 "B-Mode";
@@ -34,6 +34,7 @@
#define SAFETY_RIVIAN 33U
#define SAFETY_VOLKSWAGEN_MEB 34U
#define SAFETY_TESLA_PREAP 35U
#define SAFETY_VOLVO 36U
#define GET_BIT(msg, b) ((bool)!!(((msg)->data[((b) / 8U)] >> ((b) % 8U)) & 0x1U))
#define GET_FLAG(value, mask) (((value) & (mask)) == (mask))
@@ -380,4 +381,5 @@ extern const safety_hooks volkswagen_mqb_hooks;
extern const safety_hooks volkswagen_pq_hooks;
extern const safety_hooks rivian_hooks;
extern const safety_hooks psa_hooks;
extern const safety_hooks volvo_hooks;
extern const safety_hooks tesla_preap_hooks;
+9
View File
@@ -559,6 +559,7 @@ static safety_config gm_init(uint16_t param) {
const uint16_t GM_PARAM_REMOTE_START_BOOTS_COMMA = 8192;
const uint16_t GM_PARAM_PANDA_3D1_SCHED = 16384;
const uint16_t GM_PARAM_PANDA_PADDLE_SCHED = 32768U;
const uint16_t GM_PARAM_VOLT_CC_GATEWAY = 16384U;
static const LongitudinalLimits GM_ASCM_LONG_LIMITS = {
.max_gas = 8191,
@@ -706,6 +707,11 @@ static safety_config gm_init(uint16_t param) {
{0xBD, 0, 7, .check_relay = false}, {0x1F5, 0, 8, .check_relay = false},
{0x184, 2, 8, .check_relay = false}, {0x1E1, 2, 7, .check_relay = false}}; // camera bus
static const CanMsg GM_CC_LONG_ASCM_TX_MSGS[] = {{0x180, 0, 4, .check_relay = true}, {0x409, 0, 7, .check_relay = false},
{0x40A, 0, 7, .check_relay = false}, {0x370, 0, 6, .check_relay = false},
{0x1E1, 0, 7, .check_relay = false}, {0x3D1, 0, 8, .check_relay = false},
{0xBD, 0, 7, .check_relay = false}, {0x1F5, 0, 8, .check_relay = false}};
gm_hw = GET_FLAG(param, GM_PARAM_HW_CAM) ? GM_CAM : GM_ASCM;
gm_sdgm = GET_FLAG(param, GM_PARAM_HW_SDGM);
gm_ascm_int = GET_FLAG(param, GM_PARAM_HW_ASCM_INT);
@@ -714,6 +720,7 @@ static safety_config gm_init(uint16_t param) {
gm_cc_long = GET_FLAG(param, GM_PARAM_CC_LONG);
gm_has_acc = !GET_FLAG(param, GM_PARAM_NO_ACC);
gm_pedal_long = GET_FLAG(param, GM_PARAM_PEDAL_LONG);
const bool gm_volt_cc_gateway = GET_FLAG(param, GM_PARAM_VOLT_CC_GATEWAY) && gm_no_camera && !gm_pedal_long && !gm_has_acc;
enable_gas_interceptor = GET_FLAG(param, GM_PARAM_PEDAL_INTERCEPTOR);
gm_force_ascm = GET_FLAG(param, GM_PARAM_HW_ASCM_LONG);
gm_force_brake_c9 = GET_FLAG(param, GM_PARAM_FORCE_BRAKE_C9);
@@ -781,6 +788,8 @@ static safety_config gm_init(uint16_t param) {
} else {
ret = BUILD_SAFETY_CFG(gm_rx_checks, GM_SDGM_TX_MSGS);
}
} else if (gm_cc_long && gm_volt_cc_gateway && (gm_hw == GM_ASCM) && !gm_sdgm) {
ret = BUILD_SAFETY_CFG(gm_rx_checks, GM_CC_LONG_ASCM_TX_MSGS);
} else if ((gm_hw == GM_CAM) || gm_sdgm) {
// FIXME: cppcheck thinks that gm_cam_long is always false. This is not true
// if ALLOW_DEBUG is defined but cppcheck is run without ALLOW_DEBUG
@@ -51,6 +51,9 @@ bool hyundai_has_lda_button = false;
extern bool hyundai_aol_lkas_on_engage;
bool hyundai_aol_lkas_on_engage = false;
extern bool hyundai_aol_main_lkas_on_engage;
bool hyundai_aol_main_lkas_on_engage = false;
extern bool hyundai_non_scc;
bool hyundai_non_scc = false;
@@ -78,6 +81,7 @@ void hyundai_common_init(uint16_t param) {
const uint16_t HYUNDAI_PARAM_ALT_LIMITS_2 = 512;
const int HYUNDAI_PARAM_HAS_LDA_BUTTON = 1024;
const uint16_t HYUNDAI_PARAM_AOL_MAIN_LKAS_ON_ENGAGE = 128;
const uint16_t HYUNDAI_PARAM_AOL_LKAS_ON_ENGAGE = 2048;
const uint16_t HYUNDAI_PARAM_NON_SCC = 4096;
const uint16_t HYUNDAI_PARAM_CAN_CANFD_BLENDED = 8192;
@@ -94,6 +98,7 @@ void hyundai_common_init(uint16_t param) {
hyundai_can_canfd_blended = GET_FLAG(param, HYUNDAI_PARAM_CAN_CANFD_BLENDED);
hyundai_has_lda_button = GET_FLAG(param, HYUNDAI_PARAM_HAS_LDA_BUTTON);
hyundai_aol_main_lkas_on_engage = GET_FLAG(param, HYUNDAI_PARAM_AOL_MAIN_LKAS_ON_ENGAGE);
hyundai_aol_lkas_on_engage = GET_FLAG(param, HYUNDAI_PARAM_AOL_LKAS_ON_ENGAGE);
hyundai_non_scc = GET_FLAG(param, HYUNDAI_PARAM_NON_SCC);
hyundai_cancel_button_enable = GET_FLAG(param, HYUNDAI_PARAM_CANCEL_BTN_ENABLE);
@@ -165,7 +170,12 @@ void hyundai_common_cruise_buttons_check(const int cruise_button, const bool mai
if (main_button && !main_button_prev) {
if (!hyundai_aol_main_lkas_sync) {
acc_main_on = !acc_main_on;
const bool main_turning_on = !acc_main_on;
acc_main_on = main_turning_on;
if (main_turning_on && hyundai_aol_main_lkas_on_engage &&
((alternative_experience & ALT_EXP_ALWAYS_ON_LATERAL) != 0)) {
lkas_on = true;
}
}
}
main_button_prev = main_button;
+32 -3
View File
@@ -41,6 +41,7 @@
#define MSG_SUBARU_ES_HighBeamAssist 0x121U
#define MSG_SUBARU_ES_STATIC_1 0x22aU
#define MSG_SUBARU_ES_STATIC_2 0x325U
#define MSG_SUBARU_Dashlights 0x390U
#define SUBARU_MAIN_BUS 0U
#define SUBARU_ALT_BUS 1U
@@ -61,6 +62,9 @@
{MSG_SUBARU_ES_LKAS_State, bus, 8, .check_relay = true}, \
{MSG_SUBARU_ES_Infotainment, bus, 8, .check_relay = true}, \
#define SUBARU_STOP_START_TX_MSGS(bus) \
{MSG_SUBARU_Dashlights, bus, 8, .check_relay = false}, \
#define SUBARU_COMMON_LONG_TX_MSGS(alt_bus) \
{MSG_SUBARU_ES_Distance, alt_bus, 8, .check_relay = true}, \
{MSG_SUBARU_ES_Brake, alt_bus, 8, .check_relay = true}, \
@@ -108,6 +112,7 @@ static bool subaru_stop_and_go = false;
static bool subaru_lkas_angle = false;
static bool subaru_d_platform = false;
static bool subaru_fixed_angle_limits = false;
static bool subaru_stop_start_button = false;
static uint32_t subaru_get_checksum(const CANPacket_t *msg) {
return (uint8_t)msg->data[0];
@@ -285,6 +290,13 @@ static bool subaru_tx_hook(const CANPacket_t *msg) {
violation |= !(is_tester_present || is_button_rdbi);
}
if (msg->addr == MSG_SUBARU_Dashlights) {
violation |= !subaru_stop_start_button;
violation |= msg->bus != (subaru_d_platform ? SUBARU_ALT_BUS : SUBARU_MAIN_BUS);
violation |= !GET_BIT(msg, 54U);
violation |= subaru_get_checksum(msg) != subaru_compute_checksum(msg);
}
if (violation){
tx = false;
}
@@ -329,11 +341,23 @@ static safety_config subaru_init(uint16_t param) {
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
};
static const CanMsg SUBARU_GEN2_LKAS_ANGLE_STOP_START_TX_MSGS[] = {
SUBARU_BASE_TX_MSGS(SUBARU_ALT_BUS, MSG_SUBARU_ES_LKAS_ANGLE)
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
SUBARU_STOP_START_TX_MSGS(SUBARU_MAIN_BUS)
};
static const CanMsg SUBARU_D_PLATFORM_ANGLE_MAIN_TX_MSGS[] = {
SUBARU_D_PLATFORM_ANGLE_TX_MSGS(SUBARU_MAIN_BUS)
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
};
static const CanMsg SUBARU_D_PLATFORM_ANGLE_STOP_START_MAIN_TX_MSGS[] = {
SUBARU_D_PLATFORM_ANGLE_TX_MSGS(SUBARU_MAIN_BUS)
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
SUBARU_STOP_START_TX_MSGS(SUBARU_ALT_BUS)
};
static const CanMsg SUBARU_D_PLATFORM_ANGLE_CAMERA_TX_MSGS[] = {
SUBARU_D_PLATFORM_ANGLE_TX_MSGS(SUBARU_CAM_BUS)
SUBARU_COMMON_TX_MSGS(SUBARU_ALT_BUS)
@@ -378,6 +402,9 @@ static safety_config subaru_init(uint16_t param) {
const uint16_t SUBARU_PARAM_FIXED_ANGLE_LIMITS = 128;
subaru_fixed_angle_limits = GET_FLAG(param, SUBARU_PARAM_FIXED_ANGLE_LIMITS);
const uint16_t SUBARU_PARAM_STOP_START_BUTTON = 256;
subaru_stop_start_button = GET_FLAG(param, SUBARU_PARAM_STOP_START_BUTTON);
#ifdef ALLOW_DEBUG
const uint16_t SUBARU_PARAM_LONGITUDINAL = 2;
subaru_longitudinal = GET_FLAG(param, SUBARU_PARAM_LONGITUDINAL);
@@ -385,9 +412,11 @@ static safety_config subaru_init(uint16_t param) {
safety_config ret;
if (subaru_lkas_angle) {
ret = subaru_d_platform ? (subaru_d_platform_camera ? BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_CAMERA_TX_MSGS) : \
BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_MAIN_TX_MSGS)) : \
subaru_gen2 ? BUILD_SAFETY_CFG(subaru_gen2_lkas_angle_rx_checks, SUBARU_GEN2_LKAS_ANGLE_TX_MSGS) : \
ret = subaru_d_platform ? (subaru_stop_start_button ? BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_STOP_START_MAIN_TX_MSGS) : \
(subaru_d_platform_camera ? BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_CAMERA_TX_MSGS) : \
BUILD_SAFETY_CFG(subaru_d_platform_angle_rx_checks, SUBARU_D_PLATFORM_ANGLE_MAIN_TX_MSGS))) : \
subaru_gen2 ? (subaru_stop_start_button ? BUILD_SAFETY_CFG(subaru_gen2_lkas_angle_rx_checks, SUBARU_GEN2_LKAS_ANGLE_STOP_START_TX_MSGS) : \
BUILD_SAFETY_CFG(subaru_gen2_lkas_angle_rx_checks, SUBARU_GEN2_LKAS_ANGLE_TX_MSGS)) : \
BUILD_SAFETY_CFG(subaru_lkas_angle_rx_checks, SUBARU_LKAS_ANGLE_TX_MSGS);
} else if (subaru_gen2) {
ret = subaru_longitudinal ? BUILD_SAFETY_CFG(subaru_gen2_rx_checks, SUBARU_GEN2_LONG_TX_MSGS) : \
+341
View File
@@ -0,0 +1,341 @@
#pragma once
#include "opendbc/safety/declarations.h"
// safetyParam: 0 = CMA (XC40 Recharge), 1 = SPA (S60 Recharge, Polestar 2)
// Polestar 2 is technically CMA, but appears to use SPA DBC for CAN 1 bus
#define VOLVO_FLAG_SPA 1U
// Volvo CAN message addresses shared between CMA and SPA
#define VOLVO_LCA_STEER 0x58U // TX from VCU1 to PSCM, LCA steering command (0x58)
#define VOLVO_LCA_2 0x69U // RX from BCM, brake pedal, cruise state
#define VOLVO_SAS 0x55U // RX from SAS, steering angle sensor
#define VOLVO_PSCM 0x16U // RX from PSCM, driver steering input
#define VOLVO_GEAR_POSITION 0x80U // RX from transmission, gear position
#define VOLVO_DRIVER_INPUT 0x15U
#define VOLVO_LCA_3 0x57U // TX from VCU1 to PSCM
#define VOLVO_LCA_5 0x67U // TX LCA_5 message (formerly SPEED_1, contains wheel speeds and LCA signals)
#define VOLVO_SPEED 0x60U // RX/TX SPEED message
#define VOLVO_SPEED_2 0x68U // RX
#define VOLVO_0x1a 0x1aU // RX
#define VOLVO_EGSM 0x45U // RX from EGSM
#define VOLVO_PSCM_RELATED 0x17U // RX from PSCM, related messages
#define VOLVO_LCA_4 0x90U // TX LCA_4 message (PA status spoofing)
#define VOLVO_LCA_6 0x97U // TX LCA_6 message
#define VOLVO_LCA_7 0x92U // TX LCA_7 message
// CMA-specific PT bus addresses
#define VOLVO_CMA_BUS1_SPEED 0x70U // RX vehicle speed
#define VOLVO_CMA_ECM_1 0x250U // RX accelerator pedal position
#define VOLVO_CMA_BUS1_CRUISE_CONTROL 0x340U // RX cruise control state
// SPA-specific PT bus addresses
#define VOLVO_SPA_BUS1_SPEED 0x75U // RX vehicle speed
#define VOLVO_SPA_ECM_1 0x25U // RX accelerator pedal position
#define VOLVO_SPA_BUS1_CRUISE_CONTROL 0x349U // RX cruise control state
// SPEED (0x60) is raw counts in the DBC. Measured against GPS ground speed on two
// harnesses: implied LSB 0.0039736 and 0.0039792 m/s.
#define VOLVO_SPEED_TO_MS 0.003977f
// LCA_5_STEER is a signed 15-bit steering-wheel-angle command in 0.05596 deg/count.
// Keep the absolute envelope aligned with the software controller's 540 deg limit.
#define VOLVO_ANGLE_DEG_TO_CAN 17.869907f
#define VOLVO_MAX_ANGLE_CAN 9650
#define VOLVO_RELAY_ANGLE_TOLERANCE 54 // approximately 3 degrees
#define VOLVO_DRIVER_OVERRIDE 2
// CAN bus definitions for Volvo
// Using same naming as carstate.py for consistency: main, pt, party
#define VOLVO_MAIN_BUS 0U // Bus.main - VCU1 car side
#define VOLVO_PT_BUS 1U // Bus.pt - VCU1 ECM side (where ECM is)
#define VOLVO_PARTY_BUS 2U // Bus.party - VCU PSCM/BCM2 side (BCM2, SAS, EGSM, PSCM, where LCA is sent to)
// Runtime addresses set by volvo_init based on safetyParam
static uint16_t volvo_ecm_1_addr;
static uint16_t volvo_bus1_cruise_control_addr;
static int volvo_be_15(const CANPacket_t *msg, uint8_t byte) {
return (int)(((uint16_t)(msg->data[byte] & 0x7FU) << 8U) | msg->data[byte + 1U]);
}
static int volvo_pscm_angle(const CANPacket_t *msg) {
return to_signed(volvo_be_15(msg, 0U), 15);
}
static int volvo_lca_5_angle(const CANPacket_t *msg) {
return to_signed(volvo_be_15(msg, 6U), 15);
}
static const AngleSteeringLimits VOLVO_ANGLE_STEERING_LIMITS = {
.max_angle = VOLVO_MAX_ANGLE_CAN,
.angle_deg_to_can = VOLVO_ANGLE_DEG_TO_CAN,
.angle_rate_up_lookup = {
{0.0f, 5.0f, 25.0f},
{2.5f, 1.5f, 0.2f},
},
.angle_rate_down_lookup = {
{0.0f, 5.0f, 25.0f},
{5.0f, 2.0f, 0.3f},
},
.frequency = 50U,
};
static void volvo_rx_hook(const CANPacket_t *msg) {
// Monitor the vehicle state required for cruise, disengagement, and angle
// safety. All steering TX frames are separately constrained in volvo_tx_hook.
// Main bus (bus 0) messages
if (msg->bus == VOLVO_MAIN_BUS) {
// Update brake pedal and cruise state from BCM2
if (msg->addr == VOLVO_LCA_2) {
// DBC: SG_ BRAKE_PEDAL_PRESSED_A : 47|1@0+ (-1,1) - inverted in DBC, so we invert raw bit
// DBC: SG_ BRAKE_PEDAL_PRESSED_B : 46|1@0+ (1,0) - not inverted
//bool brake_a = !((msg->data[5] >> 7) & 1U); // Raw bit, active low (DBC inverts it)
bool brake_b = (msg->data[5] >> 6) & 1U; // Raw bit, active high
//brake_pressed = brake_a || brake_b;
brake_pressed = brake_b;
}
// Vehicle speed from the main bus, matching carstate.py. The PT bus carries a
// speed message too, but which car bus lands on PT is harness-dependent and its
// scaling differs per PT DBC, so both sides read the main bus instead.
// DBC: SG_ SPEED : 6|15@0+ (1,0) - raw counts, scaled here
if (msg->addr == VOLVO_SPEED) {
uint16_t speed_raw = ((msg->data[0] & 0x7FU) << 8) | msg->data[1];
float speed = (float)speed_raw * VOLVO_SPEED_TO_MS;
vehicle_moving = speed > 0.1;
UPDATE_VEHICLE_SPEED(speed);
}
}
// PT bus (bus 1) messages
if (msg->bus == VOLVO_PT_BUS) {
if (msg->addr == volvo_ecm_1_addr) {
if (volvo_ecm_1_addr == VOLVO_CMA_ECM_1) {
// CMA: SG_ ACCELERATOR_PEDAL_POS : 31|8@0+ (1,0) [0|255]
uint8_t gas_pedal_position = msg->data[3];
gas_pressed = gas_pedal_position > 21U; // 20 baseline + 1 tolerance
} else {
// SPA: SG_ ACCELERATOR_PEDAL_POS : 6|15@0+ (0.00390625,0) [0|32767] "%"
uint16_t gas_raw = ((msg->data[0] & 0x7FU) << 8) | msg->data[1];
gas_pressed = (gas_raw * 0.00390625) > 1.0; // > 1%
}
}
if (msg->addr == volvo_bus1_cruise_control_addr) {
bool cruise_enabled;
if (volvo_bus1_cruise_control_addr == VOLVO_CMA_BUS1_CRUISE_CONTROL) {
// CMA: SG_ CRUISE_CONTROL_ENABLED : 56|1@0+ and CRUISE_CONTROL_ENABLED_IDLE_TRAFFIC : 57|1@0+
cruise_enabled = ((msg->data[7] & 1U) || (msg->data[7] & 2U));
} else {
// SPA: SG_ CRUISE_CONTROL_SPA_ENABLED : 1|1@0+ (-1,1) — byte 0 bit 1, active low
cruise_enabled = !((msg->data[0] >> 1) & 1U);
}
pcm_cruise_check(cruise_enabled);
}
}
// Party bus (bus 2) messages - BCM2, SAS, PSCM, EGSM
if (msg->bus == VOLVO_PARTY_BUS) {
if (msg->addr == VOLVO_PSCM) {
// PSCM_ANGLE_SENSOR is the measurement consumed by carstate.py. It uses
// the same signed 0.05596 deg/count representation as LCA_5_STEER.
update_sample(&angle_meas, volvo_pscm_angle(msg));
}
// DRIVER_INPUT is the signal consumed by carstate.py for driver torque.
// The PSCM frame's DRIVER_INPUT_DEVIATION is a different signal and must
// not be substituted here: doing so leaves the hardware disengage path blind.
if (msg->addr == VOLVO_DRIVER_INPUT) {
// STEERING_DRIVER_INPUT is a Motorola signal starting at bit 55. The
// DBC also carries a +1 offset, so its raw byte is data[6].
const int driver_input = to_signed(msg->data[6], 8) + 1;
update_sample(&torque_driver, driver_input);
steering_disengage = SAFETY_ABS(driver_input) > VOLVO_DRIVER_OVERRIDE;
}
}
}
static bool volvo_tx_hook(const CANPacket_t *msg) {
bool tx = true;
// LCA_5 carries the actual angle command used by the controller. The stock
// LCA frame also contains an angle-shaped field, but the imported controller
// deliberately leaves that field at the observed vehicle value.
if (msg->addr == VOLVO_LCA_5) {
const int desired_angle = volvo_lca_5_angle(msg);
tx &= SAFETY_ABS(desired_angle) <= VOLVO_MAX_ANGLE_CAN;
tx &= !steer_angle_cmd_checks(desired_angle, controls_allowed, VOLVO_ANGLE_STEERING_LIMITS);
}
// Keep the two torque-authority arms and the companion LCA angle bounded even
// though these fields are not the primary steering command.
if (msg->addr == VOLVO_LCA_STEER) {
const int authority_pos = to_signed((int)(((uint16_t)(msg->data[0] & 0x07U) << 8U) | msg->data[1]), 11);
const int authority_neg = to_signed((int)(((uint16_t)(msg->data[2] & 0x07U) << 8U) | msg->data[3]), 11);
const int lca_angle = to_signed((int)(((uint16_t)(msg->data[5] & 0x3FU) << 8U) | msg->data[6]), 14);
tx &= authority_pos >= 0 && authority_pos <= 614;
tx &= authority_neg >= -614 && authority_neg <= 0;
tx &= SAFETY_ABS(lca_angle) <= VOLVO_MAX_ANGLE_CAN;
}
// PSCM is relayed back onto the main bus to preserve the stock hands-on-wheel
// path. Do not allow that relay to invent a steering-angle measurement.
if (msg->addr == VOLVO_PSCM) {
const int relayed_angle = volvo_pscm_angle(msg);
const int measured_max = SAFETY_CLAMP(angle_meas.max + VOLVO_RELAY_ANGLE_TOLERANCE,
-VOLVO_MAX_ANGLE_CAN, VOLVO_MAX_ANGLE_CAN);
const int measured_min = SAFETY_CLAMP(angle_meas.min - VOLVO_RELAY_ANGLE_TOLERANCE,
-VOLVO_MAX_ANGLE_CAN, VOLVO_MAX_ANGLE_CAN);
tx &= !safety_max_limit_check(relayed_angle, measured_max, measured_min);
}
// NOTE: the wrong-bus rejections below are unreachable defense-in-depth:
// safety_tx_hook() only calls this hook after the message passes the
// VOLVO_TX_MSGS allowlist, which already pins each TX address to a single
// bus (and VOLVO_DRIVER_INPUT/VOLVO_SAS are not TX'able on any bus).
// Hence the GCOV_EXCL markers, following the defaults.h convention.
// test_volvo.py's test_tx_hook_wrong_bus_blocked pins down the blocked
// wrong-bus TX behavior at the safety_tx_hook() level.
if (msg->addr == VOLVO_LCA_STEER) {
// LCA message flows: VCU1 (main bus) -> PSCM (party bus)
// We're acting as VCU1, so we send LCA message to party bus (bus 2)
// GCOV_EXCL_START
// Unreachable by design (allowlist pins VOLVO_LCA_STEER to the party bus)
if (msg->bus != VOLVO_PARTY_BUS) {
tx = false; // Wrong bus
}
// GCOV_EXCL_STOP
}
if (msg->addr == VOLVO_PSCM) {
// PSCM message: we relay from party bus (bus 2) to main bus (bus 0)
// So we TX on main bus (bus 0)
// GCOV_EXCL_START
// Unreachable by design (allowlist pins VOLVO_PSCM to the main bus)
if (msg->bus != VOLVO_MAIN_BUS) {
tx = false; // Wrong bus
}
// GCOV_EXCL_STOP
}
if (msg->addr == VOLVO_DRIVER_INPUT) {
// Driver input message: we relay from party bus (bus 2) to main bus (bus 0)
// So we TX on main bus (bus 0)
// GCOV_EXCL_START
// Unreachable by design (VOLVO_DRIVER_INPUT is not in VOLVO_TX_MSGS)
if (msg->bus != VOLVO_MAIN_BUS) {
tx = false; // Wrong bus
}
}
// GCOV_EXCL_STOP
if (msg->addr == VOLVO_SAS) {
// SAS message: we relay from party bus (bus 2) to main bus (bus 0)
// So we TX on main bus (bus 0)
// GCOV_EXCL_START
// Unreachable by design (VOLVO_SAS is not in VOLVO_TX_MSGS)
if (msg->bus != VOLVO_MAIN_BUS) {
tx = false; // Wrong bus
}
}
// GCOV_EXCL_STOP
if (msg->addr == VOLVO_LCA_2) {
// LCA_2 -> PSCM
// GCOV_EXCL_START
// Unreachable by design (allowlist pins VOLVO_LCA_2 to the party bus)
if (msg->bus != VOLVO_PARTY_BUS) {
tx = false; // Wrong bus
}
// GCOV_EXCL_STOP
}
return tx;
}
static safety_config volvo_init(uint16_t param) {
bool spa = GET_FLAG(param, VOLVO_FLAG_SPA);
// Set PT bus addresses based on platform
volvo_ecm_1_addr = spa ? VOLVO_SPA_ECM_1 : VOLVO_CMA_ECM_1;
volvo_bus1_cruise_control_addr = spa ? VOLVO_SPA_BUS1_CRUISE_CONTROL : VOLVO_CMA_BUS1_CRUISE_CONTROL;
// Define the TX messages needed to replace the stock LCA path. Payload
// limits for steering and the PSCM relay are enforced in volvo_tx_hook.
static const CanMsg VOLVO_TX_MSGS[] = {
{VOLVO_LCA_STEER, VOLVO_PARTY_BUS, 8, .check_relay = true}, // LCA steering command to party bus
{VOLVO_PSCM, VOLVO_MAIN_BUS, 8, .check_relay = true}, // PSCM message sent to main bus (relay from party bus)
{VOLVO_LCA_3, VOLVO_PARTY_BUS, 8, .check_relay = true}, // LCA_3 message sent to party bus
{VOLVO_LCA_2, VOLVO_PARTY_BUS, 8, .check_relay = true}, // LCA_2 message sent to party bus (spoof PILOT_ASSIST_ENGAGED for PSCM)
{VOLVO_LCA_4, VOLVO_PARTY_BUS, 8, .check_relay = true}, // LCA_4 message sent to party bus (spoof LCA_ENABLE for PA state)
{VOLVO_LCA_5, VOLVO_PARTY_BUS, 8, .check_relay = true}, // LCA_5 message sent to party bus (wheel speeds + LCA signals)
{VOLVO_LCA_6, VOLVO_PARTY_BUS, 8, .check_relay = true}, // LCA_6 message sent to party bus
{VOLVO_LCA_7, VOLVO_PARTY_BUS, 8, .check_relay = true}, // LCA_7 message sent to party bus
//{VOLVO_SPEED, VOLVO_PARTY_BUS, 8, .check_relay = true}, // SPEED message sent to main bus
//{VOLVO_SPEED_2, VOLVO_PARTY_BUS, 8, .check_relay = true}, // SPEED_2 message sent to main bus
//{VOLVO_0x1a, VOLVO_PARTY_BUS, 8, .check_relay = true}, // 0x1a message sent to main bus
//{VOLVO_GEAR_POSITION, VOLVO_PARTY_BUS, 8, .check_relay = true}, // GEAR_POSITION message sent from main to party bus
//{VOLVO_EGSM, VOLVO_MAIN_BUS, 8, .check_relay = true}, // EGSM message sent from party to main bus
{VOLVO_PSCM_RELATED, VOLVO_MAIN_BUS, 8, .check_relay = true}, // PSCM_RELATED message sent to party bus
};
// Define RX checks - PT bus addresses depend on CMA vs SPA
safety_config ret;
if (!spa) {
static RxCheck volvo_rx_checks_cma[] = {
{.msg = {{VOLVO_GEAR_POSITION, VOLVO_MAIN_BUS, 8, 40U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_LCA_2, VOLVO_MAIN_BUS, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_LCA_4, VOLVO_MAIN_BUS, 8, 29U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_LCA_6, VOLVO_MAIN_BUS, 8, 25U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_LCA_7, VOLVO_MAIN_BUS, 8, 29U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_SAS, VOLVO_PARTY_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_PSCM, VOLVO_PARTY_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_DRIVER_INPUT, VOLVO_PARTY_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_CMA_ECM_1, VOLVO_PT_BUS, 8, 17U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_LCA_STEER, VOLVO_MAIN_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_CMA_BUS1_CRUISE_CONTROL, VOLVO_PT_BUS, 8, 10U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_LCA_3, VOLVO_MAIN_BUS, 8, 67U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_LCA_5, VOLVO_MAIN_BUS, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_SPEED, VOLVO_MAIN_BUS, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_SPEED_2, VOLVO_MAIN_BUS, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_EGSM, VOLVO_PARTY_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_PSCM_RELATED, VOLVO_PARTY_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
};
ret = BUILD_SAFETY_CFG(volvo_rx_checks_cma, VOLVO_TX_MSGS);
} else {
static RxCheck volvo_rx_checks_spa[] = {
{.msg = {{VOLVO_GEAR_POSITION, VOLVO_MAIN_BUS, 8, 40U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_LCA_2, VOLVO_MAIN_BUS, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_LCA_4, VOLVO_MAIN_BUS, 8, 29U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_LCA_6, VOLVO_MAIN_BUS, 8, 25U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_LCA_7, VOLVO_MAIN_BUS, 8, 29U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_SAS, VOLVO_PARTY_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_PSCM, VOLVO_PARTY_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_DRIVER_INPUT, VOLVO_PARTY_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_SPA_ECM_1, VOLVO_PT_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_LCA_STEER, VOLVO_MAIN_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
// true rate is 5Hz, but safety_tick invalidates checks declared <10Hz; lagging floor is 1s either way
{.msg = {{VOLVO_SPA_BUS1_CRUISE_CONTROL, VOLVO_PT_BUS, 8, 10U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_LCA_3, VOLVO_MAIN_BUS, 8, 67U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_LCA_5, VOLVO_MAIN_BUS, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_SPEED, VOLVO_MAIN_BUS, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_SPEED_2, VOLVO_MAIN_BUS, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_EGSM, VOLVO_PARTY_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{VOLVO_PSCM_RELATED, VOLVO_PARTY_BUS, 8, 100U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, { 0 }, { 0 }}},
};
ret = BUILD_SAFETY_CFG(volvo_rx_checks_spa, VOLVO_TX_MSGS);
}
return ret;
}
const safety_hooks volvo_hooks = {
.init = volvo_init,
.rx = volvo_rx_hook,
.tx = volvo_tx_hook,
// No custom fwd hook - stock LCA always blocked by .check_relay = true
};
+2
View File
@@ -27,6 +27,7 @@
#include "opendbc/safety/modes/elm327.h"
#include "opendbc/safety/modes/body.h"
#include "opendbc/safety/modes/psa.h"
#include "opendbc/safety/modes/volvo.h"
#ifdef CANFD
#include "opendbc/safety/modes/hyundai_canfd.h"
@@ -427,6 +428,7 @@ int set_safety_hooks(uint16_t mode, uint16_t param) {
{SAFETY_RIVIAN, &rivian_hooks},
{SAFETY_TESLA, &tesla_hooks},
{SAFETY_TESLA_PREAP, &tesla_preap_hooks},
{SAFETY_VOLVO, &volvo_hooks},
#ifdef CANFD
{SAFETY_HYUNDAI_CANFD, &hyundai_canfd_hooks},
{SAFETY_VOLKSWAGEN_MEB, &volkswagen_meb_hooks},
+7 -4
View File
@@ -1075,13 +1075,14 @@ class SafetyTest(SafetyTestBase):
continue
if attr.startswith('TestSubaruGen') and current_test.startswith('TestSubaruGen'):
continue
if 'TestSubaruDPlatformAngleSafety' in {attr, current_test} and \
'Angle' in attr and 'Angle' in current_test:
if attr.startswith('TestSubaruDPlatform') and current_test.startswith('TestSubaruDPlatform'):
continue
if 'TestSubaruDPlatformAngleSafety' in {attr, current_test}:
if attr.startswith('TestSubaruDPlatform'):
# D-platform uses the same main-bus HUD messages as the other
# Subaru modes, so those modes cannot be distinguished by ID.
tx = list(filter(lambda m: not (m[1] == 0 and m[0] in [0x321, 0x322, 0x323]), tx))
tx = list(filter(lambda m: not (m[1] == 0 and m[0] in [0x124, 0x321, 0x322, 0x323]), tx))
if current_test.startswith('TestSubaruDPlatform') and attr.startswith('TestSubaruGen'):
tx = list(filter(lambda m: not (m[1] == 0 and m[0] in [0x124, 0x321, 0x322, 0x323]), tx))
if attr.startswith('TestSubaruPreglobal') and current_test.startswith('TestSubaruPreglobal'):
continue
if {attr, current_test}.issubset({'TestVolkswagenPqSafety', 'TestVolkswagenPqStockSafety', 'TestVolkswagenPqLongSafety'}):
@@ -1101,6 +1102,7 @@ class SafetyTest(SafetyTestBase):
continue
if {attr, current_test}.issubset({'TestHyundaiLongitudinalSafety', 'TestHyundaiLongitudinalSafetyCameraSCC',
'TestHyundaiSafetyFCEVLong', 'TestHyundaiLongitudinalAolLkasOnEngageSafety',
'TestHyundaiLongitudinalAolMainLkasOnEngageSafety',
'TestHyundaiSafetyCanRefreshLong', 'TestHyundaiSafetyCanRefreshLongCameraSCC',
'TestHyundaiCanCanfdBlendedLongitudinalSafety',
'TestHyundaiLegacyLongitudinalSafety',
@@ -1156,6 +1158,7 @@ class SafetyTest(SafetyTestBase):
if attr.startswith('TestHyundaiLongitudinal') or attr in ('TestHyundaiSafetyFCEVLong',
'TestHyundaiLongitudinalAolLkasOnEngageSafety',
'TestHyundaiLongitudinalAolMainLkasOnEngageSafety',
'TestHyundaiCanCanfdBlendedLongitudinalSafety',
'TestHyundaiLegacyLongitudinalSafety',
'TestHyundaiLegacyLongitudinalSafetyHEV'):
@@ -59,6 +59,28 @@ def test_gm_panda_scheduler_paths():
safety.safety_rx_hook(prndl)
def test_gm_cc_longitudinal_ascm_safety_whitelist():
safety = libsafety_py.libsafety
flags = (GMSafetyFlags.FLAG_GM_NO_CAMERA | GMSafetyFlags.FLAG_GM_NO_ACC |
GMSafetyFlags.FLAG_GM_CC_LONG | GMSafetyFlags.FLAG_GM_VOLT_CC_GATEWAY)
assert safety.set_safety_hooks(CarParams.SafetyModel.gm, flags) == 0
safety.init_tests()
safety.set_controls_allowed(True)
safety.set_cruise_engaged_prev(True)
allowed = ((0x180, 0, 4), (0x409, 0, 7), (0x40A, 0, 7), (0x370, 0, 6),
(0x1E1, 0, 7), (0x3D1, 0, 8), (0xBD, 0, 7), (0x1F5, 0, 8))
for addr, bus, length in allowed:
data = b'\x00' * length
if addr == 0x1E1:
data = data[:5] + b'\x10' + data[6:]
assert safety.safety_tx_hook(common.make_msg(bus, addr, length, data))
blocked = ((0x184, 2, 8), (0x200, 0, 6), (0x2CB, 0, 8), (0x306, 1, 8))
for addr, bus, length in blocked:
assert not safety.safety_tx_hook(common.make_msg(bus, addr, length))
def test_gm_bolt_acc_pedal_clears_stock_cruise():
safety = libsafety_py.libsafety
flags = GMSafetyFlags.HW_CAM | GMSafetyFlags.FLAG_GM_GAS_INTERCEPTOR | GMSafetyFlags.FLAG_GM_BOLT_2022_PEDAL
@@ -622,6 +622,32 @@ class TestHyundaiLongitudinalAolLkasOnEngageSafety(HyundaiAolLkasOnEngageBase, T
self.safety.init_tests()
class TestHyundaiLongitudinalAolMainLkasOnEngageSafety(TestHyundaiLongitudinalSafety):
def setUp(self):
self.packer = CANPackerSafety("hyundai_kia_generic")
self.safety = libsafety_py.libsafety
self.safety.set_safety_hooks(
CarParams.SafetyModel.hyundai,
HyundaiSafetyFlags.LONG | HyundaiStarPilotSafetyFlags.AOL_MAIN_LKAS_ON_ENGAGE,
)
self.safety.init_tests()
def test_aol_lkas_auto_enables_on_main_engagement(self):
self.safety.set_alternative_experience(ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL)
self.safety.set_controls_allowed(False)
self._rx(self._button_msg(Buttons.NONE, main_button=1))
self._rx(self._button_msg(Buttons.NONE, main_button=0))
self.assertTrue(self.safety.get_acc_main_on())
self.assertTrue(self.safety.get_lkas_on())
self.assertTrue(self.safety.get_aol_allowed())
self._rx(self._user_brake_msg(True))
self.assertFalse(self.safety.get_controls_allowed())
self._set_prev_torque(0)
self.assertTrue(self._tx(self._torque_cmd_msg(self.MAX_RATE_UP)))
class TestHyundaiAolLkasOnEngageStockSafety(HyundaiAolLkasOnEngageStockBase, TestHyundaiSafety):
def setUp(self):
self.packer = CANPackerSafety("hyundai_kia_generic")
@@ -36,6 +36,7 @@ class SubaruMsg(enum.IntEnum):
ES_HighBeamAssist = 0x121
ES_STATIC_1 = 0x22a
ES_STATIC_2 = 0x325
Dashlights = 0x390
SUBARU_MAIN_BUS = 0
@@ -369,6 +370,21 @@ class TestSubaruGen2FixedAngleSafety(TestSubaruGen2AngleStockLongitudinalSafety)
raise unittest.SkipTest("Breakpoint angle limits do not enforce a real-time message frequency")
class TestSubaruGen2FixedAngleStopStartSafety(TestSubaruGen2FixedAngleSafety):
FLAGS = SubaruSafetyFlags.GEN2 | SubaruSafetyFlags.LKAS_ANGLE | SubaruSafetyFlags.FIXED_ANGLE_LIMITS | \
SubaruSafetyFlags.STOP_START_BUTTON
TX_MSGS = TestSubaruGen2FixedAngleSafety.TX_MSGS + [[SubaruMsg.Dashlights, SUBARU_MAIN_BUS]]
def _stop_start_msg(self, pressed):
return self.packer.make_can_msg_safety(
"Dashlights", SUBARU_MAIN_BUS, {"COUNTER": 0, "STOP_START": pressed},
)
def test_stop_start_tx_requires_pressed_bit(self):
self.assertTrue(self._tx(self._stop_start_msg(True)))
self.assertFalse(self._tx(self._stop_start_msg(False)))
class TestSubaruDPlatformAngleSafety(TestSubaruStockLongitudinalSafetyBase, TestSubaruAngleSafetyBase):
FLAGS = SubaruSafetyFlags.GEN2 | SubaruSafetyFlags.LKAS_ANGLE | SubaruSafetyFlags.D_PLATFORM
ALT_MAIN_BUS = SUBARU_ALT_BUS
@@ -401,6 +417,21 @@ class TestSubaruDPlatformAngleSafety(TestSubaruStockLongitudinalSafetyBase, Test
return self.packer.make_can_msg_safety("Steering_2", SUBARU_MAIN_BUS, {"Steering_Angle": angle})
class TestSubaruDPlatformStopStartSafety(TestSubaruDPlatformAngleSafety):
FLAGS = SubaruSafetyFlags.GEN2 | SubaruSafetyFlags.LKAS_ANGLE | SubaruSafetyFlags.D_PLATFORM | \
SubaruSafetyFlags.STOP_START_BUTTON
TX_MSGS = TestSubaruDPlatformAngleSafety.TX_MSGS + [[SubaruMsg.Dashlights, SUBARU_ALT_BUS]]
def _stop_start_msg(self, pressed):
return self.packer.make_can_msg_safety(
"Dashlights", SUBARU_ALT_BUS, {"COUNTER": 0, "STOP_START": pressed},
)
def test_stop_start_tx_requires_pressed_bit(self):
self.assertTrue(self._tx(self._stop_start_msg(True)))
self.assertFalse(self._tx(self._stop_start_msg(False)))
class TestSubaruDPlatformCameraAngleSafety(TestSubaruDPlatformAngleSafety):
FLAGS = SubaruSafetyFlags.GEN2 | SubaruSafetyFlags.LKAS_ANGLE | SubaruSafetyFlags.D_PLATFORM | SubaruSafetyFlags.D_PLATFORM_CAMERA
TX_MSGS = [[SubaruMsg.ES_LKAS_ANGLE, SUBARU_CAM_BUS],
@@ -0,0 +1,320 @@
#!/usr/bin/env python3
"""
Safety tests for Volvo CMA/SPA.
The safety mode lives at ``opendbc/safety/modes/volvo.h`` and is parameterized
by ``safetyParam``:
- ``safetyParam == 0`` CMA platform (Volvo XC40 Recharge)
- ``safetyParam == VOLVO_FLAG_SPA`` SPA platform (Volvo S60 Recharge,
Polestar 2)
The two platforms share LCA/PSCM/etc. addresses on the main and party buses
but use *different* PT-bus addresses and signal scales for ECM_1 and
BUS1_CRUISE_CONTROL. Vehicle speed is read from main-bus SPEED on both, so it
is not platform-dependent. This test file exercises both platforms through the
same generic ``CarSafetyTest`` harness so that any future divergence between
``carstate.py`` and ``volvo.h`` e.g. a threshold drifting out of sync is
caught on a laptop instead of in the car.
Companion to: ``opendbc/car/volvo/carstate.py`` (must agree on thresholds).
"""
import pathlib
import re
import unittest
from opendbc.car.volvo.interface import SAFETY_VOLVO
from opendbc.safety.tests.libsafety import libsafety_py
import opendbc.safety.tests.common as common
from opendbc.safety.tests.common import CANPackerSafety
# Must match VOLVO_FLAG_SPA in opendbc/safety/modes/volvo.h
VOLVO_FLAG_SPA = 1
# Must match VOLVO_SPEED_TO_MS in volvo.h and SPEED_TO_MS in carstate.py
VOLVO_SPEED_TO_MS = 0.003977
# Bus layout (must match volvo.h)
VOLVO_MAIN_BUS = 0
VOLVO_PT_BUS = 1
VOLVO_PARTY_BUS = 2
# Shared platform-independent addresses
VOLVO_SPEED = 0x60
VOLVO_LCA_STEER = 0x58
VOLVO_LCA_2 = 0x69
VOLVO_LCA_3 = 0x57
VOLVO_LCA_4 = 0x90
VOLVO_LCA_5 = 0x67
VOLVO_LCA_6 = 0x97
VOLVO_LCA_7 = 0x92
VOLVO_PSCM = 0x16
VOLVO_PSCM_RELATED = 0x17
VOLVO_SAS = 0x55
VOLVO_DRIVER_INPUT = 0x15
# Addresses panda TX'd with .check_relay = true, grouped by bus
_TX_PARTY_BUS_ADDRS = (
VOLVO_LCA_STEER, VOLVO_LCA_2, VOLVO_LCA_3, VOLVO_LCA_4,
VOLVO_LCA_5, VOLVO_LCA_6, VOLVO_LCA_7,
)
_TX_MAIN_BUS_ADDRS = (VOLVO_PSCM, VOLVO_PSCM_RELATED)
class TestVolvoSafetyBase(common.CarSafetyTest):
"""Shared RX/TX/relay tests for both CMA and SPA.
Subclasses pin down the PT-bus DBC and per-platform gas/cruise encoding.
"""
TX_MSGS = (
[[a, VOLVO_PARTY_BUS] for a in _TX_PARTY_BUS_ADDRS]
+ [[a, VOLVO_MAIN_BUS] for a in _TX_MAIN_BUS_ADDRS]
)
RELAY_MALFUNCTION_ADDRS = {
VOLVO_PARTY_BUS: _TX_PARTY_BUS_ADDRS,
VOLVO_MAIN_BUS: _TX_MAIN_BUS_ADDRS,
}
# volvo.h installs no custom fwd hook, so the panda uses the default main<->party
# forwarding (bus 0 <-> bus 2). For each TX addr with .check_relay=true, the panda
# automatically refuses to forward from the bus where stock traffic lives TO the
# bus where openpilot is TX'ing — that's what keeps the stock ECU from fighting us.
FWD_BLACKLISTED_ADDRS = {
VOLVO_MAIN_BUS: list(_TX_PARTY_BUS_ADDRS), # stock on 0 must not forward to 2
VOLVO_PARTY_BUS: list(_TX_MAIN_BUS_ADDRS), # stock on 2 must not forward to 0
}
STANDSTILL_THRESHOLD = 0.1 # m/s, matches volvo.h
# Subclasses set these
SAFETY_PARAM: int = 0
PT_DBC: str = ""
MID_DBC = "volvo_mid_1"
@classmethod
def setUpClass(cls):
if cls.__name__ == "TestVolvoSafetyBase":
cls.safety = None
raise unittest.SkipTest
super().setUpClass()
def setUp(self):
self.pt_packer = CANPackerSafety(self.PT_DBC)
self.mid_packer = CANPackerSafety(self.MID_DBC)
# CarSafetyTest.packer is referenced by a few generic tests; point it at
# the mid-bus DBC since most shared messages live there.
self.packer = self.mid_packer
self.safety = libsafety_py.libsafety
self.safety.set_safety_hooks(SAFETY_VOLVO, self.SAFETY_PARAM)
self.safety.init_tests()
def test_tx_hook_on_wrong_safety_mode(self):
# CMA and SPA intentionally share the party-bus TX allowlist. safetyParam
# selects the PT-bus RX encodings, not a different set of steering frames.
self.skipTest("CMA and SPA intentionally share Volvo TX IDs")
# ---- Abstract methods from CarSafetyTest ----
def _user_brake_msg(self, brake):
# volvo.h reads BRAKE_PEDAL_PRESSED_B (bit 46, factor 1, active high)
values = {"BRAKE_PEDAL_PRESSED_B": 1 if brake else 0}
return self.mid_packer.make_can_msg_safety("LCA_2", VOLVO_MAIN_BUS, values)
def _speed_msg(self, speed):
# Main-bus SPEED, the same message on both platforms. The DBC carries it as
# raw counts (factor 1) and volvo.h applies VOLVO_SPEED_TO_MS, so convert here.
values = {"SPEED": round(speed / VOLVO_SPEED_TO_MS)}
return self.mid_packer.make_can_msg_safety("SPEED", VOLVO_MAIN_BUS, values)
def _speed_msg_2(self, speed):
# Volvo's safety mode only consumes a single vehicle-speed source, so the
# generic rx_hook_speed_mismatch test doesn't apply. Returning None asks
# the harness to skip it (see common.CarSafetyTest.test_rx_hook_speed_mismatch).
return None
def _angle_meas_msg(self, angle):
return self.mid_packer.make_can_msg_safety(
"PSCM", VOLVO_PARTY_BUS, {"PSCM_ANGLE_SENSOR": angle})
def _angle_cmd_msg(self, angle):
return self.mid_packer.make_can_msg_safety(
"LCA_5", VOLVO_PARTY_BUS, {"LCA_5_STEER": angle})
def _reset_angle_measurement(self, angle):
for _ in range(common.MAX_SAMPLE_VALS):
self._rx(self._angle_meas_msg(angle))
def _reset_speed_measurement(self, speed):
for _ in range(common.MAX_SAMPLE_VALS):
self._rx(self._speed_msg(speed))
def test_angle_tx_is_bounded(self):
"""The LCA_5 angle command must obey the measured-angle safety envelope."""
self._reset_angle_measurement(0)
self._reset_speed_measurement(10)
self.safety.set_controls_allowed(True)
self.safety.set_desired_angle_last(0)
self.assertTrue(self._tx(self._angle_cmd_msg(0)))
# The software controller's ±540 degree envelope is also enforced by the
# panda. Set the previous command explicitly so this test isolates the
# absolute bound from the rate limiter.
angle_can = round(540 / 0.05596)
self.safety.set_desired_angle_last(angle_can)
self.assertTrue(self._tx(self._angle_cmd_msg(540)))
self.safety.set_desired_angle_last(0)
self.assertFalse(self._tx(self._angle_cmd_msg(541)))
def test_angle_tx_follows_measurement_when_controls_disabled(self):
self._reset_angle_measurement(10)
self.safety.set_controls_allowed(False)
self.assertTrue(self._tx(self._angle_cmd_msg(10)))
self.assertFalse(self._tx(self._angle_cmd_msg(20)))
def test_lca_authority_is_bounded(self):
self.safety.set_controls_allowed(True)
valid = {
"LCA_STEER_LOOSELY": 614,
"LCA_STEER_LOOSELY_INV": -614,
"LCA_STEER": 0,
}
self.assertTrue(self._tx(self.mid_packer.make_can_msg_safety(
"LCA", VOLVO_PARTY_BUS, valid)))
for key, value in (("LCA_STEER_LOOSELY", 615),
("LCA_STEER_LOOSELY_INV", -615)):
invalid = valid | {key: value}
self.assertFalse(self._tx(self.mid_packer.make_can_msg_safety(
"LCA", VOLVO_PARTY_BUS, invalid)))
def test_pscm_relay_cannot_invent_angle(self):
self._reset_angle_measurement(10)
valid = self.mid_packer.make_can_msg_safety(
"PSCM", VOLVO_MAIN_BUS, {"PSCM_ANGLE_SENSOR": 10})
invalid = self.mid_packer.make_can_msg_safety(
"PSCM", VOLVO_MAIN_BUS, {"PSCM_ANGLE_SENSOR": 20})
self.assertTrue(self._tx(valid))
self.assertFalse(self._tx(invalid))
def test_driver_override_disengages_controls(self):
self.safety.set_controls_allowed(True)
msg = self.mid_packer.make_can_msg_safety(
"DRIVER_INPUT", VOLVO_PARTY_BUS, {"STEERING_DRIVER_INPUT": 6})
self._rx(msg)
self.assertFalse(self.safety.get_controls_allowed())
# ---- Volvo-specific consistency tests ----
def test_tx_hook_wrong_bus_blocked(self):
"""
Every TX address is pinned to exactly one bus by the VOLVO_TX_MSGS
allowlist, which safety_tx_hook() enforces before volvo_tx_hook() runs.
TX attempts anywhere off the allowlisted bus must be rejected, even with
controls allowed. Also covers the relayed-RX addresses (SAS, DRIVER_INPUT)
that volvo_tx_hook() used to re-check: they are not TX'able on any bus.
"""
tx_msgs = {tuple(m) for m in self.TX_MSGS}
addrs = {addr for addr, _ in tx_msgs} | {VOLVO_SAS, VOLVO_DRIVER_INPUT}
self.safety.set_controls_allowed(True)
for addr in sorted(addrs):
for bus in range(4):
if (addr, bus) in tx_msgs:
self.assertTrue(self._tx(common.make_msg(bus, addr)),
f"blocked TX addr={hex(addr)} on allowlisted bus={bus}")
else:
self.assertFalse(self._tx(common.make_msg(bus, addr)),
f"allowed TX addr={hex(addr)} on wrong bus={bus}")
def test_gas_threshold_self_consistent(self):
"""
Sanity: the panda's gas_pressed edge is exactly where GAS_PRESSED_THRESHOLD
is defined. Regression guard for the CMA-vs-SPA scale mismatch bug where
carstate.py applied CMA thresholds (raw 0-255) to SPA's DBC-scaled percent.
If you change the threshold in volvo.h, update carstate.py AND this test.
"""
# Just below threshold → not pressed. Use a value strictly less than threshold.
just_below = max(self.GAS_PRESSED_THRESHOLD - 1, 0)
self._rx(self._user_gas_msg(just_below))
self.assertFalse(self.safety.get_gas_pressed_prev(),
f"gas flagged at {just_below} (threshold={self.GAS_PRESSED_THRESHOLD})")
# Just above threshold → pressed.
self._rx(self._user_gas_msg(self.GAS_PRESSED_THRESHOLD + 1))
self.assertTrue(self.safety.get_gas_pressed_prev(),
f"gas not flagged above {self.GAS_PRESSED_THRESHOLD}")
def test_speed_scale_self_consistent(self):
"""
The main-bus SPEED LSB is written out in three places: volvo.h, carstate.py
and this file. They must agree, or the panda and openpilot disagree about
vehicle speed. Parsed as text so this holds without importing either.
"""
root = pathlib.Path(__file__).parents[3]
sources = {
"volvo.h": (root / "opendbc/safety/modes/volvo.h",
r"#define\s+VOLVO_SPEED_TO_MS\s+([0-9.]+)f?"),
"carstate.py": (root / "opendbc/car/volvo/carstate.py",
r"^SPEED_TO_MS\s*=\s*([0-9.]+)"),
}
for name, (path, pattern) in sources.items():
m = re.search(pattern, path.read_text(), re.MULTILINE)
self.assertIsNotNone(m, f"could not find the speed LSB in {name}")
self.assertEqual(float(m.group(1)), VOLVO_SPEED_TO_MS,
f"{name} speed LSB disagrees with test_volvo.py")
class TestVolvoCMA(TestVolvoSafetyBase):
"""
Volvo CMA: XC40 Recharge.
ECM_1.ACCELERATOR_PEDAL_POS is raw 0-255 (DBC factor 1). Idle is ~20; panda
fires gas_pressed above 21. carstate.py uses the same raw threshold.
"""
SAFETY_PARAM = 0
PT_DBC = "volvo_front_1_cma"
GAS_PRESSED_THRESHOLD = 21 # raw counts
def _user_gas_msg(self, gas):
values = {"ACCELERATOR_PEDAL_POS": gas}
return self.pt_packer.make_can_msg_safety("ECM_1", VOLVO_PT_BUS, values)
def _pcm_status_msg(self, enable):
# CMA cruise is a pair of bools; either one being high enables cruise.
values = {"CRUISE_CONTROL_ENABLED": 1 if enable else 0,
"CRUISE_CONTROL_ENABLED_IDLE_TRAFFIC": 0}
return self.pt_packer.make_can_msg_safety(
"BUS1_CRUISE_CONTROL", VOLVO_PT_BUS, values)
class TestVolvoSPA(TestVolvoSafetyBase):
"""
Volvo SPA: S60 Recharge, Polestar 2.
ECM_1.ACCELERATOR_PEDAL_POS is DBC-scaled to percent (factor 0.00390625,
range 0-100%). Idle is ~0; panda fires gas_pressed above 1.0%.
carstate.py MUST use the same percent-based threshold see
opendbc/car/volvo/carstate.py. A mismatch here manifests in test_models.py
as persistent ``panda safety doesn't agree with openpilot: {'gasPressed': N}``.
"""
SAFETY_PARAM = VOLVO_FLAG_SPA
PT_DBC = "volvo_front_1_spa"
GAS_PRESSED_THRESHOLD = 1.0 # percent
def _user_gas_msg(self, gas):
values = {"ACCELERATOR_PEDAL_POS": gas}
return self.pt_packer.make_can_msg_safety("ECM_1", VOLVO_PT_BUS, values)
def _pcm_status_msg(self, enable):
# SPA cruise signal has DBC factor -1 offset 1, so the "enabled" DBC value
# is 1 (which encodes as raw bit 0 — panda inverts it).
values = {"CRUISE_CONTROL_SPA_ENABLED": 1 if enable else 0}
return self.pt_packer.make_can_msg_safety(
"BUS1_CRUISE_CONTROL", VOLVO_PT_BUS, values)
if __name__ == "__main__":
unittest.main()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,2 +1,2 @@
extern const uint8_t gitversion[19];
const uint8_t gitversion[19] = "DEV-f8d47139-DEBUG";
const uint8_t gitversion[19] = "DEV-289f5e9d-DEBUG";
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More