mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +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:
+217
-135
@@ -50,9 +50,14 @@ uses
|
||||
Classes, SysUtils, Math,
|
||||
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
||||
WDSPEngine, AudioOutput, AudioInput,
|
||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore, BeaconLock;
|
||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
||||
|
||||
type
|
||||
// QO-100 beacon lock — статус контура (для UI/маркеров).
|
||||
TBeaconState = (bsIdle, // лок выключен
|
||||
bsSearch, // включён, маяк ещё не подтверждён (низкий prominence)
|
||||
bsLock); // маяк уверенно виден и сопровождается
|
||||
|
||||
// Поле, которое изменилось — для гранулярных уведомлений наружу.
|
||||
TRadioField = (
|
||||
rfVfoA, rfVfoB, rfActiveVfo,
|
||||
@@ -177,19 +182,20 @@ type
|
||||
FCurrentBand: Integer;
|
||||
FCurrentXvtr: Integer;
|
||||
|
||||
// ---- QO-100 beacon lock ----
|
||||
FBeaconTracker: TBeaconTracker; // владеем (Free в FreeEngines)
|
||||
// ---- QO-100 beacon lock (центральный BPSK-маяк 10489.750, по display-FFT) ----
|
||||
FBeaconLockOn: Boolean; // лок включён пользователем
|
||||
FBeaconLockHz: Double; // текущая динамич. коррекция LO (Hz), поверх LOError
|
||||
FBeaconRefHz: Double; // опорная частота маяка (Hz, downlink), деф. 10489.500e6
|
||||
FBeaconSearchTicks: Integer; // тиков без лока (троттлинг переакквизиции)
|
||||
FBeaconLockHz: Double; // коррекция, накопленная за ТЕКУЩУЮ сессию лока
|
||||
// (только индикация на кнопке); сама поправка
|
||||
// живёт в LOError активного трансвертера
|
||||
FBeaconRefHz: Double; // опорная частота маяка (Hz, downlink), деф. 10489.750e6
|
||||
FBeaconManualSeed: Boolean; // пользователь указал маяк кликом → не автопоиск
|
||||
// FFT-трекинг маяка (робастный, по display-FFT — без NCO/FLL):
|
||||
// FFT-трекинг маяка (робастный, по display-FFT):
|
||||
FBeaconTrackHz: Double; // отслеживаемая АБСОЛЮТНАЯ display-частота маяка
|
||||
// (не baseband-offset — иначе сдвиг центра/водопада ломает окно)
|
||||
FBeaconPromDB: Double; // «высота» пика над шумом окна (лок-метрика)
|
||||
FBeaconLocked: Boolean; // пик уверенно виден
|
||||
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка пайплайна)
|
||||
FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков)
|
||||
|
||||
// ---- Аудио / TX ----
|
||||
FVolume: Integer;
|
||||
@@ -336,12 +342,17 @@ 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;
|
||||
// Измерение маяка по display-FFT в окне ±WinHz вокруг ExpectOff (baseband):
|
||||
// центроид лепестка → уточнённое смещение, «высота» пика над средним окна
|
||||
// (лок-метрика). False, если лепесток не похож на маяк (узкий/широкий/
|
||||
// несимметричный сосед) или вне span — тогда трек держим на месте.
|
||||
function MeasureBeaconFFT(ExpectOff, WinHz: Double;
|
||||
out PeakOff, PromDB: Double): Boolean;
|
||||
// Складывает коррекцию маяка dLock в LOError активного трансвертера (живая
|
||||
// калибровка LNB), ретюнит радио и троттлингом сохраняет на диск.
|
||||
procedure ApplyBeaconLOError(dLock: Double);
|
||||
// Flush LOError на диск при выключении лока (если троттлинг не успел).
|
||||
procedure PersistBeaconCalibration;
|
||||
|
||||
// Применяет текущие FMode/FFilterBW/FM-параметры к DSP (RX/TX фильтр,
|
||||
// FM-девиация, squelch). Вызывается командами и UI-путями смены режима.
|
||||
@@ -448,15 +459,15 @@ type
|
||||
|
||||
// QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку.
|
||||
procedure SetBeaconLock(On_: Boolean);
|
||||
procedure ServiceBeaconLock; // одна итерация контура (зовётся UI-таймером ~4 Гц)
|
||||
procedure ServiceBeaconLock; // одна итерация контура (MeterTimer @10 Гц)
|
||||
procedure SetBeaconRefHz(Hz: Double); // опорная частота маяка (downlink)
|
||||
procedure BeaconSeedAtHz(DisplayHz: Double); // пользователь указал маяк кликом
|
||||
function BeaconState: TBeaconState;
|
||||
function BeaconCorrectionHz: Double; // текущая коррекция LO (FBeaconLockHz)
|
||||
function BeaconCorrectionHz: Double; // коррекция за сессию лока (индикация; в LOError)
|
||||
function BeaconSNRdB: Double;
|
||||
function BeaconLockEnabled: Boolean;
|
||||
function BeaconRefHz: Double; // опорная частота маяка (для маркера)
|
||||
function BeaconTrackedFreqHz: Double;// где сидит трекер: центр + measured offset
|
||||
function BeaconTrackedFreqHz: Double;// отслеживаемая display-частота маяка (центроид)
|
||||
|
||||
// Отображение / приём
|
||||
procedure SetCTun(On_: Boolean);
|
||||
@@ -553,12 +564,10 @@ 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)
|
||||
// QO-100 beacon lock: измерение по display-FFT (см. ServiceBeaconLock).
|
||||
FBeaconRefHz := 10489750000.0; // центральный BPSK-маяк NB-транспондера (downlink)
|
||||
FBeaconLockHz := 0.0;
|
||||
FBeaconLockOn := False;
|
||||
FDSPEngine.SetBeaconTracker(FBeaconTracker);
|
||||
|
||||
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
|
||||
// формы / при подключении устройства).
|
||||
@@ -577,9 +586,6 @@ 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;
|
||||
@@ -942,13 +948,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 + BeaconLock
|
||||
// FBeaconLockHz — динамическая коррекция дрейфа LNB (QO-100 beacon lock),
|
||||
// поверх статичной ручной LOError. 0 когда лок выключен.
|
||||
// f_IF = f_visible - LOOffset + LOError.
|
||||
// QO-100 beacon lock складывает свою коррекцию дрейфа LNB ПРЯМО в LOError
|
||||
// (живая калибровка): она сразу видна в настройках и сохраняется. Отдельного
|
||||
// слагаемого нет — иначе двойной счёт.
|
||||
Result := VisibleHz
|
||||
- FXvtrSettings.Entries[FCurrentXvtr].LOOffset
|
||||
+ FXvtrSettings.Entries[FCurrentXvtr].LOError
|
||||
+ FBeaconLockHz;
|
||||
+ FXvtrSettings.Entries[FCurrentXvtr].LOError;
|
||||
end;
|
||||
|
||||
function TRadioController.ActiveTXFreqHz: Double;
|
||||
@@ -970,68 +976,44 @@ end;
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const
|
||||
BEACON_GAIN = 0.4; // гейн контроллера (доля ошибки за коррекцию)
|
||||
// --- контур коррекции ---
|
||||
BEACON_GAIN = 0.9; // гейн контура: feed-forward делает шаг точным,
|
||||
// <1 — запас от перелёта при шумной оценке центроида
|
||||
BEACON_SEED_GAIN = 0.95; // первая коррекция по клику агрессивнее (1 клик → лок)
|
||||
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 ниже пика)
|
||||
BEACON_SETTLE_TICKS = 8; // пауза (~0.8 с @10 Гц) после коррекции — осадка FFT-пайплайна
|
||||
BEACON_SAVE_TICKS = 30; // троттлинг записи LOError на диск (~3 с @10 Гц)
|
||||
// --- метрика лока (prominence = пик над шумовым полом окна) ---
|
||||
BEACON_LOCK_PROM_DB = 6.0; // загорается LOCK выше этого
|
||||
BEACON_UNLOCK_PROM_DB = 3.0; // гистерезис: гаснет ниже этого
|
||||
// --- окна поиска/слежения ---
|
||||
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;
|
||||
// Ищет максимум 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;
|
||||
function TRadioController.MeasureBeaconFFT(ExpectOff, WinHz: Double;
|
||||
out PeakOff, PromDB: Double): Boolean;
|
||||
// Маяк по display-FFT в окне ±BEACON_SEARCH_HZ вокруг ExpectOff. Средний маяк —
|
||||
// BPSK с ПОДАВЛЕННОЙ несущей: в спектре не линия, а широкая симметричная «полка»
|
||||
// (~2.4 кГц), peak-finder по ней скачет. Поэтому позицию берём как
|
||||
// POWER-ВЗВЕШЕННЫЙ ЦЕНТРОИД лепестка (пиксели в пределах BEACON_LOBE_DB от пика):
|
||||
// центр симметричной полки = несущая, стабилен. PromDB — пик над средним окна.
|
||||
// Маяк по display-FFT в окне ±WinHz вокруг ExpectOff (baseband offset; центр =
|
||||
// пиксель n/2). Центральный маяк — BPSK с ПОДАВЛЕННОЙ несущей: в спектре не
|
||||
// линия, а широкая симметричная «полка» (~2.4 кГц), peak-finder по ней скачет.
|
||||
// Позицию берём как POWER-ВЗВЕШЕННЫЙ ЦЕНТРОИД лепестка (пиксели в пределах
|
||||
// BEACON_LOBE_DB от пика) — центр симметричной полки = несущая, стабилен.
|
||||
// PromDB — пик над ШУМОВЫМ ПОЛОМ окна (бины ниже лепестка), не над общим средним:
|
||||
// иначе на узком gate сам лепесток поднимал бы «средний» и занижал метрику.
|
||||
// Проверка ФОРМЫ (ширина + симметрия) отсеивает CW-несущую и одностороннего
|
||||
// соседа: False → лепесток не похож на маяк, вызывающий держит позицию.
|
||||
var
|
||||
pix: array[0..SPECTRUM_PIXELS - 1] of Single;
|
||||
n, i, best, cnt: Integer;
|
||||
hzPerPix, lo, hi, off, sum, thr, p, wsum, fsum: Double;
|
||||
n, i, best, cnt, noiseCnt: Integer;
|
||||
hzPerPix, lo, hi, off, thr, p, wsum, fsum, noiseSum: Double;
|
||||
minOff, maxOff, wLeft, wRight, widthHz, floorDb: Double;
|
||||
bestv: Single;
|
||||
begin
|
||||
Result := False; PeakOff := ExpectOff; PromDB := -99.0;
|
||||
@@ -1040,34 +1022,64 @@ begin
|
||||
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;
|
||||
lo := ExpectOff - WinHz;
|
||||
hi := ExpectOff + WinHz;
|
||||
// Проход A — пик в окне.
|
||||
best := -1; bestv := -1e30; 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);
|
||||
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 ниже пика, вес —
|
||||
// линейная мощность над порогом (шум за порогом не входит).
|
||||
// Проход B — центроид лепестка + протяжённость + шумовой пол (бины вне лепестка).
|
||||
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
|
||||
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;
|
||||
p := Power(10.0, (pix[i] - thr) / 10.0);
|
||||
if off < PeakOff then wLeft := wLeft + p else wRight := wRight + p;
|
||||
end;
|
||||
if wsum > 0 then PeakOff := fsum / wsum
|
||||
else PeakOff := (best - n / 2) * hzPerPix;
|
||||
PromDB := bestv - (sum / cnt); // пик над средним окна (dB)
|
||||
Result := True;
|
||||
if (wLeft + wRight) > 0 then
|
||||
if Abs(wLeft - wRight) / (wLeft + wRight) > BEACON_SYM_MAX then Exit;
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
procedure TRadioController.SetBeaconRefHz(Hz: Double);
|
||||
@@ -1076,20 +1088,33 @@ begin
|
||||
end;
|
||||
|
||||
procedure TRadioController.BeaconSeedAtHz(DisplayHz: Double);
|
||||
// Пользователь кликнул по маяку → задаём область отслеживания (уточняем пик по
|
||||
// display-FFT рядом с кликом) и включаем ручной режим (контур ведёт ИМЕННО этот
|
||||
// пик, без автопоиска, который в забитой полосе хватает соседей).
|
||||
var clickOff, refined, prom: Double;
|
||||
// Пользователь кликнул по маяку → ручной режим (контур ведёт ИМЕННО этот пик,
|
||||
// без автопоиска, который в забитой полосе хватает соседей). Так как клик —
|
||||
// уверенное указание, делаем СРАЗУ почти полную (deadbeat) коррекцию: один клик
|
||||
// ставит маяк на опорную за одну перестройку, без многократного «тыкания».
|
||||
var clickOff, refined, prom, e, dLock: 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);
|
||||
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;
|
||||
|
||||
procedure TRadioController.SetBeaconLock(On_: Boolean);
|
||||
@@ -1099,61 +1124,118 @@ begin
|
||||
FBeaconLockOn := On_;
|
||||
if On_ then
|
||||
begin
|
||||
FBeaconSearchTicks := 0;
|
||||
FBeaconCorrCooldown := 0;
|
||||
FBeaconSaveCooldown := 0;
|
||||
FBeaconManualSeed := 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
|
||||
else
|
||||
FBeaconTrackHz := FBeaconRefHz;
|
||||
end;
|
||||
// При выключении FBeaconLockHz замораживаем (как калибровка LNB).
|
||||
end
|
||||
else
|
||||
PersistBeaconCalibration; // фиксируем накопленную коррекцию в LOError (см. метод)
|
||||
Changed(rfBeaconLock);
|
||||
end;
|
||||
|
||||
procedure TRadioController.ServiceBeaconLock;
|
||||
// Одна итерация контура (UI-таймер @10 Гц). Меряет пик маяка по display-FFT,
|
||||
// сглаживает, и (после ручного клика) тянет FBeaconLockHz так, чтобы маяк
|
||||
// оставался на опорной частоте → весь downlink стабилен. Без NCO/FLL.
|
||||
var pk, prom, measDisp, e: Double;
|
||||
// Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Меряет маяк по
|
||||
// display-FFT и (после ручного клика) трим LOError так, чтобы маяк стоял на
|
||||
// опорной частоте → весь downlink стабилен. Коррекция складывается в LOError
|
||||
// (живая калибровка LNB: видна в настройках, сохраняется).
|
||||
//
|
||||
// Устойчивость к соседним сигналам (как в SDR Console):
|
||||
// • при локе окно УЗКОЕ (±BEACON_TRACK_HZ) — далёкий сосед вне окна;
|
||||
// • slew-отбраковка: дрейф LNB медленный, резкий скачок центроида = чужой
|
||||
// сигнал, измерение игнорируем;
|
||||
// • MeasureBeaconFFT проверяет форму лепестка (узнаём маяк, не «громкий пик»).
|
||||
// Быстрая сходимость при большой ошибке:
|
||||
// • feed-forward (FBeaconTrackHz −= dLock) — окно едет за маяком сквозь ретюн,
|
||||
// лок не теряется; во время settle позицию не двигаем (FFT в транзиенте).
|
||||
var win, pk, prom, cand, e, dLock: Double;
|
||||
begin
|
||||
if not FBeaconLockOn then Exit;
|
||||
if FCurrentXvtr < 0 then Exit; // только в трансвертер-режиме
|
||||
// Окно поиска — вокруг ТЕКУЩЕЙ display-позиции маяка, пересчитанной в baseband
|
||||
// через актуальный FCenterFreq. Поэтому сдвиг водопада/центра окно не ломает.
|
||||
if not MeasureBeaconFFT(FBeaconTrackHz - FCenterFreq, pk, prom) then Exit;
|
||||
if FBeaconSaveCooldown > 0 then Dec(FBeaconSaveCooldown);
|
||||
// Окно — вокруг ТЕКУЩЕЙ display-позиции маяка, пересчитанной в baseband через
|
||||
// актуальный 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-частоте; стабильность важнее скорости).
|
||||
FBeaconTrackHz := FBeaconTrackHz + 0.12 * ((FCenterFreq + pk) - FBeaconTrackHz);
|
||||
FBeaconPromDB := FBeaconPromDB + 0.2 * (prom - FBeaconPromDB);
|
||||
// Slew-отбраковка (только при локе/узком окне): скачок больше дрейфа LNB =
|
||||
// чужой сигнал в окне → измерение игнорируем, остаёмся на маяке.
|
||||
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 (без мерцания).
|
||||
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.
|
||||
// Слежение за пиком (выше) идёт каждый тик — окно едет за маяком.
|
||||
// Коррекция: НЕ каждый тик (display-FFT отстаёт от ретюна → перелёт). После
|
||||
// коррекции ждём BEACON_SETTLE_TICKS. Только после ручного клика (manual seed).
|
||||
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;
|
||||
// e = видимое_смещение_маяка − опорное. d(display)/d(LOError) = −1, поэтому
|
||||
// LOError += e тянет маяк к опорной; feed-forward предсказывает новую
|
||||
// display-позицию (track −= dLock), чтобы окно ехало за маяком.
|
||||
e := FBeaconTrackHz - FBeaconRefHz;
|
||||
if Abs(e) > BEACON_DEADBAND_HZ then
|
||||
begin
|
||||
FBeaconLockHz := FBeaconLockHz + BEACON_GAIN * e;
|
||||
PushNetworkState; // ретюн LNB-IF: новый LO = XvtrTranslate(центр)
|
||||
dLock := BEACON_GAIN * e;
|
||||
ApplyBeaconLOError(dLock); // в LOError + ретюн + троттл-сохранение
|
||||
FBeaconTrackHz := FBeaconTrackHz - dLock;
|
||||
FBeaconCorrCooldown := BEACON_SETTLE_TICKS;
|
||||
end;
|
||||
end;
|
||||
Changed(rfBeaconLock);
|
||||
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;
|
||||
begin
|
||||
if not FBeaconLockOn then Exit(bsIdle);
|
||||
|
||||
Reference in New Issue
Block a user