Files
ewsdr/ChannelStore.pas
T
ew8bakandClaude Sonnet 4.6 1175e42516 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>
2026-05-27 15:39:37 +03:00

273 lines
7.6 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.