mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
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:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user