initial commit
This commit is contained in:
0
Projects/SDR_Rover/protocol/.gitkeep
Normal file
0
Projects/SDR_Rover/protocol/.gitkeep
Normal file
7
Projects/SDR_Rover/protocol/__init__.py
Normal file
7
Projects/SDR_Rover/protocol/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Fri Jul 10 17:11:41 2026
|
||||
|
||||
@author: user
|
||||
"""
|
||||
|
||||
354
Projects/SDR_Rover/protocol/image_fragments.py
Normal file
354
Projects/SDR_Rover/protocol/image_fragments.py
Normal 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
|
||||
382
Projects/SDR_Rover/protocol/link_adaptation.py
Normal file
382
Projects/SDR_Rover/protocol/link_adaptation.py
Normal 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,
|
||||
)
|
||||
447
Projects/SDR_Rover/protocol/link_controller.py
Normal file
447
Projects/SDR_Rover/protocol/link_controller.py
Normal 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
|
||||
|
||||
254
Projects/SDR_Rover/protocol/packet.py
Normal file
254
Projects/SDR_Rover/protocol/packet.py
Normal 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,
|
||||
)
|
||||
Reference in New Issue
Block a user