mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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-статуса), поэтому его никто
|
||||
|
||||
Reference in New Issue
Block a user