Add channel memory: CH button, dropdown, editor form, smart CTCSS/RPT auto-clear

- New ChannelStore.pas: TChannel record + JSON persistence (channels.json)
- New ChannelsForm.pas: full channel editor (list + fields, pre-fills from current radio state)
- CH button in RX block: left-click opens dropdown, right-click opens editor
- Active channel: button highlights with channel name, deactivates on VFO move
- ApplyChannel: auto-switches XVTR/HF band based on channel frequency; saves old
  band state before switching to prevent cache corruption
- SaveCurrentBand: skip save when VFO > 61 MHz (prevents HF cache corruption
  from VHF channel application without XVTR)
- DeactivateXvtr: save full XVTR state (was only saving LastFreq)
- CTCSS/RPT from channel marked as auto: cleared automatically when tuning away
  from channel frequency; survives band switches via FXvtrSettings auto-flags
- TXvtrEntry: add LastCTCSSAutoActive, LastFMRptAutoActive fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 15:39:37 +03:00
co-authored by Claude Sonnet 4.6
parent d08317e835
commit 1175e42516
7 changed files with 1298 additions and 24 deletions
+302 -17
View File
@@ -38,7 +38,8 @@ uses
PlatformUtils,
WinFirewall,
BoardUtils, WisdomBuilder, UISync,
FlatEdit, FMRepeater;
FlatEdit, FMRepeater,
ChannelStore, ChannelsForm;
const
CLR_BG = TColor($00101010);
@@ -257,7 +258,8 @@ type
FFMStepIdx: Integer;
FFMRptDir: Integer; // RPT_NONE / RPT_MINUS / RPT_PLUS
FFMRptOffsetHz: Double;
FFMRptAutoActive: Boolean; // True = текущий minus включён автоматически
FFMRptAutoActive: Boolean; // True = RPT включён автоматически (авто или из канала)
FFMCTCSSAutoActive: Boolean; // True = CTCSS включён из канала (не вручную)
FWfManualHigh: Double;
FWfManualLow: Double;
FWfAGCOffset: Double;
@@ -403,6 +405,11 @@ type
BtnTUN: TFlatButton;
PanelRXBlock: TPanel;
BtnChannels: TFlatButton;
FChannelsDropDown: TFlatDropDown;
FChannelStore: TChannelStore;
FChannelsForm: TObject; // TChannelsForm (cast при использовании)
FActiveChannelIdx: Integer; // -1 = нет активного канала
// ---- Right panel ----
PanelRight: TPanel;
@@ -517,6 +524,14 @@ type
procedure BtnFMStepSelClick(Sender: TObject);
procedure OnCTCSSDropDownSelect(Sender: TObject; Idx: Integer);
procedure OnStepDropDownSelect(Sender: TObject; Idx: Integer);
procedure BtnChannelsClick(Sender: TObject);
procedure BtnChannelsMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure OnChannelsDropDownSelect(Sender: TObject; Idx: Integer);
procedure RefreshChannelsDropDown;
procedure OnChannelStoreChanged(Sender: TObject);
procedure ApplyChannel(const Ch: TChannel);
procedure CheckChannelActive;
procedure PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure PbSpectrumMouseMove(Sender: TObject; Shift: TShiftState;
@@ -974,15 +989,20 @@ 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;
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;
FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSAutoActive := FFMCTCSSAutoActive;
FXvtrSettings.Entries[FCurrentXvtr].LastFMRptAutoActive := FFMRptAutoActive;
end;
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
Exit;
end;
// Не сохраняем, если VFO ушёл в VHF/UHF область (например, после применения
// канала на 2м без XVTR) — иначе HF-кэш диапазона будет перезаписан VHF-частотой.
if FVfoA > 61000000 then Exit;
FBandCache[FCurrentBand] := MakeBandSettings;
FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]);
end;
@@ -1135,9 +1155,10 @@ begin
FFMSQLevel := 30;
FFMStepOn := True;
FFMStepIdx := FM_STEP_DEF;
FFMRptDir := RPT_NONE;
FFMRptOffsetHz := RPT_DEFAULT_2M;
FFMRptAutoActive := False;
FFMRptDir := RPT_NONE;
FFMRptOffsetHz := RPT_DEFAULT_2M;
FFMRptAutoActive := False;
FFMCTCSSAutoActive := False;
FSpectrumBufCount := 1024;
FWaterfallBufCount := 1024;
FWaterfallFrameInterval := 2;
@@ -1152,6 +1173,8 @@ begin
FTXSpecRange := 80.0;
FTXSpecGridStep := 10.0;
FSettingsForm := nil;
FChannelsForm := nil;
FActiveChannelIdx := -1;
FillChar(FDevMAC, SizeOf(FDevMAC), 0);
// Инициализируем кэш диапазонов умолчаниями
@@ -1162,6 +1185,10 @@ begin
// Загружаем JSON настройки
FSettings := TSettingsManager.Create;
FSettings.Load;
// Загружаем список каналов
FChannelStore := TChannelStore.Create(CHANNELS_FILE);
FChannelStore.Load;
if FSettings.LoadStartupPreview(StartupVfoA, StartupVfoB, StartupRate) then
begin
FVfoA := StartupVfoA;
@@ -1411,6 +1438,7 @@ begin
FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate);
FSettings.Save;
FSettings.Free;
FreeAndNil(FChannelStore);
FWebServer.Stop;
FWebServer.Free;
if Assigned(FCATTcp) then begin FCATTcp.Stop; FreeAndNil(FCATTcp); end;
@@ -1719,6 +1747,17 @@ begin
BtnCTun.Tag := 0;
StyleButton(BtnCTun, FCTun);
BtnChannels := MakeBtn(PanelRXBlock, 'Channel',
LeftPanelButtonLeft(LEFT_W, 3, 1), 16,
LeftPanelButtonWidth(LEFT_W, 3, 1),
BTN_H, BtnChannelsClick);
BtnChannels.OnMouseDown := BtnChannelsMouseDown;
StyleButton(BtnChannels, False);
FChannelsDropDown := TFlatDropDown.Create(BtnChannels, PanelLeft, 1, BTN_SM);
FChannelsDropDown.OnSelect := OnChannelsDropDownSelect;
RefreshChannelsDropDown;
MakeLbl(PanelRXBlock, 'VOL', 4, 50);
TrkVolume := TFlatSlider.Create(Self);
TrkVolume.Parent := PanelRXBlock; TrkVolume.Left := 34; TrkVolume.Top := 46;
@@ -2599,6 +2638,8 @@ begin
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);
if BtnChannels <> nil then StyleButton(BtnChannels, False);
if FChannelsDropDown <> nil then FChannelsDropDown.ApplyStyle(StyleButton, T.Panel);
StyleButton(BtnNR, BtnNR.Active);
StyleButton(BtnNB, BtnNB.Active);
StyleButton(BtnSNB, BtnSNB.Active);
@@ -4593,6 +4634,26 @@ begin
end;
end;
// Если нет активного канала, но CTCSS/RPT были установлены каналом (авто-флаги),
// сбрасываем их при первом же изменении частоты
if FActiveChannelIdx < 0 then
begin
if FFMCTCSSAutoActive then
begin
FFMCTCSSAutoActive := False;
FFMCTCSSOn := False;
if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, False);
if FWDSPReady then FDSPEngine.SetTXCTCSS(False, CTCSS_TONES[FFMCTCSSToneIdx]);
end;
if FFMRptAutoActive then
begin
FFMRptDir := RPT_NONE;
FFMRptAutoActive := False;
if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, False);
if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, False);
end;
end;
// Auto-enable repeater minus for 2 m repeater input segment
if FMode = MODE_FM then UpdateRptAutoState;
@@ -4613,6 +4674,8 @@ begin
FWaterfallDirty := True;
PbWaterfall.Invalidate;
end;
CheckChannelActive;
end;
// ---------------------------------------------------------------------------
@@ -5206,6 +5269,7 @@ procedure TMainForm.CloseCTCSSPopup;
begin
if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup;
CloseFMStepPopup;
if FChannelsDropDown <> nil then FChannelsDropDown.ClosePopup;
end;
procedure TMainForm.BtnFMRptMinusClick(Sender: TObject);
@@ -5299,7 +5363,8 @@ end;
procedure TMainForm.CloseFMStepPopup;
begin
if FStepDropDown <> nil then FStepDropDown.ClosePopup;
if FStepDropDown <> nil then FStepDropDown.ClosePopup;
if FChannelsDropDown <> nil then FChannelsDropDown.ClosePopup;
end;
procedure TMainForm.SetFMStep(Idx: Integer);
@@ -5331,6 +5396,208 @@ begin
SaveCurrentBand;
end;
// ---------------------------------------------------------------------------
// Channels dropdown / store
// ---------------------------------------------------------------------------
procedure TMainForm.RefreshChannelsDropDown;
var
names: array of string;
i: Integer;
begin
if (FChannelStore = nil) or (FChannelsDropDown = nil) then Exit;
SetLength(names, FChannelStore.Count);
for i := 0 to FChannelStore.Count - 1 do
names[i] := FChannelStore.GetChannel(i).Name;
FChannelsDropDown.SetItems(names);
if (FActiveChannelIdx >= 0) and (FActiveChannelIdx < FChannelStore.Count) then
FChannelsDropDown.SetItemIndex(FActiveChannelIdx)
else if BtnChannels <> nil then
BtnChannels.Caption := 'Channel';
end;
procedure TMainForm.OnChannelStoreChanged(Sender: TObject);
begin
RefreshChannelsDropDown;
end;
procedure TMainForm.BtnChannelsClick(Sender: TObject);
begin
if FChannelStore.Count = 0 then Exit;
FCTCSSDropDown.ClosePopup;
FStepDropDown.ClosePopup;
FChannelsDropDown.Toggle;
end;
procedure TMainForm.BtnChannelsMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
Def: TChannel;
begin
if Button <> mbRight then Exit;
FChannelsDropDown.ClosePopup;
if FChannelsForm = nil then
FChannelsForm := TChannelsForm.Create(Self, FChannelStore, OnChannelStoreChanged);
// Передаём текущее состояние радио как значения по умолчанию для нового канала
TChannelStore.DefaultChannel(Def);
Def.RXFreq := FVfoA;
Def.TXFreq := ActiveTXFreqHz;
Def.Mode := FMode;
Def.RptDir := FFMRptDir;
Def.RptOffsetHz := FFMRptOffsetHz;
Def.CTCSSOn := FFMCTCSSOn;
Def.CTCSSToneIdx := FFMCTCSSToneIdx;
Def.Power := 100;
Def.Name := '';
TChannelsForm(FChannelsForm).SetCurrentDefaults(Def);
TChannelsForm(FChannelsForm).ApplyTheme(DarkTheme);
TChannelsForm(FChannelsForm).Show;
end;
procedure TMainForm.OnChannelsDropDownSelect(Sender: TObject; Idx: Integer);
var Ch: TChannel;
begin
if (FChannelStore = nil) or (Idx < 0) or (Idx >= FChannelStore.Count) then Exit;
Ch := FChannelStore.GetChannel(Idx);
FActiveChannelIdx := Idx;
StyleButton(BtnChannels, True);
BtnChannels.Caption := Ch.Name;
BtnChannels.Repaint;
ApplyChannel(Ch);
end;
procedure TMainForm.ApplyChannel(const Ch: TChannel);
var
i, NewBand, XvtrIdx: Integer;
begin
// Сохраняем текущее состояние до любых изменений
SaveCurrentBand;
// Ищем подходящий XVTR-слот по частоте канала
XvtrIdx := -1;
for i := 0 to CFG_XVTR_COUNT - 1 do
if FXvtrSettings.Entries[i].Enabled and
(Ch.RXFreq >= FXvtrSettings.Entries[i].FreqBegin) and
(Ch.RXFreq <= FXvtrSettings.Entries[i].FreqEnd) then
begin
XvtrIdx := i;
Break;
end;
NewBand := -1;
if XvtrIdx >= 0 then
begin
// Канал на трансвертерном диапазоне — активируем XVTR если не активен
if FCurrentXvtr <> XvtrIdx then
ActivateXvtrBand(XvtrIdx);
end
else
begin
// Канал на HF (или неизвестная частота)
// Если были на XVTR — выходим из него, иначе ApplyVfoA заклэмпит частоту
if FCurrentXvtr >= 0 then
DeactivateXvtr;
NewBand := FreqToBandIdx(Ch.RXFreq);
if (NewBand >= 0) and (NewBand <> FCurrentBand) then
begin
StyleButton(BtnBand[FCurrentBand], False);
FCurrentBand := NewBand;
StyleButton(BtnBand[FCurrentBand], True);
FDriveLevel := CalcDriveByte;
end;
end;
// Режим (после ActivateXvtrBand/DeactivateXvtr FMode обновился — сравниваем снова)
if FMode <> Ch.Mode then
begin
FMode := Ch.Mode;
for i := 0 to MODE_COUNT - 1 do StyleButton(BtnMode[i], i = FMode);
if FWDSPReady then
begin
FDSPEngine.SetMode(FMode);
if FTuning then FDSPEngine.SetTXTone(True, FTXSettings.TUNFreq, 1.0);
end;
UpdateFilterButtons;
ApplyModeFilter;
SyncSpecViewFreq;
end;
// FM-параметры
if FMode = MODE_FM then
begin
FFMCTCSSOn := Ch.CTCSSOn;
FFMCTCSSAutoActive := Ch.CTCSSOn; // CTCSS из канала — авто, сбросится при уходе с частоты
SetFMCTCSSTone(Ch.CTCSSToneIdx);
if BtnFMCTCSS <> nil then
begin
StyleButton(BtnFMCTCSS, FFMCTCSSOn);
BtnFMCTCSS.Repaint;
end;
if FWDSPReady then
FDSPEngine.SetTXCTCSS(FFMCTCSSOn, CTCSS_TONES[FFMCTCSSToneIdx]);
FFMRptDir := Ch.RptDir;
FFMRptOffsetHz := Ch.RptOffsetHz;
FFMRptAutoActive := Ch.RptDir <> RPT_NONE; // RPT из канала — авто, сбросится при уходе
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);
end;
// Частота VFO A
ApplyVfoA(Round(Ch.RXFreq));
// Сохраняем состояние
if XvtrIdx >= 0 then
SaveCurrentBand // сохраняем в XVTR-слот (CTCSS/RPT/Mode из канала)
else if NewBand >= 0 then
SaveCurrentBand; // сохраняем в HF band-кэш
end;
procedure TMainForm.CheckChannelActive;
var Ch: TChannel;
begin
if FActiveChannelIdx < 0 then Exit;
if (FChannelStore = nil) or (FActiveChannelIdx >= FChannelStore.Count) then
begin
FActiveChannelIdx := -1;
Exit;
end;
Ch := FChannelStore.GetChannel(FActiveChannelIdx);
if Round(FVfoA) <> Round(Ch.RXFreq) then
begin
FActiveChannelIdx := -1;
StyleButton(BtnChannels, False);
BtnChannels.Caption := 'Channel';
BtnChannels.Repaint;
// Сбрасываем CTCSS если он был включён каналом (не вручную)
if FFMCTCSSAutoActive then
begin
FFMCTCSSAutoActive := False;
FFMCTCSSOn := False;
if BtnFMCTCSS <> nil then
begin
StyleButton(BtnFMCTCSS, False);
BtnFMCTCSS.Repaint;
end;
if FWDSPReady then
FDSPEngine.SetTXCTCSS(False, CTCSS_TONES[FFMCTCSSToneIdx]);
end;
// Сбрасываем RPT если он был включён каналом (не вручную), затем
// даём UpdateRptAutoState переоценить авто-MINUS для текущей частоты
if FFMRptAutoActive then
begin
FFMRptDir := RPT_NONE;
FFMRptAutoActive := False;
if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, False);
if BtnFMRptPlus <> nil then StyleButton(BtnFMRptPlus, False);
UpdateRptAutoState;
end;
end;
end;
procedure TMainForm.SetFMCTCSSTone(Idx: Integer);
begin
FFMCTCSSToneIdx := Idx;
@@ -5341,7 +5608,8 @@ end;
procedure TMainForm.BtnFMCTCSSClick(Sender: TObject);
begin
FFMCTCSSOn := not FFMCTCSSOn;
FFMCTCSSOn := not FFMCTCSSOn;
FFMCTCSSAutoActive := False; // ручное действие снимает авто-флаг
StyleButton(BtnFMCTCSS, FFMCTCSSOn);
BtnFMCTCSS.Repaint; // Flush visual state before WDSP call
if FWDSPReady then
@@ -6808,15 +7076,17 @@ begin
if FWDSPReady then FDSPEngine.SetMode(FMode);
UpdateFilterButtons; // показывает/скрывает FM-панели, обновляет SQL UI
ApplyModeFilter;
FFMCTCSSOn := E.LastCTCSSOn;
FFMCTCSSOn := E.LastCTCSSOn;
FFMCTCSSAutoActive := E.LastCTCSSAutoActive;
if BtnFMCTCSS <> nil then StyleButton(BtnFMCTCSS, FFMCTCSSOn);
SetFMCTCSSTone(E.LastCTCSSToneIdx);
ApplyFMSquelch;
FFMStepOn := E.LastFMStepOn;
SetFMStep(E.LastFMStepIdx);
if BtnFMStep <> nil then StyleButton(BtnFMStep, FFMStepOn);
FFMRptDir := E.LastFMRptDir;
FFMRptOffsetHz := E.LastFMRptOffsetHz;
FFMRptDir := E.LastFMRptDir;
FFMRptAutoActive := E.LastFMRptAutoActive;
FFMRptOffsetHz := E.LastFMRptOffsetHz;
if FFMRptOffsetHz <= 0 then
FFMRptOffsetHz := RptDefaultOffsetHz(E.FreqBegin);
if BtnFMRptMinus <> nil then StyleButton(BtnFMRptMinus, FFMRptDir = RPT_MINUS);
@@ -6871,10 +7141,25 @@ procedure TMainForm.DeactivateXvtr;
var i: Integer;
begin
if FCurrentXvtr < 0 then Exit;
// Запоминаем LastFreq для bandstack
// Сохраняем полное состояние XVTR-слота (раньше сохранялся только LastFreq)
if FCurrentXvtr < CFG_XVTR_COUNT then
begin
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
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;
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;
FXvtrSettings.Entries[FCurrentXvtr].LastCTCSSAutoActive := FFMCTCSSAutoActive;
FXvtrSettings.Entries[FCurrentXvtr].LastFMRptAutoActive := FFMRptAutoActive;
if FDevConnected then
begin
FSettings.SaveXvtr(FDevMAC, FXvtrSettings);