3б этап

This commit is contained in:
Maxim
2026-07-16 14:30:17 +03:00
parent b9846fcbdc
commit 3b6a56e54c
3 changed files with 252 additions and 33 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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 (~100300 мс) → ретрансмит (~30 мс) → подтверждение (~150250 мс)».
// Меньше — дубли жгут эфир (кадр 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);
}
// Освобождение ресурсов