Files
SDR-Rover/protocol/persistent_emergency.py
2026-08-05 13:12:49 +03:00

148 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Постоянное аварийное намерение наземной станции для 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