mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
Preserve fresh GM lateral authority while braking
This commit is contained in:
@@ -803,12 +803,12 @@ class SelfdriveD:
|
||||
|
||||
return CS
|
||||
|
||||
def held_axis_override_wait(self, CS, proof, now_ns):
|
||||
def held_axis_override_wait(self, CS, proof, now_ns, *, fresh_successor=False):
|
||||
"""Recognize an already applied override; never admit a new physical edge."""
|
||||
if (proof is None or not (qualified_ioniq6(self.CP) or qualified_gm(self.CP)) or not self.sm.seen['carControl'] or
|
||||
not self.sm.valid['carControl'] or self.sm['carControl'].longActive or
|
||||
not 0 < int(self.sm.logMonoTime['carControl']) <= now_ns or
|
||||
now_ns - int(self.sm.logMonoTime['carControl']) > INTENT_MAX_AGE_NS):
|
||||
not fresh_successor and now_ns - int(self.sm.logMonoTime['carControl']) > INTENT_MAX_AGE_NS):
|
||||
return False, False
|
||||
previous = self.CS_prev
|
||||
samples = getattr(self, 'car_state_samples', ()) or ((CS, self.conditional_car_state_valid),)
|
||||
@@ -1273,6 +1273,17 @@ class SelfdriveD:
|
||||
source_ns = self.aol_car_state_log_ns
|
||||
intent = getattr(self, 'aol_last_intent', None)
|
||||
companion = getattr(self, 'aol_intent_companion', None)
|
||||
fresh_successor = bool(proof[1] < source_ns <= now_ns and now_ns - source_ns <= INTENT_MAX_AGE_NS and
|
||||
self.conditional_car_state_valid and CS.canValid and not CS.canTimeout and
|
||||
intent is not None and intent.carStateLogMonoTime == source_ns and
|
||||
intent.producerSessionId == proof[4] and intent.settingsQualified and
|
||||
0 < intent.observedMonoTime <= now_ns <= intent.validUntilMonoTime and
|
||||
now_ns - intent.observedMonoTime <= INTENT_MAX_AGE_NS and
|
||||
companion is not None and companion.integrity_epoch == proof[5] and
|
||||
decision.native_acknowledged and not self.aol_authority_lost and
|
||||
getattr(self, 'aol_fault_reason', 'none') == 'none' and
|
||||
not self.events.contains(ET.IMMEDIATE_DISABLE) and
|
||||
(not decision.longitudinal_active or self.enabled and self.active))
|
||||
user_disable = self.events.contains(ET.USER_DISABLE)
|
||||
if user_disable:
|
||||
from opendbc.car.hyundai.canfd_angle_aol import qualified as angle_qualified
|
||||
@@ -1287,21 +1298,17 @@ class SelfdriveD:
|
||||
if stock_lateral_role(self, CS, native, intent, allow_independent_cancel=True,
|
||||
allow_independent_brake=True, now_ns=now_ns):
|
||||
user_disable = False
|
||||
if user_disable and self.held_axis_override_wait(CS, proof, now_ns)[0]:
|
||||
# A moving held brake repeats pedalPressed while ordinary control is
|
||||
# already disabled. Its existing lateral-only override is not a new edge.
|
||||
user_disable = any(event.userDisable and event.name != EventName.pedalPressed for event in self.events.to_msg())
|
||||
if (proof[1] < source_ns <= now_ns and now_ns - source_ns <= INTENT_MAX_AGE_NS and
|
||||
self.conditional_car_state_valid and CS.canValid and not CS.canTimeout and
|
||||
intent is not None and intent.carStateLogMonoTime == source_ns and
|
||||
intent.producerSessionId == proof[4] and intent.settingsQualified and
|
||||
0 < intent.observedMonoTime <= now_ns <= intent.validUntilMonoTime and
|
||||
now_ns - intent.observedMonoTime <= INTENT_MAX_AGE_NS and
|
||||
companion is not None and companion.integrity_epoch == proof[5] and
|
||||
decision.native_acknowledged and not self.aol_authority_lost and
|
||||
getattr(self, 'aol_fault_reason', 'none') == 'none' and
|
||||
not user_disable and not self.events.contains(ET.IMMEDIATE_DISABLE) and
|
||||
(not decision.longitudinal_active or self.enabled and self.active)):
|
||||
if user_disable and self.held_axis_override_wait(CS, proof, now_ns, fresh_successor=fresh_successor and qualified_gm(self.CP))[0]:
|
||||
# A fresh same-owner source can establish its own LAT permission after
|
||||
# an old output expires. Held-brake continuity does not renew that output;
|
||||
# the current native proof below still has to grant the new decision.
|
||||
stock_already_off = bool(fresh_successor and qualified_gm(self.CP) and self.CP.pcmCruise and
|
||||
not self.CP.openpilotLongitudinalControl and not CS.cruiseState.enabled and
|
||||
not self.CS_prev.cruiseState.enabled and all(valid and not sample.cruiseState.enabled
|
||||
for sample, valid in getattr(self, 'car_state_samples', ())))
|
||||
user_disable = any(event.userDisable and event.name != EventName.pedalPressed and not
|
||||
(stock_already_off and event.name == EventName.pcmDisable) for event in self.events.to_msg())
|
||||
if fresh_successor and not user_disable:
|
||||
native = current_native(self.sm, self.CP, now_ns=now_ns, axis_session_id=self.aol_session_id)
|
||||
prior_native = getattr(self, 'aol_published_input_native', None)
|
||||
if (native is not None and prior_native is not None and native.pandaSerial == prior_native.pandaSerial and
|
||||
|
||||
@@ -54,6 +54,10 @@ def test_final_handoff_full_owner(mode, condition):
|
||||
assert command.longActive == (mode == 'ordinary')
|
||||
deadline = original.validUntilMonoTime
|
||||
now = [BASE + 29_900_000]
|
||||
def empty_control_receive(timeout):
|
||||
assert timeout == 0
|
||||
actual_update_msgs(now[0] / 1e9, [])
|
||||
cleanup.enter_context(mock.patch.object(controls.sm, 'update', side_effect=empty_control_receive))
|
||||
queued, wires = deque(), deque()
|
||||
decisions, samples, states, deferred_proofs = [], [], [], []
|
||||
original_receive = messaging.recv_one_or_none
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
"""SDGM fresh held-brake successor across the recorded source/decision timing pair.
|
||||
|
||||
Model/readiness and receive transport are bench fixtures; ECU I/O is skipped.
|
||||
Neither ordinary controls_allowed nor an active steering command is seeded.
|
||||
"""
|
||||
|
||||
from contextlib import ExitStack
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from opendbc.can import CANPacker
|
||||
from opendbc.car import Bus, structs
|
||||
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
|
||||
from opendbc.car.gm.values import DBC
|
||||
from opendbc.safety.tests.libsafety import libsafety_py as native
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.starpilot.aol.intent import read_settings
|
||||
from openpilot.starpilot.aol.tests import test_gm as gm
|
||||
from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture
|
||||
from openpilot.starpilot.aol.tests.test_gm_startup_main import parsed_packets
|
||||
from openpilot.starpilot.aol.wire import PandaSlot, SafetyState, encode_safety
|
||||
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed
|
||||
|
||||
|
||||
def sdgm_packets(cp, packer, now, tick, speed, main, gesture):
|
||||
speed = (5.858360767 if tick < 390 else 5.836455345) if speed > 0 else 0.0
|
||||
frames = parsed_packets(cp, packer, now, tick, speed, main)
|
||||
engine = packer.make_can_msg(
|
||||
"ECMEngineStatus",
|
||||
0,
|
||||
{"CruiseMainOn": int(main), "BrakePressed": int(tick > 300)},
|
||||
)
|
||||
brake = packer.make_can_msg("ECMAcceleratorPos", 0, {"BrakePedalPos": 8 if tick > 300 else 0})
|
||||
frames = [f for f in frames if f[0] not in (engine[0], brake[0])]
|
||||
return frames + [engine, brake]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("alpha", [False, True])
|
||||
@pytest.mark.parametrize("c9", [False, True])
|
||||
def test_actual_sdgm_held_brake_fresh_successor_after_expired_output(c9, alpha):
|
||||
exercise_sdgm(c9=c9, alpha=alpha)
|
||||
|
||||
|
||||
def exercise_sdgm(c9, alpha, condition="healthy"):
|
||||
main_action = 0
|
||||
gesture = "held_brake"
|
||||
cp = sdgm_params(alpha=alpha, brake_c9=c9)
|
||||
packet_factory = sdgm_packets
|
||||
below_engage = False
|
||||
case = fixture.IoniqAvailabilityTests(methodName="runTest")
|
||||
with OpenpilotPrefix(), ExitStack() as cleanup:
|
||||
cleanup.callback(case.doCleanups)
|
||||
sd, controls, step = case._authority_callers(False, cp=cp, genuine_events=True)
|
||||
sd.params.put("MainCruiseButtonControl", main_action, block=True)
|
||||
sd.aol_settings = read_settings(sd.params)
|
||||
# The initial real Controls result is disabled because no authority wire exists.
|
||||
# This boot-neutral fixture establishes no Panda grant or active output.
|
||||
feed(controls, 10_000_000_000, 0, active=False, enabled=False)
|
||||
boot, boot_log = controls.state_control()
|
||||
assert not boot.latActive and not boot.longActive
|
||||
controls.publish(boot, boot_log)
|
||||
card = gm.TestGmAol.card(cp, sd.params)
|
||||
cleanup.callback(card.vehicle_startup.close)
|
||||
card.ci_initialized = True
|
||||
card.car_params_published = True
|
||||
card.sm.frame = 1
|
||||
assert card.CP.to_dict() == sd.CP.to_dict() == controls.CP.to_dict()
|
||||
original_pm = card.pm
|
||||
captured = {}
|
||||
intent_queue = []
|
||||
companion_queue = []
|
||||
original_optional = messaging.recv_one_or_none
|
||||
|
||||
def deliver(sock):
|
||||
if sd.steering_companion is not None and sock is sd.steering_companion.sock:
|
||||
return companion_queue.pop(0) if companion_queue else None
|
||||
if sd.aol_intent_companion is not None and sock is sd.aol_intent_companion.sock:
|
||||
return intent_queue.pop(0) if intent_queue else None
|
||||
return original_optional(sock)
|
||||
|
||||
cleanup.enter_context(
|
||||
patch(
|
||||
"openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one_or_none",
|
||||
side_effect=deliver,
|
||||
)
|
||||
)
|
||||
|
||||
class Tee:
|
||||
def send(self, service, message):
|
||||
original_pm.send(service, message)
|
||||
event = messaging.log_from_bytes(message) if isinstance(message, bytes) else message.as_reader()
|
||||
captured[service] = event.as_builder()
|
||||
if service == "aolIntentWire":
|
||||
intent_queue.append(event)
|
||||
if service == "starpilotCarState":
|
||||
companion_queue.append(event)
|
||||
|
||||
card.pm = Tee()
|
||||
sd_receive = sd.sm.update_msgs
|
||||
last_sd_control_stamp = 0
|
||||
|
||||
def receive_sd(stamp, rows):
|
||||
nonlocal last_sd_control_stamp
|
||||
# Deliver the original last published Controls envelope without renewing its source.
|
||||
published = controls.pm.messages.get("carControl")
|
||||
rows = [r for r in rows if r.which() != "carControl"]
|
||||
if published is not None and published.logMonoTime != last_sd_control_stamp:
|
||||
rows.append(published)
|
||||
last_sd_control_stamp = int(published.logMonoTime)
|
||||
return sd_receive(stamp, rows)
|
||||
|
||||
cleanup.enter_context(patch.object(sd.sm, "update_msgs", side_effect=receive_sd))
|
||||
safety = native.libsafety
|
||||
safety.init_tests()
|
||||
safety.set_alternative_experience(cp.alternativeExperience)
|
||||
assert safety.set_safety_hooks(4, cp.safetyConfigs[0].safetyParam) == 0
|
||||
# Establish actual neutral producer cadence before Card publishes carOutput.
|
||||
card_receive = card.sm.update_msgs
|
||||
card_receive(10.0, [controls.pm.messages["carControl"]])
|
||||
boot_rows = []
|
||||
with patch.object(
|
||||
controls.sm,
|
||||
"update_msgs",
|
||||
side_effect=lambda stamp, rows: boot_rows.extend(rows),
|
||||
):
|
||||
feed(controls, 10_010_000_000, 1, active=False, enabled=False)
|
||||
controls.sm.update_msgs(10.01, boot_rows)
|
||||
clocked = messaging.new_message
|
||||
with patch(
|
||||
"openpilot.selfdrive.controls.controlsd.messaging.new_message",
|
||||
side_effect=lambda *args, **kwargs: clocked(*args, **dict(kwargs, logMonoTime=10_010_000_000)),
|
||||
):
|
||||
neutral, neutral_log = controls.state_control()
|
||||
assert not neutral.latActive and not neutral.longActive
|
||||
controls.publish(neutral, neutral_log)
|
||||
card_receive(10.01, [controls.pm.messages["carControl"]])
|
||||
last_control_stamp = int(controls.pm.messages["carControl"].logMonoTime)
|
||||
delivered = {}
|
||||
|
||||
if card.volt_cc_selected:
|
||||
# Establish the real clock floor before any fresh physical/control input.
|
||||
with (
|
||||
patch(
|
||||
"openpilot.selfdrive.car.card.time.monotonic_ns",
|
||||
return_value=10_010_000_000,
|
||||
),
|
||||
patch(
|
||||
"openpilot.selfdrive.car.card.time.clock_gettime_ns",
|
||||
return_value=10_010_000_000,
|
||||
),
|
||||
):
|
||||
assert not card.volt_cc_control_current()
|
||||
|
||||
def wire_batch(now):
|
||||
mask = safety.aol_get_permission_mask()
|
||||
request = safety.aol_get_request_mask()
|
||||
invalid = not safety.safety_config_valid()
|
||||
relay = safety.get_relay_malfunction()
|
||||
p = messaging.new_message("pandaStates", 1, valid=True, logMonoTime=now)
|
||||
slot = p.pandaStates[0]
|
||||
slot.safetyModel = 4
|
||||
slot.safetyParam = cp.safetyConfigs[0].safetyParam
|
||||
slot.alternativeExperience = cp.alternativeExperience
|
||||
slot.controlsAllowed = safety.get_controls_allowed()
|
||||
slot.safetyRxChecksInvalid = invalid
|
||||
slot.faults = ["relayMalfunction"] if relay else []
|
||||
w = messaging.new_message("aolSafetyWire", 0, valid=True, logMonoTime=now)
|
||||
w.aolSafetyWire = encode_safety(
|
||||
SafetyState(
|
||||
1,
|
||||
True,
|
||||
now,
|
||||
now + 200_000_000,
|
||||
4,
|
||||
cp.safetyConfigs[0].safetyParam,
|
||||
bool(mask & 1),
|
||||
bool(mask & 2),
|
||||
bool(request & 1),
|
||||
bool(request & 2),
|
||||
"authority-panda",
|
||||
sd.aol_session_id,
|
||||
now,
|
||||
(
|
||||
PandaSlot(
|
||||
0,
|
||||
"authority-panda",
|
||||
4,
|
||||
cp.safetyConfigs[0].safetyParam,
|
||||
cp.alternativeExperience,
|
||||
safety.get_controls_allowed(),
|
||||
invalid,
|
||||
False,
|
||||
1 if relay else 0,
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
return p.as_reader(), w.as_reader()
|
||||
|
||||
def receive_card(now, *, control=True):
|
||||
nonlocal last_control_stamp
|
||||
rows = list(wire_batch(now))
|
||||
if card.volt_cc_selected:
|
||||
device = messaging.new_message("deviceState", valid=True, logMonoTime=now)
|
||||
device.deviceState.started, device.deviceState.startedMonoTime = (
|
||||
True,
|
||||
9_000_000_000,
|
||||
)
|
||||
rows.append(device.as_reader())
|
||||
rows += [sd.pm.messages[k] for k in ("aolAxisState", "selfdriveState", "onroadEvents") if k in sd.pm.messages]
|
||||
published = controls.pm.messages["carControl"]
|
||||
if control and published.logMonoTime != last_control_stamp:
|
||||
rows.append(published)
|
||||
last_control_stamp = int(published.logMonoTime)
|
||||
unique = []
|
||||
for event in rows:
|
||||
name = event.which()
|
||||
key = (int(event.logMonoTime), event.as_builder().to_bytes())
|
||||
if delivered.get(name) != key:
|
||||
unique.append(event)
|
||||
delivered[name] = key
|
||||
card_receive(now / 1e9, unique)
|
||||
|
||||
packer = CANPacker(DBC[cp.carFingerprint][Bus.pt])
|
||||
|
||||
def physical(tick, stamp, driving):
|
||||
speed = max(4.0, cp.minSteerSpeed + 2.0)
|
||||
if below_engage:
|
||||
assert speed < cp.minEnableSpeed
|
||||
else:
|
||||
speed = max(speed, cp.minEnableSpeed + 2.0)
|
||||
packets = packet_factory(cp, packer, stamp, tick - 100, speed if driving else 0.0, True, gesture)
|
||||
packets = [f for f in packets if f[0] not in (0x1F5, 0x12A, 0x184, 0x17D)]
|
||||
if cp.pcmCruise and cp.networkLocation == structs.CarParams.NetworkLocation.fwdCamera:
|
||||
address = packer.make_can_msg("ASCMActiveCruiseControlStatus", 2, {})[0]
|
||||
packets = [f for f in packets if not (f[0] == address and f[2] == 2)]
|
||||
# Factory camera reports adaptive standby, not non-adaptive cruise mode.
|
||||
packets.append(packer.make_can_msg("ASCMActiveCruiseControlStatus", 2, {"ACCCruiseState": 2}))
|
||||
packets += [
|
||||
packer.make_can_msg("ESPStatus", 0, {"TractionControlOn": 1}),
|
||||
packer.make_can_msg("ECMPRDNL2", 0, {"PRNDL2": 4 if driving else 1}),
|
||||
packer.make_can_msg("BCMDoorBeltStatus", 0, {"LeftSeatBelt": int(driving)}),
|
||||
packer.make_can_msg("PSCMStatus", 0, {"LKATorqueDeliveredStatus": int(driving)}),
|
||||
]
|
||||
return packets
|
||||
|
||||
for warm in range(100):
|
||||
stamp = 9_000_000_000 + warm * 10_000_000
|
||||
safety.set_timer(stamp // 1000)
|
||||
safety.set_aol_test_heartbeat(True)
|
||||
safety.aol_set_host_request(0)
|
||||
packets = physical(warm, stamp, False)
|
||||
for a, data, bus in packets:
|
||||
safety.safety_fwd_hook(bus, a)
|
||||
assert safety.safety_rx_hook(native.make_CANPacket(a, bus, data))
|
||||
card.CI.update([(stamp, packets)])
|
||||
safety.safety_tick()
|
||||
assert safety.safety_config_valid() and safety.aol_get_permission_mask() == 0
|
||||
tx = 0
|
||||
active = 0
|
||||
checkpoints = {}
|
||||
old_axis = None
|
||||
for tick in range(1, 391):
|
||||
now = 10_020_000_000 + (tick - 1) * 10_000_000
|
||||
if tick == 390:
|
||||
now += 20_271_875
|
||||
decision_ns = now + (6_268_334 if tick == 389 else 11_981_615 if tick == 390 else 0)
|
||||
driving = tick > 300
|
||||
safety.set_timer(now // 1000)
|
||||
safety.set_aol_test_heartbeat(True)
|
||||
packets = physical(tick + 100, now, driving)
|
||||
for a, data, bus in packets:
|
||||
safety.safety_fwd_hook(bus, a)
|
||||
assert safety.safety_rx_hook(native.make_CANPacket(a, bus, data))
|
||||
raw = messaging.new_message('can', len(packets), valid=True, logMonoTime=now)
|
||||
for dst, (address, data, bus) in zip(raw.can, packets, strict=True):
|
||||
dst.address, dst.dat, dst.src = address, data, bus
|
||||
with patch('openpilot.selfdrive.car.card.messaging.drain_sock_raw', return_value=[raw.to_bytes()]), \
|
||||
patch.object(card.sm, 'update', side_effect=lambda timeout, _now=now: receive_card(_now)), \
|
||||
patch('openpilot.selfdrive.car.card.time.monotonic_ns', return_value=now), \
|
||||
patch('openpilot.selfdrive.car.card.time.clock_gettime_ns', return_value=now):
|
||||
cs, _ = card.state_update(defer_radar=True)
|
||||
safety.safety_tick()
|
||||
assert cs.canValid and not cs.canTimeout
|
||||
assert card._aol_can_observation[0] is cs and card._aol_can_observation[1:3] == (now, now)
|
||||
allowed = card.aol_card_intent.output(cs)[0]
|
||||
request = int(allowed) | (2 if sd.aol_axis_decision.desired_longitudinal else 0)
|
||||
safety.set_timer(decision_ns // 1000)
|
||||
safety.aol_set_host_request(request)
|
||||
mask = safety.aol_get_permission_mask()
|
||||
message = messaging.new_message
|
||||
with patch(
|
||||
"openpilot.selfdrive.car.card.messaging.new_message",
|
||||
side_effect=lambda *args, _clock=message, _now=now, **kwargs: _clock(*args, **dict(kwargs, logMonoTime=_now)),
|
||||
):
|
||||
card.state_publish(cs, None)
|
||||
negative = tick == 390 and condition != "healthy"
|
||||
if negative:
|
||||
# Typed receiver perturbations start after a complete actual CAN/native
|
||||
# baseline; they cannot manufacture native permission or renew old output.
|
||||
from dataclasses import replace
|
||||
from openpilot.starpilot.aol.wire import decode_intent, encode_intent
|
||||
current = captured["carState"].carState
|
||||
if condition == "new_brake":
|
||||
sd.CS_prev = sd.CS_prev.as_builder()
|
||||
sd.CS_prev.brakePressed = False
|
||||
elif condition == "new_paddle":
|
||||
current.regenBraking = True
|
||||
elif condition == "new_gas":
|
||||
current.gasPressed = True
|
||||
elif condition == "cancel":
|
||||
current.buttonEvents = [structs.CarState.ButtonEvent(type="cancel", pressed=True)]
|
||||
elif condition == "invalid_current":
|
||||
current.canValid, current.canTimeout = False, True
|
||||
elif condition == "permanent_eps":
|
||||
current.steerFaultPermanent = True
|
||||
elif condition == "main_off":
|
||||
current.cruiseState.available = False
|
||||
elif condition == "gear_unknown":
|
||||
current.gearShifter = "unknown"
|
||||
elif condition == "stock_disable_edge":
|
||||
sd.CS_prev = sd.CS_prev.as_builder()
|
||||
sd.CS_prev.cruiseState.enabled = True
|
||||
if condition in ("future_current", "old_current"):
|
||||
source = decision_ns + 1 if condition == "future_current" else old_axis.sourceCarStateMonoTime
|
||||
captured["carState"].logMonoTime = source
|
||||
wire = captured["aolIntentWire"]
|
||||
wire.aolIntentWire = encode_intent(replace(decode_intent(wire.aolIntentWire), carStateLogMonoTime=source))
|
||||
changes = {"producer": {"producerSessionId": "other-card"}, "settings": {"settingsQualified": False},
|
||||
"pause_lateral": {"pauseLateral": True}}
|
||||
if condition in changes:
|
||||
wire = captured["aolIntentWire"]
|
||||
wire.aolIntentWire = encode_intent(replace(decode_intent(wire.aolIntentWire), **changes[condition]))
|
||||
if condition == "epoch":
|
||||
sd.aol_intent_companion.integrity_epoch += 1
|
||||
axis, _, cc = step(
|
||||
tick,
|
||||
state_stamp=now,
|
||||
offset_ns=decision_ns - 10_000_000_000 - tick * 10_000_000,
|
||||
car_state_message=captured["carState"],
|
||||
intent_message=captured["aolIntentWire"],
|
||||
car_output_message=captured["carOutput"],
|
||||
car_output_valid=captured["carOutput"].valid,
|
||||
allowed=bool(mask & 1) and not (negative and condition == "native_denied"),
|
||||
native_valid=not (negative and condition == "native_invalid"),
|
||||
panda_fault=negative and condition == "rx_fault",
|
||||
relay_fault=negative and condition == "relay_fault",
|
||||
long_allowed=bool(mask & 2),
|
||||
native_request_lat=bool(request & 1),
|
||||
native_request_long=bool(request & 2),
|
||||
panda_controls_allowed=safety.get_controls_allowed(),
|
||||
authority_message=captured.get("starpilotCarState"),
|
||||
)
|
||||
if negative:
|
||||
assert old_axis.validUntilMonoTime < decision_ns
|
||||
assert not axis.lateralActive and not cc.latActive, (condition, axis.to_dict(), sd.events.names)
|
||||
assert not axis.longitudinalActive and not cc.longActive
|
||||
return
|
||||
if tick == 389:
|
||||
old_axis = axis
|
||||
receive_card(decision_ns, control=False)
|
||||
if tick == 390:
|
||||
assert old_axis.validUntilMonoTime < decision_ns
|
||||
old_deadline = old_axis.validUntilMonoTime
|
||||
receive_card(decision_ns)
|
||||
assert old_axis.validUntilMonoTime == old_deadline
|
||||
assert axis.validUntilMonoTime == decision_ns + 30_000_000
|
||||
assert not cc.enabled and not cc.longActive and not safety.get_controls_allowed()
|
||||
if tick == 390:
|
||||
assert not sd.aol_authority_lost and str(axis.faultReason) == "none"
|
||||
assert axis.lateralActive and cc.latActive and not axis.longitudinalActive
|
||||
assert axis.sourceCarStateMonoTime == now and axis.observedMonoTime == decision_ns
|
||||
if tick in (360, 420, 480, 520):
|
||||
checkpoints[tick] = (
|
||||
bool(cc.enabled),
|
||||
bool(cc.longActive),
|
||||
bool(cc.latActive),
|
||||
card.v_cruise_helper.v_cruise_kph,
|
||||
bool(cs.brakePressed),
|
||||
bool(cs.regenBraking),
|
||||
)
|
||||
if not driving:
|
||||
assert str(cs.gearShifter) == "park" and cs.seatbeltUnlatched and cs.standstill
|
||||
assert not axis.lateralActive and not cc.latActive and safety.aol_get_request_mask() == 0 and mask == 0
|
||||
assert str(axis.faultReason) != "critical", (
|
||||
tick,
|
||||
axis.to_dict(),
|
||||
sd.events.names,
|
||||
)
|
||||
failed = card.aol_disarming_fault(cs, int(card.sm.logMonoTime["onroadEvents"]), decision_ns)
|
||||
if failed:
|
||||
print(
|
||||
"SDGM_FAILURE",
|
||||
tick,
|
||||
gesture,
|
||||
axis.to_dict(),
|
||||
[e.to_dict() for e in sd.events.to_msg()],
|
||||
card.aol_transport_feedback.__dict__,
|
||||
checkpoints,
|
||||
)
|
||||
assert not failed
|
||||
assert card.aol_card_intent.allowed_latch and not card.aol_card_intent._main_cycle_required
|
||||
command = card.sm["carControl"]
|
||||
captured.pop("sendcan", None)
|
||||
with (
|
||||
patch(
|
||||
"openpilot.selfdrive.car.card.time.monotonic_ns",
|
||||
return_value=decision_ns + 2,
|
||||
),
|
||||
patch(
|
||||
"openpilot.selfdrive.car.card.time.monotonic",
|
||||
return_value=(decision_ns + 2) / 1e9,
|
||||
),
|
||||
patch(
|
||||
"openpilot.selfdrive.car.card.time.clock_gettime_ns",
|
||||
return_value=decision_ns + 2,
|
||||
),
|
||||
):
|
||||
card.controls_update(cs, command)
|
||||
emitted = captured["sendcan"]
|
||||
if not emitted.valid:
|
||||
# The first command is exactly at the real source floor; it cannot emit.
|
||||
assert tick == 1 and not emitted.sendcan and not cc.enabled and not cc.latActive and not cc.longActive
|
||||
card.CS_prev = cs
|
||||
continue
|
||||
assert emitted.valid
|
||||
for frame in emitted.sendcan:
|
||||
assert safety.safety_tx_hook(native.make_CANPacket(frame.address, frame.src, frame.dat))
|
||||
tx += 1
|
||||
if frame.address == 0x180:
|
||||
if not driving:
|
||||
assert not frame.dat[0] & 8
|
||||
else:
|
||||
active += bool(frame.dat[0] & 8)
|
||||
card.CS_prev = cs
|
||||
assert active > 0 and tx > 0 and card.aol_card_intent.allowed_latch
|
||||
assert not sd.aol_authority_lost
|
||||
|
||||
|
||||
@pytest.mark.parametrize("c9", [False, True])
|
||||
@pytest.mark.parametrize("condition", ["new_brake", "new_paddle", "cancel", "invalid_current", "future_current",
|
||||
"old_current", "new_gas", "producer", "epoch", "settings", "pause_lateral", "native_denied", "native_invalid", "rx_fault",
|
||||
"relay_fault", "permanent_eps", "main_off", "gear_unknown", "stock_disable_edge"])
|
||||
def test_fresh_held_successor_rejects_changed_identity_health_or_physical_input(c9, condition):
|
||||
exercise_sdgm(c9=c9, alpha=condition != "stock_disable_edge", condition=condition)
|
||||
@@ -1,16 +1,45 @@
|
||||
"""Volt route c0508: source cutoff must not replace an unexpired axis proof."""
|
||||
from collections import deque
|
||||
from contextlib import ExitStack
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from opendbc.car.gm.tests.test_volt_sdgm_control import sdgm_params
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.starpilot.aol.intent import read_settings
|
||||
from openpilot.starpilot.aol.tests import test_ioniq_availability as fixture
|
||||
from openpilot.starpilot.aol.tests.test_input_deadline_continuity import BASE, intent
|
||||
|
||||
|
||||
def bind_steering_receipts(sd, step, cleanup):
|
||||
"""Deliver the same declared producer receipt through its actual socket path."""
|
||||
if sd.steering_companion is None:
|
||||
return step
|
||||
pending = deque()
|
||||
original = messaging.recv_one_or_none
|
||||
|
||||
def receive(sock):
|
||||
if sock is sd.steering_companion.sock:
|
||||
return pending.popleft() if pending else None
|
||||
return original(sock)
|
||||
|
||||
cleanup.enter_context(patch('openpilot.selfdrive.selfdrived.selfdrived.messaging.recv_one_or_none', side_effect=receive))
|
||||
|
||||
def observed_step(tick, **kwargs):
|
||||
if not kwargs.get('timeout', False):
|
||||
stamp = kwargs.get('state_stamp', 10_000_000_000 + tick * 10_000_000 + kwargs.get('offset_ns', 0))
|
||||
receipt = messaging.new_message('starpilotCarState', valid=True, logMonoTime=stamp)
|
||||
receipt.starpilotCarState.sourceCarStateMonoTime = stamp
|
||||
receipt.starpilotCarState.lateralAuthorityUnavailable = kwargs.get('authority_unavailable', False)
|
||||
kwargs['authority_message'] = receipt
|
||||
pending.append(receipt.as_reader())
|
||||
return step(tick, **kwargs)
|
||||
|
||||
return observed_step
|
||||
|
||||
|
||||
def profiles():
|
||||
from opendbc.car.gm.tests.test_volt_camera_control import camera_params
|
||||
from opendbc.car.gm.tests.test_volt_camera_removed import removed_params
|
||||
@@ -38,6 +67,7 @@ def test_volt_cutoff_keeps_only_original_lease_then_consumes_next_source(mode, s
|
||||
cleanup.callback(case.doCleanups)
|
||||
full = mode == 'ordinary'
|
||||
sd, controls, step = case._authority_callers(full, cp=cp.as_reader().as_builder(), genuine_events=True)
|
||||
step = bind_steering_receipts(sd, step, cleanup)
|
||||
sd.aol_settings = read_settings(sd.params)
|
||||
cs = fixture.car_state(brake=mode == 'brake')
|
||||
cs.regenBraking = mode == 'regen'
|
||||
@@ -46,6 +76,13 @@ def test_volt_cutoff_keeps_only_original_lease_then_consumes_next_source(mode, s
|
||||
assert old.lateralActive and command.latActive and bool(command.longActive) == full
|
||||
deadline = old.validUntilMonoTime
|
||||
clock = [BASE + 29_900_000]
|
||||
actual_controls_receive = controls.sm.update_msgs
|
||||
|
||||
def empty_control_receive(timeout):
|
||||
assert timeout == 0
|
||||
actual_controls_receive(clock[0] / 1e9, [])
|
||||
|
||||
cleanup.enter_context(patch.object(controls.sm, 'update', side_effect=empty_control_receive))
|
||||
original = sd.update_process_health
|
||||
|
||||
def finish_preamble():
|
||||
@@ -57,6 +94,15 @@ def test_volt_cutoff_keeps_only_original_lease_then_consumes_next_source(mode, s
|
||||
patch.object(sd, 'update_process_health', side_effect=finish_preamble):
|
||||
held, _, _ = step(5, timeout=True, state_stamp=BASE, offset_ns=-100_000,
|
||||
intent_message=intent(BASE, 3), observed_state=cs, native_request_long=full)
|
||||
from opendbc.car.gm.steering_authority import monitored_profile
|
||||
if monitored_profile(cp):
|
||||
# This monitored profile also needs its original steering-health receipt.
|
||||
# At this point that source is older than30ms. Its missing successor cannot
|
||||
# inherit health from an unexpired axis envelope or a fresh Panda grant.
|
||||
assert not sd.aol_axis_publication_deferred
|
||||
assert not held.lateralActive and not held.longitudinalActive
|
||||
assert sd.steering_authority_unavailable and sd.aol_authority_lost
|
||||
return
|
||||
assert sd.aol_axis_publication_deferred
|
||||
assert held.to_dict() == old.to_dict() and held.validUntilMonoTime == deadline
|
||||
assert not sd.aol_authority_lost
|
||||
@@ -90,6 +136,7 @@ def test_volt_deferral_cannot_hide_driver_edges_or_failed_proof(bad):
|
||||
with OpenpilotPrefix(), ExitStack() as cleanup:
|
||||
cleanup.callback(case.doCleanups)
|
||||
sd, _, step = case._authority_callers(False, cp=sdgm_params(alpha=True), genuine_events=True)
|
||||
step = bind_steering_receipts(sd, step, cleanup)
|
||||
sd.aol_settings = read_settings(sd.params)
|
||||
old, _, command = step(2, state_stamp=BASE, offset_ns=4_530_833,
|
||||
intent_message=intent(BASE, 3), native_request_long=False)
|
||||
@@ -135,6 +182,7 @@ def test_ascm_separate_steering_companion_expiry_still_withdraws_outputs(cp):
|
||||
with OpenpilotPrefix(), ExitStack() as cleanup:
|
||||
cleanup.callback(case.doCleanups)
|
||||
sd, _, step = case._authority_callers(True, cp=cp.as_reader().as_builder(), genuine_events=True)
|
||||
step = bind_steering_receipts(sd, step, cleanup)
|
||||
sd.aol_settings = read_settings(sd.params)
|
||||
from collections import deque
|
||||
queue = deque()
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"protected_inventory_sha256": "3431c43b06385ea8e9a91a7820b952792f1a1c27069c5ac04fc7f17227490b4c", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "8b4e37f4b6b70b41edab93d19b3690a747cd7eeeb7db5c681f24252e26f13818", "schema_version": 1}
|
||||
|
||||
Reference in New Issue
Block a user