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', '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'); '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 = ( BAND_NAMES: array[0..BAND_COUNT-1] of string = (
'160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m'); '160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
BAND_FREQ: array[0..BAND_COUNT-1] of Double = ( BAND_FREQ: array[0..BAND_COUNT-1] of Double = (
@@ -258,6 +264,8 @@ type
FFMCTCSSToneIdx: Integer; FFMCTCSSToneIdx: Integer;
FFMSQOn: Boolean; FFMSQOn: Boolean;
FFMSQLevel: Integer; FFMSQLevel: Integer;
FFMStepOn: Boolean;
FFMStepIdx: Integer;
FWfManualHigh: Double; FWfManualHigh: Double;
FWfManualLow: Double; FWfManualLow: Double;
FWfAGCOffset: Double; FWfAGCOffset: Double;
@@ -373,6 +381,11 @@ type
BtnFMCTCSSTone: TFlatButton; BtnFMCTCSSTone: TFlatButton;
PanelFMCTCSSPop: TPanel; PanelFMCTCSSPop: TPanel;
FBtnCTCSSTones: array[0..CTCSS_COUNT - 1] of TFlatButton; 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; PanelRX: TPanel;
BtnAGCMode: array[0..4] of TFlatButton; // FAST MED SLOW LONG OFF BtnAGCMode: array[0..4] of TFlatButton; // FAST MED SLOW LONG OFF
@@ -491,6 +504,11 @@ type
procedure BtnFMToneSelectClick(Sender: TObject); procedure BtnFMToneSelectClick(Sender: TObject);
procedure SetFMCTCSSTone(Idx: Integer); procedure SetFMCTCSSTone(Idx: Integer);
procedure CloseCTCSSPopup; 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; procedure PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer); Shift: TShiftState; X, Y: Integer);
procedure PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState; procedure PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
@@ -910,8 +928,12 @@ begin
Result.AGCTop := FAGCTop; Result.AGCTop := FAGCTop;
Result.CTun := FCTun; Result.CTun := FCTun;
Result.SpanHz := FSpanHz; Result.SpanHz := FSpanHz;
Result.FMSQOn := FFMSQOn; Result.FMSQOn := FFMSQOn;
Result.FMSQLevel := FFMSQLevel; Result.FMSQLevel := FFMSQLevel;
Result.CTCSSOn := FFMCTCSSOn;
Result.CTCSSToneIdx := FFMCTCSSToneIdx;
Result.FMStepOn := FFMStepOn;
Result.FMStepIdx := FFMStepIdx;
// Waterfall AGC/NF — глобальные (не диапазонные) // Waterfall AGC/NF — глобальные (не диапазонные)
end; end;
@@ -998,7 +1020,17 @@ begin
if FCurrentXvtr >= 0 then if FCurrentXvtr >= 0 then
begin begin
if (FCurrentXvtr < CFG_XVTR_COUNT) then 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); FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
Exit; Exit;
end; end;
@@ -1047,10 +1079,17 @@ begin
end; end;
FSpecView.AGCTop := FAGCTop; FSpecView.AGCTop := FAGCTop;
// --- FM Squelch --- // --- FM Squelch + CTCSS ---
FFMSQOn := B.FMSQOn; FFMSQOn := B.FMSQOn;
FFMSQLevel := B.FMSQLevel; FFMSQLevel := B.FMSQLevel;
// UI update happens inside UpdateFilterButtons (called above) when mode=FM // 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 --- // --- CTUN ---
FCTun := B.CTun; FCTun := B.CTun;
@@ -1139,6 +1178,8 @@ begin
FFMCTCSSToneIdx := 0; FFMCTCSSToneIdx := 0;
FFMSQOn := False; FFMSQOn := False;
FFMSQLevel := 30; FFMSQLevel := 30;
FFMStepOn := True;
FFMStepIdx := FM_STEP_DEF;
FSpectrumBufCount := 1024; FSpectrumBufCount := 1024;
FWaterfallBufCount := 1024; FWaterfallBufCount := 1024;
FWaterfallFrameInterval := 2; FWaterfallFrameInterval := 2;
@@ -1793,6 +1834,37 @@ begin
FBtnCTCSSTones[i] := B; FBtnCTCSSTones[i] := B;
end; 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) // CTUN / DUP buttons (display-related)
BtnCTun := MakeBtn(PanelLeft, 'CTUN', 2, Y, LEFT_W div 3 - 2, BTN_H, BtnCTunClick); BtnCTun := MakeBtn(PanelLeft, 'CTUN', 2, Y, LEFT_W div 3 - 2, BTN_H, BtnCTunClick);
BtnCTun.Tag := 0; BtnCTun.Tag := 0;
@@ -2394,6 +2466,8 @@ begin
if PanelFMSQ <> nil then DP(PanelFMSQ); if PanelFMSQ <> nil then DP(PanelFMSQ);
if PanelFMCTCSS <> nil then DP(PanelFMCTCSS); if PanelFMCTCSS <> nil then DP(PanelFMCTCSS);
if PanelFMCTCSSPop <> nil then DP(PanelFMCTCSSPop); if PanelFMCTCSSPop <> nil then DP(PanelFMCTCSSPop);
if PanelFMStep <> nil then DP(PanelFMStep);
if PanelFMStepPop <> nil then DP(PanelFMStepPop);
DP(PanelRX); DP(PanelTX); DP(PanelRX); DP(PanelTX);
DP(PanelRight); DP(PanelSpanButtons); DP(PanelRight); DP(PanelSpanButtons);
@@ -4037,9 +4111,9 @@ begin
begin begin
// FM: show only NFM and WFM buttons, resize them to fill the row // FM: show only NFM and WFM buttons, resize them to fill the row
CloseCTCSSPopup; CloseCTCSSPopup;
FFilter := FILT_FM_DEF; if FFilter > 1 then FFilter := FILT_FM_DEF; // sanitize, keep NFM/WFM as-is
FFilterBW := FILT_FM_BW[FILT_FM_DEF]; FFilterBW := FILT_FM_BW[FFilter];
FFMDeviation := FILT_FM_DEV[FILT_FM_DEF]; FFMDeviation := FILT_FM_DEV[FFilter];
W := (PanelFilter.Width - 6) div 2; W := (PanelFilter.Width - 6) div 2;
BtnFilter[0].Caption := FILT_FM_NAMES[0]; BtnFilter[0].Caption := FILT_FM_NAMES[0];
BtnFilter[0].Left := 2; BtnFilter[0].Left := 2;
@@ -4064,6 +4138,12 @@ begin
LblFMSQ.Caption := IntToStr(FFMSQLevel); LblFMSQ.Caption := IntToStr(FFMSQLevel);
end; end;
if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := True; 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; RelayoutBelowBands;
Exit; Exit;
end; end;
@@ -4072,6 +4152,7 @@ begin
PanelFilter.Height := 74; PanelFilter.Height := 74;
if PanelFMSQ <> nil then PanelFMSQ.Visible := False; if PanelFMSQ <> nil then PanelFMSQ.Visible := False;
if PanelFMCTCSS <> nil then PanelFMCTCSS.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; W := (PanelFilter.Width - 6) div 5;
for i := 0 to FILT_COUNT - 1 do for i := 0 to FILT_COUNT - 1 do
begin begin
@@ -4697,6 +4778,63 @@ procedure TMainForm.CloseCTCSSPopup;
begin begin
if (PanelFMCTCSSPop <> nil) and PanelFMCTCSSPop.Visible then if (PanelFMCTCSSPop <> nil) and PanelFMCTCSSPop.Visible then
PanelFMCTCSSPop.Visible := False; 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; end;
procedure TMainForm.SetFMCTCSSTone(Idx: Integer); procedure TMainForm.SetFMCTCSSTone(Idx: Integer);
@@ -5967,6 +6105,10 @@ begin
Y := PanelBands.Top + PanelBands.Height + 2; Y := PanelBands.Top + PanelBands.Height + 2;
PanelMode.Top := Y; Y := Y + PanelMode.Height + 2; PanelMode.Top := Y; Y := Y + PanelMode.Height + 2;
PanelFilter.Top := Y; Y := Y + PanelFilter.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 if (PanelFMSQ <> nil) and PanelFMSQ.Visible then
begin begin
PanelFMSQ.Top := Y; Y := Y + PanelFMSQ.Height + 2; PanelFMSQ.Top := Y; Y := Y + PanelFMSQ.Height + 2;
@@ -6066,16 +6208,16 @@ procedure TMainForm.ActivateXvtrBand(Idx: Integer);
var var
Vis: Double; Vis: Double;
i: Integer; i: Integer;
E: TXvtrEntry;
begin begin
if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit; if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit;
if not FXvtrSettings.Entries[Idx].Enabled then Exit; if not FXvtrSettings.Entries[Idx].Enabled then Exit;
FCurrentXvtr := Idx; FCurrentXvtr := Idx;
E := FXvtrSettings.Entries[Idx];
// Выбор начальной частоты: LastFreq если в диапазоне, иначе середина // Выбор начальной частоты: LastFreq если в диапазоне, иначе середина
Vis := FXvtrSettings.Entries[Idx].LastFreq; Vis := E.LastFreq;
if (Vis < FXvtrSettings.Entries[Idx].FreqBegin) or if (Vis < E.FreqBegin) or (Vis > E.FreqEnd) then
(Vis > FXvtrSettings.Entries[Idx].FreqEnd) then Vis := (E.FreqBegin + E.FreqEnd) / 2.0;
Vis := (FXvtrSettings.Entries[Idx].FreqBegin +
FXvtrSettings.Entries[Idx].FreqEnd) / 2.0;
// Отключаем кнопки HF band // Отключаем кнопки HF band
for i := 0 to BAND_COUNT - 1 do for i := 0 to BAND_COUNT - 1 do
StyleButton(BtnBand[i], False); StyleButton(BtnBand[i], False);
@@ -6085,6 +6227,22 @@ begin
StyleButton(BtnXvtrBand[i], i = Idx); StyleButton(BtnXvtrBand[i], i = Idx);
// Применяем XVTR режим к сети (XVTR enable bit + DDC IN + DisablePA + RX ant) // Применяем XVTR режим к сети (XVTR enable bit + DDC IN + DisablePA + RX ant)
ApplyXvtrToNetwork; 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 переход // VFO на видимую частоту; FCenterFreq тоже visible (как и для HF — DDC переход
// в IF происходит только в момент UpdateState/SetRunAndFreq через XvtrTranslate). // в IF происходит только в момент UpdateState/SetRunAndFreq через XvtrTranslate).
FVfoA := Vis; FVfoA := Vis;
@@ -6210,7 +6368,10 @@ var
Base: Int64; Base: Int64;
NewFreq: Int64; NewFreq: Int64;
begin 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 ssCtrl in Shift then Step := 1000
else if ssShift in Shift then Step := 10 else if ssShift in Shift then Step := 10
else Step := 100; else Step := 100;
@@ -6230,7 +6391,12 @@ begin
NewFreq := Base; // снэп к нижней границе NewFreq := Base; // снэп к нижней границе
end; 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); ApplyVfoA(NewFreq);
Handled := True; Handled := True;
+56 -10
View File
@@ -29,8 +29,12 @@ type
AGCTop: Integer; AGCTop: Integer;
CTun: Boolean; CTun: Boolean;
SpanHz: Double; SpanHz: Double;
FMSQOn: Boolean; FMSQOn: Boolean;
FMSQLevel: Integer; // 0..100 → WDSP threshold 0.0..0.5 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; end;
// TX-настройки per-device. Хранятся в JSON-секции "tx" под MAC, // TX-настройки per-device. Хранятся в JSON-секции "tx" под MAC,
@@ -131,7 +135,15 @@ type
TXPower: Integer; // 0..100 % (drive level специально для XVTR) TXPower: Integer; // 0..100 % (drive level специально для XVTR)
DisablePA: Boolean; // True — отключить внутренний PA при TX (XVTR имеет свой) DisablePA: Boolean; // True — отключить внутренний PA при TX (XVTR имеет свой)
RXAntenna: Byte; // 0=по-умолчанию(основной band), 1=ANT1, 2=ANT2, 3=ANT3 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; end;
TXvtrSettings = record TXvtrSettings = record
@@ -327,8 +339,12 @@ begin
B.AGCTop := 90; B.AGCTop := 90;
B.CTun := False; B.CTun := False;
B.SpanHz := 192000; B.SpanHz := 192000;
B.FMSQOn := False; B.FMSQOn := False;
B.FMSQLevel := 30; B.FMSQLevel := 30;
B.CTCSSOn := False;
B.CTCSSToneIdx := 0;
B.FMStepOn := True;
B.FMStepIdx := 3; // 25 kHz
end; end;
class procedure TSettingsManager.DefaultGlobal(out G: TGlobalSettings); 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,'agc_mode',B.AGCMode); JW(O,'agc_top',B.AGCTop);
JW(O,'ctun',B.CTun); JW(O,'span_hz',B.SpanHz); 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,'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; end;
function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer; function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer;
@@ -717,8 +735,12 @@ begin
B.AGCTop := JI(O,'agc_top', B.AGCTop); B.AGCTop := JI(O,'agc_top', B.AGCTop);
B.CTun := JB(O,'ctun', B.CTun); B.CTun := JB(O,'ctun', B.CTun);
B.SpanHz := JD(O,'span_hz', B.SpanHz); B.SpanHz := JD(O,'span_hz', B.SpanHz);
B.FMSQOn := JB(O,'fmsq_on', B.FMSQOn); B.FMSQOn := JB(O,'fmsq_on', B.FMSQOn);
B.FMSQLevel := JI(O,'fmsq_level', B.FMSQLevel); 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; end;
procedure TSettingsManager.SaveWindowBounds(L, T, W, H: Integer); procedure TSettingsManager.SaveWindowBounds(L, T, W, H: Integer);
@@ -1090,7 +1112,15 @@ begin
X.Entries[i].TXPower := 100; X.Entries[i].TXPower := 100;
X.Entries[i].DisablePA := False; X.Entries[i].DisablePA := False;
X.Entries[i].RXAntenna := 0; 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; end;
// Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились) // Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились)
X.Entries[0].ButtonText := '2m'; 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].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].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].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] // Валидация: 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
@@ -1165,7 +1203,15 @@ begin
JW(EObj, 'tx_pwr', X.Entries[i].TXPower); JW(EObj, 'tx_pwr', X.Entries[i].TXPower);
JW(EObj, 'dis_pa', X.Entries[i].DisablePA); JW(EObj, 'dis_pa', X.Entries[i].DisablePA);
JW(EObj, 'rx_ant', Integer(X.Entries[i].RXAntenna)); 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;
end; end;
+5 -3
View File
@@ -646,8 +646,10 @@ begin
'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;}' +
'function sendActiveFreq(f){if(activeVfo==="A")ws2({cmd:"freq",hz:f});else ws2({cmd:"freq",hz:f});}' + 'function sendActiveFreq(f){if(activeVfo==="A")ws2({cmd:"freq",hz:f});else ws2({cmd:"freq",hz:f});}' +
'function vfoMin(){return XVTR_CUR>=0?0:30000;}' +
'function vfoMax(){return XVTR_CUR>=0?999999999999:60000000;}' +
'function doWheel(e){e.preventDefault();e.stopPropagation();audioKick();const dir=e.deltaY<0?1:-1,st=tuneStep(e),base=Math.floor(activeVfoHz()/st)*st;' + 'function doWheel(e){e.preventDefault();e.stopPropagation();audioKick();const dir=e.deltaY<0?1:-1,st=tuneStep(e),base=Math.floor(activeVfoHz()/st)*st;' +
'const nf=cl(dir>0?base+st:(Math.round(activeVfoHz())===base?base-st:base),30000,60000000);' + 'const nf=cl(dir>0?base+st:(Math.round(activeVfoHz())===base?base-st:base),vfoMin(),vfoMax());' +
'setActiveVfoHz(nf);wheelActiveTs=Date.now();' + 'setActiveVfoHz(nf);wheelActiveTs=Date.now();' +
'if(activeVfo==="A"){cur.vfo_a_hz=nf;}else{cur.vfo_b_hz=nf;}' + 'if(activeVfo==="A"){cur.vfo_a_hz=nf;}else{cur.vfo_b_hz=nf;}' +
'renderVfos();pend("freq");ws2({cmd:"freq",hz:nf});}' + 'renderVfos();pend("freq");ws2({cmd:"freq",hz:nf});}' +
@@ -674,7 +676,7 @@ begin
'localCenter=Math.round(dragCen0-dHz);' + 'localCenter=Math.round(dragCen0-dHz);' +
'ws2({cmd:"set_center",hz:localCenter});' + 'ws2({cmd:"set_center",hz:localCenter});' +
'}else{' + '}else{' +
'const nf=cl(Math.round(dragVfo0-dHz),30000,60000000);' + 'const nf=cl(Math.round(dragVfo0-dHz),vfoMin(),vfoMax());' +
'setActiveVfoHz(nf);localCenter=nf;' + 'setActiveVfoHz(nf);localCenter=nf;' +
'if(activeVfo==="A")cur.vfo_a_hz=nf;else cur.vfo_b_hz=nf;' + 'if(activeVfo==="A")cur.vfo_a_hz=nf;else cur.vfo_b_hz=nf;' +
'renderVfos();pend("freq");ws2({cmd:"freq",hz:nf});' + 'renderVfos();pend("freq");ws2({cmd:"freq",hz:nf});' +
@@ -686,7 +688,7 @@ begin
'if(dragging&&Math.abs(e.offsetX-dragX0)<4){' + 'if(dragging&&Math.abs(e.offsetX-dragX0)<4){' +
// клик — не трогаем localCenter: фильтр встаёт точно туда куда кликнули. // клик — не трогаем localCenter: фильтр встаёт точно туда куда кликнули.
// Спектр остаётся на месте, сервер пришлёт center_hz через ~200ms и всё перецентрируется. // Спектр остаётся на месте, сервер пришлёт center_hz через ~200ms и всё перецентрируется.
'const nf=cl(Math.round(xToHz(e.offsetX,cv.clientWidth)/100)*100,30000,60000000);' + 'const nf=cl(Math.round(xToHz(e.offsetX,cv.clientWidth)/100)*100,vfoMin(),vfoMax());' +
'setActiveVfoHz(nf);' + 'setActiveVfoHz(nf);' +
'if(activeVfo==="A")cur.vfo_a_hz=nf;else cur.vfo_b_hz=nf;' + 'if(activeVfo==="A")cur.vfo_a_hz=nf;else cur.vfo_b_hz=nf;' +
'renderVfos();pend("freq");ws2({cmd:"freq",hz:nf});' + 'renderVfos();pend("freq");ws2({cmd:"freq",hz:nf});' +