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
+17 -1
View File
@@ -25,7 +25,7 @@ interface
uses
Classes, SysUtils, Math, SyncObjs,
WDSP;
WDSP, BeaconLock;
const
// WDSP mode integers (нет именованных констант в WDSP.pas)
@@ -292,6 +292,7 @@ type
FMuted: Boolean;
FVolume: Double;
FLastError: string;
FBeaconTracker: TBeaconTracker; // QO-100 beacon lock (НЕ владеем; см. контроллер)
function ModeToWDSP(Mode: Integer): Integer;
procedure ApplyDefaultFilter;
@@ -354,6 +355,9 @@ 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);
@@ -1119,6 +1123,7 @@ begin
// Обновляем параметры
FSampleRate := NewRate;
FBufSize := FAudioBufSize * NewRate div FAudioRate;
if FBeaconTracker <> nil then FBeaconTracker.Configure(FSampleRate);
// Перераспределяем буферы под новый размер
SetLength(FRXIn, FBufSize * 2);
@@ -1213,6 +1218,11 @@ 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);
@@ -1371,6 +1381,12 @@ 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;