diff --git a/MainForm.pas b/MainForm.pas index 0037c7e..65f159c 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -37,7 +37,8 @@ uses StatusBar, PlatformUtils, WinFirewall, - BoardUtils, WisdomBuilder, UISync; + BoardUtils, WisdomBuilder, UISync, + FlatEdit, FMRepeater; const CLR_BG = TColor($00101010); @@ -254,6 +255,9 @@ type FFMSQLevel: Integer; FFMStepOn: Boolean; FFMStepIdx: Integer; + FFMRptDir: Integer; // RPT_NONE / RPT_MINUS / RPT_PLUS + FFMRptOffsetHz: Double; + FFMRptAutoActive: Boolean; // True = текущий minus включён автоматически FWfManualHigh: Double; FWfManualLow: Double; FWfAGCOffset: Double; @@ -373,6 +377,10 @@ type BtnFMStep: TFlatButton; BtnFMStepSel: TFlatButton; FStepDropDown: TFlatDropDown; + PanelFMRpt: TPanel; + BtnFMRptMinus: TFlatButton; + BtnFMRptPlus: TFlatButton; + EdFMRptOffset: TFlatEdit; PanelAGC: TPanel; BtnAGCMode: array[0..4] of TFlatButton; // FAST MED SLOW LONG OFF @@ -498,6 +506,11 @@ type procedure BtnFMCTCSSToneClick(Sender: TObject); procedure SetFMCTCSSTone(Idx: Integer); procedure CloseCTCSSPopup; + procedure BtnFMRptMinusClick(Sender: TObject); + procedure BtnFMRptPlusClick(Sender: TObject); + procedure EdFMRptOffsetChange(Sender: TObject); + procedure ApplyFMRpt; + procedure UpdateRptAutoState; procedure SetFMStep(Idx: Integer); procedure CloseFMStepPopup; procedure BtnFMStepClick(Sender: TObject); @@ -859,6 +872,8 @@ begin Result.CTCSSToneIdx := FFMCTCSSToneIdx; Result.FMStepOn := FFMStepOn; Result.FMStepIdx := FFMStepIdx; + Result.FMRptDir := FFMRptDir; + Result.FMRptOffsetHz := FFMRptOffsetHz; // Waterfall AGC/NF — глобальные (не диапазонные) end; @@ -959,6 +974,8 @@ begin FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSToneIdx := FFMCTCSSToneIdx; FXvtrSettings.Entries[FCurrentXvtr].LastFMStepOn := FFMStepOn; FXvtrSettings.Entries[FCurrentXvtr].LastFMStepIdx := FFMStepIdx; + FXvtrSettings.Entries[FCurrentXvtr].LastFMRptDir := FFMRptDir; + FXvtrSettings.Entries[FCurrentXvtr].LastFMRptOffsetHz := FFMRptOffsetHz; FXvtrSettings.Entries[FCurrentXvtr].LastCTun := FCTun; FXvtrSettings.Entries[FCurrentXvtr].LastAGCMode := FAGCMode; FXvtrSettings.Entries[FCurrentXvtr].LastAGCTop := FAGCTop; @@ -1022,6 +1039,13 @@ begin FFMStepOn := B.FMStepOn; SetFMStep(B.FMStepIdx); if BtnFMStep <> nil then StyleButton(BtnFMStep, FFMStepOn); + FFMRptDir := B.FMRptDir; + FFMRptOffsetHz := B.FMRptOffsetHz; + if FFMRptOffsetHz <= 0 then + FFMRptOffsetHz := RptDefaultOffsetHz(B.VfoA); + if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, FFMRptDir = RPT_MINUS); + if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, FFMRptDir = RPT_PLUS); + if EdFMRptOffset <> nil then EdFMRptOffset.Text := RptFormatOffset(FFMRptOffsetHz); // --- CTUN --- FCTun := B.CTun; @@ -1111,6 +1135,9 @@ begin FFMSQLevel := 30; FFMStepOn := True; FFMStepIdx := FM_STEP_DEF; + FFMRptDir := RPT_NONE; + FFMRptOffsetHz := RPT_DEFAULT_2M; + FFMRptAutoActive := False; FSpectrumBufCount := 1024; FWaterfallBufCount := 1024; FWaterfallFrameInterval := 2; @@ -1917,6 +1944,31 @@ begin FStepDropDown.SetItemIndex(FFMStepIdx); FStepDropDown.OnSelect := OnStepDropDownSelect; + // FM Repeater offset panel (visible only in FM mode, below CTCSS) + PanelFMRpt := TPanel.Create(Self); + PanelFMRpt.Parent := PanelLeft; + PanelFMRpt.SetBounds(0, Y, LEFT_W, 50); + PanelFMRpt.BevelOuter := bvNone; + PanelFMRpt.Visible := False; + MakeLbl(PanelFMRpt, 'RPT', 4, 2); + BtnFMRptMinus := MakeBtn(PanelFMRpt, '-', + LeftPanelButtonLeft(LEFT_W, 5, 0), 16, + LeftPanelButtonWidth(LEFT_W, 5, 0), BTN_H, BtnFMRptMinusClick); + StyleButton(BtnFMRptMinus, False); + BtnFMRptPlus := MakeBtn(PanelFMRpt, '+', + LeftPanelButtonLeft(LEFT_W, 5, 4), 16, + LeftPanelButtonWidth(LEFT_W, 5, 4), BTN_H, BtnFMRptPlusClick); + StyleButton(BtnFMRptPlus, False); + W := LeftPanelButtonLeft(LEFT_W, 5, 1); + EdFMRptOffset := TFlatEdit.Create(Self); + EdFMRptOffset.Parent := PanelFMRpt; + EdFMRptOffset.Left := W; + EdFMRptOffset.Top := 18; + EdFMRptOffset.Width := LeftPanelButtonLeft(LEFT_W, 5, 4) - W - LEFT_PANEL_BTN_GAP; + EdFMRptOffset.Height := BTN_H - 4; + EdFMRptOffset.Text := RptFormatOffset(FFMRptOffsetHz); + EdFMRptOffset.OnChange := EdFMRptOffsetChange; + // ---- Right panel ---- PanelRight := TPanel.Create(Self); PanelRight.Parent := Self; @@ -2501,6 +2553,8 @@ begin if PanelFMSQ <> nil then DP(PanelFMSQ); if PanelFMCTCSS <> nil then DP(PanelFMCTCSS); if PanelFMStep <> nil then DP(PanelFMStep); + if PanelFMRpt <> nil then DP(PanelFMRpt); + if EdFMRptOffset <> nil then EdFMRptOffset.SetAppTheme(T); DP(PanelRXBlock); DP(PanelAGC); DP(PanelTX); DP(PanelRight); @@ -2541,6 +2595,8 @@ begin if TrkFMSQ <> nil then DS(TrkFMSQ); if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, BtnFMCTCSS.Active); if BtnFMCTCSSTone <> nil then StyleButton(BtnFMCTCSSTone, False); + if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, BtnFMRptMinus.Active); + if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, BtnFMRptPlus.Active); if FCTCSSDropDown <> nil then FCTCSSDropDown.ApplyStyle(StyleButton, T.Panel); if FStepDropDown <> nil then FStepDropDown.ApplyStyle(StyleButton, T.Panel); StyleButton(BtnNR, BtnNR.Active); @@ -4383,6 +4439,13 @@ begin if FStepDropDown <> nil then FStepDropDown.SetItemIndex(FFMStepIdx); PanelFMStep.Visible := True; end; + if PanelFMRpt <> nil then + begin + StyleButton(BtnFMRptMinus, FFMRptDir = RPT_MINUS); + StyleButton(BtnFMRptPlus, FFMRptDir = RPT_PLUS); + EdFMRptOffset.Text := RptFormatOffset(FFMRptOffsetHz); + PanelFMRpt.Visible := True; + end; RelayoutBelowBands; Exit; end; @@ -4392,6 +4455,7 @@ begin if PanelFMSQ <> nil then PanelFMSQ.Visible := False; if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := False; if PanelFMStep <> nil then begin CloseFMStepPopup; PanelFMStep.Visible := False; end; + if PanelFMRpt <> nil then PanelFMRpt.Visible := False; for i := 0 to FILT_COUNT - 1 do begin BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, 5, i mod 5); @@ -4529,6 +4593,9 @@ begin end; end; + // Auto-enable repeater minus for 2 m repeater input segment + if FMode = MODE_FM then UpdateRptAutoState; + // DDC: передаём FCenterFreq (при CTUN OFF = FVfoA, при CTUN ON = фиксирован) if FNetwork.Connected and FNetwork.Running then begin @@ -5141,6 +5208,95 @@ begin CloseFMStepPopup; end; +procedure TMainForm.BtnFMRptMinusClick(Sender: TObject); +begin + if FFMRptDir = RPT_MINUS then + FFMRptDir := RPT_NONE + else + FFMRptDir := RPT_MINUS; + FFMRptAutoActive := False; // ручное действие снимает авто-флаг + StyleButton(BtnFMRptMinus, FFMRptDir = RPT_MINUS); + StyleButton(BtnFMRptPlus, FFMRptDir = RPT_PLUS); + BtnFMRptMinus.Repaint; + BtnFMRptPlus.Repaint; + ApplyFMRpt; + SaveCurrentBand; +end; + +procedure TMainForm.BtnFMRptPlusClick(Sender: TObject); +begin + if FFMRptDir = RPT_PLUS then + FFMRptDir := RPT_NONE + else + FFMRptDir := RPT_PLUS; + FFMRptAutoActive := False; // ручное действие снимает авто-флаг + StyleButton(BtnFMRptMinus, FFMRptDir = RPT_MINUS); + StyleButton(BtnFMRptPlus, FFMRptDir = RPT_PLUS); + BtnFMRptMinus.Repaint; + BtnFMRptPlus.Repaint; + ApplyFMRpt; + SaveCurrentBand; +end; + +procedure TMainForm.EdFMRptOffsetChange(Sender: TObject); +var Hz: Double; +begin + Hz := RptParseOffset(EdFMRptOffset.Text); + if Hz > 0.0 then + begin + FFMRptOffsetHz := Hz; + if FFMRptDir <> RPT_NONE then + begin + ApplyFMRpt; + SaveCurrentBand; + end; + end; +end; + +procedure TMainForm.ApplyFMRpt; +begin + if FNetwork.Connected and FNetwork.Running then + begin + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), + FDriveLevel, FTransmitting, True, True); + FNetwork.SendFullHP; + end; +end; + +procedure TMainForm.UpdateRptAutoState; +var InRange: Boolean; +begin + InRange := RptAutoDir(FVfoA) = RPT_MINUS; + if InRange then + begin + // Auto-enable minus only when currently off. + if FFMRptDir = RPT_NONE then + begin + FFMRptDir := RPT_MINUS; + FFMRptAutoActive := True; + if BtnFMRptMinus <> nil then + begin + StyleButton(BtnFMRptMinus, True); + BtnFMRptMinus.Repaint; + end; + end; + end + else + begin + // Auto-disable only if this minus was set automatically. + if (FFMRptDir = RPT_MINUS) and FFMRptAutoActive then + begin + FFMRptDir := RPT_NONE; + FFMRptAutoActive := False; + if BtnFMRptMinus <> nil then + begin + StyleButton(BtnFMRptMinus, False); + BtnFMRptMinus.Repaint; + end; + end; + end; +end; + procedure TMainForm.CloseFMStepPopup; begin if FStepDropDown <> nil then FStepDropDown.ClosePopup; @@ -5283,6 +5439,11 @@ begin if FSplitTxB then Result := FVfoB else if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB; + if FMode = MODE_FM then + case FFMRptDir of + RPT_MINUS: Result := Result - FFMRptOffsetHz; + RPT_PLUS: Result := Result + FFMRptOffsetHz; + end; end; procedure TMainForm.ApplyMOX(Active: Boolean); @@ -6527,6 +6688,10 @@ begin begin PanelFMCTCSS.Top := Y; Y := Y + PanelFMCTCSS.Height + 2; end; + if (PanelFMRpt <> nil) and PanelFMRpt.Visible then + begin + PanelFMRpt.Top := Y; Y := Y + PanelFMRpt.Height + 2; + end; end; procedure TMainForm.RebuildXvtrButtons; @@ -6650,6 +6815,13 @@ begin FFMStepOn := E.LastFMStepOn; SetFMStep(E.LastFMStepIdx); if BtnFMStep <> nil then StyleButton(BtnFMStep, FFMStepOn); + FFMRptDir := E.LastFMRptDir; + FFMRptOffsetHz := E.LastFMRptOffsetHz; + if FFMRptOffsetHz <= 0 then + FFMRptOffsetHz := RptDefaultOffsetHz(E.FreqBegin); + if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, FFMRptDir = RPT_MINUS); + if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, FFMRptDir = RPT_PLUS); + if EdFMRptOffset <> nil then EdFMRptOffset.Text := RptFormatOffset(FFMRptOffsetHz); // AGC FAGCMode := E.LastAGCMode; FAGCTop := E.LastAGCTop; diff --git a/Settings.pas b/Settings.pas index b1b09be..6a3efa1 100644 --- a/Settings.pas +++ b/Settings.pas @@ -40,6 +40,8 @@ type CTCSSToneIdx: Integer; // 0..37 FMStepOn: Boolean; FMStepIdx: Integer; // 0..3 (6.25/12.5/20/25 kHz) + FMRptDir: Integer; // RPT_NONE/RPT_MINUS/RPT_PLUS + FMRptOffsetHz: Double; // Hz, 0 = use band default end; // TX-настройки per-device. Хранятся в JSON-секции "tx" под MAC, @@ -150,6 +152,8 @@ type LastCTun: Boolean; LastAGCMode: Integer; LastAGCTop: Integer; + LastFMRptDir: Integer; + LastFMRptOffsetHz: Double; end; TXvtrSettings = record @@ -356,6 +360,8 @@ begin B.CTCSSToneIdx := 0; B.FMStepOn := True; B.FMStepIdx := 3; // 25 kHz + B.FMRptDir := 0; + B.FMRptOffsetHz := 0.0; end; class procedure TSettingsManager.DefaultGlobal(out G: TGlobalSettings); @@ -737,6 +743,7 @@ begin 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); + JW(O,'fmrpt_dir',B.FMRptDir); JW(O,'fmrpt_offset',B.FMRptOffsetHz); end; function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer; @@ -763,6 +770,8 @@ begin 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); + B.FMRptDir := EnsureRange(JI(O,'fmrpt_dir', B.FMRptDir), 0, 2); + B.FMRptOffsetHz := JD(O,'fmrpt_offset', B.FMRptOffsetHz); end; procedure TSettingsManager.SaveWindowBounds(L, T, W, H, DPI: Integer; Maximized: Boolean); @@ -1173,6 +1182,8 @@ begin X.Entries[i].LastCTun := False; X.Entries[i].LastAGCMode := 1; X.Entries[i].LastAGCTop := 90; + X.Entries[i].LastFMRptDir := 0; + X.Entries[i].LastFMRptOffsetHz := 0.0; end; // Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились) X.Entries[0].ButtonText := '2m'; @@ -1229,6 +1240,8 @@ begin 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); + X.Entries[i].LastFMRptDir := EnsureRange(JI(EObj, 'last_fmrpt_dir', X.Entries[i].LastFMRptDir), 0, 2); + X.Entries[i].LastFMRptOffsetHz := JD(EObj, 'last_fmrpt_offset', X.Entries[i].LastFMRptOffsetHz); // Валидация: LastFreq должен быть в [FreqBegin..FreqEnd] if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or (X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then @@ -1270,6 +1283,8 @@ begin 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); + JW(EObj, 'last_fmrpt_dir', X.Entries[i].LastFMRptDir); + JW(EObj, 'last_fmrpt_offset', X.Entries[i].LastFMRptOffsetHz); end; end; diff --git a/ewsdr.lpi b/ewsdr.lpi index 551070f..bf57087 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -146,6 +146,10 @@ + + + +