3б этап
This commit is contained in:
@@ -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
|
||||
|
||||
150
src/receiver.c
150
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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// Освобождение ресурсов
|
||||
|
||||
Reference in New Issue
Block a user