mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +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:
+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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user