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
+2
View File
@@ -872,6 +872,8 @@ begin
FWebServer.OnFreqA := FWebAdapter.OnFreqA;
FWebServer.OnWebMic := FWebAdapter.OnMic;
FWebServer.OnXvtrBand := FWebAdapter.OnXvtrBand;
FWebServer.OnBeacon := FWebAdapter.OnBeacon;
FWebServer.OnBeaconSeed := FWebAdapter.OnBeaconSeed;
FWebServer.OnConnect := WebOnConnect;
FWebServer.OnDiscoverDev := WebOnDiscover;
FWebServer.OnDisconnectDev := WebOnDisconnect;
+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,
+22 -4
View File
@@ -203,6 +203,7 @@ begin
'<button id="snbBtn">SNB</button>' +
'<button id="anfBtn">ANF</button>' +
'<button id="nrBtn">NR</button>' +
'<button id="bcnBtn" title="QO-100 beacon lock" style="display:none">BCN</button>' +
'<span class="sl-sep">|</span>' +
'<button id="devBtn" title="Devices">DEV</button>' +
'<button id="startStopBtn" class="on">START</button>' +
@@ -264,6 +265,8 @@ begin
'let localCenter=14200000,localSpan=192000;' + // локальные копии — обновляются мгновенно при действии пользователя
'let dragging=false,dragX0=0,dragCen0=0,dragVfo0=0,dragMs=0,wheelActiveTs=0;' +
'let hoverX=-1,wTarget="",markerOn=false,markerX=0;' +
'let beaconArm=false;' + // одноразовое наведение после включения BCN (как десктоп)
'let specSm=[],wfAvg=[],wfHi=-50,wfLo=-120;' +
'let audioCtx=null,audioT=0;' +
@@ -278,7 +281,7 @@ begin
'function sb(id,txt){const e=q(id);if(e){e.textContent=txt||"";e.title=txt||"";}}' +
'function fmtTime(d,utc){const p=n=>String(n).padStart(2,"0");return(utc?p(d.getUTCHours()):p(d.getHours()))+":"+p(utc?d.getUTCMinutes():d.getMinutes())+":"+p(utc?d.getUTCSeconds():d.getSeconds());}' +
'function updClock(){const d=new Date();sb("sbUTC","UTC "+fmtTime(d,true));sb("sbLocal","Local "+fmtTime(d,false));}' +
'function updStatusBar(){sb("stEl",cur.status_text||((cur.connected?"OK":"OFF")+" | "+(cur.running?"RUN":"STOP")));sb("sbBoard",cur.board_text||"Board --");sb("sbIP",cur.ip_text||"IP --");sb("sbSupply",cur.supply_text||"Supply --");sb("sbPLL",cur.pll_text||"PLL --");sb("sbRX",cur.rx_text||"RX idle");sb("sbTX",cur.tx_text||"TX idle");sb("sbSEQ",cur.seq_text||"SEQ --");}' +
'function updStatusBar(){sb("stEl",cur.status_text||((cur.connected?"OK":"OFF")+" | "+(cur.running?"RUN":"STOP")));sb("sbBoard",cur.board_text||"Board --");sb("sbIP",cur.ip_text||"IP --");sb("sbSupply",cur.supply_text||"Supply --");sb("sbPLL",cur.beacon_visible?(cur.beacon_text||"BCN --"):(cur.pll_text||"PLL --"));sb("sbRX",cur.rx_text||"RX idle");sb("sbTX",cur.tx_text||"TX idle");sb("sbSEQ",cur.seq_text||"SEQ --");}' +
'updClock();setInterval(updClock,1000);' +
'function ws2(obj){if(ws&&ws.readyState===1)ws.send(JSON.stringify(obj));}' +
'function cmd(c,v){ws2({cmd:c,on:v});}' +
@@ -286,6 +289,8 @@ begin
'function updNbBtn(){const b=q("nbBtn");if(!b)return;const m=cur.nb_mode|0;b.textContent=(m===2)?"NB2":"NB";sw("nbBtn",m!==0);}' +
'function updSnbBtn(){sw("snbBtn",!!cur.snb);}' +
'function updAnfBtn(){sw("anfBtn",!!cur.anf);}' +
'function updBcnBtn(){const b=q("bcnBtn");if(!b)return;b.style.display=cur.beacon_visible?"":"none";sw("bcnBtn",!!cur.beacon_on);}' +
'const MODE_N=["LSB","USB","DSB","CWL","CWU","FM","AM","SAM"];' +
'const GRP_MAP={ssb:[0,1],cw:[3,4],fm:[5,5],am:[6,7]};' +
@@ -441,6 +446,7 @@ begin
// DSP кнопки — cur обновляется локально сразу, не ждём ответа сервера
'q("nrBtn").onclick=function(){cur.nr_mode=((cur.nr_mode|0)+1)%5;cur.nr=cur.nr_mode!==0;updNrBtn();pend("nr");ws2({cmd:"set_nr_mode",mode:cur.nr_mode});};' +
'q("nbBtn").onclick=function(){cur.nb_mode=((cur.nb_mode|0)+1)%3;cur.nb=cur.nb_mode!==0;updNbBtn();pend("nb");ws2({cmd:"set_nb_mode",mode:cur.nb_mode});};' +
'q("bcnBtn").onclick=function(){cur.beacon_on=!cur.beacon_on;beaconArm=cur.beacon_on;updBcnBtn();pend("beacon");cmd("set_beacon",cur.beacon_on);};' +
'q("snbBtn").onclick=function(){cur.snb=!cur.snb;updSnbBtn();pend("snb");cmd("set_snb",cur.snb);};' +
'q("anfBtn").onclick=function(){cur.anf=!cur.anf;updAnfBtn();pend("anf");cmd("set_anf",cur.anf);};' +
@@ -675,7 +681,7 @@ begin
'function isPending(field){return !!(pendingCmds[field]&&pendingCmds[field]>Date.now());}' +
// Маппинг: server JSON field → pend key
'var PEND_MAP={vfo_a_hz:"freq",vfo_b_hz:"freq",nr:"nr",nr_mode:"nr",nb:"nb",nb_mode:"nb",snb:"snb",anf:"anf",running:"run",mode:"mode",agc_mode:"agc",transmitting:"mox",drive:"drive",attn_idx:"attn",tuning:"tun",filter_bw:"filter",mute:"mute"};' +
'var PEND_MAP={vfo_a_hz:"freq",vfo_b_hz:"freq",nr:"nr",nr_mode:"nr",nb:"nb",nb_mode:"nb",snb:"snb",anf:"anf",running:"run",mode:"mode",agc_mode:"agc",transmitting:"mox",drive:"drive",attn_idx:"attn",tuning:"tun",filter_bw:"filter",mute:"mute",beacon_on:"beacon"};' +
'function applyState(msg){' +
'Object.keys(msg).forEach(function(k){var pk=PEND_MAP[k];if(!pk||!isPending(pk))cur[k]=msg[k];});' +
@@ -721,6 +727,7 @@ begin
'const rs=q("rfSl");if(rs&&!rs.matches(":active")){rs.value=cur.agc_top||90;var rv=q("rfVal");if(rv)rv.textContent=(cur.agc_top||90)+"dB";}' +
'if(!isPending("nr"))updNrBtn();if(!isPending("nb"))updNbBtn();' +
'if(!isPending("snb"))updSnbBtn();if(!isPending("anf"))updAnfBtn();' +
'if(!isPending("beacon"))updBcnBtn();' +
'if(!isPending("run"))updRunBtn(!!cur.running);' +
'if(!isPending("mox"))updMoxBtn(!!cur.transmitting);' +
'if(!isPending("tun"))updToneBtn(!!cur.tuning);' +
@@ -741,6 +748,12 @@ begin
'function drawMk(ctx,w,h){if(!markerOn)return;ctx.save();ctx.strokeStyle="rgba(255,80,80,.85)";ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(markerX,0);ctx.lineTo(markerX,h);ctx.stroke();' +
'const lbl=((xToHz(markerX,w))/1e6).toFixed(4)+" M";ctx.fillStyle="rgba(35,5,5,.82)";ctx.fillRect(cl(markerX+5,0,w-88),h-16,84,13);ctx.fillStyle="#f88";ctx.font="10px monospace";ctx.fillText(lbl,cl(markerX+8,2,w-86),h-6);ctx.restore();}' +
// Маркеры QO-100 маяка: опорная (зелёная) + измеренная/наведение (оранжевая).
'function vbar(ctx,x,w,h,col){if(x<0||x>w)return;ctx.strokeStyle=col;ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(x+.5,0);ctx.lineTo(x+.5,h);ctx.stroke();}' +
'function drawBcn(ctx,w,h){if(!cur.beacon_on)return;ctx.save();' +
'if(cur.beacon_ref_hz>0)vbar(ctx,hzToX(cur.beacon_ref_hz,w),w,h,"rgba(80,255,120,.8)");' +
'if(cur.beacon_track_hz>0)vbar(ctx,hzToX(cur.beacon_track_hz,w),w,h,"rgba(255,170,60,.85)");' +
'ctx.restore();}' +
'function drawRuler(){const w=rulerCv.clientWidth,h=rulerCv.clientHeight;rctx.fillStyle="#0c1210";rctx.fillRect(0,0,w,h);' +
'const st=localCenter-localSpan/2;' +
@@ -775,7 +788,7 @@ begin
'sctx.lineTo(w,h);sctx.lineTo(0,h);sctx.closePath();sctx.fillStyle=g;sctx.fill();' +
'sctx.beginPath();for(let x=0;x<w;x++){const i=Math.floor(x*(bins.length-1)/Math.max(1,w-1));const y=cl(((-specSm[i]-20)/120)*h,0,h-1);x===0?sctx.moveTo(0,y):sctx.lineTo(x,y);}' +
'sctx.strokeStyle="#3ddd3d";sctx.lineWidth=1.2;sctx.stroke();' +
'const vx=hzToX(avf,w);sctx.strokeStyle="#60ff80";sctx.lineWidth=1;sctx.beginPath();sctx.moveTo(vx,0);sctx.lineTo(vx,h);sctx.stroke();drawMk(sctx,w,h);}' +
'const vx=hzToX(avf,w);sctx.strokeStyle="#60ff80";sctx.lineWidth=1;sctx.beginPath();sctx.moveTo(vx,0);sctx.lineTo(vx,h);sctx.stroke();drawBcn(sctx,w,h);drawMk(sctx,w,h);}' +
'function drawWf(bins){const w=wfCv.clientWidth,h=wfCv.clientHeight;' +
'if(!bins?.length){drawMk(wctx,w,h);return;}' +
@@ -795,7 +808,7 @@ begin
'wfBCtx.putImageData(row,0,0);wctx.drawImage(wfBuf,0,0,w,h);' +
'const avf2=activeVfoHz();const[fl2,fh2]=fltLH(),x1w=hzToX(avf2+fl2,w),x2w=hzToX(avf2+fh2,w);' +
'wctx.fillStyle=(cur.transmitting||cur.tuning)?"rgba(255,60,60,.15)":"rgba(220,230,230,.10)";wctx.fillRect(Math.min(x1w,x2w),0,Math.abs(x2w-x1w),h);' +
'const vxw=hzToX(avf2,w);wctx.strokeStyle="#60ff80";wctx.lineWidth=1;wctx.beginPath();wctx.moveTo(vxw,0);wctx.lineTo(vxw,h);wctx.stroke();drawMk(wctx,w,h);}' +
'const vxw=hzToX(avf2,w);wctx.strokeStyle="#60ff80";wctx.lineWidth=1;wctx.beginPath();wctx.moveTo(vxw,0);wctx.lineTo(vxw,h);wctx.stroke();drawBcn(wctx,w,h);drawMk(wctx,w,h);}' +
'function rcv(cv){const dpr=Math.max(1,devicePixelRatio||1),w=cv.clientWidth|0,h=cv.clientHeight|0;if(cv.width!==(w*dpr|0)||cv.height!==(h*dpr|0)){cv.width=w*dpr;cv.height=h*dpr;cv.getContext("2d").setTransform(dpr,0,0,dpr,0,0);}}' +
'function resize(){rcv(specCv);rcv(wfCv);rcv(rulerCv);const dpr=Math.max(1,devicePixelRatio||1),sw=smCv.clientWidth|0,sh=smCv.clientHeight|0;' +
@@ -832,6 +845,11 @@ begin
'audioKick();wTarget=cv.id;' +
'if(e.button===2){markerOn=Math.abs(markerX-e.offsetX)>=8||!markerOn;markerX=e.offsetX;return;}' +
'if(e.button!==0)return;' +
// Наведение маяка — на mousedown с немедленным return (как десктоп FormMouseDown):
// НЕ стартуем drag, чтобы не уехал set_center/VFO и не порвался IQ под локом.
// Только когда «вооружено» (1-й клик после BCN) или с Shift (перенаведение).
'if(cur.beacon_on&&(beaconArm||e.shiftKey)){beaconArm=false;' +
'ws2({cmd:"beacon_seed",frac:cl(e.offsetX/Math.max(1,cv.clientWidth),0,1)});return;}' +
'dragging=true;dragX0=e.offsetX;' +
'dragCen0=localCenter;' +
'dragVfo0=activeVfoHz();' +
+59 -2
View File
@@ -151,6 +151,9 @@ type
// XVTR-band: web клиент кликнул кнопку трансвертера (Idx 0..CFG_XVTR_COUNT-1).
// Idx=-1 — выход в HF.
TWebCmdXvtrBand = procedure(Idx: Integer) of object;
// QO-100 beacon lock: вкл/выкл лок и наведение (клик по маяку, доля 0..1 по ширине).
TWebCmdBeacon = procedure(On_: Boolean) of object;
TWebCmdBeaconSeed = procedure(Frac: Double) of object;
// Активность web-клиента изменилась (подключился/отключился последний клиент).
// Хозяин зеркалит это в ядро (FController.FWebClientActive) для выбора mic-source.
TWebClientActiveEvent = procedure(Active: Boolean) of object;
@@ -317,6 +320,16 @@ type
FTXText: string;
FSeqText: string;
// QO-100 beacon lock (Pluto в трансвертере). Зеркалится в state как
// beacon_visible/on/text/ref_hz/track_hz; команды set_beacon/beacon_seed.
FBeaconVisible: Boolean; // кнопка/статус активны (Pluto + XVTR)
FBeaconOn: Boolean; // лок включён (подсветка кнопки)
FBeaconText: string; // компактный статус (ячейка PLL, как десктоп)
FBeaconRefHz: Double; // опорная частота маяка (маркер)
FBeaconTrackHz: Double; // измеренная частота маяка (маркер)
FOnBeacon: TWebCmdBeacon;
FOnBeaconSeed: TWebCmdBeaconSeed;
FWebClientActive: Boolean;
FOnClientActiveChanged: TWebClientActiveEvent;
@@ -400,6 +413,11 @@ type
procedure SetRatePresets(const ARates: array of Integer);
procedure SetBands(const ANames: array of string; const AFreqs: array of Double);
procedure SetFreqRange(AMin, AMax: Double);
// QO-100 beacon lock: зеркало статуса в state (зовётся из PushState хозяина).
procedure SetBeaconStatus(AVisible, AOn: Boolean; const AText: string;
ARefHz, ATrackHz: Double);
property OnBeacon: TWebCmdBeacon read FOnBeacon write FOnBeacon;
property OnBeaconSeed: TWebCmdBeaconSeed read FOnBeaconSeed write FOnBeaconSeed;
property OnSpan: TWebCmdSpan read FOnSpan write FOnSpan;
property OnVolume: TWebCmdVolume read FOnVolume write FOnVolume;
property OnWfAGC: TWebCmdWfAGC read FOnWfAGC write FOnWfAGC;
@@ -542,6 +560,11 @@ begin
FBandIdx := 5;
FCurrentXvtr := -1;
FFreqMhzDigits := 3;
FBeaconVisible := False;
FBeaconOn := False;
FBeaconText := '';
FBeaconRefHz := 0;
FBeaconTrackHz := 0;
SetLength(FXvtrBands, 0);
// Bootstrap-набор рейтов (до connect): хост перезапишет под бэкенд (HPSDR/Pluto).
SetRatePresets([48000, 96000, 192000, 384000, 768000, 1536000]);
@@ -566,6 +589,21 @@ begin
end;
end;
procedure TWebServer.SetBeaconStatus(AVisible, AOn: Boolean; const AText: string;
ARefHz, ATrackHz: Double);
begin
FStateLock.Enter;
try
FBeaconVisible := AVisible;
FBeaconOn := AOn;
FBeaconText := AText;
FBeaconRefHz := ARefHz;
FBeaconTrackHz := ATrackHz;
finally
FStateLock.Leave;
end;
end;
procedure TWebServer.SetDeviceList(const ADevices: TWebDeviceArray);
var i: Integer;
begin
@@ -1444,6 +1482,20 @@ begin
FStateLock.Enter; FTuning := On_; FStateLock.Leave;
if Assigned(FOnTun) then FOnTun(On_);
end
else if Cmd = 'set_beacon' then
begin
On_ := JsonGetBool(Json, 'on', FBeaconOn);
FStateLock.Enter; FBeaconOn := On_; FStateLock.Leave;
if Assigned(FOnBeacon) then FOnBeacon(On_);
end
else if Cmd = 'beacon_seed' then
begin
// frac = доля позиции клика по ширине спектра (0..1). Абсолютную display-частоту
// считает контроллер из ЖИВЫХ FCenterFreq/FSpanHz (как десктоп PixelToFreq) —
// клиентский localCenter может отставать при ретюне LO во время лока.
HzF := JsonGetFloat(Json, 'frac', -1);
if (HzF >= 0) and Assigned(FOnBeaconSeed) then FOnBeaconSeed(HzF);
end
else if Cmd = 'freq_a' then
begin
HzF := JsonGetFloat(Json, 'hz', FFreq);
@@ -1664,7 +1716,9 @@ begin
'"pll_text":"%s","rx_text":"%s","tx_text":"%s","seq_text":"%s",' +
'"xvtr_current":%d,"xvtr_bands":%s,"freq_mhz_digits":%d,' +
'"fmstep_idx":%d,"devices":%s,"rate_presets":%s,"bands":%s,' +
'"freq_min":%.0f,"freq_max":%.0f}',
'"freq_min":%.0f,"freq_max":%.0f,' +
'"beacon_visible":%s,"beacon_on":%s,"beacon_text":"%s",' +
'"beacon_ref_hz":%.0f,"beacon_track_hz":%.0f}',
[FFreq, FVfoB, FActiveVfo,
FMode, MODE_N[FMode mod 8],
FFilterIdx, FFilterBW,
@@ -1694,7 +1748,10 @@ begin
JsonEscape(FTXText), JsonEscape(FSeqText),
FCurrentXvtr, XvtrJson, FFreqMhzDigits,
FFMStepIdx, DevJson, RateJson, BandJson,
FFreqMin, FFreqMax
FFreqMin, FFreqMax,
BoolToStr(FBeaconVisible, 'true', 'false'),
BoolToStr(FBeaconOn, 'true', 'false'),
JsonEscape(FBeaconText), FBeaconRefHz, FBeaconTrackHz
], FS);
finally
FStateLock.Leave;
+11 -1
View File
@@ -167,6 +167,7 @@ begin
FWeb.OnCtun := @FWebAd.OnCtun; FWeb.OnActiveVfo := @FWebAd.OnActiveVfo;
FWeb.OnMOX := @FWebAd.OnMOX; FWeb.OnTun := @FWebAd.OnTun;
FWeb.OnFMStep := @FWebAd.OnFMStep; FWeb.OnXvtrBand := @FWebAd.OnXvtrBand;
FWeb.OnBeacon := @FWebAd.OnBeacon; FWeb.OnBeaconSeed := @FWebAd.OnBeaconSeed;
FWeb.OnWebMic := @FWebAd.OnMic;
FWeb.OnClientActiveChanged := @FWebAd.OnClientActiveChanged;
// Host-coupled (lifecycle) — на демон-хост (маршалит в главный поток).
@@ -409,7 +410,7 @@ var
WebCfg: TWebSettings;
Dev: THPSDRDevice;
AutoDev: TSavedDevice;
lastPush, nowMs: QWord;
lastPush, lastBeacon, nowMs: QWord;
begin
FCtrl.FSettings.LoadWebSettings(WebCfg);
if not FWeb.Start then
@@ -444,10 +445,19 @@ begin
Log('No autostart device — waiting for web connect.');
lastPush := GetTickCount64;
lastBeacon := lastPush;
while FRunning do
begin
CheckSynchronize(PUSH_INTERVAL_MS); // исполняет очередь Invoke + ждёт до 50мс
nowMs := GetTickCount64;
// QO-100 beacon lock — одна итерация контура @~10 Гц (как GUI MeterTimer). No-op
// если лок выключен. Без этого лок маяка не работал бы в headless-демоне.
if nowMs - lastBeacon >= 100 then
begin
if FCtrl.FRunning and FCtrl.FWDSPReady then
FCtrl.ServiceBeaconLock;
lastBeacon := nowMs;
end;
if nowMs - lastPush >= PUSH_INTERVAL_MS then
begin
// S-метр приходит из WDSP (не из сетевого HP-статуса), поэтому его никто