mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
The headless daemon (ewsdrd) only ever built HPSDR devices, so Pluto could not be used: backend is chosen by Dev.Kind and Pluto opens by URI, neither of which the daemon propagated. Also local audio played on the host and web Discover never probed network Plutos. Shared, backend-agnostic helpers in TRadioController (used by GUI + daemon): - ResolveDevice(IP): discovered->saved lookup restoring Kind/URI/Serial/ BoardType/MAC. MainForm.ResolveDevice now delegates here. - AddSavedDevice(name, addr): web dev_add saves Pluto when addr has a URI scheme (ip:/usb:/local:), else HPSDR. Both web hosts use it. - SeedPlutoProbeFromSaved: seed network-probe URIs from saved Plutos (no mDNS -> a network Pluto is only found by direct URI probe). - LocalAudioEnabled flag (GUI=True): when False the local sound card is neither opened nor written; RX audio goes only to web (OnAudioConsume). DeviceStore.AutoStartDevice(out Dev): full autostart record (Kind/URI/ Serial) so a saved Pluto (URI-addressed, empty IPAddress on USB) can autostart. Daemon (ewsdrd.lpr): LocalAudioEnabled:=False; SyncConnect via ResolveDevice; autostart via AutoStartDevice; SyncDiscover seeds probe URIs then Discover; SyncDevAdd via AddSavedDevice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
334 lines
10 KiB
ObjectPascal
334 lines
10 KiB
ObjectPascal
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
|
||
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;
|
||
end;
|
||
|
||
implementation
|
||
|
||
{ 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', '');
|
||
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);
|
||
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;
|
||
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 := '';
|
||
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);
|
||
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;
|
||
|
||
end.
|