initial commit

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

View File

@@ -0,0 +1,35 @@
# Инженерный журнал SDR Rover Link
## Назначение журнала
Этот файл содержит хронологию разработки проекта SDR Rover Link.
В журнале фиксируются:
- выполненные работы;
- полученные результаты;
- обнаруженные ошибки;
- принятые технические решения;
- результаты экспериментов;
- планы следующего этапа.
Записи не следует удалять. Если решение изменилось, необходимо добавить новую запись и объяснить причину изменения.
---
# Запись 001
## Дата
10 июля 2026 года
## Тема
Создание структуры проекта и локального Git-репозитория.
## Выполнено
1. Проверена установленная версия Git:
```text
git version 2.51.1.windows.1

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -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
1 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
2 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
3 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
4 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
5 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
6 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
7 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
8 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -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
1 eb_n0_db bit_errors experimental_ber theoretical_ber short_packet_per_224_bits image_packet_per_4288_bits
2 -4.0 186789 0.186789 0.186113817483389 1.0 1.0
3 -2.0 130895 0.130895 0.13064448852282923 0.999999999999976 1.0
4 0.0 78830 0.07883 0.07864960352514255 0.9999999892561172 1.0
5 2.0 37551 0.037551 0.03750612835892598 0.9998089504830603 1.0
6 4.0 12379 0.012379 0.012500818040737556 0.9402647377229729 1.0
7 6.0 2266 0.002266 0.002388290780932807 0.4146919530687869 0.9999647568582152
8 8.0 205 0.000205 0.00019090777407599314 0.041865796670504794 0.5589913040105388
9 10.0 5 5e-06 3.872108215522038e-06 0.0008669778768398397 0.016466551600075156
10 12.0 0 0.0 9.006010350628754e-09 2.017344292783227e-06 3.8617026900812036e-05

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -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
1 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
2 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
3 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
4 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
5 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
6 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
7 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
8 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
9 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
10 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
11 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
12 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
13 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
14 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
15 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
16 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
17 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
18 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
19 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
20 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
21 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -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,Выбран режим с максимальной ожидаемой полезной скоростью
1 step eb_n0_db mode fragment_size fragment_count expected_seconds effective_goodput_bps success_with_retries expected_attempts reason
2 0 12.0 1024 B 1024 4 1.50130532861719 19231.264586660185 1.0 1.0000763738844538 Выбран режим с максимальной ожидаемой полезной скоростью
3 1 10.0 1024 B 1024 4 1.5472051612507218 18660.744368678683 0.9999999648303772 1.0333805767765363 Выбран режим с максимальной ожидаемой полезной скоростью
4 2 9.0 512 B 512 8 1.797643784171777 16061.024021676303 0.9999516109774961 1.158845496802822 Выбран режим с максимальной ожидаемой полезной скоростью
5 3 8.0 128 B 128 29 2.3502958310297126 12284.41101703805 0.9994658078956576 1.2846610523483055 Выбран режим с максимальной ожидаемой полезной скоростью
6 4 7.0 128 B 128 29 4.872011036339501 5926.09495024717 0.8948830865552501 2.7569946980068387 Выбран режим с максимальной ожидаемой полезной скоростью
7 5 6.0 IMAGE OFF 0 0 0.0 0.0 0.0 Канал недостаточен для надёжной передачи изображения
8 6 7.0 128 B 128 29 4.872011036339501 5926.09495024717 0.8948830865552501 2.7569946980068387 Выбран режим с максимальной ожидаемой полезной скоростью
9 7 8.0 128 B 128 29 2.3502958310297126 12284.41101703805 0.9994658078956576 1.2846610523483055 Выбран режим с максимальной ожидаемой полезной скоростью
10 8 9.0 512 B 512 8 1.797643784171777 16061.024021676303 0.9999516109774961 1.158845496802822 Выбран режим с максимальной ожидаемой полезной скоростью
11 9 10.0 1024 B 1024 4 1.5472051612507218 18660.744368678683 0.9999999648303772 1.0333805767765363 Выбран режим с максимальной ожидаемой полезной скоростью
12 10 12.0 1024 B 1024 4 1.50130532861719 19231.264586660185 1.0 1.0000763738844538 Выбран режим с максимальной ожидаемой полезной скоростью

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View File

@@ -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,Повышение запрещено: не выдержано минимальное время режима
1 step measured_eb_n0_db filtered_eb_n0_db naive_mode stable_mode mode_changed pending_upgrade_mode pending_upgrade_count reason
2 0 10.2 10.2 1024 1024 False 0 Режим удерживается
3 1 9.7 10.024999999999999 1024 1024 False 0 Режим удерживается
4 2 9.4 9.806249999999999 512 1024 False 0 Режим удерживается
5 3 9.6 9.734062499999999 1024 1024 False 0 Режим удерживается
6 4 9.3 9.582140625 512 1024 False 0 Режим удерживается
7 5 9.7 9.62339140625 1024 1024 False 0 Режим удерживается
8 6 9.2 9.4752044140625 512 1024 False 0 Режим удерживается
9 7 9.6 9.518882869140624 1024 1024 False 0 Режим удерживается
10 8 9.0 9.337273864941405 512 1024 False 0 Режим удерживается
11 9 8.7 9.114228012211914 512 512 True 0 Понижение по фильтрованному Eb/N0
12 10 8.4 8.864248207937743 128 512 False 0 Режим удерживается
13 11 8.6 8.771761335159534 512 512 False 0 Режим удерживается
14 12 8.3 8.606644867853698 128 512 False 0 Режим удерживается
15 13 8.7 8.639319164104903 512 512 False 0 Режим удерживается
16 14 8.2 8.485557456668186 128 512 False 0 Режим удерживается
17 15 8.6 8.525612346834322 512 512 False 0 Режим удерживается
18 16 8.0 8.341648025442309 128 512 False 0 Режим удерживается
19 17 7.8 8.1520712165375 128 512 False 0 Режим удерживается
20 18 7.5 7.923846290749375 128 128 True 0 Аварийное понижение по мгновенному Eb/N0
21 19 7.7 7.8455000889870945 128 128 False 0 Режим удерживается
22 20 7.4 7.689575057841611 128 128 False 0 Режим удерживается
23 21 7.8 7.728223787597047 128 128 False 0 Режим удерживается
24 22 7.2 7.543345461938081 128 128 False 0 Режим удерживается
25 23 6.8 7.283174550259753 128 128 False 0 Режим удерживается
26 24 6.5 7.009063457668839 128 128 False 0 Режим удерживается
27 25 6.3 6.760891247484745 0 128 False 0 Режим удерживается
28 26 6.6 6.704579310865084 128 128 False 0 Режим удерживается
29 27 6.2 6.527976552062304 0 128 False 0 Режим удерживается
30 28 6.7 6.588184758840497 128 128 False 0 Режим удерживается
31 29 6.1 6.417320093246323 0 128 False 0 Режим удерживается
32 30 5.5 6.0962580606101096 0 0 True 0 Аварийное понижение по мгновенному Eb/N0
33 31 6.0 6.062567739396571 0 0 False 0 Режим удерживается
34 32 6.4 6.1806690306077705 0 0 False 0 Режим удерживается
35 33 6.7 6.362434869895051 128 0 False 0 Режим удерживается
36 34 6.9 6.550582665431783 128 0 False 0 Режим удерживается
37 35 7.1 6.742878732530659 128 0 False 0 Режим удерживается
38 36 7.3 6.937871176144928 128 0 False 128 1 Накопление подтверждений для повышения режима
39 37 8.0 7.3096162644942035 128 0 False 128 2 Накопление подтверждений для повышения режима
40 38 8.8 7.831250571921233 512 128 True 0 Режим повышен после нескольких подтверждений
41 39 9.0 8.2403128717488 512 128 False 0 Режим удерживается
42 40 9.4 8.64620336663672 512 128 False 0 Режим удерживается
43 41 9.8 9.05003218831387 1024 128 False 512 1 Накопление подтверждений для повышения режима
44 42 10.0 9.382520922404016 1024 128 False 512 2 Накопление подтверждений для повышения режима
45 43 10.2 9.668638599562609 1024 512 True 0 Режим повышен после нескольких подтверждений
46 44 10.1 9.819615089715695 1024 512 False 0 Повышение запрещено: не выдержано минимальное время режима

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

View File

@@ -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

View File

@@ -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
1 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
2 4.0 100 3 0 34 38 25 0.97 0.012500818040737556 0.98214506597389 0.8537512081010437 0.94
3 5.0 100 24 0 11 41 24 0.76 0.0059538671477786615 0.8520575402615456 0.8753653434073579 0.44
4 6.0 100 58 0 5 29 8 0.42 0.002388290780932807 0.534743836041445 0.8986643099612688 0.15
5 7.0 100 84 0 1 12 3 0.16 0.0007726748153784444 0.21913384039715306 0.9164792561326658 0.08
6 8.0 100 98 0 0 2 0 0.02 0.00019090777407599314 0.05926737573898814 0.9311581513763438 0.01
7 10.0 100 100 0 0 0 0 0.0 3.872108215522038e-06 0.001238309688911671 0.9560438959972798 0.0
8 12.0 100 100 0 0 0 0 0.0 9.006010350628754e-09 2.88191917244148e-06 0.9716348856405669 0.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

View File

@@ -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

View File

@@ -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
1 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
2 6.0 0.0 30 12 10 0 0.6 0.6666666666666667 0.876037158456595 3.9418970310617256 4.6418505363579925
3 6.0 250.0 30 0 7 0 1.0 0.7666666666666666 0.855520150369621 6.514990457450565 8.605182044644604
4 6.0 1000.0 30 0 10 0 1.0 0.6666666666666667 0.8536588011138438 4.622721985654384 5.833544274473631
5 7.0 0.0 30 22 19 0 0.2666666666666667 0.3666666666666667 0.8904198516865948 5.0932685092625904 6.288037182687809
6 7.0 250.0 30 0 16 0 1.0 0.4666666666666667 0.9051298885277537 4.734329281741164 5.920393525914459
7 7.0 1000.0 30 0 15 0 1.0 0.5 0.8993182923641181 4.926709522385838 6.258581300752829
8 8.0 0.0 30 29 28 0 0.033333333333333326 0.06666666666666665 0.9242694247736734 3.4370575935466303 4.336980161418882
9 8.0 250.0 30 3 25 0 0.9 0.16666666666666663 0.9295977117848586 3.0564854854589365 3.8003732773040912
10 8.0 1000.0 30 0 27 0 1.0 0.09999999999999998 0.9299247907073535 3.630759939907249 4.464491877765011
11 10.0 0.0 30 30 30 0 0.0 0.0 0.9540049007144441 2.953454502291149 3.678772529679632
12 10.0 250.0 30 0 30 0 1.0 0.0 0.9544231119966623 3.134578534251591 3.8788146229760336
13 10.0 1000.0 30 0 29 0 1.0 0.033333333333333326 0.9530487035097676 3.0232518700094944 3.8817678666309656

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

View File

@@ -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

View File

@@ -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
1 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
2 6.0 0.0 200 108 85 108 0.45999999999999996 0.575 0.45999999999999996 0 0.0 5.202816842065136 0.8803685059021725
3 6.0 250.0 200 3 78 75 0.985 0.61 0.625 185 0.925 5.448336211547444 0.8820273978803879
4 6.0 1000.0 200 1 83 83 0.995 0.585 0.585 198 0.99 8.473194122480628 0.8815296088302559
5 7.0 0.0 200 160 136 160 0.19999999999999996 0.31999999999999995 0.19999999999999996 0 0.0 4.744964108330319 0.9044304298907866
6 7.0 250.0 200 9 145 133 0.955 0.275 0.33499999999999996 171 0.855 4.949784071638471 0.9048829617513402
7 7.0 1000.0 200 2 145 144 0.99 0.275 0.28 195 0.975 5.126941255642888 0.9037603894844779
8 8.0 0.0 200 192 178 192 0.040000000000000036 0.10999999999999999 0.040000000000000036 0 0.0 4.277056721707394 0.9222860828489162
9 8.0 250.0 200 6 175 159 0.97 0.125 0.20499999999999996 176 0.88 4.534455139069744 0.9233618150972334
10 8.0 1000.0 200 1 179 178 0.995 0.10499999999999998 0.10999999999999999 198 0.99 4.749146564125825 0.9260637045844348
11 10.0 0.0 200 200 200 200 0.0 0.0 0.0 0 0.0 3.3448810933215865 0.9511723197247827
12 10.0 250.0 200 8 200 191 0.96 0.0 0.04500000000000004 183 0.915 3.3052349602075664 0.9514399539276094
13 10.0 1000.0 200 2 200 199 0.99 0.0 0.0050000000000000044 197 0.985 3.5081991053844495 0.9505037355131278

View File

@@ -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
1 frequency_hz frequency_mhz relative_power_db prominence_db
2 98840137.685 98.840138 -16.363 14.939
3 99201367.853 99.201368 -8.578 21.049
4 99603613.612 99.603614 -5.852 194.148
5 100099316.323 100.099316 -0.641 27.938
6 100498925.365 100.498925 0.000 200.000
7 100896483.628 100.896484 -2.662 25.928
8 101159569.346 101.159569 -11.137 18.231

View 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 дБ

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View 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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

View File

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

View File

View 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Проверка пройдена: исходный текст восстановлен без ошибок.")

View 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Проверка пройдена: русский текст восстановлен без ошибок.")

View 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 приёмник не смог определить ошибку.")

View 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Проверка пройдена: пакет сформирован и разобран без ошибок.")

View 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 обнаружил изменение одного бита.")

View 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 успешно выполнены.")

View 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Проверка пройдена: "
"все переданные пакеты учтены."
)

View 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Проверка пройдена: "
"все сообщения учтены."
)

View 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 восстановлен байт в байт."
)

View 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 восстановлен байт в байт."
)

View 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Проверка пройдена: "
"параметры передачи фотографии рассчитаны."
)

View 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Проверка пройдена: "
"варианты кадра созданы и рассчитаны."
)

View 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Проверка пройдена: "
"режимы кадра созданы и рассчитаны."
)

View 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Проверка пройдена: "
"цветные и серые варианты созданы и рассчитаны."
)

View 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-сэмплы "
"и демодулирован обратно."
)

View 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-кривая построена."
)

View 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Проверка пройдена: "
"оптимальный размер фрагмента рассчитан."
)

View 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Проверка пройдена: "
"адаптивный выбор режима работает."
)

View 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Проверка пройдена: "
"гистерезис уменьшил количество переключений."
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,172 @@
"""
Lab024A. Первый приём реальных IQ-сэмплов с Pluto+.
Схема:
антенна 40860 МГц -> 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()

View File

@@ -0,0 +1,573 @@
"""
Lab024B. Усреднённый спектр Pluto+ методом Уэлча.
Схема подключения:
антенна 40860 МГц -> 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()

Some files were not shown because too many files have changed in this diff Show More