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
+227
View File
@@ -0,0 +1,227 @@
unit RadioBackend;
{
Абстракция бэкенда железа. Базовый класс TRadioBackend, который реализуют
конкретные транспорты:
- THPSDRNetwork (openHPSDR Ethernet protocol) — HPSDRNetwork.pas
- TPlutoBackend (libiio / AD936x) — PlutoBackend.pas (позже)
TRadioController держит FNetwork: TRadioBackend и работает только через этот
интерфейс. Возможности бэкенда описывает TBackendCaps (контроллер гейтит по
ним UI и поведение).
Здесь же — общая запись устройства (TRadioDevice) и типы колбэков данных,
чтобы оба бэкенда и контроллер ссылались на один источник правды.
TDDCIQPacket/THighPriorityStatus/TMicDataPacket берём из HPSDRProtocol —
это сетевой формат HPSDR, а Pluto-бэкенд синтезирует те же структуры, поэтому
WDSP-вход остаётся неизменным.
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, HPSDRProtocol, Settings;
type
TBackendKind = (bkHPSDR, bkPluto);
TSampleRateMode = (srmDiscrete, srmContinuous);
// Описание возможностей бэкенда. Контроллер гейтит UI/поведение по этим полям.
TBackendCaps = record
Kind: TBackendKind;
HasTX: Boolean; // есть передатчик
HasPA: Boolean; // fwd power / SWR / supply (HP status)
HasAlex: Boolean; // Alex фильтры / антенный коммутатор
HasWideband: Boolean; // wideband ADC дисплей
HasDitherRandom: Boolean; // dither / random на ADC
HasHWMic: Boolean; // mic over network / HW keyer / sidetone
HasPLLStatus: Boolean; // PLL lock в статусе
HasHWGain: Boolean; // manual gain / hw-AGC (Pluto)
HasRFBandwidth: Boolean; // настройка аналоговой RF-полосы (Pluto)
HasFullDuplex: Boolean; // одновременный RX+TX (Pluto / QO-100)
MinSampleRate: Integer;
MaxSampleRate: Integer;
SampleRateMode: TSampleRateMode;
RatePresets: array of Integer; // пресеты для SampleRateOverlay
MinFreqHz: Double;
MaxFreqHz: Double;
end;
// Общая запись найденного/подключённого устройства. Поля HPSDR (MAC/BoardType/
// NumDDCs…) и Pluto (URI/Serial) сосуществуют; актуальность по Kind.
TRadioDevice = record
Kind: TBackendKind;
IPAddress: string;
Port: Word;
MAC: array[0..5] of Byte; // HPSDR; для Pluto — synthetic из Serial
BoardType: Byte;
ProtocolVersion: Byte;
FirmwareVersion: Byte;
NumDDCs: Byte;
FreqOrPhase: Byte;
EndianModes: Byte;
URI: string; // Pluto: 'usb:1.5.5' / 'ip:192.168.2.1'
Serial: string; // Pluto serial (hw_serial)
Model: string; // Pluto hw_model (для отображения)
InUse: Boolean;
Valid: Boolean;
end;
PRadioDevice = ^TRadioDevice;
TRadioDeviceArray = array of TRadioDevice;
// Колбэки данных (вызываются из рабочих потоков бэкенда).
TOnDeviceFound = procedure(const Dev: TRadioDevice) 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;
{ TRadioBackend — абстрактный бэкенд железа }
TRadioBackend = class
protected
// Колбэки — общее хранилище для всех бэкендов (свойства ниже).
FOnDeviceFound: TOnDeviceFound;
FOnDDCIQ: TOnDDCIQPacket;
FOnMic: TOnMicPacket;
FOnHPStatus: TOnHPStatus;
FOnWideband: TOnWidebandFrame;
function GetConnected: Boolean; virtual; abstract;
function GetRunning: Boolean; virtual; abstract;
function GetDevice: TRadioDevice; virtual; abstract;
function GetLastError: string; virtual; abstract;
function GetDirectIP: string; virtual;
procedure SetDirectIP(const V: string); virtual;
public
// ---- Возможности ----
function Caps: TBackendCaps; virtual; abstract;
// ---- Жизненный цикл / discovery ----
function Discover(TimeoutMs: Integer = 3000): TRadioDeviceArray; virtual; abstract;
function Connect(const Dev: TRadioDevice): Boolean; virtual; abstract;
procedure Disconnect; virtual; abstract;
// ---- Конфигурация потока RX ----
procedure ConfigureDDCs(NumDDCs: Byte; SampleRate: Word; ADCSource: Byte = 0;
DitherEnabled: Boolean = True;
RandomEnabled: Boolean = True); virtual; abstract;
// ---- Частота / drive / TX-состояние ----
procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double;
DriveLevel: Byte = 100); virtual; abstract;
procedure UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte;
Transmitting, PAEnabled, AlexEnabled: Boolean); virtual; abstract;
procedure SendFullHP; virtual; abstract;
procedure SendPTT(Active: Boolean); virtual; abstract;
// ---- TX IQ ----
procedure SendDUCIQ(const IData, QData: array of Integer); virtual;
procedure ClearDUCIQQueue; virtual;
// ---- Опциональные (HPSDR-специфичные): no-op по умолчанию ----
procedure SendGeneralPacket(const Pkt: TGeneralPacket); virtual;
procedure ConfigureWideband(ADCIndex: Integer; Enabled: Boolean;
SamplesPerPacket: Integer = 512; SampleBits: Integer = 16;
UpdateRateMS: Integer = 70; PacketsPerFrame: Integer = 32); virtual;
procedure SendDDCSpecific(const Pkt: TDDCSpecificPacket); virtual;
procedure SendDUCSpecific(const Pkt: TDUCSpecificPacket); virtual;
procedure SendHighPriority(const Pkt: THighPriorityPacket); virtual;
procedure SetStepAtten(dB: Byte); virtual;
procedure SetAlexConfig(const A: TAlexSettings); virtual;
procedure SetXvtrMode(AEnable, ADisablePA: Boolean; ARxAnt: Byte); virtual;
procedure SendDDCAudio(const LeftRight: array of SmallInt); virtual;
procedure SendSpeakerAudio(const Left, Right: array of Single; Count: Integer); virtual;
procedure SetSpeakerAudio(AEnabled: Boolean); virtual;
// ---- Свойства состояния (полиморфные через виртуальные геттеры) ----
property Connected: Boolean read GetConnected;
property Running: Boolean read GetRunning;
property Device: TRadioDevice read GetDevice;
property LastError: string read GetLastError;
property DirectIP: string read GetDirectIP write SetDirectIP;
// ---- Колбэки данных ----
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;
end;
implementation
{ TRadioBackend — пустые реализации опциональных методов (no-op).
Конкретные бэкенды переопределяют то, что поддерживают. }
function TRadioBackend.GetDirectIP: string;
begin
Result := '';
end;
procedure TRadioBackend.SetDirectIP(const V: string);
begin
// no-op по умолчанию (unicast discovery — фича HPSDR)
end;
procedure TRadioBackend.SendDUCIQ(const IData, QData: array of Integer);
begin
end;
procedure TRadioBackend.ClearDUCIQQueue;
begin
end;
procedure TRadioBackend.SendGeneralPacket(const Pkt: TGeneralPacket);
begin
end;
procedure TRadioBackend.ConfigureWideband(ADCIndex: Integer; Enabled: Boolean;
SamplesPerPacket: Integer; SampleBits: Integer;
UpdateRateMS: Integer; PacketsPerFrame: Integer);
begin
end;
procedure TRadioBackend.SendDDCSpecific(const Pkt: TDDCSpecificPacket);
begin
end;
procedure TRadioBackend.SendDUCSpecific(const Pkt: TDUCSpecificPacket);
begin
end;
procedure TRadioBackend.SendHighPriority(const Pkt: THighPriorityPacket);
begin
end;
procedure TRadioBackend.SetStepAtten(dB: Byte);
begin
end;
procedure TRadioBackend.SetAlexConfig(const A: TAlexSettings);
begin
end;
procedure TRadioBackend.SetXvtrMode(AEnable, ADisablePA: Boolean; ARxAnt: Byte);
begin
end;
procedure TRadioBackend.SendDDCAudio(const LeftRight: array of SmallInt);
begin
end;
procedure TRadioBackend.SendSpeakerAudio(const Left, Right: array of Single; Count: Integer);
begin
end;
procedure TRadioBackend.SetSpeakerAudio(AEnabled: Boolean);
begin
end;
end.