unit Settings; { Settings.pas — JSON-конфигурация HPSDR SDR приложения. Структура hpsdr_settings.json: { "AA:BB:CC:DD:EE:FF": { "global": {...}, "bands": {"5": {...}} } } } {$mode objfpc}{$H+} interface uses SysUtils, Classes, fpJSON, jsonparser, jsonscanner; const SETTINGS_FILE = 'hpsdr_settings.json'; CFG_BAND_COUNT = 11; CFG_BAND_DEFAULT_FREQ: array[0..CFG_BAND_COUNT-1] of Double = ( 1900000, 3750000, 5357000, 7100000, 10125000, 14200000, 18120000, 21200000, 24940000, 28500000, 50150000); CFG_BAND_DEFAULT_MODE: array[0..CFG_BAND_COUNT-1] of Integer = ( 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1); type TBandSettings = record VfoA: Double; VfoB: Double; Mode: Integer; FilterIdx: Integer; FilterBW: Integer; AGCMode: Integer; AGCTop: Integer; CTun: Boolean; SpanHz: Double; end; TGlobalSettings = record Volume: Integer; DriveLevel: Integer; ActiveVfo: Integer; NREnabled: Boolean; NBEnabled: Boolean; ANFEnabled: Boolean; AGCSlope: Integer; AGCHangThreshold: Integer; WfAGCEnabled: Boolean; WfNFEnabled: Boolean; LastBand: Integer; SampleRate: Integer; // глобальный — один для всех диапазонов WindowLeft: Integer; WindowTop: Integer; WindowWidth: Integer; WindowHeight: Integer; end; TSettingsManager = class private FFilePath: string; FRoot: TJSONObject; function EnsureObj(P: TJSONObject; const K: string): TJSONObject; function GetDevObj(const M: string): TJSONObject; function GetGlobalObj(D: TJSONObject): TJSONObject; function GetBandObj(D: TJSONObject; Idx: Integer): TJSONObject; 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; procedure JW(O: TJSONObject; const K: string; V: Integer); overload; procedure JW(O: TJSONObject; const K: string; V: Double); overload; procedure JW(O: TJSONObject; const K: string; V: Boolean); overload; public constructor Create(const FilePath: string = SETTINGS_FILE); destructor Destroy; override; procedure Load; procedure Save; function LoadDevice(const MAC: array of Byte; out G: TGlobalSettings; var Bands: array of TBandSettings): Boolean; procedure SaveGlobal(const MAC: array of Byte; const G: TGlobalSettings); procedure SaveBand(const MAC: array of Byte; BandIdx: Integer; const B: TBandSettings); function LoadBand(const MAC: array of Byte; BandIdx: Integer; out B: TBandSettings): Boolean; class function MacToStr(const MAC: array of Byte): string; class procedure DefaultBand(BandIdx: Integer; out B: TBandSettings); class procedure DefaultGlobal(out G: TGlobalSettings); // Размер окна — не привязан к MAC, хранится в корне JSON procedure SaveWindowBounds(L, T, W, H: Integer); procedure LoadWindowBounds(out L, T, W, H: Integer); end; implementation class function TSettingsManager.MacToStr(const MAC: array of Byte): string; begin Result := Format('%02X:%02X:%02X:%02X:%02X:%02X', [MAC[0],MAC[1],MAC[2],MAC[3],MAC[4],MAC[5]]); end; class procedure TSettingsManager.DefaultBand(BandIdx: Integer; out B: TBandSettings); begin FillChar(B, SizeOf(B), 0); B.VfoA := CFG_BAND_DEFAULT_FREQ[BandIdx]; B.VfoB := CFG_BAND_DEFAULT_FREQ[BandIdx]; B.Mode := CFG_BAND_DEFAULT_MODE[BandIdx]; B.FilterIdx := 5; B.FilterBW := 2700; B.AGCMode := 1; B.AGCTop := 90; B.CTun := False; B.SpanHz := 192000; end; class procedure TSettingsManager.DefaultGlobal(out G: TGlobalSettings); begin FillChar(G, SizeOf(G), 0); G.Volume := 70; G.DriveLevel := 50; G.ActiveVfo := 0; G.AGCSlope := 0; G.AGCHangThreshold := 100; G.LastBand := 5; G.SampleRate := 192000; end; constructor TSettingsManager.Create(const FilePath: string); begin inherited Create; FFilePath := FilePath; FRoot := TJSONObject.Create; end; destructor TSettingsManager.Destroy; begin FRoot.Free; inherited; end; procedure TSettingsManager.Load; var F: TFileStream; P: TJSONParser; D: TJSONData; begin if not FileExists(FFilePath) then Exit; try F := TFileStream.Create(FFilePath, fmOpenRead or fmShareDenyNone); try P := TJSONParser.Create(F, [joUTF8]); try D := P.Parse; if (D <> nil) and (D is TJSONObject) then begin FRoot.Free; FRoot := TJSONObject(D); end else D.Free; finally P.Free; end; finally F.Free; end; except FreeAndNil(FRoot); FRoot := TJSONObject.Create; end; end; procedure TSettingsManager.Save; var S: string; F: TFileStream; Buf: TBytes; begin try S := FRoot.FormatJSON([], 2); Buf := TEncoding.UTF8.GetBytes(S); F := TFileStream.Create(FFilePath, fmCreate); try if Length(Buf) > 0 then F.WriteBuffer(Buf[0], Length(Buf)); finally F.Free; end; except end; end; function TSettingsManager.EnsureObj(P: TJSONObject; const K: string): TJSONObject; var D: TJSONData; Idx: Integer; begin D := P.Find(K); if (D <> nil) and (D is TJSONObject) then Result := TJSONObject(D) else begin if D <> nil then begin Idx := P.IndexOfName(K); if Idx >= 0 then P.Delete(Idx); end; Result := TJSONObject.Create; P.Add(K, Result); end; end; function TSettingsManager.GetDevObj(const M: string): TJSONObject; begin Result := EnsureObj(FRoot, M); end; function TSettingsManager.GetGlobalObj(D: TJSONObject): TJSONObject; begin Result := EnsureObj(D, 'global'); end; function TSettingsManager.GetBandObj(D: TJSONObject; Idx: Integer): TJSONObject; begin Result := EnsureObj(EnsureObj(D, 'bands'), IntToStr(Idx)); end; function TSettingsManager.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 TSettingsManager.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 TSettingsManager.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; procedure TSettingsManager.JW(O: TJSONObject; const K: string; V: Integer); var Idx: Integer; begin Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx); O.Add(K, V); end; procedure TSettingsManager.JW(O: TJSONObject; const K: string; V: Double); var Idx: Integer; begin Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx); O.Add(K, V); end; procedure TSettingsManager.JW(O: TJSONObject; const K: string; V: Boolean); var Idx: Integer; begin Idx := O.IndexOfName(K); if Idx >= 0 then O.Delete(Idx); O.Add(K, V); end; function TSettingsManager.LoadDevice(const MAC: array of Byte; out G: TGlobalSettings; var Bands: array of TBandSettings): Boolean; var MacStr: string; DevObj,GObj,BObj: TJSONObject; i: Integer; begin MacStr := MacToStr(MAC); Result := FRoot.Find(MacStr) <> nil; DevObj := GetDevObj(MacStr); GObj := GetGlobalObj(DevObj); G.Volume := JI(GObj,'volume',70); G.DriveLevel := JI(GObj,'drive_level',50); G.ActiveVfo := JI(GObj,'active_vfo',0); G.NREnabled := JB(GObj,'nr_enabled',False); G.NBEnabled := JB(GObj,'nb_enabled',False); G.ANFEnabled := JB(GObj,'anf_enabled',False); G.AGCSlope := JI(GObj,'agc_slope',0); G.AGCHangThreshold := JI(GObj,'agc_hang_threshold',100); G.WfAGCEnabled := JB(GObj,'wf_agc_enabled',False); G.WfNFEnabled := JB(GObj,'wf_nf_enabled',False); G.LastBand := JI(GObj,'last_band',5); G.SampleRate := JI(GObj,'sample_rate',192000); for i := 0 to CFG_BAND_COUNT-1 do begin DefaultBand(i, Bands[i]); BObj := GetBandObj(DevObj, i); Bands[i].VfoA := JD(BObj,'vfo_a', Bands[i].VfoA); Bands[i].VfoB := JD(BObj,'vfo_b', Bands[i].VfoB); Bands[i].Mode := JI(BObj,'mode', Bands[i].Mode); Bands[i].FilterIdx := JI(BObj,'filter_idx', Bands[i].FilterIdx); Bands[i].FilterBW := JI(BObj,'filter_bw', Bands[i].FilterBW); Bands[i].AGCMode := JI(BObj,'agc_mode', Bands[i].AGCMode); Bands[i].AGCTop := JI(BObj,'agc_top', Bands[i].AGCTop); Bands[i].CTun := JB(BObj,'ctun', Bands[i].CTun); Bands[i].SpanHz := JD(BObj,'span_hz', Bands[i].SpanHz); end; end; procedure TSettingsManager.SaveGlobal(const MAC: array of Byte; const G: TGlobalSettings); var O: TJSONObject; begin O := GetGlobalObj(GetDevObj(MacToStr(MAC))); JW(O,'volume',G.Volume); JW(O,'drive_level',G.DriveLevel); JW(O,'active_vfo',G.ActiveVfo); JW(O,'nr_enabled',G.NREnabled); JW(O,'nb_enabled',G.NBEnabled); JW(O,'anf_enabled',G.ANFEnabled); JW(O,'agc_slope',G.AGCSlope); JW(O,'agc_hang_threshold',G.AGCHangThreshold); JW(O,'wf_agc_enabled',G.WfAGCEnabled); JW(O,'wf_nf_enabled',G.WfNFEnabled); JW(O,'last_band',G.LastBand); JW(O,'sample_rate',G.SampleRate); end; procedure TSettingsManager.SaveBand(const MAC: array of Byte; BandIdx: Integer; const B: TBandSettings); var O: TJSONObject; begin if (BandIdx < 0) or (BandIdx >= CFG_BAND_COUNT) then Exit; O := GetBandObj(GetDevObj(MacToStr(MAC)), BandIdx); JW(O,'vfo_a',B.VfoA); JW(O,'vfo_b',B.VfoB); JW(O,'mode',B.Mode); JW(O,'filter_idx',B.FilterIdx); JW(O,'filter_bw',B.FilterBW); JW(O,'agc_mode',B.AGCMode); JW(O,'agc_top',B.AGCTop); JW(O,'ctun',B.CTun); JW(O,'span_hz',B.SpanHz); end; function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer; out B: TBandSettings): Boolean; var MacStr: string; O: TJSONObject; begin DefaultBand(BandIdx, B); MacStr := MacToStr(MAC); Result := FRoot.Find(MacStr) <> nil; if not Result then Exit; O := GetBandObj(GetDevObj(MacStr), BandIdx); B.VfoA := JD(O,'vfo_a', B.VfoA); B.VfoB := JD(O,'vfo_b', B.VfoB); B.Mode := JI(O,'mode', B.Mode); B.FilterIdx := JI(O,'filter_idx', B.FilterIdx); B.FilterBW := JI(O,'filter_bw', B.FilterBW); B.AGCMode := JI(O,'agc_mode', B.AGCMode); B.AGCTop := JI(O,'agc_top', B.AGCTop); B.CTun := JB(O,'ctun', B.CTun); B.SpanHz := JD(O,'span_hz', B.SpanHz); end; procedure TSettingsManager.SaveWindowBounds(L, T, W, H: Integer); var O: TJSONObject; begin O := EnsureObj(FRoot, 'window'); JW(O, 'left', L); JW(O, 'top', T); JW(O, 'width', W); JW(O, 'height', H); end; procedure TSettingsManager.LoadWindowBounds(out L, T, W, H: Integer); var O: TJSONObject; begin L := 80; T := 80; W := 1400; H := 900; if FRoot.Find('window') = nil then Exit; O := EnsureObj(FRoot, 'window'); L := JI(O, 'left', 80); T := JI(O, 'top', 80); W := JI(O, 'width', 1400); H := JI(O, 'height', 900); end; end.