"""Emergency-command acknowledgement carried by unchanged Lab033 packets.""" from __future__ import annotations from dataclasses import dataclass import struct from protocol.link_packet import Direction, LinkPacket, TrafficClass ACK_MAGIC = b"ACK1" ACK_PAYLOAD_FORMAT = "!4sHI" ACK_PAYLOAD_SIZE = struct.calcsize(ACK_PAYLOAD_FORMAT) STREAM_EMERGENCY_ACK = 5 class EmergencyAckError(ValueError): """An emergency acknowledgement is malformed.""" @dataclass(frozen=True) class EmergencyIdentity: stream_id: int sequence_number: int def encode_ack_payload(identity: EmergencyIdentity) -> bytes: if not 0 <= identity.stream_id <= 0xFFFF: raise EmergencyAckError("emergency stream_id is outside uint16") if not 0 <= identity.sequence_number <= 0xFFFFFFFF: raise EmergencyAckError("emergency sequence_number is outside uint32") return struct.pack(ACK_PAYLOAD_FORMAT, ACK_MAGIC, identity.stream_id, identity.sequence_number) def decode_ack_payload(payload: bytes) -> EmergencyIdentity: if not isinstance(payload, (bytes, bytearray)): raise TypeError("payload must be bytes or bytearray") if len(payload) != ACK_PAYLOAD_SIZE: raise EmergencyAckError("invalid acknowledgement payload length") magic, stream_id, sequence_number = struct.unpack(ACK_PAYLOAD_FORMAT, bytes(payload)) if magic != ACK_MAGIC: raise EmergencyAckError("invalid acknowledgement payload magic") return EmergencyIdentity(stream_id, sequence_number) def build_emergency_ack(command: LinkPacket, ack_sequence: int, generation_time_us: int) -> LinkPacket: """Build a high-priority rover-to-ground ACK for one emergency command.""" if command.traffic_class is not TrafficClass.EMERGENCY: raise ValueError("acknowledged packet must be an emergency command") return LinkPacket( traffic_class=TrafficClass.EMERGENCY, direction=Direction.ROVER_TO_GROUND, stream_id=STREAM_EMERGENCY_ACK, sequence_number=ack_sequence, generation_time_us=generation_time_us, deadline_ms=50, payload=encode_ack_payload(EmergencyIdentity(command.stream_id, command.sequence_number)), ) def acknowledged_identity(packet: LinkPacket) -> EmergencyIdentity: """Validate ACK envelope fields and return the referenced command.""" if packet.traffic_class is not TrafficClass.EMERGENCY: raise EmergencyAckError("ack must use high-priority traffic class") if packet.direction is not Direction.ROVER_TO_GROUND: raise EmergencyAckError("ack must travel rover to ground") if packet.stream_id != STREAM_EMERGENCY_ACK: raise EmergencyAckError("unexpected acknowledgement stream") return decode_ack_payload(packet.payload) class EmergencyAckReceiver: """Recognize first and late acknowledgements independently per command.""" def __init__(self) -> None: self._received: set[EmergencyIdentity] = set() self.unique = 0 self.duplicates = 0 def accept(self, packet: LinkPacket) -> bool: identity = acknowledged_identity(packet) if identity in self._received: self.duplicates += 1 return False self._received.add(identity) self.unique += 1 return True def has_received(self, identity: EmergencyIdentity) -> bool: return identity in self._received