mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
feat: QO-100 beacon lock — stabilise LNB drift to NB transponder beacon
Click the beacon on the spectrum; the controller tracks it and continuously trims the transverter LO so the whole downlink stays put as the LNB drifts. - BeaconLock.pas: TBeaconTracker scaffold (narrowband NCO/FLL) — kept but no longer in the control path (FLL diverged on the suppressed-carrier BPSK beacon + Pluto DC offset). Measurement now uses the proven display-FFT path. - RadioController: FBeaconLockHz folded into XvtrTranslate (on top of LOError). MeasureBeaconFFT finds the beacon by POWER-WEIGHTED CENTROID of the lobe (BPSK has a suppressed carrier → broad symmetric ~2.4 kHz lobe, not a line; a peak-finder jitters, the centroid sits on the carrier). Track position in ABSOLUTE display Hz (FBeaconTrackHz) so scrolling the waterfall/centre never moves the search window off the beacon. Closed loop: e = tracked - ref, d(e)/d(LockHz) = -1 -> LockHz += gain*e; settle cooldown after each correction (display-FFT lags the LO retune -> avoids overshoot); deadband 75 Hz + centroid/prominence smoothing + lock hysteresis for a steady lock. Correction applied only after the user clicks (manual seed), so a crowded band can't pull a neighbour to the reference. - WDSPEngine: SetBeaconTracker + RX-IQ tap (tracker idle now, ~1 check/sample). - SpectrumView + GL: beacon markers — green = reference (10489.750), orange = tracked centroid. - MainForm: BEACON button (RX block, XVTR-only). Flow: BEACON -> "CLICK BCN" -> click beacon -> "L<corr> /<prom>". ServiceBeaconLock driven by MeterTimer. Reference defaults to the middle BPSK beacon 10489.750. Tested on-air against Es'hail-2: locks and holds, survives waterfall drag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+339
@@ -0,0 +1,339 @@
|
||||
unit BeaconLock;
|
||||
|
||||
{$IFDEF FPC}
|
||||
{$MODE Delphi}
|
||||
{$ENDIF}
|
||||
|
||||
{
|
||||
QO-100 Beacon Lock — выделенный узкополосный трекер несущей опорного маяка.
|
||||
|
||||
Задача: LNB-гетеродин (≈9750 МГц) дрейфует с температурой на единицы–десятки
|
||||
кГц → весь downlink плывёт. Маяк (нижний CW 10489.500 МГц) имеет ИЗВЕСТНУЮ
|
||||
частоту. Меряя, где он реально стоит, получаем ошибку LO и компенсируем её.
|
||||
|
||||
Двухступенчатая схема:
|
||||
1. ГРУБЫЙ захват — снаружи (по display-FFT) даёт начальную оценку смещения
|
||||
маяка в пределах ~±300 Гц; передаётся через Seed.
|
||||
2. ТОЧНЫЙ лок (этот модуль) — комплексный NCO сносит ожидаемую частоту маяка
|
||||
к DC, boxcar-дециматор сужает полосу до ~2 кГц (большой processing gain),
|
||||
затем FLL по фазовому дискриминатору доводит NCO ровно на несущую.
|
||||
Лок-метрика — SNR через carrier-EMA / noise-EMA на децимированном потоке.
|
||||
|
||||
Точность: единицы Гц при достаточном SNR (узкая полоса + длинное усреднение
|
||||
фазы), что и есть «железный» лок (в духе SDR Console).
|
||||
|
||||
Потоки: Feed() зовётся из DSP-потока (дёшево, на сэмпл). Результаты читаются
|
||||
из управляющего потока через снапшот под критической секцией.
|
||||
}
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
SysUtils, Math, SyncObjs;
|
||||
|
||||
type
|
||||
TBeaconState = (bsIdle, // выключен / нет данных
|
||||
bsSearch, // идёт захват (SNR ниже порога)
|
||||
bsLock); // несущая захвачена и стабильна
|
||||
|
||||
TBeaconTracker = class
|
||||
private
|
||||
FLock: TCriticalSection;
|
||||
// --- конфигурация ---
|
||||
FSampleRate: Double; // вход (Hz)
|
||||
FDecim: Integer; // коэффициент децимации (Fs/FDecim ≈ FDecimRate)
|
||||
FDecimRate: Double; // частота децимированного потока (Hz)
|
||||
FLoopGain: Double; // гейн FLL (0..1, малый)
|
||||
FLockSNRdB: Double; // порог объявления лока
|
||||
FTargetHz: Double; // ожидаемое смещение маяка от центра (Hz)
|
||||
// --- состояние NCO/децимации (DSP-поток) ---
|
||||
FNcoFreq: Double; // текущая частота NCO (быстрая, дёрганая)
|
||||
FNcoSmooth: Double; // сглаженная оценка смещения маяка (для коррекции/маркера)
|
||||
FSeedHz: Double; // якорь clamp: NCO не уходит дальше ±CLAMP от сида
|
||||
FPhCos, FPhSin: Double; // текущий фазор NCO (для сноса к DC)
|
||||
FStepCos, FStepSin: Double; // инкремент фазора за сэмпл (Δ = −2π·FNcoFreq/Fs)
|
||||
FNcoNorm: Integer; // счётчик до ренормализации фазора
|
||||
FAccI, FAccQ: Double; // аккумулятор boxcar-дециматора
|
||||
FAccN: Integer;
|
||||
FPrevI, FPrevQ: Double; // предыдущий децимированный сэмпл (для дискриминатора)
|
||||
FHasPrev: Boolean;
|
||||
FMeanI, FMeanQ: Double; // EMA комплексного децим. потока → амплитуда несущей
|
||||
FNoisePow: Double; // EMA мощности шума |z-mean|^2
|
||||
FResAvg: Double; // EMA остаточной частоты (для контроля стабильности)
|
||||
FActive: Boolean; // трекинг включён
|
||||
// --- снапшот результата (под FLock) ---
|
||||
FResMeasHz: Double; // измеренное смещение маяка = FNcoFreq на момент снапшота
|
||||
FResSNRdB: Double;
|
||||
FResState: TBeaconState;
|
||||
procedure RecomputeStep; // пересчёт шага фазора по FNcoFreq (фазу НЕ трогает)
|
||||
procedure ResetPhasor; // сброс фазора в 0 + пересчёт шага
|
||||
procedure ProcessDecimated(const zi, zq: Double);
|
||||
procedure Snapshot;
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
|
||||
// Настройка под текущий sample rate. DecimRateHz — целевая полоса
|
||||
// децимированного потока (по умолчанию ~2 кГц: ±1 кГц захват FLL).
|
||||
procedure Configure(ASampleRate: Double; ADecimRateHz: Double = 4000.0);
|
||||
// Ожидаемое смещение маяка от DDC-центра (Hz). Меняется при перестройке.
|
||||
procedure SetTarget(AOffsetHz: Double);
|
||||
// Грубая оценка от FFT — переинициализирует NCO (захват/переакквизиция).
|
||||
procedure Seed(AOffsetHz: Double);
|
||||
procedure Start;
|
||||
procedure Stop;
|
||||
procedure Reset;
|
||||
|
||||
// DSP-поток: один комплексный сэмпл @ FSampleRate.
|
||||
procedure Feed(const I, Q: Double); inline;
|
||||
|
||||
// Управляющий поток: снапшот результата.
|
||||
function MeasuredOffsetHz: Double; // измеренное смещение маяка от центра
|
||||
function ErrorHz: Double; // MeasuredOffset − Target (ошибка LO)
|
||||
function SNRdB: Double;
|
||||
function State: TBeaconState;
|
||||
function Locked: Boolean;
|
||||
property Active: Boolean read FActive;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
const
|
||||
NCO_RENORM = 2048; // ренормализация фазора каждые N сэмплов
|
||||
EMA_MEAN = 0.002; // постоянная EMA несущей (медленная — узкая полоса)
|
||||
EMA_NOISE = 0.002;
|
||||
EMA_RES = 0.01;
|
||||
EMA_NCO = 0.001; // сглаживание оценки частоты (TC ~0.25 с @4кГц)
|
||||
CLAMP_HZ = 1500.0; // NCO держим в ±CLAMP от сида (без runaway на край)
|
||||
NOISE_FLOOR = 1e-30; // ТОЛЬКО защита от деления на ноль (не маскировать сигнал!)
|
||||
SNR_MAX = 40.0;
|
||||
|
||||
constructor TBeaconTracker.Create;
|
||||
begin
|
||||
inherited Create;
|
||||
FLock := TCriticalSection.Create;
|
||||
FSampleRate := 1536000.0;
|
||||
FDecimRate := 4000.0; // квадрат-домен → захват ±FDecimRate/4 = ±1 кГц
|
||||
FLoopGain := 0.02;
|
||||
FLockSNRdB := 3.0;
|
||||
Reset;
|
||||
Configure(FSampleRate, FDecimRate);
|
||||
end;
|
||||
|
||||
destructor TBeaconTracker.Destroy;
|
||||
begin
|
||||
FLock.Free;
|
||||
inherited Destroy;
|
||||
end;
|
||||
|
||||
procedure TBeaconTracker.Configure(ASampleRate, ADecimRateHz: Double);
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
if ASampleRate < 1.0 then ASampleRate := 1536000.0;
|
||||
if ADecimRateHz < 100.0 then ADecimRateHz := 4000.0;
|
||||
FSampleRate := ASampleRate;
|
||||
FDecim := Max(1, Round(ASampleRate / ADecimRateHz));
|
||||
FDecimRate := ASampleRate / FDecim;
|
||||
// сброс рабочих аккумуляторов (NCO частоту сохраняем — это оценка маяка)
|
||||
FAccI := 0; FAccQ := 0; FAccN := 0;
|
||||
FHasPrev := False;
|
||||
FMeanI := 0; FMeanQ := 0; FNoisePow := 0; FResAvg := 0;
|
||||
ResetPhasor;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TBeaconTracker.RecomputeStep;
|
||||
// Пересчёт шага фазора под текущую FNcoFreq (снос на −FNcoFreq, маяк → DC).
|
||||
// Фазу НЕ трогаем — смена частоты непрерывна по фазе.
|
||||
var w: Double;
|
||||
begin
|
||||
w := -2.0 * Pi * FNcoFreq / FSampleRate;
|
||||
FStepCos := Cos(w);
|
||||
FStepSin := Sin(w);
|
||||
end;
|
||||
|
||||
procedure TBeaconTracker.ResetPhasor;
|
||||
begin
|
||||
FPhCos := 1.0;
|
||||
FPhSin := 0.0;
|
||||
FNcoNorm := 0;
|
||||
RecomputeStep;
|
||||
end;
|
||||
|
||||
procedure TBeaconTracker.SetTarget(AOffsetHz: Double);
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
FTargetHz := AOffsetHz;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TBeaconTracker.Seed(AOffsetHz: Double);
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
FNcoFreq := AOffsetHz;
|
||||
FNcoSmooth := AOffsetHz;
|
||||
FSeedHz := AOffsetHz; // якорь clamp
|
||||
ResetPhasor;
|
||||
FHasPrev := False;
|
||||
FMeanI := 0; FMeanQ := 0; FNoisePow := 0; FResAvg := 0;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TBeaconTracker.Start;
|
||||
begin
|
||||
FActive := True;
|
||||
end;
|
||||
|
||||
procedure TBeaconTracker.Stop;
|
||||
begin
|
||||
FActive := False;
|
||||
end;
|
||||
|
||||
procedure TBeaconTracker.Reset;
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
FNcoFreq := 0; FNcoSmooth := 0; FSeedHz := 0; FTargetHz := 0;
|
||||
FAccI := 0; FAccQ := 0; FAccN := 0;
|
||||
FHasPrev := False;
|
||||
FMeanI := 0; FMeanQ := 0; FNoisePow := 0; FResAvg := 0;
|
||||
FResMeasHz := 0; FResSNRdB := -99.0; FResState := bsIdle;
|
||||
FActive := False;
|
||||
ResetPhasor;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TBeaconTracker.Feed(const I, Q: Double);
|
||||
var
|
||||
mi, mq, nc, ns: Double;
|
||||
begin
|
||||
if not FActive then Exit;
|
||||
// снос к DC: z * conj(nco) (nco = exp(+jωt), шаг отрицательной частоты)
|
||||
mi := I * FPhCos - Q * FPhSin;
|
||||
mq := I * FPhSin + Q * FPhCos;
|
||||
// продвинуть фазор: phase *= step
|
||||
nc := FPhCos * FStepCos - FPhSin * FStepSin;
|
||||
ns := FPhCos * FStepSin + FPhSin * FStepCos;
|
||||
FPhCos := nc; FPhSin := ns;
|
||||
Inc(FNcoNorm);
|
||||
if FNcoNorm >= NCO_RENORM then
|
||||
begin
|
||||
// ренормализация (борьба с накоплением ошибки округления)
|
||||
nc := 1.5 - 0.5 * (FPhCos * FPhCos + FPhSin * FPhSin);
|
||||
FPhCos := FPhCos * nc;
|
||||
FPhSin := FPhSin * nc;
|
||||
FNcoNorm := 0;
|
||||
end;
|
||||
// boxcar-дециматор
|
||||
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 TBeaconTracker.ProcessDecimated(const zi, zq: Double);
|
||||
// Работаем в КВАДРАТЕ сигнала w = z²: для BPSK (средний маяк) это снимает
|
||||
// модуляцию ±180° (b²=1), оставляя чистую несущую на 2× частоте; для чистого
|
||||
// CW (нижний/верхний) — тоже корректно. Дискриминатор по w даёт 2·остаток,
|
||||
// делим на 2. Захват ±FDecimRate/4. carrier/noise-EMA считаем в w-домене —
|
||||
// для BPSK среднее по z = 0, а по w = z² несущая когерентна.
|
||||
var
|
||||
wi, wq, pr, pi, phase, res2, resHz, di, dq, np: Double;
|
||||
begin
|
||||
wi := zi * zi - zq * zq; // Re(z²)
|
||||
wq := 2.0 * zi * zq; // Im(z²)
|
||||
if FHasPrev then
|
||||
begin
|
||||
// фазовый дискриминатор по квадрату: arg(w[n] * conj(w[n-1]))
|
||||
pr := wi * FPrevI + wq * FPrevQ;
|
||||
pi := wq * FPrevI - wi * FPrevQ;
|
||||
phase := ArcTan2(pi, pr);
|
||||
res2 := phase * FDecimRate / (2.0 * Pi); // = 2 · остаточная частота
|
||||
resHz := 0.5 * res2;
|
||||
// FLL: подтянуть NCO на остаточную частоту, но удержать в окне ±CLAMP_HZ
|
||||
// вокруг сида — иначе на BPSK/шуме петля убегает на край спектра.
|
||||
FNcoFreq := FNcoFreq + FLoopGain * resHz;
|
||||
if FNcoFreq > FSeedHz + CLAMP_HZ then FNcoFreq := FSeedHz + CLAMP_HZ;
|
||||
if FNcoFreq < FSeedHz - CLAMP_HZ then FNcoFreq := FSeedHz - CLAMP_HZ;
|
||||
FResAvg := FResAvg + EMA_RES * (Abs(resHz) - FResAvg);
|
||||
FNcoSmooth := FNcoSmooth + EMA_NCO * (FNcoFreq - FNcoSmooth); // стаб. оценка
|
||||
RecomputeStep; // обновить шаг фазора под новую FNcoFreq (фаза непрерывна)
|
||||
end;
|
||||
FPrevI := wi; FPrevQ := wq; FHasPrev := True; // FPrev* = предыдущий w
|
||||
|
||||
// carrier-EMA в w-домене (несущая² у DC) и шум как отклонение от среднего
|
||||
FMeanI := FMeanI + EMA_MEAN * (wi - FMeanI);
|
||||
FMeanQ := FMeanQ + EMA_MEAN * (wq - FMeanQ);
|
||||
di := wi - FMeanI; dq := wq - FMeanQ;
|
||||
np := di * di + dq * dq;
|
||||
FNoisePow := FNoisePow + EMA_NOISE * (np - FNoisePow);
|
||||
|
||||
Snapshot;
|
||||
end;
|
||||
|
||||
procedure TBeaconTracker.Snapshot;
|
||||
var
|
||||
sigPow, snr: Double;
|
||||
st: TBeaconState;
|
||||
begin
|
||||
sigPow := FMeanI * FMeanI + FMeanQ * FMeanQ;
|
||||
snr := 10.0 * Log10((sigPow + 1e-30) / (FNoisePow + NOISE_FLOOR));
|
||||
if snr > SNR_MAX then snr := SNR_MAX;
|
||||
// Лок по когерентности несущей (SNR в квадрат-домене). Гейт по остаточной
|
||||
// частоте убран — на BPSK он давал ложный «не-лок».
|
||||
if snr >= FLockSNRdB then
|
||||
st := bsLock
|
||||
else
|
||||
st := bsSearch;
|
||||
FLock.Enter;
|
||||
try
|
||||
FResMeasHz := FNcoSmooth; // сглаженная оценка → стабильная коррекция/маркер
|
||||
FResSNRdB := snr;
|
||||
FResState := st;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TBeaconTracker.MeasuredOffsetHz: Double;
|
||||
begin
|
||||
FLock.Enter; try Result := FResMeasHz; finally FLock.Leave; end;
|
||||
end;
|
||||
|
||||
function TBeaconTracker.ErrorHz: Double;
|
||||
begin
|
||||
FLock.Enter; try Result := FResMeasHz - FTargetHz; finally FLock.Leave; end;
|
||||
end;
|
||||
|
||||
function TBeaconTracker.SNRdB: Double;
|
||||
begin
|
||||
FLock.Enter; try Result := FResSNRdB; finally FLock.Leave; end;
|
||||
end;
|
||||
|
||||
function TBeaconTracker.State: TBeaconState;
|
||||
begin
|
||||
if not FActive then Exit(bsIdle);
|
||||
FLock.Enter; try Result := FResState; finally FLock.Leave; end;
|
||||
end;
|
||||
|
||||
function TBeaconTracker.Locked: Boolean;
|
||||
begin
|
||||
Result := State = bsLock;
|
||||
end;
|
||||
|
||||
end.
|
||||
+86
-1
@@ -43,7 +43,7 @@ uses
|
||||
PowerInhibit,
|
||||
DeviceStore,
|
||||
RadioBackend, PlutoBackend,
|
||||
RadioController, ChannelController;
|
||||
RadioController, ChannelController, BeaconLock;
|
||||
|
||||
const
|
||||
CLR_BG = TColor($00101010);
|
||||
@@ -134,6 +134,7 @@ type
|
||||
FSpecDrag: Boolean;
|
||||
FSpecDragX0: Integer; // X при нажатии
|
||||
FSpecDragFreq: Double; // FCenterFreq при нажатии
|
||||
FBeaconArming: Boolean; // ждём клик по маяку (после включения BEACON)
|
||||
FSpectrumDirty: Boolean; // таймер должен перерисовать спектр/водопад
|
||||
// TX DUC-очередь (FDUCPending*) переехала в TRadioController (OnTXIQ).
|
||||
FSMeterPeak: Double; // верхняя граница светлой зоны
|
||||
@@ -237,6 +238,7 @@ type
|
||||
PanelFilter: TPanel;
|
||||
BtnFilter: array[0..FILT_COUNT-1] of TFlatButton;
|
||||
BtnCTun: TFlatButton;
|
||||
BtnBeacon: TFlatButton; // QO-100 beacon lock (видим только в XVTR-режиме)
|
||||
BtnDUP: TFlatButton;
|
||||
PanelFMSQ: TPanel;
|
||||
BtnFMSQ: TFlatButton;
|
||||
@@ -377,6 +379,8 @@ type
|
||||
procedure BtnModeClick(Sender: TObject);
|
||||
procedure BtnFilterClick(Sender: TObject);
|
||||
procedure BtnCTunClick(Sender: TObject);
|
||||
procedure BtnBeaconClick(Sender: TObject);
|
||||
procedure UpdateBeaconButton;
|
||||
procedure BtnDUPClick(Sender: TObject);
|
||||
procedure ApplyDUP(Active: Boolean);
|
||||
procedure BtnTUNClick(Sender: TObject);
|
||||
@@ -1190,6 +1194,12 @@ begin
|
||||
if BtnXvtrBand[i] <> nil then
|
||||
StyleButton(BtnXvtrBand[i], i = FController.FCurrentXvtr);
|
||||
UpdateFreqDisplayMax; // трансвертер может быть >6 ГГц (QO-100)
|
||||
// BEACON-кнопка актуальна только в трансвертер-режиме (QO-100 lock).
|
||||
if BtnBeacon <> nil then
|
||||
begin
|
||||
BtnBeacon.Visible := FController.FCurrentXvtr >= 0;
|
||||
UpdateBeaconButton;
|
||||
end;
|
||||
// Wideband-вид + web-зеркало xvtr (ранее в обёртках Activate/Deactivate).
|
||||
// rfXvtr эмитят ТОЛЬКО ActivateXvtr/DeactivateXvtr — состояние финальное.
|
||||
UpdateWidebandFrequencyView;
|
||||
@@ -1252,6 +1262,8 @@ begin
|
||||
FSpecView.DrawSpectrum;
|
||||
PbSpectrum.Invalidate;
|
||||
end;
|
||||
rfBeaconLock:
|
||||
UpdateBeaconButton; // статус/коррекция QO-100 beacon lock
|
||||
rfCTun:
|
||||
begin
|
||||
StyleButton(BtnCTun, FController.FCTun);
|
||||
@@ -1694,6 +1706,15 @@ begin
|
||||
FChannelsDropDown.OnSelect := OnChannelsDropDownSelect;
|
||||
RefreshChannelsDropDown;
|
||||
|
||||
// QO-100 beacon lock — 3-я колонка ряда. Видима только в XVTR-режиме
|
||||
// (см. рендер rfXvtr ниже). Подпись/подсветка обновляются по rfBeaconLock.
|
||||
BtnBeacon := MakeBtn(PanelRXBlock, 'BEACON',
|
||||
LeftPanelButtonLeft(LEFT_W, 3, 2), 16,
|
||||
LeftPanelButtonWidth(LEFT_W, 3, 2),
|
||||
BTN_H, BtnBeaconClick);
|
||||
BtnBeacon.Visible := False;
|
||||
StyleButton(BtnBeacon, False);
|
||||
|
||||
MakeLbl(PanelRXBlock, 'VOL', 4, 50);
|
||||
TrkVolume := TFlatSlider.Create(Self);
|
||||
TrkVolume.Parent := PanelRXBlock; TrkVolume.Left := 34; TrkVolume.Top := 46;
|
||||
@@ -2946,6 +2967,8 @@ begin
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// QO-100 beacon lock: одна итерация контура (no-op если лок выключен).
|
||||
FController.ServiceBeaconLock;
|
||||
|
||||
// Сглаженное среднее (EMA)
|
||||
FSMeterAvg := FSMeterAvg * (1.0 - AVG_ALPHA) + FController.FLastSMeter * AVG_ALPHA;
|
||||
@@ -3864,6 +3887,59 @@ begin
|
||||
FController.SetCTun(not FController.FCTun);
|
||||
end;
|
||||
|
||||
procedure TMainForm.BtnBeaconClick(Sender: TObject);
|
||||
begin
|
||||
FController.SetBeaconLock(not FController.BeaconLockEnabled);
|
||||
// При включении — «вооружаем» клик: следующий ЛКМ по спектру укажет маяк
|
||||
// (надёжнее автопоиска в забитой полосе). Авто-сид работает до клика.
|
||||
FBeaconArming := FController.BeaconLockEnabled;
|
||||
UpdateBeaconButton;
|
||||
end;
|
||||
|
||||
procedure TMainForm.UpdateBeaconButton;
|
||||
// Подпись + подсветка кнопки BEACON по состоянию лока. Зовётся из BtnBeaconClick
|
||||
// и из рендера rfBeaconLock (контур обновляет статус каждый тик).
|
||||
var
|
||||
cap: string;
|
||||
corr: Double;
|
||||
begin
|
||||
if BtnBeacon = nil then Exit;
|
||||
// Маркеры на спектре: опорная частота (зелёная) + позиция трекера (оранж).
|
||||
if FSpecView <> nil then
|
||||
FSpecView.SetBeaconMarkers(FController.BeaconLockEnabled,
|
||||
FController.BeaconRefHz,
|
||||
FController.BeaconTrackedFreqHz);
|
||||
if not FController.BeaconLockEnabled then
|
||||
begin
|
||||
BtnBeacon.Caption := 'BEACON';
|
||||
StyleButton(BtnBeacon, False);
|
||||
end
|
||||
else if FBeaconArming then
|
||||
begin
|
||||
BtnBeacon.Caption := 'CLICK BCN'; // ждём клик пользователя по маяку
|
||||
StyleButton(BtnBeacon, True);
|
||||
end
|
||||
else
|
||||
begin
|
||||
case FController.BeaconState of
|
||||
bsLock:
|
||||
begin
|
||||
corr := FController.BeaconCorrectionHz;
|
||||
// LOCK + коррекция LO (Гц) + SNR — видно запас.
|
||||
cap := Format('L%.0f /%.0f', [corr, FController.BeaconSNRdB]);
|
||||
StyleButton(BtnBeacon, True);
|
||||
end;
|
||||
bsSearch:
|
||||
// Показываем SNR при поиске — диагностика: растёт → маяк ловится.
|
||||
begin cap := Format('BCN ?%.0f', [FController.BeaconSNRdB]); StyleButton(BtnBeacon, True); end;
|
||||
else
|
||||
begin cap := 'BCN --'; StyleButton(BtnBeacon, False); end;
|
||||
end;
|
||||
BtnBeacon.Caption := cap;
|
||||
end;
|
||||
BtnBeacon.Repaint;
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DUP toggle (Thetis-style local display mode — без аппаратной команды)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -4057,6 +4133,15 @@ begin
|
||||
|
||||
if Button = mbLeft then
|
||||
begin
|
||||
// Наведение beacon-lock: ЛКМ указывает на маяк (вместо перестройки VFO).
|
||||
if FBeaconArming and (PbSpectrum.Width > 0) then
|
||||
begin
|
||||
FController.BeaconSeedAtHz(
|
||||
PixelToFreq(X, PbSpectrum.Width, FController.FCenterFreq, FController.FSpanHz));
|
||||
FBeaconArming := False;
|
||||
UpdateBeaconButton;
|
||||
Exit;
|
||||
end;
|
||||
// Левая кнопка: сбрасываем маркер если активен
|
||||
if FSpecView.MarkerActive then
|
||||
begin
|
||||
|
||||
+269
-3
@@ -50,7 +50,7 @@ uses
|
||||
Classes, SysUtils, Math,
|
||||
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
||||
WDSPEngine, AudioOutput, AudioInput,
|
||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore, BeaconLock;
|
||||
|
||||
type
|
||||
// Поле, которое изменилось — для гранулярных уведомлений наружу.
|
||||
@@ -71,6 +71,7 @@ type
|
||||
rfChannel,
|
||||
rfSMeter, rfPower, rfSWR, rfSupply, rfPLL, // телеметрия
|
||||
rfADCOverload, // ADC overload (рендер индикатора)
|
||||
rfBeaconLock, // QO-100 beacon lock: статус/коррекция
|
||||
rfDevice, // подключённое устройство сменилось
|
||||
rfDeviceList // список discovered устройств обновился
|
||||
);
|
||||
@@ -176,6 +177,20 @@ type
|
||||
FCurrentBand: Integer;
|
||||
FCurrentXvtr: Integer;
|
||||
|
||||
// ---- QO-100 beacon lock ----
|
||||
FBeaconTracker: TBeaconTracker; // владеем (Free в FreeEngines)
|
||||
FBeaconLockOn: Boolean; // лок включён пользователем
|
||||
FBeaconLockHz: Double; // текущая динамич. коррекция LO (Hz), поверх LOError
|
||||
FBeaconRefHz: Double; // опорная частота маяка (Hz, downlink), деф. 10489.500e6
|
||||
FBeaconSearchTicks: Integer; // тиков без лока (троттлинг переакквизиции)
|
||||
FBeaconManualSeed: Boolean; // пользователь указал маяк кликом → не автопоиск
|
||||
// FFT-трекинг маяка (робастный, по display-FFT — без NCO/FLL):
|
||||
FBeaconTrackHz: Double; // отслеживаемая АБСОЛЮТНАЯ display-частота маяка
|
||||
// (не baseband-offset — иначе сдвиг центра/водопада ломает окно)
|
||||
FBeaconPromDB: Double; // «высота» пика над шумом окна (лок-метрика)
|
||||
FBeaconLocked: Boolean; // пик уверенно виден
|
||||
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка пайплайна)
|
||||
|
||||
// ---- Аудио / TX ----
|
||||
FVolume: Integer;
|
||||
FMuted: Boolean;
|
||||
@@ -321,6 +336,12 @@ type
|
||||
// ActiveTXFreqHz — текущая TX-частота (split/active VFO + FM-репитер сдвиг).
|
||||
function XvtrTranslate(VisibleHz: Double): Double;
|
||||
function ActiveTXFreqHz: Double;
|
||||
// Грубый поиск пика маяка в display-FFT около ожидаемой позиции → baseband
|
||||
// offset (Hz). False если маяк вне span / слишком слаб. Сид для трекера.
|
||||
function CoarseBeaconOffset(ExpectOff: Double; out OffHz: Double): Boolean;
|
||||
// Измерение пика маяка по display-FFT в окне вокруг ExpectOff: возвращает
|
||||
// уточнённое смещение (parabolic) и «высоту» пика над средним окна (для лока).
|
||||
function MeasureBeaconFFT(ExpectOff: Double; out PeakOff, PromDB: Double): Boolean;
|
||||
|
||||
// Применяет текущие FMode/FFilterBW/FM-параметры к DSP (RX/TX фильтр,
|
||||
// FM-девиация, squelch). Вызывается командами и UI-путями смены режима.
|
||||
@@ -425,6 +446,18 @@ type
|
||||
procedure SetSpan(Hz: Integer);
|
||||
procedure SetSampleRate(Hz: Integer);
|
||||
|
||||
// QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку.
|
||||
procedure SetBeaconLock(On_: Boolean);
|
||||
procedure ServiceBeaconLock; // одна итерация контура (зовётся UI-таймером ~4 Гц)
|
||||
procedure SetBeaconRefHz(Hz: Double); // опорная частота маяка (downlink)
|
||||
procedure BeaconSeedAtHz(DisplayHz: Double); // пользователь указал маяк кликом
|
||||
function BeaconState: TBeaconState;
|
||||
function BeaconCorrectionHz: Double; // текущая коррекция LO (FBeaconLockHz)
|
||||
function BeaconSNRdB: Double;
|
||||
function BeaconLockEnabled: Boolean;
|
||||
function BeaconRefHz: Double; // опорная частота маяка (для маркера)
|
||||
function BeaconTrackedFreqHz: Double;// где сидит трекер: центр + measured offset
|
||||
|
||||
// Отображение / приём
|
||||
procedure SetCTun(On_: Boolean);
|
||||
procedure SetDuplex(On_: Boolean);
|
||||
@@ -520,6 +553,13 @@ begin
|
||||
// буфер вдвое сокращает построение downsampler в OpenChannel RXA.
|
||||
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
|
||||
|
||||
// QO-100 beacon lock: трекер кормится RX-IQ из движка (тап в PushIQItemToDSP).
|
||||
FBeaconTracker := TBeaconTracker.Create;
|
||||
FBeaconRefHz := 10489750000.0; // средний BPSK-маяк NB-транспондера (downlink)
|
||||
FBeaconLockHz := 0.0;
|
||||
FBeaconLockOn := False;
|
||||
FDSPEngine.SetBeaconTracker(FBeaconTracker);
|
||||
|
||||
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
|
||||
// формы / при подключении устройства).
|
||||
FAudioOut := TAudioOutput.Create(48000);
|
||||
@@ -537,6 +577,9 @@ begin
|
||||
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;
|
||||
// Трекер освобождаем ПОСЛЕ движка — движок (Close остановил DSP-поток) больше
|
||||
// не вызовет Feed. Указатель в движке уже мёртв вместе с движком.
|
||||
if Assigned(FBeaconTracker) then FreeAndNil(FBeaconTracker);
|
||||
end;
|
||||
|
||||
destructor TRadioController.Destroy;
|
||||
@@ -899,10 +942,13 @@ begin
|
||||
Result := VisibleHz;
|
||||
if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit;
|
||||
if not FXvtrSettings.Entries[FCurrentXvtr].Enabled then Exit;
|
||||
// f_IF = f_visible - LOOffset + LOError
|
||||
// f_IF = f_visible - LOOffset + LOError + BeaconLock
|
||||
// FBeaconLockHz — динамическая коррекция дрейфа LNB (QO-100 beacon lock),
|
||||
// поверх статичной ручной LOError. 0 когда лок выключен.
|
||||
Result := VisibleHz
|
||||
- FXvtrSettings.Entries[FCurrentXvtr].LOOffset
|
||||
+ FXvtrSettings.Entries[FCurrentXvtr].LOError;
|
||||
+ FXvtrSettings.Entries[FCurrentXvtr].LOError
|
||||
+ FBeaconLockHz;
|
||||
end;
|
||||
|
||||
function TRadioController.ActiveTXFreqHz: Double;
|
||||
@@ -919,6 +965,226 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// QO-100 beacon lock
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const
|
||||
BEACON_GAIN = 0.4; // гейн контроллера (доля ошибки за коррекцию)
|
||||
BEACON_DEADBAND_HZ = 75.0; // внутри — коррекции стоп (без вечного подкручивания)
|
||||
BEACON_SETTLE_TICKS = 8; // пауза (~0.8 с @10Гц) после коррекции — осадка FFT-пайплайна
|
||||
BEACON_UNLOCK_PROM_DB = 3.0; // гистерезис: лок снимается ниже этого (lock при LOCK_PROM)
|
||||
BEACON_SEARCH_HZ = 6000.0; // окно поиска пика вокруг клика/ожидаемой позиции
|
||||
BEACON_REACQ_TICKS = 30; // тиков (≈3 с @10Гц) до переакквизиции по FFT
|
||||
BEACON_LOCK_PROM_DB = 6.0; // пик над средним окна (dB) для статуса LOCK
|
||||
BEACON_LOBE_DB = 10.0; // ширина лепестка для центроида (dB ниже пика)
|
||||
|
||||
function TRadioController.CoarseBeaconOffset(ExpectOff: Double; out OffHz: Double): Boolean;
|
||||
// Ищет максимум display-FFT в окне ±BEACON_SEARCH_HZ вокруг ExpectOff (baseband
|
||||
// offset). Пиксели покрывают анализаторную полосу = FSampleRate, центр (пиксель
|
||||
// n/2) = baseband 0, рост вправо — та же конвенция, что и маркеры (проверено:
|
||||
// зелёная садится на маяк). Возвращает уточнённый baseband offset пика.
|
||||
var
|
||||
pix: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||||
n, i, best: Integer;
|
||||
hzPerPix, lo, hi, off, denom, delta: Double;
|
||||
bestv: Single;
|
||||
begin
|
||||
Result := False; OffHz := 0;
|
||||
if not FWDSPReady or not Assigned(FDSPEngine) then Exit;
|
||||
n := 0;
|
||||
FDSPEngine.GetSpectrumData(pix, n);
|
||||
if n < 8 then Exit;
|
||||
hzPerPix := FSpanHz / n;
|
||||
lo := ExpectOff - BEACON_SEARCH_HZ;
|
||||
hi := ExpectOff + BEACON_SEARCH_HZ;
|
||||
best := -1; bestv := -1e30;
|
||||
for i := 0 to n - 1 do
|
||||
begin
|
||||
off := (i - n / 2) * hzPerPix; // baseband offset пикселя i
|
||||
if (off < lo) or (off > hi) then Continue;
|
||||
if pix[i] > bestv then begin bestv := pix[i]; best := i; end;
|
||||
end;
|
||||
if best < 0 then Exit; // окно вне span
|
||||
delta := 0.0;
|
||||
// Параболическая интерполяция пика (пиксели в dB ≈ гауссов пик) — субдискретная
|
||||
// точность, чтобы сид попал в ±1 кГц окно захвата FLL даже при крупном бине.
|
||||
if (best > 0) and (best < n - 1) then
|
||||
begin
|
||||
denom := pix[best - 1] - 2.0 * pix[best] + pix[best + 1];
|
||||
if Abs(denom) > 1e-12 then
|
||||
delta := 0.5 * (pix[best - 1] - pix[best + 1]) / denom;
|
||||
if delta > 0.5 then delta := 0.5;
|
||||
if delta < -0.5 then delta := -0.5;
|
||||
end;
|
||||
OffHz := (best + delta - n / 2) * hzPerPix;
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
function TRadioController.MeasureBeaconFFT(ExpectOff: Double;
|
||||
out PeakOff, PromDB: Double): Boolean;
|
||||
// Маяк по display-FFT в окне ±BEACON_SEARCH_HZ вокруг ExpectOff. Средний маяк —
|
||||
// BPSK с ПОДАВЛЕННОЙ несущей: в спектре не линия, а широкая симметричная «полка»
|
||||
// (~2.4 кГц), peak-finder по ней скачет. Поэтому позицию берём как
|
||||
// POWER-ВЗВЕШЕННЫЙ ЦЕНТРОИД лепестка (пиксели в пределах BEACON_LOBE_DB от пика):
|
||||
// центр симметричной полки = несущая, стабилен. PromDB — пик над средним окна.
|
||||
var
|
||||
pix: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||||
n, i, best, cnt: Integer;
|
||||
hzPerPix, lo, hi, off, sum, thr, p, wsum, fsum: Double;
|
||||
bestv: Single;
|
||||
begin
|
||||
Result := False; PeakOff := ExpectOff; PromDB := -99.0;
|
||||
if not FWDSPReady or not Assigned(FDSPEngine) then Exit;
|
||||
n := 0;
|
||||
FDSPEngine.GetSpectrumData(pix, n);
|
||||
if n < 8 then Exit;
|
||||
hzPerPix := FSpanHz / n;
|
||||
lo := ExpectOff - BEACON_SEARCH_HZ;
|
||||
hi := ExpectOff + BEACON_SEARCH_HZ;
|
||||
best := -1; bestv := -1e30; sum := 0; cnt := 0;
|
||||
for i := 0 to n - 1 do
|
||||
begin
|
||||
off := (i - n / 2) * hzPerPix;
|
||||
if (off < lo) or (off > hi) then Continue;
|
||||
sum := sum + pix[i]; Inc(cnt);
|
||||
if pix[i] > bestv then begin bestv := pix[i]; best := i; end;
|
||||
end;
|
||||
if (best < 0) or (cnt < 4) then Exit;
|
||||
// Центроид лепестка: пиксели в пределах BEACON_LOBE_DB ниже пика, вес —
|
||||
// линейная мощность над порогом (шум за порогом не входит).
|
||||
thr := bestv - BEACON_LOBE_DB;
|
||||
wsum := 0; fsum := 0;
|
||||
for i := 0 to n - 1 do
|
||||
begin
|
||||
off := (i - n / 2) * hzPerPix;
|
||||
if (off < lo) or (off > hi) then Continue;
|
||||
if pix[i] < thr then Continue;
|
||||
p := Power(10.0, (pix[i] - thr) / 10.0); // линейная мощность над порогом
|
||||
wsum := wsum + p;
|
||||
fsum := fsum + p * off;
|
||||
end;
|
||||
if wsum > 0 then PeakOff := fsum / wsum
|
||||
else PeakOff := (best - n / 2) * hzPerPix;
|
||||
PromDB := bestv - (sum / cnt); // пик над средним окна (dB)
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetBeaconRefHz(Hz: Double);
|
||||
begin
|
||||
FBeaconRefHz := Hz;
|
||||
end;
|
||||
|
||||
procedure TRadioController.BeaconSeedAtHz(DisplayHz: Double);
|
||||
// Пользователь кликнул по маяку → задаём область отслеживания (уточняем пик по
|
||||
// display-FFT рядом с кликом) и включаем ручной режим (контур ведёт ИМЕННО этот
|
||||
// пик, без автопоиска, который в забитой полосе хватает соседей).
|
||||
var clickOff, refined, prom: Double;
|
||||
begin
|
||||
FBeaconManualSeed := True;
|
||||
FBeaconSearchTicks := 0;
|
||||
FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать FFT устаканиться после сида
|
||||
clickOff := DisplayHz - FCenterFreq;
|
||||
if MeasureBeaconFFT(clickOff, refined, prom) then
|
||||
FBeaconTrackHz := FCenterFreq + refined
|
||||
else
|
||||
FBeaconTrackHz := DisplayHz;
|
||||
if not FBeaconLockOn then SetBeaconLock(True);
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetBeaconLock(On_: Boolean);
|
||||
var refined, prom: Double;
|
||||
begin
|
||||
if On_ = FBeaconLockOn then Exit;
|
||||
FBeaconLockOn := On_;
|
||||
if On_ then
|
||||
begin
|
||||
FBeaconSearchTicks := 0;
|
||||
FBeaconCorrCooldown := 0;
|
||||
FBeaconManualSeed := False; // авто до первого клика пользователя
|
||||
FBeaconLocked := False;
|
||||
// Начальная область — у опорной частоты (уточняем пик, если виден).
|
||||
if MeasureBeaconFFT(FBeaconRefHz - FCenterFreq, refined, prom) then
|
||||
FBeaconTrackHz := FCenterFreq + refined
|
||||
else
|
||||
FBeaconTrackHz := FBeaconRefHz;
|
||||
end;
|
||||
// При выключении FBeaconLockHz замораживаем (как калибровка LNB).
|
||||
Changed(rfBeaconLock);
|
||||
end;
|
||||
|
||||
procedure TRadioController.ServiceBeaconLock;
|
||||
// Одна итерация контура (UI-таймер @10 Гц). Меряет пик маяка по display-FFT,
|
||||
// сглаживает, и (после ручного клика) тянет FBeaconLockHz так, чтобы маяк
|
||||
// оставался на опорной частоте → весь downlink стабилен. Без NCO/FLL.
|
||||
var pk, prom, measDisp, e: Double;
|
||||
begin
|
||||
if not FBeaconLockOn then Exit;
|
||||
if FCurrentXvtr < 0 then Exit; // только в трансвертер-режиме
|
||||
// Окно поиска — вокруг ТЕКУЩЕЙ display-позиции маяка, пересчитанной в baseband
|
||||
// через актуальный FCenterFreq. Поэтому сдвиг водопада/центра окно не ломает.
|
||||
if not MeasureBeaconFFT(FBeaconTrackHz - FCenterFreq, pk, prom) then Exit;
|
||||
|
||||
// Сопровождаем центроид плавно (в display-частоте; стабильность важнее скорости).
|
||||
FBeaconTrackHz := FBeaconTrackHz + 0.12 * ((FCenterFreq + pk) - FBeaconTrackHz);
|
||||
FBeaconPromDB := FBeaconPromDB + 0.2 * (prom - FBeaconPromDB);
|
||||
// Гистерезис лока: загорается при LOCK_PROM, гаснет ниже UNLOCK_PROM (без мерцания).
|
||||
if FBeaconPromDB >= BEACON_LOCK_PROM_DB then FBeaconLocked := True
|
||||
else if FBeaconPromDB < BEACON_UNLOCK_PROM_DB then FBeaconLocked := False;
|
||||
|
||||
// Коррекцию НЕ применяем каждый тик: display-FFT отстаёт от ретюна LO на
|
||||
// задержку пайплайна, и спам-коррекции дают перелёт (маяк улетает из окна).
|
||||
// После каждой коррекции ждём BEACON_SETTLE_TICKS, давая FFT отразить новый LO.
|
||||
// Слежение за пиком (выше) идёт каждый тик — окно едет за маяком.
|
||||
if FBeaconCorrCooldown > 0 then
|
||||
Dec(FBeaconCorrCooldown)
|
||||
else if FBeaconLocked and FBeaconManualSeed then
|
||||
begin
|
||||
// e = видимое_смещение_маяка − опорное. d(e)/d(FBeaconLockHz) = −1,
|
||||
// поэтому FBeaconLockHz += e подтягивает маяк к опорной (см. вывод знака).
|
||||
measDisp := FBeaconTrackHz;
|
||||
e := measDisp - FBeaconRefHz;
|
||||
if Abs(e) > BEACON_DEADBAND_HZ then
|
||||
begin
|
||||
FBeaconLockHz := FBeaconLockHz + BEACON_GAIN * e;
|
||||
PushNetworkState; // ретюн LNB-IF: новый LO = XvtrTranslate(центр)
|
||||
FBeaconCorrCooldown := BEACON_SETTLE_TICKS;
|
||||
end;
|
||||
end;
|
||||
Changed(rfBeaconLock);
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconState: TBeaconState;
|
||||
begin
|
||||
if not FBeaconLockOn then Exit(bsIdle);
|
||||
if FBeaconLocked then Result := bsLock else Result := bsSearch;
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconCorrectionHz: Double;
|
||||
begin
|
||||
Result := FBeaconLockHz;
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconSNRdB: Double;
|
||||
begin
|
||||
Result := FBeaconPromDB; // «высота» пика над шумом окна (dB)
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconLockEnabled: Boolean;
|
||||
begin
|
||||
Result := FBeaconLockOn;
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconRefHz: Double;
|
||||
begin
|
||||
Result := FBeaconRefHz;
|
||||
end;
|
||||
|
||||
function TRadioController.BeaconTrackedFreqHz: Double;
|
||||
begin
|
||||
Result := FBeaconTrackHz;
|
||||
end;
|
||||
|
||||
procedure TRadioController.ApplyModeFilter;
|
||||
var
|
||||
Lo, Hi, Half: Integer;
|
||||
|
||||
@@ -73,6 +73,10 @@ type
|
||||
// ── Marker ────────────────────────────────────────────────────────────────
|
||||
FMarkerActive: Boolean;
|
||||
FMarkerX: Integer;
|
||||
// ── QO-100 beacon markers (диагностика лока) ──────────────────────────────
|
||||
FBeaconMarkActive: Boolean;
|
||||
FBeaconRefFreq: Double; // опорная частота маяка (где ДОЛЖЕН быть)
|
||||
FBeaconTrkFreq: Double; // где сидит трекер (NCO) сейчас
|
||||
// ── Spectrum buffer ───────────────────────────────────────────────────────
|
||||
FSpectrumBuf: array[0..1023] of Single;
|
||||
FSpectrumBufCount: Integer;
|
||||
@@ -99,6 +103,7 @@ type
|
||||
procedure SetFillSpectrum(V: Boolean);
|
||||
procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
||||
procedure DrawMarkerLine(C: TCanvas; W, H: Integer);
|
||||
procedure DrawBeaconMarkers(C: TCanvas; W, H: Integer);
|
||||
procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
|
||||
procedure CopyGridToSpectrum(W, H: Integer);
|
||||
procedure DrawADCOverloadOverlay(C: TCanvas; W, H: Integer);
|
||||
@@ -182,6 +187,10 @@ type
|
||||
property MarkerActive: Boolean read FMarkerActive write SetMarkerActive;
|
||||
property MarkerX: Integer read FMarkerX write SetMarkerX;
|
||||
|
||||
// QO-100 beacon-маркеры: опорная частота (зелёная) + позиция трекера (оранж).
|
||||
// Спектр перерисовывается каждый кадр, поэтому отдельный invalidate не нужен.
|
||||
procedure SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double);
|
||||
|
||||
// ── S-метр ────────────────────────────────────────────────────────────────
|
||||
property LastSMeter: Double read GetLastSMeter write SetLastSMeter;
|
||||
property SMeterPeak: Double read GetSMeterPeak write SetSMeterPeak;
|
||||
@@ -485,6 +494,32 @@ begin
|
||||
C.TextOut(MX - 4 - C.TextWidth(MarkerLbl), 4, MarkerLbl);
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double);
|
||||
begin
|
||||
FBeaconMarkActive := Active;
|
||||
FBeaconRefFreq := RefHz;
|
||||
FBeaconTrkFreq := TrkHz;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.DrawBeaconMarkers(C: TCanvas; W, H: Integer);
|
||||
// Две вертикали: опорная частота маяка (зелёная пунктир) и текущая позиция
|
||||
// трекера (оранжевая сплошная). Расхождение видно глазом → понятно, на пик
|
||||
// сел трекер или на шум.
|
||||
procedure Vline(FreqHz: Double; Col: TColor; Style: TPenStyle);
|
||||
var x: Integer;
|
||||
begin
|
||||
if FSpanHz <= 0 then Exit;
|
||||
x := Round((FreqHz - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
|
||||
if (x < 0) or (x >= W) then Exit;
|
||||
C.Pen.Color := Col; C.Pen.Width := 1; C.Pen.Style := Style;
|
||||
C.MoveTo(x, 0); C.LineTo(x, H);
|
||||
end;
|
||||
begin
|
||||
Vline(FBeaconRefFreq, clLime, psDot); // где маяк ДОЛЖЕН быть
|
||||
Vline(FBeaconTrkFreq, TColor($000AA5FF), psSolid); // где сидит трекер (оранж)
|
||||
C.Pen.Style := psSolid;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
|
||||
var
|
||||
Y, X, InvA: Integer;
|
||||
@@ -940,6 +975,7 @@ begin
|
||||
end;
|
||||
|
||||
if FMarkerActive then DrawMarkerLine(C, W, H);
|
||||
if FBeaconMarkActive then DrawBeaconMarkers(C, W, H);
|
||||
DrawADCOverloadOverlay(C, W, H);
|
||||
if Assigned(FSampleRateOverlay) then
|
||||
FSampleRateOverlay.DrawOverlay(FSpectrumBitmap, C, W, H);
|
||||
|
||||
@@ -75,6 +75,7 @@ type
|
||||
procedure DrawAGCLines(W, H: Integer; DBmax, InvRange: Double);
|
||||
procedure DrawSpectrumCurve(W, H: Integer; DBmax, InvRange: Double);
|
||||
procedure DrawMarker(W, H: Integer);
|
||||
procedure DrawBeaconMarkersGL(W, H: Integer);
|
||||
procedure DrawADCOverlay(W, H: Integer);
|
||||
procedure DrawCachedOverlays(W, H: Integer);
|
||||
function FormatFreqGL(Hz: Double): string;
|
||||
@@ -656,6 +657,21 @@ begin
|
||||
else DrawTexture(FMarkerLabelTex, MX - 4 - FMarkerLabelTex.W, 4);
|
||||
end;
|
||||
|
||||
procedure TSpectrumViewOpenGL.DrawBeaconMarkersGL(W, H: Integer);
|
||||
// QO-100 beacon-маркеры: опорная частота (зелёная) + позиция трекера (оранж).
|
||||
procedure Vline(FreqHz: Double; Col: TColor);
|
||||
var x: Integer;
|
||||
begin
|
||||
if FSpanHz <= 0 then Exit;
|
||||
x := Round((FreqHz - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
|
||||
if (x < 0) or (x >= W) then Exit;
|
||||
DrawLine(x, 0, x, H, Col, 1, False);
|
||||
end;
|
||||
begin
|
||||
Vline(FBeaconRefFreq, clLime); // где маяк ДОЛЖЕН быть
|
||||
Vline(FBeaconTrkFreq, TColor($000AA5FF)); // где сидит трекер (оранж)
|
||||
end;
|
||||
|
||||
procedure TSpectrumViewOpenGL.DrawADCOverlay(W, H: Integer);
|
||||
var
|
||||
B: TBitmap;
|
||||
@@ -781,6 +797,7 @@ begin
|
||||
DrawAGCLines(W, H, DBmax, InvRange);
|
||||
DrawSpectrumCurve(W, H, DBmax, InvRange);
|
||||
DrawMarker(W, H);
|
||||
if FBeaconMarkActive then DrawBeaconMarkersGL(W, H);
|
||||
DrawADCOverlay(W, H);
|
||||
DrawCachedOverlays(W, H);
|
||||
|
||||
|
||||
+17
-1
@@ -25,7 +25,7 @@ interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, Math, SyncObjs,
|
||||
WDSP;
|
||||
WDSP, BeaconLock;
|
||||
|
||||
const
|
||||
// WDSP mode integers (нет именованных констант в WDSP.pas)
|
||||
@@ -292,6 +292,7 @@ type
|
||||
FMuted: Boolean;
|
||||
FVolume: Double;
|
||||
FLastError: string;
|
||||
FBeaconTracker: TBeaconTracker; // QO-100 beacon lock (НЕ владеем; см. контроллер)
|
||||
|
||||
function ModeToWDSP(Mode: Integer): Integer;
|
||||
procedure ApplyDefaultFilter;
|
||||
@@ -354,6 +355,9 @@ type
|
||||
procedure UpdateAGCLines(SpectrumW: Integer); // читает hang/thresh из WDSP для линии на спектре
|
||||
procedure SetSpectrumWidth(W: Integer); // обновляет FLastSpectrumW для корректных AGC линий
|
||||
procedure SetShift(ShiftHz: Double); // CTUN NCO сдвиг
|
||||
// QO-100 beacon lock: трекер кормится RX-IQ из DSP-потока. Владелец —
|
||||
// контроллер; движок лишь подаёт сэмплы и сообщает sample rate.
|
||||
procedure SetBeaconTracker(T: TBeaconTracker);
|
||||
procedure SetNRMode(Mode: Integer);
|
||||
procedure SetNR(Enable: Boolean);
|
||||
procedure SetNBMode(Mode: Integer);
|
||||
@@ -1119,6 +1123,7 @@ begin
|
||||
// Обновляем параметры
|
||||
FSampleRate := NewRate;
|
||||
FBufSize := FAudioBufSize * NewRate div FAudioRate;
|
||||
if FBeaconTracker <> nil then FBeaconTracker.Configure(FSampleRate);
|
||||
|
||||
// Перераспределяем буферы под новый размер
|
||||
SetLength(FRXIn, FBufSize * 2);
|
||||
@@ -1213,6 +1218,11 @@ begin
|
||||
|
||||
FeedDisplaySample(IR * SCALE, QR * SCALE);
|
||||
|
||||
// QO-100 beacon lock: тот же baseband-IQ @ FSampleRate (трекер сам
|
||||
// мгновенно выходит, если лок не активен — стоимость ~1 проверка/сэмпл).
|
||||
if FBeaconTracker <> nil then
|
||||
FBeaconTracker.Feed(IR * SCALE, QR * SCALE);
|
||||
|
||||
FRXAccI[FRXAccPos] := IR * SCALE;
|
||||
FRXAccQ[FRXAccPos] := QR * SCALE;
|
||||
Inc(FRXAccPos);
|
||||
@@ -1371,6 +1381,12 @@ begin
|
||||
UpdateAGCLines(FLastSpectrumW);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetBeaconTracker(T: TBeaconTracker);
|
||||
begin
|
||||
FBeaconTracker := T;
|
||||
if (T <> nil) and (FSampleRate > 0) then T.Configure(FSampleRate);
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetShift(ShiftHz: Double);
|
||||
begin
|
||||
FShiftHz := ShiftHz;
|
||||
|
||||
Reference in New Issue
Block a user