Restore EPS8 Toyota smartDSU ownership

This commit is contained in:
firestar5683
2026-10-22 19:09:43 -05:00
parent 4413c29923
commit 204a6f8534
15 changed files with 505 additions and 38 deletions
@@ -8,7 +8,7 @@ from opendbc.car.common.pid import PIDController
from opendbc.car.secoc import add_mac, build_sync_mac
from opendbc.car.interfaces import CarControllerBase
from opendbc.car.toyota import toyotacan
from opendbc.car.toyota import prius_longitudinal, rav4h_filter, pedal
from opendbc.car.toyota import prius_longitudinal, rav4h_filter, pedal, eps8_sdsu
from opendbc.car.toyota.door_locks import DoorLocks
from opendbc.car.toyota.values import CAR, CarControllerParams, ToyotaFlags, TOYOTA_AUTO_HOLD_CARS, uses_toyota_auto_hold_aeb
from opendbc.can import CANPacker
@@ -82,6 +82,7 @@ class CarController(CarControllerBase):
self.standstill_req = False
self.last_standstill = False
self.prius_longitudinal = prius_longitudinal.enabled(CP)
self.eps8_hybrid = eps8_sdsu.configured(CP) and bool(CP.flags & ToyotaFlags.HYBRID)
self.permit_braking = True
self.steer_rate_counter = 0
self.distance_button = 0
@@ -271,6 +272,10 @@ class CarController(CarControllerBase):
self.last_standstill = CS.out.standstill
if not long_active and CS.out.standstill:
self.standstill_req = True
elif self.eps8_hybrid:
# Retain the existing hybrid resume behavior while active. The shared
# owner guard below still holds neutral when LONG or its source is lost.
self.standstill_req = not long_active and CS.out.standstill
elif rav4h_long or not self.CP.flags & ToyotaFlags.HYBRID.value:
should_resume = long_active and actuators.accel > 0 if rav4h_long else actuators.accel > 0
if should_resume:
+2 -2
View File
@@ -178,7 +178,7 @@ class CarState(CarStateBase):
if self.CP.flags & ToyotaFlags.TSS2 and not self.CP.flags & ToyotaFlags.DISABLE_RADAR.value:
# The radar filter intercepts this message. Preserve stock PCS/AEB
# observations without making intercepted ACC_CONTROL a required input.
if not self.CP.flags & ToyotaFlags.RADAR_CAN_FILTER:
if not self.CP.flags & ToyotaFlags.RADAR_CAN_FILTER and not rav4h_filter_configured(self.CP):
self.acc_type = cp_acc.vl["ACC_CONTROL"]["ACC_TYPE"]
ret.stockFcw = bool(cp_acc.vl["PCS_HUD"]["FCW"])
@@ -226,7 +226,7 @@ class CarState(CarStateBase):
buttonEvents.extend(create_button_events(1, 0, {1: ButtonType.lkas}) +
create_button_events(0, 1, {1: ButtonType.lkas}))
if self.CP.flags & ToyotaFlags.TSS2 and not (self.CP.flags & (ToyotaFlags.RADAR_ACC | ToyotaFlags.SECOC)):
if self.CP.flags & ToyotaFlags.TSS2 and not (self.CP.flags & (ToyotaFlags.RADAR_ACC | ToyotaFlags.SECOC | ToyotaFlags.LONG_FILTER)):
# distance button is wired to the ACC module (camera or radar)
prev_distance_button = self.distance_button
self.distance_button = cp_acc.vl["ACC_CONTROL"]["DISTANCE"]
@@ -0,0 +1,109 @@
"""Eight-byte EPS smartDSU ownership, separate from EPS5 and radar advertisements."""
from opendbc.car import structs
from opendbc.car.toyota.values import CAR, ToyotaFlags
CARS = frozenset(
(
CAR.TOYOTA_ALPHARD_TSS2,
CAR.TOYOTA_AVALON_TSS2,
CAR.TOYOTA_CAMRY_TSS2,
CAR.TOYOTA_COROLLA_TSS2,
CAR.TOYOTA_HIGHLANDER_TSS2,
CAR.TOYOTA_PRIUS_TSS2,
CAR.TOYOTA_RAV4_TSS2,
CAR.TOYOTA_MIRAI,
CAR.LEXUS_ES_TSS2,
CAR.LEXUS_NX_TSS2,
CAR.LEXUS_LC_TSS2,
CAR.LEXUS_RX_TSS2,
CAR.LEXUS_IS_TSS2,
CAR.LEXUS_RC_TSS2,
CAR.TOYOTA_AVALON_2019,
CAR.LEXUS_LS,
CAR.TOYOTA_CAMRY,
CAR.TOYOTA_CHR,
CAR.TOYOTA_CHR_TSS2,
CAR.TOYOTA_RAV4_TSS2_2022,
CAR.TOYOTA_RAV4_TSS2_2023,
)
)
WORDS = frozenset((0x8049, 0x8249, 0x8449, 0x8649))
def words(candidate):
return (0x8449, 0x8649) if candidate == CAR.TOYOTA_RAV4_TSS2_2023 else (0x8049, 0x8249)
def hardware_supported(candidate, fingerprint, car_fw):
if (
candidate not in CARS
or fingerprint.get(0, {}).get(0x2FF) != 8
or any(0x2FF in messages for bus, messages in fingerprint.items() if bus != 0)
or any(address in messages for messages in fingerprint.values() for address in (0x201, 0x2AA))
):
return False
# TSS2 camera ACC is normally on bus2. It is not evidence of a DSU bypass.
if not CAR(candidate).config.flags & ToyotaFlags.TSS2 and any(address in fingerprint.get(2, {}) for address in (0x343, 0x4CB)):
return False
from opendbc.car.toyota.fingerprints import FW_VERSIONS
known = FW_VERSIONS[candidate]
required = {structs.CarParams.Ecu.eps, structs.CarParams.Ecu.fwdCamera}
if any(ecu == structs.CarParams.Ecu.fwdRadar for ecu, _, _ in known):
required.add(structs.CarParams.Ecu.fwdRadar)
for ecu in required:
observed = [fw for fw in car_fw if fw.ecu == ecu]
if not observed or any(
fw.logging or fw.bus != 0 or fw.brand != "toyota" or bytes(fw.fwVersion) not in known.get((ecu, fw.address, fw.subAddress or None), ()) for fw in observed
):
return False
return True
def apply(cp, alpha_long):
cp.flags |= int(ToyotaFlags.LONG_FILTER)
cp.flags &= ~int(ToyotaFlags.DISABLE_RADAR)
gated = bool(cp.flags & (ToyotaFlags.NO_DSU | ToyotaFlags.RADAR_ACC))
cp.alphaLongitudinalAvailable = gated
cp.openpilotLongitudinalControl = bool(alpha_long or not gated or cp.flags & ToyotaFlags.TSS2 and not cp.flags & ToyotaFlags.RADAR_ACC)
cp.safetyConfigs[0].safetyParam = words(cp.carFingerprint)[not cp.openpilotLongitudinalControl]
def configured(cp):
if (
cp is None
or cp.brand != "toyota"
or cp.carFingerprint not in CARS
or cp.notCar
or cp.passive
or cp.dashcamOnly
or cp.secOcRequired
or not cp.pcmCruise
or len(cp.safetyConfigs) != 1
):
return False
required = int(CAR(cp.carFingerprint).config.flags | ToyotaFlags.LONG_FILTER)
if CAR(cp.carFingerprint).config.flags & ToyotaFlags.TSS2:
required |= int(ToyotaFlags.RAISED_ACCEL_LIMIT)
allowed = required | int(ToyotaFlags.HYBRID | ToyotaFlags.HAS_BSM)
if cp.carFingerprint in (CAR.TOYOTA_RAV4_TSS2_2022, CAR.TOYOTA_RAV4_TSS2_2023):
allowed |= int(ToyotaFlags.ARS510_RADAR | ToyotaFlags.ARS510_FUSED)
expected_type = structs.CarParams.SteerControlType.angle if cp.carFingerprint == CAR.TOYOTA_RAV4_TSS2_2023 else structs.CarParams.SteerControlType.torque
return bool(
cp.flags & required == required
and not int(cp.flags) & ~allowed
and (not cp.flags & ToyotaFlags.ARS510_FUSED or cp.flags & ToyotaFlags.ARS510_RADAR)
and cp.steerControlType == expected_type
and cp.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.toyota
and cp.safetyConfigs[0].safetyParam == words(cp.carFingerprint)[not cp.openpilotLongitudinalControl]
)
def prepare_stock(cp):
if not configured(cp):
return False
cp.openpilotLongitudinalControl = False
cp.safetyConfigs[0].safetyParam = words(cp.carFingerprint)[1]
cp.autoResumeSng = False
return True
+6 -2
View File
@@ -189,12 +189,16 @@ class CarInterface(CarInterfaceBase):
stop_and_go = True
elif candidate == CAR.TOYOTA_RAV4H and 0x2FF in fingerprint.get(0, {}):
ret.dashcamOnly = True
from opendbc.car.toyota import eps8_sdsu
eps8_filter = eps8_sdsu.hardware_supported(candidate, fingerprint, car_fw)
if eps8_filter:
eps8_sdsu.apply(ret, alpha_long)
# Toyota EPS accepts lateral commands down to zero. Keep the existing
# native/axis/fault gates; a stock cruise stop does not withdraw steering.
ret.steerAtStandstill = ret.minSteerSpeed <= 0.
ret.autoResumeSng = ret.openpilotLongitudinalControl and (
(rav4h_sdsu and candidate in (CAR.TOYOTA_RAV4H, CAR.TOYOTA_HIGHLANDER, CAR.TOYOTA_SIENNA)) or
not ret.flags & ToyotaFlags.LONG_FILTER)
(eps8_filter and bool(ret.flags & ToyotaFlags.TSS2)) or not ret.flags & ToyotaFlags.LONG_FILTER)
if radar_filter and not ret.flags & ToyotaFlags.TSS2:
ret.autoResumeSng = False # Preserve the pre-TSS2 stock stop timer.
@@ -206,7 +210,7 @@ class CarInterface(CarInterfaceBase):
# to a negative value, so it won't matter.
ret.minEnableSpeed = -1. if stop_and_go else MIN_ACC_SPEED
if ret.flags & (ToyotaFlags.TSS2 | ToyotaFlags.LONG_FILTER) and not rav4h_sdsu:
if ret.flags & ToyotaFlags.TSS2 or ret.flags & ToyotaFlags.LONG_FILTER and not (rav4h_sdsu or eps8_filter):
ret.flags |= ToyotaFlags.RAISED_ACCEL_LIMIT.value
# Hybrids have much quicker longitudinal actuator response
@@ -34,6 +34,9 @@ def hardware_supported(candidate, fingerprint, car_fw):
def configured(cp):
from opendbc.car.toyota import eps8_sdsu
if eps8_sdsu.configured(cp):
return True
if (cp is None or cp.brand != "toyota" or cp.carFingerprint not in EPS5_SDSU_CARS or
not cp.pcmCruise or cp.dashcamOnly or cp.notCar or cp.passive or
cp.steerControlType != structs.CarParams.SteerControlType.torque or len(cp.safetyConfigs) != 1):
@@ -58,6 +61,9 @@ def enabled(cp):
def prepare_stock(cp):
from opendbc.car.toyota import eps8_sdsu
if eps8_sdsu.prepare_stock(cp):
return True
if not configured(cp):
return False
cp.flags &= ~int(ToyotaFlags.AUTO_BRAKE_HOLD)
+12 -8
View File
@@ -156,7 +156,7 @@ static bool toyota_get_quality_flag_valid(const CANPacket_t *msg) {
}
} else {
}
if (toyota_aol_rav4h_filter && (msg->addr == 0x2FFU)) {
if (toyota_sdsu_status && (msg->addr == 0x2FFU)) {
const unsigned int state = msg->data[2] >> 4U;
valid = (GET_LEN(msg) == 8U) && ((state == 0U) || (state == 7U));
}
@@ -211,7 +211,7 @@ static void toyota_rx_hook(const CANPacket_t *msg) {
((unsigned int)msg->data[2] << 8U) + msg->data[3]) / 2U;
gas_pressed = gas > 805U;
}
if (toyota_aol_rav4h_filter && (msg->addr == 0x2FFU) && (GET_LEN(msg) == 8U)) {
if (toyota_sdsu_status && (msg->addr == 0x2FFU) && (GET_LEN(msg) == 8U)) {
// Only normal sDSU state owns replacement LONG. AEB stays healthy and retains independent LAT.
toyota_sdsu_control = (msg->data[2] >> 4U) == 0U;
}
@@ -353,8 +353,8 @@ static bool toyota_tx_hook(const CANPacket_t *msg) {
!gas_pressed && !brake_pressed && GET_BIT(msg, 39U) && (gas1 >= 475U) && (gas1 <= 1275U) && (gas2 >= 949U) && (gas2 <= 1749U) &&
(gas2 >= (gas1 + 473U)) && (gas2 <= (gas1 + 475U))));
}
const bool filter_ready = !(toyota_aol_rav4h_filter || toyota_pedal) || aol_rx_healthy();
const bool filter_long_ready = filter_ready && (!toyota_aol_rav4h_filter || toyota_sdsu_control);
const bool filter_ready = !(toyota_sdsu_status || toyota_pedal) || aol_rx_healthy();
const bool filter_long_ready = filter_ready && (!toyota_sdsu_status || toyota_sdsu_control);
// Check if msg is sent on BUS 0
if (msg->bus == 0U) {
@@ -580,7 +580,7 @@ static safety_config toyota_init(uint16_t param) {
toyota_alt_brake = GET_FLAG(param, TOYOTA_PARAM_ALT_BRAKE);
toyota_stock_longitudinal = GET_FLAG(param, TOYOTA_PARAM_STOCK_LONGITUDINAL);
toyota_radar_diagnostics_allowed = !toyota_stock_longitudinal && !toyota_secoc && (param != 0x1049U) && (param != 0x1449U);
toyota_radar_diagnostics_allowed = !toyota_stock_longitudinal && !toyota_secoc && (param != 0x1049U) && (param != 0x1449U) && !toyota_eps8_sdsu;
toyota_lta = GET_FLAG(param, TOYOTA_PARAM_LTA);
toyota_dbc_eps_torque_factor = param & TOYOTA_EPS_FACTOR;
@@ -601,7 +601,7 @@ static safety_config toyota_init(uint16_t param) {
if (toyota_stock_longitudinal) {
SET_TX_MSGS(TOYOTA_TX_MSGS, ret);
} else {
if ((param == 0x1049U) || (param == 0x9049U) || (param == 0x1449U)) {
if ((param == 0x1049U) || (param == 0x9049U) || (param == 0x1449U) || toyota_eps8_sdsu) {
SET_TX_MSGS(TOYOTA_FILTER_LONG_TX_MSGS_SET, ret);
} else {
SET_TX_MSGS(TOYOTA_LONG_TX_MSGS, ret);
@@ -619,9 +619,13 @@ static safety_config toyota_init(uint16_t param) {
// Check the quality flag for angle measurement when using LTA, since it's not set on TSS-P cars
static RxCheck toyota_lta_rx_checks[] = {
TOYOTA_RX_CHECKS(true)
{.msg = {{0x2FF, 0, 8, 50U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = false}, { 0 }, { 0 }}},
};
SET_RX_CHECKS(toyota_lta_rx_checks, ret);
if (!toyota_sdsu_status) {
ret.rx_checks_len -= 1;
}
} else {
// The final entry belongs only to the exact RAV4H filter profile. Share
// the ordinary checks instead of duplicating their mutable status table.
@@ -637,12 +641,12 @@ static safety_config toyota_init(uint16_t param) {
.ignore_checksum = !toyota_pedal, .ignore_counter = !toyota_pedal,
.max_counter = toyota_pedal ? 15U : 0U, .ignore_quality_flag = toyota_alt_cruise};
SET_RX_CHECKS(toyota_lka_rx_checks, ret);
if (!toyota_pedal && !toyota_alt_cruise && (param != 0x9049U) && (param != 0x9249U)) {
if (!toyota_pedal && !toyota_alt_cruise && !toyota_sdsu_status) {
ret.rx_checks_len -= 1; // Ordinary profiles do not require smartDSU.
}
}
if (((param & 0xC000U) != 0U) && !toyota_alt_cruise && (param != 0x9049U) && (param != 0x9249U)) {
if (((param & 0xC000U) != 0U) && !toyota_alt_cruise && !toyota_sdsu_status) {
ret.tx_msgs_len = 0;
}
if (((param & 0x2000U) != 0U) && !toyota_pedal) {
@@ -8,6 +8,8 @@ static bool toyota_aol_secoc = false;
static bool toyota_aol_stock = false;
static bool toyota_aol_lta = false;
static bool toyota_aol_rav4h_filter = false;
static bool toyota_sdsu_status = false;
static bool toyota_eps8_sdsu = false;
static bool toyota_sdsu_control = false;
static bool toyota_aol_main = false;
static bool toyota_aol_drive = false;
@@ -51,7 +53,7 @@ static uint8_t toyota_aol_permission(void) {
if (fresh && toyota_aol_main && toyota_aol_drive && toyota_aol_doors_closed && toyota_aol_eps) {
permission = request & 1U;
if (!toyota_aol_stock && controls_allowed && toyota_aol_belt && !gas_pressed && !brake_pressed &&
(!toyota_aol_rav4h_filter || toyota_sdsu_control)) {
(!toyota_sdsu_status || toyota_sdsu_control)) {
permission |= request & 2U;
}
}
@@ -123,17 +125,19 @@ static void toyota_aol_observe(const CANPacket_t *msg) {
static void toyota_aol_configure(uint16_t param) {
toyota_aol_reset();
toyota_eps8_sdsu = (param == 0x8049U) || (param == 0x8249U) || (param == 0x8449U) || (param == 0x8649U);
toyota_alt_cruise = param == 0x424DU;
toyota_aol_lta = (param == 0x449U) || (param == 0x649U) || (param == 0x1449U) || (param == 0x1649U);
toyota_aol_lta = (param == 0x449U) || (param == 0x649U) || (param == 0x1449U) || (param == 0x1649U) || (param == 0x8449U) || (param == 0x8649U);
toyota_pedal = (param == 0x2049U) || (param == 0x2249U);
toyota_aol_rav4h_filter = (param == 0x9049U) || (param == 0x9249U);
toyota_aol_stock = (param == 585U) || (param == 4681U) || (param == 0x9249U) || (param == 0x2249U) || toyota_alt_cruise || (param == 0x649U) || (param == 0x1649U);
toyota_sdsu_status = toyota_aol_rav4h_filter || toyota_eps8_sdsu;
toyota_aol_stock = (param == 585U) || (param == 4681U) || (param == 0x9249U) || (param == 0x2249U) || toyota_alt_cruise || (param == 0x649U) || (param == 0x1649U) || (param == 0x8249U) || (param == 0x8649U);
#ifdef ALLOW_DEBUG
toyota_aol_secoc = (param == 2121U);
#else
toyota_aol_secoc = false;
#endif
toyota_aol_enabled = (toyota_aol_lta && (alternative_experience == 32)) || (toyota_aol_stock && (alternative_experience == 32)) ||
toyota_aol_enabled = (toyota_eps8_sdsu && (alternative_experience == 32)) || (toyota_aol_lta && (alternative_experience == 32)) || (toyota_aol_stock && (alternative_experience == 32)) ||
#ifdef ALLOW_DEBUG
(toyota_aol_secoc && (alternative_experience == 32)) ||
#endif
@@ -0,0 +1,148 @@
import pytest
from opendbc.can import CANPacker
from opendbc.car import structs
from opendbc.safety.tests.libsafety import libsafety_py
from openpilot.starpilot.car.toyota.tests import test_pedal_caller_native as helpers
source_matched_native = helpers.source_matched_native
WORDS = (0x8049, 0x8249, 0x8449, 0x8649)
class Physical:
def __init__(self, word, experience=32):
self.native = libsafety_py.libsafety
self.native.init_tests()
self.native.set_alternative_experience(experience)
assert self.native.set_safety_hooks(structs.CarParams.SafetyModel.toyota, word) == 0
self.native.set_aol_test_heartbeat(True)
self.word, self.now = word, 1_000_000
self.packer = CANPacker("toyota_nodsu_pt_generated")
def tick(self, *, cruise=True, status=0, bad=None, **fields):
self.now += 10_000
self.native.set_timer(self.now)
frames = helpers.physical(self.packer, cruise=cruise, **fields)
frames = [f for f in frames if f[0] not in (0x260, 0x262)]
frames += [
self.packer.make_can_msg("STEER_TORQUE_SENSOR", 0, {"STEER_ANGLE": 0.0573}),
self.packer.make_can_msg("EPS_STATUS", 0, {"LKA_STATE": fields.get("eps", 1), "LTA_STATE": fields.get("eps", 1)}),
]
if status is not None:
frames.append(self.packer.make_can_msg("SDSU", 0, {"STATE": status}))
for address, data, bus in frames:
if address == 0x262 and bad == "eps_checksum":
data = data[:-1] + bytes([data[-1] ^ 1])
if address == 0x262 and bad == "eps_length":
data = data[:5]
if address == 0x2FF and bad == "status_length":
data = data[:7]
if address == 0x2FF and bad == "status_bus":
bus = 2
self.native.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data))
self.native.safety_tick()
self.native.aol_set_host_request(3)
def accel(self, value=0.1, cancel=0):
a, d, b = self.packer.make_can_msg("ACC_CONTROL", 0, {"ACCEL_CMD": value, "CANCEL_REQ": cancel})
return self.native.safety_tx_hook(libsafety_py.make_CANPacket(a, b, d))
def close(self):
self.native.set_alternative_experience(0)
self.native.set_safety_hooks(structs.CarParams.SafetyModel.noOutput, 0)
@pytest.mark.parametrize("word", WORDS)
def test_status_lease_state_aeb_and_recovery(word):
case = Physical(word)
try:
assert case.native.aol_get_permission_mask() == 0
for _ in range(25):
case.tick(cruise=False, status=None)
assert case.native.aol_get_permission_mask() == 0
for _ in range(25):
case.tick(cruise=False)
assert case.native.aol_get_permission_mask() == 1
for _ in range(25):
case.tick()
expected = 1 if word & 0x200 else 3
assert case.native.aol_get_permission_mask() == expected
assert bool(case.accel()) == (expected == 3)
case.tick(status=7)
assert case.native.safety_config_valid()
assert case.native.aol_get_permission_mask() == 1 and not case.accel()
case.tick(status=2)
assert not case.native.safety_config_valid()
assert case.native.aol_get_permission_mask() == 0 and not case.accel()
for _ in range(25):
case.tick(cruise=False)
for _ in range(25):
case.tick()
assert case.native.aol_get_permission_mask() == expected
finally:
case.close()
@pytest.mark.parametrize("word", WORDS)
@pytest.mark.parametrize("bad", ["eps_checksum", "eps_length", "status_length", "status_bus"])
def test_malformed_or_wrong_bus_cannot_refresh_required_sources(word, bad):
case = Physical(word)
try:
for _ in range(25):
case.tick(cruise=False)
for _ in range(25):
case.tick()
assert case.native.aol_get_permission_mask() == (1 if word & 0x200 else 3)
for _ in range(25):
case.tick(bad=bad)
assert case.native.aol_get_permission_mask() == 0 and not case.accel()
finally:
case.close()
@pytest.mark.parametrize("word", WORDS)
@pytest.mark.parametrize("fault", ["main", "gear", "eps", "brake", "gas"])
def test_physical_permission_withdrawal(word, fault):
case = Physical(word)
try:
for _ in range(25):
case.tick(cruise=False)
for _ in range(25):
case.tick()
case.tick(**{"main": {"main": False}, "gear": {"gear": 16}, "eps": {"eps": 9}, "brake": {"brake": True}, "gas": {"gas": True}}[fault])
assert case.native.aol_get_permission_mask() == (1 if fault in ("brake", "gas") else 0)
assert not case.accel()
finally:
case.close()
@pytest.mark.parametrize("word", WORDS)
def test_ordinary_cruise_and_stock_cancel_only(word):
case = Physical(word, 0)
try:
for _ in range(25):
case.tick(cruise=False)
for _ in range(25):
case.tick()
assert case.native.get_controls_allowed()
assert bool(case.accel()) == (not word & 0x200)
assert case.accel(0, 1)
for value in (-3.501, 2.001):
assert not case.accel(value)
for _ in range(25):
case.tick(status=None)
assert not case.accel()
finally:
case.close()
@pytest.mark.parametrize("word", [0x8048, 0x804A, 0x8149, 0x8248, 0x8448, 0x844A, 0x8648, 0x8749, 0xA049])
def test_unqualified_adjacent_words_have_no_commands(word):
case = Physical(word)
try:
for _ in range(25):
case.tick(cruise=False)
for _ in range(25):
case.tick()
assert case.native.aol_get_permission_mask() == 0 and not case.accel()
finally:
case.close()
@@ -167,7 +167,7 @@ int main() {
}
for (uint32_t word = 0U; word <= 65535U; ++word) {
const bool expected = (word == 73U) || (word == 585U) || (word == 4169U) || (word == 4681U) || (word == 2121U) || (word == 0x9049U) || (word == 0x9249U) || (word == 0x2049U) || (word == 0x2249U) || (word == 0x424DU) || (word == 0x449U) || (word == 0x649U) || (word == 0x1449U) || (word == 0x1649U);
const bool expected = (word == 73U) || (word == 585U) || (word == 4169U) || (word == 4681U) || (word == 2121U) || (word == 0x9049U) || (word == 0x9249U) || (word == 0x2049U) || (word == 0x2249U) || (word == 0x424DU) || (word == 0x449U) || (word == 0x649U) || (word == 0x1449U) || (word == 0x1649U) || (word == 0x8049U) || (word == 0x8249U) || (word == 0x8449U) || (word == 0x8649U);
auto toyota = status();
toyota.safety_mode = 2U;
toyota.safety_param = static_cast<uint16_t>(word);
@@ -432,14 +432,19 @@ int main() {
assert(!aol_capable(absent, GM_AOL_PROFILE.mode, VEHICLE_REGISTRY));
}
// One mode28 registry entry admits the ordinary PE transport as well as
// the unchanged Ioniq6 profiles; this is not an independent AOL grant.
// Ordinary PE transport and the separately qualified LONG/AOL profile
// both require the native capability; adjacent unqualified words stay denied.
auto pe = status();
pe.safety_mode = HYUNDAI_AOL_PROFILE.mode;
pe.safety_param = 0x5491U;
assert(aol_capable(pe, HYUNDAI_AOL_PROFILE.mode, VEHICLE_REGISTRY));
assert(aol_runtime_enabled(false, pe, VEHICLE_REGISTRY));
for (uint16_t denied : {0x5490U, 0x5493U, 0x5495U, 0x5411U, 0x54B1U, 0x5691U}) {
for (uint16_t admitted : {0x5491U, 0x5495U}) {
pe.safety_param = admitted;
assert(aol_capable(pe, HYUNDAI_AOL_PROFILE.mode, VEHICLE_REGISTRY));
assert(aol_runtime_enabled(false, pe, VEHICLE_REGISTRY));
pe.capability_flags = 0U;
assert(!aol_capable(pe, HYUNDAI_AOL_PROFILE.mode, VEHICLE_REGISTRY));
pe.capability_flags = 1U;
}
for (uint16_t denied : {0x5490U, 0x5493U, 0x5497U, 0x5411U, 0x54B1U, 0x5691U}) {
pe.safety_param = denied;
assert(!aol_capable(pe, HYUNDAI_AOL_PROFILE.mode, VEHICLE_REGISTRY));
}
@@ -481,7 +486,7 @@ int main() {
0x7090U, 0x7092U, 0x7208U, 0x720AU, 0x7228U, 0x722AU, 0x7809U, 0x7829U,
};
for (uint32_t word = 0U; word <= 0xFFFFU; ++word) {
bool expected = word == 0x0815U || word == 0x0895U || word == 0x8815U || word == 0x8895U || word == 0x5491U || word == 0x5C91U || word == 0x5C95U;
bool expected = word == 0x0815U || word == 0x0895U || word == 0x8815U || word == 0x8895U || word == 0x5491U || word == 0x5C91U || word == 0x5C95U || word == 0x5495U;
for (uint16_t stock_word : stock_canfd_words) {
expected = expected || word == stock_word;
}
+1 -1
View File
@@ -5,7 +5,7 @@ from opendbc.car.toyota.values import CAR, ToyotaFlags, ToyotaSafetyFlags, EPS_S
from openpilot.starpilot.aol.policy import AolVehiclePolicy
from openpilot.starpilot.aol.intent import AolCardIntent, AOL_TOGGLE
AOL_WORDS = frozenset((73, 585, 4169, 4681, 2121, 0x9049, 0x9249, 0x2049, 0x2249, 0x424D, 0x449, 0x649, 0x1449, 0x1649))
AOL_WORDS = frozenset((73, 585, 4169, 4681, 2121, 0x9049, 0x9249, 0x2049, 0x2249, 0x424D, 0x449, 0x649, 0x1449, 0x1649, 0x8049, 0x8249, 0x8449, 0x8649))
SECOC_TORQUE_CARS = frozenset((CAR.TOYOTA_SIENNA_4TH_GEN,))
+1 -1
View File
@@ -3,7 +3,7 @@
#include "selfdrive/pandad/aol_protocol.h"
inline bool toyota_aol_param(uint16_t param) {
return (param == 73U) || (param == 585U) || (param == 4169U) || (param == 4681U) || (param == 2121U) || (param == 0x9049U) || (param == 0x9249U) || (param == 0x2049U) || (param == 0x2249U) || (param == 0x424DU) || (param == 0x449U) || (param == 0x649U) || (param == 0x1449U) || (param == 0x1649U);
return (param == 73U) || (param == 585U) || (param == 4169U) || (param == 4681U) || (param == 2121U) || (param == 0x9049U) || (param == 0x9249U) || (param == 0x2049U) || (param == 0x2249U) || (param == 0x424DU) || (param == 0x449U) || (param == 0x649U) || (param == 0x1449U) || (param == 0x1649U) || (param == 0x8049U) || (param == 0x8249U) || (param == 0x8449U) || (param == 0x8649U);
}
inline constexpr AolSafetyProfile TOYOTA_AOL_PROFILE{2U, toyota_aol_param, true};
@@ -0,0 +1,81 @@
import pytest
from opendbc.car import gen_empty_fingerprint
from opendbc.car.toyota import eps8_sdsu
from opendbc.car.toyota.interface import CarInterface
from opendbc.car.toyota.values import CAR, ToyotaFlags
from openpilot.starpilot.car.toyota.tests.test_pedal_caller_native import firmware
from openpilot.starpilot.car.toyota.aol import qualified
@pytest.mark.parametrize('candidate', sorted(eps8_sdsu.CARS))
@pytest.mark.parametrize('alpha', [False, True])
def test_eps8_sdsu_factory_and_saved_off(candidate, alpha):
fp = gen_empty_fingerprint()
fp[0][0x2FF] = 8
cp = CarInterface.get_params(candidate, fp, firmware(candidate), alpha, False, False)
assert eps8_sdsu.configured(cp) and qualified(cp)
long = (
alpha
or not candidate.config.flags & (ToyotaFlags.NO_DSU | ToyotaFlags.RADAR_ACC)
or bool(candidate.config.flags & ToyotaFlags.TSS2 and not candidate.config.flags & ToyotaFlags.RADAR_ACC)
)
assert cp.openpilotLongitudinalControl == long
assert not cp.flags & ToyotaFlags.DISABLE_RADAR
assert cp.safetyConfigs[0].safetyParam == eps8_sdsu.words(candidate)[not long]
assert eps8_sdsu.prepare_stock(cp)
assert eps8_sdsu.configured(cp) and qualified(cp) and not cp.openpilotLongitudinalControl
@pytest.mark.parametrize('candidate', sorted(eps8_sdsu.CARS))
@pytest.mark.parametrize('bad', ['length', 'mirror', 'pedal', 'mixed', 'firmware'])
def test_eps8_hardware_boundaries(candidate, bad):
fp = gen_empty_fingerprint()
fp[0][0x2FF] = 8
fw = firmware(candidate)
if bad == 'length':
fp[0][0x2FF] = 4
if bad == 'mirror':
fp[2][0x2FF] = 8
if bad == 'pedal':
fp[0][0x201] = 6
if bad == 'mixed':
fp[0][0x2AA] = 8
if bad == 'firmware':
fw = []
assert not eps8_sdsu.hardware_supported(candidate, fp, fw)
@pytest.mark.parametrize('candidate', [CAR.TOYOTA_RAV4_PRIME, CAR.TOYOTA_SIENNA_4TH_GEN, CAR.TOYOTA_RAV4H, CAR.TOYOTA_PRIUS])
def test_other_native_topologies_not_claimed(candidate):
fp = gen_empty_fingerprint()
fp[0][0x2FF] = 8
assert not eps8_sdsu.hardware_supported(candidate, fp, firmware(candidate))
@pytest.mark.parametrize("candidate", sorted(eps8_sdsu.CARS))
@pytest.mark.parametrize("alpha", [False, True])
@pytest.mark.parametrize("hybrid", [False, True])
def test_sdsu_keeps_existing_vehicle_configuration_and_tune(candidate, alpha, hybrid):
from opendbc.car import structs
from opendbc.car.toyota.values import CarControllerParams
from opendbc.car.toyota.radar_filter import PROFILES
fw = firmware(candidate)
if hybrid:
fw.append(
structs.CarParams.CarFw(ecu=structs.CarParams.Ecu.hybrid, address=0x7E2, fwVersion=b"synthetic hybrid ECU response", brand="toyota", bus=0, logging=True)
)
existing = gen_empty_fingerprint()
if candidate in PROFILES:
existing[0][0x2AA] = 8
new = gen_empty_fingerprint()
new[0][0x2FF] = 8
reference = CarInterface.get_params(candidate, existing, fw, alpha, False, False)
actual = CarInterface.get_params(candidate, new, fw, alpha, False, False)
before, after = reference.to_dict(), actual.to_dict()
for field in ("flags", "safetyConfigs", "openpilotLongitudinalControl", "alphaLongitudinalAvailable", "autoResumeSng"):
before.pop(field, None)
after.pop(field, None)
assert before == after
baseline_limits, new_limits = CarControllerParams(reference), CarControllerParams(actual)
assert (baseline_limits.ACCEL_MIN, baseline_limits.ACCEL_MAX) == (new_limits.ACCEL_MIN, new_limits.ACCEL_MAX)
@@ -0,0 +1,89 @@
import pytest
from opendbc.car.toyota import eps8_sdsu
from opendbc.car.toyota.values import CAR, ToyotaFlags
from openpilot.starpilot.car.toyota.tests import test_rav4h_caller_native as helpers
from openpilot.starpilot.car.toyota.tests.test_toyota_cpp_transport import host_heartbeat_bridge as host_heartbeat_bridge
source_matched_native = helpers.source_matched_native
daemon_gc_scope = helpers.daemon_gc_scope
def run(candidate, stock, bridge, scenario='nominal', aol=True, alpha=True, disable_setting=None, firmware=None, hybrid_release=False):
angle = candidate == CAR.TOYOTA_RAV4_TSS2_2023
camera = bool(candidate.config.flags & ToyotaFlags.TSS2 and not candidate.config.flags & ToyotaFlags.RADAR_ACC)
def physical(packer, **fields):
frames = helpers.physical(packer, **fields)
frames = [f for f in frames if f[0] not in (0x260, 0x262)]
frames += [
packer.make_can_msg('STEER_TORQUE_SENSOR', 0, {'STEER_ANGLE': 0.0573}),
packer.make_can_msg('EPS_STATUS', 0, {'LKA_STATE': fields.get('eps', 1), 'LTA_STATE': fields.get('eps', 1)}),
packer.make_can_msg('PCS_HUD', 2 if camera else 0, {}),
]
if camera:
frames.append(packer.make_can_msg('PRE_COLLISION', 2, {}))
return frames
helpers.run_case(
stock,
scenario,
candidate=candidate,
bridge=bridge,
input_clock=True,
aol=aol,
sdsu_words=eps8_sdsu.words(candidate),
alpha_long=alpha,
disable_setting=disable_setting,
physical_frames=physical,
angle=angle,
brake_stop_check_start=355,
firmware_override=firmware,
assert_hybrid_release=hybrid_release,
)
@pytest.mark.parametrize('candidate', sorted(eps8_sdsu.CARS))
@pytest.mark.parametrize('stock', [False, True])
def test_each_eps8_full_caller(candidate, stock, host_heartbeat_bridge):
run(candidate, stock, host_heartbeat_bridge)
@pytest.mark.parametrize('candidate', [CAR.TOYOTA_CAMRY, CAR.TOYOTA_COROLLA_TSS2, CAR.TOYOTA_RAV4_TSS2_2023])
@pytest.mark.parametrize('stock', [False, True])
@pytest.mark.parametrize('scenario', ['physical', 'faults', 'bad_dlc', 'off'])
def test_eps8_fault_source_and_preference_callers(candidate, stock, scenario, host_heartbeat_bridge):
run(candidate, stock, host_heartbeat_bridge, scenario)
@pytest.mark.parametrize('candidate', [CAR.TOYOTA_CAMRY, CAR.TOYOTA_COROLLA_TSS2, CAR.TOYOTA_RAV4_TSS2_2023])
@pytest.mark.parametrize('stock', [False, True])
def test_eps8_ordinary_cruise_callers(candidate, stock, host_heartbeat_bridge):
run(candidate, stock, host_heartbeat_bridge, aol=False)
@pytest.mark.parametrize(
'candidate', [CAR.TOYOTA_CAMRY, CAR.TOYOTA_CHR, CAR.TOYOTA_CHR_TSS2, CAR.TOYOTA_RAV4_TSS2_2022, CAR.TOYOTA_RAV4_TSS2_2023, CAR.TOYOTA_COROLLA_TSS2]
)
def test_eps8_alpha_off_actual_callers(candidate, host_heartbeat_bridge):
stock = bool(candidate.config.flags & ToyotaFlags.RADAR_ACC or not candidate.config.flags & ToyotaFlags.TSS2)
run(candidate, stock, host_heartbeat_bridge, alpha=False, disable_setting=False)
@pytest.mark.parametrize("candidate", [CAR.TOYOTA_CAMRY, CAR.TOYOTA_CAMRY_TSS2, CAR.TOYOTA_RAV4_TSS2_2023])
def test_detected_hybrid_preserves_active_resume(candidate, host_heartbeat_bridge):
from opendbc.car import structs
from opendbc.car.toyota.values import FW_QUERY_CONFIG
# Firmware acquisition boundary: model a response from Toyota's real logging
# hybrid query. Keep the catalog EPS/camera/radar identity used for enrollment.
assert (structs.CarParams.Ecu.hybrid, 0x7E2, None) in FW_QUERY_CONFIG.extra_ecus
fw = helpers.firmware(candidate)
fw.append(
structs.CarParams.CarFw(ecu=structs.CarParams.Ecu.hybrid, address=0x7E2, fwVersion=b"synthetic hybrid ECU response", brand="toyota", bus=0, logging=True)
)
run(candidate, False, host_heartbeat_bridge, scenario="hybrid_stop", firmware=fw, hybrid_release=True)
@pytest.mark.parametrize("candidate", [CAR.TOYOTA_AVALON_2019, CAR.LEXUS_LS])
def test_nongated_legacy_sdsu_alpha_off(candidate, host_heartbeat_bridge):
run(candidate, False, host_heartbeat_bridge, alpha=False, disable_setting=False)
@@ -23,7 +23,7 @@ from opendbc.can import CANPacker
from opendbc.car import Bus, car_helpers, gen_empty_fingerprint, structs
from opendbc.car.fw_versions import match_fw_to_car
from opendbc.car.toyota.fingerprints import FW_VERSIONS
from opendbc.car.toyota.values import CAR, DBC
from opendbc.car.toyota.values import CAR, DBC, ToyotaFlags
from opendbc.car.toyota.tests.test_auto_hold import decode
from opendbc.safety.tests.libsafety import libsafety_py
from openpilot.cereal import messaging
@@ -224,7 +224,9 @@ def control_inputs(controls, published, state, sd_events, native, now):
return values
def run_case(stock, scenario='nominal', *, aol=True, candidate=CAR.TOYOTA_RAV4H, bridge=None, input_clock=False, brake_stop_check_start=350):
def run_case(stock, scenario='nominal', *, aol=True, candidate=CAR.TOYOTA_RAV4H, bridge=None, input_clock=False, brake_stop_check_start=350,
sdsu_words=(0x9049, 0x9249), alpha_long=False, disable_setting=None, physical_frames=None, angle=False,
firmware_override=None, assert_hybrid_release=False):
SYNTHETIC_LAST.clear()
NATIVE_LAST.clear()
safety = libsafety_py.libsafety
@@ -235,11 +237,12 @@ def run_case(stock, scenario='nominal', *, aol=True, candidate=CAR.TOYOTA_RAV4H,
('OpenpilotEnabledToggle', True),
('SafeMode', False),
('AlwaysOnLateral', aol),
('DisableOpenpilotLongitudinal', stock),
('DisableOpenpilotLongitudinal', stock if disable_setting is None else disable_setting),
('AlphaLongitudinalEnabled', alpha_long),
('ToyotaAutoHold', False),
):
params.put_bool(k, v, block=True)
fw = firmware(candidate)
fw = firmware(candidate) if firmware_override is None else firmware_override
exact, found = match_fw_to_car(fw, '', log=False)
assert exact and found == {candidate}
fp = gen_empty_fingerprint()
@@ -252,7 +255,7 @@ def run_case(stock, scenario='nominal', *, aol=True, candidate=CAR.TOYOTA_RAV4H,
):
host = card_module.Car()
assert host.CP.openpilotLongitudinalControl == (not stock)
assert host.CP.safetyConfigs[0].safetyParam == (0x9249 if stock else 0x9049)
assert host.CP.safetyConfigs[0].safetyParam == sdsu_words[stock]
assert host.CP.alternativeExperience == 32
controls, sd = Controls(), sd_module.SelfdriveD(CP=host.CP)
assert controls.CP.to_dict() == host.CP.to_dict()
@@ -307,7 +310,7 @@ def run_case(stock, scenario='nominal', *, aol=True, candidate=CAR.TOYOTA_RAV4H,
patch.object(time, 'clock_gettime_ns', side_effect=lambda c: clock[0] if c in (time.CLOCK_MONOTONIC, time.CLOCK_BOOTTIME) else real_clock(c))
)
try:
for tick in range(1, {'nominal': 251, 'physical': 601, 'faults': 801, 'bad_dlc': 801, 'off': 401, 'cold_low': 251}[scenario]):
for tick in range(1, {'nominal': 251, 'physical': 601, 'faults': 801, 'bad_dlc': 801, 'off': 401, 'cold_low': 251, 'hybrid_stop': 251}[scenario]):
if deterministic:
clock[0] += 10_000_000
else:
@@ -316,6 +319,8 @@ def run_case(stock, scenario='nominal', *, aol=True, candidate=CAR.TOYOTA_RAV4H,
now = time.monotonic_ns()
safety.set_timer((now // 1000) & 0xFFFFFFFF)
fields = {'cruise': tick >= 150}
if scenario == 'hybrid_stop':
fields.update(stopped=200 <= tick < 230)
if scenario == 'cold_low':
fields.update(cruise=False, stopped=True)
if scenario == 'physical':
@@ -335,7 +340,7 @@ def run_case(stock, scenario='nominal', *, aol=True, candidate=CAR.TOYOTA_RAV4H,
)
elif scenario == 'off' and tick == 250:
params.put_bool('DisableOpenpilotLongitudinal', not stock, block=True)
frames = physical(packer, **fields)
frames = (physical if physical_frames is None else physical_frames)(packer, **fields)
if scenario == 'bad_dlc' and 630 <= tick < 665:
malformed = packer.make_can_msg('SDSU', 0, {'STATE': 0})
frames.append((malformed[0], malformed[1][:7], malformed[2]))
@@ -397,8 +402,15 @@ def run_case(stock, scenario='nominal', *, aol=True, candidate=CAR.TOYOTA_RAV4H,
if f.address == 0x2E4:
decoded = decode(host.CP, (f.address, bytes(f.dat), f.src), 'STEERING_LKA')
positive_steering |= bool(decoded['STEER_REQUEST'] and decoded['STEER_TORQUE_CMD'])
elif f.address == 0x191 and angle:
decoded = decode(host.CP, (f.address, bytes(f.dat), f.src), 'STEERING_LTA')
positive_steering |= bool(decoded['STEER_REQUEST'] and decoded['STEER_ANGLE_CMD'])
elif f.address == 0x343:
positive_accel |= decode(host.CP, (f.address, bytes(f.dat), f.src), 'ACC_CONTROL')['ACCEL_CMD'] > 0
acc = decode(host.CP, (f.address, bytes(f.dat), f.src), 'ACC_CONTROL')
positive_accel |= acc['ACCEL_CMD'] > 0
if assert_hybrid_release and cc.longActive:
assert host.CP.flags & ToyotaFlags.HYBRID
assert acc['RELEASE_STANDSTILL'] == 1, ('active hybrid resume behavior changed', tick, acc)
trace.append(
{
'tick': tick,
@@ -422,7 +434,7 @@ def run_case(stock, scenario='nominal', *, aol=True, candidate=CAR.TOYOTA_RAV4H,
}
)
nominal = tick >= 220 and (
scenario in ('nominal', 'cold_low')
scenario in ('nominal', 'cold_low', 'hybrid_stop')
or scenario == 'off'
and tick < 250
or scenario == 'physical'
@@ -458,7 +470,7 @@ def run_case(stock, scenario='nominal', *, aol=True, candidate=CAR.TOYOTA_RAV4H,
if scenario == 'off' and tick >= 310:
if stock:
assert not host.CP.openpilotLongitudinalControl and not cc.longActive
assert host.CP.safetyConfigs[0].safetyParam == 0x9249
assert host.CP.safetyConfigs[0].safetyParam == sdsu_words[1]
else:
assert not host.CI.CC.rav4h_filter_input.update(time.monotonic_ns())
for f in outgoing.sendcan:
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "7ad958f6080cde1c5e80081ee1cd24927df9a703189143fcb8d15c8918cc5564", "schema_version": 1}
{"protected_inventory_sha256": "8ea2d6a19e2bab4b32310b820f3061f94c9dd66957fc2c1ba8686131932c1949", "schema_version": 1}