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 при нажатии FSpecDragX0: Integer; // X при нажатии
FSpecDragFreq: Double; // FCenterFreq при нажатии FSpecDragFreq: Double; // FCenterFreq при нажатии
FBeaconArming: Boolean; // ждём клик по маяку (после включения BEACON) FBeaconArming: Boolean; // ждём клик по маяку (после включения BEACON)
FBeaconDecArming: Boolean; // ждём клик: навести узкий фильтр декодера на маяк
FSpectrumDirty: Boolean; // таймер должен перерисовать спектр/водопад FSpectrumDirty: Boolean; // таймер должен перерисовать спектр/водопад
// TX DUC-очередь (FDUCPending*) переехала в TRadioController (OnTXIQ). // TX DUC-очередь (FDUCPending*) переехала в TRadioController (OnTXIQ).
FSMeterPeak: Double; // верхняя граница светлой зоны FSMeterPeak: Double; // верхняя граница светлой зоны
@@ -3278,15 +3277,12 @@ function TMainForm.BeaconStatusText: string;
// Компактный статус QO-100 beacon lock для поля 7 статус-бара (Pluto). Courier 8pt, // Компактный статус QO-100 beacon lock для поля 7 статус-бара (Pluto). Courier 8pt,
// поле ~200px → держим ≤ ~24 символов. // поле ~200px → держим ≤ ~24 символов.
begin begin
if FBeaconDecArming then Exit('DEC: click beacon');
if FController.BeaconDecodeEnabled and (FController.BeaconDecodeFreqHz > 0) then
Exit('DEC: shift-click=re-aim');
if not FController.BeaconLockEnabled then Exit('BCN: off'); if not FController.BeaconLockEnabled then Exit('BCN: off');
if FBeaconArming then Exit('BCN: click beacon'); if FController.BeaconDecodeFreqHz <= 0 then Exit('BCN: click beacon');
case FController.BeaconState of case FController.BeaconState of
bsLock: Result := Format('BCN LOCK %.0fHz /%.0f', bsLock: Result := Format('BCN LOCK %.0fHz /%.0fdB',
[FController.BeaconCorrectionHz, FController.BeaconSNRdB]); [FController.BeaconCorrectionHz, FController.BeaconSNRdB]);
bsSearch: Result := Format('BCN search /%.0f', [FController.BeaconSNRdB]); bsSearch: Result := Format('BCN sync /%.0fdB', [FController.BeaconSNRdB]);
else Result := 'BCN --'; else Result := 'BCN --';
end; end;
end; end;
@@ -3989,34 +3985,34 @@ begin
end; end;
procedure TMainForm.BtnBeaconClick(Sender: TObject); procedure TMainForm.BtnBeaconClick(Sender: TObject);
// ЛКМ по BEACON: вкл/выкл лок маяка (= декодер + трим LOError). При включении
// «вооружаем» клик (следующий ЛКМ по спектру наведёт декодер на маяк) и открываем
// окно констелляции/бюллетеня. Декодер ведёт несущую, контур стекает дрейф в LOError.
begin begin
FController.SetBeaconLock(not FController.BeaconLockEnabled); FController.SetBeaconLock(not FController.BeaconLockEnabled);
// При включении — «вооружаем» клик: следующий ЛКМ по спектру укажет маяк
// (надёжнее автопоиска в забитой полосе). Авто-сид работает до клика.
FBeaconArming := 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; UpdateBeaconButton;
end; end;
procedure TMainForm.BtnBeaconMouseDown(Sender: TObject; Button: TMouseButton; procedure TMainForm.BtnBeaconMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer); Shift: TShiftState; X, Y: Integer);
// ПКМ по BEACON: включает фронтенд-декодер маяка (BPSK 400 бод) и открывает окно // ПКМ по BEACON: показать/скрыть окно констелляции+бюллетеня (диагностика декодера).
// констелляции для диагностики демодуляции (Milestone 1 — до подключения FEC). // Лок не трогает — он на ЛКМ.
begin begin
if Button <> mbRight then Exit; if Button <> mbRight then Exit;
FController.SetBeaconDecode(not FController.BeaconDecodeEnabled); if not FController.BeaconLockEnabled then Exit;
if FController.BeaconDecodeEnabled then if FBeaconScopeForm = nil then
begin FBeaconScopeForm := TBeaconScopeForm.CreateWith(Self, FController);
// «Вооружаем» наведение: следующий ЛКМ по спектру поставит узкий фильтр FBeaconScopeForm.ApplyTheme(DarkTheme);
// декодера на маяк. Потом перенавести — Shift+ЛКМ по спектру. if FBeaconScopeForm.Visible then FBeaconScopeForm.Hide
FBeaconDecArming := True; else FBeaconScopeForm.Show;
if FBeaconScopeForm = nil then
FBeaconScopeForm := TBeaconScopeForm.CreateWith(Self, FController);
FBeaconScopeForm.ApplyTheme(DarkTheme);
FBeaconScopeForm.Show;
end
else
FBeaconDecArming := False;
UpdateBeaconButton;
end; end;
procedure TMainForm.UpdateBeaconButton; procedure TMainForm.UpdateBeaconButton;
@@ -4033,7 +4029,7 @@ begin
FController.BeaconTrackedFreqHz); FController.BeaconTrackedFreqHz);
// Узкий фильтр-маркер декодера (±450 Гц ≈ полоса BPSK-маяка). // Узкий фильтр-маркер декодера (±450 Гц ≈ полоса BPSK-маяка).
FSpecView.SetBeaconDecMarker( FSpecView.SetBeaconDecMarker(
FController.BeaconDecodeEnabled and (FController.BeaconDecodeFreqHz > 0), FController.BeaconLockEnabled and (FController.BeaconDecodeFreqHz > 0),
FController.BeaconDecodeFreqHz, 450.0); FController.BeaconDecodeFreqHz, 450.0);
end; end;
UpdateStatusField7; // моментальный апдейт статус-бара (не ждём MeterTimer) UpdateStatusField7; // моментальный апдейт статус-бара (не ждём MeterTimer)
@@ -4223,19 +4219,11 @@ begin
if Button = mbLeft then if Button = mbLeft then
begin begin
// Наведение узкого фильтра ДЕКОДЕРА на маяк: после ПКМ BEACON (вооружено) // Наведение маяка: ЛКМ указывает на маяк (после включения BEACON «вооружено»,
// либо Shift+ЛКМ в любой момент при включённом декодере. Не трогает VFO. // либо Shift+ЛКМ в любой момент при включённом локе для перенаведения). Декодер
if FController.BeaconDecodeEnabled and (PbSpectrum.Width > 0) and // наводится на эту частоту, дальше сам ведёт несущую. Не трогает VFO.
(FBeaconDecArming or (ssShift in Shift)) then if FController.BeaconLockEnabled and (PbSpectrum.Width > 0) and
begin (FBeaconArming or (ssShift in Shift)) then
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
begin begin
FController.BeaconSeedAtHz( FController.BeaconSeedAtHz(
PixelToFreq(X, PbSpectrum.Width, FController.FCenterFreq, FController.FSpanHz)); PixelToFreq(X, PbSpectrum.Width, FController.FCenterFreq, FController.FSpanHz));
+114 -274
View File
@@ -182,25 +182,24 @@ type
FCurrentBand: Integer; FCurrentBand: Integer;
FCurrentXvtr: Integer; FCurrentXvtr: Integer;
// ---- QO-100 beacon lock (центральный BPSK-маяк 10489.750, по display-FFT) ---- // ---- QO-100 beacon lock (декодер маяка 10489.750 = эталон частоты) ----
FBeaconLockOn: Boolean; // лок включён пользователем // Один концепт: лок = декодер + трим LOError. Декодер точно ведёт несущую,
// его измеренный дрейф стекаем в LOError → весь даунлинк стабилен.
FBeaconLockOn: Boolean; // лок (=декодер) включён пользователем
FBeaconLockHz: Double; // коррекция, накопленная за ТЕКУЩУЮ сессию лока FBeaconLockHz: Double; // коррекция, накопленная за ТЕКУЩУЮ сессию лока
// (только индикация на кнопке); сама поправка // (только индикация на кнопке); сама поправка
// живёт в LOError активного трансвертера // живёт в LOError активного трансвертера
FBeaconRefHz: Double; // опорная частота маяка (Hz, downlink), деф. 10489.750e6 FBeaconRefHz: Double; // опорная частота маяка (Hz, downlink), деф. 10489.750e6
FBeaconManualSeed: Boolean; // пользователь указал маяк кликом → не автопоиск FBeaconTrackHz: Double; // измеренная АБСОЛЮТНАЯ display-частота маяка
// FFT-трекинг маяка (робастный, по display-FFT): // (наведение + остаток декодера) — для оранж. маркера
FBeaconTrackHz: Double; // отслеживаемая АБСОЛЮТНАЯ display-частота маяка FBeaconSNRdB: Double; // SNR из последнего снимка декодера (индикация лока)
// (не baseband-offset — иначе сдвиг центра/водопада ломает окно) FBeaconLocked: Boolean; // декодер солидно залочен (гистерезис по SNR)
FBeaconPromDB: Double; // «высота» пика над шумом окна (лок-метрика) FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка ретюна LO)
FBeaconLocked: Boolean; // пик уверенно виден
FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка пайплайна)
FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков) FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков)
// ---- QO-100 beacon DECODE (BPSK 400 бод, AO-40) — фронтенд+констелляция ---- // ---- декодер маяка (BPSK 400 бод, AO-40) — фронтенд+констелляция+бюллетень ----
FBeaconDec: TBeaconDecoder; // владеет; тап IQ в WDSPEngine FBeaconDec: TBeaconDecoder; // владеет; тап IQ в WDSPEngine
FBeaconDecOn: Boolean; // декодер включён пользователем
FBeaconDecHz: Double; // АБСОЛЮТНАЯ display-частота наведения декодера FBeaconDecHz: Double; // АБСОЛЮТНАЯ display-частота наведения декодера
// (узкий фильтр, ставится кликом); 0 = центр/DC // (узкий фильтр, ставится кликом); 0 = ещё не наведён
FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True
// ---- Аудио / TX ---- // ---- Аудио / TX ----
@@ -358,12 +357,6 @@ type
// uplink-LO не подвержен дрейфу LNB). // uplink-LO не подвержен дрейфу LNB).
function XvtrTranslateTX(VisibleHz: Double): Double; function XvtrTranslateTX(VisibleHz: Double): Double;
function ActiveTXFreqHz: Double; function ActiveTXFreqHz: Double;
// Измерение маяка по display-FFT в окне ±WinHz вокруг ExpectOff (baseband):
// центроид лепестка → уточнённое смещение, «высота» пика над средним окна
// (лок-метрика). False, если лепесток не похож на маяк (узкий/широкий/
// несимметричный сосед) или вне span — тогда трек держим на месте.
function MeasureBeaconFFT(ExpectOff, WinHz: Double;
out PeakOff, PromDB: Double): Boolean;
// Складывает коррекцию маяка dLock в LOError активного трансвертера (живая // Складывает коррекцию маяка dLock в LOError активного трансвертера (живая
// калибровка LNB), ретюнит радио и троттлингом сохраняет на диск. // калибровка LNB), ретюнит радио и троттлингом сохраняет на диск.
procedure ApplyBeaconLOError(dLock: Double); procedure ApplyBeaconLOError(dLock: Double);
@@ -481,22 +474,18 @@ type
procedure SetSampleRate(Hz: Integer); procedure SetSampleRate(Hz: Integer);
// QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку. // QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку.
procedure SetBeaconLock(On_: Boolean); // QO-100 beacon lock (= декодер + трим LOError; декодер = эталон частоты).
procedure SetBeaconLock(On_: Boolean); // вкл/выкл лок (запускает декодер)
procedure ServiceBeaconLock; // одна итерация контура (MeterTimer @10 Гц) 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; // коррекция за сессию лока (индикация; в LOError) 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;// отслеживаемая display-частота маяка (центроид) function BeaconTrackedFreqHz: Double;// измеренная display-частота маяка (наведение+остаток)
function BeaconDecodeFreqHz: Double; // частота наведения (для узкого маркера)
// QO-100 beacon DECODE (BPSK 400 бод, AO-40) — фронтенд + констелляция.
procedure SetBeaconDecode(On_: Boolean);
procedure SetBeaconDecodeAtHz(DisplayHz: Double); // навести узкий фильтр декодера
function BeaconDecodeEnabled: Boolean;
function BeaconDecodeFreqHz: Double; // частота наведения (для маркера)
procedure SetBeaconDecodeInvert(On_: Boolean); // зеркальный baseband вкл/выкл procedure SetBeaconDecodeInvert(On_: Boolean); // зеркальный baseband вкл/выкл
function BeaconDecodeInvert: Boolean; function BeaconDecodeInvert: Boolean;
function GetBeaconScope(out S: TBeaconScope): Boolean; function GetBeaconScope(out S: TBeaconScope): Boolean;
@@ -607,14 +596,13 @@ begin
// буфер вдвое сокращает построение downsampler в OpenChannel RXA. // буфер вдвое сокращает построение downsampler в OpenChannel RXA.
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512); FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
// QO-100 beacon lock: измерение по display-FFT (см. ServiceBeaconLock). // QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock).
FBeaconRefHz := 10489750000.0; // центральный BPSK-маяк NB-транспондера (downlink) FBeaconRefHz := 10489750000.0; // центральный BPSK-маяк NB-транспондера (downlink)
FBeaconLockHz := 0.0; FBeaconLockHz := 0.0;
FBeaconLockOn := False; FBeaconLockOn := False;
// QO-100 beacon DECODE: фронтенд BPSK 400 бод (тап IQ в движке). // Декодер маяка: фронтенд BPSK 400 бод (тап IQ в движке).
FBeaconDec := TBeaconDecoder.Create; FBeaconDec := TBeaconDecoder.Create;
FBeaconDecOn := False;
// Тракт QO-100 (LNB+трансвертер) зеркалит спектр: проверено приборно (symMag ×4 // Тракт QO-100 (LNB+трансвертер) зеркалит спектр: проверено приборно (symMag ×4
// и prom 500-800 при invert=ON против шума при OFF, на наведении точно на маяк). // и prom 500-800 при invert=ON против шума при OFF, на наведении точно на маяк).
// Декодеру нужно сопряжение, чтобы наведённый offset доставлял маяк к DC. // Декодеру нужно сопряжение, чтобы наведённый offset доставлял маяк к DC.
@@ -1051,111 +1039,26 @@ end;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const const
// --- контур коррекции --- // --- контур коррекции (измерение частоты — от ДЕКОДЕРА, не от display-FFT) ---
BEACON_GAIN = 0.9; // гейн контура: feed-forward делает шаг точным, // Декодер точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload):
// <1 — запас от перелёта при шумной оценке центроида // суммарный остаток ResidHz+CarFreqHz = текущее смещение маяка от точки наведения.
BEACON_SEED_GAIN = 0.95; // первая коррекция по клику агрессивнее (1 клик → лок) // Это смещение медленно стекаем в LOError активного XVTR → ретюн LO компенсирует
BEACON_DEADBAND_HZ = 75.0; // внутри — коррекции стоп (без вечного подкручивания) // дрейф LNB, весь даунлинк стоит. Внутренний NCO декодера держит маяк
BEACON_SETTLE_TICKS = 8; // пауза (~0.8 с @10 Гц) после коррекции — осадка FFT-пайплайна // декодированным во время плавного ретюна (наведение едет 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 Гц) BEACON_SAVE_TICKS = 30; // троттлинг записи LOError на диск (~3 с @10 Гц)
// --- метрика лока (prominence = пик над шумовым полом окна) --- // --- знак отображения остатка декодера ---
BEACON_LOCK_PROM_DB = 6.0; // загорается LOCK выше этого // invert=True (QO-100/LNB) выравнивает частотную ось декодера с дисплеем, поэтому
BEACON_UNLOCK_PROM_DB = 3.0; // гистерезис: гаснет ниже этого // display_маяка = наведение + (ResidHz+CarFreqHz). Если на эфире контур убегает —
// --- окна поиска/слежения --- // ось перевёрнута, флипнуть на -1.0 (см. лог: e и дрейф должны иметь один знак).
BEACON_SEARCH_HZ = 6000.0; // ШИРОКОЕ окно захвата (клик/включение/потеря лока) BEACON_RESID_SIGN = 1.0;
BEACON_TRACK_HZ = 2000.0; // УЗКИЙ gate слежения: сосед дальше — физически вне окна // --- метрика лока (берём из снимка декодера: CarrierLock + SNR) ---
BEACON_MAX_SLEW_HZ = 500.0; // дрейф LNB ≪ этого/тик; больший скачок = чужой сигнал → отвергаем BEACON_LOCK_SNR_DB = 3.0; // солидный лок: декодер несёт несущую и SNR выше этого
BEACON_TRACK_ALPHA = 0.25; // сглаживание позиции центроида за тик BEACON_UNLOCK_SNR_DB = 0.0; // гистерезис: лок гаснет ниже этого
// --- форма лепестка 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;
procedure TRadioController.SetBeaconRefHz(Hz: Double); procedure TRadioController.SetBeaconRefHz(Hz: Double);
begin begin
@@ -1163,37 +1066,26 @@ begin
end; end;
procedure TRadioController.BeaconSeedAtHz(DisplayHz: Double); procedure TRadioController.BeaconSeedAtHz(DisplayHz: Double);
// Пользователь кликнул по маяку → ручной режим (контур ведёт ИМЕННО этот пик, // Пользователь кликнул по маяку → наводим узкий тракт декодера на эту частоту и
// без автопоиска, который в забитой полосе хватает соседей). Так как клик — // переинициализируем петли (RunAcquisition заново оценит несущую). Дальше декодер
// уверенное указание, делаем СРАЗУ почти полную (deadbeat) коррекцию: один клик // сам ведёт маяк своим NCO, а ServiceBeaconLock стекает измеренный дрейф в LOError.
// ставит маяк на опорную за одну перестройку, без многократного «тыкания».
var clickOff, refined, prom, e, dLock: Double;
begin begin
if not FBeaconLockOn then SetBeaconLock(True); if not FBeaconLockOn then SetBeaconLock(True);
FBeaconManualSeed := True; FBeaconDecHz := DisplayHz;
clickOff := DisplayHz - FCenterFreq; FBeaconTrackHz := DisplayHz;
if MeasureBeaconFFT(clickOff, BEACON_SEARCH_HZ, refined, prom) then if Assigned(FBeaconDec) then
begin begin
FBeaconTrackHz := FCenterFreq + refined; FBeaconDec.SetOffsetHz(DisplayHz - FCenterFreq);
FBeaconPromDB := prom; FBeaconDec.Reseed; // одноразовый грубый захват частоты заново
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;
FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать декодеру захватиться до 1-й коррекции
Changed(rfBeaconLock);
end; end;
procedure TRadioController.SetBeaconLock(On_: Boolean); procedure TRadioController.SetBeaconLock(On_: Boolean);
var refined, prom: Double; // Один концепт: лок = декодер + трим LOError. Включение запускает декодер маяка;
// пользователь кликает по маяку (BeaconSeedAtHz) для наведения. Когда декодер
// солидно залочен, ServiceBeaconLock стекает его измеренный дрейф в LOError.
begin begin
if On_ = FBeaconLockOn then Exit; if On_ = FBeaconLockOn then Exit;
FBeaconLockOn := On_; FBeaconLockOn := On_;
@@ -1201,101 +1093,87 @@ begin
begin begin
FBeaconCorrCooldown := 0; FBeaconCorrCooldown := 0;
FBeaconSaveCooldown := 0; FBeaconSaveCooldown := 0;
FBeaconManualSeed := False; // авто до первого клика пользователя
FBeaconLocked := False; FBeaconLocked := False;
FBeaconPromDB := 0.0; FBeaconSNRdB := -99.0;
FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError уже несёт калибровку) FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError несёт калибровку)
// Начальная область — у опорной частоты (уточняем пик, если виден). FBeaconDecHz := 0.0; // ещё не наведён — ждём клик по маяку
if MeasureBeaconFFT(FBeaconRefHz - FCenterFreq, BEACON_SEARCH_HZ, refined, prom) then FBeaconTrackHz := FBeaconRefHz;
FBeaconTrackHz := FCenterFreq + refined if Assigned(FBeaconDec) then
else begin
FBeaconTrackHz := FBeaconRefHz; FBeaconDec.SetOffsetHz(0);
FBeaconDec.SetEnabled(True); // ResetDSP внутри
end;
end end
else else
PersistBeaconCalibration; // фиксируем накопленную коррекцию в LOError (см. метод) begin
if Assigned(FBeaconDec) then FBeaconDec.SetEnabled(False);
FBeaconDecHz := 0.0;
PersistBeaconCalibration; // финальный flush LOError на диск
end;
Changed(rfBeaconLock); Changed(rfBeaconLock);
end; end;
procedure TRadioController.ServiceBeaconLock; procedure TRadioController.ServiceBeaconLock;
// Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Меряет маяк по // Одна итерация контура (UI-таймер @10 Гц, MeterTimer). Эталон частоты — ДЕКОДЕР:
// display-FFT и (после ручного клика) трим LOError так, чтобы маяк стоял на // он точно ведёт несущую маяка своим NCO (RunAcquisition + Costas-offload), и его
// опорной частоте → весь downlink стабилен. Коррекция складывается в LOError // суммарный остаток ResidHz+CarFreqHz = текущее смещение маяка от точки наведения.
// (живая калибровка LNB: видна в настройках, сохраняется). // Измеренную АБСОЛЮТНУЮ частоту маяка (наведение + остаток) подтягиваем к опорной,
// // стекая ошибку в LOError активного XVTR → ретюн LO компенсирует дрейф LNB, весь
// Устойчивость к соседним сигналам (как в SDR Console): // даунлинк стоит. Наведение декодера едет feed-forward за маяком (FBeaconDecHz),
// • при локе окно УЗКОЕ (±BEACON_TRACK_HZ) — далёкий сосед вне окна; // так что внутренний NCO держит маяк декодированным сквозь плавный ретюн (нетто
// • slew-отбраковка: дрейф LNB медленный, резкий скачок центроида = чужой // baseband-смещение ≈ 0: маяк сдвинулся на −dLock железом, наведение тоже на −dLock).
// сигнал, измерение игнорируем; var
// • MeasureBeaconFFT проверяет форму лепестка (узнаём маяк, не «громкий пик»). S: TBeaconScope;
// Быстрая сходимость при большой ошибке: residHz, measured, e, dLock: Double;
// • feed-forward (FBeaconTrackHz = dLock) — окно едет за маяком сквозь ретюн,
// лок не теряется; во время settle позицию не двигаем (FFT в транзиенте).
var win, pk, prom, cand, e, dLock: Double;
begin 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 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); if FBeaconSaveCooldown > 0 then Dec(FBeaconSaveCooldown);
// Окно — вокруг ТЕКУЩЕЙ display-позиции маяка, пересчитанной в baseband через
// актуальный FCenterFreq (сдвиг водопада/центра окно не ломает). Узкое при локе. FBeaconDec.GetScope(S);
if FBeaconLocked then win := BEACON_TRACK_HZ else win := BEACON_SEARCH_HZ; FBeaconSNRdB := FBeaconSNRdB + 0.2 * (S.SNRdB - FBeaconSNRdB);
if not MeasureBeaconFFT(FBeaconTrackHz - FCenterFreq, win, pk, prom) then // Солидный лок: декодер несёт несущую И 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 begin
Changed(rfBeaconLock); // лепесток не виден/не маяк — держим позицию и лок measured := FBeaconDecHz + residHz; // абсолютная display-частота маяка
Exit; FBeaconTrackHz := measured; // для оранжевого маркера
end; end
cand := FCenterFreq + pk; else
measured := FBeaconTrackHz;
// Slew-отбраковка (только при локе/узком окне): скачок больше дрейфа LNB = // Коррекция: не каждый тик (ретюн LO имеет латентность пайплайна). После коррекции
// чужой сигнал в окне → измерение игнорируем, остаёмся на маяке. // ждём BEACON_SETTLE_TICKS. Только при солидном локе и после наведения кликом.
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).
if FBeaconCorrCooldown > 0 then if FBeaconCorrCooldown > 0 then
Dec(FBeaconCorrCooldown) Dec(FBeaconCorrCooldown)
else if FBeaconLocked and FBeaconManualSeed then else if FBeaconLocked and (FBeaconDecHz > 0) then
begin begin
// e = видимое_смещение_маяка − опорное. d(display)/d(LOError) = 1, поэтому e := measured - FBeaconRefHz;
// LOError += e тянет маяк к опорной; feed-forward предсказывает новую
// display-позицию (track = dLock), чтобы окно ехало за маяком.
e := FBeaconTrackHz - FBeaconRefHz;
if Abs(e) > BEACON_DEADBAND_HZ then if Abs(e) > BEACON_DEADBAND_HZ then
begin begin
dLock := BEACON_GAIN * e; dLock := BEACON_GAIN * e;
ApplyBeaconLOError(dLock); // в LOError + ретюн + троттл-сохранение if dLock > BEACON_MAX_STEP_HZ then dLock := BEACON_MAX_STEP_HZ;
FBeaconTrackHz := FBeaconTrackHz - dLock; 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; FBeaconCorrCooldown := BEACON_SETTLE_TICKS;
end; end;
end; end;
@@ -1343,7 +1221,7 @@ end;
function TRadioController.BeaconSNRdB: Double; function TRadioController.BeaconSNRdB: Double;
begin begin
Result := FBeaconPromDB; // «высота» пика над шумом окна (dB) Result := FBeaconSNRdB; // SNR последнего снимка декодера (dB)
end; end;
function TRadioController.BeaconLockEnabled: Boolean; function TRadioController.BeaconLockEnabled: Boolean;
@@ -1361,44 +1239,6 @@ begin
Result := FBeaconTrackHz; Result := FBeaconTrackHz;
end; end;
procedure TRadioController.SetBeaconDecode(On_: Boolean);
// Включает фронтенд-декодер маяка. Цель — маяк на DC (центр RX-спектра): стартуем
// с offset=0 (или явного клик-аима), дальше частоту держит петля несущей декодера.
// От beacon-lock декодер НЕ зависит (см. ServiceBeaconLock). Пользователь наводит
// клик на маяк либо центрирует его в спектре, acquisition/Costas доводят.
begin
FBeaconDecOn := On_;
if not Assigned(FBeaconDec) then Exit;
if not On_ then FBeaconDecHz := 0; // сброс наведения при выключении
if On_ then
begin
if FBeaconDecHz > 0 then
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq)
else
FBeaconDec.SetOffsetHz(0);
end;
FBeaconDec.SetEnabled(On_);
end;
procedure TRadioController.SetBeaconDecodeAtHz(DisplayHz: Double);
// Пользователь навёл узкий фильтр на маяк (клик по спектру). Запоминаем абсолютную
// частоту, сносим её к DC и переинициализируем петли (одноразовая грубая оценка
// заново). Снос держится в ServiceBeaconLock при перестройке/панораме.
begin
FBeaconDecHz := DisplayHz;
if Assigned(FBeaconDec) then
begin
FBeaconDec.SetOffsetHz(FBeaconDecHz - FCenterFreq);
FBeaconDec.Reseed;
end;
Changed(rfBeaconLock);
end;
function TRadioController.BeaconDecodeEnabled: Boolean;
begin
Result := FBeaconDecOn;
end;
function TRadioController.BeaconDecodeFreqHz: Double; function TRadioController.BeaconDecodeFreqHz: Double;
begin begin
Result := FBeaconDecHz; Result := FBeaconDecHz;