initial commit
51
.gitignore
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# ==========================================
|
||||||
|
# SDR Rover Link
|
||||||
|
# Файлы, не включаемые в Git
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# Python: скомпилированные и временные файлы
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Кэш инструментов проверки и тестирования
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.coverage
|
||||||
|
|
||||||
|
# Spyder
|
||||||
|
.spyproject/
|
||||||
|
.spyderproject
|
||||||
|
.spyderworkspace
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints/
|
||||||
|
|
||||||
|
# Виртуальные окружения Python
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# Временные файлы редакторов
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Временные файлы Microsoft Office
|
||||||
|
~$*.docx
|
||||||
|
~$*.xlsx
|
||||||
|
~$*.pptx
|
||||||
|
|
||||||
|
# Системные файлы Windows
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Системные файлы macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Локальные настройки и секреты
|
||||||
|
.env
|
||||||
|
*.local
|
||||||
|
|
||||||
35
Projects/SDR_Rover/PROJECT_LOG.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Инженерный журнал SDR Rover Link
|
||||||
|
|
||||||
|
## Назначение журнала
|
||||||
|
|
||||||
|
Этот файл содержит хронологию разработки проекта SDR Rover Link.
|
||||||
|
|
||||||
|
В журнале фиксируются:
|
||||||
|
|
||||||
|
- выполненные работы;
|
||||||
|
- полученные результаты;
|
||||||
|
- обнаруженные ошибки;
|
||||||
|
- принятые технические решения;
|
||||||
|
- результаты экспериментов;
|
||||||
|
- планы следующего этапа.
|
||||||
|
|
||||||
|
Записи не следует удалять. Если решение изменилось, необходимо добавить новую запись и объяснить причину изменения.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Запись 001
|
||||||
|
|
||||||
|
## Дата
|
||||||
|
|
||||||
|
10 июля 2026 года
|
||||||
|
|
||||||
|
## Тема
|
||||||
|
|
||||||
|
Создание структуры проекта и локального Git-репозитория.
|
||||||
|
|
||||||
|
## Выполнено
|
||||||
|
|
||||||
|
1. Проверена установленная версия Git:
|
||||||
|
|
||||||
|
```text
|
||||||
|
git version 2.51.1.windows.1
|
||||||
340
Projects/SDR_Rover/README.md
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
# SDR Rover Link
|
||||||
|
|
||||||
|
## 1. Назначение проекта
|
||||||
|
|
||||||
|
**SDR Rover Link** — учебно-исследовательский проект по разработке цифрового радиоканала для наземного робототехнического комплекса с использованием программно-определяемого радио — SDR, Software Defined Radio.
|
||||||
|
|
||||||
|
Проект должен пройти полный путь от передачи простого текстового пакета до передачи телеметрии и отдельных JPEG-изображений через реальный радиоканал.
|
||||||
|
|
||||||
|
Основная задача проекта — не только получить работающую систему, но и последовательно изучить все этапы цифровой радиосвязи:
|
||||||
|
|
||||||
|
- формирование данных;
|
||||||
|
- разбиение данных на пакеты;
|
||||||
|
- контроль целостности;
|
||||||
|
- исправление ошибок;
|
||||||
|
- цифровую модуляцию;
|
||||||
|
- формирование IQ-сигнала;
|
||||||
|
- передачу через SDR;
|
||||||
|
- приём и демодуляцию;
|
||||||
|
- восстановление исходных данных.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Целевой сценарий
|
||||||
|
|
||||||
|
Разрабатываемая система предназначена для исследовательского наземного ровера.
|
||||||
|
|
||||||
|
Целевые требования:
|
||||||
|
|
||||||
|
| Параметр | Требование |
|
||||||
|
|---|---|
|
||||||
|
| Дальность связи | до 10 км |
|
||||||
|
| Скорость ровера | до 25 км/ч |
|
||||||
|
| Прямая радиовидимость | может отсутствовать |
|
||||||
|
| Рабочие частоты | ниже 350 МГц |
|
||||||
|
| Основной тип данных | телеметрия и отдельные изображения |
|
||||||
|
| Формат изображения | JPEG |
|
||||||
|
| Частота передачи изображений | отдельные кадры, не видеопоток |
|
||||||
|
| Действие при потере связи | безопасная остановка ровера |
|
||||||
|
|
||||||
|
Дальность 10 км без прямой видимости является целевым исследовательским требованием, а не гарантированной характеристикой первой версии системы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Почему не используется обычное FPV-видео
|
||||||
|
|
||||||
|
Типовые FPV-системы работают преимущественно в диапазонах 2,4 и 5,8 ГГц.
|
||||||
|
|
||||||
|
Для наземного робота такие диапазоны имеют ряд ограничений:
|
||||||
|
|
||||||
|
- высокие потери при работе у поверхности земли;
|
||||||
|
- затенение рельефом, зданиями и растительностью;
|
||||||
|
- быстрое ухудшение изображения при слабом сигнале;
|
||||||
|
- высокая загруженность распространённых диапазонов;
|
||||||
|
- зависимость от постоянного широкополосного видеопотока.
|
||||||
|
|
||||||
|
В этом проекте вместо непрерывного видео предполагается передавать отдельные сжатые изображения.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Камера
|
||||||
|
↓
|
||||||
|
JPEG-изображение
|
||||||
|
↓
|
||||||
|
Разбиение на пакеты
|
||||||
|
↓
|
||||||
|
Передача через SDR
|
||||||
|
↓
|
||||||
|
Сборка изображения на наземной станции
|
||||||
|
```
|
||||||
|
|
||||||
|
Такой подход позволяет существенно снизить требуемую скорость радиоканала.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Текущее оборудование
|
||||||
|
|
||||||
|
### SDR-оборудование
|
||||||
|
|
||||||
|
- RTL-SDR — приёмник;
|
||||||
|
- ADALM-PlutoSDR — приёмопередатчик, ожидается поступление;
|
||||||
|
- HackRF One — может временно использоваться при необходимости.
|
||||||
|
|
||||||
|
### Вычислительное оборудование
|
||||||
|
|
||||||
|
- компьютер под управлением Windows;
|
||||||
|
- Raspberry Pi — планируется для установки на ровер;
|
||||||
|
- бортовой вычислитель ровера — будет выбран позднее.
|
||||||
|
|
||||||
|
### Программное обеспечение
|
||||||
|
|
||||||
|
- Python 3.12;
|
||||||
|
- Anaconda;
|
||||||
|
- Spyder;
|
||||||
|
- Git;
|
||||||
|
- GNU Radio — будет установлен на следующем этапе;
|
||||||
|
- libiio и драйверы PlutoSDR — будут установлены после получения устройства.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Предварительно рассматриваемые диапазоны
|
||||||
|
|
||||||
|
В рамках проекта рассматриваются:
|
||||||
|
|
||||||
|
- 27 МГц;
|
||||||
|
- 50 МГц;
|
||||||
|
- 144 МГц;
|
||||||
|
- другие разрешённые для лабораторных испытаний частоты ниже 350 МГц.
|
||||||
|
|
||||||
|
Конкретный диапазон будет выбран после анализа:
|
||||||
|
|
||||||
|
- доступной полосы;
|
||||||
|
- требований законодательства;
|
||||||
|
- размеров антенны;
|
||||||
|
- потерь распространения;
|
||||||
|
- уровня внешних помех;
|
||||||
|
- возможностей выходного радиочастотного тракта;
|
||||||
|
- допустимой мощности передачи.
|
||||||
|
|
||||||
|
Передача по эфиру должна выполняться только на разрешённых частотах и с соблюдением допустимой мощности. Первые опыты следует проводить через аттенюатор, эквивалент нагрузки или в экранированной лабораторной конфигурации.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Основные этапы проекта
|
||||||
|
|
||||||
|
### Этап 0. Подготовка среды
|
||||||
|
|
||||||
|
- создание репозитория;
|
||||||
|
- настройка Git;
|
||||||
|
- настройка Python и Spyder;
|
||||||
|
- подготовка структуры каталогов;
|
||||||
|
- ведение документации.
|
||||||
|
|
||||||
|
### Этап 1. Пакетный протокол
|
||||||
|
|
||||||
|
- структура пакета;
|
||||||
|
- типы сообщений;
|
||||||
|
- порядковый номер пакета;
|
||||||
|
- длина данных;
|
||||||
|
- CRC;
|
||||||
|
- кодирование и декодирование.
|
||||||
|
|
||||||
|
### Этап 2. Виртуальный канал
|
||||||
|
|
||||||
|
- передача пакетов между Python-программами;
|
||||||
|
- имитация потерь;
|
||||||
|
- имитация повреждения битов;
|
||||||
|
- измерение числа ошибочных пакетов.
|
||||||
|
|
||||||
|
### Этап 3. Передача файлов
|
||||||
|
|
||||||
|
- разбиение файла на фрагменты;
|
||||||
|
- нумерация фрагментов;
|
||||||
|
- подтверждение приёма;
|
||||||
|
- повторная передача;
|
||||||
|
- сборка исходного файла.
|
||||||
|
|
||||||
|
### Этап 4. Передача изображений
|
||||||
|
|
||||||
|
- загрузка JPEG;
|
||||||
|
- разбиение JPEG на пакеты;
|
||||||
|
- передача через виртуальный канал;
|
||||||
|
- восстановление изображения;
|
||||||
|
- обработка потерянных фрагментов.
|
||||||
|
|
||||||
|
### Этап 5. Физический уровень
|
||||||
|
|
||||||
|
- BPSK;
|
||||||
|
- QPSK;
|
||||||
|
- формирование символов;
|
||||||
|
- IQ-сэмплы;
|
||||||
|
- синхронизация;
|
||||||
|
- фильтрация;
|
||||||
|
- оценка BER и PER.
|
||||||
|
|
||||||
|
### Этап 6. PlutoSDR
|
||||||
|
|
||||||
|
- подключение устройства;
|
||||||
|
- передача несущей;
|
||||||
|
- передача тестовой последовательности;
|
||||||
|
- передача цифрового пакета;
|
||||||
|
- приём сигнала RTL-SDR или вторым SDR.
|
||||||
|
|
||||||
|
### Этап 7. Радиоканал передачи изображений
|
||||||
|
|
||||||
|
- передача JPEG через SDR;
|
||||||
|
- измерение времени доставки;
|
||||||
|
- испытания при различных уровнях сигнала;
|
||||||
|
- испытания при наличии помех;
|
||||||
|
- подбор скорости и модуляции.
|
||||||
|
|
||||||
|
### Этап 8. Полевые испытания
|
||||||
|
|
||||||
|
- испытания на малой мощности;
|
||||||
|
- испытания на малой дистанции;
|
||||||
|
- постепенное увеличение расстояния;
|
||||||
|
- испытания с ретранслятором;
|
||||||
|
- испытания на наземном ровере.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Структура репозитория
|
||||||
|
|
||||||
|
```text
|
||||||
|
SDR_Rover
|
||||||
|
│
|
||||||
|
├── README.md
|
||||||
|
├── docs
|
||||||
|
├── protocol
|
||||||
|
├── rover
|
||||||
|
├── ground_station
|
||||||
|
├── gnuradio
|
||||||
|
├── tests
|
||||||
|
└── tools
|
||||||
|
```
|
||||||
|
|
||||||
|
### `docs`
|
||||||
|
|
||||||
|
Документация, лабораторные работы, схемы и отчёты.
|
||||||
|
|
||||||
|
### `protocol`
|
||||||
|
|
||||||
|
Реализация пакетного протокола:
|
||||||
|
|
||||||
|
- структура пакета;
|
||||||
|
- CRC;
|
||||||
|
- кодирование;
|
||||||
|
- декодирование;
|
||||||
|
- типы сообщений.
|
||||||
|
|
||||||
|
### `rover`
|
||||||
|
|
||||||
|
Программы, которые будут работать на борту ровера:
|
||||||
|
|
||||||
|
- получение изображения;
|
||||||
|
- сжатие JPEG;
|
||||||
|
- передача телеметрии;
|
||||||
|
- формирование радиопакетов.
|
||||||
|
|
||||||
|
### `ground_station`
|
||||||
|
|
||||||
|
Программы наземной станции:
|
||||||
|
|
||||||
|
- приём пакетов;
|
||||||
|
- сборка изображений;
|
||||||
|
- отображение телеметрии;
|
||||||
|
- управление передачей.
|
||||||
|
|
||||||
|
### `gnuradio`
|
||||||
|
|
||||||
|
Схемы GNU Radio Companion:
|
||||||
|
|
||||||
|
- передатчик;
|
||||||
|
- приёмник;
|
||||||
|
- BPSK-модем;
|
||||||
|
- QPSK-модем;
|
||||||
|
- тестовые генераторы.
|
||||||
|
|
||||||
|
### `tests`
|
||||||
|
|
||||||
|
Автоматические и лабораторные тесты.
|
||||||
|
|
||||||
|
### `tools`
|
||||||
|
|
||||||
|
Вспомогательные программы:
|
||||||
|
|
||||||
|
- анализ пакетов;
|
||||||
|
- генераторы тестовых данных;
|
||||||
|
- построение графиков;
|
||||||
|
- анализ журналов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Принципы разработки
|
||||||
|
|
||||||
|
В проекте применяются следующие правила:
|
||||||
|
|
||||||
|
1. Каждый новый термин сначала объясняется, затем используется.
|
||||||
|
2. Каждый этап должен завершаться работающим результатом.
|
||||||
|
3. Радиоканал и пакетный протокол разрабатываются отдельно.
|
||||||
|
4. Код проверяется автоматическими тестами.
|
||||||
|
5. Все существенные решения фиксируются в документации.
|
||||||
|
6. Сложность увеличивается постепенно.
|
||||||
|
7. Испытания по эфиру начинаются только после проверки через виртуальный канал.
|
||||||
|
8. Передатчик сначала проверяется через аттенюатор или эквивалент нагрузки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Текущий статус
|
||||||
|
|
||||||
|
Выполнено:
|
||||||
|
|
||||||
|
- установлен Python;
|
||||||
|
- установлена Anaconda;
|
||||||
|
- выбран Spyder;
|
||||||
|
- установлен Git;
|
||||||
|
- создан локальный Git-репозиторий;
|
||||||
|
- создана базовая структура каталогов.
|
||||||
|
|
||||||
|
Следующая задача:
|
||||||
|
|
||||||
|
> Создать первый Python-модуль пакетного протокола и передать сообщение через виртуальный канал без SDR-оборудования.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Ближайший контрольный результат
|
||||||
|
|
||||||
|
Первая рабочая версия должна выполнять следующий цикл:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Текстовое сообщение
|
||||||
|
↓
|
||||||
|
Преобразование в байты
|
||||||
|
↓
|
||||||
|
Формирование пакета
|
||||||
|
↓
|
||||||
|
Добавление CRC
|
||||||
|
↓
|
||||||
|
Имитация радиоканала
|
||||||
|
↓
|
||||||
|
Проверка CRC
|
||||||
|
↓
|
||||||
|
Восстановление сообщения
|
||||||
|
```
|
||||||
|
|
||||||
|
Первое тестовое сообщение:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HELLO SDR
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Автор
|
||||||
|
|
||||||
|
Антон
|
||||||
|
|
||||||
|
НИЯУ МИФИ
|
||||||
|
|
||||||
|
2026
|
||||||
BIN
Projects/SDR_Rover/data/processed/lab010/01_color_640_q40.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
Projects/SDR_Rover/data/processed/lab010/02_gray_640_q40.jpg
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
Projects/SDR_Rover/data/processed/lab010/03_gray_320_q30.jpg
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
Projects/SDR_Rover/data/processed/lab010/04_gray_320_q15.jpg
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
Projects/SDR_Rover/data/processed/lab010/05_edges_320_q40.jpg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
BIN
Projects/SDR_Rover/data/processed/lab010/lab010_comparison.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
name,path,width,height,mode,file_size_bytes,fragment_count,total_radio_bytes,efficiency_percent,time_5_kbps,time_10_kbps,time_20_kbps,time_50_kbps,time_100_kbps,time_20_kbps_with_retries_x2
|
||||||
|
00_original,data\raw\lab009_source.jpg,640,960,RGB,101703,199,108867,93.42,174.1872,87.0936,43.5468,17.41872,8.70936,87.0936
|
||||||
|
01_color_640_q40,data\processed\lab010\01_color_640_q40.jpg,320,480,RGB,10610,21,11366,93.35,18.1856,9.0928,4.5464,1.81856,0.90928,9.0928
|
||||||
|
02_gray_640_q40,data\processed\lab010\02_gray_640_q40.jpg,320,480,L,9496,19,10180,93.28,16.288,8.144,4.072,1.6288,0.8144,8.144
|
||||||
|
03_gray_320_q30,data\processed\lab010\03_gray_320_q30.jpg,160,240,L,2792,6,3008,92.82,4.8128,2.4064,1.2032,0.48128,0.24064,2.4064
|
||||||
|
04_gray_320_q15,data\processed\lab010\04_gray_320_q15.jpg,160,240,L,1806,4,1950,92.62,3.12,1.56,0.78,0.312,0.156,1.56
|
||||||
|
05_edges_320_q40,data\processed\lab010\05_edges_320_q40.jpg,160,240,L,6409,13,6877,93.19,11.0032,5.5016,2.7508,1.10032,0.55016,5.5016
|
||||||
|
06_gray_edges_320_q30,data\processed\lab010\06_gray_edges_320_q30.jpg,160,240,L,2185,5,2365,92.39,3.784,1.892,0.946,0.3784,0.1892,1.892
|
||||||
|
BIN
Projects/SDR_Rover/data/processed/lab011/01_gray_320_q30.jpg
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
Projects/SDR_Rover/data/processed/lab011/02_gray_320_q15.jpg
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
BIN
Projects/SDR_Rover/data/processed/lab011/lab011_comparison.jpg
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
Projects/SDR_Rover/data/processed/lab012/01_color_320_q30.jpg
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
Projects/SDR_Rover/data/processed/lab012/02_gray_320_q30.jpg
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
Projects/SDR_Rover/data/processed/lab012/03_color_320_q15.jpg
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
Projects/SDR_Rover/data/processed/lab012/04_gray_320_q15.jpg
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
Projects/SDR_Rover/data/processed/lab012/lab012_comparison.jpg
Normal file
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 101 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
eb_n0_db,bit_errors,experimental_ber,theoretical_ber,short_packet_per_224_bits,image_packet_per_4288_bits
|
||||||
|
-4.0,186789,0.186789,0.186113817483389,1.0,1.0
|
||||||
|
-2.0,130895,0.130895,0.13064448852282923,0.999999999999976,1.0
|
||||||
|
0.0,78830,0.07883,0.07864960352514255,0.9999999892561172,1.0
|
||||||
|
2.0,37551,0.037551,0.03750612835892598,0.9998089504830603,1.0
|
||||||
|
4.0,12379,0.012379,0.012500818040737556,0.9402647377229729,1.0
|
||||||
|
6.0,2266,0.002266,0.002388290780932807,0.4146919530687869,0.9999647568582152
|
||||||
|
8.0,205,0.000205,0.00019090777407599314,0.041865796670504794,0.5589913040105388
|
||||||
|
10.0,5,5e-06,3.872108215522038e-06,0.0008669778768398397,0.016466551600075156
|
||||||
|
12.0,0,0.0,9.006010350628754e-09,2.017344292783227e-06,3.8617026900812036e-05
|
||||||
|
|
After Width: | Height: | Size: 103 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
fragment_size,fragment_count,eb_n0_db,ber,ideal_total_bytes,ideal_efficiency_percent,ack_success_probability,minimum_data_success_probability,maximum_data_success_probability,average_data_transmissions,expected_total_bytes,expected_transfer_seconds,expected_efficiency_percent,effective_goodput_bps
|
||||||
|
64,1590,6.0,0.002388290780932807,158943,63.98708971140598,0.7948911114376548,0.18574738070642127,0.5526642915589317,6.769993990326338,971131.0972436845,388.45243889747377,10.472633436274343,2094.5266872548686
|
||||||
|
64,1590,8.0,0.00019090777407599314,158943,63.98708971140598,0.9818380567064475,0.8742298920810428,0.9537538613227441,1.1649618711080167,182373.53560599152,72.94941424239661,55.766314812103005,11153.2629624206
|
||||||
|
64,1590,10.0,3.872108215522038e-06,158943,63.98708971140598,0.9996283459721081,0.9972777426196243,0.9990401762291837,1.0031013824212691,159383.9632486371,63.75358529945484,63.81005838168582,12762.011676337162
|
||||||
|
64,1590,12.0,9.006010350628754e-09,158943,63.98708971140598,0.9999991354233762,0.9999936597887839,0.9999977665119172,1.0000072022513673,158944.0240585637,63.577609623425474,63.98667744974611,12797.335489949222
|
||||||
|
128,795,6.0,0.002388290780932807,130323,78.0391795768974,0.7948911114376548,0.0546047112997041,0.1624683695213351,23.01968691435649,2793259.2450467222,1117.303698018689,3.6410154259884613,728.2030851976923
|
||||||
|
128,795,8.0,0.00019090777407599314,130323,78.0391795768974,0.9818380567064475,0.792814493179396,0.8649325435241314,1.2845263164977094,164871.50993740102,65.9486039749604,61.68621858234631,12337.243716469262
|
||||||
|
128,795,10.0,3.872108215522038e-06,130323,78.0391795768974,0.9996283459721081,0.995302574868459,0.9970615178736658,1.005090911661357,130941.54342108239,52.37661736843295,77.67053705250979,15534.10741050196
|
||||||
|
128,795,12.0,9.006010350628754e-09,130323,78.0391795768974,0.9999991354233762,0.9999890487513297,0.9999931554555267,1.0000118107896643,130324.43502447128,52.12977400978851,78.03832027424713,15607.664054849427
|
||||||
|
256,398,6.0,0.002388290780932807,116031,87.65157587196525,0.7948911114376548,0.0047189453652365446,0.1624683695213351,265.9418083130096,29641130.986424245,11856.452394569698,0.3431144379969185,68.62288759938369
|
||||||
|
256,398,8.0,0.00019090777407599314,116031,87.65157587196525,0.9818380567064475,0.6520237845186864,0.8649325435241314,1.5610902562146585,178614.3981088735,71.44575924354939,56.939978566569685,11387.995713313936
|
||||||
|
256,398,10.0,3.872108215522038e-06,116031,87.65157587196525,0.9996283459721081,0.9913639674291386,0.9970615178736658,1.00907180620962,117043.12355799062,46.81724942319625,86.89361400168875,17378.72280033775
|
||||||
|
256,398,12.0,9.006010350628754e-09,116031,87.65157587196525,0.9999991354233762,0.9999798267402067,0.9999931554555267,1.0000210047714104,116033.34347475081,46.41333738990033,87.64980561137658,17529.961122275316
|
||||||
|
512,199,6.0,0.002388290780932807,108867,93.41949351043016,0.7948911114376548,3.52431417847955e-05,0.001213384631398025,35521.68507371966,3788696213.3825846,1515478.4853530338,0.002684379909921534,0.5368759819843069
|
||||||
|
512,199,8.0,0.00019090777407599314,108867,93.41949351043016,0.9818380567064475,0.4410086959894613,0.58501358722673,2.3066167669494577,248143.0622186174,99.25722488744695,40.98563106729065,8197.126213458128
|
||||||
|
512,199,10.0,3.872108215522038e-06,108867,93.41949351043016,0.9996283459721081,0.9835334483999248,0.9891859954162041,1.017091048475429,110688.79543911858,44.275518175647434,91.88192860580818,18376.385721161638
|
||||||
|
512,199,12.0,9.006010350628754e-09,108867,93.41949351043016,0.9999991354233762,0.9999613829730992,0.9999747114425825,1.000039416147422,108871.20151010722,43.54848060404289,93.41588830592474,18683.177661184945
|
||||||
|
1024,100,6.0,0.002388290780932807,105303,96.5812939802285,0.7948911114376548,1.9657756573348356e-09,0.001213384631398025,633568545.9977559,66397982899426.766,26559193159.770706,1.5317182173146714e-07,3.063436434629343e-05
|
||||||
|
1024,100,8.0,0.00019090777407599314,105303,96.5812939802285,0.9818380567064475,0.20175048295563117,0.58501358722673,5.0152314190831655,525604.9860141345,210.2419944056538,19.349702287121193,3869.940457424238
|
||||||
|
1024,100,10.0,3.872108215522038e-06,105303,96.5812939802285,0.9996283459721081,0.9680574753267193,0.9891859954162041,1.033159851833223,108770.71688930437,43.50828675572175,93.50218782092135,18700.43756418427
|
||||||
|
1024,100,12.0,9.006010350628754e-09,105303,96.5812939802285,0.9999991354233762,0.9999244964594162,0.9999747114425825,1.0000758716835711,105310.93416073515,42.12437366429406,96.57401751348212,19314.803502696424
|
||||||
|
|
After Width: | Height: | Size: 102 KiB |
@@ -0,0 +1,12 @@
|
|||||||
|
step,eb_n0_db,mode,fragment_size,fragment_count,expected_seconds,effective_goodput_bps,success_with_retries,expected_attempts,reason
|
||||||
|
0,12.0,1024 B,1024,4,1.50130532861719,19231.264586660185,1.0,1.0000763738844538,Выбран режим с максимальной ожидаемой полезной скоростью
|
||||||
|
1,10.0,1024 B,1024,4,1.5472051612507218,18660.744368678683,0.9999999648303772,1.0333805767765363,Выбран режим с максимальной ожидаемой полезной скоростью
|
||||||
|
2,9.0,512 B,512,8,1.797643784171777,16061.024021676303,0.9999516109774961,1.158845496802822,Выбран режим с максимальной ожидаемой полезной скоростью
|
||||||
|
3,8.0,128 B,128,29,2.3502958310297126,12284.41101703805,0.9994658078956576,1.2846610523483055,Выбран режим с максимальной ожидаемой полезной скоростью
|
||||||
|
4,7.0,128 B,128,29,4.872011036339501,5926.09495024717,0.8948830865552501,2.7569946980068387,Выбран режим с максимальной ожидаемой полезной скоростью
|
||||||
|
5,6.0,IMAGE OFF,0,0,,0.0,0.0,0.0,Канал недостаточен для надёжной передачи изображения
|
||||||
|
6,7.0,128 B,128,29,4.872011036339501,5926.09495024717,0.8948830865552501,2.7569946980068387,Выбран режим с максимальной ожидаемой полезной скоростью
|
||||||
|
7,8.0,128 B,128,29,2.3502958310297126,12284.41101703805,0.9994658078956576,1.2846610523483055,Выбран режим с максимальной ожидаемой полезной скоростью
|
||||||
|
8,9.0,512 B,512,8,1.797643784171777,16061.024021676303,0.9999516109774961,1.158845496802822,Выбран режим с максимальной ожидаемой полезной скоростью
|
||||||
|
9,10.0,1024 B,1024,4,1.5472051612507218,18660.744368678683,0.9999999648303772,1.0333805767765363,Выбран режим с максимальной ожидаемой полезной скоростью
|
||||||
|
10,12.0,1024 B,1024,4,1.50130532861719,19231.264586660185,1.0,1.0000763738844538,Выбран режим с максимальной ожидаемой полезной скоростью
|
||||||
|
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 154 KiB |
@@ -0,0 +1,46 @@
|
|||||||
|
step,measured_eb_n0_db,filtered_eb_n0_db,naive_mode,stable_mode,mode_changed,pending_upgrade_mode,pending_upgrade_count,reason
|
||||||
|
0,10.2,10.2,1024,1024,False,,0,Режим удерживается
|
||||||
|
1,9.7,10.024999999999999,1024,1024,False,,0,Режим удерживается
|
||||||
|
2,9.4,9.806249999999999,512,1024,False,,0,Режим удерживается
|
||||||
|
3,9.6,9.734062499999999,1024,1024,False,,0,Режим удерживается
|
||||||
|
4,9.3,9.582140625,512,1024,False,,0,Режим удерживается
|
||||||
|
5,9.7,9.62339140625,1024,1024,False,,0,Режим удерживается
|
||||||
|
6,9.2,9.4752044140625,512,1024,False,,0,Режим удерживается
|
||||||
|
7,9.6,9.518882869140624,1024,1024,False,,0,Режим удерживается
|
||||||
|
8,9.0,9.337273864941405,512,1024,False,,0,Режим удерживается
|
||||||
|
9,8.7,9.114228012211914,512,512,True,,0,Понижение по фильтрованному Eb/N0
|
||||||
|
10,8.4,8.864248207937743,128,512,False,,0,Режим удерживается
|
||||||
|
11,8.6,8.771761335159534,512,512,False,,0,Режим удерживается
|
||||||
|
12,8.3,8.606644867853698,128,512,False,,0,Режим удерживается
|
||||||
|
13,8.7,8.639319164104903,512,512,False,,0,Режим удерживается
|
||||||
|
14,8.2,8.485557456668186,128,512,False,,0,Режим удерживается
|
||||||
|
15,8.6,8.525612346834322,512,512,False,,0,Режим удерживается
|
||||||
|
16,8.0,8.341648025442309,128,512,False,,0,Режим удерживается
|
||||||
|
17,7.8,8.1520712165375,128,512,False,,0,Режим удерживается
|
||||||
|
18,7.5,7.923846290749375,128,128,True,,0,Аварийное понижение по мгновенному Eb/N0
|
||||||
|
19,7.7,7.8455000889870945,128,128,False,,0,Режим удерживается
|
||||||
|
20,7.4,7.689575057841611,128,128,False,,0,Режим удерживается
|
||||||
|
21,7.8,7.728223787597047,128,128,False,,0,Режим удерживается
|
||||||
|
22,7.2,7.543345461938081,128,128,False,,0,Режим удерживается
|
||||||
|
23,6.8,7.283174550259753,128,128,False,,0,Режим удерживается
|
||||||
|
24,6.5,7.009063457668839,128,128,False,,0,Режим удерживается
|
||||||
|
25,6.3,6.760891247484745,0,128,False,,0,Режим удерживается
|
||||||
|
26,6.6,6.704579310865084,128,128,False,,0,Режим удерживается
|
||||||
|
27,6.2,6.527976552062304,0,128,False,,0,Режим удерживается
|
||||||
|
28,6.7,6.588184758840497,128,128,False,,0,Режим удерживается
|
||||||
|
29,6.1,6.417320093246323,0,128,False,,0,Режим удерживается
|
||||||
|
30,5.5,6.0962580606101096,0,0,True,,0,Аварийное понижение по мгновенному Eb/N0
|
||||||
|
31,6.0,6.062567739396571,0,0,False,,0,Режим удерживается
|
||||||
|
32,6.4,6.1806690306077705,0,0,False,,0,Режим удерживается
|
||||||
|
33,6.7,6.362434869895051,128,0,False,,0,Режим удерживается
|
||||||
|
34,6.9,6.550582665431783,128,0,False,,0,Режим удерживается
|
||||||
|
35,7.1,6.742878732530659,128,0,False,,0,Режим удерживается
|
||||||
|
36,7.3,6.937871176144928,128,0,False,128,1,Накопление подтверждений для повышения режима
|
||||||
|
37,8.0,7.3096162644942035,128,0,False,128,2,Накопление подтверждений для повышения режима
|
||||||
|
38,8.8,7.831250571921233,512,128,True,,0,Режим повышен после нескольких подтверждений
|
||||||
|
39,9.0,8.2403128717488,512,128,False,,0,Режим удерживается
|
||||||
|
40,9.4,8.64620336663672,512,128,False,,0,Режим удерживается
|
||||||
|
41,9.8,9.05003218831387,1024,128,False,512,1,Накопление подтверждений для повышения режима
|
||||||
|
42,10.0,9.382520922404016,1024,128,False,512,2,Накопление подтверждений для повышения режима
|
||||||
|
43,10.2,9.668638599562609,1024,512,True,,0,Режим повышен после нескольких подтверждений
|
||||||
|
44,10.1,9.819615089715695,1024,512,False,,0,Повышение запрещено: не выдержано минимальное время режима
|
||||||
|
|
After Width: | Height: | Size: 314 KiB |
BIN
Projects/SDR_Rover/data/processed/lab018/lab018_bpsk_tx_iq.cf32
Normal file
BIN
Projects/SDR_Rover/data/processed/lab018/lab018_bpsk_tx_iq.npy
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Lab018. BPSK radio frame
|
||||||
|
|
||||||
|
Message: ПРИВЕТ SDR
|
||||||
|
Protocol packet bytes: 28
|
||||||
|
Preamble bits: 64
|
||||||
|
Radio header bits: 32
|
||||||
|
Protocol packet bits: 224
|
||||||
|
Total radio frame bits: 320
|
||||||
|
Symbol rate: 20000 symbols/s
|
||||||
|
Samples per symbol: 32
|
||||||
|
Sample rate: 640000 samples/s
|
||||||
|
RRC rolloff: 0.35
|
||||||
|
RRC taps: 321
|
||||||
|
IQ sample count: 11584
|
||||||
|
Frame duration: 0.018100 s
|
||||||
|
Approximate occupied bandwidth: 27000.0 Hz
|
||||||
|
Average normalized power: 0.177911
|
||||||
|
Peak normalized power: 0.490000
|
||||||
|
PAPR: 4.40 dB
|
||||||
|
NPY file: data\processed\lab018\lab018_bpsk_tx_iq.npy
|
||||||
|
CF32 file: data\processed\lab018\lab018_bpsk_tx_iq.cf32
|
||||||
|
After Width: | Height: | Size: 162 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
Lab019. BPSK receiver
|
||||||
|
|
||||||
|
Input IQ: data\processed\lab018\lab018_bpsk_tx_iq.npy
|
||||||
|
Artificial sample delay: 11
|
||||||
|
Artificial phase offset: 37.00 deg
|
||||||
|
Detected sample phase: 11
|
||||||
|
Detected frame start symbol: 26
|
||||||
|
Correlation score: 0.999937
|
||||||
|
Estimated phase: 37.00 deg
|
||||||
|
Marker bit errors: 0
|
||||||
|
Radio sync: 0xD391
|
||||||
|
Protocol packet length: 28 bytes
|
||||||
|
Sequence number: 18
|
||||||
|
Restored message: ПРИВЕТ SDR
|
||||||
|
After Width: | Height: | Size: 203 KiB |
@@ -0,0 +1,68 @@
|
|||||||
|
Lab020. Full BPSK frame error rate
|
||||||
|
|
||||||
|
Trials per Eb/N0: 100
|
||||||
|
Detection threshold: 0.550
|
||||||
|
Frame bit count: 320
|
||||||
|
|
||||||
|
Eb/N0: 4.0 dB
|
||||||
|
Success: 3
|
||||||
|
Detection fail: 0
|
||||||
|
Header error: 34
|
||||||
|
CRC error: 38
|
||||||
|
Packet error: 25
|
||||||
|
Experimental FER: 0.970000
|
||||||
|
Ideal FER: 0.982145
|
||||||
|
|
||||||
|
Eb/N0: 5.0 dB
|
||||||
|
Success: 24
|
||||||
|
Detection fail: 0
|
||||||
|
Header error: 11
|
||||||
|
CRC error: 41
|
||||||
|
Packet error: 24
|
||||||
|
Experimental FER: 0.760000
|
||||||
|
Ideal FER: 0.852058
|
||||||
|
|
||||||
|
Eb/N0: 6.0 dB
|
||||||
|
Success: 58
|
||||||
|
Detection fail: 0
|
||||||
|
Header error: 5
|
||||||
|
CRC error: 29
|
||||||
|
Packet error: 8
|
||||||
|
Experimental FER: 0.420000
|
||||||
|
Ideal FER: 0.534744
|
||||||
|
|
||||||
|
Eb/N0: 7.0 dB
|
||||||
|
Success: 84
|
||||||
|
Detection fail: 0
|
||||||
|
Header error: 1
|
||||||
|
CRC error: 12
|
||||||
|
Packet error: 3
|
||||||
|
Experimental FER: 0.160000
|
||||||
|
Ideal FER: 0.219134
|
||||||
|
|
||||||
|
Eb/N0: 8.0 dB
|
||||||
|
Success: 98
|
||||||
|
Detection fail: 0
|
||||||
|
Header error: 0
|
||||||
|
CRC error: 2
|
||||||
|
Packet error: 0
|
||||||
|
Experimental FER: 0.020000
|
||||||
|
Ideal FER: 0.059267
|
||||||
|
|
||||||
|
Eb/N0: 10.0 dB
|
||||||
|
Success: 100
|
||||||
|
Detection fail: 0
|
||||||
|
Header error: 0
|
||||||
|
CRC error: 0
|
||||||
|
Packet error: 0
|
||||||
|
Experimental FER: 0.000000
|
||||||
|
Ideal FER: 0.001238
|
||||||
|
|
||||||
|
Eb/N0: 12.0 dB
|
||||||
|
Success: 100
|
||||||
|
Detection fail: 0
|
||||||
|
Header error: 0
|
||||||
|
CRC error: 0
|
||||||
|
Packet error: 0
|
||||||
|
Experimental FER: 0.000000
|
||||||
|
Ideal FER: 0.000003
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
eb_n0_db,trials,success_count,detection_fail_count,header_error_count,crc_error_count,packet_error_count,experimental_fer,theoretical_ber,ideal_fer,mean_correlation,mean_marker_bit_errors
|
||||||
|
4.0,100,3,0,34,38,25,0.97,0.012500818040737556,0.98214506597389,0.8537512081010437,0.94
|
||||||
|
5.0,100,24,0,11,41,24,0.76,0.0059538671477786615,0.8520575402615456,0.8753653434073579,0.44
|
||||||
|
6.0,100,58,0,5,29,8,0.42,0.002388290780932807,0.534743836041445,0.8986643099612688,0.15
|
||||||
|
7.0,100,84,0,1,12,3,0.16,0.0007726748153784444,0.21913384039715306,0.9164792561326658,0.08
|
||||||
|
8.0,100,98,0,0,2,0,0.02,0.00019090777407599314,0.05926737573898814,0.9311581513763438,0.01
|
||||||
|
10.0,100,100,0,0,0,0,0.0,3.872108215522038e-06,0.001238309688911671,0.9560438959972798,0.0
|
||||||
|
12.0,100,100,0,0,0,0,0.0,9.006010350628754e-09,2.88191917244148e-06,0.9716348856405669,0.0
|
||||||
|
|
After Width: | Height: | Size: 206 KiB |
@@ -0,0 +1,62 @@
|
|||||||
|
Lab021. Frequency offset correction
|
||||||
|
|
||||||
|
Symbol rate: 20000
|
||||||
|
Sample rate: 640000
|
||||||
|
Phase offset: 37.00 deg
|
||||||
|
Sample delay: 11
|
||||||
|
|
||||||
|
True frequency offset: 0.00 Hz
|
||||||
|
Estimated frequency offset: 0.0000 Hz
|
||||||
|
Frequency estimation error: 0.0000 Hz
|
||||||
|
Correlation score: 0.999937
|
||||||
|
Status without correction: SUCCESS
|
||||||
|
Status with correction: SUCCESS
|
||||||
|
|
||||||
|
True frequency offset: 10.00 Hz
|
||||||
|
Estimated frequency offset: 9.9963 Hz
|
||||||
|
Frequency estimation error: -0.0037 Hz
|
||||||
|
Correlation score: 0.999937
|
||||||
|
Status without correction: SUCCESS
|
||||||
|
Status with correction: SUCCESS
|
||||||
|
|
||||||
|
True frequency offset: 25.00 Hz
|
||||||
|
Estimated frequency offset: 24.9907 Hz
|
||||||
|
Frequency estimation error: -0.0093 Hz
|
||||||
|
Correlation score: 0.999937
|
||||||
|
Status without correction: CRC ERROR
|
||||||
|
Status with correction: SUCCESS
|
||||||
|
|
||||||
|
True frequency offset: 50.00 Hz
|
||||||
|
Estimated frequency offset: 49.9815 Hz
|
||||||
|
Frequency estimation error: -0.0185 Hz
|
||||||
|
Correlation score: 0.999937
|
||||||
|
Status without correction: PACKET ERROR
|
||||||
|
Status with correction: SUCCESS
|
||||||
|
|
||||||
|
True frequency offset: 100.00 Hz
|
||||||
|
Estimated frequency offset: 99.9630 Hz
|
||||||
|
Frequency estimation error: -0.0370 Hz
|
||||||
|
Correlation score: 0.999938
|
||||||
|
Status without correction: HEADER ERROR
|
||||||
|
Status with correction: SUCCESS
|
||||||
|
|
||||||
|
True frequency offset: 250.00 Hz
|
||||||
|
Estimated frequency offset: 249.9091 Hz
|
||||||
|
Frequency estimation error: -0.0909 Hz
|
||||||
|
Correlation score: 0.999943
|
||||||
|
Status without correction: PACKET ERROR
|
||||||
|
Status with correction: SUCCESS
|
||||||
|
|
||||||
|
True frequency offset: 500.00 Hz
|
||||||
|
Estimated frequency offset: 499.8285 Hz
|
||||||
|
Frequency estimation error: -0.1715 Hz
|
||||||
|
Correlation score: 0.999957
|
||||||
|
Status without correction: HEADER ERROR
|
||||||
|
Status with correction: SUCCESS
|
||||||
|
|
||||||
|
True frequency offset: 1000.00 Hz
|
||||||
|
Estimated frequency offset: 999.7270 Hz
|
||||||
|
Frequency estimation error: -0.2730 Hz
|
||||||
|
Correlation score: 0.999965
|
||||||
|
Status without correction: HEADER ERROR
|
||||||
|
Status with correction: SUCCESS
|
||||||
BIN
Projects/SDR_Rover/data/processed/lab022/lab022_noise_cfo.png
Normal file
|
After Width: | Height: | Size: 253 KiB |
@@ -0,0 +1,100 @@
|
|||||||
|
Lab022. AWGN and carrier frequency offset
|
||||||
|
|
||||||
|
Trials per point: 30
|
||||||
|
Detection threshold: 0.450
|
||||||
|
|
||||||
|
Eb/N0: 6.0 dB
|
||||||
|
Maximum absolute CFO: 0.0 Hz
|
||||||
|
Success without CFO correction: 12
|
||||||
|
Success with CFO correction: 10
|
||||||
|
FER without correction: 0.600000
|
||||||
|
FER with correction: 0.666667
|
||||||
|
CFO RMSE: 4.642 Hz
|
||||||
|
|
||||||
|
Eb/N0: 6.0 dB
|
||||||
|
Maximum absolute CFO: 250.0 Hz
|
||||||
|
Success without CFO correction: 0
|
||||||
|
Success with CFO correction: 7
|
||||||
|
FER without correction: 1.000000
|
||||||
|
FER with correction: 0.766667
|
||||||
|
CFO RMSE: 8.605 Hz
|
||||||
|
|
||||||
|
Eb/N0: 6.0 dB
|
||||||
|
Maximum absolute CFO: 1000.0 Hz
|
||||||
|
Success without CFO correction: 0
|
||||||
|
Success with CFO correction: 10
|
||||||
|
FER without correction: 1.000000
|
||||||
|
FER with correction: 0.666667
|
||||||
|
CFO RMSE: 5.834 Hz
|
||||||
|
|
||||||
|
Eb/N0: 7.0 dB
|
||||||
|
Maximum absolute CFO: 0.0 Hz
|
||||||
|
Success without CFO correction: 22
|
||||||
|
Success with CFO correction: 19
|
||||||
|
FER without correction: 0.266667
|
||||||
|
FER with correction: 0.366667
|
||||||
|
CFO RMSE: 6.288 Hz
|
||||||
|
|
||||||
|
Eb/N0: 7.0 dB
|
||||||
|
Maximum absolute CFO: 250.0 Hz
|
||||||
|
Success without CFO correction: 0
|
||||||
|
Success with CFO correction: 16
|
||||||
|
FER without correction: 1.000000
|
||||||
|
FER with correction: 0.466667
|
||||||
|
CFO RMSE: 5.920 Hz
|
||||||
|
|
||||||
|
Eb/N0: 7.0 dB
|
||||||
|
Maximum absolute CFO: 1000.0 Hz
|
||||||
|
Success without CFO correction: 0
|
||||||
|
Success with CFO correction: 15
|
||||||
|
FER without correction: 1.000000
|
||||||
|
FER with correction: 0.500000
|
||||||
|
CFO RMSE: 6.259 Hz
|
||||||
|
|
||||||
|
Eb/N0: 8.0 dB
|
||||||
|
Maximum absolute CFO: 0.0 Hz
|
||||||
|
Success without CFO correction: 29
|
||||||
|
Success with CFO correction: 28
|
||||||
|
FER without correction: 0.033333
|
||||||
|
FER with correction: 0.066667
|
||||||
|
CFO RMSE: 4.337 Hz
|
||||||
|
|
||||||
|
Eb/N0: 8.0 dB
|
||||||
|
Maximum absolute CFO: 250.0 Hz
|
||||||
|
Success without CFO correction: 3
|
||||||
|
Success with CFO correction: 25
|
||||||
|
FER without correction: 0.900000
|
||||||
|
FER with correction: 0.166667
|
||||||
|
CFO RMSE: 3.800 Hz
|
||||||
|
|
||||||
|
Eb/N0: 8.0 dB
|
||||||
|
Maximum absolute CFO: 1000.0 Hz
|
||||||
|
Success without CFO correction: 0
|
||||||
|
Success with CFO correction: 27
|
||||||
|
FER without correction: 1.000000
|
||||||
|
FER with correction: 0.100000
|
||||||
|
CFO RMSE: 4.464 Hz
|
||||||
|
|
||||||
|
Eb/N0: 10.0 dB
|
||||||
|
Maximum absolute CFO: 0.0 Hz
|
||||||
|
Success without CFO correction: 30
|
||||||
|
Success with CFO correction: 30
|
||||||
|
FER without correction: 0.000000
|
||||||
|
FER with correction: 0.000000
|
||||||
|
CFO RMSE: 3.679 Hz
|
||||||
|
|
||||||
|
Eb/N0: 10.0 dB
|
||||||
|
Maximum absolute CFO: 250.0 Hz
|
||||||
|
Success without CFO correction: 0
|
||||||
|
Success with CFO correction: 30
|
||||||
|
FER without correction: 1.000000
|
||||||
|
FER with correction: 0.000000
|
||||||
|
CFO RMSE: 3.879 Hz
|
||||||
|
|
||||||
|
Eb/N0: 10.0 dB
|
||||||
|
Maximum absolute CFO: 1000.0 Hz
|
||||||
|
Success without CFO correction: 0
|
||||||
|
Success with CFO correction: 29
|
||||||
|
FER without correction: 1.000000
|
||||||
|
FER with correction: 0.033333
|
||||||
|
CFO RMSE: 3.882 Hz
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
eb_n0_db,max_abs_cfo_hz,trials,success_without_cfo,success_with_cfo,detection_fail_count,fer_without_cfo,fer_with_cfo,mean_correlation,mean_abs_frequency_error_hz,frequency_rmse_hz
|
||||||
|
6.0,0.0,30,12,10,0,0.6,0.6666666666666667,0.876037158456595,3.9418970310617256,4.6418505363579925
|
||||||
|
6.0,250.0,30,0,7,0,1.0,0.7666666666666666,0.855520150369621,6.514990457450565,8.605182044644604
|
||||||
|
6.0,1000.0,30,0,10,0,1.0,0.6666666666666667,0.8536588011138438,4.622721985654384,5.833544274473631
|
||||||
|
7.0,0.0,30,22,19,0,0.2666666666666667,0.3666666666666667,0.8904198516865948,5.0932685092625904,6.288037182687809
|
||||||
|
7.0,250.0,30,0,16,0,1.0,0.4666666666666667,0.9051298885277537,4.734329281741164,5.920393525914459
|
||||||
|
7.0,1000.0,30,0,15,0,1.0,0.5,0.8993182923641181,4.926709522385838,6.258581300752829
|
||||||
|
8.0,0.0,30,29,28,0,0.033333333333333326,0.06666666666666665,0.9242694247736734,3.4370575935466303,4.336980161418882
|
||||||
|
8.0,250.0,30,3,25,0,0.9,0.16666666666666663,0.9295977117848586,3.0564854854589365,3.8003732773040912
|
||||||
|
8.0,1000.0,30,0,27,0,1.0,0.09999999999999998,0.9299247907073535,3.630759939907249,4.464491877765011
|
||||||
|
10.0,0.0,30,30,30,0,0.0,0.0,0.9540049007144441,2.953454502291149,3.678772529679632
|
||||||
|
10.0,250.0,30,0,30,0,1.0,0.0,0.9544231119966623,3.134578534251591,3.8788146229760336
|
||||||
|
10.0,1000.0,30,0,29,0,1.0,0.033333333333333326,0.9530487035097676,3.0232518700094944,3.8817678666309656
|
||||||
|
BIN
Projects/SDR_Rover/data/processed/lab023/lab023_guarded_cfo.png
Normal file
|
After Width: | Height: | Size: 260 KiB |
@@ -0,0 +1,101 @@
|
|||||||
|
Lab023. Guarded CFO correction
|
||||||
|
|
||||||
|
Trials per point: 200
|
||||||
|
CFO dead zone: 15.00 Hz
|
||||||
|
Minimum phase consistency: 0.550
|
||||||
|
|
||||||
|
Eb/N0: 6.0 dB
|
||||||
|
Maximum absolute CFO: 0.0 Hz
|
||||||
|
FER phase only: 0.460000
|
||||||
|
FER always CFO: 0.575000
|
||||||
|
FER guarded CFO: 0.460000
|
||||||
|
Guard apply rate: 0.000000
|
||||||
|
CFO RMSE: 5.203 Hz
|
||||||
|
|
||||||
|
Eb/N0: 6.0 dB
|
||||||
|
Maximum absolute CFO: 250.0 Hz
|
||||||
|
FER phase only: 0.985000
|
||||||
|
FER always CFO: 0.610000
|
||||||
|
FER guarded CFO: 0.625000
|
||||||
|
Guard apply rate: 0.925000
|
||||||
|
CFO RMSE: 5.448 Hz
|
||||||
|
|
||||||
|
Eb/N0: 6.0 dB
|
||||||
|
Maximum absolute CFO: 1000.0 Hz
|
||||||
|
FER phase only: 0.995000
|
||||||
|
FER always CFO: 0.585000
|
||||||
|
FER guarded CFO: 0.585000
|
||||||
|
Guard apply rate: 0.990000
|
||||||
|
CFO RMSE: 8.473 Hz
|
||||||
|
|
||||||
|
Eb/N0: 7.0 dB
|
||||||
|
Maximum absolute CFO: 0.0 Hz
|
||||||
|
FER phase only: 0.200000
|
||||||
|
FER always CFO: 0.320000
|
||||||
|
FER guarded CFO: 0.200000
|
||||||
|
Guard apply rate: 0.000000
|
||||||
|
CFO RMSE: 4.745 Hz
|
||||||
|
|
||||||
|
Eb/N0: 7.0 dB
|
||||||
|
Maximum absolute CFO: 250.0 Hz
|
||||||
|
FER phase only: 0.955000
|
||||||
|
FER always CFO: 0.275000
|
||||||
|
FER guarded CFO: 0.335000
|
||||||
|
Guard apply rate: 0.855000
|
||||||
|
CFO RMSE: 4.950 Hz
|
||||||
|
|
||||||
|
Eb/N0: 7.0 dB
|
||||||
|
Maximum absolute CFO: 1000.0 Hz
|
||||||
|
FER phase only: 0.990000
|
||||||
|
FER always CFO: 0.275000
|
||||||
|
FER guarded CFO: 0.280000
|
||||||
|
Guard apply rate: 0.975000
|
||||||
|
CFO RMSE: 5.127 Hz
|
||||||
|
|
||||||
|
Eb/N0: 8.0 dB
|
||||||
|
Maximum absolute CFO: 0.0 Hz
|
||||||
|
FER phase only: 0.040000
|
||||||
|
FER always CFO: 0.110000
|
||||||
|
FER guarded CFO: 0.040000
|
||||||
|
Guard apply rate: 0.000000
|
||||||
|
CFO RMSE: 4.277 Hz
|
||||||
|
|
||||||
|
Eb/N0: 8.0 dB
|
||||||
|
Maximum absolute CFO: 250.0 Hz
|
||||||
|
FER phase only: 0.970000
|
||||||
|
FER always CFO: 0.125000
|
||||||
|
FER guarded CFO: 0.205000
|
||||||
|
Guard apply rate: 0.880000
|
||||||
|
CFO RMSE: 4.534 Hz
|
||||||
|
|
||||||
|
Eb/N0: 8.0 dB
|
||||||
|
Maximum absolute CFO: 1000.0 Hz
|
||||||
|
FER phase only: 0.995000
|
||||||
|
FER always CFO: 0.105000
|
||||||
|
FER guarded CFO: 0.110000
|
||||||
|
Guard apply rate: 0.990000
|
||||||
|
CFO RMSE: 4.749 Hz
|
||||||
|
|
||||||
|
Eb/N0: 10.0 dB
|
||||||
|
Maximum absolute CFO: 0.0 Hz
|
||||||
|
FER phase only: 0.000000
|
||||||
|
FER always CFO: 0.000000
|
||||||
|
FER guarded CFO: 0.000000
|
||||||
|
Guard apply rate: 0.000000
|
||||||
|
CFO RMSE: 3.345 Hz
|
||||||
|
|
||||||
|
Eb/N0: 10.0 dB
|
||||||
|
Maximum absolute CFO: 250.0 Hz
|
||||||
|
FER phase only: 0.960000
|
||||||
|
FER always CFO: 0.000000
|
||||||
|
FER guarded CFO: 0.045000
|
||||||
|
Guard apply rate: 0.915000
|
||||||
|
CFO RMSE: 3.305 Hz
|
||||||
|
|
||||||
|
Eb/N0: 10.0 dB
|
||||||
|
Maximum absolute CFO: 1000.0 Hz
|
||||||
|
FER phase only: 0.990000
|
||||||
|
FER always CFO: 0.000000
|
||||||
|
FER guarded CFO: 0.005000
|
||||||
|
Guard apply rate: 0.985000
|
||||||
|
CFO RMSE: 3.508 Hz
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
eb_n0_db,max_abs_cfo_hz,trials,success_phase_only,success_always_cfo,success_guarded_cfo,fer_phase_only,fer_always_cfo,fer_guarded_cfo,guarded_apply_count,guarded_apply_rate,frequency_rmse_hz,mean_phase_consistency
|
||||||
|
6.0,0.0,200,108,85,108,0.45999999999999996,0.575,0.45999999999999996,0,0.0,5.202816842065136,0.8803685059021725
|
||||||
|
6.0,250.0,200,3,78,75,0.985,0.61,0.625,185,0.925,5.448336211547444,0.8820273978803879
|
||||||
|
6.0,1000.0,200,1,83,83,0.995,0.585,0.585,198,0.99,8.473194122480628,0.8815296088302559
|
||||||
|
7.0,0.0,200,160,136,160,0.19999999999999996,0.31999999999999995,0.19999999999999996,0,0.0,4.744964108330319,0.9044304298907866
|
||||||
|
7.0,250.0,200,9,145,133,0.955,0.275,0.33499999999999996,171,0.855,4.949784071638471,0.9048829617513402
|
||||||
|
7.0,1000.0,200,2,145,144,0.99,0.275,0.28,195,0.975,5.126941255642888,0.9037603894844779
|
||||||
|
8.0,0.0,200,192,178,192,0.040000000000000036,0.10999999999999999,0.040000000000000036,0,0.0,4.277056721707394,0.9222860828489162
|
||||||
|
8.0,250.0,200,6,175,159,0.97,0.125,0.20499999999999996,176,0.88,4.534455139069744,0.9233618150972334
|
||||||
|
8.0,1000.0,200,1,179,178,0.995,0.10499999999999998,0.10999999999999999,198,0.99,4.749146564125825,0.9260637045844348
|
||||||
|
10.0,0.0,200,200,200,200,0.0,0.0,0.0,0,0.0,3.3448810933215865,0.9511723197247827
|
||||||
|
10.0,250.0,200,8,200,191,0.96,0.0,0.04500000000000004,183,0.915,3.3052349602075664,0.9514399539276094
|
||||||
|
10.0,1000.0,200,2,200,199,0.99,0.0,0.0050000000000000044,197,0.985,3.5081991053844495,0.9505037355131278
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
frequency_hz,frequency_mhz,relative_power_db,prominence_db
|
||||||
|
98840137.685,98.840138,-16.363,14.939
|
||||||
|
99201367.853,99.201368,-8.578,21.049
|
||||||
|
99603613.612,99.603614,-5.852,194.148
|
||||||
|
100099316.323,100.099316,-0.641,27.938
|
||||||
|
100498925.365,100.498925,0.000,200.000
|
||||||
|
100896483.628,100.896484,-2.662,25.928
|
||||||
|
101159569.346,101.159569,-11.137,18.231
|
||||||
|
19
Projects/SDR_Rover/data/processed/lab024b/lab024b_report.txt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Lab024B. Усреднённый спектр Pluto+ методом Уэлча
|
||||||
|
|
||||||
|
URI: ip:192.168.2.1
|
||||||
|
Центральная частота: 100.000 МГц
|
||||||
|
Частота дискретизации: 2.400 Мвыб/с
|
||||||
|
Полоса RX: 2.000 МГц
|
||||||
|
Количество IQ-сэмплов: 1048576
|
||||||
|
Размер сегмента Уэлча: 8192
|
||||||
|
Перекрытие сегментов: 4096
|
||||||
|
|
||||||
|
Найдено кандидатов: 7
|
||||||
|
|
||||||
|
01. 98.840138 МГц, уровень -16.36 дБ, выраженность 14.94 дБ
|
||||||
|
02. 99.201368 МГц, уровень -8.58 дБ, выраженность 21.05 дБ
|
||||||
|
03. 99.603614 МГц, уровень -5.85 дБ, выраженность 194.15 дБ
|
||||||
|
04. 100.099316 МГц, уровень -0.64 дБ, выраженность 27.94 дБ
|
||||||
|
05. 100.498925 МГц, уровень 0.00 дБ, выраженность 200.00 дБ
|
||||||
|
06. 100.896484 МГц, уровень -2.66 дБ, выраженность 25.93 дБ
|
||||||
|
07. 101.159569 МГц, уровень -11.14 дБ, выраженность 18.23 дБ
|
||||||
8193
Projects/SDR_Rover/data/processed/lab024b/lab024b_welch_spectrum.csv
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
Projects/SDR_Rover/data/raw/lab007_source.jpg
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
Projects/SDR_Rover/data/raw/lab009_source.jpg
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
Projects/SDR_Rover/data/raw/lab011_scene.jpg
Normal file
|
After Width: | Height: | Size: 793 KiB |
BIN
Projects/SDR_Rover/data/raw/lab012_safety_scene.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
13
Projects/SDR_Rover/data/raw/lab024a_pluto_rx_100mhz.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"pluto_uri": "ip:192.168.2.1",
|
||||||
|
"center_frequency_hz": 100000000,
|
||||||
|
"sample_rate_hz": 2399998,
|
||||||
|
"rx_bandwidth_hz": 2000000,
|
||||||
|
"gain_control_mode": "slow_attack",
|
||||||
|
"sample_count": 262144,
|
||||||
|
"sample_dtype": "complex64",
|
||||||
|
"mean_i": 0.010410308837890625,
|
||||||
|
"mean_q": -0.01348876953125,
|
||||||
|
"rms": 506.9608154296875,
|
||||||
|
"peak": 1436.5299072265625
|
||||||
|
}
|
||||||
BIN
Projects/SDR_Rover/data/raw/lab024a_pluto_rx_100mhz.npy
Normal file
BIN
Projects/SDR_Rover/data/raw/lab024a_pluto_rx_100mhz_spectrum.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
Projects/SDR_Rover/data/received/lab007_reassembled.jpg
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
Projects/SDR_Rover/data/received/lab008_received.jpg
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
0
Projects/SDR_Rover/docs/.gitkeep
Normal file
0
Projects/SDR_Rover/gnuradio/.gitkeep
Normal file
0
Projects/SDR_Rover/ground_station/.gitkeep
Normal file
0
Projects/SDR_Rover/protocol/.gitkeep
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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||||
|
)
|
||||||
0
Projects/SDR_Rover/rover/.gitkeep
Normal file
0
Projects/SDR_Rover/tests/.gitkeep
Normal file
0
Projects/SDR_Rover/tests/__init__.py
Normal file
54
Projects/SDR_Rover/tests/lab001_text_bytes.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
Lab001. Преобразование текста в байты и биты.
|
||||||
|
|
||||||
|
Цель:
|
||||||
|
1. Взять обычную текстовую строку.
|
||||||
|
2. Преобразовать её в байты UTF-8.
|
||||||
|
3. Показать каждый байт как десятичное число.
|
||||||
|
4. Показать каждый байт как восемь бит.
|
||||||
|
5. Восстановить исходный текст.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Исходное сообщение
|
||||||
|
message = "HELLO SDR"
|
||||||
|
|
||||||
|
# Кодирование текста в байты.
|
||||||
|
# UTF-8 — способ представить символы числами.
|
||||||
|
encoded_message = message.encode("utf-8")
|
||||||
|
|
||||||
|
# Преобразование каждого байта в строку из восьми бит.
|
||||||
|
binary_message = " ".join(
|
||||||
|
f"{byte:08b}" for byte in encoded_message
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обратное преобразование байтов в текст.
|
||||||
|
decoded_message = encoded_message.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
print("Исходный текст:")
|
||||||
|
print(message)
|
||||||
|
|
||||||
|
print("\nОбъект bytes:")
|
||||||
|
print(encoded_message)
|
||||||
|
|
||||||
|
print("\nБайты в десятичном виде:")
|
||||||
|
print(list(encoded_message))
|
||||||
|
|
||||||
|
print("\nБайты в двоичном виде:")
|
||||||
|
print(binary_message)
|
||||||
|
|
||||||
|
print("\nПодробно по каждому символу:")
|
||||||
|
for character, byte_value in zip(message, encoded_message):
|
||||||
|
print(
|
||||||
|
f"{character!r:>4} "
|
||||||
|
f"→ число {byte_value:3d} "
|
||||||
|
f"→ биты {byte_value:08b}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nВосстановленный текст:")
|
||||||
|
print(decoded_message)
|
||||||
|
|
||||||
|
# Автоматическая проверка результата.
|
||||||
|
assert decoded_message == message
|
||||||
|
|
||||||
|
print("\nПроверка пройдена: исходный текст восстановлен без ошибок.")
|
||||||
61
Projects/SDR_Rover/tests/lab001_utf8_russian.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
Lab001, часть 2.
|
||||||
|
Русский текст, символы, байты и кодировка UTF-8.
|
||||||
|
|
||||||
|
Цель:
|
||||||
|
1. Увидеть разницу между количеством символов и количеством байтов.
|
||||||
|
2. Посмотреть, сколько байтов занимает каждая русская буква.
|
||||||
|
3. Восстановить исходный текст из байтов.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Исходное сообщение
|
||||||
|
message = "ПРИВЕТ SDR"
|
||||||
|
|
||||||
|
# Преобразование текста в байты UTF-8
|
||||||
|
encoded_message = message.encode("utf-8")
|
||||||
|
|
||||||
|
# Обратное преобразование байтов в текст
|
||||||
|
decoded_message = encoded_message.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
print("Исходный текст:")
|
||||||
|
print(message)
|
||||||
|
|
||||||
|
print("\nКоличество символов:")
|
||||||
|
print(len(message))
|
||||||
|
|
||||||
|
print("\nКоличество байтов UTF-8:")
|
||||||
|
print(len(encoded_message))
|
||||||
|
|
||||||
|
print("\nОбъект bytes:")
|
||||||
|
print(encoded_message)
|
||||||
|
|
||||||
|
print("\nВсе байты в десятичном виде:")
|
||||||
|
print(list(encoded_message))
|
||||||
|
|
||||||
|
print("\nПодробно по каждому символу:")
|
||||||
|
|
||||||
|
for character in message:
|
||||||
|
character_bytes = character.encode("utf-8")
|
||||||
|
|
||||||
|
decimal_bytes = list(character_bytes)
|
||||||
|
|
||||||
|
binary_bytes = " ".join(
|
||||||
|
f"{byte:08b}" for byte in character_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{character!r:>4} "
|
||||||
|
f"→ байтов: {len(character_bytes)} "
|
||||||
|
f"→ числа: {decimal_bytes} "
|
||||||
|
f"→ биты: {binary_bytes}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nВосстановленный текст:")
|
||||||
|
print(decoded_message)
|
||||||
|
|
||||||
|
# Автоматические проверки
|
||||||
|
assert decoded_message == message
|
||||||
|
assert len(encoded_message) >= len(message)
|
||||||
|
|
||||||
|
print("\nПроверка пройдена: русский текст восстановлен без ошибок.")
|
||||||
153
Projects/SDR_Rover/tests/lab002_bit_corruption.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
"""
|
||||||
|
Lab002, часть 2.
|
||||||
|
Повреждение одного бита пакета без контроля целостности.
|
||||||
|
|
||||||
|
Цель:
|
||||||
|
1. Сформировать цифровой пакет.
|
||||||
|
2. Намеренно изменить один бит полезной нагрузки.
|
||||||
|
3. Разобрать повреждённый пакет.
|
||||||
|
4. Убедиться, что без CRC повреждение не обнаруживается.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Константы протокола
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
SYNC_WORD = 0xAA55
|
||||||
|
PROTOCOL_VERSION = 1
|
||||||
|
MESSAGE_TYPE_TEXT = 1
|
||||||
|
|
||||||
|
HEADER_FORMAT = ">HBBHH"
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Формирование исходного пакета
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
message = "ПРИВЕТ SDR"
|
||||||
|
payload = message.encode("utf-8")
|
||||||
|
|
||||||
|
sequence_number = 1
|
||||||
|
message_type = MESSAGE_TYPE_TEXT
|
||||||
|
payload_length = len(payload)
|
||||||
|
|
||||||
|
header = struct.pack(
|
||||||
|
HEADER_FORMAT,
|
||||||
|
SYNC_WORD,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
|
message_type,
|
||||||
|
sequence_number,
|
||||||
|
payload_length,
|
||||||
|
)
|
||||||
|
|
||||||
|
packet = header + payload
|
||||||
|
|
||||||
|
|
||||||
|
print("Исходное сообщение:")
|
||||||
|
print(message)
|
||||||
|
|
||||||
|
print("\nИсходный пакет:")
|
||||||
|
print(packet.hex(" "))
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Имитация повреждения пакета
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# bytes нельзя изменять напрямую.
|
||||||
|
# Поэтому преобразуем пакет в изменяемый массив bytearray.
|
||||||
|
corrupted_packet = bytearray(packet)
|
||||||
|
|
||||||
|
header_size = struct.calcsize(HEADER_FORMAT)
|
||||||
|
|
||||||
|
# В строке "ПРИВЕТ SDR":
|
||||||
|
#
|
||||||
|
# "ПРИВЕТ" занимает 12 байтов UTF-8,
|
||||||
|
# пробел занимает 1 байт,
|
||||||
|
# буква S находится по индексу 13 внутри PAYLOAD.
|
||||||
|
payload_byte_index = 13
|
||||||
|
|
||||||
|
# Полный индекс внутри пакета:
|
||||||
|
packet_byte_index = header_size + payload_byte_index
|
||||||
|
|
||||||
|
original_byte = corrupted_packet[packet_byte_index]
|
||||||
|
|
||||||
|
# XOR с 0x01 изменяет младший бит:
|
||||||
|
#
|
||||||
|
# 0x53 = 01010011 = S
|
||||||
|
# 0x52 = 01010010 = R
|
||||||
|
corrupted_packet[packet_byte_index] ^= 0x01
|
||||||
|
|
||||||
|
corrupted_byte = corrupted_packet[packet_byte_index]
|
||||||
|
|
||||||
|
|
||||||
|
print("\nИзменяемый байт внутри пакета:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"До повреждения: "
|
||||||
|
f"{original_byte:3d} "
|
||||||
|
f"= 0x{original_byte:02X} "
|
||||||
|
f"= {original_byte:08b}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"После повреждения: "
|
||||||
|
f"{corrupted_byte:3d} "
|
||||||
|
f"= 0x{corrupted_byte:02X} "
|
||||||
|
f"= {corrupted_byte:08b}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nПовреждённый пакет:")
|
||||||
|
print(bytes(corrupted_packet).hex(" "))
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Разбор повреждённого пакета
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
received_header = corrupted_packet[:header_size]
|
||||||
|
received_payload = corrupted_packet[header_size:]
|
||||||
|
|
||||||
|
(
|
||||||
|
received_sync,
|
||||||
|
received_version,
|
||||||
|
received_type,
|
||||||
|
received_sequence,
|
||||||
|
received_length,
|
||||||
|
) = struct.unpack(HEADER_FORMAT, received_header)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверки структуры
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
assert received_sync == SYNC_WORD
|
||||||
|
assert received_version == PROTOCOL_VERSION
|
||||||
|
assert received_type == MESSAGE_TYPE_TEXT
|
||||||
|
assert received_sequence == sequence_number
|
||||||
|
assert received_length == len(received_payload)
|
||||||
|
|
||||||
|
print("\nВсе проверки заголовка пройдены.")
|
||||||
|
|
||||||
|
restored_message = bytes(received_payload).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
print("\nСообщение после повреждения:")
|
||||||
|
print(restored_message)
|
||||||
|
|
||||||
|
print("\nИсходное сообщение:")
|
||||||
|
print(message)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверка результата эксперимента
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
assert restored_message != message
|
||||||
|
|
||||||
|
print("\nПовреждение данных произошло.")
|
||||||
|
print("Но заголовок и длина пакета остались правильными.")
|
||||||
|
print("Без CRC приёмник не смог определить ошибку.")
|
||||||
146
Projects/SDR_Rover/tests/lab002_packet_structure.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
"""
|
||||||
|
Lab002. Формирование и разбор первого цифрового пакета.
|
||||||
|
|
||||||
|
Пакет содержит:
|
||||||
|
- маркер начала;
|
||||||
|
- версию протокола;
|
||||||
|
- тип сообщения;
|
||||||
|
- порядковый номер;
|
||||||
|
- длину полезных данных;
|
||||||
|
- полезные данные.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Константы протокола
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
SYNC_WORD = 0xAA55
|
||||||
|
PROTOCOL_VERSION = 1
|
||||||
|
|
||||||
|
MESSAGE_TYPE_TEXT = 1
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Исходные данные
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
message = "ПРИВЕТ SDR"
|
||||||
|
payload = message.encode("utf-8")
|
||||||
|
|
||||||
|
sequence_number = 1
|
||||||
|
message_type = MESSAGE_TYPE_TEXT
|
||||||
|
payload_length = len(payload)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Формирование заголовка
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# Формат:
|
||||||
|
# > — порядок байтов от старшего к младшему, big-endian
|
||||||
|
# H — беззнаковое целое число размером 2 байта
|
||||||
|
# B — беззнаковое целое число размером 1 байт
|
||||||
|
# B — ещё одно число размером 1 байт
|
||||||
|
# H — порядковый номер размером 2 байта
|
||||||
|
# H — длина данных размером 2 байта
|
||||||
|
HEADER_FORMAT = ">HBBHH"
|
||||||
|
|
||||||
|
header = struct.pack(
|
||||||
|
HEADER_FORMAT,
|
||||||
|
SYNC_WORD,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
|
message_type,
|
||||||
|
sequence_number,
|
||||||
|
payload_length,
|
||||||
|
)
|
||||||
|
|
||||||
|
packet = header + payload
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод сформированного пакета
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print("Исходное сообщение:")
|
||||||
|
print(message)
|
||||||
|
|
||||||
|
print("\nПолезная нагрузка PAYLOAD:")
|
||||||
|
print(payload)
|
||||||
|
|
||||||
|
print("\nДлина PAYLOAD:")
|
||||||
|
print(payload_length, "байт")
|
||||||
|
|
||||||
|
print("\nЗаголовок пакета:")
|
||||||
|
print(header)
|
||||||
|
|
||||||
|
print("\nПолный пакет:")
|
||||||
|
print(packet)
|
||||||
|
|
||||||
|
print("\nПакет в шестнадцатеричном виде:")
|
||||||
|
print(packet.hex(" "))
|
||||||
|
|
||||||
|
print("\nОбщая длина пакета:")
|
||||||
|
print(len(packet), "байт")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Разбор пакета на стороне приёмника
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
header_size = struct.calcsize(HEADER_FORMAT)
|
||||||
|
|
||||||
|
received_header = packet[:header_size]
|
||||||
|
received_payload = packet[header_size:]
|
||||||
|
|
||||||
|
(
|
||||||
|
received_sync,
|
||||||
|
received_version,
|
||||||
|
received_type,
|
||||||
|
received_sequence,
|
||||||
|
received_length,
|
||||||
|
) = struct.unpack(HEADER_FORMAT, received_header)
|
||||||
|
|
||||||
|
|
||||||
|
print("\n--- Разбор принятого пакета ---")
|
||||||
|
|
||||||
|
print("SYNC:")
|
||||||
|
print(hex(received_sync))
|
||||||
|
|
||||||
|
print("\nВерсия протокола:")
|
||||||
|
print(received_version)
|
||||||
|
|
||||||
|
print("\nТип сообщения:")
|
||||||
|
print(received_type)
|
||||||
|
|
||||||
|
print("\nПорядковый номер:")
|
||||||
|
print(received_sequence)
|
||||||
|
|
||||||
|
print("\nДлина из заголовка:")
|
||||||
|
print(received_length, "байт")
|
||||||
|
|
||||||
|
print("\nФактически принято данных:")
|
||||||
|
print(len(received_payload), "байт")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
assert received_sync == SYNC_WORD
|
||||||
|
assert received_version == PROTOCOL_VERSION
|
||||||
|
assert received_type == MESSAGE_TYPE_TEXT
|
||||||
|
assert received_sequence == sequence_number
|
||||||
|
assert received_length == len(received_payload)
|
||||||
|
|
||||||
|
restored_message = received_payload.decode("utf-8")
|
||||||
|
|
||||||
|
assert restored_message == message
|
||||||
|
|
||||||
|
|
||||||
|
print("\nВосстановленное сообщение:")
|
||||||
|
print(restored_message)
|
||||||
|
|
||||||
|
print("\nПроверка пройдена: пакет сформирован и разобран без ошибок.")
|
||||||
198
Projects/SDR_Rover/tests/lab003_crc32.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
"""
|
||||||
|
Lab003. Обнаружение повреждения пакета с помощью CRC-32.
|
||||||
|
|
||||||
|
Цель:
|
||||||
|
1. Сформировать пакет с CRC-32.
|
||||||
|
2. Проверить исправный пакет.
|
||||||
|
3. Изменить один бит полезной нагрузки.
|
||||||
|
4. Убедиться, что CRC обнаруживает повреждение.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Константы протокола
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
SYNC_WORD = 0xAA55
|
||||||
|
PROTOCOL_VERSION = 1
|
||||||
|
MESSAGE_TYPE_TEXT = 1
|
||||||
|
|
||||||
|
HEADER_FORMAT = ">HBBHH"
|
||||||
|
CRC_FORMAT = ">I"
|
||||||
|
|
||||||
|
CRC_SIZE = struct.calcsize(CRC_FORMAT)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Формирование исходного пакета
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
message = "ПРИВЕТ SDR"
|
||||||
|
payload = message.encode("utf-8")
|
||||||
|
|
||||||
|
sequence_number = 1
|
||||||
|
payload_length = len(payload)
|
||||||
|
|
||||||
|
header = struct.pack(
|
||||||
|
HEADER_FORMAT,
|
||||||
|
SYNC_WORD,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
|
MESSAGE_TYPE_TEXT,
|
||||||
|
sequence_number,
|
||||||
|
payload_length,
|
||||||
|
)
|
||||||
|
|
||||||
|
# CRC вычисляется по заголовку и полезной нагрузке.
|
||||||
|
packet_without_crc = header + payload
|
||||||
|
|
||||||
|
crc_value = zlib.crc32(packet_without_crc) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
crc_bytes = struct.pack(
|
||||||
|
CRC_FORMAT,
|
||||||
|
crc_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
packet = packet_without_crc + crc_bytes
|
||||||
|
|
||||||
|
|
||||||
|
print("Исходное сообщение:")
|
||||||
|
print(message)
|
||||||
|
|
||||||
|
print("\nCRC-32 исходного пакета:")
|
||||||
|
print(f"0x{crc_value:08X}")
|
||||||
|
|
||||||
|
print("\nПолный пакет:")
|
||||||
|
print(packet.hex(" "))
|
||||||
|
|
||||||
|
print("\nДлина полного пакета:")
|
||||||
|
print(len(packet), "байт")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверка исправного пакета
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
received_data = packet[:-CRC_SIZE]
|
||||||
|
received_crc_bytes = packet[-CRC_SIZE:]
|
||||||
|
|
||||||
|
(received_crc,) = struct.unpack(
|
||||||
|
CRC_FORMAT,
|
||||||
|
received_crc_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
calculated_crc = zlib.crc32(received_data) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
print("\n--- Проверка исправного пакета ---")
|
||||||
|
|
||||||
|
print(f"CRC из пакета: 0x{received_crc:08X}")
|
||||||
|
print(f"CRC вычисленный: 0x{calculated_crc:08X}")
|
||||||
|
|
||||||
|
assert received_crc == calculated_crc
|
||||||
|
|
||||||
|
print("Результат: пакет не повреждён.")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Имитация повреждения одного бита
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
corrupted_packet = bytearray(packet)
|
||||||
|
|
||||||
|
header_size = struct.calcsize(HEADER_FORMAT)
|
||||||
|
|
||||||
|
# Буква S находится по индексу 13 внутри PAYLOAD.
|
||||||
|
payload_byte_index = 13
|
||||||
|
packet_byte_index = header_size + payload_byte_index
|
||||||
|
|
||||||
|
original_byte = corrupted_packet[packet_byte_index]
|
||||||
|
|
||||||
|
# Меняем младший бит:
|
||||||
|
# S = 0x53
|
||||||
|
# R = 0x52
|
||||||
|
corrupted_packet[packet_byte_index] ^= 0x01
|
||||||
|
|
||||||
|
corrupted_byte = corrupted_packet[packet_byte_index]
|
||||||
|
|
||||||
|
|
||||||
|
print("\n--- Повреждение одного бита ---")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"До повреждения: "
|
||||||
|
f"0x{original_byte:02X} = {original_byte:08b}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"После повреждения: "
|
||||||
|
f"0x{corrupted_byte:02X} = {corrupted_byte:08b}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверка повреждённого пакета
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
corrupted_data = bytes(
|
||||||
|
corrupted_packet[:-CRC_SIZE]
|
||||||
|
)
|
||||||
|
|
||||||
|
corrupted_crc_bytes = bytes(
|
||||||
|
corrupted_packet[-CRC_SIZE:]
|
||||||
|
)
|
||||||
|
|
||||||
|
(received_crc_after_corruption,) = struct.unpack(
|
||||||
|
CRC_FORMAT,
|
||||||
|
corrupted_crc_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
calculated_crc_after_corruption = (
|
||||||
|
zlib.crc32(corrupted_data) & 0xFFFFFFFF
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\n--- Проверка повреждённого пакета ---")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"CRC из пакета: "
|
||||||
|
f"0x{received_crc_after_corruption:08X}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"CRC вычисленный: "
|
||||||
|
f"0x{calculated_crc_after_corruption:08X}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if received_crc_after_corruption != calculated_crc_after_corruption:
|
||||||
|
print("\nРезультат: CRC обнаружил повреждение пакета.")
|
||||||
|
else:
|
||||||
|
print("\nРезультат: повреждение не обнаружено.")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Показываем повреждённое сообщение
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
corrupted_payload = corrupted_data[header_size:]
|
||||||
|
|
||||||
|
corrupted_message = corrupted_payload.decode("utf-8")
|
||||||
|
|
||||||
|
print("\nСообщение внутри повреждённого пакета:")
|
||||||
|
print(corrupted_message)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Автоматические проверки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
assert corrupted_message == "ПРИВЕТ RDR"
|
||||||
|
|
||||||
|
assert (
|
||||||
|
received_crc_after_corruption
|
||||||
|
!= calculated_crc_after_corruption
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nПроверка пройдена: CRC-32 обнаружил изменение одного бита.")
|
||||||
120
Projects/SDR_Rover/tests/lab004_protocol_module.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Created on Fri Jul 10 17:19:11 2026
|
||||||
|
|
||||||
|
@author: user
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
Lab004. Проверка модуля protocol.packet.
|
||||||
|
|
||||||
|
Проверяем:
|
||||||
|
1. Формирование пакета.
|
||||||
|
2. Разбор исправного пакета.
|
||||||
|
3. Восстановление текста.
|
||||||
|
4. Обнаружение повреждения CRC.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from protocol.packet import (
|
||||||
|
CRCError,
|
||||||
|
HEADER_SIZE,
|
||||||
|
MESSAGE_TYPE_TEXT,
|
||||||
|
build_packet,
|
||||||
|
parse_packet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Формирование пакета
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
message = "ПРИВЕТ SDR"
|
||||||
|
payload = message.encode("utf-8")
|
||||||
|
|
||||||
|
sequence_number = 7
|
||||||
|
|
||||||
|
packet = build_packet(
|
||||||
|
payload=payload,
|
||||||
|
message_type=MESSAGE_TYPE_TEXT,
|
||||||
|
sequence_number=sequence_number,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("Исходное сообщение:")
|
||||||
|
print(message)
|
||||||
|
|
||||||
|
print("\nСформированный пакет:")
|
||||||
|
print(packet.hex(" "))
|
||||||
|
|
||||||
|
print("\nРазмер пакета:")
|
||||||
|
print(len(packet), "байт")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Разбор исправного пакета
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
parsed_packet = parse_packet(packet)
|
||||||
|
|
||||||
|
restored_message = parsed_packet.payload.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
print("\n--- Разобранный пакет ---")
|
||||||
|
|
||||||
|
print("Версия:")
|
||||||
|
print(parsed_packet.version)
|
||||||
|
|
||||||
|
print("\nТип сообщения:")
|
||||||
|
print(parsed_packet.message_type)
|
||||||
|
|
||||||
|
print("\nПорядковый номер:")
|
||||||
|
print(parsed_packet.sequence_number)
|
||||||
|
|
||||||
|
print("\nПолезная нагрузка:")
|
||||||
|
print(parsed_packet.payload)
|
||||||
|
|
||||||
|
print("\nВосстановленное сообщение:")
|
||||||
|
print(restored_message)
|
||||||
|
|
||||||
|
|
||||||
|
assert parsed_packet.message_type == MESSAGE_TYPE_TEXT
|
||||||
|
assert parsed_packet.sequence_number == sequence_number
|
||||||
|
assert restored_message == message
|
||||||
|
|
||||||
|
print("\nИсправный пакет успешно разобран.")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Повреждение одного бита
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
corrupted_packet = bytearray(packet)
|
||||||
|
|
||||||
|
# В строке "ПРИВЕТ SDR" буква S находится
|
||||||
|
# по индексу 13 внутри полезной нагрузки.
|
||||||
|
payload_byte_index = 13
|
||||||
|
|
||||||
|
packet_byte_index = (
|
||||||
|
HEADER_SIZE + payload_byte_index
|
||||||
|
)
|
||||||
|
|
||||||
|
corrupted_packet[packet_byte_index] ^= 0x01
|
||||||
|
|
||||||
|
|
||||||
|
print("\n--- Проверка повреждённого пакета ---")
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
parse_packet(corrupted_packet)
|
||||||
|
|
||||||
|
except CRCError as error:
|
||||||
|
print("Повреждение обнаружено.")
|
||||||
|
print(error)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise AssertionError(
|
||||||
|
"Ошибка: повреждённый пакет был принят как исправный"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nВсе проверки Lab004 успешно выполнены.")
|
||||||
254
Projects/SDR_Rover/tests/lab005_virtual_channel.py
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
"""
|
||||||
|
Lab005. Передача пакетов через виртуальный радиоканал.
|
||||||
|
|
||||||
|
Эксперимент:
|
||||||
|
1. Формируем 100 цифровых пакетов.
|
||||||
|
2. Передаём их через виртуальный канал.
|
||||||
|
3. Канал может:
|
||||||
|
- полностью потерять пакет;
|
||||||
|
- изменить отдельные биты.
|
||||||
|
4. Приёмник проверяет структуру пакета и CRC-32.
|
||||||
|
5. Выводится статистика доставки.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
from protocol.packet import (
|
||||||
|
CRCError,
|
||||||
|
MESSAGE_TYPE_TEXT,
|
||||||
|
PacketError,
|
||||||
|
build_packet,
|
||||||
|
parse_packet,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tools.virtual_channel import transmit
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Настройки эксперимента
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# Количество передаваемых пакетов.
|
||||||
|
PACKET_COUNT = 100
|
||||||
|
|
||||||
|
# Вероятность полной потери одного пакета.
|
||||||
|
#
|
||||||
|
# 0.10 означает 10 %.
|
||||||
|
DROP_PROBABILITY = 0.10
|
||||||
|
|
||||||
|
# Вероятность повреждения каждого отдельного бита.
|
||||||
|
#
|
||||||
|
# 0.002 означает:
|
||||||
|
# 0,2 % вероятности ошибки для каждого бита.
|
||||||
|
BIT_ERROR_PROBABILITY = 0.002
|
||||||
|
|
||||||
|
# Генератор случайных чисел.
|
||||||
|
#
|
||||||
|
# Число 2026 позволяет получать одинаковый результат
|
||||||
|
# при каждом запуске программы.
|
||||||
|
random_generator = random.Random(2026)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Счётчики результатов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
sent_packets = 0
|
||||||
|
|
||||||
|
# Пакеты, полностью исчезнувшие в канале.
|
||||||
|
dropped_packets = 0
|
||||||
|
|
||||||
|
# Доставленные, но повреждённые пакеты.
|
||||||
|
rejected_packets = 0
|
||||||
|
|
||||||
|
# Пакеты, правильно принятые и разобранные.
|
||||||
|
received_ok = 0
|
||||||
|
|
||||||
|
# Общее количество изменённых битов.
|
||||||
|
total_flipped_bits = 0
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Передача серии пакетов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
for sequence_number in range(PACKET_COUNT):
|
||||||
|
|
||||||
|
# Создаём уникальное сообщение для каждого пакета.
|
||||||
|
message = f"ПАКЕТ {sequence_number}"
|
||||||
|
|
||||||
|
# Текст превращается в байты UTF-8.
|
||||||
|
payload = message.encode("utf-8")
|
||||||
|
|
||||||
|
# Формируем полный пакет с заголовком и CRC-32.
|
||||||
|
packet = build_packet(
|
||||||
|
payload=payload,
|
||||||
|
message_type=MESSAGE_TYPE_TEXT,
|
||||||
|
sequence_number=sequence_number,
|
||||||
|
)
|
||||||
|
|
||||||
|
sent_packets += 1
|
||||||
|
|
||||||
|
# Передаём пакет через виртуальный канал.
|
||||||
|
received_packet, flipped_bits = transmit(
|
||||||
|
packet=packet,
|
||||||
|
drop_probability=DROP_PROBABILITY,
|
||||||
|
bit_error_probability=BIT_ERROR_PROBABILITY,
|
||||||
|
random_generator=random_generator,
|
||||||
|
)
|
||||||
|
|
||||||
|
total_flipped_bits += flipped_bits
|
||||||
|
|
||||||
|
# None означает, что пакет полностью потерян.
|
||||||
|
if received_packet is None:
|
||||||
|
dropped_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Пытаемся разобрать принятый пакет.
|
||||||
|
try:
|
||||||
|
parsed_packet = parse_packet(received_packet)
|
||||||
|
|
||||||
|
except (CRCError, PacketError):
|
||||||
|
# CRCError:
|
||||||
|
# контрольная сумма не совпала.
|
||||||
|
#
|
||||||
|
# PacketError:
|
||||||
|
# повреждены SYNC, версия, длина
|
||||||
|
# или другая часть структуры.
|
||||||
|
rejected_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Восстанавливаем текст из полезной нагрузки.
|
||||||
|
restored_message = parsed_packet.payload.decode("utf-8")
|
||||||
|
|
||||||
|
# Проверяем, что получен именно тот пакет,
|
||||||
|
# который был передан.
|
||||||
|
assert parsed_packet.sequence_number == sequence_number
|
||||||
|
assert restored_message == message
|
||||||
|
|
||||||
|
received_ok += 1
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод настроек
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print("=== Lab005. Виртуальный канал ===")
|
||||||
|
|
||||||
|
print("\nНастройки эксперимента:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Количество пакетов:",
|
||||||
|
PACKET_COUNT,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Вероятность полной потери пакета:",
|
||||||
|
DROP_PROBABILITY,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Вероятность ошибки отдельного бита:",
|
||||||
|
BIT_ERROR_PROBABILITY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод результатов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print("\nРезультаты:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Передано пакетов:",
|
||||||
|
sent_packets,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Потеряно полностью:",
|
||||||
|
dropped_packets,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Отклонено из-за повреждений:",
|
||||||
|
rejected_packets,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Принято правильно:",
|
||||||
|
received_ok,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Всего изменено битов:",
|
||||||
|
total_flipped_bits,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Расчёт процентов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
delivery_rate = (
|
||||||
|
received_ok
|
||||||
|
/ sent_packets
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
|
||||||
|
drop_rate = (
|
||||||
|
dropped_packets
|
||||||
|
/ sent_packets
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
|
||||||
|
rejection_rate = (
|
||||||
|
rejected_packets
|
||||||
|
/ sent_packets
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
|
||||||
|
packet_failure_rate = (
|
||||||
|
dropped_packets
|
||||||
|
+ rejected_packets
|
||||||
|
) / sent_packets * 100
|
||||||
|
|
||||||
|
|
||||||
|
print("\nСтатистика:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Успешно доставлено: "
|
||||||
|
f"{delivery_rate:.1f} %"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Потеряно полностью: "
|
||||||
|
f"{drop_rate:.1f} %"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Отклонено приёмником: "
|
||||||
|
f"{rejection_rate:.1f} %"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Общая доля недоставленных пакетов: "
|
||||||
|
f"{packet_failure_rate:.1f} %"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Контроль правильности
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
accounted_packets = (
|
||||||
|
received_ok
|
||||||
|
+ dropped_packets
|
||||||
|
+ rejected_packets
|
||||||
|
)
|
||||||
|
|
||||||
|
assert accounted_packets == sent_packets
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\nПроверка пройдена: "
|
||||||
|
"все переданные пакеты учтены."
|
||||||
|
)
|
||||||
325
Projects/SDR_Rover/tests/lab006_stop_and_wait_arq.py
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
"""
|
||||||
|
Lab006. Надёжная передача пакетов методом Stop-and-Wait ARQ.
|
||||||
|
|
||||||
|
Передатчик:
|
||||||
|
1. Отправляет пакет.
|
||||||
|
2. Ожидает подтверждение ACK.
|
||||||
|
3. При отсутствии правильного ACK повторяет передачу.
|
||||||
|
4. После исчерпания попыток считает сообщение недоставленным.
|
||||||
|
|
||||||
|
Приёмник:
|
||||||
|
1. Проверяет пакет.
|
||||||
|
2. Отбрасывает повреждённые пакеты.
|
||||||
|
3. Отправляет ACK для правильного пакета.
|
||||||
|
4. Не передаёт приложению повторные копии одного сообщения.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
from protocol.packet import (
|
||||||
|
CRCError,
|
||||||
|
MESSAGE_TYPE_ACK,
|
||||||
|
MESSAGE_TYPE_TEXT,
|
||||||
|
PacketError,
|
||||||
|
build_packet,
|
||||||
|
parse_packet,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tools.virtual_channel import transmit
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Настройки эксперимента
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
MESSAGE_COUNT = 100
|
||||||
|
|
||||||
|
# Максимальное количество передач одного сообщения.
|
||||||
|
MAX_ATTEMPTS = 5
|
||||||
|
|
||||||
|
# Параметры канала от передатчика к приёмнику.
|
||||||
|
DATA_DROP_PROBABILITY = 0.10
|
||||||
|
DATA_BIT_ERROR_PROBABILITY = 0.002
|
||||||
|
|
||||||
|
# Параметры обратного канала для ACK.
|
||||||
|
ACK_DROP_PROBABILITY = 0.10
|
||||||
|
ACK_BIT_ERROR_PROBABILITY = 0.002
|
||||||
|
|
||||||
|
# Фиксированное число делает эксперимент повторяемым.
|
||||||
|
random_generator = random.Random(2026)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Счётчики
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
delivered_messages = 0
|
||||||
|
failed_messages = 0
|
||||||
|
|
||||||
|
data_transmissions = 0
|
||||||
|
ack_transmissions = 0
|
||||||
|
|
||||||
|
duplicate_packets = 0
|
||||||
|
|
||||||
|
invalid_data_packets = 0
|
||||||
|
invalid_ack_packets = 0
|
||||||
|
|
||||||
|
dropped_data_packets = 0
|
||||||
|
dropped_ack_packets = 0
|
||||||
|
|
||||||
|
# Номера пакетов, уже переданных приложению приёмника.
|
||||||
|
received_sequences = set()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Передача сообщений
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
for sequence_number in range(MESSAGE_COUNT):
|
||||||
|
|
||||||
|
message = f"ПАКЕТ {sequence_number}"
|
||||||
|
payload = message.encode("utf-8")
|
||||||
|
|
||||||
|
data_packet = build_packet(
|
||||||
|
payload=payload,
|
||||||
|
message_type=MESSAGE_TYPE_TEXT,
|
||||||
|
sequence_number=sequence_number,
|
||||||
|
)
|
||||||
|
|
||||||
|
message_confirmed = False
|
||||||
|
|
||||||
|
# Повторяем передачу до MAX_ATTEMPTS раз.
|
||||||
|
for attempt_number in range(1, MAX_ATTEMPTS + 1):
|
||||||
|
|
||||||
|
data_transmissions += 1
|
||||||
|
|
||||||
|
received_data_packet, _ = transmit(
|
||||||
|
packet=data_packet,
|
||||||
|
drop_probability=DATA_DROP_PROBABILITY,
|
||||||
|
bit_error_probability=DATA_BIT_ERROR_PROBABILITY,
|
||||||
|
random_generator=random_generator,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Пакет полностью потерялся.
|
||||||
|
if received_data_packet is None:
|
||||||
|
dropped_data_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Приёмник проверяет пакет.
|
||||||
|
try:
|
||||||
|
parsed_data = parse_packet(received_data_packet)
|
||||||
|
|
||||||
|
except (CRCError, PacketError):
|
||||||
|
invalid_data_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Проверка типа сообщения.
|
||||||
|
if parsed_data.message_type != MESSAGE_TYPE_TEXT:
|
||||||
|
invalid_data_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Проверяем, не получали ли мы этот пакет ранее.
|
||||||
|
if parsed_data.sequence_number in received_sequences:
|
||||||
|
duplicate_packets += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
restored_message = parsed_data.payload.decode("utf-8")
|
||||||
|
|
||||||
|
assert restored_message == message
|
||||||
|
|
||||||
|
received_sequences.add(
|
||||||
|
parsed_data.sequence_number
|
||||||
|
)
|
||||||
|
|
||||||
|
# Формируем подтверждение.
|
||||||
|
#
|
||||||
|
# ACK имеет тот же sequence_number,
|
||||||
|
# что и подтверждаемый пакет.
|
||||||
|
ack_packet = build_packet(
|
||||||
|
payload=b"",
|
||||||
|
message_type=MESSAGE_TYPE_ACK,
|
||||||
|
sequence_number=parsed_data.sequence_number,
|
||||||
|
)
|
||||||
|
|
||||||
|
ack_transmissions += 1
|
||||||
|
|
||||||
|
received_ack_packet, _ = transmit(
|
||||||
|
packet=ack_packet,
|
||||||
|
drop_probability=ACK_DROP_PROBABILITY,
|
||||||
|
bit_error_probability=ACK_BIT_ERROR_PROBABILITY,
|
||||||
|
random_generator=random_generator,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ACK полностью потерялся.
|
||||||
|
if received_ack_packet is None:
|
||||||
|
dropped_ack_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Передатчик проверяет ACK.
|
||||||
|
try:
|
||||||
|
parsed_ack = parse_packet(received_ack_packet)
|
||||||
|
|
||||||
|
except (CRCError, PacketError):
|
||||||
|
invalid_ack_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ACK должен иметь правильный тип и номер.
|
||||||
|
if (
|
||||||
|
parsed_ack.message_type != MESSAGE_TYPE_ACK
|
||||||
|
or parsed_ack.sequence_number != sequence_number
|
||||||
|
):
|
||||||
|
invalid_ack_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Подтверждение получено.
|
||||||
|
message_confirmed = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if message_confirmed:
|
||||||
|
delivered_messages += 1
|
||||||
|
else:
|
||||||
|
failed_messages += 1
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Расчёт статистики
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
delivery_rate = (
|
||||||
|
delivered_messages
|
||||||
|
/ MESSAGE_COUNT
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
|
||||||
|
failure_rate = (
|
||||||
|
failed_messages
|
||||||
|
/ MESSAGE_COUNT
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
|
||||||
|
average_data_transmissions = (
|
||||||
|
data_transmissions
|
||||||
|
/ MESSAGE_COUNT
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод результата
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print("=== Lab006. Stop-and-Wait ARQ ===")
|
||||||
|
|
||||||
|
print("\nНастройки:")
|
||||||
|
|
||||||
|
print("Количество сообщений:", MESSAGE_COUNT)
|
||||||
|
print("Максимум попыток:", MAX_ATTEMPTS)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Вероятность потери DATA:",
|
||||||
|
DATA_DROP_PROBABILITY,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Вероятность битовой ошибки DATA:",
|
||||||
|
DATA_BIT_ERROR_PROBABILITY,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Вероятность потери ACK:",
|
||||||
|
ACK_DROP_PROBABILITY,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Вероятность битовой ошибки ACK:",
|
||||||
|
ACK_BIT_ERROR_PROBABILITY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nОсновные результаты:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Успешно подтверждено сообщений:",
|
||||||
|
delivered_messages,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Не подтверждено сообщений:",
|
||||||
|
failed_messages,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Доля успешной доставки: "
|
||||||
|
f"{delivery_rate:.1f} %"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Доля недоставленных сообщений: "
|
||||||
|
f"{failure_rate:.1f} %"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nПередачи:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Всего передач DATA:",
|
||||||
|
data_transmissions,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Всего передач ACK:",
|
||||||
|
ack_transmissions,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Среднее число передач DATA "
|
||||||
|
f"на одно сообщение: "
|
||||||
|
f"{average_data_transmissions:.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nОшибки канала:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Полностью потеряно DATA:",
|
||||||
|
dropped_data_packets,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Повреждено DATA:",
|
||||||
|
invalid_data_packets,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Полностью потеряно ACK:",
|
||||||
|
dropped_ack_packets,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Повреждено ACK:",
|
||||||
|
invalid_ack_packets,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Получено повторных DATA-пакетов:",
|
||||||
|
duplicate_packets,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Автоматические проверки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
assert (
|
||||||
|
delivered_messages
|
||||||
|
+ failed_messages
|
||||||
|
== MESSAGE_COUNT
|
||||||
|
)
|
||||||
|
|
||||||
|
assert delivered_messages <= len(received_sequences)
|
||||||
|
|
||||||
|
assert data_transmissions >= MESSAGE_COUNT
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\nПроверка пройдена: "
|
||||||
|
"все сообщения учтены."
|
||||||
|
)
|
||||||
270
Projects/SDR_Rover/tests/lab007_image_fragmentation.py
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
"""
|
||||||
|
Lab007. Разбиение JPEG на фрагменты и обратная сборка.
|
||||||
|
|
||||||
|
Этапы:
|
||||||
|
1. Создание тестового JPEG.
|
||||||
|
2. Чтение файла как последовательности байтов.
|
||||||
|
3. Разбиение изображения на фрагменты.
|
||||||
|
4. Формирование цифрового пакета для каждого фрагмента.
|
||||||
|
5. Перемешивание порядка доставки пакетов.
|
||||||
|
6. Проверка CRC и разбор пакетов.
|
||||||
|
7. Обратная сборка изображения.
|
||||||
|
8. Сравнение контрольных сумм файлов.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from hashlib import sha256
|
||||||
|
from pathlib import Path
|
||||||
|
import random
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from protocol.image_fragments import (
|
||||||
|
decode_image_fragment,
|
||||||
|
encode_image_fragment,
|
||||||
|
reassemble_image,
|
||||||
|
split_image_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
from protocol.packet import (
|
||||||
|
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||||
|
build_packet,
|
||||||
|
parse_packet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Настройки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
IMAGE_WIDTH = 320
|
||||||
|
IMAGE_HEIGHT = 240
|
||||||
|
|
||||||
|
IMAGE_ID = 20260713
|
||||||
|
|
||||||
|
FRAGMENT_DATA_SIZE = 512
|
||||||
|
|
||||||
|
random_generator = random.Random(2026)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Пути к файлам
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
source_directory = Path("data/raw")
|
||||||
|
received_directory = Path("data/received")
|
||||||
|
|
||||||
|
source_directory.mkdir(
|
||||||
|
parents=True,
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
received_directory.mkdir(
|
||||||
|
parents=True,
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
source_path = (
|
||||||
|
source_directory
|
||||||
|
/ "lab007_source.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
received_path = (
|
||||||
|
received_directory
|
||||||
|
/ "lab007_reassembled.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Создание тестового JPEG
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
image = Image.new(
|
||||||
|
"RGB",
|
||||||
|
(IMAGE_WIDTH, IMAGE_HEIGHT),
|
||||||
|
)
|
||||||
|
|
||||||
|
pixels = image.load()
|
||||||
|
|
||||||
|
for y in range(IMAGE_HEIGHT):
|
||||||
|
for x in range(IMAGE_WIDTH):
|
||||||
|
|
||||||
|
red = (
|
||||||
|
x * 255
|
||||||
|
// (IMAGE_WIDTH - 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
green = (
|
||||||
|
y * 255
|
||||||
|
// (IMAGE_HEIGHT - 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
blue = (
|
||||||
|
(x + y) * 255
|
||||||
|
// (
|
||||||
|
IMAGE_WIDTH
|
||||||
|
+ IMAGE_HEIGHT
|
||||||
|
- 2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
pixels[x, y] = (
|
||||||
|
red,
|
||||||
|
green,
|
||||||
|
blue,
|
||||||
|
)
|
||||||
|
|
||||||
|
image.save(
|
||||||
|
source_path,
|
||||||
|
format="JPEG",
|
||||||
|
quality=60,
|
||||||
|
optimize=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Чтение изображения
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
source_bytes = source_path.read_bytes()
|
||||||
|
|
||||||
|
source_hash = sha256(
|
||||||
|
source_bytes
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Разбиение изображения на фрагменты
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
fragments = split_image_bytes(
|
||||||
|
image_bytes=source_bytes,
|
||||||
|
image_id=IMAGE_ID,
|
||||||
|
fragment_data_size=FRAGMENT_DATA_SIZE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Формирование цифровых пакетов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
packets = []
|
||||||
|
|
||||||
|
for fragment in fragments:
|
||||||
|
|
||||||
|
fragment_payload = encode_image_fragment(
|
||||||
|
fragment
|
||||||
|
)
|
||||||
|
|
||||||
|
packet = build_packet(
|
||||||
|
payload=fragment_payload,
|
||||||
|
message_type=MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||||
|
sequence_number=fragment.fragment_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
packets.append(packet)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Имитация доставки в произвольном порядке
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
random_generator.shuffle(packets)
|
||||||
|
|
||||||
|
received_fragments = []
|
||||||
|
|
||||||
|
for packet in packets:
|
||||||
|
|
||||||
|
parsed_packet = parse_packet(packet)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
parsed_packet.message_type
|
||||||
|
== MESSAGE_TYPE_IMAGE_FRAGMENT
|
||||||
|
)
|
||||||
|
|
||||||
|
fragment = decode_image_fragment(
|
||||||
|
parsed_packet.payload
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
parsed_packet.sequence_number
|
||||||
|
== fragment.fragment_index
|
||||||
|
)
|
||||||
|
|
||||||
|
received_fragments.append(fragment)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Сборка изображения
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
received_bytes = reassemble_image(
|
||||||
|
received_fragments
|
||||||
|
)
|
||||||
|
|
||||||
|
received_path.write_bytes(
|
||||||
|
received_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
received_hash = sha256(
|
||||||
|
received_bytes
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
assert received_bytes == source_bytes
|
||||||
|
assert received_hash == source_hash
|
||||||
|
|
||||||
|
with Image.open(received_path) as received_image:
|
||||||
|
received_image.verify()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Результаты
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print("=== Lab007. Фрагментация изображения ===")
|
||||||
|
|
||||||
|
print("\nИсходный файл:")
|
||||||
|
print(source_path)
|
||||||
|
|
||||||
|
print("\nВосстановленный файл:")
|
||||||
|
print(received_path)
|
||||||
|
|
||||||
|
print("\nРазмер изображения:")
|
||||||
|
print(
|
||||||
|
IMAGE_WIDTH,
|
||||||
|
"x",
|
||||||
|
IMAGE_HEIGHT,
|
||||||
|
"пикселей",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nРазмер JPEG:")
|
||||||
|
print(
|
||||||
|
len(source_bytes),
|
||||||
|
"байт",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nРазмер данных одного фрагмента:")
|
||||||
|
print(
|
||||||
|
FRAGMENT_DATA_SIZE,
|
||||||
|
"байт",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nКоличество фрагментов:")
|
||||||
|
print(
|
||||||
|
len(fragments)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nSHA-256 исходного файла:")
|
||||||
|
print(source_hash)
|
||||||
|
|
||||||
|
print("\nSHA-256 восстановленного файла:")
|
||||||
|
print(received_hash)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\nПроверка пройдена: "
|
||||||
|
"JPEG восстановлен байт в байт."
|
||||||
|
)
|
||||||
428
Projects/SDR_Rover/tests/lab008_image_transfer_arq.py
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
"""
|
||||||
|
Lab008. Передача JPEG через виртуальный канал с ARQ.
|
||||||
|
|
||||||
|
Этапы:
|
||||||
|
1. Чтение тестового JPEG из Lab007.
|
||||||
|
2. Разбиение файла на фрагменты.
|
||||||
|
3. Передача каждого фрагмента через виртуальный канал.
|
||||||
|
4. Проверка CRC на приёмнике.
|
||||||
|
5. Передача ACK по обратному каналу.
|
||||||
|
6. Повторная передача при потере или повреждении.
|
||||||
|
7. Сборка принятого JPEG.
|
||||||
|
8. Сравнение SHA-256 исходного и принятого файлов.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from hashlib import sha256
|
||||||
|
from pathlib import Path
|
||||||
|
import random
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from protocol.image_fragments import (
|
||||||
|
ImageFragmentError,
|
||||||
|
decode_image_fragment,
|
||||||
|
encode_image_fragment,
|
||||||
|
reassemble_image,
|
||||||
|
split_image_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
from protocol.packet import (
|
||||||
|
CRCError,
|
||||||
|
MESSAGE_TYPE_ACK,
|
||||||
|
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||||
|
PacketError,
|
||||||
|
build_packet,
|
||||||
|
parse_packet,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tools.virtual_channel import transmit
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Настройки эксперимента
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
IMAGE_ID = 20260713
|
||||||
|
FRAGMENT_DATA_SIZE = 512
|
||||||
|
|
||||||
|
# Максимальное количество передач одного фрагмента.
|
||||||
|
MAX_ATTEMPTS = 12
|
||||||
|
|
||||||
|
# Прямой канал: передатчик → приёмник.
|
||||||
|
DATA_DROP_PROBABILITY = 0.10
|
||||||
|
DATA_BIT_ERROR_PROBABILITY = 0.0002
|
||||||
|
|
||||||
|
# Обратный канал: приёмник → передатчик.
|
||||||
|
ACK_DROP_PROBABILITY = 0.10
|
||||||
|
ACK_BIT_ERROR_PROBABILITY = 0.0002
|
||||||
|
|
||||||
|
# Фиксированное значение делает результат воспроизводимым.
|
||||||
|
random_generator = random.Random(2026)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Пути к файлам
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
source_path = Path(
|
||||||
|
"data/raw/lab007_source.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
received_path = Path(
|
||||||
|
"data/received/lab008_received.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not source_path.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
"Не найден файл data/raw/lab007_source.jpg. "
|
||||||
|
"Сначала необходимо выполнить Lab007."
|
||||||
|
)
|
||||||
|
|
||||||
|
received_path.parent.mkdir(
|
||||||
|
parents=True,
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Чтение и разбиение изображения
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
source_bytes = source_path.read_bytes()
|
||||||
|
|
||||||
|
source_hash = sha256(
|
||||||
|
source_bytes
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
source_fragments = split_image_bytes(
|
||||||
|
image_bytes=source_bytes,
|
||||||
|
image_id=IMAGE_ID,
|
||||||
|
fragment_data_size=FRAGMENT_DATA_SIZE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Счётчики
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
data_transmissions = 0
|
||||||
|
ack_transmissions = 0
|
||||||
|
|
||||||
|
dropped_data_packets = 0
|
||||||
|
invalid_data_packets = 0
|
||||||
|
|
||||||
|
dropped_ack_packets = 0
|
||||||
|
invalid_ack_packets = 0
|
||||||
|
|
||||||
|
duplicate_data_packets = 0
|
||||||
|
|
||||||
|
flipped_data_bits = 0
|
||||||
|
flipped_ack_bits = 0
|
||||||
|
|
||||||
|
confirmed_fragments = 0
|
||||||
|
failed_fragment_indexes = []
|
||||||
|
|
||||||
|
# Принятые фрагменты хранятся по их номеру.
|
||||||
|
received_fragments = {}
|
||||||
|
|
||||||
|
# Число попыток передачи каждого фрагмента.
|
||||||
|
attempts_per_fragment = {}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Передача всех фрагментов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
for source_fragment in source_fragments:
|
||||||
|
|
||||||
|
fragment_index = source_fragment.fragment_index
|
||||||
|
|
||||||
|
fragment_payload = encode_image_fragment(
|
||||||
|
source_fragment
|
||||||
|
)
|
||||||
|
|
||||||
|
data_packet = build_packet(
|
||||||
|
payload=fragment_payload,
|
||||||
|
message_type=MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||||
|
sequence_number=fragment_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
fragment_confirmed = False
|
||||||
|
|
||||||
|
for attempt_number in range(
|
||||||
|
1,
|
||||||
|
MAX_ATTEMPTS + 1,
|
||||||
|
):
|
||||||
|
data_transmissions += 1
|
||||||
|
|
||||||
|
received_data_packet, changed_bits = transmit(
|
||||||
|
packet=data_packet,
|
||||||
|
drop_probability=DATA_DROP_PROBABILITY,
|
||||||
|
bit_error_probability=DATA_BIT_ERROR_PROBABILITY,
|
||||||
|
random_generator=random_generator,
|
||||||
|
)
|
||||||
|
|
||||||
|
flipped_data_bits += changed_bits
|
||||||
|
|
||||||
|
# DATA полностью потерян.
|
||||||
|
if received_data_packet is None:
|
||||||
|
dropped_data_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Проверка структуры и CRC.
|
||||||
|
try:
|
||||||
|
parsed_data_packet = parse_packet(
|
||||||
|
received_data_packet
|
||||||
|
)
|
||||||
|
|
||||||
|
except (CRCError, PacketError):
|
||||||
|
invalid_data_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
parsed_data_packet.message_type
|
||||||
|
!= MESSAGE_TYPE_IMAGE_FRAGMENT
|
||||||
|
):
|
||||||
|
invalid_data_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Разбор заголовка фрагмента изображения.
|
||||||
|
try:
|
||||||
|
received_fragment = decode_image_fragment(
|
||||||
|
parsed_data_packet.payload
|
||||||
|
)
|
||||||
|
|
||||||
|
except ImageFragmentError:
|
||||||
|
invalid_data_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Проверяем согласованность номеров.
|
||||||
|
if (
|
||||||
|
parsed_data_packet.sequence_number
|
||||||
|
!= received_fragment.fragment_index
|
||||||
|
):
|
||||||
|
invalid_data_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if received_fragment.image_id != IMAGE_ID:
|
||||||
|
invalid_data_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Сохраняем новый фрагмент либо распознаём дубликат.
|
||||||
|
existing_fragment = received_fragments.get(
|
||||||
|
received_fragment.fragment_index
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing_fragment is None:
|
||||||
|
received_fragments[
|
||||||
|
received_fragment.fragment_index
|
||||||
|
] = received_fragment
|
||||||
|
|
||||||
|
else:
|
||||||
|
# DATA мог прийти повторно, если предыдущий ACK потерялся.
|
||||||
|
assert existing_fragment.data == received_fragment.data
|
||||||
|
duplicate_data_packets += 1
|
||||||
|
|
||||||
|
# ====================================================
|
||||||
|
# Формирование ACK
|
||||||
|
# ====================================================
|
||||||
|
|
||||||
|
ack_packet = build_packet(
|
||||||
|
payload=b"",
|
||||||
|
message_type=MESSAGE_TYPE_ACK,
|
||||||
|
sequence_number=received_fragment.fragment_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
ack_transmissions += 1
|
||||||
|
|
||||||
|
received_ack_packet, changed_ack_bits = transmit(
|
||||||
|
packet=ack_packet,
|
||||||
|
drop_probability=ACK_DROP_PROBABILITY,
|
||||||
|
bit_error_probability=ACK_BIT_ERROR_PROBABILITY,
|
||||||
|
random_generator=random_generator,
|
||||||
|
)
|
||||||
|
|
||||||
|
flipped_ack_bits += changed_ack_bits
|
||||||
|
|
||||||
|
# ACK полностью потерян.
|
||||||
|
if received_ack_packet is None:
|
||||||
|
dropped_ack_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Передатчик проверяет ACK.
|
||||||
|
try:
|
||||||
|
parsed_ack = parse_packet(
|
||||||
|
received_ack_packet
|
||||||
|
)
|
||||||
|
|
||||||
|
except (CRCError, PacketError):
|
||||||
|
invalid_ack_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
parsed_ack.message_type != MESSAGE_TYPE_ACK
|
||||||
|
or parsed_ack.sequence_number != fragment_index
|
||||||
|
or parsed_ack.payload != b""
|
||||||
|
):
|
||||||
|
invalid_ack_packets += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Фрагмент успешно подтверждён.
|
||||||
|
fragment_confirmed = True
|
||||||
|
confirmed_fragments += 1
|
||||||
|
|
||||||
|
attempts_per_fragment[
|
||||||
|
fragment_index
|
||||||
|
] = attempt_number
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
if not fragment_confirmed:
|
||||||
|
failed_fragment_indexes.append(
|
||||||
|
fragment_index
|
||||||
|
)
|
||||||
|
|
||||||
|
attempts_per_fragment[
|
||||||
|
fragment_index
|
||||||
|
] = MAX_ATTEMPTS
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Результаты передачи
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print("=== Lab008. Передача JPEG с ARQ ===")
|
||||||
|
|
||||||
|
print("\nИсходный файл:")
|
||||||
|
print(source_path)
|
||||||
|
|
||||||
|
print("\nРазмер JPEG:")
|
||||||
|
print(len(source_bytes), "байт")
|
||||||
|
|
||||||
|
print("\nКоличество фрагментов:")
|
||||||
|
print(len(source_fragments))
|
||||||
|
|
||||||
|
print("\nПодтверждено фрагментов:")
|
||||||
|
print(confirmed_fragments)
|
||||||
|
|
||||||
|
print("\nПолучено уникальных фрагментов:")
|
||||||
|
print(len(received_fragments))
|
||||||
|
|
||||||
|
print("\nНе подтверждены фрагменты:")
|
||||||
|
print(failed_fragment_indexes)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nПередачи:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Всего передач DATA:",
|
||||||
|
data_transmissions,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Всего передач ACK:",
|
||||||
|
ack_transmissions,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nОшибки прямого канала:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Полностью потеряно DATA:",
|
||||||
|
dropped_data_packets,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Повреждено DATA:",
|
||||||
|
invalid_data_packets,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Изменено битов DATA:",
|
||||||
|
flipped_data_bits,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nОшибки обратного канала:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Полностью потеряно ACK:",
|
||||||
|
dropped_ack_packets,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Повреждено ACK:",
|
||||||
|
invalid_ack_packets,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Изменено битов ACK:",
|
||||||
|
flipped_ack_bits,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Получено повторных DATA:",
|
||||||
|
duplicate_data_packets,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nКоличество попыток по фрагментам:")
|
||||||
|
|
||||||
|
for fragment_index in sorted(
|
||||||
|
attempts_per_fragment
|
||||||
|
):
|
||||||
|
print(
|
||||||
|
f"Фрагмент {fragment_index}: "
|
||||||
|
f"{attempts_per_fragment[fragment_index]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Сборка принятого изображения
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
assert not failed_fragment_indexes
|
||||||
|
|
||||||
|
assert (
|
||||||
|
len(received_fragments)
|
||||||
|
== len(source_fragments)
|
||||||
|
)
|
||||||
|
|
||||||
|
received_bytes = reassemble_image(
|
||||||
|
received_fragments.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
received_path.write_bytes(
|
||||||
|
received_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
received_hash = sha256(
|
||||||
|
received_bytes
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Финальные проверки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
assert received_bytes == source_bytes
|
||||||
|
assert received_hash == source_hash
|
||||||
|
|
||||||
|
with Image.open(received_path) as image:
|
||||||
|
image.verify()
|
||||||
|
|
||||||
|
|
||||||
|
print("\nSHA-256 исходного JPEG:")
|
||||||
|
print(source_hash)
|
||||||
|
|
||||||
|
print("\nSHA-256 принятого JPEG:")
|
||||||
|
print(received_hash)
|
||||||
|
|
||||||
|
print("\nПринятый файл:")
|
||||||
|
print(received_path)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\nПроверка пройдена: "
|
||||||
|
"все фрагменты подтверждены, "
|
||||||
|
"JPEG восстановлен байт в байт."
|
||||||
|
)
|
||||||
450
Projects/SDR_Rover/tests/lab009_real_photo_estimate.py
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
"""
|
||||||
|
Lab009. Расчёт параметров передачи реальной фотографии.
|
||||||
|
|
||||||
|
Программа:
|
||||||
|
1. Читает реальный JPEG-файл.
|
||||||
|
2. Определяет его разрешение и размер.
|
||||||
|
3. Разбивает файл на фрагменты.
|
||||||
|
4. Формирует реальные пакеты протокола.
|
||||||
|
5. Учитывает ACK для каждого фрагмента.
|
||||||
|
6. Рассчитывает время передачи при разных скоростях канала.
|
||||||
|
|
||||||
|
Расчёт времени показывает чистое эфирное время без учёта:
|
||||||
|
- пауз между пакетами;
|
||||||
|
- обработки на передатчике и приёмнике;
|
||||||
|
- ожидания ACK;
|
||||||
|
- повторных передач;
|
||||||
|
- преамбулы и служебных символов физического уровня.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from protocol.image_fragments import (
|
||||||
|
FRAGMENT_HEADER_SIZE,
|
||||||
|
encode_image_fragment,
|
||||||
|
split_image_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
from protocol.packet import (
|
||||||
|
CRC_SIZE,
|
||||||
|
HEADER_SIZE,
|
||||||
|
MESSAGE_TYPE_ACK,
|
||||||
|
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||||
|
build_packet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Настройки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
SOURCE_PATH = Path(
|
||||||
|
"data/raw/lab009_source.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
IMAGE_ID = 20260713
|
||||||
|
|
||||||
|
# Сколько байтов JPEG помещается в один фрагмент.
|
||||||
|
FRAGMENT_DATA_SIZE = 512
|
||||||
|
|
||||||
|
# Скорости полезного цифрового канала в кбит/с.
|
||||||
|
BITRATES_KBPS = [
|
||||||
|
1,
|
||||||
|
5,
|
||||||
|
10,
|
||||||
|
20,
|
||||||
|
50,
|
||||||
|
100,
|
||||||
|
200,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вспомогательные функции
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def format_bytes(byte_count: int) -> str:
|
||||||
|
"""
|
||||||
|
Представить размер одновременно в байтах и килобайтах.
|
||||||
|
"""
|
||||||
|
|
||||||
|
kibibytes = byte_count / 1024
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"{byte_count} байт "
|
||||||
|
f"({kibibytes:.2f} КиБ)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_duration(seconds: float) -> str:
|
||||||
|
"""
|
||||||
|
Представить длительность в удобной форме.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if seconds < 1:
|
||||||
|
return f"{seconds * 1000:.1f} мс"
|
||||||
|
|
||||||
|
if seconds < 60:
|
||||||
|
return f"{seconds:.2f} с"
|
||||||
|
|
||||||
|
minutes = int(seconds // 60)
|
||||||
|
remaining_seconds = seconds % 60
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"{minutes} мин "
|
||||||
|
f"{remaining_seconds:.1f} с"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверка исходного файла
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
if not SOURCE_PATH.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Не найден файл: {SOURCE_PATH}"
|
||||||
|
)
|
||||||
|
|
||||||
|
source_bytes = SOURCE_PATH.read_bytes()
|
||||||
|
|
||||||
|
if not source_bytes:
|
||||||
|
raise ValueError(
|
||||||
|
"Исходный JPEG-файл пуст"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Чтение параметров изображения
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
with Image.open(SOURCE_PATH) as image:
|
||||||
|
|
||||||
|
image_format = image.format
|
||||||
|
image_mode = image.mode
|
||||||
|
image_width, image_height = image.size
|
||||||
|
|
||||||
|
# Проверяем, что файл действительно читается как изображение.
|
||||||
|
image.verify()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Разбиение JPEG на фрагменты
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
fragments = split_image_bytes(
|
||||||
|
image_bytes=source_bytes,
|
||||||
|
image_id=IMAGE_ID,
|
||||||
|
fragment_data_size=FRAGMENT_DATA_SIZE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Формирование DATA-пакетов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
data_packets = []
|
||||||
|
|
||||||
|
for fragment in fragments:
|
||||||
|
|
||||||
|
fragment_payload = encode_image_fragment(
|
||||||
|
fragment
|
||||||
|
)
|
||||||
|
|
||||||
|
packet = build_packet(
|
||||||
|
payload=fragment_payload,
|
||||||
|
message_type=MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||||
|
sequence_number=fragment.fragment_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
data_packets.append(packet)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Формирование типового ACK-пакета
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
ack_packet = build_packet(
|
||||||
|
payload=b"",
|
||||||
|
message_type=MESSAGE_TYPE_ACK,
|
||||||
|
sequence_number=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
ack_packet_size = len(ack_packet)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Расчёт объёмов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
source_size = len(source_bytes)
|
||||||
|
|
||||||
|
fragment_count = len(fragments)
|
||||||
|
|
||||||
|
# Полный размер всех DATA-пакетов.
|
||||||
|
total_data_packet_bytes = sum(
|
||||||
|
len(packet)
|
||||||
|
for packet in data_packets
|
||||||
|
)
|
||||||
|
|
||||||
|
# Для каждого DATA-пакета ожидается один ACK.
|
||||||
|
total_ack_bytes = (
|
||||||
|
fragment_count
|
||||||
|
* ack_packet_size
|
||||||
|
)
|
||||||
|
|
||||||
|
# Идеальный радиообмен:
|
||||||
|
# каждый DATA и каждый ACK переданы ровно один раз.
|
||||||
|
total_radio_bytes = (
|
||||||
|
total_data_packet_bytes
|
||||||
|
+ total_ack_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
total_radio_bits = (
|
||||||
|
total_radio_bytes
|
||||||
|
* 8
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Детализация служебных расходов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
total_fragment_header_bytes = (
|
||||||
|
fragment_count
|
||||||
|
* FRAGMENT_HEADER_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
data_protocol_overhead = (
|
||||||
|
fragment_count
|
||||||
|
* (HEADER_SIZE + CRC_SIZE)
|
||||||
|
)
|
||||||
|
|
||||||
|
total_overhead_bytes = (
|
||||||
|
total_radio_bytes
|
||||||
|
- source_size
|
||||||
|
)
|
||||||
|
|
||||||
|
payload_efficiency = (
|
||||||
|
source_size
|
||||||
|
/ total_radio_bytes
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод параметров фотографии
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print("=== Lab009. Расчёт передачи реальной фотографии ===")
|
||||||
|
|
||||||
|
print("\nИсходный файл:")
|
||||||
|
print(SOURCE_PATH)
|
||||||
|
|
||||||
|
print("\nФормат изображения:")
|
||||||
|
print(image_format)
|
||||||
|
|
||||||
|
print("\nЦветовой режим:")
|
||||||
|
print(image_mode)
|
||||||
|
|
||||||
|
print("\nРазрешение:")
|
||||||
|
print(
|
||||||
|
image_width,
|
||||||
|
"x",
|
||||||
|
image_height,
|
||||||
|
"пикселей",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nРазмер исходного JPEG:")
|
||||||
|
print(
|
||||||
|
format_bytes(source_size)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод структуры передачи
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print("\n--- Фрагментация ---")
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Размер данных одного полного фрагмента:",
|
||||||
|
FRAGMENT_DATA_SIZE,
|
||||||
|
"байт",
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Количество фрагментов:",
|
||||||
|
fragment_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Размер заголовка фрагмента:",
|
||||||
|
FRAGMENT_HEADER_SIZE,
|
||||||
|
"байт",
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Размер заголовка основного пакета:",
|
||||||
|
HEADER_SIZE,
|
||||||
|
"байт",
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Размер CRC-32:",
|
||||||
|
CRC_SIZE,
|
||||||
|
"байт",
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Размер ACK-пакета:",
|
||||||
|
ack_packet_size,
|
||||||
|
"байт",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод полного объёма
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print("\n--- Объём передачи без повторов ---")
|
||||||
|
|
||||||
|
print("Исходные данные JPEG:")
|
||||||
|
print(
|
||||||
|
format_bytes(source_size)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nЗаголовки фрагментов:")
|
||||||
|
print(
|
||||||
|
format_bytes(total_fragment_header_bytes)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nСлужебные поля и CRC DATA-пакетов:")
|
||||||
|
print(
|
||||||
|
format_bytes(data_protocol_overhead)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nВсе DATA-пакеты:")
|
||||||
|
print(
|
||||||
|
format_bytes(total_data_packet_bytes)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nВсе ACK-пакеты:")
|
||||||
|
print(
|
||||||
|
format_bytes(total_ack_bytes)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nПолный радиообмен DATA + ACK:")
|
||||||
|
print(
|
||||||
|
format_bytes(total_radio_bytes)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nОбщие служебные расходы:")
|
||||||
|
print(
|
||||||
|
format_bytes(total_overhead_bytes)
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\nПолезная эффективность протокола:"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{payload_efficiency:.2f} %"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Расчёт времени передачи
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print("\n--- Чистое эфирное время ---")
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Скорость DATA без ACK DATA + ACK"
|
||||||
|
)
|
||||||
|
|
||||||
|
for bitrate_kbps in BITRATES_KBPS:
|
||||||
|
|
||||||
|
bitrate_bits_per_second = (
|
||||||
|
bitrate_kbps
|
||||||
|
* 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
data_time_seconds = (
|
||||||
|
total_data_packet_bytes
|
||||||
|
* 8
|
||||||
|
/ bitrate_bits_per_second
|
||||||
|
)
|
||||||
|
|
||||||
|
full_time_seconds = (
|
||||||
|
total_radio_bits
|
||||||
|
/ bitrate_bits_per_second
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{bitrate_kbps:>4} кбит/с"
|
||||||
|
f" "
|
||||||
|
f"{format_duration(data_time_seconds):>12}"
|
||||||
|
f" "
|
||||||
|
f"{format_duration(full_time_seconds):>12}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Сценарий с повторами
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# Коэффициент 2 означает, что суммарно передано
|
||||||
|
# примерно в два раза больше байтов из-за ошибок,
|
||||||
|
# потерь и повторных передач.
|
||||||
|
RETRY_TRAFFIC_MULTIPLIER = 2.0
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\n--- Оценка при удвоении трафика из-за повторов ---"
|
||||||
|
)
|
||||||
|
|
||||||
|
for bitrate_kbps in BITRATES_KBPS:
|
||||||
|
|
||||||
|
bitrate_bits_per_second = (
|
||||||
|
bitrate_kbps
|
||||||
|
* 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
estimated_seconds = (
|
||||||
|
total_radio_bits
|
||||||
|
* RETRY_TRAFFIC_MULTIPLIER
|
||||||
|
/ bitrate_bits_per_second
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{bitrate_kbps:>4} кбит/с"
|
||||||
|
f" "
|
||||||
|
f"{format_duration(estimated_seconds)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Автоматические проверки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
assert fragment_count > 0
|
||||||
|
|
||||||
|
assert (
|
||||||
|
total_data_packet_bytes
|
||||||
|
> source_size
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
total_radio_bytes
|
||||||
|
> total_data_packet_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
0 < payload_efficiency < 100
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\nПроверка пройдена: "
|
||||||
|
"параметры передачи фотографии рассчитаны."
|
||||||
|
)
|
||||||
785
Projects/SDR_Rover/tests/lab010_image_optimization.py
Normal file
@@ -0,0 +1,785 @@
|
|||||||
|
"""
|
||||||
|
Lab010. Оптимизация изображения для слабого радиоканала.
|
||||||
|
|
||||||
|
Программа формирует несколько вариантов одного кадра:
|
||||||
|
|
||||||
|
1. Исходный JPEG.
|
||||||
|
2. Цветной кадр до 640x480.
|
||||||
|
3. Серый кадр до 640x480.
|
||||||
|
4. Серый кадр до 320x240, JPEG quality 30.
|
||||||
|
5. Серый кадр до 320x240, JPEG quality 15.
|
||||||
|
6. Карта контуров 320x240.
|
||||||
|
7. Серый кадр с наложенными контурами.
|
||||||
|
|
||||||
|
Для каждого варианта рассчитываются:
|
||||||
|
|
||||||
|
- размер JPEG;
|
||||||
|
- число фрагментов;
|
||||||
|
- полный объём DATA + ACK;
|
||||||
|
- эффективность протокола;
|
||||||
|
- время передачи на нескольких скоростях;
|
||||||
|
- время при удвоении трафика из-за повторов.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from csv import DictWriter
|
||||||
|
from hashlib import sha256
|
||||||
|
from math import ceil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import (
|
||||||
|
Image,
|
||||||
|
ImageDraw,
|
||||||
|
ImageFilter,
|
||||||
|
ImageOps,
|
||||||
|
)
|
||||||
|
|
||||||
|
from protocol.image_fragments import (
|
||||||
|
encode_image_fragment,
|
||||||
|
split_image_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
from protocol.packet import (
|
||||||
|
MESSAGE_TYPE_ACK,
|
||||||
|
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||||
|
build_packet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Настройки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
SOURCE_PATH = Path(
|
||||||
|
"data/raw/lab009_source.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY = Path(
|
||||||
|
"data/processed/lab010"
|
||||||
|
)
|
||||||
|
|
||||||
|
FRAGMENT_DATA_SIZE = 512
|
||||||
|
|
||||||
|
IMAGE_ID_BASE = 2026071300
|
||||||
|
|
||||||
|
BITRATES_KBPS = [
|
||||||
|
5,
|
||||||
|
10,
|
||||||
|
20,
|
||||||
|
50,
|
||||||
|
100,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вспомогательные функции
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def resize_inside(
|
||||||
|
image: Image.Image,
|
||||||
|
maximum_size: tuple[int, int],
|
||||||
|
) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Уменьшить изображение с сохранением пропорций.
|
||||||
|
|
||||||
|
Изображение не растягивается и не искажается.
|
||||||
|
"""
|
||||||
|
|
||||||
|
resized_image = image.copy()
|
||||||
|
|
||||||
|
resized_image.thumbnail(
|
||||||
|
maximum_size,
|
||||||
|
Image.Resampling.LANCZOS,
|
||||||
|
)
|
||||||
|
|
||||||
|
return resized_image
|
||||||
|
|
||||||
|
|
||||||
|
def save_jpeg(
|
||||||
|
image: Image.Image,
|
||||||
|
output_path: Path,
|
||||||
|
quality: int,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Сохранить изображение в JPEG.
|
||||||
|
"""
|
||||||
|
|
||||||
|
output_path.parent.mkdir(
|
||||||
|
parents=True,
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
image.save(
|
||||||
|
output_path,
|
||||||
|
format="JPEG",
|
||||||
|
quality=quality,
|
||||||
|
optimize=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_bytes(byte_count: int) -> str:
|
||||||
|
"""
|
||||||
|
Представить размер в КиБ.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return f"{byte_count / 1024:.2f} КиБ"
|
||||||
|
|
||||||
|
|
||||||
|
def format_duration(seconds: float) -> str:
|
||||||
|
"""
|
||||||
|
Представить время в удобной форме.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if seconds < 1:
|
||||||
|
return f"{seconds * 1000:.0f} мс"
|
||||||
|
|
||||||
|
if seconds < 60:
|
||||||
|
return f"{seconds:.2f} с"
|
||||||
|
|
||||||
|
minutes = int(seconds // 60)
|
||||||
|
remaining_seconds = seconds % 60
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"{minutes} мин "
|
||||||
|
f"{remaining_seconds:.1f} с"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_transfer(
|
||||||
|
file_path: Path,
|
||||||
|
image_id: int,
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Рассчитать параметры передачи одного JPEG.
|
||||||
|
"""
|
||||||
|
|
||||||
|
image_bytes = file_path.read_bytes()
|
||||||
|
|
||||||
|
fragments = split_image_bytes(
|
||||||
|
image_bytes=image_bytes,
|
||||||
|
image_id=image_id,
|
||||||
|
fragment_data_size=FRAGMENT_DATA_SIZE,
|
||||||
|
)
|
||||||
|
|
||||||
|
total_data_packet_bytes = 0
|
||||||
|
|
||||||
|
for fragment in fragments:
|
||||||
|
|
||||||
|
fragment_payload = encode_image_fragment(
|
||||||
|
fragment
|
||||||
|
)
|
||||||
|
|
||||||
|
packet = build_packet(
|
||||||
|
payload=fragment_payload,
|
||||||
|
message_type=MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||||
|
sequence_number=fragment.fragment_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
total_data_packet_bytes += len(packet)
|
||||||
|
|
||||||
|
ack_packet = build_packet(
|
||||||
|
payload=b"",
|
||||||
|
message_type=MESSAGE_TYPE_ACK,
|
||||||
|
sequence_number=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
total_ack_bytes = (
|
||||||
|
len(fragments)
|
||||||
|
* len(ack_packet)
|
||||||
|
)
|
||||||
|
|
||||||
|
total_radio_bytes = (
|
||||||
|
total_data_packet_bytes
|
||||||
|
+ total_ack_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
efficiency_percent = (
|
||||||
|
len(image_bytes)
|
||||||
|
/ total_radio_bytes
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
|
||||||
|
transfer_times = {}
|
||||||
|
|
||||||
|
for bitrate_kbps in BITRATES_KBPS:
|
||||||
|
|
||||||
|
bitrate_bits_per_second = (
|
||||||
|
bitrate_kbps * 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
transfer_times[bitrate_kbps] = (
|
||||||
|
total_radio_bytes
|
||||||
|
* 8
|
||||||
|
/ bitrate_bits_per_second
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"file_size_bytes": len(image_bytes),
|
||||||
|
"fragment_count": len(fragments),
|
||||||
|
"data_packet_bytes": total_data_packet_bytes,
|
||||||
|
"ack_bytes": total_ack_bytes,
|
||||||
|
"total_radio_bytes": total_radio_bytes,
|
||||||
|
"efficiency_percent": efficiency_percent,
|
||||||
|
"transfer_times": transfer_times,
|
||||||
|
"sha256": sha256(image_bytes).hexdigest(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверка исходной фотографии
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
if not SOURCE_PATH.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Не найден исходный файл: {SOURCE_PATH}"
|
||||||
|
)
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY.mkdir(
|
||||||
|
parents=True,
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Чтение исходного изображения
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
with Image.open(SOURCE_PATH) as source_image:
|
||||||
|
|
||||||
|
source_image.load()
|
||||||
|
|
||||||
|
source_rgb = source_image.convert("RGB")
|
||||||
|
|
||||||
|
original_width, original_height = (
|
||||||
|
source_rgb.size
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Формирование вариантов изображения
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
variant_paths = []
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Вариант 0. Исходный JPEG без изменения
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
variant_paths.append(
|
||||||
|
(
|
||||||
|
"00_original",
|
||||||
|
SOURCE_PATH,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Вариант 1. Цветной кадр до 640x480, quality 40
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
color_640 = resize_inside(
|
||||||
|
source_rgb,
|
||||||
|
(640, 480),
|
||||||
|
)
|
||||||
|
|
||||||
|
color_640_path = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "01_color_640_q40.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
save_jpeg(
|
||||||
|
color_640,
|
||||||
|
color_640_path,
|
||||||
|
quality=40,
|
||||||
|
)
|
||||||
|
|
||||||
|
variant_paths.append(
|
||||||
|
(
|
||||||
|
"01_color_640_q40",
|
||||||
|
color_640_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Вариант 2. Серый кадр до 640x480, quality 40
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
gray_640 = ImageOps.grayscale(
|
||||||
|
color_640
|
||||||
|
)
|
||||||
|
|
||||||
|
gray_640_path = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "02_gray_640_q40.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
save_jpeg(
|
||||||
|
gray_640,
|
||||||
|
gray_640_path,
|
||||||
|
quality=40,
|
||||||
|
)
|
||||||
|
|
||||||
|
variant_paths.append(
|
||||||
|
(
|
||||||
|
"02_gray_640_q40",
|
||||||
|
gray_640_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Вариант 3. Серый кадр до 320x240, quality 30
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
color_320 = resize_inside(
|
||||||
|
source_rgb,
|
||||||
|
(320, 240),
|
||||||
|
)
|
||||||
|
|
||||||
|
gray_320 = ImageOps.grayscale(
|
||||||
|
color_320
|
||||||
|
)
|
||||||
|
|
||||||
|
gray_320_q30_path = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "03_gray_320_q30.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
save_jpeg(
|
||||||
|
gray_320,
|
||||||
|
gray_320_q30_path,
|
||||||
|
quality=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
variant_paths.append(
|
||||||
|
(
|
||||||
|
"03_gray_320_q30",
|
||||||
|
gray_320_q30_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Вариант 4. Серый кадр до 320x240, quality 15
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
gray_320_q15_path = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "04_gray_320_q15.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
save_jpeg(
|
||||||
|
gray_320,
|
||||||
|
gray_320_q15_path,
|
||||||
|
quality=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
variant_paths.append(
|
||||||
|
(
|
||||||
|
"04_gray_320_q15",
|
||||||
|
gray_320_q15_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Вариант 5. Только контуры
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
edge_map = gray_320.filter(
|
||||||
|
ImageFilter.FIND_EDGES
|
||||||
|
)
|
||||||
|
|
||||||
|
edge_map = ImageOps.autocontrast(
|
||||||
|
edge_map
|
||||||
|
)
|
||||||
|
|
||||||
|
# Инвертируем: белый фон, тёмные контуры.
|
||||||
|
edge_map = ImageOps.invert(
|
||||||
|
edge_map
|
||||||
|
)
|
||||||
|
|
||||||
|
edges_path = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "05_edges_320_q40.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
save_jpeg(
|
||||||
|
edge_map,
|
||||||
|
edges_path,
|
||||||
|
quality=40,
|
||||||
|
)
|
||||||
|
|
||||||
|
variant_paths.append(
|
||||||
|
(
|
||||||
|
"05_edges_320_q40",
|
||||||
|
edges_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Вариант 6. Серый кадр с усиленными контурами
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
gray_edges_overlay = Image.blend(
|
||||||
|
gray_320,
|
||||||
|
edge_map,
|
||||||
|
alpha=0.25,
|
||||||
|
)
|
||||||
|
|
||||||
|
gray_edges_path = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "06_gray_edges_320_q30.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
save_jpeg(
|
||||||
|
gray_edges_overlay,
|
||||||
|
gray_edges_path,
|
||||||
|
quality=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
variant_paths.append(
|
||||||
|
(
|
||||||
|
"06_gray_edges_320_q30",
|
||||||
|
gray_edges_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Расчёт параметров всех вариантов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for variant_number, (
|
||||||
|
variant_name,
|
||||||
|
variant_path,
|
||||||
|
) in enumerate(variant_paths):
|
||||||
|
|
||||||
|
with Image.open(variant_path) as variant_image:
|
||||||
|
|
||||||
|
width, height = variant_image.size
|
||||||
|
mode = variant_image.mode
|
||||||
|
|
||||||
|
transfer_result = estimate_transfer(
|
||||||
|
file_path=variant_path,
|
||||||
|
image_id=IMAGE_ID_BASE + variant_number,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"name": variant_name,
|
||||||
|
"path": str(variant_path),
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
"mode": mode,
|
||||||
|
**transfer_result,
|
||||||
|
}
|
||||||
|
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Создание сравнительной картинки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
CONTACT_SHEET_COLUMNS = 3
|
||||||
|
CONTACT_SHEET_CELL_WIDTH = 360
|
||||||
|
CONTACT_SHEET_CELL_HEIGHT = 290
|
||||||
|
|
||||||
|
contact_sheet_rows = ceil(
|
||||||
|
len(results)
|
||||||
|
/ CONTACT_SHEET_COLUMNS
|
||||||
|
)
|
||||||
|
|
||||||
|
contact_sheet = Image.new(
|
||||||
|
"RGB",
|
||||||
|
(
|
||||||
|
CONTACT_SHEET_COLUMNS
|
||||||
|
* CONTACT_SHEET_CELL_WIDTH,
|
||||||
|
contact_sheet_rows
|
||||||
|
* CONTACT_SHEET_CELL_HEIGHT,
|
||||||
|
),
|
||||||
|
"white",
|
||||||
|
)
|
||||||
|
|
||||||
|
draw = ImageDraw.Draw(
|
||||||
|
contact_sheet
|
||||||
|
)
|
||||||
|
|
||||||
|
for result_index, result in enumerate(results):
|
||||||
|
|
||||||
|
column = (
|
||||||
|
result_index
|
||||||
|
% CONTACT_SHEET_COLUMNS
|
||||||
|
)
|
||||||
|
|
||||||
|
row = (
|
||||||
|
result_index
|
||||||
|
// CONTACT_SHEET_COLUMNS
|
||||||
|
)
|
||||||
|
|
||||||
|
cell_x = (
|
||||||
|
column
|
||||||
|
* CONTACT_SHEET_CELL_WIDTH
|
||||||
|
)
|
||||||
|
|
||||||
|
cell_y = (
|
||||||
|
row
|
||||||
|
* CONTACT_SHEET_CELL_HEIGHT
|
||||||
|
)
|
||||||
|
|
||||||
|
with Image.open(result["path"]) as variant_image:
|
||||||
|
|
||||||
|
preview = variant_image.convert("RGB")
|
||||||
|
|
||||||
|
preview.thumbnail(
|
||||||
|
(330, 220),
|
||||||
|
Image.Resampling.LANCZOS,
|
||||||
|
)
|
||||||
|
|
||||||
|
paste_x = (
|
||||||
|
cell_x
|
||||||
|
+ (
|
||||||
|
CONTACT_SHEET_CELL_WIDTH
|
||||||
|
- preview.width
|
||||||
|
)
|
||||||
|
// 2
|
||||||
|
)
|
||||||
|
|
||||||
|
paste_y = (
|
||||||
|
cell_y + 35
|
||||||
|
)
|
||||||
|
|
||||||
|
contact_sheet.paste(
|
||||||
|
preview,
|
||||||
|
(paste_x, paste_y),
|
||||||
|
)
|
||||||
|
|
||||||
|
label = (
|
||||||
|
f"{result['name']}\n"
|
||||||
|
f"{format_bytes(result['file_size_bytes'])}, "
|
||||||
|
f"{result['fragment_count']} fragments"
|
||||||
|
)
|
||||||
|
|
||||||
|
draw.text(
|
||||||
|
(cell_x + 10, cell_y + 8),
|
||||||
|
label,
|
||||||
|
fill="black",
|
||||||
|
)
|
||||||
|
|
||||||
|
contact_sheet_path = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "lab010_comparison.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
contact_sheet.save(
|
||||||
|
contact_sheet_path,
|
||||||
|
format="JPEG",
|
||||||
|
quality=90,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Сохранение таблицы CSV
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
csv_path = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "lab010_results.csv"
|
||||||
|
)
|
||||||
|
|
||||||
|
csv_fieldnames = [
|
||||||
|
"name",
|
||||||
|
"path",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"mode",
|
||||||
|
"file_size_bytes",
|
||||||
|
"fragment_count",
|
||||||
|
"total_radio_bytes",
|
||||||
|
"efficiency_percent",
|
||||||
|
"time_5_kbps",
|
||||||
|
"time_10_kbps",
|
||||||
|
"time_20_kbps",
|
||||||
|
"time_50_kbps",
|
||||||
|
"time_100_kbps",
|
||||||
|
"time_20_kbps_with_retries_x2",
|
||||||
|
]
|
||||||
|
|
||||||
|
with csv_path.open(
|
||||||
|
"w",
|
||||||
|
newline="",
|
||||||
|
encoding="utf-8-sig",
|
||||||
|
) as csv_file:
|
||||||
|
|
||||||
|
writer = DictWriter(
|
||||||
|
csv_file,
|
||||||
|
fieldnames=csv_fieldnames,
|
||||||
|
)
|
||||||
|
|
||||||
|
writer.writeheader()
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
|
||||||
|
writer.writerow(
|
||||||
|
{
|
||||||
|
"name": result["name"],
|
||||||
|
"path": result["path"],
|
||||||
|
"width": result["width"],
|
||||||
|
"height": result["height"],
|
||||||
|
"mode": result["mode"],
|
||||||
|
"file_size_bytes": (
|
||||||
|
result["file_size_bytes"]
|
||||||
|
),
|
||||||
|
"fragment_count": (
|
||||||
|
result["fragment_count"]
|
||||||
|
),
|
||||||
|
"total_radio_bytes": (
|
||||||
|
result["total_radio_bytes"]
|
||||||
|
),
|
||||||
|
"efficiency_percent": (
|
||||||
|
f"{result['efficiency_percent']:.2f}"
|
||||||
|
),
|
||||||
|
"time_5_kbps": (
|
||||||
|
result["transfer_times"][5]
|
||||||
|
),
|
||||||
|
"time_10_kbps": (
|
||||||
|
result["transfer_times"][10]
|
||||||
|
),
|
||||||
|
"time_20_kbps": (
|
||||||
|
result["transfer_times"][20]
|
||||||
|
),
|
||||||
|
"time_50_kbps": (
|
||||||
|
result["transfer_times"][50]
|
||||||
|
),
|
||||||
|
"time_100_kbps": (
|
||||||
|
result["transfer_times"][100]
|
||||||
|
),
|
||||||
|
"time_20_kbps_with_retries_x2": (
|
||||||
|
result["transfer_times"][20]
|
||||||
|
* 2
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод результатов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print(
|
||||||
|
"=== Lab010. Оптимизация изображения ==="
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nИсходное разрешение:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
original_width,
|
||||||
|
"x",
|
||||||
|
original_height,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nСравнение вариантов:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{'Вариант':<28}"
|
||||||
|
f"{'Размер':>12}"
|
||||||
|
f"{'Фрагм.':>9}"
|
||||||
|
f"{'10 кбит/с':>13}"
|
||||||
|
f"{'20 кбит/с':>13}"
|
||||||
|
f"{'50 кбит/с':>13}"
|
||||||
|
f"{'20 кбит/с x2':>16}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("-" * 104)
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{result['name']:<28}"
|
||||||
|
f"{format_bytes(result['file_size_bytes']):>12}"
|
||||||
|
f"{result['fragment_count']:>9}"
|
||||||
|
f"{format_duration(result['transfer_times'][10]):>13}"
|
||||||
|
f"{format_duration(result['transfer_times'][20]):>13}"
|
||||||
|
f"{format_duration(result['transfer_times'][50]):>13}"
|
||||||
|
f"{format_duration(result['transfer_times'][20] * 2):>16}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Итоговые кандидаты
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
smallest_result = min(
|
||||||
|
results,
|
||||||
|
key=lambda item: item["file_size_bytes"],
|
||||||
|
)
|
||||||
|
|
||||||
|
operator_candidate = next(
|
||||||
|
result
|
||||||
|
for result in results
|
||||||
|
if result["name"] == "03_gray_320_q30"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nСамый маленький файл:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
smallest_result["name"],
|
||||||
|
"-",
|
||||||
|
format_bytes(
|
||||||
|
smallest_result["file_size_bytes"]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nБазовый кандидат для оператора:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
operator_candidate["name"],
|
||||||
|
"-",
|
||||||
|
format_bytes(
|
||||||
|
operator_candidate["file_size_bytes"]
|
||||||
|
),
|
||||||
|
"-",
|
||||||
|
format_duration(
|
||||||
|
operator_candidate[
|
||||||
|
"transfer_times"
|
||||||
|
][20]
|
||||||
|
),
|
||||||
|
"при 20 кбит/с без повторов",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nСравнительная картинка:")
|
||||||
|
|
||||||
|
print(contact_sheet_path)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nТаблица результатов CSV:")
|
||||||
|
|
||||||
|
print(csv_path)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
assert len(results) == 7
|
||||||
|
|
||||||
|
assert contact_sheet_path.exists()
|
||||||
|
|
||||||
|
assert csv_path.exists()
|
||||||
|
|
||||||
|
assert all(
|
||||||
|
result["fragment_count"] > 0
|
||||||
|
for result in results
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\nПроверка пройдена: "
|
||||||
|
"варианты кадра созданы и рассчитаны."
|
||||||
|
)
|
||||||
525
Projects/SDR_Rover/tests/lab011_scene_modes.py
Normal file
@@ -0,0 +1,525 @@
|
|||||||
|
"""
|
||||||
|
Lab011. Проверка облегчённых режимов изображения
|
||||||
|
на сцене, похожей на вид с камеры ровера.
|
||||||
|
|
||||||
|
Создаются варианты:
|
||||||
|
1. Grayscale 320x240, JPEG quality 30.
|
||||||
|
2. Grayscale 320x240, JPEG quality 15.
|
||||||
|
3. Grayscale 320x240 с наложением контуров, quality 30.
|
||||||
|
|
||||||
|
Для каждого варианта рассчитываются:
|
||||||
|
- размер JPEG;
|
||||||
|
- количество радиопакетов;
|
||||||
|
- время передачи при разных скоростях.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from math import ceil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import (
|
||||||
|
Image,
|
||||||
|
ImageChops,
|
||||||
|
ImageDraw,
|
||||||
|
ImageFilter,
|
||||||
|
ImageOps,
|
||||||
|
)
|
||||||
|
|
||||||
|
from protocol.image_fragments import (
|
||||||
|
encode_image_fragment,
|
||||||
|
split_image_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
from protocol.packet import (
|
||||||
|
MESSAGE_TYPE_ACK,
|
||||||
|
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||||
|
build_packet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Настройки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
SOURCE_PATH = Path(
|
||||||
|
"data/raw/lab011_scene.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY = Path(
|
||||||
|
"data/processed/lab011"
|
||||||
|
)
|
||||||
|
|
||||||
|
FRAME_SIZE = (320, 240)
|
||||||
|
|
||||||
|
FRAGMENT_DATA_SIZE = 512
|
||||||
|
|
||||||
|
IMAGE_ID_BASE = 2026071400
|
||||||
|
|
||||||
|
BITRATES_KBPS = [
|
||||||
|
5,
|
||||||
|
10,
|
||||||
|
20,
|
||||||
|
50,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вспомогательные функции
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def prepare_frame(
|
||||||
|
image: Image.Image,
|
||||||
|
) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Привести изображение к фиксированному кадру 320x240.
|
||||||
|
|
||||||
|
Пропорции сохраняются.
|
||||||
|
Недостающие области заполняются чёрным.
|
||||||
|
"""
|
||||||
|
|
||||||
|
image = ImageOps.exif_transpose(image)
|
||||||
|
image = image.convert("RGB")
|
||||||
|
|
||||||
|
return ImageOps.pad(
|
||||||
|
image,
|
||||||
|
FRAME_SIZE,
|
||||||
|
method=Image.Resampling.LANCZOS,
|
||||||
|
color="black",
|
||||||
|
centering=(0.5, 0.5),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def save_jpeg(
|
||||||
|
image: Image.Image,
|
||||||
|
path: Path,
|
||||||
|
quality: int,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Сохранить изображение в JPEG.
|
||||||
|
"""
|
||||||
|
|
||||||
|
path.parent.mkdir(
|
||||||
|
parents=True,
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
image.save(
|
||||||
|
path,
|
||||||
|
format="JPEG",
|
||||||
|
quality=quality,
|
||||||
|
optimize=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_size(
|
||||||
|
byte_count: int,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Представить размер в КиБ.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return f"{byte_count / 1024:.2f} КиБ"
|
||||||
|
|
||||||
|
|
||||||
|
def format_duration(
|
||||||
|
seconds: float,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Представить длительность передачи.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if seconds < 1:
|
||||||
|
return f"{seconds * 1000:.0f} мс"
|
||||||
|
|
||||||
|
if seconds < 60:
|
||||||
|
return f"{seconds:.2f} с"
|
||||||
|
|
||||||
|
minutes = int(seconds // 60)
|
||||||
|
remaining_seconds = seconds % 60
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"{minutes} мин "
|
||||||
|
f"{remaining_seconds:.1f} с"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_transfer(
|
||||||
|
file_path: Path,
|
||||||
|
image_id: int,
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Рассчитать полный объём DATA + ACK.
|
||||||
|
"""
|
||||||
|
|
||||||
|
image_bytes = file_path.read_bytes()
|
||||||
|
|
||||||
|
fragments = split_image_bytes(
|
||||||
|
image_bytes=image_bytes,
|
||||||
|
image_id=image_id,
|
||||||
|
fragment_data_size=FRAGMENT_DATA_SIZE,
|
||||||
|
)
|
||||||
|
|
||||||
|
data_packet_bytes = 0
|
||||||
|
|
||||||
|
for fragment in fragments:
|
||||||
|
|
||||||
|
fragment_payload = encode_image_fragment(
|
||||||
|
fragment
|
||||||
|
)
|
||||||
|
|
||||||
|
packet = build_packet(
|
||||||
|
payload=fragment_payload,
|
||||||
|
message_type=MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||||
|
sequence_number=fragment.fragment_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
data_packet_bytes += len(packet)
|
||||||
|
|
||||||
|
ack_packet = build_packet(
|
||||||
|
payload=b"",
|
||||||
|
message_type=MESSAGE_TYPE_ACK,
|
||||||
|
sequence_number=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
ack_bytes = (
|
||||||
|
len(fragments)
|
||||||
|
* len(ack_packet)
|
||||||
|
)
|
||||||
|
|
||||||
|
total_radio_bytes = (
|
||||||
|
data_packet_bytes
|
||||||
|
+ ack_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
times = {}
|
||||||
|
|
||||||
|
for bitrate_kbps in BITRATES_KBPS:
|
||||||
|
|
||||||
|
times[bitrate_kbps] = (
|
||||||
|
total_radio_bytes
|
||||||
|
* 8
|
||||||
|
/ (bitrate_kbps * 1000)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"file_size": len(image_bytes),
|
||||||
|
"fragment_count": len(fragments),
|
||||||
|
"total_radio_bytes": total_radio_bytes,
|
||||||
|
"times": times,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверка исходного файла
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
if not SOURCE_PATH.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Не найден файл: {SOURCE_PATH}"
|
||||||
|
)
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY.mkdir(
|
||||||
|
parents=True,
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Подготовка базового кадра
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
with Image.open(SOURCE_PATH) as source_image:
|
||||||
|
|
||||||
|
original_size = source_image.size
|
||||||
|
|
||||||
|
color_frame = prepare_frame(
|
||||||
|
source_image
|
||||||
|
)
|
||||||
|
|
||||||
|
gray_frame = ImageOps.grayscale(
|
||||||
|
color_frame
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вариант 1. Grayscale, quality 30
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
gray_q30_path = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "01_gray_320_q30.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
save_jpeg(
|
||||||
|
gray_frame,
|
||||||
|
gray_q30_path,
|
||||||
|
quality=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вариант 2. Grayscale, quality 15
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
gray_q15_path = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "02_gray_320_q15.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
save_jpeg(
|
||||||
|
gray_frame,
|
||||||
|
gray_q15_path,
|
||||||
|
quality=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вариант 3. Grayscale + контуры
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
edge_map = gray_frame.filter(
|
||||||
|
ImageFilter.FIND_EDGES
|
||||||
|
)
|
||||||
|
|
||||||
|
edge_map = ImageOps.autocontrast(
|
||||||
|
edge_map
|
||||||
|
)
|
||||||
|
|
||||||
|
# Оставляем преимущественно сильные контуры.
|
||||||
|
binary_edges = edge_map.point(
|
||||||
|
lambda value: 255 if value >= 45 else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
# После инверсии контуры становятся чёрными,
|
||||||
|
# а фон — белым.
|
||||||
|
dark_edges = ImageOps.invert(
|
||||||
|
binary_edges
|
||||||
|
)
|
||||||
|
|
||||||
|
# Сохраняем исходный серый фон и добавляем
|
||||||
|
# поверх него тёмные линии контуров.
|
||||||
|
gray_with_edges = ImageChops.darker(
|
||||||
|
gray_frame,
|
||||||
|
dark_edges
|
||||||
|
)
|
||||||
|
|
||||||
|
gray_edges_path = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "03_gray_edges_320_q30.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
save_jpeg(
|
||||||
|
gray_with_edges,
|
||||||
|
gray_edges_path,
|
||||||
|
quality=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Список вариантов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
variants = [
|
||||||
|
(
|
||||||
|
"gray_320_q30",
|
||||||
|
gray_q30_path,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"gray_320_q15",
|
||||||
|
gray_q15_path,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"gray_edges_320_q30",
|
||||||
|
gray_edges_path,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Расчёт параметров
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for variant_index, (
|
||||||
|
variant_name,
|
||||||
|
variant_path,
|
||||||
|
) in enumerate(variants):
|
||||||
|
|
||||||
|
result = estimate_transfer(
|
||||||
|
file_path=variant_path,
|
||||||
|
image_id=IMAGE_ID_BASE + variant_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"name": variant_name,
|
||||||
|
"path": variant_path,
|
||||||
|
**result,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Создание сравнительной картинки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
preview_variants = [
|
||||||
|
(
|
||||||
|
"SOURCE PREVIEW",
|
||||||
|
color_frame,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
|
||||||
|
with Image.open(result["path"]) as image:
|
||||||
|
|
||||||
|
preview_variants.append(
|
||||||
|
(
|
||||||
|
result["name"],
|
||||||
|
image.convert("RGB").copy(),
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
columns = 2
|
||||||
|
cell_width = 440
|
||||||
|
cell_height = 340
|
||||||
|
|
||||||
|
rows = ceil(
|
||||||
|
len(preview_variants)
|
||||||
|
/ columns
|
||||||
|
)
|
||||||
|
|
||||||
|
comparison_image = Image.new(
|
||||||
|
"RGB",
|
||||||
|
(
|
||||||
|
columns * cell_width,
|
||||||
|
rows * cell_height,
|
||||||
|
),
|
||||||
|
"white",
|
||||||
|
)
|
||||||
|
|
||||||
|
draw = ImageDraw.Draw(
|
||||||
|
comparison_image
|
||||||
|
)
|
||||||
|
|
||||||
|
for index, (
|
||||||
|
label,
|
||||||
|
preview,
|
||||||
|
result,
|
||||||
|
) in enumerate(preview_variants):
|
||||||
|
|
||||||
|
column = index % columns
|
||||||
|
row = index // columns
|
||||||
|
|
||||||
|
x = column * cell_width
|
||||||
|
y = row * cell_height
|
||||||
|
|
||||||
|
preview = preview.copy()
|
||||||
|
|
||||||
|
preview.thumbnail(
|
||||||
|
(400, 270),
|
||||||
|
Image.Resampling.NEAREST,
|
||||||
|
)
|
||||||
|
|
||||||
|
paste_x = (
|
||||||
|
x
|
||||||
|
+ (cell_width - preview.width) // 2
|
||||||
|
)
|
||||||
|
|
||||||
|
comparison_image.paste(
|
||||||
|
preview,
|
||||||
|
(paste_x, y + 55),
|
||||||
|
)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
|
||||||
|
text = (
|
||||||
|
f"{label}\n"
|
||||||
|
f"original: "
|
||||||
|
f"{original_size[0]}x{original_size[1]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
text = (
|
||||||
|
f"{label}\n"
|
||||||
|
f"{format_size(result['file_size'])}, "
|
||||||
|
f"{result['fragment_count']} fragments, "
|
||||||
|
f"{format_duration(result['times'][20])} "
|
||||||
|
f"at 20 kbps"
|
||||||
|
)
|
||||||
|
|
||||||
|
draw.multiline_text(
|
||||||
|
(x + 10, y + 10),
|
||||||
|
text,
|
||||||
|
fill="black",
|
||||||
|
spacing=4,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
comparison_path = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "lab011_comparison.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
comparison_image.save(
|
||||||
|
comparison_path,
|
||||||
|
format="JPEG",
|
||||||
|
quality=90,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод результатов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print(
|
||||||
|
"=== Lab011. Режимы кадра для ровера ==="
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nИсходное разрешение:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
original_size[0],
|
||||||
|
"x",
|
||||||
|
original_size[1],
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nРезультаты:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{'Вариант':<27}"
|
||||||
|
f"{'Размер':>12}"
|
||||||
|
f"{'Пакеты':>10}"
|
||||||
|
f"{'10 кбит/с':>13}"
|
||||||
|
f"{'20 кбит/с':>13}"
|
||||||
|
f"{'50 кбит/с':>13}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("-" * 88)
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{result['name']:<27}"
|
||||||
|
f"{format_size(result['file_size']):>12}"
|
||||||
|
f"{result['fragment_count']:>10}"
|
||||||
|
f"{format_duration(result['times'][10]):>13}"
|
||||||
|
f"{format_duration(result['times'][20]):>13}"
|
||||||
|
f"{format_duration(result['times'][50]):>13}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nСравнительная картинка:")
|
||||||
|
|
||||||
|
print(comparison_path)
|
||||||
|
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\nПроверка пройдена: "
|
||||||
|
"режимы кадра созданы и рассчитаны."
|
||||||
|
)
|
||||||
558
Projects/SDR_Rover/tests/lab012_color_vs_gray.py
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
"""
|
||||||
|
Lab012. Сравнение цветного и серого кадров
|
||||||
|
для обнаружения человека и препятствий.
|
||||||
|
|
||||||
|
Создаются четыре варианта:
|
||||||
|
|
||||||
|
1. Цветной 320x240, JPEG quality 30.
|
||||||
|
2. Серый 320x240, JPEG quality 30.
|
||||||
|
3. Цветной 320x240, JPEG quality 15.
|
||||||
|
4. Серый 320x240, JPEG quality 15.
|
||||||
|
|
||||||
|
Для каждого варианта рассчитываются:
|
||||||
|
|
||||||
|
- размер JPEG;
|
||||||
|
- количество фрагментов;
|
||||||
|
- полный объём DATA + ACK;
|
||||||
|
- время передачи;
|
||||||
|
- стоимость сохранения цвета.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from math import ceil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import (
|
||||||
|
Image,
|
||||||
|
ImageDraw,
|
||||||
|
ImageOps,
|
||||||
|
)
|
||||||
|
|
||||||
|
from protocol.image_fragments import (
|
||||||
|
encode_image_fragment,
|
||||||
|
split_image_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
from protocol.packet import (
|
||||||
|
MESSAGE_TYPE_ACK,
|
||||||
|
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||||
|
build_packet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Настройки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
SOURCE_PATH = Path(
|
||||||
|
"data/raw/lab012_safety_scene.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY = Path(
|
||||||
|
"data/processed/lab012"
|
||||||
|
)
|
||||||
|
|
||||||
|
FRAME_SIZE = (320, 240)
|
||||||
|
|
||||||
|
FRAGMENT_DATA_SIZE = 512
|
||||||
|
|
||||||
|
IMAGE_ID_BASE = 2026071500
|
||||||
|
|
||||||
|
BITRATES_KBPS = [
|
||||||
|
10,
|
||||||
|
20,
|
||||||
|
50,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вспомогательные функции
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def prepare_frame(
|
||||||
|
image: Image.Image,
|
||||||
|
) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Подготовить кадр фиксированного размера 320x240.
|
||||||
|
|
||||||
|
Изображение:
|
||||||
|
- автоматически поворачивается по EXIF;
|
||||||
|
- переводится в RGB;
|
||||||
|
- масштабируется с сохранением пропорций;
|
||||||
|
- при необходимости дополняется чёрными полями.
|
||||||
|
"""
|
||||||
|
|
||||||
|
image = ImageOps.exif_transpose(image)
|
||||||
|
image = image.convert("RGB")
|
||||||
|
|
||||||
|
return ImageOps.pad(
|
||||||
|
image,
|
||||||
|
FRAME_SIZE,
|
||||||
|
method=Image.Resampling.LANCZOS,
|
||||||
|
color="black",
|
||||||
|
centering=(0.5, 0.5),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def save_jpeg(
|
||||||
|
image: Image.Image,
|
||||||
|
output_path: Path,
|
||||||
|
quality: int,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Сохранить изображение в JPEG.
|
||||||
|
"""
|
||||||
|
|
||||||
|
output_path.parent.mkdir(
|
||||||
|
parents=True,
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
image.save(
|
||||||
|
output_path,
|
||||||
|
format="JPEG",
|
||||||
|
quality=quality,
|
||||||
|
optimize=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_size(
|
||||||
|
byte_count: int,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Представить размер файла в КиБ.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return f"{byte_count / 1024:.2f} КиБ"
|
||||||
|
|
||||||
|
|
||||||
|
def format_duration(
|
||||||
|
seconds: float,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Представить время передачи.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if seconds < 1:
|
||||||
|
return f"{seconds * 1000:.0f} мс"
|
||||||
|
|
||||||
|
if seconds < 60:
|
||||||
|
return f"{seconds:.2f} с"
|
||||||
|
|
||||||
|
minutes = int(seconds // 60)
|
||||||
|
remaining_seconds = seconds % 60
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"{minutes} мин "
|
||||||
|
f"{remaining_seconds:.1f} с"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_transfer(
|
||||||
|
file_path: Path,
|
||||||
|
image_id: int,
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Рассчитать параметры передачи JPEG
|
||||||
|
с использованием текущего протокола.
|
||||||
|
"""
|
||||||
|
|
||||||
|
image_bytes = file_path.read_bytes()
|
||||||
|
|
||||||
|
fragments = split_image_bytes(
|
||||||
|
image_bytes=image_bytes,
|
||||||
|
image_id=image_id,
|
||||||
|
fragment_data_size=FRAGMENT_DATA_SIZE,
|
||||||
|
)
|
||||||
|
|
||||||
|
total_data_bytes = 0
|
||||||
|
|
||||||
|
for fragment in fragments:
|
||||||
|
|
||||||
|
fragment_payload = encode_image_fragment(
|
||||||
|
fragment
|
||||||
|
)
|
||||||
|
|
||||||
|
packet = build_packet(
|
||||||
|
payload=fragment_payload,
|
||||||
|
message_type=MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||||
|
sequence_number=fragment.fragment_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
total_data_bytes += len(packet)
|
||||||
|
|
||||||
|
ack_packet = build_packet(
|
||||||
|
payload=b"",
|
||||||
|
message_type=MESSAGE_TYPE_ACK,
|
||||||
|
sequence_number=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
total_ack_bytes = (
|
||||||
|
len(fragments)
|
||||||
|
* len(ack_packet)
|
||||||
|
)
|
||||||
|
|
||||||
|
total_radio_bytes = (
|
||||||
|
total_data_bytes
|
||||||
|
+ total_ack_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
transfer_times = {}
|
||||||
|
|
||||||
|
for bitrate_kbps in BITRATES_KBPS:
|
||||||
|
|
||||||
|
transfer_times[bitrate_kbps] = (
|
||||||
|
total_radio_bytes
|
||||||
|
* 8
|
||||||
|
/ (bitrate_kbps * 1000)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"file_size": len(image_bytes),
|
||||||
|
"fragment_count": len(fragments),
|
||||||
|
"total_data_bytes": total_data_bytes,
|
||||||
|
"total_ack_bytes": total_ack_bytes,
|
||||||
|
"total_radio_bytes": total_radio_bytes,
|
||||||
|
"times": transfer_times,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверка исходного файла
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
if not SOURCE_PATH.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Не найден файл: {SOURCE_PATH}"
|
||||||
|
)
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY.mkdir(
|
||||||
|
parents=True,
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Подготовка базового кадра
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
with Image.open(SOURCE_PATH) as source_image:
|
||||||
|
|
||||||
|
original_size = source_image.size
|
||||||
|
|
||||||
|
color_frame = prepare_frame(
|
||||||
|
source_image
|
||||||
|
)
|
||||||
|
|
||||||
|
gray_frame = ImageOps.grayscale(
|
||||||
|
color_frame
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Создание вариантов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
variant_definitions = [
|
||||||
|
{
|
||||||
|
"name": "color_320_q30",
|
||||||
|
"image": color_frame,
|
||||||
|
"quality": 30,
|
||||||
|
"path": (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "01_color_320_q30.jpg"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gray_320_q30",
|
||||||
|
"image": gray_frame,
|
||||||
|
"quality": 30,
|
||||||
|
"path": (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "02_gray_320_q30.jpg"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "color_320_q15",
|
||||||
|
"image": color_frame,
|
||||||
|
"quality": 15,
|
||||||
|
"path": (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "03_color_320_q15.jpg"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gray_320_q15",
|
||||||
|
"image": gray_frame,
|
||||||
|
"quality": 15,
|
||||||
|
"path": (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "04_gray_320_q15.jpg"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
for variant in variant_definitions:
|
||||||
|
|
||||||
|
save_jpeg(
|
||||||
|
image=variant["image"],
|
||||||
|
output_path=variant["path"],
|
||||||
|
quality=variant["quality"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Расчёт параметров передачи
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for variant_index, variant in enumerate(
|
||||||
|
variant_definitions
|
||||||
|
):
|
||||||
|
|
||||||
|
transfer_result = estimate_transfer(
|
||||||
|
file_path=variant["path"],
|
||||||
|
image_id=IMAGE_ID_BASE + variant_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"name": variant["name"],
|
||||||
|
"path": variant["path"],
|
||||||
|
"quality": variant["quality"],
|
||||||
|
**transfer_result,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Создание сравнительной картинки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
columns = 2
|
||||||
|
cell_width = 440
|
||||||
|
cell_height = 340
|
||||||
|
|
||||||
|
rows = ceil(
|
||||||
|
len(results)
|
||||||
|
/ columns
|
||||||
|
)
|
||||||
|
|
||||||
|
comparison_image = Image.new(
|
||||||
|
"RGB",
|
||||||
|
(
|
||||||
|
columns * cell_width,
|
||||||
|
rows * cell_height,
|
||||||
|
),
|
||||||
|
"white",
|
||||||
|
)
|
||||||
|
|
||||||
|
draw = ImageDraw.Draw(
|
||||||
|
comparison_image
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
for result_index, result in enumerate(results):
|
||||||
|
|
||||||
|
column = result_index % columns
|
||||||
|
row = result_index // columns
|
||||||
|
|
||||||
|
cell_x = column * cell_width
|
||||||
|
cell_y = row * cell_height
|
||||||
|
|
||||||
|
with Image.open(result["path"]) as image:
|
||||||
|
|
||||||
|
preview = image.convert("RGB")
|
||||||
|
|
||||||
|
# NEAREST не сглаживает пиксели при увеличении.
|
||||||
|
# Поэтому дефекты сжатия будут хорошо видны.
|
||||||
|
preview = preview.resize(
|
||||||
|
(400, 300),
|
||||||
|
Image.Resampling.NEAREST,
|
||||||
|
)
|
||||||
|
|
||||||
|
paste_x = (
|
||||||
|
cell_x
|
||||||
|
+ (cell_width - preview.width) // 2
|
||||||
|
)
|
||||||
|
|
||||||
|
paste_y = cell_y + 35
|
||||||
|
|
||||||
|
comparison_image.paste(
|
||||||
|
preview,
|
||||||
|
(paste_x, paste_y),
|
||||||
|
)
|
||||||
|
|
||||||
|
label = (
|
||||||
|
f"{result['name']}\n"
|
||||||
|
f"{format_size(result['file_size'])}, "
|
||||||
|
f"{result['fragment_count']} fragments, "
|
||||||
|
f"{format_duration(result['times'][20])} "
|
||||||
|
f"at 20 kbps"
|
||||||
|
)
|
||||||
|
|
||||||
|
draw.multiline_text(
|
||||||
|
(cell_x + 10, cell_y + 5),
|
||||||
|
label,
|
||||||
|
fill="black",
|
||||||
|
spacing=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
comparison_path = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "lab012_comparison.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
comparison_image.save(
|
||||||
|
comparison_path,
|
||||||
|
format="JPEG",
|
||||||
|
quality=92,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Поиск результатов по имени
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
results_by_name = {
|
||||||
|
result["name"]: result
|
||||||
|
for result in results
|
||||||
|
}
|
||||||
|
|
||||||
|
color_q30 = results_by_name[
|
||||||
|
"color_320_q30"
|
||||||
|
]
|
||||||
|
|
||||||
|
gray_q30 = results_by_name[
|
||||||
|
"gray_320_q30"
|
||||||
|
]
|
||||||
|
|
||||||
|
color_q15 = results_by_name[
|
||||||
|
"color_320_q15"
|
||||||
|
]
|
||||||
|
|
||||||
|
gray_q15 = results_by_name[
|
||||||
|
"gray_320_q15"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Расчёт цены сохранения цвета
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
color_overhead_q30_bytes = (
|
||||||
|
color_q30["file_size"]
|
||||||
|
- gray_q30["file_size"]
|
||||||
|
)
|
||||||
|
|
||||||
|
color_overhead_q30_percent = (
|
||||||
|
color_overhead_q30_bytes
|
||||||
|
/ gray_q30["file_size"]
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
|
||||||
|
color_overhead_q15_bytes = (
|
||||||
|
color_q15["file_size"]
|
||||||
|
- gray_q15["file_size"]
|
||||||
|
)
|
||||||
|
|
||||||
|
color_overhead_q15_percent = (
|
||||||
|
color_overhead_q15_bytes
|
||||||
|
/ gray_q15["file_size"]
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод результатов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print(
|
||||||
|
"=== Lab012. Цветной или серый кадр ==="
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nИсходное разрешение:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
original_size[0],
|
||||||
|
"x",
|
||||||
|
original_size[1],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nСравнение вариантов:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{'Вариант':<22}"
|
||||||
|
f"{'Размер':>12}"
|
||||||
|
f"{'Пакеты':>10}"
|
||||||
|
f"{'10 кбит/с':>13}"
|
||||||
|
f"{'20 кбит/с':>13}"
|
||||||
|
f"{'50 кбит/с':>13}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("-" * 83)
|
||||||
|
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{result['name']:<22}"
|
||||||
|
f"{format_size(result['file_size']):>12}"
|
||||||
|
f"{result['fragment_count']:>10}"
|
||||||
|
f"{format_duration(result['times'][10]):>13}"
|
||||||
|
f"{format_duration(result['times'][20]):>13}"
|
||||||
|
f"{format_duration(result['times'][50]):>13}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nЦена сохранения цвета при quality 30:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
format_size(
|
||||||
|
color_overhead_q30_bytes
|
||||||
|
),
|
||||||
|
f"или {color_overhead_q30_percent:.1f} %",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nЦена сохранения цвета при quality 15:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
format_size(
|
||||||
|
color_overhead_q15_bytes
|
||||||
|
),
|
||||||
|
f"или {color_overhead_q15_percent:.1f} %",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nСравнительная картинка:")
|
||||||
|
|
||||||
|
print(comparison_path)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
assert len(results) == 4
|
||||||
|
|
||||||
|
assert comparison_path.exists()
|
||||||
|
|
||||||
|
assert all(
|
||||||
|
result["fragment_count"] > 0
|
||||||
|
for result in results
|
||||||
|
)
|
||||||
|
|
||||||
|
assert color_q30["file_size"] > 0
|
||||||
|
assert gray_q30["file_size"] > 0
|
||||||
|
assert color_q15["file_size"] > 0
|
||||||
|
assert gray_q15["file_size"] > 0
|
||||||
|
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\nПроверка пройдена: "
|
||||||
|
"цветные и серые варианты созданы и рассчитаны."
|
||||||
|
)
|
||||||
528
Projects/SDR_Rover/tests/lab013_bpsk_python.py
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
"""
|
||||||
|
Lab013. Первая BPSK-передача цифрового пакета в Python.
|
||||||
|
|
||||||
|
Программа:
|
||||||
|
1. Формирует пакет SDR Rover Link с CRC-32.
|
||||||
|
2. Преобразует байты пакета в отдельные биты.
|
||||||
|
3. Преобразует биты в BPSK-символы.
|
||||||
|
4. Представляет символы как комплексные IQ-сэмплы.
|
||||||
|
5. Добавляет комплексный белый гауссов шум.
|
||||||
|
6. Демодулирует BPSK.
|
||||||
|
7. Восстанавливает байты пакета.
|
||||||
|
8. Проверяет пакет и CRC.
|
||||||
|
9. Сравнивает результат при разных уровнях SNR.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from protocol.packet import (
|
||||||
|
CRCError,
|
||||||
|
MESSAGE_TYPE_TEXT,
|
||||||
|
PacketError,
|
||||||
|
build_packet,
|
||||||
|
parse_packet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Настройки эксперимента
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
MESSAGE = "ПРИВЕТ SDR"
|
||||||
|
|
||||||
|
SEQUENCE_NUMBER = 13
|
||||||
|
|
||||||
|
# Проверим несколько уровней отношения сигнал/шум.
|
||||||
|
SNR_VALUES_DB = [
|
||||||
|
12.0,
|
||||||
|
6.0,
|
||||||
|
2.0,
|
||||||
|
0.0,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Фиксированное значение обеспечивает повторяемость.
|
||||||
|
RANDOM_SEED = 2026
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY = Path(
|
||||||
|
"data/processed/lab013"
|
||||||
|
)
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY.mkdir(
|
||||||
|
parents=True,
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Преобразование байтов в биты
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def bytes_to_bits(
|
||||||
|
data: bytes,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Преобразовать последовательность bytes
|
||||||
|
в массив отдельных битов 0 и 1.
|
||||||
|
|
||||||
|
Каждый исходный байт превращается в восемь битов.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(data, bytes):
|
||||||
|
raise TypeError(
|
||||||
|
"data должен иметь тип bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
byte_array = np.frombuffer(
|
||||||
|
data,
|
||||||
|
dtype=np.uint8,
|
||||||
|
)
|
||||||
|
|
||||||
|
bits = np.unpackbits(
|
||||||
|
byte_array
|
||||||
|
)
|
||||||
|
|
||||||
|
return bits
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Преобразование битов обратно в байты
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def bits_to_bytes(
|
||||||
|
bits: np.ndarray,
|
||||||
|
) -> bytes:
|
||||||
|
"""
|
||||||
|
Упаковать отдельные биты обратно в bytes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bits = np.asarray(
|
||||||
|
bits,
|
||||||
|
dtype=np.uint8,
|
||||||
|
)
|
||||||
|
|
||||||
|
if bits.ndim != 1:
|
||||||
|
raise ValueError(
|
||||||
|
"bits должен быть одномерным массивом"
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(bits) % 8 != 0:
|
||||||
|
raise ValueError(
|
||||||
|
"Количество битов должно быть кратно восьми"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not np.all(
|
||||||
|
(bits == 0) | (bits == 1)
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"Массив должен содержать только 0 и 1"
|
||||||
|
)
|
||||||
|
|
||||||
|
packed_bytes = np.packbits(
|
||||||
|
bits
|
||||||
|
)
|
||||||
|
|
||||||
|
return packed_bytes.tobytes()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# BPSK-модулятор
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def bpsk_modulate(
|
||||||
|
bits: np.ndarray,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Преобразовать биты в комплексные BPSK-символы.
|
||||||
|
|
||||||
|
Используем отображение:
|
||||||
|
|
||||||
|
0 → -1 + 0j
|
||||||
|
1 → +1 + 0j
|
||||||
|
"""
|
||||||
|
|
||||||
|
bits = np.asarray(
|
||||||
|
bits,
|
||||||
|
dtype=np.uint8,
|
||||||
|
)
|
||||||
|
|
||||||
|
real_symbols = (
|
||||||
|
2.0 * bits.astype(np.float64)
|
||||||
|
- 1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
iq_symbols = real_symbols.astype(
|
||||||
|
np.complex128
|
||||||
|
)
|
||||||
|
|
||||||
|
return iq_symbols
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Модель радиошума
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def add_awgn(
|
||||||
|
iq_samples: np.ndarray,
|
||||||
|
snr_db: float,
|
||||||
|
random_generator: np.random.Generator,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Добавить комплексный белый гауссов шум AWGN.
|
||||||
|
|
||||||
|
AWGN:
|
||||||
|
Additive White Gaussian Noise —
|
||||||
|
аддитивный белый гауссов шум.
|
||||||
|
|
||||||
|
snr_db:
|
||||||
|
Отношение средней мощности сигнала
|
||||||
|
к средней мощности шума в децибелах.
|
||||||
|
"""
|
||||||
|
|
||||||
|
signal_power = np.mean(
|
||||||
|
np.abs(iq_samples) ** 2
|
||||||
|
)
|
||||||
|
|
||||||
|
snr_linear = 10.0 ** (
|
||||||
|
snr_db / 10.0
|
||||||
|
)
|
||||||
|
|
||||||
|
noise_power = (
|
||||||
|
signal_power / snr_linear
|
||||||
|
)
|
||||||
|
|
||||||
|
# Комплексный шум имеет две составляющие:
|
||||||
|
# действительную I и мнимую Q.
|
||||||
|
#
|
||||||
|
# Поэтому мощность делится между ними пополам.
|
||||||
|
noise_sigma = np.sqrt(
|
||||||
|
noise_power / 2.0
|
||||||
|
)
|
||||||
|
|
||||||
|
noise = noise_sigma * (
|
||||||
|
random_generator.standard_normal(
|
||||||
|
len(iq_samples)
|
||||||
|
)
|
||||||
|
+ 1j
|
||||||
|
* random_generator.standard_normal(
|
||||||
|
len(iq_samples)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return iq_samples + noise
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# BPSK-демодулятор
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def bpsk_demodulate(
|
||||||
|
received_iq: np.ndarray,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Преобразовать принятые IQ-сэмплы в биты.
|
||||||
|
|
||||||
|
Правило решения:
|
||||||
|
|
||||||
|
I < 0 → бит 0
|
||||||
|
I >= 0 → бит 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
received_bits = (
|
||||||
|
received_iq.real >= 0.0
|
||||||
|
).astype(np.uint8)
|
||||||
|
|
||||||
|
return received_bits
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Формирование исходного пакета
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
payload = MESSAGE.encode(
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
original_packet = build_packet(
|
||||||
|
payload=payload,
|
||||||
|
message_type=MESSAGE_TYPE_TEXT,
|
||||||
|
sequence_number=SEQUENCE_NUMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
transmitted_bits = bytes_to_bits(
|
||||||
|
original_packet
|
||||||
|
)
|
||||||
|
|
||||||
|
transmitted_iq = bpsk_modulate(
|
||||||
|
transmitted_bits
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print(
|
||||||
|
"=== Lab013. BPSK в Python ==="
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nИсходное сообщение:")
|
||||||
|
print(MESSAGE)
|
||||||
|
|
||||||
|
print("\nРазмер пакета:")
|
||||||
|
print(
|
||||||
|
len(original_packet),
|
||||||
|
"байт",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nКоличество передаваемых битов:")
|
||||||
|
print(
|
||||||
|
len(transmitted_bits)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nПервые 32 бита:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
" ".join(
|
||||||
|
str(bit)
|
||||||
|
for bit in transmitted_bits[:32]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nПервые 16 BPSK-символов:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
transmitted_iq[:16]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Передача при разных уровнях SNR
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
experiment_results = []
|
||||||
|
|
||||||
|
constellation_samples = {}
|
||||||
|
|
||||||
|
for experiment_index, snr_db in enumerate(
|
||||||
|
SNR_VALUES_DB
|
||||||
|
):
|
||||||
|
|
||||||
|
random_generator = np.random.default_rng(
|
||||||
|
RANDOM_SEED + experiment_index
|
||||||
|
)
|
||||||
|
|
||||||
|
received_iq = add_awgn(
|
||||||
|
iq_samples=transmitted_iq,
|
||||||
|
snr_db=snr_db,
|
||||||
|
random_generator=random_generator,
|
||||||
|
)
|
||||||
|
|
||||||
|
received_bits = bpsk_demodulate(
|
||||||
|
received_iq
|
||||||
|
)
|
||||||
|
|
||||||
|
bit_error_count = int(
|
||||||
|
np.count_nonzero(
|
||||||
|
transmitted_bits != received_bits
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
bit_error_rate = (
|
||||||
|
bit_error_count
|
||||||
|
/ len(transmitted_bits)
|
||||||
|
)
|
||||||
|
|
||||||
|
received_packet = bits_to_bytes(
|
||||||
|
received_bits
|
||||||
|
)
|
||||||
|
|
||||||
|
packet_status = "CRC OK"
|
||||||
|
restored_message = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed_packet = parse_packet(
|
||||||
|
received_packet
|
||||||
|
)
|
||||||
|
|
||||||
|
restored_message = (
|
||||||
|
parsed_packet.payload.decode(
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
except CRCError:
|
||||||
|
packet_status = "CRC ERROR"
|
||||||
|
|
||||||
|
except PacketError:
|
||||||
|
packet_status = "PACKET ERROR"
|
||||||
|
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
packet_status = "UTF-8 ERROR"
|
||||||
|
|
||||||
|
experiment_results.append(
|
||||||
|
{
|
||||||
|
"snr_db": snr_db,
|
||||||
|
"bit_errors": bit_error_count,
|
||||||
|
"ber": bit_error_rate,
|
||||||
|
"packet_status": packet_status,
|
||||||
|
"restored_message": restored_message,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Для графика достаточно первых 300 символов.
|
||||||
|
constellation_samples[snr_db] = (
|
||||||
|
received_iq[:300]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод результатов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print("\nРезультаты передачи:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{'SNR':>8}"
|
||||||
|
f"{'Ошибки битов':>16}"
|
||||||
|
f"{'BER':>14}"
|
||||||
|
f"{'Пакет':>18}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("-" * 56)
|
||||||
|
|
||||||
|
for result in experiment_results:
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{result['snr_db']:>6.1f} дБ"
|
||||||
|
f"{result['bit_errors']:>16}"
|
||||||
|
f"{result['ber']:>14.6f}"
|
||||||
|
f"{result['packet_status']:>18}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nВосстановленные сообщения:")
|
||||||
|
|
||||||
|
for result in experiment_results:
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"SNR {result['snr_db']:>4.1f} дБ: "
|
||||||
|
f"{result['restored_message']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# График созвездия
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
figure, axes = plt.subplots(
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
figsize=(10, 8),
|
||||||
|
)
|
||||||
|
|
||||||
|
axes = axes.ravel()
|
||||||
|
|
||||||
|
for axis, snr_db in zip(
|
||||||
|
axes,
|
||||||
|
SNR_VALUES_DB,
|
||||||
|
):
|
||||||
|
|
||||||
|
received_iq = constellation_samples[
|
||||||
|
snr_db
|
||||||
|
]
|
||||||
|
|
||||||
|
axis.scatter(
|
||||||
|
received_iq.real,
|
||||||
|
received_iq.imag,
|
||||||
|
s=12,
|
||||||
|
alpha=0.6,
|
||||||
|
)
|
||||||
|
|
||||||
|
axis.axvline(
|
||||||
|
0.0,
|
||||||
|
linewidth=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
axis.set_title(
|
||||||
|
f"BPSK, SNR = {snr_db:.1f} дБ"
|
||||||
|
)
|
||||||
|
|
||||||
|
axis.set_xlabel(
|
||||||
|
"I — синфазная компонента"
|
||||||
|
)
|
||||||
|
|
||||||
|
axis.set_ylabel(
|
||||||
|
"Q — квадратурная компонента"
|
||||||
|
)
|
||||||
|
|
||||||
|
axis.set_xlim(
|
||||||
|
-2.5,
|
||||||
|
2.5,
|
||||||
|
)
|
||||||
|
|
||||||
|
axis.set_ylim(
|
||||||
|
-1.8,
|
||||||
|
1.8,
|
||||||
|
)
|
||||||
|
|
||||||
|
axis.grid(
|
||||||
|
True
|
||||||
|
)
|
||||||
|
|
||||||
|
figure.tight_layout()
|
||||||
|
|
||||||
|
constellation_path = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "lab013_bpsk_constellation.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
figure.savefig(
|
||||||
|
constellation_path,
|
||||||
|
dpi=150,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.close(
|
||||||
|
figure
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Автоматические проверки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# Без добавления шума преобразование должно быть обратимым.
|
||||||
|
ideal_received_bits = bpsk_demodulate(
|
||||||
|
transmitted_iq
|
||||||
|
)
|
||||||
|
|
||||||
|
ideal_received_packet = bits_to_bytes(
|
||||||
|
ideal_received_bits
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ideal_received_packet == original_packet
|
||||||
|
|
||||||
|
ideal_parsed_packet = parse_packet(
|
||||||
|
ideal_received_packet
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
ideal_parsed_packet.payload.decode(
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
== MESSAGE
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(experiment_results) == len(
|
||||||
|
SNR_VALUES_DB
|
||||||
|
)
|
||||||
|
|
||||||
|
assert constellation_path.exists()
|
||||||
|
|
||||||
|
|
||||||
|
print("\nГрафик созвездия:")
|
||||||
|
print(constellation_path)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\nПроверка пройдена: "
|
||||||
|
"пакет преобразован в BPSK IQ-сэмплы "
|
||||||
|
"и демодулирован обратно."
|
||||||
|
)
|
||||||
617
Projects/SDR_Rover/tests/lab014_bpsk_ber_curve.py
Normal file
@@ -0,0 +1,617 @@
|
|||||||
|
"""
|
||||||
|
Lab014. Измерение BER для BPSK в канале AWGN.
|
||||||
|
|
||||||
|
Программа:
|
||||||
|
|
||||||
|
1. Генерирует 1 000 000 случайных битов.
|
||||||
|
2. Преобразует их в BPSK-символы.
|
||||||
|
3. Добавляет комплексный гауссов шум.
|
||||||
|
4. Демодулирует принятый сигнал.
|
||||||
|
5. Измеряет экспериментальный BER.
|
||||||
|
6. Рассчитывает теоретический BER.
|
||||||
|
7. Оценивает вероятность повреждения пакетов разной длины.
|
||||||
|
8. Строит BER-кривую.
|
||||||
|
9. Сохраняет результаты в CSV.
|
||||||
|
|
||||||
|
Упрощения модели:
|
||||||
|
|
||||||
|
- один бит передаётся одним BPSK-символом;
|
||||||
|
- один символ представлен одним IQ-сэмплом;
|
||||||
|
- частотная и фазовая синхронизация идеальны;
|
||||||
|
- межсимвольные искажения отсутствуют;
|
||||||
|
- канал содержит только AWGN.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from csv import DictWriter
|
||||||
|
from math import (
|
||||||
|
erfc,
|
||||||
|
expm1,
|
||||||
|
log1p,
|
||||||
|
sqrt,
|
||||||
|
)
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Настройки эксперимента
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
BIT_COUNT = 1_000_000
|
||||||
|
|
||||||
|
EB_N0_VALUES_DB = [
|
||||||
|
-4.0,
|
||||||
|
-2.0,
|
||||||
|
0.0,
|
||||||
|
2.0,
|
||||||
|
4.0,
|
||||||
|
6.0,
|
||||||
|
8.0,
|
||||||
|
10.0,
|
||||||
|
12.0,
|
||||||
|
]
|
||||||
|
|
||||||
|
RANDOM_SEED = 2026
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY = Path(
|
||||||
|
"data/processed/lab014"
|
||||||
|
)
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY.mkdir(
|
||||||
|
parents=True,
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
GRAPH_PATH = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "lab014_bpsk_ber_curve.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
CSV_PATH = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "lab014_bpsk_ber_results.csv"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Размеры пакетов для оценки PER
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# Пакет из Lab013:
|
||||||
|
#
|
||||||
|
# заголовок 8 байт
|
||||||
|
# PAYLOAD 16 байт
|
||||||
|
# CRC 4 байта
|
||||||
|
#
|
||||||
|
# Итого 28 байт = 224 бита
|
||||||
|
SHORT_PACKET_BITS = 224
|
||||||
|
|
||||||
|
# Типовой пакет изображения:
|
||||||
|
#
|
||||||
|
# заголовок протокола 8 байт
|
||||||
|
# заголовок фрагмента 12 байт
|
||||||
|
# данные JPEG 512 байт
|
||||||
|
# CRC 4 байта
|
||||||
|
#
|
||||||
|
# Итого 536 байт
|
||||||
|
IMAGE_PACKET_BITS = 536 * 8
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вспомогательные функции
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def bpsk_modulate(
|
||||||
|
bits: np.ndarray,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Преобразовать биты в BPSK-символы.
|
||||||
|
|
||||||
|
Отображение:
|
||||||
|
|
||||||
|
0 → -1
|
||||||
|
1 → +1
|
||||||
|
"""
|
||||||
|
|
||||||
|
bits = np.asarray(
|
||||||
|
bits,
|
||||||
|
dtype=np.uint8,
|
||||||
|
)
|
||||||
|
|
||||||
|
if bits.ndim != 1:
|
||||||
|
raise ValueError(
|
||||||
|
"bits должен быть одномерным массивом"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not np.all(
|
||||||
|
(bits == 0) | (bits == 1)
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"bits должен содержать только 0 и 1"
|
||||||
|
)
|
||||||
|
|
||||||
|
symbols = (
|
||||||
|
2.0 * bits.astype(np.float64)
|
||||||
|
- 1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
return symbols.astype(
|
||||||
|
np.complex128
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_awgn(
|
||||||
|
iq_samples: np.ndarray,
|
||||||
|
eb_n0_db: float,
|
||||||
|
random_generator: np.random.Generator,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Добавить комплексный AWGN-шум.
|
||||||
|
|
||||||
|
Энергия BPSK-символа равна единице.
|
||||||
|
|
||||||
|
Для одного бита на символ:
|
||||||
|
|
||||||
|
Es/N0 = Eb/N0
|
||||||
|
"""
|
||||||
|
|
||||||
|
eb_n0_linear = 10.0 ** (
|
||||||
|
eb_n0_db / 10.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Для комплексного AWGN каждая компонента
|
||||||
|
# I и Q получает половину полной мощности шума.
|
||||||
|
noise_sigma = sqrt(
|
||||||
|
1.0
|
||||||
|
/ (
|
||||||
|
2.0
|
||||||
|
* eb_n0_linear
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
noise = noise_sigma * (
|
||||||
|
random_generator.standard_normal(
|
||||||
|
len(iq_samples)
|
||||||
|
)
|
||||||
|
+ 1j
|
||||||
|
* random_generator.standard_normal(
|
||||||
|
len(iq_samples)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return iq_samples + noise
|
||||||
|
|
||||||
|
|
||||||
|
def bpsk_demodulate(
|
||||||
|
received_iq: np.ndarray,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Демодулировать BPSK по знаку компоненты I.
|
||||||
|
|
||||||
|
I < 0 → бит 0
|
||||||
|
I >= 0 → бит 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
return (
|
||||||
|
received_iq.real >= 0.0
|
||||||
|
).astype(np.uint8)
|
||||||
|
|
||||||
|
|
||||||
|
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 ber_to_per(
|
||||||
|
ber: float,
|
||||||
|
packet_bit_count: int,
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Оценить Packet Error Rate из BER.
|
||||||
|
|
||||||
|
Предполагается:
|
||||||
|
|
||||||
|
- ошибки отдельных битов независимы;
|
||||||
|
- пакет считается повреждённым,
|
||||||
|
если ошибся хотя бы один бит.
|
||||||
|
|
||||||
|
PER = 1 - (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 0.0
|
||||||
|
|
||||||
|
if ber == 1.0:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
# Такая запись численно устойчивее,
|
||||||
|
# чем прямое возведение в степень
|
||||||
|
# для очень маленьких BER.
|
||||||
|
return -expm1(
|
||||||
|
packet_bit_count
|
||||||
|
* log1p(-ber)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Генерация исходной последовательности
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
bit_generator = np.random.default_rng(
|
||||||
|
RANDOM_SEED
|
||||||
|
)
|
||||||
|
|
||||||
|
transmitted_bits = bit_generator.integers(
|
||||||
|
low=0,
|
||||||
|
high=2,
|
||||||
|
size=BIT_COUNT,
|
||||||
|
dtype=np.uint8,
|
||||||
|
)
|
||||||
|
|
||||||
|
transmitted_iq = bpsk_modulate(
|
||||||
|
transmitted_bits
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Эксперимент при разных Eb/N0
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for experiment_index, eb_n0_db in enumerate(
|
||||||
|
EB_N0_VALUES_DB
|
||||||
|
):
|
||||||
|
|
||||||
|
noise_generator = np.random.default_rng(
|
||||||
|
RANDOM_SEED
|
||||||
|
+ 1000
|
||||||
|
+ experiment_index
|
||||||
|
)
|
||||||
|
|
||||||
|
received_iq = add_awgn(
|
||||||
|
iq_samples=transmitted_iq,
|
||||||
|
eb_n0_db=eb_n0_db,
|
||||||
|
random_generator=noise_generator,
|
||||||
|
)
|
||||||
|
|
||||||
|
received_bits = bpsk_demodulate(
|
||||||
|
received_iq
|
||||||
|
)
|
||||||
|
|
||||||
|
bit_error_count = int(
|
||||||
|
np.count_nonzero(
|
||||||
|
transmitted_bits
|
||||||
|
!= received_bits
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
experimental_ber = (
|
||||||
|
bit_error_count
|
||||||
|
/ BIT_COUNT
|
||||||
|
)
|
||||||
|
|
||||||
|
theoretical_ber = theoretical_bpsk_ber(
|
||||||
|
eb_n0_db
|
||||||
|
)
|
||||||
|
|
||||||
|
short_packet_per = ber_to_per(
|
||||||
|
theoretical_ber,
|
||||||
|
SHORT_PACKET_BITS,
|
||||||
|
)
|
||||||
|
|
||||||
|
image_packet_per = ber_to_per(
|
||||||
|
theoretical_ber,
|
||||||
|
IMAGE_PACKET_BITS,
|
||||||
|
)
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"eb_n0_db": eb_n0_db,
|
||||||
|
"bit_errors": bit_error_count,
|
||||||
|
"experimental_ber": experimental_ber,
|
||||||
|
"theoretical_ber": theoretical_ber,
|
||||||
|
"short_packet_per": short_packet_per,
|
||||||
|
"image_packet_per": image_packet_per,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод основных результатов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print(
|
||||||
|
"=== Lab014. BER-кривая BPSK ==="
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nКоличество переданных битов:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{BIT_COUNT:,}".replace(",", " ")
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nРезультаты:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{'Eb/N0':>9}"
|
||||||
|
f"{'Ошибки':>12}"
|
||||||
|
f"{'BER эксперимент':>19}"
|
||||||
|
f"{'BER теория':>16}"
|
||||||
|
f"{'PER 224 бит':>16}"
|
||||||
|
f"{'PER 4288 бит':>17}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("-" * 89)
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{result['eb_n0_db']:>6.1f} дБ"
|
||||||
|
f"{result['bit_errors']:>12}"
|
||||||
|
f"{result['experimental_ber']:>19.6e}"
|
||||||
|
f"{result['theoretical_ber']:>16.6e}"
|
||||||
|
f"{result['short_packet_per']:>16.6f}"
|
||||||
|
f"{result['image_packet_per']:>17.6f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Сохранение CSV
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
with CSV_PATH.open(
|
||||||
|
"w",
|
||||||
|
newline="",
|
||||||
|
encoding="utf-8-sig",
|
||||||
|
) as csv_file:
|
||||||
|
|
||||||
|
fieldnames = [
|
||||||
|
"eb_n0_db",
|
||||||
|
"bit_errors",
|
||||||
|
"experimental_ber",
|
||||||
|
"theoretical_ber",
|
||||||
|
"short_packet_per_224_bits",
|
||||||
|
"image_packet_per_4288_bits",
|
||||||
|
]
|
||||||
|
|
||||||
|
writer = DictWriter(
|
||||||
|
csv_file,
|
||||||
|
fieldnames=fieldnames,
|
||||||
|
)
|
||||||
|
|
||||||
|
writer.writeheader()
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
|
||||||
|
writer.writerow(
|
||||||
|
{
|
||||||
|
"eb_n0_db": result["eb_n0_db"],
|
||||||
|
"bit_errors": result["bit_errors"],
|
||||||
|
"experimental_ber": (
|
||||||
|
result["experimental_ber"]
|
||||||
|
),
|
||||||
|
"theoretical_ber": (
|
||||||
|
result["theoretical_ber"]
|
||||||
|
),
|
||||||
|
"short_packet_per_224_bits": (
|
||||||
|
result["short_packet_per"]
|
||||||
|
),
|
||||||
|
"image_packet_per_4288_bits": (
|
||||||
|
result["image_packet_per"]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Подготовка данных для графика
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
eb_n0_plot_values = np.array(
|
||||||
|
[
|
||||||
|
result["eb_n0_db"]
|
||||||
|
for result in results
|
||||||
|
],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
|
||||||
|
experimental_ber_values = np.array(
|
||||||
|
[
|
||||||
|
result["experimental_ber"]
|
||||||
|
for result in results
|
||||||
|
],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
|
||||||
|
theoretical_ber_values = np.array(
|
||||||
|
[
|
||||||
|
result["theoretical_ber"]
|
||||||
|
for result in results
|
||||||
|
],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Нулевой измеренный BER невозможно показать
|
||||||
|
# на логарифмической шкале.
|
||||||
|
#
|
||||||
|
# Поэтому для графика ставим такую точку
|
||||||
|
# на уровень половины одного наблюдаемого события.
|
||||||
|
measurement_floor = (
|
||||||
|
0.5 / BIT_COUNT
|
||||||
|
)
|
||||||
|
|
||||||
|
experimental_ber_for_plot = np.maximum(
|
||||||
|
experimental_ber_values,
|
||||||
|
measurement_floor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Построение BER-графика
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
figure = plt.figure(
|
||||||
|
figsize=(10, 7)
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.semilogy(
|
||||||
|
eb_n0_plot_values,
|
||||||
|
theoretical_ber_values,
|
||||||
|
marker="o",
|
||||||
|
label="Теоретический BER BPSK",
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.semilogy(
|
||||||
|
eb_n0_plot_values,
|
||||||
|
experimental_ber_for_plot,
|
||||||
|
marker="s",
|
||||||
|
linestyle="--",
|
||||||
|
label="Экспериментальный BER",
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.axhline(
|
||||||
|
measurement_floor,
|
||||||
|
linestyle=":",
|
||||||
|
label=(
|
||||||
|
"Предел измерения "
|
||||||
|
f"{measurement_floor:.1e}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.xlabel(
|
||||||
|
"Eb/N0, дБ"
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.ylabel(
|
||||||
|
"BER"
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.title(
|
||||||
|
"BPSK в канале AWGN: "
|
||||||
|
"эксперимент и теория"
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.grid(
|
||||||
|
True,
|
||||||
|
which="both",
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.legend()
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
figure.savefig(
|
||||||
|
GRAPH_PATH,
|
||||||
|
dpi=160,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.close(
|
||||||
|
figure
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Инженерные контрольные точки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print("\nИнженерные контрольные точки:")
|
||||||
|
|
||||||
|
for target_db in [
|
||||||
|
6.0,
|
||||||
|
8.0,
|
||||||
|
10.0,
|
||||||
|
]:
|
||||||
|
|
||||||
|
result = next(
|
||||||
|
item
|
||||||
|
for item in results
|
||||||
|
if item["eb_n0_db"] == target_db
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"\nEb/N0 = {target_db:.1f} дБ"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Теоретический BER:",
|
||||||
|
f"{result['theoretical_ber']:.6e}",
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"PER короткого пакета 224 бита:",
|
||||||
|
f"{result['short_packet_per'] * 100:.2f} %",
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"PER JPEG-пакета 4288 бит:",
|
||||||
|
f"{result['image_packet_per'] * 100:.2f} %",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Автоматические проверки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
assert len(transmitted_bits) == BIT_COUNT
|
||||||
|
|
||||||
|
assert len(received_bits) == BIT_COUNT
|
||||||
|
|
||||||
|
assert all(
|
||||||
|
0.0
|
||||||
|
<= result["experimental_ber"]
|
||||||
|
<= 1.0
|
||||||
|
for result in results
|
||||||
|
)
|
||||||
|
|
||||||
|
assert all(
|
||||||
|
0.0
|
||||||
|
<= result["theoretical_ber"]
|
||||||
|
<= 1.0
|
||||||
|
for result in results
|
||||||
|
)
|
||||||
|
|
||||||
|
assert GRAPH_PATH.exists()
|
||||||
|
|
||||||
|
assert CSV_PATH.exists()
|
||||||
|
|
||||||
|
|
||||||
|
print("\nГрафик BER:")
|
||||||
|
|
||||||
|
print(GRAPH_PATH)
|
||||||
|
|
||||||
|
print("\nТаблица CSV:")
|
||||||
|
|
||||||
|
print(CSV_PATH)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\nПроверка пройдена: "
|
||||||
|
"экспериментальная BER-кривая построена."
|
||||||
|
)
|
||||||
810
Projects/SDR_Rover/tests/lab015_fragment_size_optimization.py
Normal file
@@ -0,0 +1,810 @@
|
|||||||
|
"""
|
||||||
|
Lab015. Подбор оптимального размера фрагмента изображения.
|
||||||
|
|
||||||
|
Программа сравнивает фрагменты размером:
|
||||||
|
|
||||||
|
- 64 байта;
|
||||||
|
- 128 байт;
|
||||||
|
- 256 байт;
|
||||||
|
- 512 байт;
|
||||||
|
- 1024 байта.
|
||||||
|
|
||||||
|
Для каждого размера рассчитываются:
|
||||||
|
|
||||||
|
1. Количество фрагментов изображения.
|
||||||
|
2. Служебные расходы протокола.
|
||||||
|
3. Теоретический BER BPSK.
|
||||||
|
4. Вероятность успешной доставки DATA-пакета.
|
||||||
|
5. Вероятность успешной доставки ACK.
|
||||||
|
6. Среднее число передач одного фрагмента.
|
||||||
|
7. Ожидаемый полный радиообмен с ARQ.
|
||||||
|
8. Время передачи фотографии при 20 кбит/с.
|
||||||
|
9. Эффективная полезная скорость.
|
||||||
|
|
||||||
|
Модель канала:
|
||||||
|
|
||||||
|
- когерентная BPSK;
|
||||||
|
- AWGN;
|
||||||
|
- ошибки битов независимы;
|
||||||
|
- повреждённый пакет отбрасывается по CRC;
|
||||||
|
- применяется Stop-and-Wait ARQ;
|
||||||
|
- DATA и ACK работают при одинаковом Eb/N0.
|
||||||
|
|
||||||
|
Не учитываются:
|
||||||
|
|
||||||
|
- ожидание тайм-аута;
|
||||||
|
- паузы между DATA и ACK;
|
||||||
|
- преамбула;
|
||||||
|
- синхронизация;
|
||||||
|
- FEC;
|
||||||
|
- многолучёвость;
|
||||||
|
- частотные и фазовые ошибки.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from csv import DictWriter
|
||||||
|
from math import (
|
||||||
|
erfc,
|
||||||
|
exp,
|
||||||
|
log1p,
|
||||||
|
sqrt,
|
||||||
|
)
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
from protocol.image_fragments import (
|
||||||
|
encode_image_fragment,
|
||||||
|
split_image_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
from protocol.packet import (
|
||||||
|
MESSAGE_TYPE_ACK,
|
||||||
|
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||||
|
build_packet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Настройки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
SOURCE_PATH = Path(
|
||||||
|
"data/raw/lab009_source.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY = Path(
|
||||||
|
"data/processed/lab015"
|
||||||
|
)
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY.mkdir(
|
||||||
|
parents=True,
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
FRAGMENT_SIZES = [
|
||||||
|
64,
|
||||||
|
128,
|
||||||
|
256,
|
||||||
|
512,
|
||||||
|
1024,
|
||||||
|
]
|
||||||
|
|
||||||
|
EB_N0_VALUES_DB = [
|
||||||
|
6.0,
|
||||||
|
8.0,
|
||||||
|
10.0,
|
||||||
|
12.0,
|
||||||
|
]
|
||||||
|
|
||||||
|
CHANNEL_BITRATE_BPS = 20_000
|
||||||
|
|
||||||
|
IMAGE_ID_BASE = 2026071600
|
||||||
|
|
||||||
|
CSV_PATH = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "lab015_fragment_results.csv"
|
||||||
|
)
|
||||||
|
|
||||||
|
TIME_GRAPH_PATH = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "lab015_transfer_time.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
GOODPUT_GRAPH_PATH = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "lab015_effective_goodput.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вспомогательные функции
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
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_success = (1 - BER) ** N
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not 0.0 <= ber <= 1.0:
|
||||||
|
raise ValueError(
|
||||||
|
"BER должен находиться в диапазоне 0...1"
|
||||||
|
)
|
||||||
|
|
||||||
|
if packet_bit_count <= 0:
|
||||||
|
raise ValueError(
|
||||||
|
"Размер пакета должен быть положительным"
|
||||||
|
)
|
||||||
|
|
||||||
|
if ber == 0.0:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
if ber == 1.0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
# Численно устойчивый вариант выражения:
|
||||||
|
#
|
||||||
|
# (1 - BER) ** N
|
||||||
|
return exp(
|
||||||
|
packet_bit_count
|
||||||
|
* log1p(-ber)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_bytes(
|
||||||
|
byte_count: float,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Представить объём в удобном виде.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if byte_count < 1024:
|
||||||
|
return f"{byte_count:.0f} байт"
|
||||||
|
|
||||||
|
kibibytes = byte_count / 1024
|
||||||
|
|
||||||
|
if kibibytes < 1024:
|
||||||
|
return f"{kibibytes:.2f} КиБ"
|
||||||
|
|
||||||
|
mebibytes = kibibytes / 1024
|
||||||
|
|
||||||
|
return f"{mebibytes:.2f} МиБ"
|
||||||
|
|
||||||
|
|
||||||
|
def format_duration(
|
||||||
|
seconds: float,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Представить длительность в удобном виде.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if seconds < 1:
|
||||||
|
return f"{seconds * 1000:.0f} мс"
|
||||||
|
|
||||||
|
if seconds < 60:
|
||||||
|
return f"{seconds:.2f} с"
|
||||||
|
|
||||||
|
if seconds < 3600:
|
||||||
|
minutes = int(seconds // 60)
|
||||||
|
remaining_seconds = seconds % 60
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"{minutes} мин "
|
||||||
|
f"{remaining_seconds:.1f} с"
|
||||||
|
)
|
||||||
|
|
||||||
|
hours = int(seconds // 3600)
|
||||||
|
remaining_minutes = (
|
||||||
|
seconds % 3600
|
||||||
|
) / 60
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"{hours} ч "
|
||||||
|
f"{remaining_minutes:.1f} мин"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверка исходного JPEG
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
if not SOURCE_PATH.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Не найден файл: {SOURCE_PATH}. "
|
||||||
|
"Сначала необходимо выполнить Lab009."
|
||||||
|
)
|
||||||
|
|
||||||
|
source_bytes = SOURCE_PATH.read_bytes()
|
||||||
|
|
||||||
|
if not source_bytes:
|
||||||
|
raise ValueError(
|
||||||
|
"Исходный JPEG пуст"
|
||||||
|
)
|
||||||
|
|
||||||
|
source_size = len(source_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Размер ACK
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
ack_packet = build_packet(
|
||||||
|
payload=b"",
|
||||||
|
message_type=MESSAGE_TYPE_ACK,
|
||||||
|
sequence_number=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
ack_packet_size_bytes = len(
|
||||||
|
ack_packet
|
||||||
|
)
|
||||||
|
|
||||||
|
ack_packet_size_bits = (
|
||||||
|
ack_packet_size_bytes * 8
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Основной расчёт
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for fragment_size_index, fragment_size in enumerate(
|
||||||
|
FRAGMENT_SIZES
|
||||||
|
):
|
||||||
|
|
||||||
|
fragments = split_image_bytes(
|
||||||
|
image_bytes=source_bytes,
|
||||||
|
image_id=(
|
||||||
|
IMAGE_ID_BASE
|
||||||
|
+ fragment_size_index
|
||||||
|
),
|
||||||
|
fragment_data_size=fragment_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
data_packet_sizes_bytes = []
|
||||||
|
|
||||||
|
for fragment in fragments:
|
||||||
|
|
||||||
|
fragment_payload = encode_image_fragment(
|
||||||
|
fragment
|
||||||
|
)
|
||||||
|
|
||||||
|
data_packet = build_packet(
|
||||||
|
payload=fragment_payload,
|
||||||
|
message_type=(
|
||||||
|
MESSAGE_TYPE_IMAGE_FRAGMENT
|
||||||
|
),
|
||||||
|
sequence_number=(
|
||||||
|
fragment.fragment_index
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
data_packet_sizes_bytes.append(
|
||||||
|
len(data_packet)
|
||||||
|
)
|
||||||
|
|
||||||
|
ideal_data_bytes = sum(
|
||||||
|
data_packet_sizes_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
ideal_ack_bytes = (
|
||||||
|
len(fragments)
|
||||||
|
* ack_packet_size_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
ideal_total_bytes = (
|
||||||
|
ideal_data_bytes
|
||||||
|
+ ideal_ack_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
ideal_efficiency_percent = (
|
||||||
|
source_size
|
||||||
|
/ ideal_total_bytes
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
|
||||||
|
for eb_n0_db in EB_N0_VALUES_DB:
|
||||||
|
|
||||||
|
ber = theoretical_bpsk_ber(
|
||||||
|
eb_n0_db
|
||||||
|
)
|
||||||
|
|
||||||
|
ack_success_probability = (
|
||||||
|
packet_success_probability(
|
||||||
|
ber=ber,
|
||||||
|
packet_bit_count=(
|
||||||
|
ack_packet_size_bits
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_data_bytes = 0.0
|
||||||
|
expected_ack_bytes = 0.0
|
||||||
|
|
||||||
|
expected_data_transmissions = 0.0
|
||||||
|
|
||||||
|
data_success_probabilities = []
|
||||||
|
|
||||||
|
for data_packet_size_bytes in (
|
||||||
|
data_packet_sizes_bytes
|
||||||
|
):
|
||||||
|
|
||||||
|
data_packet_size_bits = (
|
||||||
|
data_packet_size_bytes * 8
|
||||||
|
)
|
||||||
|
|
||||||
|
data_success_probability = (
|
||||||
|
packet_success_probability(
|
||||||
|
ber=ber,
|
||||||
|
packet_bit_count=(
|
||||||
|
data_packet_size_bits
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
data_success_probabilities.append(
|
||||||
|
data_success_probability
|
||||||
|
)
|
||||||
|
|
||||||
|
# Для успешного завершения попытки должны
|
||||||
|
# одновременно правильно пройти DATA и ACK.
|
||||||
|
confirmed_attempt_probability = (
|
||||||
|
data_success_probability
|
||||||
|
* ack_success_probability
|
||||||
|
)
|
||||||
|
|
||||||
|
if confirmed_attempt_probability == 0.0:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Вероятность подтверждения "
|
||||||
|
"оказалась равной нулю"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Число DATA-передач до успешного ACK
|
||||||
|
# подчиняется геометрическому распределению.
|
||||||
|
expected_attempt_count = (
|
||||||
|
1.0
|
||||||
|
/ confirmed_attempt_probability
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_data_transmissions += (
|
||||||
|
expected_attempt_count
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_data_bytes += (
|
||||||
|
data_packet_size_bytes
|
||||||
|
* expected_attempt_count
|
||||||
|
)
|
||||||
|
|
||||||
|
# ACK передаётся только после правильно
|
||||||
|
# принятого DATA.
|
||||||
|
#
|
||||||
|
# Среднее количество передач ACK до
|
||||||
|
# успешного ACK равно 1 / P_ACK.
|
||||||
|
expected_ack_bytes += (
|
||||||
|
ack_packet_size_bytes
|
||||||
|
/ ack_success_probability
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_total_bytes = (
|
||||||
|
expected_data_bytes
|
||||||
|
+ expected_ack_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_transfer_seconds = (
|
||||||
|
expected_total_bytes
|
||||||
|
* 8
|
||||||
|
/ CHANNEL_BITRATE_BPS
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_efficiency_percent = (
|
||||||
|
source_size
|
||||||
|
/ expected_total_bytes
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
|
||||||
|
effective_goodput_bps = (
|
||||||
|
source_size
|
||||||
|
* 8
|
||||||
|
/ expected_transfer_seconds
|
||||||
|
)
|
||||||
|
|
||||||
|
average_data_transmissions = (
|
||||||
|
expected_data_transmissions
|
||||||
|
/ len(fragments)
|
||||||
|
)
|
||||||
|
|
||||||
|
minimum_data_success_probability = min(
|
||||||
|
data_success_probabilities
|
||||||
|
)
|
||||||
|
|
||||||
|
maximum_data_success_probability = max(
|
||||||
|
data_success_probabilities
|
||||||
|
)
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"fragment_size": fragment_size,
|
||||||
|
"fragment_count": len(fragments),
|
||||||
|
"eb_n0_db": eb_n0_db,
|
||||||
|
"ber": ber,
|
||||||
|
"ideal_total_bytes": (
|
||||||
|
ideal_total_bytes
|
||||||
|
),
|
||||||
|
"ideal_efficiency_percent": (
|
||||||
|
ideal_efficiency_percent
|
||||||
|
),
|
||||||
|
"ack_success_probability": (
|
||||||
|
ack_success_probability
|
||||||
|
),
|
||||||
|
"minimum_data_success_probability": (
|
||||||
|
minimum_data_success_probability
|
||||||
|
),
|
||||||
|
"maximum_data_success_probability": (
|
||||||
|
maximum_data_success_probability
|
||||||
|
),
|
||||||
|
"average_data_transmissions": (
|
||||||
|
average_data_transmissions
|
||||||
|
),
|
||||||
|
"expected_total_bytes": (
|
||||||
|
expected_total_bytes
|
||||||
|
),
|
||||||
|
"expected_transfer_seconds": (
|
||||||
|
expected_transfer_seconds
|
||||||
|
),
|
||||||
|
"expected_efficiency_percent": (
|
||||||
|
expected_efficiency_percent
|
||||||
|
),
|
||||||
|
"effective_goodput_bps": (
|
||||||
|
effective_goodput_bps
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод общих данных
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print(
|
||||||
|
"=== Lab015. Оптимальный размер фрагмента ==="
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nИсходный JPEG:")
|
||||||
|
|
||||||
|
print(SOURCE_PATH)
|
||||||
|
|
||||||
|
print("\nРазмер JPEG:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
format_bytes(source_size)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nСкорость физического канала:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
CHANNEL_BITRATE_BPS / 1000,
|
||||||
|
"кбит/с",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nРазмер ACK-пакета:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
ack_packet_size_bytes,
|
||||||
|
"байт",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод результатов отдельно для каждого Eb/N0
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
for eb_n0_db in EB_N0_VALUES_DB:
|
||||||
|
|
||||||
|
selected_results = [
|
||||||
|
result
|
||||||
|
for result in results
|
||||||
|
if result["eb_n0_db"] == eb_n0_db
|
||||||
|
]
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"\n--- Eb/N0 = {eb_n0_db:.1f} дБ ---"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{'Фрагмент':>10}"
|
||||||
|
f"{'Кол-во':>9}"
|
||||||
|
f"{'P DATA':>12}"
|
||||||
|
f"{'Попыток':>11}"
|
||||||
|
f"{'Объём с ARQ':>16}"
|
||||||
|
f"{'Время':>16}"
|
||||||
|
f"{'Goodput':>13}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("-" * 87)
|
||||||
|
|
||||||
|
for result in selected_results:
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{result['fragment_size']:>8} Б"
|
||||||
|
f"{result['fragment_count']:>9}"
|
||||||
|
f"{result['minimum_data_success_probability']:>12.6f}"
|
||||||
|
f"{result['average_data_transmissions']:>11.2f}"
|
||||||
|
f"{format_bytes(result['expected_total_bytes']):>16}"
|
||||||
|
f"{format_duration(result['expected_transfer_seconds']):>16}"
|
||||||
|
f"{result['effective_goodput_bps'] / 1000:>10.2f} кбит/с"
|
||||||
|
)
|
||||||
|
|
||||||
|
optimum_result = min(
|
||||||
|
selected_results,
|
||||||
|
key=lambda item: (
|
||||||
|
item["expected_transfer_seconds"]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\nОптимальный размер фрагмента:"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
optimum_result["fragment_size"],
|
||||||
|
"байт",
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Ожидаемое время:",
|
||||||
|
format_duration(
|
||||||
|
optimum_result[
|
||||||
|
"expected_transfer_seconds"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Полезная скорость:",
|
||||||
|
f"{optimum_result['effective_goodput_bps'] / 1000:.2f}",
|
||||||
|
"кбит/с",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Сохранение CSV
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
with CSV_PATH.open(
|
||||||
|
"w",
|
||||||
|
newline="",
|
||||||
|
encoding="utf-8-sig",
|
||||||
|
) as csv_file:
|
||||||
|
|
||||||
|
fieldnames = [
|
||||||
|
"fragment_size",
|
||||||
|
"fragment_count",
|
||||||
|
"eb_n0_db",
|
||||||
|
"ber",
|
||||||
|
"ideal_total_bytes",
|
||||||
|
"ideal_efficiency_percent",
|
||||||
|
"ack_success_probability",
|
||||||
|
"minimum_data_success_probability",
|
||||||
|
"maximum_data_success_probability",
|
||||||
|
"average_data_transmissions",
|
||||||
|
"expected_total_bytes",
|
||||||
|
"expected_transfer_seconds",
|
||||||
|
"expected_efficiency_percent",
|
||||||
|
"effective_goodput_bps",
|
||||||
|
]
|
||||||
|
|
||||||
|
writer = DictWriter(
|
||||||
|
csv_file,
|
||||||
|
fieldnames=fieldnames,
|
||||||
|
)
|
||||||
|
|
||||||
|
writer.writeheader()
|
||||||
|
|
||||||
|
writer.writerows(results)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# График ожидаемого времени передачи
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
plt.figure(
|
||||||
|
figsize=(10, 7)
|
||||||
|
)
|
||||||
|
|
||||||
|
for eb_n0_db in EB_N0_VALUES_DB:
|
||||||
|
|
||||||
|
selected_results = [
|
||||||
|
result
|
||||||
|
for result in results
|
||||||
|
if result["eb_n0_db"] == eb_n0_db
|
||||||
|
]
|
||||||
|
|
||||||
|
fragment_sizes = [
|
||||||
|
result["fragment_size"]
|
||||||
|
for result in selected_results
|
||||||
|
]
|
||||||
|
|
||||||
|
transfer_times = [
|
||||||
|
result["expected_transfer_seconds"]
|
||||||
|
for result in selected_results
|
||||||
|
]
|
||||||
|
|
||||||
|
plt.plot(
|
||||||
|
fragment_sizes,
|
||||||
|
transfer_times,
|
||||||
|
marker="o",
|
||||||
|
label=f"Eb/N0 = {eb_n0_db:.0f} дБ",
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.xscale(
|
||||||
|
"log",
|
||||||
|
base=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.yscale(
|
||||||
|
"log",
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.xlabel(
|
||||||
|
"Размер данных фрагмента, байт"
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.ylabel(
|
||||||
|
"Ожидаемое время передачи, с"
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.title(
|
||||||
|
"Влияние размера фрагмента "
|
||||||
|
"на время передачи JPEG"
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.grid(
|
||||||
|
True,
|
||||||
|
which="both",
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.legend()
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
plt.savefig(
|
||||||
|
TIME_GRAPH_PATH,
|
||||||
|
dpi=160,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# График эффективной полезной скорости
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
plt.figure(
|
||||||
|
figsize=(10, 7)
|
||||||
|
)
|
||||||
|
|
||||||
|
for eb_n0_db in EB_N0_VALUES_DB:
|
||||||
|
|
||||||
|
selected_results = [
|
||||||
|
result
|
||||||
|
for result in results
|
||||||
|
if result["eb_n0_db"] == eb_n0_db
|
||||||
|
]
|
||||||
|
|
||||||
|
fragment_sizes = [
|
||||||
|
result["fragment_size"]
|
||||||
|
for result in selected_results
|
||||||
|
]
|
||||||
|
|
||||||
|
goodput_values = [
|
||||||
|
result["effective_goodput_bps"] / 1000
|
||||||
|
for result in selected_results
|
||||||
|
]
|
||||||
|
|
||||||
|
plt.plot(
|
||||||
|
fragment_sizes,
|
||||||
|
goodput_values,
|
||||||
|
marker="o",
|
||||||
|
label=f"Eb/N0 = {eb_n0_db:.0f} дБ",
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.xscale(
|
||||||
|
"log",
|
||||||
|
base=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.xlabel(
|
||||||
|
"Размер данных фрагмента, байт"
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.ylabel(
|
||||||
|
"Полезная скорость, кбит/с"
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.title(
|
||||||
|
"Полезная скорость JPEG "
|
||||||
|
"с учётом CRC и ARQ"
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.grid(
|
||||||
|
True,
|
||||||
|
which="both",
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.legend()
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
plt.savefig(
|
||||||
|
GOODPUT_GRAPH_PATH,
|
||||||
|
dpi=160,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Автоматические проверки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
assert results
|
||||||
|
|
||||||
|
assert CSV_PATH.exists()
|
||||||
|
|
||||||
|
assert TIME_GRAPH_PATH.exists()
|
||||||
|
|
||||||
|
assert GOODPUT_GRAPH_PATH.exists()
|
||||||
|
|
||||||
|
assert all(
|
||||||
|
result["expected_transfer_seconds"] > 0
|
||||||
|
for result in results
|
||||||
|
)
|
||||||
|
|
||||||
|
assert all(
|
||||||
|
0.0
|
||||||
|
< result["ack_success_probability"]
|
||||||
|
<= 1.0
|
||||||
|
for result in results
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nCSV:")
|
||||||
|
|
||||||
|
print(CSV_PATH)
|
||||||
|
|
||||||
|
print("\nГрафик времени:")
|
||||||
|
|
||||||
|
print(TIME_GRAPH_PATH)
|
||||||
|
|
||||||
|
print("\nГрафик полезной скорости:")
|
||||||
|
|
||||||
|
print(GOODPUT_GRAPH_PATH)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\nПроверка пройдена: "
|
||||||
|
"оптимальный размер фрагмента рассчитан."
|
||||||
|
)
|
||||||
605
Projects/SDR_Rover/tests/lab016_adaptive_fragment_size.py
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
"""
|
||||||
|
Lab016. Адаптивный выбор размера фрагмента изображения.
|
||||||
|
|
||||||
|
Программа имитирует изменение качества канала во времени.
|
||||||
|
|
||||||
|
Для каждой оценки Eb/N0 передатчик решает:
|
||||||
|
|
||||||
|
- отключить изображения;
|
||||||
|
- использовать 128 байт;
|
||||||
|
- использовать 512 байт;
|
||||||
|
- использовать 1024 байта.
|
||||||
|
|
||||||
|
Дополнительно рассчитывается ожидаемое время передачи
|
||||||
|
реального JPEG-файла.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from csv import DictWriter
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from protocol.image_fragments import (
|
||||||
|
encode_image_fragment,
|
||||||
|
split_image_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
from protocol.link_adaptation import (
|
||||||
|
choose_image_mode,
|
||||||
|
packet_success_probability,
|
||||||
|
)
|
||||||
|
|
||||||
|
from protocol.packet import (
|
||||||
|
MESSAGE_TYPE_ACK,
|
||||||
|
MESSAGE_TYPE_IMAGE_FRAGMENT,
|
||||||
|
build_packet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Настройки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
CHANNEL_BITRATE_BPS = 20_000
|
||||||
|
|
||||||
|
MAX_ATTEMPTS = 5
|
||||||
|
|
||||||
|
CANDIDATE_FRAGMENT_SIZES = (
|
||||||
|
128,
|
||||||
|
512,
|
||||||
|
1024,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Изменение качества канала во времени.
|
||||||
|
EB_N0_PROFILE_DB = [
|
||||||
|
12.0,
|
||||||
|
10.0,
|
||||||
|
9.0,
|
||||||
|
8.0,
|
||||||
|
7.0,
|
||||||
|
6.0,
|
||||||
|
7.0,
|
||||||
|
8.0,
|
||||||
|
9.0,
|
||||||
|
10.0,
|
||||||
|
12.0,
|
||||||
|
]
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY = Path(
|
||||||
|
"data/processed/lab016"
|
||||||
|
)
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY.mkdir(
|
||||||
|
parents=True,
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
CSV_PATH = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "lab016_adaptation_results.csv"
|
||||||
|
)
|
||||||
|
|
||||||
|
MODE_GRAPH_PATH = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "lab016_selected_mode.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
TIME_GRAPH_PATH = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "lab016_expected_transfer_time.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Выбор исходного кадра
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
source_candidates = [
|
||||||
|
Path(
|
||||||
|
"data/processed/lab012/"
|
||||||
|
"03_color_320_q15.jpg"
|
||||||
|
),
|
||||||
|
Path(
|
||||||
|
"data/raw/lab009_source.jpg"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
SOURCE_PATH = next(
|
||||||
|
(
|
||||||
|
path
|
||||||
|
for path in source_candidates
|
||||||
|
if path.exists()
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if SOURCE_PATH is None:
|
||||||
|
raise FileNotFoundError(
|
||||||
|
"Не найден кадр для передачи. "
|
||||||
|
"Необходимо выполнить Lab009 или Lab012."
|
||||||
|
)
|
||||||
|
|
||||||
|
source_bytes = SOURCE_PATH.read_bytes()
|
||||||
|
|
||||||
|
if not source_bytes:
|
||||||
|
raise ValueError(
|
||||||
|
"Исходный JPEG пуст"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Оценка передачи всего изображения
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def estimate_image_transfer(
|
||||||
|
image_bytes: bytes,
|
||||||
|
fragment_size: int,
|
||||||
|
ber: float,
|
||||||
|
image_id: int,
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Оценить передачу полного изображения с бесконечным ARQ.
|
||||||
|
|
||||||
|
Расчёт учитывает фактический размер последнего фрагмента.
|
||||||
|
"""
|
||||||
|
|
||||||
|
fragments = split_image_bytes(
|
||||||
|
image_bytes=image_bytes,
|
||||||
|
image_id=image_id,
|
||||||
|
fragment_data_size=fragment_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
ack_packet = build_packet(
|
||||||
|
payload=b"",
|
||||||
|
message_type=MESSAGE_TYPE_ACK,
|
||||||
|
sequence_number=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
ack_size_bytes = len(
|
||||||
|
ack_packet
|
||||||
|
)
|
||||||
|
|
||||||
|
ack_success_probability = (
|
||||||
|
packet_success_probability(
|
||||||
|
ber=ber,
|
||||||
|
packet_bit_count=(
|
||||||
|
ack_size_bytes * 8
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_total_bytes = 0.0
|
||||||
|
|
||||||
|
for fragment in fragments:
|
||||||
|
|
||||||
|
fragment_payload = encode_image_fragment(
|
||||||
|
fragment
|
||||||
|
)
|
||||||
|
|
||||||
|
data_packet = build_packet(
|
||||||
|
payload=fragment_payload,
|
||||||
|
message_type=(
|
||||||
|
MESSAGE_TYPE_IMAGE_FRAGMENT
|
||||||
|
),
|
||||||
|
sequence_number=(
|
||||||
|
fragment.fragment_index
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
data_success_probability = (
|
||||||
|
packet_success_probability(
|
||||||
|
ber=ber,
|
||||||
|
packet_bit_count=(
|
||||||
|
len(data_packet) * 8
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
confirmed_probability = (
|
||||||
|
data_success_probability
|
||||||
|
* ack_success_probability
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_total_bytes += (
|
||||||
|
len(data_packet)
|
||||||
|
/ confirmed_probability
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_total_bytes += (
|
||||||
|
ack_size_bytes
|
||||||
|
/ ack_success_probability
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_seconds = (
|
||||||
|
expected_total_bytes
|
||||||
|
* 8
|
||||||
|
/ CHANNEL_BITRATE_BPS
|
||||||
|
)
|
||||||
|
|
||||||
|
effective_goodput_bps = (
|
||||||
|
len(image_bytes)
|
||||||
|
* 8
|
||||||
|
/ expected_seconds
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"fragment_count": len(fragments),
|
||||||
|
"expected_total_bytes": expected_total_bytes,
|
||||||
|
"expected_seconds": expected_seconds,
|
||||||
|
"effective_goodput_bps": (
|
||||||
|
effective_goodput_bps
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Адаптация по профилю канала
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for step_index, eb_n0_db in enumerate(
|
||||||
|
EB_N0_PROFILE_DB
|
||||||
|
):
|
||||||
|
|
||||||
|
decision = choose_image_mode(
|
||||||
|
eb_n0_db=eb_n0_db,
|
||||||
|
candidate_fragment_sizes=(
|
||||||
|
CANDIDATE_FRAGMENT_SIZES
|
||||||
|
),
|
||||||
|
channel_bitrate_bps=(
|
||||||
|
CHANNEL_BITRATE_BPS
|
||||||
|
),
|
||||||
|
max_attempts=MAX_ATTEMPTS,
|
||||||
|
minimum_success_probability=0.85,
|
||||||
|
minimum_goodput_bps=2_000.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if decision.images_enabled:
|
||||||
|
|
||||||
|
selected_estimate = next(
|
||||||
|
estimate
|
||||||
|
for estimate in decision.estimates
|
||||||
|
if (
|
||||||
|
estimate.fragment_size
|
||||||
|
== decision.selected_fragment_size
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
image_result = estimate_image_transfer(
|
||||||
|
image_bytes=source_bytes,
|
||||||
|
fragment_size=(
|
||||||
|
decision.selected_fragment_size
|
||||||
|
),
|
||||||
|
ber=selected_estimate.ber,
|
||||||
|
image_id=2026071700 + step_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
fragment_size = (
|
||||||
|
decision.selected_fragment_size
|
||||||
|
)
|
||||||
|
|
||||||
|
fragment_count = (
|
||||||
|
image_result["fragment_count"]
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_seconds = (
|
||||||
|
image_result["expected_seconds"]
|
||||||
|
)
|
||||||
|
|
||||||
|
effective_goodput_bps = (
|
||||||
|
image_result[
|
||||||
|
"effective_goodput_bps"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
success_with_retries = (
|
||||||
|
selected_estimate
|
||||||
|
.success_probability_with_retries
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_attempts = (
|
||||||
|
selected_estimate.expected_attempts
|
||||||
|
)
|
||||||
|
|
||||||
|
mode_name = (
|
||||||
|
f"{fragment_size} B"
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
fragment_size = 0
|
||||||
|
fragment_count = 0
|
||||||
|
expected_seconds = None
|
||||||
|
effective_goodput_bps = 0.0
|
||||||
|
success_with_retries = 0.0
|
||||||
|
expected_attempts = 0.0
|
||||||
|
mode_name = "IMAGE OFF"
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"step": step_index,
|
||||||
|
"eb_n0_db": eb_n0_db,
|
||||||
|
"mode": mode_name,
|
||||||
|
"fragment_size": fragment_size,
|
||||||
|
"fragment_count": fragment_count,
|
||||||
|
"expected_seconds": expected_seconds,
|
||||||
|
"effective_goodput_bps": (
|
||||||
|
effective_goodput_bps
|
||||||
|
),
|
||||||
|
"success_with_retries": (
|
||||||
|
success_with_retries
|
||||||
|
),
|
||||||
|
"expected_attempts": (
|
||||||
|
expected_attempts
|
||||||
|
),
|
||||||
|
"reason": decision.reason,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print(
|
||||||
|
"=== Lab016. Адаптация размера фрагмента ==="
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nИсходный кадр:")
|
||||||
|
|
||||||
|
print(SOURCE_PATH)
|
||||||
|
|
||||||
|
print("\nРазмер JPEG:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
len(source_bytes),
|
||||||
|
"байт",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nРезультаты адаптации:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{'Шаг':>5}"
|
||||||
|
f"{'Eb/N0':>10}"
|
||||||
|
f"{'Режим':>14}"
|
||||||
|
f"{'Фрагм.':>9}"
|
||||||
|
f"{'Попыток':>11}"
|
||||||
|
f"{'Успех x5':>12}"
|
||||||
|
f"{'Время кадра':>15}"
|
||||||
|
f"{'Goodput':>13}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("-" * 89)
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
|
||||||
|
if result["expected_seconds"] is None:
|
||||||
|
time_text = "—"
|
||||||
|
else:
|
||||||
|
time_text = (
|
||||||
|
f"{result['expected_seconds']:.2f} с"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{result['step']:>5}"
|
||||||
|
f"{result['eb_n0_db']:>7.1f} дБ"
|
||||||
|
f"{result['mode']:>14}"
|
||||||
|
f"{result['fragment_count']:>9}"
|
||||||
|
f"{result['expected_attempts']:>11.2f}"
|
||||||
|
f"{result['success_with_retries'] * 100:>10.1f} %"
|
||||||
|
f"{time_text:>15}"
|
||||||
|
f"{result['effective_goodput_bps'] / 1000:>10.2f} кбит/с"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Сохранение CSV
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
with CSV_PATH.open(
|
||||||
|
"w",
|
||||||
|
newline="",
|
||||||
|
encoding="utf-8-sig",
|
||||||
|
) as csv_file:
|
||||||
|
|
||||||
|
fieldnames = list(
|
||||||
|
results[0].keys()
|
||||||
|
)
|
||||||
|
|
||||||
|
writer = DictWriter(
|
||||||
|
csv_file,
|
||||||
|
fieldnames=fieldnames,
|
||||||
|
)
|
||||||
|
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(results)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# График выбранного режима
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
result["step"]
|
||||||
|
for result in results
|
||||||
|
]
|
||||||
|
|
||||||
|
fragment_sizes = [
|
||||||
|
result["fragment_size"]
|
||||||
|
for result in results
|
||||||
|
]
|
||||||
|
|
||||||
|
plt.figure(
|
||||||
|
figsize=(11, 6)
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.step(
|
||||||
|
steps,
|
||||||
|
fragment_sizes,
|
||||||
|
where="mid",
|
||||||
|
marker="o",
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.yticks(
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
128,
|
||||||
|
512,
|
||||||
|
1024,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"IMAGE OFF",
|
||||||
|
"128 B",
|
||||||
|
"512 B",
|
||||||
|
"1024 B",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.xlabel(
|
||||||
|
"Шаг времени"
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.ylabel(
|
||||||
|
"Выбранный режим"
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.title(
|
||||||
|
"Автоматический выбор размера фрагмента"
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.grid(
|
||||||
|
True
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
plt.savefig(
|
||||||
|
MODE_GRAPH_PATH,
|
||||||
|
dpi=160,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# График времени передачи кадра
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
transfer_times = np.array(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
result["expected_seconds"]
|
||||||
|
if result["expected_seconds"] is not None
|
||||||
|
else np.nan
|
||||||
|
)
|
||||||
|
for result in results
|
||||||
|
],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.figure(
|
||||||
|
figsize=(11, 6)
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.plot(
|
||||||
|
steps,
|
||||||
|
transfer_times,
|
||||||
|
marker="o",
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.xlabel(
|
||||||
|
"Шаг времени"
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.ylabel(
|
||||||
|
"Ожидаемое время передачи кадра, с"
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.title(
|
||||||
|
"Время передачи кадра при адаптации канала"
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.grid(
|
||||||
|
True
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
plt.savefig(
|
||||||
|
TIME_GRAPH_PATH,
|
||||||
|
dpi=160,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверки ожидаемой логики
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
decision_at_6_db = next(
|
||||||
|
result
|
||||||
|
for result in results
|
||||||
|
if result["eb_n0_db"] == 6.0
|
||||||
|
)
|
||||||
|
|
||||||
|
decision_at_8_db = next(
|
||||||
|
result
|
||||||
|
for result in results
|
||||||
|
if result["eb_n0_db"] == 8.0
|
||||||
|
)
|
||||||
|
|
||||||
|
decision_at_9_db = next(
|
||||||
|
result
|
||||||
|
for result in results
|
||||||
|
if result["eb_n0_db"] == 9.0
|
||||||
|
)
|
||||||
|
|
||||||
|
decision_at_10_db = next(
|
||||||
|
result
|
||||||
|
for result in results
|
||||||
|
if result["eb_n0_db"] == 10.0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
decision_at_6_db["fragment_size"]
|
||||||
|
== 0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
decision_at_8_db["fragment_size"]
|
||||||
|
== 128
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
decision_at_9_db["fragment_size"]
|
||||||
|
== 512
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
decision_at_10_db["fragment_size"]
|
||||||
|
== 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
assert CSV_PATH.exists()
|
||||||
|
assert MODE_GRAPH_PATH.exists()
|
||||||
|
assert TIME_GRAPH_PATH.exists()
|
||||||
|
|
||||||
|
|
||||||
|
print("\nCSV:")
|
||||||
|
|
||||||
|
print(CSV_PATH)
|
||||||
|
|
||||||
|
print("\nГрафик режимов:")
|
||||||
|
|
||||||
|
print(MODE_GRAPH_PATH)
|
||||||
|
|
||||||
|
print("\nГрафик времени:")
|
||||||
|
|
||||||
|
print(TIME_GRAPH_PATH)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\nПроверка пройдена: "
|
||||||
|
"адаптивный выбор режима работает."
|
||||||
|
)
|
||||||
468
Projects/SDR_Rover/tests/lab017_link_hysteresis.py
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
"""
|
||||||
|
Lab017. Фильтрация Eb/N0 и гистерезис режимов.
|
||||||
|
|
||||||
|
Эксперимент сравнивает два адаптера:
|
||||||
|
|
||||||
|
1. Наивный адаптер:
|
||||||
|
переключает режим при каждом пересечении порога.
|
||||||
|
|
||||||
|
2. Устойчивый адаптер:
|
||||||
|
использует:
|
||||||
|
- экспоненциальное усреднение;
|
||||||
|
- гистерезис;
|
||||||
|
- подтверждение повышения;
|
||||||
|
- аварийное быстрое понижение.
|
||||||
|
|
||||||
|
Профиль Eb/N0 специально содержит колебания
|
||||||
|
около порогов переключения.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from csv import DictWriter
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
from protocol.link_controller import (
|
||||||
|
FRAGMENT_MODE_1024,
|
||||||
|
IMAGE_OFF,
|
||||||
|
LinkModeController,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Настройки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY = Path(
|
||||||
|
"data/processed/lab017"
|
||||||
|
)
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY.mkdir(
|
||||||
|
parents=True,
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
CSV_PATH = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "lab017_hysteresis_results.csv"
|
||||||
|
)
|
||||||
|
|
||||||
|
GRAPH_PATH = (
|
||||||
|
OUTPUT_DIRECTORY
|
||||||
|
/ "lab017_hysteresis_comparison.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Профиль качества канала
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
EB_N0_PROFILE_DB = [
|
||||||
|
10.2,
|
||||||
|
9.7,
|
||||||
|
9.4,
|
||||||
|
9.6,
|
||||||
|
9.3,
|
||||||
|
9.7,
|
||||||
|
9.2,
|
||||||
|
9.6,
|
||||||
|
9.0,
|
||||||
|
8.7,
|
||||||
|
8.4,
|
||||||
|
8.6,
|
||||||
|
8.3,
|
||||||
|
8.7,
|
||||||
|
8.2,
|
||||||
|
8.6,
|
||||||
|
8.0,
|
||||||
|
7.8,
|
||||||
|
7.5,
|
||||||
|
7.7,
|
||||||
|
7.4,
|
||||||
|
7.8,
|
||||||
|
7.2,
|
||||||
|
6.8,
|
||||||
|
6.5,
|
||||||
|
6.3,
|
||||||
|
6.6,
|
||||||
|
6.2,
|
||||||
|
6.7,
|
||||||
|
6.1,
|
||||||
|
5.5,
|
||||||
|
6.0,
|
||||||
|
6.4,
|
||||||
|
6.7,
|
||||||
|
6.9,
|
||||||
|
7.1,
|
||||||
|
7.3,
|
||||||
|
8.0,
|
||||||
|
8.8,
|
||||||
|
9.0,
|
||||||
|
9.4,
|
||||||
|
9.8,
|
||||||
|
10.0,
|
||||||
|
10.2,
|
||||||
|
10.1,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Наивное решение без гистерезиса
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def choose_naive_mode(
|
||||||
|
eb_n0_db: float,
|
||||||
|
) -> int:
|
||||||
|
"""
|
||||||
|
Немедленно выбрать режим по одному измерению.
|
||||||
|
|
||||||
|
Гистерезиса и фильтрации нет.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if eb_n0_db < 6.5:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if eb_n0_db < 8.5:
|
||||||
|
return 128
|
||||||
|
|
||||||
|
if eb_n0_db < 9.5:
|
||||||
|
return 512
|
||||||
|
|
||||||
|
return 1024
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Запуск устойчивого контроллера
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
controller = LinkModeController(
|
||||||
|
initial_mode=FRAGMENT_MODE_1024,
|
||||||
|
filter_alpha=0.35,
|
||||||
|
upgrade_confirmation_count=3,
|
||||||
|
minimum_hold_steps=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for measured_eb_n0_db in EB_N0_PROFILE_DB:
|
||||||
|
|
||||||
|
stable_result = controller.update(
|
||||||
|
measured_eb_n0_db
|
||||||
|
)
|
||||||
|
|
||||||
|
naive_mode = choose_naive_mode(
|
||||||
|
measured_eb_n0_db
|
||||||
|
)
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"step": stable_result.step,
|
||||||
|
"measured_eb_n0_db": (
|
||||||
|
stable_result.measured_eb_n0_db
|
||||||
|
),
|
||||||
|
"filtered_eb_n0_db": (
|
||||||
|
stable_result.filtered_eb_n0_db
|
||||||
|
),
|
||||||
|
"naive_mode": naive_mode,
|
||||||
|
"stable_mode": (
|
||||||
|
stable_result.selected_mode
|
||||||
|
),
|
||||||
|
"mode_changed": (
|
||||||
|
stable_result.mode_changed
|
||||||
|
),
|
||||||
|
"pending_upgrade_mode": (
|
||||||
|
stable_result.pending_upgrade_mode
|
||||||
|
),
|
||||||
|
"pending_upgrade_count": (
|
||||||
|
stable_result.pending_upgrade_count
|
||||||
|
),
|
||||||
|
"reason": stable_result.reason,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Число переключений
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def count_mode_switches(
|
||||||
|
modes: list[int],
|
||||||
|
) -> int:
|
||||||
|
"""
|
||||||
|
Посчитать число изменений режима.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return sum(
|
||||||
|
previous_mode != current_mode
|
||||||
|
for previous_mode, current_mode in zip(
|
||||||
|
modes,
|
||||||
|
modes[1:],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
naive_modes = [
|
||||||
|
result["naive_mode"]
|
||||||
|
for result in results
|
||||||
|
]
|
||||||
|
|
||||||
|
stable_modes = [
|
||||||
|
result["stable_mode"]
|
||||||
|
for result in results
|
||||||
|
]
|
||||||
|
|
||||||
|
naive_switch_count = count_mode_switches(
|
||||||
|
naive_modes
|
||||||
|
)
|
||||||
|
|
||||||
|
stable_switch_count = count_mode_switches(
|
||||||
|
stable_modes
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Вывод результатов
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
print(
|
||||||
|
"=== Lab017. Гистерезис режима канала ==="
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nРезультаты по шагам:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{'Шаг':>5}"
|
||||||
|
f"{'Eb/N0':>10}"
|
||||||
|
f"{'Фильтр':>11}"
|
||||||
|
f"{'Наивный':>11}"
|
||||||
|
f"{'Устойчивый':>13}"
|
||||||
|
f"{'Изменён':>10}"
|
||||||
|
f"{'Причина':>45}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("-" * 105)
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{result['step']:>5}"
|
||||||
|
f"{result['measured_eb_n0_db']:>7.1f} дБ"
|
||||||
|
f"{result['filtered_eb_n0_db']:>8.2f} дБ"
|
||||||
|
f"{result['naive_mode']:>11}"
|
||||||
|
f"{result['stable_mode']:>13}"
|
||||||
|
f"{str(result['mode_changed']):>10}"
|
||||||
|
f" {result['reason']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
print("\nЧисло переключений:")
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Наивный адаптер:",
|
||||||
|
naive_switch_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Устойчивый адаптер:",
|
||||||
|
stable_switch_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Предотвращено лишних переключений:",
|
||||||
|
naive_switch_count - stable_switch_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Сохранение CSV
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
with CSV_PATH.open(
|
||||||
|
"w",
|
||||||
|
newline="",
|
||||||
|
encoding="utf-8-sig",
|
||||||
|
) as csv_file:
|
||||||
|
|
||||||
|
writer = DictWriter(
|
||||||
|
csv_file,
|
||||||
|
fieldnames=list(
|
||||||
|
results[0].keys()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(results)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# График
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
result["step"]
|
||||||
|
for result in results
|
||||||
|
]
|
||||||
|
|
||||||
|
measured_values = [
|
||||||
|
result["measured_eb_n0_db"]
|
||||||
|
for result in results
|
||||||
|
]
|
||||||
|
|
||||||
|
filtered_values = [
|
||||||
|
result["filtered_eb_n0_db"]
|
||||||
|
for result in results
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
figure, axes = plt.subplots(
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
figsize=(12, 9),
|
||||||
|
sharex=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Верхний график: Eb/N0
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
axes[0].plot(
|
||||||
|
steps,
|
||||||
|
measured_values,
|
||||||
|
marker=".",
|
||||||
|
label="Измеренное Eb/N0",
|
||||||
|
)
|
||||||
|
|
||||||
|
axes[0].plot(
|
||||||
|
steps,
|
||||||
|
filtered_values,
|
||||||
|
linewidth=2,
|
||||||
|
label="Фильтрованное Eb/N0",
|
||||||
|
)
|
||||||
|
|
||||||
|
axes[0].axhline(
|
||||||
|
6.5,
|
||||||
|
linestyle=":",
|
||||||
|
label="Порог наивного адаптера",
|
||||||
|
)
|
||||||
|
|
||||||
|
axes[0].axhline(
|
||||||
|
8.5,
|
||||||
|
linestyle=":",
|
||||||
|
)
|
||||||
|
|
||||||
|
axes[0].axhline(
|
||||||
|
9.5,
|
||||||
|
linestyle=":",
|
||||||
|
)
|
||||||
|
|
||||||
|
axes[0].set_ylabel(
|
||||||
|
"Eb/N0, дБ"
|
||||||
|
)
|
||||||
|
|
||||||
|
axes[0].set_title(
|
||||||
|
"Фильтрация измерения качества канала"
|
||||||
|
)
|
||||||
|
|
||||||
|
axes[0].grid(
|
||||||
|
True
|
||||||
|
)
|
||||||
|
|
||||||
|
axes[0].legend()
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Нижний график: режимы
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
axes[1].step(
|
||||||
|
steps,
|
||||||
|
naive_modes,
|
||||||
|
where="mid",
|
||||||
|
marker=".",
|
||||||
|
label="Наивный адаптер",
|
||||||
|
)
|
||||||
|
|
||||||
|
axes[1].step(
|
||||||
|
steps,
|
||||||
|
stable_modes,
|
||||||
|
where="mid",
|
||||||
|
marker="o",
|
||||||
|
label="Гистерезис и фильтрация",
|
||||||
|
)
|
||||||
|
|
||||||
|
axes[1].set_yticks(
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
128,
|
||||||
|
512,
|
||||||
|
1024,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"IMAGE OFF",
|
||||||
|
"128 B",
|
||||||
|
"512 B",
|
||||||
|
"1024 B",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
axes[1].set_xlabel(
|
||||||
|
"Шаг времени"
|
||||||
|
)
|
||||||
|
|
||||||
|
axes[1].set_ylabel(
|
||||||
|
"Режим"
|
||||||
|
)
|
||||||
|
|
||||||
|
axes[1].set_title(
|
||||||
|
"Сравнение переключений режимов"
|
||||||
|
)
|
||||||
|
|
||||||
|
axes[1].grid(
|
||||||
|
True
|
||||||
|
)
|
||||||
|
|
||||||
|
axes[1].legend()
|
||||||
|
|
||||||
|
|
||||||
|
figure.tight_layout()
|
||||||
|
|
||||||
|
figure.savefig(
|
||||||
|
GRAPH_PATH,
|
||||||
|
dpi=160,
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.close(
|
||||||
|
figure
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Проверки
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
assert results
|
||||||
|
|
||||||
|
assert IMAGE_OFF in stable_modes
|
||||||
|
|
||||||
|
assert FRAGMENT_MODE_1024 in stable_modes
|
||||||
|
|
||||||
|
assert stable_switch_count < naive_switch_count
|
||||||
|
|
||||||
|
assert CSV_PATH.exists()
|
||||||
|
|
||||||
|
assert GRAPH_PATH.exists()
|
||||||
|
|
||||||
|
|
||||||
|
print("\nCSV:")
|
||||||
|
|
||||||
|
print(CSV_PATH)
|
||||||
|
|
||||||
|
print("\nГрафик:")
|
||||||
|
|
||||||
|
print(GRAPH_PATH)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\nПроверка пройдена: "
|
||||||
|
"гистерезис уменьшил количество переключений."
|
||||||
|
)
|
||||||
1148
Projects/SDR_Rover/tests/lab018_bpsk_radio_frame.py
Normal file
1212
Projects/SDR_Rover/tests/lab019_bpsk_receiver.py
Normal file
1494
Projects/SDR_Rover/tests/lab020_bpsk_frame_error_rate.py
Normal file
1483
Projects/SDR_Rover/tests/lab021_frequency_offset_correction.py
Normal file
1716
Projects/SDR_Rover/tests/lab022_noise_and_frequency_offset.py
Normal file
1637
Projects/SDR_Rover/tests/lab023_guarded_cfo_correction.py
Normal file
172
Projects/SDR_Rover/tests/lab024a_pluto_rx_capture.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
"""
|
||||||
|
Lab024A. Первый приём реальных IQ-сэмплов с Pluto+.
|
||||||
|
|
||||||
|
Схема:
|
||||||
|
антенна 40–860 МГц -> RX1
|
||||||
|
|
||||||
|
Передатчики TX1 и TX2 не используются.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
|
||||||
|
import adi
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# Параметры приёмника
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
PLUTO_URI = "ip:192.168.2.1"
|
||||||
|
|
||||||
|
CENTER_FREQUENCY_HZ = 100_000_000
|
||||||
|
SAMPLE_RATE_HZ = 2_400_000
|
||||||
|
RX_BANDWIDTH_HZ = 2_000_000
|
||||||
|
|
||||||
|
RX_BUFFER_SIZE = 262_144
|
||||||
|
|
||||||
|
OUTPUT_DIRECTORY = Path("data/raw")
|
||||||
|
IQ_FILE_PATH = OUTPUT_DIRECTORY / "lab024a_pluto_rx_100mhz.npy"
|
||||||
|
METADATA_FILE_PATH = OUTPUT_DIRECTORY / "lab024a_pluto_rx_100mhz.json"
|
||||||
|
SPECTRUM_FILE_PATH = OUTPUT_DIRECTORY / "lab024a_pluto_rx_100mhz_spectrum.png"
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_spectrum(
|
||||||
|
samples: np.ndarray,
|
||||||
|
sample_rate_hz: float,
|
||||||
|
center_frequency_hz: float,
|
||||||
|
) -> tuple[np.ndarray, np.ndarray]:
|
||||||
|
"""
|
||||||
|
Рассчитывает спектр принятого комплексного IQ-сигнала.
|
||||||
|
|
||||||
|
Возвращает:
|
||||||
|
frequencies_hz — абсолютные радиочастоты;
|
||||||
|
power_db — относительная мощность спектра в дБ.
|
||||||
|
"""
|
||||||
|
|
||||||
|
sample_count = len(samples)
|
||||||
|
|
||||||
|
window = np.hanning(sample_count)
|
||||||
|
windowed_samples = samples * window
|
||||||
|
|
||||||
|
spectrum = np.fft.fftshift(np.fft.fft(windowed_samples))
|
||||||
|
power = np.abs(spectrum) ** 2
|
||||||
|
|
||||||
|
power_db = 10.0 * np.log10(power + 1e-12)
|
||||||
|
power_db -= np.max(power_db)
|
||||||
|
|
||||||
|
baseband_frequencies_hz = np.fft.fftshift(
|
||||||
|
np.fft.fftfreq(sample_count, d=1.0 / sample_rate_hz)
|
||||||
|
)
|
||||||
|
|
||||||
|
absolute_frequencies_hz = (
|
||||||
|
center_frequency_hz + baseband_frequencies_hz
|
||||||
|
)
|
||||||
|
|
||||||
|
return absolute_frequencies_hz, power_db
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
OUTPUT_DIRECTORY.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
print("Подключение к Pluto+...")
|
||||||
|
sdr = adi.Pluto(uri=PLUTO_URI)
|
||||||
|
|
||||||
|
# Используем только первый приёмный канал RX1.
|
||||||
|
sdr.rx_enabled_channels = [0]
|
||||||
|
|
||||||
|
sdr.sample_rate = SAMPLE_RATE_HZ
|
||||||
|
sdr.rx_lo = CENTER_FREQUENCY_HZ
|
||||||
|
sdr.rx_rf_bandwidth = RX_BANDWIDTH_HZ
|
||||||
|
|
||||||
|
# Автоматическая регулировка усиления.
|
||||||
|
sdr.gain_control_mode_chan0 = "slow_attack"
|
||||||
|
|
||||||
|
sdr.rx_buffer_size = RX_BUFFER_SIZE
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Параметры приёмника:")
|
||||||
|
print(f" URI: {PLUTO_URI}")
|
||||||
|
print(f" Центральная частота: {sdr.rx_lo / 1e6:.3f} МГц")
|
||||||
|
print(f" Частота дискретизации: {sdr.sample_rate / 1e6:.3f} Мвыб/с")
|
||||||
|
print(f" Полоса RX: {sdr.rx_rf_bandwidth / 1e6:.3f} МГц")
|
||||||
|
print(f" Режим усиления: {sdr.gain_control_mode_chan0}")
|
||||||
|
print(f" Размер буфера: {sdr.rx_buffer_size} отсчётов")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Получение IQ-сэмплов...")
|
||||||
|
|
||||||
|
# Первый буфер после перенастройки иногда содержит переходный процесс.
|
||||||
|
_ = sdr.rx()
|
||||||
|
|
||||||
|
# Второй буфер сохраняем и анализируем.
|
||||||
|
samples = np.asarray(sdr.rx(), dtype=np.complex64)
|
||||||
|
|
||||||
|
print("IQ-сэмплы получены.")
|
||||||
|
|
||||||
|
np.save(IQ_FILE_PATH, samples)
|
||||||
|
|
||||||
|
mean_value = np.mean(samples)
|
||||||
|
rms_value = np.sqrt(np.mean(np.abs(samples) ** 2))
|
||||||
|
peak_value = np.max(np.abs(samples))
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"pluto_uri": PLUTO_URI,
|
||||||
|
"center_frequency_hz": int(sdr.rx_lo),
|
||||||
|
"sample_rate_hz": int(sdr.sample_rate),
|
||||||
|
"rx_bandwidth_hz": int(sdr.rx_rf_bandwidth),
|
||||||
|
"gain_control_mode": sdr.gain_control_mode_chan0,
|
||||||
|
"sample_count": int(len(samples)),
|
||||||
|
"sample_dtype": str(samples.dtype),
|
||||||
|
"mean_i": float(np.real(mean_value)),
|
||||||
|
"mean_q": float(np.imag(mean_value)),
|
||||||
|
"rms": float(rms_value),
|
||||||
|
"peak": float(peak_value),
|
||||||
|
}
|
||||||
|
|
||||||
|
with METADATA_FILE_PATH.open("w", encoding="utf-8") as metadata_file:
|
||||||
|
json.dump(metadata, metadata_file, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
frequencies_hz, power_db = calculate_spectrum(
|
||||||
|
samples=samples,
|
||||||
|
sample_rate_hz=float(sdr.sample_rate),
|
||||||
|
center_frequency_hz=float(sdr.rx_lo),
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.figure(figsize=(12, 6))
|
||||||
|
plt.plot(frequencies_hz / 1e6, power_db)
|
||||||
|
|
||||||
|
plt.title("Lab024A. Спектр сигнала, принятого Pluto+")
|
||||||
|
plt.xlabel("Частота, МГц")
|
||||||
|
plt.ylabel("Относительная мощность, дБ")
|
||||||
|
plt.grid(True)
|
||||||
|
plt.ylim(-100, 5)
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
plt.savefig(SPECTRUM_FILE_PATH, dpi=150)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Статистика:")
|
||||||
|
print(f" Количество сэмплов: {len(samples)}")
|
||||||
|
print(f" Тип данных: {samples.dtype}")
|
||||||
|
print(f" Среднее I: {np.real(mean_value):.3f}")
|
||||||
|
print(f" Среднее Q: {np.imag(mean_value):.3f}")
|
||||||
|
print(f" RMS: {rms_value:.3f}")
|
||||||
|
print(f" Пиковая амплитуда: {peak_value:.3f}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Созданы файлы:")
|
||||||
|
print(f" IQ: {IQ_FILE_PATH}")
|
||||||
|
print(f" Метаданные:{METADATA_FILE_PATH}")
|
||||||
|
print(f" Спектр: {SPECTRUM_FILE_PATH}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Lab024A выполнена успешно.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||