unit ChannelStore; { TChannelStore — хранилище каналов (channel memory). Формат файла channels.json (рядом с hpsdr_settings.json). } {$mode objfpc}{$H+} interface uses SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner, PlatformUtils, RadioModes; const CHANNELS_FILE = 'channels.json'; CHANNEL_MODE_MIN = RadioModes.MODE_MIN; CHANNEL_MODE_MAX = RadioModes.MODE_MAX; type TChannel = record Group: string; Name: string; RXFreq: Double; // Hz TXFreq: Double; // Hz Mode: Integer; // RadioModes.MODE_* persisted index 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 = ''); 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 := MODE_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; if AFilePath = '' then FFilePath := GetAppCfgDir + CHANNELS_FILE else 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', MODE_FM), CHANNEL_MODE_MIN, CHANNEL_MODE_MAX); 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.