feat: Pluto/AD936x SDR backend (discovery, RX, VHF band plan)

Add ADALM-Pluto / AD9361 support as a second hardware backend alongside
openHPSDR, sharing the WDSP DSP pipeline and the existing controller API
(UI stays decoupled from logic).

- RadioBackend.pas: abstract TRadioBackend + TBackendCaps + TRadioDevice
  (Kind/URI/Serial). THPSDRNetwork now derives from it (state via virtual
  getters); TRadioController.FNetwork is the base type.
- IIOBindings.pas: dynamic libiio loader (runs without libiio present).
- PlutoBackend.pas: scan/probe-by-URI, connect, LO/rate/bandwidth/gain
  control, RX streaming thread (int16->24bit BE -> OnDDCIQ), Q conjugated
  to match WDSP IQ convention. Verified on LibreSDR (AD9361) over network.
- Unified discovery: TDiscoverThread scans both backends; network Plutos
  found via direct ProbeURI (no mDNS needed). ConnectDevice dispatches by
  Dev.Kind (EnsureBackend swaps backend, preserving callbacks).
- DeviceStore/DeviceForm: persist Kind/URI/Serial; save Pluto via
  AddSavedPluto so saved/autostart devices reconnect across restarts.
- VHF/UHF band plan (BoardUtils, kind-aware): 6m..ADS-B for Pluto; fixes
  HF clamps (band detect, mouse-wheel 60 MHz cap, freq-display max).
- SampleRateOverlay: configurable presets (Pluto 576k..5760k, >520 ksps),
  auto-width to fit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 18:55:20 +03:00
co-authored by Claude Opus 4.8
parent 0d82d1d8e6
commit a988849d3e
13 changed files with 1940 additions and 164 deletions
+41 -3
View File
@@ -11,7 +11,7 @@ unit DeviceStore;
interface
uses
Classes, SysUtils, IniFiles, BoardUtils, PlatformUtils;
Classes, SysUtils, IniFiles, BoardUtils, PlatformUtils, RadioBackend;
const
DEVICE_CFG_NAME = 'hpsdr_devices.ini';
@@ -23,6 +23,9 @@ type
IPAddress: string;
BoardType: Integer;
AutoStart: Boolean; // запускать автоматически при старте
Kind: TBackendKind; // bkHPSDR / bkPluto
URI: string; // Pluto: 'ip:..' / 'usb:..'
Serial: string; // Pluto serial
end;
// Найденное устройство (результат discovery)
@@ -31,6 +34,9 @@ type
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 }
@@ -50,6 +56,8 @@ type
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;
@@ -60,7 +68,9 @@ type
// ---- Найденные (discovery) ----
procedure ClearDiscovered;
procedure AddDiscovered(const IP, DisplayName: string; BoardType: Integer;
const MAC: array of Byte);
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 и т.п.)
@@ -100,6 +110,9 @@ begin
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
@@ -123,6 +136,9 @@ begin
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;
@@ -155,6 +171,25 @@ begin
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;
@@ -233,7 +268,7 @@ begin
end;
procedure TDeviceStore.AddDiscovered(const IP, DisplayName: string; BoardType: Integer;
const MAC: array of Byte);
const MAC: array of Byte; Kind: TBackendKind; const URI: string; const Serial: string);
var
Idx, J: Integer;
begin
@@ -243,6 +278,9 @@ begin
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];