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