feat: QO-100 beacon lock — robustness, persistence, FT8-aware UI

Builds on the beacon-lock baseline: keeps the proven display-FFT control
path (hardware-LO trim folded into the active transverter LOError), and
hardens it. The narrowband NCO/FLL scaffold (BeaconLock.pas) is removed —
it was never in the control path.

- RadioController:
  * Persist live: each correction folds straight into the active xvtr
    LOError (visible in Settings, survives restart as LNB calibration);
    disk writes throttled. FBeaconLockHz is now only a session indicator.
  * Robust tracking vs nearby signals: narrow ±2 kHz gate while locked
    (±6 kHz only for acquisition), slew-rate outlier rejection (a sudden
    centroid jump = interferer, not slow LNB drift → ignored), and lobe
    shape validation in MeasureBeaconFFT (width + power symmetry) so a CW
    carrier / one-sided neighbour isn't mistaken for the BPSK beacon.
    Prominence measured over the in-window noise floor (gate-width robust).
  * Fast convergence: feed-forward (predict post-retune position so the
    window follows the beacon through the jump) + near-deadbeat correction
    on the manual click → one click locks instead of repeated tapping.
  * Single central BPSK beacon (10489.750); dead CoarseBeaconOffset and
    unused throttle counters removed; stale NCO/FLL/4 Hz comments fixed.
- WDSPEngine: drop the per-sample TBeaconTracker RX-IQ tap (tracker gone).
- MainForm: BEACON is now a plain button, shown only on Pluto in a
  transverter. Lock status (off / click / search / LOCK + correction +
  prominence) moves to status-bar field 7 in place of PLL on Pluto;
  openHPSDR keeps PLL there.
- SettingsForm/StatusBar: widen LO Error field to ±10 MHz (QO-100 LNBs
  drift hundreds of kHz) and widen status field 7 for the beacon text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 10:02:48 +03:00
co-authored by Claude Opus 4.8
parent 42610b304e
commit 4728e02459
8 changed files with 277 additions and 545 deletions
+47 -45
View File
@@ -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;