diff --git a/BeaconLock.pas b/BeaconLock.pas deleted file mode 100644 index afa5338..0000000 --- a/BeaconLock.pas +++ /dev/null @@ -1,339 +0,0 @@ -unit BeaconLock; - -{$IFDEF FPC} - {$MODE Delphi} -{$ENDIF} - -{ - QO-100 Beacon Lock — выделенный узкополосный трекер несущей опорного маяка. - - Задача: LNB-гетеродин (≈9750 МГц) дрейфует с температурой на единицы–десятки - кГц → весь downlink плывёт. Маяк (нижний CW 10489.500 МГц) имеет ИЗВЕСТНУЮ - частоту. Меряя, где он реально стоит, получаем ошибку LO и компенсируем её. - - Двухступенчатая схема: - 1. ГРУБЫЙ захват — снаружи (по display-FFT) даёт начальную оценку смещения - маяка в пределах ~±300 Гц; передаётся через Seed. - 2. ТОЧНЫЙ лок (этот модуль) — комплексный NCO сносит ожидаемую частоту маяка - к DC, boxcar-дециматор сужает полосу до ~2 кГц (большой processing gain), - затем FLL по фазовому дискриминатору доводит NCO ровно на несущую. - Лок-метрика — SNR через carrier-EMA / noise-EMA на децимированном потоке. - - Точность: единицы Гц при достаточном SNR (узкая полоса + длинное усреднение - фазы), что и есть «железный» лок (в духе SDR Console). - - Потоки: Feed() зовётся из DSP-потока (дёшево, на сэмпл). Результаты читаются - из управляющего потока через снапшот под критической секцией. -} - -interface - -uses - SysUtils, Math, SyncObjs; - -type - TBeaconState = (bsIdle, // выключен / нет данных - bsSearch, // идёт захват (SNR ниже порога) - bsLock); // несущая захвачена и стабильна - - TBeaconTracker = class - private - FLock: TCriticalSection; - // --- конфигурация --- - FSampleRate: Double; // вход (Hz) - FDecim: Integer; // коэффициент децимации (Fs/FDecim ≈ FDecimRate) - FDecimRate: Double; // частота децимированного потока (Hz) - FLoopGain: Double; // гейн FLL (0..1, малый) - FLockSNRdB: Double; // порог объявления лока - FTargetHz: Double; // ожидаемое смещение маяка от центра (Hz) - // --- состояние NCO/децимации (DSP-поток) --- - FNcoFreq: Double; // текущая частота NCO (быстрая, дёрганая) - FNcoSmooth: Double; // сглаженная оценка смещения маяка (для коррекции/маркера) - FSeedHz: Double; // якорь clamp: NCO не уходит дальше ±CLAMP от сида - FPhCos, FPhSin: Double; // текущий фазор NCO (для сноса к DC) - FStepCos, FStepSin: Double; // инкремент фазора за сэмпл (Δ = −2π·FNcoFreq/Fs) - FNcoNorm: Integer; // счётчик до ренормализации фазора - FAccI, FAccQ: Double; // аккумулятор boxcar-дециматора - FAccN: Integer; - FPrevI, FPrevQ: Double; // предыдущий децимированный сэмпл (для дискриминатора) - FHasPrev: Boolean; - FMeanI, FMeanQ: Double; // EMA комплексного децим. потока → амплитуда несущей - FNoisePow: Double; // EMA мощности шума |z-mean|^2 - FResAvg: Double; // EMA остаточной частоты (для контроля стабильности) - FActive: Boolean; // трекинг включён - // --- снапшот результата (под FLock) --- - FResMeasHz: Double; // измеренное смещение маяка = FNcoFreq на момент снапшота - FResSNRdB: Double; - FResState: TBeaconState; - procedure RecomputeStep; // пересчёт шага фазора по FNcoFreq (фазу НЕ трогает) - procedure ResetPhasor; // сброс фазора в 0 + пересчёт шага - procedure ProcessDecimated(const zi, zq: Double); - procedure Snapshot; - public - constructor Create; - destructor Destroy; override; - - // Настройка под текущий sample rate. DecimRateHz — целевая полоса - // децимированного потока (по умолчанию ~2 кГц: ±1 кГц захват FLL). - procedure Configure(ASampleRate: Double; ADecimRateHz: Double = 4000.0); - // Ожидаемое смещение маяка от DDC-центра (Hz). Меняется при перестройке. - procedure SetTarget(AOffsetHz: Double); - // Грубая оценка от FFT — переинициализирует NCO (захват/переакквизиция). - procedure Seed(AOffsetHz: Double); - procedure Start; - procedure Stop; - procedure Reset; - - // DSP-поток: один комплексный сэмпл @ FSampleRate. - procedure Feed(const I, Q: Double); inline; - - // Управляющий поток: снапшот результата. - function MeasuredOffsetHz: Double; // измеренное смещение маяка от центра - function ErrorHz: Double; // MeasuredOffset − Target (ошибка LO) - function SNRdB: Double; - function State: TBeaconState; - function Locked: Boolean; - property Active: Boolean read FActive; - end; - -implementation - -const - NCO_RENORM = 2048; // ренормализация фазора каждые N сэмплов - EMA_MEAN = 0.002; // постоянная EMA несущей (медленная — узкая полоса) - EMA_NOISE = 0.002; - EMA_RES = 0.01; - EMA_NCO = 0.001; // сглаживание оценки частоты (TC ~0.25 с @4кГц) - CLAMP_HZ = 1500.0; // NCO держим в ±CLAMP от сида (без runaway на край) - NOISE_FLOOR = 1e-30; // ТОЛЬКО защита от деления на ноль (не маскировать сигнал!) - SNR_MAX = 40.0; - -constructor TBeaconTracker.Create; -begin - inherited Create; - FLock := TCriticalSection.Create; - FSampleRate := 1536000.0; - FDecimRate := 4000.0; // квадрат-домен → захват ±FDecimRate/4 = ±1 кГц - FLoopGain := 0.02; - FLockSNRdB := 3.0; - Reset; - Configure(FSampleRate, FDecimRate); -end; - -destructor TBeaconTracker.Destroy; -begin - FLock.Free; - inherited Destroy; -end; - -procedure TBeaconTracker.Configure(ASampleRate, ADecimRateHz: Double); -begin - FLock.Enter; - try - if ASampleRate < 1.0 then ASampleRate := 1536000.0; - if ADecimRateHz < 100.0 then ADecimRateHz := 4000.0; - FSampleRate := ASampleRate; - FDecim := Max(1, Round(ASampleRate / ADecimRateHz)); - FDecimRate := ASampleRate / FDecim; - // сброс рабочих аккумуляторов (NCO частоту сохраняем — это оценка маяка) - FAccI := 0; FAccQ := 0; FAccN := 0; - FHasPrev := False; - FMeanI := 0; FMeanQ := 0; FNoisePow := 0; FResAvg := 0; - ResetPhasor; - finally - FLock.Leave; - end; -end; - -procedure TBeaconTracker.RecomputeStep; -// Пересчёт шага фазора под текущую FNcoFreq (снос на −FNcoFreq, маяк → DC). -// Фазу НЕ трогаем — смена частоты непрерывна по фазе. -var w: Double; -begin - w := -2.0 * Pi * FNcoFreq / FSampleRate; - FStepCos := Cos(w); - FStepSin := Sin(w); -end; - -procedure TBeaconTracker.ResetPhasor; -begin - FPhCos := 1.0; - FPhSin := 0.0; - FNcoNorm := 0; - RecomputeStep; -end; - -procedure TBeaconTracker.SetTarget(AOffsetHz: Double); -begin - FLock.Enter; - try - FTargetHz := AOffsetHz; - finally - FLock.Leave; - end; -end; - -procedure TBeaconTracker.Seed(AOffsetHz: Double); -begin - FLock.Enter; - try - FNcoFreq := AOffsetHz; - FNcoSmooth := AOffsetHz; - FSeedHz := AOffsetHz; // якорь clamp - ResetPhasor; - FHasPrev := False; - FMeanI := 0; FMeanQ := 0; FNoisePow := 0; FResAvg := 0; - finally - FLock.Leave; - end; -end; - -procedure TBeaconTracker.Start; -begin - FActive := True; -end; - -procedure TBeaconTracker.Stop; -begin - FActive := False; -end; - -procedure TBeaconTracker.Reset; -begin - FLock.Enter; - try - FNcoFreq := 0; FNcoSmooth := 0; FSeedHz := 0; FTargetHz := 0; - FAccI := 0; FAccQ := 0; FAccN := 0; - FHasPrev := False; - FMeanI := 0; FMeanQ := 0; FNoisePow := 0; FResAvg := 0; - FResMeasHz := 0; FResSNRdB := -99.0; FResState := bsIdle; - FActive := False; - ResetPhasor; - finally - FLock.Leave; - end; -end; - -procedure TBeaconTracker.Feed(const I, Q: Double); -var - mi, mq, nc, ns: Double; -begin - if not FActive then Exit; - // снос к DC: z * conj(nco) (nco = exp(+jωt), шаг отрицательной частоты) - mi := I * FPhCos - Q * FPhSin; - mq := I * FPhSin + Q * FPhCos; - // продвинуть фазор: phase *= step - nc := FPhCos * FStepCos - FPhSin * FStepSin; - ns := FPhCos * FStepSin + FPhSin * FStepCos; - FPhCos := nc; FPhSin := ns; - Inc(FNcoNorm); - if FNcoNorm >= NCO_RENORM then - begin - // ренормализация (борьба с накоплением ошибки округления) - nc := 1.5 - 0.5 * (FPhCos * FPhCos + FPhSin * FPhSin); - FPhCos := FPhCos * nc; - FPhSin := FPhSin * nc; - FNcoNorm := 0; - end; - // boxcar-дециматор - FAccI := FAccI + mi; - FAccQ := FAccQ + mq; - Inc(FAccN); - if FAccN >= FDecim then - begin - ProcessDecimated(FAccI / FDecim, FAccQ / FDecim); - FAccI := 0; FAccQ := 0; FAccN := 0; - end; -end; - -procedure TBeaconTracker.ProcessDecimated(const zi, zq: Double); -// Работаем в КВАДРАТЕ сигнала w = z²: для BPSK (средний маяк) это снимает -// модуляцию ±180° (b²=1), оставляя чистую несущую на 2× частоте; для чистого -// CW (нижний/верхний) — тоже корректно. Дискриминатор по w даёт 2·остаток, -// делим на 2. Захват ±FDecimRate/4. carrier/noise-EMA считаем в w-домене — -// для BPSK среднее по z = 0, а по w = z² несущая когерентна. -var - wi, wq, pr, pi, phase, res2, resHz, di, dq, np: Double; -begin - wi := zi * zi - zq * zq; // Re(z²) - wq := 2.0 * zi * zq; // Im(z²) - if FHasPrev then - begin - // фазовый дискриминатор по квадрату: arg(w[n] * conj(w[n-1])) - pr := wi * FPrevI + wq * FPrevQ; - pi := wq * FPrevI - wi * FPrevQ; - phase := ArcTan2(pi, pr); - res2 := phase * FDecimRate / (2.0 * Pi); // = 2 · остаточная частота - resHz := 0.5 * res2; - // FLL: подтянуть NCO на остаточную частоту, но удержать в окне ±CLAMP_HZ - // вокруг сида — иначе на BPSK/шуме петля убегает на край спектра. - FNcoFreq := FNcoFreq + FLoopGain * resHz; - if FNcoFreq > FSeedHz + CLAMP_HZ then FNcoFreq := FSeedHz + CLAMP_HZ; - if FNcoFreq < FSeedHz - CLAMP_HZ then FNcoFreq := FSeedHz - CLAMP_HZ; - FResAvg := FResAvg + EMA_RES * (Abs(resHz) - FResAvg); - FNcoSmooth := FNcoSmooth + EMA_NCO * (FNcoFreq - FNcoSmooth); // стаб. оценка - RecomputeStep; // обновить шаг фазора под новую FNcoFreq (фаза непрерывна) - end; - FPrevI := wi; FPrevQ := wq; FHasPrev := True; // FPrev* = предыдущий w - - // carrier-EMA в w-домене (несущая² у DC) и шум как отклонение от среднего - FMeanI := FMeanI + EMA_MEAN * (wi - FMeanI); - FMeanQ := FMeanQ + EMA_MEAN * (wq - FMeanQ); - di := wi - FMeanI; dq := wq - FMeanQ; - np := di * di + dq * dq; - FNoisePow := FNoisePow + EMA_NOISE * (np - FNoisePow); - - Snapshot; -end; - -procedure TBeaconTracker.Snapshot; -var - sigPow, snr: Double; - st: TBeaconState; -begin - sigPow := FMeanI * FMeanI + FMeanQ * FMeanQ; - snr := 10.0 * Log10((sigPow + 1e-30) / (FNoisePow + NOISE_FLOOR)); - if snr > SNR_MAX then snr := SNR_MAX; - // Лок по когерентности несущей (SNR в квадрат-домене). Гейт по остаточной - // частоте убран — на BPSK он давал ложный «не-лок». - if snr >= FLockSNRdB then - st := bsLock - else - st := bsSearch; - FLock.Enter; - try - FResMeasHz := FNcoSmooth; // сглаженная оценка → стабильная коррекция/маркер - FResSNRdB := snr; - FResState := st; - finally - FLock.Leave; - end; -end; - -function TBeaconTracker.MeasuredOffsetHz: Double; -begin - FLock.Enter; try Result := FResMeasHz; finally FLock.Leave; end; -end; - -function TBeaconTracker.ErrorHz: Double; -begin - FLock.Enter; try Result := FResMeasHz - FTargetHz; finally FLock.Leave; end; -end; - -function TBeaconTracker.SNRdB: Double; -begin - FLock.Enter; try Result := FResSNRdB; finally FLock.Leave; end; -end; - -function TBeaconTracker.State: TBeaconState; -begin - if not FActive then Exit(bsIdle); - FLock.Enter; try Result := FResState; finally FLock.Leave; end; -end; - -function TBeaconTracker.Locked: Boolean; -begin - Result := State = bsLock; -end; - -end. diff --git a/MainForm.pas b/MainForm.pas index 66fd52c..8e85ed2 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -43,7 +43,7 @@ uses PowerInhibit, DeviceStore, RadioBackend, PlutoBackend, - RadioController, ChannelController, BeaconLock; + RadioController, ChannelController; const CLR_BG = TColor($00101010); @@ -238,7 +238,7 @@ type PanelFilter: TPanel; BtnFilter: array[0..FILT_COUNT-1] of TFlatButton; BtnCTun: TFlatButton; - BtnBeacon: TFlatButton; // QO-100 beacon lock (видим только в XVTR-режиме) + BtnBeacon: TFlatButton; // QO-100 beacon lock (видим только на Pluto в трансвертере) BtnDUP: TFlatButton; PanelFMSQ: TPanel; BtnFMSQ: TFlatButton; @@ -352,6 +352,8 @@ type // Public UI update (called from sync objects) procedure UpdateTXMeters; + procedure UpdateStatusField7; // поле 7: PLL (HPSDR) или beacon (Pluto) + function BeaconStatusText: string; // компактный статус QO-100 beacon lock procedure DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord); // Event handlers @@ -1194,10 +1196,11 @@ begin if BtnXvtrBand[i] <> nil then StyleButton(BtnXvtrBand[i], i = FController.FCurrentXvtr); UpdateFreqDisplayMax; // трансвертер может быть >6 ГГц (QO-100) - // BEACON-кнопка актуальна только в трансвертер-режиме (QO-100 lock). + // BEACON-кнопка — только Pluto в трансвертере (QO-100 lock); для + // openHPSDR смысла не имеет (нет дрейфующего LNB → прячем). if BtnBeacon <> nil then begin - BtnBeacon.Visible := FController.FCurrentXvtr >= 0; + BtnBeacon.Visible := FController.IsPluto and (FController.FCurrentXvtr >= 0); UpdateBeaconButton; end; // Wideband-вид + web-зеркало xvtr (ранее в обёртках Activate/Deactivate). @@ -3175,18 +3178,14 @@ procedure TMainForm.UpdateTXMeters; // FLastSupplyV/FLastSupplyA обновляются на rate HP Status (~150/с) // в TRadioController.ApplyHPStatus. begin + UpdateStatusField7; // PLL (openHPSDR) или статус beacon lock (Pluto) + if not FController.FRunning then begin SetStatusText(3, 'Supply --'); - SetStatusText(7, 'PLL --'); Exit; end; - if FController.FLastPLLLock then - SetStatusText(7, 'PLL OK') - else - SetStatusText(7, 'PLL?'); - if not BoardSupportsSupplyVoltage(FController.FNetwork.Device.BoardType) then begin SetStatusText(3, 'Supply n/a'); @@ -3205,6 +3204,35 @@ begin SetStatusText(3, 'Supply --'); end; +function TMainForm.BeaconStatusText: string; +// Компактный статус QO-100 beacon lock для поля 7 статус-бара (Pluto). Courier 8pt, +// поле ~200px → держим ≤ ~24 символов. +begin + if not FController.BeaconLockEnabled then Exit('BCN: off'); + if FBeaconArming then Exit('BCN: click beacon'); + case FController.BeaconState of + bsLock: Result := Format('BCN LOCK %.0fHz /%.0f', + [FController.BeaconCorrectionHz, FController.BeaconSNRdB]); + bsSearch: Result := Format('BCN search /%.0f', [FController.BeaconSNRdB]); + else Result := 'BCN --'; + end; +end; + +procedure TMainForm.UpdateStatusField7; +// Поле 7 статус-бара. openHPSDR — PLL-lock (как прежде). Pluto в трансвертере — +// статус QO-100 beacon lock (у Pluto нет PLL-телеметрии); Pluto без XVTR — пусто. +begin + if FController.IsPluto then + begin + if FController.FCurrentXvtr >= 0 then SetStatusText(7, BeaconStatusText) + else SetStatusText(7, ''); + Exit; + end; + if not FController.FRunning then SetStatusText(7, 'PLL --') + else if FController.FLastPLLLock then SetStatusText(7, 'PLL OK') + else SetStatusText(7, 'PLL?'); +end; + procedure TMainForm.OnWidebandCB(ADCIndex: Integer; const Samples: array of SmallInt; Count: Integer); begin @@ -3897,46 +3925,20 @@ begin end; procedure TMainForm.UpdateBeaconButton; -// Подпись + подсветка кнопки BEACON по состоянию лока. Зовётся из BtnBeaconClick -// и из рендера rfBeaconLock (контур обновляет статус каждый тик). -var - cap: string; - corr: Double; +// Beacon-маркеры + кнопка BEACON. Кнопка видима только на Pluto в трансвертере +// (см. rfXvtr) — это ПРОСТО кнопка: подпись 'BEACON', подсветка = лок включён. +// Вся инфа (lock/коррекция/поиск/«click») — в статус-баре, поле 7 (вместо PLL), +// см. UpdateStatusField7. На openHPSDR кнопка скрыта, beacon-маркеры неактивны. begin - if BtnBeacon = nil then Exit; - // Маркеры на спектре: опорная частота (зелёная) + позиция трекера (оранж). + // Маркеры на спектре: опорная частота (зелёная) + отслеживаемый центроид (оранж). if FSpecView <> nil then FSpecView.SetBeaconMarkers(FController.BeaconLockEnabled, FController.BeaconRefHz, FController.BeaconTrackedFreqHz); - if not FController.BeaconLockEnabled then - begin - BtnBeacon.Caption := 'BEACON'; - StyleButton(BtnBeacon, False); - end - else if FBeaconArming then - begin - BtnBeacon.Caption := 'CLICK BCN'; // ждём клик пользователя по маяку - StyleButton(BtnBeacon, True); - end - else - begin - case FController.BeaconState of - bsLock: - begin - corr := FController.BeaconCorrectionHz; - // LOCK + коррекция LO (Гц) + SNR — видно запас. - cap := Format('L%.0f /%.0f', [corr, FController.BeaconSNRdB]); - StyleButton(BtnBeacon, True); - end; - bsSearch: - // Показываем SNR при поиске — диагностика: растёт → маяк ловится. - begin cap := Format('BCN ?%.0f', [FController.BeaconSNRdB]); StyleButton(BtnBeacon, True); end; - else - begin cap := 'BCN --'; StyleButton(BtnBeacon, False); end; - end; - BtnBeacon.Caption := cap; - end; + UpdateStatusField7; // моментальный апдейт статус-бара (не ждём MeterTimer) + if BtnBeacon = nil then Exit; + BtnBeacon.Caption := 'BEACON'; + StyleButton(BtnBeacon, FController.BeaconLockEnabled); BtnBeacon.Repaint; end; diff --git a/RadioController.pas b/RadioController.pas index fc9ca93..d89a38f 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -50,9 +50,14 @@ uses Classes, SysUtils, Math, HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings, WDSPEngine, AudioOutput, AudioInput, - Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore, BeaconLock; + Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore; type + // QO-100 beacon lock — статус контура (для UI/маркеров). + TBeaconState = (bsIdle, // лок выключен + bsSearch, // включён, маяк ещё не подтверждён (низкий prominence) + bsLock); // маяк уверенно виден и сопровождается + // Поле, которое изменилось — для гранулярных уведомлений наружу. TRadioField = ( rfVfoA, rfVfoB, rfActiveVfo, @@ -177,19 +182,20 @@ type FCurrentBand: Integer; FCurrentXvtr: Integer; - // ---- QO-100 beacon lock ---- - FBeaconTracker: TBeaconTracker; // владеем (Free в FreeEngines) + // ---- QO-100 beacon lock (центральный BPSK-маяк 10489.750, по display-FFT) ---- FBeaconLockOn: Boolean; // лок включён пользователем - FBeaconLockHz: Double; // текущая динамич. коррекция LO (Hz), поверх LOError - FBeaconRefHz: Double; // опорная частота маяка (Hz, downlink), деф. 10489.500e6 - FBeaconSearchTicks: Integer; // тиков без лока (троттлинг переакквизиции) + FBeaconLockHz: Double; // коррекция, накопленная за ТЕКУЩУЮ сессию лока + // (только индикация на кнопке); сама поправка + // живёт в LOError активного трансвертера + FBeaconRefHz: Double; // опорная частота маяка (Hz, downlink), деф. 10489.750e6 FBeaconManualSeed: Boolean; // пользователь указал маяк кликом → не автопоиск - // FFT-трекинг маяка (робастный, по display-FFT — без NCO/FLL): + // FFT-трекинг маяка (робастный, по display-FFT): FBeaconTrackHz: Double; // отслеживаемая АБСОЛЮТНАЯ display-частота маяка // (не baseband-offset — иначе сдвиг центра/водопада ломает окно) FBeaconPromDB: Double; // «высота» пика над шумом окна (лок-метрика) FBeaconLocked: Boolean; // пик уверенно виден FBeaconCorrCooldown: Integer; // тиков паузы после коррекции (осадка пайплайна) + FBeaconSaveCooldown: Integer; // троттлинг записи LOError на диск (тиков) // ---- Аудио / TX ---- FVolume: Integer; @@ -336,12 +342,17 @@ type // ActiveTXFreqHz — текущая TX-частота (split/active VFO + FM-репитер сдвиг). function XvtrTranslate(VisibleHz: Double): Double; function ActiveTXFreqHz: Double; - // Грубый поиск пика маяка в display-FFT около ожидаемой позиции → baseband - // offset (Hz). False если маяк вне span / слишком слаб. Сид для трекера. - function CoarseBeaconOffset(ExpectOff: Double; out OffHz: Double): Boolean; - // Измерение пика маяка по display-FFT в окне вокруг ExpectOff: возвращает - // уточнённое смещение (parabolic) и «высоту» пика над средним окна (для лока). - function MeasureBeaconFFT(ExpectOff: Double; out PeakOff, PromDB: Double): Boolean; + // Измерение маяка по display-FFT в окне ±WinHz вокруг ExpectOff (baseband): + // центроид лепестка → уточнённое смещение, «высота» пика над средним окна + // (лок-метрика). False, если лепесток не похож на маяк (узкий/широкий/ + // несимметричный сосед) или вне span — тогда трек держим на месте. + function MeasureBeaconFFT(ExpectOff, WinHz: Double; + out PeakOff, PromDB: Double): Boolean; + // Складывает коррекцию маяка dLock в LOError активного трансвертера (живая + // калибровка LNB), ретюнит радио и троттлингом сохраняет на диск. + procedure ApplyBeaconLOError(dLock: Double); + // Flush LOError на диск при выключении лока (если троттлинг не успел). + procedure PersistBeaconCalibration; // Применяет текущие FMode/FFilterBW/FM-параметры к DSP (RX/TX фильтр, // FM-девиация, squelch). Вызывается командами и UI-путями смены режима. @@ -448,15 +459,15 @@ type // QO-100 beacon lock — стабилизация дрейфа LNB по опорному маяку. procedure SetBeaconLock(On_: Boolean); - procedure ServiceBeaconLock; // одна итерация контура (зовётся UI-таймером ~4 Гц) + procedure ServiceBeaconLock; // одна итерация контура (MeterTimer @10 Гц) procedure SetBeaconRefHz(Hz: Double); // опорная частота маяка (downlink) procedure BeaconSeedAtHz(DisplayHz: Double); // пользователь указал маяк кликом function BeaconState: TBeaconState; - function BeaconCorrectionHz: Double; // текущая коррекция LO (FBeaconLockHz) + function BeaconCorrectionHz: Double; // коррекция за сессию лока (индикация; в LOError) function BeaconSNRdB: Double; function BeaconLockEnabled: Boolean; function BeaconRefHz: Double; // опорная частота маяка (для маркера) - function BeaconTrackedFreqHz: Double;// где сидит трекер: центр + measured offset + function BeaconTrackedFreqHz: Double;// отслеживаемая display-частота маяка (центроид) // Отображение / приём procedure SetCTun(On_: Boolean); @@ -553,12 +564,10 @@ begin // буфер вдвое сокращает построение downsampler в OpenChannel RXA. FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512); - // QO-100 beacon lock: трекер кормится RX-IQ из движка (тап в PushIQItemToDSP). - FBeaconTracker := TBeaconTracker.Create; - FBeaconRefHz := 10489750000.0; // средний BPSK-маяк NB-транспондера (downlink) + // QO-100 beacon lock: измерение по display-FFT (см. ServiceBeaconLock). + FBeaconRefHz := 10489750000.0; // центральный BPSK-маяк NB-транспондера (downlink) FBeaconLockHz := 0.0; FBeaconLockOn := False; - FDSPEngine.SetBeaconTracker(FBeaconTracker); // Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа // формы / при подключении устройства). @@ -577,9 +586,6 @@ begin if Assigned(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end; if Assigned(FAudioIn) then begin FAudioIn.Close; FreeAndNil(FAudioIn); end; if Assigned(FDSPEngine) then begin FDSPEngine.Close; FreeAndNil(FDSPEngine); end; - // Трекер освобождаем ПОСЛЕ движка — движок (Close остановил DSP-поток) больше - // не вызовет Feed. Указатель в движке уже мёртв вместе с движком. - if Assigned(FBeaconTracker) then FreeAndNil(FBeaconTracker); end; destructor TRadioController.Destroy; @@ -942,13 +948,13 @@ begin Result := VisibleHz; if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit; if not FXvtrSettings.Entries[FCurrentXvtr].Enabled then Exit; - // f_IF = f_visible - LOOffset + LOError + BeaconLock - // FBeaconLockHz — динамическая коррекция дрейфа LNB (QO-100 beacon lock), - // поверх статичной ручной LOError. 0 когда лок выключен. + // f_IF = f_visible - LOOffset + LOError. + // QO-100 beacon lock складывает свою коррекцию дрейфа LNB ПРЯМО в LOError + // (живая калибровка): она сразу видна в настройках и сохраняется. Отдельного + // слагаемого нет — иначе двойной счёт. Result := VisibleHz - FXvtrSettings.Entries[FCurrentXvtr].LOOffset - + FXvtrSettings.Entries[FCurrentXvtr].LOError - + FBeaconLockHz; + + FXvtrSettings.Entries[FCurrentXvtr].LOError; end; function TRadioController.ActiveTXFreqHz: Double; @@ -970,68 +976,44 @@ end; // --------------------------------------------------------------------------- const - BEACON_GAIN = 0.4; // гейн контроллера (доля ошибки за коррекцию) + // --- контур коррекции --- + 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-пайплайна - BEACON_UNLOCK_PROM_DB = 3.0; // гистерезис: лок снимается ниже этого (lock при LOCK_PROM) - BEACON_SEARCH_HZ = 6000.0; // окно поиска пика вокруг клика/ожидаемой позиции - BEACON_REACQ_TICKS = 30; // тиков (≈3 с @10Гц) до переакквизиции по FFT - BEACON_LOCK_PROM_DB = 6.0; // пик над средним окна (dB) для статуса LOCK - BEACON_LOBE_DB = 10.0; // ширина лепестка для центроида (dB ниже пика) + BEACON_SETTLE_TICKS = 8; // пауза (~0.8 с @10 Гц) после коррекции — осадка FFT-пайплайна + 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.CoarseBeaconOffset(ExpectOff: Double; out OffHz: Double): Boolean; -// Ищет максимум display-FFT в окне ±BEACON_SEARCH_HZ вокруг ExpectOff (baseband -// offset). Пиксели покрывают анализаторную полосу = FSampleRate, центр (пиксель -// n/2) = baseband 0, рост вправо — та же конвенция, что и маркеры (проверено: -// зелёная садится на маяк). Возвращает уточнённый baseband offset пика. -var - pix: array[0..SPECTRUM_PIXELS - 1] of Single; - n, i, best: Integer; - hzPerPix, lo, hi, off, denom, delta: Double; - bestv: Single; -begin - Result := False; OffHz := 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 - BEACON_SEARCH_HZ; - hi := ExpectOff + BEACON_SEARCH_HZ; - best := -1; bestv := -1e30; - for i := 0 to n - 1 do - begin - off := (i - n / 2) * hzPerPix; // baseband offset пикселя i - if (off < lo) or (off > hi) then Continue; - if pix[i] > bestv then begin bestv := pix[i]; best := i; end; - end; - if best < 0 then Exit; // окно вне span - delta := 0.0; - // Параболическая интерполяция пика (пиксели в dB ≈ гауссов пик) — субдискретная - // точность, чтобы сид попал в ±1 кГц окно захвата FLL даже при крупном бине. - if (best > 0) and (best < n - 1) then - begin - denom := pix[best - 1] - 2.0 * pix[best] + pix[best + 1]; - if Abs(denom) > 1e-12 then - delta := 0.5 * (pix[best - 1] - pix[best + 1]) / denom; - if delta > 0.5 then delta := 0.5; - if delta < -0.5 then delta := -0.5; - end; - OffHz := (best + delta - n / 2) * hzPerPix; - Result := True; -end; - -function TRadioController.MeasureBeaconFFT(ExpectOff: Double; +function TRadioController.MeasureBeaconFFT(ExpectOff, WinHz: Double; out PeakOff, PromDB: Double): Boolean; -// Маяк по display-FFT в окне ±BEACON_SEARCH_HZ вокруг ExpectOff. Средний маяк — -// BPSK с ПОДАВЛЕННОЙ несущей: в спектре не линия, а широкая симметричная «полка» -// (~2.4 кГц), peak-finder по ней скачет. Поэтому позицию берём как -// POWER-ВЗВЕШЕННЫЙ ЦЕНТРОИД лепестка (пиксели в пределах BEACON_LOBE_DB от пика): -// центр симметричной полки = несущая, стабилен. PromDB — пик над средним окна. +// Маяк по 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: Integer; - hzPerPix, lo, hi, off, sum, thr, p, wsum, fsum: Double; + 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; @@ -1040,34 +1022,64 @@ begin FDSPEngine.GetSpectrumData(pix, n); if n < 8 then Exit; hzPerPix := FSpanHz / n; - lo := ExpectOff - BEACON_SEARCH_HZ; - hi := ExpectOff + BEACON_SEARCH_HZ; - best := -1; bestv := -1e30; sum := 0; cnt := 0; + 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; - sum := sum + pix[i]; Inc(cnt); + Inc(cnt); if pix[i] > bestv then begin bestv := pix[i]; best := i; end; end; if (best < 0) or (cnt < 4) then Exit; - // Центроид лепестка: пиксели в пределах BEACON_LOBE_DB ниже пика, вес — - // линейная мощность над порогом (шум за порогом не входит). + // Проход B — центроид лепестка + протяжённость + шумовой пол (бины вне лепестка). thr := bestv - BEACON_LOBE_DB; - wsum := 0; fsum := 0; + 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); // линейная мощность над порогом - wsum := wsum + p; - fsum := fsum + p * off; + p := Power(10.0, (pix[i] - thr) / 10.0); + if off < PeakOff then wLeft := wLeft + p else wRight := wRight + p; end; - if wsum > 0 then PeakOff := fsum / wsum - else PeakOff := (best - n / 2) * hzPerPix; - PromDB := bestv - (sum / cnt); // пик над средним окна (dB) - Result := True; + if (wLeft + wRight) > 0 then + if Abs(wLeft - wRight) / (wLeft + wRight) > BEACON_SYM_MAX then Exit; + Result := True; end; procedure TRadioController.SetBeaconRefHz(Hz: Double); @@ -1076,20 +1088,33 @@ begin end; procedure TRadioController.BeaconSeedAtHz(DisplayHz: Double); -// Пользователь кликнул по маяку → задаём область отслеживания (уточняем пик по -// display-FFT рядом с кликом) и включаем ручной режим (контур ведёт ИМЕННО этот -// пик, без автопоиска, который в забитой полосе хватает соседей). -var clickOff, refined, prom: Double; +// Пользователь кликнул по маяку → ручной режим (контур ведёт ИМЕННО этот пик, +// без автопоиска, который в забитой полосе хватает соседей). Так как клик — +// уверенное указание, делаем СРАЗУ почти полную (deadbeat) коррекцию: один клик +// ставит маяк на опорную за одну перестройку, без многократного «тыкания». +var clickOff, refined, prom, e, dLock: Double; begin - FBeaconManualSeed := True; - FBeaconSearchTicks := 0; - FBeaconCorrCooldown := BEACON_SETTLE_TICKS; // дать FFT устаканиться после сида - clickOff := DisplayHz - FCenterFreq; - if MeasureBeaconFFT(clickOff, refined, prom) then - FBeaconTrackHz := FCenterFreq + refined - else - FBeaconTrackHz := DisplayHz; if not FBeaconLockOn then SetBeaconLock(True); + FBeaconManualSeed := True; + clickOff := DisplayHz - FCenterFreq; + if MeasureBeaconFFT(clickOff, BEACON_SEARCH_HZ, refined, prom) 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; + end; end; procedure TRadioController.SetBeaconLock(On_: Boolean); @@ -1099,61 +1124,118 @@ begin FBeaconLockOn := On_; if On_ then begin - FBeaconSearchTicks := 0; FBeaconCorrCooldown := 0; + FBeaconSaveCooldown := 0; FBeaconManualSeed := False; // авто до первого клика пользователя FBeaconLocked := False; + FBeaconPromDB := 0.0; + FBeaconLockHz := 0.0; // сессионный аккумулятор индикации (LOError уже несёт калибровку) // Начальная область — у опорной частоты (уточняем пик, если виден). - if MeasureBeaconFFT(FBeaconRefHz - FCenterFreq, refined, prom) then + if MeasureBeaconFFT(FBeaconRefHz - FCenterFreq, BEACON_SEARCH_HZ, refined, prom) then FBeaconTrackHz := FCenterFreq + refined else FBeaconTrackHz := FBeaconRefHz; - end; - // При выключении FBeaconLockHz замораживаем (как калибровка LNB). + end + else + PersistBeaconCalibration; // фиксируем накопленную коррекцию в LOError (см. метод) Changed(rfBeaconLock); end; procedure TRadioController.ServiceBeaconLock; -// Одна итерация контура (UI-таймер @10 Гц). Меряет пик маяка по display-FFT, -// сглаживает, и (после ручного клика) тянет FBeaconLockHz так, чтобы маяк -// оставался на опорной частоте → весь downlink стабилен. Без NCO/FLL. -var pk, prom, measDisp, e: Double; +// Одна итерация контура (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; begin if not FBeaconLockOn then Exit; if FCurrentXvtr < 0 then Exit; // только в трансвертер-режиме - // Окно поиска — вокруг ТЕКУЩЕЙ display-позиции маяка, пересчитанной в baseband - // через актуальный FCenterFreq. Поэтому сдвиг водопада/центра окно не ломает. - if not MeasureBeaconFFT(FBeaconTrackHz - FCenterFreq, pk, prom) then Exit; + 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 + begin + Changed(rfBeaconLock); // лепесток не виден/не маяк — держим позицию и лок + Exit; + end; + cand := FCenterFreq + pk; - // Сопровождаем центроид плавно (в display-частоте; стабильность важнее скорости). - FBeaconTrackHz := FBeaconTrackHz + 0.12 * ((FCenterFreq + pk) - FBeaconTrackHz); - FBeaconPromDB := FBeaconPromDB + 0.2 * (prom - FBeaconPromDB); + // 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 отстаёт от ретюна LO на - // задержку пайплайна, и спам-коррекции дают перелёт (маяк улетает из окна). - // После каждой коррекции ждём BEACON_SETTLE_TICKS, давая FFT отразить новый LO. - // Слежение за пиком (выше) идёт каждый тик — окно едет за маяком. + // Коррекция: НЕ каждый тик (display-FFT отстаёт от ретюна → перелёт). После + // коррекции ждём BEACON_SETTLE_TICKS. Только после ручного клика (manual seed). if FBeaconCorrCooldown > 0 then Dec(FBeaconCorrCooldown) else if FBeaconLocked and FBeaconManualSeed then begin - // e = видимое_смещение_маяка − опорное. d(e)/d(FBeaconLockHz) = −1, - // поэтому FBeaconLockHz += e подтягивает маяк к опорной (см. вывод знака). - measDisp := FBeaconTrackHz; - e := measDisp - FBeaconRefHz; + // e = видимое_смещение_маяка − опорное. d(display)/d(LOError) = −1, поэтому + // LOError += e тянет маяк к опорной; feed-forward предсказывает новую + // display-позицию (track −= dLock), чтобы окно ехало за маяком. + e := FBeaconTrackHz - FBeaconRefHz; if Abs(e) > BEACON_DEADBAND_HZ then begin - FBeaconLockHz := FBeaconLockHz + BEACON_GAIN * e; - PushNetworkState; // ретюн LNB-IF: новый LO = XvtrTranslate(центр) + dLock := BEACON_GAIN * e; + ApplyBeaconLOError(dLock); // в LOError + ретюн + троттл-сохранение + FBeaconTrackHz := FBeaconTrackHz - dLock; FBeaconCorrCooldown := BEACON_SETTLE_TICKS; end; end; Changed(rfBeaconLock); end; +procedure TRadioController.ApplyBeaconLOError(dLock: Double); +// Складывает коррекцию маяка ПРЯМО в LOError активного трансвертера — это и есть +// живая калибровка дрейфа LNB: сразу видна в настройках, переживает рестарт. +// FBeaconLockHz — лишь сессионный аккумулятор для индикации. Запись на диск +// троттлим (BEACON_SAVE_TICKS): коррекции редки (deadband), но при долгом прогреве +// LNB их много — не молотим диск каждую. +begin + if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit; + FXvtrSettings.Entries[FCurrentXvtr].LOError := + FXvtrSettings.Entries[FCurrentXvtr].LOError + dLock; + FBeaconLockHz := FBeaconLockHz + dLock; // сколько накрутили за сессию (индикация) + PushNetworkState; // ретюн: XvtrTranslate читает новый LOError + if (FBeaconSaveCooldown = 0) and FDevConnected then + begin + FSettings.SaveXvtr(FDevMAC, FXvtrSettings); + FBeaconSaveCooldown := BEACON_SAVE_TICKS; + end; +end; + +procedure TRadioController.PersistBeaconCalibration; +// Коррекция уже живёт в LOError (ApplyBeaconLOError). Здесь — финальный flush на +// диск при выключении лока, на случай неотправленной из-за троттлинга. +begin + if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit; + if FDevConnected then FSettings.SaveXvtr(FDevMAC, FXvtrSettings); + FBeaconSaveCooldown := 0; +end; + function TRadioController.BeaconState: TBeaconState; begin if not FBeaconLockOn then Exit(bsIdle); diff --git a/SettingsForm.pas b/SettingsForm.pas index fd22279..80aea30 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -3211,7 +3211,9 @@ begin FXvtrBegin[i] := MakeFlt(Grp, BEG_X, Y, BEG_W, 0.0, 50000.0, 0.001, 3); FXvtrEnd[i] := MakeFlt(Grp, END_X, Y, END_W, 0.0, 50000.0, 0.001, 3); FXvtrLOOff[i] := MakeFlt(Grp, LO_X, Y, LO_W, -50000.0, 50000.0, 0.001, 6); - FXvtrLOErr[i] := MakeFlt(Grp, ERR_X, Y, ERR_W, -99999.0, 99999.0, 1.0, 0); + // диапазон ±10 МГц: QO-100 LNB без опоры легко уходит на сотни кГц, и + // beacon-lock складывает свою коррекцию сюда (живая калибровка LNB). + FXvtrLOErr[i] := MakeFlt(Grp, ERR_X, Y, ERR_W, -9999999.0, 9999999.0, 1.0, 0); FXvtrRXGain[i]:= MakeFlt(Grp, GAIN_X, Y, GAIN_W, -60.0, 60.0, 0.1, 1); // Чекбокс RX Only центрируем FXvtrRXOnly[i]:= MakeChk(Grp, RXO_X + (RXO_W - 22) div 2, Y + 2); diff --git a/SpectrumView.pas b/SpectrumView.pas index 96bb578..0e01336 100644 --- a/SpectrumView.pas +++ b/SpectrumView.pas @@ -76,7 +76,7 @@ type // ── QO-100 beacon markers (диагностика лока) ────────────────────────────── FBeaconMarkActive: Boolean; FBeaconRefFreq: Double; // опорная частота маяка (где ДОЛЖЕН быть) - FBeaconTrkFreq: Double; // где сидит трекер (NCO) сейчас + FBeaconTrkFreq: Double; // отслеживаемый центроид маяка сейчас // ── Spectrum buffer ─────────────────────────────────────────────────────── FSpectrumBuf: array[0..1023] of Single; FSpectrumBufCount: Integer; @@ -502,9 +502,9 @@ begin end; procedure TSpectrumView.DrawBeaconMarkers(C: TCanvas; W, H: Integer); -// Две вертикали: опорная частота маяка (зелёная пунктир) и текущая позиция -// трекера (оранжевая сплошная). Расхождение видно глазом → понятно, на пик -// сел трекер или на шум. +// Две вертикали: опорная частота маяка (зелёная пунктир) и отслеживаемый +// центроид (оранжевая сплошная). Расхождение видно глазом → понятно, сел ли +// контур на пик маяка или на шум. procedure Vline(FreqHz: Double; Col: TColor; Style: TPenStyle); var x: Integer; begin @@ -516,7 +516,7 @@ procedure TSpectrumView.DrawBeaconMarkers(C: TCanvas; W, H: Integer); end; begin Vline(FBeaconRefFreq, clLime, psDot); // где маяк ДОЛЖЕН быть - Vline(FBeaconTrkFreq, TColor($000AA5FF), psSolid); // где сидит трекер (оранж) + Vline(FBeaconTrkFreq, TColor($000AA5FF), psSolid); // отслеживаемый центроид (оранж) C.Pen.Style := psSolid; end; diff --git a/SpectrumViewOpengl.pas b/SpectrumViewOpengl.pas index d8a8de7..43205e1 100644 --- a/SpectrumViewOpengl.pas +++ b/SpectrumViewOpengl.pas @@ -658,7 +658,7 @@ begin end; procedure TSpectrumViewOpenGL.DrawBeaconMarkersGL(W, H: Integer); -// QO-100 beacon-маркеры: опорная частота (зелёная) + позиция трекера (оранж). +// QO-100 beacon-маркеры: опорная частота (зелёная) + отслеживаемый центроид (оранж). procedure Vline(FreqHz: Double; Col: TColor); var x: Integer; begin @@ -669,7 +669,7 @@ procedure TSpectrumViewOpenGL.DrawBeaconMarkersGL(W, H: Integer); end; begin Vline(FBeaconRefFreq, clLime); // где маяк ДОЛЖЕН быть - Vline(FBeaconTrkFreq, TColor($000AA5FF)); // где сидит трекер (оранж) + Vline(FBeaconTrkFreq, TColor($000AA5FF)); // отслеживаемый центроид (оранж) end; procedure TSpectrumViewOpenGL.DrawADCOverlay(W, H: Integer); diff --git a/StatusBar.pas b/StatusBar.pas index 2a6c2cf..5616fe4 100644 --- a/StatusBar.pas +++ b/StatusBar.pas @@ -33,7 +33,8 @@ const PAD_X = 8; SEQ_MIN_W = 90; STATUS_ORDER: array[0..7] of Integer = (2, 0, 1, 3, 7, 4, 5, 6); - BASE_W: array[0..7] of Integer = (280, 160, 170, 150, 190, 180, 130, 90); + // поле 7: PLL (openHPSDR) ИЛИ статус QO-100 beacon lock (Pluto) — шире под "BCN LOCK …". + BASE_W: array[0..7] of Integer = (280, 160, 170, 150, 190, 180, 130, 200); INITIAL_TEXT: array[0..7] of string = ( 'Board --', 'IP --', diff --git a/WDSPEngine.pas b/WDSPEngine.pas index af363f1..0fc15a9 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -25,7 +25,7 @@ interface uses Classes, SysUtils, Math, SyncObjs, - WDSP, BeaconLock; + WDSP; const // WDSP mode integers (нет именованных констант в WDSP.pas) @@ -292,7 +292,6 @@ type FMuted: Boolean; FVolume: Double; FLastError: string; - FBeaconTracker: TBeaconTracker; // QO-100 beacon lock (НЕ владеем; см. контроллер) function ModeToWDSP(Mode: Integer): Integer; procedure ApplyDefaultFilter; @@ -355,9 +354,6 @@ type procedure UpdateAGCLines(SpectrumW: Integer); // читает hang/thresh из WDSP для линии на спектре procedure SetSpectrumWidth(W: Integer); // обновляет FLastSpectrumW для корректных AGC линий procedure SetShift(ShiftHz: Double); // CTUN NCO сдвиг - // QO-100 beacon lock: трекер кормится RX-IQ из DSP-потока. Владелец — - // контроллер; движок лишь подаёт сэмплы и сообщает sample rate. - procedure SetBeaconTracker(T: TBeaconTracker); procedure SetNRMode(Mode: Integer); procedure SetNR(Enable: Boolean); procedure SetNBMode(Mode: Integer); @@ -1123,7 +1119,6 @@ begin // Обновляем параметры FSampleRate := NewRate; FBufSize := FAudioBufSize * NewRate div FAudioRate; - if FBeaconTracker <> nil then FBeaconTracker.Configure(FSampleRate); // Перераспределяем буферы под новый размер SetLength(FRXIn, FBufSize * 2); @@ -1218,11 +1213,6 @@ begin FeedDisplaySample(IR * SCALE, QR * SCALE); - // QO-100 beacon lock: тот же baseband-IQ @ FSampleRate (трекер сам - // мгновенно выходит, если лок не активен — стоимость ~1 проверка/сэмпл). - if FBeaconTracker <> nil then - FBeaconTracker.Feed(IR * SCALE, QR * SCALE); - FRXAccI[FRXAccPos] := IR * SCALE; FRXAccQ[FRXAccPos] := QR * SCALE; Inc(FRXAccPos); @@ -1381,12 +1371,6 @@ begin UpdateAGCLines(FLastSpectrumW); end; -procedure TWDSPEngine.SetBeaconTracker(T: TBeaconTracker); -begin - FBeaconTracker := T; - if (T <> nil) and (FSampleRate > 0) then T.Configure(FSampleRate); -end; - procedure TWDSPEngine.SetShift(ShiftHz: Double); begin FShiftHz := ShiftHz;