Web TX meter: show power/SWR in S-meter canvas during transmit

- WebServer: add fwd_w, swr, pa_max_power to state JSON and PushSpectrum
- MainForm: pass FLastFwdW, FLastSWR, FPAMaxPower to PushSpectrum
- Web UI: paintSM switches to paintTXMeter when transmitting/tuning;
  same layout as desktop — two sub-bars at 44-58% height, power ticks
  above, SWR ticks below, SWR>2.5 turns red with "SWR High" label

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 14:14:52 +03:00
co-authored by Claude Sonnet 4.6
parent 96b0ccb2c7
commit 76e5c5514a
3 changed files with 86 additions and 5 deletions
+66
View File
@@ -412,6 +412,7 @@ begin
'let smAvg=-130,smPeak=-130,smMin=-130;' +
'function dbToX(db,bx,bw){const DB_MIN=-127,DB_MAX=-13;return Math.max(bx,Math.min(bx+bw,Math.round(bx+(db-DB_MIN)/(DB_MAX-DB_MIN)*bw)));}' +
'function paintSM(dbm){' +
'if(cur.transmitting||cur.tuning){paintTXMeter();return;}' +
'const w=smCv.clientWidth||340,h=smCv.clientHeight||64;' +
'const AVG_ALPHA=0.40,ZONE_DB=6,PEAK_BASE=0.12,MIN_BASE=0.12,ACCEL=0.030,MAX_A=0.92;' +
'const DBM_MARKS=[-120,-100,-80,-60,-40,-20];' +
@@ -460,6 +461,71 @@ begin
'mctx.font="8px Courier New";mctx.fillStyle="#90A070";mctx.fillText("dBm",lblZoneL+4+twDbm+2,YT-10);' +
'mctx.fillStyle="#F0F0F0";mctx.font="bold 12px Courier New";mctx.fillText(sLbl,lblZoneL+4,YB+14);}' +
'function paintTXMeter(){' +
'const w=smCv.clientWidth||340,h=smCv.clientHeight||64;' +
'const fwdW=cur.fwd_w||0,swr=Math.max(1,cur.swr||1),maxW=cur.pa_max_power||100;' +
'const SWR_HIGH=2.5,SWR_MAX=10.0,TICK_LONG=5;' +
'const swrHigh=swr>SWR_HIGH;' +
'mctx.font="bold 12px Courier New";' +
'const lblPwr=fwdW.toFixed(0)+"W",lblSWR="SWR:"+swr.toFixed(1);' +
'const maxTWPwr=mctx.measureText(maxW.toFixed(0)+"W").width;' +
'const maxTWSWR=mctx.measureText("SWR:10.0").width;' +
'mctx.font="8px Courier New";' +
'const labelNeed=Math.max(maxTWPwr,maxTWSWR)+8;' +
'const leftInfo=Math.max(62,labelNeed+4),BX=leftInfo,BW=Math.max(10,w-leftInfo-8),lblZoneL=BX-labelNeed;' +
'const YT=Math.round(h*44/100),YB=Math.round(h*58/100);' +
'const YM=Math.round((YT+YB)/2);' +
'const PY1=YT,PY2=YM-1,SY1=YM+1,SY2=YB;' +
'const swarnX=BX+Math.round(Math.max(0,Math.min(1,(SWR_HIGH-1)/(SWR_MAX-1)))*BW);' +
'function pwrX(p){return BX+Math.round(Math.max(0,Math.min(1,p))*BW);}' +
'function swrX(s){return BX+Math.round(Math.max(0,Math.min(1,(s-1)/(SWR_MAX-1)))*BW);}' +
'mctx.clearRect(0,0,w,h);' +
// фоновые зоны
'mctx.fillStyle="#001F09";mctx.fillRect(BX,PY1,BW,PY2-PY1);' +
'mctx.fillStyle="#001F09";mctx.fillRect(BX,SY1,swarnX-BX,SY2-SY1);' +
'mctx.fillStyle="#281509";mctx.fillRect(swarnX,SY1,BX+BW-swarnX,SY2-SY1);' +
// бар мощности
'const pEnd=pwrX(maxW>0?fwdW/maxW:0);' +
'if(pEnd>BX){mctx.fillStyle="#44CC00";mctx.fillRect(BX,PY1+1,pEnd-BX,PY2-PY1-2);}' +
// бар КСВ
'const sEnd=swrX(swr);' +
'if(sEnd>BX){' +
'if(!swrHigh){mctx.fillStyle="#FFAA00";mctx.fillRect(BX,SY1+1,sEnd-BX,SY2-SY1-2);}' +
'else{const safeEnd=Math.min(swarnX,sEnd);' +
'if(safeEnd>BX){mctx.fillStyle="#FFAA00";mctx.fillRect(BX,SY1+1,safeEnd-BX,SY2-SY1-2);}' +
'if(sEnd>swarnX){mctx.fillStyle="#CC0000";mctx.fillRect(swarnX,SY1+1,sEnd-swarnX,SY2-SY1-2);}}}' +
// делитель 2.5
'mctx.strokeStyle="#404040";mctx.lineWidth=1;mctx.beginPath();mctx.moveTo(swarnX,SY1);mctx.lineTo(swarnX,SY2);mctx.stroke();' +
// рамки
'mctx.lineWidth=1;mctx.strokeStyle="#283030";mctx.strokeRect(lblZoneL,PY1,BX+BW-lblZoneL,PY2-PY1);' +
'mctx.strokeStyle=swrHigh?"#CC0000":"#283030";mctx.strokeRect(lblZoneL,SY1,BX+BW-lblZoneL,SY2-SY1);' +
// разделители зона надписей / шкала
'mctx.strokeStyle="#404040";mctx.lineWidth=1;' +
'mctx.beginPath();mctx.moveTo(BX,PY1+1);mctx.lineTo(BX,PY2-1);mctx.stroke();' +
'mctx.beginPath();mctx.moveTo(BX,SY1+1);mctx.lineTo(BX,SY2-1);mctx.stroke();' +
// тики мощности выше power-бара
'mctx.font="8px Courier New";' +
'const pwrMarks=[0,0.25,0.5,0.75,1.0];' +
'for(let i=0;i<pwrMarks.length;i++){const x=pwrX(pwrMarks[i]);' +
'mctx.strokeStyle="#DCE0E0";mctx.lineWidth=1;mctx.beginPath();mctx.moveTo(x,PY1-TICK_LONG);mctx.lineTo(x,PY1-1);mctx.stroke();' +
'const t=(pwrMarks[i]*maxW).toFixed(0),tw=mctx.measureText(t).width;' +
'mctx.fillStyle="#DCDCD0";mctx.fillText(t,Math.round(x-tw/2),PY1-TICK_LONG-3);}' +
// тики КСВ ниже SWR-бара
'const swrMarks=[1,2.5,5,10],swrLbls=["1","2.5","5","10"];' +
'for(let i=0;i<swrMarks.length;i++){const x=swrX(swrMarks[i]),isW=swrMarks[i]>=SWR_HIGH;' +
'mctx.strokeStyle=isW?"#90A070":"#708070";mctx.lineWidth=1;mctx.beginPath();mctx.moveTo(x,SY2+1);mctx.lineTo(x,SY2+TICK_LONG);mctx.stroke();' +
'const t=swrLbls[i],tw=mctx.measureText(t).width;' +
'mctx.fillStyle=isW?(swrHigh?"#CC0000":"#90D090"):"#E0E0E0";' +
'mctx.fillText(t,Math.round(x-tw/2),SY2+TICK_LONG+11);}' +
// надписи в левой зоне
'mctx.font="bold 12px Courier New";' +
'mctx.fillStyle="#DCD896";mctx.fillText(lblPwr,lblZoneL+4,PY1-12-2);' +
'mctx.fillStyle=swrHigh?"#CC0000":"#F0F0F0";mctx.fillText(lblSWR,lblZoneL+4,SY2+14);' +
// "SWR High" рядом с мощностью
'if(swrHigh){const twP=mctx.measureText(lblPwr).width;' +
'mctx.font="bold 8px Courier New";mctx.fillStyle="#CC0000";' +
'mctx.fillText("SWR High",lblZoneL+4+twP+2,PY1-8);}}' +
// pendingCmds: команды в пути — не перезаписываем эти поля из server state
'var pendingCmds={};' +
'function pend(field){pendingCmds[field]=Date.now()+600;}' +