web: fix A=B/SWAP VFO ops, wire attenuator, TUN button, FNP filter overlay

- Add freq_a command: sets VFO A regardless of active VFO; fixes A=B and
  SWAP when VFO B is active (prev. sent freq_b which overwrote wrong VFO)
- Add pend("freq") to B=A / A=B / SWAP to guard local display during round-trip
- RX step attenuator: HPSDRNetwork.SetStepAtten → bytes 1442/1443 in HP packet;
  WebServer attn command; MainForm FAtten + SyncWebAttn; attSel.onchange in JS
- TUN mode: set_tun WebSocket command wired to ApplyTUN(); toneBtn toggles
  with on/off highlight and pending protection; tuning field in state JSON
- FNP button opens semi-transparent filter overlay (10 buttons, 5×2 grid)
  with mode-appropriate widths (SSB/CW/AM/FM); active filter highlighted;
  cur.filter_bw updated immediately on click so passband redraws within 50ms

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 15:04:23 +03:00
co-authored by Claude Sonnet 4.6
parent 9f208006aa
commit 154b6eccba
4 changed files with 165 additions and 20 deletions
+64 -13
View File
@@ -205,12 +205,67 @@ begin
'q("modGrpSel").onchange=function(){const grp=GRP_MAP[modGrp()];const defaultMode=grp[0];cur.mode=defaultMode;updModTglLabel();pend("mode");ws2({cmd:"mode",mode:defaultMode});};' +
'q("modTglBtn").onclick=function(){audioKick();const next=modTglTarget();cur.mode=next;updModTglLabel();pend("mode");ws2({cmd:"mode",mode:next});};' +
'q("agcSel").onchange=function(e){pend("agc");ws2({cmd:"agc",mode:+e.target.value});};' +
'q("attSel").onchange=function(e){pend("attn");ws2({cmd:"attn",idx:+e.target.value});};' +
// FNP — фильтровый оверлей
'var FNP_N={ssb:["5.0k","4.4k","3.8k","3.3k","2.9k","2.7k","2.4k","2.1k","1.8k","1.0k"],' +
'cw:["1.0k","800","750","600","500","400","250","100","50","25"],' +
'am:["16k","12k","10k","8k","6.6k","5.2k","4.0k","3.1k","2.9k","2.4k"],' +
'fm:["20k","15k","12k","10k","8k","6k","5k","4k","3k","2k"]};' +
'var FNP_BW={ssb:[5000,4400,3800,3300,2900,2700,2400,2100,1800,1000],' +
'cw:[1000,800,750,600,500,400,250,100,50,25],' +
'am:[16000,12000,10000,8000,6600,5200,4000,3100,2900,2400],' +
'fm:[20000,15000,12000,10000,8000,6000,5000,4000,3000,2000]};' +
'var fnpOv=null;' +
'function fnpGrp(){var m=cur.mode|0;return(m===3||m===4)?"cw":(m===5)?"fm":(m===2||m===6||m===7)?"am":"ssb";}' +
'function closeFnp(){if(fnpOv){fnpOv.remove();fnpOv=null;}document.removeEventListener("click",fnpOutside);}' +
'function fnpOutside(e){if(fnpOv&&!fnpOv.contains(e.target)&&e.target!==q("fnpBtn"))closeFnp();}' +
'function showFnp(){' +
'if(fnpOv){closeFnp();return;}' +
'var g=fnpGrp(),ns=FNP_N[g],bws=FNP_BW[g],cb=cur.filter_bw||2700;' +
'fnpOv=document.createElement("div");' +
'fnpOv.style.cssText="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);' +
'background:rgba(6,14,6,0.93);border:1px solid rgba(70,120,70,0.65);border-radius:8px;' +
'padding:12px 14px;z-index:2000;backdrop-filter:blur(6px);box-shadow:0 4px 24px rgba(0,0,0,0.7);min-width:260px;";' +
// заголовок
'var hd=document.createElement("div");' +
'hd.style.cssText="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;";' +
'var tl=document.createElement("span");' +
'tl.textContent="Filter — "+g.toUpperCase();' +
'tl.style.cssText="color:#80b880;font-family:monospace;font-size:12px;letter-spacing:1px;";' +
'var xb=document.createElement("button");' +
'xb.textContent="×";' +
'xb.style.cssText="background:none;border:none;color:#6a9a6a;font-size:16px;cursor:pointer;padding:0 2px;line-height:1;";' +
'xb.onclick=closeFnp;' +
'hd.appendChild(tl);hd.appendChild(xb);fnpOv.appendChild(hd);' +
// сетка кнопок
'var gd=document.createElement("div");' +
'gd.style.cssText="display:grid;grid-template-columns:repeat(5,1fr);gap:5px;";' +
'ns.forEach(function(n,i){' +
'var b=document.createElement("button"),act=bws[i]===cb;' +
'b.textContent=n;' +
'b.style.cssText="background:"+(act?"rgba(25,70,25,0.95)":"rgba(12,28,12,0.85)")+";' +
'color:"+(act?"#b0ffb0":"#7aaa7a")+";border:1px solid "+(act?"rgba(120,220,120,0.7)":"rgba(55,95,55,0.45)")+";' +
'border-radius:4px;padding:5px 0;font-family:monospace;font-size:12px;cursor:pointer;' +
'transition:background 0.1s;";' +
'b.onmouseenter=function(){if(!act)this.style.background="rgba(20,50,20,0.95)";}; ' +
'b.onmouseleave=function(){if(!act)this.style.background="rgba(12,28,12,0.85)";}; ' +
'b.onclick=function(){cur.filter_bw=bws[i];pend("filter");ws2({cmd:"filter",bw:bws[i]});closeFnp();};' +
'gd.appendChild(b);' +
'});' +
'fnpOv.appendChild(gd);' +
'document.body.appendChild(fnpOv);' +
'setTimeout(function(){document.addEventListener("click",fnpOutside);},0);' +
'}' +
'q("fnpBtn").onclick=showFnp;' +
'q("volSl").oninput=function(e){ws2({cmd:"volume",v:+e.target.value});var v=q("volVal");if(v)v.textContent=e.target.value;};' +
'q("rfSl").oninput=function(e){ws2({cmd:"agctop",db:+e.target.value});var v=q("rfVal");if(v)v.textContent=e.target.value+"dB";};' +
'q("txSl").oninput=function(e){var v=+e.target.value;var lbl=q("txVal");if(lbl)lbl.textContent=v;pend("drive");ws2({cmd:"drive",v:v});};' +
'function updMoxBtn(tx){var b=q("moxBtn");if(!b)return;b.textContent=tx?"TX":"MOX";b.classList.toggle("mox-on",!!tx);}' +
'q("moxBtn").onclick=function(){audioKick();cur.transmitting=!cur.transmitting;pend("mox");updMoxBtn(cur.transmitting);cmd("set_mox",cur.transmitting);};' +
'function updToneBtn(t){var b=q("toneBtn");if(!b)return;b.classList.toggle("on",!!t);}' +
'q("toneBtn").onclick=function(){audioKick();cur.tuning=!cur.tuning;pend("tun");updToneBtn(cur.tuning);cmd("set_tun",cur.tuning);};' +
// 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});};' +
@@ -274,18 +329,12 @@ begin
'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)});' +
'};' +
'q("btnBA").onclick=()=>{vfoB=vfoA;renderVfos();pend("freq");ws2({cmd:"freq_b",hz:Math.round(vfoB)});};' +
// A=B: copy B freq into A — always freq_a (sets VFO A regardless of active VFO)
'q("btnAB").onclick=()=>{vfoA=vfoB;renderVfos();pend("freq");ws2({cmd:"freq_a",hz:Math.round(vfoA)});};' +
// SWAP — freq_a always sets VFO A, freq_b always sets VFO B
'q("btnSwap").onclick=()=>{const t=vfoA;vfoA=vfoB;vfoB=t;renderVfos();pend("freq");' +
'ws2({cmd:"freq_a",hz:Math.round(vfoA)});ws2({cmd:"freq_b",hz:Math.round(vfoB)});};' +
// SPAN кнопки: только 48K..1536K
'const SPANS=[48000,96000,192000,384000,768000,1536000];' +
@@ -355,7 +404,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"};' +
'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"};' +
'function applyState(msg){' +
'Object.keys(msg).forEach(function(k){var pk=PEND_MAP[k];if(!pk||!isPending(pk))cur[k]=msg[k];});' +
@@ -381,8 +430,10 @@ begin
'if(!isPending("snb"))updSnbBtn();if(!isPending("anf"))updAnfBtn();' +
'if(!isPending("run"))updRunBtn(!!cur.running);' +
'if(!isPending("mox"))updMoxBtn(!!cur.transmitting);' +
'if(!isPending("tun"))updToneBtn(!!cur.tuning);' +
'if(!isPending("drive")){const ts=q("txSl");if(ts&&!ts.matches(":active")){ts.value=cur.drive||0;var tv=q("txVal");if(tv)tv.textContent=cur.drive||0;}}' +
'if(!isPending("mode")){syncModGrp(cur.mode||0);}' +
'if(!isPending("attn")){const at=q("attSel");if(at)at.value=cur.attn_idx||0;}' +
'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]||"?");' +