mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
Merge feature/qo100-beacon-decode: QO-100 central beacon decoder (BPSK 400 / AO-40 FEC → bulletin text)
M1: stable BPSK demod. M2: AO-40 FEC decode end-to-end (libfec). Phase C: decode 256-byte frame as ASCII bulletin (CRC), display in BeaconScopeForm.
This commit is contained in:
@@ -0,0 +1,848 @@
|
||||
unit BeaconDecoder;
|
||||
|
||||
{
|
||||
QO-100 central beacon — DSP front-end (Milestone 1).
|
||||
|
||||
Средний маяк QO-100 (10489.750) = AMSAT AO-40 FEC телеметрия: BPSK 400 бод.
|
||||
Этот юнит делает ТОЛЬКО демодуляцию до мягких символов и снимает констелляцию
|
||||
для визуализации/диагностики. FEC-бэкенд (Viterbi k=7 + RS(160,128) + интерливер
|
||||
+ дескремблер) подключается отдельно (libfec Карна) на следующем этапе.
|
||||
|
||||
Цепочка (вход — комплексный baseband IQ на FSampleRate из DDC, после beacon-lock):
|
||||
NCO-снос смещения маяка к DC
|
||||
→ boxcar-дециматор до ~9600 Гц (целое D)
|
||||
→ RRC матч-фильтр (beta 0.5)
|
||||
→ Gardner TED + линейный интерполятор (символьная синхра 400 бод)
|
||||
→ Costas (BPSK, 2-й порядок) (фаза/несущая)
|
||||
→ мягкий символ sign(I), констелляция, метрики (SNR, lock).
|
||||
|
||||
Feed() зовётся из DSP-потока (WDSPEngine.PushIQItemToDSP), снимок читается из UI —
|
||||
обмен под критической секцией, как в прежнем BeaconLock.
|
||||
}
|
||||
|
||||
{$IFDEF FPC}
|
||||
{$MODE Delphi}
|
||||
{$ENDIF}
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, SyncObjs, Math, BeaconFEC;
|
||||
|
||||
const
|
||||
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 в символах (полу-)
|
||||
BCN_SCOPE_PTS = 256; // точек констелляции в снимке
|
||||
BCN_NFFT = 4096; // FFT грубой carrier-acquisition (~0.43 c @ 9600)
|
||||
BCN_NFFT_LOG2 = 12;
|
||||
|
||||
type
|
||||
// Снимок состояния для UI (констелляция + метрики). Копируется под CS.
|
||||
TBeaconScope = record
|
||||
Enabled: Boolean;
|
||||
Count: Integer; // заполнено точек [0..BCN_SCOPE_PTS]
|
||||
PtI, PtQ: array[0..BCN_SCOPE_PTS-1] of Single; // нормированные символы
|
||||
SNRdB: Single;
|
||||
SymRate: Single; // измеренная скорость, ~400
|
||||
CarrierLock: Boolean;
|
||||
SymbolLock: Boolean;
|
||||
OffsetHz: Double; // текущий снос NCO
|
||||
ResidHz: Single; // найденный остаточный снос несущей
|
||||
CarProm: Single; // prominence линии несущей (squaring-FFT)
|
||||
FsHz: Single; // rate IQ-тапа (диагностика)
|
||||
FdecHz: Single; // rate после децимации
|
||||
Sps: Single; // отсчётов на символ
|
||||
DecRMS: Single; // RMS децимированного сигнала (до RRC)
|
||||
SymMag: Single; // |символ| после RRC (lock-независимо)
|
||||
TimErr: Single; // EMA |ошибки Gardner| (таймер-петля)
|
||||
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
|
||||
private
|
||||
FLock: TCriticalSection;
|
||||
FEnabled: Boolean;
|
||||
|
||||
// --- параметры rate ---
|
||||
FFs: Double; // вход (DDC) Гц
|
||||
FDecim: Integer; // целая децимация
|
||||
FFdec: Double; // rate после децимации
|
||||
FSps: Double; // отсчётов на символ после децимации
|
||||
|
||||
// --- NCO сноса ---
|
||||
FOffsetHz: Double;
|
||||
FNcoPh: Double;
|
||||
FNcoInc: Double;
|
||||
FInvert: Boolean; // зеркальный baseband (QO-100/LNB): сопряжение IQ
|
||||
|
||||
// --- boxcar дециматор ---
|
||||
FAccI, FAccQ: Double;
|
||||
FAccN: Integer;
|
||||
|
||||
// --- RRC матч-фильтр ---
|
||||
FRRC: array of Double;
|
||||
FHistI: array of Double; // кольцевой буфер decimated I
|
||||
FHistQ: array of Double;
|
||||
FHistPos: Integer;
|
||||
FNtaps: Integer;
|
||||
|
||||
// --- ML signal×slope symbol timing ---
|
||||
FMuAcc: 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;
|
||||
|
||||
// --- Costas (BPSK) ---
|
||||
FCarPh: Double;
|
||||
FCarFreq: Double;
|
||||
FCarAlpha: Double; // proportional
|
||||
FCarBeta: Double; // integral
|
||||
|
||||
// --- грубая carrier-acquisition (squaring-FFT) ---
|
||||
// BPSK² снимает модуляцию → линия на 2·Δf. Находит подавленную несущую
|
||||
// независимо от знака сноса (алиасинг сворачивает её в полосу), игнорируя
|
||||
// DC-offset Pluto (вычитаем среднее перед возведением в квадрат).
|
||||
FAcqRe, FAcqIm: array of Double; // буфер декадированных отсчётов (NFFT)
|
||||
FAcqPos: Integer;
|
||||
FResidDf: Double; // накопленный остаточный снос несущей (Гц)
|
||||
FResidPh: Double;
|
||||
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; // холдовер-счётчик лока (анти-мерцание)
|
||||
|
||||
// --- FLL (частотная петля Costas, decision-directed) ---
|
||||
FFllGain: Double;
|
||||
FPrevRre, FPrevRim: Double; // прошлый символ с убранной модуляцией
|
||||
FBitRev: array of Integer; // бит-реверс FFT
|
||||
FTwRe, FTwIm: array of Double;// твидлы
|
||||
FFre, FFim: array of Double; // рабочие буферы FFT
|
||||
|
||||
// --- метрики ---
|
||||
FSigAmp: Double; // EMA |I|
|
||||
FQVar: Double; // EMA Q^2
|
||||
FConstScale: Double;
|
||||
FDecRMS: Double; // EMA RMS децимированного сигнала (диагностика тракта)
|
||||
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;
|
||||
|
||||
// --- констелляция (кольцо) ---
|
||||
FRingI, FRingQ: array[0..BCN_SCOPE_PTS-1] of Single;
|
||||
FRingPos: Integer;
|
||||
FRingCnt: Integer;
|
||||
|
||||
procedure BuildRRC;
|
||||
procedure BuildFFT;
|
||||
procedure FFTInplace;
|
||||
procedure RunAcquisition;
|
||||
procedure ProcessDecimated(const xi, xq: Double);
|
||||
procedure ProcessSymbol(const zi, zq: Double);
|
||||
procedure ProcessChip(const ci, cq: Double); // Manchester + дифф-декод → FEC
|
||||
procedure FECFrame(const Frame: TBeaconFrame; RSErrors: Integer);
|
||||
procedure ResetDSP;
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
|
||||
procedure Configure(SampleRateHz: Double); // при смене rate
|
||||
procedure SetOffsetHz(Hz: Double); // снос маяка к DC (display − center)
|
||||
procedure SetInvert(On_: Boolean); // зеркальный baseband (инверсия спектра)
|
||||
function Inverted: Boolean;
|
||||
procedure Reseed; // переинициализация петель (новое наведение)
|
||||
procedure SetEnabled(On_: Boolean);
|
||||
property Enabled: Boolean read FEnabled;
|
||||
|
||||
// Из DSP-потока, по одному комплексному отсчёту на FSampleRate.
|
||||
procedure Feed(const I, Q: Double); inline;
|
||||
|
||||
procedure GetScope(out S: TBeaconScope);
|
||||
function GetLastFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
constructor TBeaconDecoder.Create;
|
||||
begin
|
||||
inherited Create;
|
||||
FLock := TCriticalSection.Create;
|
||||
FFEC := TBeaconFEC.Create;
|
||||
FFEC.OnFrame := FECFrame;
|
||||
FFs := 0;
|
||||
FEnabled := False;
|
||||
FConstScale := 1.0;
|
||||
Configure(192000);
|
||||
end;
|
||||
|
||||
destructor TBeaconDecoder.Destroy;
|
||||
begin
|
||||
FFEC.Free;
|
||||
FLock.Free;
|
||||
inherited Destroy;
|
||||
end;
|
||||
|
||||
procedure TBeaconDecoder.BuildRRC;
|
||||
// Root-raised-cosine матч-фильтр, длина BCN_RRC_SPAN символов, нормировка по энергии.
|
||||
var
|
||||
i, mid: Integer;
|
||||
t, beta, num, den, h, ssum: Double;
|
||||
begin
|
||||
beta := BCN_RRC_BETA;
|
||||
FNtaps := Round(BCN_RRC_SPAN * FSps);
|
||||
if FNtaps < 8 then FNtaps := 8;
|
||||
if (FNtaps and 1) = 0 then Inc(FNtaps); // нечётное → симметрично
|
||||
SetLength(FRRC, FNtaps);
|
||||
mid := FNtaps div 2;
|
||||
ssum := 0;
|
||||
for i := 0 to FNtaps - 1 do
|
||||
begin
|
||||
t := (i - mid) / FSps;
|
||||
if Abs(t) < 1e-9 then
|
||||
h := 1.0 - beta + 4.0 * beta / Pi
|
||||
else if Abs(Abs(4.0 * beta * t) - 1.0) < 1e-6 then
|
||||
h := (beta / Sqrt(2.0)) *
|
||||
((1.0 + 2.0 / Pi) * Sin(Pi / (4.0 * beta)) +
|
||||
(1.0 - 2.0 / Pi) * Cos(Pi / (4.0 * beta)))
|
||||
else
|
||||
begin
|
||||
num := Sin(Pi * t * (1.0 - beta)) +
|
||||
4.0 * beta * t * Cos(Pi * t * (1.0 + beta));
|
||||
den := Pi * t * (1.0 - Sqr(4.0 * beta * t));
|
||||
h := num / den;
|
||||
end;
|
||||
FRRC[i] := h;
|
||||
ssum := ssum + h * h;
|
||||
end;
|
||||
if ssum > 0 then
|
||||
for i := 0 to FNtaps - 1 do
|
||||
FRRC[i] := FRRC[i] / Sqrt(ssum);
|
||||
|
||||
SetLength(FHistI, FNtaps);
|
||||
SetLength(FHistQ, FNtaps);
|
||||
end;
|
||||
|
||||
procedure TBeaconDecoder.BuildFFT;
|
||||
// Таблицы бит-реверса и твидлов для радикс-2 FFT длины BCN_NFFT.
|
||||
var
|
||||
i, j, bit, n: Integer;
|
||||
begin
|
||||
n := BCN_NFFT;
|
||||
SetLength(FBitRev, n);
|
||||
for i := 0 to n - 1 do
|
||||
begin
|
||||
j := 0;
|
||||
for bit := 0 to BCN_NFFT_LOG2 - 1 do
|
||||
if (i and (1 shl bit)) <> 0 then
|
||||
j := j or (1 shl (BCN_NFFT_LOG2 - 1 - bit));
|
||||
FBitRev[i] := j;
|
||||
end;
|
||||
SetLength(FTwRe, n div 2);
|
||||
SetLength(FTwIm, n div 2);
|
||||
for i := 0 to (n div 2) - 1 do
|
||||
begin
|
||||
FTwRe[i] := Cos(-2.0 * Pi * i / n);
|
||||
FTwIm[i] := Sin(-2.0 * Pi * i / n);
|
||||
end;
|
||||
SetLength(FFre, n);
|
||||
SetLength(FFim, n);
|
||||
SetLength(FAcqRe, n);
|
||||
SetLength(FAcqIm, n);
|
||||
end;
|
||||
|
||||
procedure TBeaconDecoder.FFTInplace;
|
||||
// Итеративный радикс-2 DIT FFT над FFre/FFim (длина BCN_NFFT), in-place.
|
||||
var
|
||||
n, half, step, i, j, k, twi: Integer;
|
||||
ur, ui, tr, ti: Double;
|
||||
begin
|
||||
n := BCN_NFFT;
|
||||
// бит-реверс перестановка
|
||||
for i := 0 to n - 1 do
|
||||
begin
|
||||
j := FBitRev[i];
|
||||
if j > i then
|
||||
begin
|
||||
tr := FFre[i]; FFre[i] := FFre[j]; FFre[j] := tr;
|
||||
ti := FFim[i]; FFim[i] := FFim[j]; FFim[j] := ti;
|
||||
end;
|
||||
end;
|
||||
half := 1;
|
||||
while half < n do
|
||||
begin
|
||||
step := half * 2;
|
||||
i := 0;
|
||||
while i < n do
|
||||
begin
|
||||
k := 0;
|
||||
while k < half do
|
||||
begin
|
||||
twi := (k * (n div step));
|
||||
ur := FTwRe[twi]; ui := FTwIm[twi];
|
||||
tr := ur * FFre[i + k + half] - ui * FFim[i + k + half];
|
||||
ti := ur * FFim[i + k + half] + ui * FFre[i + k + half];
|
||||
FFre[i + k + half] := FFre[i + k] - tr;
|
||||
FFim[i + k + half] := FFim[i + k] - ti;
|
||||
FFre[i + k] := FFre[i + k] + tr;
|
||||
FFim[i + k] := FFim[i + k] + ti;
|
||||
Inc(k);
|
||||
end;
|
||||
Inc(i, step);
|
||||
end;
|
||||
half := step;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TBeaconDecoder.RunAcquisition;
|
||||
// Грубый захват несущей маяка: вычитаем 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, pkBin: Integer;
|
||||
mr, mi, re, im, mag, pk, sumMag, meanMag, fHz, fcHz, avgHz: Double;
|
||||
begin
|
||||
n := BCN_NFFT;
|
||||
// среднее (DC) для подавления offset Pluto
|
||||
mr := 0; mi := 0;
|
||||
for i := 0 to n - 1 do begin mr := mr + FAcqRe[i]; mi := mi + FAcqIm[i]; end;
|
||||
mr := mr / n; mi := mi / n;
|
||||
// z² с вычтенным DC
|
||||
for i := 0 to n - 1 do
|
||||
begin
|
||||
re := FAcqRe[i] - mr; im := FAcqIm[i] - mi;
|
||||
FFre[i] := (re * re - im * im);
|
||||
FFim[i] := (2.0 * re * im);
|
||||
end;
|
||||
|
||||
FFTInplace;
|
||||
|
||||
// самая сильная линия, исключая ±loBin вокруг DC (там сидит остаток DC при Δf≈0)
|
||||
loBin := 2;
|
||||
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 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;
|
||||
var i: Integer;
|
||||
begin
|
||||
FNcoPh := 0;
|
||||
FAccI := 0; FAccQ := 0; FAccN := 0;
|
||||
for i := 0 to High(FHistI) do begin FHistI[i] := 0; FHistQ[i] := 0; end;
|
||||
FHistPos := 0;
|
||||
FMuAcc := FSps / 2.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;
|
||||
FDecRMS := 0; FSymMag := 0; FRrcAmp := 0;
|
||||
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);
|
||||
var
|
||||
bn, zeta, theta, d: Double;
|
||||
begin
|
||||
if SampleRateHz < 1000 then Exit;
|
||||
FLock.Enter;
|
||||
try
|
||||
if SameValue(SampleRateHz, FFs) and (FNtaps > 0) then Exit;
|
||||
FFs := SampleRateHz;
|
||||
FDecim := Max(1, Round(FFs / BCN_TARGET_FS));
|
||||
FFdec := FFs / FDecim;
|
||||
FSps := FFdec / BCN_CHIP_BAUD; // отсчётов на ЧИП (Manchester), таймер на 800
|
||||
|
||||
FNcoInc := 2.0 * Pi * FOffsetHz / FFs;
|
||||
|
||||
BuildRRC;
|
||||
if Length(FBitRev) <> BCN_NFFT then BuildFFT;
|
||||
|
||||
// Costas: 2-й порядок, нормированный на символ. Bn ~ 1.5% символьной.
|
||||
bn := 0.02;
|
||||
zeta := 0.707;
|
||||
theta := bn / (zeta + 1.0 / (4.0 * zeta));
|
||||
d := 1.0 + 2.0 * zeta * theta + theta * theta;
|
||||
FCarAlpha := (4.0 * zeta * theta) / d;
|
||||
FCarBeta := (4.0 * theta * theta) / d;
|
||||
FFllGain := 0.002; // частотная петля (decision-directed), мягкая
|
||||
|
||||
ResetDSP;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TBeaconDecoder.SetOffsetHz(Hz: Double);
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
FOffsetHz := Hz;
|
||||
if FFs > 0 then FNcoInc := 2.0 * Pi * FOffsetHz / FFs;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TBeaconDecoder.SetInvert(On_: Boolean);
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
if On_ = FInvert then Exit;
|
||||
FInvert := On_;
|
||||
ResetDSP; // смена ориентации → перезахват
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TBeaconDecoder.Inverted: Boolean;
|
||||
begin
|
||||
Result := FInvert;
|
||||
end;
|
||||
|
||||
procedure TBeaconDecoder.Reseed;
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
ResetDSP;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TBeaconDecoder.SetEnabled(On_: Boolean);
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
if On_ = FEnabled then Exit;
|
||||
FEnabled := On_;
|
||||
if On_ then ResetDSP;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TBeaconDecoder.Feed(const I, Q: Double);
|
||||
// Один комплексный отсчёт @ FFs. Снос маяка к DC + boxcar-децимация.
|
||||
var
|
||||
c, s, mi, mq, qq: Double;
|
||||
begin
|
||||
if not FEnabled then Exit;
|
||||
|
||||
// Зеркальный baseband (QO-100/LNB): сопряжение IQ переворачивает ось частот,
|
||||
// тогда снос совпадает с осью дисплея.
|
||||
if FInvert then qq := -Q else qq := Q;
|
||||
|
||||
// exp(-j*ph): сносим +OffsetHz к DC.
|
||||
c := Cos(FNcoPh); s := Sin(FNcoPh);
|
||||
mi := I * c + qq * s;
|
||||
mq := qq * c - I * s;
|
||||
FNcoPh := FNcoPh + FNcoInc;
|
||||
if FNcoPh > Pi then FNcoPh := FNcoPh - 2.0 * Pi
|
||||
else if FNcoPh < -Pi then FNcoPh := FNcoPh + 2.0 * Pi;
|
||||
|
||||
FAccI := FAccI + mi;
|
||||
FAccQ := FAccQ + mq;
|
||||
Inc(FAccN);
|
||||
if FAccN >= FDecim then
|
||||
begin
|
||||
ProcessDecimated(FAccI / FDecim, FAccQ / FDecim);
|
||||
FAccI := 0; FAccQ := 0; FAccN := 0;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TBeaconDecoder.ProcessDecimated(const xi, xq: Double);
|
||||
// RRC матч-фильтр + Gardner-таймер @ FFdec. Без CS (зовётся из Feed, под потоком DSP);
|
||||
// снимок констелляции защищён в GetScope собственным копированием полей-массивов.
|
||||
var
|
||||
k, idx: Integer;
|
||||
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);
|
||||
ri := xi * cr + xq * sr;
|
||||
ii := xq * cr - xi * sr;
|
||||
FResidPh := FResidPh + FResidInc;
|
||||
if FResidPh > Pi then FResidPh := FResidPh - 2.0 * Pi
|
||||
else if FResidPh < -Pi then FResidPh := FResidPh + 2.0 * Pi;
|
||||
|
||||
// Диагностика тракта (lock-независимо): RMS децимированного сигнала на входе RRC.
|
||||
FDecRMS := FDecRMS + 0.001 * (Sqrt(ri * ri + ii * ii) - FDecRMS);
|
||||
|
||||
// Буфер для грубой carrier-acquisition: кладём сигнал ПОСЛЕ главного NCO, но
|
||||
// ДО остаточного — тогда squaring-FFT меряет АБСОЛЮТНЫЙ остаток (не зависит от
|
||||
// FResidDf) → коррекция = прямое присвоение, без накопления и runaway.
|
||||
FAcqRe[FAcqPos] := xi; FAcqIm[FAcqPos] := xq;
|
||||
Inc(FAcqPos);
|
||||
if FAcqPos >= BCN_NFFT then
|
||||
begin
|
||||
RunAcquisition;
|
||||
FAcqPos := 0;
|
||||
end;
|
||||
|
||||
// вставляем в кольцо истории (уже с остаточной коррекцией)
|
||||
FHistI[FHistPos] := ri;
|
||||
FHistQ[FHistPos] := ii;
|
||||
|
||||
// свёртка RRC
|
||||
yi := 0; yq := 0;
|
||||
idx := FHistPos;
|
||||
for k := 0 to FNtaps - 1 do
|
||||
begin
|
||||
yi := yi + FRRC[k] * FHistI[idx];
|
||||
yq := yq + FRRC[k] * FHistQ[idx];
|
||||
Dec(idx); if idx < 0 then idx := FNtaps - 1;
|
||||
end;
|
||||
FHistPos := FHistPos + 1; if FHistPos >= FNtaps then FHistPos := 0;
|
||||
|
||||
// AGC: нормируем выход RRC к ~1. Без этого при слабом сигнале (~−60 dBFS) ошибки
|
||||
// петель (Costas ∝ амплитуде, Gardner ∝ амплитуде²) на 3 порядка меньше калибровки
|
||||
// усилений → петли практически заморожены, лока нет. Нормировка делает динамику
|
||||
// петель независимой от абсолютного уровня. FRrcAmp медленный — это и метрика силы.
|
||||
if FRrcAmp <= 0 then
|
||||
FRrcAmp := Sqrt(yi * yi + yq * yq) + 1e-12 // сид на первом отсчёте (без скачка)
|
||||
else
|
||||
FRrcAmp := FRrcAmp + 0.0005 * (Sqrt(yi * yi + yq * yq) - FRrcAmp);
|
||||
if FRrcAmp > 1e-12 then
|
||||
begin
|
||||
yi := yi / FRrcAmp;
|
||||
yq := yq / FRrcAmp;
|
||||
end;
|
||||
FSymMag := FRrcAmp; // диагностика силы сигнала (сырой уровень до AGC)
|
||||
|
||||
// линия задержки 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;
|
||||
|
||||
// Таймер 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]
|
||||
if frac < 0 then frac := 0;
|
||||
// центр (между 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;
|
||||
|
||||
procedure TBeaconDecoder.ProcessSymbol(const zi, zq: Double);
|
||||
// Costas BPSK: дероворот по FCarPh, решающе-направленный детектор фазы, обновление
|
||||
// контура; нормированный символ → констелляция + метрики.
|
||||
var
|
||||
c, s, dr, di, ce, na, absI, ef, leak: Double;
|
||||
begin
|
||||
c := Cos(FCarPh); s := Sin(FCarPh);
|
||||
dr := zi * c + zq * s; // умножение на exp(-j FCarPh)
|
||||
di := zq * c - zi * s;
|
||||
|
||||
if dr >= 0 then ce := di else ce := -di; // Costas: фазовый детектор sign(I)*Q
|
||||
absI := Abs(dr);
|
||||
|
||||
// FLL (decision-directed): убираем модуляцию (rk = |I| + j·ce), частотная ошибка
|
||||
// = Im(rk·conj(rk-1)). Расширяет диапазон захвата и держит частоту устойчиво
|
||||
// (отрицательная ОС) — в отличие от open-loop squaring-FFT, не «убегает».
|
||||
ef := ce * FPrevRre - absI * FPrevRim;
|
||||
FPrevRre := absI; FPrevRim := ce;
|
||||
|
||||
FCarFreq := FCarFreq + FCarBeta * ce + FFllGain * ef;
|
||||
if FCarFreq > 3.0 then FCarFreq := 3.0;
|
||||
if FCarFreq < -3.0 then FCarFreq := -3.0;
|
||||
|
||||
// OFFLOAD частоты: медленно «стекаем» накопленный снос Costas в пред-RRC NCO
|
||||
// (FResidInc), у которого НЕТ предела ±π/символ. По логам carHz упирался в кламп
|
||||
// (±127/±191) из-за ошибки наведения (1 пиксель≈300Гц) + дрейфа LNB и срывал лок.
|
||||
// Консервативно: сколько добавили в FResidInc, столько убрали из FCarFreq (без
|
||||
// рывка). Утечка медленная (TC~100 символов >> группозадержки RRC ~4 симв) — стабильно.
|
||||
// Итог: Costas держит лишь мелкий быстрый остаток, весь медленный снос — в FResidInc.
|
||||
// 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); // для лога/диагностики
|
||||
FCarPh := FCarPh + FCarFreq + FCarAlpha * ce;
|
||||
if FCarPh > Pi then FCarPh := FCarPh - 2.0 * Pi
|
||||
else if FCarPh < -Pi then FCarPh := FCarPh + 2.0 * Pi;
|
||||
|
||||
// метрики — медленные EMA (~0.8с): SNR по логам скачет −8…+6 за секунду, на быстрой
|
||||
// EMA порог лока дребезжит. Замедляем, чтобы оценка была устойчивой.
|
||||
FSigAmp := FSigAmp + 0.003 * (Abs(dr) - FSigAmp);
|
||||
FQVar := FQVar + 0.003 * (di * di - FQVar);
|
||||
|
||||
// Холдовер-лок (анти-мерцание): копим счётчик при хорошем SNR, тратим при плохом.
|
||||
// Лок держится сквозь кратковременные провалы (~секунды), не отваливаясь на каждом.
|
||||
if FQVar > 1e-12 then
|
||||
begin
|
||||
if (FSigAmp * FSigAmp) / FQVar > 2.0 then
|
||||
begin
|
||||
if FLockHold < 2400 then Inc(FLockHold, 3); // захват быстрее, чем потеря
|
||||
end
|
||||
else
|
||||
if FLockHold > 0 then Dec(FLockHold);
|
||||
end;
|
||||
if FLockHold >= 600 then FFreqLocked := True
|
||||
else if FLockHold <= 150 then FFreqLocked := False;
|
||||
|
||||
// нормировка констелляции к ±1 по амплитуде сигнала
|
||||
if FSigAmp > 1e-9 then FConstScale := 1.0 / FSigAmp;
|
||||
na := FConstScale;
|
||||
|
||||
FRingI[FRingPos] := dr * na;
|
||||
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.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);
|
||||
var
|
||||
i, idx: Integer;
|
||||
snr: Double;
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
S.Enabled := FEnabled;
|
||||
S.OffsetHz := FOffsetHz;
|
||||
S.SymRate := BCN_BAUD * (1.0 + FWsym);
|
||||
S.ResidHz := FResidDf;
|
||||
S.CarProm := FCarProm;
|
||||
S.FsHz := FFs;
|
||||
S.FdecHz := FFdec;
|
||||
S.Sps := FSps;
|
||||
S.DecRMS := FDecRMS;
|
||||
S.SymMag := FSymMag;
|
||||
S.TimErr := FTimErrEMA;
|
||||
S.Wsym := FWsym;
|
||||
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
|
||||
snr := (FSigAmp * FSigAmp) / FQVar
|
||||
else
|
||||
snr := 0;
|
||||
if snr > 1e-6 then S.SNRdB := 10.0 * Log10(snr) else S.SNRdB := -99;
|
||||
|
||||
// CARRIER = линия несущей видна (squaring-FFT) И Costas держит фазу.
|
||||
S.CarrierLock := FCarPresent and FFreqLocked;
|
||||
S.SymbolLock := FFreqLocked and
|
||||
(FTimErrEMA < 0.25 * Max(FSigAmp, 1e-9)) and (FRingCnt >= BCN_SCOPE_PTS);
|
||||
|
||||
S.Count := FRingCnt;
|
||||
// выгружаем кольцо в хронологическом порядке (старейший → новейший)
|
||||
for i := 0 to FRingCnt - 1 do
|
||||
begin
|
||||
if FRingCnt < BCN_SCOPE_PTS then
|
||||
idx := i
|
||||
else
|
||||
begin
|
||||
idx := FRingPos + i;
|
||||
if idx >= BCN_SCOPE_PTS then Dec(idx, BCN_SCOPE_PTS);
|
||||
end;
|
||||
S.PtI[i] := FRingI[idx];
|
||||
S.PtQ[i] := FRingQ[idx];
|
||||
end;
|
||||
finally
|
||||
FLock.Leave;
|
||||
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.
|
||||
+341
@@ -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.
|
||||
@@ -0,0 +1,234 @@
|
||||
unit BeaconScopeForm;
|
||||
|
||||
{ TBeaconScopeForm — окно диагностики QO-100 beacon-декодера (Milestone 1).
|
||||
Показывает BPSK-констелляцию демодулированного маяка + метрики захвата:
|
||||
carrier-lock, symbol-lock, SNR, измеренную скорость, снос NCO.
|
||||
|
||||
Это смотровое окно фронтенда ДО подключения FEC: если констелляция сжимается
|
||||
в два чётких сгустка по оси I и горят оба лока — демодуляция корректна, можно
|
||||
цеплять Viterbi/RS (libfec). Открывается из RX-блока MainForm (ПКМ по BEACON).
|
||||
|
||||
Данные тянутся из TRadioController.GetBeaconScope по таймеру (~25 Гц). }
|
||||
|
||||
{$mode objfpc}{$H+}
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, Math, StrUtils,
|
||||
Forms, Controls, Graphics, ExtCtrls, StdCtrls,
|
||||
AppTheme, BeaconDecoder, BeaconFEC, RadioController;
|
||||
|
||||
type
|
||||
TBeaconScopeForm = class(TForm)
|
||||
private
|
||||
FCtl: TRadioController;
|
||||
FBox: TPaintBox;
|
||||
FMemo: TMemo; // декодированный текст бюллетеня (AO-40 кадры)
|
||||
FTimer: TTimer;
|
||||
FTheme: TAppTheme;
|
||||
FLastFrames: Int64; // счётчик кадров на прошлом тике (детект нового)
|
||||
procedure DoTick(Sender: TObject);
|
||||
procedure DoPaint(Sender: TObject);
|
||||
procedure PollFrame;
|
||||
function FrameToText(const Frame: TBeaconFrame): string;
|
||||
public
|
||||
constructor CreateWith(AOwner: TComponent; ACtl: TRadioController);
|
||||
procedure ApplyTheme(const T: TAppTheme);
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
constructor TBeaconScopeForm.CreateWith(AOwner: TComponent; ACtl: TRadioController);
|
||||
begin
|
||||
inherited CreateNew(AOwner);
|
||||
FCtl := ACtl;
|
||||
FTheme := DarkTheme;
|
||||
|
||||
Caption := 'QO-100 Beacon — Constellation + Bulletin';
|
||||
BorderStyle := bsSizeable;
|
||||
Width := 600;
|
||||
Height := 680;
|
||||
Position := poScreenCenter;
|
||||
|
||||
// Текст бюллетеня (декодированные AO-40 кадры) — снизу, моноширинный, ReadOnly.
|
||||
FMemo := TMemo.Create(Self);
|
||||
FMemo.Parent := Self;
|
||||
FMemo.Align := alBottom;
|
||||
FMemo.Height := 230;
|
||||
FMemo.ReadOnly := True;
|
||||
FMemo.ScrollBars := ssVertical;
|
||||
FMemo.WordWrap := False;
|
||||
FMemo.Font.Name := 'Courier New';
|
||||
FMemo.Font.Size := 9;
|
||||
FMemo.Color := FTheme.BG;
|
||||
FMemo.Font.Color := FTheme.Text;
|
||||
|
||||
FBox := TPaintBox.Create(Self);
|
||||
FBox.Parent := Self;
|
||||
FBox.Align := alClient;
|
||||
FBox.OnPaint := @DoPaint;
|
||||
|
||||
FTimer := TTimer.Create(Self);
|
||||
FTimer.Interval := 40; // ~25 Гц
|
||||
FTimer.OnTimer := @DoTick;
|
||||
FTimer.Enabled := True;
|
||||
end;
|
||||
|
||||
procedure TBeaconScopeForm.ApplyTheme(const T: TAppTheme);
|
||||
begin
|
||||
FTheme := T;
|
||||
if FMemo <> nil then
|
||||
begin
|
||||
FMemo.Color := FTheme.BG;
|
||||
FMemo.Font.Color := FTheme.Text;
|
||||
end;
|
||||
if FBox <> nil then FBox.Invalidate;
|
||||
end;
|
||||
|
||||
procedure TBeaconScopeForm.DoTick(Sender: TObject);
|
||||
begin
|
||||
if not Visible then Exit;
|
||||
PollFrame;
|
||||
if FBox <> nil then FBox.Invalidate;
|
||||
end;
|
||||
|
||||
function TBeaconScopeForm.FrameToText(const Frame: TBeaconFrame): string;
|
||||
// AO-40 кадр QO-100 = ASCII-текст (space-padded), последние 2 байта = CRC-16.
|
||||
// Как gr-satellites qo100.parse: отбрасываем CRC, печатные ASCII, строки по 64.
|
||||
var
|
||||
i, b: Integer;
|
||||
line: string;
|
||||
begin
|
||||
Result := '';
|
||||
i := 0;
|
||||
while i <= 253 do
|
||||
begin
|
||||
line := '';
|
||||
for b := i to Min(i + 63, 253) do
|
||||
if (Frame[b] >= 32) and (Frame[b] < 127) then line := line + Chr(Frame[b])
|
||||
else line := line + ' ';
|
||||
Result := Result + TrimRight(line) + LineEnding;
|
||||
Inc(i, 64);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TBeaconScopeForm.PollFrame;
|
||||
// Раз/тик: если декодер выдал НОВЫЙ кадр (S.Frames вырос) — печатаем бюллетень.
|
||||
var
|
||||
S: TBeaconScope;
|
||||
Frame: TBeaconFrame;
|
||||
rs: Integer;
|
||||
begin
|
||||
if FCtl = nil then Exit;
|
||||
if not FCtl.GetBeaconScope(S) then Exit;
|
||||
if S.Frames <= FLastFrames then Exit;
|
||||
FLastFrames := S.Frames;
|
||||
if not FCtl.GetBeaconFrame(Frame, rs) then Exit;
|
||||
FMemo.Append(Format('--- frame %d (RS err %d) ---', [S.Frames, rs]));
|
||||
FMemo.Append(FrameToText(Frame));
|
||||
while FMemo.Lines.Count > 400 do FMemo.Lines.Delete(0); // ограничиваем рост
|
||||
FMemo.SelStart := Length(FMemo.Text); // автоскролл вниз
|
||||
end;
|
||||
|
||||
procedure TBeaconScopeForm.DoPaint(Sender: TObject);
|
||||
var
|
||||
C: TCanvas;
|
||||
W, H, cx, cy, r, i, px, py: Integer;
|
||||
S: TBeaconScope;
|
||||
ok: Boolean;
|
||||
C1, C2: TColor;
|
||||
st: string;
|
||||
begin
|
||||
C := FBox.Canvas;
|
||||
W := FBox.Width; H := FBox.Height;
|
||||
|
||||
// фон
|
||||
C.Brush.Style := bsSolid;
|
||||
C.Brush.Color := FTheme.BG;
|
||||
C.FillRect(0, 0, W, H);
|
||||
|
||||
// область констелляции — квадрат сверху, текст снизу
|
||||
r := (Min(W, H - 70) - 20) div 2;
|
||||
if r < 20 then r := 20;
|
||||
if r > 110 then r := 110; // кап: оставляем место метрикам над memo
|
||||
cx := W div 2;
|
||||
cy := 10 + r;
|
||||
|
||||
// сетка/оси
|
||||
C.Pen.Style := psSolid;
|
||||
C.Pen.Color := FTheme.SpecGrid;
|
||||
C.Line(cx - r, cy, cx + r, cy); // ось I (горизонт)
|
||||
C.Line(cx, cy - r, cx, cy + r); // ось Q (вертикаль)
|
||||
C.Pen.Color := FTheme.Border;
|
||||
C.Brush.Style := bsClear;
|
||||
C.Rectangle(cx - r, cy - r, cx + r, cy + r);
|
||||
|
||||
// целевые точки BPSK (±1, 0) — ориентир
|
||||
C.Pen.Color := FTheme.TextDim;
|
||||
C.Ellipse(cx + r div 2 - 3, cy - 3, cx + r div 2 + 3, cy + 3);
|
||||
C.Ellipse(cx - r div 2 - 3, cy - 3, cx - r div 2 + 3, cy + 3);
|
||||
|
||||
ok := (FCtl <> nil) and FCtl.GetBeaconScope(S);
|
||||
|
||||
if ok and S.Enabled then
|
||||
begin
|
||||
// точки констелляции (нормированы к ±1; масштаб r/2 → ±1 на половине радиуса)
|
||||
C.Pen.Style := psClear;
|
||||
C.Brush.Style := bsSolid;
|
||||
C.Brush.Color := FTheme.MeterOn;
|
||||
for i := 0 to S.Count - 1 do
|
||||
begin
|
||||
px := cx + Round(S.PtI[i] * (r / 2));
|
||||
py := cy - Round(S.PtQ[i] * (r / 2));
|
||||
if (px >= cx - r) and (px <= cx + r) and (py >= cy - r) and (py <= cy + r) then
|
||||
C.FillRect(px - 1, py - 1, px + 2, py + 2);
|
||||
end;
|
||||
end;
|
||||
|
||||
// ----- метрики -----
|
||||
C.Brush.Style := bsClear;
|
||||
C.Font.Name := 'Courier New';
|
||||
C.Font.Size := 9;
|
||||
py := cy + r + 12;
|
||||
|
||||
if not ok then
|
||||
begin
|
||||
C.Font.Color := FTheme.TextDim;
|
||||
C.TextOut(14, py, 'decoder unavailable');
|
||||
Exit;
|
||||
end;
|
||||
if not S.Enabled then
|
||||
begin
|
||||
C.Font.Color := FTheme.TextDim;
|
||||
C.TextOut(14, py, 'decoder OFF (enable via BEACON \ decode)');
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if S.CarrierLock then C1 := FTheme.MeterOn else C1 := FTheme.TextDim;
|
||||
if S.SymbolLock then C2 := FTheme.MeterOn else C2 := FTheme.TextDim;
|
||||
|
||||
C.Font.Color := C1;
|
||||
C.TextOut(14, py, 'CARRIER ' + IfThen(S.CarrierLock, 'LOCK', '----'));
|
||||
C.Font.Color := C2;
|
||||
C.TextOut(14, py + 18, 'SYMBOL ' + IfThen(S.SymbolLock, 'LOCK', '----'));
|
||||
|
||||
C.Font.Color := FTheme.Text;
|
||||
if S.SNRdB > -50 then st := Format('%.1f dB', [S.SNRdB]) else st := '--';
|
||||
C.TextOut(14, py + 36, 'SNR ' + st);
|
||||
C.TextOut(14, py + 54, Format('BAUD %.1f (off %.0f Hz)', [S.SymRate, S.OffsetHz]));
|
||||
C.Font.Color := FTheme.TextDim;
|
||||
C.TextOut(14, py + 72, Format('CARRIER resid %.0f Hz prom %.0f', [S.ResidHz, S.CarProm]));
|
||||
if FCtl <> nil then
|
||||
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.
|
||||
+49
-1
@@ -39,7 +39,7 @@ uses
|
||||
WinFirewall,
|
||||
BoardUtils, WisdomBuilder, UISync,
|
||||
FlatEdit, FMRepeater,
|
||||
ChannelStore, ChannelsForm,
|
||||
ChannelStore, ChannelsForm, BeaconScopeForm,
|
||||
PowerInhibit,
|
||||
DeviceStore,
|
||||
RadioBackend, PlutoBackend,
|
||||
@@ -135,6 +135,7 @@ type
|
||||
FSpecDragX0: Integer; // X при нажатии
|
||||
FSpecDragFreq: Double; // FCenterFreq при нажатии
|
||||
FBeaconArming: Boolean; // ждём клик по маяку (после включения BEACON)
|
||||
FBeaconDecArming: Boolean; // ждём клик: навести узкий фильтр декодера на маяк
|
||||
FSpectrumDirty: Boolean; // таймер должен перерисовать спектр/водопад
|
||||
// TX DUC-очередь (FDUCPending*) переехала в TRadioController (OnTXIQ).
|
||||
FSMeterPeak: Double; // верхняя граница светлой зоны
|
||||
@@ -287,6 +288,7 @@ type
|
||||
BtnChannels: TFlatButton;
|
||||
FChannelsDropDown: TFlatDropDown;
|
||||
FChannelsForm: TObject; // TChannelsForm (cast при использовании)
|
||||
FBeaconScopeForm: TBeaconScopeForm; // окно констелляции маяка (ПКМ по BEACON)
|
||||
|
||||
// ---- Right panel ----
|
||||
PanelRight: TPanel;
|
||||
@@ -384,6 +386,8 @@ type
|
||||
procedure BtnFilterClick(Sender: TObject);
|
||||
procedure BtnCTunClick(Sender: TObject);
|
||||
procedure BtnBeaconClick(Sender: TObject);
|
||||
procedure BtnBeaconMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
procedure UpdateBeaconButton;
|
||||
procedure BtnDUPClick(Sender: TObject);
|
||||
procedure ApplyDUP(Active: Boolean);
|
||||
@@ -795,6 +799,7 @@ begin
|
||||
FController.FTXSpecGridStep := 10.0;
|
||||
FSettingsForm := nil;
|
||||
FChannelsForm := nil;
|
||||
FBeaconScopeForm := nil;
|
||||
FillChar(FController.FDevMAC, SizeOf(FController.FDevMAC), 0);
|
||||
|
||||
// Инициализируем кэш диапазонов умолчаниями
|
||||
@@ -1752,6 +1757,7 @@ begin
|
||||
LeftPanelButtonWidth(LEFT_W, 3, 2),
|
||||
BTN_H, BtnBeaconClick);
|
||||
BtnBeacon.Visible := False;
|
||||
BtnBeacon.OnMouseDown := BtnBeaconMouseDown; // ПКМ → декодер + окно констелляции
|
||||
StyleButton(BtnBeacon, False);
|
||||
|
||||
MakeLbl(PanelRXBlock, 'VOL', 4, 50);
|
||||
@@ -3272,6 +3278,9 @@ function TMainForm.BeaconStatusText: string;
|
||||
// Компактный статус QO-100 beacon lock для поля 7 статус-бара (Pluto). Courier 8pt,
|
||||
// поле ~200px → держим ≤ ~24 символов.
|
||||
begin
|
||||
if FBeaconDecArming then Exit('DEC: click beacon');
|
||||
if FController.BeaconDecodeEnabled and (FController.BeaconDecodeFreqHz > 0) then
|
||||
Exit('DEC: shift-click=re-aim');
|
||||
if not FController.BeaconLockEnabled then Exit('BCN: off');
|
||||
if FBeaconArming then Exit('BCN: click beacon');
|
||||
case FController.BeaconState of
|
||||
@@ -3988,6 +3997,28 @@ begin
|
||||
UpdateBeaconButton;
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnBeaconMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
// ПКМ по BEACON: включает фронтенд-декодер маяка (BPSK 400 бод) и открывает окно
|
||||
// констелляции для диагностики демодуляции (Milestone 1 — до подключения FEC).
|
||||
begin
|
||||
if Button <> mbRight then Exit;
|
||||
FController.SetBeaconDecode(not FController.BeaconDecodeEnabled);
|
||||
if FController.BeaconDecodeEnabled then
|
||||
begin
|
||||
// «Вооружаем» наведение: следующий ЛКМ по спектру поставит узкий фильтр
|
||||
// декодера на маяк. Потом перенавести — Shift+ЛКМ по спектру.
|
||||
FBeaconDecArming := True;
|
||||
if FBeaconScopeForm = nil then
|
||||
FBeaconScopeForm := TBeaconScopeForm.CreateWith(Self, FController);
|
||||
FBeaconScopeForm.ApplyTheme(DarkTheme);
|
||||
FBeaconScopeForm.Show;
|
||||
end
|
||||
else
|
||||
FBeaconDecArming := False;
|
||||
UpdateBeaconButton;
|
||||
end;
|
||||
|
||||
procedure TMainForm.UpdateBeaconButton;
|
||||
// Beacon-маркеры + кнопка BEACON. Кнопка видима только на Pluto в трансвертере
|
||||
// (см. rfXvtr) — это ПРОСТО кнопка: подпись 'BEACON', подсветка = лок включён.
|
||||
@@ -3996,9 +4027,15 @@ procedure TMainForm.UpdateBeaconButton;
|
||||
begin
|
||||
// Маркеры на спектре: опорная частота (зелёная) + отслеживаемый центроид (оранж).
|
||||
if FSpecView <> nil then
|
||||
begin
|
||||
FSpecView.SetBeaconMarkers(FController.BeaconLockEnabled,
|
||||
FController.BeaconRefHz,
|
||||
FController.BeaconTrackedFreqHz);
|
||||
// Узкий фильтр-маркер декодера (±450 Гц ≈ полоса BPSK-маяка).
|
||||
FSpecView.SetBeaconDecMarker(
|
||||
FController.BeaconDecodeEnabled and (FController.BeaconDecodeFreqHz > 0),
|
||||
FController.BeaconDecodeFreqHz, 450.0);
|
||||
end;
|
||||
UpdateStatusField7; // моментальный апдейт статус-бара (не ждём MeterTimer)
|
||||
if BtnBeacon = nil then Exit;
|
||||
BtnBeacon.Caption := 'BEACON';
|
||||
@@ -4186,6 +4223,17 @@ begin
|
||||
|
||||
if Button = mbLeft then
|
||||
begin
|
||||
// Наведение узкого фильтра ДЕКОДЕРА на маяк: после ПКМ BEACON (вооружено)
|
||||
// либо Shift+ЛКМ в любой момент при включённом декодере. Не трогает VFO.
|
||||
if FController.BeaconDecodeEnabled and (PbSpectrum.Width > 0) and
|
||||
(FBeaconDecArming or (ssShift in Shift)) then
|
||||
begin
|
||||
FController.SetBeaconDecodeAtHz(
|
||||
PixelToFreq(X, PbSpectrum.Width, FController.FCenterFreq, FController.FSpanHz));
|
||||
FBeaconDecArming := False;
|
||||
UpdateBeaconButton;
|
||||
Exit;
|
||||
end;
|
||||
// Наведение beacon-lock: ЛКМ указывает на маяк (вместо перестройки VFO).
|
||||
if FBeaconArming and (PbSpectrum.Width > 0) then
|
||||
begin
|
||||
|
||||
+120
-2
@@ -49,7 +49,7 @@ interface
|
||||
uses
|
||||
Classes, SysUtils, Math,
|
||||
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
||||
WDSPEngine, AudioOutput, AudioInput,
|
||||
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC,
|
||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
||||
|
||||
type
|
||||
@@ -196,6 +196,12 @@ type
|
||||
FBeaconLocked: Boolean; // пик уверенно виден
|
||||
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка пайплайна)
|
||||
FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков)
|
||||
// ---- QO-100 beacon DECODE (BPSK 400 бод, AO-40) — фронтенд+констелляция ----
|
||||
FBeaconDec: TBeaconDecoder; // владеет; тап IQ в WDSPEngine
|
||||
FBeaconDecOn: Boolean; // декодер включён пользователем
|
||||
FBeaconDecHz: Double; // АБСОЛЮТНАЯ display-частота наведения декодера
|
||||
// (узкий фильтр, ставится кликом); 0 = центр/DC
|
||||
FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True
|
||||
|
||||
// ---- Аудио / TX ----
|
||||
FVolume: Integer;
|
||||
@@ -486,6 +492,16 @@ type
|
||||
function BeaconRefHz: Double; // опорная частота маяка (для маркера)
|
||||
function BeaconTrackedFreqHz: Double;// отслеживаемая display-частота маяка (центроид)
|
||||
|
||||
// QO-100 beacon DECODE (BPSK 400 бод, AO-40) — фронтенд + констелляция.
|
||||
procedure SetBeaconDecode(On_: Boolean);
|
||||
procedure SetBeaconDecodeAtHz(DisplayHz: Double); // навести узкий фильтр декодера
|
||||
function BeaconDecodeEnabled: Boolean;
|
||||
function BeaconDecodeFreqHz: Double; // частота наведения (для маркера)
|
||||
procedure SetBeaconDecodeInvert(On_: Boolean); // зеркальный baseband вкл/выкл
|
||||
function BeaconDecodeInvert: Boolean;
|
||||
function GetBeaconScope(out S: TBeaconScope): Boolean;
|
||||
function GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
|
||||
|
||||
// Отображение / приём
|
||||
procedure SetCTun(On_: Boolean);
|
||||
procedure SetDuplex(On_: Boolean);
|
||||
@@ -596,6 +612,16 @@ begin
|
||||
FBeaconLockHz := 0.0;
|
||||
FBeaconLockOn := False;
|
||||
|
||||
// QO-100 beacon DECODE: фронтенд BPSK 400 бод (тап IQ в движке).
|
||||
FBeaconDec := TBeaconDecoder.Create;
|
||||
FBeaconDecOn := False;
|
||||
// Тракт QO-100 (LNB+трансвертер) зеркалит спектр: проверено приборно (symMag ×4
|
||||
// и prom 500-800 при invert=ON против шума при OFF, на наведении точно на маяк).
|
||||
// Декодеру нужно сопряжение, чтобы наведённый offset доставлял маяк к DC.
|
||||
FBeaconDecInvert := True;
|
||||
FBeaconDec.SetInvert(FBeaconDecInvert);
|
||||
FDSPEngine.SetBeaconDecoder(FBeaconDec);
|
||||
|
||||
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
|
||||
// формы / при подключении устройства).
|
||||
FAudioOut := TAudioOutput.Create(48000);
|
||||
@@ -612,7 +638,13 @@ begin
|
||||
end;
|
||||
if Assigned(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end;
|
||||
if Assigned(FAudioIn) then begin FAudioIn.Close; FreeAndNil(FAudioIn); end;
|
||||
if Assigned(FDSPEngine) then begin FDSPEngine.Close; FreeAndNil(FDSPEngine); end;
|
||||
if Assigned(FDSPEngine) then
|
||||
begin
|
||||
FDSPEngine.SetBeaconDecoder(nil);
|
||||
FDSPEngine.Close;
|
||||
FreeAndNil(FDSPEngine);
|
||||
end;
|
||||
if Assigned(FBeaconDec) then FreeAndNil(FBeaconDec);
|
||||
end;
|
||||
|
||||
destructor TRadioController.Destroy;
|
||||
@@ -1200,6 +1232,25 @@ procedure TRadioController.ServiceBeaconLock;
|
||||
// лок не теряется; во время settle позицию не двигаем (FFT в транзиенте).
|
||||
var win, pk, prom, cand, e, dLock: Double;
|
||||
begin
|
||||
// Beacon-декодер: ЦЕЛЬ — маяк на DC IQ-тапа (= центр RX-спектра, тот же сырой
|
||||
// DDC-IQ). По умолчанию offset=0: пользователь ставит маяк в центр спектра
|
||||
// (CTUN/перестройка), squaring-FFT acquisition доводит остаток. ТОЛЬКО когда
|
||||
// beacon-lock реально захвачен (зелёный) — доверяем его оценке и сносим
|
||||
// FBeaconTrackHz−FCenterFreq (авто-центровка). Развязано от сырого FBeaconTrackHz,
|
||||
// т.к. при выключенном/несошедшемся локе он недостоверен.
|
||||
// Декодер НЕ зависит от beacon-lock: lock физически ретюнит железо
|
||||
// (ApplyBeaconLOError → PushNetworkState), что рвёт IQ-поток и сбивает петли
|
||||
// декодера (Costas/FLL/Gardner), которым нужны секунды непрерывного сигнала.
|
||||
// У декодера своя петля несущей — она точнее, чем display-FFT-lock. Поэтому
|
||||
// offset = только явный клик-аим, иначе DC. Авто-offset от FBeaconTrackHz убран.
|
||||
if FBeaconDecOn and Assigned(FBeaconDec) then
|
||||
begin
|
||||
if FBeaconDecHz > 0 then
|
||||
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq) // наведённый узкий фильтр
|
||||
else
|
||||
FBeaconDec.SetOffsetHz(0); // иначе — центр/DC
|
||||
end;
|
||||
|
||||
if not FBeaconLockOn then Exit;
|
||||
if FCurrentXvtr < 0 then Exit; // только в трансвертер-режиме
|
||||
if FBeaconSaveCooldown > 0 then Dec(FBeaconSaveCooldown);
|
||||
@@ -1310,6 +1361,73 @@ begin
|
||||
Result := FBeaconTrackHz;
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetBeaconDecode(On_: Boolean);
|
||||
// Включает фронтенд-декодер маяка. Цель — маяк на DC (центр RX-спектра): стартуем
|
||||
// с offset=0 (или явного клик-аима), дальше частоту держит петля несущей декодера.
|
||||
// От beacon-lock декодер НЕ зависит (см. ServiceBeaconLock). Пользователь наводит
|
||||
// клик на маяк либо центрирует его в спектре, acquisition/Costas доводят.
|
||||
begin
|
||||
FBeaconDecOn := On_;
|
||||
if not Assigned(FBeaconDec) then Exit;
|
||||
if not On_ then FBeaconDecHz := 0; // сброс наведения при выключении
|
||||
if On_ then
|
||||
begin
|
||||
if FBeaconDecHz > 0 then
|
||||
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq)
|
||||
else
|
||||
FBeaconDec.SetOffsetHz(0);
|
||||
end;
|
||||
FBeaconDec.SetEnabled(On_);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetBeaconDecodeAtHz(DisplayHz: Double);
|
||||
// Пользователь навёл узкий фильтр на маяк (клик по спектру). Запоминаем абсолютную
|
||||
// частоту, сносим её к DC и переинициализируем петли (одноразовая грубая оценка
|
||||
// заново). Снос держится в ServiceBeaconLock при перестройке/панораме.
|
||||
begin
|
||||
FBeaconDecHz := DisplayHz;
|
||||
if Assigned(FBeaconDec) then
|
||||
begin
|
||||
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq);
|
||||
FBeaconDec.Reseed;
|
||||
end;
|
||||
Changed(rfBeaconLock);
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconDecodeEnabled: Boolean;
|
||||
begin
|
||||
Result := FBeaconDecOn;
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconDecodeFreqHz: Double;
|
||||
begin
|
||||
Result := FBeaconDecHz;
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetBeaconDecodeInvert(On_: Boolean);
|
||||
begin
|
||||
FBeaconDecInvert := On_;
|
||||
if Assigned(FBeaconDec) then FBeaconDec.SetInvert(On_);
|
||||
Changed(rfBeaconLock);
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconDecodeInvert: Boolean;
|
||||
begin
|
||||
Result := FBeaconDecInvert;
|
||||
end;
|
||||
|
||||
function TRadioController.GetBeaconScope(out S: TBeaconScope): Boolean;
|
||||
begin
|
||||
Result := Assigned(FBeaconDec);
|
||||
if Result then FBeaconDec.GetScope(S)
|
||||
else FillChar(S, SizeOf(S), 0);
|
||||
end;
|
||||
|
||||
function TRadioController.GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
|
||||
begin
|
||||
Result := Assigned(FBeaconDec) and FBeaconDec.GetLastFrame(Frame, RSErrors);
|
||||
end;
|
||||
|
||||
procedure TRadioController.ApplyModeFilter;
|
||||
var
|
||||
Lo, Hi, Half: Integer;
|
||||
|
||||
+30
-3
@@ -77,6 +77,10 @@ type
|
||||
FBeaconMarkActive: Boolean;
|
||||
FBeaconRefFreq: Double; // опорная частота маяка (где ДОЛЖЕН быть)
|
||||
FBeaconTrkFreq: Double; // отслеживаемый центроид маяка сейчас
|
||||
// узкий фильтр-маркер декодера (наведение пользователем)
|
||||
FBeaconDecActive: Boolean;
|
||||
FBeaconDecFreq: Double;
|
||||
FBeaconDecHalf: Double; // полуширина фильтра, Гц
|
||||
// ── Spectrum buffer ───────────────────────────────────────────────────────
|
||||
FSpectrumBuf: array[0..1023] of Single;
|
||||
FSpectrumBufCount: Integer;
|
||||
@@ -190,6 +194,7 @@ type
|
||||
// QO-100 beacon-маркеры: опорная частота (зелёная) + позиция трекера (оранж).
|
||||
// Спектр перерисовывается каждый кадр, поэтому отдельный invalidate не нужен.
|
||||
procedure SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double);
|
||||
procedure SetBeaconDecMarker(Active: Boolean; FreqHz, HalfHz: Double);
|
||||
|
||||
// ── S-метр ────────────────────────────────────────────────────────────────
|
||||
property LastSMeter: Double read GetLastSMeter write SetLastSMeter;
|
||||
@@ -501,6 +506,13 @@ begin
|
||||
FBeaconTrkFreq := TrkHz;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.SetBeaconDecMarker(Active: Boolean; FreqHz, HalfHz: Double);
|
||||
begin
|
||||
FBeaconDecActive := Active;
|
||||
FBeaconDecFreq := FreqHz;
|
||||
FBeaconDecHalf := HalfHz;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.DrawBeaconMarkers(C: TCanvas; W, H: Integer);
|
||||
// Две вертикали: опорная частота маяка (зелёная пунктир) и отслеживаемый
|
||||
// центроид (оранжевая сплошная). Расхождение видно глазом → понятно, сел ли
|
||||
@@ -514,9 +526,24 @@ procedure TSpectrumView.DrawBeaconMarkers(C: TCanvas; W, H: Integer);
|
||||
C.Pen.Color := Col; C.Pen.Width := 1; C.Pen.Style := Style;
|
||||
C.MoveTo(x, 0); C.LineTo(x, H);
|
||||
end;
|
||||
var
|
||||
xc, x1, x2: Integer;
|
||||
begin
|
||||
Vline(FBeaconRefFreq, clLime, psDot); // где маяк ДОЛЖЕН быть
|
||||
Vline(FBeaconTrkFreq, TColor($000AA5FF), psSolid); // отслеживаемый центроид (оранж)
|
||||
if FBeaconMarkActive then
|
||||
begin
|
||||
Vline(FBeaconRefFreq, clLime, psDot); // где маяк ДОЛЖЕН быть
|
||||
Vline(FBeaconTrkFreq, TColor($000AA5FF), psSolid); // отслеживаемый центроид (оранж)
|
||||
end;
|
||||
// Узкий фильтр-маркер декодера: жёлтые грани полосы + центральная линия.
|
||||
if FBeaconDecActive and (FSpanHz > 0) then
|
||||
begin
|
||||
xc := Round((FBeaconDecFreq - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
|
||||
x1 := Round((FBeaconDecFreq - FBeaconDecHalf - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
|
||||
x2 := Round((FBeaconDecFreq + FBeaconDecHalf - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
|
||||
BlendBand(x1, x2, H, $FF, $D0, $20, 48); // полупрозрачная полоса
|
||||
C.Pen.Color := TColor($0020D0FF); C.Pen.Width := 1; C.Pen.Style := psSolid;
|
||||
if (xc >= 0) and (xc < W) then begin C.MoveTo(xc, 0); C.LineTo(xc, H); end;
|
||||
end;
|
||||
C.Pen.Style := psSolid;
|
||||
end;
|
||||
|
||||
@@ -975,7 +1002,7 @@ begin
|
||||
end;
|
||||
|
||||
if FMarkerActive then DrawMarkerLine(C, W, H);
|
||||
if FBeaconMarkActive then DrawBeaconMarkers(C, W, H);
|
||||
if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkers(C, W, H);
|
||||
DrawADCOverloadOverlay(C, W, H);
|
||||
if Assigned(FSampleRateOverlay) then
|
||||
FSampleRateOverlay.DrawOverlay(FSpectrumBitmap, C, W, H);
|
||||
|
||||
+12
-3
@@ -668,8 +668,17 @@ procedure TSpectrumViewOpenGL.DrawBeaconMarkersGL(W, H: Integer);
|
||||
DrawLine(x, 0, x, H, Col, 1, False);
|
||||
end;
|
||||
begin
|
||||
Vline(FBeaconRefFreq, clLime); // где маяк ДОЛЖЕН быть
|
||||
Vline(FBeaconTrkFreq, TColor($000AA5FF)); // отслеживаемый центроид (оранж)
|
||||
if FBeaconMarkActive then
|
||||
begin
|
||||
Vline(FBeaconRefFreq, clLime); // где маяк ДОЛЖЕН быть
|
||||
Vline(FBeaconTrkFreq, TColor($000AA5FF)); // отслеживаемый центроид (оранж)
|
||||
end;
|
||||
if FBeaconDecActive then
|
||||
begin
|
||||
Vline(FBeaconDecFreq - FBeaconDecHalf, TColor($0020D0FF)); // грань фильтра
|
||||
Vline(FBeaconDecFreq + FBeaconDecHalf, TColor($0020D0FF));
|
||||
Vline(FBeaconDecFreq, TColor($0020D0FF)); // центр наведения
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TSpectrumViewOpenGL.DrawADCOverlay(W, H: Integer);
|
||||
@@ -797,7 +806,7 @@ begin
|
||||
DrawAGCLines(W, H, DBmax, InvRange);
|
||||
DrawSpectrumCurve(W, H, DBmax, InvRange);
|
||||
DrawMarker(W, H);
|
||||
if FBeaconMarkActive then DrawBeaconMarkersGL(W, H);
|
||||
if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkersGL(W, H);
|
||||
DrawADCOverlay(W, H);
|
||||
DrawCachedOverlays(W, H);
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, Math, SyncObjs,
|
||||
BeaconDecoder,
|
||||
WDSP;
|
||||
|
||||
const
|
||||
@@ -292,6 +293,7 @@ type
|
||||
FMuted: Boolean;
|
||||
FVolume: Double;
|
||||
FLastError: string;
|
||||
FBeaconDec: TBeaconDecoder; // не владеет; тап маяка в PushIQItemToDSP
|
||||
|
||||
function ModeToWDSP(Mode: Integer): Integer;
|
||||
procedure ApplyDefaultFilter;
|
||||
@@ -331,6 +333,9 @@ type
|
||||
procedure Close;
|
||||
procedure ChangeSampleRate(NewRate: Integer);
|
||||
|
||||
// QO-100 beacon-декодер: тап RX-IQ для демодуляции маяка (не владеет).
|
||||
procedure SetBeaconDecoder(D: TBeaconDecoder);
|
||||
|
||||
// Подача 24-bit big-endian IQ из DDC пакета
|
||||
// Buf — массив байт, DataOffset — смещение до IQ данных внутри Buf
|
||||
procedure PushDDCPacket(const Buf: array of Byte;
|
||||
@@ -1131,6 +1136,15 @@ begin
|
||||
|
||||
// Переинициализируем WDSP с новым rate
|
||||
Open;
|
||||
|
||||
// Beacon-декодер пересчитывает децимацию/RRC под новый IQ-rate.
|
||||
if FBeaconDec <> nil then FBeaconDec.Configure(FSampleRate);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetBeaconDecoder(D: TBeaconDecoder);
|
||||
begin
|
||||
FBeaconDec := D;
|
||||
if (D <> nil) and (FSampleRate > 0) then D.Configure(FSampleRate);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.PushDDCPacket(const Buf: array of Byte;
|
||||
@@ -1213,6 +1227,8 @@ begin
|
||||
|
||||
FeedDisplaySample(IR * SCALE, QR * SCALE);
|
||||
|
||||
if FBeaconDec <> nil then FBeaconDec.Feed(IR * SCALE, QR * SCALE);
|
||||
|
||||
FRXAccI[FRXAccPos] := IR * SCALE;
|
||||
FRXAccQ[FRXAccPos] := QR * SCALE;
|
||||
Inc(FRXAccPos);
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<UseVersionInfo Value="True"/>
|
||||
<AutoIncrementBuild Value="True"/>
|
||||
<MinorVersionNr Value="9"/>
|
||||
<BuildNr Value="213"/>
|
||||
<BuildNr Value="214"/>
|
||||
</VersionInfo>
|
||||
<MacroValues Count="25">
|
||||
<MacroValues Count="26">
|
||||
<Macro1 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro2 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro3 Name="LCLWidgetType" Value="qt6"/>
|
||||
@@ -45,6 +45,7 @@
|
||||
<Macro23 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro24 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro25 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro26 Name="LCLWidgetType" Value="qt6"/>
|
||||
</MacroValues>
|
||||
<BuildModes>
|
||||
<Item Name="Debug" Default="True"/>
|
||||
@@ -130,6 +131,18 @@
|
||||
<Filename Value="WDSPEngine.pas"/>
|
||||
<IsPartOfProject Value="True"/>
|
||||
</Unit>
|
||||
<Unit>
|
||||
<Filename Value="BeaconDecoder.pas"/>
|
||||
<IsPartOfProject Value="True"/>
|
||||
</Unit>
|
||||
<Unit>
|
||||
<Filename Value="BeaconFEC.pas"/>
|
||||
<IsPartOfProject Value="True"/>
|
||||
</Unit>
|
||||
<Unit>
|
||||
<Filename Value="BeaconScopeForm.pas"/>
|
||||
<IsPartOfProject Value="True"/>
|
||||
</Unit>
|
||||
<Unit>
|
||||
<Filename Value="VfoOverlay.pas"/>
|
||||
<IsPartOfProject Value="True"/>
|
||||
|
||||
+18
@@ -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.
|
||||
+104
@@ -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.
|
||||
Reference in New Issue
Block a user