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 при нажатии 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;
+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.Pen.Color := Col; C.Pen.Width := 1; C.Pen.Style := Style;
C.MoveTo(x, 0); C.LineTo(x, H); C.MoveTo(x, 0); C.LineTo(x, H);
end; end;
var
xc, x1, x2: Integer;
begin begin
if FBeaconMarkActive then if FBeaconMarkActive then
begin begin
Vline(FBeaconRefFreq, clLime, psDot); // где маяк ДОЛЖЕН быть Vline(FBeaconRefFreq, clLime, psDot); // где маяк ДОЛЖЕН быть
Vline(FBeaconTrkFreq, TColor($000AA5FF), psSolid); // отслеживаемый центроид (оранж) Vline(FBeaconTrkFreq, TColor($000AA5FF), psSolid); // отслеживаемый центроид (оранж)
end; end;
// Узкий фильтр-маркер декодера: жёлтые грани полосы + центральная линия. // Узкий фильтр-маркер декодера: грани полосы + центральная линия. Три линии без
// полупрозрачной заливки (как в GL-рендере) — единый вид и дешевле для CPU.
if FBeaconDecActive and (FSpanHz > 0) then if FBeaconDecActive and (FSpanHz > 0) then
begin begin
xc := Round((FBeaconDecFreq - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W); Vline(FBeaconDecFreq - FBeaconDecHalf, TColor($0020D0FF), psSolid); // грань фильтра
x1 := Round((FBeaconDecFreq - FBeaconDecHalf - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W); Vline(FBeaconDecFreq + FBeaconDecHalf, TColor($0020D0FF), psSolid);
x2 := Round((FBeaconDecFreq + FBeaconDecHalf - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W); Vline(FBeaconDecFreq, TColor($0020D0FF), psSolid); // центр наведения
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;
end; end;
C.Pen.Style := psSolid; C.Pen.Style := psSolid;
end; end;