Этап 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.
This commit is contained in:
@@ -42,6 +42,8 @@ PHY: OFDM через liquid-dsp 1.8.0 + FFTW/NEON, FEC RS(255,223).
|
||||
bit5 GF_Q (Q иначе P), bits4-0 idx кадра в группе (0..17). 0 = legacy-кадр.
|
||||
Group-FEC 16+2: 16 кадров данных + P(⊕) + Q(Σgⁱ) над GF(256), закрывает ≤2
|
||||
стёртых кадра/группу без обратного канала (src/group_fec.c). RX автодетект.
|
||||
Сигнатура F0 E7 — END (этап 3): payload 12 Б, total_seq u32 + total_bytes u64,
|
||||
маркер конца передачи для хендшейка END/COMPLETE (feedback.h, §12.9).
|
||||
КРИТИЧНО: liquid по умолчанию передаёт 8 Б заголовка. Обязательно
|
||||
`ofdmflexframegen_set_header_len(fg,12)` и
|
||||
`ofdmflexframesync_set_header_len(fs,12)` на обеих сторонах.
|
||||
@@ -55,7 +57,8 @@ TX LO: канал `altvoltage1` (output); RX LO: `altvoltage0` (output).
|
||||
#1 header 12Б (см. выше) · #2 лимит nblocks ≤ 18, не 100 (стек recovered[4096])
|
||||
· #3 fec_decode() liquid не сообщает об ошибках — rs_saved фиктивен
|
||||
· #4 внешний NCO в receiver вреден, убрать · #5 TX пушит лишние сэмплы
|
||||
· #6 нет детектора потерь по seq · #7 reciever.c → receiver.c
|
||||
· #6 ✅ закрыт на FDD: ARQ (NACK→ретрансмит) + END/COMPLETE, §12.9. На симплексе
|
||||
ARQ невозможен — там детектор [ПОТЕРЯ] + group-FEC · #7 reciever.c → receiver.c
|
||||
|
||||
## Команды
|
||||
Сборка: `make` · Деплой: `make deploy` (scp -O! sftp на плате нет)
|
||||
|
||||
56
README.md
56
README.md
@@ -277,7 +277,7 @@ ssh root@192.168.2.1 '/tmp/ofdm_bench 1.92'
|
||||
| 3 | `fec_decode()` liquid не сообщает о неисправимых RS-блоках → счётчик `rs_saved` фиктивен. Достоверный критерий — только CRC/md5 поверх данных | ✅ исправлено: CRC32 поверх данных до RS на TX, сверка после декода на RX; `rs_saved`/`rs_fail` достоверны (подтверждено loopback'ом, §12.2) |
|
||||
| 4 | Внешний NCO-CFO в receiver: `set_phase(0)` на границах буферов рвёт фазу; ofdmflexframesync и так компенсирует CFO сам | ✅ исправлено: внешний NCO удалён, CFO оставлен только как телеметрия |
|
||||
| 5 | TX пушит весь буфер 16384 сэмпла при кадре ~10300 → ~35% эфира впустую + паузы `-p` | ✅ исправлено: `iio_buffer_push_partial(idx)` + дренаж хвоста DMA перед закрытием |
|
||||
| 6 | Нет ARQ: потерянный кадр = молчаливая дыра в файле. seq в заголовке есть, но RX его игнорирует | ⚙️ частично: детектор пропусков seq (лог `[ПОТЕРЯ]` + счётчик) + **групповой стирающий FEC 16+2 закрывает ≤2 потери/группу на симплексе без обратного канала (§12.7, п.6)**; полный ARQ — на дуплексе (п.7) |
|
||||
| 6 | Нет ARQ: потерянный кадр = молчаливая дыра в файле. seq в заголовке есть, но RX его игнорирует | ✅ исправлено на FDD (§12.9, §10 п.7): детектор пропусков seq → NACK в STATUS → селективный ретрансмит из окна TX 2048 кадров + кольцо переупорядочивания на RX + хендшейк END/COMPLETE (хвостовые потери). 10 МБ байт-в-байт, A возвращает 0 только по подтверждённому COMPLETE. **На симплексе (без `-F`) ARQ невозможен** — там остаются детектор `[ПОТЕРЯ]` и групповой FEC 16+2 (≤2 потери/группу, §12.7, п.6) |
|
||||
|
||||
## 10. Дорожная карта
|
||||
|
||||
@@ -303,7 +303,15 @@ ssh root@192.168.2.1 '/tmp/ofdm_bench 1.92'
|
||||
байт-в-байт на рабочей точке `-p 4000`. Защита информации (лёгкий AEAD,
|
||||
+16 Б/кадр, <1% CPU) — отложена, требования ОКР «R-Link» нет; формат
|
||||
с резервом под неё.
|
||||
7. FDD 868/915 двусторонняя + ARQ с обратным каналом + меры из docs/NOTES.md
|
||||
7. ✅ **FDD 868/915 + обратный канал + ARQ** (§12.8, §12.9) — техдолг #6 закрыт.
|
||||
Этап 1: две несущие, A TX915/RX868, B зеркально. Этап 2: обратный канал
|
||||
STATUS (кумулятивный снимок RX: highest_seq + NACK-список), gate пройден —
|
||||
PER форварда не деградирует от обратного TX. Этап 3: EOF-хендшейк END/COMPLETE
|
||||
(хвостовые потери молчаливы — END несёт итог передачи) + селективный
|
||||
ретрансмит по NACK (окно TX 2048 кадров, повтор не чаще 400 мс) + кольцо
|
||||
переупорядочивания на RX (1024 кадра). **Побайтовый лосслесс на 10 МБ:
|
||||
44 потери вытянуты 45 ретрансмитами, md5 ==.** ARQ и group-FEC работают
|
||||
вместе: паритет гасит одиночные потери без задержки, ARQ добирает остальное.
|
||||
8. UDP-туннель поверх линка; затем web-настройка (libmicrohttpd)
|
||||
9. Видео (raw UDP, пакеты ≤1472 Б) — при устойчивом PER
|
||||
|
||||
@@ -580,6 +588,50 @@ TX-бёрстов (duty 57% при `-p 4000`), STATUS выживает в пау
|
||||
И PER₁≈PER₀), fallback TDD не требуется. Разбор — NOTES §2.2. Дальше — этап 3:
|
||||
селективный ретрансмит по NACK + END/COMPLETE-хендшейк.
|
||||
|
||||
### 12.9 ARQ: END/COMPLETE + селективный ретрансмит (§10 п.7, этап 3) — 2026-07-16
|
||||
|
||||
Закрытие техдолга #6. Три подэтапа, каждый принят на железе отдельно.
|
||||
|
||||
**Механизм.** (а) **END** (кадр `F0 E7`, payload 12 Б: total_seq + total_bytes):
|
||||
хвостовые потери молчаливы — у последних кадров нет следующего seq, по которому
|
||||
детектор увидел бы дыру. A шлёт END каждые 100 мс после EOF, пока не услышит
|
||||
COMPLETE (таймаут 10 с → **exit 2**). (б) **Окно TX** `txwin[2048]` на A хранит
|
||||
сырой payload: `phy_tx_frame` детерминирован, повтор уходит байт-в-байт.
|
||||
(в) **NACK** едут в кумулятивном STATUS (50 старейших seq); ретрансмит
|
||||
per-seq не чаще **400 мс** — расчёт петли потеря→STATUS→повтор→подтверждение при
|
||||
50% доставки STATUS (§12.8). (г) **Кольцо переупорядочивания** `rb[1024]` на B:
|
||||
ретрансмит приезжает вне очереди, в stdout уходит только непрерывный префикс
|
||||
(курсор `next_out`). COMPLETE ⇔ `next_out == total_seq`.
|
||||
|
||||
**Результаты** (кабель+аттенюатор, `TXGAIN −30`, `FBGAIN −15`, `-p 4000`):
|
||||
|
||||
| Прогон | Потери seq | Ретрансмитов | Итог |
|
||||
|---|---|---|---|
|
||||
| 1 МБ, GROUPFEC=0 | 4 / 1024 | 4 | md5 ==, COMPLETE за 88 мс, END × 1 |
|
||||
| 10 МБ, GROUPFEC=0 | 44 / 10240 | 45 | md5 ==, `NACK вне окна: 0`, STATUS 93% |
|
||||
| негатив, FBGAIN −89 | — | — | A честно **exit 2** за 10 с (раньше рапортовал успех) |
|
||||
|
||||
Окно TX на 10 МБ провернулось ~5 раз без единого «NACK вне окна», кольцо RX не
|
||||
переполнялось ни разу — оба расчётных риска закрыты замером. Доставка STATUS в
|
||||
END-фазе растёт до ~93% (эфир пуст, самоглушение A прекращается) — плато §12.8
|
||||
на хендшейк не влияет, как и предполагалось.
|
||||
|
||||
**Клифф канала.** Стресс `TXGAIN −38/−40/−45` бесполезен: канала там нет вовсе —
|
||||
`Заголовков 3322, CRC 0` (заголовок BPSK живёт, payload QPSK+RS падает первым).
|
||||
Переход −30 (PER 0.4%) → −38 (100%) резкий, промежуточной точки на кабеле почти
|
||||
нет. Поведение в обрыве корректное: A упёрся в расчётный потолок rate-limit
|
||||
(2246 ретрансмитов ≈ 50 × 18 с / 0.4 с — эфир не залит дублями), B не поднял
|
||||
COMPLETE, A вышел с 2. Настоящий PER-стресс — свипом в §12.4, не здесь.
|
||||
|
||||
**Симплекс идёт мимо кольца.** Без `-F` ретрансмитов нет, seq только растёт —
|
||||
переупорядочивать нечего, а дыру закрыть некому: кольцо лишь задержало бы вывод
|
||||
до EOF. Отсюда ветка `g_fb_fg ? rb : stdout` в `process_frame`/`group_flush`.
|
||||
|
||||
**Гибрид с group-FEC.** Паритет гасит одиночные потери без задержки (RX снимает
|
||||
восстановленные seq с NACK — ретрансмит не нужен), ARQ добирает то, что паритету
|
||||
не по силам (>2 стираний/группу). `grp_failed > 0` при целом md5 — норма: группа
|
||||
не собралась, но ARQ её дозакрыл.
|
||||
|
||||
---
|
||||
*Ввод в строй: 14–15-07-2026. Замеры CPU и базовые решения — 10-07-2026.
|
||||
Прошивка v0.38, liquid-dsp 1.8.0.*
|
||||
@@ -109,6 +109,44 @@ PER₁≈PER₀ — **выполнен, этап 5 (fallback TDD, цена ~8% t
|
||||
FBGAIN=0 (они и дали три точки плато). Гипотеза компрессии входа при RSSI
|
||||
−33…−35 (см. §2.1) остаётся непроверенной, но gate она не блокирует.
|
||||
|
||||
### 2.3. Находки этапа 3 (ARQ, 2026-07-16)
|
||||
|
||||
**Клифф канала на кабеле — свипу PER здесь не место.** `TXGAIN −30` даёт
|
||||
PER 0.4%, `−38` — уже `CRC: 0` при живых `Заголовках: 3322`. Порог заголовка и
|
||||
порога payload разнесены: заголовок ofdmflexframe идёт BPSK с собственным FEC,
|
||||
payload — QPSK+RS(255,223), и падает он первым. Практический смысл: **живой
|
||||
счётчик «Заголовков» при нулевом CRC — это не «плохой канал», а его отсутствие**;
|
||||
диагностику по такому прогону строить нельзя, ARQ там нечего вытягивать.
|
||||
Аттенюатор даёт слишком грубый шаг — карта PER снята свипом в README §12.4.
|
||||
|
||||
**Направления несимметричны.** На одном и том же кабеле A→B даёт `RS битых: 0`,
|
||||
B→A — `RS битых: 20…28` на 1024 кадрах. Разброс устойчив между прогонами, то
|
||||
есть это не статистика, а свойство пары плат/тракта. Для ARQ безразлично
|
||||
(ретрансмит закроет), но при выборе направления для чувствительного трафика и
|
||||
при трактовке PER-замеров это надо помнить: цифры одного направления не
|
||||
переносятся на другое.
|
||||
|
||||
**Ловушка «битый кадр выпадает из NACK».** `miss_ack(seq)` по факту приёма кадра
|
||||
(до декода) — тихо ломает ARQ: кадр, не прошедший RS/CRC, снимался с NACK
|
||||
навсегда, A его не досылал, а RX считал файл собранным → **A рапортовал успех
|
||||
с битым md5**. Учёт NACK обязан идти ПОСЛЕ декода: `ok → miss_ack`,
|
||||
`!ok → miss_add_sorted`. Битый кадр — такая же дыра, как непришедший, просто
|
||||
обнаруженная не детектором seq. Не выстрелило на приёмке 3a лишь потому, что
|
||||
там случились `RS битых: 0`.
|
||||
|
||||
**Ловушка «COMPLETE съеден снимком».** STATUS на A забирается снимком
|
||||
(latest-wins, `fb_take` гасит флаг свежести). Если бы COMPLETE читался из
|
||||
снимка, `arq_service` съел бы тот самый STATUS, в котором он приехал, и A ушёл
|
||||
бы в 10-секундный таймаут на ЦЕЛОМ файле. COMPLETE поэтому — отдельная
|
||||
монотонная защёлка, взводимая прямо в `fb_callback`.
|
||||
|
||||
**Переполнение MISS_CAP — тупик без пересборки.** `miss_add` при переполнении
|
||||
молча теряет seq, и вернуть его в NACK уже некому: детектор дыр по нему второй
|
||||
раз не сработает (`highest_seq` ушёл вперёд). В тяжёлом канале это заклинивало
|
||||
приём насмерть. Лечится тем, что кольцо reorder знает точно, каких кадров нет:
|
||||
`miss_rebuild()` пересобирает NACK из кольца на каждом END (A шлёт их 10 Гц до
|
||||
самого COMPLETE, скан ≤1024 — сотые доли ядра).
|
||||
|
||||
## 3. Регуляторика (868/915 МГц в регионе ETSI)
|
||||
|
||||
- **868 МГц** — SRD-диапазон: ограничения на полосу канала (сотни кГц),
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#
|
||||
# Использование: scripts/test_arq.sh [ipA] [ipB] [размер_КБ]
|
||||
# SIZE_KB=10240 — длинный прогон (или третьим позиционным аргументом)
|
||||
# TXGAIN=-45 — стресс: высокий PER, ARQ обязан вытянуть md5
|
||||
# TXGAIN=-33 — стресс: высокий PER, ARQ обязан вытянуть md5. Клифф на
|
||||
# кабеле резкий: −30 ≈ PER 0.4%, −38 и глубже — CRC 0
|
||||
# (канала нет вовсе, проверять там нечего)
|
||||
# NEGATIVE=1 FBGAIN=-89 — обратный канал заглушён: успех = A НЕ подтвердил
|
||||
# FBGAIN (деф. −15) — середина рабочего окна −20…−10 (README §12.8)
|
||||
# FBFREQ, PAUSE, TXGAIN, FEC, GROUPFEC — как в test_fdd_fwd.sh
|
||||
@@ -134,7 +136,8 @@ else
|
||||
echo " - md5 разошёлся при коде A=0 → COMPLETE поднят рано (проверь miss/next_out)."
|
||||
echo " - код A=2 при целом md5 → COMPLETE не долетел: подними FBGAIN к −10."
|
||||
echo " - размер 0 → RX не декодит: остаточный процесс на B или самоглушение."
|
||||
echo " - GROUPFEC=1: ARQ там пока выключен (этап 3c) → потеря кадра даёт код A=2."
|
||||
echo " - CRC: 0 при живых 'Заголовках' → канал за клиффом (TXGAIN ≤ −38 на кабеле):"
|
||||
echo " заголовок BPSK ещё держится, payload QPSK — нет. Это не стресс ARQ, а обрыв."
|
||||
echo " - 'NACK вне окна' > 0 на B → дыра пережила TX-окно (2048 кадров ≈ 21 с)."
|
||||
echo "--- хвост rx_arq.log (B) ---"
|
||||
"${SSH[@]}" "root@$B" "tail -8 /tmp/rx_arq.log" || true
|
||||
|
||||
146
src/receiver.c
146
src/receiver.c
@@ -77,7 +77,7 @@ static uint32_t highest_seq = 0; // максимальный увиденн
|
||||
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])
|
||||
static uint64_t miss_rebuilt = 0; // пересборок NACK из кольца (этап 3c)
|
||||
// Разделяемых на запись счётчиков нет. main читает их раз в секунду для
|
||||
// телеметрии без синхронизации: на 32-бит ARM возможен косметический «рваный»
|
||||
// показ 64-бит значения — на итоговую строку (печатается после join) не влияет.
|
||||
@@ -164,18 +164,16 @@ static int64_t now_ms(void) {
|
||||
return (int64_t)t.tv_sec * 1000 + t.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
// Файл собран? Legacy: курсор вывода дошёл до конца — это сильнее, чем «дыр
|
||||
// нет»: next_out двигается только по непрерывному префиксу реально записанных
|
||||
// кадров. Group-FEC: ARQ выключен (этап 3c), вывод идёт мимо кольца — критерий
|
||||
// прежний, по фронту; ветка уйдёт вместе с гибридом GF+ARQ.
|
||||
// Файл собран? Критерий один для обоих режимов (этап 3c): курсор вывода дошёл до
|
||||
// конца. Это сильнее, чем «дыр нет»: next_out двигается только по непрерывному
|
||||
// префиксу реально записанных кадров. Паритетные кадры group-FEC занимают слот
|
||||
// нулевой длины — они тратят seq, но в файл не идут, и курсор их проходит.
|
||||
// Держим флаг 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 &&
|
||||
(gf_seen ? (seq_init && highest_seq == end_total_seq - 1)
|
||||
: next_out == end_total_seq));
|
||||
(miss_cnt == 0 && next_out == end_total_seq);
|
||||
if (!done) { complete_since_ms = 0; return; }
|
||||
|
||||
int64_t t = now_ms();
|
||||
@@ -342,12 +340,36 @@ static void rb_flush_all(void) {
|
||||
rb_force_to(last);
|
||||
}
|
||||
|
||||
// Пересобрать NACK-набор из кольца (этап 3c, только FDD, только по END).
|
||||
// Зачем: miss_add при переполнении MISS_CAP молча теряет seq, и вернуть его туда
|
||||
// уже некому — детектор дыр по нему не сработает второй раз (highest_seq ушёл
|
||||
// вперёд). В тяжёлом канале это тупик: A не знает, что дослать, next_out стоит,
|
||||
// COMPLETE не наступает, B досиживает до сторожа. Кольцо же знает точно, каких
|
||||
// кадров нет: слот либо занят своим seq, либо это дыра.
|
||||
// Скан ограничен окном кольца: seq дальше next_out+RB_CAP всё равно принять
|
||||
// некуда (rb_insert их отбросит), просить их у A рано.
|
||||
static void miss_rebuild(void) {
|
||||
if (!g_fb_fg || !end_seen) return;
|
||||
if (end_total_seq <= next_out) { miss_cnt = 0; return; } // всё выведено; заодно
|
||||
// страховка от переполнения вычитания ниже
|
||||
uint32_t to = end_total_seq;
|
||||
if (to - next_out > RB_CAP) to = next_out + RB_CAP;
|
||||
miss_cnt = 0;
|
||||
for (uint32_t s = next_out; s < to && miss_cnt < MISS_CAP; s++) {
|
||||
rbslot_t *sl = &rb[s % RB_CAP];
|
||||
if (sl->filled && sl->seq == s) continue; // кадр уже принят
|
||||
miss[miss_cnt++] = s; // возрастает → сортировка цела
|
||||
}
|
||||
miss_rebuilt++;
|
||||
}
|
||||
|
||||
// --- Групповой стирающий FEC (поток C, README §12.7) --------------------
|
||||
// Собирает группу из 16 кадров данных + 2 паритетных (P,Q). Пропавший на эфире
|
||||
// или битый по CRC кадр = стирание; ≤2 стираний на группу закрываются gf_recover.
|
||||
// Всё состояние — только поток C (как остальные C-счётчики), синхронизации нет.
|
||||
static struct {
|
||||
int active;
|
||||
int seen; // хоть одна группа была: g.base осмыслен (этап 3c)
|
||||
uint32_t base; // seq первого кадра группы (= seq − idx)
|
||||
int k; // число кадров данных (из меты паритета)
|
||||
int k_known; // k получен из паритета
|
||||
@@ -355,6 +377,7 @@ static struct {
|
||||
uint8_t data[GF_K][GF_CHUNK];
|
||||
uint16_t dlen[GF_K]; // фактическая длина каждого кадра данных
|
||||
uint8_t have[GF_K]; // 1 = кадр idx получен и валиден
|
||||
uint8_t fixed[GF_K]; // 1 = кадр восстановлен паритетом, а не принят
|
||||
uint8_t P[GF_CHUNK], Q[GF_CHUNK];
|
||||
int haveP, haveQ;
|
||||
uint16_t tail_len; // длина последнего кадра данных (из меты)
|
||||
@@ -362,8 +385,9 @@ static struct {
|
||||
|
||||
static void group_reset(uint32_t base) {
|
||||
memset(g.have, 0, sizeof(g.have));
|
||||
memset(g.fixed, 0, sizeof(g.fixed));
|
||||
memset(g.dlen, 0, sizeof(g.dlen));
|
||||
g.active = 1; g.base = base; g.k = 0; g.k_known = 0;
|
||||
g.active = 1; g.seen = 1; g.base = base; g.k = 0; g.k_known = 0;
|
||||
g.maxidx = -1; g.haveP = g.haveQ = 0; g.tail_len = 0;
|
||||
}
|
||||
|
||||
@@ -383,20 +407,38 @@ static void group_flush(void) {
|
||||
// прочие кадры полные 1024, последний — tail_len
|
||||
g.dlen[i] = (i == k - 1 && g.tail_len) ? g.tail_len : GF_CHUNK;
|
||||
g.have[i] = 1;
|
||||
g.fixed[i] = 1; // закрыт паритетом — досылать по ARQ не нужно
|
||||
}
|
||||
} else {
|
||||
grp_failed++; // >2 стираний либо не хватило паритета
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < k; i++) {
|
||||
if (!g.have[i]) continue; // невосстановимая дыра
|
||||
uint16_t len = g.dlen[i] > GF_CHUNK ? GF_CHUNK : g.dlen[i];
|
||||
fwrite(g.data[i], 1, len, stdout);
|
||||
total_bytes += len;
|
||||
crc_final++;
|
||||
if (g_fb_fg) {
|
||||
// FDD (гибрид GF+ARQ, этап 3c): порядок держит кольцо, и принятые кадры
|
||||
// уже легли в него на приёме (group_route). Отсюда идут ТОЛЬКО кадры,
|
||||
// поднятые паритетом, — и их seq снимаются с NACK: GF закрыл дыру,
|
||||
// ретрансмит был бы лишним трафиком. Невосстановимые остаются в miss —
|
||||
// их дошлёт ARQ, для того он здесь и включён.
|
||||
for (int i = 0; i < k; i++) {
|
||||
if (!g.fixed[i]) continue;
|
||||
rb_insert(g.base + i, g.data[i], g.dlen[i]);
|
||||
miss_ack(g.base + i);
|
||||
}
|
||||
rb_drain();
|
||||
} else {
|
||||
// Симплекс: кольца нет (ретрансмитов не будет — переупорядочивать нечего),
|
||||
// группа сама себе буфер. Пишем её целиком в порядке idx, восстановленные
|
||||
// встают на свои места. Невосстановимая дыра = сдвиг, прежняя семантика.
|
||||
for (int i = 0; i < k; i++) {
|
||||
if (!g.have[i]) continue;
|
||||
uint16_t len = g.dlen[i] > GF_CHUNK ? GF_CHUNK : g.dlen[i];
|
||||
fwrite(g.data[i], 1, len, stdout);
|
||||
total_bytes += len;
|
||||
crc_final++;
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
fflush(stdout);
|
||||
g.active = 0;
|
||||
}
|
||||
|
||||
@@ -407,6 +449,24 @@ static void group_route(uint8_t flags, uint32_t seq,
|
||||
int is_par = flags & GF_FLAG_PAR;
|
||||
uint32_t base = seq - idx; // idx=k для P, k+1 для Q → та же база
|
||||
|
||||
// FDD: кольцо — единственный источник порядка, и оно живёт независимо от
|
||||
// сборки групп. Кладём кадр в слот ДО всякого bypass: даже ретрансмит давно
|
||||
// закрытой группы обязан занять своё место, иначе next_out встанет на нём
|
||||
// навсегда и COMPLETE не наступит. Паритет тратит seq, но данных не несёт —
|
||||
// ему слот нулевой длины, курсор его проходит насквозь.
|
||||
if (ok && g_fb_fg) {
|
||||
rb_insert(seq, is_par ? NULL : dec, is_par ? 0 : dec_len);
|
||||
rb_drain();
|
||||
}
|
||||
|
||||
// Ретрансмит кадра уже закрытой группы (этап 3c). Пересобирать нечего: в FDD
|
||||
// кадр только что лёг в кольцо, а группа нужна лишь ради паритета. Ловим два
|
||||
// случая: старая группа (base < g.base) — иначе она преждевременно flush-нула
|
||||
// бы активную и сбросила её стирания; и текущая, уже финализированная по END
|
||||
// (base == g.base && !g.active) — иначе кадр «воскресил» бы группу с одним
|
||||
// элементом, наврав в grp_failed.
|
||||
if (g.seen && (base < g.base || (base == g.base && !g.active))) return;
|
||||
|
||||
if (!g.active || base != g.base) { // новая группа → финализировать прежнюю
|
||||
group_flush();
|
||||
group_reset(base);
|
||||
@@ -451,24 +511,40 @@ static void process_end(const uint8_t *hdr, uint8_t *pay, unsigned pay_len,
|
||||
}
|
||||
|
||||
end_rx++;
|
||||
if (end_seen) return;
|
||||
end_seen = 1;
|
||||
end_total_seq = e.total_seq;
|
||||
end_total_bytes = e.total_bytes;
|
||||
if (!end_seen) {
|
||||
end_seen = 1;
|
||||
end_total_seq = e.total_seq;
|
||||
end_total_bytes = e.total_bytes;
|
||||
|
||||
// Backfill: всё от текущего фронта до total_seq-1 в эфире было, но до нас не
|
||||
// дошло. Массив miss остаётся отсортированным — добавляем seq строго больше
|
||||
// всех имеющихся. Фронт двигаем на конец файла: дальше приходить могут только
|
||||
// ретрансмиты (seq ≤ highest_seq), ложных дыр они не породят.
|
||||
if (e.total_seq > 0 && (!seq_init || highest_seq < e.total_seq - 1)) {
|
||||
uint32_t from = seq_init ? highest_seq + 1 : 0;
|
||||
for (uint32_t m = from; m < e.total_seq; m++) miss_add(m);
|
||||
lost += e.total_seq - from;
|
||||
seq_init = 1;
|
||||
highest_seq = e.total_seq - 1;
|
||||
// Хвостовые потери молчаливы: у последних кадров нет следующего seq, по
|
||||
// которому детектор увидел бы разрыв. Двигаем фронт на конец файла —
|
||||
// дальше приходить могут только ретрансмиты (seq ≤ highest_seq), ложных
|
||||
// дыр они не породят.
|
||||
if (e.total_seq > 0 && (!seq_init || highest_seq < e.total_seq - 1)) {
|
||||
lost += e.total_seq - (seq_init ? highest_seq + 1 : 0);
|
||||
seq_init = 1;
|
||||
highest_seq = e.total_seq - 1;
|
||||
}
|
||||
}
|
||||
fprintf(stderr, "\n[ARQ] END принят: всего %u кадров, %llu байт; не хватает %d\n",
|
||||
e.total_seq, (unsigned long long)e.total_bytes, miss_cnt);
|
||||
|
||||
// Досрочно финализировать последнюю группу (group-FEC + FDD). Штатно группа
|
||||
// закрывается приходом первого кадра СЛЕДУЮЩЕЙ группы — а для последней его
|
||||
// не будет, и без этого её восстановление паритетом случилось бы только при
|
||||
// выходе потока C, то есть уже после COMPLETE. Дыры пришлось бы закрывать
|
||||
// ретрансмитом вместо готового паритета. END значит «новых данных не будет»
|
||||
// (дальше только повторы), поэтому финализировать безопасно; повторные END
|
||||
// попадают в no-op (g.active уже 0).
|
||||
group_flush();
|
||||
|
||||
// NACK пересобираем на КАЖДОМ END, а не только на первом (этап 3c). A шлёт
|
||||
// END каждые 100 мс до самого COMPLETE, так что это ещё и штатный способ
|
||||
// вернуть в запрос seq, выпавшие из miss[] по MISS_CAP: пока хвост пуст,
|
||||
// дыры видит только кольцо. Скан ≤1024 при 10 Гц — сотые доли ядра.
|
||||
miss_rebuild();
|
||||
|
||||
if (end_rx == 1)
|
||||
fprintf(stderr, "\n[ARQ] END принят: всего %u кадров, %llu байт; не хватает %d\n",
|
||||
e.total_seq, (unsigned long long)e.total_bytes, miss_cnt);
|
||||
}
|
||||
|
||||
// Разбор одного кадра в потоке C: seq-детектор + RS + CRC-поверх-RS, затем
|
||||
@@ -584,7 +660,6 @@ route:
|
||||
else miss_add_sorted(seq);
|
||||
|
||||
if (flags & GF_FLAG_MODE) {
|
||||
gf_seen = 1;
|
||||
group_route(flags, seq, dec, dec_len, ok);
|
||||
} else if (ok) {
|
||||
if (g.active) group_flush(); // TX не мешает режимы, но подстрахуемся
|
||||
@@ -899,8 +974,9 @@ int main(int argc, char *argv[]) {
|
||||
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);
|
||||
fprintf(stderr, "ARQ: окно reorder переполнено: %llu | NACK переполнен: %llu | пересборок NACK: %llu\n",
|
||||
(unsigned long long)rb_over, (unsigned long long)miss_over,
|
||||
(unsigned long long)miss_rebuilt);
|
||||
}
|
||||
|
||||
if (decoder) fec_destroy(decoder);
|
||||
|
||||
@@ -151,7 +151,8 @@ typedef struct {
|
||||
} txslot_t;
|
||||
|
||||
static txslot_t txwin[TXWIN]; // ~2.1 МБ .bss (из 1 ГБ платы)
|
||||
static int g_arq_on = 0; // ARQ включён (FDD + legacy; GF — этап 3c)
|
||||
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 с эфира)
|
||||
|
||||
@@ -353,16 +354,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;
|
||||
// Этап 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);
|
||||
if (g_arq_on)
|
||||
fprintf(stderr, "ARQ: вкл (окно %d кадров, повтор не чаще %d мс)\n",
|
||||
TXWIN, ARQ_RETX_MS);
|
||||
else
|
||||
fprintf(stderr, "ARQ: выкл (group-FEC — этап 3c)\n");
|
||||
fprintf(stderr, "ARQ: вкл (окно %d кадров, повтор не чаще %d мс)\n",
|
||||
TXWIN, ARQ_RETX_MS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,6 +420,7 @@ int main(int argc, char *argv[]) {
|
||||
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];
|
||||
@@ -432,6 +434,7 @@ int main(int argc, char *argv[]) {
|
||||
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)",
|
||||
|
||||
Reference in New Issue
Block a user