148 lines
6.4 KiB
Python
148 lines
6.4 KiB
Python
"""Постоянное аварийное намерение наземной станции для 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
|