196 lines
5.8 KiB
Python
196 lines
5.8 KiB
Python
"""Common Lab033 link packet shared by commands, telemetry, and video.
|
|
|
|
The fixed 32-byte header uses network byte order and no implicit padding::
|
|
|
|
!4sBBBBHIQHII
|
|
|
|
Offset Size Field
|
|
0 4 magic (b"SLP1")
|
|
4 1 version
|
|
5 1 traffic_class
|
|
6 1 direction
|
|
7 1 flags
|
|
8 2 stream_id
|
|
10 4 sequence_number
|
|
14 8 generation_time_us
|
|
22 2 deadline_ms
|
|
24 4 payload_length
|
|
28 4 packet_crc32
|
|
|
|
``packet_crc32`` is IEEE CRC32 over the complete header with that field set
|
|
to zero, followed by the complete payload.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, replace
|
|
from enum import IntEnum
|
|
import struct
|
|
import zlib
|
|
|
|
|
|
MAGIC = b"SLP1"
|
|
VERSION = 1
|
|
HEADER_FORMAT = "!4sBBBBHIQHII"
|
|
HEADER_SIZE = struct.calcsize(HEADER_FORMAT)
|
|
SUPPORTED_FLAGS_MASK = 0
|
|
|
|
|
|
class TrafficClass(IntEnum):
|
|
"""Traffic classes in descending scheduling priority."""
|
|
|
|
EMERGENCY = 1
|
|
CONTROL = 2
|
|
TELEMETRY = 3
|
|
VIDEO = 4
|
|
|
|
|
|
class Direction(IntEnum):
|
|
"""Logical direction of a packet on the shared modelled resource."""
|
|
|
|
GROUND_TO_ROVER = 1
|
|
ROVER_TO_GROUND = 2
|
|
|
|
|
|
class LinkPacketError(ValueError):
|
|
"""Base class for malformed Lab033 link packets."""
|
|
|
|
|
|
class LinkPacketCRCError(LinkPacketError):
|
|
"""The Lab033 packet header or payload failed CRC32 validation."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LinkPacket:
|
|
"""Decoded common packet, including its CRC-validated payload."""
|
|
|
|
traffic_class: TrafficClass
|
|
direction: Direction
|
|
stream_id: int
|
|
sequence_number: int
|
|
generation_time_us: int
|
|
deadline_ms: int
|
|
payload: bytes
|
|
flags: int = 0
|
|
version: int = VERSION
|
|
packet_crc32: int = 0
|
|
|
|
|
|
def crc32(data: bytes) -> int:
|
|
"""Return unsigned IEEE CRC32."""
|
|
|
|
return zlib.crc32(data) & 0xFFFFFFFF
|
|
|
|
|
|
def _validated(packet: LinkPacket) -> LinkPacket:
|
|
if not isinstance(packet, LinkPacket):
|
|
raise TypeError("packet must be LinkPacket")
|
|
try:
|
|
traffic_class = TrafficClass(packet.traffic_class)
|
|
direction = Direction(packet.direction)
|
|
except ValueError as error:
|
|
raise LinkPacketError("unsupported enumeration value") from error
|
|
if packet.version != VERSION:
|
|
raise LinkPacketError("unsupported link packet version")
|
|
if packet.flags & ~SUPPORTED_FLAGS_MASK:
|
|
raise LinkPacketError("unsupported link packet flags")
|
|
if not 0 <= packet.stream_id <= 0xFFFF:
|
|
raise LinkPacketError("stream_id is outside uint16")
|
|
if not 0 <= packet.sequence_number <= 0xFFFFFFFF:
|
|
raise LinkPacketError("sequence_number is outside uint32")
|
|
if not 0 <= packet.generation_time_us <= 0xFFFFFFFFFFFFFFFF:
|
|
raise LinkPacketError("generation_time_us is outside uint64")
|
|
if not 0 <= packet.deadline_ms <= 0xFFFF:
|
|
raise LinkPacketError("deadline_ms is outside uint16")
|
|
if not isinstance(packet.payload, (bytes, bytearray)):
|
|
raise TypeError("payload must be bytes or bytearray")
|
|
payload = bytes(packet.payload)
|
|
if len(payload) > 0xFFFFFFFF:
|
|
raise LinkPacketError("payload is outside uint32 length")
|
|
return replace(
|
|
packet,
|
|
traffic_class=traffic_class,
|
|
direction=direction,
|
|
payload=payload,
|
|
)
|
|
|
|
|
|
def _pack_header(packet: LinkPacket, crc_value: int) -> bytes:
|
|
return struct.pack(
|
|
HEADER_FORMAT,
|
|
MAGIC,
|
|
packet.version,
|
|
int(packet.traffic_class),
|
|
int(packet.direction),
|
|
packet.flags,
|
|
packet.stream_id,
|
|
packet.sequence_number,
|
|
packet.generation_time_us,
|
|
packet.deadline_ms,
|
|
len(packet.payload),
|
|
crc_value,
|
|
)
|
|
|
|
|
|
def encode_link_packet(packet: LinkPacket) -> bytes:
|
|
"""Serialize a common packet and calculate its CRC32."""
|
|
|
|
packet = _validated(packet)
|
|
header_without_crc = _pack_header(packet, 0)
|
|
packet_crc32 = crc32(header_without_crc + packet.payload)
|
|
return _pack_header(packet, packet_crc32) + packet.payload
|
|
|
|
|
|
def decode_link_packet(wire_packet: bytes) -> LinkPacket:
|
|
"""Deserialize a common packet and validate its format and CRC32."""
|
|
|
|
if not isinstance(wire_packet, (bytes, bytearray)):
|
|
raise TypeError("wire_packet must be bytes or bytearray")
|
|
wire_packet = bytes(wire_packet)
|
|
if len(wire_packet) < HEADER_SIZE:
|
|
raise LinkPacketError("link packet is shorter than its header")
|
|
(
|
|
magic,
|
|
version,
|
|
traffic_class,
|
|
direction,
|
|
flags,
|
|
stream_id,
|
|
sequence_number,
|
|
generation_time_us,
|
|
deadline_ms,
|
|
payload_length,
|
|
received_crc32,
|
|
) = struct.unpack(HEADER_FORMAT, wire_packet[:HEADER_SIZE])
|
|
if magic != MAGIC:
|
|
raise LinkPacketError("invalid link packet magic")
|
|
if len(wire_packet) != HEADER_SIZE + payload_length:
|
|
raise LinkPacketError("payload_length does not match packet length")
|
|
try:
|
|
packet = LinkPacket(
|
|
traffic_class=TrafficClass(traffic_class),
|
|
direction=Direction(direction),
|
|
stream_id=stream_id,
|
|
sequence_number=sequence_number,
|
|
generation_time_us=generation_time_us,
|
|
deadline_ms=deadline_ms,
|
|
payload=wire_packet[HEADER_SIZE:],
|
|
flags=flags,
|
|
version=version,
|
|
packet_crc32=received_crc32,
|
|
)
|
|
except ValueError as error:
|
|
raise LinkPacketError("unsupported enumeration value") from error
|
|
packet = _validated(packet)
|
|
calculated_crc32 = crc32(_pack_header(packet, 0) + packet.payload)
|
|
if calculated_crc32 != received_crc32:
|
|
raise LinkPacketCRCError(
|
|
"link packet CRC mismatch: "
|
|
f"received 0x{received_crc32:08X}, "
|
|
f"calculated 0x{calculated_crc32:08X}"
|
|
)
|
|
return packet
|
|
|
|
|
|
assert HEADER_SIZE == 32
|