Files
Pluto-SDR/src/transmitter.c
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

489 lines
27 KiB
C
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.

/**
* transmitter.c — OFDM-передатчик с RS(255,223) FEC для PlutoSDR
*
* ОПТИМАЛЬНАЯ КОМАНДА ЗАПУСКА (кабель + аттенюатор):
* cat test.bin | ./transmitter -f 915000000 -r 1920000 -b 1500000 -g -40 -a 0.2 -p 0 -c rs8 -u local:
*/
#include "common.h"
#include "group_fec.h"
#include "frame_tx.h"
#include "feedback.h" // приём STATUS обратного канала (этап 2, §10 п.7)
// Параметры передатчика по умолчанию
#define DEFAULT_GAIN -30.0 // Усиление передатчика: -30 дБ
#define DEFAULT_AMP 0.2f // Амплитуда сигнала
#define DEFAULT_PAUSE 2000 // Пауза между кадрами: 2000 мкс
#define BUF_SAMPLES 16384 // Размер буфера в отсчётах
#define MAX_PAYLOAD GF_CHUNK // 1024: размер чанка данных (== GF_CHUNK)
#define FB_RX_GAIN 50.0 // Усиление RX обратного канала (ручное), §10 п.7
// Внешняя переменная флага завершения (определена в common.c)
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-декодер. Слушает 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;
static ofdmflexframesync g_fbrx_fs = NULL;
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'ом
// 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;
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
// декод, распаковка, лог и публикация снимка для 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) {
(void)pay_valid; (void)stats; (void)user;
if (!hdr_valid) return 0;
if (hdr[0] != FB_SIG_0 || hdr[1] != FB_SIG_1) return 0; // не STATUS
static uint8_t dec[RS_ENC * 2]; // STATUS ≤ 1 RS-блок, запас на будущее
size_t dlen = 0;
if (!phy_rx_decode(hdr, pay, pay_len, g_fbrx_dec, dec, sizeof(dec), &dlen))
return 0; // RS/CRC не прошли — молча дропаем
fb_status_t st;
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;
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, ",…");
fprintf(stderr, "}\n");
return 0;
}
static void *fb_rx_thread(void *arg) {
(void)arg;
pin_to_cpu(1);
static liquid_float_complex conv[FB_RX_BUF_SAMPLES];
while (!fb_stop && !stop) {
ssize_t nb = iio_buffer_refill(g_fbrx_buf);
if (nb < 0) { if (fb_stop || stop) break; usleep(500); continue; }
int16_t *bb = (int16_t *)iio_buffer_start(g_fbrx_buf);
int ns = (iio_buffer_end(g_fbrx_buf) - iio_buffer_start(g_fbrx_buf))
/ (int)sizeof(int16_t) / 2;
if (ns > FB_RX_BUF_SAMPLES) ns = FB_RX_BUF_SAMPLES;
for (int i = 0; i < ns; i++)
conv[i] = ((float)bb[2*i] / 2048.0f) +
((float)bb[2*i+1] / 2048.0f) * _Complex_I;
ofdmflexframesync_execute(g_fbrx_fs, conv, ns);
}
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 включён (требует -F: без обратного канала
// NACK взять неоткуда). Работает и с group-FEC.
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, 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);
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 && !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 && !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));
return 2;
}
int main(int argc, char *argv[]) {
// Инициализация параметров
long long freq = DEFAULT_FREQ, rate = DEFAULT_RATE, bw = DEFAULT_BW;
double gain = DEFAULT_GAIN;
float amp = DEFAULT_AMP;
const char *uri = TX_URI_DEFAULT;
int pause_us = DEFAULT_PAUSE, use_fec = 1, group_fec = 0;
long long fb_freq = 0; // 0 = симплекс; иначе FDD, LO обратного RX
// Обработка аргументов командной строки
int opt;
while ((opt = getopt(argc, argv, "f:r:b:g:a:p:u:c:F:Gh")) != -1) {
switch (opt) {
case 'f': freq = atoll(optarg); break;
case 'r': rate = atoll(optarg); break;
case 'b': bw = atoll(optarg); break;
case 'g': gain = atof(optarg); break;
case 'a': amp = atof(optarg); break;
case 'p': pause_us = atoi(optarg); break;
case 'u': uri = optarg; break;
case 'c': use_fec = strcmp(optarg, "none") ? 1 : 0; break;
case 'F': fb_freq = atoll(optarg); break;
case 'G': group_fec = 1; break;
case 'h':
print_usage(argv[0], "ПЕРЕДАТЧИК");
fprintf(stderr, "\nСПЕЦИФИЧНЫЕ ОПЦИИ TX:\n");
fprintf(stderr, " -g УСИЛЕНИЕ Усиление в дБ (по умолчанию: %.0f)\n", DEFAULT_GAIN);
fprintf(stderr, " -a АМПЛИТ. Амплитуда сигнала (по умолчанию: %.2f)\n", DEFAULT_AMP);
fprintf(stderr, " -p ПАУЗА Пауза между кадрами в мкс (по умолчанию: %d)\n", DEFAULT_PAUSE);
fprintf(stderr, " -G Групповой стирающий FEC 16+2 (§10 п.6)\n");
fprintf(stderr, " -F ЧАСТОТА FDD: частота обратного RX в Гц (§10 п.7). Без -F — симплекс\n");
fprintf(stderr, "\nОПТИМАЛЬНЫЙ ЗАПУСК:\n");
fprintf(stderr, " cat test.bin | %s -f %lld -r %lld -b %lld -c rs8 > /dev/null\n",
argv[0], DEFAULT_FREQ, DEFAULT_RATE, DEFAULT_BW);
return 0;
default: return 1;
}
}
// Настройка обработчиков сигналов
setup_signal_handlers();
if (group_fec) gf_init();
// Вывод конфигурации
print_config("ПЕРЕДАТЧИК", freq, rate, bw, use_fec);
fprintf(stderr, "Усиление: %.0f дБ\n", gain);
fprintf(stderr, "Амплитуда: %.2f\n", amp);
fprintf(stderr, "Пауза: %d мкс\n", pause_us);
fprintf(stderr, "Group-FEC: %s\n", group_fec ? "16+2 (§10 п.6)" : "выкл");
fprintf(stderr, "Дуплекс: %s\n", fb_freq
? "FDD (обратный канал: приём STATUS на 868)" : "симплекс");
if (fb_freq) fprintf(stderr, "Обр. RX LO: %.1f МГц\n", fb_freq / 1e6);
fprintf(stderr, "URI: %s\n", uri);
fprintf(stderr, "═══════════════════════════════════════════════════\n");
// Инициализация PlutoSDR. С -F открываем оба cf-девайса и настраиваем оба
// LO (обратный RX пока только сконфигурирован; приём по нему — этап 2).
struct iio_context *ctx = NULL;
struct iio_device *phy = NULL, *tx = NULL, *rxfb = NULL;
if (fb_freq) {
if (pluto_init_fdd(uri, &ctx, &phy, &tx, &rxfb) != 0) return 1;
if (pluto_configure_fdd(phy, freq, fb_freq, rate, bw, gain, FB_RX_GAIN) != 0) {
LOG_ERROR("Настройка FDD-радиотракта не удалась");
iio_context_destroy(ctx);
return 1;
}
} else {
if (pluto_init(uri, &ctx, &phy, &tx, 1) != 0) return 1;
if (pluto_configure(phy, freq, rate, bw, gain, 1) != 0) {
LOG_ERROR("Настройка радиотракта не удалась");
iio_context_destroy(ctx);
return 1;
}
}
// Включение каналов передатчика
iio_channel_enable(iio_device_find_channel(tx, "voltage0", true));
iio_channel_enable(iio_device_find_channel(tx, "voltage1", true));
// Создание буфера передачи
struct iio_buffer *buf = iio_device_create_buffer(tx, BUF_SAMPLES, false);
if (!buf) {
LOG_ERROR("Ошибка создания буфера передачи");
iio_context_destroy(ctx);
return 1;
}
// Инициализация FEC и OFDM
fec enc = use_fec ? fec_create(LIQUID_FEC_RS_M8, NULL) : NULL;
ofdmflexframegenprops_s fgp;
ofdmflexframegenprops_init_default(&fgp);
fgp.check = LIQUID_CRC_32;
ofdmflexframegen fg = ofdmflexframegen_create(OFDM_M, OFDM_CP, OFDM_TAPER, NULL, &fgp);
// Наш заголовок 12 Б; по умолчанию liquid шлёт 8 — байты 8..11 (nblocks,
// last_block_bytes) без этого в эфир не уходят. RX выставляет то же.
ofdmflexframegen_set_header_len(fg, HDR_SIZE);
// Обратный канал (этап 2): поток fb_rx слушает STATUS на 868 (rxfb). Пока
// только логирует. Данные TX уводим на ядро 0, fb_rx — на ядро 1 (§affinity).
pthread_t fb_tid;
int fb_started = 0;
if (fb_freq) {
iio_channel_enable(iio_device_find_channel(rxfb, "voltage0", false));
iio_channel_enable(iio_device_find_channel(rxfb, "voltage1", false));
g_fbrx_buf = iio_device_create_buffer(rxfb, FB_RX_BUF_SAMPLES, false);
if (!g_fbrx_buf) {
LOG_ERROR("Буфер обратного канала не создан — STATUS не принимаются");
} else {
g_fbrx_fs = ofdmflexframesync_create(OFDM_M, OFDM_CP, OFDM_TAPER,
NULL, fb_callback, NULL);
ofdmflexframesync_set_header_len(g_fbrx_fs, HDR_SIZE);
g_fbrx_dec = fec_create(LIQUID_FEC_RS_M8, NULL);
pin_to_cpu(0); // TX-данные — ядро 0
pthread_create(&fb_tid, NULL, fb_rx_thread, NULL);
fb_started = 1;
// Этап 3c: ARQ работает и с group-FEC. Ретрансмит кадра старой группы
// больше не ломает сборку на RX (правило base < g.base в group_route),
// а дыры, закрытые паритетом, RX снимает с NACK сам — досылать их не
// придётся. GF и ARQ дополняют друг друга: GF гасит одиночные потери
// без задержки, ARQ добирает то, что паритету не по силам.
g_arq_on = 1;
fprintf(stderr, "Обратный канал: слушаю STATUS на %.1f МГц (ядро 1)\n",
fb_freq / 1e6);
fprintf(stderr, "ARQ: вкл (окно %d кадров, повтор не чаще %d мс)\n",
TXWIN, ARQ_RETX_MS);
}
}
// Счётчики статистики
uint64_t frames = 0, bytes_tx = 0;
uint32_t seq = 0;
time_t last = time(NULL);
fprintf(stderr, " Передатчик готов. Ожидание данных...\n\n");
// Основной цикл передачи. Legacy: кадр = чанк ≤1024 Б. Group-FEC: копим до
// 16 чанков, затем шлём k данных + 2 паритета (P, Q) с флагами в hdr[11].
if (!group_fec) {
while (!stop) {
uint8_t raw[MAX_PAYLOAD];
size_t n = fread(raw, 1, MAX_PAYLOAD, stdin);
if (!n) {
if (feof(stdin) || stop) break;
usleep(10000);
continue;
}
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);
fflush(stderr);
last = time(NULL);
}
}
} else {
static uint8_t gdata[GF_K][GF_CHUNK]; // накопитель группы
const uint8_t *gptr[GF_K];
uint16_t glen[GF_K];
int k = 0, done_reading = 0;
while (!stop && !done_reading) {
size_t r = fread(gdata[k], 1, GF_CHUNK, stdin);
if (r > 0) {
if (r < GF_CHUNK) memset(gdata[k] + r, 0, GF_CHUNK - r); // паддинг под GF
glen[k] = (uint16_t)r;
gptr[k] = gdata[k];
k++;
}
if (r < GF_CHUNK) done_reading = 1; // fread добирает до конца → короткий = EOF
if (k == GF_K || (done_reading && k > 0)) {
// k кадров данных: idx=0..k-1
for (int i = 0; i < k; i++) {
tx_send_data(fg, buf, enc, seq++, GF_FLAG_MODE | i,
gdata[i], glen[i], amp, pause_us);
frames++; bytes_tx += glen[i];
arq_service(fg, buf, enc, amp, pause_us, 2); // как в legacy
}
// паритеты P (idx=k) и Q (idx=k+1); мета = k + длина хвоста
uint8_t P[GF_CHUNK], Q[GF_CHUNK], par[GF_PAR_PAY];
gf_encode(gptr, k, P, Q);
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);
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);
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;
arq_service(fg, buf, enc, amp, pause_us, 2);
k = 0;
if (time(NULL) > last) {
fprintf(stderr, "\r📤 Передано: %llu кадров, %llu байт (group-FEC)",
(unsigned long long)frames, (unsigned long long)bytes_tx);
fflush(stderr);
last = time(NULL);
}
}
}
}
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, pause_us, seq, bytes_tx);
// Дренаж: выпихнуть последний кадр из kernel-буферов DMA перед закрытием.
// Без паузы iio_buffer_destroy() срезает хвост файла в эфире (техдолг #5).
{
int16_t *out = (int16_t *)iio_buffer_start(buf);
memset(out, 0, BUF_SAMPLES * 2 * sizeof(int16_t));
iio_buffer_push(buf);
usleep(100000); // ~4 kernel-буфера при 1.92 MSPS ≈ 35 мс, с запасом
}
// Остановить обратный канал: fb_rx выходит по fb_stop в течение одного
// refill (~8.5 мс на 16384 сэмпл @ 1.92 Msps), затем join.
if (fb_started) {
fb_stop = 1;
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);
}
// Освобождение ресурсов
if (enc) fec_destroy(enc);
ofdmflexframegen_destroy(fg);
iio_buffer_destroy(buf);
if (g_fbrx_fs) ofdmflexframesync_destroy(g_fbrx_fs);
if (g_fbrx_dec) fec_destroy(g_fbrx_dec);
if (g_fbrx_buf) iio_buffer_destroy(g_fbrx_buf);
iio_context_destroy(ctx);
return rc;
}