Skip to content
Open
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 401 Known Cars
# Support Information for 402 Known Cars

|Make|Model|Package|Support Level|
|---|---|---|:---:|
Expand Down Expand Up @@ -34,6 +34,7 @@
|Audi|Q5 2017-24|All|[Not compatible](#flexray)|
|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)|
|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)|
|BYD|ATTO3 2022-24|All|[Community](#upstream)|
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|[Upstream](#upstream)|
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|[Upstream](#upstream)|
|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
Expand Down
Empty file added opendbc/car/byd/__init__.py
Empty file.
201 changes: 201 additions & 0 deletions opendbc/car/byd/bydcan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
from opendbc.car.byd.values import CanBus, LKAS_HUD_PASSTHROUGH

# BYD CAN message checksum implementation
CHECKSUM_KEY = 0xAF # BYD CAN message checksum key


def byd_checksum(byte_key: int, dat: bytes) -> int:
"""Calculate BYD's CAN message checksum.

The checksum is calculated by processing the message bytes in two parts:
- First calculating sums of the high and low nibbles separately
- Then applying a specific algorithm involving remainders and offsets

Args:
byte_key: The checksum key specific to the message type
dat: The message data bytes to calculate checksum for

Returns:
The calculated checksum byte
"""
first_bytes_sum = sum(byte >> 4 for byte in dat)
second_bytes_sum = sum(byte & 0xF for byte in dat)
remainder = second_bytes_sum >> 4
second_bytes_sum += byte_key >> 4
first_bytes_sum += byte_key & 0xF
first_part = ((-first_bytes_sum + 0x9) & 0xF)
second_part = ((-second_bytes_sum + 0x9) & 0xF)
return (((first_part + (-remainder + 5)) << 4) + second_part) & 0xFF


def create_steering_control(packer, apply_angle, template, idx):
"""
Create the steering command for BYD ATTO3 — STEERING_MODULE_ADAS (0x1E2).

STEER_ANGLE is an absolute steering wheel angle target (DBC factor 0.1 deg); the EPS
servos the wheel to that position. Every other field is copied from `template`, the
constant the camera holds for the whole of a steering episode, because the car
validates fields we do not understand (the DBC's 14-bit UNKNOWN and SET_ME_XE) and
drops its ADAS when they take values it never emits itself. `template` is a fixed
triple, not something to vary frame to frame — see carstate.STEER_TEMPLATE_DEFAULT.

Note STEER_REQ_ACTIVE_LOW is *not* the inverse of STEER_REQ despite its name — the
camera holds it at 0 in both states.
"""

values = {
"STEER_ANGLE": apply_angle, # degrees; DBC factor 0.1 → raw = deg × 10
"STEER_REQ": 1,
"STEER_REQ_ACTIVE_LOW": 0,
# constants the camera holds fixed while it steers; never derived, never advanced
"UNKNOWN": template["UNKNOWN"],
"SET_ME_X01": template["SET_ME_X01"],
"SET_ME_XE": template["SET_ME_XE"],
"SET_ME_FF": 0xFF,
"SET_ME_F": 0xF,
"SET_ME_1_1": 1,
"SET_ME_1_2": 1,
"COUNTER": idx % 16,
"CHECKSUM": 0, # placeholder, computed below
}

# Sent on bus 0, straight to the EPS. panda blocks the camera's copy from being
# forwarded 2->0 for as long as we keep transmitting, so the EPS sees one source.
msg = packer.make_can_msg("STEERING_MODULE_ADAS", CanBus.pt, values)
values["CHECKSUM"] = byd_checksum(CHECKSUM_KEY, msg[1])

return packer.make_can_msg("STEERING_MODULE_ADAS", CanBus.pt, values)


def create_acc_control(packer, accel, acc_enabled, idx):
"""
Create ACC longitudinal control message — ACC_CMD (814).

NOTE: not reachable today. openpilotLongitudinalControl is False and panda leaves
ACC_CMD out of the TX allowlist, so this is blocked. The ACCEL_CMD scale below is
inferred, not measured — calibrate it on the car before enabling longitudinal.
"""

# ACCEL_CMD physical units are roughly m/s^2 * 16.67; the DBC applies the -100 offset
accel_cmd = max(-50, min(30, int(round(accel * 16.67))))

# ACC control flags
acc_on_1 = 1 if acc_enabled else 0
acc_on_2 = 1 if acc_enabled else 0
cmd_req_active_low = 0 if acc_enabled else 1 # Inverted logic
acc_controllable_and_on = 1 if acc_enabled else 0
acc_req_not_standstill = 1 if abs(accel_cmd) > 0 else 0

# Fixed values from DBC analysis
set_me_25_1 = 0x25
set_me_25_2 = 0x25
set_me_xf = 0xF
set_me_x8 = 0x8
set_me_1 = 1

values = {
"ACCEL_CMD": accel_cmd,
"ACC_ON_1": acc_on_1,
"ACC_ON_2": acc_on_2,
"CMD_REQ_ACTIVE_LOW": cmd_req_active_low,
"ACC_CONTROLLABLE_AND_ON": acc_controllable_and_on,
"ACC_REQ_NOT_STANDSTILL": acc_req_not_standstill,
"SET_ME_25_1": set_me_25_1,
"SET_ME_25_2": set_me_25_2,
"SET_ME_XF": set_me_xf,
"SET_ME_X8": set_me_x8,
"SET_ME_1": set_me_1,
"ACCEL_FACTOR": 10, # Default acceleration factor
"DECEL_FACTOR": 10, # Default deceleration factor
"STANDSTILL_STATE": 0,
"ACC_OVERRIDE_OR_STANDSTILL": 0,
"STANDSTILL_RESUME": 0,
"COUNTER": idx % 16,
"CHECKSUM": 0, # Temporary, will be calculated below
}

# Create message with temporary checksum
msg = packer.make_can_msg("ACC_CMD", CanBus.pt, values)

# Calculate and set proper BYD checksum
checksum = byd_checksum(CHECKSUM_KEY, msg[1])
values["CHECKSUM"] = checksum

return packer.make_can_msg("ACC_CMD", CanBus.pt, values)


def create_lkas_hud(packer, cam, idx):
"""
Create the LKAS HUD message — LKAS_HUD_ADAS (0x316), sent on bus 0 to the cluster.

Only sent while openpilot is steering, and panda blocks the camera's copy for as long
as we keep sending — the camera keeps the HUD the rest of the time. This exists because
the camera drops its own LKAS within seconds of us blocking its steering command, and
without this the cluster then shows LKAS off while openpilot is in fact holding the
wheel. It is cosmetic: the EPS steers regardless of what this message says.

Field values are the camera's own, measured while its LKAS was engaged: the three
STEER_ACTIVE bits go to 1 together and STEER_ACTIVE_ACTIVE_LOW stays at 0 (it is *not*
the inverse of them — sending the inverse is what made the cluster show LKAS engaged at
all times). Everything else — lane-line state, traffic sign recognition, high beam
assist, the PT2-PT5 / SET_ME_* passthrough — is mirrored from the camera's copy read on
bus 2, because zeroing those blanks out unrelated driver-assist icons.
"""

values = {
"STEER_ACTIVE_1_1": 1,
"STEER_ACTIVE_1_2": 1,
"STEER_ACTIVE_1_3": 1,
"STEER_ACTIVE_ACTIVE_LOW": 0,
# camera passthrough
**{k: cam[k] for k in LKAS_HUD_PASSTHROUGH},
"COUNTER": idx % 16,
"CHECKSUM": 0, # placeholder, computed below
}

msg = packer.make_can_msg("LKAS_HUD_ADAS", CanBus.pt, values)
values["CHECKSUM"] = byd_checksum(CHECKSUM_KEY, msg[1])

return packer.make_can_msg("LKAS_HUD_ADAS", CanBus.pt, values)


def create_acc_hud(packer, acc_active, set_speed, lead_visible, idx):
"""
Create ACC HUD display message
Based on ACC_HUD_ADAS message (813)
"""

# ACC status indicators
acc_on1 = 1 if acc_active else 0
acc_on2 = 1 if acc_active else 0

# Speed conversion (km/h to DBC units)
set_speed_dbc = int(set_speed * 2) if set_speed > 0 else 0 # 0.5 km/h units
set_speed_dbc = max(0, min(255, set_speed_dbc)) # Limit to valid range

# Distance setting (default to middle setting)
set_distance = 2 # "2bar" setting

# Fixed values from DBC
set_me_xf = 0xF
set_me_xff = 0xFF

values = {
"ACC_ON1": acc_on1,
"ACC_ON2": acc_on2,
"SET_SPEED": set_speed_dbc,
"SET_DISTANCE": set_distance,
"SET_ME_XF": set_me_xf,
"SET_ME_XFF": set_me_xff,
"COUNTER": idx % 16,
"CHECKSUM": 0, # Temporary, will be calculated below
}

# Create message with temporary checksum
msg = packer.make_can_msg("ACC_HUD_ADAS", CanBus.pt, values)

# Calculate and set proper BYD checksum
checksum = byd_checksum(CHECKSUM_KEY, msg[1])
values["CHECKSUM"] = checksum

return packer.make_can_msg("ACC_HUD_ADAS", CanBus.pt, values)
96 changes: 96 additions & 0 deletions opendbc/car/byd/carcontroller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import numpy as np

from opendbc.can.packer import CANPacker
from opendbc.car import Bus
from opendbc.car.byd.values import CarControllerParams
from opendbc.car.byd import bydcan
from opendbc.car.byd.carstate import STEER_TEMPLATE_DEFAULT
from opendbc.car.lateral import apply_std_steer_angle_limits
from opendbc.car.interfaces import CarControllerBase


class CarController(CarControllerBase):
def __init__(self, dbc_names, CP):
super().__init__(dbc_names, CP)
self.CP = CP
self.packer = CANPacker(dbc_names[Bus.pt])
self.params = CarControllerParams(CP)

self.apply_angle_last = 0.0
self.acc_idx = 0

def update(self, CC, CS, now_nanos):
actuators = CC.actuators
hud_control = CC.hudControl
pcm_cancel_cmd = CC.cruiseControl.cancel

can_sends = []

# === STEERING ===
# The EPS is a position servo: STEER_ANGLE is an absolute wheel angle target.
# The command must therefore stay anchored to the measured angle at all times —
# a limiter that only tracks its own previous output can ratchet away from the
# wheel, saturate at the clamp and get every frame rejected by panda.
if self.frame % self.params.STEER_STEP == 0:
apply_angle = apply_std_steer_angle_limits(actuators.steeringAngleDeg, self.apply_angle_last,
CS.out.vEgoRaw, CS.out.steeringAngleDeg,
CC.latActive, self.params.ANGLE_LIMITS)

# Hand control back to the driver rather than fighting them. DRIVER_EPS_TORQUE is
# an unsigned magnitude from the column sensor, so this is a pure override test.
if CS.out.steeringTorque > self.params.STEER_DRIVER_ALLOWANCE:
apply_angle = CS.out.steeringAngleDeg

# Windup guard: never let the command drift outside a fixed window around the
# measured angle. Makes the saturation failure mode structurally impossible.
apply_angle = float(np.clip(apply_angle,
CS.out.steeringAngleDeg - self.params.MAX_ANGLE_ERROR,
CS.out.steeringAngleDeg + self.params.MAX_ANGLE_ERROR))

self.apply_angle_last = apply_angle

# Transmit only while actually steering. Whenever we go quiet, panda stops
# blocking the camera's command after ~150ms and the stock LKAS takes the wheel
# back — so there is never a window with nobody driving the EPS. It also lets the
# camera keep refreshing the frame template we copy.
if CC.latActive:
# Everything in the frame other than the angle is held at the constant the
# camera itself sends while steering (bytes 0-2 = 2b 55 eb, at every speed).
# It must not be varied: walking these fields across their range is what made
# the car drop its whole ADAS mid-drive. Use the camera's own latched value
# when we have seen it steer, and the captured constant otherwise — a blocked
# lens means the camera never steers and never gives us one.
template = CS.steer_template or STEER_TEMPLATE_DEFAULT

can_sends.append(bydcan.create_steering_control(self.packer, apply_angle,
template,
self.frame // self.params.STEER_STEP))

# Tell the cluster openpilot has the wheel. The camera drops its own LKAS
# within seconds of us blocking its steering command, so without this the
# LKAS indicator goes dark while openpilot is still steering. Sent on the
# same cadence as the steering command, so panda hands 0x316 back to the
# camera at the same moment it hands back 0x1E2.
can_sends.append(bydcan.create_lkas_hud(self.packer, CS.lkas_hud,
self.frame // self.params.STEER_STEP))

# === LONGITUDINAL ===
if self.CP.openpilotLongitudinalControl:
if self.frame % self.params.STEER_STEP == 0:
accel = float(np.clip(actuators.accel, self.params.ACCEL_MIN, self.params.ACCEL_MAX))
if not CC.longActive or pcm_cancel_cmd:
accel = 0.0

can_sends.append(bydcan.create_acc_control(self.packer, accel,
CC.longActive and not pcm_cancel_cmd, self.acc_idx))

set_speed = hud_control.setSpeed if hud_control.setSpeed > 0 else CS.out.cruiseState.speed
can_sends.append(bydcan.create_acc_hud(self.packer, CC.enabled, set_speed * 3.6,
hud_control.leadVisible, self.acc_idx))
self.acc_idx += 1

new_actuators = actuators.as_builder()
new_actuators.steeringAngleDeg = self.apply_angle_last

self.frame += 1
return new_actuators, can_sends
Loading
Loading