mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-03 19:13:45 +08:00
Lateral authority
This commit is contained in:
@@ -568,7 +568,7 @@ class CarController(CarControllerBase):
|
||||
if self.CP.openpilotLongitudinalControl:
|
||||
stock_steer_ready = (stock_steer_ready and CS.pedal_sensor_healthy and
|
||||
0 < CS.pedal_sensor_ts_nanos <= now_nanos <= CS.pedal_sensor_ts_nanos + PEDAL_SENSOR_TIMEOUT_NS)
|
||||
lat_active = CC.latActive and stock_steer_ready
|
||||
lat_active = CC.latActive and stock_steer_ready and not CS.steering_authority.latched
|
||||
|
||||
# Steering (Active: 50Hz, inactive: 10Hz)
|
||||
steer_step = self.params.STEER_STEP if lat_active else self.params.INACTIVE_STEER_STEP
|
||||
@@ -605,6 +605,7 @@ class CarController(CarControllerBase):
|
||||
self.apply_torque_last = apply_torque
|
||||
idx = self.lka_steering_cmd_counter % 4
|
||||
can_sends.append(gmcan.create_steering_control(self.packer_pt, CanBus.POWERTRAIN, apply_torque, idx, lat_active))
|
||||
CS.steering_authority.command(now_nanos, lat_active)
|
||||
|
||||
status_bolt = self.CP.carFingerprint in NO_ACC_BOLT_CAR and (
|
||||
is_bolt_present_no_acc_pedal_profile(self.CP) or is_bolt_pedal_removed_profile(self.CP))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from opendbc.car.gm.steering_authority import SteeringAuthority
|
||||
from opendbc.car.gps import CarGpsTracker, get_car_gps_config
|
||||
from opendbc.car.gm.values import gm_control_word, is_volt_one_pedal, camera_acc_pedal_profile, BrakeSource, volt_cc_pedal_profile
|
||||
from opendbc.car.gm.values import is_volt_longitudinal, is_gm_auto_hold
|
||||
@@ -35,6 +36,7 @@ BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelCruise, CruiseButtons.D
|
||||
class CarState(CarStateBase):
|
||||
def __init__(self, CP):
|
||||
super().__init__(CP)
|
||||
self.steering_authority = SteeringAuthority(CP)
|
||||
self.car_gps_tracker = CarGpsTracker(CP)
|
||||
self.car_gps_supported = self.car_gps_tracker.config is not None
|
||||
self.hybrid_profile = malibu_hybrid_profile(CP)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from math import fabs, exp
|
||||
import numpy as np
|
||||
|
||||
from opendbc.car import get_safety_config, structs
|
||||
from opendbc.car import Bus, get_safety_config, structs
|
||||
from opendbc.car.pedal import supported_pedal_detected
|
||||
from opendbc.car.gm.pedal_capability import pedal_candidate
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
@@ -50,6 +50,7 @@ class CarInterface(CarInterfaceBase):
|
||||
return super().apply(c, now_nanos)
|
||||
|
||||
def update(self, can_packets):
|
||||
self.CS.steering_authority.observe(can_packets)
|
||||
if malibu_hybrid_profile(self.CP) is not None:
|
||||
self.CS.hybrid_buttons.observe_packets(can_packets)
|
||||
if (is_bolt_pedal_removed_profile(self.CP) or is_bolt_pedal_removed_profile(self.CP, stock_only=True)):
|
||||
@@ -63,7 +64,9 @@ class CarInterface(CarInterfaceBase):
|
||||
if is_conventional_cc_pedal_profile(self.CP):
|
||||
self.CS.conventional_cancel_credit.observe(can_packets)
|
||||
if not is_bolt_cc_profile(self.CP):
|
||||
return super().update(can_packets)
|
||||
ret = super().update(can_packets)
|
||||
self.CS.steering_authority.update(ret, self.can_parsers[Bus.pt]._last_update_nanos)
|
||||
return ret
|
||||
adaptive = (self.CP.carFingerprint == CAR.CHEVROLET_BOLT_ACC_2022_2023_PEDAL and
|
||||
self.CP.safetyConfigs[0].safetyParam == 0xC140)
|
||||
camera_source = getattr(self, "bolt_cc_camera_observed", (0, b""))
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
from opendbc.car import structs
|
||||
from opendbc.car.gm.values import GMFlags, camera_acc_pedal_profile, is_volt_ascm_longitudinal
|
||||
|
||||
# PSCMStatus is expected at 10 Hz. Three periods bound first acquisition and missing feedback.
|
||||
EPS_STATUS_TIMEOUT_NS = 300_000_000
|
||||
STEERING_COMMAND_TIMEOUT_NS = 100_000_000
|
||||
ButtonType = structs.CarState.ButtonEvent.Type
|
||||
|
||||
|
||||
def monitored_profile(CP: structs.CarParams) -> bool:
|
||||
return (is_volt_ascm_longitudinal(CP) and camera_acc_pedal_profile(CP) is None and
|
||||
not CP.flags & GMFlags.PEDAL_LONG)
|
||||
|
||||
|
||||
class SteeringAuthority:
|
||||
def __init__(self, CP: structs.CarParams):
|
||||
self.enabled = monitored_profile(CP)
|
||||
self.min_speed = CP.minSteerSpeed
|
||||
self.latched = False
|
||||
self.command_active = False
|
||||
self.seen_active = False
|
||||
self.last_active_ns = 0
|
||||
self.active_since_ns = 0
|
||||
self.status_ns = 0
|
||||
self.status = -1
|
||||
self.bad_since_ns = 0
|
||||
self.bad_samples = 0
|
||||
self.last_sample_ns = 0
|
||||
self.disable_ns = 0
|
||||
self.neutral_ns = 0
|
||||
self.main_off = False
|
||||
self.source_ns: dict[int, int] = {}
|
||||
|
||||
def observe(self, can_packets):
|
||||
if not self.enabled:
|
||||
return
|
||||
self.source_ns.clear()
|
||||
malformed = any(bus == 0 and address == 0x184 and (stamp <= 0 or len(data) != 8)
|
||||
for stamp, packets in can_packets for address, data, bus in packets)
|
||||
lengths = {0xC9: 8, 0xBE: 6, 0xBD: 7, 0x1E1: 7}
|
||||
invalid = {address for stamp, packets in can_packets for address, data, bus in packets
|
||||
if bus == 0 and address in lengths and (stamp <= 0 or len(data) != lengths[address])}
|
||||
for address in invalid:
|
||||
self.source_ns.pop(address, None)
|
||||
for stamp, packets in can_packets:
|
||||
for address, data, bus in packets:
|
||||
if bus != 0 or stamp <= 0:
|
||||
continue
|
||||
if address in lengths and address not in invalid:
|
||||
self.source_ns[address] = max(stamp, self.source_ns.get(address, 0))
|
||||
if address == 0x184 and stamp > self.status_ns:
|
||||
if self.active_since_ns and stamp - max(self.status_ns, self.active_since_ns) > EPS_STATUS_TIMEOUT_NS:
|
||||
self.latched = True
|
||||
self.status_ns = stamp
|
||||
self.status = -1 if malformed else (data[0] >> 3) & 7
|
||||
|
||||
def command(self, now_ns: int, active: bool):
|
||||
if not self.enabled:
|
||||
return
|
||||
self.command_active = active
|
||||
if active:
|
||||
self.last_active_ns = now_ns
|
||||
if not self.active_since_ns:
|
||||
self.active_since_ns = now_ns
|
||||
elif self.disable_ns and now_ns > self.disable_ns:
|
||||
self.neutral_ns = now_ns
|
||||
|
||||
def update(self, CS: structs.CarState, now_ns: int):
|
||||
if not self.enabled:
|
||||
return
|
||||
healthy = CS.canValid and not CS.canTimeout
|
||||
|
||||
def fresh(address):
|
||||
stamp = self.source_ns.get(address, 0)
|
||||
return 0 < stamp <= now_ns and now_ns - stamp <= STEERING_COMMAND_TIMEOUT_NS
|
||||
|
||||
main_fresh = fresh(0xC9)
|
||||
buttons_fresh = fresh(0x1E1)
|
||||
pedal_fresh = (CS.brakePressed and (main_fresh or fresh(0xBE)) or
|
||||
CS.regenBraking and fresh(0xBD))
|
||||
cancel = buttons_fresh and any(b.type == ButtonType.cancel and b.pressed for b in CS.buttonEvents)
|
||||
if self.latched and healthy:
|
||||
if pedal_fresh or cancel or main_fresh and not CS.cruiseState.available:
|
||||
if not self.disable_ns:
|
||||
self.disable_ns = now_ns
|
||||
self.main_off |= main_fresh and not CS.cruiseState.available
|
||||
physical_enable = buttons_fresh and self.source_ns[0x1E1] > self.neutral_ns and CS.buttonEnable
|
||||
main_enable = self.main_off and main_fresh and self.source_ns[0xC9] > self.neutral_ns and CS.cruiseState.available
|
||||
if (self.neutral_ns > self.disable_ns > 0 and (physical_enable or main_enable) and
|
||||
CS.cruiseState.available and not CS.brakePressed and not CS.regenBraking):
|
||||
self.latched = False
|
||||
self.disable_ns = self.neutral_ns = 0
|
||||
self.main_off = False
|
||||
self.last_active_ns = self.active_since_ns = 0
|
||||
self.seen_active = False
|
||||
self.bad_since_ns = self.bad_samples = 0
|
||||
|
||||
deliberate_pause = (pedal_fresh or cancel or main_fresh and not CS.cruiseState.available or
|
||||
CS.standstill or CS.vEgo < self.min_speed or CS.steeringPressed or
|
||||
self.status in (2, 3) and 0 < self.status_ns <= now_ns and
|
||||
now_ns - self.status_ns <= EPS_STATUS_TIMEOUT_NS)
|
||||
if healthy and deliberate_pause and not self.latched:
|
||||
self.seen_active = False
|
||||
self.command_active = False
|
||||
self.last_active_ns = self.active_since_ns = 0
|
||||
self.bad_since_ns = self.bad_samples = 0
|
||||
self.last_sample_ns = self.status_ns
|
||||
# A missing EPS feed must retain a fault even after host health gates emit neutral.
|
||||
if (self.active_since_ns and not self.latched and
|
||||
now_ns - max(self.status_ns, self.active_since_ns) > EPS_STATUS_TIMEOUT_NS):
|
||||
self.latched = True
|
||||
|
||||
recent_command = 0 < self.last_active_ns <= now_ns and now_ns - self.last_active_ns <= STEERING_COMMAND_TIMEOUT_NS
|
||||
eligible = ((healthy or self.status == -1) and self.command_active and recent_command and CS.cruiseState.available and
|
||||
not CS.brakePressed and not CS.regenBraking and not cancel and
|
||||
not CS.standstill and CS.vEgo >= self.min_speed and not CS.steeringPressed)
|
||||
fresh_status = 0 < self.status_ns <= now_ns and now_ns - self.status_ns <= EPS_STATUS_TIMEOUT_NS
|
||||
if eligible and fresh_status and self.status_ns > self.active_since_ns and self.status_ns != self.last_sample_ns:
|
||||
self.last_sample_ns = self.status_ns
|
||||
if self.status == 1:
|
||||
self.seen_active = True
|
||||
self.bad_since_ns = self.bad_samples = 0
|
||||
elif self.status not in (2, 3):
|
||||
if not self.bad_since_ns:
|
||||
self.bad_since_ns = self.status_ns
|
||||
self.bad_samples += 1
|
||||
if self.seen_active:
|
||||
self.latched = True
|
||||
else:
|
||||
self.bad_since_ns = self.bad_samples = 0
|
||||
if (eligible and self.bad_samples >= 2 and self.bad_since_ns and
|
||||
now_ns - self.active_since_ns >= EPS_STATUS_TIMEOUT_NS):
|
||||
self.latched = True
|
||||
if not recent_command or CS.steeringPressed or CS.standstill or CS.vEgo < self.min_speed:
|
||||
self.bad_since_ns = self.bad_samples = 0
|
||||
if self.latched:
|
||||
CS.steerFaultTemporary = True
|
||||
@@ -1,9 +1,11 @@
|
||||
import unittest
|
||||
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car import gen_empty_fingerprint
|
||||
from opendbc.car.gm.carstate import CarState
|
||||
from opendbc.car.gm import gmcan
|
||||
from opendbc.car.gm.values import CruiseButtons
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.fingerprints import FINGERPRINTS, FW_VERSIONS
|
||||
from opendbc.car.gm.values import ASCM_INTERCEPT_CAR, CAR, DBC, GMSafetyFlags, CarControllerParams
|
||||
@@ -22,6 +24,183 @@ def params(car, *, sascm=False, accelerator=True, radar=False, alpha=False, rele
|
||||
|
||||
|
||||
class TestAscmIntercept(unittest.TestCase):
|
||||
def authority_frame(self, ci, packer, tick, *, status=1, active=True, main=True,
|
||||
regen=False, brake=False, button=CruiseButtons.UNPRESS, driver=0., speed=8., torque=0.02,
|
||||
malformed=False, physical_delay_ns=0, eps_missing=False):
|
||||
now = 1_000_000_000 + tick * 10_000_000
|
||||
values = {
|
||||
'ECMCruiseControl': {'CruiseActive': 0},
|
||||
'ECMEngineStatus': {'CruiseMainOn': int(main), 'BrakePressed': int(brake)},
|
||||
'AcceleratorPedal2': {'CruiseState': 0},
|
||||
'ECMPRDNL2': {'PRNDL2': 4},
|
||||
'EBCMWheelSpdRear': {'RLWheelSpd': speed * 3.6, 'RRWheelSpd': speed * 3.6, 'RLWheelDir': 1, 'RRWheelDir': 1},
|
||||
'EBCMWheelSpdFront': {'FLWheelSpd': speed * 3.6, 'FRWheelSpd': speed * 3.6},
|
||||
'ESPStatus': {'TractionControlOn': 1},
|
||||
'BCMDoorBeltStatus': {'LeftSeatBelt': 1},
|
||||
'BCMGeneralPlatformStatus': {}, 'BCMTurnSignals': {}, 'PSCMSteeringAngle': {},
|
||||
'ECMAcceleratorPos': {'BrakePedalPos': 30 if brake else 0},
|
||||
'EBCMFrictionBrakeStatus': {}, 'EBCMRegenPaddle': {'RegenPaddle': int(regen)},
|
||||
}
|
||||
packets = [packer.make_can_msg(name, 0, signals) for name, signals in values.items()]
|
||||
packets += [gmcan.create_buttons(packer, 0, tick % 4, button),
|
||||
packer.make_can_msg('ASCMLKASteeringCmd', 2, {}),
|
||||
packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {}),
|
||||
packer.make_can_msg('AEBCmd', 2, {})]
|
||||
if tick % 10 == 0 and not eps_missing:
|
||||
pscm = packer.make_can_msg('PSCMStatus', 0, {'LKATorqueDeliveredStatus': status,
|
||||
'LKADriverAppldTrq': driver, 'LKATorqueDelivered': 0})
|
||||
if malformed:
|
||||
packets.append((0x184, b'\x00', 0))
|
||||
packets.append(pscm)
|
||||
physical = [packet for packet in packets if packet[0] in (0xC9, 0xBE, 0xBD, 0x1E1)]
|
||||
other = [packet for packet in packets if packet[0] not in (0xC9, 0xBE, 0xBD, 0x1E1)]
|
||||
cs = ci.update([(now - physical_delay_ns, physical), (now, other)])
|
||||
if tick >= 10 and not eps_missing:
|
||||
self.assertTrue(cs.canValid, (tick, status))
|
||||
self.assertFalse(cs.canTimeout, tick)
|
||||
cc = structs.CarControl()
|
||||
cc.enabled = active
|
||||
cc.latActive = active
|
||||
cc.actuators.torque = torque
|
||||
cc.hudControl.setSpeed = 30
|
||||
_, sends = ci.apply(cc.as_reader(), now)
|
||||
steering = [data for address, data, bus in sends if address == 0x180 and bus == 0]
|
||||
if steering:
|
||||
self.assertEqual(bool((steering[-1][0] >> 3) & 1), active and not ci.CS.steering_authority.latched, tick)
|
||||
return cs
|
||||
|
||||
def test_volt_steering_loss_requires_driver_rearm(self):
|
||||
for reset in ('regen', 'brake', 'cancel', 'set', 'main'):
|
||||
with self.subTest(reset=reset):
|
||||
cp = params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, radar=True)
|
||||
ci, packer = CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(30):
|
||||
self.authority_frame(ci, packer, tick)
|
||||
self.assertTrue(ci.CS.steering_authority.seen_active)
|
||||
# One emitted neutral frame must not erase the confirmed EPS episode.
|
||||
for tick in range(30, 34):
|
||||
self.authority_frame(ci, packer, tick, active=False)
|
||||
for tick in range(34, 41):
|
||||
cs = self.authority_frame(ci, packer, tick, status=0)
|
||||
self.assertTrue(cs.steerFaultTemporary)
|
||||
self.assertTrue(ci.CS.steering_authority.latched)
|
||||
for tick in range(41, 60):
|
||||
cs = self.authority_frame(ci, packer, tick, active=False)
|
||||
self.assertTrue(cs.steerFaultTemporary) # EPS recovery/automatic CC off cannot rearm.
|
||||
for tick in range(60, 70):
|
||||
self.authority_frame(ci, packer, tick, active=False, main=reset != 'main',
|
||||
regen=reset == 'regen', brake=reset == 'brake',
|
||||
button=CruiseButtons.CANCEL if reset == 'cancel' else CruiseButtons.UNPRESS)
|
||||
cs = self.authority_frame(ci, packer, 70, active=False, physical_delay_ns=200_000_000 if reset == 'main' else 1_000_000,
|
||||
button=CruiseButtons.DECEL_SET if reset == 'set' else
|
||||
CruiseButtons.RES_ACCEL if reset != 'main' else CruiseButtons.UNPRESS)
|
||||
if reset in ('set', 'main'):
|
||||
self.assertTrue(cs.steerFaultTemporary, reset)
|
||||
cs = self.authority_frame(ci, packer, 71, active=False)
|
||||
resume_tick = 72
|
||||
if reset == 'set':
|
||||
# SET alone is not the physical disable required to reset a loss latch.
|
||||
self.assertTrue(cs.steerFaultTemporary, reset)
|
||||
self.assertTrue(ci.CS.steering_authority.latched, reset)
|
||||
self.assertEqual(ci.CS.steering_authority.disable_ns, 0, reset)
|
||||
for tick in range(72, 82):
|
||||
self.authority_frame(ci, packer, tick, active=False, regen=True)
|
||||
self.assertGreater(ci.CS.steering_authority.neutral_ns, ci.CS.steering_authority.disable_ns, reset)
|
||||
cs = self.authority_frame(ci, packer, 82, active=False, button=CruiseButtons.DECEL_SET)
|
||||
self.assertTrue(cs.steerFaultTemporary, reset)
|
||||
cs = self.authority_frame(ci, packer, 83, active=False)
|
||||
resume_tick = 84
|
||||
context = (reset, cs.buttonEnable, cs.brakePressed, cs.regenBraking,
|
||||
ci.CS.steering_authority.disable_ns, ci.CS.steering_authority.neutral_ns)
|
||||
self.assertFalse(cs.steerFaultTemporary, context)
|
||||
self.assertFalse(ci.CS.steering_authority.latched, context)
|
||||
for tick in range(resume_tick, resume_tick + 20):
|
||||
self.authority_frame(ci, packer, tick)
|
||||
self.assertTrue(ci.CS.steering_authority.seen_active, context)
|
||||
|
||||
def test_volt_steering_acquisition_and_invalid_status(self):
|
||||
for status, malformed in ((0, False), (4, False), (1, True)):
|
||||
with self.subTest(status=status, malformed=malformed):
|
||||
cp = params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, radar=True)
|
||||
ci, packer = CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(30):
|
||||
cs = self.authority_frame(ci, packer, tick, status=status, malformed=malformed)
|
||||
self.assertFalse(cs.steerFaultTemporary, tick)
|
||||
for tick in range(30, 41):
|
||||
cs = self.authority_frame(ci, packer, tick, status=status, malformed=malformed)
|
||||
self.assertTrue(cs.steerFaultTemporary)
|
||||
self.assertTrue(ci.CS.steering_authority.latched)
|
||||
|
||||
cp = params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, radar=True)
|
||||
ci, packer = CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(30):
|
||||
self.authority_frame(ci, packer, tick)
|
||||
for tick in range(30, 70):
|
||||
cs = self.authority_frame(ci, packer, tick, active=tick < 40, eps_missing=True)
|
||||
self.assertTrue(cs.steerFaultTemporary)
|
||||
for tick in range(70, 90):
|
||||
cs = self.authority_frame(ci, packer, tick, active=False)
|
||||
self.assertTrue(cs.steerFaultTemporary) # Fresh EPS does not erase a missing-feed latch.
|
||||
|
||||
# A total receive gap cannot conceal a stale EPS episode on the first fresh return.
|
||||
cp = params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, radar=True)
|
||||
ci, packer = CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(30):
|
||||
self.authority_frame(ci, packer, tick)
|
||||
cs = self.authority_frame(ci, packer, 70)
|
||||
self.assertTrue(cs.steerFaultTemporary)
|
||||
self.assertTrue(ci.CS.steering_authority.latched)
|
||||
|
||||
def test_volt_steering_healthy_zero_and_existing_faults(self):
|
||||
for status, active, speed, driver in ((1, True, 8., 0.), (0, False, 8., 0.),
|
||||
(0, True, 0., 0.), (0, True, 8., 3.),
|
||||
(2, True, 8., 0.), (3, True, 8., 0.)):
|
||||
with self.subTest(status=status, active=active, speed=speed, driver=driver):
|
||||
cp = params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, radar=True)
|
||||
ci, packer = CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(60):
|
||||
cs = self.authority_frame(ci, packer, tick, status=status, active=active, speed=speed, driver=driver,
|
||||
torque=0. if status == 1 else 0.02)
|
||||
self.assertFalse(ci.CS.steering_authority.latched)
|
||||
self.assertEqual(cs.steerFaultTemporary, status == 2)
|
||||
self.assertEqual(cs.steerFaultPermanent, status == 3)
|
||||
# A genuine driver pause starts a new acquisition window, unlike an unexplained brief CC drop.
|
||||
for pause in ('regen', 'brake', 'cancel', 'main', 'override', 'standstill'):
|
||||
with self.subTest(pause=pause):
|
||||
cp = params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, radar=True)
|
||||
ci, packer = CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(30):
|
||||
self.authority_frame(ci, packer, tick)
|
||||
for tick in range(30, 50):
|
||||
cs = self.authority_frame(ci, packer, tick, status=0, active=False,
|
||||
regen=pause == 'regen', brake=pause == 'brake', main=pause != 'main',
|
||||
button=CruiseButtons.CANCEL if pause == 'cancel' else CruiseButtons.UNPRESS,
|
||||
driver=3. if pause == 'override' else 0., speed=0. if pause == 'standstill' else 8.)
|
||||
self.assertFalse(cs.steerFaultTemporary)
|
||||
for tick in range(50, 70):
|
||||
cs = self.authority_frame(ci, packer, tick, status=0 if tick < 65 else 1)
|
||||
self.assertFalse(cs.steerFaultTemporary, tick)
|
||||
for tick in range(70, 80):
|
||||
cs = self.authority_frame(ci, packer, tick)
|
||||
self.assertFalse(cs.steerFaultTemporary, tick)
|
||||
self.assertTrue(ci.CS.steering_authority.seen_active)
|
||||
# Ordinary temporary EPS faults retain their normal recovery/acquisition behavior.
|
||||
cp = params(CAR.CHEVROLET_VOLT_ASCM, sascm=True, alpha=True, radar=True)
|
||||
ci, packer = CarInterface(cp), CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
for tick in range(30):
|
||||
self.authority_frame(ci, packer, tick)
|
||||
for tick in range(30, 50):
|
||||
cs = self.authority_frame(ci, packer, tick, status=2, active=False)
|
||||
self.assertTrue(cs.steerFaultTemporary)
|
||||
self.assertFalse(ci.CS.steering_authority.latched)
|
||||
for tick in range(50, 80):
|
||||
cs = self.authority_frame(ci, packer, tick, status=0 if tick < 65 else 1)
|
||||
self.assertFalse(cs.steerFaultTemporary, tick)
|
||||
self.assertTrue(ci.CS.steering_authority.seen_active)
|
||||
for cp in (params(CAR.CHEVROLET_VOLT_ASCM), params(CAR.CHEVROLET_VOLT),
|
||||
params(CAR.CHEVROLET_BOLT_EUV)):
|
||||
self.assertFalse(CarState(cp).steering_authority.enabled)
|
||||
|
||||
def test_eight_manual_ids_have_stock_acc_default(self):
|
||||
self.assertEqual(len(ASCM_INTERCEPT_CAR), 8)
|
||||
for car in ASCM_INTERCEPT_CAR:
|
||||
|
||||
@@ -127,6 +127,8 @@ struct StarPilotCarState @0xf35cc4560bbf6ec2 {
|
||||
decelHardCruise @29 :Bool; # current/releasing decel cruise button came from GM hard-press signal
|
||||
pulseAndGlide @30 :Bool; # developer-only wheel-button pulse-and-glide mode is enabled
|
||||
gps @31 :Gps;
|
||||
lateralAuthorityUnavailable @32 :Bool;
|
||||
sourceCarStateMonoTime @33 :UInt64;
|
||||
|
||||
struct Gps {
|
||||
sourceMonoTime @0 :UInt64; # CAN observation converted to CLOCK_MONOTONIC
|
||||
|
||||
@@ -440,7 +440,6 @@ class Car:
|
||||
|
||||
# Update carState from CAN
|
||||
CS = self.CI.update(can_list)
|
||||
self.car_gps_publisher.update(self.CI.CS, self.pm)
|
||||
self.observe_ioniq6_long_authority(can_list, CS)
|
||||
media_owner = getattr(self, 'ioniq6_media', None)
|
||||
media_observation = media_owner.update(can_list) if media_owner is not None else None
|
||||
@@ -775,6 +774,13 @@ class Car:
|
||||
publish_stock_status(self.pm, self.CP, internal, session=self.slc_producer_session,
|
||||
sequence=self.slc_cruise_event_id, car_state_stamp=int(cs_send.logMonoTime),
|
||||
valid=bool(cs_send.valid and not CS.canTimeout))
|
||||
companion = None
|
||||
steering_authority = getattr(self.CI.CS, 'steering_authority', None)
|
||||
if self.CP.brand == 'gm' and steering_authority is not None and steering_authority.enabled:
|
||||
companion = messaging.new_message('starpilotCarState', valid=bool(cs_send.valid), logMonoTime=int(cs_send.logMonoTime))
|
||||
companion.starpilotCarState.lateralAuthorityUnavailable = bool(steering_authority.latched)
|
||||
companion.starpilotCarState.sourceCarStateMonoTime = int(cs_send.logMonoTime)
|
||||
self.car_gps_publisher.update(self.CI.CS, self.pm, companion)
|
||||
self.pm.send('carState', cs_send)
|
||||
|
||||
wheel = getattr(self, 'wheel_publisher', None)
|
||||
@@ -1010,13 +1016,16 @@ class Car:
|
||||
for ps, cfg in zip(pandas, self.CP.safetyConfigs, strict=True)))
|
||||
|
||||
def publish_sendcan(self, frames, valid=True):
|
||||
steering_authority = getattr(getattr(getattr(self, 'CI', None), 'CS', None), 'steering_authority', None)
|
||||
monitored_steering = (getattr(getattr(self, 'CP', None), 'brand', None) == 'gm' and
|
||||
steering_authority is not None and steering_authority.enabled)
|
||||
packet = can_list_to_can_capnp(frames, msgtype='sendcan', valid=valid)
|
||||
if getattr(self, 'volt_cc_selected', False):
|
||||
# CAN producer/parser and pandad send age use BOOTTIME; Python controls/device envelopes use MONOTONIC.
|
||||
builder = messaging.log_from_bytes(packet).as_builder()
|
||||
builder.logMonoTime = int(getattr(self, 'volt_cc_now_boot_ns', 0))
|
||||
packet = builder.to_bytes()
|
||||
elif self.volt_startup_keepalive():
|
||||
elif self.volt_startup_keepalive() or monitored_steering:
|
||||
builder = messaging.log_from_bytes(packet).as_builder()
|
||||
builder.logMonoTime = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
|
||||
packet = builder.to_bytes()
|
||||
@@ -1195,10 +1204,13 @@ class Car:
|
||||
if self.ioniq6_long_lost:
|
||||
self.publish_sendcan([], valid=False)
|
||||
return
|
||||
steering_authority = getattr(self.CI.CS, 'steering_authority', None)
|
||||
monitored_steering = self.CP.brand == 'gm' and steering_authority is not None and steering_authority.enabled
|
||||
# send car controls over can
|
||||
now_nanos = (self.volt_cc_now_boot_ns if getattr(self, 'volt_cc_selected', False) else
|
||||
self.can_log_mono_time if REPLAY else
|
||||
time.clock_gettime_ns(time.CLOCK_BOOTTIME) if self.volt_startup_keepalive() else int(time.monotonic() * 1e9))
|
||||
time.clock_gettime_ns(time.CLOCK_BOOTTIME) if self.volt_startup_keepalive() or monitored_steering else
|
||||
int(time.monotonic() * 1e9))
|
||||
self.last_actuators_output, can_sends = self.CI.apply(CC, now_nanos)
|
||||
self.publish_sendcan(can_sends, valid=CS.canValid)
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from openpilot.selfdrive.selfdrived.helpers import ExcessiveActuationCheck
|
||||
from openpilot.system.manager.process_health import driving_process_failures
|
||||
from openpilot.selfdrive.selfdrived.state import StateMachine
|
||||
from opendbc.car.gm.aol import native_bootstrap_supported
|
||||
from opendbc.car.gm.steering_authority import monitored_profile
|
||||
from opendbc.car.gm.values import GMFlags, is_volt_ascm_longitudinal
|
||||
from openpilot.starpilot.aol.intent import read_settings
|
||||
from openpilot.starpilot.aol.runtime import (INTENT_MAX_AGE_NS, AxisDecision, current_intent, current_native, decide_axes,
|
||||
@@ -95,6 +96,9 @@ class SelfdriveD:
|
||||
self.aol_sequence = 0
|
||||
self.aol_axis_decision = AxisDecision()
|
||||
self.aol_dm_lateral_inhibit = False
|
||||
self.aol_authority_lost = False
|
||||
self.steering_authority_unavailable = False
|
||||
self.steering_authority_pair = None
|
||||
self.force_stop_hold_alert = HoldAlertState()
|
||||
self.aol_car_state_log_ns = 0
|
||||
self.aol_last_intent = None
|
||||
@@ -262,10 +266,44 @@ class SelfdriveD:
|
||||
self.big_model_chestnut = False
|
||||
return big_failed
|
||||
|
||||
def _update_steering_authority(self, CS):
|
||||
if not monitored_profile(self.CP):
|
||||
self.steering_authority_unavailable = False
|
||||
return
|
||||
now_ns = self.aol_car_state_log_ns if REPLAY and self.aol_car_state_log_ns else time.monotonic_ns()
|
||||
service = 'starpilotCarState'
|
||||
source_ns = self.aol_car_state_log_ns
|
||||
source_current = bool(getattr(self, 'conditional_car_state_valid', False) and CS.canValid and not CS.canTimeout and
|
||||
0 < source_ns <= now_ns and now_ns - source_ns <= INTENT_MAX_AGE_NS)
|
||||
envelope_current = bool(self.sm.seen.get(service, False) and self.sm.valid.get(service, False) and
|
||||
self.sm.alive.get(service, False))
|
||||
pair = None
|
||||
if source_current and envelope_current:
|
||||
message_ns = int(self.sm.logMonoTime[service])
|
||||
companion = self.sm[service]
|
||||
if (int(companion.sourceCarStateMonoTime) == message_ns and 0 < message_ns <= now_ns and
|
||||
now_ns - message_ns <= INTENT_MAX_AGE_NS):
|
||||
if message_ns == source_ns:
|
||||
pair = (source_ns, bool(companion.lateralAuthorityUnavailable))
|
||||
self.steering_authority_pair = pair
|
||||
elif message_ns > source_ns:
|
||||
previous = getattr(self, 'steering_authority_pair', None)
|
||||
if previous is not None and previous[0] == source_ns:
|
||||
pair = previous
|
||||
previous_axes = self.aol_axis_decision
|
||||
expected_active = self.active or previous_axes.lateral_active or previous_axes.longitudinal_active
|
||||
if pair is not None:
|
||||
self.steering_authority_unavailable = pair[1]
|
||||
elif expected_active:
|
||||
self.steering_authority_unavailable = True
|
||||
if getattr(self, 'steering_authority_unavailable', False):
|
||||
self.events.add(EventName.controlsMismatch)
|
||||
|
||||
def update_events(self, CS):
|
||||
"""Compute onroadEvents from carState"""
|
||||
|
||||
self.events.clear()
|
||||
self._update_steering_authority(CS)
|
||||
self.tesla_stock_alerts = []
|
||||
self.nostalgia_paddle_cancel = False
|
||||
action_owner = getattr(self, 'controller_action_owner', None)
|
||||
@@ -842,21 +880,37 @@ class SelfdriveD:
|
||||
self.conditional_status = result.status
|
||||
self.conditional_result = result
|
||||
|
||||
def _decide_aol_axes(self, CS, intent, native):
|
||||
return decide_axes(
|
||||
standard_lateral=self.active, standard_longitudinal=self.enabled and self.CP.openpilotLongitudinalControl,
|
||||
intent=intent, native=native, car_state=CS, initialized=self.initialized,
|
||||
model_ready=bool(self.sm.all_checks(['modelV2', 'extrinsicsCalibration']) and
|
||||
self.sm['extrinsicsCalibration'].calStatus == log.ExtrinsicsCalibration.Status.calibrated),
|
||||
no_entry=aol_no_entry(self.events.names, CS, paddle_only_cancel=self.nostalgia_paddle_cancel,
|
||||
allow_lateral_cancel=bool(intent is not None and intent.allowedLatch and intent.optionalSetRelease
|
||||
and ev6_aol_qualified(self.CP, marked_only=True)),
|
||||
cruise_main_required=getattr(self, 'aol_cruise_main_required', True),
|
||||
allow_below_engage_speed=bool(is_volt_ascm_longitudinal(self.CP) and
|
||||
not self.CP.flags & GMFlags.PEDAL_LONG)),
|
||||
immediate_disable=self.events.contains(ET.IMMEDIATE_DISABLE),
|
||||
dm_lockout=bool(not self.sm.all_checks(['driverMonitoringState']) or
|
||||
self.sm['driverMonitoringState'].lockout or self.sm['driverMonitoringState'].alwaysOnLockout or
|
||||
self.sm['driverMonitoringState'].alertLevel == AlertLevel.three),
|
||||
pause_brake_mps=self.aol_settings.pause_brake_mps if self.aol_settings is not None else 0.0,
|
||||
lateral_inhibit=self.aol_dm_lateral_inhibit)
|
||||
|
||||
def step(self):
|
||||
CS = self.data_sample()
|
||||
self.update_events(CS)
|
||||
native = None
|
||||
lost_active_aol = False
|
||||
lost_active_aol = bool(self.aol_replay and getattr(self, 'steering_authority_unavailable', False) and
|
||||
(self.aol_axis_decision.lateral_active or self.aol_axis_decision.longitudinal_active))
|
||||
if self.aol_replay or getattr(self, 'ordinary_axis_ack_required', False):
|
||||
now_ns = self.aol_car_state_log_ns if REPLAY and self.aol_car_state_log_ns else time.monotonic_ns()
|
||||
native = current_native(self.sm, self.CP, now_ns=now_ns, axis_session_id=self.aol_session_id)
|
||||
if native is None and self.initialized:
|
||||
lost_active_aol = self.aol_axis_decision.lateral_active or self.aol_axis_decision.longitudinal_active
|
||||
self.events.add(EventName.controlsMismatch)
|
||||
if not self.CP.passive and self.initialized:
|
||||
self.enabled, self.active = self.state_machine.update(self.events)
|
||||
if lost_active_aol and ET.IMMEDIATE_DISABLE not in self.state_machine.current_alert_types:
|
||||
self.state_machine.current_alert_types.append(ET.IMMEDIATE_DISABLE)
|
||||
if self.aol_replay:
|
||||
intent = current_intent(self.sm, car_state_ns=self.aol_car_state_log_ns, now_ns=now_ns,
|
||||
previous=getattr(self, 'aol_last_intent', None))
|
||||
@@ -865,23 +919,32 @@ class SelfdriveD:
|
||||
self.aol_dm_lateral_inhibit = True
|
||||
elif intent is not None and not intent.allowedLatch:
|
||||
self.aol_dm_lateral_inhibit = False
|
||||
self.aol_axis_decision = decide_axes(
|
||||
standard_lateral=self.active, standard_longitudinal=self.enabled and self.CP.openpilotLongitudinalControl,
|
||||
intent=intent, native=native, car_state=CS, initialized=self.initialized,
|
||||
model_ready=bool(self.sm.all_checks(['modelV2', 'extrinsicsCalibration']) and
|
||||
self.sm['extrinsicsCalibration'].calStatus == log.ExtrinsicsCalibration.Status.calibrated),
|
||||
no_entry=aol_no_entry(self.events.names, CS, paddle_only_cancel=self.nostalgia_paddle_cancel,
|
||||
allow_lateral_cancel=bool(intent is not None and intent.allowedLatch and intent.optionalSetRelease
|
||||
and ev6_aol_qualified(self.CP, marked_only=True)),
|
||||
cruise_main_required=getattr(self, 'aol_cruise_main_required', True),
|
||||
allow_below_engage_speed=bool(is_volt_ascm_longitudinal(self.CP) and
|
||||
not self.CP.flags & GMFlags.PEDAL_LONG)),
|
||||
immediate_disable=self.events.contains(ET.IMMEDIATE_DISABLE),
|
||||
dm_lockout=bool(not self.sm.all_checks(['driverMonitoringState']) or
|
||||
self.sm['driverMonitoringState'].lockout or self.sm['driverMonitoringState'].alwaysOnLockout or
|
||||
self.sm['driverMonitoringState'].alertLevel == AlertLevel.three),
|
||||
pause_brake_mps=self.aol_settings.pause_brake_mps if self.aol_settings is not None else 0.0,
|
||||
lateral_inhibit=self.aol_dm_lateral_inhibit)
|
||||
previous = self.aol_axis_decision
|
||||
requested = self._decide_aol_axes(CS, intent, native)
|
||||
source_current = bool(intent is not None and getattr(self, 'conditional_car_state_valid', False) and
|
||||
CS.canValid and not CS.canTimeout)
|
||||
driver_override = bool(CS.brakePressed or CS.regenBraking or CS.gasPressed or
|
||||
self.events.contains(ET.USER_DISABLE))
|
||||
lost_companion = bool((previous.lateral_active or previous.longitudinal_active) and
|
||||
not source_current and not self.events.contains(ET.USER_DISABLE))
|
||||
lost_permission = bool(native is not None and not driver_override and (
|
||||
previous.lateral_active and requested.desired_lateral and native.requestedLateral and
|
||||
not native.lateralAllowed and not CS.steeringPressed and not self.events.contains(ET.OVERRIDE_LATERAL) or
|
||||
previous.longitudinal_active and requested.desired_longitudinal and native.requestedLongitudinal and
|
||||
not native.longitudinalAllowed and not self.events.contains(ET.OVERRIDE_LONGITUDINAL)))
|
||||
if self.initialized and (lost_active_aol or lost_companion or lost_permission):
|
||||
lost_active_aol = True
|
||||
self.aol_authority_lost = True
|
||||
elif source_current and not intent.allowedLatch:
|
||||
self.aol_authority_lost = False
|
||||
if getattr(self, 'aol_authority_lost', False):
|
||||
self.events.add(EventName.controlsMismatch)
|
||||
if not self.CP.passive and self.initialized:
|
||||
self.enabled, self.active = self.state_machine.update(self.events)
|
||||
if lost_active_aol and ET.IMMEDIATE_DISABLE not in self.state_machine.current_alert_types:
|
||||
self.state_machine.current_alert_types.append(ET.IMMEDIATE_DISABLE)
|
||||
if self.aol_replay:
|
||||
self.aol_axis_decision = self._decide_aol_axes(CS, intent, native)
|
||||
elif getattr(self, 'ordinary_axis_ack_required', False):
|
||||
self.aol_axis_decision = decide_ordinary_axis(
|
||||
requested=bool(self.initialized and self.conditional_car_state_valid and
|
||||
|
||||
@@ -1162,6 +1162,7 @@ class TestGmAol(unittest.TestCase):
|
||||
self.assertEqual(controls.CP.to_dict(), cp.to_dict(), msg=ctx)
|
||||
sd = SelfdriveD.__new__(SelfdriveD)
|
||||
sd.CP, sd.initialized, sd.aol_replay = cp, True, True
|
||||
sd.enabled = sd.active = False
|
||||
sd.aol_session_id, sd.aol_axis_decision = 'bolt-cancel', AxisDecision()
|
||||
sd.aol_dm_lateral_inhibit, sd.aol_settings = False, selected.aol_settings
|
||||
sd.nostalgia_paddle_cancel = False
|
||||
@@ -1322,6 +1323,7 @@ class TestGmAol(unittest.TestCase):
|
||||
sd.sm.update_msgs(now / 1e9, [intent.as_reader(), receipt.as_reader(), model.as_reader(),
|
||||
calibration.as_reader(), monitoring.as_reader()])
|
||||
sd.aol_car_state_log_ns = now
|
||||
sd.conditional_car_state_valid = cs.canValid and not cs.canTimeout
|
||||
sd.events = events
|
||||
with patch('openpilot.selfdrive.selfdrived.selfdrived.REPLAY', True), \
|
||||
patch.object(sd, 'data_sample', return_value=cs), patch.object(sd, 'update_events'), \
|
||||
@@ -1471,6 +1473,7 @@ class TestGmAol(unittest.TestCase):
|
||||
self.assertEqual(controls.CP.to_dict(), cp.to_dict())
|
||||
sd = SelfdriveD.__new__(SelfdriveD)
|
||||
sd.CP, sd.initialized, sd.aol_replay = cp, True, True
|
||||
sd.enabled = sd.active = False
|
||||
sd.aol_session_id, sd.aol_axis_decision = 'volt-brake', AxisDecision()
|
||||
sd.aol_dm_lateral_inhibit, sd.aol_settings = False, selected.aol_settings
|
||||
sd.nostalgia_paddle_cancel = False
|
||||
@@ -1484,6 +1487,7 @@ class TestGmAol(unittest.TestCase):
|
||||
self.assertEqual(safety.set_safety_hooks(structs.CarParams.SafetyModel.gm, 0xD114), 0)
|
||||
safety.init_tests()
|
||||
resumed = paused = 0
|
||||
previous_sd_onroad = None
|
||||
ctx = {'identity': str(cp.carFingerprint), 'word': '0xd114', 'phase': 'prime'}
|
||||
try:
|
||||
# Warm all selected parser/native sources, with stock cruise inactive.
|
||||
@@ -1491,6 +1495,8 @@ class TestGmAol(unittest.TestCase):
|
||||
stamp = 800_000_000 + warmup * 10_000_000
|
||||
_, frames = feed_car(SimpleNamespace(update=lambda _: None), packer, stamp,
|
||||
counter=warmup % 4, active=False, speed=8., camera=True)
|
||||
frames = [frame for frame in frames if frame[0] != 0x184]
|
||||
frames.append(packer.make_can_msg('PSCMStatus', 0, {'LKATorqueDeliveredStatus': 1}))
|
||||
frames.append(packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {'ACCCruiseState': 0}))
|
||||
previous = ci.update([(stamp, frames)])
|
||||
for frame in frames:
|
||||
@@ -1499,23 +1505,25 @@ class TestGmAol(unittest.TestCase):
|
||||
self.assertTrue(previous.canValid, msg=ctx)
|
||||
self.assertTrue(safety.safety_config_valid(), msg=ctx)
|
||||
self.assertFalse(safety.get_controls_allowed(), msg=ctx)
|
||||
for tick in range(140):
|
||||
for tick in range(146):
|
||||
now = 1_000_000_000 + tick * 10_000_000
|
||||
brake = any(start <= tick < start + 12 for start in (20, 60, 100))
|
||||
fault = tick == 138
|
||||
ctx.update(phase='scenario', tick=tick, brake=brake, fault=fault)
|
||||
main = tick not in (140, 141)
|
||||
ctx.update(phase='scenario', tick=tick, brake=brake, fault=fault, main=main)
|
||||
_, packets = feed_car(SimpleNamespace(update=lambda _: None), packer, now,
|
||||
counter=tick % 4, active=False, speed=8., camera=True)
|
||||
packets = [frame for frame in packets if frame[0] not in (0xC9, 0x1C4, 0xBE)]
|
||||
packets += [packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {'ACCCruiseState': 0}),
|
||||
packer.make_can_msg('ECMEngineStatus', 0, {'CruiseMainOn': 1}),
|
||||
packets = [frame for frame in packets if frame[0] not in (0xC9, 0x1C4, 0xBE, 0x184)]
|
||||
packets += [packer.make_can_msg('PSCMStatus', 0, {'LKATorqueDeliveredStatus': 1}),
|
||||
packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {'ACCCruiseState': 0}),
|
||||
packer.make_can_msg('ECMEngineStatus', 0, {'CruiseMainOn': int(main)}),
|
||||
packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': 0}),
|
||||
packer.make_can_msg('ECMAcceleratorPos', 0, {'BrakePedalPos': 30 if brake else 0})]
|
||||
cs = ci.update([(now, packets)])
|
||||
self.assertTrue(cs.canValid, msg=ctx)
|
||||
self.assertEqual(cs.brakePressed, brake, msg=ctx)
|
||||
self.assertFalse(cs.cruiseState.enabled, msg=ctx)
|
||||
self.assertTrue(cs.cruiseState.available, msg=ctx)
|
||||
self.assertEqual(cs.cruiseState.available, main, msg=ctx)
|
||||
for frame in packets:
|
||||
self.assertTrue(native('rx', frame, now // 1000), (frame, ctx))
|
||||
safety.safety_tick()
|
||||
@@ -1531,27 +1539,30 @@ class TestGmAol(unittest.TestCase):
|
||||
self.assertEqual(events.contains(ET.IMMEDIATE_DISABLE), fault, msg=ctx)
|
||||
onroad = messaging.new_message('onroadEvents', 0, valid=True, logMonoTime=now)
|
||||
onroad.onroadEvents = events.to_msg()
|
||||
selected.sm.update_msgs(now / 1e9, [onroad.as_reader()])
|
||||
disarm = selected.aol_disarming_fault(cs, now, now)
|
||||
self.assertEqual(disarm, fault, msg=ctx)
|
||||
card_onroad = onroad.as_reader() if fault or previous_sd_onroad is None else previous_sd_onroad
|
||||
selected.sm.update_msgs(now / 1e9, [card_onroad])
|
||||
disarm = selected.aol_disarming_fault(cs, int(card_onroad.logMonoTime), now)
|
||||
self.assertEqual(disarm, tick in (136, 138, 139), msg=ctx)
|
||||
selected.aol_card_intent.update(cs, fault_active=disarm, now_ns=now, standard_enabled=False)
|
||||
self.assertEqual(selected.aol_card_intent.allowed_latch, tick < 138, msg=ctx)
|
||||
self.assertEqual(selected.aol_card_intent._main_cycle_required, tick >= 138, msg=ctx)
|
||||
expected_latch = main and (tick < 136 or tick >= 142)
|
||||
self.assertEqual(selected.aol_card_intent.allowed_latch, expected_latch, msg=ctx)
|
||||
self.assertEqual(selected.aol_card_intent._main_cycle_required, 136 <= tick < 140, msg=ctx)
|
||||
allowed, pause_lat, pause_long = selected.aol_card_intent.output(cs)
|
||||
intent_value = IntentState('card', tick + 1, now, now, now + 200_000_000,
|
||||
allowed, pause_lat, pause_long, selected.aol_qualified,
|
||||
selected.aol_card_intent.allowed_latch)
|
||||
intent = decode_intent(encode_intent(intent_value))
|
||||
self.assertIsNotNone(intent, msg=ctx)
|
||||
self.assertEqual(intent.allowedLatch, tick < 138, msg=ctx)
|
||||
self.assertEqual(intent.allowedLatch, expected_latch, msg=ctx)
|
||||
self.assertFalse(intent.pauseLateral, msg=ctx)
|
||||
self.assertFalse(intent.pauseLongitudinal, msg=ctx)
|
||||
args = dict(standard_lateral=False, standard_longitudinal=False, intent=intent, car_state=cs,
|
||||
initialized=True, model_ready=True,
|
||||
no_entry=aol_no_entry(events.names, cs, paddle_only_cancel=False, allow_below_engage_speed=True),
|
||||
immediate_disable=events.contains(ET.IMMEDIATE_DISABLE), dm_lockout=False, pause_brake_mps=10.)
|
||||
wanted = decide_axes(native=None, **args)
|
||||
expected_lat = not brake and tick < 138
|
||||
no_entry = aol_no_entry(events.names, cs, paddle_only_cancel=False, allow_below_engage_speed=True)
|
||||
immediate_disable = events.contains(ET.IMMEDIATE_DISABLE)
|
||||
wanted = decide_axes(native=None, standard_lateral=False, standard_longitudinal=False,
|
||||
intent=intent, car_state=cs, initialized=True, model_ready=True,
|
||||
no_entry=no_entry, immediate_disable=immediate_disable,
|
||||
dm_lockout=False, pause_brake_mps=10.)
|
||||
expected_lat = not brake and expected_latch
|
||||
self.assertEqual(wanted.desired_lateral, expected_lat, msg=ctx)
|
||||
self.assertFalse(wanted.desired_longitudinal, msg=ctx)
|
||||
safety.aol_set_host_request(int(wanted.desired_lateral))
|
||||
@@ -1573,6 +1584,7 @@ class TestGmAol(unittest.TestCase):
|
||||
sd.sm.update_msgs(now / 1e9, [intent_message.as_reader(), receipt.as_reader(), model.as_reader(),
|
||||
calibration.as_reader(), monitoring.as_reader()])
|
||||
sd.aol_car_state_log_ns = now
|
||||
sd.conditional_car_state_valid = cs.canValid and not cs.canTimeout
|
||||
sd.events = events
|
||||
with patch('openpilot.selfdrive.selfdrived.selfdrived.REPLAY', True), \
|
||||
patch.object(sd, 'data_sample', return_value=cs), patch.object(sd, 'update_events'), \
|
||||
@@ -1580,12 +1592,22 @@ class TestGmAol(unittest.TestCase):
|
||||
patch.object(sd, 'publish_selfdriveState'):
|
||||
sd.step()
|
||||
actual_lat = expected_lat and tick != 135
|
||||
actual_onroad = messaging.new_message('onroadEvents', 0, valid=True, logMonoTime=now)
|
||||
actual_onroad.onroadEvents = sd.events.to_msg()
|
||||
previous_sd_onroad = actual_onroad.as_reader()
|
||||
self.assertFalse(sd.enabled or sd.active, msg=ctx)
|
||||
self.assertEqual(sd.aol_axis_decision.lateral_active, actual_lat, msg=ctx)
|
||||
self.assertFalse(sd.aol_axis_decision.longitudinal_active, msg=ctx)
|
||||
if tick == 135:
|
||||
self.assertTrue(sd.aol_authority_lost, msg=ctx)
|
||||
self.assertIn(EventName.controlsMismatch, sd.events.names, msg=ctx)
|
||||
self.assertFalse(sd.aol_axis_decision.native_acknowledged, msg=ctx)
|
||||
if tick in (136, 137):
|
||||
self.assertFalse(sd.aol_axis_decision.lateral_active, msg=ctx)
|
||||
self.assertFalse(selected.aol_card_intent.allowed_latch, msg=ctx)
|
||||
if tick == 142:
|
||||
self.assertTrue(sd.aol_axis_decision.lateral_active, msg=ctx)
|
||||
self.assertFalse(selected.aol_card_intent._main_cycle_required, msg=ctx)
|
||||
feed(controls, now, tick, active=sd.active, enabled=sd.enabled)
|
||||
axis = messaging.new_message('aolAxisState', valid=True, logMonoTime=now)
|
||||
axis.aolAxisState.qualified = True
|
||||
@@ -1618,7 +1640,10 @@ class TestGmAol(unittest.TestCase):
|
||||
probe.brakePressed = True
|
||||
for speed in (9.99, 10., 10.01):
|
||||
probe.vEgo = speed
|
||||
self.assertEqual(decide_axes(native=receipt_state, **dict(args, car_state=probe)).desired_lateral,
|
||||
self.assertEqual(decide_axes(native=receipt_state, standard_lateral=False, standard_longitudinal=False,
|
||||
intent=intent, car_state=probe, initialized=True, model_ready=True,
|
||||
no_entry=no_entry, immediate_disable=immediate_disable,
|
||||
dm_lockout=False, pause_brake_mps=10.).desired_lateral,
|
||||
speed >= 10., (speed, ctx))
|
||||
previous = cs
|
||||
self.assertEqual(resumed, 3)
|
||||
@@ -1664,6 +1689,7 @@ class TestGmAol(unittest.TestCase):
|
||||
self.assertEqual(controls.CP.to_dict(), cp.to_dict())
|
||||
sd = SelfdriveD.__new__(SelfdriveD)
|
||||
sd.CP, sd.initialized, sd.aol_replay = cp, True, True
|
||||
sd.enabled = sd.active = False
|
||||
sd.aol_session_id, sd.aol_axis_decision = 'ascm-acc-fault', AxisDecision()
|
||||
sd.aol_dm_lateral_inhibit, sd.aol_settings = False, selected.aol_settings
|
||||
sd.nostalgia_paddle_cancel = False
|
||||
@@ -1707,10 +1733,9 @@ class TestGmAol(unittest.TestCase):
|
||||
packets = [packet for packet in packets if packet[0] not in (0x1C4, 0x3D1, 0x184)]
|
||||
packets += [packer.make_can_msg('AcceleratorPedal2', 0, {'CruiseState': AccState.FAULTED if fault else AccState.ACTIVE if ordinary else 0}),
|
||||
packer.make_can_msg('ECMEngineStatus', 0, {'CruiseMainOn': int(main)}),
|
||||
packer.make_can_msg('PSCMStatus', 0, {'LKATorqueDeliveredStatus': 3 if critical == 'permanentEPS' and fault else 2 if tick == 20 else 0}),
|
||||
packer.make_can_msg('PSCMStatus', 0, {'LKATorqueDeliveredStatus': 3 if critical == 'permanentEPS' and fault else 2 if tick == 20 else 1}),
|
||||
packer.make_can_msg('ASCMActiveCruiseControlStatus', 2, {'ACCCruiseState': 0})]
|
||||
self.assertFalse(any(address == 0x201 for address, _, _ in packets))
|
||||
ci.update([(now - 1_000_000, packets)])
|
||||
cs = ci.update([(now, packets)])
|
||||
self.assertTrue(cs.canValid, tick)
|
||||
self.assertEqual(cs.accFaulted, fault)
|
||||
@@ -1758,6 +1783,7 @@ class TestGmAol(unittest.TestCase):
|
||||
sd.sm.update_msgs(now / 1e9, [intent.as_reader(), receipt.as_reader(), model.as_reader(),
|
||||
calibration.as_reader(), monitoring.as_reader()])
|
||||
sd.aol_car_state_log_ns = now
|
||||
sd.conditional_car_state_valid = cs.canValid and not cs.canTimeout
|
||||
sd.events = events
|
||||
with patch('openpilot.selfdrive.selfdrived.selfdrived.REPLAY', True), \
|
||||
patch.object(sd, 'data_sample', return_value=cs), patch.object(sd, 'update_events'), \
|
||||
|
||||
@@ -646,6 +646,245 @@ class CardIntentTests(unittest.TestCase):
|
||||
|
||||
|
||||
class IpcAxisContractTests(unittest.TestCase):
|
||||
def _authority_callers(self, full_op):
|
||||
self.enterContext(mock.patch('openpilot.selfdrive.selfdrived.selfdrived.REPLAY', False))
|
||||
from opendbc.car.gm.tests.test_bolt_volt_configurations import ordinary_params
|
||||
from opendbc.car.gm.values import CAR
|
||||
from openpilot.selfdrive.controls.controlsd import Controls
|
||||
from openpilot.starpilot.aol.vehicle import policy_for
|
||||
from openpilot.starpilot.controllers.mode_actions import SwitchbackCooldown
|
||||
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed
|
||||
|
||||
params = Params()
|
||||
params.put_bool('OpenpilotEnabledToggle', True, block=True)
|
||||
params.put_bool('SafeMode', False, block=True)
|
||||
params.put_bool('AlwaysOnLateral', True, block=True)
|
||||
cp = ordinary_params(CAR.CHEVROLET_VOLT_ASCM, alpha=True, sascm=True, radar=True)
|
||||
policy = policy_for(cp)
|
||||
cp.safetyConfigs[-1].safetyParam |= policy.safety_param_addition
|
||||
cp.alternativeExperience |= policy.alternative_experience_addition
|
||||
params.put('CarParams', cp.to_bytes(), block=True)
|
||||
controls = Controls()
|
||||
sd = SelfdriveD.__new__(SelfdriveD)
|
||||
sd.params = params
|
||||
sd.CP = cp.as_reader()
|
||||
services = ['deviceState', 'pandaStates', 'driverMonitoringState', 'extrinsicsCalibration', 'modelV2',
|
||||
'carState', 'aolIntentWire', 'aolSafetyWire', 'starpilotCarState']
|
||||
self.enterContext(mock.patch.object(sd, 'sm', messaging.SubMaster(services, ignore_avg_freq=services), create=True))
|
||||
sd.sm.update = mock.Mock()
|
||||
sd.events, sd.state_machine, sd.AM = Events(), StateMachine(), AlertManager()
|
||||
sd.state_machine.state = log.SelfdriveState.OpenpilotState.enabled if full_op else log.SelfdriveState.OpenpilotState.disabled
|
||||
sd.enabled = sd.active = full_op
|
||||
sd.mismatch_counter = 0
|
||||
sd.events_prev = []
|
||||
sd.experimental_mode = sd.requested_experimental_mode = False
|
||||
sd.initialized = sd.aol_replay = True
|
||||
sd.aol_authority_lost = sd.aol_dm_lateral_inhibit = False
|
||||
sd.aol_axis_decision = AxisDecision()
|
||||
sd.aol_settings = None
|
||||
sd.aol_car_state_log_ns = 0
|
||||
sd.conditional_car_state_valid = False
|
||||
sd.aol_last_intent = None
|
||||
sd.aol_session_id, sd.aol_sequence = 'authority-session', 0
|
||||
sd.conditional_replay = False
|
||||
sd.nostalgia_paddle_cancel = False
|
||||
sd.switchback_capable = False
|
||||
sd.switchback_cooldown = SwitchbackCooldown()
|
||||
sd.switchback_setting_ns, sd.switchback_cooldown_ns = 0, 300_000_000_000
|
||||
sd.personality, sd.is_metric = 1, False
|
||||
sd.CS_prev = car_state()
|
||||
sd.car_state_sock = object()
|
||||
sd.update_conditional_mode = mock.Mock()
|
||||
self.enterContext(mock.patch.object(sd, 'update_events', lambda cs: (sd.events.clear(), sd._update_steering_authority(cs))))
|
||||
|
||||
class Publisher:
|
||||
def __init__(self):
|
||||
self.messages = {}
|
||||
def send(self, service, message):
|
||||
self.messages[service] = messaging.log_from_bytes(message.to_bytes())
|
||||
self.enterContext(mock.patch.object(sd, 'pm', Publisher(), create=True))
|
||||
self.enterContext(mock.patch.object(controls, 'pm', Publisher()))
|
||||
clock = self.enterContext(mock.patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns'))
|
||||
recv = self.enterContext(mock.patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one'))
|
||||
base = 10_000_000_000
|
||||
original_message = messaging.new_message
|
||||
def clocked_message(*args, **kwargs):
|
||||
message = original_message(*args, **kwargs)
|
||||
if 'logMonoTime' not in kwargs:
|
||||
message.logMonoTime = clock.return_value
|
||||
return message
|
||||
self.enterContext(mock.patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.new_message', clocked_message))
|
||||
|
||||
def step(tick, *, timeout=False, old_intent=False, allowed=True, pause=False, latched=True,
|
||||
brake=False, steering=False, gas=False, user_disable=False, observed_state=None, authority_unavailable=False, companion=True,
|
||||
long_allowed=True, pause_long=False):
|
||||
now = base + tick * 10_000_000
|
||||
clock.return_value = now
|
||||
state = car_state(brake=brake) if observed_state is None else observed_state
|
||||
state.cruiseState.available = True
|
||||
state.steeringPressed, state.gasPressed = steering, gas
|
||||
state_event = messaging.new_message('carState', valid=True, logMonoTime=now)
|
||||
state_event.carState = state
|
||||
recv.return_value = None if timeout else state_event.as_reader()
|
||||
frames = []
|
||||
for service in ('deviceState', 'driverMonitoringState', 'extrinsicsCalibration', 'modelV2'):
|
||||
message = messaging.new_message(service, valid=True, logMonoTime=now)
|
||||
if service == 'deviceState':
|
||||
message.deviceState.started, message.deviceState.startedMonoTime = True, base - 1_000_000_000
|
||||
elif service == 'extrinsicsCalibration':
|
||||
message.extrinsicsCalibration.calStatus = 'calibrated'
|
||||
frames.append(message.as_reader())
|
||||
intent_ns = base if old_intent else now
|
||||
intent = messaging.new_message('aolIntentWire', 0, valid=True, logMonoTime=intent_ns)
|
||||
intent.aolIntentWire = encode_intent(IntentState('authority-card', tick + 1, intent_ns, intent_ns,
|
||||
intent_ns + 200_000_000, latched, pause, pause_long, True, latched))
|
||||
native = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=now)
|
||||
native.aolSafetyWire = encode_safety(SafetyState(1, True, now, now + 200_000_000,
|
||||
int(car.CarParams.SafetyModel.gm), cp.safetyConfigs[-1].safetyParam,
|
||||
allowed, full_op and long_allowed, True, full_op, 'authority-panda', sd.aol_session_id))
|
||||
if companion:
|
||||
authority = messaging.new_message('starpilotCarState', valid=companion != 'invalid',
|
||||
logMonoTime=now - 1 if companion == 'wrong_stamp' else now)
|
||||
authority.starpilotCarState.sourceCarStateMonoTime = now - 1 if companion == 'wrong_source' else now
|
||||
authority.starpilotCarState.lateralAuthorityUnavailable = authority_unavailable
|
||||
frames.append(authority.as_reader())
|
||||
sd.sm.update_msgs(now / 1e9, frames + [intent.as_reader(), native.as_reader()])
|
||||
if user_disable:
|
||||
self.enterContext(mock.patch.object(sd, 'update_events', lambda cs: (
|
||||
sd.events.clear(), sd._update_steering_authority(cs), sd.events.add(log.OnroadEvent.EventName.pedalPressed))))
|
||||
sd.step()
|
||||
control_frames = []
|
||||
with mock.patch.object(controls.sm, 'update_msgs', side_effect=lambda _timestamp, messages: control_frames.extend(messages)):
|
||||
feed(controls, now, tick * 5, active=False, enabled=False)
|
||||
model = messaging.new_message('modelV2', valid=True, logMonoTime=now)
|
||||
model.modelV2 = next(message.modelV2 for message in control_frames if message.which() == 'modelV2')
|
||||
model.modelV2.action.desiredCurvature = 0.
|
||||
control_state = messaging.new_message('carState', valid=sd.conditional_car_state_valid,
|
||||
logMonoTime=sd.aol_car_state_log_ns)
|
||||
control_state.carState = sd.CS_prev
|
||||
replaced = ('carState', 'aolSafetyWire', 'modelV2', 'aolAxisState', 'selfdriveState')
|
||||
controls.sm.update_msgs(now / 1e9, [message for message in control_frames if message.which() not in replaced] +
|
||||
[control_state.as_reader(), native.as_reader(), model.as_reader(),
|
||||
sd.pm.messages['aolAxisState'], sd.pm.messages['selfdriveState']])
|
||||
command, lac_log = controls.state_control()
|
||||
controls.publish(command, lac_log)
|
||||
return sd.pm.messages['aolAxisState'].aolAxisState, sd.pm.messages['selfdriveState'].selfdriveState, command
|
||||
return sd, controls, step
|
||||
|
||||
def _assert_withdrawal_alert(self, controls, message):
|
||||
from openpilot.starpilot.audio.axis_alerts import AxisAlerts
|
||||
from openpilot.starpilot.ui.runtime_snapshot import current_alert
|
||||
now = int(message.logMonoTime)
|
||||
alert = current_alert(controls.sm, now, after_frame=0)
|
||||
self.assertTrue(alert.critical)
|
||||
self.assertEqual(alert.size, 'full')
|
||||
self.assertEqual(alert.text1, 'TAKE CONTROL IMMEDIATELY')
|
||||
self.assertEqual(AxisAlerts().update(controls.sm, int(message.selfdriveState.alertSound.raw), now),
|
||||
log.SelfdriveState.AudibleAlert.warningImmediate)
|
||||
|
||||
def test_actual_callers_companion_loss_disables_and_requires_intent_withdrawal(self):
|
||||
for full_op in (False, True):
|
||||
for loss in ('car_state', 'intent', 'authority_companion', 'wrong_stamp', 'wrong_source', 'invalid'):
|
||||
with self.subTest(full_op=full_op, loss=loss), OpenpilotPrefix():
|
||||
sd, controls, step = self._authority_callers(full_op)
|
||||
axis, _, command = step(0)
|
||||
self.assertTrue(axis.lateralActive and axis.nativeAcknowledged and command.latActive)
|
||||
self.assertAlmostEqual(command.actuators.torque, 0.)
|
||||
retained, _, command = step(2, timeout=True)
|
||||
self.assertTrue(retained.lateralActive and command.latActive)
|
||||
self.assertNotIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
|
||||
self.assertEqual(retained.sourceCarStateMonoTime, 10_000_000_000)
|
||||
axis, state, command = step(7, timeout=loss == 'car_state', old_intent=loss == 'intent', companion=False if loss == 'authority_companion' else loss)
|
||||
self.assertFalse(sd.enabled or sd.active or axis.lateralActive or axis.longitudinalActive or command.latActive or command.longActive)
|
||||
self.assertIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
|
||||
self.assertEqual(state.alertText1, 'TAKE CONTROL IMMEDIATELY')
|
||||
self.assertEqual(state.alertSound, log.SelfdriveState.AudibleAlert.warningImmediate)
|
||||
self.assertFalse(controls.pm.messages['carControl'].carControl.latActive)
|
||||
self._assert_withdrawal_alert(controls, sd.pm.messages['selfdriveState'])
|
||||
self.assertEqual(axis.sourceCarStateMonoTime, 0 if loss == 'car_state' else 10_070_000_000)
|
||||
self.assertFalse(sd.pm.messages['aolAxisState'].valid if loss == 'car_state' else axis.lateralActive)
|
||||
self.assertFalse(step(8)[0].lateralActive) # Fresh input alone cannot restart the latched axis.
|
||||
self.assertFalse(step(9, latched=False)[0].lateralActive)
|
||||
self.assertTrue(step(10)[0].lateralActive) # Intent owner supplies its fresh rearm after withdrawal.
|
||||
|
||||
def test_actual_callers_fresh_native_denial_preserves_deliberate_pauses(self):
|
||||
for full_op in (False, True):
|
||||
for reason in ('denial', 'pause', 'brake', 'steering', 'gas', 'user_disable', 'startup',
|
||||
*(('long_denial', 'long_pause') if full_op else ())):
|
||||
with self.subTest(full_op=full_op, reason=reason), OpenpilotPrefix():
|
||||
sd, controls, step = self._authority_callers(full_op)
|
||||
if reason != 'startup':
|
||||
self.assertTrue(step(0)[0].lateralActive)
|
||||
axis, state, command = step(1, allowed=reason in ('long_denial', 'long_pause'), pause=reason == 'pause',
|
||||
long_allowed=reason not in ('long_denial', 'long_pause'), pause_long=reason == 'long_pause',
|
||||
brake=reason == 'brake', steering=reason == 'steering',
|
||||
gas=reason == 'gas', user_disable=reason == 'user_disable')
|
||||
self.assertEqual(bool(axis.lateralActive), reason == 'long_pause')
|
||||
self.assertEqual(bool(command.latActive), reason == 'long_pause')
|
||||
if reason in ('denial', 'long_denial'):
|
||||
self.assertFalse(sd.enabled or sd.active)
|
||||
self.assertEqual(state.alertText2, 'Controls Mismatch')
|
||||
self.assertEqual(state.alertSound, log.SelfdriveState.AudibleAlert.warningImmediate)
|
||||
self.assertTrue(sd.aol_authority_lost)
|
||||
self._assert_withdrawal_alert(controls, sd.pm.messages['selfdriveState'])
|
||||
else:
|
||||
self.assertNotIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
|
||||
self.assertFalse(sd.aol_authority_lost)
|
||||
|
||||
def test_actual_eps_monitor_fault_reaches_both_axis_callers(self):
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.tests.test_ascm_intercept import TestAscmIntercept
|
||||
from opendbc.car.gm.values import DBC
|
||||
for full_op in (False, True):
|
||||
with self.subTest(full_op=full_op), OpenpilotPrefix():
|
||||
sd, _, step = self._authority_callers(full_op)
|
||||
ci = CarInterface(sd.CP)
|
||||
packer = CANPacker(DBC[sd.CP.carFingerprint][Bus.pt])
|
||||
producer = TestAscmIntercept()
|
||||
for tick in range(20):
|
||||
producer.authority_frame(ci, packer, tick, torque=0.)
|
||||
healthy = producer.authority_frame(ci, packer, 20, torque=0.)
|
||||
self.assertTrue(healthy.canValid and not healthy.canTimeout)
|
||||
self.assertTrue(ci.CS.steering_authority.seen_active)
|
||||
self.assertFalse(healthy.steerFaultTemporary)
|
||||
self.assertTrue(step(0, observed_state=healthy)[0].lateralActive)
|
||||
for tick in range(21, 30):
|
||||
producer.authority_frame(ci, packer, tick, torque=0.)
|
||||
loss = producer.authority_frame(ci, packer, 30, status=0, torque=0.)
|
||||
self.assertTrue(loss.canValid and not loss.canTimeout)
|
||||
self.assertTrue(loss.steerFaultTemporary)
|
||||
axis, state, command = step(1, observed_state=loss, authority_unavailable=ci.CS.steering_authority.latched)
|
||||
self.assertFalse(axis.lateralActive or command.latActive)
|
||||
self.assertEqual(state.alertText2, 'Controls Mismatch')
|
||||
self.assertEqual(state.alertSound, log.SelfdriveState.AudibleAlert.warningImmediate)
|
||||
self.assertFalse(sd.enabled or sd.active)
|
||||
self.assertIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
|
||||
for tick in range(31, 40):
|
||||
producer.authority_frame(ci, packer, tick, status=0, torque=0.)
|
||||
recovered_status = producer.authority_frame(ci, packer, 40, torque=0.)
|
||||
self.assertTrue(recovered_status.canValid and not recovered_status.canTimeout)
|
||||
self.assertTrue(recovered_status.steerFaultTemporary)
|
||||
self.assertFalse(step(2, observed_state=recovered_status, authority_unavailable=ci.CS.steering_authority.latched)[0].lateralActive)
|
||||
# A fresh physical status2 is ordinary temporary EPS limitation, not the monitor's latched loss.
|
||||
sd, _, step = self._authority_callers(full_op)
|
||||
ci = CarInterface(sd.CP)
|
||||
for tick in range(20):
|
||||
producer.authority_frame(ci, packer, tick, torque=0.)
|
||||
healthy = producer.authority_frame(ci, packer, 20, torque=0.)
|
||||
self.assertTrue(healthy.canValid and not healthy.canTimeout)
|
||||
self.assertTrue(step(0, observed_state=healthy)[0].lateralActive)
|
||||
for tick in range(21, 30):
|
||||
producer.authority_frame(ci, packer, tick, torque=0.)
|
||||
temporary = producer.authority_frame(ci, packer, 30, status=2, torque=0.)
|
||||
self.assertTrue(temporary.canValid and not temporary.canTimeout)
|
||||
self.assertFalse(ci.CS.steering_authority.latched)
|
||||
axis, state, command = step(1, observed_state=temporary)
|
||||
self.assertFalse(axis.lateralActive or command.latActive)
|
||||
self.assertEqual(state.alertSound, log.SelfdriveState.AudibleAlert.prompt)
|
||||
self.assertNotIn(log.OnroadEvent.EventName.controlsMismatch, sd.events.names)
|
||||
|
||||
def test_car_state_timeout_retains_only_original_fresh_valid_sample(self):
|
||||
sd = SelfdriveD.__new__(SelfdriveD)
|
||||
sd.car_state_sock = object()
|
||||
@@ -727,6 +966,7 @@ class IpcAxisContractTests(unittest.TestCase):
|
||||
sd.initialized = True
|
||||
sd.aol_replay = True
|
||||
sd.aol_car_state_log_ns = 0
|
||||
sd.conditional_car_state_valid = True
|
||||
sd.aol_session_id = 'drive-session'
|
||||
sd.aol_axis_decision = AxisDecision()
|
||||
sd.aol_dm_lateral_inhibit = False
|
||||
@@ -799,6 +1039,7 @@ class IpcAxisContractTests(unittest.TestCase):
|
||||
sd.enabled = sd.active = False
|
||||
sd.initialized = sd.aol_replay = True
|
||||
sd.aol_car_state_log_ns = 0
|
||||
sd.conditional_car_state_valid = True
|
||||
sd.aol_session_id = 'drive-session'
|
||||
sd.aol_axis_decision = AxisDecision()
|
||||
sd.aol_dm_lateral_inhibit = False
|
||||
@@ -1299,6 +1540,7 @@ class IpcAxisContractTests(unittest.TestCase):
|
||||
sd.initialized = True
|
||||
sd.aol_replay = aol_enabled
|
||||
sd.aol_car_state_log_ns = 0
|
||||
sd.conditional_car_state_valid = True
|
||||
sd.aol_session_id = 'drive-session'
|
||||
sd.aol_axis_decision = AxisDecision()
|
||||
sd.aol_dm_lateral_inhibit = False
|
||||
|
||||
@@ -15,25 +15,31 @@ class CarGpsPublisher:
|
||||
def __init__(self, *, mono_clock=time.monotonic_ns, boot_clock=boot_time_ns):
|
||||
self.mono_clock, self.boot_clock = mono_clock, boot_clock
|
||||
self.last_source_ns = 0
|
||||
self.cached_gps = None
|
||||
|
||||
def update(self, car_state, pm):
|
||||
def update(self, car_state, pm, message=None):
|
||||
getter = getattr(car_state, "get_car_gps", None)
|
||||
fix = getter() if getter is not None else None
|
||||
if fix is None:
|
||||
return
|
||||
source_ns = int(fix["timestamp_nanos"])
|
||||
if source_ns <= self.last_source_ns:
|
||||
return
|
||||
self.last_source_ns = source_ns
|
||||
before, boot, after = self.mono_clock(), self.boot_clock(), self.mono_clock()
|
||||
# pandad timestamps received CAN in BOOTTIME; Python Events use MONOTONIC.
|
||||
age = boot - source_ns
|
||||
if not 0 <= after - before <= 1_000_000 or not 0 <= age <= GPS_MAX_AGE_NS:
|
||||
return
|
||||
stamp = before - age
|
||||
if stamp <= 0:
|
||||
return
|
||||
message = messaging.new_message("starpilotCarState", valid=bool(fix["hasFix"]), logMonoTime=after)
|
||||
message.starpilotCarState.gps = {key: fix[key] for key in FIELDS}
|
||||
message.starpilotCarState.gps.sourceMonoTime = stamp
|
||||
gps, after = None, 0
|
||||
if fix is not None:
|
||||
source_ns = int(fix["timestamp_nanos"])
|
||||
if source_ns > self.last_source_ns:
|
||||
self.last_source_ns = source_ns
|
||||
before, boot, after = self.mono_clock(), self.boot_clock(), self.mono_clock()
|
||||
# pandad timestamps received CAN in BOOTTIME; Python Events use MONOTONIC.
|
||||
age = boot - source_ns
|
||||
if 0 <= after - before <= 1_000_000 and 0 <= age <= GPS_MAX_AGE_NS:
|
||||
stamp = before - age
|
||||
if stamp > 0:
|
||||
gps = {key: fix[key] for key in FIELDS}
|
||||
gps["vNED"] = list(fix["vNED"])
|
||||
gps["sourceMonoTime"] = stamp
|
||||
self.cached_gps = gps if fix["hasFix"] else None
|
||||
if message is None:
|
||||
if gps is None:
|
||||
return
|
||||
message = messaging.new_message("starpilotCarState", valid=bool(gps["hasFix"]), logMonoTime=after)
|
||||
gps = gps if gps is not None else self.cached_gps
|
||||
if gps is not None and 0 < gps["sourceMonoTime"] <= int(message.logMonoTime) <= gps["sourceMonoTime"] + GPS_MAX_AGE_NS:
|
||||
message.starpilotCarState.gps = gps
|
||||
pm.send("starpilotCarState", message)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
@@ -6,6 +7,7 @@ import pytest
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car.gm.interface import CarInterface
|
||||
from opendbc.car.gm.values import CAR
|
||||
from opendbc.car import structs as car
|
||||
from openpilot.cereal import log, messaging
|
||||
from openpilot.starpilot.gps.publisher import CarGpsPublisher
|
||||
from openpilot.starpilot.gps.source import GPS_SOURCES, observation, select_location
|
||||
@@ -130,3 +132,115 @@ def test_old_car_state_wire_has_no_gps_fix():
|
||||
sm = Messages()
|
||||
sm.send('starpilotCarState', event)
|
||||
assert select_location(sm, 101_000_000_000) is None
|
||||
|
||||
|
||||
def test_fault_companion_without_gps_retains_car_state_stamp_and_validity():
|
||||
owner, sm = CarGpsPublisher(), Messages()
|
||||
for valid in (True, False):
|
||||
cs_send = messaging.new_message('carState', valid=valid, logMonoTime=101_000_000_000)
|
||||
companion = messaging.new_message('starpilotCarState', valid=bool(cs_send.valid), logMonoTime=int(cs_send.logMonoTime))
|
||||
companion.starpilotCarState.lateralAuthorityUnavailable = True
|
||||
companion.starpilotCarState.sourceCarStateMonoTime = int(cs_send.logMonoTime)
|
||||
owner.update(SimpleNamespace(), sm, companion)
|
||||
assert sm.valid['starpilotCarState'] == cs_send.valid
|
||||
assert sm.logMonoTime['starpilotCarState'] == cs_send.logMonoTime
|
||||
assert sm['starpilotCarState'].sourceCarStateMonoTime == cs_send.logMonoTime
|
||||
assert sm['starpilotCarState'].lateralAuthorityUnavailable
|
||||
assert not sm['starpilotCarState'].gps.hasFix
|
||||
assert observation(sm, 'starpilotCarState', cs_send.logMonoTime) is None
|
||||
|
||||
|
||||
def test_fault_companion_cached_gps_keeps_original_source_stamp_and_expires():
|
||||
state, sm, clock = live_can_fix(), Messages(), [101_000_000_000]
|
||||
owner = CarGpsPublisher(mono_clock=lambda: clock[0], boot_clock=lambda: clock[0] + 50_000_000_000)
|
||||
owner.update(state, sm)
|
||||
source_stamp = sm['starpilotCarState'].gps.sourceMonoTime
|
||||
for elapsed in (1_000_000_000, 2_500_000_001):
|
||||
clock[0] = source_stamp + elapsed
|
||||
companion = messaging.new_message('starpilotCarState', valid=True, logMonoTime=clock[0])
|
||||
companion.starpilotCarState.sourceCarStateMonoTime = clock[0]
|
||||
companion.starpilotCarState.lateralAuthorityUnavailable = True
|
||||
owner.update(state, sm, companion)
|
||||
assert sm.logMonoTime['starpilotCarState'] == clock[0]
|
||||
assert sm['starpilotCarState'].sourceCarStateMonoTime == clock[0]
|
||||
assert sm['starpilotCarState'].lateralAuthorityUnavailable
|
||||
if elapsed <= 2_500_000_000:
|
||||
assert sm['starpilotCarState'].gps.sourceMonoTime == source_stamp
|
||||
assert location(sm, clock[0]) == (source_stamp, (-110., 40.), 0., None)
|
||||
else:
|
||||
assert not sm['starpilotCarState'].gps.hasFix
|
||||
assert location(sm, clock[0]) is None
|
||||
|
||||
|
||||
def test_fault_companion_clock_interruption_cannot_invent_gps_fix():
|
||||
clock, sm = iter([101_000_000_000, 101_002_000_000]), Messages()
|
||||
owner = CarGpsPublisher(mono_clock=lambda: next(clock), boot_clock=lambda: 151_000_000_000)
|
||||
companion = messaging.new_message('starpilotCarState', valid=True, logMonoTime=101_002_000_000)
|
||||
companion.starpilotCarState.sourceCarStateMonoTime = companion.logMonoTime
|
||||
companion.starpilotCarState.lateralAuthorityUnavailable = True
|
||||
owner.update(live_can_fix(), sm, companion)
|
||||
assert sm['starpilotCarState'].lateralAuthorityUnavailable
|
||||
assert not sm['starpilotCarState'].gps.hasFix
|
||||
assert location(sm, companion.logMonoTime) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('brand,enabled,valid', [('gm', True, True), ('gm', True, False), ('gm', False, True), ('honda', True, True)])
|
||||
def test_actual_card_publishes_fault_companion_before_matching_car_state(brand, enabled, valid):
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
|
||||
card = cast(Any, Car.__new__(Car))
|
||||
card.CP = SimpleNamespace(brand=brand)
|
||||
card.CI = SimpleNamespace(CS=SimpleNamespace(steering_authority=SimpleNamespace(enabled=enabled, latched=True)))
|
||||
card.pm = Messages()
|
||||
card.sm = SimpleNamespace(frame=1, all_checks=lambda _services: True)
|
||||
card.car_params_published = True
|
||||
card.last_actuators_output = car.CarControl.Actuators.new_message()
|
||||
card.slc_replay = card.curve_replay = card.aol_replay = False
|
||||
card.can_rcv_cum_timeout_counter = 0
|
||||
card.rk = SimpleNamespace(remaining=0.)
|
||||
card.v_cruise_helper = SimpleNamespace(slc_cruise_change=None)
|
||||
card.car_gps_publisher = CarGpsPublisher()
|
||||
CS = car.CarState.new_message(canValid=valid)
|
||||
card.state_publish(CS, None)
|
||||
assert ('starpilotCarState' in card.pm.data) == (brand == 'gm' and enabled)
|
||||
if brand == 'gm' and enabled:
|
||||
assert list(card.pm.data).index('starpilotCarState') < list(card.pm.data).index('carState')
|
||||
assert card.pm.valid['starpilotCarState'] == card.pm.valid['carState'] == valid
|
||||
assert card.pm.logMonoTime['starpilotCarState'] == card.pm.logMonoTime['carState']
|
||||
assert card.pm['starpilotCarState'].sourceCarStateMonoTime == card.pm.logMonoTime['carState']
|
||||
assert card.pm['starpilotCarState'].lateralAuthorityUnavailable
|
||||
assert not card.pm['starpilotCarState'].gps.hasFix
|
||||
|
||||
|
||||
@pytest.mark.parametrize('brand,enabled,keepalive', [('gm', True, False), ('gm', False, False), ('honda', True, False),
|
||||
('gm', False, True)])
|
||||
def test_actual_card_monitored_apply_and_sendcan_use_same_boottime(brand, enabled, keepalive):
|
||||
from openpilot.selfdrive.car.card import Car
|
||||
|
||||
card = cast(Any, Car.__new__(Car))
|
||||
card.CP = SimpleNamespace(brand=brand, alternativeExperience=0)
|
||||
card.CI = SimpleNamespace(CS=SimpleNamespace(steering_authority=SimpleNamespace(enabled=enabled)), CC=SimpleNamespace(),
|
||||
apply=Mock(return_value=(car.CarControl.Actuators.new_message(), [])))
|
||||
card.pm = Mock()
|
||||
card.sm = SimpleNamespace(seen={'carControl': True}, alive={'carControl': True}, valid={'carControl': True},
|
||||
logMonoTime={'carControl': 101_000_000_000}, recv_time={'carControl': 101.},
|
||||
all_alive=lambda _services: True)
|
||||
card.ci_initialized = True
|
||||
card.ioniq6_long_prearmed = False
|
||||
card.vehicle_startup = SimpleNamespace(owner=None, check=Mock())
|
||||
card.volt_startup_keepalive = Mock(return_value=keepalive)
|
||||
CS, CC = car.CarState.new_message(canValid=True), car.CarControl.new_message()
|
||||
with (patch('openpilot.selfdrive.car.card.REPLAY', False),
|
||||
patch('openpilot.selfdrive.car.card.time.monotonic', return_value=101.),
|
||||
patch('openpilot.selfdrive.car.card.time.monotonic_ns', return_value=101_000_000_000),
|
||||
patch('openpilot.selfdrive.car.card.time.clock_gettime_ns', return_value=151_000_000_000)):
|
||||
card.controls_update(CS, CC)
|
||||
expected = 151_000_000_000 if keepalive or brand == 'gm' and enabled else 101_000_000_000
|
||||
card.CI.apply.assert_called_once_with(CC, expected)
|
||||
service, packet = card.pm.send.call_args.args
|
||||
assert service == 'sendcan'
|
||||
with log.Event.from_bytes(packet) as event:
|
||||
assert event.logMonoTime == expected
|
||||
assert event.valid
|
||||
assert len(event.sendcan) == 0
|
||||
card.vehicle_startup.check.assert_called_once_with()
|
||||
|
||||
Reference in New Issue
Block a user