initial commit
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()
|
||||
|
||||
573
Projects/SDR_Rover/tests/lab024b_pluto_welch_spectrum.py
Normal file
@@ -0,0 +1,573 @@
|
||||
"""
|
||||
Lab024B. Усреднённый спектр Pluto+ методом Уэлча.
|
||||
|
||||
Схема подключения:
|
||||
антенна 40–860 МГц -> RX1
|
||||
|
||||
Передатчики TX1 и TX2 не используются.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import csv
|
||||
|
||||
import adi
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from scipy.signal import find_peaks, welch
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Настройки Pluto+
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
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 = 65_536
|
||||
BUFFER_COUNT = 16
|
||||
DISCARD_BUFFER_COUNT = 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Настройки спектрального анализа
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
WELCH_SEGMENT_LENGTH = 8_192
|
||||
WELCH_OVERLAP_LENGTH = 4_096
|
||||
|
||||
SMOOTHING_BANDWIDTH_HZ = 40_000
|
||||
|
||||
MINIMUM_PEAK_DISTANCE_HZ = 120_000
|
||||
MINIMUM_PEAK_PROMINENCE_DB = 4.0
|
||||
MINIMUM_PEAK_HEIGHT_DB = -25.0
|
||||
|
||||
DC_EXCLUSION_HALF_WIDTH_HZ = 40_000
|
||||
|
||||
MAXIMUM_CANDIDATE_COUNT = 12
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Выходные файлы
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
OUTPUT_DIRECTORY = Path("data/processed/lab024b")
|
||||
|
||||
SPECTRUM_CSV_PATH = OUTPUT_DIRECTORY / "lab024b_welch_spectrum.csv"
|
||||
CANDIDATES_CSV_PATH = OUTPUT_DIRECTORY / "lab024b_fm_candidates.csv"
|
||||
GRAPH_PATH = OUTPUT_DIRECTORY / "lab024b_welch_spectrum.png"
|
||||
REPORT_PATH = OUTPUT_DIRECTORY / "lab024b_report.txt"
|
||||
|
||||
|
||||
def configure_receiver() -> adi.Pluto:
|
||||
"""
|
||||
Подключается к Pluto+ и настраивает приёмник RX1.
|
||||
"""
|
||||
|
||||
print("Подключение к Pluto+...")
|
||||
|
||||
sdr = adi.Pluto(uri=PLUTO_URI)
|
||||
|
||||
# Используем первый приёмный канал.
|
||||
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
|
||||
|
||||
return sdr
|
||||
|
||||
|
||||
def receive_samples(sdr: adi.Pluto) -> np.ndarray:
|
||||
"""
|
||||
Получает несколько последовательных IQ-буферов.
|
||||
|
||||
Первые буферы отбрасываются, поскольку сразу после настройки
|
||||
приёмника могут наблюдаться переходные процессы АРУ и фильтров.
|
||||
"""
|
||||
|
||||
print()
|
||||
print("Отбрасывание переходных буферов...")
|
||||
|
||||
for _ in range(DISCARD_BUFFER_COUNT):
|
||||
_ = sdr.rx()
|
||||
|
||||
received_buffers: list[np.ndarray] = []
|
||||
|
||||
print("Получение рабочих IQ-буферов...")
|
||||
|
||||
for buffer_number in range(1, BUFFER_COUNT + 1):
|
||||
samples = np.asarray(sdr.rx(), dtype=np.complex64)
|
||||
received_buffers.append(samples)
|
||||
|
||||
print(
|
||||
f" Буфер {buffer_number:02d}/{BUFFER_COUNT}: "
|
||||
f"{len(samples)} отсчётов"
|
||||
)
|
||||
|
||||
combined_samples = np.concatenate(received_buffers)
|
||||
|
||||
if len(combined_samples) == 0:
|
||||
raise RuntimeError("Pluto+ не вернул IQ-сэмплы.")
|
||||
|
||||
if not np.all(np.isfinite(combined_samples)):
|
||||
raise RuntimeError("В IQ-буфере обнаружены NaN или Inf.")
|
||||
|
||||
return combined_samples
|
||||
|
||||
|
||||
def calculate_welch_spectrum(
|
||||
samples: np.ndarray,
|
||||
sample_rate_hz: float,
|
||||
center_frequency_hz: float,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Рассчитывает спектральную плотность мощности методом Уэлча.
|
||||
|
||||
Возвращает:
|
||||
absolute_frequencies_hz — абсолютные радиочастоты;
|
||||
power_density — мощность в линейном масштабе.
|
||||
"""
|
||||
|
||||
baseband_frequencies_hz, power_density = welch(
|
||||
samples,
|
||||
fs=sample_rate_hz,
|
||||
window="hann",
|
||||
nperseg=WELCH_SEGMENT_LENGTH,
|
||||
noverlap=WELCH_OVERLAP_LENGTH,
|
||||
detrend=False,
|
||||
return_onesided=False,
|
||||
scaling="density",
|
||||
)
|
||||
|
||||
baseband_frequencies_hz = np.fft.fftshift(
|
||||
baseband_frequencies_hz
|
||||
)
|
||||
|
||||
power_density = np.fft.fftshift(power_density)
|
||||
|
||||
absolute_frequencies_hz = (
|
||||
center_frequency_hz + baseband_frequencies_hz
|
||||
)
|
||||
|
||||
return absolute_frequencies_hz, power_density
|
||||
|
||||
|
||||
def smooth_power_spectrum(
|
||||
power_density: np.ndarray,
|
||||
frequency_step_hz: float,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Сглаживает спектр скользящим средним в линейном масштабе.
|
||||
|
||||
Усреднять следует мощность, а не значения в децибелах.
|
||||
"""
|
||||
|
||||
smoothing_bin_count = int(
|
||||
round(SMOOTHING_BANDWIDTH_HZ / frequency_step_hz)
|
||||
)
|
||||
|
||||
smoothing_bin_count = max(1, smoothing_bin_count)
|
||||
|
||||
# Нечётная длина ядра обеспечивает симметрию относительно центра.
|
||||
if smoothing_bin_count % 2 == 0:
|
||||
smoothing_bin_count += 1
|
||||
|
||||
kernel = (
|
||||
np.ones(smoothing_bin_count, dtype=np.float64)
|
||||
/ smoothing_bin_count
|
||||
)
|
||||
|
||||
smoothed_power = np.convolve(
|
||||
power_density,
|
||||
kernel,
|
||||
mode="same",
|
||||
)
|
||||
|
||||
return smoothed_power
|
||||
|
||||
|
||||
def convert_power_to_relative_db(
|
||||
raw_power: np.ndarray,
|
||||
smoothed_power: np.ndarray,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Переводит мощность в относительные децибелы.
|
||||
|
||||
0 дБ соответствует максимальному значению сглаженного спектра.
|
||||
"""
|
||||
|
||||
minimum_positive_value = np.finfo(np.float64).tiny
|
||||
|
||||
raw_power_db = 10.0 * np.log10(
|
||||
raw_power + minimum_positive_value
|
||||
)
|
||||
|
||||
smoothed_power_db = 10.0 * np.log10(
|
||||
smoothed_power + minimum_positive_value
|
||||
)
|
||||
|
||||
reference_power_db = np.max(smoothed_power_db)
|
||||
|
||||
raw_relative_db = raw_power_db - reference_power_db
|
||||
smoothed_relative_db = smoothed_power_db - reference_power_db
|
||||
|
||||
return raw_relative_db, smoothed_relative_db
|
||||
|
||||
|
||||
def detect_signal_candidates(
|
||||
frequencies_hz: np.ndarray,
|
||||
smoothed_power_db: np.ndarray,
|
||||
center_frequency_hz: float,
|
||||
) -> list[dict[str, float]]:
|
||||
"""
|
||||
Ищет локальные максимумы сглаженного спектра.
|
||||
|
||||
Максимумы сначала рассчитываются по исходному спектру.
|
||||
После этого отбрасываются точки из DC-зоны и с краёв диапазона.
|
||||
|
||||
Такой порядок не создаёт искусственных провалов -200 дБ
|
||||
и не искажает значение prominence.
|
||||
"""
|
||||
|
||||
frequency_step_hz = float(
|
||||
np.mean(np.diff(frequencies_hz))
|
||||
)
|
||||
|
||||
minimum_distance_bins = max(
|
||||
1,
|
||||
int(
|
||||
round(
|
||||
MINIMUM_PEAK_DISTANCE_HZ
|
||||
/ abs(frequency_step_hz)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
edge_bin_count = max(
|
||||
1,
|
||||
int(
|
||||
round(
|
||||
SMOOTHING_BANDWIDTH_HZ
|
||||
/ abs(frequency_step_hz)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
# Важно: find_peaks получает настоящий спектр без вставок -200 дБ.
|
||||
peak_indices, peak_properties = find_peaks(
|
||||
smoothed_power_db,
|
||||
height=MINIMUM_PEAK_HEIGHT_DB,
|
||||
prominence=MINIMUM_PEAK_PROMINENCE_DB,
|
||||
distance=minimum_distance_bins,
|
||||
)
|
||||
|
||||
candidates: list[dict[str, float]] = []
|
||||
|
||||
for peak_number, peak_index in enumerate(peak_indices):
|
||||
frequency_hz = float(frequencies_hz[peak_index])
|
||||
|
||||
# Отбрасываем возможный аппаратный DC-пик.
|
||||
if (
|
||||
abs(frequency_hz - center_frequency_hz)
|
||||
<= DC_EXCLUSION_HALF_WIDTH_HZ
|
||||
):
|
||||
continue
|
||||
|
||||
# Отбрасываем максимумы возле краёв наблюдаемого диапазона.
|
||||
if peak_index < edge_bin_count:
|
||||
continue
|
||||
|
||||
if peak_index >= len(smoothed_power_db) - edge_bin_count:
|
||||
continue
|
||||
|
||||
candidates.append(
|
||||
{
|
||||
"frequency_hz": frequency_hz,
|
||||
"relative_power_db": float(
|
||||
smoothed_power_db[peak_index]
|
||||
),
|
||||
"prominence_db": float(
|
||||
peak_properties["prominences"][peak_number]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# Оставляем наиболее выраженные сигналы.
|
||||
candidates.sort(
|
||||
key=lambda item: item["prominence_db"],
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
candidates = candidates[:MAXIMUM_CANDIDATE_COUNT]
|
||||
|
||||
# В итоговой таблице располагаем сигналы по частоте.
|
||||
candidates.sort(
|
||||
key=lambda item: item["frequency_hz"]
|
||||
)
|
||||
|
||||
return candidates
|
||||
|
||||
def save_candidates_csv(
|
||||
candidates: list[dict[str, float]],
|
||||
) -> None:
|
||||
"""
|
||||
Сохраняет найденные кандидаты на радиосигналы.
|
||||
"""
|
||||
|
||||
with CANDIDATES_CSV_PATH.open(
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
newline="",
|
||||
) as csv_file:
|
||||
writer = csv.writer(csv_file)
|
||||
|
||||
writer.writerow(
|
||||
[
|
||||
"frequency_hz",
|
||||
"frequency_mhz",
|
||||
"relative_power_db",
|
||||
"prominence_db",
|
||||
]
|
||||
)
|
||||
|
||||
for candidate in candidates:
|
||||
writer.writerow(
|
||||
[
|
||||
f"{candidate['frequency_hz']:.3f}",
|
||||
f"{candidate['frequency_hz'] / 1e6:.6f}",
|
||||
f"{candidate['relative_power_db']:.3f}",
|
||||
f"{candidate['prominence_db']:.3f}",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def save_report(
|
||||
sample_count: int,
|
||||
candidates: list[dict[str, float]],
|
||||
) -> None:
|
||||
"""
|
||||
Создаёт текстовый отчёт лабораторной.
|
||||
"""
|
||||
|
||||
report_lines = [
|
||||
"Lab024B. Усреднённый спектр Pluto+ методом Уэлча",
|
||||
"",
|
||||
f"URI: {PLUTO_URI}",
|
||||
f"Центральная частота: {CENTER_FREQUENCY_HZ / 1e6:.3f} МГц",
|
||||
f"Частота дискретизации: {SAMPLE_RATE_HZ / 1e6:.3f} Мвыб/с",
|
||||
f"Полоса RX: {RX_BANDWIDTH_HZ / 1e6:.3f} МГц",
|
||||
f"Количество IQ-сэмплов: {sample_count}",
|
||||
f"Размер сегмента Уэлча: {WELCH_SEGMENT_LENGTH}",
|
||||
f"Перекрытие сегментов: {WELCH_OVERLAP_LENGTH}",
|
||||
"",
|
||||
f"Найдено кандидатов: {len(candidates)}",
|
||||
"",
|
||||
]
|
||||
|
||||
for candidate_number, candidate in enumerate(
|
||||
candidates,
|
||||
start=1,
|
||||
):
|
||||
report_lines.append(
|
||||
f"{candidate_number:02d}. "
|
||||
f"{candidate['frequency_hz'] / 1e6:.6f} МГц, "
|
||||
f"уровень {candidate['relative_power_db']:.2f} дБ, "
|
||||
f"выраженность {candidate['prominence_db']:.2f} дБ"
|
||||
)
|
||||
|
||||
REPORT_PATH.write_text(
|
||||
"\n".join(report_lines),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def create_graph(
|
||||
frequencies_hz: np.ndarray,
|
||||
raw_power_db: np.ndarray,
|
||||
smoothed_power_db: np.ndarray,
|
||||
candidates: list[dict[str, float]],
|
||||
) -> None:
|
||||
"""
|
||||
Строит исходный и сглаженный спектры.
|
||||
"""
|
||||
|
||||
plt.figure(figsize=(13, 7))
|
||||
|
||||
plt.plot(
|
||||
frequencies_hz / 1e6,
|
||||
raw_power_db,
|
||||
linewidth=0.6,
|
||||
alpha=0.45,
|
||||
label="Спектр Уэлча",
|
||||
)
|
||||
|
||||
plt.plot(
|
||||
frequencies_hz / 1e6,
|
||||
smoothed_power_db,
|
||||
linewidth=1.5,
|
||||
label="Сглаженный спектр",
|
||||
)
|
||||
|
||||
for candidate in candidates:
|
||||
frequency_mhz = candidate["frequency_hz"] / 1e6
|
||||
power_db = candidate["relative_power_db"]
|
||||
|
||||
plt.scatter(
|
||||
[frequency_mhz],
|
||||
[power_db],
|
||||
marker="o",
|
||||
)
|
||||
|
||||
plt.annotate(
|
||||
f"{frequency_mhz:.3f}",
|
||||
xy=(frequency_mhz, power_db),
|
||||
xytext=(0, 10),
|
||||
textcoords="offset points",
|
||||
ha="center",
|
||||
fontsize=8,
|
||||
rotation=45,
|
||||
)
|
||||
|
||||
plt.axvspan(
|
||||
(CENTER_FREQUENCY_HZ - DC_EXCLUSION_HALF_WIDTH_HZ) / 1e6,
|
||||
(CENTER_FREQUENCY_HZ + DC_EXCLUSION_HALF_WIDTH_HZ) / 1e6,
|
||||
alpha=0.15,
|
||||
label="Исключённая DC-зона",
|
||||
)
|
||||
|
||||
plt.title(
|
||||
"Lab024B. Усреднённый спектр Pluto+ и кандидаты на FM-сигналы"
|
||||
)
|
||||
plt.xlabel("Частота, МГц")
|
||||
plt.ylabel("Относительная мощность, дБ")
|
||||
plt.grid(True)
|
||||
plt.ylim(-60, 5)
|
||||
plt.legend()
|
||||
plt.tight_layout()
|
||||
|
||||
plt.savefig(GRAPH_PATH, dpi=160)
|
||||
plt.show()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUTPUT_DIRECTORY.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
sdr = configure_receiver()
|
||||
|
||||
print()
|
||||
print("Параметры приёмника:")
|
||||
print(f" URI: {PLUTO_URI}")
|
||||
print(
|
||||
f" Центральная частота: "
|
||||
f"{sdr.rx_lo / 1e6:.3f} МГц"
|
||||
)
|
||||
print(
|
||||
f" Частота дискретизации: "
|
||||
f"{sdr.sample_rate / 1e6:.3f} Мвыб/с"
|
||||
)
|
||||
print(
|
||||
f" Полоса RX: "
|
||||
f"{sdr.rx_rf_bandwidth / 1e6:.3f} МГц"
|
||||
)
|
||||
print(
|
||||
f" Режим усиления: "
|
||||
f"{sdr.gain_control_mode_chan0}"
|
||||
)
|
||||
|
||||
samples = receive_samples(sdr)
|
||||
|
||||
print()
|
||||
print(f"Всего получено: {len(samples)} IQ-сэмплов")
|
||||
|
||||
frequencies_hz, power_density = calculate_welch_spectrum(
|
||||
samples=samples,
|
||||
sample_rate_hz=float(sdr.sample_rate),
|
||||
center_frequency_hz=float(sdr.rx_lo),
|
||||
)
|
||||
|
||||
frequency_step_hz = float(
|
||||
np.mean(np.diff(frequencies_hz))
|
||||
)
|
||||
|
||||
smoothed_power = smooth_power_spectrum(
|
||||
power_density=power_density,
|
||||
frequency_step_hz=frequency_step_hz,
|
||||
)
|
||||
|
||||
raw_power_db, smoothed_power_db = (
|
||||
convert_power_to_relative_db(
|
||||
raw_power=power_density,
|
||||
smoothed_power=smoothed_power,
|
||||
)
|
||||
)
|
||||
|
||||
candidates = detect_signal_candidates(
|
||||
frequencies_hz=frequencies_hz,
|
||||
smoothed_power_db=smoothed_power_db,
|
||||
center_frequency_hz=float(sdr.rx_lo),
|
||||
)
|
||||
|
||||
save_spectrum_csv(
|
||||
frequencies_hz=frequencies_hz,
|
||||
raw_power_db=raw_power_db,
|
||||
smoothed_power_db=smoothed_power_db,
|
||||
)
|
||||
|
||||
save_candidates_csv(candidates)
|
||||
save_report(len(samples), candidates)
|
||||
|
||||
print()
|
||||
print("Найденные кандидаты на радиосигналы:")
|
||||
print()
|
||||
|
||||
if candidates:
|
||||
print(
|
||||
" № Частота, МГц Уровень, дБ "
|
||||
"Выраженность, дБ"
|
||||
)
|
||||
print(
|
||||
" -- ------------ ----------- "
|
||||
"-----------------"
|
||||
)
|
||||
|
||||
for candidate_number, candidate in enumerate(
|
||||
candidates,
|
||||
start=1,
|
||||
):
|
||||
print(
|
||||
f" {candidate_number:2d} "
|
||||
f"{candidate['frequency_hz'] / 1e6:12.6f} "
|
||||
f"{candidate['relative_power_db']:11.2f} "
|
||||
f"{candidate['prominence_db']:17.2f}"
|
||||
)
|
||||
else:
|
||||
print(" Кандидаты не найдены.")
|
||||
|
||||
create_graph(
|
||||
frequencies_hz=frequencies_hz,
|
||||
raw_power_db=raw_power_db,
|
||||
smoothed_power_db=smoothed_power_db,
|
||||
candidates=candidates,
|
||||
)
|
||||
|
||||
print()
|
||||
print("Созданы файлы:")
|
||||
print(f" Спектр: {SPECTRUM_CSV_PATH}")
|
||||
print(f" Кандидаты: {CANDIDATES_CSV_PATH}")
|
||||
print(f" График: {GRAPH_PATH}")
|
||||
print(f" Отчёт: {REPORT_PATH}")
|
||||
|
||||
print()
|
||||
print("Lab024B выполнена успешно.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||