mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-27 01:37:33 +00:00
feat: QO-100 central beacon BPSK decoder (Milestone 1 — stable demod)
Декодер центрального маяка QO-100 (10489.750, AO-40 BPSK 400 бод): фронтенд до мягких символов + констелляция, с устойчивым захватом несущей/символа. Цепочка (BeaconDecoder.pas): NCO-снос смещения → boxcar-децим до 9600 → RRC матч-фильтр → AGC-нормировка → Gardner timing → Costas BPSK с frequency offload в пред-RRC NCO. squaring-FFT оставлена индикатором несущей (prom). Ключевые решения по итогам эфирной отладки: - invert=True для тракта QO-100 (LNB+трансвертер зеркалит спектр). - Декодер развязан от beacon-lock (тот ретюнил железо, рвал IQ). - AGC-нормировка петель: сигнал ~-60 dBFS замораживал Costas/Gardner. - Frequency offload: накопл. снос Costas стекает в FResidInc (без потолка ±π/символ) → держит ошибку наведения (1пкс≈300Гц) + дрейф LNB. Интеграция: тап IQ в WDSPEngine.PushIQItemToDSP; RadioController владеет декодером, клик-наведение (ПКМ BEACON + ЛКМ/Shift+ЛКМ по спектру); узкий фильтр-маркер в SpectrumView/GL; окно констелляции BeaconScopeForm; файловый лог HOME/ewsdr_beacon.log для диагностики. Milestone 2 (далее): FEC — Viterbi k=7 + RS(160,128) + интерливер + дескремблер. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,689 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
const
|
||||||
|
BCN_BAUD = 400.0; // символьная скорость маяка
|
||||||
|
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; // холдовер-счётчик лока
|
||||||
|
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;
|
||||||
|
|
||||||
|
// --- Gardner timing ---
|
||||||
|
FMuAcc: Double; // обратный отсчёт до следующего полу-символьного строба
|
||||||
|
FHalf: Boolean; // текущий строб = середина (true) или центр символа
|
||||||
|
FMidI, FMidQ: Double; // отсчёт середины (для Gardner)
|
||||||
|
FPrevI, FPrevQ: Double; // прошлый центр символа
|
||||||
|
FYd0I, FYd0Q: Double; // линия задержки RRC-выхода (новейший)
|
||||||
|
FYd1I, FYd1Q: Double; // предыдущий
|
||||||
|
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; // линия несущей видна
|
||||||
|
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 (нормировка петель)
|
||||||
|
|
||||||
|
// --- констелляция (кольцо) ---
|
||||||
|
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 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);
|
||||||
|
end;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
constructor TBeaconDecoder.Create;
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FLock := TCriticalSection.Create;
|
||||||
|
FFs := 0;
|
||||||
|
FEnabled := False;
|
||||||
|
FConstScale := 1.0;
|
||||||
|
Configure(192000);
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TBeaconDecoder.Destroy;
|
||||||
|
begin
|
||||||
|
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), возводим в квадрат
|
||||||
|
// (снимаем BPSK), FFT → ищем самую сильную линию вне зоны DC. prominence (pk/среднее)
|
||||||
|
// = «несущая видна» (FCarPresent). Частоту НЕ оцениваем — снос целиком ведёт
|
||||||
|
// Costas + offload (см. ProcessSymbol); squaring-FFT давала неверный знак.
|
||||||
|
var
|
||||||
|
i, n, loBin: Integer;
|
||||||
|
mr, mi, re, im, mag, pk, sumMag, meanMag: 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; 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;
|
||||||
|
end;
|
||||||
|
meanMag := sumMag / n;
|
||||||
|
if meanMag > 1e-30 then
|
||||||
|
FCarProm := FCarProm + 0.4 * (pk / meanMag - FCarProm);
|
||||||
|
FCarPresent := FCarProm > 20.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;
|
||||||
|
FHalf := False;
|
||||||
|
FMidI := 0; FMidQ := 0; FPrevI := 0; FPrevQ := 0;
|
||||||
|
FYd0I := 0; FYd0Q := 0; FYd1I := 0; FYd1Q := 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;
|
||||||
|
FLockHold := 0;
|
||||||
|
FPrevRre := 0; FPrevRim := 0;
|
||||||
|
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_BAUD;
|
||||||
|
|
||||||
|
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, step, cr, sr, ri, ii: Double;
|
||||||
|
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-выхода для линейной интерполяции
|
||||||
|
FYd1I := FYd0I; FYd1Q := FYd0Q;
|
||||||
|
FYd0I := yi; FYd0Q := yq;
|
||||||
|
|
||||||
|
// Gardner: полу-символьные стробы. FMuAcc убывает на 1/отсчёт; при переходе
|
||||||
|
// через 0 интерполируем и планируем следующий строб через ~FSps/2 отсчётов.
|
||||||
|
FMuAcc := FMuAcc - 1.0;
|
||||||
|
if FMuAcc <= 0.0 then
|
||||||
|
begin
|
||||||
|
frac := FMuAcc + 1.0; // позиция в (0..1] между Yd1 и Yd0
|
||||||
|
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;
|
||||||
|
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.
|
||||||
|
leak := 0.01 * FCarFreq;
|
||||||
|
FResidInc := FResidInc + leak / FSps; // рад/символ → рад/децим-отсчёт
|
||||||
|
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;
|
||||||
|
|
||||||
|
// метрики — медленные 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);
|
||||||
|
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_BAUD / (2.0 * Pi); // рад/символ → Гц
|
||||||
|
S.LockHold := FLockHold;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
end.
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
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,
|
||||||
|
AppTheme, BeaconDecoder, RadioController;
|
||||||
|
|
||||||
|
type
|
||||||
|
TBeaconScopeForm = class(TForm)
|
||||||
|
private
|
||||||
|
FCtl: TRadioController;
|
||||||
|
FBox: TPaintBox;
|
||||||
|
FTimer: TTimer;
|
||||||
|
FTheme: TAppTheme;
|
||||||
|
procedure DoTick(Sender: TObject);
|
||||||
|
procedure DoPaint(Sender: TObject);
|
||||||
|
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';
|
||||||
|
BorderStyle := bsSizeable;
|
||||||
|
Width := 360;
|
||||||
|
Height := 510;
|
||||||
|
Position := poScreenCenter;
|
||||||
|
|
||||||
|
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 FBox <> nil then FBox.Invalidate;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TBeaconScopeForm.DoTick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
if (FBox <> nil) and Visible then FBox.Invalidate;
|
||||||
|
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;
|
||||||
|
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]));
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
+49
-1
@@ -39,7 +39,7 @@ uses
|
|||||||
WinFirewall,
|
WinFirewall,
|
||||||
BoardUtils, WisdomBuilder, UISync,
|
BoardUtils, WisdomBuilder, UISync,
|
||||||
FlatEdit, FMRepeater,
|
FlatEdit, FMRepeater,
|
||||||
ChannelStore, ChannelsForm,
|
ChannelStore, ChannelsForm, BeaconScopeForm,
|
||||||
PowerInhibit,
|
PowerInhibit,
|
||||||
DeviceStore,
|
DeviceStore,
|
||||||
RadioBackend, PlutoBackend,
|
RadioBackend, PlutoBackend,
|
||||||
@@ -135,6 +135,7 @@ type
|
|||||||
FSpecDragX0: Integer; // X при нажатии
|
FSpecDragX0: Integer; // X при нажатии
|
||||||
FSpecDragFreq: Double; // FCenterFreq при нажатии
|
FSpecDragFreq: Double; // FCenterFreq при нажатии
|
||||||
FBeaconArming: Boolean; // ждём клик по маяку (после включения BEACON)
|
FBeaconArming: Boolean; // ждём клик по маяку (после включения BEACON)
|
||||||
|
FBeaconDecArming: Boolean; // ждём клик: навести узкий фильтр декодера на маяк
|
||||||
FSpectrumDirty: Boolean; // таймер должен перерисовать спектр/водопад
|
FSpectrumDirty: Boolean; // таймер должен перерисовать спектр/водопад
|
||||||
// TX DUC-очередь (FDUCPending*) переехала в TRadioController (OnTXIQ).
|
// TX DUC-очередь (FDUCPending*) переехала в TRadioController (OnTXIQ).
|
||||||
FSMeterPeak: Double; // верхняя граница светлой зоны
|
FSMeterPeak: Double; // верхняя граница светлой зоны
|
||||||
@@ -287,6 +288,7 @@ type
|
|||||||
BtnChannels: TFlatButton;
|
BtnChannels: TFlatButton;
|
||||||
FChannelsDropDown: TFlatDropDown;
|
FChannelsDropDown: TFlatDropDown;
|
||||||
FChannelsForm: TObject; // TChannelsForm (cast при использовании)
|
FChannelsForm: TObject; // TChannelsForm (cast при использовании)
|
||||||
|
FBeaconScopeForm: TBeaconScopeForm; // окно констелляции маяка (ПКМ по BEACON)
|
||||||
|
|
||||||
// ---- Right panel ----
|
// ---- Right panel ----
|
||||||
PanelRight: TPanel;
|
PanelRight: TPanel;
|
||||||
@@ -384,6 +386,8 @@ type
|
|||||||
procedure BtnFilterClick(Sender: TObject);
|
procedure BtnFilterClick(Sender: TObject);
|
||||||
procedure BtnCTunClick(Sender: TObject);
|
procedure BtnCTunClick(Sender: TObject);
|
||||||
procedure BtnBeaconClick(Sender: TObject);
|
procedure BtnBeaconClick(Sender: TObject);
|
||||||
|
procedure BtnBeaconMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
|
Shift: TShiftState; X, Y: Integer);
|
||||||
procedure UpdateBeaconButton;
|
procedure UpdateBeaconButton;
|
||||||
procedure BtnDUPClick(Sender: TObject);
|
procedure BtnDUPClick(Sender: TObject);
|
||||||
procedure ApplyDUP(Active: Boolean);
|
procedure ApplyDUP(Active: Boolean);
|
||||||
@@ -795,6 +799,7 @@ begin
|
|||||||
FController.FTXSpecGridStep := 10.0;
|
FController.FTXSpecGridStep := 10.0;
|
||||||
FSettingsForm := nil;
|
FSettingsForm := nil;
|
||||||
FChannelsForm := nil;
|
FChannelsForm := nil;
|
||||||
|
FBeaconScopeForm := nil;
|
||||||
FillChar(FController.FDevMAC, SizeOf(FController.FDevMAC), 0);
|
FillChar(FController.FDevMAC, SizeOf(FController.FDevMAC), 0);
|
||||||
|
|
||||||
// Инициализируем кэш диапазонов умолчаниями
|
// Инициализируем кэш диапазонов умолчаниями
|
||||||
@@ -1752,6 +1757,7 @@ begin
|
|||||||
LeftPanelButtonWidth(LEFT_W, 3, 2),
|
LeftPanelButtonWidth(LEFT_W, 3, 2),
|
||||||
BTN_H, BtnBeaconClick);
|
BTN_H, BtnBeaconClick);
|
||||||
BtnBeacon.Visible := False;
|
BtnBeacon.Visible := False;
|
||||||
|
BtnBeacon.OnMouseDown := BtnBeaconMouseDown; // ПКМ → декодер + окно констелляции
|
||||||
StyleButton(BtnBeacon, False);
|
StyleButton(BtnBeacon, False);
|
||||||
|
|
||||||
MakeLbl(PanelRXBlock, 'VOL', 4, 50);
|
MakeLbl(PanelRXBlock, 'VOL', 4, 50);
|
||||||
@@ -3272,6 +3278,9 @@ function TMainForm.BeaconStatusText: string;
|
|||||||
// Компактный статус QO-100 beacon lock для поля 7 статус-бара (Pluto). Courier 8pt,
|
// Компактный статус QO-100 beacon lock для поля 7 статус-бара (Pluto). Courier 8pt,
|
||||||
// поле ~200px → держим ≤ ~24 символов.
|
// поле ~200px → держим ≤ ~24 символов.
|
||||||
begin
|
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 not FController.BeaconLockEnabled then Exit('BCN: off');
|
||||||
if FBeaconArming then Exit('BCN: click beacon');
|
if FBeaconArming then Exit('BCN: click beacon');
|
||||||
case FController.BeaconState of
|
case FController.BeaconState of
|
||||||
@@ -3988,6 +3997,28 @@ begin
|
|||||||
UpdateBeaconButton;
|
UpdateBeaconButton;
|
||||||
end;
|
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;
|
procedure TMainForm.UpdateBeaconButton;
|
||||||
// Beacon-маркеры + кнопка BEACON. Кнопка видима только на Pluto в трансвертере
|
// Beacon-маркеры + кнопка BEACON. Кнопка видима только на Pluto в трансвертере
|
||||||
// (см. rfXvtr) — это ПРОСТО кнопка: подпись 'BEACON', подсветка = лок включён.
|
// (см. rfXvtr) — это ПРОСТО кнопка: подпись 'BEACON', подсветка = лок включён.
|
||||||
@@ -3996,9 +4027,15 @@ procedure TMainForm.UpdateBeaconButton;
|
|||||||
begin
|
begin
|
||||||
// Маркеры на спектре: опорная частота (зелёная) + отслеживаемый центроид (оранж).
|
// Маркеры на спектре: опорная частота (зелёная) + отслеживаемый центроид (оранж).
|
||||||
if FSpecView <> nil then
|
if FSpecView <> nil then
|
||||||
|
begin
|
||||||
FSpecView.SetBeaconMarkers(FController.BeaconLockEnabled,
|
FSpecView.SetBeaconMarkers(FController.BeaconLockEnabled,
|
||||||
FController.BeaconRefHz,
|
FController.BeaconRefHz,
|
||||||
FController.BeaconTrackedFreqHz);
|
FController.BeaconTrackedFreqHz);
|
||||||
|
// Узкий фильтр-маркер декодера (±450 Гц ≈ полоса BPSK-маяка).
|
||||||
|
FSpecView.SetBeaconDecMarker(
|
||||||
|
FController.BeaconDecodeEnabled and (FController.BeaconDecodeFreqHz > 0),
|
||||||
|
FController.BeaconDecodeFreqHz, 450.0);
|
||||||
|
end;
|
||||||
UpdateStatusField7; // моментальный апдейт статус-бара (не ждём MeterTimer)
|
UpdateStatusField7; // моментальный апдейт статус-бара (не ждём MeterTimer)
|
||||||
if BtnBeacon = nil then Exit;
|
if BtnBeacon = nil then Exit;
|
||||||
BtnBeacon.Caption := 'BEACON';
|
BtnBeacon.Caption := 'BEACON';
|
||||||
@@ -4186,6 +4223,17 @@ begin
|
|||||||
|
|
||||||
if Button = mbLeft then
|
if Button = mbLeft then
|
||||||
begin
|
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).
|
// Наведение beacon-lock: ЛКМ указывает на маяк (вместо перестройки VFO).
|
||||||
if FBeaconArming and (PbSpectrum.Width > 0) then
|
if FBeaconArming and (PbSpectrum.Width > 0) then
|
||||||
begin
|
begin
|
||||||
|
|||||||
+162
-2
@@ -49,7 +49,7 @@ interface
|
|||||||
uses
|
uses
|
||||||
Classes, SysUtils, Math,
|
Classes, SysUtils, Math,
|
||||||
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
||||||
WDSPEngine, AudioOutput, AudioInput,
|
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder,
|
||||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
||||||
|
|
||||||
type
|
type
|
||||||
@@ -196,6 +196,14 @@ type
|
|||||||
FBeaconLocked: Boolean; // пик уверенно виден
|
FBeaconLocked: Boolean; // пик уверенно виден
|
||||||
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка пайплайна)
|
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка пайплайна)
|
||||||
FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков)
|
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
|
||||||
|
FBeaconLogTick: Integer; // троттл периодического лога состояния декодера
|
||||||
|
FBeaconLogScope: TBeaconScope; // буфер снимка для лога (не аллоцируем в контуре)
|
||||||
|
|
||||||
// ---- Аудио / TX ----
|
// ---- Аудио / TX ----
|
||||||
FVolume: Integer;
|
FVolume: Integer;
|
||||||
@@ -477,6 +485,7 @@ type
|
|||||||
// QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку.
|
// QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку.
|
||||||
procedure SetBeaconLock(On_: Boolean);
|
procedure SetBeaconLock(On_: Boolean);
|
||||||
procedure ServiceBeaconLock; // одна итерация контура (MeterTimer @10 Гц)
|
procedure ServiceBeaconLock; // одна итерация контура (MeterTimer @10 Гц)
|
||||||
|
procedure BeaconLog(const Msg: string); // диагностика декодера → HOME/ewsdr_beacon.log
|
||||||
procedure SetBeaconRefHz(Hz: Double); // опорная частота маяка (downlink)
|
procedure SetBeaconRefHz(Hz: Double); // опорная частота маяка (downlink)
|
||||||
procedure BeaconSeedAtHz(DisplayHz: Double); // пользователь указал маяк кликом
|
procedure BeaconSeedAtHz(DisplayHz: Double); // пользователь указал маяк кликом
|
||||||
function BeaconState: TBeaconState;
|
function BeaconState: TBeaconState;
|
||||||
@@ -486,6 +495,15 @@ type
|
|||||||
function BeaconRefHz: Double; // опорная частота маяка (для маркера)
|
function BeaconRefHz: Double; // опорная частота маяка (для маркера)
|
||||||
function BeaconTrackedFreqHz: Double;// отслеживаемая display-частота маяка (центроид)
|
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;
|
||||||
|
|
||||||
// Отображение / приём
|
// Отображение / приём
|
||||||
procedure SetCTun(On_: Boolean);
|
procedure SetCTun(On_: Boolean);
|
||||||
procedure SetDuplex(On_: Boolean);
|
procedure SetDuplex(On_: Boolean);
|
||||||
@@ -596,6 +614,16 @@ begin
|
|||||||
FBeaconLockHz := 0.0;
|
FBeaconLockHz := 0.0;
|
||||||
FBeaconLockOn := False;
|
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 вызывается позже (после показа
|
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
|
||||||
// формы / при подключении устройства).
|
// формы / при подключении устройства).
|
||||||
FAudioOut := TAudioOutput.Create(48000);
|
FAudioOut := TAudioOutput.Create(48000);
|
||||||
@@ -612,7 +640,13 @@ begin
|
|||||||
end;
|
end;
|
||||||
if Assigned(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end;
|
if Assigned(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end;
|
||||||
if Assigned(FAudioIn) then begin FAudioIn.Close; FreeAndNil(FAudioIn); 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;
|
end;
|
||||||
|
|
||||||
destructor TRadioController.Destroy;
|
destructor TRadioController.Destroy;
|
||||||
@@ -1200,6 +1234,45 @@ procedure TRadioController.ServiceBeaconLock;
|
|||||||
// лок не теряется; во время settle позицию не двигаем (FFT в транзиенте).
|
// лок не теряется; во время settle позицию не двигаем (FFT в транзиенте).
|
||||||
var win, pk, prom, cand, e, dLock: Double;
|
var win, pk, prom, cand, e, dLock: Double;
|
||||||
begin
|
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
|
||||||
|
|
||||||
|
// Периодический снимок состояния (раз в ~1с при @10Гц-таймере) в лог-файл.
|
||||||
|
Inc(FBeaconLogTick);
|
||||||
|
if FBeaconLogTick >= 10 then
|
||||||
|
begin
|
||||||
|
FBeaconLogTick := 0;
|
||||||
|
GetBeaconScope(FBeaconLogScope);
|
||||||
|
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',
|
||||||
|
[FBeaconDecHz, FCenterFreq, FBeaconLogScope.OffsetHz,
|
||||||
|
FBeaconLogScope.FsHz, FBeaconLogScope.FdecHz, FBeaconLogScope.Sps,
|
||||||
|
FBeaconLogScope.ResidHz, FBeaconLogScope.CarProm, FBeaconLogScope.SNRdB,
|
||||||
|
FBeaconLogScope.SymRate, FBeaconLogScope.DecRMS, FBeaconLogScope.SymMag,
|
||||||
|
FBeaconLogScope.TimErr, FBeaconLogScope.Wsym, FBeaconLogScope.CarFreqHz,
|
||||||
|
FBeaconLogScope.LockHold,
|
||||||
|
Ord(FBeaconLogScope.CarrierLock), Ord(FBeaconLogScope.SymbolLock),
|
||||||
|
Ord(FBeaconDecInvert), Ord(FBeaconLockOn), Ord(FBeaconLocked)]));
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
if not FBeaconLockOn then Exit;
|
if not FBeaconLockOn then Exit;
|
||||||
if FCurrentXvtr < 0 then Exit; // только в трансвертер-режиме
|
if FCurrentXvtr < 0 then Exit; // только в трансвертер-режиме
|
||||||
if FBeaconSaveCooldown > 0 then Dec(FBeaconSaveCooldown);
|
if FBeaconSaveCooldown > 0 then Dec(FBeaconSaveCooldown);
|
||||||
@@ -1310,6 +1383,93 @@ begin
|
|||||||
Result := FBeaconTrackHz;
|
Result := FBeaconTrackHz;
|
||||||
end;
|
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;
|
||||||
|
BeaconLog(Format('AIM click: DisplayHz=%.1f CenterFreq=%.1f -> offset=%.1f invert=%d span=%.0f',
|
||||||
|
[DisplayHz, FCenterFreq, FBeaconDecHz - FCenterFreq, Ord(FBeaconDecInvert), FSpanHz]));
|
||||||
|
Changed(rfBeaconLock);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.BeaconLog(const Msg: string);
|
||||||
|
// Диагностика декодера маяка: дописываем строку в HOME/ewsdr_beacon.log.
|
||||||
|
// Дёшево и кросс-платформенно; файл потом скидывается на разбор.
|
||||||
|
var
|
||||||
|
f: TextFile;
|
||||||
|
path, home: string;
|
||||||
|
begin
|
||||||
|
home := GetEnvironmentVariable('HOME');
|
||||||
|
if home = '' then home := GetTempDir;
|
||||||
|
path := IncludeTrailingPathDelimiter(home) + 'ewsdr_beacon.log';
|
||||||
|
try
|
||||||
|
AssignFile(f, path);
|
||||||
|
if FileExists(path) then Append(f) else Rewrite(f);
|
||||||
|
try
|
||||||
|
WriteLn(f, FormatDateTime('hh:nn:ss.zzz', Now), ' ', Msg);
|
||||||
|
finally
|
||||||
|
CloseFile(f);
|
||||||
|
end;
|
||||||
|
except
|
||||||
|
// лог не должен ронять контур — глотаем ошибки I/O
|
||||||
|
end;
|
||||||
|
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;
|
||||||
|
|
||||||
procedure TRadioController.ApplyModeFilter;
|
procedure TRadioController.ApplyModeFilter;
|
||||||
var
|
var
|
||||||
Lo, Hi, Half: Integer;
|
Lo, Hi, Half: Integer;
|
||||||
|
|||||||
+30
-3
@@ -77,6 +77,10 @@ type
|
|||||||
FBeaconMarkActive: Boolean;
|
FBeaconMarkActive: Boolean;
|
||||||
FBeaconRefFreq: Double; // опорная частота маяка (где ДОЛЖЕН быть)
|
FBeaconRefFreq: Double; // опорная частота маяка (где ДОЛЖЕН быть)
|
||||||
FBeaconTrkFreq: Double; // отслеживаемый центроид маяка сейчас
|
FBeaconTrkFreq: Double; // отслеживаемый центроид маяка сейчас
|
||||||
|
// узкий фильтр-маркер декодера (наведение пользователем)
|
||||||
|
FBeaconDecActive: Boolean;
|
||||||
|
FBeaconDecFreq: Double;
|
||||||
|
FBeaconDecHalf: Double; // полуширина фильтра, Гц
|
||||||
// ── Spectrum buffer ───────────────────────────────────────────────────────
|
// ── Spectrum buffer ───────────────────────────────────────────────────────
|
||||||
FSpectrumBuf: array[0..1023] of Single;
|
FSpectrumBuf: array[0..1023] of Single;
|
||||||
FSpectrumBufCount: Integer;
|
FSpectrumBufCount: Integer;
|
||||||
@@ -190,6 +194,7 @@ type
|
|||||||
// QO-100 beacon-маркеры: опорная частота (зелёная) + позиция трекера (оранж).
|
// QO-100 beacon-маркеры: опорная частота (зелёная) + позиция трекера (оранж).
|
||||||
// Спектр перерисовывается каждый кадр, поэтому отдельный invalidate не нужен.
|
// Спектр перерисовывается каждый кадр, поэтому отдельный invalidate не нужен.
|
||||||
procedure SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double);
|
procedure SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double);
|
||||||
|
procedure SetBeaconDecMarker(Active: Boolean; FreqHz, HalfHz: Double);
|
||||||
|
|
||||||
// ── S-метр ────────────────────────────────────────────────────────────────
|
// ── S-метр ────────────────────────────────────────────────────────────────
|
||||||
property LastSMeter: Double read GetLastSMeter write SetLastSMeter;
|
property LastSMeter: Double read GetLastSMeter write SetLastSMeter;
|
||||||
@@ -501,6 +506,13 @@ begin
|
|||||||
FBeaconTrkFreq := TrkHz;
|
FBeaconTrkFreq := TrkHz;
|
||||||
end;
|
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);
|
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.Pen.Color := Col; C.Pen.Width := 1; C.Pen.Style := Style;
|
||||||
C.MoveTo(x, 0); C.LineTo(x, H);
|
C.MoveTo(x, 0); C.LineTo(x, H);
|
||||||
end;
|
end;
|
||||||
|
var
|
||||||
|
xc, x1, x2: Integer;
|
||||||
begin
|
begin
|
||||||
Vline(FBeaconRefFreq, clLime, psDot); // где маяк ДОЛЖЕН быть
|
if FBeaconMarkActive then
|
||||||
Vline(FBeaconTrkFreq, TColor($000AA5FF), psSolid); // отслеживаемый центроид (оранж)
|
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;
|
C.Pen.Style := psSolid;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -975,7 +1002,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
if FMarkerActive then DrawMarkerLine(C, W, H);
|
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);
|
DrawADCOverloadOverlay(C, W, H);
|
||||||
if Assigned(FSampleRateOverlay) then
|
if Assigned(FSampleRateOverlay) then
|
||||||
FSampleRateOverlay.DrawOverlay(FSpectrumBitmap, C, W, H);
|
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);
|
DrawLine(x, 0, x, H, Col, 1, False);
|
||||||
end;
|
end;
|
||||||
begin
|
begin
|
||||||
Vline(FBeaconRefFreq, clLime); // где маяк ДОЛЖЕН быть
|
if FBeaconMarkActive then
|
||||||
Vline(FBeaconTrkFreq, TColor($000AA5FF)); // отслеживаемый центроид (оранж)
|
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;
|
end;
|
||||||
|
|
||||||
procedure TSpectrumViewOpenGL.DrawADCOverlay(W, H: Integer);
|
procedure TSpectrumViewOpenGL.DrawADCOverlay(W, H: Integer);
|
||||||
@@ -797,7 +806,7 @@ begin
|
|||||||
DrawAGCLines(W, H, DBmax, InvRange);
|
DrawAGCLines(W, H, DBmax, InvRange);
|
||||||
DrawSpectrumCurve(W, H, DBmax, InvRange);
|
DrawSpectrumCurve(W, H, DBmax, InvRange);
|
||||||
DrawMarker(W, H);
|
DrawMarker(W, H);
|
||||||
if FBeaconMarkActive then DrawBeaconMarkersGL(W, H);
|
if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkersGL(W, H);
|
||||||
DrawADCOverlay(W, H);
|
DrawADCOverlay(W, H);
|
||||||
DrawCachedOverlays(W, H);
|
DrawCachedOverlays(W, H);
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ interface
|
|||||||
|
|
||||||
uses
|
uses
|
||||||
Classes, SysUtils, Math, SyncObjs,
|
Classes, SysUtils, Math, SyncObjs,
|
||||||
|
BeaconDecoder,
|
||||||
WDSP;
|
WDSP;
|
||||||
|
|
||||||
const
|
const
|
||||||
@@ -292,6 +293,7 @@ type
|
|||||||
FMuted: Boolean;
|
FMuted: Boolean;
|
||||||
FVolume: Double;
|
FVolume: Double;
|
||||||
FLastError: string;
|
FLastError: string;
|
||||||
|
FBeaconDec: TBeaconDecoder; // не владеет; тап маяка в PushIQItemToDSP
|
||||||
|
|
||||||
function ModeToWDSP(Mode: Integer): Integer;
|
function ModeToWDSP(Mode: Integer): Integer;
|
||||||
procedure ApplyDefaultFilter;
|
procedure ApplyDefaultFilter;
|
||||||
@@ -331,6 +333,9 @@ type
|
|||||||
procedure Close;
|
procedure Close;
|
||||||
procedure ChangeSampleRate(NewRate: Integer);
|
procedure ChangeSampleRate(NewRate: Integer);
|
||||||
|
|
||||||
|
// QO-100 beacon-декодер: тап RX-IQ для демодуляции маяка (не владеет).
|
||||||
|
procedure SetBeaconDecoder(D: TBeaconDecoder);
|
||||||
|
|
||||||
// Подача 24-bit big-endian IQ из DDC пакета
|
// Подача 24-bit big-endian IQ из DDC пакета
|
||||||
// Buf — массив байт, DataOffset — смещение до IQ данных внутри Buf
|
// Buf — массив байт, DataOffset — смещение до IQ данных внутри Buf
|
||||||
procedure PushDDCPacket(const Buf: array of Byte;
|
procedure PushDDCPacket(const Buf: array of Byte;
|
||||||
@@ -1131,6 +1136,15 @@ begin
|
|||||||
|
|
||||||
// Переинициализируем WDSP с новым rate
|
// Переинициализируем WDSP с новым rate
|
||||||
Open;
|
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;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.PushDDCPacket(const Buf: array of Byte;
|
procedure TWDSPEngine.PushDDCPacket(const Buf: array of Byte;
|
||||||
@@ -1213,6 +1227,8 @@ begin
|
|||||||
|
|
||||||
FeedDisplaySample(IR * SCALE, QR * SCALE);
|
FeedDisplaySample(IR * SCALE, QR * SCALE);
|
||||||
|
|
||||||
|
if FBeaconDec <> nil then FBeaconDec.Feed(IR * SCALE, QR * SCALE);
|
||||||
|
|
||||||
FRXAccI[FRXAccPos] := IR * SCALE;
|
FRXAccI[FRXAccPos] := IR * SCALE;
|
||||||
FRXAccQ[FRXAccPos] := QR * SCALE;
|
FRXAccQ[FRXAccPos] := QR * SCALE;
|
||||||
Inc(FRXAccPos);
|
Inc(FRXAccPos);
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
<UseVersionInfo Value="True"/>
|
<UseVersionInfo Value="True"/>
|
||||||
<AutoIncrementBuild Value="True"/>
|
<AutoIncrementBuild Value="True"/>
|
||||||
<MinorVersionNr Value="9"/>
|
<MinorVersionNr Value="9"/>
|
||||||
<BuildNr Value="213"/>
|
<BuildNr Value="214"/>
|
||||||
</VersionInfo>
|
</VersionInfo>
|
||||||
<MacroValues Count="25">
|
<MacroValues Count="26">
|
||||||
<Macro1 Name="LCLWidgetType" Value="qt6"/>
|
<Macro1 Name="LCLWidgetType" Value="qt6"/>
|
||||||
<Macro2 Name="LCLWidgetType" Value="qt6"/>
|
<Macro2 Name="LCLWidgetType" Value="qt6"/>
|
||||||
<Macro3 Name="LCLWidgetType" Value="qt6"/>
|
<Macro3 Name="LCLWidgetType" Value="qt6"/>
|
||||||
@@ -45,6 +45,7 @@
|
|||||||
<Macro23 Name="LCLWidgetType" Value="qt6"/>
|
<Macro23 Name="LCLWidgetType" Value="qt6"/>
|
||||||
<Macro24 Name="LCLWidgetType" Value="qt6"/>
|
<Macro24 Name="LCLWidgetType" Value="qt6"/>
|
||||||
<Macro25 Name="LCLWidgetType" Value="qt6"/>
|
<Macro25 Name="LCLWidgetType" Value="qt6"/>
|
||||||
|
<Macro26 Name="LCLWidgetType" Value="qt6"/>
|
||||||
</MacroValues>
|
</MacroValues>
|
||||||
<BuildModes>
|
<BuildModes>
|
||||||
<Item Name="Debug" Default="True"/>
|
<Item Name="Debug" Default="True"/>
|
||||||
@@ -130,6 +131,14 @@
|
|||||||
<Filename Value="WDSPEngine.pas"/>
|
<Filename Value="WDSPEngine.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
</Unit>
|
</Unit>
|
||||||
|
<Unit>
|
||||||
|
<Filename Value="BeaconDecoder.pas"/>
|
||||||
|
<IsPartOfProject Value="True"/>
|
||||||
|
</Unit>
|
||||||
|
<Unit>
|
||||||
|
<Filename Value="BeaconScopeForm.pas"/>
|
||||||
|
<IsPartOfProject Value="True"/>
|
||||||
|
</Unit>
|
||||||
<Unit>
|
<Unit>
|
||||||
<Filename Value="VfoOverlay.pas"/>
|
<Filename Value="VfoOverlay.pas"/>
|
||||||
<IsPartOfProject Value="True"/>
|
<IsPartOfProject Value="True"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user