feat: QO-100 decoder-based beacon lock (replace FFT lock)

Replace the display-FFT centroid beacon lock with a decoder-based loop.
The beacon decoder already tracks the carrier precisely via its NCO
(squaring-FFT acquisition + Costas offload); its residual ResidHz+CarFreqHz
is an exact measurement of the beacon's offset from the aim point. Feed that
into LOError of the active transverter so the LO retunes to compensate LNB
drift — the whole downlink stays put, calibration persists.

RadioController:
- Remove MeasureBeaconFFT and all display-FFT lock state/constants
  (lobe/search/track/slew/sym/shape/seed-gain/prom), fields
  FBeaconManualSeed/FBeaconPromDB/FBeaconDecOn, methods
  SetBeaconDecode/SetBeaconDecodeAtHz/BeaconDecodeEnabled.
- SetBeaconLock is now the master switch (lock = decoder); BeaconSeedAtHz
  aims the decoder; ServiceBeaconLock measures beacon freq from the decoder
  scope and trims LOError (gain 0.5, deadband 20 Hz, step clamp 400 Hz,
  settle 8 ticks). Feed-forward of the aim keeps the decoder locked through
  the retune (net baseband ~0). Lock/SNR now come from the decoder.

MainForm: single BEACON button — left-click toggles lock (+ opens the
constellation/bulletin scope, arms the spectrum click for aiming),
right-click shows/hides the scope. Spectrum click (armed or Shift) aims via
BeaconSeedAtHz. Simplified status field and markers.

Both targets build (lazbuild --ws=qt6 + build-ewsdrd.sh). Not yet verified
on air. Risk: BEACON_RESID_SIGN=+1.0 — flip to -1.0 if the loop runs away.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 19:51:01 +03:00
co-authored by Claude Opus 4.8
parent 59338b2d04
commit 6246b6bad4
2 changed files with 141 additions and 313 deletions
+27 -39
View File
@@ -135,7 +135,6 @@ type
FSpecDragX0: Integer; // X при нажатии
FSpecDragFreq: Double; // FCenterFreq при нажатии
FBeaconArming: Boolean; // ждём клик по маяку (после включения BEACON)
FBeaconDecArming: Boolean; // ждём клик: навести узкий фильтр декодера на маяк
FSpectrumDirty: Boolean; // таймер должен перерисовать спектр/водопад
// TX DUC-очередь (FDUCPending*) переехала в TRadioController (OnTXIQ).
FSMeterPeak: Double; // верхняя граница светлой зоны
@@ -3278,15 +3277,12 @@ function TMainForm.BeaconStatusText: string;
// Компактный статус QO-100 beacon lock для поля 7 статус-бара (Pluto). Courier 8pt,
// поле ~200px → держим ≤ ~24 символов.
begin
if FBeaconDecArming then Exit('DEC: click beacon');
if FController.BeaconDecodeEnabled and (FController.BeaconDecodeFreqHz > 0) then
Exit('DEC: shift-click=re-aim');
if not FController.BeaconLockEnabled then Exit('BCN: off');
if FBeaconArming then Exit('BCN: click beacon');
if FController.BeaconDecodeFreqHz <= 0 then Exit('BCN: click beacon');
case FController.BeaconState of
bsLock: Result := Format('BCN LOCK %.0fHz /%.0f',
bsLock: Result := Format('BCN LOCK %.0fHz /%.0fdB',
[FController.BeaconCorrectionHz, FController.BeaconSNRdB]);
bsSearch: Result := Format('BCN search /%.0f', [FController.BeaconSNRdB]);
bsSearch: Result := Format('BCN sync /%.0fdB', [FController.BeaconSNRdB]);
else Result := 'BCN --';
end;
end;
@@ -3989,34 +3985,34 @@ begin
end;
procedure TMainForm.BtnBeaconClick(Sender: TObject);
// ЛКМ по BEACON: вкл/выкл лок маяка (= декодер + трим LOError). При включении
// «вооружаем» клик (следующий ЛКМ по спектру наведёт декодер на маяк) и открываем
// окно констелляции/бюллетеня. Декодер ведёт несущую, контур стекает дрейф в LOError.
begin
FController.SetBeaconLock(not FController.BeaconLockEnabled);
// При включении — «вооружаем» клик: следующий ЛКМ по спектру укажет маяк
// (надёжнее автопоиска в забитой полосе). Авто-сид работает до клика.
FBeaconArming := FController.BeaconLockEnabled;
if FController.BeaconLockEnabled then
begin
if FBeaconScopeForm = nil then
FBeaconScopeForm := TBeaconScopeForm.CreateWith(Self, FController);
FBeaconScopeForm.ApplyTheme(DarkTheme);
FBeaconScopeForm.Show;
end;
UpdateBeaconButton;
end;
procedure TMainForm.BtnBeaconMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
// ПКМ по BEACON: включает фронтенд-декодер маяка (BPSK 400 бод) и открывает окно
// констелляции для диагностики демодуляции (Milestone 1 — до подключения FEC).
// ПКМ по BEACON: показать/скрыть окно констелляции+бюллетеня (диагностика декодера).
// Лок не трогает — он на ЛКМ.
begin
if Button <> mbRight then Exit;
FController.SetBeaconDecode(not FController.BeaconDecodeEnabled);
if FController.BeaconDecodeEnabled then
begin
// «Вооружаем» наведение: следующий ЛКМ по спектру поставит узкий фильтр
// декодера на маяк. Потом перенавести — Shift+ЛКМ по спектру.
FBeaconDecArming := True;
if FBeaconScopeForm = nil then
FBeaconScopeForm := TBeaconScopeForm.CreateWith(Self, FController);
FBeaconScopeForm.ApplyTheme(DarkTheme);
FBeaconScopeForm.Show;
end
else
FBeaconDecArming := False;
UpdateBeaconButton;
if not FController.BeaconLockEnabled then Exit;
if FBeaconScopeForm = nil then
FBeaconScopeForm := TBeaconScopeForm.CreateWith(Self, FController);
FBeaconScopeForm.ApplyTheme(DarkTheme);
if FBeaconScopeForm.Visible then FBeaconScopeForm.Hide
else FBeaconScopeForm.Show;
end;
procedure TMainForm.UpdateBeaconButton;
@@ -4033,7 +4029,7 @@ begin
FController.BeaconTrackedFreqHz);
// Узкий фильтр-маркер декодера (±450 Гц ≈ полоса BPSK-маяка).
FSpecView.SetBeaconDecMarker(
FController.BeaconDecodeEnabled and (FController.BeaconDecodeFreqHz > 0),
FController.BeaconLockEnabled and (FController.BeaconDecodeFreqHz > 0),
FController.BeaconDecodeFreqHz, 450.0);
end;
UpdateStatusField7; // моментальный апдейт статус-бара (не ждём MeterTimer)
@@ -4223,19 +4219,11 @@ begin
if Button = mbLeft then
begin
// Наведение узкого фильтра ДЕКОДЕРА на маяк: после ПКМ BEACON (вооружено)
// либо Shift+ЛКМ в любой момент при включённом декодере. Не трогает VFO.
if FController.BeaconDecodeEnabled and (PbSpectrum.Width > 0) and
(FBeaconDecArming or (ssShift in Shift)) then
begin
FController.SetBeaconDecodeAtHz(
PixelToFreq(X, PbSpectrum.Width, FController.FCenterFreq, FController.FSpanHz));
FBeaconDecArming := False;
UpdateBeaconButton;
Exit;
end;
// Наведение beacon-lock: ЛКМ указывает на маяк (вместо перестройки VFO).
if FBeaconArming and (PbSpectrum.Width > 0) then
// Наведение маяка: ЛКМ указывает на маяк (после включения BEACON «вооружено»,
// либо Shift+ЛКМ в любой момент при включённом локе для перенаведения). Декодер
// наводится на эту частоту, дальше сам ведёт несущую. Не трогает VFO.
if FController.BeaconLockEnabled and (PbSpectrum.Width > 0) and
(FBeaconArming or (ssShift in Shift)) then
begin
FController.BeaconSeedAtHz(
PixelToFreq(X, PbSpectrum.Width, FController.FCenterFreq, FController.FSpanHz));
+114 -274
View File
@@ -182,25 +182,24 @@ type
FCurrentBand: Integer;
FCurrentXvtr: Integer;
// ---- QO-100 beacon lock (центральный BPSK-маяк 10489.750, по display-FFT) ----
FBeaconLockOn: Boolean; // лок включён пользователем
// ---- QO-100 beacon lock (декодер маяка 10489.750 = эталон частоты) ----
// Один концепт: лок = декодер + трим LOError. Декодер точно ведёт несущую,
// его измеренный дрейф стекаем в LOError → весь даунлинк стабилен.
FBeaconLockOn: Boolean; // лок (=декодер) включён пользователем
FBeaconLockHz: Double; // коррекция, накопленная за ТЕКУЩУЮ сессию лока
// (только индикация на кнопке); сама поправка
// живёт в LOError активного трансвертера
FBeaconRefHz: Double; // опорная частота маяка (Hz, downlink), деф. 10489.750e6
FBeaconManualSeed: Boolean; // пользователь указал маяк кликом → не автопоиск
// FFT-трекинг маяка (робастный, по display-FFT):
FBeaconTrackHz: Double; // отслеживаемая АБСОЛЮТНАЯ display-частота маяка
// (не baseband-offset — иначе сдвиг центра/водопада ломает окно)
FBeaconPromDB: Double; // «высота» пика над шумом окна (лок-метрика)
FBeaconLocked: Boolean; // пик уверенно виден
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка пайплайна)
FBeaconTrackHz: Double; // измеренная АБСОЛЮТНАЯ display-частота маяка
// (наведение + остаток декодера) — для оранж. маркера
FBeaconSNRdB: Double; // SNR из последнего снимка декодера (индикация лока)
FBeaconLocked: Boolean; // декодер солидно залочен (гистерезис по SNR)
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка ретюна LO)
FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков)
// ---- QO-100 beacon DECODE (BPSK 400 бод, AO-40) — фронтенд+констелляция ----
// ---- декодер маяка (BPSK 400 бод, AO-40) — фронтенд+констелляция+бюллетень ----
FBeaconDec: TBeaconDecoder; // владеет; тап IQ в WDSPEngine
FBeaconDecOn: Boolean; // декодер включён пользователем
FBeaconDecHz: Double; // АБСОЛЮТНАЯ display-частота наведения декодера
// (узкий фильтр, ставится кликом); 0 = центр/DC
// (узкий фильтр, ставится кликом); 0 = ещё не наведён
FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True
// ---- Аудио / TX ----
@@ -358,12 +357,6 @@ type
// uplink-LO не подвержен дрейфу LNB).
function XvtrTranslateTX(VisibleHz: Double): Double;
function ActiveTXFreqHz: Double;
// Измерение маяка по display-FFT в окне ±WinHz вокруг ExpectOff (baseband):
// центроид лепестка → уточнённое смещение, «высота» пика над средним окна
// (лок-метрика). False, если лепесток не похож на маяк (узкий/широкий/
// несимметричный сосед) или вне span — тогда трек держим на месте.
function MeasureBeaconFFT(ExpectOff, WinHz: Double;
out PeakOff, PromDB: Double): Boolean;
// Складывает коррекцию маяка dLock в LOError активного трансвертера (живая
// калибровка LNB), ретюнит радио и троттлингом сохраняет на диск.
procedure ApplyBeaconLOError(dLock: Double);
@@ -481,22 +474,18 @@ type
procedure SetSampleRate(Hz: Integer);
// QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку.
procedure SetBeaconLock(On_: Boolean);
// QO-100 beacon lock (= декодер + трим LOError; декодер = эталон частоты).
procedure SetBeaconLock(On_: Boolean); // вкл/выкл лок (запускает декодер)
procedure ServiceBeaconLock; // одна итерация контура (MeterTimer @10 Гц)
procedure SetBeaconRefHz(Hz: Double); // опорная частота маяка (downlink)
procedure BeaconSeedAtHz(DisplayHz: Double); // пользователь указал маяк кликом
procedure BeaconSeedAtHz(DisplayHz: Double); // клик по маяку → наведение декодера
function BeaconState: TBeaconState;
function BeaconCorrectionHz: Double; // коррекция за сессию лока (индикация; в LOError)
function BeaconSNRdB: Double;
function BeaconLockEnabled: Boolean;
function BeaconRefHz: Double; // опорная частота маяка (для маркера)
function BeaconTrackedFreqHz: Double;// отслеживаемая display-частота маяка (центроид)
// QO-100 beacon DECODE (BPSK 400 бод, AO-40) — фронтенд + констелляция.
procedure SetBeaconDecode(On_: Boolean);
procedure SetBeaconDecodeAtHz(DisplayHz: Double); // навести узкий фильтр декодера
function BeaconDecodeEnabled: Boolean;
function BeaconDecodeFreqHz: Double; // частота наведения (для маркера)
function BeaconTrackedFreqHz: Double;// измеренная display-частота маяка (наведение+остаток)
function BeaconDecodeFreqHz: Double; // частота наведения (для узкого маркера)
procedure SetBeaconDecodeInvert(On_: Boolean); // зеркальный baseband вкл/выкл
function BeaconDecodeInvert: Boolean;
function GetBeaconScope(out S: TBeaconScope): Boolean;
@@ -607,14 +596,13 @@ begin
// буфер вдвое сокращает построение downsampler в OpenChannel RXA.
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
// QO-100 beacon lock: измерение по display-FFT (см. ServiceBeaconLock).
// QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock).
FBeaconRefHz := 10489750000.0; // центральный BPSK-маяк NB-транспондера (downlink)
FBeaconLockHz := 0.0;
FBeaconLockOn := False;
// QO-100 beacon DECODE: фронтенд BPSK 400 бод (тап IQ в движке).
// Декодер маяка: фронтенд BPSK 400 бод (тап IQ в движке).
FBeaconDec := TBeaconDecoder.Create;
FBeaconDecOn := False;
// Тракт QO-100 (LNB+трансвертер) зеркалит спектр: проверено приборно (symMag ×4
// и prom 500-800 при invert=ON против шума при OFF, на наведении точно на маяк).
// Декодеру нужно сопряжение, чтобы наведённый offset доставлял маяк к DC.
@@ -1051,111 +1039,26 @@ end;
// ---------------------------------------------------------------------------
const
// --- контур коррекции ---
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-пайплайна
// --- контур коррекции (измерение частоты — от ДЕКОДЕРА, не от display-FFT) ---
// Декодер точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload):
// суммарный остаток ResidHz+CarFreqHz = текущее смещение маяка от точки наведения.
// Это смещение медленно стекаем в LOError активного XVTR → ретюн LO компенсирует
// дрейф LNB, весь даунлинк стоит. Внутренний NCO декодера держит маяк
// декодированным во время плавного ретюна (наведение едет feed-forward за маяком).
BEACON_GAIN = 0.5; // гейн контура: <1 — запас от перелёта (декодерная
// оценка точная, но ретюн LO имеет латентность пайплайна)
BEACON_DEADBAND_HZ = 20.0; // внутри — коррекции стоп (декодер точнее FFT → дедбэнд уже)
BEACON_MAX_STEP_HZ = 400.0; // клам один шаг коррекции (защита от выброса оценки)
BEACON_SETTLE_TICKS = 8; // пауза (~0.8 с @10 Гц) после коррекции — осадка ретюна LO
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; // |LR|/(L+R) выше → односторонний сосед, не маяк
BEACON_SHAPE_MAX_HZPP = 600.0; // форму лепестка проверяем лишь если бин ≤ этого
function TRadioController.MeasureBeaconFFT(ExpectOff, WinHz: Double;
out PeakOff, PromDB: Double): Boolean;
// Маяк по 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, 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;
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 - 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;
Inc(cnt);
if pix[i] > bestv then begin bestv := pix[i]; best := i; end;
end;
if (best < 0) or (cnt < 4) then Exit;
// Проход B — центроид лепестка + протяжённость + шумовой пол (бины вне лепестка).
thr := bestv - BEACON_LOBE_DB;
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);
if off < PeakOff then wLeft := wLeft + p else wRight := wRight + p;
end;
if (wLeft + wRight) > 0 then
if Abs(wLeft - wRight) / (wLeft + wRight) > BEACON_SYM_MAX then Exit;
Result := True;
end;
// --- знак отображения остатка декодера ---
// invert=True (QO-100/LNB) выравнивает частотную ось декодера с дисплеем, поэтому
// display_маяка = наведение + (ResidHz+CarFreqHz). Если на эфире контур убегает —
// ось перевёрнута, флипнуть на -1.0 (см. лог: e и дрейф должны иметь один знак).
BEACON_RESID_SIGN = 1.0;
// --- метрика лока (берём из снимка декодера: CarrierLock + SNR) ---
BEACON_LOCK_SNR_DB = 3.0; // солидный лок: декодер несёт несущую и SNR выше этого
BEACON_UNLOCK_SNR_DB = 0.0; // гистерезис: лок гаснет ниже этого
procedure TRadioController.SetBeaconRefHz(Hz: Double);
begin
@@ -1163,37 +1066,26 @@ begin
end;
procedure TRadioController.BeaconSeedAtHz(DisplayHz: Double);
// Пользователь кликнул по маяку → ручной режим (контур ведёт ИМЕННО этот пик,
// без автопоиска, который в забитой полосе хватает соседей). Так как клик —
// уверенное указание, делаем СРАЗУ почти полную (deadbeat) коррекцию: один клик
// ставит маяк на опорную за одну перестройку, без многократного «тыкания».
var clickOff, refined, prom, e, dLock: Double;
// Пользователь кликнул по маяку → наводим узкий тракт декодера на эту частоту и
// переинициализируем петли (RunAcquisition заново оценит несущую). Дальше декодер
// сам ведёт маяк своим NCO, а ServiceBeaconLock стекает измеренный дрейф в LOError.
begin
if not FBeaconLockOn then SetBeaconLock(True);
FBeaconManualSeed := True;
clickOff := DisplayHz - FCenterFreq;
if MeasureBeaconFFT(clickOff, BEACON_SEARCH_HZ, refined, prom) then
FBeaconDecHz := DisplayHz;
FBeaconTrackHz := DisplayHz;
if Assigned(FBeaconDec) 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;
FBeaconDec.SetOffsetHz(DisplayHz - FCenterFreq);
FBeaconDec.Reseed; // одноразовый грубый захват частоты заново
end;
FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать декодеру захватиться до 1-й коррекции
Changed(rfBeaconLock);
end;
procedure TRadioController.SetBeaconLock(On_: Boolean);
var refined, prom: Double;
// Один концепт: лок = декодер + трим LOError. Включение запускает декодер маяка;
// пользователь кликает по маяку (BeaconSeedAtHz) для наведения. Когда декодер
// солидно залочен, ServiceBeaconLock стекает его измеренный дрейф в LOError.
begin
if On_ = FBeaconLockOn then Exit;
FBeaconLockOn := On_;
@@ -1201,101 +1093,87 @@ begin
begin
FBeaconCorrCooldown := 0;
FBeaconSaveCooldown := 0;
FBeaconManualSeed := False; // авто до первого клика пользователя
FBeaconLocked := False;
FBeaconPromDB := 0.0;
FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError уже несёт калибровку)
// Начальная область — у опорной частоты (уточняем пик, если виден).
if MeasureBeaconFFT(FBeaconRefHz - FCenterFreq, BEACON_SEARCH_HZ, refined, prom) then
FBeaconTrackHz := FCenterFreq + refined
else
FBeaconTrackHz := FBeaconRefHz;
FBeaconSNRdB := -99.0;
FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError несёт калибровку)
FBeaconDecHz := 0.0; // ещё не наведён — ждём клик по маяку
FBeaconTrackHz := FBeaconRefHz;
if Assigned(FBeaconDec) then
begin
FBeaconDec.SetOffsetHz(0);
FBeaconDec.SetEnabled(True); // ResetDSP внутри
end;
end
else
PersistBeaconCalibration; // фиксируем накопленную коррекцию в LOError (см. метод)
begin
if Assigned(FBeaconDec) then FBeaconDec.SetEnabled(False);
FBeaconDecHz := 0.0;
PersistBeaconCalibration; // финальный flush LOError на диск
end;
Changed(rfBeaconLock);
end;
procedure TRadioController.ServiceBeaconLock;
// Одна итерация контура (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;
// Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Эталон частоты — ДЕКОДЕР:
// он точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload), и его
// суммарный остаток ResidHz+CarFreqHz = текущее смещение маяка от точки наведения.
// Измеренную АБСОЛЮТНУЮ частоту маяка (наведение + остаток) подтягиваем к опорной,
// стекая ошибку в LOError активного XVTR → ретюн LO компенсирует дрейф LNB, весь
// даунлинк стоит. Наведение декодера едет feed-forward за маяком (FBeaconDecHz),
// так что внутренний NCO держит маяк декодированным сквозь плавный ретюн (нетто
// baseband-смещение ≈ 0: маяк сдвинулся на −dLock железом, наведение тоже на −dLock).
var
S: TBeaconScope;
residHz, measured, e, dLock: Double;
begin
// Beacon-декодер: ЦЕЛЬ — маяк на DC IQ-тапа (= центр RX-спектра, тот же сырой
// DDC-IQ). По умолчанию offset=0: пользователь ставит маяк в центр спектра
// (CTUN/перестройка), squaring-FFT acquisition доводит остаток. ТОЛЬКО когда
// beacon-lock реально захвачен (зелёный) — доверяем его оценке и сносим
// FBeaconTrackHzFCenterFreq (авто-центровка). Развязано от сырого FBeaconTrackHz,
// т.к. при выключенном/несошедшемся локе он недостоверен.
// Декодер НЕ зависит от beacon-lock: lock физически ретюнит железо
// (ApplyBeaconLOError → PushNetworkState), что рвёт IQ-поток и сбивает петли
// декодера (Costas/FLL/Gardner), которым нужны секунды непрерывного сигнала.
// У декодера своя петля несущей — она точнее, чем display-FFT-lock. Поэтому
// offset = только явный клик-аим, иначе DC. Авто-offset от FBeaconTrackHz убран.
if FBeaconDecOn and Assigned(FBeaconDec) then
begin
if FBeaconDecHz > 0 then
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq) // наведённый узкий фильтр
else
FBeaconDec.SetOffsetHz(0); // иначе — центр/DC
end;
if not FBeaconLockOn then Exit;
if FCurrentXvtr < 0 then Exit; // только в трансвертер-режиме
if not Assigned(FBeaconDec) then Exit;
// Держим наведение декодера через АКТУАЛЬНЫЙ FCenterFreq (панорама/CTUN не ломают
// снос). До клика (FBeaconDecHz=0) — DC; acquisition попробует поймать у центра.
if FBeaconDecHz > 0 then
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq)
else
FBeaconDec.SetOffsetHz(0);
if FCurrentXvtr < 0 then Exit; // трим LOError только в XVTR-режиме
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
FBeaconDec.GetScope(S);
FBeaconSNRdB := FBeaconSNRdB + 0.2 * (S.SNRdB - FBeaconSNRdB);
// Солидный лок: декодер несёт несущую И SNR выше порога (гистерезис против мерцания).
if S.CarrierLock and (FBeaconSNRdB >= BEACON_LOCK_SNR_DB) then FBeaconLocked := True
else if FBeaconSNRdB < BEACON_UNLOCK_SNR_DB then FBeaconLocked := False;
// Остаток несущей декодера (Гц, относительно наведения). invert выравнивает ось
// декодера с дисплеем → display_маяка = наведение + остаток (знак BEACON_RESID_SIGN).
residHz := BEACON_RESID_SIGN * (S.ResidHz + S.CarFreqHz);
if FBeaconDecHz > 0 then
begin
Changed(rfBeaconLock); // лепесток не виден/не маяк — держим позицию и лок
Exit;
end;
cand := FCenterFreq + pk;
measured := FBeaconDecHz + residHz; // абсолютная display-частота маяка
FBeaconTrackHz := measured; // для оранжевого маркера
end
else
measured := FBeaconTrackHz;
// 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 отстаёт от ретюна → перелёт). После
// коррекции ждём BEACON_SETTLE_TICKS. Только после ручного клика (manual seed).
// Коррекция: не каждый тик (ретюн LO имеет латентность пайплайна). После коррекции
// ждём BEACON_SETTLE_TICKS. Только при солидном локе и после наведения кликом.
if FBeaconCorrCooldown > 0 then
Dec(FBeaconCorrCooldown)
else if FBeaconLocked and FBeaconManualSeed then
else if FBeaconLocked and (FBeaconDecHz > 0) then
begin
// e = видимое_смещение_маяка − опорное. d(display)/d(LOError) = 1, поэтому
// LOError += e тянет маяк к опорной; feed-forward предсказывает новую
// display-позицию (track = dLock), чтобы окно ехало за маяком.
e := FBeaconTrackHz - FBeaconRefHz;
e := measured - FBeaconRefHz;
if Abs(e) > BEACON_DEADBAND_HZ then
begin
dLock := BEACON_GAIN * e;
ApplyBeaconLOError(dLock); // в LOError + ретюн + троттл-сохранение
FBeaconTrackHz := FBeaconTrackHz - dLock;
if dLock > BEACON_MAX_STEP_HZ then dLock := BEACON_MAX_STEP_HZ;
if dLock < -BEACON_MAX_STEP_HZ then dLock := -BEACON_MAX_STEP_HZ;
ApplyBeaconLOError(dLock); // в LOError + ретюн + троттл-сохранение
// Маяк уедет на dLock (display); ведём наведение туда же feed-forward, чтобы
// baseband декодера не дёрнулся и лок не потерялся.
FBeaconDecHz := measured - dLock;
FBeaconTrackHz := FBeaconDecHz;
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq);
FBeaconCorrCooldown := BEACON_SETTLE_TICKS;
end;
end;
@@ -1343,7 +1221,7 @@ end;
function TRadioController.BeaconSNRdB: Double;
begin
Result := FBeaconPromDB; // «высота» пика над шумом окна (dB)
Result := FBeaconSNRdB; // SNR последнего снимка декодера (dB)
end;
function TRadioController.BeaconLockEnabled: Boolean;
@@ -1361,44 +1239,6 @@ begin
Result := FBeaconTrackHz;
end;
procedure TRadioController.SetBeaconDecode(On_: Boolean);
// Включает фронтенд-декодер маяка. Цель — маяк на DC (центр RX-спектра): стартуем
// с offset=0 (или явного клик-аима), дальше частоту держит петля несущей декодера.
// От beacon-lock декодер НЕ зависит (см. ServiceBeaconLock). Пользователь наводит
// клик на маяк либо центрирует его в спектре, acquisition/Costas доводят.
begin
FBeaconDecOn := On_;
if not Assigned(FBeaconDec) then Exit;
if not On_ then FBeaconDecHz := 0; // сброс наведения при выключении
if On_ then
begin
if FBeaconDecHz > 0 then
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq)
else
FBeaconDec.SetOffsetHz(0);
end;
FBeaconDec.SetEnabled(On_);
end;
procedure TRadioController.SetBeaconDecodeAtHz(DisplayHz: Double);
// Пользователь навёл узкий фильтр на маяк (клик по спектру). Запоминаем абсолютную
// частоту, сносим её к DC и переинициализируем петли (одноразовая грубая оценка
// заново). Снос держится в ServiceBeaconLock при перестройке/панораме.
begin
FBeaconDecHz := DisplayHz;
if Assigned(FBeaconDec) then
begin
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq);
FBeaconDec.Reseed;
end;
Changed(rfBeaconLock);
end;
function TRadioController.BeaconDecodeEnabled: Boolean;
begin
Result := FBeaconDecOn;
end;
function TRadioController.BeaconDecodeFreqHz: Double;
begin
Result := FBeaconDecHz;