diff --git a/CLAUDE.md b/CLAUDE.md index f65aa75..133d1cf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,11 @@ PHY: OFDM через liquid-dsp 1.8.0 + FFTW/NEON, FEC RS(255,223). ## Формат кадра (заголовок 12 Б) 0-1: 0xF0 0xAA | 2-5: seq u32 BE | 6-7: len u16 BE | 8-9: nblocks | -10: last_block_bytes | 11: резерв +10: last_block_bytes | 11: group-FEC флаги (§10 п.6) +Байт 11: bit7 GF_MODE (группа активна), bit6 GF_PARITY (P/Q-кадр), +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 автодетект. КРИТИЧНО: liquid по умолчанию передаёт 8 Б заголовка. Обязательно `ofdmflexframegen_set_header_len(fg,12)` и `ofdmflexframesync_set_header_len(fs,12)` на обеих сторонах. diff --git a/makefile b/makefile index 6f64308..1c5b7cf 100644 --- a/makefile +++ b/makefile @@ -25,18 +25,24 @@ all: $(BIN)/transmitter $(BIN)/receiver $(BIN)/ofdm_bench $(BIN): mkdir -p $(BIN) -$(BIN)/transmitter: $(SRC)/transmitter.c $(SRC)/common.c $(SRC)/common.h $(DEPS_LIBS) | $(BIN) - $(CC) $(CFLAGS) $(SRC)/transmitter.c $(SRC)/common.c -o $@ $(LDFLAGS) -lad9361 -liio $(LDLIBS) +$(BIN)/transmitter: $(SRC)/transmitter.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/common.h $(SRC)/group_fec.h $(DEPS_LIBS) | $(BIN) + $(CC) $(CFLAGS) $(SRC)/transmitter.c $(SRC)/common.c $(SRC)/group_fec.c -o $@ $(LDFLAGS) -lad9361 -liio $(LDLIBS) -$(BIN)/receiver: $(SRC)/receiver.c $(SRC)/common.c $(SRC)/common.h $(DEPS_LIBS) | $(BIN) - $(CC) $(CFLAGS) $(SRC)/receiver.c $(SRC)/common.c -o $@ $(LDFLAGS) -lad9361 -liio $(LDLIBS) +$(BIN)/receiver: $(SRC)/receiver.c $(SRC)/common.c $(SRC)/group_fec.c $(SRC)/common.h $(SRC)/group_fec.h $(DEPS_LIBS) | $(BIN) + $(CC) $(CFLAGS) $(SRC)/receiver.c $(SRC)/common.c $(SRC)/group_fec.c -o $@ $(LDFLAGS) -lad9361 -liio $(LDLIBS) $(BIN)/ofdm_bench: $(SRC)/ofdm_bench.c | $(BIN) $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) $(LDLIBS) -tx: $(BIN)/transmitter -rx: $(BIN)/receiver -bench: $(BIN)/ofdm_bench +# Оффлайн-самопроверка GF(256) — нативная сборка (gcc хоста, напр. RPi/aarch64) +$(BIN)/gftest: $(SRC)/group_fec.c $(SRC)/group_fec.h | $(BIN) + cc -O2 -Wall -DGF_SELFTEST $(SRC)/group_fec.c -o $@ + +tx: $(BIN)/transmitter +rx: $(BIN)/receiver +bench: $(BIN)/ofdm_bench +gftest: $(BIN)/gftest + $(BIN)/gftest # --- деплой (scp -O: на прошивке v0.38 нет sftp-server) --- deploy: all @@ -60,10 +66,11 @@ clean: help: @echo "make [all] — собрать transmitter, receiver, ofdm_bench (static armhf)" @echo "make tx|rx|bench — собрать отдельную цель" + @echo "make gftest — оффлайн-самопроверка group-FEC (нативный cc)" @echo "make deploy — залить бинарники на обе платы (scp -O в /tmp)" @echo "make test — e2e-тест передачи файла с md5-сверкой" @echo "make check-env — проверить тулчейн и $(XARM)" @echo "make clean — удалить build/" @echo "переменные: PLUTO_A=$(PLUTO_A) PLUTO_B=$(PLUTO_B) XARM=$(XARM)" -.PHONY: all tx rx bench deploy test check-env clean help \ No newline at end of file +.PHONY: all tx rx bench gftest deploy test check-env clean help \ No newline at end of file diff --git a/scripts/test_file_link.sh b/scripts/test_file_link.sh index 1c50c53..2419f32 100755 --- a/scripts/test_file_link.sh +++ b/scripts/test_file_link.sh @@ -18,7 +18,11 @@ TXGAIN="${TXGAIN:--30}" # OTA: переопредели, напр. TXGAIN=-2 FEC="${FEC:-rs8}" # rs8 | none — фаза 3.2 гоняет сначала none, потом rs8 PAUSE="${PAUSE:-3000}" # пауза TX между кадрами, мкс. Рабочая точка 3-стадийного # RX (§12.6): чисто, ~120 КБ/с. Ниже ~2500 — Overrun на ядре 0 -TIMEOUT=$(( SIZE_KB / 80 + 20 )) # ~120 КБ/с на -p 3000 + запас (см. §12.6) +GROUPFEC="${GROUPFEC:-1}" # 1 = групповой стирающий FEC 16+2 (§12.7): TX шлёт -G, + # RX автодетектит по hdr[11]. 0 = legacy (потери = дыры) +[ "$GROUPFEC" = 1 ] && GFLAG="-G" || GFLAG="" +# Group-FEC: +12.5% overhead → ~107 КБ/с (SIZE_KB/85); legacy — ~120 (SIZE_KB/95) +if [ "$GROUPFEC" = 1 ]; then TIMEOUT=$(( SIZE_KB / 85 + 20 )); else TIMEOUT=$(( SIZE_KB / 95 + 20 )); fi if command -v sshpass >/dev/null; then SSH=(sshpass -p "$PLUTO_PASS" ssh -o StrictHostKeyChecking=no) @@ -48,7 +52,7 @@ sleep 3 # дать RX прогреться до старта TX echo "=== [4/5] Передача с Pluto A ===" "${SSH[@]}" "root@$A" \ "cat /tmp/link_test.bin | /tmp/transmitter -f $FREQ -r $RATE -b $BW \ - -g $TXGAIN -p $PAUSE -c $FEC -u local: 2>/tmp/tx.log" + -g $TXGAIN -p $PAUSE -c $FEC $GFLAG -u local: 2>/tmp/tx.log" sleep 3 # дохлебать хвост эфира "${SSH[@]}" "root@$B" "killall receiver 2>/dev/null || true" wait "$RX_PID" 2>/dev/null || true diff --git a/src/group_fec.c b/src/group_fec.c new file mode 100644 index 0000000..442aa32 --- /dev/null +++ b/src/group_fec.c @@ -0,0 +1,165 @@ +// group_fec.c — GF(256) для группового стирающего FEC 16+2 (см. group_fec.h) +// Поле GF(2⁸), примитивный полином 0x11d (как в RS/RAID-6), генератор g=2. +// Паритеты: P[j] = ⊕ᵢ dᵢ[j]; Q[j] = ⊕ᵢ gⁱ·dᵢ[j]. Восстановление ≤2 стираний +// данных — прямое решение системы из P и Q (Anvin, «RAID-6»). + +#include "group_fec.h" +#include + +static uint8_t gflog[256]; +static uint8_t gfexp[512]; // удвоено: gfexp[a+b] без модуля для a,b≤254 + +void gf_init(void) { + uint16_t x = 1; + for (int i = 0; i < 255; i++) { + gfexp[i] = (uint8_t)x; + gflog[(uint8_t)x] = (uint8_t)i; + x <<= 1; + if (x & 0x100) x ^= 0x11d; + } + for (int i = 255; i < 512; i++) gfexp[i] = gfexp[i - 255]; + gflog[0] = 0; // не определён; умножение на 0 отсекается явно +} + +static inline uint8_t gf_mul(uint8_t a, uint8_t b) { + if (a == 0 || b == 0) return 0; + return gfexp[gflog[a] + gflog[b]]; +} + +static inline uint8_t gf_div(uint8_t a, uint8_t b) { // b != 0 + if (a == 0) return 0; + return gfexp[gflog[a] + 255 - gflog[b]]; +} + +void gf_encode(const uint8_t *const *data, int k, uint8_t *P, uint8_t *Q) { + for (int j = 0; j < GF_CHUNK; j++) { + uint8_t p = 0, q = 0; + for (int i = 0; i < k; i++) { + uint8_t d = data[i][j]; + p ^= d; + q ^= gf_mul(gfexp[i], d); // gⁱ·dᵢ + } + P[j] = p; + Q[j] = q; + } +} + +int gf_recover(uint8_t data[][GF_CHUNK], const uint8_t *have, int k, + const uint8_t *P, int have_P, const uint8_t *Q, int have_Q) { + int er[3], ne = 0; + for (int i = 0; i < k; i++) + if (!have[i]) { if (ne < 3) er[ne] = i; ne++; } + + if (ne == 0) return 1; // нечего восстанавливать + if (ne > 2) return 0; // за пределами 16+2 + + if (ne == 1) { + int e = er[0]; + if (have_P) { // dₑ = P ⊕ (⊕_{i≠e} dᵢ) + for (int j = 0; j < GF_CHUNK; j++) { + uint8_t v = P[j]; + for (int i = 0; i < k; i++) if (i != e) v ^= data[i][j]; + data[e][j] = v; + } + return 1; + } + if (have_Q) { // dₑ = g^(−e) · (Q ⊕ (⊕_{i≠e} gⁱ·dᵢ)) + uint8_t inv = gfexp[(255 - e) % 255]; + for (int j = 0; j < GF_CHUNK; j++) { + uint8_t v = Q[j]; + for (int i = 0; i < k; i++) if (i != e) v ^= gf_mul(gfexp[i], data[i][j]); + data[e][j] = gf_mul(inv, v); + } + return 1; + } + return 0; // потеряны и данные, и оба паритета? (не бывает) + } + + // ne == 2: нужны оба паритета. + if (!have_P || !have_Q) return 0; + int e1 = er[0], e2 = er[1]; + uint8_t ge1 = gfexp[e1], ge2 = gfexp[e2]; + uint8_t denom = ge1 ^ ge2; // ≠ 0: gⁱ различны для i≤254 + for (int j = 0; j < GF_CHUNK; j++) { + uint8_t A = P[j], B = Q[j]; + for (int i = 0; i < k; i++) if (i != e1 && i != e2) { + A ^= data[i][j]; + B ^= gf_mul(gfexp[i], data[i][j]); + } + // dₑ₁ = (B ⊕ ge2·A) / denom ; dₑ₂ = A ⊕ dₑ₁ + uint8_t d1 = gf_div(B ^ gf_mul(ge2, A), denom); + data[e1][j] = d1; + data[e2][j] = A ^ d1; + } + return 1; +} + +// --- Оффлайн-самопроверка: cc -O2 -DGF_SELFTEST group_fec.c -o gftest ----- +#ifdef GF_SELFTEST +#include +#include + +int main(void) { + gf_init(); + + // Проверка поля: g^255 == 1, лог/эксп обратны. + for (int a = 1; a < 256; a++) + if (gfexp[gflog[a]] != a) { printf("FAIL: log/exp a=%d\n", a); return 1; } + + srand(12345); + int fails = 0, cases = 0; + + for (int trial = 0; trial < 200; trial++) { + for (int k = 1; k <= GF_K; k++) { + uint8_t ref[GF_K][GF_CHUNK]; + const uint8_t *ptr[GF_K]; + for (int i = 0; i < k; i++) { + for (int j = 0; j < GF_CHUNK; j++) ref[i][j] = rand() & 0xFF; + ptr[i] = ref[i]; + } + uint8_t P[GF_CHUNK], Q[GF_CHUNK]; + gf_encode(ptr, k, P, Q); + + // 0, 1 и 2 стирания во всех комбинациях наличия паритета. + for (int e1 = -1; e1 < k; e1++) { + for (int e2 = e1; e2 < k; e2++) { + // e1==-1: нет стираний; e2==e1: одно; иначе два. + for (int hp = 0; hp <= 1; hp++) { + for (int hq = 0; hq <= 1; hq++) { + uint8_t work[GF_K][GF_CHUNK]; + uint8_t have[GF_K]; + memcpy(work, ref, sizeof(uint8_t) * k * GF_CHUNK); + for (int i = 0; i < k; i++) have[i] = 1; + int ne = 0; + if (e1 >= 0) { have[e1] = 0; memset(work[e1], 0, GF_CHUNK); ne++; } + if (e2 >= 0 && e2 != e1) { have[e2] = 0; memset(work[e2], 0, GF_CHUNK); ne++; } + + int ok = gf_recover(work, have, k, P, hp, Q, hq); + cases++; + + // Ожидаемая восстановимость: 0 стираний всегда; 1 — если + // есть хоть один паритет; 2 — если есть оба. + int expect = (ne == 0) || (ne == 1 && (hp || hq)) || + (ne == 2 && hp && hq); + if (ok != expect) { + printf("FAIL recover-flag k=%d e=%d,%d hp=%d hq=%d ok=%d exp=%d\n", + k, e1, e2, hp, hq, ok, expect); + if (++fails > 10) return 1; + continue; + } + if (ok && memcmp(work, ref, sizeof(uint8_t) * k * GF_CHUNK) != 0) { + printf("FAIL data-mismatch k=%d e=%d,%d hp=%d hq=%d\n", + k, e1, e2, hp, hq); + if (++fails > 10) return 1; + } + } + } + } + } + } + } + printf("gftest: %d кейсов, %d ошибок → %s\n", cases, fails, + fails ? "ПРОВАЛ" : "OK"); + return fails ? 1 : 0; +} +#endif diff --git a/src/group_fec.h b/src/group_fec.h new file mode 100644 index 0000000..9ad6a7e --- /dev/null +++ b/src/group_fec.h @@ -0,0 +1,42 @@ +// group_fec.h — групповой стирающий FEC 16+2 (RAID-6-стиль над GF(256)) +// Роадмап §10 п.6: восстановление молчаливых потерь кадров БЕЗ обратного канала. +// Позиции потерь известны из seq (это стирания) → 2 паритетных кадра (P=XOR, +// Q=Σ gⁱ·dᵢ) закрывают до 2 стёртых кадров данных на группу из 16. +// Стоимость GF на 120 КБ/с < 0.2% ядра — считается в потоке C приёмника. + +#ifndef GROUP_FEC_H +#define GROUP_FEC_H + +#include + +#define GF_K 16 // кадров данных в группе +#define GF_CHUNK 1024 // байт данных на кадр (== MAX_PAYLOAD) +#define GF_META 2 // байт метаданных в начале паритетного payload +#define GF_PAR_PAY (GF_META + GF_CHUNK) // 1026: payload паритетного кадра + +// Флаги в резервном байте заголовка hdr[11] (см. CLAUDE.md «Формат кадра»): +#define GF_FLAG_MODE 0x80 // bit7: группа активна (иначе legacy-кадр) +#define GF_FLAG_PAR 0x40 // bit6: паритетный кадр (иначе данные) +#define GF_FLAG_Q 0x20 // bit5: это Q (иначе P) — только для паритета +#define GF_IDX_MASK 0x1F // bits4..0: idx кадра в группе (0..17) + +// Метаданные паритета (u16 BE в начале payload): k′ и длина хвостового кадра. +// (k′−1) в битах 11..14 (≤15), tail_len в битах 0..10 (≤1024). +#define GF_META_PACK(kprime, tail) (uint16_t)((((kprime) - 1) << 11) | ((tail) & 0x7FF)) +#define GF_META_K(meta) (int)((((meta) >> 11) & 0x1F) + 1) +#define GF_META_TAIL(meta) (uint16_t)((meta) & 0x7FF) + +void gf_init(void); + +// Кодирование: data[i] — указатели на GF_CHUNK-байтовые чанки (короткий +// последний дополняется нулями вызывающим). k ≤ GF_K. P, Q — выход GF_CHUNK. +void gf_encode(const uint8_t *const *data, int k, uint8_t *P, uint8_t *Q); + +// Восстановление ≤2 стёртых чанков данных на месте. data[i] валиден при +// have[i]=1 (с нулевым паддингом короткого последнего), иначе будет восстановлен. +// k — число кадров данных. P/Q — паритеты, наличие через have_P/have_Q. +// Возврат: 1 — восстановлено (или нечего восстанавливать), 0 — не хватило паритета. +int gf_recover(uint8_t data[][GF_CHUNK], const uint8_t *have, int k, + const uint8_t *P, int have_P, const uint8_t *Q, int have_Q); + +#endif diff --git a/src/receiver.c b/src/receiver.c index 8388964..2c66235 100644 --- a/src/receiver.c +++ b/src/receiver.c @@ -9,6 +9,7 @@ #define _GNU_SOURCE // pthread_setaffinity_np, cpu_set_t #include "common.h" +#include "group_fec.h" #include #include @@ -70,6 +71,7 @@ static float rssi = -100, evm = 0, cfo_est = 0.0f; // Счётчики C (пишет process_frame): целостность и вывод. static uint64_t rs_saved = 0, crc_final = 0, total_bytes = 0; static uint64_t rs_fail = 0, lost = 0; +static uint64_t grp_recovered = 0, grp_failed = 0; // group-FEC: восст. кадров / провал. групп static uint32_t last_seq = 0; static int seq_init = 0; // Разделяемых на запись счётчиков нет. main читает их раз в секунду для @@ -121,8 +123,98 @@ static int callback(unsigned char *hdr, int hdr_valid, return 0; } -// Разбор одного кадра в потоке C: seq-детектор + RS + CRC-поверх-RS + запись. -// Порядок сохранён (fq FIFO, единственный писатель в stdout — этот поток). +// --- Групповой стирающий FEC (поток C, README §12.7) -------------------- +// Собирает группу из 16 кадров данных + 2 паритетных (P,Q). Пропавший на эфире +// или битый по CRC кадр = стирание; ≤2 стираний на группу закрываются gf_recover. +// Всё состояние — только поток C (как остальные C-счётчики), синхронизации нет. +static struct { + int active; + uint32_t base; // seq первого кадра группы (= seq − idx) + int k; // число кадров данных (из меты паритета) + int k_known; // k получен из паритета + int maxidx; // макс. полученный idx данных — оценка k без паритета + uint8_t data[GF_K][GF_CHUNK]; + uint16_t dlen[GF_K]; // фактическая длина каждого кадра данных + uint8_t have[GF_K]; // 1 = кадр idx получен и валиден + uint8_t P[GF_CHUNK], Q[GF_CHUNK]; + int haveP, haveQ; + uint16_t tail_len; // длина последнего кадра данных (из меты) +} g; + +static void group_reset(uint32_t base) { + memset(g.have, 0, sizeof(g.have)); + memset(g.dlen, 0, sizeof(g.dlen)); + g.active = 1; g.base = base; g.k = 0; g.k_known = 0; + g.maxidx = -1; g.haveP = g.haveQ = 0; g.tail_len = 0; +} + +// Финализировать текущую группу: восстановить стирания и записать данные по idx. +static void group_flush(void) { + if (!g.active) return; + int k = g.k_known ? g.k : (g.maxidx + 1); + if (k <= 0 || k > GF_K) { g.active = 0; return; } + + int erasures = 0; + for (int i = 0; i < k; i++) if (!g.have[i]) erasures++; + + if (erasures > 0) { + if (gf_recover(g.data, g.have, k, g.P, g.haveP, g.Q, g.haveQ)) { + grp_recovered += erasures; + for (int i = 0; i < k; i++) if (!g.have[i]) { + // прочие кадры полные 1024, последний — tail_len + g.dlen[i] = (i == k - 1 && g.tail_len) ? g.tail_len : GF_CHUNK; + g.have[i] = 1; + } + } 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++; + } + fflush(stdout); + g.active = 0; +} + +// Разложить один декодированный group-FEC кадр по группе. +static void group_route(uint8_t flags, uint32_t seq, + const uint8_t *dec, size_t dec_len, int ok) { + int idx = flags & GF_IDX_MASK; + int is_par = flags & GF_FLAG_PAR; + uint32_t base = seq - idx; // idx=k для P, k+1 для Q → та же база + + if (!g.active || base != g.base) { // новая группа → финализировать прежнюю + group_flush(); + group_reset(base); + } + if (!ok) return; // битый кадр остаётся стиранием + + if (!is_par) { + if (idx < GF_K && dec_len <= GF_CHUNK) { + memcpy(g.data[idx], dec, dec_len); + if (dec_len < GF_CHUNK) memset(g.data[idx] + dec_len, 0, GF_CHUNK - dec_len); + g.dlen[idx] = (uint16_t)dec_len; + g.have[idx] = 1; + if (idx > g.maxidx) g.maxidx = idx; + } + } else if (dec_len >= GF_PAR_PAY) { // паритет: [0..1] мета, [2..] P/Q + uint16_t meta = ((uint16_t)dec[0] << 8) | dec[1]; + g.k = GF_META_K(meta); + g.tail_len = GF_META_TAIL(meta); + g.k_known = 1; + if (flags & GF_FLAG_Q) { memcpy(g.Q, dec + GF_META, GF_CHUNK); g.haveQ = 1; } + else { memcpy(g.P, dec + GF_META, GF_CHUNK); g.haveP = 1; } + } +} + +// Разбор одного кадра в потоке C: seq-детектор + RS + CRC-поверх-RS, затем +// маршрутизация (прямая запись для legacy, сборка группы для group-FEC). +// Единственный писатель в stdout — этот поток, порядок сохранён (fq FIFO). static void process_frame(fq_slot_t *fs) { const uint8_t *hdr = fs->hdr; uint8_t *pay = fs->pay; // не const: fec_decode() liquid не const-correct @@ -131,7 +223,8 @@ static void process_frame(fq_slot_t *fs) { // Детектор пропусков seq (техдолг #6). Кадр, выброшенный переполнением fq, // тоже проявится здесь как дыра seq → в `lost`; счётчик FQdrop разделяет - // причины (эфирная потеря против переполнения очереди). + // причины (эфирная потеря против переполнения очереди). В group-FEC потеря + // одновременно фиксируется как стирание в group_route по idx. uint32_t seq = ((uint32_t)hdr[2] << 24) | ((uint32_t)hdr[3] << 16) | ((uint32_t)hdr[4] << 8) | hdr[5]; if (seq_init && seq > last_seq + 1) { @@ -144,14 +237,20 @@ static void process_frame(fq_slot_t *fs) { uint16_t original_len = (hdr[6] << 8) | hdr[7]; uint8_t last_block_bytes = hdr[10]; + uint8_t flags = hdr[11]; + + // Декод payload → dec/dec_len, ok=1 при пройденном CRC-поверх-RS. + static uint8_t recovered[4096]; // поток C один → static безопасен + const uint8_t *dec = NULL; + size_t dec_len = 0; + int ok = 0; if (fec_on && decoder && pay_len >= RS_ENC) { size_t nblocks = pay_len / RS_ENC; // Защита в глубину: B уже отсёк pay_len>FQ_PAY_MAX, но guard оставлен // (recovered[4096] вмещает 4096/RS_DATA = 18 блоков, техдолг #2). - if (nblocks == 0 || nblocks > 4096 / RS_DATA) return; + if (nblocks == 0 || nblocks > 4096 / RS_DATA) goto route; - uint8_t recovered[4096]; for (size_t b = 0; b < nblocks; b++) { // fec_decode() liquid всегда "успешен" — код возврата бесполезен. fec_decode(decoder, RS_DATA, pay + b * RS_ENC, @@ -166,7 +265,7 @@ static void process_frame(fq_slot_t *fs) { if (original_len == 0 || (size_t)original_len + 4 > framed_len) { rs_fail++; - return; + goto route; } unsigned int rx_crc = crc_generate_key(LIQUID_CRC_32, recovered, original_len); unsigned int tx_crc = ((uint32_t)recovered[original_len] << 24) | @@ -175,19 +274,26 @@ static void process_frame(fq_slot_t *fs) { recovered[original_len + 3]; if (rx_crc != tx_crc) { rs_fail++; // RS не справился, кадр битый — не пишем - return; + goto route; } if (!pay_valid) rs_saved++; // liquid CRC по payload не прошёл, а наш // CRC поверх RS — да: значит RS реально спас - fwrite(recovered, 1, original_len, stdout); - fflush(stdout); - total_bytes += original_len; - crc_final++; + dec = recovered; dec_len = original_len; ok = 1; } else if (pay_valid && original_len > 0 && original_len <= pay_len) { - fwrite(pay, 1, original_len, stdout); - fflush(stdout); - total_bytes += original_len; - crc_final++; + dec = pay; dec_len = original_len; ok = 1; + } + +route: + if (flags & GF_FLAG_MODE) { + 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++; + } } } @@ -298,6 +404,7 @@ static void *fec_thread(void *arg) { fq.count--; pthread_mutex_unlock(&fq.mtx); } + group_flush(); // дописать последнюю группу файла (нет кадра-триггера) return NULL; } @@ -329,6 +436,7 @@ int main(int argc, char *argv[]) { fec_on = !strcmp(fec_opt, "rs8"); setup_signal_handlers(); + gf_init(); // group-FEC автодетектится по hdr[11] первого кадра print_config("ПРИЁМНИК", freq, rate, bw, fec_on); fprintf(stderr, "Усиление: %.0f дБ (ручное)\n", RX_GAIN); @@ -383,8 +491,8 @@ int main(int argc, char *argv[]) { ofdmflexframesync_set_header_len(fs, HDR_SIZE); fprintf(stderr, "Приёмник готов. Ожидание сигнала...\n\n"); - fprintf(stderr, "Кадры | Загол. | CRC | RS испр | RS бит | Потери | Overr | FQdrp | Вывод | RSSI | EVM | CFO\n"); - fprintf(stderr, "--------+--------+------+---------+--------+--------+--------+--------+--------+-------+-------+----------\n"); + fprintf(stderr, "Кадры | Загол. | CRC | RS испр | RS бит | Потери | Overr | FQdrp | Rec | GrpF | Вывод | RSSI | EVM | CFO\n"); + fprintf(stderr, "--------+--------+------+---------+--------+--------+--------+--------+--------+--------+--------+-------+-------+----------\n"); // Три стадии на двух ядрах. main крутит статистику 1 Гц и ждёт stop, затем // join A→B→C: A ставит ring.eof, B дочищает кольцо и ставит fq.eof, C @@ -402,11 +510,12 @@ int main(int argc, char *argv[]) { nanosleep(&nap, NULL); time_t now = time(NULL); if (now > last) { - fprintf(stderr, "\r%6llu | %5llu | %4llu | %6llu | %5llu | %6llu | %6llu | %6llu | %6llu | %+5.0f | %+5.1f | %+8.4f", + fprintf(stderr, "\r%6llu | %5llu | %4llu | %6llu | %5llu | %6llu | %6llu | %6llu | %6llu | %6llu | %6llu | %+5.0f | %+5.1f | %+8.4f", (unsigned long long)calls, (unsigned long long)hdr_ok, (unsigned long long)crc_raw, (unsigned long long)rs_saved, (unsigned long long)rs_fail, (unsigned long long)lost, (unsigned long long)ring.overruns, (unsigned long long)fq.drops, + (unsigned long long)grp_recovered, (unsigned long long)grp_failed, (unsigned long long)crc_final, rssi, evm, cfo_est); fflush(stderr); last = now; @@ -418,11 +527,12 @@ int main(int argc, char *argv[]) { pthread_join(tc, NULL); fprintf(stderr, "\n\nПРИЁМ ЗАВЕРШЁН\n"); - fprintf(stderr, "Кадров: %llu | Заголовков: %llu | CRC: %llu | RS испр: %llu | RS битых: %llu | Потери seq: %llu | Overrun: %llu | FQ drops: %llu | Вывод: %llu | Байт: %llu\n", + fprintf(stderr, "Кадров: %llu | Заголовков: %llu | CRC: %llu | RS испр: %llu | RS битых: %llu | Потери seq: %llu | Overrun: %llu | FQ drops: %llu | GF восст: %llu | GF провал: %llu | Вывод: %llu | Байт: %llu\n", (unsigned long long)calls, (unsigned long long)hdr_ok, (unsigned long long)crc_raw, (unsigned long long)rs_saved, (unsigned long long)rs_fail, (unsigned long long)lost, (unsigned long long)ring.overruns, (unsigned long long)fq.drops, + (unsigned long long)grp_recovered, (unsigned long long)grp_failed, (unsigned long long)crc_final, (unsigned long long)total_bytes); if (decoder) fec_destroy(decoder); diff --git a/src/transmitter.c b/src/transmitter.c index 01824c3..b7e3ede 100644 --- a/src/transmitter.c +++ b/src/transmitter.c @@ -6,28 +6,95 @@ */ #include "common.h" +#include "group_fec.h" // Параметры передатчика по умолчанию #define DEFAULT_GAIN -30.0 // Усиление передатчика: -30 дБ #define DEFAULT_AMP 0.2f // Амплитуда сигнала #define DEFAULT_PAUSE 2000 // Пауза между кадрами: 2000 мкс #define BUF_SAMPLES 16384 // Размер буфера в отсчётах -#define MAX_PAYLOAD 1024 // Максимальный размер полезной нагрузки +#define MAX_PAYLOAD GF_CHUNK // 1024: размер чанка данных (== GF_CHUNK) // Внешняя переменная флага завершения (определена в common.c) extern volatile sig_atomic_t stop; +// Кодирование и отправка одного кадра: CRC-поверх-RS (техдолг #3) → заголовок +// 12 Б → OFDM-сборка → push_partial → пауза. hdr11 несёт флаги group-FEC +// (0 для legacy). pl/n — полезная нагрузка (данные ≤1024 или паритет 1026). +static void tx_frame(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) { + uint8_t *tx_payload = NULL; + size_t tx_len = 0; + uint8_t hdr[HDR_SIZE] = {0}; + + if (enc) { + // CRC32 поверх исходных данных ДО RS: fec_decode() liquid не сообщает о + // неисправимых блоках — единственный надёжный критерий на RX (техдолг #3). + uint8_t framed[GF_PAR_PAY + 4]; // ≥ данные+4 и ≥ паритет 1026+4 + memcpy(framed, pl, n); + unsigned int crc = crc_generate_key(LIQUID_CRC_32, pl, n); + framed[n] = (crc >> 24) & 0xFF; + framed[n + 1] = (crc >> 16) & 0xFF; + framed[n + 2] = (crc >> 8) & 0xFF; + framed[n + 3] = crc & 0xFF; + size_t framed_n = n + 4; + + size_t total_blocks = (framed_n + RS_DATA - 1) / RS_DATA; + tx_len = total_blocks * RS_ENC; + tx_payload = malloc(tx_len); + for (size_t b = 0; b < total_blocks; b++) { + uint8_t blk[RS_DATA] = {0}; + size_t bytes = (b == total_blocks - 1) ? framed_n - b * RS_DATA : RS_DATA; + memcpy(blk, framed + b * RS_DATA, bytes); + fec_encode(enc, RS_DATA, blk, tx_payload + b * RS_ENC); + } + hdr[8] = (total_blocks >> 8) & 0xFF; hdr[9] = total_blocks & 0xFF; + hdr[10] = framed_n % RS_DATA; + } else { + tx_len = n; + tx_payload = malloc(tx_len); + memcpy(tx_payload, pl, n); + } + + hdr[0] = HDR_SIGNATURE_0; hdr[1] = HDR_SIGNATURE_1; + hdr[2] = (seq >> 24) & 0xFF; hdr[3] = (seq >> 16) & 0xFF; + hdr[4] = (seq >> 8) & 0xFF; hdr[5] = seq & 0xFF; + hdr[6] = (n >> 8) & 0xFF; hdr[7] = n & 0xFF; + hdr[11] = hdr11; // group-FEC флаги (0 = legacy) + + ofdmflexframegen_assemble(fg, hdr, tx_payload, tx_len); + free(tx_payload); + + int16_t *out = (int16_t *)iio_buffer_start(buf); + memset(out, 0, BUF_SAMPLES * 2 * sizeof(int16_t)); + int idx = 0, done = 0; + float complex symb[OFDM_M + OFDM_CP]; + while (!done && (idx + OFDM_M + OFDM_CP) <= BUF_SAMPLES) { + done = ofdmflexframegen_write(fg, symb, OFDM_M + OFDM_CP); + for (int i = 0; i < OFDM_M + OFDM_CP; i++) { + out[2 * idx] = (int16_t)(crealf(symb[i]) * 32767 * amp); + out[2 * idx + 1] = (int16_t)(cimagf(symb[i]) * 32767 * amp); + idx++; + } + } + // Только фактически заполненные idx сэмплов (техдолг #5): иначе мёртвый эфир + // и сбитый межкадровый тайминг. push_partial даёт ту же длину, что write. + if (idx > 0) iio_buffer_push_partial(buf, idx); + if (pause_us > 0) usleep(pause_us); +} + 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; + int pause_us = DEFAULT_PAUSE, use_fec = 1, group_fec = 0; // Обработка аргументов командной строки int opt; - while ((opt = getopt(argc, argv, "f:r:b:g:a:p:u:c:h")) != -1) { + while ((opt = getopt(argc, argv, "f:r:b:g:a:p:u:c:Gh")) != -1) { switch (opt) { case 'f': freq = atoll(optarg); break; case 'r': rate = atoll(optarg); break; @@ -37,12 +104,14 @@ int main(int argc, char *argv[]) { case 'p': pause_us = atoi(optarg); break; case 'u': uri = optarg; break; case 'c': use_fec = strcmp(optarg, "none") ? 1 : 0; 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, "\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); @@ -53,12 +122,14 @@ int main(int argc, char *argv[]) { // Настройка обработчиков сигналов 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, "URI: %s\n", uri); fprintf(stderr, "═══════════════════════════════════════════════════\n"); @@ -104,95 +175,70 @@ int main(int argc, char *argv[]) { fprintf(stderr, " Передатчик готов. Ожидание данных...\n\n"); - // Основной цикл передачи - 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; - } - - uint8_t *tx_payload = NULL; - size_t tx_len = 0; - uint8_t hdr[HDR_SIZE] = {0}; - - // Кодирование данных - if (enc) { - // CRC32 поверх исходных данных ДО RS: fec_decode() liquid не - // сообщает о неисправимых блоках, поэтому единственный надёжный - // критерий целостности на RX — этот CRC поверх декода (техдолг #3). - uint8_t framed[MAX_PAYLOAD + 4]; - memcpy(framed, raw, n); - unsigned int crc = crc_generate_key(LIQUID_CRC_32, raw, n); - framed[n] = (crc >> 24) & 0xFF; - framed[n + 1] = (crc >> 16) & 0xFF; - framed[n + 2] = (crc >> 8) & 0xFF; - framed[n + 3] = crc & 0xFF; - size_t framed_n = n + 4; - - size_t total_blocks = (framed_n + RS_DATA - 1) / RS_DATA; - tx_len = total_blocks * RS_ENC; - tx_payload = malloc(tx_len); - - for (size_t b = 0; b < total_blocks; b++) { - uint8_t blk[RS_DATA] = {0}; - size_t bytes = (b == total_blocks - 1) ? framed_n - b * RS_DATA : RS_DATA; - memcpy(blk, framed + b * RS_DATA, bytes); - fec_encode(enc, RS_DATA, blk, tx_payload + b * RS_ENC); + // Основной цикл передачи. 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; } - - // Заголовок: len = исходные n (без 4 байт CRC), nblocks/last по framed - hdr[0] = HDR_SIGNATURE_0; hdr[1] = HDR_SIGNATURE_1; - hdr[2] = (seq >> 24) & 0xFF; hdr[3] = (seq >> 16) & 0xFF; - hdr[4] = (seq >> 8) & 0xFF; hdr[5] = seq & 0xFF; - hdr[6] = (n >> 8) & 0xFF; hdr[7] = n & 0xFF; - hdr[8] = (total_blocks >> 8) & 0xFF; hdr[9] = total_blocks & 0xFF; - hdr[10] = framed_n % RS_DATA; hdr[11] = 0; - } else { - tx_len = n; - tx_payload = malloc(tx_len); - memcpy(tx_payload, raw, n); - - hdr[0] = HDR_SIGNATURE_0; hdr[1] = HDR_SIGNATURE_1; - hdr[2] = (seq >> 24) & 0xFF; hdr[3] = (seq >> 16) & 0xFF; - hdr[4] = (seq >> 8) & 0xFF; hdr[5] = seq & 0xFF; - hdr[6] = (n >> 8) & 0xFF; hdr[7] = n & 0xFF; - } - - // Сборка OFDM-кадра - ofdmflexframegen_assemble(fg, hdr, tx_payload, tx_len); - free(tx_payload); - - // Формирование выходного сигнала - int16_t *out = (int16_t *)iio_buffer_start(buf); - memset(out, 0, BUF_SAMPLES * 2 * sizeof(int16_t)); - int idx = 0, done = 0; - float complex symb[OFDM_M + OFDM_CP]; - - while (!done && (idx + OFDM_M + OFDM_CP) <= BUF_SAMPLES) { - done = ofdmflexframegen_write(fg, symb, OFDM_M + OFDM_CP); - for (int i = 0; i < OFDM_M + OFDM_CP; i++) { - out[2 * idx] = (int16_t)(crealf(symb[i]) * 32767 * amp); - out[2 * idx + 1] = (int16_t)(cimagf(symb[i]) * 32767 * amp); - idx++; + tx_frame(fg, buf, enc, seq++, 0, raw, n, amp, pause_us); + frames++; bytes_tx += n; + if (time(NULL) > last) { + fprintf(stderr, "\r📤 Передано: %llu кадров, %llu байт", + (unsigned long long)frames, (unsigned long long)bytes_tx); + fflush(stderr); + last = time(NULL); } } - // Пушим только фактически заполненные idx сэмплов, а не весь буфер - // 16384 (техдолг #5): иначе ~35% мёртвого эфира на кадр и сбитый - // межкадровый тайминг. push_partial даёт ту же длину, что write. - if (idx > 0) iio_buffer_push_partial(buf, idx); + } 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; - // Обновление статистики - frames++; bytes_tx += n; seq++; - if (time(NULL) > last) { - fprintf(stderr, "\r📤 Передано: %llu кадров, %llu байт", - (unsigned long long)frames, (unsigned long long)bytes_tx); - fflush(stderr); - last = time(NULL); + 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_frame(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′ + длина хвоста + 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_frame(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_frame(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) { + fprintf(stderr, "\r📤 Передано: %llu кадров, %llu байт (group-FEC)", + (unsigned long long)frames, (unsigned long long)bytes_tx); + fflush(stderr); + last = time(NULL); + } + } } - if (pause_us > 0) usleep(pause_us); } fprintf(stderr, "\nПередача завершена: %llu кадров, %llu байт\n",