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.