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
+103 -64
View File
@@ -19,7 +19,7 @@ uses
{$ELSE}
Sockets, BaseUnix,
{$ENDIF}
HPSDRProtocol, SyncObjs, Settings;
HPSDRProtocol, SyncObjs, Settings, RadioBackend;
const
DUC_TX_QUEUE_SIZE = 256; // power of two, bounded latency on TX underrun/overrun
@@ -70,33 +70,19 @@ const
type
THPSDRDevice = record
IPAddress: string;
Port: Word;
MAC: array[0..5] of Byte;
BoardType: Byte;
ProtocolVersion: Byte;
FirmwareVersion: Byte;
NumDDCs: Byte;
FreqOrPhase: Byte;
EndianModes: Byte;
InUse: Boolean;
Valid: Boolean;
end;
PHPSDRDevice = ^THPSDRDevice;
THPSDRDeviceArray = array of THPSDRDevice;
// Запись устройства и типы колбэков теперь общие (RadioBackend) — здесь
// алиасы для обратной совместимости со старым кодом (THPSDRDevice и т.д.).
THPSDRDevice = RadioBackend.TRadioDevice;
PHPSDRDevice = RadioBackend.PRadioDevice;
THPSDRDeviceArray = RadioBackend.TRadioDeviceArray;
TOnDeviceFound = RadioBackend.TOnDeviceFound;
TOnDDCIQPacket = RadioBackend.TOnDDCIQPacket;
TOnMicPacket = RadioBackend.TOnMicPacket;
TOnHPStatus = RadioBackend.TOnHPStatus;
TOnWidebandFrame = RadioBackend.TOnWidebandFrame;
TOnDeviceFound = procedure(const Dev: THPSDRDevice) of object;
TOnDDCIQPacket = procedure(DDCIndex: Integer;
const Data: TDDCIQPacket) of object;
TOnMicPacket = procedure(const Data: TMicDataPacket) of object;
TOnHPStatus = procedure(const Status: THighPriorityStatus) of object;
TOnWidebandFrame = procedure(ADCIndex: Integer;
const Samples: array of SmallInt;
Count: Integer) of object;
{ THPSDRNetwork }
THPSDRNetwork = class
{ THPSDRNetwork — openHPSDR Ethernet protocol бэкенд }
THPSDRNetwork = class(TRadioBackend)
private
FSocket: TSocket;
FLocalPort: Word;
@@ -125,11 +111,8 @@ type
FKeepaliveThread: TThread;
FDUCIQThread: TThread;
FOnDeviceFound: TOnDeviceFound;
FOnDDCIQ: TOnDDCIQPacket;
FOnMic: TOnMicPacket;
FOnHPStatus: TOnHPStatus;
FOnWideband: TOnWidebandFrame;
// Колбэки данных (FOnDeviceFound/FOnDDCIQ/FOnMic/FOnHPStatus/FOnWideband)
// унаследованы из TRadioBackend (protected) — здесь не дублируем.
FPortDDCSpec: Word;
FPortDUCSpec: Word;
@@ -210,59 +193,61 @@ type
function NextSeq(var S: LongWord): LongWord;
procedure PackSeqBytes(var B: array of Byte; Seq: LongWord);
protected
// Полиморфные геттеры состояния (свойства Connected/Running/Device/LastError/
// DirectIP объявлены в TRadioBackend).
function GetConnected: Boolean; override;
function GetRunning: Boolean; override;
function GetDevice: TRadioDevice; override;
function GetLastError: string; override;
function GetDirectIP: string; override;
procedure SetDirectIP(const V: string); override;
public
constructor Create;
destructor Destroy; override;
function Discover(TimeoutMs: Integer = 3000): THPSDRDeviceArray;
function Connect(const Dev: THPSDRDevice): Boolean;
procedure Disconnect;
function Caps: TBackendCaps; override;
procedure SendGeneralPacket(const Pkt: TGeneralPacket);
function Discover(TimeoutMs: Integer = 3000): THPSDRDeviceArray; override;
function Connect(const Dev: THPSDRDevice): Boolean; override;
procedure Disconnect; override;
procedure SendGeneralPacket(const Pkt: TGeneralPacket); override;
procedure ConfigureWideband(ADCIndex: Integer; Enabled: Boolean;
SamplesPerPacket: Integer = 512; SampleBits: Integer = 16;
UpdateRateMS: Integer = 70; PacketsPerFrame: Integer = 32);
procedure SendDDCSpecific(const Pkt: TDDCSpecificPacket);
UpdateRateMS: Integer = 70; PacketsPerFrame: Integer = 32); override;
procedure SendDDCSpecific(const Pkt: TDDCSpecificPacket); override;
procedure ConfigureDDCs(NumDDCs: Byte; SampleRate: Word; ADCSource: Byte = 0;
DitherEnabled: Boolean = True; RandomEnabled: Boolean = True);
procedure SendDUCSpecific(const Pkt: TDUCSpecificPacket);
procedure SendHighPriority(const Pkt: THighPriorityPacket);
DitherEnabled: Boolean = True; RandomEnabled: Boolean = True); override;
procedure SendDUCSpecific(const Pkt: TDUCSpecificPacket); override;
procedure SendHighPriority(const Pkt: THighPriorityPacket); override;
procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double;
DriveLevel: Byte = 100);
DriveLevel: Byte = 100); override;
procedure UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte;
Transmitting, PAEnabled, AlexEnabled: Boolean);
procedure SetStepAtten(dB: Byte);
procedure SetAlexConfig(const A: TAlexSettings);
Transmitting, PAEnabled, AlexEnabled: Boolean); override;
procedure SetStepAtten(dB: Byte); override;
procedure SetAlexConfig(const A: TAlexSettings); override;
// Включение/настройка XVTR-режима. Все параметры применяются мгновенно
// (отправляется HP-пакет). Когда AEnable=False, XVTR-bit гасится,
// антенна и Alex routing возвращаются к per-band дефолту.
procedure SetXvtrMode(AEnable, ADisablePA: Boolean; ARxAnt: Byte);
procedure SendFullHP;
procedure SetXvtrMode(AEnable, ADisablePA: Boolean; ARxAnt: Byte); override;
procedure SendFullHP; override;
// Отдельная отправка PTT: переключает реле (bit27) и drive ДО, потом PTT.
// Не меняет FIsTransmitting — вызывать после UpdateState+SendFullHP.
procedure SendPTT(Active: Boolean);
procedure SendDDCAudio(const LeftRight: array of SmallInt);
procedure SendPTT(Active: Boolean); override;
procedure SendDDCAudio(const LeftRight: array of SmallInt); override;
// Принимает RX-аудио (float [-1..1]) из DSP, конвертирует в 16-bit и
// отправляет на радио пакетами по 64 L/R пары. Хвост копится между вызовами.
procedure SendSpeakerAudio(const Left, Right: array of Single; Count: Integer);
procedure SendSpeakerAudio(const Left, Right: array of Single; Count: Integer); override;
// Единый переключатель встроенного динамика: True = слать аудио и снять мьют,
// False = замьютить динамик (byte 1400 bit1) и не накапливать поток.
// Применяется немедленно (шлёт HP-пакет, если подключены).
procedure SetSpeakerAudio(AEnabled: Boolean);
procedure SendDUCIQ(const IData, QData: array of Integer);
procedure ClearDUCIQQueue;
procedure SetSpeakerAudio(AEnabled: Boolean); override;
procedure SendDUCIQ(const IData, QData: array of Integer); override;
procedure ClearDUCIQQueue; override;
property Connected: Boolean read FConnected;
property Running: Boolean read FRunning;
property Device: THPSDRDevice read FDevice;
property LocalPort: Word read FLocalPort;
property LastError: string read FLastError;
property OnDeviceFound: TOnDeviceFound read FOnDeviceFound write FOnDeviceFound;
property OnDDCIQ: TOnDDCIQPacket read FOnDDCIQ write FOnDDCIQ;
property OnMicPacket: TOnMicPacket read FOnMic write FOnMic;
property OnHPStatus: TOnHPStatus read FOnHPStatus write FOnHPStatus;
property OnWideband: TOnWidebandFrame read FOnWideband write FOnWideband;
property DirectIP: string read FDirectIP write FDirectIP; // unicast discovery
// LocalPort — HPSDR-специфично (локальный UDP-порт), не часть базы.
property LocalPort: Word read FLocalPort;
end;
implementation
@@ -599,6 +584,60 @@ end;
// THPSDRNetwork
// ===========================================================================
// ---- Полиморфные геттеры состояния (TRadioBackend) ----
function THPSDRNetwork.GetConnected: Boolean;
begin
Result := FConnected;
end;
function THPSDRNetwork.GetRunning: Boolean;
begin
Result := FRunning;
end;
function THPSDRNetwork.GetDevice: TRadioDevice;
begin
Result := FDevice;
end;
function THPSDRNetwork.GetLastError: string;
begin
Result := FLastError;
end;
function THPSDRNetwork.GetDirectIP: string;
begin
Result := FDirectIP;
end;
procedure THPSDRNetwork.SetDirectIP(const V: string);
begin
FDirectIP := V;
end;
// ---- Возможности HPSDR-бэкенда ----
function THPSDRNetwork.Caps: TBackendCaps;
begin
FillChar(Result, SizeOf(Result), 0);
Result.Kind := bkHPSDR;
Result.HasTX := True;
Result.HasPA := True;
Result.HasAlex := True;
Result.HasWideband := True;
Result.HasDitherRandom := True;
Result.HasHWMic := True;
Result.HasPLLStatus := True;
Result.HasHWGain := False;
Result.HasRFBandwidth := False;
Result.HasFullDuplex := True;
Result.MinSampleRate := 48000;
Result.MaxSampleRate := 1536000;
Result.SampleRateMode := srmDiscrete;
Result.RatePresets := nil; // контроллер использует свой HPSDR-набор
Result.MinFreqHz := 0;
Result.MaxFreqHz := 61440000;
end;
constructor THPSDRNetwork.Create;
begin
inherited;