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.
489 lines
27 KiB
C
489 lines
27 KiB
C
/**
|
||
* 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 (~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 включён (требует -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;
|
||
}
|