mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
feat: QO-100 beacon lock — robustness, persistence, FT8-aware UI
Builds on the beacon-lock baseline: keeps the proven display-FFT control
path (hardware-LO trim folded into the active transverter LOError), and
hardens it. The narrowband NCO/FLL scaffold (BeaconLock.pas) is removed —
it was never in the control path.
- RadioController:
* Persist live: each correction folds straight into the active xvtr
LOError (visible in Settings, survives restart as LNB calibration);
disk writes throttled. FBeaconLockHz is now only a session indicator.
* Robust tracking vs nearby signals: narrow ±2 kHz gate while locked
(±6 kHz only for acquisition), slew-rate outlier rejection (a sudden
centroid jump = interferer, not slow LNB drift → ignored), and lobe
shape validation in MeasureBeaconFFT (width + power symmetry) so a CW
carrier / one-sided neighbour isn't mistaken for the BPSK beacon.
Prominence measured over the in-window noise floor (gate-width robust).
* Fast convergence: feed-forward (predict post-retune position so the
window follows the beacon through the jump) + near-deadbeat correction
on the manual click → one click locks instead of repeated tapping.
* Single central BPSK beacon (10489.750); dead CoarseBeaconOffset and
unused throttle counters removed; stale NCO/FLL/4 Hz comments fixed.
- WDSPEngine: drop the per-sample TBeaconTracker RX-IQ tap (tracker gone).
- MainForm: BEACON is now a plain button, shown only on Pluto in a
transverter. Lock status (off / click / search / LOCK + correction +
prominence) moves to status-bar field 7 in place of PLL on Pluto;
openHPSDR keeps PLL there.
- SettingsForm/StatusBar: widen LO Error field to ±10 MHz (QO-100 LNBs
drift hundreds of kHz) and widen status field 7 for the beacon text.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
-339
@@ -1,339 +0,0 @@
|
|||||||
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.
|
|
||||||
+47
-45
@@ -43,7 +43,7 @@ uses
|
|||||||
PowerInhibit,
|
PowerInhibit,
|
||||||
DeviceStore,
|
DeviceStore,
|
||||||
RadioBackend, PlutoBackend,
|
RadioBackend, PlutoBackend,
|
||||||
RadioController, ChannelController, BeaconLock;
|
RadioController, ChannelController;
|
||||||
|
|
||||||
const
|
const
|
||||||
CLR_BG = TColor($00101010);
|
CLR_BG = TColor($00101010);
|
||||||
@@ -238,7 +238,7 @@ type
|
|||||||
PanelFilter: TPanel;
|
PanelFilter: TPanel;
|
||||||
BtnFilter: array[0..FILT_COUNT-1] of TFlatButton;
|
BtnFilter: array[0..FILT_COUNT-1] of TFlatButton;
|
||||||
BtnCTun: TFlatButton;
|
BtnCTun: TFlatButton;
|
||||||
BtnBeacon: TFlatButton; // QO-100 beacon lock (видим только в XVTR-режиме)
|
BtnBeacon: TFlatButton; // QO-100 beacon lock (видим только на Pluto в трансвертере)
|
||||||
BtnDUP: TFlatButton;
|
BtnDUP: TFlatButton;
|
||||||
PanelFMSQ: TPanel;
|
PanelFMSQ: TPanel;
|
||||||
BtnFMSQ: TFlatButton;
|
BtnFMSQ: TFlatButton;
|
||||||
@@ -352,6 +352,8 @@ type
|
|||||||
|
|
||||||
// Public UI update (called from sync objects)
|
// Public UI update (called from sync objects)
|
||||||
procedure UpdateTXMeters;
|
procedure UpdateTXMeters;
|
||||||
|
procedure UpdateStatusField7; // поле 7: PLL (HPSDR) или beacon (Pluto)
|
||||||
|
function BeaconStatusText: string; // компактный статус QO-100 beacon lock
|
||||||
procedure DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
|
procedure DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
@@ -1194,10 +1196,11 @@ begin
|
|||||||
if BtnXvtrBand[i] <> nil then
|
if BtnXvtrBand[i] <> nil then
|
||||||
StyleButton(BtnXvtrBand[i], i = FController.FCurrentXvtr);
|
StyleButton(BtnXvtrBand[i], i = FController.FCurrentXvtr);
|
||||||
UpdateFreqDisplayMax; // трансвертер может быть >6 ГГц (QO-100)
|
UpdateFreqDisplayMax; // трансвертер может быть >6 ГГц (QO-100)
|
||||||
// BEACON-кнопка актуальна только в трансвертер-режиме (QO-100 lock).
|
// BEACON-кнопка — только Pluto в трансвертере (QO-100 lock); для
|
||||||
|
// openHPSDR смысла не имеет (нет дрейфующего LNB → прячем).
|
||||||
if BtnBeacon <> nil then
|
if BtnBeacon <> nil then
|
||||||
begin
|
begin
|
||||||
BtnBeacon.Visible := FController.FCurrentXvtr >= 0;
|
BtnBeacon.Visible := FController.IsPluto and (FController.FCurrentXvtr >= 0);
|
||||||
UpdateBeaconButton;
|
UpdateBeaconButton;
|
||||||
end;
|
end;
|
||||||
// Wideband-вид + web-зеркало xvtr (ранее в обёртках Activate/Deactivate).
|
// Wideband-вид + web-зеркало xvtr (ранее в обёртках Activate/Deactivate).
|
||||||
@@ -3175,18 +3178,14 @@ procedure TMainForm.UpdateTXMeters;
|
|||||||
// FLastSupplyV/FLastSupplyA обновляются на rate HP Status (~150/с)
|
// FLastSupplyV/FLastSupplyA обновляются на rate HP Status (~150/с)
|
||||||
// в TRadioController.ApplyHPStatus.
|
// в TRadioController.ApplyHPStatus.
|
||||||
begin
|
begin
|
||||||
|
UpdateStatusField7; // PLL (openHPSDR) или статус beacon lock (Pluto)
|
||||||
|
|
||||||
if not FController.FRunning then
|
if not FController.FRunning then
|
||||||
begin
|
begin
|
||||||
SetStatusText(3, 'Supply --');
|
SetStatusText(3, 'Supply --');
|
||||||
SetStatusText(7, 'PLL --');
|
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
if FController.FLastPLLLock then
|
|
||||||
SetStatusText(7, 'PLL OK')
|
|
||||||
else
|
|
||||||
SetStatusText(7, 'PLL?');
|
|
||||||
|
|
||||||
if not BoardSupportsSupplyVoltage(FController.FNetwork.Device.BoardType) then
|
if not BoardSupportsSupplyVoltage(FController.FNetwork.Device.BoardType) then
|
||||||
begin
|
begin
|
||||||
SetStatusText(3, 'Supply n/a');
|
SetStatusText(3, 'Supply n/a');
|
||||||
@@ -3205,6 +3204,35 @@ begin
|
|||||||
SetStatusText(3, 'Supply --');
|
SetStatusText(3, 'Supply --');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TMainForm.BeaconStatusText: string;
|
||||||
|
// Компактный статус QO-100 beacon lock для поля 7 статус-бара (Pluto). Courier 8pt,
|
||||||
|
// поле ~200px → держим ≤ ~24 символов.
|
||||||
|
begin
|
||||||
|
if not FController.BeaconLockEnabled then Exit('BCN: off');
|
||||||
|
if FBeaconArming then Exit('BCN: click beacon');
|
||||||
|
case FController.BeaconState of
|
||||||
|
bsLock: Result := Format('BCN LOCK %.0fHz /%.0f',
|
||||||
|
[FController.BeaconCorrectionHz, FController.BeaconSNRdB]);
|
||||||
|
bsSearch: Result := Format('BCN search /%.0f', [FController.BeaconSNRdB]);
|
||||||
|
else Result := 'BCN --';
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.UpdateStatusField7;
|
||||||
|
// Поле 7 статус-бара. openHPSDR — PLL-lock (как прежде). Pluto в трансвертере —
|
||||||
|
// статус QO-100 beacon lock (у Pluto нет PLL-телеметрии); Pluto без XVTR — пусто.
|
||||||
|
begin
|
||||||
|
if FController.IsPluto then
|
||||||
|
begin
|
||||||
|
if FController.FCurrentXvtr >= 0 then SetStatusText(7, BeaconStatusText)
|
||||||
|
else SetStatusText(7, '');
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
if not FController.FRunning then SetStatusText(7, 'PLL --')
|
||||||
|
else if FController.FLastPLLLock then SetStatusText(7, 'PLL OK')
|
||||||
|
else SetStatusText(7, 'PLL?');
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnWidebandCB(ADCIndex: Integer;
|
procedure TMainForm.OnWidebandCB(ADCIndex: Integer;
|
||||||
const Samples: array of SmallInt; Count: Integer);
|
const Samples: array of SmallInt; Count: Integer);
|
||||||
begin
|
begin
|
||||||
@@ -3897,46 +3925,20 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.UpdateBeaconButton;
|
procedure TMainForm.UpdateBeaconButton;
|
||||||
// Подпись + подсветка кнопки BEACON по состоянию лока. Зовётся из BtnBeaconClick
|
// Beacon-маркеры + кнопка BEACON. Кнопка видима только на Pluto в трансвертере
|
||||||
// и из рендера rfBeaconLock (контур обновляет статус каждый тик).
|
// (см. rfXvtr) — это ПРОСТО кнопка: подпись 'BEACON', подсветка = лок включён.
|
||||||
var
|
// Вся инфа (lock/коррекция/поиск/«click») — в статус-баре, поле 7 (вместо PLL),
|
||||||
cap: string;
|
// см. UpdateStatusField7. На openHPSDR кнопка скрыта, beacon-маркеры неактивны.
|
||||||
corr: Double;
|
|
||||||
begin
|
begin
|
||||||
if BtnBeacon = nil then Exit;
|
// Маркеры на спектре: опорная частота (зелёная) + отслеживаемый центроид (оранж).
|
||||||
// Маркеры на спектре: опорная частота (зелёная) + позиция трекера (оранж).
|
|
||||||
if FSpecView <> nil then
|
if FSpecView <> nil then
|
||||||
FSpecView.SetBeaconMarkers(FController.BeaconLockEnabled,
|
FSpecView.SetBeaconMarkers(FController.BeaconLockEnabled,
|
||||||
FController.BeaconRefHz,
|
FController.BeaconRefHz,
|
||||||
FController.BeaconTrackedFreqHz);
|
FController.BeaconTrackedFreqHz);
|
||||||
if not FController.BeaconLockEnabled then
|
UpdateStatusField7; // моментальный апдейт статус-бара (не ждём MeterTimer)
|
||||||
begin
|
if BtnBeacon = nil then Exit;
|
||||||
BtnBeacon.Caption := 'BEACON';
|
BtnBeacon.Caption := 'BEACON';
|
||||||
StyleButton(BtnBeacon, False);
|
StyleButton(BtnBeacon, FController.BeaconLockEnabled);
|
||||||
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;
|
BtnBeacon.Repaint;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
+217
-135
@@ -50,9 +50,14 @@ uses
|
|||||||
Classes, SysUtils, Math,
|
Classes, SysUtils, Math,
|
||||||
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
||||||
WDSPEngine, AudioOutput, AudioInput,
|
WDSPEngine, AudioOutput, AudioInput,
|
||||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore, BeaconLock;
|
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
||||||
|
|
||||||
type
|
type
|
||||||
|
// QO-100 beacon lock — статус контура (для UI/маркеров).
|
||||||
|
TBeaconState = (bsIdle, // лок выключен
|
||||||
|
bsSearch, // включён, маяк ещё не подтверждён (низкий prominence)
|
||||||
|
bsLock); // маяк уверенно виден и сопровождается
|
||||||
|
|
||||||
// Поле, которое изменилось — для гранулярных уведомлений наружу.
|
// Поле, которое изменилось — для гранулярных уведомлений наружу.
|
||||||
TRadioField = (
|
TRadioField = (
|
||||||
rfVfoA, rfVfoB, rfActiveVfo,
|
rfVfoA, rfVfoB, rfActiveVfo,
|
||||||
@@ -177,19 +182,20 @@ type
|
|||||||
FCurrentBand: Integer;
|
FCurrentBand: Integer;
|
||||||
FCurrentXvtr: Integer;
|
FCurrentXvtr: Integer;
|
||||||
|
|
||||||
// ---- QO-100 beacon lock ----
|
// ---- QO-100 beacon lock (центральный BPSK-маяк 10489.750, по display-FFT) ----
|
||||||
FBeaconTracker: TBeaconTracker; // владеем (Free в FreeEngines)
|
|
||||||
FBeaconLockOn: Boolean; // лок включён пользователем
|
FBeaconLockOn: Boolean; // лок включён пользователем
|
||||||
FBeaconLockHz: Double; // текущая динамич. коррекция LO (Hz), поверх LOError
|
FBeaconLockHz: Double; // коррекция, накопленная за ТЕКУЩУЮ сессию лока
|
||||||
FBeaconRefHz: Double; // опорная частота маяка (Hz, downlink), деф. 10489.500e6
|
// (только индикация на кнопке); сама поправка
|
||||||
FBeaconSearchTicks: Integer; // тиков без лока (троттлинг переакквизиции)
|
// живёт в LOError активного трансвертера
|
||||||
|
FBeaconRefHz: Double; // опорная частота маяка (Hz, downlink), деф. 10489.750e6
|
||||||
FBeaconManualSeed: Boolean; // пользователь указал маяк кликом → не автопоиск
|
FBeaconManualSeed: Boolean; // пользователь указал маяк кликом → не автопоиск
|
||||||
// FFT-трекинг маяка (робастный, по display-FFT — без NCO/FLL):
|
// FFT-трекинг маяка (робастный, по display-FFT):
|
||||||
FBeaconTrackHz: Double; // отслеживаемая АБСОЛЮТНАЯ display-частота маяка
|
FBeaconTrackHz: Double; // отслеживаемая АБСОЛЮТНАЯ display-частота маяка
|
||||||
// (не baseband-offset — иначе сдвиг центра/водопада ломает окно)
|
// (не baseband-offset — иначе сдвиг центра/водопада ломает окно)
|
||||||
FBeaconPromDB: Double; // «высота» пика над шумом окна (лок-метрика)
|
FBeaconPromDB: Double; // «высота» пика над шумом окна (лок-метрика)
|
||||||
FBeaconLocked: Boolean; // пик уверенно виден
|
FBeaconLocked: Boolean; // пик уверенно виден
|
||||||
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка пайплайна)
|
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка пайплайна)
|
||||||
|
FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков)
|
||||||
|
|
||||||
// ---- Аудио / TX ----
|
// ---- Аудио / TX ----
|
||||||
FVolume: Integer;
|
FVolume: Integer;
|
||||||
@@ -336,12 +342,17 @@ type
|
|||||||
// ActiveTXFreqHz — текущая TX-частота (split/active VFO + FM-репитер сдвиг).
|
// ActiveTXFreqHz — текущая TX-частота (split/active VFO + FM-репитер сдвиг).
|
||||||
function XvtrTranslate(VisibleHz: Double): Double;
|
function XvtrTranslate(VisibleHz: Double): Double;
|
||||||
function ActiveTXFreqHz: Double;
|
function ActiveTXFreqHz: Double;
|
||||||
// Грубый поиск пика маяка в display-FFT около ожидаемой позиции → baseband
|
// Измерение маяка по display-FFT в окне ±WinHz вокруг ExpectOff (baseband):
|
||||||
// offset (Hz). False если маяк вне span / слишком слаб. Сид для трекера.
|
// центроид лепестка → уточнённое смещение, «высота» пика над средним окна
|
||||||
function CoarseBeaconOffset(ExpectOff: Double; out OffHz: Double): Boolean;
|
// (лок-метрика). False, если лепесток не похож на маяк (узкий/широкий/
|
||||||
// Измерение пика маяка по display-FFT в окне вокруг ExpectOff: возвращает
|
// несимметричный сосед) или вне span — тогда трек держим на месте.
|
||||||
// уточнённое смещение (parabolic) и «высоту» пика над средним окна (для лока).
|
function MeasureBeaconFFT(ExpectOff, WinHz: Double;
|
||||||
function MeasureBeaconFFT(ExpectOff: Double; out PeakOff, PromDB: Double): Boolean;
|
out PeakOff, PromDB: Double): Boolean;
|
||||||
|
// Складывает коррекцию маяка dLock в LOError активного трансвертера (живая
|
||||||
|
// калибровка LNB), ретюнит радио и троттлингом сохраняет на диск.
|
||||||
|
procedure ApplyBeaconLOError(dLock: Double);
|
||||||
|
// Flush LOError на диск при выключении лока (если троттлинг не успел).
|
||||||
|
procedure PersistBeaconCalibration;
|
||||||
|
|
||||||
// Применяет текущие FMode/FFilterBW/FM-параметры к DSP (RX/TX фильтр,
|
// Применяет текущие FMode/FFilterBW/FM-параметры к DSP (RX/TX фильтр,
|
||||||
// FM-девиация, squelch). Вызывается командами и UI-путями смены режима.
|
// FM-девиация, squelch). Вызывается командами и UI-путями смены режима.
|
||||||
@@ -448,15 +459,15 @@ type
|
|||||||
|
|
||||||
// QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку.
|
// QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку.
|
||||||
procedure SetBeaconLock(On_: Boolean);
|
procedure SetBeaconLock(On_: Boolean);
|
||||||
procedure ServiceBeaconLock; // одна итерация контура (зовётся UI-таймером ~4 Гц)
|
procedure ServiceBeaconLock; // одна итерация контура (MeterTimer @10 Гц)
|
||||||
procedure SetBeaconRefHz(Hz: Double); // опорная частота маяка (downlink)
|
procedure SetBeaconRefHz(Hz: Double); // опорная частота маяка (downlink)
|
||||||
procedure BeaconSeedAtHz(DisplayHz: Double); // пользователь указал маяк кликом
|
procedure BeaconSeedAtHz(DisplayHz: Double); // пользователь указал маяк кликом
|
||||||
function BeaconState: TBeaconState;
|
function BeaconState: TBeaconState;
|
||||||
function BeaconCorrectionHz: Double; // текущая коррекция LO (FBeaconLockHz)
|
function BeaconCorrectionHz: Double; // коррекция за сессию лока (индикация; в LOError)
|
||||||
function BeaconSNRdB: Double;
|
function BeaconSNRdB: Double;
|
||||||
function BeaconLockEnabled: Boolean;
|
function BeaconLockEnabled: Boolean;
|
||||||
function BeaconRefHz: Double; // опорная частота маяка (для маркера)
|
function BeaconRefHz: Double; // опорная частота маяка (для маркера)
|
||||||
function BeaconTrackedFreqHz: Double;// где сидит трекер: центр + measured offset
|
function BeaconTrackedFreqHz: Double;// отслеживаемая display-частота маяка (центроид)
|
||||||
|
|
||||||
// Отображение / приём
|
// Отображение / приём
|
||||||
procedure SetCTun(On_: Boolean);
|
procedure SetCTun(On_: Boolean);
|
||||||
@@ -553,12 +564,10 @@ begin
|
|||||||
// буфер вдвое сокращает построение downsampler в OpenChannel RXA.
|
// буфер вдвое сокращает построение downsampler в OpenChannel RXA.
|
||||||
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
|
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
|
||||||
|
|
||||||
// QO-100 beacon lock: трекер кормится RX-IQ из движка (тап в PushIQItemToDSP).
|
// QO-100 beacon lock: измерение по display-FFT (см. ServiceBeaconLock).
|
||||||
FBeaconTracker := TBeaconTracker.Create;
|
FBeaconRefHz := 10489750000.0; // центральный BPSK-маяк NB-транспондера (downlink)
|
||||||
FBeaconRefHz := 10489750000.0; // средний BPSK-маяк NB-транспондера (downlink)
|
|
||||||
FBeaconLockHz := 0.0;
|
FBeaconLockHz := 0.0;
|
||||||
FBeaconLockOn := False;
|
FBeaconLockOn := False;
|
||||||
FDSPEngine.SetBeaconTracker(FBeaconTracker);
|
|
||||||
|
|
||||||
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
|
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
|
||||||
// формы / при подключении устройства).
|
// формы / при подключении устройства).
|
||||||
@@ -577,9 +586,6 @@ begin
|
|||||||
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.Close; FreeAndNil(FDSPEngine); end;
|
||||||
// Трекер освобождаем ПОСЛЕ движка — движок (Close остановил DSP-поток) больше
|
|
||||||
// не вызовет Feed. Указатель в движке уже мёртв вместе с движком.
|
|
||||||
if Assigned(FBeaconTracker) then FreeAndNil(FBeaconTracker);
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
destructor TRadioController.Destroy;
|
destructor TRadioController.Destroy;
|
||||||
@@ -942,13 +948,13 @@ begin
|
|||||||
Result := VisibleHz;
|
Result := VisibleHz;
|
||||||
if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit;
|
if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit;
|
||||||
if not FXvtrSettings.Entries[FCurrentXvtr].Enabled then Exit;
|
if not FXvtrSettings.Entries[FCurrentXvtr].Enabled then Exit;
|
||||||
// f_IF = f_visible - LOOffset + LOError + BeaconLock
|
// f_IF = f_visible - LOOffset + LOError.
|
||||||
// FBeaconLockHz — динамическая коррекция дрейфа LNB (QO-100 beacon lock),
|
// QO-100 beacon lock складывает свою коррекцию дрейфа LNB ПРЯМО в LOError
|
||||||
// поверх статичной ручной LOError. 0 когда лок выключен.
|
// (живая калибровка): она сразу видна в настройках и сохраняется. Отдельного
|
||||||
|
// слагаемого нет — иначе двойной счёт.
|
||||||
Result := VisibleHz
|
Result := VisibleHz
|
||||||
- FXvtrSettings.Entries[FCurrentXvtr].LOOffset
|
- FXvtrSettings.Entries[FCurrentXvtr].LOOffset
|
||||||
+ FXvtrSettings.Entries[FCurrentXvtr].LOError
|
+ FXvtrSettings.Entries[FCurrentXvtr].LOError;
|
||||||
+ FBeaconLockHz;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TRadioController.ActiveTXFreqHz: Double;
|
function TRadioController.ActiveTXFreqHz: Double;
|
||||||
@@ -970,68 +976,44 @@ end;
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const
|
const
|
||||||
BEACON_GAIN = 0.4; // гейн контроллера (доля ошибки за коррекцию)
|
// --- контур коррекции ---
|
||||||
|
BEACON_GAIN = 0.9; // гейн контура: feed-forward делает шаг точным,
|
||||||
|
// <1 — запас от перелёта при шумной оценке центроида
|
||||||
|
BEACON_SEED_GAIN = 0.95; // первая коррекция по клику агрессивнее (1 клик → лок)
|
||||||
BEACON_DEADBAND_HZ = 75.0; // внутри — коррекции стоп (без вечного подкручивания)
|
BEACON_DEADBAND_HZ = 75.0; // внутри — коррекции стоп (без вечного подкручивания)
|
||||||
BEACON_SETTLE_TICKS = 8; // пауза (~0.8 с @10Гц) после коррекции — осадка FFT-пайплайна
|
BEACON_SETTLE_TICKS = 8; // пауза (~0.8 с @10 Гц) после коррекции — осадка FFT-пайплайна
|
||||||
BEACON_UNLOCK_PROM_DB = 3.0; // гистерезис: лок снимается ниже этого (lock при LOCK_PROM)
|
BEACON_SAVE_TICKS = 30; // троттлинг записи LOError на диск (~3 с @10 Гц)
|
||||||
BEACON_SEARCH_HZ = 6000.0; // окно поиска пика вокруг клика/ожидаемой позиции
|
// --- метрика лока (prominence = пик над шумовым полом окна) ---
|
||||||
BEACON_REACQ_TICKS = 30; // тиков (≈3 с @10Гц) до переакквизиции по FFT
|
BEACON_LOCK_PROM_DB = 6.0; // загорается LOCK выше этого
|
||||||
BEACON_LOCK_PROM_DB = 6.0; // пик над средним окна (dB) для статуса LOCK
|
BEACON_UNLOCK_PROM_DB = 3.0; // гистерезис: гаснет ниже этого
|
||||||
BEACON_LOBE_DB = 10.0; // ширина лепестка для центроида (dB ниже пика)
|
// --- окна поиска/слежения ---
|
||||||
|
BEACON_SEARCH_HZ = 6000.0; // ШИРОКОЕ окно захвата (клик/включение/потеря лока)
|
||||||
|
BEACON_TRACK_HZ = 2000.0; // УЗКИЙ gate слежения: сосед дальше — физически вне окна
|
||||||
|
BEACON_MAX_SLEW_HZ = 500.0; // дрейф LNB ≪ этого/тик; больший скачок = чужой сигнал → отвергаем
|
||||||
|
BEACON_TRACK_ALPHA = 0.25; // сглаживание позиции центроида за тик
|
||||||
|
// --- форма лепестка BPSK-маяка (узнаём маяк, а не «самый громкий пик») ---
|
||||||
|
BEACON_LOBE_DB = 10.0; // лепесток = пиксели в пределах этого ниже пика
|
||||||
|
BEACON_LOBE_MIN_HZ = 700.0; // уже → CW-несущая, не BPSK-маяк
|
||||||
|
BEACON_LOBE_MAX_HZ = 5000.0; // шире → слились два сигнала
|
||||||
|
BEACON_SYM_MAX = 0.70; // |L−R|/(L+R) выше → односторонний сосед, не маяк
|
||||||
|
BEACON_SHAPE_MAX_HZPP = 600.0; // форму лепестка проверяем лишь если бин ≤ этого
|
||||||
|
|
||||||
function TRadioController.CoarseBeaconOffset(ExpectOff: Double; out OffHz: Double): Boolean;
|
function TRadioController.MeasureBeaconFFT(ExpectOff, WinHz: Double;
|
||||||
// Ищет максимум 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;
|
out PeakOff, PromDB: Double): Boolean;
|
||||||
// Маяк по display-FFT в окне ±BEACON_SEARCH_HZ вокруг ExpectOff. Средний маяк —
|
// Маяк по display-FFT в окне ±WinHz вокруг ExpectOff (baseband offset; центр =
|
||||||
// BPSK с ПОДАВЛЕННОЙ несущей: в спектре не линия, а широкая симметричная «полка»
|
// пиксель n/2). Центральный маяк — BPSK с ПОДАВЛЕННОЙ несущей: в спектре не
|
||||||
// (~2.4 кГц), peak-finder по ней скачет. Поэтому позицию берём как
|
// линия, а широкая симметричная «полка» (~2.4 кГц), peak-finder по ней скачет.
|
||||||
// POWER-ВЗВЕШЕННЫЙ ЦЕНТРОИД лепестка (пиксели в пределах BEACON_LOBE_DB от пика):
|
// Позицию берём как POWER-ВЗВЕШЕННЫЙ ЦЕНТРОИД лепестка (пиксели в пределах
|
||||||
// центр симметричной полки = несущая, стабилен. PromDB — пик над средним окна.
|
// BEACON_LOBE_DB от пика) — центр симметричной полки = несущая, стабилен.
|
||||||
|
// PromDB — пик над ШУМОВЫМ ПОЛОМ окна (бины ниже лепестка), не над общим средним:
|
||||||
|
// иначе на узком gate сам лепесток поднимал бы «средний» и занижал метрику.
|
||||||
|
// Проверка ФОРМЫ (ширина + симметрия) отсеивает CW-несущую и одностороннего
|
||||||
|
// соседа: False → лепесток не похож на маяк, вызывающий держит позицию.
|
||||||
var
|
var
|
||||||
pix: array[0..SPECTRUM_PIXELS - 1] of Single;
|
pix: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||||||
n, i, best, cnt: Integer;
|
n, i, best, cnt, noiseCnt: Integer;
|
||||||
hzPerPix, lo, hi, off, sum, thr, p, wsum, fsum: Double;
|
hzPerPix, lo, hi, off, thr, p, wsum, fsum, noiseSum: Double;
|
||||||
|
minOff, maxOff, wLeft, wRight, widthHz, floorDb: Double;
|
||||||
bestv: Single;
|
bestv: Single;
|
||||||
begin
|
begin
|
||||||
Result := False; PeakOff := ExpectOff; PromDB := -99.0;
|
Result := False; PeakOff := ExpectOff; PromDB := -99.0;
|
||||||
@@ -1040,34 +1022,64 @@ begin
|
|||||||
FDSPEngine.GetSpectrumData(pix, n);
|
FDSPEngine.GetSpectrumData(pix, n);
|
||||||
if n < 8 then Exit;
|
if n < 8 then Exit;
|
||||||
hzPerPix := FSpanHz / n;
|
hzPerPix := FSpanHz / n;
|
||||||
lo := ExpectOff - BEACON_SEARCH_HZ;
|
lo := ExpectOff - WinHz;
|
||||||
hi := ExpectOff + BEACON_SEARCH_HZ;
|
hi := ExpectOff + WinHz;
|
||||||
best := -1; bestv := -1e30; sum := 0; cnt := 0;
|
// Проход A — пик в окне.
|
||||||
|
best := -1; bestv := -1e30; cnt := 0;
|
||||||
for i := 0 to n - 1 do
|
for i := 0 to n - 1 do
|
||||||
begin
|
begin
|
||||||
off := (i - n / 2) * hzPerPix;
|
off := (i - n / 2) * hzPerPix;
|
||||||
if (off < lo) or (off > hi) then Continue;
|
if (off < lo) or (off > hi) then Continue;
|
||||||
sum := sum + pix[i]; Inc(cnt);
|
Inc(cnt);
|
||||||
if pix[i] > bestv then begin bestv := pix[i]; best := i; end;
|
if pix[i] > bestv then begin bestv := pix[i]; best := i; end;
|
||||||
end;
|
end;
|
||||||
if (best < 0) or (cnt < 4) then Exit;
|
if (best < 0) or (cnt < 4) then Exit;
|
||||||
// Центроид лепестка: пиксели в пределах BEACON_LOBE_DB ниже пика, вес —
|
// Проход B — центроид лепестка + протяжённость + шумовой пол (бины вне лепестка).
|
||||||
// линейная мощность над порогом (шум за порогом не входит).
|
|
||||||
thr := bestv - BEACON_LOBE_DB;
|
thr := bestv - BEACON_LOBE_DB;
|
||||||
wsum := 0; fsum := 0;
|
wsum := 0; fsum := 0; minOff := 1e30; maxOff := -1e30;
|
||||||
|
noiseSum := 0; noiseCnt := 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
|
||||||
|
begin
|
||||||
|
p := Power(10.0, (pix[i] - thr) / 10.0); // линейная мощность над порогом
|
||||||
|
wsum := wsum + p;
|
||||||
|
fsum := fsum + p * off;
|
||||||
|
if off < minOff then minOff := off;
|
||||||
|
if off > maxOff then maxOff := off;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
noiseSum := noiseSum + pix[i]; Inc(noiseCnt);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
if wsum <= 0 then Exit;
|
||||||
|
PeakOff := fsum / wsum;
|
||||||
|
if noiseCnt > 0 then floorDb := noiseSum / noiseCnt
|
||||||
|
else floorDb := bestv - BEACON_LOBE_DB; // весь gate занят лепестком — fallback
|
||||||
|
PromDB := bestv - floorDb;
|
||||||
|
// Форма лепестка — оцениваем ТОЛЬКО при достаточном разрешении (лепесток
|
||||||
|
// разрешается в несколько бинов). На крупном бине (широкий span) форму не
|
||||||
|
// отличить от CW → пропускаем проверку, полагаемся на gate+slew в контуре.
|
||||||
|
if hzPerPix > BEACON_SHAPE_MAX_HZPP then begin Result := True; Exit; end;
|
||||||
|
// Ширина лепестка (BPSK ~2.4 кГц). Узкий = CW-несущая, широкий = слились два.
|
||||||
|
widthHz := maxOff - minOff;
|
||||||
|
if (widthHz < BEACON_LOBE_MIN_HZ) or (widthHz > BEACON_LOBE_MAX_HZ) then Exit;
|
||||||
|
// Симметрия мощности вокруг центроида — односторонний сосед в окне ломает её.
|
||||||
|
wLeft := 0; wRight := 0;
|
||||||
for i := 0 to n - 1 do
|
for i := 0 to n - 1 do
|
||||||
begin
|
begin
|
||||||
off := (i - n / 2) * hzPerPix;
|
off := (i - n / 2) * hzPerPix;
|
||||||
if (off < lo) or (off > hi) then Continue;
|
if (off < lo) or (off > hi) then Continue;
|
||||||
if pix[i] < thr then Continue;
|
if pix[i] < thr then Continue;
|
||||||
p := Power(10.0, (pix[i] - thr) / 10.0); // линейная мощность над порогом
|
p := Power(10.0, (pix[i] - thr) / 10.0);
|
||||||
wsum := wsum + p;
|
if off < PeakOff then wLeft := wLeft + p else wRight := wRight + p;
|
||||||
fsum := fsum + p * off;
|
|
||||||
end;
|
end;
|
||||||
if wsum > 0 then PeakOff := fsum / wsum
|
if (wLeft + wRight) > 0 then
|
||||||
else PeakOff := (best - n / 2) * hzPerPix;
|
if Abs(wLeft - wRight) / (wLeft + wRight) > BEACON_SYM_MAX then Exit;
|
||||||
PromDB := bestv - (sum / cnt); // пик над средним окна (dB)
|
Result := True;
|
||||||
Result := True;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.SetBeaconRefHz(Hz: Double);
|
procedure TRadioController.SetBeaconRefHz(Hz: Double);
|
||||||
@@ -1076,20 +1088,33 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.BeaconSeedAtHz(DisplayHz: Double);
|
procedure TRadioController.BeaconSeedAtHz(DisplayHz: Double);
|
||||||
// Пользователь кликнул по маяку → задаём область отслеживания (уточняем пик по
|
// Пользователь кликнул по маяку → ручной режим (контур ведёт ИМЕННО этот пик,
|
||||||
// display-FFT рядом с кликом) и включаем ручной режим (контур ведёт ИМЕННО этот
|
// без автопоиска, который в забитой полосе хватает соседей). Так как клик —
|
||||||
// пик, без автопоиска, который в забитой полосе хватает соседей).
|
// уверенное указание, делаем СРАЗУ почти полную (deadbeat) коррекцию: один клик
|
||||||
var clickOff, refined, prom: Double;
|
// ставит маяк на опорную за одну перестройку, без многократного «тыкания».
|
||||||
|
var clickOff, refined, prom, e, dLock: Double;
|
||||||
begin
|
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);
|
if not FBeaconLockOn then SetBeaconLock(True);
|
||||||
|
FBeaconManualSeed := True;
|
||||||
|
clickOff := DisplayHz - FCenterFreq;
|
||||||
|
if MeasureBeaconFFT(clickOff, BEACON_SEARCH_HZ, refined, prom) then
|
||||||
|
begin
|
||||||
|
FBeaconTrackHz := FCenterFreq + refined;
|
||||||
|
FBeaconPromDB := prom;
|
||||||
|
FBeaconLocked := prom >= BEACON_LOCK_PROM_DB;
|
||||||
|
// deadbeat-коррекция + feed-forward: маяк уедет на −dLock, окно ведём туда же.
|
||||||
|
e := FBeaconTrackHz - FBeaconRefHz;
|
||||||
|
dLock := BEACON_SEED_GAIN * e;
|
||||||
|
ApplyBeaconLOError(dLock); // складываем в LOError + сохраняем
|
||||||
|
FBeaconTrackHz := FBeaconTrackHz - dLock; // ≈ FBeaconRefHz
|
||||||
|
FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать FFT отразить новый LO
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
// маяк под кликом не распознан — просто ставим окно туда, ждём захвата.
|
||||||
|
FBeaconTrackHz := DisplayHz;
|
||||||
|
FBeaconCorrCooldown := BEACON_SETTLE_TICKS;
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.SetBeaconLock(On_: Boolean);
|
procedure TRadioController.SetBeaconLock(On_: Boolean);
|
||||||
@@ -1099,61 +1124,118 @@ begin
|
|||||||
FBeaconLockOn := On_;
|
FBeaconLockOn := On_;
|
||||||
if On_ then
|
if On_ then
|
||||||
begin
|
begin
|
||||||
FBeaconSearchTicks := 0;
|
|
||||||
FBeaconCorrCooldown := 0;
|
FBeaconCorrCooldown := 0;
|
||||||
|
FBeaconSaveCooldown := 0;
|
||||||
FBeaconManualSeed := False; // авто до первого клика пользователя
|
FBeaconManualSeed := False; // авто до первого клика пользователя
|
||||||
FBeaconLocked := False;
|
FBeaconLocked := False;
|
||||||
|
FBeaconPromDB := 0.0;
|
||||||
|
FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError уже несёт калибровку)
|
||||||
// Начальная область — у опорной частоты (уточняем пик, если виден).
|
// Начальная область — у опорной частоты (уточняем пик, если виден).
|
||||||
if MeasureBeaconFFT(FBeaconRefHz - FCenterFreq, refined, prom) then
|
if MeasureBeaconFFT(FBeaconRefHz - FCenterFreq, BEACON_SEARCH_HZ, refined, prom) then
|
||||||
FBeaconTrackHz := FCenterFreq + refined
|
FBeaconTrackHz := FCenterFreq + refined
|
||||||
else
|
else
|
||||||
FBeaconTrackHz := FBeaconRefHz;
|
FBeaconTrackHz := FBeaconRefHz;
|
||||||
end;
|
end
|
||||||
// При выключении FBeaconLockHz замораживаем (как калибровка LNB).
|
else
|
||||||
|
PersistBeaconCalibration; // фиксируем накопленную коррекцию в LOError (см. метод)
|
||||||
Changed(rfBeaconLock);
|
Changed(rfBeaconLock);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.ServiceBeaconLock;
|
procedure TRadioController.ServiceBeaconLock;
|
||||||
// Одна итерация контура (UI-таймер @10 Гц). Меряет пик маяка по display-FFT,
|
// Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Меряет маяк по
|
||||||
// сглаживает, и (после ручного клика) тянет FBeaconLockHz так, чтобы маяк
|
// display-FFT и (после ручного клика) трим LOError так, чтобы маяк стоял на
|
||||||
// оставался на опорной частоте → весь downlink стабилен. Без NCO/FLL.
|
// опорной частоте → весь downlink стабилен. Коррекция складывается в LOError
|
||||||
var pk, prom, measDisp, e: Double;
|
// (живая калибровка LNB: видна в настройках, сохраняется).
|
||||||
|
//
|
||||||
|
// Устойчивость к соседним сигналам (как в SDR Console):
|
||||||
|
// • при локе окно УЗКОЕ (±BEACON_TRACK_HZ) — далёкий сосед вне окна;
|
||||||
|
// • slew-отбраковка: дрейф LNB медленный, резкий скачок центроида = чужой
|
||||||
|
// сигнал, измерение игнорируем;
|
||||||
|
// • MeasureBeaconFFT проверяет форму лепестка (узнаём маяк, не «громкий пик»).
|
||||||
|
// Быстрая сходимость при большой ошибке:
|
||||||
|
// • feed-forward (FBeaconTrackHz −= dLock) — окно едет за маяком сквозь ретюн,
|
||||||
|
// лок не теряется; во время settle позицию не двигаем (FFT в транзиенте).
|
||||||
|
var win, pk, prom, cand, e, dLock: Double;
|
||||||
begin
|
begin
|
||||||
if not FBeaconLockOn then Exit;
|
if not FBeaconLockOn then Exit;
|
||||||
if FCurrentXvtr < 0 then Exit; // только в трансвертер-режиме
|
if FCurrentXvtr < 0 then Exit; // только в трансвертер-режиме
|
||||||
// Окно поиска — вокруг ТЕКУЩЕЙ display-позиции маяка, пересчитанной в baseband
|
if FBeaconSaveCooldown > 0 then Dec(FBeaconSaveCooldown);
|
||||||
// через актуальный FCenterFreq. Поэтому сдвиг водопада/центра окно не ломает.
|
// Окно — вокруг ТЕКУЩЕЙ display-позиции маяка, пересчитанной в baseband через
|
||||||
if not MeasureBeaconFFT(FBeaconTrackHz - FCenterFreq, pk, prom) then Exit;
|
// актуальный FCenterFreq (сдвиг водопада/центра окно не ломает). Узкое при локе.
|
||||||
|
if FBeaconLocked then win := BEACON_TRACK_HZ else win := BEACON_SEARCH_HZ;
|
||||||
|
if not MeasureBeaconFFT(FBeaconTrackHz - FCenterFreq, win, pk, prom) then
|
||||||
|
begin
|
||||||
|
Changed(rfBeaconLock); // лепесток не виден/не маяк — держим позицию и лок
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
cand := FCenterFreq + pk;
|
||||||
|
|
||||||
// Сопровождаем центроид плавно (в display-частоте; стабильность важнее скорости).
|
// Slew-отбраковка (только при локе/узком окне): скачок больше дрейфа LNB =
|
||||||
FBeaconTrackHz := FBeaconTrackHz + 0.12 * ((FCenterFreq + pk) - FBeaconTrackHz);
|
// чужой сигнал в окне → измерение игнорируем, остаёмся на маяке.
|
||||||
FBeaconPromDB := FBeaconPromDB + 0.2 * (prom - FBeaconPromDB);
|
if FBeaconLocked and (Abs(cand - FBeaconTrackHz) > BEACON_MAX_SLEW_HZ) then
|
||||||
|
begin
|
||||||
|
Changed(rfBeaconLock);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Сопровождение центроида. Во время settle НЕ двигаем — FFT ещё отражает
|
||||||
|
// старый LO (доверяем feed-forward-предсказанию позиции).
|
||||||
|
if FBeaconCorrCooldown = 0 then
|
||||||
|
FBeaconTrackHz := FBeaconTrackHz + BEACON_TRACK_ALPHA * (cand - FBeaconTrackHz);
|
||||||
|
FBeaconPromDB := FBeaconPromDB + 0.2 * (prom - FBeaconPromDB);
|
||||||
// Гистерезис лока: загорается при LOCK_PROM, гаснет ниже UNLOCK_PROM (без мерцания).
|
// Гистерезис лока: загорается при LOCK_PROM, гаснет ниже UNLOCK_PROM (без мерцания).
|
||||||
if FBeaconPromDB >= BEACON_LOCK_PROM_DB then FBeaconLocked := True
|
if FBeaconPromDB >= BEACON_LOCK_PROM_DB then FBeaconLocked := True
|
||||||
else if FBeaconPromDB < BEACON_UNLOCK_PROM_DB then FBeaconLocked := False;
|
else if FBeaconPromDB < BEACON_UNLOCK_PROM_DB then FBeaconLocked := False;
|
||||||
|
|
||||||
// Коррекцию НЕ применяем каждый тик: display-FFT отстаёт от ретюна LO на
|
// Коррекция: НЕ каждый тик (display-FFT отстаёт от ретюна → перелёт). После
|
||||||
// задержку пайплайна, и спам-коррекции дают перелёт (маяк улетает из окна).
|
// коррекции ждём BEACON_SETTLE_TICKS. Только после ручного клика (manual seed).
|
||||||
// После каждой коррекции ждём BEACON_SETTLE_TICKS, давая FFT отразить новый LO.
|
|
||||||
// Слежение за пиком (выше) идёт каждый тик — окно едет за маяком.
|
|
||||||
if FBeaconCorrCooldown > 0 then
|
if FBeaconCorrCooldown > 0 then
|
||||||
Dec(FBeaconCorrCooldown)
|
Dec(FBeaconCorrCooldown)
|
||||||
else if FBeaconLocked and FBeaconManualSeed then
|
else if FBeaconLocked and FBeaconManualSeed then
|
||||||
begin
|
begin
|
||||||
// e = видимое_смещение_маяка − опорное. d(e)/d(FBeaconLockHz) = −1,
|
// e = видимое_смещение_маяка − опорное. d(display)/d(LOError) = −1, поэтому
|
||||||
// поэтому FBeaconLockHz += e подтягивает маяк к опорной (см. вывод знака).
|
// LOError += e тянет маяк к опорной; feed-forward предсказывает новую
|
||||||
measDisp := FBeaconTrackHz;
|
// display-позицию (track −= dLock), чтобы окно ехало за маяком.
|
||||||
e := measDisp - FBeaconRefHz;
|
e := FBeaconTrackHz - FBeaconRefHz;
|
||||||
if Abs(e) > BEACON_DEADBAND_HZ then
|
if Abs(e) > BEACON_DEADBAND_HZ then
|
||||||
begin
|
begin
|
||||||
FBeaconLockHz := FBeaconLockHz + BEACON_GAIN * e;
|
dLock := BEACON_GAIN * e;
|
||||||
PushNetworkState; // ретюн LNB-IF: новый LO = XvtrTranslate(центр)
|
ApplyBeaconLOError(dLock); // в LOError + ретюн + троттл-сохранение
|
||||||
|
FBeaconTrackHz := FBeaconTrackHz - dLock;
|
||||||
FBeaconCorrCooldown := BEACON_SETTLE_TICKS;
|
FBeaconCorrCooldown := BEACON_SETTLE_TICKS;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
Changed(rfBeaconLock);
|
Changed(rfBeaconLock);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.ApplyBeaconLOError(dLock: Double);
|
||||||
|
// Складывает коррекцию маяка ПРЯМО в LOError активного трансвертера — это и есть
|
||||||
|
// живая калибровка дрейфа LNB: сразу видна в настройках, переживает рестарт.
|
||||||
|
// FBeaconLockHz — лишь сессионный аккумулятор для индикации. Запись на диск
|
||||||
|
// троттлим (BEACON_SAVE_TICKS): коррекции редки (deadband), но при долгом прогреве
|
||||||
|
// LNB их много — не молотим диск каждую.
|
||||||
|
begin
|
||||||
|
if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit;
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LOError :=
|
||||||
|
FXvtrSettings.Entries[FCurrentXvtr].LOError + dLock;
|
||||||
|
FBeaconLockHz := FBeaconLockHz + dLock; // сколько накрутили за сессию (индикация)
|
||||||
|
PushNetworkState; // ретюн: XvtrTranslate читает новый LOError
|
||||||
|
if (FBeaconSaveCooldown = 0) and FDevConnected then
|
||||||
|
begin
|
||||||
|
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
||||||
|
FBeaconSaveCooldown := BEACON_SAVE_TICKS;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.PersistBeaconCalibration;
|
||||||
|
// Коррекция уже живёт в LOError (ApplyBeaconLOError). Здесь — финальный flush на
|
||||||
|
// диск при выключении лока, на случай неотправленной из-за троттлинга.
|
||||||
|
begin
|
||||||
|
if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit;
|
||||||
|
if FDevConnected then FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
|
||||||
|
FBeaconSaveCooldown := 0;
|
||||||
|
end;
|
||||||
|
|
||||||
function TRadioController.BeaconState: TBeaconState;
|
function TRadioController.BeaconState: TBeaconState;
|
||||||
begin
|
begin
|
||||||
if not FBeaconLockOn then Exit(bsIdle);
|
if not FBeaconLockOn then Exit(bsIdle);
|
||||||
|
|||||||
+3
-1
@@ -3211,7 +3211,9 @@ begin
|
|||||||
FXvtrBegin[i] := MakeFlt(Grp, BEG_X, Y, BEG_W, 0.0, 50000.0, 0.001, 3);
|
FXvtrBegin[i] := MakeFlt(Grp, BEG_X, Y, BEG_W, 0.0, 50000.0, 0.001, 3);
|
||||||
FXvtrEnd[i] := MakeFlt(Grp, END_X, Y, END_W, 0.0, 50000.0, 0.001, 3);
|
FXvtrEnd[i] := MakeFlt(Grp, END_X, Y, END_W, 0.0, 50000.0, 0.001, 3);
|
||||||
FXvtrLOOff[i] := MakeFlt(Grp, LO_X, Y, LO_W, -50000.0, 50000.0, 0.001, 6);
|
FXvtrLOOff[i] := MakeFlt(Grp, LO_X, Y, LO_W, -50000.0, 50000.0, 0.001, 6);
|
||||||
FXvtrLOErr[i] := MakeFlt(Grp, ERR_X, Y, ERR_W, -99999.0, 99999.0, 1.0, 0);
|
// диапазон ±10 МГц: QO-100 LNB без опоры легко уходит на сотни кГц, и
|
||||||
|
// beacon-lock складывает свою коррекцию сюда (живая калибровка LNB).
|
||||||
|
FXvtrLOErr[i] := MakeFlt(Grp, ERR_X, Y, ERR_W, -9999999.0, 9999999.0, 1.0, 0);
|
||||||
FXvtrRXGain[i]:= MakeFlt(Grp, GAIN_X, Y, GAIN_W, -60.0, 60.0, 0.1, 1);
|
FXvtrRXGain[i]:= MakeFlt(Grp, GAIN_X, Y, GAIN_W, -60.0, 60.0, 0.1, 1);
|
||||||
// Чекбокс RX Only центрируем
|
// Чекбокс RX Only центрируем
|
||||||
FXvtrRXOnly[i]:= MakeChk(Grp, RXO_X + (RXO_W - 22) div 2, Y + 2);
|
FXvtrRXOnly[i]:= MakeChk(Grp, RXO_X + (RXO_W - 22) div 2, Y + 2);
|
||||||
|
|||||||
+5
-5
@@ -76,7 +76,7 @@ type
|
|||||||
// ── QO-100 beacon markers (диагностика лока) ──────────────────────────────
|
// ── QO-100 beacon markers (диагностика лока) ──────────────────────────────
|
||||||
FBeaconMarkActive: Boolean;
|
FBeaconMarkActive: Boolean;
|
||||||
FBeaconRefFreq: Double; // опорная частота маяка (где ДОЛЖЕН быть)
|
FBeaconRefFreq: Double; // опорная частота маяка (где ДОЛЖЕН быть)
|
||||||
FBeaconTrkFreq: Double; // где сидит трекер (NCO) сейчас
|
FBeaconTrkFreq: Double; // отслеживаемый центроид маяка сейчас
|
||||||
// ── Spectrum buffer ───────────────────────────────────────────────────────
|
// ── Spectrum buffer ───────────────────────────────────────────────────────
|
||||||
FSpectrumBuf: array[0..1023] of Single;
|
FSpectrumBuf: array[0..1023] of Single;
|
||||||
FSpectrumBufCount: Integer;
|
FSpectrumBufCount: Integer;
|
||||||
@@ -502,9 +502,9 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSpectrumView.DrawBeaconMarkers(C: TCanvas; W, H: Integer);
|
procedure TSpectrumView.DrawBeaconMarkers(C: TCanvas; W, H: Integer);
|
||||||
// Две вертикали: опорная частота маяка (зелёная пунктир) и текущая позиция
|
// Две вертикали: опорная частота маяка (зелёная пунктир) и отслеживаемый
|
||||||
// трекера (оранжевая сплошная). Расхождение видно глазом → понятно, на пик
|
// центроид (оранжевая сплошная). Расхождение видно глазом → понятно, сел ли
|
||||||
// сел трекер или на шум.
|
// контур на пик маяка или на шум.
|
||||||
procedure Vline(FreqHz: Double; Col: TColor; Style: TPenStyle);
|
procedure Vline(FreqHz: Double; Col: TColor; Style: TPenStyle);
|
||||||
var x: Integer;
|
var x: Integer;
|
||||||
begin
|
begin
|
||||||
@@ -516,7 +516,7 @@ procedure TSpectrumView.DrawBeaconMarkers(C: TCanvas; W, H: Integer);
|
|||||||
end;
|
end;
|
||||||
begin
|
begin
|
||||||
Vline(FBeaconRefFreq, clLime, psDot); // где маяк ДОЛЖЕН быть
|
Vline(FBeaconRefFreq, clLime, psDot); // где маяк ДОЛЖЕН быть
|
||||||
Vline(FBeaconTrkFreq, TColor($000AA5FF), psSolid); // где сидит трекер (оранж)
|
Vline(FBeaconTrkFreq, TColor($000AA5FF), psSolid); // отслеживаемый центроид (оранж)
|
||||||
C.Pen.Style := psSolid;
|
C.Pen.Style := psSolid;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
@@ -658,7 +658,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSpectrumViewOpenGL.DrawBeaconMarkersGL(W, H: Integer);
|
procedure TSpectrumViewOpenGL.DrawBeaconMarkersGL(W, H: Integer);
|
||||||
// QO-100 beacon-маркеры: опорная частота (зелёная) + позиция трекера (оранж).
|
// QO-100 beacon-маркеры: опорная частота (зелёная) + отслеживаемый центроид (оранж).
|
||||||
procedure Vline(FreqHz: Double; Col: TColor);
|
procedure Vline(FreqHz: Double; Col: TColor);
|
||||||
var x: Integer;
|
var x: Integer;
|
||||||
begin
|
begin
|
||||||
@@ -669,7 +669,7 @@ procedure TSpectrumViewOpenGL.DrawBeaconMarkersGL(W, H: Integer);
|
|||||||
end;
|
end;
|
||||||
begin
|
begin
|
||||||
Vline(FBeaconRefFreq, clLime); // где маяк ДОЛЖЕН быть
|
Vline(FBeaconRefFreq, clLime); // где маяк ДОЛЖЕН быть
|
||||||
Vline(FBeaconTrkFreq, TColor($000AA5FF)); // где сидит трекер (оранж)
|
Vline(FBeaconTrkFreq, TColor($000AA5FF)); // отслеживаемый центроид (оранж)
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TSpectrumViewOpenGL.DrawADCOverlay(W, H: Integer);
|
procedure TSpectrumViewOpenGL.DrawADCOverlay(W, H: Integer);
|
||||||
|
|||||||
+2
-1
@@ -33,7 +33,8 @@ const
|
|||||||
PAD_X = 8;
|
PAD_X = 8;
|
||||||
SEQ_MIN_W = 90;
|
SEQ_MIN_W = 90;
|
||||||
STATUS_ORDER: array[0..7] of Integer = (2, 0, 1, 3, 7, 4, 5, 6);
|
STATUS_ORDER: array[0..7] of Integer = (2, 0, 1, 3, 7, 4, 5, 6);
|
||||||
BASE_W: array[0..7] of Integer = (280, 160, 170, 150, 190, 180, 130, 90);
|
// поле 7: PLL (openHPSDR) ИЛИ статус QO-100 beacon lock (Pluto) — шире под "BCN LOCK …".
|
||||||
|
BASE_W: array[0..7] of Integer = (280, 160, 170, 150, 190, 180, 130, 200);
|
||||||
INITIAL_TEXT: array[0..7] of string = (
|
INITIAL_TEXT: array[0..7] of string = (
|
||||||
'Board --',
|
'Board --',
|
||||||
'IP --',
|
'IP --',
|
||||||
|
|||||||
+1
-17
@@ -25,7 +25,7 @@ interface
|
|||||||
|
|
||||||
uses
|
uses
|
||||||
Classes, SysUtils, Math, SyncObjs,
|
Classes, SysUtils, Math, SyncObjs,
|
||||||
WDSP, BeaconLock;
|
WDSP;
|
||||||
|
|
||||||
const
|
const
|
||||||
// WDSP mode integers (нет именованных констант в WDSP.pas)
|
// WDSP mode integers (нет именованных констант в WDSP.pas)
|
||||||
@@ -292,7 +292,6 @@ type
|
|||||||
FMuted: Boolean;
|
FMuted: Boolean;
|
||||||
FVolume: Double;
|
FVolume: Double;
|
||||||
FLastError: string;
|
FLastError: string;
|
||||||
FBeaconTracker: TBeaconTracker; // QO-100 beacon lock (НЕ владеем; см. контроллер)
|
|
||||||
|
|
||||||
function ModeToWDSP(Mode: Integer): Integer;
|
function ModeToWDSP(Mode: Integer): Integer;
|
||||||
procedure ApplyDefaultFilter;
|
procedure ApplyDefaultFilter;
|
||||||
@@ -355,9 +354,6 @@ type
|
|||||||
procedure UpdateAGCLines(SpectrumW: Integer); // читает hang/thresh из WDSP для линии на спектре
|
procedure UpdateAGCLines(SpectrumW: Integer); // читает hang/thresh из WDSP для линии на спектре
|
||||||
procedure SetSpectrumWidth(W: Integer); // обновляет FLastSpectrumW для корректных AGC линий
|
procedure SetSpectrumWidth(W: Integer); // обновляет FLastSpectrumW для корректных AGC линий
|
||||||
procedure SetShift(ShiftHz: Double); // CTUN NCO сдвиг
|
procedure SetShift(ShiftHz: Double); // CTUN NCO сдвиг
|
||||||
// QO-100 beacon lock: трекер кормится RX-IQ из DSP-потока. Владелец —
|
|
||||||
// контроллер; движок лишь подаёт сэмплы и сообщает sample rate.
|
|
||||||
procedure SetBeaconTracker(T: TBeaconTracker);
|
|
||||||
procedure SetNRMode(Mode: Integer);
|
procedure SetNRMode(Mode: Integer);
|
||||||
procedure SetNR(Enable: Boolean);
|
procedure SetNR(Enable: Boolean);
|
||||||
procedure SetNBMode(Mode: Integer);
|
procedure SetNBMode(Mode: Integer);
|
||||||
@@ -1123,7 +1119,6 @@ begin
|
|||||||
// Обновляем параметры
|
// Обновляем параметры
|
||||||
FSampleRate := NewRate;
|
FSampleRate := NewRate;
|
||||||
FBufSize := FAudioBufSize * NewRate div FAudioRate;
|
FBufSize := FAudioBufSize * NewRate div FAudioRate;
|
||||||
if FBeaconTracker <> nil then FBeaconTracker.Configure(FSampleRate);
|
|
||||||
|
|
||||||
// Перераспределяем буферы под новый размер
|
// Перераспределяем буферы под новый размер
|
||||||
SetLength(FRXIn, FBufSize * 2);
|
SetLength(FRXIn, FBufSize * 2);
|
||||||
@@ -1218,11 +1213,6 @@ begin
|
|||||||
|
|
||||||
FeedDisplaySample(IR * SCALE, QR * SCALE);
|
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;
|
FRXAccI[FRXAccPos] := IR * SCALE;
|
||||||
FRXAccQ[FRXAccPos] := QR * SCALE;
|
FRXAccQ[FRXAccPos] := QR * SCALE;
|
||||||
Inc(FRXAccPos);
|
Inc(FRXAccPos);
|
||||||
@@ -1381,12 +1371,6 @@ begin
|
|||||||
UpdateAGCLines(FLastSpectrumW);
|
UpdateAGCLines(FLastSpectrumW);
|
||||||
end;
|
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);
|
procedure TWDSPEngine.SetShift(ShiftHz: Double);
|
||||||
begin
|
begin
|
||||||
FShiftHz := ShiftHz;
|
FShiftHz := ShiftHz;
|
||||||
|
|||||||
Reference in New Issue
Block a user