diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index 6071df1a10..fdef66f300 100644 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -1138,8 +1138,16 @@ class SelfdriveD: clear_event_types.add(ET.NO_ENTRY) pers = LONGITUDINAL_PERSONALITY_MAP[self.personality] + soft_disable_time = self.state_machine.soft_disable_timer + if (getattr(self, 'aol_ordinary_source_pause', False) and self.enabled and + self.state_machine.state in (log.SelfdriveState.OpenpilotState.enabled, log.SelfdriveState.OpenpilotState.overriding) and + availability_only(self.events.to_msg())): + # A publication-time fault precedes the next ordinary timer transition. + # Present its initial soft warning without ticking or resetting that timer. + from openpilot.selfdrive.selfdrived.state import SOFT_DISABLE_TIME + soft_disable_time = int(SOFT_DISABLE_TIME / DT_CTRL) alerts = self.events.create_alerts(self.state_machine.current_alert_types, [self.CP, CS, self.sm, self.is_metric, - self.state_machine.soft_disable_timer, pers]) + soft_disable_time, pers]) if getattr(self, 'aol_lag_takeover_alert', False) and not self.events.contains(ET.IMMEDIATE_DISABLE): lag = Events(self.events.event_rules) lag.add(EventName.selfdrivedLagging) @@ -1306,10 +1314,10 @@ class SelfdriveD: self.aol_fault_reason = 'availabilityPause' self.aol_car_state_log_ns = 0 self.conditional_car_state_valid = False - # A late source fault still enters the real state machine before alerts - # consume its timer; appending SOFT_DISABLE alone leaves an expired timer. + # If publication crosses the cutoff after StateMachine ran, emit the fault + # now; its existing next-cycle transition owns the timer and driver state. if self.enabled and ET.SOFT_DISABLE not in self.state_machine.current_alert_types: - self.enabled, self.active = self.state_machine.update(self.events) + self.state_machine.current_alert_types.append(ET.SOFT_DISABLE) return True def _expire_axis_source(self, CS, now_ns): diff --git a/openpilot/starpilot/aol/tests/test_ford_long_only_withdrawal.py b/openpilot/starpilot/aol/tests/test_ford_long_only_withdrawal.py index 68e16146f2..d6ae7a7390 100644 --- a/openpilot/starpilot/aol/tests/test_ford_long_only_withdrawal.py +++ b/openpilot/starpilot/aol/tests/test_ford_long_only_withdrawal.py @@ -85,20 +85,37 @@ def test_ford_long_only_health_and_proof_refusals_remain_immediate(fault): assert not axis.lateralActive and not axis.longitudinalActive -def test_late_publication_fault_starts_real_soft_disable_timer(): +def test_late_publication_fault_preserves_timer_and_uses_initial_soft_warning(): with ExitStack() as cleanup: sd, _, drive = callers(cleanup) # The last valid axis proof expires at +30ms; output must still withdraw. now = sd.aol_car_state_log_ns + 30_000_001 with patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=now): - assert sd.retain_ordinary_source_state(sd.CS_prev, now) - assert sd.state_machine.state == log.SelfdriveState.OpenpilotState.softDisabling, 'LATE_SOURCE_FAULT_TIMER' - assert 0 < sd.state_machine.soft_disable_timer <= 300 + with patch.object(sd.state_machine, 'update', wraps=sd.state_machine.update) as updates: + assert sd.retain_ordinary_source_state(sd.CS_prev, now) + assert updates.call_count == 0 + assert sd.state_machine.state == log.SelfdriveState.OpenpilotState.enabled, 'LATE_SOURCE_SINGLE_TRANSITION' + assert sd.state_machine.soft_disable_timer == 0 sd.update_alerts(sd.CS_prev) assert log.OnroadEvent.EventName.commIssue in sd.events.names assert not sd.aol_axis_decision.lateral_active and not sd.aol_axis_decision.longitudinal_active assert sd.AM.current_alert.alert_text_1 == 'TAKE CONTROL IMMEDIATELY' - assert sd.AM.current_alert.alert_status == log.SelfdriveState.AlertStatus.userPrompt + assert sd.AM.current_alert.alert_status == log.SelfdriveState.AlertStatus.userPrompt, 'LATE_SOURCE_WARNING_SEVERITY' assert sd.AM.current_alert.audible_alert == log.SelfdriveState.AudibleAlert.warningSoft assert sd.AM.current_alert.duration == int(2. / DT_CTRL) assert sd.AM.current_alert.alert_text_2 == 'Communication Issue Between Processes' + + +@pytest.mark.parametrize('timer', (40, 0)) +def test_started_or_expired_countdown_keeps_immediate_takeover(timer): + with ExitStack() as cleanup: + sd, _, _ = callers(cleanup) + now = sd.aol_car_state_log_ns + 30_000_001 + with patch('openpilot.selfdrive.selfdrived.selfdrived.time.monotonic_ns', return_value=now): + assert sd.retain_ordinary_source_state(sd.CS_prev, now) + sd.state_machine.state = log.SelfdriveState.OpenpilotState.softDisabling + sd.state_machine.soft_disable_timer = timer + sd.update_alerts(sd.CS_prev) + assert sd.state_machine.soft_disable_timer == timer + assert sd.AM.current_alert.alert_status == log.SelfdriveState.AlertStatus.critical + assert sd.AM.current_alert.audible_alert == log.SelfdriveState.AudibleAlert.warningImmediate diff --git a/openpilot/starpilot/aol/tests/test_ioniq_availability.py b/openpilot/starpilot/aol/tests/test_ioniq_availability.py index 34ec145c3e..030f7c5884 100644 --- a/openpilot/starpilot/aol/tests/test_ioniq_availability.py +++ b/openpilot/starpilot/aol/tests/test_ioniq_availability.py @@ -22,8 +22,12 @@ def car_state(*, brake=False): def assert_ordinary_source_pause(sd, *, publication_only=False): """Host loss withdraws commands while the existing state machine owns life.""" assert sd.enabled and sd.active - assert sd.state_machine.state == log.SelfdriveState.OpenpilotState.softDisabling - assert 0 < sd.state_machine.soft_disable_timer <= 300 + if publication_only: + assert sd.state_machine.state == log.SelfdriveState.OpenpilotState.enabled + assert sd.state_machine.soft_disable_timer == 0 + else: + assert sd.state_machine.state == log.SelfdriveState.OpenpilotState.softDisabling + assert 0 < sd.state_machine.soft_disable_timer <= 300 assert log.OnroadEvent.EventName.commIssue in sd.events.names assert log.OnroadEvent.EventName.controlsMismatch not in sd.events.names assert not sd.events.contains(ET.ENABLE) diff --git a/prebuilt.json b/prebuilt.json index c57a2acf4e..334a0421d6 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "9aace2884a3f72c6099f4dc83984ed6d3ec9178f4609d3588db782d800e9f8c7", "schema_version": 1} +{"protected_inventory_sha256": "83b21005d009cbd5a2c0711b8e9647874520d9553960cf99eb78a3ff34550c3e", "schema_version": 1}