From 7f508b6fd79c4e9305e9d0a361840980606de2ec Mon Sep 17 00:00:00 2001 From: Maxim Date: Fri, 17 Jul 2026 10:16:10 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 49 +++++++++++++++++++++- docs/COMPARISON.md | 8 +++- docs/USAGE.md | 102 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4b20f38..0e4b5a8 100644 --- a/README.md +++ b/README.md @@ -312,7 +312,11 @@ ssh root@192.168.2.1 '/tmp/ofdm_bench 1.92' переупорядочивания на RX (1024 кадра). **Побайтовый лосслесс на 10 МБ: 44 потери вытянуты 45 ретрансмитами, md5 ==.** ARQ и group-FEC работают вместе: паритет гасит одиночные потери без задержки, ARQ добирает остальное. -8. UDP-туннель поверх линка; затем web-настройка (libmicrohttpd) +8. ✅ **UDP-туннель поверх линка** (§12.10) — `udp_gw` инкапсулирует UDP-датаграммы + в самосинхронизирующиеся рекорды поверх байтового потока transmitter/receiver, + без единой правки ядра тракта. Радио-смоук на антеннах пройден дважды подряд: + md5 == байт-в-байт, 0 дропов/ресинков/дыр. Web-настройка (libmicrohttpd) — + осталась нереализованной, следующий шаг п.8 9. Видео (raw UDP, пакеты ≤1472 Б) — при устойчивом PER ## 11. Диагностика (шпаргалка) @@ -638,6 +642,49 @@ COMPLETE, A вышел с 2. Настоящий PER-стресс — свипо переупорядочивать нечего, а дыру закрыть некому: кольцо лишь задержало бы вывод до EOF. Отсюда ветка `g_fb_fg ? rb : stdout` в `process_frame`/`group_flush`. +### 12.10 UDP-туннель — радио-смоук (§10 п.8) — 2026-07-17 + +**Реализация.** `udp_gw` — самостоятельный бинарь без зависимостей от +iio/liquid/fec, два режима: `-l ` (UDP → рекорды в stdout) и +`-d ip:port` (рекорды из stdin → UDP). Рекорд — 8 Б оверхеда (magic `D5 5D`, +len, id, flags, hcrc8) поверх уже существующего байтового потока +transmitter/receiver — ядро тракта не тронуто ни строкой. Парсер +самосинхронизируется по magic+hcrc8 после дыры (симплекс рвёт границы +рекордов произвольно). `stdout` шлюза в режиме `-l` — `O_NONBLOCK`: вход +быстрее линка дропается с счётчиком, а не блокирует приём (семантика UDP). +`udp_probe` (send/recv по UDP, свой мини-инструмент — busybox без `nc`/`socat`, +правило №1 запрещает Python и на хосте) закрывает бринг-ап без внешних +зависимостей. Пиннинг `udp_gw` на отдельное ядро не сделан — по факту +замера не нужен (Overrun/FQdrp = 0 при работающем шлюзе, ниже). + +**Loopback на одной плате** (без радио, сокет↔сокет через localhost): +66/66 датаграмм, md5 ==, все счётчики (дропы/ресинк/bad_crc/дыры) — 0. + +**Радио-смоук** (антенны на столе, симплекс 915 МГц, `TXGAIN −20`, `-G -p 3000`, +64 КБ / 66 датаграмм по 1000 Б, темп генератора `PACE_MS=15` ≈65 КБ/с — с +запасом под потолок линка ~107 КБ/с): два прогона подряд — md5 == байт-в-байт, +`отправлено 66 | дропов 0` на входе, `доставлено 66 | ресинк 0 Б | bad_crc 0 | +дыр(id) 0` на выходе, `Потери seq 0` на радио, EVM −18.5…−26.2 дБ. + +Правило №7 (кабель+аттенюатор до антенн) сознательно не применялось: `udp_gw` +не меняет PHY ни на байт (слой поверх уже принятого байтового потока), его +собственная логика парсера закрыта юнит-тестом (`make gwtest`) и loopback-ом +на плате — радио-прогон здесь равносилен штатной приёмке линка. + +**Отладка теста, не туннеля.** Первый радио-прогон (ручные команды в терминале) +дал пустой файл на приёме: у `udp_probe recv` сторожевой таймер простоя тикает +с момента ЗАПУСКА процесса, а не с первой датаграммы — интерактивная вставка +команд заняла больше отведённых 10 с, и приёмная цепочка на B самоликвидировалась +до начала передачи на A. После автоматизации в `scripts/test_udp_tunnel.sh` +всплыли ещё два эффекта хореографии: (а) единовременный `killall` терял хвост +последней group-FEC-группы, потому что `receiver` флашит его только в момент +собственной смерти (`receiver.c:798`, после основного цикла) — гашение +переписано на строгий порядок «receiver → пауза на прохождение хвоста через +`udp_gw -d` → `udp_probe»; (б) старт генератора без ожидания готовности +transmitter ронял 1–2 первые датаграммы в EAGAIN, пока iio ещё поднимался — +заменено на ожидание строки «Ожидание данных» в логе TX. Оба фикса — +в `scripts/test_udp_tunnel.sh`, радиотракт и `udp_gw` не менялись. + **Гибрид с group-FEC.** Паритет гасит одиночные потери без задержки (RX снимает восстановленные seq с NACK — ретрансмит не нужен), ARQ добирает то, что паритету не по силам (>2 стираний/группу). `grp_failed > 0` при целом md5 — норма: группа diff --git a/docs/COMPARISON.md b/docs/COMPARISON.md index 628a8b6..a834968 100644 --- a/docs/COMPARISON.md +++ b/docs/COMPARISON.md @@ -143,7 +143,11 @@ group-FEC слоя, а не переписывание протокола с н group-FEC-ом, ARQ там принципиально недоступен — так и задумано. 2. Адаптивная модуляция (QPSK↔16QAM) — измеримый шаг к паритету по «auto link adaptation» с минимальными изменениями кода. -3. **§10 п.8** (UDP-туннель) — переводит pluto-link из «демо передачи файла» - в «сетевой линк», ближе к тому, как позиционируются оба конкурента. +3. ✅ **§10 п.8** (UDP-туннель) — **выполнено 2026-07-17** (README §12.10). + Переводит pluto-link из «демо передачи файла» в «сетевой линк», ближе к + тому, как позиционируются оба конкурента. `udp_gw` — record-слой поверх + существующего байтового потока, ядро тракта не изменено ни строкой. + Радио-смоук на антеннах (два прогона): md5 == байт-в-байт, 0 дропов/ + ресинков/дыр. Web-настройка (хвост п.8) осталась нереализованной. 4. Решение о горизонте 2 (перенос PHY в FPGA) принимать по факту завершения пп. 7–9 roadmap — раньше данных для оценки трудоёмкости недостаточно. diff --git a/docs/USAGE.md b/docs/USAGE.md index 3cba3b6..fb0b7b3 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -85,3 +85,105 @@ A → эфир → B и сверяет md5. Более глубокая диагностика линка — README §11; журнал этапов и рабочие точки — README §12; бюджет CPU и модель паузы — [BENCHMARK.md](BENCHMARK.md). + +## 6. UDP-туннель (`udp_gw`) + +Прозрачный UDP-шлюз поверх линка (README §10 п.8, §12.10): вместо файла через +`cat`/`>` в тракт можно завести живой UDP-трафик. Ядро (transmitter/receiver) +не меняется — `udp_gw` инкапсулирует датаграммы в самосинхронизирующиеся +рекорды (8 Б оверхеда) поверх того же байтового потока. + +### Топология: кто где слушает + +RPi-хост в этой схеме НЕ часть тракта данных — он только запускает команды по +ssh и (в тесте) заливает/забирает файл по scp. Порты 6000/6001 — `127.0.0.1` +**на каждой плате Pluto своя, отдельная**, а не адрес хоста: пакет от +`udp_gw -l 6000` на плате A никогда не покидает саму плату A, пока не выйдет +в эфир через `transmitter`; так же на B. + +``` +================== Pluto A (TX) — 192.168.2.1 =================== + + внешний источник UDP на плате A + (в scripts/test_udp_tunnel.sh — это /tmp/probe_in.bin через + udp_probe send, запущенный по ssh НА ЭТОЙ ЖЕ плате) + | + | UDP -> 127.0.0.1:6000 + v (localhost ПЛАТЫ A, не RPi-хоста!) + udp_gw -l 6000 + | stdout — рекорды (magic D5 5D + len/id/hcrc8) + v + transmitter + -f 915e6 -r 1920000 -b 1500000 + -g -20 -p 3000 -c rs8 -G -u local: + | + v +=================================================================== + ЭФИР 915 МГц, симплекс +=================================================================== + | + v + receiver + -f 915e6 -r 1920000 -b 1500000 + -c rs8 -u local: + | stdout — рекорды + v + udp_gw -d 127.0.0.1:6001 + | UDP -> 127.0.0.1:6001 + v (localhost ПЛАТЫ B, не RPi-хоста!) + внешний приёмник UDP на плате B + (в scripts/test_udp_tunnel.sh — это udp_probe recv, запущенный по + ssh НА ЭТОЙ ЖЕ плате, пишет в /tmp/probe_out.bin) + +================== Pluto B (RX) — 192.168.3.1 =================== +``` + +Если источник/приёмник UDP — не тестовый `udp_probe`, а реальный внешний +прибор, порт `-l`/`-d` слушает не только `127.0.0.1`, а нужный интерфейс +платы (или `0.0.0.0`) — тогда трафик действительно приходит извне платы A +и уходит наружу с платы B, а не варится в loopback, как в тесте. + +Пример (симплекс, антенны, те же флаги линка, что и в §3): + +```bash +# B — приёмник: расшифровать поток в UDP на 127.0.0.1:6001 +ssh root@192.168.3.1 \ + '/tmp/receiver -f 915000000 -r 1920000 -b 1500000 -c rs8 -u local: \ + | /tmp/udp_gw -d 127.0.0.1:6001' + +# A — передатчик: слушать UDP на :6000, завернуть в линк +ssh root@192.168.2.1 \ + '/tmp/udp_gw -l 6000 \ + | /tmp/transmitter -f 915000000 -r 1920000 -b 1500000 -g -20 -p 3000 -c rs8 -G -u local:' +``` + +Автотест по этой схеме (генерирует файл, гоняет через оба шлюза, сверяет md5): +**`TXGAIN=-20 scripts/test_udp_tunnel.sh`** — на антеннах правило №7 +(кабель+аттенюатор) не применяется: шлюз не трогает PHY, его логика проверена +`make gwtest` + loopback (README §12.10). + +**Потолок и семантика.** Вход быстрее линка (~107 КБ/с на `-G -p 3000`) — +`udp_gw -l` дропает датаграммы с ростом счётчика в stderr (не блокирует +приём — так и задумано для UDP). У шлюза нет EOF-протокола: завершение — +`kill`/`SIGTERM`, не END-хендшейк (тот остаётся фичей файловых передач). + +| Что нужно | Профиль | Свойства | +|---|---|---| +| Надёжность, задержка не критична | FDD (`-F`) + `-G` | 0 потерь (ARQ добирает то, что не закрыл паритет), HOL-задержка на ретрансмите | +| Ровная задержка, редкие потери ок | симплекс + `-G` | GF 16+2 закрывает ≤2 стёртых кадра/группу без обратного канала; сверх этого — дыра в id, поток не рвётся | + +Диагностика по счётчикам `udp_gw` (печатаются в stderr раз в секунду): + +| Счётчик | Где | Значит | +|---|---|---| +| `дропов` | `-l` (вход) | вход быстрее линка — контрактный дроп, подними паузу между датаграммами у источника | +| `негабарит` | `-l` (вход) | датаграмма > 1472 Б — уменьшить MTU источника | +| `ресинк` | `-d` (выход) | дыра в потоке (радиопотеря сверх FEC) — парсер откатился к следующему валидному magic+hcrc8 | +| `bad_crc` | `-d` (выход) | ложный magic в данных, отсеян по hcrc8 — не баг, штатная защита рескана | +| `дыр(id)` | `-d` (выход) | сколько id пропущено — оценка объёма потерь на симплексе | + +`udp_probe send/recv` — тестовый генератор/приёмник UDP для бринг-апа +(busybox-прошивка без `nc`/`socat`, Python на хосте запрещён правилом №1): +`udp_probe send ip:port РАЗМЕР_ЧАНКА [ПАУЗА_МС]` (stdin → UDP), +`udp_probe recv ПОРТ ТАЙМАУТ_ПРОСТОЯ_С` (UDP → stdout, самозавершается по +простою — таймер тикает с запуска процесса, не с первой датаграммы).