forked from IQ.Lvbs/IQ.Pilot
428 lines
19 KiB
Python
428 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
import unittest
|
|
|
|
import numpy as np
|
|
|
|
from iqdbc.can.packer import CANPacker
|
|
from iqdbc.can.parser import CANParser
|
|
from iqdbc.car.byd import bydcan
|
|
from iqdbc.car.byd.carstate import (EPS_STATE_OFF, EPS_STATE_PREPARED, EPS_STATE_ACTUATING,
|
|
EPS_STATE_LATCHED_FAULT)
|
|
from iqdbc.car.byd.fingerprints import FW_VERSIONS
|
|
from iqdbc.car.byd.interface import CarInterface
|
|
from iqdbc.car.byd.values import CAR, DBC, BydFlags, BydSafetyFlags, CarControllerParams
|
|
from iqdbc.car.fw_versions import match_fw_to_car_exact, build_fw_dict
|
|
from iqdbc.car import structs
|
|
from iqdbc.car.structs import CarParams
|
|
|
|
DBC_NAME = DBC[CAR.BYD_SEALION_7]['pt']
|
|
|
|
Ecu = CarParams.Ecu
|
|
|
|
|
|
def _unpack(dbc_name, msg_name, dat):
|
|
"""Decode one frame with the DBC, bypassing the parser's liveness tracking."""
|
|
dbc = CANParser(dbc_name, [], 0).dbc
|
|
msg = dbc.name_to_msg[msg_name]
|
|
out = {}
|
|
for sig in msg.sigs.values():
|
|
val = 0
|
|
if sig.is_little_endian:
|
|
for i in range(sig.size):
|
|
bit = sig.lsb + i
|
|
val |= ((dat[bit // 8] >> (bit % 8)) & 1) << i
|
|
else:
|
|
be_bits = [j + i * 8 for i in range(64) for j in range(7, -1, -1)]
|
|
idx = be_bits.index(sig.start_bit)
|
|
for i in range(sig.size):
|
|
bit = be_bits[idx + i]
|
|
val = (val << 1) | ((dat[bit // 8] >> (bit % 8)) & 1)
|
|
if sig.is_signed and (val & (1 << (sig.size - 1))):
|
|
val -= (1 << sig.size)
|
|
out[sig.name] = val * sig.factor + sig.offset
|
|
return out
|
|
|
|
|
|
class TestBydChecksum(unittest.TestCase):
|
|
def test_checksum_is_inverted_sum(self):
|
|
for dat in (bytearray(8), bytearray(b'\x01' * 8), bytearray(b'\xff' * 8),
|
|
bytearray(b'\x12\x34\x56\x78\x9a\xbc\xde\x00')):
|
|
self.assertEqual(bydcan.byd_checksum(0, None, dat), (~sum(dat[:7])) & 0xFF)
|
|
|
|
def test_packer_fills_checksum_and_counter(self):
|
|
packer = CANPacker(DBC_NAME)
|
|
seen = []
|
|
for _ in range(18):
|
|
_, dat, _ = packer.make_can_msg("STEERING_MODULE_ADAS", 0, {"STEER_REQ": 1})
|
|
self.assertEqual(dat[7], (~sum(dat[:7])) & 0xFF, "checksum not filled by the DBC layer")
|
|
seen.append(dat[6] >> 4) # COUNTER is 55|4@0
|
|
# rolls 0..15 and wraps, never repeating within a cycle
|
|
self.assertEqual(seen[:16], list(range(16)))
|
|
self.assertEqual(seen[16:], [0, 1])
|
|
|
|
|
|
class TestBydSteeringControl(unittest.TestCase):
|
|
def setUp(self):
|
|
self.packer = CANPacker(DBC_NAME)
|
|
|
|
def test_steer_req_and_angle_round_trip(self):
|
|
for angle in (-390.0, -100.5, 0.0, 12.3, 390.0):
|
|
for lat_active in (True, False):
|
|
_, dat, _ = bydcan.create_steering_control(self.packer, angle, lat_active)
|
|
vals = _unpack(DBC_NAME, "STEERING_MODULE_ADAS", dat)
|
|
self.assertAlmostEqual(vals["STEER_ANGLE"], angle, places=4)
|
|
self.assertEqual(vals["STEER_REQ"], 1 if lat_active else 0)
|
|
# STEER_REQ_ACTIVE_LOW is the inverse of STEER_REQ
|
|
self.assertEqual(vals["STEER_REQ_ACTIVE_LOW"], 0 if lat_active else 1)
|
|
self.assertEqual(vals["E2E_ALIVE_1"], 1)
|
|
self.assertEqual(vals["E2E_ALIVE_2"], 1)
|
|
|
|
def test_rate_limits_zeroed_when_inactive(self):
|
|
_, dat, _ = bydcan.create_steering_control(self.packer, 0.0, True)
|
|
vals = _unpack(DBC_NAME, "STEERING_MODULE_ADAS", dat)
|
|
self.assertEqual(vals["ANGLE_RATE_LIMIT_UPPER"], bydcan.ANGLE_RATE_LIMIT_UPPER)
|
|
self.assertEqual(vals["ANGLE_RATE_LIMIT_LOWER"], bydcan.ANGLE_RATE_LIMIT_LOWER)
|
|
|
|
_, dat, _ = bydcan.create_steering_control(self.packer, 0.0, False)
|
|
vals = _unpack(DBC_NAME, "STEERING_MODULE_ADAS", dat)
|
|
self.assertEqual(vals["ANGLE_RATE_LIMIT_UPPER"], 0)
|
|
self.assertEqual(vals["ANGLE_RATE_LIMIT_LOWER"], 0)
|
|
|
|
|
|
class TestBydLkasHud(unittest.TestCase):
|
|
def setUp(self):
|
|
self.packer = CANPacker(DBC_NAME)
|
|
# a stock frame with bits set in every field we touch and several we must not
|
|
self.stock = {
|
|
"HMA_STATE": 3, "LEFT_LANE_STATE": 1, "LKS_MODE": 2, "HANDS_ON_WHEEL_REQ": 1,
|
|
"TJA_ICA_STATE": 5, "HMA_ON_OFF": 1, "LKAS_OUTPUT": -20, "LKAS_REQ_PREPARE": 1,
|
|
"LKAS_ACTIVE": 1, "SLA_STATE": 3, "RIGHT_LANE_STATE": 1, "LKAS_STATE": 0b1000,
|
|
"SPEED_LIMIT_VALUE": 100, "LDSW_TYPE": 2, "COUNTER": 9, "CHECKSUM": 0x11,
|
|
}
|
|
|
|
def test_passes_stock_bits_through(self):
|
|
# The ADAS modules cross-check this frame; every bit we do not own must survive.
|
|
_, dat, _ = bydcan.create_lkas_hud(self.packer, False, self.stock, None)
|
|
vals = _unpack(DBC_NAME, "LKAS_HUD_ADAS", dat)
|
|
for name in ("HMA_STATE", "LKS_MODE", "HANDS_ON_WHEEL_REQ", "TJA_ICA_STATE", "HMA_ON_OFF",
|
|
"LKAS_OUTPUT", "LKAS_REQ_PREPARE", "LKAS_ACTIVE", "SLA_STATE",
|
|
"SPEED_LIMIT_VALUE", "LDSW_TYPE"):
|
|
self.assertEqual(vals[name], self.stock[name], f"{name} was modified")
|
|
|
|
def test_hands_on_wheel_req_never_cleared(self):
|
|
for lat_active in (True, False):
|
|
_, dat, _ = bydcan.create_lkas_hud(self.packer, lat_active, self.stock, None)
|
|
vals = _unpack(DBC_NAME, "LKAS_HUD_ADAS", dat)
|
|
self.assertEqual(vals["HANDS_ON_WHEEL_REQ"], 1)
|
|
|
|
def test_active_asserts_eps_arming_bits_only(self):
|
|
_, dat, _ = bydcan.create_lkas_hud(self.packer, True, self.stock, None)
|
|
vals = _unpack(DBC_NAME, "LKAS_HUD_ADAS", dat)
|
|
# low 2 bits become 0b10, the stock upper 2 bits are preserved
|
|
self.assertEqual(int(vals["LKAS_STATE"]), 0b1010)
|
|
self.assertEqual(int(vals["LEFT_LANE_STATE"]), 1 | 2)
|
|
self.assertEqual(int(vals["RIGHT_LANE_STATE"]), 1 | 2)
|
|
|
|
def test_counter_not_inherited_from_stock(self):
|
|
# inheriting the camera's counter would make our 50 Hz stream non-monotonic
|
|
counters = []
|
|
for _ in range(4):
|
|
_, dat, _ = bydcan.create_lkas_hud(self.packer, True, self.stock, None)
|
|
counters.append(int(_unpack(DBC_NAME, "LKAS_HUD_ADAS", dat)["COUNTER"]))
|
|
self.assertNotEqual(counters, [self.stock["COUNTER"]] * 4)
|
|
self.assertEqual(counters, [(counters[0] + i) % 16 for i in range(4)])
|
|
|
|
def test_checksum_recomputed_not_inherited(self):
|
|
_, dat, _ = bydcan.create_lkas_hud(self.packer, True, self.stock, None)
|
|
self.assertEqual(dat[7], (~sum(dat[:7])) & 0xFF)
|
|
|
|
|
|
class TestBydAccCmd(unittest.TestCase):
|
|
def setUp(self):
|
|
self.packer = CANPacker(DBC_NAME)
|
|
self.stock = {"ACCEL_CMD": 0.0, "COUNTER": 7, "CHECKSUM": 0x22}
|
|
|
|
def test_accel_scale_is_physical(self):
|
|
# raw x 0.05 - 5 m/s^2, so 0 m/s^2 is raw 100
|
|
for accel in (-3.0, -1.5, 0.0, 0.5, 1.5):
|
|
_, dat, _ = bydcan.create_acc_cmd(self.packer, accel, True, self.stock)
|
|
self.assertEqual(dat[0], round((accel + 5.0) / 0.05))
|
|
vals = _unpack(DBC_NAME, "ACC_CMD", dat)
|
|
self.assertAlmostEqual(vals["ACCEL_CMD"], accel, places=6)
|
|
|
|
def test_inactive_commands_zero_accel(self):
|
|
_, dat, _ = bydcan.create_acc_cmd(self.packer, -2.0, False, self.stock)
|
|
vals = _unpack(DBC_NAME, "ACC_CMD", dat)
|
|
self.assertEqual(vals["ACCEL_CMD"], 0.0)
|
|
self.assertEqual(dat[0], 100)
|
|
self.assertEqual(vals["ACC_ON_1"], 0)
|
|
self.assertEqual(vals["ACC_ON_2"], 0)
|
|
self.assertEqual(vals["ACC_CONTROLLABLE_AND_ON"], 0)
|
|
self.assertEqual(vals["CMD_REQ_ACTIVE_LOW"], 1)
|
|
|
|
def test_standstill_hold_and_resume(self):
|
|
_, dat, _ = bydcan.create_acc_cmd(self.packer, -0.5, True, self.stock, standstill=True)
|
|
vals = _unpack(DBC_NAME, "ACC_CMD", dat)
|
|
self.assertEqual(vals["STANDSTILL_STATE"], 1)
|
|
self.assertEqual(vals["ACC_OVERRIDE_OR_STANDSTILL"], 1)
|
|
self.assertEqual(vals["ACC_REQ_NOT_STANDSTILL"], 0)
|
|
self.assertEqual(vals["STANDSTILL_RESUME"], 0)
|
|
|
|
_, dat, _ = bydcan.create_acc_cmd(self.packer, 0.5, True, self.stock, standstill=True, resume=True)
|
|
vals = _unpack(DBC_NAME, "ACC_CMD", dat)
|
|
self.assertEqual(vals["STANDSTILL_RESUME"], 1)
|
|
self.assertEqual(vals["STANDSTILL_STATE"], 0)
|
|
self.assertEqual(vals["ACC_REQ_NOT_STANDSTILL"], 1)
|
|
|
|
def test_regime_pairs(self):
|
|
for accel, expected in ((0.0, (0, 0)), (0.05, (0, 0)), (0.8, (12, 5)),
|
|
(-1.0, (13, 1)), (-2.5, (1, 1))):
|
|
_, dat, _ = bydcan.create_acc_cmd(self.packer, accel, True, self.stock)
|
|
vals = _unpack(DBC_NAME, "ACC_CMD", dat)
|
|
self.assertEqual((int(vals["ACCEL_FACTOR"]), int(vals["DECEL_FACTOR"])), expected, f"{accel=}")
|
|
|
|
def test_accel_within_safety_bounds(self):
|
|
# the comfort envelope must stay inside what byd.h allows (-3.5 .. +2.0)
|
|
self.assertGreaterEqual(CarControllerParams.ACCEL_MIN, -3.5)
|
|
self.assertLessEqual(CarControllerParams.ACCEL_MAX, 2.0)
|
|
|
|
|
|
class TestBydEpsState(unittest.TestCase):
|
|
"""The 0x1FC decode is the core fix over the Sealion 7 PR, which inherited a stub that
|
|
packed these status bits into a fake 16-bit torque value."""
|
|
|
|
def test_state_nibble_table(self):
|
|
for prepared, activated, expected in (
|
|
(0, 0, EPS_STATE_OFF),
|
|
(1, 0, EPS_STATE_PREPARED),
|
|
(0, 1, EPS_STATE_ACTUATING),
|
|
(1, 1, EPS_STATE_LATCHED_FAULT),
|
|
):
|
|
self.assertEqual(EPS_STATE_OFF + prepared + 2 * activated, expected)
|
|
|
|
def test_steering_torque_signals_exist_and_are_signed(self):
|
|
dbc = CANParser(DBC_NAME, [], 0).dbc
|
|
sigs = dbc.name_to_msg["STEERING_TORQUE"].sigs
|
|
for name in ("LKS_PREPARED", "CRUISE_ACTIVATED", "TORQUE_FAILED", "DRIVER_TORQUE",
|
|
"TARGET_ANGLE", "MAIN_TORQUE"):
|
|
self.assertIn(name, sigs, f"{name} missing from STEERING_TORQUE")
|
|
# driver torque must be signed or override detection cannot see direction
|
|
self.assertTrue(sigs["DRIVER_TORQUE"].is_signed)
|
|
self.assertTrue(sigs["MAIN_TORQUE"].is_signed)
|
|
self.assertEqual(sigs["DRIVER_TORQUE"].start_bit, 4)
|
|
self.assertEqual(sigs["DRIVER_TORQUE"].size, 12)
|
|
self.assertEqual(sigs["MAIN_TORQUE"].start_bit, 32)
|
|
self.assertEqual(sigs["MAIN_TORQUE"].size, 12)
|
|
|
|
def test_driver_torque_decodes_negative(self):
|
|
packer = CANPacker(DBC_NAME)
|
|
for torque in (-20.0, -0.5, 0.0, 0.5, 20.0):
|
|
_, dat, _ = packer.make_can_msg("STEERING_TORQUE", 0, {"DRIVER_TORQUE": torque})
|
|
vals = _unpack(DBC_NAME, "STEERING_TORQUE", dat)
|
|
self.assertAlmostEqual(vals["DRIVER_TORQUE"], torque, places=4)
|
|
|
|
|
|
class TestBydWheelSpeeds(unittest.TestCase):
|
|
def test_four_independent_wheels(self):
|
|
dbc = CANParser(DBC_NAME, [], 0).dbc
|
|
sigs = dbc.name_to_msg["WHEEL_SPEEDS"].sigs
|
|
for name, start in (("FL", 0), ("FR", 16), ("RL", 28), ("RR", 40)):
|
|
self.assertEqual(sigs[name].start_bit, start)
|
|
self.assertEqual(sigs[name].size, 12)
|
|
self.assertAlmostEqual(sigs[name].factor, 0.0725)
|
|
|
|
def test_wheel_speeds_round_trip(self):
|
|
packer = CANPacker(DBC_NAME)
|
|
_, dat, _ = packer.make_can_msg("WHEEL_SPEEDS", 0, {"FL": 50.0, "FR": 51.0, "RL": 52.0, "RR": 53.0})
|
|
vals = _unpack(DBC_NAME, "WHEEL_SPEEDS", dat)
|
|
for name, expected in (("FL", 50.0), ("FR", 51.0), ("RL", 52.0), ("RR", 53.0)):
|
|
self.assertAlmostEqual(vals[name], expected, delta=0.0725)
|
|
|
|
|
|
class TestBydFingerprint(unittest.TestCase):
|
|
def test_placeholder_never_matches_a_real_car(self):
|
|
# a platform whose ECU dict is empty survives as a candidate for EVERY car, so the
|
|
# placeholder must be a version no car reports rather than an empty dict
|
|
self.assertTrue(FW_VERSIONS[CAR.BYD_SEALION_7], "empty ECU dict would match every car")
|
|
|
|
live = build_fw_dict([CarParams.CarFw(ecu=Ecu.engine, fwVersion=b'REAL_CAR_FW', brand='byd',
|
|
address=0x7e0, subAddress=0)])
|
|
self.assertNotIn(str(CAR.BYD_SEALION_7), match_fw_to_car_exact(live, 'byd'))
|
|
|
|
def test_fuzzy_match_requires_vds(self):
|
|
# WMI + model year alone would claim every BYD of that year
|
|
from iqdbc.car.byd.values import match_fw_to_car_fuzzy
|
|
self.assertEqual(CAR.BYD_SEALION_7.config.vds_prefixes, set())
|
|
vin = "LGX" + "A" * 6 + "R" + "A" * 7 # LGX, 2024 model year
|
|
self.assertEqual(match_fw_to_car_fuzzy({}, vin, {}), set())
|
|
|
|
|
|
class TestBydCarController(unittest.TestCase):
|
|
"""The EPS latches a fault (state 11) if the 0x1E2 stream stops while it is actuating, and
|
|
re-arms only on a STEER_REQ rising edge over a continuous stream. The safety also statically
|
|
blocks the camera's own 0x1E2/0x316, so openpilot is the only source of both."""
|
|
|
|
def _run(self, lat_active, long_active=False, frames=20):
|
|
CP = CarInterface.get_non_essential_params("BYD_SEALION_7")
|
|
CP_IQ = CarInterface.get_non_essential_params_iq(CP, "BYD_SEALION_7")
|
|
CC_obj = structs.CarControl()
|
|
CC_obj.enabled = lat_active
|
|
CC_obj.latActive = lat_active
|
|
CC_obj.longActive = long_active
|
|
CC = CC_obj.as_reader()
|
|
CC_IQ = structs.IQCarControl()
|
|
|
|
carcontroller = CarInterface.CarController({'pt': DBC_NAME}, CP, CP_IQ)
|
|
carstate = CarInterface.CarState(CP, CP_IQ)
|
|
parsers = CarInterface.CarState.get_can_parsers(CP, CP_IQ)
|
|
cs_out, _ = carstate.update(parsers)
|
|
|
|
class _CS:
|
|
pass
|
|
cs = _CS()
|
|
cs.out = cs_out
|
|
cs.lkas_hud = carstate.lkas_hud
|
|
cs.acc_cmd = carstate.acc_cmd
|
|
cs.buttons = carstate.buttons
|
|
|
|
sent = []
|
|
for i in range(frames):
|
|
_, can_sends = carcontroller.update(CC, CC_IQ, cs, i * 10_000_000)
|
|
sent.append([addr for addr, _, _ in can_sends])
|
|
return sent
|
|
|
|
def test_steering_stream_is_continuous_when_inactive(self):
|
|
for lat_active in (True, False):
|
|
sent = self._run(lat_active)
|
|
steering = [i for i, addrs in enumerate(sent) if 0x1E2 in addrs]
|
|
hud = [i for i, addrs in enumerate(sent) if 0x316 in addrs]
|
|
# every other frame, whether or not lateral is active
|
|
self.assertEqual(steering, list(range(0, 20, 2)), f"{lat_active=}")
|
|
self.assertEqual(hud, list(range(0, 20, 2)), f"{lat_active=}")
|
|
|
|
def test_steer_req_gates_actuation_not_transmission(self):
|
|
CP = CarInterface.get_non_essential_params("BYD_SEALION_7")
|
|
CP_IQ = CarInterface.get_non_essential_params_iq(CP, "BYD_SEALION_7")
|
|
carcontroller = CarInterface.CarController({'pt': DBC_NAME}, CP, CP_IQ)
|
|
for lat_active in (False, True):
|
|
_, dat, _ = bydcan.create_steering_control(carcontroller.packer, 0.0, lat_active)
|
|
vals = _unpack(DBC_NAME, "STEERING_MODULE_ADAS", dat)
|
|
self.assertEqual(vals["STEER_REQ"], 1 if lat_active else 0)
|
|
|
|
def test_no_acc_cmd_without_openpilot_longitudinal(self):
|
|
sent = self._run(True, long_active=True)
|
|
self.assertFalse(any(0x32E in addrs for addrs in sent),
|
|
"0x32E sent while openpilotLongitudinalControl is off")
|
|
|
|
|
|
class TestBydLowSpeedAngleRate(unittest.TestCase):
|
|
"""Regression for the 2026-08-05 EPS latch. At 0.29 m/s the planner oscillated and the command
|
|
swung -5.9 to +2.4 deg against a stationary wheel in 220 ms; the EPS went from state 9 straight
|
|
to a latched 11 and took LKAS with it. The vehicle-model jerk limit cannot catch this because
|
|
it scales as 1/v^2."""
|
|
|
|
def _slew(self, v_ego, targets):
|
|
CP = CarInterface.get_non_essential_params("BYD_SEALION_7")
|
|
CP_IQ = CarInterface.get_non_essential_params_iq(CP, "BYD_SEALION_7")
|
|
cc = CarInterface.CarController({'pt': DBC_NAME}, CP, CP_IQ)
|
|
carstate = CarInterface.CarState(CP, CP_IQ)
|
|
cs_out, _ = carstate.update(CarInterface.CarState.get_can_parsers(CP, CP_IQ))
|
|
cs_out.vEgoRaw = v_ego
|
|
cs_out.vEgo = v_ego
|
|
cs_out.steeringAngleDeg = 0.1
|
|
|
|
class _CS:
|
|
pass
|
|
cs = _CS()
|
|
cs.out = cs_out
|
|
cs.lkas_hud = carstate.lkas_hud
|
|
cs.acc_cmd = carstate.acc_cmd
|
|
cs.buttons = carstate.buttons
|
|
|
|
CC_obj = structs.CarControl()
|
|
CC_obj.enabled = True
|
|
CC_obj.latActive = True
|
|
CC_IQ = structs.IQCarControl()
|
|
|
|
sent = []
|
|
for i, tgt in enumerate(targets):
|
|
CC_obj.actuators.steeringAngleDeg = tgt
|
|
cc.update(CC_obj.as_reader(), CC_IQ, cs, i * 10_000_000)
|
|
sent.append(cc.apply_angle_last)
|
|
return sent
|
|
|
|
# the actual planner output recorded during the fault
|
|
OSCILLATION = [-2.9, -5.9, -2.9, 0.1, 1.7, 1.8, 2.4, 2.2] * 3
|
|
|
|
def test_standstill_slew_is_bounded(self):
|
|
v = 0.29 # the speed at which the EPS latched
|
|
sent = self._slew(v, self.OSCILLATION)
|
|
cap = float(np.interp(v, CarControllerParams.ANGLE_RATE_BP, CarControllerParams.ANGLE_RATE_V))
|
|
steps = [abs(b - a) for a, b in zip(sent, sent[1:], strict=False)]
|
|
self.assertLessEqual(max(steps), cap + 1e-6,
|
|
"command slews faster than the standstill rate cap")
|
|
# the uncapped path stepped a full 3.0 deg/frame here
|
|
self.assertLess(cap, 1.0)
|
|
# and it must never wander far from the stationary wheel
|
|
self.assertLess(max(abs(a - 0.1) for a in sent), 2.0,
|
|
"command diverged from the measured angle at a standstill")
|
|
|
|
def test_rate_cap_scales_with_speed(self):
|
|
slow = self._slew(0.0, [30.0] * 10)
|
|
fast = self._slew(20.0, [30.0] * 10)
|
|
slow_step = max(abs(b - a) for a, b in zip(slow, slow[1:], strict=False))
|
|
fast_step = max(abs(b - a) for a, b in zip(fast, fast[1:], strict=False))
|
|
self.assertLess(slow_step, fast_step, "low-speed cap must be tighter than at speed")
|
|
self.assertLessEqual(fast_step, CarControllerParams.ANGLE_LIMITS.MAX_ANGLE_RATE + 1e-6)
|
|
|
|
|
|
class TestBydHarnessType(unittest.TestCase):
|
|
"""Longitudinal requires the ACC ECU to sit behind the relay so 0x32E is filterable. That is
|
|
a property of the harness, and it cannot be inferred from the fingerprint: fingerprinting
|
|
runs with the relay closed, which ties bus 2 to bus 0, so bus 2 shows the whole car either
|
|
way. Default must therefore be the camera harness (lateral only)."""
|
|
|
|
@staticmethod
|
|
def _params(cam_bus_addrs, alpha_long=True):
|
|
fp = {0: {0x1FC: 8, 0x1F0: 8}, 1: {}, 2: dict.fromkeys(cam_bus_addrs, 8)}
|
|
return CarInterface.get_params("BYD_SEALION_7", fp, [], alpha_long, False, False)
|
|
|
|
def test_defaults_to_camera_harness_lateral_only(self):
|
|
CP = self._params([0x1E2, 0x316])
|
|
self.assertFalse(CP.flags & BydFlags.GATEWAY_HARNESS)
|
|
self.assertFalse(CP.alphaLongitudinalAvailable)
|
|
self.assertFalse(CP.openpilotLongitudinalControl)
|
|
self.assertFalse(CP.safetyConfigs[0].safetyParam & BydSafetyFlags.LONG_CONTROL)
|
|
|
|
def test_acc_cmd_on_fingerprint_bus2_does_not_imply_gateway(self):
|
|
# the relay is closed while fingerprinting, so bus 2 sees the chassis bus too. Seeing
|
|
# 0x32E there must NOT unlock longitudinal.
|
|
CP = self._params([0x1E2, 0x316, 0x32E, 0x32D, 0x1FC])
|
|
self.assertFalse(CP.flags & BydFlags.GATEWAY_HARNESS)
|
|
self.assertFalse(CP.alphaLongitudinalAvailable)
|
|
self.assertFalse(CP.openpilotLongitudinalControl)
|
|
|
|
def test_lateral_still_available_on_camera_harness(self):
|
|
CP = self._params([0x1E2, 0x316])
|
|
self.assertFalse(CP.dashcamOnly)
|
|
self.assertEqual(CP.steerControlType, CarParams.SteerControlType.angle)
|
|
|
|
|
|
class TestBydCarParams(unittest.TestCase):
|
|
def test_angle_control_and_no_radar(self):
|
|
CP = CarInterface.get_non_essential_params("BYD_SEALION_7")
|
|
self.assertEqual(CP.brand, "byd")
|
|
self.assertEqual(CP.steerControlType, CarParams.SteerControlType.angle)
|
|
self.assertEqual(CP.safetyConfigs[0].safetyModel, CarParams.SafetyModel.byd)
|
|
# the BYD-6 harness jumpers the Veoneer private CAN-FD pair straight through
|
|
self.assertTrue(CP.radarUnavailable)
|
|
self.assertFalse(CP.dashcamOnly)
|
|
|
|
def test_steer_step_matches_safety_frequency(self):
|
|
# byd.h declares .frequency = 50U for the angle limiter
|
|
self.assertEqual(CarControllerParams.STEER_STEP, 2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|