From c4cd8afb066476469fa376dfef122b5b829d891f Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Mon, 18 May 2026 22:08:11 +0300 Subject: [PATCH] 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 --- MainForm.pas | 194 ++++++++++++++++++++++++++++++++++++++++++++---- Settings.pas | 66 +++++++++++++--- WebPageHtml.pas | 8 +- 3 files changed, 241 insertions(+), 27 deletions(-) diff --git a/MainForm.pas b/MainForm.pas index 0f77209..90e7814 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -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; diff --git a/Settings.pas b/Settings.pas index 6a3e8b0..55d1e74 100644 --- a/Settings.pas +++ b/Settings.pas @@ -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; diff --git a/WebPageHtml.pas b/WebPageHtml.pas index bd12091..c0c83ee 100644 --- a/WebPageHtml.pas +++ b/WebPageHtml.pas @@ -646,8 +646,10 @@ begin 'function activeVfoHz(){return activeVfo==="A"?vfoA:vfoB;}' + '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 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;' + - '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();' + 'if(activeVfo==="A"){cur.vfo_a_hz=nf;}else{cur.vfo_b_hz=nf;}' + 'renderVfos();pend("freq");ws2({cmd:"freq",hz:nf});}' + @@ -674,7 +676,7 @@ begin 'localCenter=Math.round(dragCen0-dHz);' + 'ws2({cmd:"set_center",hz:localCenter});' + '}else{' + - 'const nf=cl(Math.round(dragVfo0-dHz),30000,60000000);' + + 'const nf=cl(Math.round(dragVfo0-dHz),vfoMin(),vfoMax());' + 'setActiveVfoHz(nf);localCenter=nf;' + 'if(activeVfo==="A")cur.vfo_a_hz=nf;else cur.vfo_b_hz=nf;' + 'renderVfos();pend("freq");ws2({cmd:"freq",hz:nf});' + @@ -686,7 +688,7 @@ begin 'if(dragging&&Math.abs(e.offsetX-dragX0)<4){' + // клик — не трогаем localCenter: фильтр встаёт точно туда куда кликнули. // Спектр остаётся на месте, сервер пришлёт 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);' + 'if(activeVfo==="A")cur.vfo_a_hz=nf;else cur.vfo_b_hz=nf;' + 'renderVfos();pend("freq");ws2({cmd:"freq",hz:nf});' +