Files
ewsdr/DeviceStore.pas
T

438 lines
14 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.
{
Copyright (C)
2026 - Uladzimir Karpenka, EW8BAK
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
}
unit DeviceStore;
{$mode objfpc}{$H+}
// Общее хранилище устройств — единый источник правды для десктоп-диалога
// (DeviceForm) и web-оверлея. Владеет сохранёнными устройствами (persist в
// hpsdr_devices.ini) и текущим списком найденных при discovery.
// Контроллер (TRadioController) держит один экземпляр; фронтенды работают
// через него, поэтому списки не расходятся.
interface
uses
Classes, SysUtils, IniFiles, BoardUtils, PlatformUtils, RadioBackend;
const
DEVICE_CFG_NAME = 'hpsdr_devices.ini';
type
// Запись о сохранённом устройстве
TSavedDevice = record
Name: string; // пользовательское имя
IPAddress: string;
BoardType: Integer;
AutoStart: Boolean; // запускать автоматически при старте
Kind: TBackendKind; // bkHPSDR / bkPluto
URI: string; // Pluto: 'ip:..' / 'usb:..'
Serial: string; // Pluto serial
// MAC радио, узнанный при прошлом подключении. Настройки персистятся по MAC,
// а старт по сохранённому IP (AutoStart/CONNECT) идёт без дискавери — без
// этого поля профиль уезжал бы в «нулевой» MAC. Пусто = ещё не знаем.
MAC: array[0..5] of Byte;
end;
// Найденное устройство (результат discovery)
TDiscoveredDevice = record
IPAddress: string;
DisplayName: string;
BoardType: Integer;
MAC: array[0..5] of Byte; // нужен для preload-rate lookup (LoadDevice по MAC)
Kind: TBackendKind; // bkHPSDR / bkPluto
URI: string; // Pluto: context URI
Serial: string; // Pluto serial
end;
{ TDeviceStore }
TDeviceStore = class
private
FSaved: array of TSavedDevice;
FSavedCount: Integer;
FDiscovered: array of TDiscoveredDevice;
FDiscoveredCount: Integer;
public
constructor Create;
// ---- Сохранённые устройства ----
procedure LoadSaved;
procedure SaveSaved;
function SavedCount: Integer;
function Saved(Idx: Integer): TSavedDevice;
// Добавляет запись (Name пустое → 'HPSDR'); возвращает индекс новой записи.
function AddSaved(const AName, IP: string; BoardType: Integer): Integer;
// Добавляет Pluto-устройство (Kind=bkPluto) с URI/Serial.
function AddSavedPluto(const AName, AURI, ASerial: string): Integer;
procedure RemoveSaved(Idx: Integer);
procedure SetAutoStart(Idx: Integer); // эксклюзивно: только одно autostart
function AutoStartIP: string;
function AutoStartBoardType: Integer;
// Полная autostart-запись (Kind/URI/Serial) — для бэкенд-агностичного автозапуска
// (Pluto открывается по URI, у USB-Pluto IPAddress пуст). False — нет autostart.
function AutoStartDevice(out Dev: TSavedDevice): Boolean;
function SavedBoardType(Idx: Integer): Integer;
function SavedDisplay(Idx: Integer): string; // строка для списка (с * и board)
// ---- Найденные (discovery) ----
procedure ClearDiscovered;
procedure AddDiscovered(const IP, DisplayName: string; BoardType: Integer;
const MAC: array of Byte;
Kind: TBackendKind = bkHPSDR;
const URI: string = ''; const Serial: string = '');
function DiscoveredCount: Integer;
function Discovered(Idx: Integer): TDiscoveredDevice;
// IP найденного устройства по IP (для preload-rate lookup и т.п.)
function FindDiscoveredByIP(const IP: string): Integer;
// ---- MAC сохранённого устройства ----
// Поиск сохранённой записи по IP или URI (Pluto хранит адрес в обоих полях).
function FindSavedByAddr(const Addr: string): Integer;
// Запоминает MAC за адресом (после успешного connect). Пишет ini только при
// изменении, чтобы не дёргать диск на каждом старте.
procedure SetSavedMac(const Addr: string; const MAC: array of Byte);
end;
// MAC == 00:00:00:00:00:00 — «неизвестен».
function MacIsZero(const MAC: array of Byte): Boolean;
implementation
function MacIsZero(const MAC: array of Byte): Boolean;
var I: Integer;
begin
Result := True;
for I := 0 to High(MAC) do
if MAC[I] <> 0 then Exit(False);
end;
function MacToIniStr(const MAC: array of Byte): string;
var I: Integer;
begin
Result := '';
for I := 0 to 5 do
begin
if I > 0 then Result := Result + ':';
Result := Result + IntToHex(MAC[I], 2);
end;
end;
procedure IniStrToMac(const S: string; out MAC: array of Byte);
var Parts: TStringArray; I, V: Integer;
begin
FillChar(MAC[0], 6, 0);
Parts := S.Split([':']);
if Length(Parts) <> 6 then Exit;
for I := 0 to 5 do
begin
V := StrToIntDef('$' + Parts[I], -1);
if (V < 0) or (V > 255) then begin FillChar(MAC[0], 6, 0); Exit; end;
MAC[I] := Byte(V);
end;
end;
{ TDeviceStore }
constructor TDeviceStore.Create;
begin
inherited Create;
FSavedCount := 0;
FDiscoveredCount := 0;
LoadSaved;
end;
procedure TDeviceStore.LoadSaved;
var
Ini: TIniFile;
I, N: Integer;
Section: string;
begin
FSavedCount := 0;
SetLength(FSaved, 0);
if not FileExists(GetAppCfgDir + DEVICE_CFG_NAME) then Exit;
Ini := TIniFile.Create(GetAppCfgDir + DEVICE_CFG_NAME);
try
N := Ini.ReadInteger('Devices', 'Count', 0);
SetLength(FSaved, N);
for I := 0 to N - 1 do
begin
Section := 'Device' + IntToStr(I);
FSaved[I].Name := Ini.ReadString (Section, 'Name', 'HPSDR');
FSaved[I].IPAddress := Ini.ReadString (Section, 'IP', '');
FSaved[I].BoardType := Ini.ReadInteger(Section, 'BoardType', 0);
FSaved[I].AutoStart := Ini.ReadBool (Section, 'AutoStart', False);
FSaved[I].Kind := TBackendKind(Ini.ReadInteger(Section, 'Kind', 0));
FSaved[I].URI := Ini.ReadString (Section, 'URI', '');
FSaved[I].Serial := Ini.ReadString (Section, 'Serial', '');
IniStrToMac(Ini.ReadString(Section, 'MAC', ''), FSaved[I].MAC);
Inc(FSavedCount);
end;
finally
Ini.Free;
end;
end;
procedure TDeviceStore.SaveSaved;
var
Ini: TIniFile;
I: Integer;
Section: string;
begin
Ini := TIniFile.Create(GetAppCfgDir + DEVICE_CFG_NAME);
try
Ini.WriteInteger('Devices', 'Count', FSavedCount);
for I := 0 to FSavedCount - 1 do
begin
Section := 'Device' + IntToStr(I);
Ini.WriteString (Section, 'Name', FSaved[I].Name);
Ini.WriteString (Section, 'IP', FSaved[I].IPAddress);
Ini.WriteInteger(Section, 'BoardType', FSaved[I].BoardType);
Ini.WriteBool (Section, 'AutoStart', FSaved[I].AutoStart);
Ini.WriteInteger(Section, 'Kind', Ord(FSaved[I].Kind));
Ini.WriteString (Section, 'URI', FSaved[I].URI);
Ini.WriteString (Section, 'Serial', FSaved[I].Serial);
if MacIsZero(FSaved[I].MAC) then
Ini.DeleteKey(Section, 'MAC')
else
Ini.WriteString(Section, 'MAC', MacToIniStr(FSaved[I].MAC));
end;
finally
Ini.Free;
end;
end;
function TDeviceStore.SavedCount: Integer;
begin
Result := FSavedCount;
end;
function TDeviceStore.Saved(Idx: Integer): TSavedDevice;
begin
if (Idx >= 0) and (Idx < FSavedCount) then
Result := FSaved[Idx]
else
begin
Result.Name := ''; Result.IPAddress := '';
Result.BoardType := 0; Result.AutoStart := False;
FillChar(Result.MAC, SizeOf(Result.MAC), 0);
end;
end;
function TDeviceStore.AddSaved(const AName, IP: string; BoardType: Integer): Integer;
begin
Result := FSavedCount;
Inc(FSavedCount);
SetLength(FSaved, FSavedCount);
if Trim(AName) <> '' then FSaved[Result].Name := Trim(AName)
else FSaved[Result].Name := 'HPSDR';
FSaved[Result].IPAddress := Trim(IP);
FSaved[Result].BoardType := BoardType;
FSaved[Result].AutoStart := False;
FSaved[Result].Kind := bkHPSDR;
FSaved[Result].URI := '';
FSaved[Result].Serial := '';
FillChar(FSaved[Result].MAC, SizeOf(FSaved[Result].MAC), 0);
SaveSaved;
end;
function TDeviceStore.AddSavedPluto(const AName, AURI, ASerial: string): Integer;
begin
Result := FSavedCount;
Inc(FSavedCount);
SetLength(FSaved, FSavedCount);
if Trim(AName) <> '' then FSaved[Result].Name := Trim(AName)
else FSaved[Result].Name := 'PlutoSDR';
FSaved[Result].IPAddress := Trim(AURI); // в списке показываем URI
FSaved[Result].BoardType := 0;
FSaved[Result].AutoStart := False;
FSaved[Result].Kind := bkPluto;
FSaved[Result].URI := Trim(AURI);
FSaved[Result].Serial := Trim(ASerial);
FillChar(FSaved[Result].MAC, SizeOf(FSaved[Result].MAC), 0);
SaveSaved;
end;
procedure TDeviceStore.RemoveSaved(Idx: Integer);
var
I: Integer;
begin
if (Idx < 0) or (Idx >= FSavedCount) then Exit;
for I := Idx to FSavedCount - 2 do
FSaved[I] := FSaved[I + 1];
Dec(FSavedCount);
SetLength(FSaved, FSavedCount);
SaveSaved;
end;
procedure TDeviceStore.SetAutoStart(Idx: Integer);
var
I: Integer;
begin
if (Idx < 0) or (Idx >= FSavedCount) then Exit;
// Только одно устройство может быть AutoStart
for I := 0 to FSavedCount - 1 do
FSaved[I].AutoStart := (I = Idx);
SaveSaved;
end;
function TDeviceStore.AutoStartIP: string;
var
I: Integer;
begin
Result := '';
for I := 0 to FSavedCount - 1 do
if FSaved[I].AutoStart then
begin
Result := FSaved[I].IPAddress;
Exit;
end;
end;
function TDeviceStore.AutoStartBoardType: Integer;
var
I: Integer;
begin
Result := 0;
for I := 0 to FSavedCount - 1 do
if FSaved[I].AutoStart then
begin
Result := FSaved[I].BoardType;
Exit;
end;
end;
function TDeviceStore.AutoStartDevice(out Dev: TSavedDevice): Boolean;
var
I: Integer;
begin
Result := False;
for I := 0 to FSavedCount - 1 do
if FSaved[I].AutoStart then
begin
Dev := FSaved[I];
Exit(True);
end;
end;
function TDeviceStore.SavedBoardType(Idx: Integer): Integer;
begin
if (Idx >= 0) and (Idx < FSavedCount) then
Result := FSaved[Idx].BoardType
else
Result := 0;
end;
function TDeviceStore.SavedDisplay(Idx: Integer): string;
begin
Result := '';
if (Idx < 0) or (Idx >= FSavedCount) then Exit;
Result := FSaved[Idx].Name + ' [' + FSaved[Idx].IPAddress + ']';
if FSaved[Idx].BoardType > 0 then
Result := Result + ' ' + BoardTypeName(FSaved[Idx].BoardType);
if FSaved[Idx].AutoStart then
Result := '* ' + Result;
end;
procedure TDeviceStore.ClearDiscovered;
begin
FDiscoveredCount := 0;
SetLength(FDiscovered, 0);
end;
procedure TDeviceStore.AddDiscovered(const IP, DisplayName: string; BoardType: Integer;
const MAC: array of Byte; Kind: TBackendKind; const URI: string; const Serial: string);
var
Idx, J: Integer;
begin
Idx := FDiscoveredCount;
Inc(FDiscoveredCount);
SetLength(FDiscovered, FDiscoveredCount);
FDiscovered[Idx].IPAddress := IP;
FDiscovered[Idx].DisplayName := DisplayName;
FDiscovered[Idx].BoardType := BoardType;
FDiscovered[Idx].Kind := Kind;
FDiscovered[Idx].URI := URI;
FDiscovered[Idx].Serial := Serial;
FillChar(FDiscovered[Idx].MAC, SizeOf(FDiscovered[Idx].MAC), 0);
for J := 0 to High(MAC) do
if J <= 5 then FDiscovered[Idx].MAC[J] := MAC[J];
end;
function TDeviceStore.DiscoveredCount: Integer;
begin
Result := FDiscoveredCount;
end;
function TDeviceStore.Discovered(Idx: Integer): TDiscoveredDevice;
begin
if (Idx >= 0) and (Idx < FDiscoveredCount) then
Result := FDiscovered[Idx]
else
begin
Result.IPAddress := ''; Result.DisplayName := ''; Result.BoardType := 0;
end;
end;
function TDeviceStore.FindDiscoveredByIP(const IP: string): Integer;
var
I: Integer;
begin
Result := -1;
for I := 0 to FDiscoveredCount - 1 do
if SameText(FDiscovered[I].IPAddress, IP) then
begin
Result := I;
Exit;
end;
end;
function TDeviceStore.FindSavedByAddr(const Addr: string): Integer;
var
I: Integer;
begin
Result := -1;
if Trim(Addr) = '' then Exit;
for I := 0 to FSavedCount - 1 do
if SameText(FSaved[I].IPAddress, Addr) or SameText(FSaved[I].URI, Addr) then
begin
Result := I;
Exit;
end;
end;
procedure TDeviceStore.SetSavedMac(const Addr: string; const MAC: array of Byte);
var
Idx, J: Integer;
Cur: array[0..5] of Byte;
begin
if MacIsZero(MAC) then Exit;
Idx := FindSavedByAddr(Addr);
if Idx < 0 then Exit;
FillChar(Cur, SizeOf(Cur), 0);
for J := 0 to High(MAC) do
if J <= 5 then Cur[J] := MAC[J];
if CompareByte(Cur, FSaved[Idx].MAC, 6) = 0 then Exit; // уже записан
Move(Cur[0], FSaved[Idx].MAC[0], 6);
SaveSaved;
end;
end.