Files
SDR-Rover/protocol/packet.py

254 lines
6.5 KiB
Python

"""
Пакетный протокол SDR Rover Link.
Модуль содержит функции:
- build_packet() — формирование пакета;
- parse_packet() — проверка и разбор пакета.
Версия протокола: 0.1
"""
from dataclasses import dataclass
import struct
import zlib
# ============================================================
# Константы протокола
# ============================================================
SYNC_WORD = 0xAA55
PROTOCOL_VERSION = 1
MESSAGE_TYPE_TEXT = 1
MESSAGE_TYPE_TELEMETRY = 2
MESSAGE_TYPE_IMAGE_FRAGMENT = 3
MESSAGE_TYPE_ACK = 4
# Структура заголовка:
#
# H — SYNC, 2 байта
# B — версия, 1 байт
# B — тип сообщения, 1 байт
# H — порядковый номер, 2 байта
# H — длина полезной нагрузки, 2 байта
HEADER_FORMAT = ">HBBHH"
# CRC-32 хранится как беззнаковое 32-битное число.
CRC_FORMAT = ">I"
HEADER_SIZE = struct.calcsize(HEADER_FORMAT)
CRC_SIZE = struct.calcsize(CRC_FORMAT)
MAX_PAYLOAD_SIZE = 65535
# ============================================================
# Ошибки протокола
# ============================================================
class PacketError(ValueError):
"""Общая ошибка структуры пакета."""
class CRCError(PacketError):
"""Ошибка контрольной суммы CRC."""
# ============================================================
# Результат разбора пакета
# ============================================================
@dataclass(frozen=True)
class ParsedPacket:
"""
Представление успешно принятого пакета.
Атрибуты:
version: версия протокола;
message_type: тип сообщения;
sequence_number: порядковый номер;
payload: полезная нагрузка в байтах.
"""
version: int
message_type: int
sequence_number: int
payload: bytes
# ============================================================
# Формирование пакета
# ============================================================
def build_packet(
payload: bytes,
message_type: int,
sequence_number: int,
) -> bytes:
"""
Сформировать пакет SDR Rover Link.
Параметры:
payload:
Полезная нагрузка в байтах.
message_type:
Тип сообщения от 0 до 255.
sequence_number:
Порядковый номер от 0 до 65535.
Возвращает:
Полный пакет в виде bytes.
"""
if not isinstance(payload, (bytes, bytearray)):
raise TypeError(
"payload должен иметь тип bytes или bytearray"
)
payload = bytes(payload)
if not 0 <= message_type <= 255:
raise ValueError(
"message_type должен находиться в диапазоне 0...255"
)
if not 0 <= sequence_number <= 65535:
raise ValueError(
"sequence_number должен находиться в диапазоне 0...65535"
)
payload_length = len(payload)
if payload_length > MAX_PAYLOAD_SIZE:
raise ValueError(
f"Полезная нагрузка превышает {MAX_PAYLOAD_SIZE} байт"
)
header = struct.pack(
HEADER_FORMAT,
SYNC_WORD,
PROTOCOL_VERSION,
message_type,
sequence_number,
payload_length,
)
packet_without_crc = header + payload
crc_value = (
zlib.crc32(packet_without_crc) & 0xFFFFFFFF
)
crc_bytes = struct.pack(
CRC_FORMAT,
crc_value,
)
return packet_without_crc + crc_bytes
# ============================================================
# Разбор пакета
# ============================================================
def parse_packet(packet: bytes) -> ParsedPacket:
"""
Проверить и разобрать пакет SDR Rover Link.
Проверяются:
- минимальный размер;
- маркер SYNC;
- версия протокола;
- длина пакета;
- CRC-32.
Возвращает:
ParsedPacket.
Вызывает:
PacketError — при ошибке структуры;
CRCError — при несовпадении CRC.
"""
if not isinstance(packet, (bytes, bytearray)):
raise TypeError(
"packet должен иметь тип bytes или bytearray"
)
packet = bytes(packet)
minimum_packet_size = HEADER_SIZE + CRC_SIZE
if len(packet) < minimum_packet_size:
raise PacketError(
"Пакет слишком короткий"
)
header = packet[:HEADER_SIZE]
(
sync_word,
version,
message_type,
sequence_number,
payload_length,
) = struct.unpack(
HEADER_FORMAT,
header,
)
if sync_word != SYNC_WORD:
raise PacketError(
f"Неверный SYNC: 0x{sync_word:04X}"
)
if version != PROTOCOL_VERSION:
raise PacketError(
f"Неподдерживаемая версия протокола: {version}"
)
expected_packet_size = (
HEADER_SIZE
+ payload_length
+ CRC_SIZE
)
if len(packet) != expected_packet_size:
raise PacketError(
"Длина пакета не соответствует полю LENGTH: "
f"ожидалось {expected_packet_size} байт, "
f"получено {len(packet)} байт"
)
data_without_crc = packet[:-CRC_SIZE]
received_crc_bytes = packet[-CRC_SIZE:]
(received_crc,) = struct.unpack(
CRC_FORMAT,
received_crc_bytes,
)
calculated_crc = (
zlib.crc32(data_without_crc) & 0xFFFFFFFF
)
if received_crc != calculated_crc:
raise CRCError(
"CRC не совпадает: "
f"получено 0x{received_crc:08X}, "
f"вычислено 0x{calculated_crc:08X}"
)
payload = packet[
HEADER_SIZE:-CRC_SIZE
]
return ParsedPacket(
version=version,
message_type=message_type,
sequence_number=sequence_number,
payload=payload,
)