Fix XVTR/FM frequency, mode and step persistence

- Fix web spectrum scroll resetting freq to XVTR minimum (same root
  cause as desktop: hardcoded 60 MHz clamp; now delegates to server)
- Save/restore FM mode, NFM/WFM filter, squelch, CTCSS per XVTR slot
  and per HF band (CTCSSOn/ToneIdx added to TBandSettings; full state
  added to TXvtrEntry; ActivateXvtrBand restores all on slot switch)
- Fix UpdateFilterButtons always resetting NFM/WFM to default on
  mode restore; now preserves saved filter index
- Add FM Step tuning: panel with STEP toggle + 6.25/12.5/20/25 kHz
  selector (popup); mouse wheel uses selected step in FM mode;
  state saved per band and XVTR slot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 22:08:11 +03:00
co-authored by Claude Sonnet 4.6
parent 87f449c737
commit c4cd8afb06
3 changed files with 241 additions and 27 deletions
+180 -14
View File
@@ -104,6 +104,12 @@ const
'156.7', '162.2', '167.9', '173.8', '179.9', '186.2', '192.8', '203.5',
'210.7', '218.1', '225.7', '233.6', '241.8', '250.3');
// FM Step tuning
FM_STEP_COUNT = 4;
FM_STEP_HZ: array[0..3] of Integer = (6250, 12500, 20000, 25000);
FM_STEP_NAMES: array[0..3] of string = ('6.25k', '12.5k', '20k', '25k');
FM_STEP_DEF = 3; // 25 kHz
BAND_NAMES: array[0..BAND_COUNT-1] of string = (
'160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
BAND_FREQ: array[0..BAND_COUNT-1] of Double = (
@@ -258,6 +264,8 @@ type
FFMCTCSSToneIdx: Integer;
FFMSQOn: Boolean;
FFMSQLevel: Integer;
FFMStepOn: Boolean;
FFMStepIdx: Integer;
FWfManualHigh: Double;
FWfManualLow: Double;
FWfAGCOffset: Double;
@@ -373,6 +381,11 @@ type
BtnFMCTCSSTone: TFlatButton;
PanelFMCTCSSPop: TPanel;
FBtnCTCSSTones: array[0..CTCSS_COUNT - 1] of TFlatButton;
PanelFMStep: TPanel;
BtnFMStep: TFlatButton;
BtnFMStepSel: TFlatButton;
PanelFMStepPop: TPanel;
FBtnFMSteps: array[0..FM_STEP_COUNT - 1] of TFlatButton;
PanelRX: TPanel;
BtnAGCMode: array[0..4] of TFlatButton; // FAST MED SLOW LONG OFF
@@ -491,6 +504,11 @@ type
procedure BtnFMToneSelectClick(Sender: TObject);
procedure SetFMCTCSSTone(Idx: Integer);
procedure CloseCTCSSPopup;
procedure SetFMStep(Idx: Integer);
procedure CloseFMStepPopup;
procedure BtnFMStepClick(Sender: TObject);
procedure BtnFMStepSelClick(Sender: TObject);
procedure BtnFMStepSelectClick(Sender: TObject);
procedure PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
@@ -910,8 +928,12 @@ begin
Result.AGCTop := FAGCTop;
Result.CTun := FCTun;
Result.SpanHz := FSpanHz;
Result.FMSQOn := FFMSQOn;
Result.FMSQLevel := FFMSQLevel;
Result.FMSQOn := FFMSQOn;
Result.FMSQLevel := FFMSQLevel;
Result.CTCSSOn := FFMCTCSSOn;
Result.CTCSSToneIdx := FFMCTCSSToneIdx;
Result.FMStepOn := FFMStepOn;
Result.FMStepIdx := FFMStepIdx;
// Waterfall AGC/NF — глобальные (не диапазонные)
end;
@@ -998,7 +1020,17 @@ begin
if FCurrentXvtr >= 0 then
begin
if (FCurrentXvtr < CFG_XVTR_COUNT) then
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
begin
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
FXvtrSettings.Entries[FCurrentXvtr].LastMode := FMode;
FXvtrSettings.Entries[FCurrentXvtr].LastFilterIdx := FFilter;
FXvtrSettings.Entries[FCurrentXvtr].LastFMSQOn := FFMSQOn;
FXvtrSettings.Entries[FCurrentXvtr].LastFMSQLevel := FFMSQLevel;
FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSOn := FFMCTCSSOn;
FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSToneIdx := FFMCTCSSToneIdx;
FXvtrSettings.Entries[FCurrentXvtr].LastFMStepOn := FFMStepOn;
FXvtrSettings.Entries[FCurrentXvtr].LastFMStepIdx := FFMStepIdx;
end;
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
Exit;
end;
@@ -1047,10 +1079,17 @@ begin
end;
FSpecView.AGCTop := FAGCTop;
// --- FM Squelch ---
// --- FM Squelch + CTCSS ---
FFMSQOn := B.FMSQOn;
FFMSQLevel := B.FMSQLevel;
// UI update happens inside UpdateFilterButtons (called above) when mode=FM
FFMCTCSSOn := B.CTCSSOn;
if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, FFMCTCSSOn);
SetFMCTCSSTone(B.CTCSSToneIdx);
ApplyFMSquelch;
FFMStepOn := B.FMStepOn;
SetFMStep(B.FMStepIdx);
if BtnFMStep <> nil then StyleButton(BtnFMStep, FFMStepOn);
// --- CTUN ---
FCTun := B.CTun;
@@ -1139,6 +1178,8 @@ begin
FFMCTCSSToneIdx := 0;
FFMSQOn := False;
FFMSQLevel := 30;
FFMStepOn := True;
FFMStepIdx := FM_STEP_DEF;
FSpectrumBufCount := 1024;
FWaterfallBufCount := 1024;
FWaterfallFrameInterval := 2;
@@ -1793,6 +1834,37 @@ begin
FBtnCTCSSTones[i] := B;
end;
// FM Step panel (visible only in FM mode, between Filter and SQL)
PanelFMStep := TPanel.Create(Self);
PanelFMStep.Parent := PanelLeft;
PanelFMStep.SetBounds(0, Y, LEFT_W, 50);
PanelFMStep.BevelOuter := bvNone;
PanelFMStep.Visible := False;
MakeLbl(PanelFMStep, 'STEP', 4, 4);
W := LEFT_W div 3 - 2;
BtnFMStep := MakeBtn(PanelFMStep, 'STEP', 2, 20, W, BTN_H, BtnFMStepClick);
StyleButton(BtnFMStep, FFMStepOn);
BtnFMStepSel := MakeBtn(PanelFMStep, FM_STEP_NAMES[FFMStepIdx],
W + 4, 20, LEFT_W - W - 8, BTN_H, BtnFMStepSelClick);
StyleButton(BtnFMStepSel, False);
// Step picker popup (child of PanelLeft)
PanelFMStepPop := TPanel.Create(Self);
PanelFMStepPop.Parent := PanelLeft;
PanelFMStepPop.BevelOuter := bvNone;
PanelFMStepPop.Color := CLR_PANEL;
PanelFMStepPop.Visible := False;
W := (LEFT_W - 4) div FM_STEP_COUNT;
PanelFMStepPop.SetBounds(0, 0, LEFT_W, BTN_SM);
for i := 0 to FM_STEP_COUNT - 1 do
begin
B := MakeBtn(PanelFMStepPop, FM_STEP_NAMES[i],
2 + i * W, 0, W - 2, BTN_SM, BtnFMStepSelectClick);
B.Tag := i;
StyleButton(B, i = FFMStepIdx);
FBtnFMSteps[i] := B;
end;
// CTUN / DUP buttons (display-related)
BtnCTun := MakeBtn(PanelLeft, 'CTUN', 2, Y, LEFT_W div 3 - 2, BTN_H, BtnCTunClick);
BtnCTun.Tag := 0;
@@ -2394,6 +2466,8 @@ begin
if PanelFMSQ <> nil then DP(PanelFMSQ);
if PanelFMCTCSS <> nil then DP(PanelFMCTCSS);
if PanelFMCTCSSPop <> nil then DP(PanelFMCTCSSPop);
if PanelFMStep <> nil then DP(PanelFMStep);
if PanelFMStepPop <> nil then DP(PanelFMStepPop);
DP(PanelRX); DP(PanelTX);
DP(PanelRight); DP(PanelSpanButtons);
@@ -4037,9 +4111,9 @@ begin
begin
// FM: show only NFM and WFM buttons, resize them to fill the row
CloseCTCSSPopup;
FFilter := FILT_FM_DEF;
FFilterBW := FILT_FM_BW[FILT_FM_DEF];
FFMDeviation := FILT_FM_DEV[FILT_FM_DEF];
if FFilter > 1 then FFilter := FILT_FM_DEF; // sanitize, keep NFM/WFM as-is
FFilterBW := FILT_FM_BW[FFilter];
FFMDeviation := FILT_FM_DEV[FFilter];
W := (PanelFilter.Width - 6) div 2;
BtnFilter[0].Caption := FILT_FM_NAMES[0];
BtnFilter[0].Left := 2;
@@ -4064,6 +4138,12 @@ begin
LblFMSQ.Caption := IntToStr(FFMSQLevel);
end;
if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := True;
if PanelFMStep <> nil then
begin
StyleButton(BtnFMStep, FFMStepOn);
BtnFMStepSel.Caption := FM_STEP_NAMES[FFMStepIdx];
PanelFMStep.Visible := True;
end;
RelayoutBelowBands;
Exit;
end;
@@ -4072,6 +4152,7 @@ begin
PanelFilter.Height := 74;
if PanelFMSQ <> nil then PanelFMSQ.Visible := False;
if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := False;
if PanelFMStep <> nil then begin CloseFMStepPopup; PanelFMStep.Visible := False; end;
W := (PanelFilter.Width - 6) div 5;
for i := 0 to FILT_COUNT - 1 do
begin
@@ -4697,6 +4778,63 @@ procedure TMainForm.CloseCTCSSPopup;
begin
if (PanelFMCTCSSPop <> nil) and PanelFMCTCSSPop.Visible then
PanelFMCTCSSPop.Visible := False;
CloseFMStepPopup;
end;
procedure TMainForm.CloseFMStepPopup;
begin
if (PanelFMStepPop <> nil) and PanelFMStepPop.Visible then
PanelFMStepPop.Visible := False;
end;
procedure TMainForm.SetFMStep(Idx: Integer);
var OldIdx: Integer;
begin
OldIdx := FFMStepIdx;
FFMStepIdx := Idx;
if BtnFMStepSel <> nil then
begin
BtnFMStepSel.Caption := FM_STEP_NAMES[Idx];
BtnFMStepSel.Invalidate;
end;
if (OldIdx <> Idx) and (FBtnFMSteps[OldIdx] <> nil) then
FBtnFMSteps[OldIdx].Active := False;
if FBtnFMSteps[Idx] <> nil then
FBtnFMSteps[Idx].Active := True;
end;
procedure TMainForm.BtnFMStepClick(Sender: TObject);
begin
FFMStepOn := not FFMStepOn;
StyleButton(BtnFMStep, FFMStepOn);
BtnFMStep.Repaint;
SaveCurrentBand;
end;
procedure TMainForm.BtnFMStepSelClick(Sender: TObject);
var Y0: Integer;
begin
if PanelFMStepPop.Visible then
begin
CloseFMStepPopup;
Exit;
end;
CloseCTCSSPopup;
Y0 := PanelFMStep.Top + PanelFMStep.Height + 2;
if Y0 + PanelFMStepPop.Height > PanelLeft.ClientHeight then
Y0 := PanelFMStep.Top - PanelFMStepPop.Height - 2;
if Y0 < 0 then Y0 := 0;
PanelFMStepPop.Top := Y0;
PanelFMStepPop.Left := 0;
PanelFMStepPop.BringToFront;
PanelFMStepPop.Visible := True;
end;
procedure TMainForm.BtnFMStepSelectClick(Sender: TObject);
begin
CloseFMStepPopup;
SetFMStep((Sender as TFlatButton).Tag);
SaveCurrentBand;
end;
procedure TMainForm.SetFMCTCSSTone(Idx: Integer);
@@ -5967,6 +6105,10 @@ begin
Y := PanelBands.Top + PanelBands.Height + 2;
PanelMode.Top := Y; Y := Y + PanelMode.Height + 2;
PanelFilter.Top := Y; Y := Y + PanelFilter.Height + 2;
if (PanelFMStep <> nil) and PanelFMStep.Visible then
begin
PanelFMStep.Top := Y; Y := Y + PanelFMStep.Height + 2;
end;
if (PanelFMSQ <> nil) and PanelFMSQ.Visible then
begin
PanelFMSQ.Top := Y; Y := Y + PanelFMSQ.Height + 2;
@@ -6066,16 +6208,16 @@ procedure TMainForm.ActivateXvtrBand(Idx: Integer);
var
Vis: Double;
i: Integer;
E: TXvtrEntry;
begin
if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit;
if not FXvtrSettings.Entries[Idx].Enabled then Exit;
FCurrentXvtr := Idx;
E := FXvtrSettings.Entries[Idx];
// Выбор начальной частоты: LastFreq если в диапазоне, иначе середина
Vis := FXvtrSettings.Entries[Idx].LastFreq;
if (Vis < FXvtrSettings.Entries[Idx].FreqBegin) or
(Vis > FXvtrSettings.Entries[Idx].FreqEnd) then
Vis := (FXvtrSettings.Entries[Idx].FreqBegin +
FXvtrSettings.Entries[Idx].FreqEnd) / 2.0;
Vis := E.LastFreq;
if (Vis < E.FreqBegin) or (Vis > E.FreqEnd) then
Vis := (E.FreqBegin + E.FreqEnd) / 2.0;
// Отключаем кнопки HF band
for i := 0 to BAND_COUNT - 1 do
StyleButton(BtnBand[i], False);
@@ -6085,6 +6227,22 @@ begin
StyleButton(BtnXvtrBand[i], i = Idx);
// Применяем XVTR режим к сети (XVTR enable bit + DDC IN + DisablePA + RX ant)
ApplyXvtrToNetwork;
// Восстанавливаем Mode + FM Squelch + CTCSS для этого XVTR-слота
FFMSQOn := E.LastFMSQOn;
FFMSQLevel := E.LastFMSQLevel;
FMode := E.LastMode;
FFilter := E.LastFilterIdx;
for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FMode);
if FWDSPReady then FDSPEngine.SetMode(FMode);
UpdateFilterButtons; // показывает/скрывает FM-панели, обновляет SQL UI
ApplyModeFilter;
FFMCTCSSOn := E.LastCTCSSOn;
if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, FFMCTCSSOn);
SetFMCTCSSTone(E.LastCTCSSToneIdx);
ApplyFMSquelch;
FFMStepOn := E.LastFMStepOn;
SetFMStep(E.LastFMStepIdx);
if BtnFMStep <> nil then StyleButton(BtnFMStep, FFMStepOn);
// VFO на видимую частоту; FCenterFreq тоже visible (как и для HF — DDC переход
// в IF происходит только в момент UpdateState/SetRunAndFreq через XvtrTranslate).
FVfoA := Vis;
@@ -6210,7 +6368,10 @@ var
Base: Int64;
NewFreq: Int64;
begin
if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000
if (FMode = MODE_FM) and FFMStepOn and
not ((ssCtrl in Shift) or (ssShift in Shift)) then
Step := FM_STEP_HZ[FFMStepIdx]
else if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000
else if ssCtrl in Shift then Step := 1000
else if ssShift in Shift then Step := 10
else Step := 100;
@@ -6230,7 +6391,12 @@ begin
NewFreq := Base; // снэп к нижней границе
end;
NewFreq := Max(30000, Min(60000000, NewFreq));
// В XVTR-режиме HF-клэмп не применяем: ApplyVfoA сам ограничит по FreqBegin/FreqEnd
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and
FXvtrSettings.Entries[FCurrentXvtr].Enabled then
NewFreq := Max(0, NewFreq)
else
NewFreq := Max(30000, Min(60000000, NewFreq));
ApplyVfoA(NewFreq);
Handled := True;