feat: QO-100 beacon lock in web UI + headless daemon

Mirror the desktop QO-100 beacon lock into the web interface and make it
work in the headless daemon.

Backend:
- WebServer: set_beacon/beacon_seed commands (+OnBeacon/OnBeaconSeed
  events), beacon state fields, SetBeaconStatus, and beacon_visible/on/
  text/ref_hz/track_hz in BuildStateJson. beacon_seed carries 'frac'
  (0..1 click position); absolute Hz is computed controller-side from
  live FCenterFreq/FSpanHz (mirrors desktop PixelToFreq).
- WebAdapter: OnBeacon->SetBeaconLock, OnBeaconSeed->BeaconSeedAtHz;
  PushState mirrors compact status (matches MainForm.BeaconStatusText)
  into the PLL status cell when visible (Pluto + active XVTR).
- MainForm/ewsdrd: wire the two callbacks.
- ewsdrd: call ServiceBeaconLock in the main loop @~10Hz (FRunning+
  FWDSPReady gated). Without this the lock loop never ran headless.

Frontend (WebPageHtml):
- BCN button (visible only on Pluto+XVTR), highlighted while locked.
- Seed-on-mousedown with immediate return (like desktop FormMouseDown):
  no drag/set_center so the gesture never moves the center / tears IQ.
  Armed (first click after BCN) or Shift, mirroring desktop arm/Shift.
- PLL status cell shows beacon status (field-7 parity with desktop).
- Ref (green) + tracked (orange) beacon markers on spectrum/waterfall.

Known issue (unresolved): after lock the beacon can slowly drift off and
drop to "BCN sync". Suspected residual-sign anti-drift or retune-timing
on the web/daemon path; needs on-air diagnostics. See memory
project_web_beacon for the investigation state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 16:53:02 +03:00
co-authored by Claude Opus 4.8
parent 8472c8e7b8
commit 0fbe5d5edd
5 changed files with 144 additions and 7 deletions
+50
View File
@@ -66,6 +66,9 @@ type
procedure SyncMOX;
procedure SyncTun;
procedure SyncFMStep;
procedure SyncBeacon;
procedure SyncBeaconSeed;
function BeaconStatusText: string; // компактный статус (как десктоп поле 7)
public
constructor Create(AController: TRadioController; AServer: TWebServer;
AHost: IWebHost);
@@ -97,6 +100,9 @@ type
procedure OnMOX(On_: Boolean);
procedure OnTun(On_: Boolean);
procedure OnFMStep(Idx: Integer);
// QO-100 beacon: вкл/выкл лок и наведение по клику в спектре (frac 0..1).
procedure OnBeacon(On_: Boolean);
procedure OnBeaconSeed(Frac: Double);
procedure OnClientActiveChanged(Active: Boolean);
// TX-mic от web-клиента → WDSP. Вызывается из WS-потока; без маршалинга —
// подача в движок thread-safe (как OnMicPacket/OnDDCIQ контроллера).
@@ -344,6 +350,29 @@ begin
FController.SaveCurrentBand;
end;
procedure TWebAdapter.OnBeacon(On_: Boolean);
begin FSyncBool := On_; FController.Invoke(@SyncBeacon); end;
procedure TWebAdapter.SyncBeacon;
begin
// Вкл/выкл лок маяка (= запуск декодера + трим LOError). Идемпотентно.
FController.SetBeaconLock(FSyncBool);
end;
procedure TWebAdapter.OnBeaconSeed(Frac: Double);
begin FSyncFreq := Frac; FController.Invoke(@SyncBeaconSeed); end;
procedure TWebAdapter.SyncBeaconSeed;
var Hz: Double;
begin
// Frac (доля 0..1 по ширине спектра) → абс. display-Hz от ЖИВЫХ центра/спана
// контроллера (зеркалит десктоп PixelToFreq(X,W,FCenterFreq,FSpanHz)). Так
// наведение точно, даже когда LO ретюнится локом и клиентский center отстаёт.
if (FSyncFreq < 0) or (FSyncFreq > 1) then Exit;
Hz := FController.FCenterFreq + (FSyncFreq - 0.5) * FController.FSpanHz;
FController.BeaconSeedAtHz(Hz);
end;
procedure TWebAdapter.OnClientActiveChanged(Active: Boolean);
// Из потока веб-сервера при connect/disconnect клиента. Простая запись флага —
// ядро (SetMOX, в т.ч. HWPTT-путь) выбирает по нему mic-source. Без маршалинга.
@@ -387,6 +416,19 @@ end;
// ── Исходящее зеркало состояния ──────────────────────────────────────────────
function TWebAdapter.BeaconStatusText: string;
// Зеркалит TMainForm.BeaconStatusText (поле 7 десктопа). Pluto в трансвертере.
begin
if not FController.BeaconLockEnabled then Exit('BCN: off');
if FController.BeaconDecodeFreqHz <= 0 then Exit('BCN: click beacon');
case FController.BeaconState of
bsLock: Result := Format('BCN LOCK %.0fHz /%.0fdB',
[FController.BeaconCorrectionHz, FController.BeaconSNRdB]);
bsSearch: Result := Format('BCN sync /%.0fdB', [FController.BeaconSNRdB]);
else Result := 'BCN --';
end;
end;
procedure TWebAdapter.PushState;
var
StatusText, BoardText, IPText, SupplyText: string;
@@ -456,6 +498,14 @@ begin
else
SeqText := 'SEQ --';
// QO-100 beacon lock — отдельный канал состояния (видим только на Pluto в XVTR).
FServer.SetBeaconStatus(
FController.IsPluto and (FController.FCurrentXvtr >= 0),
FController.BeaconLockEnabled,
BeaconStatusText,
FController.BeaconRefHz,
FController.BeaconTrackedFreqHz);
FServer.PushSpectrum(
FController.FSpectrumBuf, 1024,
FController.FWaterfallBuf,