Lab040: add persistent emergency intent and safe reset
This commit is contained in:
147
protocol/persistent_emergency.py
Normal file
147
protocol/persistent_emergency.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""Постоянное аварийное намерение наземной станции для Lab040.
|
||||
|
||||
Автомат состояний намеренно отделяет подтверждение аварийного пакета от
|
||||
снятия намерения оператора. Только принятое подтверждение сброса разрешает
|
||||
сформировать новую команду движения.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from protocol.control_failsafe import ControlState, SAFE_STATE
|
||||
from protocol.emergency_ack import EmergencyIdentity
|
||||
|
||||
|
||||
class GroundState(str, Enum):
|
||||
GROUND_NORMAL = "ground_normal"
|
||||
GROUND_EMERGENCY_REQUESTED = "ground_emergency_requested"
|
||||
GROUND_EMERGENCY_CONFIRMED = "ground_emergency_confirmed"
|
||||
GROUND_RESET_REQUESTED = "ground_reset_requested"
|
||||
GROUND_MOVEMENT_REAUTHORIZED = "ground_movement_reauthorized"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroundSnapshot:
|
||||
state: GroundState
|
||||
emergency_intent: bool
|
||||
movement_permitted: bool
|
||||
emergency_acknowledged: bool
|
||||
reset_acknowledged: bool
|
||||
|
||||
|
||||
class PersistentEmergencyController:
|
||||
"""Автомат наземной станции, не снимающий намерение по подтверждению аварии."""
|
||||
|
||||
def __init__(self, initial_speed_mps: float) -> None:
|
||||
if initial_speed_mps < 0.0:
|
||||
raise ValueError("начальная скорость не может быть отрицательной")
|
||||
self.state = GroundState.GROUND_NORMAL
|
||||
self.emergency_intent = False
|
||||
self.emergency_event_id: int | None = None
|
||||
self.emergency_stream_id: int | None = None
|
||||
self.emergency_sequence: int | None = None
|
||||
self.emergency_acknowledged = False
|
||||
self.reset_request_id: int | None = None
|
||||
self.reset_sequence: int | None = None
|
||||
self.reset_acknowledged = False
|
||||
self.current_motion_speed_mps = initial_speed_mps
|
||||
self.last_operator_sequence = 0
|
||||
self._sequence_at_emergency = -1
|
||||
|
||||
@property
|
||||
def snapshot(self) -> GroundSnapshot:
|
||||
control = self.control_state()
|
||||
return GroundSnapshot(
|
||||
self.state,
|
||||
self.emergency_intent,
|
||||
control.movement_allowed,
|
||||
self.emergency_acknowledged,
|
||||
self.reset_acknowledged,
|
||||
)
|
||||
|
||||
def request_emergency(
|
||||
self,
|
||||
event_id: int,
|
||||
stream_id: int,
|
||||
sequence_number: int,
|
||||
) -> None:
|
||||
if self.state is not GroundState.GROUND_NORMAL:
|
||||
raise RuntimeError("аварийную команду можно запросить только из нормального состояния")
|
||||
if event_id < 0:
|
||||
raise ValueError("идентификатор события не может быть отрицательным")
|
||||
self.emergency_event_id = event_id
|
||||
self.emergency_stream_id = stream_id
|
||||
self.emergency_sequence = sequence_number
|
||||
self.emergency_intent = True
|
||||
self.emergency_acknowledged = False
|
||||
self.reset_acknowledged = False
|
||||
self._sequence_at_emergency = self.last_operator_sequence
|
||||
self.state = GroundState.GROUND_EMERGENCY_REQUESTED
|
||||
|
||||
def receive_emergency_ack(self, identity: EmergencyIdentity) -> bool:
|
||||
expected = EmergencyIdentity(
|
||||
self.emergency_stream_id if self.emergency_stream_id is not None else -1,
|
||||
self.emergency_sequence if self.emergency_sequence is not None else -1,
|
||||
)
|
||||
if identity != expected:
|
||||
return False
|
||||
first = not self.emergency_acknowledged
|
||||
self.emergency_acknowledged = True
|
||||
if self.state is GroundState.GROUND_EMERGENCY_REQUESTED:
|
||||
self.state = GroundState.GROUND_EMERGENCY_CONFIRMED
|
||||
return first
|
||||
|
||||
def request_reset(self, reset_request_id: int, sequence_number: int) -> None:
|
||||
if not self.emergency_intent:
|
||||
raise RuntimeError("для сброса требуется постоянное аварийное намерение")
|
||||
if self.state not in (
|
||||
GroundState.GROUND_EMERGENCY_REQUESTED,
|
||||
GroundState.GROUND_EMERGENCY_CONFIRMED,
|
||||
):
|
||||
raise RuntimeError("сброс уже ожидается или завершён")
|
||||
if reset_request_id < 0 or sequence_number < 0:
|
||||
raise ValueError("идентификаторы сброса не могут быть отрицательными")
|
||||
self.reset_request_id = reset_request_id
|
||||
self.reset_sequence = sequence_number
|
||||
self.reset_acknowledged = False
|
||||
self.state = GroundState.GROUND_RESET_REQUESTED
|
||||
|
||||
def receive_reset_ack(
|
||||
self,
|
||||
event_id: int,
|
||||
reset_request_id: int,
|
||||
accepted: bool,
|
||||
) -> bool:
|
||||
if self.state is not GroundState.GROUND_RESET_REQUESTED:
|
||||
return False
|
||||
if event_id != self.emergency_event_id or reset_request_id != self.reset_request_id:
|
||||
return False
|
||||
if not accepted:
|
||||
return False
|
||||
self.reset_acknowledged = True
|
||||
self.emergency_intent = False
|
||||
self.current_motion_speed_mps = 0.0
|
||||
self.state = GroundState.GROUND_MOVEMENT_REAUTHORIZED
|
||||
return True
|
||||
|
||||
def control_state(self) -> ControlState:
|
||||
if self.state is GroundState.GROUND_NORMAL:
|
||||
return ControlState(self.current_motion_speed_mps, 0.0, False, True)
|
||||
return SAFE_STATE
|
||||
|
||||
def new_operator_motion(self, sequence_number: int, speed_mps: float) -> ControlState:
|
||||
if self.state is not GroundState.GROUND_MOVEMENT_REAUTHORIZED:
|
||||
raise RuntimeError("повторное движение не разрешено")
|
||||
if sequence_number <= self._sequence_at_emergency or sequence_number <= self.last_operator_sequence:
|
||||
raise ValueError("требуется новый номер последовательности команды оператора")
|
||||
if speed_mps <= 0.0:
|
||||
raise ValueError("новая скорость движения должна быть положительной")
|
||||
self.last_operator_sequence = sequence_number
|
||||
self.current_motion_speed_mps = speed_mps
|
||||
self.state = GroundState.GROUND_NORMAL
|
||||
return self.control_state()
|
||||
|
||||
def reject_old_operator_motion(self, sequence_number: int) -> bool:
|
||||
return sequence_number <= self._sequence_at_emergency
|
||||
Reference in New Issue
Block a user