Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/CARS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!--- AUTOGENERATED FROM selfdrive/car/CARS_template.md, DO NOT EDIT. --->

# Support Information for 402 Known Cars
# Support Information for 403 Known Cars

|Make|Model|Package|Support Level|
|---|---|---|:---:|
Expand Down Expand Up @@ -263,6 +263,7 @@
|Nissan|Rogue 2018-20|ProPILOT Assist|[Upstream](#upstream)|
|Nissan|X-Trail 2017|ProPILOT Assist|[Upstream](#upstream)|
|Peugeot|208 2019-25|Adaptive Cruise Control (ACC) & Lane Assist|[Dashcam mode](#dashcam)|
|Peugeot|308 2018|Conventional cruise control|[Dashcam mode](#dashcam)|
|Porsche|Macan 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|[Dashcam mode](#dashcam)|
|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|Ram|2500 2020-24|Adaptive Cruise Control (ACC)|[Dashcam mode](#dashcam)|
Expand Down
11 changes: 9 additions & 2 deletions opendbc/car/psa/carcontroller.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from opendbc.can.packer import CANPacker
from opendbc.car import Bus
from opendbc.car import Bus, structs
from opendbc.car.lateral import apply_std_steer_angle_limits
from opendbc.car.interfaces import CarControllerBase
from opendbc.car.psa.psacan import create_lka_steering
from opendbc.car.psa.values import CarControllerParams
from opendbc.car.psa.values import CAR, CarControllerParams


class CarController(CarControllerBase):
Expand All @@ -12,8 +12,15 @@ def __init__(self, dbc_names, CP):
self.packer = CANPacker(dbc_names[Bus.main])
self.apply_angle_last = 0
self.status = 2
self.read_only = CP.carFingerprint == CAR.PSA_PEUGEOT_308_T9

def update(self, CC, CS, now_nanos):
if self.read_only:
self.frame += 1
# No CAN output, even if a caller explicitly requests lateral/longitudinal
# control. Report zero applied actuators rather than echoing the request.
return structs.CarControl.Actuators(), []

can_sends = []
actuators = CC.actuators

Expand Down
67 changes: 66 additions & 1 deletion opendbc/car/psa/carstate.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from opendbc.car import structs, Bus
from opendbc.can.parser import CANParser
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.psa.values import DBC, CarControllerParams
from opendbc.car.psa.values import CAR, DBC, CarControllerParams
from opendbc.car.interfaces import CarStateBase

GearShifter = structs.CarState.GearShifter

class CarState(CarStateBase):
def update(self, can_parsers) -> structs.CarState:
if self.CP.carFingerprint == CAR.PSA_PEUGEOT_308_T9:
return self._update_t9(can_parsers[Bus.main])

cp = can_parsers[Bus.main]
cp_adas = can_parsers[Bus.adas]
cp_cam = can_parsers[Bus.cam]
Expand Down Expand Up @@ -62,8 +65,70 @@ def update(self, can_parsers) -> structs.CarState:
ret.seatbeltUnlatched = cp_cam.vl['RESTRAINTS']['DRIVER_SEATBELT'] != 2
return ret

def _update_t9(self, cp) -> structs.CarState:
ret = structs.CarState()
self.parse_wheel_speeds(ret,
cp.vl['T9_WHEEL_SPEEDS_30D']['WheelSpeedFrontLeftKph'],
cp.vl['T9_WHEEL_SPEEDS_30D']['WheelSpeedFrontRightKph'],
cp.vl['T9_WHEEL_SPEEDS_30D']['WheelSpeedRearLeftKph'],
cp.vl['T9_WHEEL_SPEEDS_30D']['WheelSpeedRearRightKph'],
)
ret.standstill = ret.vEgoRaw < 0.1
ret.yawRate = cp.vl['T9_BRAKE_DYNAMICS_3CD']['YawRateDegS'] * CV.DEG_TO_RAD
ret.gasPressed = cp.vl['T9_ENGINE_DYNAMICS_208']['AcceleratorPositionPct'] > 0
ret.brakePressed = bool(cp.vl['T9_BODY_STATUS_412']['BrakePedalActive'])
# The 0x412 parking-brake bit never became active in the local corpus.
ret.parkingBrake = cp.vl['T9_EASY_MOVE_3AD']['ParkingBrakeState'] == 1

ret.steeringAngleDeg = cp.vl['T9_STEERING_DYNAMICS_305']['SteeringAngleDeg']
rate = cp.vl['T9_STEERING_DYNAMICS_305']['SteeringRateMagnitudeDegS']
rate_sign = cp.vl['T9_STEERING_DYNAMICS_305']['SteeringRateSign']
ret.steeringRateDeg = rate * (1 if rate_sign == 0 else -1)
ret.steeringTorque = cp.vl['T9_STEERING_TORQUE_2F5']['DriverTorqueRaw']
ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > CarControllerParams.T9_STEER_DRIVER_THRESHOLD_RAW, 5)

# 0x208 carries the persistent RVV state; 0x452 carries a request, not a latch.
cruise_mode = cp.vl['T9_CRUISE_SETPOINT_50E']['CruiseMode']
ret.cruiseState.available = cruise_mode == 1
ret.cruiseState.enabled = cruise_mode == 1 and cp.vl['T9_ENGINE_DYNAMICS_208']['CruiseStateCandidate'] == 2
setpoint = cp.vl['T9_CRUISE_SETPOINT_50E']['CruiseSetpointKph']
ret.cruiseState.speed = setpoint * CV.KPH_TO_MS if cruise_mode == 1 and setpoint < 255 else 0.
# Only the conventional-cruise reference vehicle is covered.
ret.cruiseState.nonAdaptive = True

reverse = bool(cp.vl['T9_BODY_STATUS_412']['ReverseGearActive'])
# The candidate gear field in 0x348 stays zero even in the moving capture.
# Only the independent reverse indication is used until gear is validated.
ret.gearShifter = GearShifter.reverse if reverse else GearShifter.unknown

blinker = cp.vl['T9_DRIVER_CRUISE_COMMAND_452']['TurnSignalStatus']
ret.leftBlinker = blinker in (2, 3)
ret.rightBlinker = blinker in (1, 3)
ret.doorOpen = any(cp.vl['T9_BODY_STATUS_412'][signal] for signal in (
'DriverDoorOpen', 'PassengerDoorOpen', 'RearLeftDoorOpen', 'RearRightDoorOpen',
))
ret.seatbeltUnlatched = cp.vl['T9_RESTRAINTS_572']['DriverSeatbeltState'] != 2
return ret

@staticmethod
def get_can_parsers(CP):
if CP.carFingerprint == CAR.PSA_PEUGEOT_308_T9:
# Only the observed live CAN stream is mapped to logical bus 0.
# A split camera/powertrain harness topology has not been validated.
messages = [
('T9_ENGINE_DYNAMICS_208', 100),
('T9_STEERING_TORQUE_2F5', 100),
('T9_STEERING_DYNAMICS_305', 100),
('T9_WHEEL_SPEEDS_30D', 50),
('T9_EASY_MOVE_3AD', 50),
('T9_BRAKE_DYNAMICS_3CD', 100),
('T9_BODY_STATUS_412', 20),
('T9_DRIVER_CRUISE_COMMAND_452', 20),
('T9_CRUISE_SETPOINT_50E', 10),
('T9_RESTRAINTS_572', 10),
]
return {Bus.main: CANParser(DBC[CP.carFingerprint][Bus.pt], messages, 0)}

return {
Bus.main: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 0),
Bus.adas: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 1),
Expand Down
74 changes: 74 additions & 0 deletions opendbc/car/psa/fingerprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,80 @@

Ecu = CarParams.Ecu

FINGERPRINTS = {
# Captured from the reference 308 T9 on the AEE2010 live bus. Diagnostic
# response 0x7E8 is intentionally excluded by openpilot fingerprinting.
CAR.PSA_PEUGEOT_308_T9: [{
0x072: 5,
0x1E8: 8,
0x208: 8,
0x228: 8,
0x2E8: 4,
0x2ED: 7,
0x2F5: 7,
0x305: 7,
0x30D: 8,
0x329: 4,
0x348: 8,
0x349: 8,
0x34D: 8,
0x389: 5,
0x38D: 8,
0x3AD: 8,
0x3B8: 7,
0x3C8: 2,
0x3C9: 8,
0x3CD: 8,
0x3F2: 8,
0x40D: 8,
0x412: 8,
0x42D: 4,
0x432: 8,
0x438: 8,
0x44D: 8,
0x452: 6,
0x468: 7,
0x488: 8,
0x489: 8,
0x48E: 4,
0x492: 6,
0x495: 4,
0x4B2: 8,
0x4CE: 8,
0x4D2: 3,
0x4F2: 8,
0x50D: 8,
0x50E: 8,
0x517: 7,
0x52E: 8,
0x532: 5,
0x54E: 8,
0x552: 8,
0x56E: 6,
0x572: 8,
0x57C: 6,
0x588: 8,
0x58E: 8,
0x592: 8,
0x5AE: 5,
0x5B2: 8,
0x5CE: 3,
0x5D2: 4,
0x5ED: 3,
0x5EE: 4,
0x5F8: 5,
0x608: 8,
0x612: 8,
0x788: 8,
0x789: 8,
0x78D: 8,
0x792: 8,
0x795: 6,
0x797: 6,
0x7A8: 4,
}],
}

FW_VERSIONS = {
CAR.PSA_PEUGEOT_208: {
(Ecu.fwdRadar, 0x6b6, None): [
Expand Down
16 changes: 15 additions & 1 deletion opendbc/car/psa/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from opendbc.car.interfaces import CarInterfaceBase
from opendbc.car.psa.carcontroller import CarController
from opendbc.car.psa.carstate import CarState
from opendbc.car.psa.values import CAR

TransmissionType = structs.CarParams.TransmissionType

Expand All @@ -27,4 +28,17 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo

ret.alphaLongitudinalAvailable = False

return ret
if candidate == CAR.PSA_PEUGEOT_308_T9:
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.noOutput)]
# Factory captures suggest a torque API; no EPS calibration is supplied.
# Keep an inert PID configuration for this observation-only interface.
ret.steerControlType = structs.CarParams.SteerControlType.torque
ret.lateralTuning.pid.kpBP = [0.]
ret.lateralTuning.pid.kpV = [0.]
ret.lateralTuning.pid.kiBP = [0.]
ret.lateralTuning.pid.kiV = [0.]
ret.steerAtStandstill = False
ret.openpilotLongitudinalControl = False
ret.autoResumeSng = False

return ret
2 changes: 1 addition & 1 deletion opendbc/car/psa/psacan.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
def psa_checksum(address: int, sig, d: bytearray) -> int:
chk_ini = {0x452: 0x4, 0x38D: 0x7, 0x42D: 0xC}.get(address, 0xB)
chk_ini = {0x3AD: 0xD, 0x3CD: 0x3, 0x452: 0x4, 0x38D: 0x7, 0x42D: 0xC}.get(address, 0xB)
byte = sig.start_bit // 8
d[byte] &= 0x0F if sig.start_bit % 8 >= 4 else 0xF0
checksum = sum((b >> 4) + (b & 0xF) for b in d)
Expand Down
Empty file.
40 changes: 40 additions & 0 deletions opendbc/car/psa/tests/fixtures/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Peugeot 308 T9 RX fixtures

These are two-second excerpts from a single 2018 Peugeot 308 II T9 reference
vehicle with conventional cruise control. They are ESP32 recordings, not comma
routes. `provenance.json` records the source and exported file SHA-256 hashes,
source clock offsets and frame counts.

The export keeps only standard, non-RTR, RX frames from the `live` bus, with the
address and DLC declared in `psa_308_t9_2018.dbc`. Payload bytes and frame order
are unchanged. `time_us` is the ESP32 source timestamp minus `source_start_us`.
Only frames in `[source_start_us, source_start_us + duration_us)` are retained.
The live stream is mapped to logical bus 0. This does not establish the physical
camera/powertrain bus topology on a comma harness.

| Fixture | Frames | Final mean wheel speed | Final steering angle |
| --- | ---: | ---: | ---: |
| `stationary.csv` | 1,120 | 0 km/h | 534.7 degrees |
| `moving.csv` | 1,140 | 125.815 km/h | 5.2 degrees |

The files contain ten allowlisted messages. VIN, diagnostic traffic, GPS,
absolute wall-clock timestamps, session metadata and actuation messages are
excluded. The source journals remain local to the research repository.

Run the replay, protection, fingerprint and no-output tests from the repo root:

```sh
uv run python -m unittest opendbc.car.psa.tests.test_peugeot_308_t9
```

The test feeds frames at their recorded relative times, checks validity after
initialization, verifies final decoded values and tests timeout after reception
stops. Other tests use explicitly synthetic signal overrides and single-bit
corruptions; those are not additional vehicle observations.

Known gaps: no comma route or validated production harness, no ECU firmware
fingerprint, no EPS torque calibration, and no validated forward/Park/Neutral
gear signal. The former candidate in the low nibble of `0x348` stays zero in
both complete source captures, including the moving one, so it is omitted.
Geometry and the inactive lateral-acceleration placeholder need separate review
before any future control work. This contribution provides no actuation support.
Loading
Loading