Merge feature/qo100-decoder-lock: decoder-based QO-100 beacon lock

Replace the display-FFT centroid beacon lock with a decoder-based loop that
trims the active transverter's LOError from the beacon decoder's precise NCO
carrier estimate, compensating LNB drift so the whole downlink (incl. FT8)
stays put. Single BEACON button (lock = decoder). Also unifies the beacon
decoder filter marker between the CPU and OpenGL spectrum renderers.

Verified on air: lock holds, FT8 decodes cleanly.
This commit is contained in:
2026-06-22 22:14:35 +03:00
3 changed files with 146 additions and 322 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;
+5 -9
View File
@@ -526,23 +526,19 @@ procedure TSpectrumView.DrawBeaconMarkers(C: TCanvas; W, H: Integer);
C.Pen.Color := Col; C.Pen.Width := 1; C.Pen.Style := Style;
C.MoveTo(x, 0); C.LineTo(x, H);
end;
var
xc, x1, x2: Integer;
begin
if FBeaconMarkActive then
begin
Vline(FBeaconRefFreq, clLime, psDot); // где маяк ДОЛЖЕН быть
Vline(FBeaconTrkFreq, TColor($000AA5FF), psSolid); // отслеживаемый центроид (оранж)
end;
// Узкий фильтр-маркер декодера: жёлтые грани полосы + центральная линия.
// Узкий фильтр-маркер декодера: грани полосы + центральная линия. Три линии без
// полупрозрачной заливки (как в GL-рендере) — единый вид и дешевле для CPU.
if FBeaconDecActive and (FSpanHz > 0) then
begin
xc := Round((FBeaconDecFreq - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
x1 := Round((FBeaconDecFreq - FBeaconDecHalf - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
x2 := Round((FBeaconDecFreq + FBeaconDecHalf - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
BlendBand(x1, x2, H, $FF, $D0, $20, 48); // полупрозрачная полоса
C.Pen.Color := TColor($0020D0FF); C.Pen.Width := 1; C.Pen.Style := psSolid;
if (xc >= 0) and (xc < W) then begin C.MoveTo(xc, 0); C.LineTo(xc, H); end;
Vline(FBeaconDecFreq - FBeaconDecHalf, TColor($0020D0FF), psSolid); // грань фильтра
Vline(FBeaconDecFreq + FBeaconDecHalf, TColor($0020D0FF), psSolid);
Vline(FBeaconDecFreq, TColor($0020D0FF), psSolid); // центр наведения
end;
C.Pen.Style := psSolid;
end;