Этап 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:
Maxim
2026-07-16 15:29:25 +03:00
parent 6294a472cd
commit ab1e87a17e
6 changed files with 224 additions and 49 deletions

View File

@@ -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-диапазон: ограничения на полосу канала (сотни кГц),