initial commit

This commit is contained in:
LittleSam129
2026-07-20 13:58:58 +03:00
commit acf7a76546
104 changed files with 27464 additions and 0 deletions

View File

View File

@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
"""
Created on Fri Jul 10 17:11:41 2026
@author: user
"""

View File

@@ -0,0 +1,354 @@
"""
Фрагментация и сборка изображений SDR Rover Link.
Изображение рассматривается как обычная последовательность байтов.
Модулю неважно, используется JPEG, PNG или другой формат.
"""
from dataclasses import dataclass
import struct
from typing import Iterable
from protocol.packet import MAX_PAYLOAD_SIZE
# ============================================================
# Формат заголовка фрагмента изображения
# ============================================================
# I — идентификатор изображения, 4 байта
# H — номер фрагмента, 2 байта
# H — общее количество фрагментов, 2 байта
# I — размер исходного изображения, 4 байта
FRAGMENT_HEADER_FORMAT = ">IHHI"
FRAGMENT_HEADER_SIZE = struct.calcsize(
FRAGMENT_HEADER_FORMAT
)
MAX_FRAGMENT_DATA_SIZE = (
MAX_PAYLOAD_SIZE - FRAGMENT_HEADER_SIZE
)
# ============================================================
# Ошибки
# ============================================================
class ImageFragmentError(ValueError):
"""Ошибка структуры фрагмента изображения."""
class MissingFragmentsError(ImageFragmentError):
"""Отсутствуют необходимые фрагменты изображения."""
# ============================================================
# Представление одного фрагмента
# ============================================================
@dataclass(frozen=True)
class ImageFragment:
"""
Один фрагмент изображения.
Атрибуты
--------
image_id:
Идентификатор изображения.
fragment_index:
Номер фрагмента. Нумерация начинается с нуля.
fragment_count:
Общее количество фрагментов.
original_size:
Размер исходного изображения в байтах.
data:
Часть исходного изображения.
"""
image_id: int
fragment_index: int
fragment_count: int
original_size: int
data: bytes
# ============================================================
# Разбиение изображения
# ============================================================
def split_image_bytes(
image_bytes: bytes,
image_id: int,
fragment_data_size: int = 512,
) -> list[ImageFragment]:
"""
Разбить изображение на фрагменты.
Параметры
--------
image_bytes:
Полное содержимое файла изображения.
image_id:
Идентификатор изображения от 0 до 4294967295.
fragment_data_size:
Максимальный размер данных одного фрагмента.
Возвращает
----------
Список объектов ImageFragment.
"""
if not isinstance(image_bytes, (bytes, bytearray)):
raise TypeError(
"image_bytes должен иметь тип bytes или bytearray"
)
image_bytes = bytes(image_bytes)
if not image_bytes:
raise ValueError(
"Изображение не должно быть пустым"
)
if not 0 <= image_id <= 0xFFFFFFFF:
raise ValueError(
"image_id должен находиться "
"в диапазоне 0...4294967295"
)
if not 1 <= fragment_data_size <= MAX_FRAGMENT_DATA_SIZE:
raise ValueError(
"Некорректный размер фрагмента"
)
fragment_count = (
len(image_bytes)
+ fragment_data_size
- 1
) // fragment_data_size
if fragment_count > 65535:
raise ValueError(
"Изображение требует более 65535 фрагментов"
)
fragments = []
for fragment_index in range(fragment_count):
start = fragment_index * fragment_data_size
end = start + fragment_data_size
fragment_data = image_bytes[start:end]
fragments.append(
ImageFragment(
image_id=image_id,
fragment_index=fragment_index,
fragment_count=fragment_count,
original_size=len(image_bytes),
data=fragment_data,
)
)
return fragments
# ============================================================
# Преобразование фрагмента в байты
# ============================================================
def encode_image_fragment(
fragment: ImageFragment,
) -> bytes:
"""
Преобразовать ImageFragment в полезную нагрузку пакета.
"""
if not isinstance(fragment, ImageFragment):
raise TypeError(
"fragment должен иметь тип ImageFragment"
)
if not 0 <= fragment.fragment_index < fragment.fragment_count:
raise ImageFragmentError(
"Некорректный номер фрагмента"
)
if not fragment.data:
raise ImageFragmentError(
"Фрагмент не должен быть пустым"
)
if len(fragment.data) > MAX_FRAGMENT_DATA_SIZE:
raise ImageFragmentError(
"Данные фрагмента слишком велики"
)
header = struct.pack(
FRAGMENT_HEADER_FORMAT,
fragment.image_id,
fragment.fragment_index,
fragment.fragment_count,
fragment.original_size,
)
return header + fragment.data
# ============================================================
# Преобразование байтов обратно во фрагмент
# ============================================================
def decode_image_fragment(
payload: bytes,
) -> ImageFragment:
"""
Разобрать полезную нагрузку пакета изображения.
"""
if not isinstance(payload, (bytes, bytearray)):
raise TypeError(
"payload должен иметь тип bytes или bytearray"
)
payload = bytes(payload)
if len(payload) <= FRAGMENT_HEADER_SIZE:
raise ImageFragmentError(
"Фрагмент слишком короткий"
)
header = payload[:FRAGMENT_HEADER_SIZE]
fragment_data = payload[FRAGMENT_HEADER_SIZE:]
(
image_id,
fragment_index,
fragment_count,
original_size,
) = struct.unpack(
FRAGMENT_HEADER_FORMAT,
header,
)
if fragment_count == 0:
raise ImageFragmentError(
"Количество фрагментов не может быть равно нулю"
)
if fragment_index >= fragment_count:
raise ImageFragmentError(
"Номер фрагмента выходит за допустимый диапазон"
)
if original_size == 0:
raise ImageFragmentError(
"Размер исходного изображения равен нулю"
)
return ImageFragment(
image_id=image_id,
fragment_index=fragment_index,
fragment_count=fragment_count,
original_size=original_size,
data=fragment_data,
)
# ============================================================
# Сборка изображения
# ============================================================
def reassemble_image(
fragments: Iterable[ImageFragment],
) -> bytes:
"""
Собрать исходное изображение из фрагментов.
Фрагменты могут поступать в произвольном порядке.
"""
fragments = list(fragments)
if not fragments:
raise MissingFragmentsError(
"Список фрагментов пуст"
)
first_fragment = fragments[0]
image_id = first_fragment.image_id
fragment_count = first_fragment.fragment_count
original_size = first_fragment.original_size
fragments_by_index = {}
for fragment in fragments:
if fragment.image_id != image_id:
raise ImageFragmentError(
"Фрагменты относятся к разным изображениям"
)
if fragment.fragment_count != fragment_count:
raise ImageFragmentError(
"Различается общее количество фрагментов"
)
if fragment.original_size != original_size:
raise ImageFragmentError(
"Различается размер исходного изображения"
)
existing_fragment = fragments_by_index.get(
fragment.fragment_index
)
if existing_fragment is not None:
if existing_fragment.data != fragment.data:
raise ImageFragmentError(
"Получены разные данные "
"для одного номера фрагмента"
)
# Полностью одинаковый повторный фрагмент
# можно безопасно проигнорировать.
continue
fragments_by_index[
fragment.fragment_index
] = fragment
missing_indexes = [
index
for index in range(fragment_count)
if index not in fragments_by_index
]
if missing_indexes:
raise MissingFragmentsError(
"Отсутствуют фрагменты: "
+ ", ".join(map(str, missing_indexes))
)
assembled_bytes = b"".join(
fragments_by_index[index].data
for index in range(fragment_count)
)
if len(assembled_bytes) != original_size:
raise ImageFragmentError(
"Размер собранного изображения "
"не совпадает с исходным размером"
)
return assembled_bytes

View File

@@ -0,0 +1,382 @@
"""
Адаптация параметров SDR Rover Link к качеству канала.
Модуль оценивает несколько размеров фрагмента изображения
и выбирает режим с максимальной полезной скоростью.
Используемая модель:
- когерентная BPSK;
- канал AWGN;
- независимые битовые ошибки;
- CRC отбрасывает повреждённые пакеты;
- Stop-and-Wait ARQ;
- одинаковое Eb/N0 для DATA и ACK.
"""
from dataclasses import dataclass
from math import (
erfc,
exp,
log1p,
sqrt,
)
from protocol.image_fragments import (
FRAGMENT_HEADER_SIZE,
)
from protocol.packet import (
CRC_SIZE,
HEADER_SIZE,
)
# ============================================================
# Результат оценки одного размера фрагмента
# ============================================================
@dataclass(frozen=True)
class FragmentModeEstimate:
"""
Оценка одного возможного размера фрагмента.
"""
fragment_size: int
ber: float
data_packet_size_bytes: int
ack_packet_size_bytes: int
data_success_probability: float
ack_success_probability: float
confirmed_attempt_probability: float
expected_attempts: float
success_probability_with_retries: float
expected_goodput_bps: float
# ============================================================
# Итоговое решение адаптера
# ============================================================
@dataclass(frozen=True)
class LinkDecision:
"""
Решение о режиме передачи изображения.
"""
eb_n0_db: float
images_enabled: bool
selected_fragment_size: int | None
reason: str
estimates: tuple[FragmentModeEstimate, ...]
# ============================================================
# Теоретический BER
# ============================================================
def theoretical_bpsk_ber(
eb_n0_db: float,
) -> float:
"""
Теоретический BER когерентной BPSK в AWGN.
BER = 0.5 * erfc(sqrt(Eb/N0))
"""
eb_n0_linear = 10.0 ** (
eb_n0_db / 10.0
)
return 0.5 * erfc(
sqrt(eb_n0_linear)
)
# ============================================================
# Вероятность передачи пакета без ошибок
# ============================================================
def packet_success_probability(
ber: float,
packet_bit_count: int,
) -> float:
"""
Рассчитать вероятность того, что пакет не содержит
ни одной битовой ошибки.
P = (1 - BER) ** N
"""
if not 0.0 <= ber <= 1.0:
raise ValueError(
"BER должен находиться в диапазоне 0...1"
)
if packet_bit_count <= 0:
raise ValueError(
"packet_bit_count должен быть положительным"
)
if ber == 0.0:
return 1.0
if ber == 1.0:
return 0.0
return exp(
packet_bit_count
* log1p(-ber)
)
# ============================================================
# Оценка отдельного режима
# ============================================================
def evaluate_fragment_mode(
eb_n0_db: float,
fragment_size: int,
channel_bitrate_bps: int,
max_attempts: int,
) -> FragmentModeEstimate:
"""
Оценить передачу полного фрагмента заданного размера.
"""
if fragment_size <= 0:
raise ValueError(
"fragment_size должен быть положительным"
)
if channel_bitrate_bps <= 0:
raise ValueError(
"channel_bitrate_bps должен быть положительным"
)
if max_attempts <= 0:
raise ValueError(
"max_attempts должен быть положительным"
)
ber = theoretical_bpsk_ber(
eb_n0_db
)
# DATA содержит:
#
# заголовок протокола;
# заголовок фрагмента изображения;
# данные JPEG;
# CRC-32.
data_packet_size_bytes = (
HEADER_SIZE
+ FRAGMENT_HEADER_SIZE
+ fragment_size
+ CRC_SIZE
)
# ACK содержит только заголовок протокола и CRC.
ack_packet_size_bytes = (
HEADER_SIZE
+ CRC_SIZE
)
data_success_probability = (
packet_success_probability(
ber=ber,
packet_bit_count=(
data_packet_size_bytes * 8
),
)
)
ack_success_probability = (
packet_success_probability(
ber=ber,
packet_bit_count=(
ack_packet_size_bytes * 8
),
)
)
# Одна попытка считается завершённой,
# только если правильно прошли DATA и ACK.
confirmed_attempt_probability = (
data_success_probability
* ack_success_probability
)
if confirmed_attempt_probability == 0.0:
expected_attempts = float("inf")
expected_goodput_bps = 0.0
else:
expected_attempts = (
1.0
/ confirmed_attempt_probability
)
# Ожидаемый объём DATA до подтверждения.
expected_data_bytes = (
data_packet_size_bytes
/ confirmed_attempt_probability
)
# ACK отправляется только после исправного DATA.
expected_ack_bytes = (
ack_packet_size_bytes
/ ack_success_probability
)
expected_total_bytes = (
expected_data_bytes
+ expected_ack_bytes
)
expected_goodput_bps = (
channel_bitrate_bps
* fragment_size
/ expected_total_bytes
)
success_probability_with_retries = (
1.0
- (
1.0
- confirmed_attempt_probability
) ** max_attempts
)
return FragmentModeEstimate(
fragment_size=fragment_size,
ber=ber,
data_packet_size_bytes=data_packet_size_bytes,
ack_packet_size_bytes=ack_packet_size_bytes,
data_success_probability=(
data_success_probability
),
ack_success_probability=(
ack_success_probability
),
confirmed_attempt_probability=(
confirmed_attempt_probability
),
expected_attempts=expected_attempts,
success_probability_with_retries=(
success_probability_with_retries
),
expected_goodput_bps=(
expected_goodput_bps
),
)
# ============================================================
# Выбор режима
# ============================================================
def choose_image_mode(
eb_n0_db: float,
candidate_fragment_sizes: tuple[int, ...] = (
128,
512,
1024,
),
channel_bitrate_bps: int = 20_000,
max_attempts: int = 5,
minimum_success_probability: float = 0.85,
minimum_goodput_bps: float = 2_000.0,
) -> LinkDecision:
"""
Выбрать режим передачи изображения.
Режим допускается, если:
1. вероятность подтверждения за max_attempts
не ниже minimum_success_probability;
2. ожидаемый goodput не ниже minimum_goodput_bps.
Из подходящих режимов выбирается режим
с максимальной полезной скоростью.
"""
if not candidate_fragment_sizes:
raise ValueError(
"Список размеров фрагментов пуст"
)
if not 0.0 <= minimum_success_probability <= 1.0:
raise ValueError(
"minimum_success_probability должен быть 0...1"
)
if minimum_goodput_bps < 0.0:
raise ValueError(
"minimum_goodput_bps не может быть отрицательным"
)
estimates = tuple(
evaluate_fragment_mode(
eb_n0_db=eb_n0_db,
fragment_size=fragment_size,
channel_bitrate_bps=channel_bitrate_bps,
max_attempts=max_attempts,
)
for fragment_size in candidate_fragment_sizes
)
eligible_estimates = [
estimate
for estimate in estimates
if (
estimate.success_probability_with_retries
>= minimum_success_probability
and estimate.expected_goodput_bps
>= minimum_goodput_bps
)
]
if not eligible_estimates:
return LinkDecision(
eb_n0_db=eb_n0_db,
images_enabled=False,
selected_fragment_size=None,
reason=(
"Канал недостаточен для надёжной "
"передачи изображения"
),
estimates=estimates,
)
selected_estimate = max(
eligible_estimates,
key=lambda estimate: (
estimate.expected_goodput_bps
),
)
return LinkDecision(
eb_n0_db=eb_n0_db,
images_enabled=True,
selected_fragment_size=(
selected_estimate.fragment_size
),
reason=(
"Выбран режим с максимальной "
"ожидаемой полезной скоростью"
),
estimates=estimates,
)

View File

@@ -0,0 +1,447 @@
"""
Устойчивый контроллер режима радиоканала SDR Rover Link.
Контроллер устраняет частое переключение режимов
из-за шума измерения Eb/N0.
Реализованы:
1. Экспоненциальное усреднение Eb/N0.
2. Гистерезис порогов.
3. Подтверждение повышения режима.
4. Минимальное время удержания режима.
5. Быстрое аварийное понижение режима.
Доступные режимы:
0 — изображения отключены;
128 — фрагменты по 128 байт;
512 — фрагменты по 512 байт;
1024 — фрагменты по 1024 байта.
"""
from dataclasses import dataclass
# ============================================================
# Допустимые режимы
# ============================================================
IMAGE_OFF = 0
FRAGMENT_MODE_128 = 128
FRAGMENT_MODE_512 = 512
FRAGMENT_MODE_1024 = 1024
VALID_MODES = (
IMAGE_OFF,
FRAGMENT_MODE_128,
FRAGMENT_MODE_512,
FRAGMENT_MODE_1024,
)
# ============================================================
# Результат одного шага контроллера
# ============================================================
@dataclass(frozen=True)
class LinkControllerResult:
"""
Состояние контроллера после нового измерения Eb/N0.
"""
step: int
measured_eb_n0_db: float
filtered_eb_n0_db: float
previous_mode: int
selected_mode: int
mode_changed: bool
pending_upgrade_mode: int | None
pending_upgrade_count: int
reason: str
# ============================================================
# Контроллер
# ============================================================
class LinkModeController:
"""
Устойчивый адаптивный контроллер режима изображения.
Пороговые значения выбраны по результатам Lab015 и Lab016.
Гистерезис означает, что повышение и понижение режима
происходят при разных значениях Eb/N0.
Пример для режима 512 байт:
повышение 128 → 512: Eb/N0 >= 8,7 дБ
понижение 512 → 128: Eb/N0 < 8,1 дБ
Благодаря разнице 0,6 дБ небольшие колебания около
порога не вызывают постоянного переключения.
"""
def __init__(
self,
initial_mode: int = IMAGE_OFF,
filter_alpha: float = 0.35,
upgrade_confirmation_count: int = 3,
minimum_hold_steps: int = 2,
) -> None:
"""
Создать контроллер.
Параметры
----------
initial_mode:
Начальный режим.
filter_alpha:
Коэффициент экспоненциального фильтра.
Большое значение:
быстрее реакция;
слабее фильтрация шума.
Малое значение:
медленнее реакция;
сильнее фильтрация.
upgrade_confirmation_count:
Сколько последовательных измерений требуется
для повышения режима.
minimum_hold_steps:
Минимальное число шагов удержания режима
перед очередным повышением.
"""
if initial_mode not in VALID_MODES:
raise ValueError(
f"Недопустимый начальный режим: {initial_mode}"
)
if not 0.0 < filter_alpha <= 1.0:
raise ValueError(
"filter_alpha должен находиться в диапазоне 0...1"
)
if upgrade_confirmation_count <= 0:
raise ValueError(
"upgrade_confirmation_count должен быть положительным"
)
if minimum_hold_steps < 0:
raise ValueError(
"minimum_hold_steps не может быть отрицательным"
)
self.current_mode = initial_mode
self.filter_alpha = filter_alpha
self.upgrade_confirmation_count = (
upgrade_confirmation_count
)
self.minimum_hold_steps = (
minimum_hold_steps
)
self.filtered_eb_n0_db: float | None = None
self.step = 0
self.steps_in_mode = 0
self.pending_upgrade_mode: int | None = None
self.pending_upgrade_count = 0
# ========================================================
# Внутренние функции
# ========================================================
def _update_filter(
self,
measured_eb_n0_db: float,
) -> float:
"""
Обновить экспоненциальное среднее.
Формула:
filtered =
alpha * measured
+ (1 - alpha) * previous_filtered
"""
if self.filtered_eb_n0_db is None:
self.filtered_eb_n0_db = measured_eb_n0_db
else:
self.filtered_eb_n0_db = (
self.filter_alpha
* measured_eb_n0_db
+ (
1.0
- self.filter_alpha
)
* self.filtered_eb_n0_db
)
return self.filtered_eb_n0_db
def _emergency_safe_mode(
self,
measured_eb_n0_db: float,
) -> int:
"""
Определить максимально безопасный режим
по мгновенному измерению.
Используется для быстрого понижения режима,
не дожидаясь усреднения.
"""
if measured_eb_n0_db < 5.8:
return IMAGE_OFF
if measured_eb_n0_db < 7.7:
return FRAGMENT_MODE_128
if measured_eb_n0_db < 8.7:
return FRAGMENT_MODE_512
return FRAGMENT_MODE_1024
def _desired_mode_from_filtered(
self,
filtered_eb_n0_db: float,
) -> int:
"""
Определить желаемый режим с учётом гистерезиса.
Повышение выполняется только на один уровень за шаг.
"""
if self.current_mode == IMAGE_OFF:
if filtered_eb_n0_db >= 6.8:
return FRAGMENT_MODE_128
return IMAGE_OFF
if self.current_mode == FRAGMENT_MODE_128:
if filtered_eb_n0_db < 6.2:
return IMAGE_OFF
if filtered_eb_n0_db >= 8.7:
return FRAGMENT_MODE_512
return FRAGMENT_MODE_128
if self.current_mode == FRAGMENT_MODE_512:
if filtered_eb_n0_db < 8.1:
return FRAGMENT_MODE_128
if filtered_eb_n0_db >= 9.8:
return FRAGMENT_MODE_1024
return FRAGMENT_MODE_512
if self.current_mode == FRAGMENT_MODE_1024:
if filtered_eb_n0_db < 9.2:
return FRAGMENT_MODE_512
return FRAGMENT_MODE_1024
raise RuntimeError(
f"Неизвестный текущий режим: {self.current_mode}"
)
def _reset_pending_upgrade(
self,
) -> None:
"""
Сбросить накопленное подтверждение повышения.
"""
self.pending_upgrade_mode = None
self.pending_upgrade_count = 0
def _change_mode(
self,
new_mode: int,
) -> None:
"""
Немедленно установить новый режим.
"""
if new_mode not in VALID_MODES:
raise ValueError(
f"Недопустимый режим: {new_mode}"
)
self.current_mode = new_mode
self.steps_in_mode = 0
self._reset_pending_upgrade()
# ========================================================
# Основной шаг
# ========================================================
def update(
self,
measured_eb_n0_db: float,
) -> LinkControllerResult:
"""
Обработать новое измерение Eb/N0.
Логика:
1. Обновить фильтр.
2. Проверить аварийное понижение.
3. Проверить обычное понижение по фильтру.
4. Для повышения потребовать несколько подтверждений.
5. Вернуть результат.
"""
measured_eb_n0_db = float(
measured_eb_n0_db
)
previous_mode = self.current_mode
filtered_eb_n0_db = self._update_filter(
measured_eb_n0_db
)
reason = "Режим удерживается"
# ----------------------------------------------------
# Аварийное быстрое понижение
# ----------------------------------------------------
emergency_mode = self._emergency_safe_mode(
measured_eb_n0_db
)
if emergency_mode < self.current_mode:
self._change_mode(
emergency_mode
)
reason = (
"Аварийное понижение по мгновенному Eb/N0"
)
else:
desired_mode = self._desired_mode_from_filtered(
filtered_eb_n0_db
)
# ------------------------------------------------
# Обычное понижение
# ------------------------------------------------
if desired_mode < self.current_mode:
self._change_mode(
desired_mode
)
reason = (
"Понижение по фильтрованному Eb/N0"
)
# ------------------------------------------------
# Возможное повышение
# ------------------------------------------------
elif desired_mode > self.current_mode:
if (
self.steps_in_mode
< self.minimum_hold_steps
):
self._reset_pending_upgrade()
reason = (
"Повышение запрещено: "
"не выдержано минимальное время режима"
)
else:
if (
self.pending_upgrade_mode
== desired_mode
):
self.pending_upgrade_count += 1
else:
self.pending_upgrade_mode = (
desired_mode
)
self.pending_upgrade_count = 1
reason = (
"Накопление подтверждений "
"для повышения режима"
)
if (
self.pending_upgrade_count
>= self.upgrade_confirmation_count
):
self._change_mode(
desired_mode
)
reason = (
"Режим повышен после "
"нескольких подтверждений"
)
else:
self._reset_pending_upgrade()
mode_changed = (
self.current_mode != previous_mode
)
result = LinkControllerResult(
step=self.step,
measured_eb_n0_db=measured_eb_n0_db,
filtered_eb_n0_db=filtered_eb_n0_db,
previous_mode=previous_mode,
selected_mode=self.current_mode,
mode_changed=mode_changed,
pending_upgrade_mode=(
self.pending_upgrade_mode
),
pending_upgrade_count=(
self.pending_upgrade_count
),
reason=reason,
)
self.step += 1
self.steps_in_mode += 1
return result

View File

@@ -0,0 +1,254 @@
"""
Пакетный протокол 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,
)