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
+272
View File
@@ -0,0 +1,272 @@
unit ChannelStore;
{ TChannelStore — хранилище каналов (channel memory).
Формат файла channels.json (рядом с hpsdr_settings.json). }
{$mode objfpc}{$H+}
interface
uses
SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner;
const
CHANNELS_FILE = 'channels.json';
type
TChannel = record
Group: string;
Name: string;
RXFreq: Double; // Hz
TXFreq: Double; // Hz
Mode: Integer; // 0=LSB 1=USB 2=DSB 3=CWL 4=CWU 5=FM 6=AM 7=SAM
RptDir: Integer; // 0=none 1=minus 2=plus
RptOffsetHz: Double; // Hz
CTCSSOn: Boolean;
CTCSSToneIdx: Integer; // 0..37
Power: Integer; // 0..100
end;
TChannelStore = class
private
FChannels: array of TChannel;
FFilePath: string;
function JI(O: TJSONObject; const K: string; Def: Integer): Integer;
function JD(O: TJSONObject; const K: string; Def: Double): Double;
function JB(O: TJSONObject; const K: string; Def: Boolean): Boolean;
function JS(O: TJSONObject; const K: string; const Def: string): string;
public
constructor Create(const AFilePath: string = CHANNELS_FILE);
procedure Load;
procedure Save;
function Count: Integer;
function GetChannel(Idx: Integer): TChannel;
procedure AddChannel(const Ch: TChannel);
procedure UpdateChannel(Idx: Integer; const Ch: TChannel);
procedure DeleteChannel(Idx: Integer);
procedure MoveUp(Idx: Integer);
procedure MoveDown(Idx: Integer);
class procedure DefaultChannel(out Ch: TChannel);
end;
implementation
class procedure TChannelStore.DefaultChannel(out Ch: TChannel);
begin
FillChar(Ch, SizeOf(Ch), 0);
Ch.Group := '';
Ch.Name := 'New Channel';
Ch.RXFreq := 145500000.0;
Ch.TXFreq := 145500000.0;
Ch.Mode := 5; // FM
Ch.RptDir := 0;
Ch.RptOffsetHz := 600000.0;
Ch.CTCSSOn := False;
Ch.CTCSSToneIdx := 0;
Ch.Power := 100;
end;
constructor TChannelStore.Create(const AFilePath: string);
begin
inherited Create;
FFilePath := AFilePath;
SetLength(FChannels, 0);
end;
function TChannelStore.Count: Integer;
begin
Result := Length(FChannels);
end;
function TChannelStore.GetChannel(Idx: Integer): TChannel;
begin
if (Idx >= 0) and (Idx < Length(FChannels)) then
Result := FChannels[Idx]
else
DefaultChannel(Result);
end;
procedure TChannelStore.AddChannel(const Ch: TChannel);
var N: Integer;
begin
N := Length(FChannels);
SetLength(FChannels, N + 1);
FChannels[N] := Ch;
end;
procedure TChannelStore.UpdateChannel(Idx: Integer; const Ch: TChannel);
begin
if (Idx >= 0) and (Idx < Length(FChannels)) then
FChannels[Idx] := Ch;
end;
procedure TChannelStore.DeleteChannel(Idx: Integer);
var i: Integer;
begin
if (Idx < 0) or (Idx >= Length(FChannels)) then Exit;
for i := Idx to Length(FChannels) - 2 do
FChannels[i] := FChannels[i + 1];
SetLength(FChannels, Length(FChannels) - 1);
end;
procedure TChannelStore.MoveUp(Idx: Integer);
var Tmp: TChannel;
begin
if (Idx < 1) or (Idx >= Length(FChannels)) then Exit;
Tmp := FChannels[Idx];
FChannels[Idx] := FChannels[Idx - 1];
FChannels[Idx - 1] := Tmp;
end;
procedure TChannelStore.MoveDown(Idx: Integer);
var Tmp: TChannel;
begin
if (Idx < 0) or (Idx >= Length(FChannels) - 1) then Exit;
Tmp := FChannels[Idx];
FChannels[Idx] := FChannels[Idx + 1];
FChannels[Idx + 1] := Tmp;
end;
// ---------------------------------------------------------------------------
// JSON helpers
// ---------------------------------------------------------------------------
function TChannelStore.JI(O: TJSONObject; const K: string; Def: Integer): Integer;
var D: TJSONData;
begin
D := O.Find(K);
if D <> nil then try Result := D.AsInteger; except Result := Def; end
else Result := Def;
end;
function TChannelStore.JD(O: TJSONObject; const K: string; Def: Double): Double;
var D: TJSONData;
begin
D := O.Find(K);
if D <> nil then try Result := D.AsFloat; except Result := Def; end
else Result := Def;
end;
function TChannelStore.JB(O: TJSONObject; const K: string; Def: Boolean): Boolean;
var D: TJSONData;
begin
D := O.Find(K);
if D <> nil then try Result := D.AsBoolean; except Result := Def; end
else Result := Def;
end;
function TChannelStore.JS(O: TJSONObject; const K: string; const Def: string): string;
var D: TJSONData;
begin
D := O.Find(K);
if D <> nil then try Result := D.AsString; except Result := Def; end
else Result := Def;
end;
// ---------------------------------------------------------------------------
// Load / Save
// ---------------------------------------------------------------------------
procedure TChannelStore.Load;
var
F: TFileStream;
P: TJSONParser;
Root: TJSONData;
Arr: TJSONData;
Item: TJSONData;
O: TJSONObject;
Ch: TChannel;
i: Integer;
begin
SetLength(FChannels, 0);
if not FileExists(FFilePath) then Exit;
try
F := TFileStream.Create(FFilePath, fmOpenRead or fmShareDenyNone);
try
P := TJSONParser.Create(F, [joUTF8]);
try
Root := P.Parse;
if (Root <> nil) and (Root is TJSONObject) then
begin
Arr := TJSONObject(Root).Find('channels');
if (Arr <> nil) and (Arr is TJSONArray) then
begin
SetLength(FChannels, TJSONArray(Arr).Count);
for i := 0 to TJSONArray(Arr).Count - 1 do
begin
Item := TJSONArray(Arr).Items[i];
DefaultChannel(Ch);
if Item is TJSONObject then
begin
O := TJSONObject(Item);
Ch.Group := JS(O, 'group', '');
Ch.Name := JS(O, 'name', 'Channel');
Ch.RXFreq := JD(O, 'rx_freq', 145500000.0);
Ch.TXFreq := JD(O, 'tx_freq', 145500000.0);
Ch.Mode := EnsureRange(JI(O, 'mode', 5), 0, 7);
Ch.RptDir := EnsureRange(JI(O, 'rpt_dir', 0), 0, 2);
Ch.RptOffsetHz:= JD(O, 'rpt_offset', 600000.0);
Ch.CTCSSOn := JB(O, 'ctcss_on', False);
Ch.CTCSSToneIdx := EnsureRange(JI(O, 'ctcss_idx', 0), 0, 37);
Ch.Power := EnsureRange(JI(O, 'power', 100), 0, 100);
end;
FChannels[i] := Ch;
end;
end;
Root.Free;
end;
finally
P.Free;
end;
finally
F.Free;
end;
except
SetLength(FChannels, 0);
end;
end;
procedure TChannelStore.Save;
var
F: TFileStream;
Root: TJSONObject;
Arr: TJSONArray;
Obj: TJSONObject;
i: Integer;
S: string;
begin
Root := TJSONObject.Create;
try
Arr := TJSONArray.Create;
Root.Add('channels', Arr);
for i := 0 to Length(FChannels) - 1 do
begin
Obj := TJSONObject.Create;
Obj.Add('group', FChannels[i].Group);
Obj.Add('name', FChannels[i].Name);
Obj.Add('rx_freq', FChannels[i].RXFreq);
Obj.Add('tx_freq', FChannels[i].TXFreq);
Obj.Add('mode', FChannels[i].Mode);
Obj.Add('rpt_dir', FChannels[i].RptDir);
Obj.Add('rpt_offset', FChannels[i].RptOffsetHz);
Obj.Add('ctcss_on', FChannels[i].CTCSSOn);
Obj.Add('ctcss_idx', FChannels[i].CTCSSToneIdx);
Obj.Add('power', FChannels[i].Power);
Arr.Add(Obj);
end;
try
S := Root.FormatJSON([], 2);
F := TFileStream.Create(FFilePath, fmCreate);
try
if Length(S) > 0 then F.WriteBuffer(S[1], Length(S));
finally
F.Free;
end;
except
end;
finally
Root.Free;
end;
end;
end.
+620
View File
@@ -0,0 +1,620 @@
unit ChannelsForm;
{ TChannelsForm — редактор списка каналов (channel memory).
Открывается по правой кнопке мыши на кнопке CH в блоке RX левой панели.
Список каналов слева (FlatListBox), поля редактирования справа.
Все изменения сохраняются немедленно в TChannelStore и на диск. }
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Math,
Forms, Controls, Graphics, StdCtrls, ExtCtrls,
FlatButton, FlatCheckBox, FlatComboBox, FlatEdit,
FlatSpinEdit, FlatFloatSpinEdit, FlatListBox,
AppTheme, ChannelStore;
const
// Копии из MainForm: нужны для выпадающих списков
CH_MODE_COUNT = 8;
CH_MODE_NAMES: array[0..CH_MODE_COUNT-1] of string =
('LSB','USB','DSB','CWL','CWU','FM','AM','SAM');
CH_CTCSS_COUNT = 38;
CH_CTCSS_NAMES: array[0..CH_CTCSS_COUNT-1] of string = (
'67.0', '71.9', '74.4', '77.0', '79.7', '82.5', '85.4', '88.5',
'91.5', '94.8', '97.4', '100.0', '103.5', '107.2', '110.9', '114.8',
'118.8', '123.0', '127.3', '131.8', '136.5', '141.3', '146.2', '151.4',
'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');
type
TChannelsForm = class(TForm)
private
// --- Левая панель ---
FPanelLeft: TPanel;
FBtnAdd: TFlatButton;
FBtnDelete: TFlatButton;
FBtnUp: TFlatButton;
FBtnDown: TFlatButton;
FList: TFlatListBox;
// --- Правая панель (поля редактирования) ---
FPanelRight: TPanel;
// Labels (для перекраски темой)
FLbls: array of TLabel;
// Поля
FEdGroup: TFlatEdit;
FEdName: TFlatEdit;
FSpinRXFreq: TFlatFloatSpinEdit;
FSpinTXFreq: TFlatFloatSpinEdit;
FCmbMode: TFlatComboBox;
FBtnRptNone: TFlatButton;
FBtnRptMinus: TFlatButton;
FBtnRptPlus: TFlatButton;
FSpinRptOffset: TFlatFloatSpinEdit;
FChkCTCSS: TFlatCheckBox;
FCmbCTCSS: TFlatComboBox;
FSpinPower: TFlatSpinEdit;
// Данные
FStore: TChannelStore;
FUpdating: Boolean;
FOnChanged: TNotifyEvent;
FCurrentDefaults: TChannel; // значения по умолчанию для новых каналов
procedure BuildUI;
procedure StyleBtn(B: TFlatButton; AActive: Boolean = False);
function MakeLabel(AParent: TWinControl; const Cap: string;
X, Y, W, H: Integer): TLabel;
procedure LoadChannelToFields(Idx: Integer);
procedure SaveFieldsToChannel;
procedure RefreshList(KeepIdx: Integer = -1);
function SelectedIdx: Integer;
// Handlers — список
procedure OnListClick(Sender: TObject);
procedure OnListDblClick(Sender: TObject);
// Handlers — тулбар
procedure OnAddClick(Sender: TObject);
procedure OnDeleteClick(Sender: TObject);
procedure OnUpClick(Sender: TObject);
procedure OnDownClick(Sender: TObject);
// Handlers — поля
procedure OnFieldChange(Sender: TObject);
procedure OnRptBtnClick(Sender: TObject);
procedure OnCTCSSChkChange(Sender: TObject);
procedure OnCTCSSCmbChange(Sender: TObject);
// Handlers — форма
procedure OnFormClose(Sender: TObject; var CloseAction: TCloseAction);
public
constructor Create(AOwner: TComponent; AStore: TChannelStore;
AOnChanged: TNotifyEvent); reintroduce;
procedure ApplyTheme(const T: TAppTheme);
procedure SetCurrentDefaults(const Ch: TChannel);
property OnChannelsChanged: TNotifyEvent read FOnChanged write FOnChanged;
end;
implementation
const
// Геометрия
LPANEL_W = 185; // ширина левой панели
TOOLBAR_H = 32; // высота тулбара с кнопками Add/Del/Up/Down
BTN_H = 24;
// Правая панель
LBL_X = 8;
LBL_W = 95;
CTRL_X = 106;
ROW_H = 30; // высота строки
CTRL_H = 24; // высота контрола
constructor TChannelsForm.Create(AOwner: TComponent; AStore: TChannelStore;
AOnChanged: TNotifyEvent);
begin
inherited CreateNew(AOwner);
FStore := AStore;
FOnChanged := AOnChanged;
FUpdating := False;
TChannelStore.DefaultChannel(FCurrentDefaults);
Caption := 'Channels';
BorderStyle := bsSizeable;
Width := 720;
Height := 480;
Position := poScreenCenter;
KeyPreview := True;
OnClose := @OnFormClose;
BuildUI;
ApplyTheme(DarkTheme);
RefreshList(0);
end;
procedure TChannelsForm.BuildUI;
var
i: Integer;
BtnW: Integer;
RW: Integer; // ширина правой панели
Y: Integer;
begin
// ----- Левая панель -----
FPanelLeft := TPanel.Create(Self);
FPanelLeft.Parent := Self;
FPanelLeft.Align := alLeft;
FPanelLeft.Width := LPANEL_W;
FPanelLeft.BevelOuter := bvNone;
BtnW := (LPANEL_W - 4 - 3 * 2) div 4; // 4 кнопки с зазорами
FBtnAdd := TFlatButton.Create(Self);
FBtnAdd.Parent := FPanelLeft;
FBtnAdd.SetBounds(2, 4, BtnW, BTN_H);
FBtnAdd.Caption := 'ADD';
FBtnAdd.OnClick := @OnAddClick;
FBtnDelete := TFlatButton.Create(Self);
FBtnDelete.Parent := FPanelLeft;
FBtnDelete.SetBounds(2 + (BtnW + 2), 4, BtnW, BTN_H);
FBtnDelete.Caption := 'DEL';
FBtnDelete.OnClick := @OnDeleteClick;
FBtnUp := TFlatButton.Create(Self);
FBtnUp.Parent := FPanelLeft;
FBtnUp.SetBounds(2 + 2 * (BtnW + 2), 4, BtnW, BTN_H);
FBtnUp.Caption := '↑';
FBtnUp.OnClick := @OnUpClick;
FBtnDown := TFlatButton.Create(Self);
FBtnDown.Parent := FPanelLeft;
FBtnDown.SetBounds(2 + 3 * (BtnW + 2), 4, BtnW, BTN_H);
FBtnDown.Caption := '↓';
FBtnDown.OnClick := @OnDownClick;
FList := TFlatListBox.Create(Self);
FList.Parent := FPanelLeft;
FList.SetBounds(0, TOOLBAR_H, LPANEL_W, FPanelLeft.Height - TOOLBAR_H);
FList.Anchors := [akLeft, akTop, akRight, akBottom];
FList.OnClick := @OnListClick;
FList.OnDblClick := @OnListDblClick;
// ----- Правая панель -----
FPanelRight := TPanel.Create(Self);
FPanelRight.Parent := Self;
FPanelRight.Align := alClient;
FPanelRight.BevelOuter := bvNone;
SetLength(FLbls, 0);
RW := Width - LPANEL_W;
Y := 8;
// Group
MakeLabel(FPanelRight, 'Group', LBL_X, Y + 1, LBL_W, CTRL_H);
FEdGroup := TFlatEdit.Create(Self);
FEdGroup.Parent := FPanelRight;
FEdGroup.SetBounds(CTRL_X, Y, RW - CTRL_X - 8, CTRL_H);
FEdGroup.Anchors := [akLeft, akTop, akRight];
FEdGroup.OnChange := @OnFieldChange;
Inc(Y, ROW_H);
// Name
MakeLabel(FPanelRight, 'Name', LBL_X, Y + 1, LBL_W, CTRL_H);
FEdName := TFlatEdit.Create(Self);
FEdName.Parent := FPanelRight;
FEdName.SetBounds(CTRL_X, Y, RW - CTRL_X - 8, CTRL_H);
FEdName.Anchors := [akLeft, akTop, akRight];
FEdName.OnChange := @OnFieldChange;
Inc(Y, ROW_H);
// RX Freq
MakeLabel(FPanelRight, 'RX Freq', LBL_X, Y + 1, LBL_W, CTRL_H);
FSpinRXFreq := TFlatFloatSpinEdit.Create(Self);
FSpinRXFreq.Parent := FPanelRight;
FSpinRXFreq.SetBounds(CTRL_X, Y, RW - CTRL_X - 50, CTRL_H);
FSpinRXFreq.Anchors := [akLeft, akTop, akRight];
FSpinRXFreq.MinValue := 0.001;
FSpinRXFreq.MaxValue := 2000.0;
FSpinRXFreq.Increment := 0.001;
FSpinRXFreq.DecimalPlaces := 6;
FSpinRXFreq.Value := 145.5;
FSpinRXFreq.OnChange := @OnFieldChange;
MakeLabel(FPanelRight, 'MHz', RW - 46, Y + 1, 40, CTRL_H);
Inc(Y, ROW_H);
// TX Freq
MakeLabel(FPanelRight, 'TX Freq', LBL_X, Y + 1, LBL_W, CTRL_H);
FSpinTXFreq := TFlatFloatSpinEdit.Create(Self);
FSpinTXFreq.Parent := FPanelRight;
FSpinTXFreq.SetBounds(CTRL_X, Y, RW - CTRL_X - 50, CTRL_H);
FSpinTXFreq.Anchors := [akLeft, akTop, akRight];
FSpinTXFreq.MinValue := 0.001;
FSpinTXFreq.MaxValue := 2000.0;
FSpinTXFreq.Increment := 0.001;
FSpinTXFreq.DecimalPlaces := 6;
FSpinTXFreq.Value := 145.5;
FSpinTXFreq.OnChange := @OnFieldChange;
MakeLabel(FPanelRight, 'MHz', RW - 46, Y + 1, 40, CTRL_H);
Inc(Y, ROW_H);
// Mode
MakeLabel(FPanelRight, 'Mode', LBL_X, Y + 1, LBL_W, CTRL_H);
FCmbMode := TFlatComboBox.Create(Self);
FCmbMode.Parent := FPanelRight;
FCmbMode.SetBounds(CTRL_X, Y, 160, CTRL_H);
for i := 0 to CH_MODE_COUNT - 1 do FCmbMode.Items.Add(CH_MODE_NAMES[i]);
FCmbMode.ItemIndex := 5; // FM default
FCmbMode.OnChange := @OnFieldChange;
Inc(Y, ROW_H);
// RPTR direction
MakeLabel(FPanelRight, 'RPTR', LBL_X, Y + 1, LBL_W, CTRL_H);
FBtnRptNone := TFlatButton.Create(Self);
FBtnRptNone.Parent := FPanelRight;
FBtnRptNone.SetBounds(CTRL_X, Y, 55, CTRL_H);
FBtnRptNone.Caption := 'NONE';
FBtnRptNone.Tag := 0;
FBtnRptNone.OnClick := @OnRptBtnClick;
FBtnRptMinus := TFlatButton.Create(Self);
FBtnRptMinus.Parent := FPanelRight;
FBtnRptMinus.SetBounds(CTRL_X + 58, Y, 38, CTRL_H);
FBtnRptMinus.Caption := '';
FBtnRptMinus.Tag := 1;
FBtnRptMinus.OnClick := @OnRptBtnClick;
FBtnRptPlus := TFlatButton.Create(Self);
FBtnRptPlus.Parent := FPanelRight;
FBtnRptPlus.SetBounds(CTRL_X + 99, Y, 38, CTRL_H);
FBtnRptPlus.Caption := '+';
FBtnRptPlus.Tag := 2;
FBtnRptPlus.OnClick := @OnRptBtnClick;
Inc(Y, ROW_H);
// RPTR Offset
MakeLabel(FPanelRight, 'RPT Offset', LBL_X, Y + 1, LBL_W, CTRL_H);
FSpinRptOffset := TFlatFloatSpinEdit.Create(Self);
FSpinRptOffset.Parent := FPanelRight;
FSpinRptOffset.SetBounds(CTRL_X, Y, RW - CTRL_X - 50, CTRL_H);
FSpinRptOffset.Anchors := [akLeft, akTop, akRight];
FSpinRptOffset.MinValue := 0.0;
FSpinRptOffset.MaxValue := 100.0;
FSpinRptOffset.Increment := 0.025;
FSpinRptOffset.DecimalPlaces := 3;
FSpinRptOffset.Value := 0.6;
FSpinRptOffset.OnChange := @OnFieldChange;
MakeLabel(FPanelRight, 'MHz', RW - 46, Y + 1, 40, CTRL_H);
Inc(Y, ROW_H);
// CTCSS
MakeLabel(FPanelRight, 'CTCSS', LBL_X, Y + 1, LBL_W, CTRL_H);
FChkCTCSS := TFlatCheckBox.Create(Self);
FChkCTCSS.Parent := FPanelRight;
FChkCTCSS.SetBounds(CTRL_X, Y, 26, CTRL_H);
FChkCTCSS.Caption := '';
FChkCTCSS.Checked := False;
FChkCTCSS.OnChange := @OnCTCSSChkChange;
FCmbCTCSS := TFlatComboBox.Create(Self);
FCmbCTCSS.Parent := FPanelRight;
FCmbCTCSS.SetBounds(CTRL_X + 30, Y, RW - CTRL_X - 38, CTRL_H);
FCmbCTCSS.Anchors := [akLeft, akTop, akRight];
for i := 0 to CH_CTCSS_COUNT - 1 do FCmbCTCSS.Items.Add(CH_CTCSS_NAMES[i]);
FCmbCTCSS.ItemIndex := 0;
FCmbCTCSS.OnChange := @OnCTCSSCmbChange;
Inc(Y, ROW_H);
// Power
MakeLabel(FPanelRight, 'Power', LBL_X, Y + 1, LBL_W, CTRL_H);
FSpinPower := TFlatSpinEdit.Create(Self);
FSpinPower.Parent := FPanelRight;
FSpinPower.SetBounds(CTRL_X, Y, 80, CTRL_H);
FSpinPower.MinValue := 0;
FSpinPower.MaxValue := 100;
FSpinPower.Value := 100;
FSpinPower.OnChange := @OnFieldChange;
MakeLabel(FPanelRight, '%', CTRL_X + 84, Y + 1, 20, CTRL_H);
end;
function TChannelsForm.MakeLabel(AParent: TWinControl; const Cap: string;
X, Y, W, H: Integer): TLabel;
var L: TLabel;
begin
L := TLabel.Create(Self);
L.Parent := AParent;
L.Caption := Cap;
L.SetBounds(X, Y, W, H);
L.Font.Name := 'Courier New';
L.Font.Size := 8;
L.Font.Color := DarkTheme.TextDim;
// Сохраняем для последующей перекраски темой
SetLength(FLbls, Length(FLbls) + 1);
FLbls[High(FLbls)] := L;
Result := L;
end;
procedure TChannelsForm.StyleBtn(B: TFlatButton; AActive: Boolean);
var T: TAppTheme;
begin
T := DarkTheme;
B.Active := AActive;
B.ClrNorm := T.BtnNorm;
B.ClrActive := T.BtnActive;
B.ClrHot := T.BtnHot;
B.ClrBorder := IfThen(AActive, T.BtnBorderActive, T.BtnBorderNorm);
B.ClrText := T.BtnText;
B.ClrTextAct := T.BtnTextActive;
end;
procedure TChannelsForm.ApplyTheme(const T: TAppTheme);
var
i: Integer;
Ch: TChannel;
begin
Color := T.Panel;
FPanelLeft.Color := T.Panel;
FPanelRight.Color := T.Panel;
FList.SetAppTheme(T);
StyleBtn(FBtnAdd);
StyleBtn(FBtnDelete);
StyleBtn(FBtnUp);
StyleBtn(FBtnDown);
// RPT buttons — пересчитываем Active из текущего состояния
if SelectedIdx >= 0 then
begin
Ch := FStore.GetChannel(SelectedIdx);
StyleBtn(FBtnRptNone, Ch.RptDir = 0);
StyleBtn(FBtnRptMinus, Ch.RptDir = 1);
StyleBtn(FBtnRptPlus, Ch.RptDir = 2);
end
else
begin
StyleBtn(FBtnRptNone, True);
StyleBtn(FBtnRptMinus, False);
StyleBtn(FBtnRptPlus, False);
end;
FEdGroup.SetAppTheme(T);
FEdName.SetAppTheme(T);
FSpinRXFreq.SetAppTheme(T);
FSpinTXFreq.SetAppTheme(T);
FCmbMode.SetAppTheme(T);
FSpinRptOffset.SetAppTheme(T);
FChkCTCSS.SetAppTheme(T);
FCmbCTCSS.SetAppTheme(T);
FSpinPower.SetAppTheme(T);
for i := 0 to High(FLbls) do
begin
FLbls[i].Font.Color := T.TextDim;
FLbls[i].Color := T.Panel;
FLbls[i].ParentColor := False;
end;
end;
// ---------------------------------------------------------------------------
// Список каналов
// ---------------------------------------------------------------------------
function TChannelsForm.SelectedIdx: Integer;
begin
Result := FList.ItemIndex;
end;
procedure TChannelsForm.RefreshList(KeepIdx: Integer);
var
i, NewIdx: Integer;
Ch: TChannel;
begin
FList.Items.Clear;
for i := 0 to FStore.Count - 1 do
begin
Ch := FStore.GetChannel(i);
if Ch.Group <> '' then
FList.Items.Add(Ch.Group + ': ' + Ch.Name)
else
FList.Items.Add(Ch.Name);
end;
if FStore.Count = 0 then
begin
LoadChannelToFields(-1);
Exit;
end;
NewIdx := EnsureRange(KeepIdx, 0, FStore.Count - 1);
FList.ItemIndex := NewIdx;
LoadChannelToFields(NewIdx);
end;
procedure TChannelsForm.LoadChannelToFields(Idx: Integer);
var Ch: TChannel;
HasSel: Boolean;
begin
FUpdating := True;
try
HasSel := (Idx >= 0) and (Idx < FStore.Count);
FEdGroup.Enabled := HasSel;
FEdName.Enabled := HasSel;
FSpinRXFreq.Enabled := HasSel;
FSpinTXFreq.Enabled := HasSel;
FCmbMode.Enabled := HasSel;
FBtnRptNone.Enabled := HasSel;
FBtnRptMinus.Enabled := HasSel;
FBtnRptPlus.Enabled := HasSel;
FSpinRptOffset.Enabled := HasSel;
FChkCTCSS.Enabled := HasSel;
FCmbCTCSS.Enabled := HasSel;
FSpinPower.Enabled := HasSel;
if not HasSel then Exit;
Ch := FStore.GetChannel(Idx);
FEdGroup.Text := Ch.Group;
FEdName.Text := Ch.Name;
FSpinRXFreq.Value := Ch.RXFreq / 1000000.0;
FSpinTXFreq.Value := Ch.TXFreq / 1000000.0;
FCmbMode.ItemIndex := EnsureRange(Ch.Mode, 0, CH_MODE_COUNT - 1);
StyleBtn(FBtnRptNone, Ch.RptDir = 0);
StyleBtn(FBtnRptMinus, Ch.RptDir = 1);
StyleBtn(FBtnRptPlus, Ch.RptDir = 2);
FSpinRptOffset.Value := Ch.RptOffsetHz / 1000000.0;
FChkCTCSS.Checked := Ch.CTCSSOn;
FCmbCTCSS.ItemIndex := EnsureRange(Ch.CTCSSToneIdx, 0, CH_CTCSS_COUNT - 1);
FSpinPower.Value := Ch.Power;
finally
FUpdating := False;
end;
end;
procedure TChannelsForm.SaveFieldsToChannel;
var
Idx: Integer;
Ch: TChannel;
i: Integer;
begin
if FUpdating then Exit;
Idx := SelectedIdx;
if (Idx < 0) or (Idx >= FStore.Count) then Exit;
Ch := FStore.GetChannel(Idx);
Ch.Group := FEdGroup.Text;
Ch.Name := FEdName.Text;
Ch.RXFreq := FSpinRXFreq.Value * 1000000.0;
Ch.TXFreq := FSpinTXFreq.Value * 1000000.0;
Ch.Mode := EnsureRange(FCmbMode.ItemIndex, 0, CH_MODE_COUNT - 1);
// RptDir берётся из кнопок (Active)
if FBtnRptMinus.Active then Ch.RptDir := 1
else if FBtnRptPlus.Active then Ch.RptDir := 2
else Ch.RptDir := 0;
Ch.RptOffsetHz := FSpinRptOffset.Value * 1000000.0;
Ch.CTCSSOn := FChkCTCSS.Checked;
Ch.CTCSSToneIdx := EnsureRange(FCmbCTCSS.ItemIndex, 0, CH_CTCSS_COUNT - 1);
Ch.Power := FSpinPower.Value;
FStore.UpdateChannel(Idx, Ch);
FStore.Save;
// Обновляем имя в списке
FUpdating := True;
try
if Ch.Group <> '' then
FList.Items[Idx] := Ch.Group + ': ' + Ch.Name
else
FList.Items[Idx] := Ch.Name;
finally
FUpdating := False;
end;
if Assigned(FOnChanged) then FOnChanged(Self);
end;
// ---------------------------------------------------------------------------
// Обработчики кнопок тулбара
// ---------------------------------------------------------------------------
procedure TChannelsForm.OnListClick(Sender: TObject);
begin
if FUpdating then Exit;
LoadChannelToFields(FList.ItemIndex);
end;
procedure TChannelsForm.OnListDblClick(Sender: TObject);
begin
LoadChannelToFields(FList.ItemIndex);
end;
procedure TChannelsForm.OnAddClick(Sender: TObject);
var
Ch: TChannel;
NewIdx: Integer;
begin
Ch := FCurrentDefaults; // берём значения текущего состояния радио
FStore.AddChannel(Ch);
FStore.Save;
NewIdx := FStore.Count - 1;
RefreshList(NewIdx);
if Assigned(FOnChanged) then FOnChanged(Self);
// Фокус на имя для быстрого переименования
if FEdName.Enabled then FEdName.SetFocus;
FEdName.SelectAll;
end;
procedure TChannelsForm.OnDeleteClick(Sender: TObject);
var Idx: Integer;
begin
Idx := SelectedIdx;
if (Idx < 0) or (Idx >= FStore.Count) then Exit;
FStore.DeleteChannel(Idx);
FStore.Save;
RefreshList(EnsureRange(Idx, 0, FStore.Count - 1));
if Assigned(FOnChanged) then FOnChanged(Self);
end;
procedure TChannelsForm.OnUpClick(Sender: TObject);
var Idx: Integer;
begin
Idx := SelectedIdx;
if Idx < 1 then Exit;
FStore.MoveUp(Idx);
FStore.Save;
RefreshList(Idx - 1);
if Assigned(FOnChanged) then FOnChanged(Self);
end;
procedure TChannelsForm.OnDownClick(Sender: TObject);
var Idx: Integer;
begin
Idx := SelectedIdx;
if (Idx < 0) or (Idx >= FStore.Count - 1) then Exit;
FStore.MoveDown(Idx);
FStore.Save;
RefreshList(Idx + 1);
if Assigned(FOnChanged) then FOnChanged(Self);
end;
// ---------------------------------------------------------------------------
// Обработчики полей редактирования
// ---------------------------------------------------------------------------
procedure TChannelsForm.OnFieldChange(Sender: TObject);
begin
SaveFieldsToChannel;
end;
procedure TChannelsForm.OnRptBtnClick(Sender: TObject);
var Dir: Integer;
begin
Dir := (Sender as TFlatButton).Tag;
StyleBtn(FBtnRptNone, Dir = 0);
StyleBtn(FBtnRptMinus, Dir = 1);
StyleBtn(FBtnRptPlus, Dir = 2);
SaveFieldsToChannel;
end;
procedure TChannelsForm.OnCTCSSChkChange(Sender: TObject);
begin
SaveFieldsToChannel;
end;
procedure TChannelsForm.OnCTCSSCmbChange(Sender: TObject);
begin
SaveFieldsToChannel;
end;
procedure TChannelsForm.OnFormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
CloseAction := caHide; // скрываем, не уничтожаем — указатель в MainForm остаётся валидным
end;
procedure TChannelsForm.SetCurrentDefaults(const Ch: TChannel);
begin
FCurrentDefaults := Ch;
end;
end.
+79
View File
@@ -0,0 +1,79 @@
unit FMRepeater;
{ FM repeater offset helpers — constants, default offsets, auto-direction logic. }
{$mode objfpc}{$H+}
interface
const
RPT_NONE = 0;
RPT_MINUS = 1;
RPT_PLUS = 2;
RPT_2M_BEGIN = 144000000.0;
RPT_2M_END = 148000000.0;
RPT_70CM_BEGIN = 430000000.0;
RPT_70CM_END = 440000000.0;
RPT_DEFAULT_2M = 600000.0; // 0.600 MHz
RPT_DEFAULT_70CM = 7600000.0; // 7.600 MHz
// European 2m repeater input segment: auto-enable minus direction
RPT_AUTO_MINUS_LO = 145600000.0; // 145.600 MHz
RPT_AUTO_MINUS_HI = 145787500.0; // 145.7875 MHz
{ Returns default offset (Hz) for the given visible frequency.
70 cm band 7.600 MHz; everything else 0.600 MHz. }
function RptDefaultOffsetHz(FreqHz: Double): Double;
{ Returns RPT_MINUS when FreqHz falls in the 2 m auto-minus segment,
RPT_NONE otherwise. }
function RptAutoDir(FreqHz: Double): Integer;
{ Formats OffsetHz as MHz with 3 decimal places, e.g. "0.600". }
function RptFormatOffset(OffsetHz: Double): string;
{ Parses a MHz string ("0.600", "7.6", "7,600") to Hz.
Returns 0 on parse error or out-of-range value. }
function RptParseOffset(const S: string): Double;
implementation
uses SysUtils;
function RptDefaultOffsetHz(FreqHz: Double): Double;
begin
if (FreqHz >= RPT_70CM_BEGIN) and (FreqHz <= RPT_70CM_END) then
Result := RPT_DEFAULT_70CM
else
Result := RPT_DEFAULT_2M;
end;
function RptAutoDir(FreqHz: Double): Integer;
begin
if (FreqHz >= RPT_AUTO_MINUS_LO) and (FreqHz <= RPT_AUTO_MINUS_HI) then
Result := RPT_MINUS
else
Result := RPT_NONE;
end;
function RptFormatOffset(OffsetHz: Double): string;
begin
Result := Format('%.3f', [OffsetHz / 1000000.0]);
end;
function RptParseOffset(const S: string): Double;
var
V: Double;
Code: Integer;
Norm: string;
begin
Norm := StringReplace(Trim(S), ',', '.', [rfReplaceAll]);
Val(Norm, V, Code);
if (Code = 0) and (V > 0.0) and (V <= 100.0) then
Result := V * 1000000.0
else
Result := 0.0;
end;
end.
+3 -1
View File
@@ -42,7 +42,9 @@ type
property ClrBorder: TColor read FClrBorder write FClrBorder;
property ClrText: TColor read FClrText write FClrText;
property ClrTextAct:TColor read FClrTextAct write FClrTextAct;
property OnClick: TNotifyEvent read FOnClick write FOnClick;
property OnClick: TNotifyEvent read FOnClick write FOnClick;
property OnMouseDown;
property OnMouseUp;
property Caption;
property Font;
property Enabled;
+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);
+14 -6
View File
@@ -152,8 +152,10 @@ type
LastCTun: Boolean;
LastAGCMode: Integer;
LastAGCTop: Integer;
LastFMRptDir: Integer;
LastFMRptOffsetHz: Double;
LastFMRptDir: Integer;
LastFMRptOffsetHz: Double;
LastCTCSSAutoActive: Boolean; // CTCSS был включён каналом (авто)
LastFMRptAutoActive: Boolean; // RPT был включён каналом (авто)
end;
TXvtrSettings = record
@@ -1182,8 +1184,10 @@ 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;
X.Entries[i].LastFMRptDir := 0;
X.Entries[i].LastFMRptOffsetHz := 0.0;
X.Entries[i].LastCTCSSAutoActive := False;
X.Entries[i].LastFMRptAutoActive := False;
end;
// Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились)
X.Entries[0].ButtonText := '2m';
@@ -1240,8 +1244,10 @@ 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);
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);
X.Entries[i].LastCTCSSAutoActive := JB(EObj, 'last_ctcss_auto', X.Entries[i].LastCTCSSAutoActive);
X.Entries[i].LastFMRptAutoActive := JB(EObj, 'last_fmrpt_auto', X.Entries[i].LastFMRptAutoActive);
// Валидация: LastFreq должен быть в [FreqBegin..FreqEnd]
if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or
(X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then
@@ -1285,6 +1291,8 @@ begin
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);
JW(EObj, 'last_ctcss_auto', X.Entries[i].LastCTCSSAutoActive);
JW(EObj, 'last_fmrpt_auto', X.Entries[i].LastFMRptAutoActive);
end;
end;
+8
View File
@@ -150,6 +150,14 @@
<Filename Value="FMRepeater.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit>
<Filename Value="ChannelStore.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit>
<Filename Value="ChannelsForm.pas"/>
<IsPartOfProject Value="True"/>
</Unit>
<Unit>
<Filename Value="FlatSpinEdit.pas"/>
<IsPartOfProject Value="True"/>