From 3b6a56e54c95d9534c5c7429c644179b4ac352c4 Mon Sep 17 00:00:00 2001 From: Maxim Date: Thu, 16 Jul 2026 14:30:17 +0300 Subject: [PATCH] =?UTF-8?q?3=D0=B1=20=D1=8D=D1=82=D0=B0=D0=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/test_arq.sh | 5 +- src/receiver.c | 150 ++++++++++++++++++++++++++++++++++++++++---- src/transmitter.c | 130 ++++++++++++++++++++++++++++++++------ 3 files changed, 252 insertions(+), 33 deletions(-) diff --git a/scripts/test_arq.sh b/scripts/test_arq.sh index f9be882..26b3797 100755 --- a/scripts/test_arq.sh +++ b/scripts/test_arq.sh @@ -111,6 +111,8 @@ 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 @@ -132,7 +134,8 @@ else echo " - md5 разошёлся при коде A=0 → COMPLETE поднят рано (проверь miss/next_out)." echo " - код A=2 при целом md5 → COMPLETE не долетел: подними FBGAIN к −10." echo " - размер 0 → RX не декодит: остаточный процесс на B или самоглушение." - echo " - на этапе 3a ретрансмитов ещё нет: любая потеря кадра → код A=2 ОЖИДАЕМО." + echo " - GROUPFEC=1: ARQ там пока выключен (этап 3c) → потеря кадра даёт код A=2." + echo " - 'NACK вне окна' > 0 на B → дыра пережила TX-окно (2048 кадров ≈ 21 с)." echo "--- хвост rx_arq.log (B) ---" "${SSH[@]}" "root@$B" "tail -8 /tmp/rx_arq.log" || true exit 1 diff --git a/src/receiver.c b/src/receiver.c index 249aefe..f85c21a 100644 --- a/src/receiver.c +++ b/src/receiver.c @@ -75,6 +75,9 @@ static uint64_t grp_recovered = 0, grp_failed = 0; // group-FEC: восст. static uint64_t sig_bad = 0; // hdr[0..1] не совпал с известной сигнатурой static uint32_t highest_seq = 0; // максимальный увиденный seq (не откатывается назад) static int seq_init = 0; +static uint32_t next_out = 0; // курсор вывода: следующий seq в stdout (этап 3b) +static uint64_t rb_over = 0; // кадров отброшено переполнением окна reorder +static int gf_seen = 0; // в эфире group-FEC (автодетект по hdr[11]) // Разделяемых на запись счётчиков нет. main читает их раз в секунду для // телеметрии без синхронизации: на 32-бит ARM возможен косметический «рваный» // показ 64-бит значения — на итоговую строку (печатается после join) не влияет. @@ -91,11 +94,29 @@ static int fec_on = 0; static uint32_t miss[MISS_CAP]; static int miss_cnt = 0; +static uint64_t miss_over = 0; // дыр не влезло в MISS_CAP + static void miss_add(uint32_t seq) { - if (miss_cnt >= MISS_CAP) return; // переполнение: старейшие дыры важнее + if (miss_cnt >= MISS_CAP) { miss_over++; return; } // старейшие дыры важнее miss[miss_cnt++] = seq; // highest_seq только растёт → массив сортирован } +// Вставка с сохранением порядка и дедупом — для seq, который МЕНЬШЕ фронта: +// битый кадр (rs_fail) пришёл, дырой в seq не выглядит, но дослать его надо. +// miss_add для такого seq сломал бы сортировку (в STATUS уходят FB_MAX_NACK +// первых элементов — они обязаны быть старейшими). +static void miss_add_sorted(uint32_t seq) { + int i; + for (i = 0; i < miss_cnt; i++) { + if (miss[i] == seq) return; // уже в списке + if (miss[i] > seq) break; // точка вставки + } + if (miss_cnt >= MISS_CAP) { miss_over++; return; } + memmove(&miss[i + 1], &miss[i], (size_t)(miss_cnt - i) * sizeof(miss[0])); + miss[i] = seq; + miss_cnt++; +} + static void miss_ack(uint32_t seq) { for (int i = 0; i < miss_cnt; i++) { if (miss[i] == seq) { @@ -143,15 +164,18 @@ 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). +// Файл собран? Legacy: курсор вывода дошёл до конца — это сильнее, чем «дыр +// нет»: next_out двигается только по непрерывному префиксу реально записанных +// кадров. Group-FEC: ARQ выключен (этап 3c), вывод идёт мимо кольца — критерий +// прежний, по фронту; ветка уйдёт вместе с гибридом GF+ARQ. // Держим флаг 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); + (miss_cnt == 0 && + (gf_seen ? (seq_init && highest_seq == end_total_seq - 1) + : next_out == end_total_seq)); if (!done) { complete_since_ms = 0; return; } int64_t t = now_ms(); @@ -235,6 +259,90 @@ static int callback(unsigned char *hdr, int hdr_valid, return 0; } +// --- Кольцо переупорядочивания вывода (поток C, этап 3b) ---------------- +// До ARQ кадры приходили строго по возрастанию seq и писались в stdout сразу. +// Ретрансмит ломает это: кадр 5 приедет после кадра 40 — прямая запись сдвинула +// бы данные и убила md5. Кольцо по seq % RB_CAP + курсор next_out: в stdout +// уходит только непрерывный префикс. Дыра держит вывод, пока ARQ её не закроет. +// len == 0 — «слот занят, но в файл ничего»: паритетный кадр GF тратит seq, но +// данных не несёт (нужно для этапа 3c, чтобы next_out доходил до total_seq). +#define RB_CAP 1024 // ~4 с эфира при 9.3 мс/кадр; TXWIN у A вдвое + // больше — досылка для любого seq в окне есть +typedef struct { + uint32_t seq; + uint16_t len; + uint8_t filled; + uint8_t data[GF_CHUNK]; +} rbslot_t; + +static rbslot_t rb[RB_CAP]; // ~1 МБ .bss (next_out/rb_over — выше, к ним + // обращается arq_check_complete до этой секции) + +// Выдать в stdout непрерывный префикс, начиная с next_out. +static void rb_drain(void) { + int wrote = 0; + for (;;) { + rbslot_t *s = &rb[next_out % RB_CAP]; + if (!s->filled || s->seq != next_out) break; // дыра — дальше нельзя + if (s->len) { + fwrite(s->data, 1, s->len, stdout); + total_bytes += s->len; + crc_final++; + } + s->filled = 0; + next_out++; + wrote = 1; + } + if (wrote) fflush(stdout); +} + +// Принудительно продвинуть курсор до seq `to`, схлопывая дыры: то, что есть — +// в stdout, чего нет — пропало навсегда (прежняя семантика «потеря = сдвиг»). +static void rb_force_to(uint32_t to) { + while (next_out < to) { + rbslot_t *s = &rb[next_out % RB_CAP]; + if (s->filled && s->seq == next_out && s->len) { + fwrite(s->data, 1, s->len, stdout); + total_bytes += s->len; + crc_final++; + } + s->filled = 0; + next_out++; + } + fflush(stdout); +} + +static void rb_insert(uint32_t seq, const uint8_t *d, size_t len) { + if (len > GF_CHUNK) len = GF_CHUNK; // защита в глубину: len берётся из + // hdr, слот кольца — ровно GF_CHUNK + if (seq < next_out) return; // уже выведен: дубль ретрансмита + if (seq - next_out >= RB_CAP) { + // Дыра в голове старше окна. С ARQ это не тупик: дропаем новый кадр и + // возвращаем его seq в NACK — A дошлёт, когда голова разъедется. Без + // обратного канала досылать некому → схлопываем окно вперёд. + if (g_fb_fg) { + rb_over++; + miss_add_sorted(seq); + return; + } + rb_force_to(seq - RB_CAP + 1); + } + rbslot_t *s = &rb[seq % RB_CAP]; + s->seq = seq; + s->len = (uint16_t)len; + s->filled = 1; + if (len) memcpy(s->data, d, len); +} + +// Досдать всё, что осталось в кольце (поток C на выходе): приём кончился, ARQ +// уже не поможет — дыры схлопываются, хвост уходит в файл. +static void rb_flush_all(void) { + uint32_t last = next_out; + for (int i = 0; i < RB_CAP; i++) + if (rb[i].filled && rb[i].seq >= last) last = rb[i].seq + 1; + rb_force_to(last); +} + // --- Групповой стирающий FEC (поток C, README §12.7) -------------------- // Собирает группу из 16 кадров данных + 2 паритетных (P,Q). Пропавший на эфире // или битый по CRC кадр = стирание; ≤2 стираний на группу закрываются gf_recover. @@ -400,6 +508,12 @@ static void process_frame(fq_slot_t *fs) { if (!seq_init) { seq_init = 1; highest_seq = seq; + if (seq > 0) { // приём начался не с нуля: кадры до + for (uint32_t m = 0; m < seq; m++) miss_add(m); // первого пойманного + lost += seq; // потеряны, а дырой не выглядят — + fprintf(stderr, "\n[ПОТЕРЯ] старт с seq %u, дыра %u кадров в начале\n", + seq, seq); // им не предшествовал никакой seq + } } else if (seq > highest_seq + 1) { // Пропавшие seq → NACK-набор обратного канала (этап 2) + счётчик потерь. for (uint32_t m = highest_seq + 1; m < seq; m++) miss_add(m); @@ -411,7 +525,8 @@ static void process_frame(fq_slot_t *fs) { highest_seq = seq; } // seq ≤ highest_seq: старый/повторный кадр — highest_seq не трогаем. - miss_ack(seq); // кадр пришёл (в т.ч. ретрансмит старого seq) — снять из NACK + // Снятие seq с NACK — НЕ здесь, а после декода (см. метку route): факт + // приёма кадра ещё не значит, что он целый. uint16_t original_len = (hdr[6] << 8) | hdr[7]; uint8_t last_block_bytes = hdr[10]; @@ -462,15 +577,21 @@ static void process_frame(fq_slot_t *fs) { } route: + // Учёт NACK — ТОЛЬКО после декода. Раньше seq снимался с NACK по факту + // приёма кадра, и битый кадр (rs_fail) выпадал из списка навсегда: ARQ его + // уже не дослал бы, а RX считал бы файл собранным. Битый кадр — такая же + // дыра, как непришедший, просто обнаруженная не детектором seq. + if (ok) miss_ack(seq); + else miss_add_sorted(seq); + if (flags & GF_FLAG_MODE) { + gf_seen = 1; group_route(flags, seq, dec, dec_len, ok); } else { if (g.active) group_flush(); // TX не мешает режимы, но подстрахуемся if (ok) { - fwrite(dec, 1, dec_len, stdout); - fflush(stdout); - total_bytes += dec_len; - crc_final++; + rb_insert(seq, dec, dec_len); // порядок восстанавливает кольцо: + rb_drain(); // ретрансмит приходит вне очереди } } } @@ -593,7 +714,8 @@ static void *fec_thread(void *arg) { maybe_send_status(); // под нагрузкой — период гейтит частоту } group_flush(); // дописать последнюю группу файла (нет кадра-триггера) - return NULL; + rb_flush_all(); // и хвост кольца: приём кончился, дыры схлопываем — + return NULL; // досылать их уже некому } int main(int argc, char *argv[]) { @@ -767,9 +889,11 @@ int main(int argc, char *argv[]) { (unsigned long long)crc_final, (unsigned long long)total_bytes); 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, + fprintf(stderr, "ARQ: END принято: %llu | ожидалось кадров: %u | выведено seq: %u | не хватает: %d | COMPLETE: %s\n", + (unsigned long long)end_rx, end_total_seq, next_out, miss_cnt, complete_since_ms ? "да" : "НЕТ"); + fprintf(stderr, "ARQ: окно reorder переполнено: %llu | NACK переполнен: %llu\n", + (unsigned long long)rb_over, (unsigned long long)miss_over); } if (decoder) fec_destroy(decoder); diff --git a/src/transmitter.c b/src/transmitter.c index 8ee8f60..c111e1a 100644 --- a/src/transmitter.c +++ b/src/transmitter.c @@ -60,6 +60,11 @@ 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'ом +// COMPLETE — отдельная защёлка, а не поле снимка: снимок «съедает» arq_service, +// и подтверждение, пришедшее в том же STATUS, потерялось бы. Флаг монотонный — +// once complete, always complete. +static volatile sig_atomic_t g_fb_complete = 0; + // Забрать свежий снимок. Возврат 1 — *out заполнен, 0 — нового STATUS не было. static int fb_take(fb_status_t *out) { int fresh; @@ -87,6 +92,7 @@ static int fb_callback(unsigned char *hdr, int hdr_valid, if (fb_status_unpack(dec, dlen, &st) != 0) return 0; g_fb_status_rx++; + if (st.flags & FB_FLAG_COMPLETE) g_fb_complete = 1; pthread_mutex_lock(&g_fb_mtx); g_fb_st = st; g_fb_fresh = 1; @@ -123,12 +129,83 @@ static void *fb_rx_thread(void *arg) { return NULL; } +// --- TX-окно и ARQ (этап 3b) -------------------------------------------- +// Копия каждого переданного кадра: без неё дослать нечего — stdin уже прочитан +// и не перематывается. Кольцо по seq % TXWIN, хранит СЫРОЙ payload (до RS/OFDM): +// phy_tx_frame детерминирован, поэтому повтор уходит в эфир байт-в-байт. +// TXWIN(2048) > RB_CAP(1024) приёмника — кадр, который B ещё может принять в +// своё окно переупорядочивания, гарантированно есть у нас в окне. +#define TXWIN 2048 +#define ARQ_RETX_MS 400 // per-seq пауза между повторами одного seq. +// 400 = петля «потеря → детект (~10 мс) → слот STATUS (~50 мс) → доставка при +// p≈0.5 (~100–300 мс) → ретрансмит (~30 мс) → подтверждение (~150–250 мс)». +// Меньше — дубли жгут эфир (кадр 9.3 мс); больше — дыра дольше держит окно B. + +typedef struct { + uint32_t seq; + int64_t last_ms; // когда seq последний раз уходил в эфир + uint16_t len; + uint8_t hdr11; // флаги group-FEC (0 = legacy) + uint8_t valid; + uint8_t pay[GF_PAR_PAY]; // 1026: паритетный кадр GF — самый большой +} txslot_t; + +static txslot_t txwin[TXWIN]; // ~2.1 МБ .bss (из 1 ГБ платы) +static int g_arq_on = 0; // ARQ включён (FDD + legacy; GF — этап 3c) +static uint64_t g_retx = 0; // отправлено ретрансмитов +static uint64_t g_retx_old = 0; // NACK на seq, вышедший из окна (≈21 с эфира) + +// Последний известный список дыр B. Живёт между снимками: пока не пришёл новый +// STATUS, дожимаем прежний — от дублей защищает ARQ_RETX_MS, а не свежесть. +static uint32_t g_nack[FB_MAX_NACK]; +static int g_nack_cnt = 0; + +// Передать НОВЫЙ кадр данных/паритета, попутно сохранив его в окне. +static void tx_send_data(ofdmflexframegen fg, struct iio_buffer *buf, fec enc, + uint32_t seq, uint8_t hdr11, const uint8_t *pl, size_t n, + float amp, int pause_us) { + txslot_t *s = &txwin[seq % TXWIN]; + s->seq = seq; s->hdr11 = hdr11; s->len = (uint16_t)n; s->valid = 1; + memcpy(s->pay, pl, n); + phy_tx_frame(fg, buf, BUF_SAMPLES, enc, SIG_DATA, seq, hdr11, pl, n, amp, pause_us); + s->last_ms = now_ms(); // после отправки: паузу считаем от конца кадра +} + +// Досылка по NACK. budget — потолок ретрансмитов за один вызов: очередь дыр +// не должна вытеснять новые данные (голодание фронта). Вызывается ТОЛЬКО из +// main — TX-буфер один и владеет им этот поток. +static void arq_service(ofdmflexframegen fg, struct iio_buffer *buf, fec enc, + float amp, int pause_us, int budget) { + if (!g_arq_on) return; + fb_status_t st; + if (fb_take(&st)) { // свежий снимок → обновить список дыр + g_nack_cnt = st.nack_cnt < FB_MAX_NACK ? st.nack_cnt : FB_MAX_NACK; + memcpy(g_nack, st.nack, (size_t)g_nack_cnt * sizeof(g_nack[0])); + } + int64_t t = now_ms(); + for (int i = 0; i < g_nack_cnt && budget > 0; i++) { + uint32_t seq = g_nack[i]; + txslot_t *s = &txwin[seq % TXWIN]; + if (!s->valid || s->seq != seq) { // окно провернулось — дослать нечем + g_retx_old++; // → END-таймаут, честный отказ + continue; + } + if (t - s->last_ms < ARQ_RETX_MS) continue; // повтор ещё в полёте + phy_tx_frame(fg, buf, BUF_SAMPLES, enc, SIG_DATA, seq, s->hdr11, + s->pay, s->len, amp, pause_us); + t = now_ms(); + s->last_ms = t; + g_retx++; + budget--; + } +} + // 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) { + float amp, int pause_us, 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); @@ -140,19 +217,20 @@ static int end_phase(ofdmflexframegen fg, struct iio_buffer *buf, fec enc, 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) { + while (!stop && !g_fb_complete && now_ms() - t0 < END_TIMEOUT_MS) { phy_tx_frame(fg, buf, BUF_SAMPLES, enc, SIG_END, eseq++, 0, pay, n, amp, 0); sent++; + // Бюджет выше, чем в data-фазе: новых данных больше нет, эфир свободен, + // и закрыть остаток дыр надо именно сейчас — таймаут тикает. + arq_service(fg, buf, enc, amp, pause_us, 4); 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); - } + while (!stop && !g_fb_complete && now_ms() - w < END_PERIOD_MS) + usleep(5000); // пауза между END: опрос защёлки, не sleep + } + if (g_fb_complete) { + fprintf(stderr, "[ARQ] COMPLETE подтверждён: END × %d, ретрансмитов %llu, %lld мс\n", + sent, (unsigned long long)g_retx, (long long)(now_ms() - t0)); + return 0; } fprintf(stderr, "[ARQ] таймаут COMPLETE: END × %d за %lld мс — приём НЕ подтверждён\n", sent, (long long)(now_ms() - t0)); @@ -275,8 +353,16 @@ int main(int argc, char *argv[]) { pin_to_cpu(0); // TX-данные — ядро 0 pthread_create(&fb_tid, NULL, fb_rx_thread, NULL); fb_started = 1; + // ARQ пока только на legacy-пути: ретрансмит кадра старой группы + // ломает сборку group_route на RX (правило base < g.base — этап 3c). + g_arq_on = !group_fec; fprintf(stderr, "Обратный канал: слушаю STATUS на %.1f МГц (ядро 1)\n", fb_freq / 1e6); + if (g_arq_on) + fprintf(stderr, "ARQ: вкл (окно %d кадров, повтор не чаще %d мс)\n", + TXWIN, ARQ_RETX_MS); + else + fprintf(stderr, "ARQ: выкл (group-FEC — этап 3c)\n"); } } @@ -298,8 +384,12 @@ int main(int argc, char *argv[]) { usleep(10000); continue; } - phy_tx_frame(fg, buf, BUF_SAMPLES, enc, SIG_DATA, seq++, 0, raw, n, amp, pause_us); + tx_send_data(fg, buf, enc, seq++, 0, raw, n, amp, pause_us); frames++; bytes_tx += n; + // Досылка вперемешку с новыми данными: дыра закрывается, пока B ещё + // держит её в окне переупорядочивания. Бюджет 2 — потолок, чтобы + // очередь NACK не вытеснила фронт передачи. + arq_service(fg, buf, enc, amp, pause_us, 2); if (time(NULL) > last) { fprintf(stderr, "\r📤 Передано: %llu кадров, %llu байт", (unsigned long long)frames, (unsigned long long)bytes_tx); @@ -326,8 +416,8 @@ int main(int argc, char *argv[]) { if (k == GF_K || (done_reading && k > 0)) { // k кадров данных: idx=0..k-1 for (int i = 0; i < k; i++) { - phy_tx_frame(fg, buf, BUF_SAMPLES, enc, SIG_DATA, seq++, GF_FLAG_MODE | i, - gdata[i], glen[i], amp, pause_us); + tx_send_data(fg, buf, enc, seq++, GF_FLAG_MODE | i, + gdata[i], glen[i], amp, pause_us); frames++; bytes_tx += glen[i]; } // паритеты P (idx=k) и Q (idx=k+1); мета = k′ + длина хвоста @@ -336,11 +426,11 @@ int main(int argc, char *argv[]) { uint16_t meta = GF_META_PACK(k, glen[k - 1]); par[0] = meta >> 8; par[1] = meta & 0xFF; memcpy(par + GF_META, P, GF_CHUNK); - phy_tx_frame(fg, buf, BUF_SAMPLES, enc, SIG_DATA, seq++, GF_FLAG_MODE | GF_FLAG_PAR | k, - par, GF_PAR_PAY, amp, pause_us); + tx_send_data(fg, buf, enc, seq++, GF_FLAG_MODE | GF_FLAG_PAR | k, + par, GF_PAR_PAY, amp, pause_us); memcpy(par + GF_META, Q, GF_CHUNK); - phy_tx_frame(fg, buf, BUF_SAMPLES, enc, SIG_DATA, seq++, GF_FLAG_MODE | GF_FLAG_PAR | GF_FLAG_Q | (k + 1), - par, GF_PAR_PAY, amp, pause_us); + tx_send_data(fg, buf, enc, seq++, GF_FLAG_MODE | GF_FLAG_PAR | GF_FLAG_Q | (k + 1), + par, GF_PAR_PAY, amp, pause_us); frames += 2; k = 0; if (time(NULL) > last) { @@ -361,7 +451,7 @@ int main(int argc, char *argv[]) { // прежним путём — молча в дренаж, exit 0 (регрессия §12 не меняется). int rc = 0; if (fb_started) - rc = end_phase(fg, buf, enc, amp, seq, bytes_tx); + rc = end_phase(fg, buf, enc, amp, pause_us, seq, bytes_tx); // Дренаж: выпихнуть последний кадр из kernel-буферов DMA перед закрытием. // Без паузы iio_buffer_destroy() срезает хвост файла в эфире (техдолг #5). @@ -379,6 +469,8 @@ int main(int argc, char *argv[]) { pthread_join(fb_tid, NULL); fprintf(stderr, "Обратный канал: принято STATUS: %llu\n", (unsigned long long)g_fb_status_rx); + fprintf(stderr, "ARQ: ретрансмитов: %llu | NACK вне окна: %llu\n", + (unsigned long long)g_retx, (unsigned long long)g_retx_old); } // Освобождение ресурсов