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
+56 -10
View File
@@ -29,8 +29,12 @@ type
AGCTop: Integer;
CTun: Boolean;
SpanHz: Double;
FMSQOn: Boolean;
FMSQLevel: Integer; // 0..100 → WDSP threshold 0.0..0.5
FMSQOn: Boolean;
FMSQLevel: Integer; // 0..100 → WDSP threshold 0.0..0.5
CTCSSOn: Boolean;
CTCSSToneIdx: Integer; // 0..37
FMStepOn: Boolean;
FMStepIdx: Integer; // 0..3 (6.25/12.5/20/25 kHz)
end;
// TX-настройки per-device. Хранятся в JSON-секции "tx" под MAC,
@@ -131,7 +135,15 @@ type
TXPower: Integer; // 0..100 % (drive level специально для XVTR)
DisablePA: Boolean; // True — отключить внутренний PA при TX (XVTR имеет свой)
RXAntenna: Byte; // 0=по-умолчанию(основной band), 1=ANT1, 2=ANT2, 3=ANT3
LastFreq: Double; // Hz — последняя tuned visible частота (BandStack)
LastFreq: Double; // Hz — последняя tuned visible частота (BandStack)
LastMode: Integer; // Mode (LSB=0,USB=1,…FM=5)
LastFilterIdx: Integer; // FM: 0=NFM, 1=WFM; иначе 0..FILT_COUNT-1
LastFMSQOn: Boolean;
LastFMSQLevel: Integer;
LastCTCSSOn: Boolean;
LastCTCSSToneIdx: Integer;
LastFMStepOn: Boolean;
LastFMStepIdx: Integer;
end;
TXvtrSettings = record
@@ -327,8 +339,12 @@ begin
B.AGCTop := 90;
B.CTun := False;
B.SpanHz := 192000;
B.FMSQOn := False;
B.FMSQLevel := 30;
B.FMSQOn := False;
B.FMSQLevel := 30;
B.CTCSSOn := False;
B.CTCSSToneIdx := 0;
B.FMStepOn := True;
B.FMStepIdx := 3; // 25 kHz
end;
class procedure TSettingsManager.DefaultGlobal(out G: TGlobalSettings);
@@ -697,6 +713,8 @@ begin
JW(O,'agc_mode',B.AGCMode); JW(O,'agc_top',B.AGCTop);
JW(O,'ctun',B.CTun); JW(O,'span_hz',B.SpanHz);
JW(O,'fmsq_on',B.FMSQOn); JW(O,'fmsq_level',B.FMSQLevel);
JW(O,'ctcss_on',B.CTCSSOn); JW(O,'ctcss_idx',B.CTCSSToneIdx);
JW(O,'fmstep_on',B.FMStepOn); JW(O,'fmstep_idx',B.FMStepIdx);
end;
function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer;
@@ -717,8 +735,12 @@ begin
B.AGCTop := JI(O,'agc_top', B.AGCTop);
B.CTun := JB(O,'ctun', B.CTun);
B.SpanHz := JD(O,'span_hz', B.SpanHz);
B.FMSQOn := JB(O,'fmsq_on', B.FMSQOn);
B.FMSQLevel := JI(O,'fmsq_level', B.FMSQLevel);
B.FMSQOn := JB(O,'fmsq_on', B.FMSQOn);
B.FMSQLevel := JI(O,'fmsq_level', B.FMSQLevel);
B.CTCSSOn := JB(O,'ctcss_on', B.CTCSSOn);
B.CTCSSToneIdx := EnsureRange(JI(O,'ctcss_idx', B.CTCSSToneIdx), 0, 37);
B.FMStepOn := JB(O,'fmstep_on', B.FMStepOn);
B.FMStepIdx := EnsureRange(JI(O,'fmstep_idx', B.FMStepIdx), 0, 3);
end;
procedure TSettingsManager.SaveWindowBounds(L, T, W, H: Integer);
@@ -1090,7 +1112,15 @@ begin
X.Entries[i].TXPower := 100;
X.Entries[i].DisablePA := False;
X.Entries[i].RXAntenna := 0;
X.Entries[i].LastFreq := 0.0;
X.Entries[i].LastFreq := 0.0;
X.Entries[i].LastMode := 1; // USB — VHF SSB calling default
X.Entries[i].LastFilterIdx := 0; // NFM
X.Entries[i].LastFMSQOn := False;
X.Entries[i].LastFMSQLevel := 30;
X.Entries[i].LastCTCSSOn := False;
X.Entries[i].LastCTCSSToneIdx := 0;
X.Entries[i].LastFMStepOn := True;
X.Entries[i].LastFMStepIdx := 3; // 25 kHz
end;
// Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились)
X.Entries[0].ButtonText := '2m';
@@ -1135,7 +1165,15 @@ begin
X.Entries[i].TXPower := EnsureRange(JI(EObj, 'tx_pwr', X.Entries[i].TXPower), 0, 100);
X.Entries[i].DisablePA := JB(EObj, 'dis_pa', X.Entries[i].DisablePA);
X.Entries[i].RXAntenna := EnsureRange(JI(EObj, 'rx_ant', X.Entries[i].RXAntenna), 0, 3);
X.Entries[i].LastFreq := JD(EObj, 'last_freq', X.Entries[i].LastFreq);
X.Entries[i].LastFreq := JD(EObj, 'last_freq', X.Entries[i].LastFreq);
X.Entries[i].LastMode := EnsureRange(JI(EObj, 'last_mode', X.Entries[i].LastMode), 0, 7);
X.Entries[i].LastFilterIdx := EnsureRange(JI(EObj, 'last_filt', X.Entries[i].LastFilterIdx), 0, 9);
X.Entries[i].LastFMSQOn := JB(EObj, 'last_fmsq_on', X.Entries[i].LastFMSQOn);
X.Entries[i].LastFMSQLevel := EnsureRange(JI(EObj, 'last_fmsq_lvl', X.Entries[i].LastFMSQLevel), 0, 100);
X.Entries[i].LastCTCSSOn := JB(EObj, 'last_ctcss_on', X.Entries[i].LastCTCSSOn);
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].LastFMStepIdx := EnsureRange(JI(EObj, 'last_fmstep_idx', X.Entries[i].LastFMStepIdx), 0, 3);
// Валидация: LastFreq должен быть в [FreqBegin..FreqEnd]
if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or
(X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then
@@ -1165,7 +1203,15 @@ begin
JW(EObj, 'tx_pwr', X.Entries[i].TXPower);
JW(EObj, 'dis_pa', X.Entries[i].DisablePA);
JW(EObj, 'rx_ant', Integer(X.Entries[i].RXAntenna));
JW(EObj, 'last_freq', X.Entries[i].LastFreq);
JW(EObj, 'last_freq', X.Entries[i].LastFreq);
JW(EObj, 'last_mode', X.Entries[i].LastMode);
JW(EObj, 'last_filt', X.Entries[i].LastFilterIdx);
JW(EObj, 'last_fmsq_on', X.Entries[i].LastFMSQOn);
JW(EObj, 'last_fmsq_lvl', X.Entries[i].LastFMSQLevel);
JW(EObj, 'last_ctcss_on', X.Entries[i].LastCTCSSOn);
JW(EObj, 'last_ctcss_idx',X.Entries[i].LastCTCSSToneIdx);
JW(EObj, 'last_fmstep_on', X.Entries[i].LastFMStepOn);
JW(EObj, 'last_fmstep_idx',X.Entries[i].LastFMStepIdx);
end;
end;