From 6246b6bad471dbb4a336a9655c536f70b3bf5437 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 22 Jun 2026 19:51:01 +0300 Subject: [PATCH] feat: QO-100 decoder-based beacon lock (replace FFT lock) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- MainForm.pas | 66 +++----- RadioController.pas | 388 +++++++++++++------------------------------- 2 files changed, 141 insertions(+), 313 deletions(-) diff --git a/MainForm.pas b/MainForm.pas index beef32c..71ee8e5 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -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)); diff --git a/RadioController.pas b/RadioController.pas index 4a83cf7..a25c2f0 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -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; // |L−R|/(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 реально захвачен (зелёный) — доверяем его оценке и сносим - // FBeaconTrackHz−FCenterFreq (авто-центровка). Развязано от сырого 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;