mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
- 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>
273 lines
7.6 KiB
ObjectPascal
273 lines
7.6 KiB
ObjectPascal
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.
|