Доработка. Проверка шаг 4: радио-смоук на антеннах через test_udp_tunnel.sh (TXGAIN=-20)

This commit is contained in:
Maxim
2026-07-17 09:55:33 +03:00
parent 1472531f2f
commit bbda48433c

129
scripts/test_udp_tunnel.sh Normal file
View File

@@ -0,0 +1,129 @@
#!/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' <<EOF &
rm -f /tmp/probe_out.bin /tmp/rx.log /tmp/gwd.log /tmp/probe_rx.log
/tmp/receiver -f $FREQ -r $RATE -b $BW -c $FEC -u local: 2>/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' <<EOF
rm -f /tmp/tx.log /tmp/gwl.log /tmp/probe_tx.log /tmp/gw.fifo
mkfifo /tmp/gw.fifo
/tmp/udp_gw -l $PORT_A > /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