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
+58 -15
View File
@@ -7,12 +7,46 @@ unit BoardUtils;
interface interface
function BoardTypeName(BoardType: Integer): string; function BoardTypeName(BoardType: Integer): string;
function FreqToBandIdx(Hz: Double): Integer; // Индекс диапазона по частоте. Pluto=True → VHF/UHF план, иначе HF (openHPSDR).
function FreqToBandIdx(Hz: Double; Pluto: Boolean = False): Integer;
// Короткое имя диапазона для кнопки (зависит от плана).
function BandName(Idx: Integer; Pluto: Boolean = False): string;
// Частота по умолчанию при входе в диапазон (если нет сохранённой/в диапазоне).
function BandDefaultFreq(Idx: Integer; Pluto: Boolean = False): Double;
implementation implementation
uses SysUtils; uses SysUtils;
const
BAND_COUNT = 11;
// ---- HF/6m план (openHPSDR) ----
HF_NAMES: array[0..BAND_COUNT-1] of string = (
'160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
HF_LO: array[0..BAND_COUNT-1] of Double = (
1800000, 3500000, 5330000, 7000000, 10100000,
14000000, 18068000, 21000000, 24890000, 28000000, 50000000);
HF_HI: array[0..BAND_COUNT-1] of Double = (
2000000, 4000000, 5410000, 7300000, 10150000,
14350000, 18168000, 21450000, 24990000, 29700000, 54000000);
HF_DEF: array[0..BAND_COUNT-1] of Double = (
1840000, 3700000, 5357000, 7100000, 10120000,
14200000, 18130000, 21300000, 24940000, 28400000, 50150000);
// ---- VHF/UHF/SHF план (Pluto / AD936x), 11 слотов ----
VU_NAMES: array[0..BAND_COUNT-1] of string = (
'6m','4m','2m','1.25m','70cm','23cm','13cm','9cm','FM','Air','ADS-B');
VU_LO: array[0..BAND_COUNT-1] of Double = (
50000000, 70000000, 144000000, 222000000, 430000000,
1240000000, 2300000000, 3300000000, 88000000, 118000000, 1087000000);
VU_HI: array[0..BAND_COUNT-1] of Double = (
54000000, 70500000, 148000000, 225000000, 440000000,
1300000000, 2450000000, 3500000000, 108000000, 137000000, 1093000000);
VU_DEF: array[0..BAND_COUNT-1] of Double = (
50150000, 70200000, 145000000, 223500000, 433500000,
1296200000, 2320000000, 3400100000, 100000000, 124000000, 1090000000);
function BoardTypeName(BoardType: Integer): string; function BoardTypeName(BoardType: Integer): string;
begin begin
case BoardType of case BoardType of
@@ -27,24 +61,33 @@ begin
end; end;
end; end;
function FreqToBandIdx(Hz: Double): Integer; function FreqToBandIdx(Hz: Double; Pluto: Boolean): Integer;
const
BAND_LO: array[0..10] of Double = (
1800000, 3500000, 5330000, 7000000, 10100000,
14000000, 18068000, 21000000, 24890000, 28000000, 50000000);
BAND_HI: array[0..10] of Double = (
2000000, 4000000, 5410000, 7300000, 10150000,
14350000, 18168000, 21450000, 24990000, 29700000, 54000000);
var var
i: Integer; i: Integer;
begin begin
Result := -1; Result := -1;
for i := 0 to 10 do if Pluto then
if (Hz >= BAND_LO[i]) and (Hz <= BAND_HI[i]) then begin
begin for i := 0 to BAND_COUNT - 1 do
Result := i; if (Hz >= VU_LO[i]) and (Hz <= VU_HI[i]) then Exit(i);
Exit; end
end; else
begin
for i := 0 to BAND_COUNT - 1 do
if (Hz >= HF_LO[i]) and (Hz <= HF_HI[i]) then Exit(i);
end;
end;
function BandName(Idx: Integer; Pluto: Boolean): string;
begin
if (Idx < 0) or (Idx >= BAND_COUNT) then Exit('');
if Pluto then Result := VU_NAMES[Idx] else Result := HF_NAMES[Idx];
end;
function BandDefaultFreq(Idx: Integer; Pluto: Boolean): Double;
begin
if (Idx < 0) or (Idx >= BAND_COUNT) then Exit(0);
if Pluto then Result := VU_DEF[Idx] else Result := HF_DEF[Idx];
end; end;
end. end.
+15 -2
View File
@@ -7,7 +7,7 @@ interface
uses uses
Classes, SysUtils, FlatButton, FlatEdit, FlatListBox, AppTheme, Forms, Controls, Graphics, Dialogs, Classes, SysUtils, FlatButton, FlatEdit, FlatListBox, AppTheme, Forms, Controls, Graphics, Dialogs,
StdCtrls, ExtCtrls, ComCtrls, StdCtrls, ExtCtrls, ComCtrls,
BoardUtils, PlatformUtils, DeviceStore; BoardUtils, PlatformUtils, DeviceStore, RadioBackend;
type type
// Результат диалога // Результат диалога
@@ -86,6 +86,9 @@ type
function GetAutoStartBoardType: Integer; function GetAutoStartBoardType: Integer;
function GetSavedBoardType(Idx: Integer): Integer; function GetSavedBoardType(Idx: Integer): Integer;
// Текст поля ручного ввода IP/URI (для probe сетевых Pluto при discovery).
function ManualIP: string;
// Получить/сбросить результат // Получить/сбросить результат
procedure ClearResult; procedure ClearResult;
// Общее хранилище устройств — назначается владельцем (MainForm/контроллер) // Общее хранилище устройств — назначается владельцем (MainForm/контроллер)
@@ -466,7 +469,12 @@ begin
Exit; Exit;
end; end;
D := FStore.Discovered(Idx); D := FStore.Discovered(Idx);
NewIdx := FStore.AddSaved(D.DisplayName, D.IPAddress, D.BoardType); // Pluto сохраняем с Kind/URI/Serial (иначе после перезапуска резолвится как
// HPSDR по голому IP и не подключается).
if D.Kind = bkPluto then
NewIdx := FStore.AddSavedPluto('PlutoSDR', D.URI, D.Serial)
else
NewIdx := FStore.AddSaved(D.DisplayName, D.IPAddress, D.BoardType);
RefreshSavedList; RefreshSavedList;
LstSaved.ItemIndex := NewIdx; LstSaved.ItemIndex := NewIdx;
end; end;
@@ -518,6 +526,11 @@ begin
ModalResult := mrCancel; ModalResult := mrCancel;
end; end;
function TDeviceDialog.ManualIP: string;
begin
if EdIP <> nil then Result := Trim(EdIP.Text) else Result := '';
end;
procedure TDeviceDialog.ClearResult; procedure TDeviceDialog.ClearResult;
begin begin
FResult.Accepted := False; FResult.Accepted := False;
+41 -3
View File
@@ -11,7 +11,7 @@ unit DeviceStore;
interface interface
uses uses
Classes, SysUtils, IniFiles, BoardUtils, PlatformUtils; Classes, SysUtils, IniFiles, BoardUtils, PlatformUtils, RadioBackend;
const const
DEVICE_CFG_NAME = 'hpsdr_devices.ini'; DEVICE_CFG_NAME = 'hpsdr_devices.ini';
@@ -23,6 +23,9 @@ type
IPAddress: string; IPAddress: string;
BoardType: Integer; BoardType: Integer;
AutoStart: Boolean; // запускать автоматически при старте AutoStart: Boolean; // запускать автоматически при старте
Kind: TBackendKind; // bkHPSDR / bkPluto
URI: string; // Pluto: 'ip:..' / 'usb:..'
Serial: string; // Pluto serial
end; end;
// Найденное устройство (результат discovery) // Найденное устройство (результат discovery)
@@ -31,6 +34,9 @@ type
DisplayName: string; DisplayName: string;
BoardType: Integer; BoardType: Integer;
MAC: array[0..5] of Byte; // нужен для preload-rate lookup (LoadDevice по MAC) 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; end;
{ TDeviceStore } { TDeviceStore }
@@ -50,6 +56,8 @@ type
function Saved(Idx: Integer): TSavedDevice; function Saved(Idx: Integer): TSavedDevice;
// Добавляет запись (Name пустое → 'HPSDR'); возвращает индекс новой записи. // Добавляет запись (Name пустое → 'HPSDR'); возвращает индекс новой записи.
function AddSaved(const AName, IP: string; BoardType: Integer): Integer; 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 RemoveSaved(Idx: Integer);
procedure SetAutoStart(Idx: Integer); // эксклюзивно: только одно autostart procedure SetAutoStart(Idx: Integer); // эксклюзивно: только одно autostart
function AutoStartIP: string; function AutoStartIP: string;
@@ -60,7 +68,9 @@ type
// ---- Найденные (discovery) ---- // ---- Найденные (discovery) ----
procedure ClearDiscovered; procedure ClearDiscovered;
procedure AddDiscovered(const IP, DisplayName: string; BoardType: Integer; 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 DiscoveredCount: Integer;
function Discovered(Idx: Integer): TDiscoveredDevice; function Discovered(Idx: Integer): TDiscoveredDevice;
// IP найденного устройства по IP (для preload-rate lookup и т.п.) // IP найденного устройства по IP (для preload-rate lookup и т.п.)
@@ -100,6 +110,9 @@ begin
FSaved[I].IPAddress := Ini.ReadString (Section, 'IP', ''); FSaved[I].IPAddress := Ini.ReadString (Section, 'IP', '');
FSaved[I].BoardType := Ini.ReadInteger(Section, 'BoardType', 0); FSaved[I].BoardType := Ini.ReadInteger(Section, 'BoardType', 0);
FSaved[I].AutoStart := Ini.ReadBool (Section, 'AutoStart', False); 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); Inc(FSavedCount);
end; end;
finally finally
@@ -123,6 +136,9 @@ begin
Ini.WriteString (Section, 'IP', FSaved[I].IPAddress); Ini.WriteString (Section, 'IP', FSaved[I].IPAddress);
Ini.WriteInteger(Section, 'BoardType', FSaved[I].BoardType); Ini.WriteInteger(Section, 'BoardType', FSaved[I].BoardType);
Ini.WriteBool (Section, 'AutoStart', FSaved[I].AutoStart); 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; end;
finally finally
Ini.Free; Ini.Free;
@@ -155,6 +171,25 @@ begin
FSaved[Result].IPAddress := Trim(IP); FSaved[Result].IPAddress := Trim(IP);
FSaved[Result].BoardType := BoardType; FSaved[Result].BoardType := BoardType;
FSaved[Result].AutoStart := False; 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; SaveSaved;
end; end;
@@ -233,7 +268,7 @@ begin
end; end;
procedure TDeviceStore.AddDiscovered(const IP, DisplayName: string; BoardType: Integer; 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 var
Idx, J: Integer; Idx, J: Integer;
begin begin
@@ -243,6 +278,9 @@ begin
FDiscovered[Idx].IPAddress := IP; FDiscovered[Idx].IPAddress := IP;
FDiscovered[Idx].DisplayName := DisplayName; FDiscovered[Idx].DisplayName := DisplayName;
FDiscovered[Idx].BoardType := BoardType; 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); FillChar(FDiscovered[Idx].MAC, SizeOf(FDiscovered[Idx].MAC), 0);
for J := 0 to High(MAC) do for J := 0 to High(MAC) do
if J <= 5 then FDiscovered[Idx].MAC[J] := MAC[J]; if J <= 5 then FDiscovered[Idx].MAC[J] := MAC[J];
+103 -64
View File
@@ -19,7 +19,7 @@ uses
{$ELSE} {$ELSE}
Sockets, BaseUnix, Sockets, BaseUnix,
{$ENDIF} {$ENDIF}
HPSDRProtocol, SyncObjs, Settings; HPSDRProtocol, SyncObjs, Settings, RadioBackend;
const const
DUC_TX_QUEUE_SIZE = 256; // power of two, bounded latency on TX underrun/overrun DUC_TX_QUEUE_SIZE = 256; // power of two, bounded latency on TX underrun/overrun
@@ -70,33 +70,19 @@ const
type type
THPSDRDevice = record // Запись устройства и типы колбэков теперь общие (RadioBackend) — здесь
IPAddress: string; // алиасы для обратной совместимости со старым кодом (THPSDRDevice и т.д.).
Port: Word; THPSDRDevice = RadioBackend.TRadioDevice;
MAC: array[0..5] of Byte; PHPSDRDevice = RadioBackend.PRadioDevice;
BoardType: Byte; THPSDRDeviceArray = RadioBackend.TRadioDeviceArray;
ProtocolVersion: Byte; TOnDeviceFound = RadioBackend.TOnDeviceFound;
FirmwareVersion: Byte; TOnDDCIQPacket = RadioBackend.TOnDDCIQPacket;
NumDDCs: Byte; TOnMicPacket = RadioBackend.TOnMicPacket;
FreqOrPhase: Byte; TOnHPStatus = RadioBackend.TOnHPStatus;
EndianModes: Byte; TOnWidebandFrame = RadioBackend.TOnWidebandFrame;
InUse: Boolean;
Valid: Boolean;
end;
PHPSDRDevice = ^THPSDRDevice;
THPSDRDeviceArray = array of THPSDRDevice;
TOnDeviceFound = procedure(const Dev: THPSDRDevice) of object; { THPSDRNetwork — openHPSDR Ethernet protocol бэкенд }
TOnDDCIQPacket = procedure(DDCIndex: Integer; THPSDRNetwork = class(TRadioBackend)
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
private private
FSocket: TSocket; FSocket: TSocket;
FLocalPort: Word; FLocalPort: Word;
@@ -125,11 +111,8 @@ type
FKeepaliveThread: TThread; FKeepaliveThread: TThread;
FDUCIQThread: TThread; FDUCIQThread: TThread;
FOnDeviceFound: TOnDeviceFound; // Колбэки данных (FOnDeviceFound/FOnDDCIQ/FOnMic/FOnHPStatus/FOnWideband)
FOnDDCIQ: TOnDDCIQPacket; // унаследованы из TRadioBackend (protected) — здесь не дублируем.
FOnMic: TOnMicPacket;
FOnHPStatus: TOnHPStatus;
FOnWideband: TOnWidebandFrame;
FPortDDCSpec: Word; FPortDDCSpec: Word;
FPortDUCSpec: Word; FPortDUCSpec: Word;
@@ -210,59 +193,61 @@ type
function NextSeq(var S: LongWord): LongWord; function NextSeq(var S: LongWord): LongWord;
procedure PackSeqBytes(var B: array of Byte; Seq: 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 public
constructor Create; constructor Create;
destructor Destroy; override; destructor Destroy; override;
function Discover(TimeoutMs: Integer = 3000): THPSDRDeviceArray; function Caps: TBackendCaps; override;
function Connect(const Dev: THPSDRDevice): Boolean;
procedure Disconnect;
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; procedure ConfigureWideband(ADCIndex: Integer; Enabled: Boolean;
SamplesPerPacket: Integer = 512; SampleBits: Integer = 16; SamplesPerPacket: Integer = 512; SampleBits: Integer = 16;
UpdateRateMS: Integer = 70; PacketsPerFrame: Integer = 32); UpdateRateMS: Integer = 70; PacketsPerFrame: Integer = 32); override;
procedure SendDDCSpecific(const Pkt: TDDCSpecificPacket); procedure SendDDCSpecific(const Pkt: TDDCSpecificPacket); override;
procedure ConfigureDDCs(NumDDCs: Byte; SampleRate: Word; ADCSource: Byte = 0; procedure ConfigureDDCs(NumDDCs: Byte; SampleRate: Word; ADCSource: Byte = 0;
DitherEnabled: Boolean = True; RandomEnabled: Boolean = True); DitherEnabled: Boolean = True; RandomEnabled: Boolean = True); override;
procedure SendDUCSpecific(const Pkt: TDUCSpecificPacket); procedure SendDUCSpecific(const Pkt: TDUCSpecificPacket); override;
procedure SendHighPriority(const Pkt: THighPriorityPacket); procedure SendHighPriority(const Pkt: THighPriorityPacket); override;
procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double; procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double;
DriveLevel: Byte = 100); DriveLevel: Byte = 100); override;
procedure UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte; procedure UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte;
Transmitting, PAEnabled, AlexEnabled: Boolean); Transmitting, PAEnabled, AlexEnabled: Boolean); override;
procedure SetStepAtten(dB: Byte); procedure SetStepAtten(dB: Byte); override;
procedure SetAlexConfig(const A: TAlexSettings); procedure SetAlexConfig(const A: TAlexSettings); override;
// Включение/настройка XVTR-режима. Все параметры применяются мгновенно // Включение/настройка XVTR-режима. Все параметры применяются мгновенно
// (отправляется HP-пакет). Когда AEnable=False, XVTR-bit гасится, // (отправляется HP-пакет). Когда AEnable=False, XVTR-bit гасится,
// антенна и Alex routing возвращаются к per-band дефолту. // антенна и Alex routing возвращаются к per-band дефолту.
procedure SetXvtrMode(AEnable, ADisablePA: Boolean; ARxAnt: Byte); procedure SetXvtrMode(AEnable, ADisablePA: Boolean; ARxAnt: Byte); override;
procedure SendFullHP; procedure SendFullHP; override;
// Отдельная отправка PTT: переключает реле (bit27) и drive ДО, потом PTT. // Отдельная отправка PTT: переключает реле (bit27) и drive ДО, потом PTT.
// Не меняет FIsTransmitting — вызывать после UpdateState+SendFullHP. // Не меняет FIsTransmitting — вызывать после UpdateState+SendFullHP.
procedure SendPTT(Active: Boolean); procedure SendPTT(Active: Boolean); override;
procedure SendDDCAudio(const LeftRight: array of SmallInt); procedure SendDDCAudio(const LeftRight: array of SmallInt); override;
// Принимает RX-аудио (float [-1..1]) из DSP, конвертирует в 16-bit и // Принимает RX-аудио (float [-1..1]) из DSP, конвертирует в 16-bit и
// отправляет на радио пакетами по 64 L/R пары. Хвост копится между вызовами. // отправляет на радио пакетами по 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 = слать аудио и снять мьют, // Единый переключатель встроенного динамика: True = слать аудио и снять мьют,
// False = замьютить динамик (byte 1400 bit1) и не накапливать поток. // False = замьютить динамик (byte 1400 bit1) и не накапливать поток.
// Применяется немедленно (шлёт HP-пакет, если подключены). // Применяется немедленно (шлёт HP-пакет, если подключены).
procedure SetSpeakerAudio(AEnabled: Boolean); procedure SetSpeakerAudio(AEnabled: Boolean); override;
procedure SendDUCIQ(const IData, QData: array of Integer); procedure SendDUCIQ(const IData, QData: array of Integer); override;
procedure ClearDUCIQQueue; procedure ClearDUCIQQueue; override;
property Connected: Boolean read FConnected; // LocalPort — HPSDR-специфично (локальный UDP-порт), не часть базы.
property Running: Boolean read FRunning; property LocalPort: Word read FLocalPort;
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
end; end;
implementation implementation
@@ -599,6 +584,60 @@ end;
// THPSDRNetwork // 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; constructor THPSDRNetwork.Create;
begin begin
inherited; inherited;
+199
View File
@@ -0,0 +1,199 @@
unit IIOBindings;
{
Минимальный биндинг к libiio (классический API 0.x; в системе 0.26) с
ДИНАМИЧЕСКОЙ загрузкой. Если libiio нет IIOLoad возвращает False, и
Pluto-бэкенд просто не участвует в discovery (приложение работает с HPSDR).
Загружаем libiio.so / libiio.so.0 (Linux), iio.dll (Windows),
libiio.dylib (macOS). Имена символов одинаковые на всех платформах.
Покрыты функции для: scan (discovery), context create/destroy, поиск
device/channel, чтение/запись атрибутов, буферы RX/TX. Этого набора хватает
на фазы 14 (discovery RX управление TX).
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
dynlibs, ctypes, SysUtils;
type
// Непрозрачные указатели libiio
Piio_scan_context = Pointer;
Piio_context_info = Pointer;
PPiio_context_info = ^Piio_context_info; // struct iio_context_info **
Piio_context = Pointer;
Piio_device = Pointer;
Piio_channel = Pointer;
Piio_buffer = Pointer;
var
// ---- Scan / discovery ----
iio_create_scan_context: function(backend: PAnsiChar; flags: cuint): Piio_scan_context; cdecl;
iio_scan_context_destroy: procedure(ctx: Piio_scan_context); cdecl;
iio_scan_context_get_info_list: function(ctx: Piio_scan_context;
var info: PPiio_context_info): ptrint; cdecl;
iio_context_info_list_free: procedure(info: PPiio_context_info); cdecl;
iio_context_info_get_description: function(info: Piio_context_info): PAnsiChar; cdecl;
iio_context_info_get_uri: function(info: Piio_context_info): PAnsiChar; cdecl;
// ---- Context ----
iio_create_context_from_uri: function(uri: PAnsiChar): Piio_context; cdecl;
iio_context_destroy: procedure(ctx: Piio_context); cdecl;
iio_context_get_name: function(ctx: Piio_context): PAnsiChar; cdecl;
iio_context_find_device: function(ctx: Piio_context; name: PAnsiChar): Piio_device; cdecl;
iio_context_get_attr_value: function(ctx: Piio_context; name: PAnsiChar): PAnsiChar; cdecl;
// ---- Device / channel ----
// output — C '_Bool' (1 байт). Передаём явный байт 0/1 (ByteBool/True давал
// некорректное значение → output-каналы не находились).
iio_device_find_channel: function(dev: Piio_device; name: PAnsiChar;
output: cuchar): Piio_channel; cdecl;
iio_device_set_kernel_buffers_count: function(dev: Piio_device; nb: cuint): cint; cdecl;
iio_channel_enable: procedure(chn: Piio_channel); cdecl;
iio_channel_disable: procedure(chn: Piio_channel); cdecl;
// ---- Атрибуты ----
iio_channel_attr_read_longlong: function(chn: Piio_channel; attr: PAnsiChar;
out val: clonglong): cint; cdecl;
iio_channel_attr_write_longlong: function(chn: Piio_channel; attr: PAnsiChar;
val: clonglong): cint; cdecl;
iio_channel_attr_write: function(chn: Piio_channel; attr: PAnsiChar;
src: PAnsiChar): ptrint; cdecl;
iio_channel_attr_write_bool: function(chn: Piio_channel; attr: PAnsiChar;
val: ByteBool): cint; cdecl;
iio_channel_attr_write_double: function(chn: Piio_channel; attr: PAnsiChar;
val: cdouble): cint; cdecl;
// ---- Буферы (RX refill / TX push) ----
iio_device_create_buffer: function(dev: Piio_device; samples_count: csize_t;
cyclic: ByteBool): Piio_buffer; cdecl;
iio_buffer_destroy: procedure(buf: Piio_buffer); cdecl;
iio_buffer_cancel: procedure(buf: Piio_buffer); cdecl; // разблокирует refill/push
iio_buffer_refill: function(buf: Piio_buffer): ptrint; cdecl;
iio_buffer_push: function(buf: Piio_buffer): ptrint; cdecl;
iio_buffer_first: function(buf: Piio_buffer; chn: Piio_channel): Pointer; cdecl;
iio_buffer_step: function(buf: Piio_buffer): ptrint; cdecl;
iio_buffer_end: function(buf: Piio_buffer): Pointer; cdecl;
// ---- Утилиты ----
iio_strerror: procedure(err: cint; dst: PAnsiChar; len: csize_t); cdecl;
// Загружает libiio (один раз). True — готово к использованию. Идемпотентно.
function IIOLoad: Boolean;
// True, если libiio загружена и символы разрешены.
function IIOLoaded: Boolean;
// Расшифровка кода ошибки libiio (err — отрицательный errno).
function IIOStrError(err: cint): string;
implementation
var
GLib: TLibHandle = NilHandle;
GLoaded: Boolean = False;
const
{$IFDEF WINDOWS}
IIO_LIBS: array[0..1] of string = ('libiio.dll', 'iio.dll');
{$ELSE}
{$IFDEF DARWIN}
IIO_LIBS: array[0..1] of string = ('libiio.dylib', 'libiio.1.dylib');
{$ELSE}
IIO_LIBS: array[0..1] of string = ('libiio.so', 'libiio.so.0');
{$ENDIF}
{$ENDIF}
function Sym(const Name: string): Pointer;
begin
Result := GetProcedureAddress(GLib, Name);
if Result = nil then
raise Exception.CreateFmt('libiio: symbol "%s" not found', [Name]);
end;
function IIOLoaded: Boolean;
begin
Result := GLoaded;
end;
function IIOLoad: Boolean;
var
I: Integer;
begin
if GLoaded then Exit(True);
if GLib = NilHandle then
for I := 0 to High(IIO_LIBS) do
begin
GLib := LoadLibrary(IIO_LIBS[I]);
if GLib <> NilHandle then Break;
end;
if GLib = NilHandle then Exit(False);
try
iio_create_scan_context := Sym('iio_create_scan_context');
iio_scan_context_destroy := Sym('iio_scan_context_destroy');
iio_scan_context_get_info_list := Sym('iio_scan_context_get_info_list');
iio_context_info_list_free := Sym('iio_context_info_list_free');
iio_context_info_get_description := Sym('iio_context_info_get_description');
iio_context_info_get_uri := Sym('iio_context_info_get_uri');
iio_create_context_from_uri := Sym('iio_create_context_from_uri');
iio_context_destroy := Sym('iio_context_destroy');
iio_context_get_name := Sym('iio_context_get_name');
iio_context_find_device := Sym('iio_context_find_device');
iio_context_get_attr_value := Sym('iio_context_get_attr_value');
iio_device_find_channel := Sym('iio_device_find_channel');
iio_device_set_kernel_buffers_count := Sym('iio_device_set_kernel_buffers_count');
iio_channel_enable := Sym('iio_channel_enable');
iio_channel_disable := Sym('iio_channel_disable');
iio_channel_attr_read_longlong := Sym('iio_channel_attr_read_longlong');
iio_channel_attr_write_longlong := Sym('iio_channel_attr_write_longlong');
iio_channel_attr_write := Sym('iio_channel_attr_write');
iio_channel_attr_write_bool := Sym('iio_channel_attr_write_bool');
iio_channel_attr_write_double := Sym('iio_channel_attr_write_double');
iio_device_create_buffer := Sym('iio_device_create_buffer');
iio_buffer_destroy := Sym('iio_buffer_destroy');
iio_buffer_cancel := Sym('iio_buffer_cancel');
iio_buffer_refill := Sym('iio_buffer_refill');
iio_buffer_push := Sym('iio_buffer_push');
iio_buffer_first := Sym('iio_buffer_first');
iio_buffer_step := Sym('iio_buffer_step');
iio_buffer_end := Sym('iio_buffer_end');
iio_strerror := Sym('iio_strerror');
GLoaded := True;
except
on E: Exception do
begin
UnloadLibrary(GLib);
GLib := NilHandle;
GLoaded := False;
end;
end;
Result := GLoaded;
end;
function IIOStrError(err: cint): string;
var
Buf: array[0..255] of AnsiChar;
begin
if GLoaded and Assigned(iio_strerror) then
begin
FillChar(Buf, SizeOf(Buf), 0);
iio_strerror(Abs(err), @Buf[0], SizeOf(Buf));
Result := string(PAnsiChar(@Buf[0]));
end
else
Result := Format('iio error %d', [err]);
end;
finalization
if GLib <> NilHandle then
begin
UnloadLibrary(GLib);
GLib := NilHandle;
GLoaded := False;
end;
end.
+130 -10
View File
@@ -42,6 +42,7 @@ uses
ChannelStore, ChannelsForm, ChannelStore, ChannelsForm,
PowerInhibit, PowerInhibit,
DeviceStore, DeviceStore,
RadioBackend, PlutoBackend,
RadioController, ChannelController; RadioController, ChannelController;
const const
@@ -324,6 +325,9 @@ type
procedure OnWidebandCB(ADCIndex: Integer; const Samples: array of SmallInt; procedure OnWidebandCB(ADCIndex: Integer; const Samples: array of SmallInt;
Count: Integer); Count: Integer);
procedure OnWDSPOpenDone(Success: Boolean); procedure OnWDSPOpenDone(Success: Boolean);
function ResolveDevice(const IP: string): THPSDRDevice;
procedure RelabelBands; // подписи band-кнопок под план бэкенда
procedure ApplyBackendCapsToUI; // sample-rate пресеты + гейтинг полей по Caps
procedure DoConnectDevice(const Dev: THPSDRDevice); procedure DoConnectDevice(const Dev: THPSDRDevice);
// Старт с уже выбранным устройством (preload rate + WDSP open + connect). // Старт с уже выбранным устройством (preload rate + WDSP open + connect).
// Переиспользуется десктоп-диалогом и web-оверлеем — без модального диалога. // Переиспользуется десктоп-диалогом и web-оверлеем — без модального диалога.
@@ -1313,6 +1317,8 @@ begin
// возвращаем живое значение в persist-буфер, чтобы blob нёс актуальную тему. // возвращаем живое значение в persist-буфер, чтобы blob нёс актуальную тему.
FController.FLoadedGlobal.LightTheme := FLightTheme; FController.FLoadedGlobal.LightTheme := FLightTheme;
FCATAdapter.ApplySettings(FController.FLoadedGlobal); FCATAdapter.ApplySettings(FController.FLoadedGlobal);
RelabelBands; // HF ↔ VHF/UHF подписи под тип бэкенда
ApplyBackendCapsToUI; // пресеты sample rate + гейтинг полей
RebuildXvtrButtons; RebuildXvtrButtons;
PushXvtrToWeb; PushXvtrToWeb;
TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive% TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive%
@@ -2664,10 +2670,12 @@ const
MARGIN_Y = 4; MARGIN_Y = 4;
begin begin
if (FSampleRateOverlay = nil) or (PbSpectrum = nil) then Exit; if (FSampleRateOverlay = nil) or (PbSpectrum = nil) then Exit;
// Ширина = доступная ширина спектра справа от MARGIN_X (контент сам клампится
// к этому пределу). Раньше фиксировалось 320px → 8 Pluto-кнопок не влезали.
FSampleRateOverlay.SetBounds( FSampleRateOverlay.SetBounds(
MARGIN_X, MARGIN_X,
MARGIN_Y, MARGIN_Y,
FSampleRateOverlay.Width, Max(320, PbSpectrum.Width - MARGIN_X * 2),
FSampleRateOverlay.Height); FSampleRateOverlay.Height);
end; end;
@@ -3296,7 +3304,40 @@ end;
procedure TMainForm.BtnDiscoverFromDialog(Sender: TObject); procedure TMainForm.BtnDiscoverFromDialog(Sender: TObject);
// Запускается когда пользователь нажимает DISCOVER внутри диалога. // Запускается когда пользователь нажимает DISCOVER внутри диалога.
// Контроллер чистит список и стартует поток; UI слушает rfDeviceList. // Контроллер чистит список и стартует поток; UI слушает rfDeviceList.
// Сетевые Pluto (без mDNS) не находятся scan'ом — собираем список URI для
// прямого probe: сохранённые Pluto + ручной ввод IP в поле диалога.
var
URIs: array of string;
i: Integer;
ip: string;
procedure AddURI(const U: string);
var k: Integer;
begin
if U = '' then Exit;
for k := 0 to High(URIs) do if SameText(URIs[k], U) then Exit;
SetLength(URIs, Length(URIs) + 1);
URIs[High(URIs)] := U;
end;
begin begin
SetLength(URIs, 0);
for i := 0 to FController.FDeviceStore.SavedCount - 1 do
if FController.FDeviceStore.Saved(i).Kind = bkPluto then
AddURI(FController.FDeviceStore.Saved(i).URI);
if Assigned(FDeviceDialog) then
begin
ip := FDeviceDialog.ManualIP;
if ip <> '' then
begin
if (Pos('ip:', ip) = 1) or (Pos('usb:', ip) = 1) or (Pos('local:', ip) = 1) then
AddURI(ip)
else
AddURI('ip:' + ip);
end;
end;
FController.SetPlutoProbeURIs(URIs);
SetStatusText(2, 'Discovering...'); SetStatusText(2, 'Discovering...');
FController.Discover; FController.Discover;
end; end;
@@ -3352,13 +3393,52 @@ begin
end; end;
end; end;
// Создаём запись устройства из IP + BoardType (из диалога/autostart) и стартуем. // Создаём запись устройства. Если это известный (discovered/saved) Pluto —
FillChar(Dev, SizeOf(Dev), 0); // восстанавливаем Kind/URI/Serial/MAC из store по IP; иначе HPSDR по IP.
Dev.IPAddress := FPendingIP; Dev := ResolveDevice(FPendingIP);
Dev.BoardType := FPendingBoardType; if Dev.Kind = bkHPSDR then Dev.BoardType := FPendingBoardType;
StartWithDevice(Dev); StartWithDevice(Dev);
end; end;
function TMainForm.ResolveDevice(const IP: string): THPSDRDevice;
// По IP/URI ищет устройство среди discovered, затем saved, чтобы восстановить
// Kind/URI/Serial/MAC (нужно для Pluto). Если не найдено — простой HPSDR по IP.
var
i: Integer;
dd: TDiscoveredDevice;
sd: TSavedDevice;
begin
// Result — managed-поля (строки) при входе nil, FillChar безопасен.
FillChar(Result, SizeOf(Result), 0);
Result.Kind := bkHPSDR;
Result.IPAddress := IP;
i := FController.FDeviceStore.FindDiscoveredByIP(IP);
if i >= 0 then
begin
dd := FController.FDeviceStore.Discovered(i);
Result.Kind := dd.Kind;
Result.URI := dd.URI;
Result.Serial := dd.Serial;
Result.BoardType := dd.BoardType;
Move(dd.MAC[0], Result.MAC[0], 6);
Exit;
end;
for i := 0 to FController.FDeviceStore.SavedCount - 1 do
begin
sd := FController.FDeviceStore.Saved(i);
if SameText(sd.IPAddress, IP) or SameText(sd.URI, IP) then
begin
Result.Kind := sd.Kind;
Result.URI := sd.URI;
Result.Serial := sd.Serial;
Result.BoardType := sd.BoardType;
Exit;
end;
end;
end;
procedure TMainForm.DoStopAndDisconnect; procedure TMainForm.DoStopAndDisconnect;
// STOP-teardown, общий для десктоп-STOP и web-disconnect. Движок (graceful Run=0, // STOP-teardown, общий для десктоп-STOP и web-disconnect. Движок (graceful Run=0,
// сброс TX/телеметрии) + персист настроек + Disconnect — в контроллере // сброс TX/телеметрии) + персист настроек + Disconnect — в контроллере
@@ -3435,6 +3515,33 @@ begin
DoConnectDevice(FPendingDev); DoConnectDevice(FPendingDev);
end; end;
procedure TMainForm.RelabelBands;
// Переписывает подписи band-кнопок под план активного бэкенда (HF ↔ VHF/UHF).
var i: Integer; pl: Boolean;
begin
pl := FController.IsPluto;
for i := 0 to BAND_COUNT - 1 do
if BtnBand[i] <> nil then
BtnBand[i].Caption := BandName(i, pl);
end;
procedure TMainForm.ApplyBackendCapsToUI;
// Пресеты sample-rate оверлея из Caps (Pluto >560k) + (TODO) гейтинг полей,
// которых нет у бэкенда (PA/SWR/Alex/Wideband).
var
Caps: TBackendCaps;
begin
Caps := FController.BackendCaps;
if Assigned(FSampleRateOverlay) then
begin
if (Caps.SampleRateMode = srmContinuous) and (Length(Caps.RatePresets) > 0) then
FSampleRateOverlay.SetRatePresets(Caps.RatePresets)
else
FSampleRateOverlay.SetRatePresetsDefault; // HPSDR 48k..1536k
FSampleRateOverlay.SetCurrentRate(FController.FSampleRate);
end;
end;
procedure TMainForm.DoConnectDevice(const Dev: THPSDRDevice); procedure TMainForm.DoConnectDevice(const Dev: THPSDRDevice);
begin begin
BtnStartStop.Enabled := True; BtnStartStop.Enabled := True;
@@ -3798,7 +3905,7 @@ begin
if FController.FCurrentXvtr < 0 then if FController.FCurrentXvtr < 0 then
begin begin
BandIdx := FreqToBandIdx(ClickFreq); BandIdx := FreqToBandIdx(ClickFreq, FController.IsPluto);
if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then
begin begin
if FController.FDevConnected then if FController.FDevConnected then
@@ -3834,7 +3941,7 @@ begin
end; end;
if FController.FCurrentXvtr < 0 then if FController.FCurrentXvtr < 0 then
begin begin
BandIdx := FreqToBandIdx(FController.FVfoB); BandIdx := FreqToBandIdx(FController.FVfoB, FController.IsPluto);
if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then
begin begin
if FController.FCurrentBand >= 0 then if FController.FCurrentBand >= 0 then
@@ -5114,10 +5221,14 @@ begin
NewFreq := Base; // снэп к нижней границе NewFreq := Base; // снэп к нижней границе
end; end;
// В XVTR-режиме HF-клэмп не применяем: ApplyVfoA сам ограничит по FreqBegin/FreqEnd // Клэмп диапазона перестройки. XVTR — ApplyVfoA сам ограничит по FreqBegin/End.
// Pluto — по диапазону бэкенда (Caps). Иначе HF (30 кГц..60 МГц).
if (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) and if (FController.FCurrentXvtr >= 0) and (FController.FCurrentXvtr < CFG_XVTR_COUNT) and
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].Enabled then FController.FXvtrSettings.Entries[FController.FCurrentXvtr].Enabled then
NewFreq := Max(0, NewFreq) NewFreq := Max(0, NewFreq)
else if FController.IsPluto then
NewFreq := Max(Round(FController.BackendCaps.MinFreqHz),
Min(Round(FController.BackendCaps.MaxFreqHz), NewFreq))
else else
NewFreq := Max(30000, Min(60000000, NewFreq)); NewFreq := Max(30000, Min(60000000, NewFreq));
if FController.FActiveVfo = 0 then if FController.FActiveVfo = 0 then
@@ -5329,8 +5440,17 @@ begin
FController.FLoadedGlobal.FreqMhzDigits := Digits; // зеркало в persist-буфер контроллера FController.FLoadedGlobal.FreqMhzDigits := Digits; // зеркало в persist-буфер контроллера
FreqDispA.MinMhzDigits := Digits; FreqDispA.MinMhzDigits := Digits;
FreqDispB.MinMhzDigits := Digits; FreqDispB.MinMhzDigits := Digits;
FreqDispA.MaxFreq := MaxFreqs[Digits]; // Pluto/VHF/UHF: верхняя граница из Caps (до 6 ГГц), иначе HF по числу цифр.
FreqDispB.MaxFreq := MaxFreqs[Digits]; if FController.IsPluto then
begin
FreqDispA.MaxFreq := Round(FController.BackendCaps.MaxFreqHz);
FreqDispB.MaxFreq := Round(FController.BackendCaps.MaxFreqHz);
end
else
begin
FreqDispA.MaxFreq := MaxFreqs[Digits];
FreqDispB.MaxFreq := MaxFreqs[Digits];
end;
if Assigned(FWebServer) then if Assigned(FWebServer) then
FWebServer.FreqMhzDigits := Digits; FWebServer.FreqMhzDigits := Digits;
LayoutTopVfoBlock; LayoutTopVfoBlock;
+555
View File
@@ -0,0 +1,555 @@
unit PlutoBackend;
{
Бэкенд ADALM-PLUTO (libiio / AD936x), реализует TRadioBackend.
Фаза 1 (текущая): discovery через iio_scan_context + Caps + открытие/закрытие
контекста (Connect/Disconnect) + геттеры состояния. RX/TX-потоки и реальная
подача IQ фазы 2/4 (методы-заглушки сохраняют состояние).
Карта устройств/каналов Pluto (для фаз 24):
ad9361-phy:
RX LO channel 'altvoltage0' (output), attr 'frequency'
TX LO channel 'altvoltage1' (output), attr 'frequency'
RX rate/bw— channel 'voltage0' (input), attr 'sampling_frequency','rf_bandwidth'
RX gain channel 'voltage0' (input), attr 'hardwaregain','gain_control_mode'
TX atten channel 'voltage0' (output), attr 'hardwaregain' (отриц. дБ)
cf-ad9361-lpc RX-стрим: 'voltage0'(I),'voltage1'(Q) input
cf-ad9361-dds-core-lpc TX-стрим: 'voltage0'(I),'voltage1'(Q) output
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, ctypes,
RadioBackend, IIOBindings, HPSDRProtocol, Settings;
type
{ TPlutoBackend }
TPlutoBackend = class(TRadioBackend)
private
FCtx: Piio_context;
FPhy: Piio_device; // ad9361-phy (control)
FRxDev: Piio_device; // cf-ad9361-lpc (RX stream)
FTxDev: Piio_device; // cf-ad9361-dds-core-lpc (TX stream)
FConnected: Boolean;
FRunning: Boolean;
FDevice: TRadioDevice;
FLastError: string;
// Текущее состояние (применяется к железу в фазах 2/4)
FRXFreq: Double;
FTXFreq: Double;
FSampleRate: Integer;
FTransmitting: Boolean;
FGainMode: string; // gain_control_mode (manual/slow_attack/...)
FGainDb: Integer; // hardwaregain при ручном режиме
// RX-стрим (cf-ad9361-lpc): I/Q-каналы, буфер, поток refill.
FRxBuf: Piio_buffer;
FRxChI: Piio_channel;
FRxChQ: Piio_channel;
FRxThread: TThread;
FStreaming: Boolean;
FRxSeq: LongWord;
// Низкоуровневые помощники применения атрибутов к ad9361-phy
procedure ApplyRxLO(Hz: Double);
procedure ApplyTxLO(Hz: Double);
procedure ApplySampleRate(Hz: Integer);
procedure ApplyGain;
procedure StartRX;
procedure StopRX;
protected
function GetConnected: Boolean; override;
function GetRunning: Boolean; override;
function GetDevice: TRadioDevice; override;
function GetLastError: string; override;
public
constructor Create;
destructor Destroy; override;
function Caps: TBackendCaps; override;
function Discover(TimeoutMs: Integer = 3000): TRadioDeviceArray; override;
function Connect(const Dev: TRadioDevice): Boolean; override;
procedure Disconnect; override;
// Прямой probe конкретного URI (например 'ip:172.16.2.199' или 'usb:1.5.5').
// Открывает контекст, проверяет наличие ad9361-phy, читает hw_serial/hw_model,
// заполняет Dev и (если задан) дёргает OnDeviceFound. True — это AD936x-SDR.
// Нужен для сетевых устройств (без mDNS scan их иначе не найти).
function ProbeURI(const AURI: string; out Dev: TRadioDevice): Boolean;
procedure ConfigureDDCs(NumDDCs: Byte; SampleRate: Word; ADCSource: Byte = 0;
DitherEnabled: Boolean = True;
RandomEnabled: Boolean = True); override;
procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double;
DriveLevel: Byte = 100); override;
procedure UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte;
Transmitting, PAEnabled, AlexEnabled: Boolean); override;
procedure SendFullHP; override;
procedure SendPTT(Active: Boolean); override;
end;
// Стабильный 6-байтовый ключ (synthetic MAC) из serial/URI — для per-device
// настроек (Settings ключует по MAC). FNV-1a свёртка строки в 6 байт.
function PlutoSyntheticMAC(const S: string): TBytes;
implementation
const
PLUTO_MIN_SR = 521000; // нижний предел AD936x sampling_frequency
PLUTO_MAX_SR = 61440000;
PLUTO_MIN_HZ = 46875000; // AD9361 нативно (LibreSDR/AntSDR); AD9363 — 325 МГц
PLUTO_MAX_HZ = 6000000000.0;
PLUTO_DEF_SR = 1536000; // дефолтный rate если контроллер задал невалидный
RX_BUF_SAMPLES = 8192; // IQ-пар за один refill (~5.3 мс @ 1.536 MS/s)
PAIRS_PER_PKT = 238; // как DDC IQ HPSDR (238*6 = 1428 байт IQData)
type
// RX-поток: блокирующий refill libiio → конверсия int16→24bit BE → OnDDCIQ.
// Работает с private-полями TPlutoBackend (одна единица компиляции).
TPlutoRXThread = class(TThread)
private
FBk: TPlutoBackend;
protected
procedure Execute; override;
public
constructor Create(ABk: TPlutoBackend);
end;
constructor TPlutoRXThread.Create(ABk: TPlutoBackend);
begin
FBk := ABk;
FreeOnTerminate := False;
inherited Create(False);
end;
procedure TPlutoRXThread.Execute;
var
n, step: ptrint;
pFirst, pEnd, p: PByte;
pkt: TDDCIQPacket;
outc: Integer;
vi, vq: LongInt;
u: LongWord;
procedure FlushPkt;
begin
Inc(FBk.FRxSeq);
pkt.Seq[0] := (FBk.FRxSeq shr 24) and $FF;
pkt.Seq[1] := (FBk.FRxSeq shr 16) and $FF;
pkt.Seq[2] := (FBk.FRxSeq shr 8) and $FF;
pkt.Seq[3] := FBk.FRxSeq and $FF;
pkt.SamplesPerFrame[0] := (outc shr 8) and $FF;
pkt.SamplesPerFrame[1] := outc and $FF;
if Assigned(FBk.FOnDDCIQ) then FBk.FOnDDCIQ(0, pkt);
outc := 0;
end;
begin
FillChar(pkt, SizeOf(pkt), 0);
pkt.BitsPerSample[1] := 24;
outc := 0;
while not Terminated do
begin
if FBk.FRxBuf = nil then begin Sleep(5); Continue; end;
n := iio_buffer_refill(FBk.FRxBuf);
if Terminated then Break;
if n <= 0 then begin Sleep(2); Continue; end;
step := iio_buffer_step(FBk.FRxBuf);
pFirst := iio_buffer_first(FBk.FRxBuf, FBk.FRxChI);
pEnd := iio_buffer_end(FBk.FRxBuf);
p := pFirst;
while PtrUInt(p) < PtrUInt(pEnd) do
begin
// I в смещении 0, Q в смещении 2 (два int16-канала, step=4).
// Q инвертируем (сопряжение): IQ-конвенция AD936x зеркальна ожидаемой WDSP,
// иначе спектр перевёрнут (USB↔LSB, перестройка вправо идёт влево).
vi := LongInt(PSmallInt(p)^) * 4096; // 12-bit (±2048) → 24-bit
vq := -LongInt(PSmallInt(p + 2)^) * 4096;
if vi > 8388607 then vi := 8388607 else if vi < -8388608 then vi := -8388608;
if vq > 8388607 then vq := 8388607 else if vq < -8388608 then vq := -8388608;
u := LongWord(vi) and $FFFFFF;
pkt.IQData[outc*6+0] := (u shr 16) and $FF;
pkt.IQData[outc*6+1] := (u shr 8) and $FF;
pkt.IQData[outc*6+2] := u and $FF;
u := LongWord(vq) and $FFFFFF;
pkt.IQData[outc*6+3] := (u shr 16) and $FF;
pkt.IQData[outc*6+4] := (u shr 8) and $FF;
pkt.IQData[outc*6+5] := u and $FF;
Inc(outc);
if outc >= PAIRS_PER_PKT then FlushPkt;
Inc(p, step);
end;
if outc > 0 then FlushPkt; // хвост блока
end;
end;
function PlutoSyntheticMAC(const S: string): TBytes;
var
H: QWord;
I: Integer;
begin
// FNV-1a 64-bit, затем берём младшие 6 байт; первый байт делаем locally-
// administered/unicast (bit1=1, bit0=0), чтобы не коллизировать с реальными MAC.
H := QWord(14695981039346656037);
for I := 1 to Length(S) do
begin
H := H xor QWord(Ord(S[I]));
H := H * QWord(1099511628211);
end;
SetLength(Result, 6);
Result[0] := (Byte(H shr 40) and $FC) or $02;
Result[1] := Byte(H shr 32);
Result[2] := Byte(H shr 24);
Result[3] := Byte(H shr 16);
Result[4] := Byte(H shr 8);
Result[5] := Byte(H);
end;
{ TPlutoBackend }
constructor TPlutoBackend.Create;
begin
inherited Create;
FCtx := nil; FPhy := nil; FRxDev := nil; FTxDev := nil;
FConnected := False;
FRunning := False;
FSampleRate := 1536000;
FGainMode := 'slow_attack'; // аппаратный AGC по умолчанию
FGainDb := 40;
FRxBuf := nil; FRxChI := nil; FRxChQ := nil; FRxThread := nil;
FStreaming := False; FRxSeq := 0;
FillChar(FDevice, SizeOf(FDevice), 0);
end;
destructor TPlutoBackend.Destroy;
begin
Disconnect;
inherited Destroy;
end;
function TPlutoBackend.GetConnected: Boolean; begin Result := FConnected; end;
function TPlutoBackend.GetRunning: Boolean; begin Result := FRunning; end;
function TPlutoBackend.GetDevice: TRadioDevice; begin Result := FDevice; end;
function TPlutoBackend.GetLastError: string; begin Result := FLastError; end;
function TPlutoBackend.Caps: TBackendCaps;
begin
FillChar(Result, SizeOf(Result), 0);
Result.Kind := bkPluto;
Result.HasTX := True;
Result.HasPA := False; // нет PA → нет fwd/SWR/supply
Result.HasAlex := False;
Result.HasWideband := False;
Result.HasDitherRandom := False;
Result.HasHWMic := False;
Result.HasPLLStatus := False;
Result.HasHWGain := True; // manual gain / hw-AGC
Result.HasRFBandwidth := True;
Result.HasFullDuplex := True;
Result.MinSampleRate := PLUTO_MIN_SR;
Result.MaxSampleRate := PLUTO_MAX_SR;
Result.SampleRateMode := srmContinuous;
// Пресеты для SampleRateOverlay (все > 520 ksps, минимум AD936x).
SetLength(Result.RatePresets, 8);
Result.RatePresets[0] := 576000;
Result.RatePresets[1] := 768000;
Result.RatePresets[2] := 960000;
Result.RatePresets[3] := 1536000;
Result.RatePresets[4] := 2304000;
Result.RatePresets[5] := 3072000;
Result.RatePresets[6] := 3840000;
Result.RatePresets[7] := 5760000;
Result.MinFreqHz := PLUTO_MIN_HZ;
Result.MaxFreqHz := PLUTO_MAX_HZ;
end;
function TPlutoBackend.ProbeURI(const AURI: string; out Dev: TRadioDevice): Boolean;
// Открываем контекст по URI, проверяем что это AD936x-SDR (есть ad9361-phy),
// читаем hw_serial/hw_model. Контекст закрываем (это лишь probe, не Connect).
var
Ctx: Piio_context;
Ser, Model, IPStr: string;
PV: PAnsiChar;
MAC: TBytes;
begin
Result := False;
FillChar(Dev, SizeOf(Dev), 0);
if not IIOLoad then Exit;
Ctx := iio_create_context_from_uri(PAnsiChar(AnsiString(AURI)));
if Ctx = nil then Exit;
try
// Признак нашего железа — наличие управляющего устройства ad9361-phy.
if iio_context_find_device(Ctx, 'ad9361-phy') = nil then Exit;
Ser := '';
PV := iio_context_get_attr_value(Ctx, 'hw_serial');
if PV <> nil then Ser := string(PV);
if Ser = '' then Ser := AURI; // fallback: URI как идентификатор
Model := 'AD936x SDR';
PV := iio_context_get_attr_value(Ctx, 'hw_model');
if PV <> nil then Model := string(PV);
IPStr := AURI;
PV := iio_context_get_attr_value(Ctx, 'ip,ip-addr');
if PV <> nil then IPStr := string(PV);
MAC := PlutoSyntheticMAC(Ser);
Dev.Kind := bkPluto;
Dev.URI := AURI;
Dev.Serial := Ser;
Dev.Model := Model;
Dev.IPAddress := IPStr;
Dev.BoardType := 0;
Dev.Valid := True;
Move(MAC[0], Dev.MAC[0], 6);
Result := True;
finally
iio_context_destroy(Ctx);
end;
if Result and Assigned(FOnDeviceFound) then
FOnDeviceFound(Dev);
end;
function TPlutoBackend.Discover(TimeoutMs: Integer): TRadioDeviceArray;
var
Scan: Piio_scan_context;
Info: PPiio_context_info;
Cnt, I: ptrint;
Item: Piio_context_info;
URI: string;
Dev: TRadioDevice;
begin
SetLength(Result, 0);
if not IIOLoad then Exit;
// Сканируем backend 'usb' — надёжно и без зависимости от avahi/dnssd (её тянет
// backend=nil/'ip'). Для каждого найденного контекста делаем ProbeURI: детект по
// наличию ad9361-phy (а не по строке описания — клоны вроде LibreSDR не содержат
// 'pluto'). Сетевые устройства (ip:) находим отдельным ProbeURI по сохранённым/
// введённым адресам (см. контроллер).
Scan := iio_create_scan_context('usb', 0);
if Scan = nil then Exit;
try
Info := nil;
Cnt := iio_scan_context_get_info_list(Scan, Info);
if Cnt <= 0 then Exit;
try
I := 0;
while I < Cnt do
begin
Item := PPiio_context_info(PByte(Info) + I * SizeOf(Pointer))^;
URI := string(iio_context_info_get_uri(Item));
Inc(I);
if ProbeURI(URI, Dev) then
begin
SetLength(Result, Length(Result) + 1);
Result[High(Result)] := Dev;
end;
end;
finally
iio_context_info_list_free(Info);
end;
finally
iio_scan_context_destroy(Scan);
end;
end;
function TPlutoBackend.Connect(const Dev: TRadioDevice): Boolean;
var
M: TBytes;
begin
Result := False;
if not IIOLoad then begin FLastError := 'libiio not available'; Exit; end;
if FConnected then Disconnect;
FDevice := Dev;
// Гарантируем стабильный synthetic-MAC из serial (ключ per-device настроек),
// даже если запись пришла без MAC (сохранённое Pluto-устройство).
if FDevice.Serial <> '' then
begin
M := PlutoSyntheticMAC(FDevice.Serial);
Move(M[0], FDevice.MAC[0], 6);
end;
FCtx := iio_create_context_from_uri(PAnsiChar(AnsiString(Dev.URI)));
if FCtx = nil then
begin
FLastError := 'Cannot open Pluto context: ' + Dev.URI;
Exit;
end;
FPhy := iio_context_find_device(FCtx, 'ad9361-phy');
FRxDev := iio_context_find_device(FCtx, 'cf-ad9361-lpc');
FTxDev := iio_context_find_device(FCtx, 'cf-ad9361-dds-core-lpc');
if FPhy = nil then
begin
FLastError := 'ad9361-phy not found in context';
iio_context_destroy(FCtx); FCtx := nil;
Exit;
end;
FConnected := True;
// Приводим железо в известное состояние: валидный rate + усиление.
if FSampleRate < PLUTO_MIN_SR then FSampleRate := PLUTO_DEF_SR;
ApplySampleRate(FSampleRate);
ApplyGain;
Result := True;
end;
procedure TPlutoBackend.Disconnect;
begin
StopRX;
if FCtx <> nil then
begin
iio_context_destroy(FCtx);
FCtx := nil;
end;
FPhy := nil; FRxDev := nil; FTxDev := nil;
FConnected := False;
FRunning := False;
end;
// ---- Применение атрибутов к ad9361-phy ----
procedure TPlutoBackend.ApplyRxLO(Hz: Double);
var Ch: Piio_channel;
begin
if (FPhy = nil) or (Hz <= 0) then Exit;
Ch := iio_device_find_channel(FPhy, 'altvoltage0', 1);
if Ch <> nil then
iio_channel_attr_write_longlong(Ch, 'frequency', Round(Hz));
end;
procedure TPlutoBackend.ApplyTxLO(Hz: Double);
var Ch: Piio_channel;
begin
if (FPhy = nil) or (Hz <= 0) then Exit;
Ch := iio_device_find_channel(FPhy, 'altvoltage1', 1);
if Ch <> nil then
iio_channel_attr_write_longlong(Ch, 'frequency', Round(Hz));
end;
procedure TPlutoBackend.ApplySampleRate(Hz: Integer);
var Ch: Piio_channel; WasStreaming: Boolean;
begin
if (FPhy = nil) or (Hz < PLUTO_MIN_SR) then Exit;
WasStreaming := FStreaming;
if WasStreaming then StopRX; // менять rate с открытым буфером нельзя
Ch := iio_device_find_channel(FPhy, 'voltage0', 0);
if Ch <> nil then
begin
iio_channel_attr_write_longlong(Ch, 'sampling_frequency', Hz);
iio_channel_attr_write_longlong(Ch, 'rf_bandwidth', Hz);
end;
if WasStreaming then StartRX;
end;
procedure TPlutoBackend.ApplyGain;
var Ch: Piio_channel;
begin
if FPhy = nil then Exit;
Ch := iio_device_find_channel(FPhy, 'voltage0', 0); // RX gain — input voltage0
if Ch = nil then Exit;
iio_channel_attr_write(Ch, 'gain_control_mode', PAnsiChar(AnsiString(FGainMode)));
if FGainMode = 'manual' then
iio_channel_attr_write_longlong(Ch, 'hardwaregain', FGainDb);
end;
procedure TPlutoBackend.StartRX;
begin
if FStreaming or not FConnected or (FRxDev = nil) then Exit;
FRxChI := iio_device_find_channel(FRxDev, 'voltage0', 0);
FRxChQ := iio_device_find_channel(FRxDev, 'voltage1', 0);
if (FRxChI = nil) or (FRxChQ = nil) then
begin FLastError := 'RX I/Q channels not found'; Exit; end;
iio_channel_enable(FRxChI);
iio_channel_enable(FRxChQ);
if Assigned(iio_device_set_kernel_buffers_count) then
iio_device_set_kernel_buffers_count(FRxDev, 4);
FRxBuf := iio_device_create_buffer(FRxDev, RX_BUF_SAMPLES, False);
if FRxBuf = nil then begin FLastError := 'RX create_buffer failed'; Exit; end;
FStreaming := True;
FRxThread := TPlutoRXThread.Create(Self);
end;
procedure TPlutoBackend.StopRX;
begin
if FRxThread <> nil then
begin
FRxThread.Terminate;
if (FRxBuf <> nil) and Assigned(iio_buffer_cancel) then
iio_buffer_cancel(FRxBuf); // разблокировать refill немедленно
FRxThread.WaitFor;
FreeAndNil(FRxThread);
end;
FStreaming := False;
if FRxBuf <> nil then
begin
iio_buffer_destroy(FRxBuf);
FRxBuf := nil;
end;
end;
// ---- Команды контроллера (фаза 1: сохраняем состояние + базовое применение) ----
procedure TPlutoBackend.ConfigureDDCs(NumDDCs: Byte; SampleRate: Word;
ADCSource: Byte; DitherEnabled, RandomEnabled: Boolean);
begin
// SampleRate приходит в ksps (как у HPSDR ConfigureDDCs). Pluto — в Гц.
// Контроллер может прислать HPSDR-наследие (<520k) — клампим к дефолту.
FSampleRate := Integer(SampleRate) * 1000;
if FSampleRate < PLUTO_MIN_SR then FSampleRate := PLUTO_DEF_SR;
if FConnected then ApplySampleRate(FSampleRate); // перезапустит RX при смене
end;
procedure TPlutoBackend.SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double;
DriveLevel: Byte);
begin
FRXFreq := DDC0FreqHz;
FTXFreq := DUCFreqHz;
FRunning := Run;
if FConnected then
begin
ApplyRxLO(FRXFreq);
ApplyTxLO(FTXFreq);
if Run then StartRX else StopRX; // RX-стрим; TX-стрим — фаза 4
end;
end;
procedure TPlutoBackend.UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte;
Transmitting, PAEnabled, AlexEnabled: Boolean);
begin
FRXFreq := RXFreqHz;
FTXFreq := TXFreqHz;
FTransmitting := Transmitting;
if FConnected then
begin
ApplyRxLO(FRXFreq);
ApplyTxLO(FTXFreq);
end;
end;
procedure TPlutoBackend.SendFullHP;
begin
// У Pluto нет HP-пакета: частоты/усиление применяются напрямую в UpdateState/
// SetRunAndFreq. Здесь — no-op (состояние уже применено).
end;
procedure TPlutoBackend.SendPTT(Active: Boolean);
begin
FTransmitting := Active;
// Реальный RX↔TX (вкл/выкл стримов) — фаза 4.
end;
end.
+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.
+145 -22
View File
@@ -48,7 +48,7 @@ interface
uses uses
Classes, SysUtils, Math, Classes, SysUtils, Math,
HPSDRProtocol, HPSDRNetwork, HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
WDSPEngine, AudioOutput, AudioInput, WDSPEngine, AudioOutput, AudioInput,
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore; Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
@@ -145,7 +145,9 @@ type
FWaterfallBufCount: Integer; FWaterfallBufCount: Integer;
// ---- Движки (владение: см. шапку юнита) ---- // ---- Движки (владение: см. шапку юнита) ----
FNetwork: THPSDRNetwork; // FNetwork — абстрактный бэкенд железа (TRadioBackend): конкретный класс
// (THPSDRNetwork / TPlutoBackend позже) выбирается при подключении.
FNetwork: TRadioBackend;
FDSPEngine: TWDSPEngine; FDSPEngine: TWDSPEngine;
FAudioOut: TAudioOutput; FAudioOut: TAudioOutput;
FAudioIn: TAudioInput; FAudioIn: TAudioInput;
@@ -272,6 +274,8 @@ type
// через Invoke; Synchronize блокирующий — устройства приходят по одному). // через Invoke; Synchronize блокирующий — устройства приходят по одному).
FPendingFoundDev: THPSDRDevice; FPendingFoundDev: THPSDRDevice;
FPendingFoundEntry: string; FPendingFoundEntry: string;
// URI для probe сетевых Pluto при discovery (см. SetPlutoProbeURIs).
FPlutoProbeURIs: array of string;
constructor Create; constructor Create;
destructor Destroy; override; destructor Destroy; override;
@@ -447,6 +451,17 @@ type
function ConnectAndRun(const Dev: THPSDRDevice; SpectrumWidth: Integer): Boolean; // ConnectDevice + band/xvtr/vfo restore + go-live function ConnectAndRun(const Dev: THPSDRDevice; SpectrumWidth: Integer): Boolean; // ConnectDevice + band/xvtr/vfo restore + go-live
procedure StopAndDisconnect; // graceful Run=0 + персист настроек + Disconnect (headless) procedure StopAndDisconnect; // graceful Run=0 + персист настроек + Disconnect (headless)
procedure Discover; procedure Discover;
// Гарантирует, что FNetwork — бэкенд нужного типа (HPSDR/Pluto): при
// несовпадении пересоздаёт его, перенося колбэки данных. Вызывается из
// ConnectDevice по Dev.Kind. Discovery использует транзиентные экземпляры.
procedure EnsureBackend(Kind: TBackendKind);
// Возможности текущего бэкенда (для гейтинга UI / пресетов sample rate).
function BackendCaps: TBackendCaps;
// True если активный бэкенд — Pluto (VHF/UHF band-план вместо HF).
function IsPluto: Boolean;
// Список URI для probe сетевых Pluto (mDNS недоступен) — задаёт фронтенд
// перед Discover (сохранённые Pluto + ручной ввод IP).
procedure SetPlutoProbeURIs(const URIs: array of string);
// ---- События наружу ---- // ---- События наружу ----
property OnInvoke: TUIInvokeEvent read FOnInvoke write FOnInvoke; property OnInvoke: TUIInvokeEvent read FOnInvoke write FOnInvoke;
@@ -1074,8 +1089,8 @@ begin
// Bandstack: VFO A и B независимы и могут стоять на разных бэндах. В кэш // Bandstack: VFO A и B независимы и могут стоять на разных бэндах. В кэш
// ЭТОГО бэнда пишем только ту частоту VFO, что реально в нём — иначе чужая // ЭТОГО бэнда пишем только ту частоту VFO, что реально в нём — иначе чужая
// частота (напр. VFO A на 40м при сохранении 30м) загрязнит memory бэнда. // частота (напр. VFO A на 40м при сохранении 30м) загрязнит memory бэнда.
if FreqToBandIdx(FVfoA) <> FCurrentBand then B.VfoA := FBandCache[FCurrentBand].VfoA; if FreqToBandIdx(FVfoA, IsPluto) <> FCurrentBand then B.VfoA := FBandCache[FCurrentBand].VfoA;
if FreqToBandIdx(FVfoB) <> FCurrentBand then B.VfoB := FBandCache[FCurrentBand].VfoB; if FreqToBandIdx(FVfoB, IsPluto) <> FCurrentBand then B.VfoB := FBandCache[FCurrentBand].VfoB;
FBandCache[FCurrentBand] := B; FBandCache[FCurrentBand] := B;
FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]); FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]);
end; end;
@@ -1220,6 +1235,14 @@ begin
B := FBandCache[BandIdx]; B := FBandCache[BandIdx];
FCurrentBand := BandIdx; FCurrentBand := BandIdx;
// Pluto: кэш диапазона может нести HF-частоту (дефолты Settings). Если VfoA не
// попадает в VHF-слот — берём частоту по умолчанию из плана.
if IsPluto and (FreqToBandIdx(B.VfoA, True) <> BandIdx) then
begin
B.VfoA := BandDefaultFreq(BandIdx, True);
B.VfoB := B.VfoA;
end;
ApplyBandDSP(B); ApplyBandDSP(B);
// --- VFO / drive (только RestoreBand — bandstack-путь VFO не трогает) --- // --- VFO / drive (только RestoreBand — bandstack-путь VFO не трогает) ---
@@ -1303,7 +1326,7 @@ begin
// FreqToBandIdx вернёт -1 для VHF/UHF). // FreqToBandIdx вернёт -1 для VHF/UHF).
if FCurrentXvtr < 0 then if FCurrentXvtr < 0 then
begin begin
BandIdx := FreqToBandIdx(FVfoA); BandIdx := FreqToBandIdx(FVfoA, IsPluto);
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
begin begin
FCurrentBand := BandIdx; FCurrentBand := BandIdx;
@@ -1333,7 +1356,7 @@ begin
FVfoA := Hz; FVfoA := Hz;
if FCurrentXvtr < 0 then if FCurrentXvtr < 0 then
begin begin
BandIdx := FreqToBandIdx(FVfoA); BandIdx := FreqToBandIdx(FVfoA, IsPluto);
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
begin begin
FCurrentBand := BandIdx; FCurrentBand := BandIdx;
@@ -1369,7 +1392,7 @@ begin
ApplyTuneCore(FVfoB); ApplyTuneCore(FVfoB);
if FCurrentXvtr < 0 then if FCurrentXvtr < 0 then
begin begin
BandIdx := FreqToBandIdx(FVfoB); BandIdx := FreqToBandIdx(FVfoB, IsPluto);
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
begin begin
FCurrentBand := BandIdx; FCurrentBand := BandIdx;
@@ -1403,7 +1426,7 @@ begin
// --- Bandstack: активируемый VFO на другом ham-бэнде → восстановить бэнд --- // --- Bandstack: активируемый VFO на другом ham-бэнде → восстановить бэнд ---
if FCurrentXvtr < 0 then if FCurrentXvtr < 0 then
begin begin
NewBand := FreqToBandIdx(ActiveFreq); NewBand := FreqToBandIdx(ActiveFreq, IsPluto);
if (NewBand >= 0) and (NewBand <> FCurrentBand) then if (NewBand >= 0) and (NewBand <> FCurrentBand) then
begin begin
SaveCurrentBand; // сохранить покидаемый бэнд SaveCurrentBand; // сохранить покидаемый бэнд
@@ -1482,7 +1505,7 @@ begin
// при подсвеченной кнопке нового бэнда (рассинхрон). // при подсвеченной кнопке нового бэнда (рассинхрон).
if FCurrentXvtr < 0 then if FCurrentXvtr < 0 then
begin begin
NewBand := FreqToBandIdx(ActiveFreq); NewBand := FreqToBandIdx(ActiveFreq, IsPluto);
if (NewBand >= 0) and (NewBand <> FCurrentBand) then if (NewBand >= 0) and (NewBand <> FCurrentBand) then
begin begin
SaveCurrentBand; SaveCurrentBand;
@@ -2303,6 +2326,8 @@ function TRadioController.ConnectDevice(const Dev: THPSDRDevice): Boolean;
var var
i: Integer; i: Integer;
begin begin
// Подбираем бэкенд под тип устройства (HPSDR/Pluto) до подключения.
EnsureBackend(Dev.Kind);
Result := FNetwork.Connect(Dev); Result := FNetwork.Connect(Dev);
if not Result then Exit; if not Result then Exit;
@@ -2341,6 +2366,14 @@ begin
FSampleRate := FLoadedGlobal.SampleRate; FSampleRate := FLoadedGlobal.SampleRate;
FSpanHz := FSampleRate; FSpanHz := FSampleRate;
end; end;
// Pluto: HPSDR-наследие rate (48..1536k, часто <520k) невалидно. Клампим к
// дефолту бэкенда, если ниже минимума (overlay покажет валидный набор).
with FNetwork.Caps do
if (Kind = bkPluto) and (FSampleRate < MinSampleRate) then
begin
FSampleRate := 1536000;
FSpanHz := FSampleRate;
end;
FNRMode := EnsureRange(FLoadedGlobal.NRMode, 0, 4); FNRMode := EnsureRange(FLoadedGlobal.NRMode, 0, 4);
FNBMode := EnsureRange(FLoadedGlobal.NBMode, 0, 2); FNBMode := EnsureRange(FLoadedGlobal.NBMode, 0, 2);
FSNB := FLoadedGlobal.SNBEnabled; FSNB := FLoadedGlobal.SNBEnabled;
@@ -2420,6 +2453,16 @@ begin
// Перестройка диапазона — команда несёт финальную перестройку по активному VFO. // Перестройка диапазона — команда несёт финальную перестройку по активному VFO.
RestoreBand(FCurrentBand); RestoreBand(FCurrentBand);
// Pluto/VHF: HF-частоты вне диапазона. Если восстановленный VFO ниже минимума
// бэкенда — ставим дефолт VHF (временно 100 МГц, пока нет VHF band-плана).
with FNetwork.Caps do
if (Kind = bkPluto) and (FVfoA < MinFreqHz) then
begin
FVfoB := 100000000;
FCenterFreq := 100000000;
SetVfoA(100000000); // перестроит дисплей + LO
end;
// Восстанавливаем XVTR-режим, если он был активен при выходе. FCurrentBand уже // Восстанавливаем XVTR-режим, если он был активен при выходе. FCurrentBand уже
// загружен (HF-диапазон до XVTR) — ActivateXvtr только выставит FVfoA/FCurrentXvtr. // загружен (HF-диапазон до XVTR) — ActivateXvtr только выставит FVfoA/FCurrentXvtr.
if (FLoadedGlobal.LastXvtr >= 0) and (FLoadedGlobal.LastXvtr < CFG_XVTR_COUNT) if (FLoadedGlobal.LastXvtr >= 0) and (FLoadedGlobal.LastXvtr < CFG_XVTR_COUNT)
@@ -2482,25 +2525,103 @@ begin
end; end;
procedure TDiscoverThread.Execute; procedure TDiscoverThread.Execute;
// Сканируем ОБА бэкенда транзиентными экземплярами (независимо от FNetwork —
// он может быть уже подключён/другого типа). Найденные сигналятся колбэком
// OnDeviceFound во время Discover() и копятся в store.
var var
Devs: THPSDRDeviceArray; Found: Integer;
HP: THPSDRNetwork;
Pluto: TPlutoBackend;
PDev: TRadioDevice;
i: Integer;
begin begin
Devs := FCtrl.FNetwork.Discover(2000); Found := 0;
// Найденные устройства сигналятся колбэком OnDeviceFound во время Discover().
if Length(Devs) = 0 then // openHPSDR (UDP broadcast)
HP := THPSDRNetwork.Create;
try
HP.OnDeviceFound := FCtrl.OnDeviceFound;
Found := Found + Length(HP.Discover(2000));
finally
HP.Free;
end;
// Pluto (libiio scan + прямой probe сетевых URI) — если libiio доступна.
if IIOLoad then
begin
Pluto := TPlutoBackend.Create;
try
Pluto.OnDeviceFound := FCtrl.OnDeviceFound;
// USB scan
Found := Found + Length(Pluto.Discover(1500));
// Прямой probe сетевых адресов (mDNS недоступен) — из списка фронтенда.
for i := 0 to High(FCtrl.FPlutoProbeURIs) do
if Pluto.ProbeURI(FCtrl.FPlutoProbeURIs[i], PDev) then
Inc(Found);
finally
Pluto.Free;
end;
end;
if Found = 0 then
FCtrl.Invoke(FCtrl.DiscoverFinishedNone); FCtrl.Invoke(FCtrl.DiscoverFinishedNone);
end; end;
procedure TRadioController.OnDeviceFound(const Dev: THPSDRDevice); procedure TRadioController.EnsureBackend(Kind: TBackendKind);
// Сетевой поток: строим строку для списка → pending → маршалинг в ApplyDeviceFound. // Пересоздаёт FNetwork нужного класса, перенося колбэки данных. No-op если тип
// уже верный. Старый бэкенд отключается и освобождается.
var var
BoardName: string; NewB: TRadioBackend;
begin begin
BoardName := BoardTypeName(Dev.BoardType); if (FNetwork <> nil) and (FNetwork.Caps.Kind = Kind) then Exit;
FPendingFoundDev := Dev;
FPendingFoundEntry := Format('%s %s FW:%d DDC:%d', case Kind of
[Dev.IPAddress, BoardName, bkPluto: NewB := TPlutoBackend.Create;
Dev.FirmwareVersion, Dev.NumDDCs]); else NewB := THPSDRNetwork.Create;
end;
if FNetwork <> nil then
begin
if FNetwork.Connected then FNetwork.Disconnect;
NewB.OnDeviceFound := FNetwork.OnDeviceFound;
NewB.OnDDCIQ := FNetwork.OnDDCIQ;
NewB.OnMicPacket := FNetwork.OnMicPacket;
NewB.OnHPStatus := FNetwork.OnHPStatus;
NewB.OnWideband := FNetwork.OnWideband;
FreeAndNil(FNetwork);
end;
FNetwork := NewB;
end;
function TRadioController.BackendCaps: TBackendCaps;
begin
if FNetwork <> nil then Result := FNetwork.Caps
else FillChar(Result, SizeOf(Result), 0);
end;
function TRadioController.IsPluto: Boolean;
begin
Result := (FNetwork <> nil) and (FNetwork.Caps.Kind = bkPluto);
end;
procedure TRadioController.SetPlutoProbeURIs(const URIs: array of string);
var i: Integer;
begin
SetLength(FPlutoProbeURIs, Length(URIs));
for i := 0 to High(URIs) do FPlutoProbeURIs[i] := URIs[i];
end;
procedure TRadioController.OnDeviceFound(const Dev: THPSDRDevice);
// Сетевой/scan-поток: строим строку для списка → pending → маршалинг в
// ApplyDeviceFound. Строка зависит от типа бэкенда (HPSDR vs Pluto).
begin
FPendingFoundDev := Dev;
if Dev.Kind = bkPluto then
FPendingFoundEntry := Format('PlutoSDR %s', [Dev.URI])
else
FPendingFoundEntry := Format('%s %s FW:%d DDC:%d',
[Dev.IPAddress, BoardTypeName(Dev.BoardType),
Dev.FirmwareVersion, Dev.NumDDCs]);
Invoke(ApplyDeviceFound); Invoke(ApplyDeviceFound);
end; end;
@@ -2509,7 +2630,9 @@ procedure TRadioController.ApplyDeviceFound;
begin begin
FDiscoverNoneFound := False; FDiscoverNoneFound := False;
FDeviceStore.AddDiscovered(FPendingFoundDev.IPAddress, FPendingFoundEntry, FDeviceStore.AddDiscovered(FPendingFoundDev.IPAddress, FPendingFoundEntry,
FPendingFoundDev.BoardType, FPendingFoundDev.MAC); FPendingFoundDev.BoardType, FPendingFoundDev.MAC,
FPendingFoundDev.Kind, FPendingFoundDev.URI,
FPendingFoundDev.Serial);
Changed(rfDeviceList); Changed(rfDeviceList);
end; end;
+48 -19
View File
@@ -22,6 +22,11 @@ type
TSampleRateOverlay = class(TComponent) TSampleRateOverlay = class(TComponent)
private private
FCurrentRate: Integer; FCurrentRate: Integer;
// Набор пресетов (зависит от бэкенда): HPSDR 48k..1536k или Pluto >560k.
// До 10 слотов (Pluto может иметь 8: 576k..5760k).
FRates: array[0..9] of Integer;
FNames: array[0..9] of string;
FRateCount: Integer;
FPanelHidden: Boolean; FPanelHidden: Boolean;
FOnSpanSelect: TSpanSelectEvent; FOnSpanSelect: TSpanSelectEvent;
FOnHidePanel: THidePanelEvent; FOnHidePanel: THidePanelEvent;
@@ -31,8 +36,8 @@ type
FWidth: Integer; FWidth: Integer;
FHeight: Integer; FHeight: Integer;
FHitRects: array[0..6] of TRect; // [0]=hide button, [1..6]=span buttons FHitRects: array[0..10] of TRect; // [0]=hide button, [1..]=span buttons
FHitValues: array[0..6] of Integer; // 0=hide, else sample rate FHitValues: array[0..10] of Integer; // 0=hide, else sample rate
FHitCount: Integer; FHitCount: Integer;
FHotIdx: Integer; FHotIdx: Integer;
@@ -67,6 +72,9 @@ type
function HandleMouseDown(Button: TMouseButton; X, Y: Integer): Boolean; function HandleMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
function HandleMouseLeave: Boolean; function HandleMouseLeave: Boolean;
procedure SetCurrentRate(Rate: Integer); procedure SetCurrentRate(Rate: Integer);
// Задаёт пресеты (Гц, до 6 шт.); имена формируются как '<kHz>k'.
procedure SetRatePresets(const Rates: array of Integer);
procedure SetRatePresetsDefault; // HPSDR 48k..1536k
procedure SetPanelHidden(Hidden: Boolean); procedure SetPanelHidden(Hidden: Boolean);
property Left: Integer read FLeft; property Left: Integer read FLeft;
property Top: Integer read FTop; property Top: Integer read FTop;
@@ -139,14 +147,15 @@ begin
C.Font.Name := 'Courier New'; C.Font.Name := 'Courier New';
C.Font.Size := 7; C.Font.Size := 7;
C.Font.Style := []; C.Font.Style := [];
Result := C.TextWidth(Name) + 10; Result := C.TextWidth(Name) + 8;
if Result < 36 then Result := 36; if Result < 30 then Result := 30;
end; end;
constructor TSampleRateOverlay.Create(AOwner: TComponent); constructor TSampleRateOverlay.Create(AOwner: TComponent);
begin begin
inherited Create(AOwner); inherited Create(AOwner);
FCurrentRate := 192000; FCurrentRate := 192000;
SetRatePresetsDefault;
FPanelHidden := False; FPanelHidden := False;
FHitCount := 0; FHitCount := 0;
FHotIdx := -1; FHotIdx := -1;
@@ -207,10 +216,10 @@ begin
TH := C.TextHeight('A'); TH := C.TextHeight('A');
ContentW := PAD_X + HIDE_W + BTN_GAP; ContentW := PAD_X + HIDE_W + BTN_GAP;
for I := 0 to High(SPAN_NAMES) do for I := 0 to FRateCount-1 do
begin begin
Inc(ContentW, SpanBtnWidth(SPAN_NAMES[I], C)); Inc(ContentW, SpanBtnWidth(FNames[I], C));
if I < High(SPAN_NAMES) then Inc(ContentW, BTN_GAP); if I < FRateCount-1 then Inc(ContentW, BTN_GAP);
end; end;
Inc(ContentW, PAD_X); Inc(ContentW, PAD_X);
if ContentW > W then ContentW := W; if ContentW > W then ContentW := W;
@@ -247,11 +256,11 @@ begin
RegisterHit(HR, 0); RegisterHit(HR, 0);
Inc(X, HIDE_W + BTN_GAP); Inc(X, HIDE_W + BTN_GAP);
for I := 0 to High(SPAN_RATES) do for I := 0 to FRateCount-1 do
begin begin
BtnW := SpanBtnWidth(SPAN_NAMES[I], C); BtnW := SpanBtnWidth(FNames[I], C);
HR := Rect(X, OriginY + PAD_Y, X + BtnW, OriginY + PAD_Y + SPAN_H); HR := Rect(X, OriginY + PAD_Y, X + BtnW, OriginY + PAD_Y + SPAN_H);
IsAct := SPAN_RATES[I] = FCurrentRate; IsAct := FRates[I] = FCurrentRate;
IsHot := FHotIdx = I + 1; IsHot := FHotIdx = I + 1;
if IsAct then BlendRect(Target, HR, $20, $5A, $78, 220) if IsAct then BlendRect(Target, HR, $20, $5A, $78, 220)
@@ -262,13 +271,13 @@ begin
C.Brush.Style := bsClear; C.Brush.Style := bsClear;
C.Rectangle(HR); C.Rectangle(HR);
TW := C.TextWidth(SPAN_NAMES[I]); TW := C.TextWidth(FNames[I]);
C.Font.Color := CLR_TEXT; C.Font.Color := CLR_TEXT;
C.Brush.Style := bsClear; C.Brush.Style := bsClear;
C.TextOut(HR.Left + (BtnW - TW) div 2, C.TextOut(HR.Left + (BtnW - TW) div 2,
HR.Top + (SPAN_H - TH) div 2, SPAN_NAMES[I]); HR.Top + (SPAN_H - TH) div 2, FNames[I]);
RegisterHit(HR, SPAN_RATES[I]); RegisterHit(HR, FRates[I]);
Inc(X, BtnW + BTN_GAP); Inc(X, BtnW + BTN_GAP);
end; end;
end; end;
@@ -278,10 +287,10 @@ var I, CW: Integer;
begin begin
C.Font.Name := 'Courier New'; C.Font.Size := 7; C.Font.Style := []; C.Font.Name := 'Courier New'; C.Font.Size := 7; C.Font.Style := [];
CW := PAD_X + HIDE_W + BTN_GAP; CW := PAD_X + HIDE_W + BTN_GAP;
for I := 0 to High(SPAN_NAMES) do for I := 0 to FRateCount-1 do
begin begin
Inc(CW, SpanBtnWidth(SPAN_NAMES[I], C)); Inc(CW, SpanBtnWidth(FNames[I], C));
if I < High(SPAN_NAMES) then Inc(CW, BTN_GAP); if I < FRateCount-1 then Inc(CW, BTN_GAP);
end; end;
Inc(CW, PAD_X); Inc(CW, PAD_X);
if CW > AvailW then CW := AvailW; if CW > AvailW then CW := AvailW;
@@ -444,10 +453,10 @@ begin
Target.Canvas.Font.Size := 7; Target.Canvas.Font.Size := 7;
Target.Canvas.Font.Style := []; Target.Canvas.Font.Style := [];
ContentW := PAD_X + HIDE_W + BTN_GAP; ContentW := PAD_X + HIDE_W + BTN_GAP;
for I := 0 to High(SPAN_NAMES) do for I := 0 to FRateCount-1 do
begin begin
Inc(ContentW, SpanBtnWidth(SPAN_NAMES[I], Target.Canvas)); Inc(ContentW, SpanBtnWidth(FNames[I], Target.Canvas));
if I < High(SPAN_NAMES) then Inc(ContentW, BTN_GAP); if I < FRateCount-1 then Inc(ContentW, BTN_GAP);
end; end;
Inc(ContentW, PAD_X); Inc(ContentW, PAD_X);
ContentW := Min(ContentW, W); ContentW := Min(ContentW, W);
@@ -524,6 +533,26 @@ begin
end; end;
end; end;
procedure TSampleRateOverlay.SetRatePresets(const Rates: array of Integer);
var i, n: Integer;
begin
n := Length(Rates);
if n > 10 then n := 10;
FRateCount := n;
for i := 0 to n - 1 do
begin
FRates[i] := Rates[i];
FNames[i] := IntToStr(Rates[i] div 1000) + 'k';
end;
FCacheDirty := True;
RequestInvalidate;
end;
procedure TSampleRateOverlay.SetRatePresetsDefault;
begin
SetRatePresets(SPAN_RATES);
end;
procedure TSampleRateOverlay.SetCurrentRate(Rate: Integer); procedure TSampleRateOverlay.SetCurrentRate(Rate: Integer);
begin begin
if FCurrentRate = Rate then Exit; if FCurrentRate = Rate then Exit;
+320
View File
@@ -0,0 +1,320 @@
# План интеграции ADALM-PLUTO (PlutoSDR) в EWSDR
Ветка: `feature/pluto-sdr-integration` (от `feature/radio-controller-refactor`).
Документ — результат анализа текущей кодовой базы и предлагаемый поэтапный план.
Цель: добавить второй бэкенд железа (Pluto, libiio/AD936x) рядом с openHPSDR так,
чтобы оба находились в одном окне discovery, а различия возможностей корректно
отражались в UI (включая оверлей samplerate с другими значениями для Pluto).
**Ключевые решения по продукту:**
- Pluto — **только VHF/UHF**, HF исключён. Свой band-план: 2 м, 70 см, 23 см,
13 см (и т.д.).
- Отдельный **режим QO-100** (геостационарный транспондер): RX через LNB,
TX напрямую на 2400 МГц, раздельные RX/TX LO, full-duplex.
---
## 1. Анализ текущей архитектуры
### 1.1 Слои и владение
```
TMainForm (GUI) ─┐
ewsdrd.lpr (демон)─┤── владеют ─→ TRadioController (backend-агностичное ядро)
WebServer/Adapter ─┘ │
├─ FNetwork: THPSDRNetwork ← ЕДИНСТВЕННАЯ точка железа
├─ FDSPEngine: TWDSPEngine
├─ FAudioOut / FAudioIn
├─ FSettings: TSettingsManager (per-device по MAC)
└─ FDeviceStore: TDeviceStore (saved + discovered)
```
`TRadioController` уже спроектирован как **backend-агностичное ядро** — в коде есть
прямые комментарии-намёки на Pluto (RadioController.pas:541, :603). Но
`FNetwork` объявлен конкретным типом `THPSDRNetwork`, а не интерфейсом, и
используется в **52 местах** контроллера.
### 1.2 Путь данных (ключевое для бэкенда)
**RX (вход IQ):**
```
THPSDRNetwork (RX-поток, UDP 1035) → OnDDCIQ(DDCIndex, TDDCIQPacket)
→ FDSPEngine.PushDDCPacket(Data.IQData, 0, SamplesPerFrame)
```
IQ — **24-битные** интерливнутые пары I/Q (6 байт/пара), формат зашит в
`PushDDCPacket` (WDSPEngine.pas:1136) и в декодере DSP-потока.
**TX (выход IQ):**
```
WDSP TXA → OnTXIQ(Buf, Count) → накопление 240 пар (24-бит, clamp)
→ FNetwork.SendDUCIQ(I[], Q[])
```
**Телеметрия:** `OnHPStatus(THighPriorityStatus)` → fwd power / SWR / supply V/A /
PLL lock / ADC overload. S-meter считается отдельно в WDSP (`FLastSMeter`).
### 1.3 Discovery
```
FController.Discover → TDiscoverThread → FNetwork.Discover(2000)
(UDP broadcast :1024, CMD_DISCOVERY)
→ per device: OnDeviceFound(THPSDRDevice)
→ FDeviceStore.AddDiscovered(...) → Changed(rfDeviceList)
→ DeviceForm.RefreshFound (один список «DISCOVERED DEVICES»)
```
- `THPSDRDevice`: IP, Port, MAC[6], BoardType, ProtocolVersion, NumDDCs…
- `TDeviceStore`: `TSavedDevice` (Name/IP/BoardType/AutoStart) и
`TDiscoveredDevice` (IP/DisplayName/BoardType/MAC[6]).
- **Per-device настройки** (`Settings.pas`) ключуются по MAC через
`MacToStr(MAC)` → строковая секция JSON. `LoadDevice/LoadTX/LoadAlex/LoadXvtr`
все принимают `MAC: array of Byte`.
### 1.4 Sample rate
- `SampleRateOverlay.pas`**жёстко зашитый** массив
`SPAN_RATES = (48000,96000,192000,384000,768000,1536000)` и `SPAN_NAMES`.
- `SetSampleRate(Hz)` (RadioController.pas:1791): span ≡ sample rate;
`FDSPEngine.ChangeSampleRate(Hz)` + `FNetwork.ConfigureDDCs(1, Hz div 1000, …)`.
- Дискретные значения HPSDR кратны DSP-clock 122.88 МГц.
### 1.5 Частота / диапазоны / XVTR
- `FreqToPhaseWord` (HPSDRProtocol.pas): phase = 2³² · f / 122.88 МГц.
- `FreqToBandIdx` / band-план — **HF/6m** (1.8–54 МГц, 11 диапазонов).
- Есть полноценная **XVTR-подсистема** (8 слотов, LO offset/error, трансляция
видимой частоты в IF, `XvtrTranslate`) — но **один** LO-offset на слот (RX=TX).
Для QO-100 нужны **раздельные RX/TX LO** → отдельная модель (см. §4).
- `SetDuplex` (RadioController.pas:1851) — сейчас заглушка (`{TODO wiring}`).
Для QO-100 full-duplex это нужно довести.
---
## 2. Pluto: что отличается (capability-матрица)
| Возможность | openHPSDR | Pluto (AD936x) |
|--------------------------------|-----------------------|---------------------------------------|
| Транспорт | UDP/Ethernet (свой) | libiio: USB / IP / local |
| Discovery | UDP broadcast :1024 | `iio_scan_context` (USB/network/local)|
| Идентификатор | MAC[6] | serial / context URI |
| Кол-во приёмников (DDC) | до 80, sync | 1 RX (2×2 только с хаком прошивки) |
| Sample rate | дискрет 48k…1536k | **непрерывный, ≥ ~521 ksps**, до 61.44 MSPS (по USB2 практически ~56 MSPS) |
| RF-полоса (аналог. фильтр) | нет (фикс CIC) | отдельная настройка RX/TX LPF |
| Диапазон частот | 061.44 МГц (HF/6m) | 325 МГц–3.8 ГГц (сток), **70 МГц–6 ГГц с хаком**. HF не используем |
| Разрядность IQ | 24-бит | 12-бит ADC, по шине 16-бит |
| Усиление RX | step atten + dither/random | manual gain 073 дБ / AGC (slow/fast/hybrid/manual) |
| TX | DUC + PA + Alex T/R | 1 DAC, TX atten 89…0 дБ, **без PA/SWR** |
| Full-duplex | (зависит) | да (раздельные RX/TX LO) — нужно для QO-100 |
| S-meter | WDSP (есть) | WDSP (есть) + аппаратный RSSI |
| Fwd power / SWR / supply V/A | HP status | **нет** (нет PA) |
| PLL lock | HP status | нет (другой механизм) |
| ADC overload | HP status | косвенно (gain/RSSI) |
| Wideband ADC дисплей | есть | нет |
| Alex / антенный коммутатор | есть | нет |
| Mic over network / HW CW keyer / sidetone / open collector | есть | нет (TX-аудио через звуковую карту/web) |
| 10 МГц reference / PA config | есть | нет; есть XO ppm-калибровка, темп.сенсор |
**Только у Pluto (нужно в UI):** RF bandwidth, manual gain / режим AGC железа,
XO ppm-коррекция, выбор транспорта (USB/IP), buffer size/timeout, температура.
---
## 3. Band-план Pluto (VHF/UHF) и режим QO-100
### 3.1 VHF/UHF band-план
HF исключён. Для Pluto — **отдельная таблица диапазонов** (используется, когда
подключён бэкенд `bkPluto`); `FreqToBandIdx`/band-кнопки переключаются на неё:
| Band | Частоты | Примечание |
|--------|----------------------|---------------------------------------------|
| 2 м | 144146 МГц | требует freq-range хак Pluto (<325 МГц) |
| 70 см | 430440 МГц | требует freq-range хак Pluto |
| 23 см | 12401300 МГц | нативно |
| 13 см | 23002450 МГц | нативно (включает QO-100 uplink 2400) |
| (опц.) FM bcast / ADS-B / прочие — позже |
⚠️ 2 м и 70 см ниже стокового минимума 325 МГц → нужен AD9363→AD9361
frequency-range мод прошивки Pluto. В UI/доках это отметить.
### 3.2 Режим QO-100 (геостационар Es'hail-2 NB-транспондер)
Геометрия сигнала:
- **Downlink (RX):** 10489.5010489.99 МГц (10 ГГц). Принимается через LNB:
LNB LO (обычно 9750 МГц) → Pluto RX ≈ 739.5739.99 МГц. Нативный диапазон Pluto.
- **Uplink (TX):** 2400.052400.54 МГц. Pluto TX напрямую (нативно).
- **Связь частот:** uplink = downlink 8089.5 МГц (offset транспондера).
- **Full-duplex** — стандартный режим работы (слышишь себя через спутник).
Отсюда QO-100 — это **трансвертер с раздельными RX/TX LO**, чего нынешняя
XVTR-модель (один offset) не даёт. Предлагаемая модель — отдельная подсистема/слот:
```pascal
TQO100Settings = record
Enabled: Boolean;
DisplayBase: Double; // отображаем downlink (10489.x ГГц)
LnbLoHz: Double; // LO LNB, по умолч. 9750e6
LnbPpm: Double; // калибровка дрейфа LNB (по маяку)
TransponderOfs: Double; // 8089.5e6 (downlink - uplink)
TxAttenDb: Double; // -89..0
FullDuplex: Boolean; // RX во время TX
end;
```
Трансляция:
- RX: `PlutoRxLO = DisplayFreq LnbLoHz` (с учётом `LnbPpm`).
- TX: `PlutoTxLO = DisplayFreq TransponderOfs`.
- Дисплей/VFO/курсор работают в терминах downlink (10 ГГц).
- Калибровка по маяку: подстройка `LnbPpm`, чтобы наблюдаемый маяк совпал с
опорной частотой.
Требует довести `SetDuplex` (сейчас TODO) до реального full-duplex (одновременно
RX-поток refill и TX-поток push в libiio — Pluto это поддерживает аппаратно).
---
## 4. Предлагаемая архитектура интеграции
### 4.1 Абстракция бэкенда (ядро плана)
Базовый класс/интерфейс **`TRadioBackend`**, который реализуют оба:
`THPSDRNetwork` и новый `TPlutoBackend`. Контроллер держит `FBackend: TRadioBackend`
(сохранить имя поля `FNetwork`, сменив только тип на базовый — минимизирует дифф).
Методы, которых у Pluto нет, — пустые no-op (Alex, wideband, step atten, speaker
audio, DUC specific и т.д.).
Дескриптор возможностей:
```pascal
TBackendKind = (bkHPSDR, bkPluto);
TSampleRateMode = (srmDiscrete, srmContinuous);
TBackendCaps = record
Kind: TBackendKind;
HasTX, HasPA, HasAlex, HasWideband: Boolean;
HasDitherRandom, HasHWMic, HasPLLStatus: Boolean;
HasHWGain: Boolean; // manual gain / hw AGC (Pluto)
HasRFBandwidth: Boolean;
HasFullDuplex: Boolean;
MinSampleRate, MaxSampleRate: Integer;
SampleRateMode: TSampleRateMode;
RatePresets: array of Integer; // для оверлея
MinFreqHz, MaxFreqHz: Double;
end;
```
Контроллер по `FBackend.Caps`: гейтит UI, подставляет пресеты samplerate,
выбирает band-план (VHF/UHF), путь телеметрии.
### 4.2 libiio binding
- FPC-биндинг к `libiio` (и опц. `libad9361`) через **динамическую загрузку**
(`dynlibs`): `libiio.so` / `libiio.dll` / `libiio.dylib`. Приложение
запускается без libiio (Pluto просто не появляется в discovery).
- Новые юниты: `IIOBindings.pas` (типы/прототипы) + `PlutoBackend.pas`
(`TPlutoBackend`: scan/connect, RX/TX потоки, gain/freq/rate/bandwidth).
### 4.3 Discovery в одном окне
- Расширить записи устройств: `Kind: TBackendKind`, `URI: string`
(Pluto: `usb:1.5.5` / `ip:192.168.2.1`), `Serial: string`.
- `TDiscoverThread` запускает **оба** скана параллельно, сливает в один store:
HPSDR (`FNetwork.Discover`) + Pluto (`iio_create_scan_context`).
- `DeviceForm` структурно не меняется: тот же список, лишь метка типа в строке
(`PlutoSDR usb:1.5.5`). `BoardTypeName`/рендер дополнить веткой Pluto.
- `ConnectDevice` диспетчеризует по `Kind`: создаёт нужный бэкенд перед Connect.
### 4.4 Ключ per-device настроек
Минимальное изменение: для Pluto **синтезировать 6-байтовый ключ из serial**
(хэш serial → 6 байт) и подавать в существующие `LoadDevice/LoadTX/...`. Settings.pas
не трогаем. (Альтернатива — обобщить ключ до строки; дороже, отложить.)
### 4.5 Sample rate overlay
- `TSampleRateOverlay` получает список значений/имён от контроллера
(`SetRatePresets`). HPSDR — текущие 48k…1536k. Pluto — пресеты **> 560 кбит**,
напр. `768k / 1536k / 2304k / 3072k / 3840k / 5760k`.
- `SetSampleRate` для Pluto зовёт `FBackend` (set `sampling_frequency` +
`rf_bandwidth`), без `ConfigureDDCs`.
### 4.6 IQ-конвертация (формат WDSP не меняем)
- **RX:** `TPlutoBackend` читает int16 из `iio_buffer`, конвертирует 16→24-бит,
упаковывает в `TDDCIQPacket` (seq синтезируется), вызывает `OnDDCIQ`.
- **TX:** `SendDUCIQ` конвертирует 24→16-бит, пишет в tx-буфер libiio с пейсингом
(модель FIFO-cushion, см. memory `project_txiq_pacing`). ⚠️ TX-rate Pluto
≥ 521 ksps, WDSP TXA отдаёт 192k — нужен ресэмпл 192k→PlutoTX или смена TX-rate.
### 4.7 Усиление / телеметрия
- RX gain: контрол manual gain (073 дБ) / режим hw-AGC при `Caps.HasHWGain`
(можно расширить UI step-atten). WDSP AGC поверх.
- Телеметрия: S-meter (WDSP) — для обоих. fwd/SWR/supply/PLL/overload — скрыть при
`not Caps.HasPA`. Опц.: RSSI/температура Pluto.
---
## 5. Поэтапный план (фазы)
**Фаза 0 — Абстракция (без поведения Pluto)**
- `TRadioBackend` (база) + `TBackendCaps`; `THPSDRNetwork` реализует, заполняет
caps. Сменить тип `FNetwork`/вызовы. Регрессия: HPSDR как раньше. ✅
**Фаза 1 — libiio binding + discovery**
- `IIOBindings.pas`, `PlutoBackend.pas` (skeleton: scan/connect/disconnect/caps).
- `Kind/URI/Serial` в store, объединённый discovery, диспетчер по `Kind`.
- UI: оба типа в одном окне. ✅ (видим Pluto, ещё без RX).
**Фаза 2 — RX-тракт Pluto + VHF/UHF band-план**
- RX-поток libiio → 16→24-бит → `OnDDCIQ` → WDSP. Set freq (Hz в LO), gain,
sample rate, RF bandwidth.
- VHF/UHF band-план (§3.1) при `bkPluto`; samplerate-оверлей с Pluto-пресетами.
- Гейтинг UI по caps. ✅ приём на Pluto (спектр/водопад/аудио на 2м/70см/23см/13см).
**Фаза 3 — Управление/калибровка**
- RX gain / hw-AGC, XO ppm, транспорт (USB/IP), buffer/timeout, температура/RSSI.
- Persist per-device по synthetic-MAC из serial.
**Фаза 4 — TX-тракт Pluto**
- `SendDUCIQ` 24→16 + пейсинг; ресэмпл/смена TX-rate (§4.6); TX atten вместо
drive/PA; гейтинг PA/SWR/Alex/keyer. ✅ передача (SSB/CW/FM).
**Фаза 5 — Режим QO-100**
- `TQO100Settings` + раздельные RX/TX LO (§3.2), дисплей в downlink-терминах,
калибровка LNB по маяку, **full-duplex** (довести `SetDuplex`).
- UI: вход/выход в режим QO-100 (как XVTR-слот), поля LNB LO / ppm / TX atten.
- ✅ полноценная работа через спутник.
**Фаза 6 — Полировка**
- Web/CAT-фронтенды (команды уже агностичны) + гейтинг полей; демон `ewsdrd.lpr`
(discovery/connect Pluto headless); кросс-платформенная сборка с/без libiio; доки.
---
## 6. Риски и открытые вопросы
1. **2 м / 70 см ниже 325 МГц** → требуется freq-range мод прошивки Pluto.
Документировать; возможно гейтить эти кнопки при стоковой прошивке.
2. **TX sample-rate mismatch** (192k WDSP vs ≥521k Pluto) — ресэмпл или смена
TX-rate WDSP.
3. **QO-100 full-duplex** — довести `SetDuplex` (сейчас TODO) до реального
одновременного RX+TX; проверить латентность/буферы libiio.
4. **LNB дрейф** — калибровка `LnbPpm` по маяку (ручная/полуавтомат).
5. **Ключ настроек** — synthetic-MAC из serial (быстро) vs обобщение до строки.
6. **libiio как зависимость** — динамическая загрузка, поведение при отсутствии.
7. **USB-латентность/пропускная** — реальный максимум sample-rate по USB2.
---
## 7. Затрагиваемые файлы (ориентир)
| Файл | Изменение |
|-------------------------|-------------------------------------------------------|
| `RadioController.pas` | тип `FBackend`, диспетчер по Kind, гейтинг по caps, QO-100 трансляция |
| `HPSDRNetwork.pas` | реализовать `TRadioBackend` + заполнить caps |
| `IIOBindings.pas` (нов) | биндинг libiio (dynamic) |
| `PlutoBackend.pas` (нов)| `TPlutoBackend` (scan/connect/RX/TX/gain/rate/bw) |
| `DeviceStore.pas` | `Kind/URI/Serial` в записях |
| `DeviceForm.pas` | метка типа в списке (структурно без изменений) |
| `SampleRateOverlay.pas` | пресеты из контроллера вместо констант |
| `BoardUtils.pas` | имя Pluto, VHF/UHF band-план |
| `Settings.pas` | без изменений (synthetic-MAC) / QO-100 persist |
| `MainForm.pas` / `ewsdrd.lpr` | гейтинг виджетов по caps, wiring, QO-100 UI |
+16 -29
View File
@@ -17,36 +17,23 @@
<UseVersionInfo Value="True"/> <UseVersionInfo Value="True"/>
<AutoIncrementBuild Value="True"/> <AutoIncrementBuild Value="True"/>
<MinorVersionNr Value="9"/> <MinorVersionNr Value="9"/>
<BuildNr Value="192"/> <BuildNr Value="202"/>
</VersionInfo> </VersionInfo>
<MacroValues Count="27"> <MacroValues Count="14">
<Macro1 Name="LCLWidgetType" Value="cocoa"/> <Macro1 Name="LCLWidgetType" Value="qt6"/>
<Macro2 Name="LCLWidgetType" Value="cocoa"/> <Macro2 Name="LCLWidgetType" Value="qt6"/>
<Macro3 Name="LCLWidgetType" Value="cocoa"/> <Macro3 Name="LCLWidgetType" Value="qt6"/>
<Macro4 Name="LCLWidgetType" Value="cocoa"/> <Macro4 Name="LCLWidgetType" Value="qt6"/>
<Macro5 Name="LCLWidgetType" Value="cocoa"/> <Macro5 Name="LCLWidgetType" Value="qt6"/>
<Macro6 Name="LCLWidgetType" Value="cocoa"/> <Macro6 Name="LCLWidgetType" Value="qt6"/>
<Macro7 Name="LCLWidgetType" Value="cocoa"/> <Macro7 Name="LCLWidgetType" Value="qt6"/>
<Macro8 Name="LCLWidgetType" Value="cocoa"/> <Macro8 Name="LCLWidgetType" Value="qt6"/>
<Macro9 Name="LCLWidgetType" Value="cocoa"/> <Macro9 Name="LCLWidgetType" Value="qt6"/>
<Macro10 Name="LCLWidgetType" Value="cocoa"/> <Macro10 Name="LCLWidgetType" Value="qt6"/>
<Macro11 Name="LCLWidgetType" Value="cocoa"/> <Macro11 Name="LCLWidgetType" Value="qt6"/>
<Macro12 Name="LCLWidgetType" Value="cocoa"/> <Macro12 Name="LCLWidgetType" Value="qt6"/>
<Macro13 Name="LCLWidgetType" Value="cocoa"/> <Macro13 Name="LCLWidgetType" Value="qt6"/>
<Macro14 Name="LCLWidgetType" Value="cocoa"/> <Macro14 Name="LCLWidgetType" Value="qt6"/>
<Macro15 Name="LCLWidgetType" Value="cocoa"/>
<Macro16 Name="LCLWidgetType" Value="cocoa"/>
<Macro17 Name="LCLWidgetType" Value="cocoa"/>
<Macro18 Name="LCLWidgetType" Value="cocoa"/>
<Macro19 Name="LCLWidgetType" Value="cocoa"/>
<Macro20 Name="LCLWidgetType" Value="cocoa"/>
<Macro21 Name="LCLWidgetType" Value="cocoa"/>
<Macro22 Name="LCLWidgetType" Value="cocoa"/>
<Macro23 Name="LCLWidgetType" Value="cocoa"/>
<Macro24 Name="LCLWidgetType" Value="cocoa"/>
<Macro25 Name="LCLWidgetType" Value="cocoa"/>
<Macro26 Name="LCLWidgetType" Value="cocoa"/>
<Macro27 Name="LCLWidgetType" Value="cocoa"/>
</MacroValues> </MacroValues>
<BuildModes> <BuildModes>
<Item Name="Debug" Default="True"/> <Item Name="Debug" Default="True"/>
+83
View File
@@ -0,0 +1,83 @@
program test_pluto;
{$MODE Delphi}
// Ручной тест Pluto/AD936x-бэкенда против реального железа (без UI/DSP).
// Сборка: fpc -Mobjfpc -dHEADLESS -Fu. -FUlib-headless/<cpu-os> test_pluto.lpr
// Запуск: ./test_pluto ip:172.16.2.199
uses
{$IFDEF UNIX}cthreads,{$ENDIF}
SysUtils, Classes, ctypes, Math,
RadioBackend, IIOBindings, HPSDRProtocol, PlutoBackend;
type
// Приёмник OnDDCIQ: считает пакеты/пары и средний уровень I (dBFS).
THelper = class
Pkts: Integer;
Pairs: Int64;
SumSq: Double;
procedure OnIQ(DDCIndex: Integer; const Data: TDDCIQPacket);
end;
procedure THelper.OnIQ(DDCIndex: Integer; const Data: TDDCIQPacket);
const SCALE = 1.0 / 8388608.0;
var spf, i, pos, ir: Integer; fi: Double;
begin
spf := (Integer(Data.SamplesPerFrame[0]) shl 8) or Integer(Data.SamplesPerFrame[1]);
Inc(Pkts);
Pairs := Pairs + spf;
for i := 0 to spf - 1 do
begin
pos := i * 6;
ir := (Integer(Data.IQData[pos]) shl 16) or (Integer(Data.IQData[pos+1]) shl 8)
or Integer(Data.IQData[pos+2]);
if (ir and $800000) <> 0 then ir := ir or Integer($FF000000);
fi := ir * SCALE;
SumSq := SumSq + fi * fi;
end;
end;
var
URI: string;
pl: TPlutoBackend;
dev: TRadioDevice;
h: THelper;
rms: Double;
begin
if ParamCount >= 1 then URI := ParamStr(1) else URI := 'ip:172.16.2.199';
if not IIOLoad then begin writeln('FAIL: libiio not loaded'); Halt(1); end;
writeln('libiio loaded OK');
pl := TPlutoBackend.Create;
h := THelper.Create;
try
if not pl.ProbeURI(URI, dev) then begin writeln('FAIL: probe'); Halt(1); end;
writeln('PROBE OK: ', dev.Model, ' serial=', dev.Serial, ' ip=', dev.IPAddress);
if not pl.Connect(dev) then begin writeln('FAIL connect: ', pl.LastError); Halt(1); end;
writeln('CONNECT OK');
pl.OnDDCIQ := h.OnIQ;
pl.ConfigureDDCs(1, 1536, 0, True, True); // 1.536 MS/s
pl.SetRunAndFreq(True, 100000000, 435000000, 100); // RX 100 MHz (FM band)
writeln('RUN: streaming 800 ms @ RX=100MHz, rate=1.536M ...');
Sleep(800);
pl.SetRunAndFreq(False, 100000000, 435000000, 0);
if h.Pairs > 0 then rms := Sqrt(h.SumSq / h.Pairs) else rms := 0;
writeln(' packets=', h.Pkts, ' pairs=', h.Pairs,
' (~', (h.Pairs / 0.8 / 1e6):0:3, ' MS/s)');
if rms > 0 then
writeln(' I level: rms=', rms:0:5, ' (', (20*Log10(rms)):0:1, ' dBFS)')
else
writeln(' I level: zero (no signal/clip)');
pl.Disconnect;
writeln('DISCONNECT OK');
finally
h.Free;
pl.Free;
end;
writeln('DONE');
end.