Files
ewsdr/DeviceStore.pas
T
ew8bakandClaude Opus 4.8 66c9fa63b9 fix(settings): профиль устройства уезжал в нулевой MAC при старте по AutoStart
Настройки персистятся секцией по MAC радио, но ResolveDevice брал MAC только из
списка найденных дискавери. У сохранённых устройств поля MAC не было вовсе, и
START по AutoStart/CONNECT без дискавери шёл с MAC 00:00:00:00:00:00 — грузился
и сохранялся чужой профиль. Пользователь видел «настройки сбросились»: выключался
wideband, менялся sample rate, терялись банды и паны, причём в зависимости от
того, нажимал ли он перед стартом DISCOVER.

- DeviceStore: у TSavedDevice поле MAC + persist в hpsdr_devices.ini (MAC=..),
  FindSavedByAddr/SetSavedMac (пишет ini только при изменении), MacIsZero.
- RadioController.ResolveDevice: saved-ветка восстанавливает и MAC.
- RadioController.EnsureDeviceMac: добор неизвестного MAC — найденное в этой
  сессии → ini → короткий служебный поиск в сети (700 мс, unicast на DirectIP,
  OnDeviceFound временно снят, чтобы не трогать список устройств в UI).
- ConnectDevice: добор MAC до Connect, после успеха MAC запоминается за
  сохранённым устройством, так что следующий AutoStart идёт без пробника.

Проверено на железе (ANAN 172.16.2.200): в hpsdr_devices.ini появился
MAC=04:91:62:FD:7B:86, секция нулевого MAC больше не создаётся, старт по
AutoStart и старт после DISCOVER дают один и тот же профиль.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 22:43:32 +03:00

419 lines
13 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.
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.