Lab040: add persistent emergency intent and safe reset
This commit is contained in:
192
protocol/safe_reset.py
Normal file
192
protocol/safe_reset.py
Normal file
@@ -0,0 +1,192 @@
|
||||
"""Команда безопасного аварийного сброса и её подтверждение для Lab040.
|
||||
|
||||
Оба прикладных блока данных передаются внутри неизменного пакета LinkPacket
|
||||
из Lab033.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
import struct
|
||||
|
||||
from protocol.link_packet import Direction, LinkPacket, TrafficClass
|
||||
from protocol.two_stage_failsafe import SafetyState, TwoStageFailsafe
|
||||
|
||||
|
||||
RESET_MAGIC = b"RST1"
|
||||
RESET_ACK_MAGIC = b"RAK1"
|
||||
RESET_FORMAT = "!4sIIfB"
|
||||
RESET_ACK_FORMAT = "!4sIIBB"
|
||||
RESET_SIZE = struct.calcsize(RESET_FORMAT)
|
||||
RESET_ACK_SIZE = struct.calcsize(RESET_ACK_FORMAT)
|
||||
STREAM_RESET = 6
|
||||
STREAM_RESET_ACK = 7
|
||||
|
||||
|
||||
class ResetReason(IntEnum):
|
||||
ACCEPTED = 0
|
||||
NOT_LATCHED = 1
|
||||
MOVING = 2
|
||||
STALE_LINK = 3
|
||||
EVENT_MISMATCH = 4
|
||||
OLD_SEQUENCE = 5
|
||||
NONZERO_REQUEST = 6
|
||||
MOVEMENT_PERMITTED = 7
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResetCommand:
|
||||
emergency_event_id: int
|
||||
reset_request_id: int
|
||||
requested_speed_mps: float
|
||||
movement_permitted: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResetAcknowledgement:
|
||||
emergency_event_id: int
|
||||
reset_request_id: int
|
||||
accepted: bool
|
||||
reason: ResetReason
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResetDecision:
|
||||
acknowledgement: ResetAcknowledgement
|
||||
performed: bool
|
||||
duplicate: bool
|
||||
|
||||
|
||||
def encode_reset(command: ResetCommand) -> bytes:
|
||||
if command.emergency_event_id < 0 or command.reset_request_id < 0:
|
||||
raise ValueError("идентификаторы сброса не могут быть отрицательными")
|
||||
return struct.pack(
|
||||
RESET_FORMAT,
|
||||
RESET_MAGIC,
|
||||
command.emergency_event_id,
|
||||
command.reset_request_id,
|
||||
float(command.requested_speed_mps),
|
||||
int(bool(command.movement_permitted)),
|
||||
)
|
||||
|
||||
|
||||
def decode_reset(payload: bytes) -> ResetCommand:
|
||||
if len(payload) != RESET_SIZE:
|
||||
raise ValueError("неверная длина данных запроса сброса")
|
||||
magic, event_id, request_id, speed, permitted = struct.unpack(RESET_FORMAT, payload)
|
||||
if magic != RESET_MAGIC or permitted not in (0, 1):
|
||||
raise ValueError("неверные данные запроса сброса")
|
||||
return ResetCommand(event_id, request_id, speed, bool(permitted))
|
||||
|
||||
|
||||
def encode_reset_ack(ack: ResetAcknowledgement) -> bytes:
|
||||
return struct.pack(
|
||||
RESET_ACK_FORMAT,
|
||||
RESET_ACK_MAGIC,
|
||||
ack.emergency_event_id,
|
||||
ack.reset_request_id,
|
||||
int(ack.accepted),
|
||||
int(ack.reason),
|
||||
)
|
||||
|
||||
|
||||
def decode_reset_ack(payload: bytes) -> ResetAcknowledgement:
|
||||
if len(payload) != RESET_ACK_SIZE:
|
||||
raise ValueError("неверная длина подтверждения сброса")
|
||||
magic, event_id, request_id, accepted, reason = struct.unpack(RESET_ACK_FORMAT, payload)
|
||||
if magic != RESET_ACK_MAGIC or accepted not in (0, 1):
|
||||
raise ValueError("неверное подтверждение сброса")
|
||||
return ResetAcknowledgement(event_id, request_id, bool(accepted), ResetReason(reason))
|
||||
|
||||
|
||||
def build_reset_packet(
|
||||
command: ResetCommand,
|
||||
sequence_number: int,
|
||||
generation_time_us: int,
|
||||
) -> LinkPacket:
|
||||
return LinkPacket(
|
||||
traffic_class=TrafficClass.EMERGENCY,
|
||||
direction=Direction.GROUND_TO_ROVER,
|
||||
stream_id=STREAM_RESET,
|
||||
sequence_number=sequence_number,
|
||||
generation_time_us=generation_time_us,
|
||||
deadline_ms=50,
|
||||
payload=encode_reset(command),
|
||||
)
|
||||
|
||||
|
||||
def build_reset_ack(
|
||||
decision: ResetDecision,
|
||||
sequence_number: int,
|
||||
generation_time_us: int,
|
||||
) -> LinkPacket:
|
||||
return LinkPacket(
|
||||
traffic_class=TrafficClass.EMERGENCY,
|
||||
direction=Direction.ROVER_TO_GROUND,
|
||||
stream_id=STREAM_RESET_ACK,
|
||||
sequence_number=sequence_number,
|
||||
generation_time_us=generation_time_us,
|
||||
deadline_ms=50,
|
||||
payload=encode_reset_ack(decision.acknowledgement),
|
||||
)
|
||||
|
||||
|
||||
class SafeResetReceiver:
|
||||
"""Проверяет условия сброса и повторно подтверждает принятые дубликаты."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.last_sequence = -1
|
||||
self.accepted_keys: set[tuple[int, int, int]] = set()
|
||||
self.accepted = 0
|
||||
self.rejected = 0
|
||||
self.duplicates = 0
|
||||
self.reacknowledgements = 0
|
||||
|
||||
def process(
|
||||
self,
|
||||
packet: LinkPacket,
|
||||
*,
|
||||
rover: TwoStageFailsafe,
|
||||
actual_speed_mps: float,
|
||||
last_link_age_seconds: float,
|
||||
emergency_event_id: int | None,
|
||||
) -> ResetDecision:
|
||||
if packet.traffic_class is not TrafficClass.EMERGENCY:
|
||||
raise ValueError("запрос сброса должен использовать аварийный класс трафика")
|
||||
if packet.direction is not Direction.GROUND_TO_ROVER or packet.stream_id != STREAM_RESET:
|
||||
raise ValueError("неверная оболочка запроса сброса")
|
||||
command = decode_reset(packet.payload)
|
||||
key = command.emergency_event_id, command.reset_request_id, packet.sequence_number
|
||||
if key in self.accepted_keys:
|
||||
self.duplicates += 1
|
||||
self.reacknowledgements += 1
|
||||
ack = ResetAcknowledgement(command.emergency_event_id, command.reset_request_id, True, ResetReason.ACCEPTED)
|
||||
return ResetDecision(ack, False, True)
|
||||
|
||||
reason = ResetReason.ACCEPTED
|
||||
if rover.state is not SafetyState.EMERGENCY_LATCHED:
|
||||
reason = ResetReason.NOT_LATCHED
|
||||
elif actual_speed_mps > 1e-9:
|
||||
reason = ResetReason.MOVING
|
||||
elif last_link_age_seconds > 0.250 + 1e-12:
|
||||
reason = ResetReason.STALE_LINK
|
||||
elif command.emergency_event_id != emergency_event_id:
|
||||
reason = ResetReason.EVENT_MISMATCH
|
||||
elif packet.sequence_number < self.last_sequence:
|
||||
reason = ResetReason.OLD_SEQUENCE
|
||||
elif abs(command.requested_speed_mps) > 1e-9:
|
||||
reason = ResetReason.NONZERO_REQUEST
|
||||
elif command.movement_permitted:
|
||||
reason = ResetReason.MOVEMENT_PERMITTED
|
||||
|
||||
self.last_sequence = max(self.last_sequence, packet.sequence_number)
|
||||
accepted = reason is ResetReason.ACCEPTED
|
||||
if accepted:
|
||||
self.accepted_keys.add(key)
|
||||
self.accepted += 1
|
||||
rover.state = SafetyState.NORMAL
|
||||
else:
|
||||
self.rejected += 1
|
||||
ack = ResetAcknowledgement(command.emergency_event_id, command.reset_request_id, accepted, reason)
|
||||
return ResetDecision(ack, accepted, False)
|
||||
Reference in New Issue
Block a user