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 @@
+
+
+
+