Restore Forester first generation angle control

This commit is contained in:
firestar5683
2026-10-22 19:52:53 -05:00
parent 94f88b41a9
commit d747e5ca08
16 changed files with 385 additions and 48 deletions
@@ -13,7 +13,7 @@ from opendbc.car.vehicle_model import VehicleModel
# involves the total steering angle change rather than rate, but these limits work well for now
MAX_STEER_RATE = 25 # deg/s
MAX_STEER_RATE_FRAMES = 7 # tx control frames needed before torque can be cut
GEN2_ANGLE_PORTS = frozenset((CAR.SUBARU_CROSSTREK_2025, CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023))
GEN2_ANGLE_PORTS = frozenset((CAR.SUBARU_CROSSTREK_2025, CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023, CAR.SUBARU_FORESTER_2022))
class CarController(CarControllerBase):
@@ -136,12 +136,13 @@ class CarController(CarControllerBase):
CS.out.vEgoRaw, CS.out.steeringAngleDeg, angle_active,
self.p.FIXED_ANGLE_LIMITS)
else:
assert self.VM is not None
apply_angle = apply_steer_angle_limits_vm(actuators.steeringAngleDeg, self.apply_angle_last,
CS.out.vEgoRaw, CS.out.steeringAngleDeg, angle_active,
self.p, self.VM)
self.apply_angle_last = apply_angle
self.angle_was_active = angle_active
angle_bus = CanBus.main if self.CP.carFingerprint == CAR.SUBARU_ASCENT_2023 else CanBus.alt
angle_bus = CanBus.main if self.CP.carFingerprint in (CAR.SUBARU_ASCENT_2023, CAR.SUBARU_FORESTER_2022) else CanBus.alt
can_sends.append(subarucan.create_steering_control_angle(self.packer, apply_angle, angle_active, angle_bus))
else:
apply_torque = int(round(actuators.torque * self.p.STEER_MAX))
+1 -1
View File
@@ -99,7 +99,7 @@ class CarState(CarStateBase):
cp_cruise = cp_alt if self.CP.flags & SubaruFlags.GLOBAL_GEN2 else cp
cp_es_brake = cp_alt if self.CP.flags & SubaruFlags.GLOBAL_GEN2 else cp_cam
if self.CP.carFingerprint in (CAR.SUBARU_CROSSTREK_2025, CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023):
if self.CP.carFingerprint in (CAR.SUBARU_CROSSTREK_2025, CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023, CAR.SUBARU_FORESTER_2022):
ret.cruiseState.enabled = cp_es_brake.vl["ES_Status"]["Cruise_Activated"] != 0
ret.cruiseState.available = cp_cam.vl["ES_DashStatus"]["Cruise_On"] != 0
elif self.CP.flags & (SubaruFlags.HYBRID | SubaruFlags.LKAS_ANGLE):
@@ -1,9 +1,9 @@
"""Reject contradictory discovery evidence for the two supported Gen2 angle identities."""
"""Reject contradictory discovery evidence for the scoped Subaru angle identities."""
from opendbc.car.fw_query_definitions import LiveFwVersions, OfflineFwVersions
from opendbc.car.structs import CarParams
SCOPED_CANDIDATES = frozenset(("SUBARU_CROSSTREK_2025", "SUBARU_LEGACY_2025"))
SCOPED_CANDIDATES = frozenset(("SUBARU_CROSSTREK_2025", "SUBARU_LEGACY_2025", "SUBARU_FORESTER_2022"))
REQUIRED_ECUS = frozenset((CarParams.Ecu.abs, CarParams.Ecu.fwdCamera, CarParams.Ecu.engine))
+1 -1
View File
@@ -7,7 +7,7 @@ from opendbc.car.subaru.carcontroller import CarController
from opendbc.car.subaru.carstate import CarState
from opendbc.car.subaru.values import CAR, GLOBAL_ES_ADDR, CanBus, SubaruFlags, SubaruSafetyFlags
GEN2_ANGLE_PORTS = frozenset((CAR.SUBARU_CROSSTREK_2025, CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023))
GEN2_ANGLE_PORTS = frozenset((CAR.SUBARU_CROSSTREK_2025, CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023, CAR.SUBARU_FORESTER_2022))
class CarInterface(CarInterfaceBase):
+2 -2
View File
@@ -5,7 +5,7 @@ from opendbc.car.subaru.values import CAR, SubaruFlags, SubaruSafetyFlags
AOL_EXPERIENCE = 32
TORQUE_CARS = frozenset((CAR.SUBARU_ASCENT, CAR.SUBARU_OUTBACK, CAR.SUBARU_LEGACY,
CAR.SUBARU_IMPREZA, CAR.SUBARU_IMPREZA_2020, CAR.SUBARU_FORESTER))
ANGLE_CARS = frozenset((CAR.SUBARU_CROSSTREK_2025, CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023))
ANGLE_CARS = frozenset((CAR.SUBARU_CROSSTREK_2025, CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023, CAR.SUBARU_FORESTER_2022))
def qualified(cp, *, marked_only=False):
@@ -26,7 +26,7 @@ def qualified(cp, *, marked_only=False):
angle = cp.carFingerprint in ANGLE_CARS
if angle:
expected |= int(SubaruSafetyFlags.LKAS_ANGLE)
if cp.carFingerprint != CAR.SUBARU_CROSSTREK_2025:
if cp.carFingerprint in (CAR.SUBARU_LEGACY_2025, CAR.SUBARU_ASCENT_2023):
expected |= int(SubaruSafetyFlags.FIXED_ANGLE_LIMITS)
if cp.carFingerprint == CAR.SUBARU_ASCENT_2023:
expected |= int(SubaruSafetyFlags.ANGLE_MAIN_BUS)
@@ -7,7 +7,7 @@ from opendbc.car import Bus, structs
from opendbc.car.subaru import subarucan
from opendbc.car.subaru.stock_aol import ANGLE_CARS, TORQUE_CARS
from opendbc.car.subaru.tests.test_ascent_angle import setup_controller, step
from opendbc.car.subaru.values import CAR, DBC
from opendbc.car.subaru.values import DBC, SubaruSafetyFlags
from opendbc.safety.tests import test_subaru_gen2_angle_pair as pair
from opendbc.safety.tests.libsafety import libsafety_py
@@ -17,7 +17,7 @@ from opendbc.safety.tests.libsafety import libsafety_py
@pytest.mark.parametrize('position', ('within', 'limit', 'beyond'))
def test_inactive_sensor_angle_is_bounded_and_native_accepts(car, sign, position):
cp, controller, command, state = setup_controller(car)
limit = 650.0 if car == CAR.SUBARU_CROSSTREK_2025 else 545.0
limit = 545.0 if cp.safetyConfigs[0].safetyParam & SubaruSafetyFlags.FIXED_ANGLE_LIMITS else 650.0
measured = sign * {'within': 100.0, 'limit': limit, 'beyond': limit + 5.0}[position]
packer, frames = pair.TestSubaruGen2AnglePair.sources(car, cruise=False)
measurement = packer.make_can_msg('Steering_2', 0, {'Steering_Angle': measured})
+29 -5
View File
@@ -77,6 +77,7 @@
{.msg = {{MSG_SUBARU_ES_DashStatus, SUBARU_CAM_BUS, 8, 10U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}}, \
static bool subaru_gen2 = false;
static bool subaru_forester_angle = false;
static bool subaru_lkas_angle = false;
static bool subaru_fixed_angle_limits = false;
static bool subaru_angle_main_bus = false;
@@ -123,7 +124,7 @@ static void subaru_rx_hook(const CANPacket_t *msg) {
update_sample(&angle_meas, angle_meas_new);
}
if (subaru_lkas_angle && (msg->addr == MSG_SUBARU_ES_Status) && (msg->bus == SUBARU_ALT_BUS)) {
if (subaru_lkas_angle && (msg->addr == MSG_SUBARU_ES_Status) && (msg->bus == (subaru_forester_angle ? SUBARU_CAM_BUS : SUBARU_ALT_BUS))) {
pcm_cruise_check(GET_BIT(msg, 29U));
subaru_aol_grant(GET_BIT(msg, 29U));
}
@@ -200,6 +201,12 @@ static bool subaru_tx_hook(const CANPacket_t *msg) {
.wheelbase = 2.6700000762939453,
};
const AngleSteeringParams SUBARU_FORESTER_2022_ANGLE_PARAMS = {
.slip_factor = -0.0006281956135115129,
.steer_ratio = 17.0,
.wheelbase = 2.6700000762939453,
};
const LongitudinalLimits SUBARU_LONG_LIMITS = {
.min_gas = 808, // appears to be engine braking
.max_gas = 3400, // approx 2 m/s^2 when maxing cruise_rpm and cruise_throttle
@@ -244,7 +251,7 @@ static bool subaru_tx_hook(const CANPacket_t *msg) {
if (subaru_fixed_angle_limits) {
violation |= steer_angle_cmd_checks(desired_angle, lkas_request, SUBARU_FIXED_ANGLE_STEERING_LIMITS);
} else {
violation |= steer_angle_cmd_checks_vm(desired_angle, lkas_request, SUBARU_ANGLE_STEERING_LIMITS, SUBARU_CROSSTREK_2025_ANGLE_PARAMS);
violation |= steer_angle_cmd_checks_vm(desired_angle, lkas_request, SUBARU_ANGLE_STEERING_LIMITS, subaru_forester_angle ? SUBARU_FORESTER_2022_ANGLE_PARAMS : SUBARU_CROSSTREK_2025_ANGLE_PARAMS);
}
}
@@ -344,6 +351,11 @@ static safety_config subaru_init(uint16_t param) {
{0x390U, SUBARU_ALT_BUS, 8, .check_relay = false},
};
static const CanMsg SUBARU_FORESTER_ANGLE_TX_MSGS[] = {
SUBARU_BASE_TX_MSGS(SUBARU_MAIN_BUS, MSG_SUBARU_ES_LKAS_ANGLE)
SUBARU_COMMON_TX_MSGS(SUBARU_MAIN_BUS)
};
const uint16_t SUBARU_PARAM_ASCENT_ALERT = 2048;
const bool ascent_alert = param == (1U | 16U | 128U | 32U | SUBARU_PARAM_ASCENT_ALERT);
const uint16_t SUBARU_PARAM_GEN2 = 1;
@@ -353,12 +365,14 @@ static safety_config subaru_init(uint16_t param) {
const uint16_t SUBARU_PARAM_ANGLE_MAIN_BUS = 32;
subaru_angle_main_bus = GET_FLAG(param, SUBARU_PARAM_ANGLE_MAIN_BUS);
subaru_forester_angle = (param == 16U) &&
(((unsigned int)alternative_experience == 0U) || ((unsigned int)alternative_experience == 32U));
subaru_gen2 = GET_FLAG(param, SUBARU_PARAM_GEN2);
subaru_lkas_angle = GET_FLAG(param, SUBARU_PARAM_LKAS_ANGLE);
subaru_fixed_angle_limits = GET_FLAG(param, SUBARU_PARAM_FIXED_ANGLE);
subaru_angle_invalid = (subaru_lkas_angle && (base_param != (SUBARU_PARAM_GEN2 | SUBARU_PARAM_LKAS_ANGLE)) &&
(base_param != (SUBARU_PARAM_GEN2 | SUBARU_PARAM_LKAS_ANGLE | SUBARU_PARAM_FIXED_ANGLE)) &&
(base_param != (SUBARU_PARAM_GEN2 | SUBARU_PARAM_LKAS_ANGLE | SUBARU_PARAM_FIXED_ANGLE | SUBARU_PARAM_ANGLE_MAIN_BUS)) && !ascent_alert) ||
(base_param != (SUBARU_PARAM_GEN2 | SUBARU_PARAM_LKAS_ANGLE | SUBARU_PARAM_FIXED_ANGLE | SUBARU_PARAM_ANGLE_MAIN_BUS)) && !ascent_alert && !subaru_forester_angle) ||
((subaru_fixed_angle_limits || subaru_angle_main_bus) && !subaru_lkas_angle);
// TODO: re-enable once more work is done on the limits
@@ -372,7 +386,7 @@ static safety_config subaru_init(uint16_t param) {
((unsigned int)alternative_experience == 32U)));
subaru_aol_reset();
subaru_aol_enabled = ((unsigned int)alternative_experience == 32U) &&
((param == 0U) || (param == 1U) || (param == 17U) || (param == 145U) ||
((param == 16U) || (param == 0U) || (param == 1U) || (param == 17U) || (param == 145U) ||
avh || (param == 177U) || (param == 401U) || (param == 8U) || (param == 520U) || (param == 512U) || (param == 2225U));
if (subaru_aol_enabled) {
static const AolSafetyPolicy policy = {
@@ -390,7 +404,17 @@ static safety_config subaru_init(uint16_t param) {
static RxCheck subaru_gen2_angle_rx_checks[] = {
SUBARU_GEN2_ANGLE_RX_CHECKS()
};
ret = avh ? (stop_start ? BUILD_SAFETY_CFG(subaru_gen2_angle_rx_checks, SUBARU_AVH_STOP_START_TX_MSGS) :
static RxCheck subaru_forester_angle_rx_checks[] = {
{.msg = {{MSG_SUBARU_Throttle, SUBARU_MAIN_BUS, 8, 100U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{MSG_SUBARU_Steering_Torque, SUBARU_MAIN_BUS, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{MSG_SUBARU_Steering_2, SUBARU_MAIN_BUS, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{MSG_SUBARU_Wheel_Speeds, SUBARU_MAIN_BUS, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{MSG_SUBARU_Brake_Status, SUBARU_MAIN_BUS, 8, 50U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{MSG_SUBARU_ES_Status, SUBARU_CAM_BUS, 8, 20U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
{.msg = {{MSG_SUBARU_ES_DashStatus, SUBARU_CAM_BUS, 8, 10U, .max_counter = 15U, .ignore_quality_flag = true}, { 0 }, { 0 }}},
};
ret = subaru_forester_angle ? BUILD_SAFETY_CFG(subaru_forester_angle_rx_checks, SUBARU_FORESTER_ANGLE_TX_MSGS) :
avh ? (stop_start ? BUILD_SAFETY_CFG(subaru_gen2_angle_rx_checks, SUBARU_AVH_STOP_START_TX_MSGS) :
BUILD_SAFETY_CFG(subaru_gen2_angle_rx_checks, SUBARU_AVH_TX_MSGS)) :
stop_start ? BUILD_SAFETY_CFG(subaru_gen2_angle_rx_checks, SUBARU_STOP_START_TX_MSGS) :
ascent_alert ? BUILD_SAFETY_CFG(subaru_gen2_angle_rx_checks, SUBARU_ASCENT_ALERT_TX_MSGS) :
@@ -0,0 +1,294 @@
import unittest
from opendbc.can import CANPacker
from opendbc.car import Bus, structs
from opendbc.car.fw_versions import match_fw_to_car
from opendbc.car.subaru.carcontroller import CarController
from opendbc.car.subaru.carstate import CarState
from opendbc.car.subaru.interface import CarInterface
from opendbc.car.subaru.values import CAR, DBC, SubaruSafetyFlags
from opendbc.safety.tests.libsafety import libsafety_py
CARS = (CAR.SUBARU_FORESTER_2022,)
class TestSubaruForesterAngle(unittest.TestCase):
def setUp(self):
self.safety = libsafety_py.libsafety
@staticmethod
def packet(frame):
return libsafety_py.make_CANPacket(frame[0], frame[2], frame[1])
@staticmethod
def params(car, *, alpha=False, release=False):
return CarInterface.get_params(car, {0: {}, 1: {}, 2: {}}, [], alpha, release, False)
def mode(self, cp, *, extra_flags=0):
cfg = cp.safetyConfigs[0]
self.assertEqual(self.safety.set_safety_hooks(cfg.safetyModel.raw, cfg.safetyParam | extra_flags), 0)
self.safety.init_tests()
self.safety.set_timer(1_000_000)
@staticmethod
def sources(car, *, brake=False, cruise=True, speed=30, main=True, torque=0):
packer = CANPacker(DBC[car][Bus.pt])
inputs: tuple[tuple[str, int, dict[str, float]], ...] = (
('Throttle', 0, {}),
('Steering_Torque', 0, {'Steer_Torque_Sensor': torque}),
('Steering_2', 0, {'Steering_Angle': 0}),
('Wheel_Speeds', 0, {'FL': speed, 'FR': speed, 'RL': speed, 'RR': speed}),
('Brake_Status', 0, {'Brake': int(brake)}),
('ES_Status', 2, {'Cruise_Activated': int(cruise)}),
('ES_DashStatus', 2, {'Cruise_On': int(main)}),
('Transmission', 0, {'Gear': 121}),
('Dashlights', 0, {}),
('BodyInfo', 0, {}),
('ES_Distance', 2, {}),
('ES_LKAS_State', 2, {}),
('ES_Brake', 2, {}),
)
return packer, [packer.make_can_msg(name, bus, values) for name, bus, values in inputs]
def controller(self, car, cp, frames, *, active=True):
state = CarState(cp)
parsers = state.get_can_parsers(cp)
state.update(parsers)
for parser in parsers.values():
parser.update([(1_000_000_000, frames)])
state.out = state.update(parsers).as_reader()
self.assertEqual(state.out.gearShifter, structs.CarState.GearShifter.drive)
self.assertGreater(state.out.vEgoRaw, 0)
command = structs.CarControl()
command.enabled = True
command.latActive = active
command.actuators.steeringAngleDeg = 1.0
_, sends = CarController(DBC[car], cp).update(command.as_reader(), state, 1_100_000_000)
steer = next(frame for frame in sends if frame[0] == 0x124)
self.assertEqual(steer[2], 0)
self.assertEqual(bool(steer[1][1] & 0x10), active and state.out.cruiseState.available and
state.out.cruiseState.enabled and not state.out.brakePressed)
return state, steer, sends
def arm(self, cp, frames):
self.mode(cp)
required = {0x40, 0x119, 0x11A, 0x13A, 0x13C, 0x222, 0x321}
for frame in frames:
if frame[0] in required:
self.assertTrue(self.safety.safety_rx_hook(self.packet(frame)), hex(frame[0]))
self.safety.safety_tick()
self.assertTrue(self.safety.safety_config_valid())
self.assertTrue(self.safety.get_controls_allowed())
def test_stock_packed_host_to_native_and_inactive(self):
for car in CARS:
for release in (False, True):
with self.subTest(car=car, release=release):
cp = self.params(car, alpha=True, release=release)
self.assertFalse(cp.dashcamOnly)
self.assertFalse(cp.openpilotLongitudinalControl)
self.assertFalse(cp.alphaLongitudinalAvailable)
self.assertEqual(cp.steerControlType, structs.CarParams.SteerControlType.angle)
packer, frames = self.sources(car)
state, steer, sends = self.controller(car, cp, frames)
self.assertTrue(state.out.cruiseState.enabled)
self.assertNotEqual(steer[1][5:7], b'\x00\x00')
self.arm(cp, frames)
self.assertTrue(self.safety.safety_tx_hook(self.packet(steer)))
self.assertEqual({frame[0] for frame in sends}, {0x124, 0x321, 0x322})
self.assertFalse(self.safety.safety_tx_hook(self.packet((0x122, bytes(8), 0))))
self.assertFalse(self.safety.safety_tx_hook(self.packet((0x124, steer[1], 1))))
self.assertFalse(self.safety.safety_tx_hook(self.packet((0x124, steer[1][:-1], 0))))
self.assertFalse(self.safety.safety_tx_hook(self.packet(packer.make_can_msg('ES_Distance', 2, {}))))
cancel = structs.CarControl()
cancel.cruiseControl.cancel = True
_, cancel_sends = CarController(DBC[car], cp).update(cancel.as_reader(), state, 1_100_000_000)
distance = next(frame for frame in cancel_sends if frame[0] == 0x221)
self.assertEqual(distance[2], 0)
self.assertTrue(self.safety.safety_tx_hook(self.packet(distance)))
_, inactive, _ = self.controller(car, cp, frames, active=False)
self.assertTrue(self.safety.safety_tx_hook(self.packet(inactive)))
brake_frames = [packer.make_can_msg('Brake_Status', 0, {'Brake': 1}) if f[0] == 0x13C else f for f in frames]
brake_state, brake_steer, _ = self.controller(car, cp, brake_frames)
self.assertTrue(brake_state.out.brakePressed)
self.assertFalse(brake_steer[1][1] & 0x10)
self.assertTrue(self.safety.safety_rx_hook(self.packet(brake_frames[4])))
self.assertFalse(self.safety.get_controls_allowed())
self.assertTrue(self.safety.safety_tx_hook(self.packet(brake_steer)))
self.assertFalse(self.safety.safety_tx_hook(self.packet(steer)))
def test_required_health_and_raw_long_conflict(self):
for car in CARS:
cp = self.params(car)
_, frames = self.sources(car)
required = [f for f in frames if f[0] in {0x40, 0x119, 0x11A, 0x13A, 0x13C, 0x222, 0x321}]
for missing in required:
with self.subTest(car=car, missing=hex(missing[0])):
self.mode(cp)
for frame in required:
if frame is not missing:
self.safety.safety_rx_hook(self.packet(frame))
self.safety.safety_tick()
self.assertFalse(self.safety.safety_config_valid())
self.arm(cp, frames)
_, steer, _ = self.controller(car, cp, frames)
invalid_option = 0x200 if car == CAR.SUBARU_LEGACY_2025 else 0x100
for flags in (SubaruSafetyFlags.LONG, 0x20, invalid_option):
with self.subTest(car=car, flags=flags):
self.mode(cp, extra_flags=flags)
for frame in required:
self.safety.safety_rx_hook(self.packet(frame))
self.safety.safety_tick()
self.assertFalse(self.safety.safety_tx_hook(self.packet(steer)))
for raw_param in (0x50, 0x30, 0x31, 0x111):
with self.subTest(car=car, raw_param=hex(raw_param)):
self.assertEqual(self.safety.set_safety_hooks(structs.CarParams.SafetyModel.subaru, raw_param), 0)
self.safety.init_tests()
for frame in required:
self.safety.safety_rx_hook(self.packet(frame))
self.safety.safety_tick()
self.assertFalse(self.safety.safety_tx_hook(self.packet(steer)))
self.mode(cp)
for frame in required:
if frame[0] == 0x222:
frame = (frame[0], frame[1][:-1] + bytes([frame[1][-1] ^ 1]), frame[2])
self.safety.safety_rx_hook(self.packet(frame))
self.safety.safety_tick()
self.assertFalse(self.safety.safety_config_valid())
for source in required:
for bad in ((source[0], source[1], (source[2] + 1) % 3),
(source[0], source[1][:-1], source[2])):
with self.subTest(car=car, source=hex(source[0]), bad=bad[2:]):
self.mode(cp)
for frame in required:
self.safety.safety_rx_hook(self.packet(bad if frame is source else frame))
self.safety.safety_tick()
self.assertFalse(self.safety.safety_config_valid())
self.arm(cp, frames)
self.safety.set_timer(10_000_000)
self.safety.safety_tick()
self.assertFalse(self.safety.safety_config_valid())
self.mode(cp)
for frame in required:
self.safety.safety_rx_hook(self.packet(frame))
for _ in range(6):
self.safety.safety_rx_hook(self.packet(next(f for f in required if f[0] == 0x222)))
self.safety.safety_tick()
self.assertFalse(self.safety.safety_config_valid())
def test_stock_main_driver_and_cruise_handoff(self):
for car in CARS:
cp = self.params(car)
for override in ({'main': False}, {'cruise': False}, {'brake': True}, {'torque': 250}):
with self.subTest(car=car, override=override):
_, frames = self.sources(car, **override)
self.mode(cp)
for frame in frames:
self.safety.safety_rx_hook(self.packet(frame))
state, steer, _ = self.controller(car, cp, frames)
if override.get('torque'):
# The host deliberately waits for a second steering period before
# cutting a request on persistent driver input.
command = structs.CarControl()
command.enabled = command.latActive = True
command.actuators.steeringAngleDeg = 1.0
controller = CarController(DBC[car], cp)
controller.update(command.as_reader(), state, 1_100_000_000)
controller.update(command.as_reader(), state, 1_110_000_000)
_, sends = controller.update(command.as_reader(), state, 1_120_000_000)
steer = next(frame for frame in sends if frame[0] == 0x124)
self.assertFalse(steer[1][1] & 0x10)
self.assertTrue(self.safety.safety_tx_hook(self.packet(steer)))
_, frames = self.sources(car)
self.arm(cp, frames)
_, steer, _ = self.controller(car, cp, frames)
self.assertTrue(self.safety.safety_tx_hook(self.packet(steer)))
_, main_off = self.sources(car, main=False)
dash = next(frame for frame in main_off if frame[0] == 0x321)
self.assertTrue(self.safety.safety_rx_hook(self.packet(dash)))
self.assertFalse(self.safety.safety_tx_hook(self.packet(steer)))
def test_forwarding_and_relay(self):
for car in CARS:
cp = self.params(car)
self.mode(cp)
self.assertEqual(self.safety.safety_fwd_hook(0, 0x124), 2)
self.assertEqual(self.safety.safety_fwd_hook(2, 0x124), -1)
self.assertEqual(self.safety.safety_fwd_hook(2, 0x321), -1)
self.assertEqual(self.safety.safety_fwd_hook(2, 0x322), -1)
self.assertEqual(self.safety.safety_fwd_hook(2, 0x221), 0)
self.assertEqual(self.safety.safety_fwd_hook(1, 0x124), -1)
_, frames = self.sources(car)
_, steer, _ = self.controller(car, cp, frames)
self.arm(cp, frames)
self.assertTrue(self.safety.safety_tx_hook(self.packet(steer)))
self.safety.set_relay_malfunction(True)
self.assertFalse(self.safety.safety_tx_hook(self.packet(steer)))
self.assertEqual(self.safety.safety_fwd_hook(0, 0x124), -1)
self.assertEqual(self.safety.safety_fwd_hook(2, 0x221), -1)
def test_host_native_angle_rate_matrix(self):
for car in CARS:
for speed in (5, 30, 80):
for target in (-650., 650.):
with self.subTest(car=car, speed=speed, target=target):
cp = self.params(car)
_, frames = self.sources(car, speed=speed)
state = CarState(cp)
parsers = state.get_can_parsers(cp)
state.update(parsers)
for parser in parsers.values():
parser.update([(1_000_000_000, frames)])
state.out = state.update(parsers).as_reader()
self.arm(cp, frames)
controller = CarController(DBC[car], cp)
command = structs.CarControl()
command.enabled = command.latActive = True
command.actuators.steeringAngleDeg = target
seen = []
for i in range(120):
self.safety.set_timer(1_000_000 + i * 10_000)
_, sends = controller.update(command.as_reader(), state, 1_000_000_000 + i * 10_000_000)
for frame in sends:
if frame[0] == 0x124:
self.assertTrue(self.safety.safety_tx_hook(self.packet(frame)),
f'{car.value} {speed=} {target=} {i=} {frame[1].hex()}')
seen.append(frame)
self.assertGreater(len(seen), 50)
self.assertNotEqual(seen[-1][1][5:7], b'\x00\x00')
def test_own_vehicle_model_and_exact_configuration(self):
from opendbc.car.vehicle_model import VehicleModel, calc_slip_factor
cp = self.params(CARS[0])
self.assertEqual(cp.safetyConfigs[0].safetyParam, 16)
self.assertAlmostEqual(cp.mass, 1704.)
self.assertAlmostEqual(calc_slip_factor(VehicleModel(cp)), -0.0006281956135115129, places=13)
self.assertFalse(cp.openpilotLongitudinalControl or cp.alphaLongitudinalAvailable)
def test_firmware_complete_partial_and_mixed(self):
from opendbc.car.subaru.fingerprints import FW_VERSIONS
car = CARS[0]
rows = [(ecu, addr, sub, values[0]) for (ecu, addr, sub), values in FW_VERSIONS[car].items()]
def records(entries):
return [structs.CarParams.CarFw(ecu=ecu, address=addr, subAddress=0 if sub is None else sub, fwVersion=value, brand='subaru')
for ecu, addr, sub, value in entries]
exact, matches = match_fw_to_car(records(rows), '00000000000000000', log=False)
self.assertTrue(exact)
self.assertEqual(matches, {car})
for partial in (rows[:1], rows[1:2]):
_, matches = match_fw_to_car(records(partial), '00000000000000000', log=False)
self.assertNotIn(car, matches)
foreign = next((ecu, addr, sub, value) for sibling, firmware in FW_VERSIONS.items() if sibling != car
for (ecu, addr, sub), values in firmware.items() for value in values
if (ecu, addr, sub) in FW_VERSIONS[car] and value not in FW_VERSIONS[car][(ecu, addr, sub)])
_, matches = match_fw_to_car(records([*rows, foreign]), '00000000000000000', log=False)
self.assertNotIn(car, matches)
@@ -34,20 +34,22 @@ class TestSubaruGen2AnglePair(unittest.TestCase):
@staticmethod
def sources(car, *, brake=False, cruise=True, speed=30, main=True, torque=0):
packer = CANPacker(DBC[car][Bus.pt])
inputs = (
wheel_bus = 0 if car == CAR.SUBARU_FORESTER_2022 else 1
status_bus = 2 if car == CAR.SUBARU_FORESTER_2022 else 1
inputs: tuple[tuple[str, int, dict[str, float]], ...] = (
('Throttle', 0, {}),
('Steering_Torque', 0, {'Steer_Torque_Sensor': torque}),
('Steering_2', 0, {'Steering_Angle': 0}),
('Wheel_Speeds', 1, {'FL': speed, 'FR': speed, 'RL': speed, 'RR': speed}),
('Brake_Status', 1, {'Brake': int(brake)}),
('ES_Status', 1, {'Cruise_Activated': int(cruise)}),
('Wheel_Speeds', wheel_bus, {'FL': speed, 'FR': speed, 'RL': speed, 'RR': speed}),
('Brake_Status', wheel_bus, {'Brake': int(brake)}),
('ES_Status', status_bus, {'Cruise_Activated': int(cruise)}),
('ES_DashStatus', 2, {'Cruise_On': int(main)}),
('Transmission', 0, {'Gear': 121}),
('Dashlights', 0, {}),
('BodyInfo', 0, {}),
('ES_Distance', 1, {}),
('ES_Distance', status_bus, {}),
('ES_LKAS_State', 2, {}),
('ES_Brake', 1, {}),
('ES_Brake', status_bus, {}),
)
return packer, [packer.make_can_msg(name, bus, values) for name, bus, values in inputs]
@@ -65,7 +65,7 @@ class TestSubaruStockAol(unittest.TestCase):
self.assertTrue(self.safety.get_controls_allowed())
def test_all_stock_profiles_full_engagement_brake_reverse_drive_main_off(self):
for param in (0, 1, 17, 145, 177, 2225):
for param in (0, 1, 16, 17, 145, 177, 2225):
with self.subTest(param=param):
self.configure(param)
self.arm()
@@ -177,7 +177,7 @@ class TestSubaruStockAol(unittest.TestCase):
self.assertEqual(self.feed(cruise=True), 1)
def test_unrecognized_words_cannot_acquire_independent_permission(self):
for param in (2, 3, 16, 209, 65535):
for param in (2, 3, 48, 80, 209, 65535):
self.configure(param)
for _ in range(12):
self.feed(cruise=False)
@@ -94,7 +94,7 @@ int main() {
subaru.safety_mode = 11U;
subaru.safety_param = static_cast<uint16_t>(word);
subaru.capability_flags = 1U;
const bool expected = (word == 0U) || (word == 1U) || (word == 17U) ||
const bool expected = (word == 16U) || (word == 0U) || (word == 1U) || (word == 17U) ||
(word == 145U) || (word == 1169U) || (word == 1425U) || (word == 401U) || (word == 177U) || (word == 8U) || (word == 520U) || (word == 512U) || (word == 2225U);
assert(aol_capable(subaru, 11U, VEHICLE_REGISTRY) == expected);
subaru.capability_flags = 0U;
+1 -1
View File
@@ -29,7 +29,7 @@ def policy_for(cp):
def native_profile_supported(model, param):
return model == int(structs.CarParams.SafetyModel.subaru) and param in (0, 1, 8, 17, 145, 177, 401, 512, 520, 1169, 1425, 2225)
return model == int(structs.CarParams.SafetyModel.subaru) and param in (0, 1, 8, 16, 17, 145, 177, 401, 512, 520, 1169, 1425, 2225)
def native_accepts_cp(cp, model, param):
+1 -1
View File
@@ -3,7 +3,7 @@
#include "selfdrive/pandad/aol_protocol.h"
inline bool subaru_aol_param(uint16_t param) {
return (param == 0U) || (param == 1U) || (param == 17U) || (param == 145U) ||
return (param == 16U) || (param == 0U) || (param == 1U) || (param == 17U) || (param == 145U) ||
(param == 1169U) || (param == 1425U) || (param == 401U) || (param == 177U) || (param == 8U) || (param == 520U) || (param == 512U) || (param == 2225U);
}
@@ -0,0 +1,12 @@
"""Actual automatic/manual factory, Card, SelfdriveD, Controls and native Forester angle joins."""
import pytest
from opendbc.car.subaru.values import CAR
from openpilot.starpilot.car.subaru.tests.native_fixture import subaru_native_pair as subaru_native_pair
@pytest.mark.parametrize('aol', [False, True])
@pytest.mark.parametrize('manual', [False, True])
def test_actual_forester_full_lifecycle(aol, manual, subaru_native_pair, tmp_path):
from openpilot.starpilot.car.subaru.tests.test_stock_aol_callers import test_actual_full_engage_brake_reverse_temporary_recovery_mainoff
test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(CAR.SUBARU_FORESTER_2022, subaru_native_pair, tmp_path,
aol=aol, manual=manual)
@@ -22,20 +22,19 @@ from openpilot.starpilot.car.subaru.tests.native_fixture import subaru_native_pa
@pytest.mark.parametrize('candidate', [CAR.SUBARU_ASCENT_2023, CAR.SUBARU_LEGACY_2025, CAR.SUBARU_CROSSTREK_2025, CAR.SUBARU_FORESTER, CAR.SUBARU_OUTBACK])
def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate, subaru_native_pair, tmp_path):
aol = True
def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate, subaru_native_pair, tmp_path, aol=True, manual=False):
fw = firmware_records(candidate, FW_VERSIONS, 'subaru')
rows = []
with actual_factory(candidate, gen_empty_fingerprint(), fw, '0' * 17, manual=False, aol=aol) as (card, params, matches):
with actual_factory(candidate, gen_empty_fingerprint(), fw, '0' * 17, manual=manual, aol=aol) as (card, params, matches):
cp = card.CP
ci = card.CI
safety = libsafety_py.libsafety
assert cp.pcmCruise and not cp.openpilotLongitudinalControl and not cp.alphaLongitudinalAvailable
assert cp.alternativeExperience == 32 and not cp.dashcamOnly
assert cp.alternativeExperience == (32 if aol else 0) and not cp.dashcamOnly
sd, controls = SelfdriveD(CP=cp), Controls()
stream = CompleteCan(ci)
safety.init_tests()
safety.set_alternative_experience(32)
safety.set_alternative_experience(cp.alternativeExperience)
assert safety.set_safety_hooks(cp.safetyConfigs[0].safetyModel.raw, cp.safetyConfigs[0].safetyParam) == 0
safety.set_aol_test_heartbeat(False)
library, bridge = subaru_native_pair
@@ -47,7 +46,7 @@ def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate,
receipt = None
steering_name = 'ES_LKAS_ANGLE' if cp.steerControlType == structs.CarParams.SteerControlType.angle else 'ES_LKAS'
steering_address = ci.CC.packer.dbc.name_to_msg[steering_name].address
steering_bus = 0 if candidate in (CAR.SUBARU_ASCENT_2023, CAR.SUBARU_FORESTER, CAR.SUBARU_OUTBACK) else 1
steering_bus = 0 if candidate in (CAR.SUBARU_ASCENT_2023, CAR.SUBARU_FORESTER, CAR.SUBARU_OUTBACK, CAR.SUBARU_FORESTER_2022) else 1
decoder = CANParser(DBC[candidate][Bus.pt], [(steering_name, 0)], steering_bus)
dash_decoder = CANParser(DBC[candidate][Bus.pt], [('ES_LKAS_State', 0)], 0)
dash_emissions = 0
@@ -79,7 +78,7 @@ def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate,
panda = messaging.new_message('pandaStates', 1, valid=True, logMonoTime=now)
panda.pandaStates[0].safetyModel = cp.safetyConfigs[0].safetyModel
panda.pandaStates[0].safetyParam = cp.safetyConfigs[0].safetyParam
panda.pandaStates[0].alternativeExperience = 32
panda.pandaStates[0].alternativeExperience = cp.alternativeExperience
panda.pandaStates[0].ignitionLine = True
panda.pandaStates[0].controlsAllowed = bool(safety.get_controls_allowed())
panda.pandaStates[0].safetyRxChecksInvalid = not bool(safety.safety_config_valid())
@@ -91,7 +90,7 @@ def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate,
peers.append(panda.as_reader())
if 'carControl' in latest_controls:
peers.append(latest_controls['carControl'])
peers.extend(driven_new[s] for s in ('onroadEvents', 'selfdriveState', 'aolAxisState') if s in driven_new)
peers.extend(driven_new[s] for s in ('onroadEvents', 'selfdriveState', 'aolAxisState') if s in driven_new and s in card.sm.data)
if receipt is not None and tick % 10 == 1:
peers.append(receipt.as_reader())
card.sm.update_msgs((now + 1000) / 1e9, peers)
@@ -128,13 +127,13 @@ def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate,
driven_new = selfdrive_step(sd, published, new_native, previous_controls, cp, safety, now, calibration='calibrated')
driven.update(driven_new)
order = None
if tick % 10 == 0:
if aol and tick % 10 == 0:
receipt, order = pandad.step(driven, published, now)
feed_perception_only(controls, now, tick, enabled=sd.enabled, active=sd.active)
plan = messaging.new_message('longitudinalPlan', valid=True, logMonoTime=now)
plan.longitudinalPlan.aTarget = 0.5
control_peers = [published['carState'], published['carOutput'], plan.as_reader()]
control_peers.extend(driven_new[s] for s in ('selfdriveState', 'onroadEvents', 'aolAxisState') if s in driven_new)
control_peers.extend(driven_new[s] for s in ('selfdriveState', 'onroadEvents', 'aolAxisState') if s in driven_new and s in controls.sm.data)
if receipt is not None and tick % 10 == 0:
control_peers.append(receipt.as_reader())
controls.sm.update_msgs((now + 1000) / 1e9, control_peers)
@@ -176,12 +175,12 @@ def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate,
active_emissions.append(tick)
if 165 <= tick < 180 or 240 <= tick < 255 or not aol and (125 <= tick < 145 or 205 <= tick < 220 or 280 <= tick < 300):
assert not request and output == 0, (tick, decoded_steering)
intent = decode_intent(published['aolIntentWire'].aolIntentWire)
intent = decode_intent(published['aolIntentWire'].aolIntentWire) if aol else None
row = {
'tick': tick,
'intent': intent.allowedLatch,
'armed': intent.lateralArmed,
'cardLatch': card.aol_card_intent.allowed_latch,
'intent': bool(intent and intent.allowedLatch),
'armed': bool(intent and intent.lateralArmed),
'cardLatch': bool(card.aol_card_intent and card.aol_card_intent.allowed_latch),
'lat': command.latActive,
'long': command.longActive,
'enabled': sd.enabled,
@@ -194,25 +193,26 @@ def test_actual_full_engage_brake_reverse_temporary_recovery_mainoff(candidate,
'order': order,
'tx': tx,
'steering': decoded_steering,
'fault': str(driven['aolAxisState'].aolAxisState.faultReason),
'fault': str(driven['aolAxisState'].aolAxisState.faultReason) if aol else 'unsupported',
'events': [str(e.name) for e in driven['onroadEvents'].onroadEvents],
}
rows.append(row)
assert not command.longActive, row
if 10 <= tick < 30:
assert not command.latActive and not intent.allowedLatch and row['permission'] == 0, row
assert not command.latActive and not bool(intent and intent.allowedLatch) and row['permission'] == 0, row
if 65 <= tick < 90:
assert command.latActive and intent.allowedLatch == aol and row['permission'] == 1, row
assert command.latActive and bool(intent and intent.allowedLatch) == aol and row['permission'] == int(aol), row
if 125 <= tick < 145 or 205 <= tick < 220 or 280 <= tick < 300:
assert command.latActive == aol and intent.allowedLatch == aol and row['permission'] == int(aol) and not row['controls'], row
assert command.latActive == aol and bool(intent and intent.allowedLatch) == aol and row['permission'] == int(aol) and not row['controls'], row
if 165 <= tick < 180:
assert not command.latActive and not intent.allowedLatch and intent.lateralArmed == aol and row['cardLatch'] == aol and row['permission'] == 0, row
assert (not command.latActive and not bool(intent and intent.allowedLatch) and
bool(intent and intent.lateralArmed) == aol and row['cardLatch'] == aol and row['permission'] == 0), row
if 240 <= tick < 255:
assert not command.latActive and intent.lateralArmed == aol and row['cardLatch'] == aol and row['permission'] == 0, row
assert not command.latActive and bool(intent and intent.lateralArmed) == aol and row['cardLatch'] == aol and row['permission'] == 0, row
if 330 <= tick < 400:
assert not command.latActive and not intent.allowedLatch, row
assert not command.latActive and not bool(intent and intent.allowedLatch), row
if tick >= 440:
assert command.latActive and intent.allowedLatch == aol and row['permission'] == 1, row
assert command.latActive and bool(intent and intent.allowedLatch) == aol and row['permission'] == int(aol), row
assert len(active_emissions) >= 4, rows[65:90]
assert dash_emissions >= 40
finally:
+4
View File
@@ -99,6 +99,8 @@ AOL_LIFECYCLE_TARGETS = (
"openpilot/starpilot/car/subaru/tests/test_avh_callers.py",
"opendbc_repo/opendbc/safety/tests/test_subaru_avh_startup.py",
"openpilot/starpilot/car/subaru/tests/test_angle_tune.py",
"openpilot/starpilot/car/subaru/tests/test_forester_angle_callers.py",
"opendbc_repo/opendbc/safety/tests/test_subaru_forester_angle.py",
)
AOL_LIFECYCLE_SCOPE = {
@@ -374,6 +376,8 @@ RELEASE_TARGETS = (
"opendbc_repo/opendbc/safety/tests/test_subaru_sng.py",
"opendbc_repo/opendbc/safety/tests/test_subaru_stop_start.py",
"opendbc_repo/opendbc/safety/tests/test_subaru_avh_startup.py",
"opendbc_repo/opendbc/safety/tests/test_subaru_forester_angle.py",
"opendbc_repo/opendbc/safety/tests/test_subaru_receive_guards.py",
)
# Vehicle tests import both the vendored car code and these host control owners.