Save/restore CTun, AGC, FM step per XVTR slot; add FM step to web

- Add LastCTun, LastAGCMode, LastAGCTop to TXvtrEntry; SaveCurrentBand
  now captures all three in the XVTR branch; ActivateXvtrBand restores
  them on slot switch/startup (previously CTun was always reset to False
  and AGC was never saved/restored for XVTR)
- WebServer: expose fmstep_idx in state JSON so web clients sync the
  FM step from desktop state
- WebPageHtml: swap stepSel options to FM steps (6.25k/12.5k/20k/25k)
  when mode=FM, restore HF steps when leaving FM; syncs fmstep_idx
  from server on connect and mode change

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 22:27:14 +03:00
co-authored by Claude Sonnet 4.6
parent c4cd8afb06
commit a59501f2c4
4 changed files with 53 additions and 13 deletions
+20 -8
View File
@@ -1030,6 +1030,9 @@ begin
FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSToneIdx := FFMCTCSSToneIdx; FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSToneIdx := FFMCTCSSToneIdx;
FXvtrSettings.Entries[FCurrentXvtr].LastFMStepOn := FFMStepOn; FXvtrSettings.Entries[FCurrentXvtr].LastFMStepOn := FFMStepOn;
FXvtrSettings.Entries[FCurrentXvtr].LastFMStepIdx := FFMStepIdx; FXvtrSettings.Entries[FCurrentXvtr].LastFMStepIdx := FFMStepIdx;
FXvtrSettings.Entries[FCurrentXvtr].LastCTun := FCTun;
FXvtrSettings.Entries[FCurrentXvtr].LastAGCMode := FAGCMode;
FXvtrSettings.Entries[FCurrentXvtr].LastAGCTop := FAGCTop;
end; end;
FSettings.SaveXvtr(FDevMAC, FXvtrSettings); FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
Exit; Exit;
@@ -4801,6 +4804,7 @@ begin
FBtnFMSteps[OldIdx].Active := False; FBtnFMSteps[OldIdx].Active := False;
if FBtnFMSteps[Idx] <> nil then if FBtnFMSteps[Idx] <> nil then
FBtnFMSteps[Idx].Active := True; FBtnFMSteps[Idx].Active := True;
if FWebServer <> nil then FWebServer.FMStepIdx := Idx;
end; end;
procedure TMainForm.BtnFMStepClick(Sender: TObject); procedure TMainForm.BtnFMStepClick(Sender: TObject);
@@ -6243,17 +6247,25 @@ begin
FFMStepOn := E.LastFMStepOn; FFMStepOn := E.LastFMStepOn;
SetFMStep(E.LastFMStepIdx); SetFMStep(E.LastFMStepIdx);
if BtnFMStep <> nil then StyleButton(BtnFMStep, FFMStepOn); if BtnFMStep <> nil then StyleButton(BtnFMStep, FFMStepOn);
// VFO на видимую частоту; FCenterFreq тоже visible (как и для HF — DDC переход // AGC
// в IF происходит только в момент UpdateState/SetRunAndFreq через XvtrTranslate). FAGCMode := E.LastAGCMode;
FAGCTop := E.LastAGCTop;
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FAGCMode);
TrkAGC.Position := FAGCTop;
LblAGCTop.Caption := Format('%ddB', [FAGCTop]);
if FWDSPReady then
begin
FDSPEngine.SetAGCTop(FAGCTop);
FDSPEngine.SetAGC(TWDSPAGCMode(FAGCMode), 50.0);
end;
FSpecView.AGCTop := FAGCTop;
// CTUN
FCTun := E.LastCTun;
StyleButton(BtnCTun, FCTun);
// VFO на видимую частоту; FCenterFreq тоже visible
FVfoA := Vis; FVfoA := Vis;
FCenterFreq := Vis; FCenterFreq := Vis;
FreqDispA.Frequency := Round(FVfoA); FreqDispA.Frequency := Round(FVfoA);
// Сбросить CTUN — не имеет смысла в начальном состоянии XVTR-band
if FCTun then
begin
FCTun := False;
StyleButton(BtnCTun, False);
end;
if FWDSPReady then FDSPEngine.SetShift(0.0); if FWDSPReady then FDSPEngine.SetShift(0.0);
// Применяем драйв (если есть кастомный TX power для этого XVTR) // Применяем драйв (если есть кастомный TX power для этого XVTR)
// Сделано на стороне ActiveTXFreqHz / TX path — здесь только обновляем дисплей. // Сделано на стороне ActiveTXFreqHz / TX path — здесь только обновляем дисплей.
+12
View File
@@ -144,6 +144,9 @@ type
LastCTCSSToneIdx: Integer; LastCTCSSToneIdx: Integer;
LastFMStepOn: Boolean; LastFMStepOn: Boolean;
LastFMStepIdx: Integer; LastFMStepIdx: Integer;
LastCTun: Boolean;
LastAGCMode: Integer;
LastAGCTop: Integer;
end; end;
TXvtrSettings = record TXvtrSettings = record
@@ -1121,6 +1124,9 @@ begin
X.Entries[i].LastCTCSSToneIdx := 0; X.Entries[i].LastCTCSSToneIdx := 0;
X.Entries[i].LastFMStepOn := True; X.Entries[i].LastFMStepOn := True;
X.Entries[i].LastFMStepIdx := 3; // 25 kHz X.Entries[i].LastFMStepIdx := 3; // 25 kHz
X.Entries[i].LastCTun := False;
X.Entries[i].LastAGCMode := 1;
X.Entries[i].LastAGCTop := 90;
end; end;
// Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились) // Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились)
X.Entries[0].ButtonText := '2m'; X.Entries[0].ButtonText := '2m';
@@ -1174,6 +1180,9 @@ begin
X.Entries[i].LastCTCSSToneIdx := EnsureRange(JI(EObj, 'last_ctcss_idx', X.Entries[i].LastCTCSSToneIdx), 0, 37); X.Entries[i].LastCTCSSToneIdx := EnsureRange(JI(EObj, 'last_ctcss_idx', X.Entries[i].LastCTCSSToneIdx), 0, 37);
X.Entries[i].LastFMStepOn := JB(EObj, 'last_fmstep_on', X.Entries[i].LastFMStepOn); X.Entries[i].LastFMStepOn := JB(EObj, 'last_fmstep_on', X.Entries[i].LastFMStepOn);
X.Entries[i].LastFMStepIdx := EnsureRange(JI(EObj, 'last_fmstep_idx', X.Entries[i].LastFMStepIdx), 0, 3); X.Entries[i].LastFMStepIdx := EnsureRange(JI(EObj, 'last_fmstep_idx', X.Entries[i].LastFMStepIdx), 0, 3);
X.Entries[i].LastCTun := JB(EObj, 'last_ctun', X.Entries[i].LastCTun);
X.Entries[i].LastAGCMode := EnsureRange(JI(EObj, 'last_agc_mode', X.Entries[i].LastAGCMode), 0, 4);
X.Entries[i].LastAGCTop := EnsureRange(JI(EObj, 'last_agc_top', X.Entries[i].LastAGCTop), 0, 120);
// Валидация: LastFreq должен быть в [FreqBegin..FreqEnd] // Валидация: LastFreq должен быть в [FreqBegin..FreqEnd]
if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or
(X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then (X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then
@@ -1212,6 +1221,9 @@ begin
JW(EObj, 'last_ctcss_idx',X.Entries[i].LastCTCSSToneIdx); JW(EObj, 'last_ctcss_idx',X.Entries[i].LastCTCSSToneIdx);
JW(EObj, 'last_fmstep_on', X.Entries[i].LastFMStepOn); JW(EObj, 'last_fmstep_on', X.Entries[i].LastFMStepOn);
JW(EObj, 'last_fmstep_idx',X.Entries[i].LastFMStepIdx); JW(EObj, 'last_fmstep_idx',X.Entries[i].LastFMStepIdx);
JW(EObj, 'last_ctun', X.Entries[i].LastCTun);
JW(EObj, 'last_agc_mode', X.Entries[i].LastAGCMode);
JW(EObj, 'last_agc_top', X.Entries[i].LastAGCTop);
end; end;
end; end;
+12 -1
View File
@@ -216,6 +216,9 @@ begin
'const stEl=document.getElementById("stEl"),latEl=document.getElementById("latEl");' + 'const stEl=document.getElementById("stEl"),latEl=document.getElementById("latEl");' +
'let cur={},vfoA=14200000,vfoB=14200000,activeVfo="A";' + 'let cur={},vfoA=14200000,vfoB=14200000,activeVfo="A";' +
'const FM_STEP_HZ=[6250,12500,20000,25000];' +
'const FM_STEP_LBL=["6.25k","12.5k","20k","25k"];' +
'let fmStepIdx=3,lastHFStep=100,inFMStep=false;' +
'let localCenter=14200000,localSpan=192000;' + // локальные копии — обновляются мгновенно при действии пользователя 'let localCenter=14200000,localSpan=192000;' + // локальные копии — обновляются мгновенно при действии пользователя
'let dragging=false,dragX0=0,dragCen0=0,dragVfo0=0,dragMs=0,wheelActiveTs=0;' + 'let dragging=false,dragX0=0,dragCen0=0,dragVfo0=0,dragMs=0,wheelActiveTs=0;' +
'let hoverX=-1,wTarget="",markerOn=false,markerX=0;' + 'let hoverX=-1,wTarget="",markerOn=false,markerX=0;' +
@@ -566,6 +569,7 @@ begin
'if(!isPending("freq")){vfoA=cur.vfo_a_hz||vfoA;}' + 'if(!isPending("freq")){vfoA=cur.vfo_a_hz||vfoA;}' +
'if(msg.vfo_b_hz!=null)vfoB=msg.vfo_b_hz;' + 'if(msg.vfo_b_hz!=null)vfoB=msg.vfo_b_hz;' +
'if(msg.freq_mhz_digits!=null){freqMhzDigits=Math.max(3,Math.min(5,msg.freq_mhz_digits|0));freqMaxHz=Math.pow(10,freqMhzDigits+6)-1;}' + 'if(msg.freq_mhz_digits!=null){freqMhzDigits=Math.max(3,Math.min(5,msg.freq_mhz_digits|0));freqMaxHz=Math.pow(10,freqMhzDigits+6)-1;}' +
'if(msg.fmstep_idx!=null){fmStepIdx=Math.max(0,Math.min(3,msg.fmstep_idx|0));if(inFMStep)syncStepSel();}' +
'if(msg.active_vfo!=null&&!isPending("freq")){' + 'if(msg.active_vfo!=null&&!isPending("freq")){' +
'const sv=msg.active_vfo===0?"A":"B";' + 'const sv=msg.active_vfo===0?"A":"B";' +
'if(sv!==activeVfo){activeVfo=sv;' + 'if(sv!==activeVfo){activeVfo=sv;' +
@@ -583,7 +587,7 @@ begin
'if(!isPending("mox"))updMoxBtn(!!cur.transmitting);' + 'if(!isPending("mox"))updMoxBtn(!!cur.transmitting);' +
'if(!isPending("tun"))updToneBtn(!!cur.tuning);' + '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("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("mode")){syncModGrp(cur.mode||0);syncStepSel();}' +
'if(!isPending("attn")){const at=q("attSel");if(at)at.value=cur.attn_idx||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);' + 'updBand(activeVfo==="A"?(cur.vfo_a_hz||0):(vfoB||0));updSpan(cur.span_hz||192000);' +
'paintSM(Number(cur.smeter_dbm||-130));' + 'paintSM(Number(cur.smeter_dbm||-130));' +
@@ -642,6 +646,13 @@ begin
'if(smCv.width!==(sw*dpr|0)||smCv.height!==(sh*dpr|0)){smCv.width=sw*dpr|0;smCv.height=sh*dpr|0;mctx.setTransform(dpr,0,0,dpr,0,0);}' + 'if(smCv.width!==(sw*dpr|0)||smCv.height!==(sh*dpr|0)){smCv.width=sw*dpr|0;smCv.height=sh*dpr|0;mctx.setTransform(dpr,0,0,dpr,0,0);}' +
'if(wfBuf.width!==wfCv.clientWidth||wfBuf.height!==wfCv.clientHeight){wfBuf.width=wfCv.clientWidth;wfBuf.height=wfCv.clientHeight;wfBCtx.fillStyle="#000";wfBCtx.fillRect(0,0,wfBuf.width,wfBuf.height);}}' + 'if(wfBuf.width!==wfCv.clientWidth||wfBuf.height!==wfCv.clientHeight){wfBuf.width=wfCv.clientWidth;wfBuf.height=wfCv.clientHeight;wfBCtx.fillStyle="#000";wfBCtx.fillRect(0,0,wfBuf.width,wfBuf.height);}}' +
'function syncStepSel(){const isFM=(cur.mode===5);const sel=q("stepSel");if(!sel)return;' +
'if(isFM&&!inFMStep){lastHFStep=+sel.value||100;sel.innerHTML="";' +
'FM_STEP_LBL.forEach((l,i)=>{const o=document.createElement("option");o.value=FM_STEP_HZ[i];o.textContent=l;sel.appendChild(o);});' +
'sel.value=FM_STEP_HZ[fmStepIdx];inFMStep=true;}' +
'else if(!isFM&&inFMStep){sel.innerHTML=["10 Hz","100 Hz","1 kHz","10 kHz"].map((l,i)=>`<option value="${[10,100,1000,10000][i]}">${l}</option>`).join("");' +
'sel.value=lastHFStep;inFMStep=false;}' +
'else if(isFM){sel.value=FM_STEP_HZ[fmStepIdx];}}' +
'function tuneStep(e){const s=+q("stepSel").value||100;if(e.ctrlKey&&e.shiftKey)return 1000000;if(e.ctrlKey)return 1000;if(e.shiftKey)return 10;return s;}' + 'function tuneStep(e){const s=+q("stepSel").value||100;if(e.ctrlKey&&e.shiftKey)return 1000000;if(e.ctrlKey)return 1000;if(e.shiftKey)return 10;return s;}' +
'function activeVfoHz(){return activeVfo==="A"?vfoA:vfoB;}' + 'function activeVfoHz(){return activeVfo==="A"?vfoA:vfoB;}' +
'function setActiveVfoHz(f){if(activeVfo==="A")vfoA=f;else vfoB=f;}' + 'function setActiveVfoHz(f){if(activeVfo==="A")vfoA=f;else vfoB=f;}' +
+9 -4
View File
@@ -212,6 +212,7 @@ type
FFilterIdx: Integer; FFilterIdx: Integer;
FVfoB: Double; FVfoB: Double;
FActiveVfo: Integer; // 0=A, 1=B FActiveVfo: Integer; // 0=A, 1=B
FFMStepIdx: Integer;
FStateLock: TCriticalSection; FStateLock: TCriticalSection;
// ── Callbacks ── // ── Callbacks ──
@@ -304,6 +305,7 @@ type
property WebClientActive: Boolean read FWebClientActive; property WebClientActive: Boolean read FWebClientActive;
property FreqMhzDigits: Integer read FFreqMhzDigits write FFreqMhzDigits; property FreqMhzDigits: Integer read FFreqMhzDigits write FFreqMhzDigits;
property FMStepIdx: Integer read FFMStepIdx write FFMStepIdx;
property OnFreq: TWebCmdFreq read FOnFreq write FOnFreq; property OnFreq: TWebCmdFreq read FOnFreq write FOnFreq;
property OnMode: TWebCmdMode read FOnMode write FOnMode; property OnMode: TWebCmdMode read FOnMode write FOnMode;
@@ -425,8 +427,9 @@ begin
FClientLock := TCriticalSection.Create; FClientLock := TCriticalSection.Create;
FStateLock := TCriticalSection.Create; FStateLock := TCriticalSection.Create;
// Начальные значения состояния // Начальные значения состояния
FFreq := 14200000; FFreq := 14200000;
FMode := 1; FMode := 1;
FFMStepIdx := 3; // 25 kHz default
FFilterBW:= 2700; FFilterBW:= 2700;
FAGCMode := 1; FAGCMode := 1;
FAGCTop := 90; FAGCTop := 90;
@@ -1296,7 +1299,8 @@ begin
'"nr_mode":%d,"nr":%s,"nb_mode":%d,"nb":%s,"snb":%s,"anf":%s,"connected":%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,' + '"transmitting":%s,"drive":%d,"attn_idx":%d,"tuning":%s,"dup":%s,' +
'"fwd_w":%.1f,"swr":%.2f,"pa_max_power":%.0f,' + '"fwd_w":%.1f,"swr":%.2f,"pa_max_power":%.0f,' +
'"xvtr_current":%d,"xvtr_bands":%s,"freq_mhz_digits":%d}', '"xvtr_current":%d,"xvtr_bands":%s,"freq_mhz_digits":%d,' +
'"fmstep_idx":%d}',
[FFreq, FVfoB, FActiveVfo, [FFreq, FVfoB, FActiveVfo,
FMode, MODE_N[FMode mod 8], FMode, MODE_N[FMode mod 8],
FFilterIdx, FFilterBW, FFilterIdx, FFilterBW,
@@ -1321,7 +1325,8 @@ begin
BoolToStr(FTuning, 'true', 'false'), BoolToStr(FTuning, 'true', 'false'),
BoolToStr(FDuplex, 'true', 'false'), BoolToStr(FDuplex, 'true', 'false'),
FFwdW, FSWR, FPAMaxPower, FFwdW, FSWR, FPAMaxPower,
FCurrentXvtr, XvtrJson, FFreqMhzDigits FCurrentXvtr, XvtrJson, FFreqMhzDigits,
FFMStepIdx
], FS); ], FS);
finally finally
FStateLock.Leave; FStateLock.Leave;