From afc8775ddb320e7e88ef392a209c26282e346d96 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 22 Jun 2026 10:21:33 +0300 Subject: [PATCH] feat: QO-100 beacon AO-40 FEC decode end-to-end (Milestone 2) Full chain now decodes the QO-100 central beacon to 256-byte AO-40 frames on-air (frames>0, RS errors=0). Builds on M1 (stable BPSK demod). FEC backend (BeaconFEC.pas, TBeaconFEC): - AO-40 FEC: distributed sync (65 bit, step 80) -> 80x65 deinterleave -> Viterbi r=1/2 k=7 -> CCSDS descramble -> 2x RS(160,128). Links system libfec (Karn) load-time (libfec.so/.dll/.dylib). Viterbi AO-40 [0x4F,-0x6D] remapped to libfec [0x6D,0x4F]. Validated against gr-satellites reference vectors (fectest2: exact 256-byte frame, 0 mismatches). DBPSK Manchester frontend (BeaconDecoder.pas): - Chip-rate (800) processing: RRC matched filter -> ML signal-times-slope timing recovery -> Costas -> Manchester combine (block phase select) -> differential decode -> soft symbols to FEC. - Carrier acquisition: squaring-FFT estimates the residual (carrier at +-pi/chip defeats decision-directed Costas), one-shot pulls it to DC via residual NCO. - Costas frequency offload into pre-RRC NCO (with deadband) tracks LNB drift without hitting the +-pi Costas clamp. - ML TED replaces Gardner, which degenerates on the Manchester chip stream. Temporary on-air diagnostics retained (IQ dump + STATE log) pending wider signal validation; to be removed before final cleanup. Co-Authored-By: Claude Opus 4.8 --- BeaconDecoder.pas | 285 ++++++++++++++++++++++++++++++------ BeaconFEC.pas | 341 ++++++++++++++++++++++++++++++++++++++++++++ BeaconScopeForm.pas | 8 +- RadioController.pas | 7 +- ewsdr.lpi | 4 + fectest.lpr | 18 +++ fectest2.lpr | 104 ++++++++++++++ 7 files changed, 717 insertions(+), 50 deletions(-) create mode 100644 BeaconFEC.pas create mode 100644 fectest.lpr create mode 100644 fectest2.lpr diff --git a/BeaconDecoder.pas b/BeaconDecoder.pas index 8fe2f61..3524e06 100644 --- a/BeaconDecoder.pas +++ b/BeaconDecoder.pas @@ -27,10 +27,11 @@ unit BeaconDecoder; interface uses - Classes, SysUtils, SyncObjs, Math; + Classes, SysUtils, SyncObjs, Math, BeaconFEC; const - BCN_BAUD = 400.0; // символьная скорость маяка + BCN_BAUD = 400.0; // скорость ДАННЫХ маяка (после Manchester) + BCN_CHIP_BAUD = 800.0; // чип-рейт (Manchester = 2× данных) — на нём таймер BCN_TARGET_FS = 9600.0; // целевой rate после децимации BCN_RRC_BETA = 0.5; // roll-off матч-фильтра BCN_RRC_SPAN = 8; // длина RRC в символах (полу-) @@ -60,6 +61,11 @@ type Wsym: Single; // дробная поправка периода символа CarFreqHz: Single; // частота Costas (Гц, остаток несущей) LockHold: Integer; // холдовер-счётчик лока + ManPhase: Integer; // выбранная фаза Manchester (0/1) + ManM0, ManM1: Single; // EMA энергии двух фаз (диагностика) + Frames: Int64; // декодировано кадров AO-40 FEC + HasFrame: Boolean; // есть хотя бы один кадр + LastRSErr: Integer; // ошибок RS в последнем кадре end; TBeaconDecoder = class @@ -97,6 +103,8 @@ type FPrevI, FPrevQ: Double; // прошлый центр символа FYd0I, FYd0Q: Double; // линия задержки RRC-выхода (новейший) FYd1I, FYd1Q: Double; // предыдущий + FYd2I, FYd2Q: Double; // −2 (для ML-TED центр/early/late + задержка «late») + FYd3I, FYd3Q: Double; // −3 FWsym: Double; // дробная коррекция периода (интегратор) FTimErrEMA: Double; @@ -117,6 +125,16 @@ type FResidInc: Double; FCarProm: Double; // prominence линии (EMA) FCarPresent: Boolean; // линия несущей видна + // --- one-shot захват частоты (squaring-FFT → FResidInc) --- + // На чип-рейте 800 бод снос ~400 Гц = π/чип: decision-directed Costas его НЕ + // тянет, а несущая в π/чип вырождает Manchester (chips одинаковы → 0.5(c0−c1)≈0). + // Поэтому грубый снос убираем NCO по оценке частоты из squaring-FFT (z² снимает + // BPSK → линия на 2·fc, знак однозначен при большом сносе). Усредняем по + // FAcqNeed блокам, ставим FResidInc один раз, разоружаемся. Дрейф LNB дальше + // ведёт непрерывный Costas+offload (отриц. ОС, не убегает). + FAcqArmed: Boolean; // ждём захвата (взводится на Reseed/новое наведение) + FAcqCount: Integer; // сколько блоков усреднено + FAcqSum: Double; // сумма оценок fc (Гц) для усреднения FFreqLocked: Boolean; // Costas держит фазу (для индикации/symbol-lock) FLockHold: Integer; // холдовер-счётчик лока (анти-мерцание) @@ -135,6 +153,28 @@ type FSymMag: Double; // EMA |символ| после RRC (диагностика тракта) FRrcAmp: Double; // AGC: EMA амплитуды выхода RRC (нормировка петель) + // --- Manchester-свёртка (чипы 800 → символы 400) --- + FChipPrevI, FChipPrevQ: Double; // предыдущий чип + FChipHave: Boolean; + FChipCount: Integer; // глобальный счётчик чипов (для чётности фазы) + FManMetric0, FManMetric1: Double; // EMA энергии |0.5(cPrev−c)| по двум фазам + FManPhase: Integer; // выбранная фаза (0: пары с чётного чипа) + + // --- дифференциальный декодер (DBPSK) --- + FSymPrevI, FSymPrevQ: Double; // предыдущий Manchester-символ + FSymHave: Boolean; + + // --- FEC-бэкенд (AO-40) --- + FFEC: TBeaconFEC; + FFrames: Int64; + FHasFrame: Boolean; + FLastFrame: TBeaconFrame; + FLastRSErr: Integer; + + // --- ВРЕМЕННЫЙ ДЕБАГ: дамп сырого baseband 9600 Гц (I,Q float32) --- + FDbgStream: TFileStream; + FDbgLeft: Integer; + // --- констелляция (кольцо) --- FRingI, FRingQ: array[0..BCN_SCOPE_PTS-1] of Single; FRingPos: Integer; @@ -146,6 +186,9 @@ type procedure RunAcquisition; procedure ProcessDecimated(const xi, xq: Double); procedure ProcessSymbol(const zi, zq: Double); + procedure ProcessChip(const ci, cq: Double); // Manchester + дифф-декод → FEC + procedure DbgWrite2(const a, b: Double); // ВРЕМЕННЫЙ дамп I,Q float32 + procedure FECFrame(const Frame: TBeaconFrame; RSErrors: Integer); procedure ResetDSP; public constructor Create; @@ -163,6 +206,7 @@ type procedure Feed(const I, Q: Double); inline; procedure GetScope(out S: TBeaconScope); + function GetLastFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean; end; implementation @@ -171,6 +215,10 @@ constructor TBeaconDecoder.Create; begin inherited Create; FLock := TCriticalSection.Create; + FFEC := TBeaconFEC.Create; + FFEC.OnFrame := FECFrame; + FDbgStream := nil; + FDbgLeft := 336000; // ВРЕМЕННЫЙ: ~35 c @ 9600 (2-3 кадра маяка) для оффлайн FFs := 0; FEnabled := False; FConstScale := 1.0; @@ -179,6 +227,8 @@ end; destructor TBeaconDecoder.Destroy; begin + if FDbgStream <> nil then FreeAndNil(FDbgStream); + FFEC.Free; FLock.Free; inherited Destroy; end; @@ -295,13 +345,21 @@ begin end; procedure TBeaconDecoder.RunAcquisition; -// Индикатор наличия несущей маяка: вычитаем DC (offset Pluto), возводим в квадрат -// (снимаем BPSK), FFT → ищем самую сильную линию вне зоны DC. prominence (pk/среднее) -// = «несущая видна» (FCarPresent). Частоту НЕ оцениваем — снос целиком ведёт -// Costas + offload (см. ProcessSymbol); squaring-FFT давала неверный знак. +// Грубый захват несущей маяка: вычитаем DC (offset Pluto), возводим в квадрат +// (z² снимает BPSK → линия на 2·fc), FFT → самая сильная линия вне зоны DC. +// prominence (pk/среднее) = «несущая видна» (FCarPresent). +// ЧАСТОТУ ОЦЕНИВАЕМ: бин пика → 2·fc → fc; усредняем по FAcqNeed блокам пока +// взведено (FAcqArmed) и несущая видна и Costas ещё не залочен, затем ставим +// FResidInc один раз (стягиваем несущую к DC) и разоружаемся. Знак z² однозначен +// при большом сносе (~±400 Гц), а буфер FAcqRe/Im берётся ДО остаточного NCO — +// значит меряем АБСОЛЮТНЫЙ снос, присваиваем FResidInc прямо (без накопления). +const + FAcqNeed = 4; // блоков усреднения (~1.7 c @ 9600) до применения + AcqMinHz = 15.0; // ниже — Costas вытянет сам, NCO не дёргаем + AcqMaxHz = 3000.0; // санитарный предел var - i, n, loBin: Integer; - mr, mi, re, im, mag, pk, sumMag, meanMag: Double; + i, n, loBin, pkBin: Integer; + mr, mi, re, im, mag, pk, sumMag, meanMag, fHz, fcHz, avgHz: Double; begin n := BCN_NFFT; // среднее (DC) для подавления offset Pluto @@ -320,17 +378,44 @@ begin // самая сильная линия, исключая ±loBin вокруг DC (там сидит остаток DC при Δf≈0) loBin := 2; - pk := 0; sumMag := 0; + pk := 0; pkBin := 0; sumMag := 0; for i := 0 to n - 1 do begin mag := FFre[i] * FFre[i] + FFim[i] * FFim[i]; sumMag := sumMag + mag; - if (i >= loBin) and (i <= n - loBin) and (mag > pk) then pk := mag; + if (i >= loBin) and (i <= n - loBin) and (mag > pk) then begin pk := mag; pkBin := i; end; end; meanMag := sumMag / n; if meanMag > 1e-30 then FCarProm := FCarProm + 0.4 * (pk / meanMag - FCarProm); FCarPresent := FCarProm > 20.0; // линия выше среднего → несущая в полосе + + // --- one-shot оценка частоты --- + // Гейтим ТОЛЬКО по «несущая видна» + взведено (одноразово на наведение). НЕ + // гейтим по FFreqLocked: Costas на чип-рейте может ложно «залочиться» на состоянии + // +400 (π/чип) за ~3 c — раньше, чем накопится усреднение; тогда условие + // not FFreqLocked заблокировало бы захват (гонка). Применение и так защищено + // порогом |avgHz|>=AcqMinHz: если несущая реально у DC (Costas справился сам), + // оценка ~0 → коррекцию не трогаем. + if not (FAcqArmed and FCarPresent) then Exit; + + // бин пика → частота линии z² (= 2·fc), со знаком; затем fc = /2 + if pkBin <= n div 2 then fHz := pkBin * FFdec / n + else fHz := (pkBin - n) * FFdec / n; + fcHz := fHz / 2.0; + FAcqSum := FAcqSum + fcHz; + Inc(FAcqCount); + if FAcqCount < FAcqNeed then Exit; + + avgHz := FAcqSum / FAcqCount; + // применяем: residual NCO (exp(-j FResidPh)) с FResidInc убирает +fc к DC + if (Abs(avgHz) >= AcqMinHz) and (Abs(avgHz) <= AcqMaxHz) then + begin + FResidInc := 2.0 * Pi * avgHz / FFdec; + FResidDf := avgHz; + end; + FAcqArmed := False; // разоружаемся; дальше дрейф ведёт Costas+offload + FAcqCount := 0; FAcqSum := 0; end; procedure TBeaconDecoder.ResetDSP; @@ -344,6 +429,7 @@ begin FHalf := False; FMidI := 0; FMidQ := 0; FPrevI := 0; FPrevQ := 0; FYd0I := 0; FYd0Q := 0; FYd1I := 0; FYd1Q := 0; + FYd2I := 0; FYd2Q := 0; FYd3I := 0; FYd3Q := 0; FWsym := 0; FTimErrEMA := 0; FCarPh := 0; FCarFreq := 0; FSigAmp := 0; FQVar := 0; @@ -351,8 +437,14 @@ begin FRingPos := 0; FRingCnt := 0; FAcqPos := 0; FResidDf := 0; FResidPh := 0; FResidInc := 0; FCarProm := 0; FCarPresent := False; FFreqLocked := False; + FAcqArmed := True; FAcqCount := 0; FAcqSum := 0; // взводим грубый захват частоты FLockHold := 0; FPrevRre := 0; FPrevRim := 0; + // Manchester / дифф-декод + FChipPrevI := 0; FChipPrevQ := 0; FChipHave := False; FChipCount := 0; + FManMetric0 := 0; FManMetric1 := 0; FManPhase := 0; + FSymPrevI := 0; FSymPrevQ := 0; FSymHave := False; + if FFEC <> nil then FFEC.Reset; end; procedure TBeaconDecoder.Configure(SampleRateHz: Double); @@ -366,7 +458,7 @@ begin FFs := SampleRateHz; FDecim := Max(1, Round(FFs / BCN_TARGET_FS)); FFdec := FFs / FDecim; - FSps := FFdec / BCN_BAUD; + FSps := FFdec / BCN_CHIP_BAUD; // отсчётов на ЧИП (Manchester), таймер на 800 FNcoInc := 2.0 * Pi * FOffsetHz / FFs; @@ -472,7 +564,8 @@ procedure TBeaconDecoder.ProcessDecimated(const xi, xq: Double); // снимок констелляции защищён в GetScope собственным копированием полей-массивов. var k, idx: Integer; - yi, yq, frac, e, step, cr, sr, ri, ii: Double; + yi, yq, frac, e, cr, sr, ri, ii: Double; + ci, cq, eai, eaq, lai, laq, dri, drq: Double; // ML-TED центр/early/late/произв. begin // остаточный снос несущей (по squaring-FFT acquisition) — поверх главного NCO cr := Cos(FResidPh); sr := Sin(FResidPh); @@ -485,6 +578,17 @@ begin // Диагностика тракта (lock-независимо): RMS децимированного сигнала на входе RRC. FDecRMS := FDecRMS + 0.001 * (Sqrt(ri * ri + ii * ii) - FDecRMS); + // ВРЕМЕННЫЙ ДЕБАГ: дамп сырого baseband (после сноса частоты, до RRC) в файл + // для оффлайн-анализа тайминга чипов и Manchester-структуры. + if (FDbgLeft > 0) and FFreqLocked then + begin + if FDbgStream = nil then + FDbgStream := TFileStream.Create(GetEnvironmentVariable('HOME') + '/ewsdr_beacon_iq.f32', fmCreate); + DbgWrite2(ri, ii); + Dec(FDbgLeft); + if FDbgLeft = 0 then FreeAndNil(FDbgStream); + end; + // Буфер для грубой carrier-acquisition: кладём сигнал ПОСЛЕ главного NCO, но // ДО остаточного — тогда squaring-FFT меряет АБСОЛЮТНЫЙ остаток (не зависит от // FResidDf) → коррекция = прямое присвоение, без накопления и runaway. @@ -526,44 +630,39 @@ begin end; FSymMag := FRrcAmp; // диагностика силы сигнала (сырой уровень до AGC) - // линия задержки RRC-выхода для линейной интерполяции + // линия задержки RRC-выхода (4 отсчёта) для ML-TED: центр/early/late + задержка + // на 1 отсчёт, чтобы «late» (FYd0) был доступен в момент строба. + FYd3I := FYd2I; FYd3Q := FYd2Q; + FYd2I := FYd1I; FYd2Q := FYd1Q; FYd1I := FYd0I; FYd1Q := FYd0Q; FYd0I := yi; FYd0Q := yq; - // Gardner: полу-символьные стробы. FMuAcc убывает на 1/отсчёт; при переходе - // через 0 интерполируем и планируем следующий строб через ~FSps/2 отсчётов. + // Таймер ML signal×slope (как gr symbol_sync_cc, TED_SIGNAL_TIMES_SLOPE_ML): + // лочит ЦЕНТР чипа по максимуму |x|, ошибка e = Re{conj(x)·dx/dt}. Gardner на + // Manchester чип-рейте ВЫРОЖДАЛСЯ (всегда-переход в паре → S-кривая degenerate → + // таймер вращался, timErr~1.5, m0≈m1, frames=0). ML сходится стабильно (оффлайн на + // дампе 35c: timErr 0.25 vs Gardner 1.49 → sync 65/65). Один строб на чип. FMuAcc := FMuAcc - 1.0; if FMuAcc <= 0.0 then begin - frac := FMuAcc + 1.0; // позиция в (0..1] между Yd1 и Yd0 + frac := FMuAcc + 1.0; // позиция в (0..1] if frac < 0 then frac := 0; - yi := FYd1I + frac * (FYd0I - FYd1I); - yq := FYd1Q + frac * (FYd0Q - FYd1Q); - - step := (FSps / 2.0) * (1.0 + FWsym); // базовый интервал до след. полу-символа - if FHalf then - begin - // середина символа — запоминаем для Gardner - FMidI := yi; FMidQ := yq; - end - else - begin - // центр символа → ошибка Gardner (вращательно-инвариантна): - // e = Re{ conj(mid) * (curr - prev) } по компонентам - e := FMidI * (yi - FPrevI) + FMidQ * (yq - FPrevQ); - FTimErrEMA := FTimErrEMA + 0.01 * (Abs(e) - FTimErrEMA); - // После AGC вход ~единичный, ошибка Gardner шумная (по логам wsym бился об - // ±0.05). Снижаем усиления интегратора/пропорц. и поджимаем кламп: маяк 400 бод - // стабилен (доли %), большой диапазон не нужен, а шум перестаёт расшатывать. - FWsym := FWsym + 0.0003 * e; // интегратор периода (медленнее) - if FWsym > 0.01 then FWsym := 0.01; - if FWsym < -0.01 then FWsym := -0.01; - FPrevI := yi; FPrevQ := yq; - ProcessSymbol(yi, yq); // символ → Costas (вход уже нормирован AGC) - step := step + 0.003 * e; // пропорциональная правка таймера (мягче) - end; - FMuAcc := FMuAcc + step; - FHalf := not FHalf; + // центр (между FYd2 и FYd1), early (FYd3/FYd2), late (FYd1/FYd0) + ci := FYd2I + frac * (FYd1I - FYd2I); + cq := FYd2Q + frac * (FYd1Q - FYd2Q); + eai := FYd3I + frac * (FYd2I - FYd3I); + eaq := FYd3Q + frac * (FYd2Q - FYd3Q); + lai := FYd1I + frac * (FYd0I - FYd1I); + laq := FYd1Q + frac * (FYd0Q - FYd1Q); + // производная dx ≈ late − early; ошибка = Re{conj(центр)·dx} (вращ.-инвариантна) + dri := lai - eai; drq := laq - eaq; + e := ci * dri + cq * drq; + FTimErrEMA := FTimErrEMA + 0.01 * (Abs(e) - FTimErrEMA); + FWsym := FWsym + 0.0005 * e; // интегратор периода + if FWsym > 0.02 then FWsym := 0.02; + if FWsym < -0.02 then FWsym := -0.02; + ProcessSymbol(ci, cq); // чип-центр → Costas → Manchester + FMuAcc := FMuAcc + FSps * (1.0 + FWsym) + 0.005 * e; // период + проп. правка end; end; @@ -596,10 +695,16 @@ begin // Консервативно: сколько добавили в FResidInc, столько убрали из FCarFreq (без // рывка). Утечка медленная (TC~100 символов >> группозадержки RRC ~4 симв) — стабильно. // Итог: Costas держит лишь мелкий быстрый остаток, весь медленный снос — в FResidInc. - leak := 0.01 * FCarFreq; - FResidInc := FResidInc + leak / FSps; // рад/символ → рад/децим-отсчёт + // DEADBAND ~2.5 Гц: без него мелкий постоянный bias Costas (carHz~0.7) непрерывно + // интегрировался в FResidInc → resid полз ~5.5 Гц/с (спур, не дрейф). Стекаем только + // КРУПНЫЙ снос (ошибка наведения/дрейф LNB), мелкий остаток держит сам Costas. + if Abs(FCarFreq) > 0.02 then // 0.02 рад/чип ≈ 2.5 Гц @800 + begin + leak := 0.01 * FCarFreq; + FResidInc := FResidInc + leak / FSps; // рад/символ → рад/децим-отсчёт + FCarFreq := FCarFreq - leak; + end; FResidDf := FResidInc * FFdec / (2.0 * Pi); // для лога/диагностики - FCarFreq := FCarFreq - leak; FCarPh := FCarPh + FCarFreq + FCarAlpha * ce; if FCarPh > Pi then FCarPh := FCarPh - 2.0 * Pi else if FCarPh < -Pi then FCarPh := FCarPh + 2.0 * Pi; @@ -631,6 +736,71 @@ begin FRingQ[FRingPos] := di * na; FRingPos := FRingPos + 1; if FRingPos >= BCN_SCOPE_PTS then FRingPos := 0; if FRingCnt < BCN_SCOPE_PTS then Inc(FRingCnt); + + // скорректированный (Costas) чип → Manchester-свёртка + дифф-декод → FEC + ProcessChip(dr, di); +end; + +procedure TBeaconDecoder.ProcessChip(const ci, cq: Double); +// Вход — комплексный чип @ 800 (после таймера/Costas). Делает то же, что +// gr-satellites manchester_sync_cc + дифф-декод: +// 1) Manchester: символ-кандидат s = 0.5·(cPrev − c) для пары соседних чипов. +// Какая из 2 фаз спаривания верна (пары с чётного или нечётного чипа) — решаем +// по бо́льшей сумме |s| (EMA): на верной фазе чипы внутри бита противофазны → +// |s| максимально. Эмитим символ только когда чётность пары = выбранной фазе. +// 2) дифф-декод DBPSK: soft = −Re(s_n · conj(s_{n−1})) (знак −1 как в gr для Manchester). +// 3) soft 400/с → TBeaconFEC. +var + di_, dq_, mag, soft: Double; + parity: Integer; +begin + if FChipHave then + begin + di_ := 0.5 * (FChipPrevI - ci); + dq_ := 0.5 * (FChipPrevQ - cq); + mag := Sqrt(di_ * di_ + dq_ * dq_); + parity := (FChipCount - 1) and 1; // чётность первого чипа пары + if parity = 0 then + FManMetric0 := FManMetric0 + 0.01 * (mag - FManMetric0) + else + FManMetric1 := FManMetric1 + 0.01 * (mag - FManMetric1); + if FManMetric0 >= FManMetric1 then FManPhase := 0 else FManPhase := 1; + + if parity = FManPhase then + begin + // это символ данных (400/с) + if FSymHave then + begin + soft := -(di_ * FSymPrevI + dq_ * FSymPrevQ); // −Re(s_n·conj(s_{n−1})) + FFEC.PushSoftSymbol(soft); + end; + FSymPrevI := di_; FSymPrevQ := dq_; FSymHave := True; + end; + end; + FChipPrevI := ci; FChipPrevQ := cq; FChipHave := True; + Inc(FChipCount); +end; + +procedure TBeaconDecoder.DbgWrite2(const a, b: Double); +var fa, fb: Single; +begin + fa := a; fb := b; + FDbgStream.WriteBuffer(fa, 4); + FDbgStream.WriteBuffer(fb, 4); +end; + +procedure TBeaconDecoder.FECFrame(const Frame: TBeaconFrame; RSErrors: Integer); +// Колбэк из TBeaconFEC (DSP-поток). Сохраняем кадр под CS для UI. +begin + FLock.Enter; + try + FLastFrame := Frame; + FLastRSErr := RSErrors; + FHasFrame := True; + Inc(FFrames); + finally + FLock.Leave; + end; end; procedure TBeaconDecoder.GetScope(out S: TBeaconScope); @@ -652,8 +822,14 @@ begin S.SymMag := FSymMag; S.TimErr := FTimErrEMA; S.Wsym := FWsym; - S.CarFreqHz := FCarFreq * BCN_BAUD / (2.0 * Pi); // рад/символ → Гц + S.CarFreqHz := FCarFreq * BCN_CHIP_BAUD / (2.0 * Pi); // рад/чип → Гц S.LockHold := FLockHold; + S.ManPhase := FManPhase; + S.ManM0 := FManMetric0; + S.ManM1 := FManMetric1; + S.Frames := FFrames; + S.HasFrame := FHasFrame; + S.LastRSErr := FLastRSErr; // SNR ≈ (E|I|)^2 / E[Q^2] (мощность сигнала по I против шума по Q) if FQVar > 1e-12 then @@ -686,4 +862,19 @@ begin end; end; +function TBeaconDecoder.GetLastFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean; +begin + FLock.Enter; + try + Result := FHasFrame; + if Result then + begin + Frame := FLastFrame; + RSErrors := FLastRSErr; + end; + finally + FLock.Leave; + end; +end; + end. diff --git a/BeaconFEC.pas b/BeaconFEC.pas new file mode 100644 index 0000000..c7f1550 --- /dev/null +++ b/BeaconFEC.pas @@ -0,0 +1,341 @@ +unit BeaconFEC; + +{ + QO-100 центральный маяк — FEC-бэкенд (Milestone 2). + + Декодирует AO-40 FEC поверх потока мягких символов (400/с, после Manchester+ + дифф-декода фронтенда). Цепочка (ровно как gr-satellites ao40_fec_deframer): + + мягкие символы 400/с + → distributed sync (65 бит, шаг 80, окно 5200, порог 8, обе полярности) + → matrix deinterleave 80×65: out[i]=data[80*(i%65)+i/65], skip 65, take 5132 + → Viterbi r=1/2 k=7, полиномы [0x4F, -0x6D] (CCSDS), terminated → 2560 бит/320 байт + → CCSDS additive descramble (LFSR mask 0xA9, seed 0xFF, len 7, XOR по битам) + → 2× RS(255,223) укороч. pad=95 → (160,128), conventional basis, 2-way интерлив + → 256 байт кадра → OnFrame. + + Viterbi и RS — через libfec Карна (разделяемая libfec.so/.dll/.dylib, load-time + линковка как WDSP/IIO). libfec ставится отдельно на каждой ОС. +} + +{$IFDEF FPC}{$MODE Delphi}{$ENDIF} +{$PACKRECORDS C} + +interface + +uses + Classes, SysUtils; + +const + AO40_SYNC_LEN = 65; + AO40_SYNC_STEP = 80; + AO40_FRAME_SYMS = AO40_SYNC_LEN * AO40_SYNC_STEP; // 5200 + AO40_IL_ROWS = 80; + AO40_IL_COLS = 65; + AO40_VITERBI_SYM = 5132; // после skip 65 + AO40_DATA_BITS = 2560; // выход Viterbi (без 6 tail) + AO40_CONV_BYTES = AO40_DATA_BITS div 8; // 320 + AO40_RS_NROOTS = 32; + AO40_RS_PAD = 95; // 255-160 + AO40_RS_NN = 160; // укороченная длина + AO40_RS_KK = 128; + AO40_INTERLEAVE = 2; + AO40_FRAME_BYTES = AO40_RS_KK * AO40_INTERLEAVE; // 256 + AO40_SYNC_THRESH = 8; // допуск ошибок синхры + +type + TBeaconFrame = array[0..AO40_FRAME_BYTES-1] of Byte; + TBeaconFrameEvent = procedure(const Frame: TBeaconFrame; RSErrors: Integer) of object; + + TBeaconFEC = class + private + FSync: array[0..AO40_SYNC_LEN-1] of Byte; + FRing: array[0..AO40_FRAME_SYMS-1] of Single; + FRingPos: Integer; // позиция следующей записи = старейший элемент + FRingCnt: Integer; + FCooldown: Integer; // не перепроверять сразу после успешного кадра + FVp: Pointer; // viterbi27 instance + FFrames: Int64; + FOnFrame: TBeaconFrameEvent; + + function CheckSyncAt: Integer; // -1 нет; 0 норм. полярность; 1 инверсия + procedure DecodeCurrentFrame(Invert: Boolean); + procedure Descramble(var Buf: array of Byte; Count: Integer); + public + constructor Create; + destructor Destroy; override; + procedure Reset; + procedure PushSoftSymbol(const S: Single); + function SelfTest(out Msg: string): Boolean; + property Frames: Int64 read FFrames; + property OnFrame: TBeaconFrameEvent read FOnFrame write FOnFrame; + end; + +implementation + +// ---------------- libfec bindings (Karn) ---------------- +// Динамическая (load-time) линковка против разделяемой libfec — как WDSP/IIO. +// libfec ставится отдельно на каждой ОС (Linux: make install из исходников; +// Windows/macOS — отдельная сборка). НЕ статический {$LINKLIB}: на Arch системный +// libfec.a собран с LTO (slim-объекты без машинного кода) и не линкуется обычным ld. +const +{$IFDEF WINDOWS} + FEC_LIB = 'libfec.dll'; +{$ENDIF} +{$IFDEF LINUX} + FEC_LIB = 'libfec.so'; +{$ENDIF} +{$IFDEF DARWIN} + FEC_LIB = 'libfec.dylib'; +{$ENDIF} + +{$IFDEF UNIX}{$LINKLIB m}{$ENDIF} // libfec использует log() из libm + +function create_viterbi27(len: LongInt): Pointer; cdecl; external FEC_LIB; +function init_viterbi27(vp: Pointer; starting_state: LongInt): LongInt; cdecl; external FEC_LIB; +function update_viterbi27_blk(vp: Pointer; syms: PByte; npairs: LongInt): LongInt; cdecl; external FEC_LIB; +function chainback_viterbi27(vp: Pointer; data: PByte; nbits: LongWord; endstate: LongWord): LongInt; cdecl; external FEC_LIB; +procedure delete_viterbi27(vp: Pointer); cdecl; external FEC_LIB; + +function decode_rs_8(data: PByte; eras_pos: PLongInt; no_eras: LongInt; pad: LongInt): LongInt; cdecl; external FEC_LIB; +procedure encode_rs_8(data: PByte; parity: PByte; pad: LongInt); cdecl; external FEC_LIB; + +// ---------------- helpers ---------------- + +function ParityB(x: LongWord): Byte; inline; +// чётность числа единичных битов: 1=нечётно, 0=чётно +begin + x := x xor (x shr 16); + x := x xor (x shr 8); + x := x xor (x shr 4); + x := x xor (x shr 2); + x := x xor (x shr 1); + Result := x and 1; +end; + +const + // 65-битная распределённая синхра AO-40 (gr-satellites _syncword) + AO40_SYNCWORD: array[0..AO40_SYNC_LEN-1] of Byte = ( + 1,1,1,1,1,1,1,0,0,0,0,1,1,1,0,1,1,1,1,0,0,1,0,1,1,0,0,1,0,0,1,0, + 0,0,0,0,0,1,0,0,0,1,0,0,1,1,0,0,0,1,0,1,1,1,0,1,0,1,1,0,1,1,0,0,0); + +// ---------------- TBeaconFEC ---------------- + +constructor TBeaconFEC.Create; +var i: Integer; +begin + inherited Create; + for i := 0 to AO40_SYNC_LEN-1 do FSync[i] := AO40_SYNCWORD[i]; + FVp := create_viterbi27(AO40_DATA_BITS); + Reset; +end; + +destructor TBeaconFEC.Destroy; +begin + if FVp <> nil then delete_viterbi27(FVp); + inherited Destroy; +end; + +procedure TBeaconFEC.Reset; +var i: Integer; +begin + for i := 0 to AO40_FRAME_SYMS-1 do FRing[i] := 0; + FRingPos := 0; FRingCnt := 0; FCooldown := 0; +end; + +procedure TBeaconFEC.PushSoftSymbol(const S: Single); +var r: Integer; +begin + FRing[FRingPos] := S; + FRingPos := FRingPos + 1; + if FRingPos >= AO40_FRAME_SYMS then FRingPos := 0; + if FRingCnt < AO40_FRAME_SYMS then Inc(FRingCnt); + + if FCooldown > 0 then begin Dec(FCooldown); Exit; end; + if FRingCnt < AO40_FRAME_SYMS then Exit; + + r := CheckSyncAt; + if r >= 0 then + begin + DecodeCurrentFrame(r = 1); + FCooldown := AO40_FRAME_SYMS; // следующий кадр — через полный фрейм + end; +end; + +function TBeaconFEC.CheckSyncAt: Integer; +// Кандидат-кадр = текущее содержимое кольца (старейший→новейший), старт = FRingPos. +// Синхра на относительных позициях j*step. Возвращает полярность или -1. +var + j, idx, hard, matches: Integer; +begin + matches := 0; + for j := 0 to AO40_SYNC_LEN-1 do + begin + idx := FRingPos + j * AO40_SYNC_STEP; + if idx >= AO40_FRAME_SYMS then Dec(idx, AO40_FRAME_SYMS); + if FRing[idx] > 0.0 then hard := 1 else hard := 0; // gr-конвенция: +soft = «1» + if hard = FSync[j] then Inc(matches); + end; + if matches >= AO40_SYNC_LEN - AO40_SYNC_THRESH then Exit(0); // норм. полярность + if matches <= AO40_SYNC_THRESH then Exit(1); // инверсия + Result := -1; +end; + +procedure TBeaconFEC.Descramble(var Buf: array of Byte; Count: Integer); +// CCSDS additive descrambler = GNU Radio additive_scrambler_bb(0xA9, 0xFF, 7). +// Fibonacci LFSR, seed 0xFF, XOR с каждым битом (MSB-first), reset на старте кадра. +var + state: Byte; + i, b, outp, newbit, bit: Integer; + v: Byte; +begin + state := $FF; + for i := 0 to Count-1 do + begin + v := Buf[i]; + for b := 7 downto 0 do + begin + outp := state and 1; + newbit := ParityB(state and $A9); + state := (state shr 1) or (newbit shl 7); + bit := (v shr b) and 1; + bit := bit xor outp; + if bit <> 0 then v := v or (1 shl b) else v := v and not (1 shl b); + end; + Buf[i] := v; + end; +end; + +procedure TBeaconFEC.DecodeCurrentFrame(Invert: Boolean); +var + win: array[0..AO40_FRAME_SYMS-1] of Single; + deint: array[0..AO40_FRAME_SYMS-1] of Single; + syms: array[0..AO40_VITERBI_SYM-1] of Byte; + conv: array[0..AO40_CONV_BYTES-1] of Byte; + cw: array[0..AO40_RS_NN-1] of Byte; + frame: TBeaconFrame; + i, idx, k, j, sgn, totErr, res: Integer; + v: Double; +begin + // 1. снимок кольца в хронологическом порядке (+ инверсия полярности) + if Invert then sgn := -1 else sgn := 1; + for i := 0 to AO40_FRAME_SYMS-1 do + begin + idx := FRingPos + i; + if idx >= AO40_FRAME_SYMS then Dec(idx, AO40_FRAME_SYMS); + win[i] := sgn * FRing[idx]; + end; + + // 2. матричный деинтерливер 80×65, out[i]=data[80*(i%65)+i/65] + for i := 0 to AO40_FRAME_SYMS-1 do + deint[i] := win[AO40_IL_ROWS * (i mod AO40_IL_COLS) + (i div AO40_IL_COLS)]; + + // 3. skip 65 (синхра) → 5132 мягких символа → байты для libfec (255=«1»). + // РЕМАП КОНВЕНЦИИ Viterbi: AO-40/gr-satellites кодирует полиномами [0x4F, -0x6D] + // (cc_decoder), а libfec viterbi27 ЗАШИВАЕТ [0x6D, 0x4F] БЕЗ инверсии. Эмпирически + // (брут-форс против gr post_viterbi_reference, см. историю) точное соответствие: + // libfec[2p] = -soft[2p+1] (своп пары + инверсия первого символа) + // libfec[2p+1] = soft[2p] + // Без ремапа Viterbi выдаёт мусор → RS не корректируется. + for i := 0 to (AO40_VITERBI_SYM div 2) - 1 do + begin + v := 128.0 - deint[AO40_SYNC_LEN + 2*i + 1] * 100.0; // -soft[2p+1] + if v > 255.0 then v := 255.0 else if v < 0.0 then v := 0.0; + syms[2*i] := Round(v); + v := 128.0 + deint[AO40_SYNC_LEN + 2*i] * 100.0; // soft[2p] + if v > 255.0 then v := 255.0 else if v < 0.0 then v := 0.0; + syms[2*i+1] := Round(v); + end; + + // 4. Viterbi r=1/2 k=7 terminated → 2560 бит = 320 байт (MSB-first) + init_viterbi27(FVp, 0); + update_viterbi27_blk(FVp, @syms[0], (AO40_DATA_BITS + 6)); // 2566 пар + chainback_viterbi27(FVp, @conv[0], AO40_DATA_BITS, 0); + + // 5. CCSDS дескремблер + Descramble(conv, AO40_CONV_BYTES); + + // 6. RS: 2-way байт-интерлив, decode_rs_8 укороч. pad=95 + totErr := 0; + for j := 0 to AO40_INTERLEAVE-1 do + begin + for k := 0 to AO40_RS_NN-1 do + cw[k] := conv[j + k * AO40_INTERLEAVE]; + res := decode_rs_8(@cw[0], nil, 0, AO40_RS_PAD); + if res < 0 then Exit; // некорректируемо → кадр невалиден + Inc(totErr, res); + for k := 0 to AO40_RS_KK-1 do + frame[j + k * AO40_INTERLEAVE] := cw[k]; + end; + + Inc(FFrames); + if Assigned(FOnFrame) then FOnFrame(frame, totErr); +end; + +// ---------------- self-test ---------------- + +function TBeaconFEC.SelfTest(out Msg: string): Boolean; +// Проверяет линковку libfec и базовую корректность: RS encode/decode round-trip +// с внесёнными ошибками (укорочение pad=95) + Viterbi encode/decode round-trip. +var + data: array[0..AO40_RS_KK-1] of Byte; + par: array[0..AO40_RS_NROOTS-1] of Byte; + cw: array[0..AO40_RS_NN-1] of Byte; + i, res, encst, sym0, sym1, errs: Integer; + bits: array[0..AO40_DATA_BITS-1] of Byte; + vsyms: array[0..AO40_VITERBI_SYM-1] of Byte; + outb: array[0..AO40_CONV_BYTES-1] of Byte; + b: Integer; +begin + Result := False; + + // --- RS round-trip --- + for i := 0 to AO40_RS_KK-1 do data[i] := (i * 37 + 11) and $FF; + encode_rs_8(@data[0], @par[0], AO40_RS_PAD); + for i := 0 to AO40_RS_KK-1 do cw[i] := data[i]; + for i := 0 to AO40_RS_NROOTS-1 do cw[AO40_RS_KK + i] := par[i]; + // вносим 16 байтовых ошибок (= предел исправления для nroots=32) + for i := 0 to 15 do cw[i * 9] := cw[i * 9] xor $A5; + res := decode_rs_8(@cw[0], nil, 0, AO40_RS_PAD); + if res < 0 then begin Msg := 'RS decode FAILED (link/params?)'; Exit; end; + for i := 0 to AO40_RS_KK-1 do + if cw[i] <> data[i] then begin Msg := Format('RS mismatch at %d', [i]); Exit; end; + + // --- Viterbi round-trip --- + // libfec viterbi27 зашивает полиномы V27POLYA=$6D, V27POLYB=$4F (CCSDS) БЕЗ + // инверсии и без API смены. Энкодер строго как у Карна (vtest27.c): + // sr=(sr shl 1)|bit; sym0=parity(sr & $6D); sym1=parity(sr & $4F). + // Мягкий «1» = 255, «0» = 0. + 6 хвостовых нулей (terminated). + Randomize; + for i := 0 to AO40_DATA_BITS-1 do bits[i] := Random(2); + encst := 0; + for i := 0 to AO40_DATA_BITS + 6 - 1 do + begin + if i < AO40_DATA_BITS then b := bits[i] else b := 0; + encst := ((encst shl 1) or b) and $7F; + sym0 := ParityB(encst and $6D); + sym1 := ParityB(encst and $4F); + vsyms[2*i] := sym0 * 255; + vsyms[2*i+1] := sym1 * 255; + end; + init_viterbi27(FVp, 0); + update_viterbi27_blk(FVp, @vsyms[0], AO40_DATA_BITS + 6); + chainback_viterbi27(FVp, @outb[0], AO40_DATA_BITS, 0); + errs := 0; + for i := 0 to AO40_DATA_BITS-1 do + begin + b := (outb[i div 8] shr (7 - (i mod 8))) and 1; // MSB-first + if b <> bits[i] then Inc(errs); + end; + if errs <> 0 then + begin + Msg := Format('Viterbi round-trip: %d/%d bit errors (convention mismatch)', + [errs, AO40_DATA_BITS]); + Exit; + end; + + Msg := Format('OK: RS corrected 16 errs; Viterbi 0/%d bit errors', [AO40_DATA_BITS]); + Result := True; +end; + +end. diff --git a/BeaconScopeForm.pas b/BeaconScopeForm.pas index b2a703f..df40fc8 100644 --- a/BeaconScopeForm.pas +++ b/BeaconScopeForm.pas @@ -44,7 +44,7 @@ begin Caption := 'QO-100 Beacon — Constellation'; BorderStyle := bsSizeable; Width := 360; - Height := 510; + Height := 560; Position := poScreenCenter; FBox := TPaintBox.Create(Self); @@ -160,6 +160,12 @@ begin C.TextOut(14, py + 90, 'INVERT ' + IfThen(FCtl.BeaconDecodeInvert, 'ON', 'OFF')); C.TextOut(14, py + 108, Format('RATE Fs %.0f Fdec %.0f sps %.1f', [S.FsHz, S.FdecHz, S.Sps])); + + // ----- FEC (AO-40) ----- + if S.HasFrame then C.Font.Color := FTheme.MeterOn else C.Font.Color := FTheme.TextDim; + C.TextOut(14, py + 132, Format('FRAMES %d (RS err %d)', [S.Frames, S.LastRSErr])); + C.Font.Color := FTheme.TextDim; + C.TextOut(14, py + 150, Format('MANCHESTER phase %d', [S.ManPhase])); end; end. diff --git a/RadioController.pas b/RadioController.pas index 97c52e9..0e1bf5e 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -1261,7 +1261,8 @@ begin BeaconLog(Format( 'STATE decHz=%.1f center=%.1f off=%.1f | Fs=%.0f Fdec=%.0f sps=%.1f | ' + 'resid=%.1f prom=%.1f snr=%.1f baud=%.1f decRMS=%.5f symMag=%.5f ' + - 'timErr=%.5f wsym=%.4f carHz=%.1f hold=%d | CAR=%d SYM=%d invert=%d lockOn=%d locked=%d', + 'timErr=%.5f wsym=%.4f carHz=%.1f hold=%d | CAR=%d SYM=%d invert=%d lockOn=%d locked=%d ' + + '| manPh=%d m0=%.5f m1=%.5f frames=%d rsErr=%d', [FBeaconDecHz, FCenterFreq, FBeaconLogScope.OffsetHz, FBeaconLogScope.FsHz, FBeaconLogScope.FdecHz, FBeaconLogScope.Sps, FBeaconLogScope.ResidHz, FBeaconLogScope.CarProm, FBeaconLogScope.SNRdB, @@ -1269,7 +1270,9 @@ begin FBeaconLogScope.TimErr, FBeaconLogScope.Wsym, FBeaconLogScope.CarFreqHz, FBeaconLogScope.LockHold, Ord(FBeaconLogScope.CarrierLock), Ord(FBeaconLogScope.SymbolLock), - Ord(FBeaconDecInvert), Ord(FBeaconLockOn), Ord(FBeaconLocked)])); + Ord(FBeaconDecInvert), Ord(FBeaconLockOn), Ord(FBeaconLocked), + FBeaconLogScope.ManPhase, FBeaconLogScope.ManM0, FBeaconLogScope.ManM1, + FBeaconLogScope.Frames, FBeaconLogScope.LastRSErr])); end; end; diff --git a/ewsdr.lpi b/ewsdr.lpi index 8d00486..0ac3d73 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -135,6 +135,10 @@ + + + + diff --git a/fectest.lpr b/fectest.lpr new file mode 100644 index 0000000..89225e3 --- /dev/null +++ b/fectest.lpr @@ -0,0 +1,18 @@ +program fectest; +{$MODE Delphi} +uses SysUtils, BeaconFEC; +var + fec: TBeaconFEC; + msg: string; + ok: Boolean; +begin + fec := TBeaconFEC.Create; + try + ok := fec.SelfTest(msg); + WriteLn('SelfTest: ', msg); + if ok then WriteLn('==> PASS') else WriteLn('==> FAIL'); + finally + fec.Free; + end; + if not ok then Halt(1); +end. diff --git a/fectest2.lpr b/fectest2.lpr new file mode 100644 index 0000000..df82c28 --- /dev/null +++ b/fectest2.lpr @@ -0,0 +1,104 @@ +program fectest2; +{$MODE Delphi} +{ + Валидация TBeaconFEC против эталонных тест-векторов gr-satellites + (python/components/deframers/qa_ao40_fec_deframer_*). Гоним поток мягких + символов через PushSoftSymbol и сверяем выданный 256-байтовый кадр с эталоном. + Решает вопрос конвенции Viterbi (libfec [0x6D,0x4F] vs AO-40 [0x4F,-0x6D]). +} +uses SysUtils, Classes, BeaconFEC; + +const + GR = '/home/vladimir/Документы/source/gr-satellites/python/components/deframers/'; + SymPath = GR + 'qa_ao40_fec_deframer_symbols.f32'; + + // qa_ao40_fec_deframer.py: финальный кадр (256 байт) + FrameRef: array[0..255] of Byte = ( + 137, 0, 0, 0, 0, 0, 0, 0, 0, 31, 204, 0, 206, 2, 209, 0, 0, + 7, 8, 9, 9, 0, 0, 5, 1, 1, 0, 64, 19, 47, 200, 242, 92, + 143, 52, 35, 243, 186, 11, 93, 98, 116, 81, 199, 234, 250, + 105, 74, 154, 159, 0, 9, 239, 160, 31, 244, 167, 234, 74, + 198, 143, 17, 64, 17, 30, 16, 247, 1, 62, 32, 100, 0, 215, + 139, 248, 215, 148, 200, 147, 168, 42, 218, 82, 166, 14, 88, + 14, 200, 15, 78, 1, 29, 32, 90, 0, 219, 148, 168, 170, 138, + 152, 19, 172, 105, 10, 166, 168, 16, 230, 16, 146, 15, 184, + 1, 80, 32, 100, 0, 215, 150, 168, 193, 139, 72, 37, 171, 169, + 202, 206, 157, 16, 118, 15, 201, 16, 85, 1, 58, 32, 90, 0, 215, + 151, 41, 8, 140, 72, 79, 169, 106, 90, 242, 164, 16, 57, 15, + 123, 15, 134, 1, 73, 32, 100, 0, 215, 148, 8, 208, 138, 216, + 42, 173, 106, 90, 126, 180, 14, 83, 14, 155, 14, 183, 1, 9, 32, + 90, 0, 219, 153, 168, 242, 143, 232, 56, 175, 170, 138, 194, + 158, 14, 222, 15, 72, 14, 49, 1, 49, 32, 90, 0, 206, 155, 200, + 255, 136, 104, 27, 178, 106, 90, 202, 167, 15, 195, 14, 116, 14, + 88, 1, 52, 32, 90, 0, 215, 155, 57, 27, 151, 184, 197, 176, 43, + 58, 214, 181, 1, 107, 0, 106, 2, 158, 0, 3, 32, 19, 0); + +type + TFrameChecker = class + Got: Boolean; + Match: Boolean; + NErr: Integer; + RSErr: Integer; + procedure OnFrame(const Frame: TBeaconFrame; RSErrors: Integer); + end; + +procedure TFrameChecker.OnFrame(const Frame: TBeaconFrame; RSErrors: Integer); +var i, e: Integer; +begin + if Got then Exit; // берём первый кадр + Got := True; + RSErr := RSErrors; + e := 0; + for i := 0 to 255 do + if Frame[i] <> FrameRef[i] then Inc(e); + NErr := e; + Match := (e = 0); +end; + +var + fec: TBeaconFEC; + chk: TFrameChecker; + fs: TFileStream; + n, i: Integer; + buf: array of Single; +begin + if not FileExists(SymPath) then + begin + WriteLn('symbols file not found: ', SymPath); Halt(2); + end; + fs := TFileStream.Create(SymPath, fmOpenRead); + try + n := fs.Size div 4; + SetLength(buf, n); + fs.ReadBuffer(buf[0], n * 4); + finally + fs.Free; + end; + WriteLn('loaded ', n, ' soft symbols'); + + chk := TFrameChecker.Create; + fec := TBeaconFEC.Create; + try + fec.OnFrame := chk.OnFrame; + for i := 0 to n - 1 do + fec.PushSoftSymbol(buf[i]); + + WriteLn('frames decoded: ', fec.Frames); + if not chk.Got then + begin + WriteLn('==> FAIL: no frame decoded (sync not found / RS uncorrectable)'); + Halt(1); + end; + WriteLn(Format('frame: %d/256 byte mismatches, RS errors=%d', [chk.NErr, chk.RSErr])); + if chk.Match then + WriteLn('==> PASS: frame matches gr-satellites reference') + else + begin + WriteLn('==> FAIL: frame does not match reference'); + Halt(1); + end; + finally + fec.Free; + chk.Free; + end; +end.