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
+2 -1
View File
@@ -2804,7 +2804,8 @@ begin
FCenterFreq, FFilter,
FVfoB, FActiveVfo,
FTransmitting, TrkDrive.Position,
FAtten, FTuning, FDisplayDuplex);
FAtten, FTuning, FDisplayDuplex,
FLastFwdW, FLastSWR, FPAMaxPower);
end;
// ===========================================================================
+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;}' +
+18 -4
View File
@@ -247,6 +247,10 @@ type
FCurrentXvtr: Integer; // -1 = HF, иначе индекс активного XVTR
FOnXvtrBand: TWebCmdXvtrBand;
FFwdW: Double;
FSWR: Double;
FPAMaxPower: Double;
FWebClientActive: Boolean;
// ── Внутренние методы ──
@@ -290,7 +294,8 @@ type
CenterHz: Double; FilterIdx: Integer;
VfoB: Double; ActiveVfo: Integer;
Transmitting: Boolean; DriveLevel: Integer;
AttnIdx: Integer; Tuning: Boolean; Duplex: Boolean);
AttnIdx: Integer; Tuning: Boolean; Duplex: Boolean;
FwdW, SWRV, PAMaxPower: Double);
property WebClientActive: Boolean read FWebClientActive;
property FreqMhzDigits: Integer read FFreqMhzDigits write FFreqMhzDigits;
@@ -419,8 +424,11 @@ begin
FAGCTop := 90;
FSpanHz := 192000;
FVolume := 70;
FSMeter := -120;
FBandIdx := 5;
FSMeter := -120;
FFwdW := 0;
FSWR := 1.0;
FPAMaxPower := 100.0;
FBandIdx := 5;
FCurrentXvtr := -1;
FFreqMhzDigits := 3;
SetLength(FXvtrBands, 0);
@@ -1236,6 +1244,7 @@ begin
'"running":%s,"mute":%s,"ctun":%s,' +
'"nr_mode":%d,"nr":%s,"nb_mode":%d,"nb":%s,"snb":%s,"anf":%s,"connected":%s,' +
'"transmitting":%s,"drive":%d,"attn_idx":%d,"tuning":%s,"dup":%s,' +
'"fwd_w":%.1f,"swr":%.2f,"pa_max_power":%.0f,' +
'"xvtr_current":%d,"xvtr_bands":%s,"freq_mhz_digits":%d}',
[FFreq, FVfoB, FActiveVfo,
FMode, MODE_N[FMode mod 8],
@@ -1260,6 +1269,7 @@ begin
FAttnIdx,
BoolToStr(FTuning, 'true', 'false'),
BoolToStr(FDuplex, 'true', 'false'),
FFwdW, FSWR, FPAMaxPower,
FCurrentXvtr, XvtrJson, FFreqMhzDigits
], FS);
finally
@@ -1327,7 +1337,8 @@ procedure TWebServer.PushSpectrum(
CenterHz: Double; FilterIdx: Integer;
VfoB: Double; ActiveVfo: Integer;
Transmitting: Boolean; DriveLevel: Integer;
AttnIdx: Integer; Tuning: Boolean; Duplex: Boolean);
AttnIdx: Integer; Tuning: Boolean; Duplex: Boolean;
FwdW, SWRV, PAMaxPower: Double);
var N, i: Integer;
begin
if not FRunning then Exit;
@@ -1364,6 +1375,9 @@ begin
FAttnIdx := AttnIdx;
FTuning := Tuning;
FDuplex := Duplex;
FFwdW := FwdW;
FSWR := SWRV;
FPAMaxPower := PAMaxPower;
finally
FStateLock.Leave;
end;