mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +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.
|
||||
Reference in New Issue
Block a user