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
4 changes: 4 additions & 0 deletions .github/labeler.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ body:
- changed-files:
- any-glob-to-any-file: 'opendbc/car/body/**'

byd:
- changed-files:
- any-glob-to-any-file: 'opendbc/car/byd/**'

chrysler:
- changed-files:
- any-glob-to-any-file: 'opendbc/car/chrysler/**'
Expand Down
4 changes: 4 additions & 0 deletions opendbc/can/dbc.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from opendbc.car.volkswagen.mqbcan import volkswagen_mqb_meb_checksum, xor_checksum
from opendbc.car.tesla.teslacan import tesla_checksum
from opendbc.car.body.bodycan import body_checksum
from opendbc.car.byd.bydcan import byd_checksum
from opendbc.car.psa.psacan import psa_checksum


Expand All @@ -34,6 +35,7 @@ class SignalType:
TESLA_CHECKSUM = 11
PSA_CHECKSUM = 12
VOLKSWAGEN_MLB_CHECKSUM = 13
BYD_CHECKSUM = 14


@dataclass
Expand Down Expand Up @@ -212,6 +214,8 @@ def get_checksum_state(dbc_name: str) -> ChecksumState | None:
return ChecksumState(8, -1, 0, -1, True, SignalType.TESLA_CHECKSUM, tesla_checksum, tesla_setup_signal)
elif dbc_name.startswith("psa_"):
return ChecksumState(4, 4, 7, 3, False, SignalType.PSA_CHECKSUM, psa_checksum)
elif dbc_name.startswith("byd_"):
return ChecksumState(8, 4, 56, 55, True, SignalType.BYD_CHECKSUM, byd_checksum)
return None


Expand Down
Empty file added opendbc/car/byd/__init__.py
Empty file.
39 changes: 39 additions & 0 deletions opendbc/car/byd/bydcan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
def byd_checksum(address: int, sig, d: bytearray) -> int:
return (~sum(d[:7])) & 0xFF


def create_steering_control(packer, apply_angle: float, lat_active: bool, counter: int):
# Stock saturates the rate limits at ±299 when engaged, 0 when disengaged
rate_limit = 299 if lat_active else 0
values = {
"STEER_REQ": 1 if lat_active else 0,
"STEER_REQ_ACTIVE_LOW": 0 if lat_active else 1,
"STEER_ANGLE": apply_angle,
"ANGLE_RATE_LIMIT_UPPER": rate_limit,
"ANGLE_RATE_LIMIT_LOWER": -rate_limit,
"E2E_ALIVE_1": 1,
"E2E_ALIVE_2": 1,
"SET_ME_FF": 0xFF,
"SET_ME_F": 0xF,
"COUNTER": counter,
}
return packer.make_can_msg("STEERING_MODULE_ADAS", 0, values)


def create_buttons(packer, cancel: bool):
values = {
"SET_ME_1_1": 1,
"SET_ME_1_2": 1,
"ACC_ON_BTN": 1 if cancel else 0,
}
return packer.make_can_msg("PCM_BUTTONS", 0, values)


def create_lkas_hud(packer, lat_active: bool, counter: int, stock_lkas_hud: dict):
values = {**stock_lkas_hud, "COUNTER": counter, "HANDS_ON_WHEEL_REQ": 0}
if lat_active:
values["LKS_MODE"] = 2
values["LKAS_STATE"] = 2
values["LEFT_LANE_STATE"] = 2
values["RIGHT_LANE_STATE"] = 2
return packer.make_can_msg("LKAS_HUD_ADAS", 0, values)
43 changes: 43 additions & 0 deletions opendbc/car/byd/carcontroller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from opendbc.can.packer import CANPacker
from opendbc.car import Bus
from opendbc.car.lateral import apply_steer_angle_limits_vm
from opendbc.car.interfaces import CarControllerBase
from opendbc.car.byd import bydcan
from opendbc.car.byd.values import CarControllerParams
from opendbc.car.vehicle_model import VehicleModel


def get_safety_CP():
from opendbc.car.byd.interface import CarInterface
return CarInterface.get_non_essential_params("BYD_ATTO_3")


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

# Vehicle model used for lateral limiting
self.VM = VehicleModel(get_safety_CP())

def update(self, CC, CS, now_nanos):
can_sends = []
actuators = CC.actuators

if self.frame % 2:
self.apply_angle_last = apply_steer_angle_limits_vm(actuators.steeringAngleDeg, self.apply_angle_last, CS.out.vEgoRaw,
CS.out.steeringAngleDeg, CC.latActive, CarControllerParams, self.VM)

cntr = (self.frame // 2) % 16
can_sends.append(bydcan.create_steering_control(self.packer, self.apply_angle_last, CC.latActive, cntr))
can_sends.append(bydcan.create_lkas_hud(self.packer, CC.latActive, cntr, CS.lkas_hud))

if CC.cruiseControl.cancel and self.frame % 10 == 0:
can_sends.append(bydcan.create_buttons(self.packer, cancel=True))

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

self.frame += 1
return new_actuators, can_sends
95 changes: 95 additions & 0 deletions opendbc/car/byd/carstate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import copy

from opendbc.car import Bus, structs
from opendbc.can.parser import CANParser
from opendbc.car.common.conversions import Conversions as CV
from opendbc.car.byd.values import CAR, DBC, CarControllerParams as CCP
from opendbc.car.interfaces import CarStateBase

GearShifter = structs.CarState.GearShifter

# BYD gear enum from DRIVE_STATE.GEAR
GEAR_MAP = {
1: GearShifter.park,
2: GearShifter.reverse,
3: GearShifter.neutral,
4: GearShifter.drive,
}


class CarState(CarStateBase):
def __init__(self, CP):
super().__init__(CP)
self.lkas_hud = {}

def update(self, can_parsers) -> structs.CarState:
cp = can_parsers[Bus.pt]
cp_cam = can_parsers[Bus.cam]
ret = structs.CarState()

if self.CP.carFingerprint == CAR.BYD_SEALION_7:
ret.wheelSpeeds.fl = cp.vl["WHEEL_SPEEDS"]["FL"] * CV.KPH_TO_MS
ret.wheelSpeeds.rl = cp.vl["WHEEL_SPEEDS"]["RL"] * CV.KPH_TO_MS
ret.vEgoRaw = (ret.wheelSpeeds.rl + ret.wheelSpeeds.fl) / 2.0
ret.standstill = ret.vEgoRaw < 0.01
ret.vEgoCluster = ret.vEgo * 1.068 # FIXME: update dbc multiplier to get correct kph
else:
# speed
speed_kph = cp.vl["WHEELSPEED_CLEAN"]["WHEELSPEED_CLEAN"]
ret.vEgoRaw = speed_kph * CV.KPH_TO_MS
ret.standstill = speed_kph < 0.1
ret.vEgoCluster = ret.vEgo

ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)

# steering wheel
ret.steeringAngleDeg = cp.vl["STEER_MODULE_2"]["STEER_ANGLE_2"]
ret.steeringTorque = cp.vl["STEERING_TORQUE"]["MAIN_TORQUE"]
ret.steeringTorqueEps = cp.vl["STEER_MODULE_2"]["DRIVER_EPS_TORQUE"]
ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorqueEps) > CCP.STEER_DRIVER_OVERRIDE, 5)
ret.steeringDisengage = abs(ret.steeringTorqueEps) > CCP.STEER_DRIVER_DISENGAGE

# gas / brake
ret.gasPressed = cp.vl["DRIVE_STATE"]["RAW_THROTTLE"] > 0
ret.brake = cp.vl["PEDAL"]["BRAKE_PEDAL"]
ret.brakePressed = bool(cp.vl["DRIVE_STATE"]["BRAKE_PRESSED"])

# gear
ret.gearShifter = GEAR_MAP.get(int(cp.vl["DRIVE_STATE"]["GEAR"]), GearShifter.unknown)

# blinkers
ret.leftBlinker = bool(cp.vl["STALKS"]["LEFT_BLINKER"])
ret.rightBlinker = bool(cp.vl["STALKS"]["RIGHT_BLINKER"])

# blind spot monitor
ret.leftBlindspot = cp.vl["BSD_RADAR"]["LEFT_APPROACH"] != 0
ret.rightBlindspot = cp.vl["BSD_RADAR"]["RIGHT_APPROACH"] != 0

# doors / belt
ret.doorOpen = any((
cp.vl["METER_CLUSTER"]["FRONT_LEFT_DOOR"],
cp.vl["METER_CLUSTER"]["FRONT_RIGHT_DOOR"],
cp.vl["METER_CLUSTER"]["BACK_LEFT_DOOR"],
cp.vl["METER_CLUSTER"]["BACK_RIGHT_DOOR"],
))
ret.seatbeltUnlatched = not bool(cp.vl["METER_CLUSTER"]["SEATBELT_DRIVER"])

# cruise state: ACC messages come from camera bus on Atto 3
# ACC_STATE: 0=OFF, 2=ACC_ON (available), 3=ACC_ACTIVE (enabled), 5=FORCE_ACCEL, 7=ERROR
ret.cruiseState.speed = cp_cam.vl["ACC_HUD_ADAS"]["SET_SPEED"] * CV.KPH_TO_MS
acc_state = int(cp_cam.vl["ACC_HUD_ADAS"]["ACC_STATE"])
ret.cruiseState.available = acc_state in (2, 3, 5)
ret.cruiseState.enabled = acc_state in (3, 5)
ret.cruiseState.standstill = bool(cp_cam.vl["ACC_CMD"]["STANDSTILL_STATE"])

# forward stock LKAS HUD
self.lkas_hud = copy.copy(cp_cam.vl["LKAS_HUD_ADAS"])

return ret

@staticmethod
def get_can_parsers(CP):
return {
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 0),
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 2),
}
13 changes: 13 additions & 0 deletions opendbc/car/byd/fingerprints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
""" AUTO-FORMATTED USING opendbc/car/debug/format_fingerprints.py, EDIT STRUCTURE THERE."""
from opendbc.car.structs import CarParams
from opendbc.car.byd.values import CAR

Ecu = CarParams.Ecu

FW_VERSIONS = {
CAR.BYD_ATTO_3: {
(Ecu.engine, 0x7e0, None): [
b'PLACEHOLDER_FOR_VIN_FINGERPRINT',
],
},
}
30 changes: 30 additions & 0 deletions opendbc/car/byd/interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from opendbc.car import get_safety_config, structs
from opendbc.car.interfaces import CarInterfaceBase
from opendbc.car.byd.carcontroller import CarController
from opendbc.car.byd.carstate import CarState
from opendbc.car.byd.values import CAR


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 = "byd"
ret.dashcamOnly = True
ret.radarUnavailable = True
ret.alphaLongitudinalAvailable = False

ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.byd)]
ret.steerControlType = structs.CarParams.SteerControlType.angle

if candidate == CAR.BYD_ATTO_3:
ret.steerActuatorDelay = 0.2
ret.steerLimitTimer = 0.4

elif candidate == CAR.BYD_SEALION_7:
ret.steerActuatorDelay = 0.1
ret.steerLimitTimer = 0.4

return ret
109 changes: 109 additions & 0 deletions opendbc/car/byd/values.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from dataclasses import dataclass, field
from enum import IntFlag, StrEnum

from opendbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, CarSpecs, DbcDict, PlatformConfig, Platforms, structs
from opendbc.car.lateral import AngleSteeringLimits, ISO_LATERAL_ACCEL
from opendbc.car.docs_definitions import CarDocs, CarHarness, CarParts
from opendbc.car.fw_query_definitions import FwQueryConfig
from opendbc.car.vin import Vin

Ecu = structs.CarParams.Ecu


# Add extra tolerance for average banked road since safety doesn't have the roll
AVERAGE_ROAD_ROLL = 0.06 # ~3.4 degrees, 6% superelevation. higher actual roll lowers lateral acceleration


class CarControllerParams:
STEER_STEP = 2 # Angle command is sent at 50 Hz

ANGLE_LIMITS: AngleSteeringLimits = AngleSteeringLimits(
390, # deg
# BYD uses a vehicle model instead, check carcontroller.py for details
([], []),
([], []),

# Vehicle model angle limits
# Add extra tolerance for average banked road since safety doesn't have the roll
MAX_LATERAL_ACCEL=ISO_LATERAL_ACCEL + (ACCELERATION_DUE_TO_GRAVITY * AVERAGE_ROAD_ROLL), # ~3.6 m/s^2
MAX_LATERAL_JERK=3.0 + (ACCELERATION_DUE_TO_GRAVITY * AVERAGE_ROAD_ROLL), # ~3.6 m/s^3

# limit angle rate to both prevent a fault and for low speed comfort (~12 mph rate down to 0 mph)
MAX_ANGLE_RATE=5, # deg/20ms frame, EPS faults at 12 at a standstill
)

STEER_DRIVER_OVERRIDE = 10 # EPS torque threshold for soft override
STEER_DRIVER_DISENGAGE = 30 # EPS torque threshold for hard disengage


class BydSafetyFlags(IntFlag):
LONG_CONTROL = 1


class WMI(StrEnum):
BYD_AUTO = "LGX" # BYD Auto Co., Ltd. (Shenzhen)


class ModelYear(StrEnum):
N_2022 = "N"
P_2023 = "P"
R_2024 = "R"
S_2025 = "S"


@dataclass
class BydCarDocs(CarDocs):
package: str = "All"
car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.custom]))

@dataclass
class BydPlatformConfig(PlatformConfig):
dbc_dict: DbcDict = field(default_factory=lambda: {
Bus.pt: 'byd_atto3',
})
wmis: set[WMI] = field(default_factory=set)
years: set[ModelYear] = field(default_factory=set)

@dataclass
class BydSealionPlatformConfig(PlatformConfig):
dbc_dict: DbcDict = field(default_factory=lambda: {
Bus.pt: 'byd_sealion_7',
})
wmis: set[WMI] = field(default_factory=set)
years: set[ModelYear] = field(default_factory=set)


class CAR(Platforms):
BYD_ATTO_3 = BydPlatformConfig(
[BydCarDocs("BYD Atto 3 2022-25")],
CarSpecs(mass=1750, wheelbase=2.72, steerRatio=14.8),
wmis={WMI.BYD_AUTO},
years={ModelYear.N_2022, ModelYear.P_2023, ModelYear.R_2024, ModelYear.S_2025},
)
BYD_SEALION_7 = BydSealionPlatformConfig(
[BydCarDocs("BYD Sealion 7 2024")],
CarSpecs(mass=2090., wheelbase=2.72, steerRatio=16.0, centerToFrontRatio=0.44)
)


def match_fw_to_car_fuzzy(live_fw_versions, vin, offline_fw_versions) -> set[str]:
# BYD Atto 3 VIN: LGX (WMI) + <VDS> + <year><plant><seq> (VIS).
# TODO: currently we only match on WMI + model year
vin_obj = Vin(vin)
year = vin_obj.vis[:1]

candidates = set()
for platform in CAR:
if vin_obj.wmi in platform.config.wmis and year in platform.config.years:
candidates.add(platform)

return {str(c) for c in candidates}


FW_QUERY_CONFIG = FwQueryConfig(
requests=[],
match_fw_to_car_fuzzy=match_fw_to_car_fuzzy,
)


DBC = CAR.create_dbc_map()
1 change: 1 addition & 0 deletions opendbc/car/car.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,7 @@ struct CarParams {
fcaGiorgio @32;
rivian @33;
volkswagenMeb @34;
byd @35;
}

enum SteerControlType {
Expand Down
4 changes: 4 additions & 0 deletions opendbc/car/tests/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from opendbc.car.values import Platform
from opendbc.car.volkswagen.values import CAR as VOLKSWAGEN
from opendbc.car.body.values import CAR as COMMA
from opendbc.car.byd.values import CAR as BYD
from opendbc.car.psa.values import CAR as PSA

# FIXME: add routes for these cars
Expand Down Expand Up @@ -341,6 +342,9 @@ class CarTestRoute(NamedTuple):

CarTestRoute("6a7075a4fdd765ee/0000004e--1f612006dd", PSA.PSA_PEUGEOT_208),

CarTestRoute("148fa33c79475c93/00000002--bb5e1aa449", BYD.BYD_ATTO_3),
CarTestRoute("148fa33c79475c93/00000002--bb5e1aa449", BYD.BYD_SEALION_7), # FIXME: add route for sealion 7

CarTestRoute("bc095dc92e101734/000000db--ee9fe46e57", RIVIAN.RIVIAN_R1),
CarTestRoute("c70d59e4150956fc/0000006e--48bfbfda01", RIVIAN.RIVIAN_R1), # GEN2

Expand Down
Loading
Loading