mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
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:
+58
-15
@@ -7,12 +7,46 @@ unit BoardUtils;
|
||||
interface
|
||||
|
||||
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
|
||||
|
||||
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;
|
||||
begin
|
||||
case BoardType of
|
||||
@@ -27,24 +61,33 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
function FreqToBandIdx(Hz: Double): 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);
|
||||
function FreqToBandIdx(Hz: Double; Pluto: Boolean): Integer;
|
||||
var
|
||||
i: Integer;
|
||||
begin
|
||||
Result := -1;
|
||||
for i := 0 to 10 do
|
||||
if (Hz >= BAND_LO[i]) and (Hz <= BAND_HI[i]) then
|
||||
begin
|
||||
Result := i;
|
||||
Exit;
|
||||
end;
|
||||
if Pluto then
|
||||
begin
|
||||
for i := 0 to BAND_COUNT - 1 do
|
||||
if (Hz >= VU_LO[i]) and (Hz <= VU_HI[i]) then Exit(i);
|
||||
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.
|
||||
|
||||
+15
-2
@@ -7,7 +7,7 @@ interface
|
||||
uses
|
||||
Classes, SysUtils, FlatButton, FlatEdit, FlatListBox, AppTheme, Forms, Controls, Graphics, Dialogs,
|
||||
StdCtrls, ExtCtrls, ComCtrls,
|
||||
BoardUtils, PlatformUtils, DeviceStore;
|
||||
BoardUtils, PlatformUtils, DeviceStore, RadioBackend;
|
||||
|
||||
type
|
||||
// Результат диалога
|
||||
@@ -86,6 +86,9 @@ type
|
||||
function GetAutoStartBoardType: Integer;
|
||||
function GetSavedBoardType(Idx: Integer): Integer;
|
||||
|
||||
// Текст поля ручного ввода IP/URI (для probe сетевых Pluto при discovery).
|
||||
function ManualIP: string;
|
||||
|
||||
// Получить/сбросить результат
|
||||
procedure ClearResult;
|
||||
// Общее хранилище устройств — назначается владельцем (MainForm/контроллер)
|
||||
@@ -466,7 +469,12 @@ begin
|
||||
Exit;
|
||||
end;
|
||||
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;
|
||||
LstSaved.ItemIndex := NewIdx;
|
||||
end;
|
||||
@@ -518,6 +526,11 @@ begin
|
||||
ModalResult := mrCancel;
|
||||
end;
|
||||
|
||||
function TDeviceDialog.ManualIP: string;
|
||||
begin
|
||||
if EdIP <> nil then Result := Trim(EdIP.Text) else Result := '';
|
||||
end;
|
||||
|
||||
procedure TDeviceDialog.ClearResult;
|
||||
begin
|
||||
FResult.Accepted := False;
|
||||
|
||||
+41
-3
@@ -11,7 +11,7 @@ unit DeviceStore;
|
||||
interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, IniFiles, BoardUtils, PlatformUtils;
|
||||
Classes, SysUtils, IniFiles, BoardUtils, PlatformUtils, RadioBackend;
|
||||
|
||||
const
|
||||
DEVICE_CFG_NAME = 'hpsdr_devices.ini';
|
||||
@@ -23,6 +23,9 @@ type
|
||||
IPAddress: string;
|
||||
BoardType: Integer;
|
||||
AutoStart: Boolean; // запускать автоматически при старте
|
||||
Kind: TBackendKind; // bkHPSDR / bkPluto
|
||||
URI: string; // Pluto: 'ip:..' / 'usb:..'
|
||||
Serial: string; // Pluto serial
|
||||
end;
|
||||
|
||||
// Найденное устройство (результат discovery)
|
||||
@@ -31,6 +34,9 @@ type
|
||||
DisplayName: string;
|
||||
BoardType: Integer;
|
||||
MAC: array[0..5] of Byte; // нужен для preload-rate lookup (LoadDevice по MAC)
|
||||
Kind: TBackendKind; // bkHPSDR / bkPluto
|
||||
URI: string; // Pluto: context URI
|
||||
Serial: string; // Pluto serial
|
||||
end;
|
||||
|
||||
{ TDeviceStore }
|
||||
@@ -50,6 +56,8 @@ type
|
||||
function Saved(Idx: Integer): TSavedDevice;
|
||||
// Добавляет запись (Name пустое → 'HPSDR'); возвращает индекс новой записи.
|
||||
function AddSaved(const AName, IP: string; BoardType: Integer): Integer;
|
||||
// Добавляет Pluto-устройство (Kind=bkPluto) с URI/Serial.
|
||||
function AddSavedPluto(const AName, AURI, ASerial: string): Integer;
|
||||
procedure RemoveSaved(Idx: Integer);
|
||||
procedure SetAutoStart(Idx: Integer); // эксклюзивно: только одно autostart
|
||||
function AutoStartIP: string;
|
||||
@@ -60,7 +68,9 @@ type
|
||||
// ---- Найденные (discovery) ----
|
||||
procedure ClearDiscovered;
|
||||
procedure AddDiscovered(const IP, DisplayName: string; BoardType: Integer;
|
||||
const MAC: array of Byte);
|
||||
const MAC: array of Byte;
|
||||
Kind: TBackendKind = bkHPSDR;
|
||||
const URI: string = ''; const Serial: string = '');
|
||||
function DiscoveredCount: Integer;
|
||||
function Discovered(Idx: Integer): TDiscoveredDevice;
|
||||
// IP найденного устройства по IP (для preload-rate lookup и т.п.)
|
||||
@@ -100,6 +110,9 @@ begin
|
||||
FSaved[I].IPAddress := Ini.ReadString (Section, 'IP', '');
|
||||
FSaved[I].BoardType := Ini.ReadInteger(Section, 'BoardType', 0);
|
||||
FSaved[I].AutoStart := Ini.ReadBool (Section, 'AutoStart', False);
|
||||
FSaved[I].Kind := TBackendKind(Ini.ReadInteger(Section, 'Kind', 0));
|
||||
FSaved[I].URI := Ini.ReadString (Section, 'URI', '');
|
||||
FSaved[I].Serial := Ini.ReadString (Section, 'Serial', '');
|
||||
Inc(FSavedCount);
|
||||
end;
|
||||
finally
|
||||
@@ -123,6 +136,9 @@ begin
|
||||
Ini.WriteString (Section, 'IP', FSaved[I].IPAddress);
|
||||
Ini.WriteInteger(Section, 'BoardType', FSaved[I].BoardType);
|
||||
Ini.WriteBool (Section, 'AutoStart', FSaved[I].AutoStart);
|
||||
Ini.WriteInteger(Section, 'Kind', Ord(FSaved[I].Kind));
|
||||
Ini.WriteString (Section, 'URI', FSaved[I].URI);
|
||||
Ini.WriteString (Section, 'Serial', FSaved[I].Serial);
|
||||
end;
|
||||
finally
|
||||
Ini.Free;
|
||||
@@ -155,6 +171,25 @@ begin
|
||||
FSaved[Result].IPAddress := Trim(IP);
|
||||
FSaved[Result].BoardType := BoardType;
|
||||
FSaved[Result].AutoStart := False;
|
||||
FSaved[Result].Kind := bkHPSDR;
|
||||
FSaved[Result].URI := '';
|
||||
FSaved[Result].Serial := '';
|
||||
SaveSaved;
|
||||
end;
|
||||
|
||||
function TDeviceStore.AddSavedPluto(const AName, AURI, ASerial: string): Integer;
|
||||
begin
|
||||
Result := FSavedCount;
|
||||
Inc(FSavedCount);
|
||||
SetLength(FSaved, FSavedCount);
|
||||
if Trim(AName) <> '' then FSaved[Result].Name := Trim(AName)
|
||||
else FSaved[Result].Name := 'PlutoSDR';
|
||||
FSaved[Result].IPAddress := Trim(AURI); // в списке показываем URI
|
||||
FSaved[Result].BoardType := 0;
|
||||
FSaved[Result].AutoStart := False;
|
||||
FSaved[Result].Kind := bkPluto;
|
||||
FSaved[Result].URI := Trim(AURI);
|
||||
FSaved[Result].Serial := Trim(ASerial);
|
||||
SaveSaved;
|
||||
end;
|
||||
|
||||
@@ -233,7 +268,7 @@ begin
|
||||
end;
|
||||
|
||||
procedure TDeviceStore.AddDiscovered(const IP, DisplayName: string; BoardType: Integer;
|
||||
const MAC: array of Byte);
|
||||
const MAC: array of Byte; Kind: TBackendKind; const URI: string; const Serial: string);
|
||||
var
|
||||
Idx, J: Integer;
|
||||
begin
|
||||
@@ -243,6 +278,9 @@ begin
|
||||
FDiscovered[Idx].IPAddress := IP;
|
||||
FDiscovered[Idx].DisplayName := DisplayName;
|
||||
FDiscovered[Idx].BoardType := BoardType;
|
||||
FDiscovered[Idx].Kind := Kind;
|
||||
FDiscovered[Idx].URI := URI;
|
||||
FDiscovered[Idx].Serial := Serial;
|
||||
FillChar(FDiscovered[Idx].MAC, SizeOf(FDiscovered[Idx].MAC), 0);
|
||||
for J := 0 to High(MAC) do
|
||||
if J <= 5 then FDiscovered[Idx].MAC[J] := MAC[J];
|
||||
|
||||
+103
-64
@@ -19,7 +19,7 @@ uses
|
||||
{$ELSE}
|
||||
Sockets, BaseUnix,
|
||||
{$ENDIF}
|
||||
HPSDRProtocol, SyncObjs, Settings;
|
||||
HPSDRProtocol, SyncObjs, Settings, RadioBackend;
|
||||
|
||||
const
|
||||
DUC_TX_QUEUE_SIZE = 256; // power of two, bounded latency on TX underrun/overrun
|
||||
@@ -70,33 +70,19 @@ const
|
||||
|
||||
|
||||
type
|
||||
THPSDRDevice = record
|
||||
IPAddress: string;
|
||||
Port: Word;
|
||||
MAC: array[0..5] of Byte;
|
||||
BoardType: Byte;
|
||||
ProtocolVersion: Byte;
|
||||
FirmwareVersion: Byte;
|
||||
NumDDCs: Byte;
|
||||
FreqOrPhase: Byte;
|
||||
EndianModes: Byte;
|
||||
InUse: Boolean;
|
||||
Valid: Boolean;
|
||||
end;
|
||||
PHPSDRDevice = ^THPSDRDevice;
|
||||
THPSDRDeviceArray = array of THPSDRDevice;
|
||||
// Запись устройства и типы колбэков теперь общие (RadioBackend) — здесь
|
||||
// алиасы для обратной совместимости со старым кодом (THPSDRDevice и т.д.).
|
||||
THPSDRDevice = RadioBackend.TRadioDevice;
|
||||
PHPSDRDevice = RadioBackend.PRadioDevice;
|
||||
THPSDRDeviceArray = RadioBackend.TRadioDeviceArray;
|
||||
TOnDeviceFound = RadioBackend.TOnDeviceFound;
|
||||
TOnDDCIQPacket = RadioBackend.TOnDDCIQPacket;
|
||||
TOnMicPacket = RadioBackend.TOnMicPacket;
|
||||
TOnHPStatus = RadioBackend.TOnHPStatus;
|
||||
TOnWidebandFrame = RadioBackend.TOnWidebandFrame;
|
||||
|
||||
TOnDeviceFound = procedure(const Dev: THPSDRDevice) of object;
|
||||
TOnDDCIQPacket = procedure(DDCIndex: Integer;
|
||||
const Data: TDDCIQPacket) of object;
|
||||
TOnMicPacket = procedure(const Data: TMicDataPacket) of object;
|
||||
TOnHPStatus = procedure(const Status: THighPriorityStatus) of object;
|
||||
TOnWidebandFrame = procedure(ADCIndex: Integer;
|
||||
const Samples: array of SmallInt;
|
||||
Count: Integer) of object;
|
||||
|
||||
{ THPSDRNetwork }
|
||||
THPSDRNetwork = class
|
||||
{ THPSDRNetwork — openHPSDR Ethernet protocol бэкенд }
|
||||
THPSDRNetwork = class(TRadioBackend)
|
||||
private
|
||||
FSocket: TSocket;
|
||||
FLocalPort: Word;
|
||||
@@ -125,11 +111,8 @@ type
|
||||
FKeepaliveThread: TThread;
|
||||
FDUCIQThread: TThread;
|
||||
|
||||
FOnDeviceFound: TOnDeviceFound;
|
||||
FOnDDCIQ: TOnDDCIQPacket;
|
||||
FOnMic: TOnMicPacket;
|
||||
FOnHPStatus: TOnHPStatus;
|
||||
FOnWideband: TOnWidebandFrame;
|
||||
// Колбэки данных (FOnDeviceFound/FOnDDCIQ/FOnMic/FOnHPStatus/FOnWideband)
|
||||
// унаследованы из TRadioBackend (protected) — здесь не дублируем.
|
||||
|
||||
FPortDDCSpec: Word;
|
||||
FPortDUCSpec: Word;
|
||||
@@ -210,59 +193,61 @@ type
|
||||
function NextSeq(var S: LongWord): LongWord;
|
||||
procedure PackSeqBytes(var B: array of Byte; Seq: LongWord);
|
||||
|
||||
protected
|
||||
// Полиморфные геттеры состояния (свойства Connected/Running/Device/LastError/
|
||||
// DirectIP объявлены в TRadioBackend).
|
||||
function GetConnected: Boolean; override;
|
||||
function GetRunning: Boolean; override;
|
||||
function GetDevice: TRadioDevice; override;
|
||||
function GetLastError: string; override;
|
||||
function GetDirectIP: string; override;
|
||||
procedure SetDirectIP(const V: string); override;
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
|
||||
function Discover(TimeoutMs: Integer = 3000): THPSDRDeviceArray;
|
||||
function Connect(const Dev: THPSDRDevice): Boolean;
|
||||
procedure Disconnect;
|
||||
function Caps: TBackendCaps; override;
|
||||
|
||||
procedure SendGeneralPacket(const Pkt: TGeneralPacket);
|
||||
function Discover(TimeoutMs: Integer = 3000): THPSDRDeviceArray; override;
|
||||
function Connect(const Dev: THPSDRDevice): Boolean; override;
|
||||
procedure Disconnect; override;
|
||||
|
||||
procedure SendGeneralPacket(const Pkt: TGeneralPacket); override;
|
||||
procedure ConfigureWideband(ADCIndex: Integer; Enabled: Boolean;
|
||||
SamplesPerPacket: Integer = 512; SampleBits: Integer = 16;
|
||||
UpdateRateMS: Integer = 70; PacketsPerFrame: Integer = 32);
|
||||
procedure SendDDCSpecific(const Pkt: TDDCSpecificPacket);
|
||||
UpdateRateMS: Integer = 70; PacketsPerFrame: Integer = 32); override;
|
||||
procedure SendDDCSpecific(const Pkt: TDDCSpecificPacket); override;
|
||||
procedure ConfigureDDCs(NumDDCs: Byte; SampleRate: Word; ADCSource: Byte = 0;
|
||||
DitherEnabled: Boolean = True; RandomEnabled: Boolean = True);
|
||||
procedure SendDUCSpecific(const Pkt: TDUCSpecificPacket);
|
||||
procedure SendHighPriority(const Pkt: THighPriorityPacket);
|
||||
DitherEnabled: Boolean = True; RandomEnabled: Boolean = True); override;
|
||||
procedure SendDUCSpecific(const Pkt: TDUCSpecificPacket); override;
|
||||
procedure SendHighPriority(const Pkt: THighPriorityPacket); override;
|
||||
procedure SetRunAndFreq(Run: Boolean; DDC0FreqHz, DUCFreqHz: Double;
|
||||
DriveLevel: Byte = 100);
|
||||
DriveLevel: Byte = 100); override;
|
||||
procedure UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte;
|
||||
Transmitting, PAEnabled, AlexEnabled: Boolean);
|
||||
procedure SetStepAtten(dB: Byte);
|
||||
procedure SetAlexConfig(const A: TAlexSettings);
|
||||
Transmitting, PAEnabled, AlexEnabled: Boolean); override;
|
||||
procedure SetStepAtten(dB: Byte); override;
|
||||
procedure SetAlexConfig(const A: TAlexSettings); override;
|
||||
// Включение/настройка XVTR-режима. Все параметры применяются мгновенно
|
||||
// (отправляется HP-пакет). Когда AEnable=False, XVTR-bit гасится,
|
||||
// антенна и Alex routing возвращаются к per-band дефолту.
|
||||
procedure SetXvtrMode(AEnable, ADisablePA: Boolean; ARxAnt: Byte);
|
||||
procedure SendFullHP;
|
||||
procedure SetXvtrMode(AEnable, ADisablePA: Boolean; ARxAnt: Byte); override;
|
||||
procedure SendFullHP; override;
|
||||
// Отдельная отправка PTT: переключает реле (bit27) и drive ДО, потом PTT.
|
||||
// Не меняет FIsTransmitting — вызывать после UpdateState+SendFullHP.
|
||||
procedure SendPTT(Active: Boolean);
|
||||
procedure SendDDCAudio(const LeftRight: array of SmallInt);
|
||||
procedure SendPTT(Active: Boolean); override;
|
||||
procedure SendDDCAudio(const LeftRight: array of SmallInt); override;
|
||||
// Принимает RX-аудио (float [-1..1]) из DSP, конвертирует в 16-bit и
|
||||
// отправляет на радио пакетами по 64 L/R пары. Хвост копится между вызовами.
|
||||
procedure SendSpeakerAudio(const Left, Right: array of Single; Count: Integer);
|
||||
procedure SendSpeakerAudio(const Left, Right: array of Single; Count: Integer); override;
|
||||
// Единый переключатель встроенного динамика: True = слать аудио и снять мьют,
|
||||
// False = замьютить динамик (byte 1400 bit1) и не накапливать поток.
|
||||
// Применяется немедленно (шлёт HP-пакет, если подключены).
|
||||
procedure SetSpeakerAudio(AEnabled: Boolean);
|
||||
procedure SendDUCIQ(const IData, QData: array of Integer);
|
||||
procedure ClearDUCIQQueue;
|
||||
procedure SetSpeakerAudio(AEnabled: Boolean); override;
|
||||
procedure SendDUCIQ(const IData, QData: array of Integer); override;
|
||||
procedure ClearDUCIQQueue; override;
|
||||
|
||||
property Connected: Boolean read FConnected;
|
||||
property Running: Boolean read FRunning;
|
||||
property Device: THPSDRDevice read FDevice;
|
||||
property LocalPort: Word read FLocalPort;
|
||||
property LastError: string read FLastError;
|
||||
property OnDeviceFound: TOnDeviceFound read FOnDeviceFound write FOnDeviceFound;
|
||||
property OnDDCIQ: TOnDDCIQPacket read FOnDDCIQ write FOnDDCIQ;
|
||||
property OnMicPacket: TOnMicPacket read FOnMic write FOnMic;
|
||||
property OnHPStatus: TOnHPStatus read FOnHPStatus write FOnHPStatus;
|
||||
property OnWideband: TOnWidebandFrame read FOnWideband write FOnWideband;
|
||||
property DirectIP: string read FDirectIP write FDirectIP; // unicast discovery
|
||||
// LocalPort — HPSDR-специфично (локальный UDP-порт), не часть базы.
|
||||
property LocalPort: Word read FLocalPort;
|
||||
end;
|
||||
|
||||
implementation
|
||||
@@ -599,6 +584,60 @@ end;
|
||||
// THPSDRNetwork
|
||||
// ===========================================================================
|
||||
|
||||
// ---- Полиморфные геттеры состояния (TRadioBackend) ----
|
||||
function THPSDRNetwork.GetConnected: Boolean;
|
||||
begin
|
||||
Result := FConnected;
|
||||
end;
|
||||
|
||||
function THPSDRNetwork.GetRunning: Boolean;
|
||||
begin
|
||||
Result := FRunning;
|
||||
end;
|
||||
|
||||
function THPSDRNetwork.GetDevice: TRadioDevice;
|
||||
begin
|
||||
Result := FDevice;
|
||||
end;
|
||||
|
||||
function THPSDRNetwork.GetLastError: string;
|
||||
begin
|
||||
Result := FLastError;
|
||||
end;
|
||||
|
||||
function THPSDRNetwork.GetDirectIP: string;
|
||||
begin
|
||||
Result := FDirectIP;
|
||||
end;
|
||||
|
||||
procedure THPSDRNetwork.SetDirectIP(const V: string);
|
||||
begin
|
||||
FDirectIP := V;
|
||||
end;
|
||||
|
||||
// ---- Возможности HPSDR-бэкенда ----
|
||||
function THPSDRNetwork.Caps: TBackendCaps;
|
||||
begin
|
||||
FillChar(Result, SizeOf(Result), 0);
|
||||
Result.Kind := bkHPSDR;
|
||||
Result.HasTX := True;
|
||||
Result.HasPA := True;
|
||||
Result.HasAlex := True;
|
||||
Result.HasWideband := True;
|
||||
Result.HasDitherRandom := True;
|
||||
Result.HasHWMic := True;
|
||||
Result.HasPLLStatus := True;
|
||||
Result.HasHWGain := False;
|
||||
Result.HasRFBandwidth := False;
|
||||
Result.HasFullDuplex := True;
|
||||
Result.MinSampleRate := 48000;
|
||||
Result.MaxSampleRate := 1536000;
|
||||
Result.SampleRateMode := srmDiscrete;
|
||||
Result.RatePresets := nil; // контроллер использует свой HPSDR-набор
|
||||
Result.MinFreqHz := 0;
|
||||
Result.MaxFreqHz := 61440000;
|
||||
end;
|
||||
|
||||
constructor THPSDRNetwork.Create;
|
||||
begin
|
||||
inherited;
|
||||
|
||||
+199
@@ -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. Этого набора хватает
|
||||
на фазы 1–4 (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
@@ -42,6 +42,7 @@ uses
|
||||
ChannelStore, ChannelsForm,
|
||||
PowerInhibit,
|
||||
DeviceStore,
|
||||
RadioBackend, PlutoBackend,
|
||||
RadioController, ChannelController;
|
||||
|
||||
const
|
||||
@@ -324,6 +325,9 @@ type
|
||||
procedure OnWidebandCB(ADCIndex: Integer; const Samples: array of SmallInt;
|
||||
Count: Integer);
|
||||
procedure OnWDSPOpenDone(Success: Boolean);
|
||||
function ResolveDevice(const IP: string): THPSDRDevice;
|
||||
procedure RelabelBands; // подписи band-кнопок под план бэкенда
|
||||
procedure ApplyBackendCapsToUI; // sample-rate пресеты + гейтинг полей по Caps
|
||||
procedure DoConnectDevice(const Dev: THPSDRDevice);
|
||||
// Старт с уже выбранным устройством (preload rate + WDSP open + connect).
|
||||
// Переиспользуется десктоп-диалогом и web-оверлеем — без модального диалога.
|
||||
@@ -1313,6 +1317,8 @@ begin
|
||||
// возвращаем живое значение в persist-буфер, чтобы blob нёс актуальную тему.
|
||||
FController.FLoadedGlobal.LightTheme := FLightTheme;
|
||||
FCATAdapter.ApplySettings(FController.FLoadedGlobal);
|
||||
RelabelBands; // HF ↔ VHF/UHF подписи под тип бэкенда
|
||||
ApplyBackendCapsToUI; // пресеты sample rate + гейтинг полей
|
||||
RebuildXvtrButtons;
|
||||
PushXvtrToWeb;
|
||||
TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive%
|
||||
@@ -2664,10 +2670,12 @@ const
|
||||
MARGIN_Y = 4;
|
||||
begin
|
||||
if (FSampleRateOverlay = nil) or (PbSpectrum = nil) then Exit;
|
||||
// Ширина = доступная ширина спектра справа от MARGIN_X (контент сам клампится
|
||||
// к этому пределу). Раньше фиксировалось 320px → 8 Pluto-кнопок не влезали.
|
||||
FSampleRateOverlay.SetBounds(
|
||||
MARGIN_X,
|
||||
MARGIN_Y,
|
||||
FSampleRateOverlay.Width,
|
||||
Max(320, PbSpectrum.Width - MARGIN_X * 2),
|
||||
FSampleRateOverlay.Height);
|
||||
end;
|
||||
|
||||
@@ -3296,7 +3304,40 @@ end;
|
||||
procedure TMainForm.BtnDiscoverFromDialog(Sender: TObject);
|
||||
// Запускается когда пользователь нажимает DISCOVER внутри диалога.
|
||||
// Контроллер чистит список и стартует поток; 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
|
||||
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...');
|
||||
FController.Discover;
|
||||
end;
|
||||
@@ -3352,13 +3393,52 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
// Создаём запись устройства из IP + BoardType (из диалога/autostart) и стартуем.
|
||||
FillChar(Dev, SizeOf(Dev), 0);
|
||||
Dev.IPAddress := FPendingIP;
|
||||
Dev.BoardType := FPendingBoardType;
|
||||
// Создаём запись устройства. Если это известный (discovered/saved) Pluto —
|
||||
// восстанавливаем Kind/URI/Serial/MAC из store по IP; иначе HPSDR по IP.
|
||||
Dev := ResolveDevice(FPendingIP);
|
||||
if Dev.Kind = bkHPSDR then Dev.BoardType := FPendingBoardType;
|
||||
StartWithDevice(Dev);
|
||||
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;
|
||||
// STOP-teardown, общий для десктоп-STOP и web-disconnect. Движок (graceful Run=0,
|
||||
// сброс TX/телеметрии) + персист настроек + Disconnect — в контроллере
|
||||
@@ -3435,6 +3515,33 @@ begin
|
||||
DoConnectDevice(FPendingDev);
|
||||
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);
|
||||
begin
|
||||
BtnStartStop.Enabled := True;
|
||||
@@ -3798,7 +3905,7 @@ begin
|
||||
|
||||
if FController.FCurrentXvtr < 0 then
|
||||
begin
|
||||
BandIdx := FreqToBandIdx(ClickFreq);
|
||||
BandIdx := FreqToBandIdx(ClickFreq, FController.IsPluto);
|
||||
if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then
|
||||
begin
|
||||
if FController.FDevConnected then
|
||||
@@ -3834,7 +3941,7 @@ begin
|
||||
end;
|
||||
if FController.FCurrentXvtr < 0 then
|
||||
begin
|
||||
BandIdx := FreqToBandIdx(FController.FVfoB);
|
||||
BandIdx := FreqToBandIdx(FController.FVfoB, FController.IsPluto);
|
||||
if (BandIdx >= 0) and (BandIdx <> FController.FCurrentBand) then
|
||||
begin
|
||||
if FController.FCurrentBand >= 0 then
|
||||
@@ -5114,10 +5221,14 @@ begin
|
||||
NewFreq := Base; // снэп к нижней границе
|
||||
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
|
||||
FController.FXvtrSettings.Entries[FController.FCurrentXvtr].Enabled then
|
||||
NewFreq := Max(0, NewFreq)
|
||||
else if FController.IsPluto then
|
||||
NewFreq := Max(Round(FController.BackendCaps.MinFreqHz),
|
||||
Min(Round(FController.BackendCaps.MaxFreqHz), NewFreq))
|
||||
else
|
||||
NewFreq := Max(30000, Min(60000000, NewFreq));
|
||||
if FController.FActiveVfo = 0 then
|
||||
@@ -5329,8 +5440,17 @@ begin
|
||||
FController.FLoadedGlobal.FreqMhzDigits := Digits; // зеркало в persist-буфер контроллера
|
||||
FreqDispA.MinMhzDigits := Digits;
|
||||
FreqDispB.MinMhzDigits := Digits;
|
||||
FreqDispA.MaxFreq := MaxFreqs[Digits];
|
||||
FreqDispB.MaxFreq := MaxFreqs[Digits];
|
||||
// Pluto/VHF/UHF: верхняя граница из Caps (до 6 ГГц), иначе HF по числу цифр.
|
||||
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
|
||||
FWebServer.FreqMhzDigits := Digits;
|
||||
LayoutTopVfoBlock;
|
||||
|
||||
@@ -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 (для фаз 2–4):
|
||||
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.
|
||||
@@ -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
@@ -48,7 +48,7 @@ interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, Math,
|
||||
HPSDRProtocol, HPSDRNetwork,
|
||||
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
||||
WDSPEngine, AudioOutput, AudioInput,
|
||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
||||
|
||||
@@ -145,7 +145,9 @@ type
|
||||
FWaterfallBufCount: Integer;
|
||||
|
||||
// ---- Движки (владение: см. шапку юнита) ----
|
||||
FNetwork: THPSDRNetwork;
|
||||
// FNetwork — абстрактный бэкенд железа (TRadioBackend): конкретный класс
|
||||
// (THPSDRNetwork / TPlutoBackend позже) выбирается при подключении.
|
||||
FNetwork: TRadioBackend;
|
||||
FDSPEngine: TWDSPEngine;
|
||||
FAudioOut: TAudioOutput;
|
||||
FAudioIn: TAudioInput;
|
||||
@@ -272,6 +274,8 @@ type
|
||||
// через Invoke; Synchronize блокирующий — устройства приходят по одному).
|
||||
FPendingFoundDev: THPSDRDevice;
|
||||
FPendingFoundEntry: string;
|
||||
// URI для probe сетевых Pluto при discovery (см. SetPlutoProbeURIs).
|
||||
FPlutoProbeURIs: array of string;
|
||||
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
@@ -447,6 +451,17 @@ type
|
||||
function ConnectAndRun(const Dev: THPSDRDevice; SpectrumWidth: Integer): Boolean; // ConnectDevice + band/xvtr/vfo restore + go-live
|
||||
procedure StopAndDisconnect; // graceful Run=0 + персист настроек + Disconnect (headless)
|
||||
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;
|
||||
@@ -1074,8 +1089,8 @@ begin
|
||||
// Bandstack: VFO A и B независимы и могут стоять на разных бэндах. В кэш
|
||||
// ЭТОГО бэнда пишем только ту частоту VFO, что реально в нём — иначе чужая
|
||||
// частота (напр. VFO A на 40м при сохранении 30м) загрязнит memory бэнда.
|
||||
if FreqToBandIdx(FVfoA) <> FCurrentBand then B.VfoA := FBandCache[FCurrentBand].VfoA;
|
||||
if FreqToBandIdx(FVfoB) <> FCurrentBand then B.VfoB := FBandCache[FCurrentBand].VfoB;
|
||||
if FreqToBandIdx(FVfoA, IsPluto) <> FCurrentBand then B.VfoA := FBandCache[FCurrentBand].VfoA;
|
||||
if FreqToBandIdx(FVfoB, IsPluto) <> FCurrentBand then B.VfoB := FBandCache[FCurrentBand].VfoB;
|
||||
FBandCache[FCurrentBand] := B;
|
||||
FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]);
|
||||
end;
|
||||
@@ -1220,6 +1235,14 @@ begin
|
||||
B := FBandCache[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);
|
||||
|
||||
// --- VFO / drive (только RestoreBand — bandstack-путь VFO не трогает) ---
|
||||
@@ -1303,7 +1326,7 @@ begin
|
||||
// FreqToBandIdx вернёт -1 для VHF/UHF).
|
||||
if FCurrentXvtr < 0 then
|
||||
begin
|
||||
BandIdx := FreqToBandIdx(FVfoA);
|
||||
BandIdx := FreqToBandIdx(FVfoA, IsPluto);
|
||||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||||
begin
|
||||
FCurrentBand := BandIdx;
|
||||
@@ -1333,7 +1356,7 @@ begin
|
||||
FVfoA := Hz;
|
||||
if FCurrentXvtr < 0 then
|
||||
begin
|
||||
BandIdx := FreqToBandIdx(FVfoA);
|
||||
BandIdx := FreqToBandIdx(FVfoA, IsPluto);
|
||||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||||
begin
|
||||
FCurrentBand := BandIdx;
|
||||
@@ -1369,7 +1392,7 @@ begin
|
||||
ApplyTuneCore(FVfoB);
|
||||
if FCurrentXvtr < 0 then
|
||||
begin
|
||||
BandIdx := FreqToBandIdx(FVfoB);
|
||||
BandIdx := FreqToBandIdx(FVfoB, IsPluto);
|
||||
if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then
|
||||
begin
|
||||
FCurrentBand := BandIdx;
|
||||
@@ -1403,7 +1426,7 @@ begin
|
||||
// --- Bandstack: активируемый VFO на другом ham-бэнде → восстановить бэнд ---
|
||||
if FCurrentXvtr < 0 then
|
||||
begin
|
||||
NewBand := FreqToBandIdx(ActiveFreq);
|
||||
NewBand := FreqToBandIdx(ActiveFreq, IsPluto);
|
||||
if (NewBand >= 0) and (NewBand <> FCurrentBand) then
|
||||
begin
|
||||
SaveCurrentBand; // сохранить покидаемый бэнд
|
||||
@@ -1482,7 +1505,7 @@ begin
|
||||
// при подсвеченной кнопке нового бэнда (рассинхрон).
|
||||
if FCurrentXvtr < 0 then
|
||||
begin
|
||||
NewBand := FreqToBandIdx(ActiveFreq);
|
||||
NewBand := FreqToBandIdx(ActiveFreq, IsPluto);
|
||||
if (NewBand >= 0) and (NewBand <> FCurrentBand) then
|
||||
begin
|
||||
SaveCurrentBand;
|
||||
@@ -2303,6 +2326,8 @@ function TRadioController.ConnectDevice(const Dev: THPSDRDevice): Boolean;
|
||||
var
|
||||
i: Integer;
|
||||
begin
|
||||
// Подбираем бэкенд под тип устройства (HPSDR/Pluto) до подключения.
|
||||
EnsureBackend(Dev.Kind);
|
||||
Result := FNetwork.Connect(Dev);
|
||||
if not Result then Exit;
|
||||
|
||||
@@ -2341,6 +2366,14 @@ begin
|
||||
FSampleRate := FLoadedGlobal.SampleRate;
|
||||
FSpanHz := FSampleRate;
|
||||
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);
|
||||
FNBMode := EnsureRange(FLoadedGlobal.NBMode, 0, 2);
|
||||
FSNB := FLoadedGlobal.SNBEnabled;
|
||||
@@ -2420,6 +2453,16 @@ begin
|
||||
// Перестройка диапазона — команда несёт финальную перестройку по активному VFO.
|
||||
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 уже
|
||||
// загружен (HF-диапазон до XVTR) — ActivateXvtr только выставит FVfoA/FCurrentXvtr.
|
||||
if (FLoadedGlobal.LastXvtr >= 0) and (FLoadedGlobal.LastXvtr < CFG_XVTR_COUNT)
|
||||
@@ -2482,25 +2525,103 @@ begin
|
||||
end;
|
||||
|
||||
procedure TDiscoverThread.Execute;
|
||||
// Сканируем ОБА бэкенда транзиентными экземплярами (независимо от FNetwork —
|
||||
// он может быть уже подключён/другого типа). Найденные сигналятся колбэком
|
||||
// OnDeviceFound во время Discover() и копятся в store.
|
||||
var
|
||||
Devs: THPSDRDeviceArray;
|
||||
Found: Integer;
|
||||
HP: THPSDRNetwork;
|
||||
Pluto: TPlutoBackend;
|
||||
PDev: TRadioDevice;
|
||||
i: Integer;
|
||||
begin
|
||||
Devs := FCtrl.FNetwork.Discover(2000);
|
||||
// Найденные устройства сигналятся колбэком OnDeviceFound во время Discover().
|
||||
if Length(Devs) = 0 then
|
||||
Found := 0;
|
||||
|
||||
// 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);
|
||||
end;
|
||||
|
||||
procedure TRadioController.OnDeviceFound(const Dev: THPSDRDevice);
|
||||
// Сетевой поток: строим строку для списка → pending → маршалинг в ApplyDeviceFound.
|
||||
procedure TRadioController.EnsureBackend(Kind: TBackendKind);
|
||||
// Пересоздаёт FNetwork нужного класса, перенося колбэки данных. No-op если тип
|
||||
// уже верный. Старый бэкенд отключается и освобождается.
|
||||
var
|
||||
BoardName: string;
|
||||
NewB: TRadioBackend;
|
||||
begin
|
||||
BoardName := BoardTypeName(Dev.BoardType);
|
||||
FPendingFoundDev := Dev;
|
||||
FPendingFoundEntry := Format('%s %s FW:%d DDC:%d',
|
||||
[Dev.IPAddress, BoardName,
|
||||
Dev.FirmwareVersion, Dev.NumDDCs]);
|
||||
if (FNetwork <> nil) and (FNetwork.Caps.Kind = Kind) then Exit;
|
||||
|
||||
case Kind of
|
||||
bkPluto: NewB := TPlutoBackend.Create;
|
||||
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);
|
||||
end;
|
||||
|
||||
@@ -2509,7 +2630,9 @@ procedure TRadioController.ApplyDeviceFound;
|
||||
begin
|
||||
FDiscoverNoneFound := False;
|
||||
FDeviceStore.AddDiscovered(FPendingFoundDev.IPAddress, FPendingFoundEntry,
|
||||
FPendingFoundDev.BoardType, FPendingFoundDev.MAC);
|
||||
FPendingFoundDev.BoardType, FPendingFoundDev.MAC,
|
||||
FPendingFoundDev.Kind, FPendingFoundDev.URI,
|
||||
FPendingFoundDev.Serial);
|
||||
Changed(rfDeviceList);
|
||||
end;
|
||||
|
||||
|
||||
+48
-19
@@ -22,6 +22,11 @@ type
|
||||
TSampleRateOverlay = class(TComponent)
|
||||
private
|
||||
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;
|
||||
FOnSpanSelect: TSpanSelectEvent;
|
||||
FOnHidePanel: THidePanelEvent;
|
||||
@@ -31,8 +36,8 @@ type
|
||||
FWidth: Integer;
|
||||
FHeight: Integer;
|
||||
|
||||
FHitRects: array[0..6] of TRect; // [0]=hide button, [1..6]=span buttons
|
||||
FHitValues: array[0..6] of Integer; // 0=hide, else sample rate
|
||||
FHitRects: array[0..10] of TRect; // [0]=hide button, [1..]=span buttons
|
||||
FHitValues: array[0..10] of Integer; // 0=hide, else sample rate
|
||||
FHitCount: Integer;
|
||||
FHotIdx: Integer;
|
||||
|
||||
@@ -67,6 +72,9 @@ type
|
||||
function HandleMouseDown(Button: TMouseButton; X, Y: Integer): Boolean;
|
||||
function HandleMouseLeave: Boolean;
|
||||
procedure SetCurrentRate(Rate: Integer);
|
||||
// Задаёт пресеты (Гц, до 6 шт.); имена формируются как '<kHz>k'.
|
||||
procedure SetRatePresets(const Rates: array of Integer);
|
||||
procedure SetRatePresetsDefault; // HPSDR 48k..1536k
|
||||
procedure SetPanelHidden(Hidden: Boolean);
|
||||
property Left: Integer read FLeft;
|
||||
property Top: Integer read FTop;
|
||||
@@ -139,14 +147,15 @@ begin
|
||||
C.Font.Name := 'Courier New';
|
||||
C.Font.Size := 7;
|
||||
C.Font.Style := [];
|
||||
Result := C.TextWidth(Name) + 10;
|
||||
if Result < 36 then Result := 36;
|
||||
Result := C.TextWidth(Name) + 8;
|
||||
if Result < 30 then Result := 30;
|
||||
end;
|
||||
|
||||
constructor TSampleRateOverlay.Create(AOwner: TComponent);
|
||||
begin
|
||||
inherited Create(AOwner);
|
||||
FCurrentRate := 192000;
|
||||
SetRatePresetsDefault;
|
||||
FPanelHidden := False;
|
||||
FHitCount := 0;
|
||||
FHotIdx := -1;
|
||||
@@ -207,10 +216,10 @@ begin
|
||||
TH := C.TextHeight('A');
|
||||
|
||||
ContentW := PAD_X + HIDE_W + BTN_GAP;
|
||||
for I := 0 to High(SPAN_NAMES) do
|
||||
for I := 0 to FRateCount-1 do
|
||||
begin
|
||||
Inc(ContentW, SpanBtnWidth(SPAN_NAMES[I], C));
|
||||
if I < High(SPAN_NAMES) then Inc(ContentW, BTN_GAP);
|
||||
Inc(ContentW, SpanBtnWidth(FNames[I], C));
|
||||
if I < FRateCount-1 then Inc(ContentW, BTN_GAP);
|
||||
end;
|
||||
Inc(ContentW, PAD_X);
|
||||
if ContentW > W then ContentW := W;
|
||||
@@ -247,11 +256,11 @@ begin
|
||||
RegisterHit(HR, 0);
|
||||
Inc(X, HIDE_W + BTN_GAP);
|
||||
|
||||
for I := 0 to High(SPAN_RATES) do
|
||||
for I := 0 to FRateCount-1 do
|
||||
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);
|
||||
IsAct := SPAN_RATES[I] = FCurrentRate;
|
||||
IsAct := FRates[I] = FCurrentRate;
|
||||
IsHot := FHotIdx = I + 1;
|
||||
|
||||
if IsAct then BlendRect(Target, HR, $20, $5A, $78, 220)
|
||||
@@ -262,13 +271,13 @@ begin
|
||||
C.Brush.Style := bsClear;
|
||||
C.Rectangle(HR);
|
||||
|
||||
TW := C.TextWidth(SPAN_NAMES[I]);
|
||||
TW := C.TextWidth(FNames[I]);
|
||||
C.Font.Color := CLR_TEXT;
|
||||
C.Brush.Style := bsClear;
|
||||
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);
|
||||
end;
|
||||
end;
|
||||
@@ -278,10 +287,10 @@ var I, CW: Integer;
|
||||
begin
|
||||
C.Font.Name := 'Courier New'; C.Font.Size := 7; C.Font.Style := [];
|
||||
CW := PAD_X + HIDE_W + BTN_GAP;
|
||||
for I := 0 to High(SPAN_NAMES) do
|
||||
for I := 0 to FRateCount-1 do
|
||||
begin
|
||||
Inc(CW, SpanBtnWidth(SPAN_NAMES[I], C));
|
||||
if I < High(SPAN_NAMES) then Inc(CW, BTN_GAP);
|
||||
Inc(CW, SpanBtnWidth(FNames[I], C));
|
||||
if I < FRateCount-1 then Inc(CW, BTN_GAP);
|
||||
end;
|
||||
Inc(CW, PAD_X);
|
||||
if CW > AvailW then CW := AvailW;
|
||||
@@ -444,10 +453,10 @@ begin
|
||||
Target.Canvas.Font.Size := 7;
|
||||
Target.Canvas.Font.Style := [];
|
||||
ContentW := PAD_X + HIDE_W + BTN_GAP;
|
||||
for I := 0 to High(SPAN_NAMES) do
|
||||
for I := 0 to FRateCount-1 do
|
||||
begin
|
||||
Inc(ContentW, SpanBtnWidth(SPAN_NAMES[I], Target.Canvas));
|
||||
if I < High(SPAN_NAMES) then Inc(ContentW, BTN_GAP);
|
||||
Inc(ContentW, SpanBtnWidth(FNames[I], Target.Canvas));
|
||||
if I < FRateCount-1 then Inc(ContentW, BTN_GAP);
|
||||
end;
|
||||
Inc(ContentW, PAD_X);
|
||||
ContentW := Min(ContentW, W);
|
||||
@@ -524,6 +533,26 @@ begin
|
||||
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);
|
||||
begin
|
||||
if FCurrentRate = Rate then Exit;
|
||||
|
||||
@@ -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 практически ~5–6 MSPS) |
|
||||
| RF-полоса (аналог. фильтр) | нет (фикс CIC) | отдельная настройка RX/TX LPF |
|
||||
| Диапазон частот | 0–61.44 МГц (HF/6m) | 325 МГц–3.8 ГГц (сток), **70 МГц–6 ГГц с хаком**. HF не используем |
|
||||
| Разрядность IQ | 24-бит | 12-бит ADC, по шине 16-бит |
|
||||
| Усиление RX | step atten + dither/random | manual gain 0–73 дБ / 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 м | 144–146 МГц | требует freq-range хак Pluto (<325 МГц) |
|
||||
| 70 см | 430–440 МГц | требует freq-range хак Pluto |
|
||||
| 23 см | 1240–1300 МГц | нативно |
|
||||
| 13 см | 2300–2450 МГц | нативно (включает 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.50–10489.99 МГц (10 ГГц). Принимается через LNB:
|
||||
LNB LO (обычно 9750 МГц) → Pluto RX ≈ 739.5–739.99 МГц. Нативный диапазон Pluto.
|
||||
- **Uplink (TX):** 2400.05–2400.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 (0–73 дБ) / режим 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 |
|
||||
@@ -17,36 +17,23 @@
|
||||
<UseVersionInfo Value="True"/>
|
||||
<AutoIncrementBuild Value="True"/>
|
||||
<MinorVersionNr Value="9"/>
|
||||
<BuildNr Value="192"/>
|
||||
<BuildNr Value="202"/>
|
||||
</VersionInfo>
|
||||
<MacroValues Count="27">
|
||||
<Macro1 Name="LCLWidgetType" Value="cocoa"/>
|
||||
<Macro2 Name="LCLWidgetType" Value="cocoa"/>
|
||||
<Macro3 Name="LCLWidgetType" Value="cocoa"/>
|
||||
<Macro4 Name="LCLWidgetType" Value="cocoa"/>
|
||||
<Macro5 Name="LCLWidgetType" Value="cocoa"/>
|
||||
<Macro6 Name="LCLWidgetType" Value="cocoa"/>
|
||||
<Macro7 Name="LCLWidgetType" Value="cocoa"/>
|
||||
<Macro8 Name="LCLWidgetType" Value="cocoa"/>
|
||||
<Macro9 Name="LCLWidgetType" Value="cocoa"/>
|
||||
<Macro10 Name="LCLWidgetType" Value="cocoa"/>
|
||||
<Macro11 Name="LCLWidgetType" Value="cocoa"/>
|
||||
<Macro12 Name="LCLWidgetType" Value="cocoa"/>
|
||||
<Macro13 Name="LCLWidgetType" Value="cocoa"/>
|
||||
<Macro14 Name="LCLWidgetType" Value="cocoa"/>
|
||||
<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 Count="14">
|
||||
<Macro1 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro2 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro3 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro4 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro5 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro6 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro7 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro8 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro9 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro10 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro11 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro12 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro13 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro14 Name="LCLWidgetType" Value="qt6"/>
|
||||
</MacroValues>
|
||||
<BuildModes>
|
||||
<Item Name="Debug" Default="True"/>
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user