diff --git a/scripts/test_arq.sh b/scripts/test_arq.sh new file mode 100755 index 0000000..f9be882 --- /dev/null +++ b/scripts/test_arq.sh @@ -0,0 +1,140 @@ +#!/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=-45 — стресс: высокий PER, ARQ обязан вытянуть md5 +# NEGATIVE=1 FBGAIN=-89 — обратный канал заглушён: успех = A НЕ подтвердил +# FBGAIN (деф. −15) — середина рабочего окна −20…−10 (README §12.8) +# FBFREQ, PAUSE, TXGAIN, FEC, GROUPFEC — как в test_fdd_fwd.sh +# ВНИМАНИЕ: сначала кабель + аттенюатор 30–40 дБ! +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 + +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 " - на этапе 3a ретрансмитов ещё нет: любая потеря кадра → код A=2 ОЖИДАЕМО." + echo "--- хвост rx_arq.log (B) ---" + "${SSH[@]}" "root@$B" "tail -8 /tmp/rx_arq.log" || true + exit 1 + fi +fi diff --git a/src/common.h b/src/common.h index 50f033e..cedf957 100644 --- a/src/common.h +++ b/src/common.h @@ -46,9 +46,12 @@ #define TX_URI_DEFAULT "local:" #define RX_URI_DEFAULT "local:" -// Формат заголовка кадра +// Формат заголовка кадра. hdr[0..1] различает тип: данные/паритет GF — F0 AA, +// END (маркер конца передачи, этап 3) — F0 E7, STATUS обратного канала — F0 55 +// (FB_SIG_* в feedback.h). Байт 0 общий у всех типов. #define HDR_SIGNATURE_0 0xF0 #define HDR_SIGNATURE_1 0xAA +#define HDR_SIG_END_1 0xE7 #define HDR_SIZE 12 // Макросы логирования diff --git a/src/feedback.c b/src/feedback.c index a1bdafc..9a2eadd 100644 --- a/src/feedback.c +++ b/src/feedback.c @@ -14,6 +14,15 @@ static uint32_t get_u32(const uint8_t *p) { ((uint32_t)p[2] << 8) | (uint32_t)p[3]; } +static void put_u64(uint8_t *p, uint64_t v) { + put_u32(p, (uint32_t)(v >> 32)); + put_u32(p + 4, (uint32_t)(v & 0xFFFFFFFFu)); +} + +static uint64_t get_u64(const uint8_t *p) { + return ((uint64_t)get_u32(p) << 32) | (uint64_t)get_u32(p + 4); +} + size_t fb_status_pack(const fb_status_t *s, uint8_t *out) { uint8_t cnt = s->nack_cnt > FB_MAX_NACK ? FB_MAX_NACK : s->nack_cnt; out[0] = FB_TYPE_STATUS; @@ -46,3 +55,16 @@ int fb_status_unpack(const uint8_t *in, size_t len, fb_status_t *s) { s->nack[i] = get_u32(in + FB_STATUS_HDR + (size_t)i * 4); return 0; } + +size_t fb_end_pack(const fb_end_t *e, uint8_t *out) { + put_u32(out, e->total_seq); + put_u64(out + 4, e->total_bytes); + return FB_END_LEN; +} + +int fb_end_unpack(const uint8_t *in, size_t len, fb_end_t *e) { + if (len < FB_END_LEN) return -1; + e->total_seq = get_u32(in); + e->total_bytes = get_u64(in + 4); + return 0; +} diff --git a/src/feedback.h b/src/feedback.h index 08de6e1..9e6bdd8 100644 --- a/src/feedback.h +++ b/src/feedback.h @@ -42,4 +42,22 @@ size_t fb_status_pack(const fb_status_t *s, uint8_t *out); // Распаковка. Возврат 0 при успехе, -1 при кривой длине/типе. int fb_status_unpack(const uint8_t *in, size_t len, fb_status_t *s); +// --- END-кадр A→B (этап 3, сигнатура F0 E7 в hdr[0..1]) ----------------- +// Хвостовые потери молчаливы: у последних кадров нет следующего seq, по которому +// видна дыра. END несёт итог передачи — RX добивает недостающие seq в NACK и +// понимает, когда файл собран целиком (COMPLETE). Идёт тем же путём RS+CRC32, +// что и данные (1 RS-блок). Тип не кодируется в payload — его задаёт сигнатура. +typedef struct { + uint32_t total_seq; // отправлено кадров: валидные seq = 0..total_seq-1 + uint64_t total_bytes; // исходных байт всего (сверка длины файла) +} fb_end_t; + +// Раскладка payload (BE): 0..3 total_seq | 4..11 total_bytes +#define FB_END_LEN 12 + +// Упаковка → out (≥ FB_END_LEN). Возврат — длина payload. +size_t fb_end_pack(const fb_end_t *e, uint8_t *out); +// Распаковка. Возврат 0 при успехе, -1 при кривой длине. +int fb_end_unpack(const uint8_t *in, size_t len, fb_end_t *e); + #endif diff --git a/src/receiver.c b/src/receiver.c index 5707601..249aefe 100644 --- a/src/receiver.c +++ b/src/receiver.c @@ -106,6 +106,21 @@ static void miss_ack(uint32_t seq) { } } +// --- EOF-хендшейк (этап 3, поток C) ------------------------------------- +// END (F0 E7) несёт итог передачи: сколько кадров и байт ушло в эфир. Без него +// хвостовые потери молчаливы — у последних кадров нет следующего seq, по которому +// детектор видит дыру. Приняв END, RX добивает недостающие seq в NACK, а собрав +// всё — поднимает FB_FLAG_COMPLETE в STATUS и завершается сам. +#define ARQ_COMPLETE_HOLD_MS 600 // держать COMPLETE ≥6 периодов STATUS: в + // тихом эфире END-фазы доставка ~100%, но + // повтор дешевле, чем таймаут A на 10 с + +static int end_seen = 0; +static uint32_t end_total_seq = 0; +static uint64_t end_total_bytes = 0; +static uint64_t end_rx = 0; // принято валидных END (диагностика) +static int64_t complete_since_ms = 0; // 0 = файл ещё не собран + // --- Отправитель STATUS на feedback-TX (только при -F, поток C) ---------- // Свой framegen + буфер на txfb (868), свой RS-энкодер. Кадр STATUS шлётся не // чаще FB_PERIOD_MS. Данные RX и обратный TX — независимые cf-девайсы (FDD), @@ -128,8 +143,34 @@ static int64_t now_ms(void) { return (int64_t)t.tv_sec * 1000 + t.tv_nsec / 1000000; } +// Файл собран? Условие: END принят, дыр нет, фронт дошёл до конца. После +// backfill'а в process_end highest_seq == end_total_seq-1 всегда, так что решает +// miss_cnt — но проверку фронта оставляем явной (в 3b её заменит next_out). +// Держим флаг ARQ_COMPLETE_HOLD_MS, чтобы A успел услышать хотя бы один STATUS, +// затем завершаемся штатным путём через stop (main → join A→B→C). +static void arq_check_complete(void) { + if (!g_fb_fg || !end_seen) return; // симплекс либо передача ещё идёт + int done = (end_total_seq == 0) || + (miss_cnt == 0 && seq_init && highest_seq == end_total_seq - 1); + if (!done) { complete_since_ms = 0; return; } + + int64_t t = now_ms(); + if (!complete_since_ms) { + complete_since_ms = t; // с этого момента STATUS несёт COMPLETE + fprintf(stderr, "\n[ARQ] COMPLETE: %u кадров, %llu байт — держу флаг %d мс\n", + end_total_seq, (unsigned long long)end_total_bytes, + ARQ_COMPLETE_HOLD_MS); + } + if (t - complete_since_ms >= ARQ_COMPLETE_HOLD_MS) { + fprintf(stderr, "[ARQ] COMPLETE отправлен — завершаю приём\n"); + stop = 1; + } +} + static void maybe_send_status(void) { if (!g_fb_fg) return; // симплекс — обратного канала нет + arq_check_complete(); // до гейта периода: решение о завершении + // не должно ждать своего слота STATUS int64_t t = now_ms(); if (g_fb_last_ms && t - g_fb_last_ms < FB_PERIOD_MS) return; g_fb_last_ms = t; @@ -137,7 +178,8 @@ static void maybe_send_status(void) { fb_status_t st; memset(&st, 0, sizeof(st)); st.type = FB_TYPE_STATUS; - st.flags = 0; // END_SEEN/COMPLETE — этап 3 + st.flags = (end_seen ? FB_FLAG_END_SEEN : 0) | + (complete_since_ms ? FB_FLAG_COMPLETE : 0); st.highest_seq = seq_init ? highest_seq : 0xFFFFFFFFu; st.rx_frames = (uint32_t)crc_final; st.overruns = (uint32_t)ring.overruns; // телеметрия, гонка косметическая @@ -282,6 +324,46 @@ static void group_route(uint8_t flags, uint32_t seq, } } +// Разбор END-кадра (поток C, этап 3). Зафиксировать итог передачи и добить +// хвостовые дыры в NACK. Повторы END (A шлёт их каждые 100 мс до COMPLETE) +// идемпотентны: состояние берём с первого валидного. +static void process_end(const uint8_t *hdr, uint8_t *pay, unsigned pay_len, + int pay_valid) { + static uint8_t edec[RS_ENC]; // END = 12 Б → ровно 1 RS-блок (223 Б) + fb_end_t e; + + if (fec_on && decoder) { + size_t elen = 0; + if (!phy_rx_decode(hdr, pay, pay_len, decoder, edec, sizeof(edec), &elen)) + return; // RS/CRC не прошли — ждём следующий END + if (fb_end_unpack(edec, elen, &e) != 0) return; + } else { // -c none: payload как есть + uint16_t olen = (uint16_t)((hdr[6] << 8) | hdr[7]); + if (!pay_valid || olen < FB_END_LEN || olen > pay_len) return; + if (fb_end_unpack(pay, olen, &e) != 0) return; + } + + end_rx++; + if (end_seen) return; + end_seen = 1; + end_total_seq = e.total_seq; + end_total_bytes = e.total_bytes; + + // Backfill: всё от текущего фронта до total_seq-1 в эфире было, но до нас не + // дошло. Массив miss остаётся отсортированным — добавляем seq строго больше + // всех имеющихся. Фронт двигаем на конец файла: дальше приходить могут только + // ретрансмиты (seq ≤ highest_seq), ложных дыр они не породят. + if (e.total_seq > 0 && (!seq_init || highest_seq < e.total_seq - 1)) { + uint32_t from = seq_init ? highest_seq + 1 : 0; + for (uint32_t m = from; m < e.total_seq; m++) miss_add(m); + lost += e.total_seq - from; + seq_init = 1; + highest_seq = e.total_seq - 1; + } + fprintf(stderr, "\n[ARQ] END принят: всего %u кадров, %llu байт; не хватает %d\n", + e.total_seq, (unsigned long long)e.total_bytes, miss_cnt); +} + // Разбор одного кадра в потоке C: seq-детектор + RS + CRC-поверх-RS, затем // маршрутизация (прямая запись для legacy, сборка группы для group-FEC). // Единственный писатель в stdout — этот поток, порядок сохранён (fq FIFO). @@ -291,10 +373,16 @@ static void process_frame(fq_slot_t *fs) { unsigned pay_len = fs->pay_len; int pay_valid = fs->pay_valid; + // END (F0 E7) — маркер конца передачи. Ветвимся ДО seq-детектора: у END свой + // счётчик seq, в пространстве seq данных его быть не должно. + if (hdr[0] == HDR_SIGNATURE_0 && hdr[1] == HDR_SIG_END_1) { + process_end(hdr, pay, pay_len, pay_valid); + return; + } + // Проверка сигнатуры hdr[0..1]: liquid-CRC (hdr_valid) страхует только - // целостность байт, но не то, что это наш протокол. Пока в эфире один тип - // кадра (данные/паритет GF, F0 AA) — это no-op; готовит почву под доп. - // типы кадров обратного канала (roadmap §10 п.7: STATUS/END на др. частоте). + // целостность байт, но не то, что это наш протокол. Отсеивает чужие типы + // кадров (roadmap §10 п.7) и мусор, прошедший CRC заголовка. if (hdr[0] != HDR_SIGNATURE_0 || hdr[1] != HDR_SIGNATURE_1) { sig_bad++; return; @@ -677,8 +765,12 @@ int main(int argc, char *argv[]) { (unsigned long long)ring.overruns, (unsigned long long)fq.drops, (unsigned long long)grp_recovered, (unsigned long long)grp_failed, (unsigned long long)crc_final, (unsigned long long)total_bytes); - if (fb_freq) + if (fb_freq) { fprintf(stderr, "Обратный канал: отправлено STATUS: %u\n", g_fb_seq); + fprintf(stderr, "ARQ: END принято: %llu | ожидалось кадров: %u | не хватает: %d | COMPLETE: %s\n", + (unsigned long long)end_rx, end_total_seq, miss_cnt, + complete_since_ms ? "да" : "НЕТ"); + } if (decoder) fec_destroy(decoder); ofdmflexframesync_destroy(fs); diff --git a/src/transmitter.c b/src/transmitter.c index f2d91cc..2d1c5f4 100644 --- a/src/transmitter.c +++ b/src/transmitter.c @@ -24,11 +24,26 @@ extern volatile sig_atomic_t stop; // Сигнатура кадров данных/паритета GF (см. frame_tx.h — тип кадра различает // hdr[0..1]; END/STATUS обратного канала используют другие сигнатуры, §10 п.7). static const uint8_t SIG_DATA[2] = { HDR_SIGNATURE_0, HDR_SIGNATURE_1 }; +static const uint8_t SIG_END[2] = { HDR_SIGNATURE_0, HDR_SIG_END_1 }; + +// Параметры EOF-хендшейка (этап 3). После EOF A шлёт END каждые END_PERIOD_MS и +// ждёт STATUS с флагом COMPLETE. В этой фазе эфир 915 почти пуст (END ≈ 1.4 мс +// на 100 мс), поэтому fb-RX платы A больше не глохнет в собственные бёрсты и +// слышит практически все STATUS (плато 50% из §12.8 — эффект duty 57%). +#define END_PERIOD_MS 100 +#define END_TIMEOUT_MS 10000 // 100 попыток; молчание = честный отказ (exit 2) + +static int64_t now_ms(void) { + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return (int64_t)t.tv_sec * 1000 + t.tv_nsec / 1000000; +} // --- Обратный канал: приём STATUS на 868 (этап 2, §10 п.7) -------------- // Отдельный поток на ядре 1 (данные TX — ядро 0). Свой синхронизатор с -// header_len 12 и свой RS-декодер. Пока ТОЛЬКО логирует; ARQ-реакция (ретрансмит -// по NACK) — этап 3. Слушает rxfb — второй cf-девайс, открытый pluto_init_fdd. +// header_len 12 и свой RS-декодер. Слушает rxfb — второй cf-девайс, открытый +// pluto_init_fdd. Декодированный STATUS публикуется снимком для main: пока его +// читает только END-фаза (этап 3a), ретрансмит по NACK — этап 3b. #define FB_RX_BUF_SAMPLES 16384 static struct iio_buffer *g_fbrx_buf = NULL; @@ -37,8 +52,26 @@ static fec g_fbrx_dec = NULL; static volatile sig_atomic_t fb_stop = 0; static uint64_t g_fb_status_rx = 0; // сколько STATUS декодировано (для итога) +// Снимок последнего STATUS для main (этап 3). STATUS кумулятивен — это состояние +// RX целиком, а не событие, поэтому очередь не нужна: latest-wins. Пишет поток +// fb_rx, читает main; TX-буфером по-прежнему владеет ТОЛЬКО main (передавать из +// callback нельзя — это ядро 1 и чужой буфер). +static pthread_mutex_t g_fb_mtx = PTHREAD_MUTEX_INITIALIZER; +static fb_status_t g_fb_st; +static int g_fb_fresh = 0; // 1 = снимок не прочитан main'ом + +// Забрать свежий снимок. Возврат 1 — *out заполнен, 0 — нового STATUS не было. +static int fb_take(fb_status_t *out) { + int fresh; + pthread_mutex_lock(&g_fb_mtx); + fresh = g_fb_fresh; + if (fresh) { *out = g_fb_st; g_fb_fresh = 0; } + pthread_mutex_unlock(&g_fb_mtx); + return fresh; +} + // callback синхронизатора обратного канала: отсев по сигнатуре F0 55, RS+CRC -// декод, распаковка и лог. Этап 2 — без действий над состоянием передачи. +// декод, распаковка, лог и публикация снимка для main. static int fb_callback(unsigned char *hdr, int hdr_valid, unsigned char *pay, unsigned int pay_len, int pay_valid, framesyncstats_s stats, void *user) { @@ -54,8 +87,16 @@ static int fb_callback(unsigned char *hdr, int hdr_valid, if (fb_status_unpack(dec, dlen, &st) != 0) return 0; g_fb_status_rx++; - fprintf(stderr, "\n[FB] highest=%u rx_frames=%u overruns=%u nack=%u{", - st.highest_seq, st.rx_frames, st.overruns, st.nack_cnt); + pthread_mutex_lock(&g_fb_mtx); + g_fb_st = st; + g_fb_fresh = 1; + pthread_mutex_unlock(&g_fb_mtx); + + fprintf(stderr, "\n[FB] highest=%u rx_frames=%u overruns=%u%s%s nack=%u{", + st.highest_seq, st.rx_frames, st.overruns, + (st.flags & FB_FLAG_END_SEEN) ? " END" : "", + (st.flags & FB_FLAG_COMPLETE) ? " COMPLETE" : "", + st.nack_cnt); for (int i = 0; i < st.nack_cnt && i < 8; i++) fprintf(stderr, "%s%u", i ? "," : "", st.nack[i]); if (st.nack_cnt > 8) fprintf(stderr, ",…"); @@ -82,6 +123,42 @@ static void *fb_rx_thread(void *arg) { return NULL; } +// EOF-хендшейк (этап 3, ядро 0/main): шлём END с итогом передачи, пока B не +// подтвердит COMPLETE флагом в STATUS. Возврат 0 — подтверждено, 2 — таймаут или +// прерывание: на той стороне файл неполон. Честный отказ вместо тихой дыры +// (техдолг #6) — раньше A просто закрывал буфер и рапортовал успех. +static int end_phase(ofdmflexframegen fg, struct iio_buffer *buf, fec enc, + float amp, uint32_t total_seq, uint64_t total_bytes) { + fb_end_t e = { .total_seq = total_seq, .total_bytes = total_bytes }; + uint8_t pay[FB_END_LEN]; + size_t n = fb_end_pack(&e, pay); + uint32_t eseq = 0; // свой счётчик: RX ветвится по сигнатуре ДО + // seq-детектора данных → дыр от END не будет + int64_t t0 = now_ms(); + int sent = 0; + + fprintf(stderr, "[ARQ] END: кадров %u, байт %llu — жду COMPLETE (таймаут %d мс)\n", + total_seq, (unsigned long long)total_bytes, END_TIMEOUT_MS); + + while (!stop && now_ms() - t0 < END_TIMEOUT_MS) { + phy_tx_frame(fg, buf, BUF_SAMPLES, enc, SIG_END, eseq++, 0, pay, n, amp, 0); + sent++; + int64_t w = now_ms(); + while (!stop && now_ms() - w < END_PERIOD_MS) { // пауза между END — + fb_status_t st; // опрос снимка, не sleep + if (fb_take(&st) && (st.flags & FB_FLAG_COMPLETE)) { + fprintf(stderr, "[ARQ] COMPLETE подтверждён: END × %d за %lld мс\n", + sent, (long long)(now_ms() - t0)); + return 0; + } + usleep(5000); + } + } + fprintf(stderr, "[ARQ] таймаут COMPLETE: END × %d за %lld мс — приём НЕ подтверждён\n", + sent, (long long)(now_ms() - t0)); + return 2; +} + int main(int argc, char *argv[]) { // Инициализация параметров long long freq = DEFAULT_FREQ, rate = DEFAULT_RATE, bw = DEFAULT_BW; @@ -279,6 +356,13 @@ int main(int argc, char *argv[]) { fprintf(stderr, "\nПередача завершена: %llu кадров, %llu байт\n", (unsigned long long)frames, (unsigned long long)bytes_tx); + // EOF-хендшейк (этап 3). Только при живом обратном канале: без него COMPLETE + // услышать нечем и END-фаза всегда упиралась бы в таймаут. Симплекс идёт + // прежним путём — молча в дренаж, exit 0 (регрессия §12 не меняется). + int rc = 0; + if (fb_started) + rc = end_phase(fg, buf, enc, amp, seq, bytes_tx); + // Дренаж: выпихнуть последний кадр из kernel-буферов DMA перед закрытием. // Без паузы iio_buffer_destroy() срезает хвост файла в эфире (техдолг #5). { @@ -305,5 +389,5 @@ int main(int argc, char *argv[]) { if (g_fbrx_dec) fec_destroy(g_fbrx_dec); if (g_fbrx_buf) iio_buffer_destroy(g_fbrx_buf); iio_context_destroy(ctx); - return 0; + return rc; } \ No newline at end of file