feat: QO-100 beacon lock — stabilise LNB drift to NB transponder beacon

Click the beacon on the spectrum; the controller tracks it and continuously
trims the transverter LO so the whole downlink stays put as the LNB drifts.

- BeaconLock.pas: TBeaconTracker scaffold (narrowband NCO/FLL) — kept but no
  longer in the control path (FLL diverged on the suppressed-carrier BPSK
  beacon + Pluto DC offset). Measurement now uses the proven display-FFT path.
- RadioController: FBeaconLockHz folded into XvtrTranslate (on top of LOError).
  MeasureBeaconFFT finds the beacon by POWER-WEIGHTED CENTROID of the lobe
  (BPSK has a suppressed carrier → broad symmetric ~2.4 kHz lobe, not a line;
  a peak-finder jitters, the centroid sits on the carrier). Track position in
  ABSOLUTE display Hz (FBeaconTrackHz) so scrolling the waterfall/centre never
  moves the search window off the beacon. Closed loop: e = tracked - ref,
  d(e)/d(LockHz) = -1 -> LockHz += gain*e; settle cooldown after each correction
  (display-FFT lags the LO retune -> avoids overshoot); deadband 75 Hz +
  centroid/prominence smoothing + lock hysteresis for a steady lock.
  Correction applied only after the user clicks (manual seed), so a crowded
  band can't pull a neighbour to the reference.
- WDSPEngine: SetBeaconTracker + RX-IQ tap (tracker idle now, ~1 check/sample).
- SpectrumView + GL: beacon markers — green = reference (10489.750), orange =
  tracked centroid.
- MainForm: BEACON button (RX block, XVTR-only). Flow: BEACON -> "CLICK BCN"
  -> click beacon -> "L<corr> /<prom>". ServiceBeaconLock driven by MeterTimer.

Reference defaults to the middle BPSK beacon 10489.750. Tested on-air against
Es'hail-2: locks and holds, survives waterfall drag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 11:04:14 +03:00
co-authored by Claude Opus 4.8
parent 40dca01c40
commit 42610b304e
6 changed files with 764 additions and 5 deletions
+86 -1
View File
@@ -43,7 +43,7 @@ uses
PowerInhibit,
DeviceStore,
RadioBackend, PlutoBackend,
RadioController, ChannelController;
RadioController, ChannelController, BeaconLock;
const
CLR_BG = TColor($00101010);
@@ -134,6 +134,7 @@ type
FSpecDrag: Boolean;
FSpecDragX0: Integer; // X при нажатии
FSpecDragFreq: Double; // FCenterFreq при нажатии
FBeaconArming: Boolean; // ждём клик по маяку (после включения BEACON)
FSpectrumDirty: Boolean; // таймер должен перерисовать спектр/водопад
// TX DUC-очередь (FDUCPending*) переехала в TRadioController (OnTXIQ).
FSMeterPeak: Double; // верхняя граница светлой зоны
@@ -237,6 +238,7 @@ type
PanelFilter: TPanel;
BtnFilter: array[0..FILT_COUNT-1] of TFlatButton;
BtnCTun: TFlatButton;
BtnBeacon: TFlatButton; // QO-100 beacon lock (видим только в XVTR-режиме)
BtnDUP: TFlatButton;
PanelFMSQ: TPanel;
BtnFMSQ: TFlatButton;
@@ -377,6 +379,8 @@ type
procedure BtnModeClick(Sender: TObject);
procedure BtnFilterClick(Sender: TObject);
procedure BtnCTunClick(Sender: TObject);
procedure BtnBeaconClick(Sender: TObject);
procedure UpdateBeaconButton;
procedure BtnDUPClick(Sender: TObject);
procedure ApplyDUP(Active: Boolean);
procedure BtnTUNClick(Sender: TObject);
@@ -1190,6 +1194,12 @@ begin
if BtnXvtrBand[i] <> nil then
StyleButton(BtnXvtrBand[i], i = FController.FCurrentXvtr);
UpdateFreqDisplayMax; // трансвертер может быть >6 ГГц (QO-100)
// BEACON-кнопка актуальна только в трансвертер-режиме (QO-100 lock).
if BtnBeacon <> nil then
begin
BtnBeacon.Visible := FController.FCurrentXvtr >= 0;
UpdateBeaconButton;
end;
// Wideband-вид + web-зеркало xvtr (ранее в обёртках Activate/Deactivate).
// rfXvtr эмитят ТОЛЬКО ActivateXvtr/DeactivateXvtr — состояние финальное.
UpdateWidebandFrequencyView;
@@ -1252,6 +1262,8 @@ begin
FSpecView.DrawSpectrum;
PbSpectrum.Invalidate;
end;
rfBeaconLock:
UpdateBeaconButton; // статус/коррекция QO-100 beacon lock
rfCTun:
begin
StyleButton(BtnCTun, FController.FCTun);
@@ -1694,6 +1706,15 @@ begin
FChannelsDropDown.OnSelect := OnChannelsDropDownSelect;
RefreshChannelsDropDown;
// QO-100 beacon lock — 3-я колонка ряда. Видима только в XVTR-режиме
// (см. рендер rfXvtr ниже). Подпись/подсветка обновляются по rfBeaconLock.
BtnBeacon := MakeBtn(PanelRXBlock, 'BEACON',
LeftPanelButtonLeft(LEFT_W, 3, 2), 16,
LeftPanelButtonWidth(LEFT_W, 3, 2),
BTN_H, BtnBeaconClick);
BtnBeacon.Visible := False;
StyleButton(BtnBeacon, False);
MakeLbl(PanelRXBlock, 'VOL', 4, 50);
TrkVolume := TFlatSlider.Create(Self);
TrkVolume.Parent := PanelRXBlock; TrkVolume.Left := 34; TrkVolume.Top := 46;
@@ -2946,6 +2967,8 @@ begin
Exit;
end;
// QO-100 beacon lock: одна итерация контура (no-op если лок выключен).
FController.ServiceBeaconLock;
// Сглаженное среднее (EMA)
FSMeterAvg := FSMeterAvg * (1.0 - AVG_ALPHA) + FController.FLastSMeter * AVG_ALPHA;
@@ -3864,6 +3887,59 @@ begin
FController.SetCTun(not FController.FCTun);
end;
procedure TMainForm.BtnBeaconClick(Sender: TObject);
begin
FController.SetBeaconLock(not FController.BeaconLockEnabled);
// При включении — «вооружаем» клик: следующий ЛКМ по спектру укажет маяк
// (надёжнее автопоиска в забитой полосе). Авто-сид работает до клика.
FBeaconArming := FController.BeaconLockEnabled;
UpdateBeaconButton;
end;
procedure TMainForm.UpdateBeaconButton;
// Подпись + подсветка кнопки BEACON по состоянию лока. Зовётся из BtnBeaconClick
// и из рендера rfBeaconLock (контур обновляет статус каждый тик).
var
cap: string;
corr: Double;
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;
BtnBeacon.Repaint;
end;
// ---------------------------------------------------------------------------
// DUP toggle (Thetis-style local display mode — без аппаратной команды)
// ---------------------------------------------------------------------------
@@ -4057,6 +4133,15 @@ begin
if Button = mbLeft then
begin
// Наведение beacon-lock: ЛКМ указывает на маяк (вместо перестройки VFO).
if FBeaconArming and (PbSpectrum.Width > 0) then
begin
FController.BeaconSeedAtHz(
PixelToFreq(X, PbSpectrum.Width, FController.FCenterFreq, FController.FSpanHz));
FBeaconArming := False;
UpdateBeaconButton;
Exit;
end;
// Левая кнопка: сбрасываем маркер если активен
if FSpecView.MarkerActive then
begin