Files
Pluto-SDR/scripts/test_arq.sh
Maxim ab1e87a17e Этап 3c — гибрид GF+ARQ, пересборка NACK, приёмка. Техдолг #6 закрыт
ARQ включён и для group-FEC. Паритет и ретрансмит перестали быть альтернативами:
GF гасит одиночные потери без задержки, ARQ добирает то, что паритету не по силам
(>2 стираний/группу). Дыру, закрытую паритетом, RX снимает с NACK сам — лишнего
трафика нет.

receiver.c:
- group_route: кадр ложится в кольцо ДО любого bypass. Кольцо — единственный
  источник порядка и живёт независимо от сборки групп; иначе ретрансмит закрытой
  группы не занял бы свой слот и next_out встал бы на нём навсегда. Паритет
  получает слот нулевой длины: тратит seq, но в файл не идёт.
- group_route: bypass для кадра уже закрытой группы (base < g.base, а также
  base == g.base при !active — группа финализирована по END). Без него ретрансмит
  преждевременно flush-ал активную группу, сбрасывая её стирания, либо «воскрешал»
  закрытую с одним элементом.
- group_flush: под FDD пишет в кольцо ТОЛЬКО восстановленные паритетом кадры
  (g.fixed) и снимает их seq с NACK — принятые уже в кольце. Симплекс сохраняет
  прямой вывод группы: ретрансмитов там нет, переупорядочивать нечего.
- process_end: досрочный group_flush последней группы. Штатно группа закрывается
  первым кадром следующей — для последней его не будет, и её восстановление
  случилось бы только при выходе потока C, то есть уже после COMPLETE.
- process_end/miss_rebuild: NACK пересобирается из кольца на каждом END. Находка
  стресса 3b: miss_add при переполнении MISS_CAP молча терял seq, и вернуть его
  было некому (детектор дыр по нему второй раз не срабатывает) — приём заклинивало
  насмерть. Кольцо знает точно, каких кадров нет.
- arq_check_complete: критерий единый (next_out == total_seq), временный gf_seen
  убран.

transmitter.c: g_arq_on = 1 безусловно при -F; arq_service в GF-цикле как в legacy.
2026-07-16 15:29:25 +03:00

147 lines
9.2 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# test_arq.sh — e2e-тест ARQ и EOF-хендшейка (этап 3, §10 п.7, техдолг #6).
#
# Отличие от test_fdd_fwd.sh: там успех = md5 совпал, а завершение обеих сторон
# организует скрипт (killall + сторожевой kill). Здесь проверяется сам ПРОТОКОЛ:
# • B, собрав файл целиком, поднимает FB_FLAG_COMPLETE в STATUS и выходит САМ;
# • A ждёт этот COMPLETE и возвращает 0; не дождался за 10 с → exit 2.
# Поэтому сторожевой kill приёмника = провал (B не смог завершиться сам), а
# ненулевой код A — не аварийная ситуация, а штатный отчёт «файл не подтверждён».
#
# Использование: scripts/test_arq.sh [ipA] [ipB] [размер_КБ]
# SIZE_KB=10240 — длинный прогон (или третьим позиционным аргументом)
# TXGAIN=-33 — стресс: высокий PER, ARQ обязан вытянуть md5. Клифф на
# кабеле резкий: 30 ≈ PER 0.4%, 38 и глубже — CRC 0
# (канала нет вовсе, проверять там нечего)
# NEGATIVE=1 FBGAIN=-89 — обратный канал заглушён: успех = A НЕ подтвердил
# FBGAIN (деф. 15) — середина рабочего окна 20…10 (README §12.8)
# FBFREQ, PAUSE, TXGAIN, FEC, GROUPFEC — как в test_fdd_fwd.sh
# ВНИМАНИЕ: сначала кабель + аттенюатор 3040 дБ!
set -euo pipefail
source "$(dirname "$0")/../toolchain.env"
A="${1:-$PLUTO_A}" # передатчик данных (TX 915 / fb-RX 868)
B="${2:-$PLUTO_B}" # приёмник данных (RX 915 / fb-TX 868)
SIZE_KB="${3:-${SIZE_KB:-1024}}"
FREQ=915000000
FBFREQ="${FBFREQ:-868000000}"
RATE=1920000 # НЕ поднимать: бюджет CPU, docs/BENCHMARK.md
BW=1500000
TXGAIN="${TXGAIN:--30}"
FBGAIN="${FBGAIN:--15}" # штатный обратный канал; 89 ≈ выкл (NEGATIVE)
FEC="${FEC:-rs8}"
PAUSE="${PAUSE:-4000}" # §12.7: соук/маргинальный канал — только 4000, не 3000
GROUPFEC="${GROUPFEC:-1}"
NEGATIVE="${NEGATIVE:-0}"
[ "$GROUPFEC" = 1 ] && GFLAG="-G" || GFLAG=""
# +40 (против +20 у test_fdd_fwd): END-фаза до 10 с плюс запас на ретрансмиты.
TIMEOUT=$(( SIZE_KB / 85 + 40 ))
if command -v sshpass >/dev/null; then
SSH=(sshpass -p "$PLUTO_PASS" ssh -o StrictHostKeyChecking=no)
SCP=(sshpass -p "$PLUTO_PASS" scp -O -o StrictHostKeyChecking=no)
else
SSH=(ssh); SCP=(scp -O)
echo "подсказка: apt install sshpass — и пароль вводить не придётся"
fi
echo "=== ARQ e2e: A=$A (TX $((FREQ/1000000))/fb-RX $((FBFREQ/1000000))) → B=$B ==="
echo " ${SIZE_KB} КБ TXGAIN=$TXGAIN FBGAIN=$FBGAIN PAUSE=$PAUSE GROUPFEC=$GROUPFEC NEGATIVE=$NEGATIVE"
echo "=== [1/6] Тестовый файл ${SIZE_KB} КБ ==="
dd if=/dev/urandom of=/tmp/arq_test.bin bs=1024 count="$SIZE_KB" status=none
MD5_SRC=$(md5sum /tmp/arq_test.bin | cut -d' ' -f1)
echo "md5: $MD5_SRC"
echo "=== [2/6] Зачистка хвостов на обеих платах ==="
# Остаточный процесс держит RX-DMA → пустой out.bin (грабля из README §12.6).
"${SSH[@]}" "root@$A" "killall -9 transmitter receiver 2>/dev/null || true"
"${SSH[@]}" "root@$B" "killall -9 transmitter receiver 2>/dev/null || true"
echo "=== [3/6] Заливка файла на Pluto A ($A) ==="
"${SCP[@]}" /tmp/arq_test.bin "root@$A:/tmp/"
echo "=== [4/6] Старт приёмника на Pluto B ($B), сторож ${TIMEOUT}с ==="
# busybox без `timeout` → сторожевой sleep+kill на плате. Штатно B выходит САМ по
# COMPLETE задолго до сторожа; сторож снимается сразу после выхода B, иначе ssh
# висел бы до конца sleep и скрывал реальное время завершения.
"${SSH[@]}" "root@$B" \
"rm -f /tmp/out.bin; /tmp/receiver -f $FREQ -r $RATE -b $BW -c $FEC \
-F $FBFREQ -g $FBGAIN -u local: > /tmp/out.bin 2>/tmp/rx_arq.log & \
RXP=\$!; (sleep $TIMEOUT; kill \$RXP 2>/dev/null) & WDP=\$!; \
wait \$RXP; RC=\$?; kill \$WDP 2>/dev/null; exit \$RC" &
RX_PID=$!
sleep 4 # дать RX прогреться (FDD-инициализация двух трактов дольше)
echo "=== [5/6] Передача с Pluto A + EOF-хендшейк ==="
TX_RC=0
"${SSH[@]}" "root@$A" \
"cat /tmp/arq_test.bin | /tmp/transmitter -f $FREQ -r $RATE -b $BW \
-g $TXGAIN -p $PAUSE -c $FEC $GFLAG -F $FBFREQ -u local: 2>/tmp/tx_arq.log" \
|| TX_RC=$?
echo "код возврата передатчика: $TX_RC (0 = COMPLETE подтверждён, 2 = таймаут)"
wait "$RX_PID" 2>/dev/null || true # B выходит сам либо снимается сторожем
"${SSH[@]}" "root@$B" "killall receiver 2>/dev/null || true" # страховка
echo "=== [6/6] Сверка ==="
MD5_DST=$("${SSH[@]}" "root@$B" "md5sum /tmp/out.bin 2>/dev/null | cut -d' ' -f1" || echo ет_файла")
SIZE_DST=$("${SSH[@]}" "root@$B" "wc -c < /tmp/out.bin 2>/dev/null" || echo 0)
echo "источник: $MD5_SRC ($((SIZE_KB*1024)) байт)"
echo "приёмник: $MD5_DST ($SIZE_DST байт)"
# Самозавершение B: маркер печатается ТОЛЬКО когда COMPLETE ушёл в эфир и B
# закрывается штатно. Нет маркера → B досидел до сторожевого kill = провал 3a.
SELF_DONE=0
"${SSH[@]}" "root@$B" "grep -q 'завершаю приём' /tmp/rx_arq.log 2>/dev/null" && SELF_DONE=1 || true
echo "--- Хендшейк ---"
"${SSH[@]}" "root@$B" "grep ARQ /tmp/rx_arq.log 2>/dev/null | sed 's/^/B: /'" || true
"${SSH[@]}" "root@$A" "grep ARQ /tmp/tx_arq.log 2>/dev/null | sed 's/^/A: /'" || true
echo "B самозавершился: $([ "$SELF_DONE" = 1 ] && echo да || echo 'НЕТ (снят сторожем)')"
echo "--- Обратный канал STATUS (B→A) ---"
SENT=$("${SSH[@]}" "root@$B" "grep -o 'отправлено STATUS: [0-9]*' /tmp/rx_arq.log 2>/dev/null | grep -o '[0-9]*'" || echo "")
RECV=$("${SSH[@]}" "root@$A" "grep -o 'принято STATUS: [0-9]*' /tmp/tx_arq.log 2>/dev/null | grep -o '[0-9]*'" || echo "")
SENT=${SENT:-0}; RECV=${RECV:-0}
if [ "$SENT" -gt 0 ]; then
echo "B отправил: $SENT | A принял: $RECV | доставка ≈ $(( RECV * 100 / SENT ))%"
else
echo "B отправил: $SENT | A принял: $RECV"
fi
echo "--- RX-статистика (PER) ---"
"${SSH[@]}" "root@$B" "grep '^Кадров:' /tmp/rx_arq.log 2>/dev/null | tail -1" || true
# Ретрансмиты — главная метрика 3b: сырой PER остаётся, но md5 обязан сойтись.
"${SSH[@]}" "root@$A" "grep 'ретрансмитов' /tmp/tx_arq.log 2>/dev/null | tail -1 | sed 's/^/A: /'" || true
echo "=== РЕЗУЛЬТАТ ==="
if [ "$NEGATIVE" = 1 ]; then
# Обратный канал заглушён: B файл соберёт и выйдет сам, но подтвердить A
# некому. Проверяем именно честность отказа — раньше A рапортовал успех.
if [ "$TX_RC" != 0 ]; then
echo "OK (негатив) — A не получил COMPLETE и честно вышел с кодом $TX_RC"
else
echo "FAIL (негатив) — A вернул 0 без обратного канала: хендшейк фиктивен"
exit 1
fi
else
if [ "$MD5_SRC" = "$MD5_DST" ] && [ "$SELF_DONE" = 1 ] && [ "$TX_RC" = 0 ]; then
echo "OK — md5 совпал, B завершился по COMPLETE, A подтвердил"
else
echo "FAIL: md5 $([ "$MD5_SRC" = "$MD5_DST" ] && echo ок || echo РАСХОЖДЕНИЕ) |" \
"самозавершение $([ "$SELF_DONE" = 1 ] && echo ок || echo НЕТ) | код A $TX_RC"
echo "Диагностика:"
echo " - md5 разошёлся при коде A=0 → COMPLETE поднят рано (проверь miss/next_out)."
echo " - код A=2 при целом md5 → COMPLETE не долетел: подними FBGAIN к 10."
echo " - размер 0 → RX не декодит: остаточный процесс на B или самоглушение."
echo " - CRC: 0 при живых 'Заголовках' → канал за клиффом (TXGAIN ≤ 38 на кабеле):"
echo " заголовок BPSK ещё держится, payload QPSK — нет. Это не стресс ARQ, а обрыв."
echo " - 'NACK вне окна' > 0 на B → дыра пережила TX-окно (2048 кадров ≈ 21 с)."
echo "--- хвост rx_arq.log (B) ---"
"${SSH[@]}" "root@$B" "tail -8 /tmp/rx_arq.log" || true
exit 1
fi
fi