From 8d64bba6fc7cd48b43e74ce1511391f918a2f45a Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Wed, 11 Mar 2026 21:58:23 +0300 Subject: [PATCH] fix vfo on web --- MainForm.pas | 85 ++++++++++++++++++++++++++++++++++++--- WebPageHtml.pas | 103 +++++++++++++++++++++++++++++++++--------------- WebServer.pas | 45 ++++++++++++++++----- 3 files changed, 186 insertions(+), 47 deletions(-) diff --git a/MainForm.pas b/MainForm.pas index 5707e6a..cfe3f72 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -476,6 +476,8 @@ type procedure WebOnNB(Mode: Integer); procedure WebOnSNB(On_: Boolean); procedure WebOnANF(On_: Boolean); + procedure WebOnFreqB(Hz: Double); + procedure WebOnActiveVfo(Idx: Integer); // Synchronize-обёртки (выполняются в UI-потоке) procedure SyncWebFreq; procedure SyncWebMode; @@ -494,6 +496,8 @@ type procedure SyncWebNB; procedure SyncWebSNB; procedure SyncWebANF; + procedure SyncWebFreqB; + procedure SyncWebActiveVfo; procedure BtnWfAGCClick(Sender: TObject); procedure BtnWfNFClick(Sender: TObject); procedure BtnHidePanelClick(Sender: TObject); @@ -822,6 +826,8 @@ begin FWebServer.OnNB := WebOnNB; FWebServer.OnSNB := WebOnSNB; FWebServer.OnANF := WebOnANF; + FWebServer.OnFreqB := WebOnFreqB; + FWebServer.OnActiveVfo := WebOnActiveVfo; FWebServer.Start; FSMeterPeak := -130; FSMeterMin := -130; @@ -2906,7 +2912,8 @@ begin FRunning and FNetwork.Connected, FRunning, FMuted, FCTun, BtnNR.Tag, BtnNB.Tag, BtnSNB.Tag <> 0, BtnANF.Tag <> 0, - FCenterFreq, FFilter); + FCenterFreq, FFilter, + FVfoB, FActiveVfo); end; // =========================================================================== @@ -4357,15 +4364,30 @@ begin end; procedure TMainForm.SyncWebFreq; +var BandIdx: Integer; begin - FVfoA := FWebSyncFreq; - if not FCTun then FCenterFreq := FVfoA; - UpdateVfoDisplay; - if FWDSPReady then + // Веб шлёт "freq" для активного VFO + if FActiveVfo = 0 then + ApplyVfoA(Round(FWebSyncFreq)) + else begin - FDSPEngine.SetShift(FVfoA - FCenterFreq); + FVfoB := FWebSyncFreq; + FreqDispB.Frequency := Round(FVfoB); + FCenterFreq := FVfoB; + if FWDSPReady then FDSPEngine.SetShift(0.0); if FRunning then FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel); + BandIdx := FreqToBandIdx(FVfoB); + if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then + begin + StyleButton(BtnBand[FCurrentBand], False); + FCurrentBand := BandIdx; + StyleButton(BtnBand[FCurrentBand], True); + end; + FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; + DrawSpectrum; PbSpectrum.Invalidate; + if PbRuler <> nil then PbRuler.Invalidate; + UpdateVfoDisplay; end; end; @@ -4682,6 +4704,57 @@ begin BtnANFClick(BtnANF); end; +// ── VFO-B freq (из веба) ───────────────────────────────────────────────── + +procedure TMainForm.WebOnFreqB(Hz: Double); +begin + FWebSyncFreq := Hz; + FWebSyncM := SyncWebFreqB; + TThread.Synchronize(nil, FWebSyncM); +end; + +procedure TMainForm.SyncWebFreqB; +var BandIdx: Integer; +begin + FVfoB := FWebSyncFreq; + FreqDispB.Frequency := Round(FVfoB); + if FActiveVfo = 1 then + begin + FCenterFreq := FVfoB; + if not FCTun then + if FWDSPReady then FDSPEngine.SetShift(0.0); + if FRunning then + FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel); + BandIdx := FreqToBandIdx(FVfoB); + if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then + begin + StyleButton(BtnBand[FCurrentBand], False); + FCurrentBand := BandIdx; + StyleButton(BtnBand[FCurrentBand], True); + end; + FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; + DrawSpectrum; PbSpectrum.Invalidate; + if PbRuler <> nil then PbRuler.Invalidate; + end; + UpdateVfoDisplay; +end; + +// ── Активный VFO (из веба) ─────────────────────────────────────────────── + +procedure TMainForm.WebOnActiveVfo(Idx: Integer); +begin + FWebSyncInt := Idx; + FWebSyncM := SyncWebActiveVfo; + TThread.Synchronize(nil, FWebSyncM); +end; + +procedure TMainForm.SyncWebActiveVfo; +begin + if (FWebSyncInt < 0) or (FWebSyncInt > 1) then Exit; + if FWebSyncInt = FActiveVfo then Exit; + ActivateVfo(FWebSyncInt); +end; + procedure TMainForm.TrkDriveChange(Sender: TObject); begin FDriveLevel := Round(TrkDrive.Position / 100.0 * 255); diff --git a/WebPageHtml.pas b/WebPageHtml.pas index 0e7d913..df50cd4 100644 --- a/WebPageHtml.pas +++ b/WebPageHtml.pas @@ -70,16 +70,16 @@ begin '.vfo-mid{flex:1;display:flex;align-items:center;justify-content:center;gap:5px}' + '.vbtn{width:38px;height:38px;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;cursor:pointer;border-radius:3px;color:#888;border:1px solid #444;background:linear-gradient(180deg,#2a2a2a,#1e1e1e);padding:0}' + '.vbtn.active{color:#7fff7f;border-color:#3d7a3d;background:linear-gradient(180deg,#1e3e1e,#142814)}' + - '.vfo-ro{font:400 44px "Orbitron","Consolas",monospace;letter-spacing:0.02em;color:#7fff7f;background:#0c0c0c;border:none;border-radius:0;padding:0 6px;cursor:pointer;user-select:none;height:62px;display:inline-flex;align-items:baseline;padding-top:10px;white-space:nowrap;overflow:hidden}' + - '.vfo-ro.dim{color:#2a4a2a;background:#1e1e1e;border:none}' + + '.vfo-ro{font:400 44px "Orbitron","Consolas",monospace;letter-spacing:0.02em;color:#7fff7f;border:none;border-radius:0;padding:0 6px;cursor:pointer;user-select:none;height:62px;display:inline-flex;align-items:baseline;padding-top:10px;white-space:nowrap;overflow:hidden;background:transparent}' + + '.vfo-ro.dim{color:#1e501e;background:transparent}' + '.vfo-ro .dg{display:inline-block;min-width:0.55em;font-size:44px;line-height:1}' + - '.vfo-ro .dg.lead{color:#2a3a2a}.vfo-ro.dim .dg.lead{color:#1e281e}' + + '.vfo-ro .dg.lead{color:#2a3a2a}.vfo-ro.dim .dg.lead{color:#162016}' + '.vfo-ro .sp{display:inline-block;color:#2a6030;min-width:0.3em;font-size:44px;line-height:1}' + - '.vfo-ro.dim .sp{color:#1a2a1a}' + + '.vfo-ro.dim .sp{color:#142014}' + '.vfo-ro .dg.sel{background:rgba(127,255,127,.18);outline:1px solid rgba(127,255,127,.35);border-radius:2px}' + '.vfo-hz{display:inline-flex;align-items:baseline}' + '.vfo-hz .dg{display:inline-block;font-size:22px;line-height:1;min-width:0.55em;font-weight:400}' + - '.vfo-ro.dim .vfo-hz .dg{color:#2a4a2a}' + + '.vfo-ro.dim .vfo-hz .dg{color:#1e501e}' + '.swap-g{display:flex;gap:3px}' + '.swap-g button{padding:0 7px;height:22px;font-size:10px}' + '.right-g{display:flex;align-items:center;gap:4px;margin-left:auto}' + @@ -238,7 +238,7 @@ begin 'return h;}' + 'function renderVfos(){q("vDispA").innerHTML=fmtVfo(vfoA,stA);q("vDispB").innerHTML=fmtVfo(vfoB,stB);}' + - 'function bindVfo(dispId,getHz,setHz,getSt,setSt,getMem,setMem){' + + 'function bindVfo(dispId,getHz,setHz,getSt,setSt,getMem,setMem,onSend){' + 'const el=q(dispId);' + 'el.addEventListener("mousemove",e=>{const t=e.target,s=Number(t?.dataset?.s||0);if(s>0){setSt(s);setMem(s);renderVfos();}});' + 'el.addEventListener("click",e=>{const t=e.target,s=Number(t?.dataset?.s||0);if(s>0){setSt(s);setMem(s);}});' + @@ -246,21 +246,41 @@ begin 'el.addEventListener("wheel",e=>{e.preventDefault();e.stopPropagation();audioKick();' + 'const dir=e.deltaY<0?1:-1,st=getSt()||getMem()||100;' + 'const nf=cl(Math.round(getHz())+st*dir,30000,60000000);' + - 'setHz(nf);renderVfos();pend("freq");ws2({cmd:"freq",hz:nf});},{passive:false});' + + 'setHz(nf);onSend(nf);},{passive:false});' + 'el.addEventListener("dblclick",()=>{' + 'const s=prompt("Set Hz",String(Math.round(getHz())));if(!s)return;' + 'const f=parseInt(s.replace(/[^0-9]/g,""),10);if(!isFinite(f))return;' + - 'const nf=cl(f,30000,60000000);setHz(nf);renderVfos();ws2({cmd:"freq",hz:nf});' + + 'const nf=cl(f,30000,60000000);setHz(nf);renderVfos();onSend(nf);' + '});}' + - 'bindVfo("vDispA",()=>vfoA,v=>{vfoA=v;},()=>stA,v=>{stA=v;},()=>stAmem,v=>{stAmem=v;});' + - 'bindVfo("vDispB",()=>vfoB,v=>{vfoB=v;},()=>stB,v=>{stB=v;},()=>stBmem,v=>{stBmem=v;});' + + 'bindVfo("vDispA",()=>vfoA,v=>{vfoA=v;},()=>stA,v=>{stA=v;},()=>stAmem,v=>{stAmem=v;},' + + 'function(nf){renderVfos();pend("freq");if(activeVfo==="A")ws2({cmd:"freq",hz:nf});else ws2({cmd:"freq_b",hz:nf});});' + + 'bindVfo("vDispB",()=>vfoB,v=>{vfoB=v;},()=>stB,v=>{stB=v;},()=>stBmem,v=>{stBmem=v;},' + + 'function(nf){renderVfos();pend("freq");if(activeVfo==="B")ws2({cmd:"freq",hz:nf});else ws2({cmd:"freq_b",hz:nf});});' + - 'function selVfo(w){activeVfo=w;q("vBtnA").classList.toggle("active",w==="A");q("vBtnB").classList.toggle("active",w==="B");q("vDispA").classList.toggle("dim",w!="A");q("vDispB").classList.toggle("dim",w!="B");ws2({cmd:"freq",hz:Math.round(w==="A"?vfoA:vfoB)});}' + - 'q("vBtnA").onclick=()=>selVfo("A");' + - 'q("vBtnB").onclick=()=>selVfo("B");' + - 'q("btnBA").onclick=()=>{vfoA=vfoB;renderVfos();ws2({cmd:"freq",hz:Math.round(vfoA)});};' + - 'q("btnAB").onclick=()=>{vfoB=vfoA;renderVfos();};' + - 'q("btnSwap").onclick=()=>{const t=vfoA;vfoA=vfoB;vfoB=t;renderVfos();ws2({cmd:"freq",hz:Math.round(activeVfo==="A"?vfoA:vfoB)});};' + + 'function selVfo(w){' + + 'activeVfo=w;' + + 'q("vBtnA").classList.toggle("active",w==="A");' + + 'q("vBtnB").classList.toggle("active",w==="B");' + + 'q("vDispA").classList.toggle("dim",w!=="A");' + + 'q("vDispB").classList.toggle("dim",w!=="B");' + + // Notify server: switch active VFO (server calls ActivateVfo which retunes DDC) + 'ws2({cmd:"set_active_vfo",idx:w==="A"?0:1});' + + '}' + + 'q("vBtnA").onclick=()=>{if(activeVfo!=="A")selVfo("A");};' + + 'q("vBtnB").onclick=()=>{if(activeVfo!=="B")selVfo("B");};' + + // B=A: copy A freq into B + 'q("btnBA").onclick=()=>{vfoB=vfoA;renderVfos();ws2({cmd:"freq_b",hz:Math.round(vfoB)});};' + + // A=B: copy B freq into A + 'q("btnAB").onclick=()=>{vfoA=vfoB;renderVfos();' + + 'if(activeVfo==="A")ws2({cmd:"freq",hz:Math.round(vfoA)});' + + 'else ws2({cmd:"freq_b",hz:Math.round(vfoA)});' + + // Actually A=B means set vfoA=vfoB, so send freq_a regardless of active + '};' + + // SWAP + 'q("btnSwap").onclick=()=>{const t=vfoA;vfoA=vfoB;vfoB=t;renderVfos();' + + 'ws2({cmd:"freq",hz:Math.round(activeVfo==="A"?vfoA:vfoB)});' + + 'ws2({cmd:"freq_b",hz:Math.round(activeVfo==="A"?vfoB:vfoA)});' + + '};' + // SPAN кнопки: только 48K..1536K 'const SPANS=[48000,96000,192000,384000,768000,1536000];' + @@ -330,7 +350,7 @@ begin 'function isPending(field){return !!(pendingCmds[field]&&pendingCmds[field]>Date.now());}' + // Маппинг: server JSON field → pend key - 'var PEND_MAP={vfo_a_hz:"freq",nr:"nr",nr_mode:"nr",nb:"nb",nb_mode:"nb",snb:"snb",anf:"anf",running:"run",mode:"mode",agc_mode:"agc"};' + + '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"};' + 'function applyState(msg){' + // Мержим только поля которые не pending @@ -338,8 +358,21 @@ begin 'var pk=PEND_MAP[k];' + 'if(!pk||!isPending(pk))cur[k]=msg[k];' + '});' + - // VFO - 'if(!isPending("freq")){vfoA=cur.vfo_a_hz||vfoA;renderVfos();}' + + // VFO A + 'if(!isPending("freq")){vfoA=cur.vfo_a_hz||vfoA;}' + + // VFO B (always sync - no separate pending for B) + 'if(msg.vfo_b_hz!=null)vfoB=msg.vfo_b_hz;' + + // Active VFO from server (authoritative) + 'if(msg.active_vfo!=null&&!isPending("freq")){' + + 'const sv=msg.active_vfo===0?"A":"B";' + + 'if(sv!==activeVfo){' + + 'activeVfo=sv;' + + 'q("vBtnA").classList.toggle("active",activeVfo==="A");' + + 'q("vBtnB").classList.toggle("active",activeVfo==="B");' + + 'q("vDispA").classList.toggle("dim",activeVfo!=="A");' + + 'q("vDispB").classList.toggle("dim",activeVfo!=="B");' + + '}}' + + 'renderVfos();' + 'const as=q("agcSel");if(as&&cur.agc_mode!=null&&!isPending("agc"))as.value=cur.agc_mode;' + 'const vs=q("volSl");if(vs&&!vs.matches(":active")){vs.value=cur.volume||70;var vv=q("volVal");if(vv)vv.textContent=cur.volume||70;}' + '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";}' + @@ -348,12 +381,11 @@ begin 'if(!isPending("snb"))updSnbBtn();' + 'if(!isPending("anf"))updAnfBtn();' + 'if(!isPending("run"))updRunBtn(!!cur.running);' + - // syncModGrp syncs dropdown AND calls updModTglLabel internally 'if(!isPending("mode")){syncModGrp(cur.mode||0);}' + - 'updBand(cur.vfo_a_hz||0);updSpan(cur.span_hz||192000);' + + 'updBand(activeVfo==="A"?(cur.vfo_a_hz||0):(vfoB||0));updSpan(cur.span_hz||192000);' + 'paintSM(Number(cur.smeter_dbm||-130));' + 'const mn=cur.mode_name||(MODE_N[cur.mode||0]||"?");' + - 'stEl.textContent=(cur.connected?"OK":"OFF")+" | "+(cur.running?"RUN":"STOP")+" | "+mn+" | "+((vfoA/1e6).toFixed(3))+" MHz";}' + + 'stEl.textContent=(cur.connected?"OK":"OFF")+" | "+(cur.running?"RUN":"STOP")+" | "+mn+" | "+((activeVfoHz()/1e6).toFixed(3))+" MHz";}' + 'function pal(db){const v=Math.round(cl((db-wfLo)/Math.max(1,wfHi-wfLo),0,1)*255);const c=PAL[v]|0;return[(c>>16)&255,(c>>8)&255,c&255];}' + 'function fltLH(){const bw=cur.filter_bw||2700;const m=cur.mode|0;if(m===0)return[-bw,-100];if(m===1)return[100,bw];return[-bw/2,bw/2];}' + @@ -366,20 +398,20 @@ begin 'function drawRuler(){const w=rulerCv.clientWidth,h=rulerCv.clientHeight;rctx.fillStyle="#0c1210";rctx.fillRect(0,0,w,h);' + 'const st=(cur.center_hz||0)-(cur.span_hz||0)/2;' + 'for(let i=0;i<=8;i++){const x=i*w/8,f=st+i*(cur.span_hz||0)/8;rctx.strokeStyle="#2a3a2e";rctx.lineWidth=1;rctx.beginPath();rctx.moveTo(x+.5,0);rctx.lineTo(x+.5,7);rctx.stroke();rctx.fillStyle="#7a9a80";rctx.font="10px monospace";const t=(f/1e6).toFixed(3);rctx.fillText(t,x-14,21);}' + - 'const vx=hzToX(vfoA,w);rctx.strokeStyle="#60d070";rctx.lineWidth=1;rctx.beginPath();rctx.moveTo(vx+.5,0);rctx.lineTo(vx+.5,h);rctx.stroke();}' + + 'const vxr=hzToX(activeVfoHz(),w);rctx.strokeStyle="#60d070";rctx.lineWidth=1;rctx.beginPath();rctx.moveTo(vxr+.5,0);rctx.lineTo(vxr+.5,h);rctx.stroke();}' + 'function drawSpec(bins){const w=specCv.clientWidth,h=specCv.clientHeight;sctx.fillStyle="#050a05";sctx.fillRect(0,0,w,h);' + 'for(let i=1;i<5;i++){const y=i*h/5;sctx.strokeStyle="#121e12";sctx.lineWidth=1;sctx.beginPath();sctx.moveTo(0,y);sctx.lineTo(w,y);sctx.stroke();}' + 'if(!bins?.length){drawMk(sctx,w,h);return;}' + 'if(specSm.length!==bins.length)specSm=new Array(bins.length).fill(-130);' + - 'const[fl,fh]=fltLH(),x1=hzToX(vfoA+fl,w),x2=hzToX(vfoA+fh,w);' + + 'const avf=activeVfoHz();const[fl,fh]=fltLH(),x1=hzToX(avf+fl,w),x2=hzToX(avf+fh,w);' + 'sctx.fillStyle="rgba(80,150,255,.12)";sctx.fillRect(Math.min(x1,x2),0,Math.abs(x2-x1),h);' + 'sctx.beginPath();for(let x=0;x0?base+st:(Math.round(vfoA)===base?base-st:base),30000,60000000);cur.vfo_a_hz=vfoA;renderVfos();pend("freq");ws2({cmd:"freq",hz:vfoA});}' + + 'function activeVfoHz(){return activeVfo==="A"?vfoA:vfoB;}' + + 'function setActiveVfoHz(f){if(activeVfo==="A")vfoA=f;else vfoB=f;}' + + 'function sendActiveFreq(f){if(activeVfo==="A")ws2({cmd:"freq",hz:f});else ws2({cmd:"freq",hz:f});}' + + 'function doWheel(e){e.preventDefault();e.stopPropagation();audioKick();const dir=e.deltaY<0?1:-1,st=tuneStep(e),base=Math.floor(activeVfoHz()/st)*st;' + + 'const nf=cl(dir>0?base+st:(Math.round(activeVfoHz())===base?base-st:base),30000,60000000);' + + 'setActiveVfoHz(nf);' + + 'if(activeVfo==="A"){cur.vfo_a_hz=nf;}else{cur.vfo_b_hz=nf;}' + + 'renderVfos();pend("freq");ws2({cmd:"freq",hz:nf});}' + 'function bindCv(cv){let wT=0;const wh=e=>{const n=performance.now();if(n-wT<20)return;wT=n;wTarget=cv.id;doWheel(e);};' + 'cv.addEventListener("contextmenu",e=>e.preventDefault());' + 'cv.addEventListener("mouseenter",()=>{wTarget=cv.id;});' + 'cv.addEventListener("wheel",wh,{passive:false,capture:true});' + - 'cv.addEventListener("mousedown",e=>{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;dragging=true;dragX0=e.offsetX;dragCen0=cur.center_hz||0;dragVfo0=vfoA;dragMs=0;});' + - 'cv.addEventListener("mousemove",e=>{wTarget=cv.id;hoverX=e.offsetX;if(markerOn)markerX=e.offsetX;if(!dragging)return;const n=Date.now();if(n-dragMs<35)return;dragMs=n;const dHz=(e.offsetX-dragX0)/Math.max(1,cv.clientWidth)*(cur.span_hz||0);if(cur.ctun)ws2({cmd:"freq",hz:Math.round(dragCen0-dHz)});else{vfoA=cl(Math.round(dragVfo0-dHz),30000,60000000);cur.vfo_a_hz=vfoA;renderVfos();pend("freq");ws2({cmd:"freq",hz:vfoA});}});' + - 'cv.addEventListener("mouseup",e=>{if(e.button===2)return;if(Math.abs(e.offsetX-dragX0)<4){vfoA=cl(Math.round(xToHz(e.offsetX,cv.clientWidth)/100)*100,30000,60000000);cur.vfo_a_hz=vfoA;renderVfos();pend("freq");ws2({cmd:"freq",hz:vfoA});}dragging=false;});' + + 'cv.addEventListener("mousedown",e=>{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;dragging=true;dragX0=e.offsetX;dragCen0=cur.center_hz||0;dragVfo0=activeVfoHz();dragMs=0;});' + + 'cv.addEventListener("mousemove",e=>{wTarget=cv.id;hoverX=e.offsetX;if(markerOn)markerX=e.offsetX;if(!dragging)return;const n=Date.now();if(n-dragMs<35)return;dragMs=n;const dHz=(e.offsetX-dragX0)/Math.max(1,cv.clientWidth)*(cur.span_hz||0);if(cur.ctun)ws2({cmd:"freq",hz:Math.round(dragCen0-dHz)});else{const nf=cl(Math.round(dragVfo0-dHz),30000,60000000);setActiveVfoHz(nf);if(activeVfo==="A")cur.vfo_a_hz=nf;else cur.vfo_b_hz=nf;renderVfos();pend("freq");ws2({cmd:"freq",hz:nf});}});' + + 'cv.addEventListener("mouseup",e=>{if(e.button===2)return;if(Math.abs(e.offsetX-dragX0)<4){const nf=cl(Math.round(xToHz(e.offsetX,cv.clientWidth)/100)*100,30000,60000000);setActiveVfoHz(nf);if(activeVfo==="A")cur.vfo_a_hz=nf;else cur.vfo_b_hz=nf;renderVfos();pend("freq");ws2({cmd:"freq",hz:nf});}dragging=false;});' + 'cv.addEventListener("mouseleave",()=>{if(!markerOn)hoverX=-1;wTarget="";dragging=false;});}' + 'bindCv(specCv);bindCv(wfCv);' + 'window.addEventListener("wheel",e=>{if(e.target?.closest?.(".vfo-ro"))return;if(!wTarget){if(e.target===specCv)wTarget="spec";else if(e.target===wfCv)wTarget="wf";}if(wTarget)doWheel(e);},{passive:false,capture:true});' + diff --git a/WebServer.pas b/WebServer.pas index d0f4187..4064c87 100644 --- a/WebServer.pas +++ b/WebServer.pas @@ -119,8 +119,10 @@ type TWebCmdCtun = procedure(On_: Boolean) of object; TWebCmdNRMode = procedure(Mode: Integer) of object; TWebCmdNBMode = procedure(Mode: Integer) of object; - TWebCmdSNB = procedure(On_: Boolean) of object; - TWebCmdANF = procedure(On_: Boolean) of object; + TWebCmdSNB = procedure(On_: Boolean) of object; + TWebCmdANF = procedure(On_: Boolean) of object; + TWebCmdFreqB = procedure(Hz: Double) of object; + TWebCmdActiveVfo = procedure(Idx: Integer) of object; // ── Главный класс сервера ───────────────────────────────────────────────── TWebServer = class @@ -176,6 +178,8 @@ type FANF: Boolean; FCenterHz: Double; FFilterIdx: Integer; + FVfoB: Double; + FActiveVfo: Integer; // 0=A, 1=B FStateLock: TCriticalSection; // ── Callbacks ── @@ -196,6 +200,8 @@ type FOnNB: TWebCmdNBMode; FOnSNB: TWebCmdSNB; FOnANF: TWebCmdANF; + FOnFreqB: TWebCmdFreqB; + FOnActiveVfo: TWebCmdActiveVfo; FWebClientActive: Boolean; @@ -237,7 +243,8 @@ type TrxConnected: Boolean; TrxRunning, Muted, Ctun: Boolean; NRMode, NBMode: Integer; SNB, ANF: Boolean; - CenterHz: Double; FilterIdx: Integer); + CenterHz: Double; FilterIdx: Integer; + VfoB: Double; ActiveVfo: Integer); property WebClientActive: Boolean read FWebClientActive; @@ -256,8 +263,10 @@ type property OnCtun: TWebCmdCtun read FOnCtun write FOnCtun; property OnNR: TWebCmdNRMode read FOnNR write FOnNR; property OnNB: TWebCmdNBMode read FOnNB write FOnNB; - property OnSNB: TWebCmdSNB read FOnSNB write FOnSNB; - property OnANF: TWebCmdANF read FOnANF write FOnANF; + property OnSNB: TWebCmdSNB read FOnSNB write FOnSNB; + property OnANF: TWebCmdANF read FOnANF write FOnANF; + property OnFreqB: TWebCmdFreqB read FOnFreqB write FOnFreqB; + property OnActiveVfo: TWebCmdActiveVfo read FOnActiveVfo write FOnActiveVfo; end; implementation @@ -949,6 +958,20 @@ begin On_ := JsonGetBool(Json, 'on', FANF); FStateLock.Enter; FANF := On_; FStateLock.Leave; if Assigned(FOnANF) then FOnANF(On_); + end + else if Cmd = 'freq_b' then + begin + HzF := JsonGetFloat(Json, 'hz', FVfoB); + FStateLock.Enter; FVfoB := HzF; FStateLock.Leave; + if Assigned(FOnFreqB) then FOnFreqB(HzF); + end + else if Cmd = 'set_active_vfo' then + begin + ModeValue := JsonGetInt(Json, 'idx', FActiveVfo); + if ModeValue < 0 then ModeValue := 0; + if ModeValue > 1 then ModeValue := 1; + FStateLock.Enter; FActiveVfo := ModeValue; FStateLock.Leave; + if Assigned(FOnActiveVfo) then FOnActiveVfo(ModeValue); end; end; @@ -1038,14 +1061,16 @@ begin FStateLock.Enter; try Result := Format( - '{"vfo_a_hz":%.0f,"mode":%d,"mode_name":"%s",' + + '{"vfo_a_hz":%.0f,"vfo_b_hz":%.0f,"active_vfo":%d,' + + '"mode":%d,"mode_name":"%s",' + '"filter":%d,"filter_bw":%d,' + '"agc_mode":%d,"agc_top":%d,' + '"span_hz":%.0f,"center_hz":%.0f,"volume":%d,' + '"wf_agc":%s,"wf_nf":%s,"band_idx":%d,"smeter_dbm":%.1f,' + '"running":%s,"mute":%s,"ctun":%s,' + '"nr_mode":%d,"nr":%s,"nb_mode":%d,"nb":%s,"snb":%s,"anf":%s,"connected":%s}', - [FFreq, FMode, MODE_N[FMode mod 8], + [FFreq, FVfoB, FActiveVfo, + FMode, MODE_N[FMode mod 8], FFilterIdx, FFilterBW, FAGCMode, FAGCTop, FSpanHz, FCenterHz, FVolume, @@ -1125,7 +1150,8 @@ procedure TWebServer.PushSpectrum( TrxConnected: Boolean; TrxRunning, Muted, Ctun: Boolean; NRMode, NBMode: Integer; SNB, ANF: Boolean; - CenterHz: Double; FilterIdx: Integer); + CenterHz: Double; FilterIdx: Integer; + VfoB: Double; ActiveVfo: Integer); var N, i: Integer; begin if not FRunning then Exit; @@ -1155,11 +1181,12 @@ begin FANF := ANF; FCenterHz := CenterHz; FFilterIdx := FilterIdx; + FVfoB := VfoB; + FActiveVfo := ActiveVfo; finally FStateLock.Leave; end; end; - { ═══════════════════════════════════════════════════════════════════════════ Stub-методы (реализация встроена в HandleClient) ═══════════════════════════════════════════════════════════════════════════ }