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:
@@ -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.
|
||||
Reference in New Issue
Block a user