#!/usr/bin/env bash # test_udp_tunnel.sh — e2e-тест UDP-туннеля (§10 п.8): # хост-файл → udp_probe send → UDP → udp_gw -l → transmitter → ЭФИР → # receiver → udp_gw -d → UDP → udp_probe recv → файл → md5-сверка # Использование: scripts/test_udp_tunnel.sh [ipA] [ipB] [размер_КБ] # По умолчанию: адреса из toolchain.env, 64 КБ, симплекс 915 МГц. # # ВНИМАНИЕ: правило №7 (кабель+аттенюатор) для udp_gw не применимо — шлюз не # трогает PHY ни на байт (record-слой поверх уже принятого байтового потока), # его логика проверена `make gwtest` + loopback на плате. Прогон штатно идёт # на антеннах: TXGAIN=-20 scripts/test_udp_tunnel.sh set -euo pipefail source "$(dirname "$0")/../toolchain.env" A="${1:-$PLUTO_A}" # передатчик B="${2:-$PLUTO_B}" # приёмник SIZE_KB="${3:-64}" FREQ=915000000 RATE=1920000 # НЕ поднимать: бюджет CPU, docs/BENCHMARK.md BW=1500000 TXGAIN="${TXGAIN:--30}" # OTA: переопредели, напр. TXGAIN=-20 scripts/test_udp_tunnel.sh FEC="${FEC:-rs8}" PAUSE="${PAUSE:-3000}" # ≤1 МБ — 3000 чист (§12.6); длинные прогоны — 4000 GROUPFEC="${GROUPFEC:-1}" [ "$GROUPFEC" = 1 ] && GFLAG="-G" || GFLAG="" PORT_A="${PORT_A:-6000}" # udp_gw -l на плате A (вход туннеля) PORT_B="${PORT_B:-6001}" # udp_probe recv на плате B (выход туннеля) CHUNK="${CHUNK:-1000}" # размер датаграммы; >1016 Б рекорд рвётся на 2 кадра PACE_MS="${PACE_MS:-15}" # 1000 Б / 15 мс ≈ 65 КБ/с — с запасом под потолок # `-G -p 3000` ≈107 КБ/с. На 10 мс (≈100 КБ/с) вход # упирается в потолок и шлюз штатно дропает. IDLE="${IDLE:-30}" # сторожевой таймер udp_probe recv. ВАЖНО: тикает # с момента СТАРТА, не с первой датаграммы → зазор # старт-B→передача должен быть скриптованным (~4 с), # а не ручной вставкой команд. Штатное завершение — # активный killall из шага [5], не по таймауту. if command -v sshpass >/dev/null; then SSH=(sshpass -p "$PLUTO_PASS" ssh -o StrictHostKeyChecking=no) else SSH=(ssh) echo "подсказка: apt install sshpass — и пароль вводить не придётся" fi if command -v sshpass >/dev/null; then SCP=(sshpass -p "$PLUTO_PASS" scp -O -o StrictHostKeyChecking=no) else SCP=(scp -O) fi echo "=== [1/6] Тестовый файл ${SIZE_KB} КБ (чанк ${CHUNK} Б, темп ${PACE_MS} мс) ===" dd if=/dev/urandom of=/tmp/probe_in.bin bs=1024 count="$SIZE_KB" status=none MD5_SRC=$(md5sum /tmp/probe_in.bin | cut -d' ' -f1) DGRAMS=$(( (SIZE_KB * 1024 + CHUNK - 1) / CHUNK )) echo "md5: $MD5_SRC | ожидается датаграмм: $DGRAMS" echo "=== [2/6] Заливка файла на Pluto A ($A) ===" "${SCP[@]}" /tmp/probe_in.bin "root@$A:/tmp/" echo "=== [3/6] Старт цепочки на Pluto B ($B): receiver | udp_gw -d → udp_probe recv ===" # heredoc в `sh -s`: stdin ssh занят скриптом → фоновый ssh не ловит SIGTTIN # (без этого `ssh ... &` уходит в «Остановлен» при попытке читать терминал). "${SSH[@]}" "root@$B" 'sh -s' </tmp/rx.log \ | /tmp/udp_gw -d 127.0.0.1:$PORT_B 2>/tmp/gwd.log & /tmp/udp_probe recv $PORT_B $IDLE > /tmp/probe_out.bin 2>/tmp/probe_rx.log killall receiver udp_gw 2>/dev/null || true EOF B_PID=$! sleep 4 # дать приёмнику прогреться до старта TX echo "=== [4/6] Передача с Pluto A: udp_probe send → udp_gw -l → transmitter ===" # mkfifo: у пайпа `$!` — только последний процесс, а гейтвей надо убить отдельно # (его выход = EOF транзмиттеру → штатное завершение передачи). "${SSH[@]}" "root@$A" 'sh -s' < /tmp/gw.fifo 2>/tmp/gwl.log & GWP=\$! /tmp/transmitter -f $FREQ -r $RATE -b $BW -g $TXGAIN -p $PAUSE \ -c $FEC $GFLAG -u local: < /tmp/gw.fifo 2>/tmp/tx.log & TXP=\$! sleep 1 /tmp/udp_probe send 127.0.0.1:$PORT_A $CHUNK $PACE_MS \ < /tmp/probe_in.bin 2>/tmp/probe_tx.log sleep 2 kill \$GWP 2>/dev/null || true wait \$TXP rm -f /tmp/gw.fifo EOF echo "=== [5/6] Гашение цепочки B ===" sleep 3 # дохлебать хвост эфира # udp_probe по SIGTERM печатает статистику и закрывает файл — гасим активно, # не дожидаясь IDLE (тот же паттерн sleep+kill, что в test_file_link.sh). "${SSH[@]}" -n "root@$B" "killall udp_probe receiver udp_gw 2>/dev/null || true" || true wait "$B_PID" 2>/dev/null || true echo "=== [6/6] Сверка ===" MD5_DST=$("${SSH[@]}" -n "root@$B" "md5sum /tmp/probe_out.bin 2>/dev/null | cut -d' ' -f1" || echo "нет_файла") SIZE_DST=$("${SSH[@]}" -n "root@$B" "wc -c < /tmp/probe_out.bin 2>/dev/null" || echo 0) echo "источник: $MD5_SRC ($((SIZE_KB*1024)) байт)" echo "туннель: $MD5_DST ($SIZE_DST байт)" echo echo "--- A: udp_probe send ---"; "${SSH[@]}" -n "root@$A" "cat /tmp/probe_tx.log" || true echo "--- A: udp_gw -l (вход) ---"; "${SSH[@]}" -n "root@$A" "tail -2 /tmp/gwl.log" || true echo "--- A: transmitter ---"; "${SSH[@]}" -n "root@$A" "tail -4 /tmp/tx.log" || true echo "--- B: receiver ---"; "${SSH[@]}" -n "root@$B" "tail -6 /tmp/rx.log" || true echo "--- B: udp_gw -d (выход) ---";"${SSH[@]}" -n "root@$B" "tail -2 /tmp/gwd.log" || true echo "--- B: udp_probe recv ---"; "${SSH[@]}" -n "root@$B" "cat /tmp/probe_rx.log" || true echo if [ "$MD5_SRC" = "$MD5_DST" ]; then echo "РЕЗУЛЬТАТ: OK — туннель прозрачен, $DGRAMS датаграмм доставлены байт-в-байт" else echo "РЕЗУЛЬТАТ: FAIL — разбор по счётчикам выше:" echo " - 'дыр(id)' > 0 или 'ресинк' > 0 при живом потоке → НЕ баг туннеля:" echo " радиопотери сверх group-FEC 16+2 (на симплексе ARQ нет, техдолг #6)." echo " Контракт соблюдён: дыры посчитаны, поток рекордов не развалился." echo " Меры: TXGAIN ближе к −20, антенны разнести/переориентировать." echo " - 'дропов' > 0 у 'udp_gw -l (вход)' → вход быстрее линка (~107 КБ/с" echo " на -G -p 3000): подними PACE_MS. Это контрактный дроп, семантика UDP." echo " - размер 0 и в rx.log 0 кадров → приёмник не работал во время передачи:" echo " частота/усиление/антенны, либо цепочка B умерла по IDLE (=$IDLE с)." echo " - размер совпал, md5 нет → искажение полезной нагрузки: техдолг #1/#3." fi