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 при ручном режиме FTxAttDb: Double; // hardwaregain voltage0(out), дБ (<0); off на приёме // RX-стрим (cf-ad9361-lpc): I/Q-каналы, буфер, поток refill. FRxBuf: Piio_buffer; FRxChI: Piio_channel; FRxChQ: Piio_channel; FRxThread: TThread; FStreaming: Boolean; FRxSeq: LongWord; // TX-стрим (cf-ad9361-dds-core-lpc): I/Q output-каналы, буфер, поток push. // Full-duplex: стрим живёт всё время Run; на приёме гоним нули (мощности нет // за счёт макс. аттенюации). IQ от WDSP (192k) апсемплятся ×FTxRatio до рейта. FTxBuf: Piio_buffer; FTxChI: Piio_channel; FTxChQ: Piio_channel; FTxThread: TThread; FTxStreaming: Boolean; FTxRatio: Integer; // FSampleRate div 192000 (целочисленный апсемпл) FTxPrevI: LongInt; // последний 24-бит сэмпл (линейная интерполяция между вызовами) FTxPrevQ: LongInt; // FIFO int16-пар @ device-rate (кольцо): продюсер — SendDUCIQ, консьюмер — push-поток. FTxFifo: array of SmallInt; // интерливнутые I,Q FTxFifoCap: Integer; // ёмкость в SmallInt-элементах (пар*2) FTxFifoHead: Integer; // индекс чтения (консьюмер) FTxFifoTail: Integer; // индекс записи (продюсер) FTxFifoCount: Integer; // занято элементов FTxLock: TRTLCriticalSection; // Низкоуровневые помощники применения атрибутов к ad9361-phy procedure ApplyRxLO(Hz: Double); procedure ApplyTxLO(Hz: Double); procedure ApplySampleRate(Hz: Integer); procedure ApplyGain; procedure ApplyTxAtten; procedure StartRX; procedure StopRX; procedure StartTX; procedure StopTX; 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; procedure SetRxGain(ModeIdx: Integer; GainDb: Integer); override; procedure SetTxAtten(Db: Double); override; procedure SendDUCIQ(const IData, QData: array of Integer); override; procedure ClearDUCIQQueue; override; function ReadTelemetry(out TempC, RSSIdB: Double): 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) TX_SRC_RATE = 192000; // фиксированный TX-рейт WDSP (TX_SAMPLE_RATE) TX_BUF_PAIRS = 4096; // IQ-пар в одном TX-буфере push (~2.7 мс @ 1.536 MS/s) PLUTO_TX_OFF = -89.75; // макс. аттенюация AD9361 — TX практически выключен 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; // TX-поток: тянет int16-пары из FIFO бэкенда (или нули при underrun), пишет в // iio-буфер и push'ит. iio_buffer_push блокирует до приёма DMA → пейсинг ровно // на device-rate. Работает с private-полями TPlutoBackend (одна единица компиляции). TPlutoTXThread = 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; constructor TPlutoTXThread.Create(ABk: TPlutoBackend); begin FBk := ABk; FreeOnTerminate := False; inherited Create(False); end; procedure TPlutoTXThread.Execute; var step: ptrint; pFirst, pEnd, p: PByte; h: Integer; n: ptrint; begin while not Terminated do begin if FBk.FTxBuf = nil then begin Sleep(5); Continue; end; step := iio_buffer_step(FBk.FTxBuf); pFirst := iio_buffer_first(FBk.FTxBuf, FBk.FTxChI); pEnd := iio_buffer_end(FBk.FTxBuf); // Наполняем буфер из FIFO; при underrun добиваем нулями (full-duplex idle). EnterCriticalSection(FBk.FTxLock); p := pFirst; while PtrUInt(p) < PtrUInt(pEnd) do begin if FBk.FTxFifoCount > 0 then begin h := FBk.FTxFifoHead; PSmallInt(p)^ := FBk.FTxFifo[h]; PSmallInt(p + 2)^ := FBk.FTxFifo[h + 1]; FBk.FTxFifoHead := (h + 2) mod FBk.FTxFifoCap; Dec(FBk.FTxFifoCount, 2); end else begin PSmallInt(p)^ := 0; PSmallInt(p + 2)^ := 0; end; Inc(p, step); end; LeaveCriticalSection(FBk.FTxLock); if Terminated then Break; n := iio_buffer_push(FBk.FTxBuf); // блокирует → пейсинг на device-rate if n < 0 then Sleep(2); 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; FTxBuf := nil; FTxChI := nil; FTxChQ := nil; FTxThread := nil; FTxStreaming := False; FTxRatio := 8; FTxPrevI := 0; FTxPrevQ := 0; FTxAttDb := PLUTO_TX_OFF; // на старте TX выключен (макс. аттенюация) // FIFO на ~4 TX-буфера запаса под джиттер продюсера/консьюмера. FTxFifoCap := TX_BUF_PAIRS * 2 * 4; SetLength(FTxFifo, FTxFifoCap); FTxFifoHead := 0; FTxFifoTail := 0; FTxFifoCount := 0; InitCriticalSection(FTxLock); FillChar(FDevice, SizeOf(FDevice), 0); end; destructor TPlutoBackend.Destroy; begin Disconnect; DoneCriticalSection(FTxLock); 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; ApplyTxAtten; // TX заглушён (макс. аттенюация) до явного PTT Result := True; end; procedure TPlutoBackend.Disconnect; begin StopTX; 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, WasTxStreaming: Boolean; begin if (FPhy = nil) or (Hz < PLUTO_MIN_SR) then Exit; WasStreaming := FStreaming; WasTxStreaming := FTxStreaming; if WasTxStreaming then StopTX; // device-rate общий для RX/TX — пересоздать оба if WasStreaming then StopRX; // менять rate с открытым буфером нельзя // Предпочтительно ad9361_set_bb_rate: настраивает BBPLL И загружает FIR-таблицу // децимации под частоту. Без этого на холодную (битый/дефолтный FIR) спектр — // широкая палка по центру, не реагирует на перестройку. Fallback — прямая // запись sampling_frequency (работает, если FIR уже корректен). if Assigned(ad9361_set_bb_rate) then ad9361_set_bb_rate(FPhy, culong(Hz)) else begin Ch := iio_device_find_channel(FPhy, 'voltage0', 0); if Ch <> nil then iio_channel_attr_write_longlong(Ch, 'sampling_frequency', Hz); end; // RF-полоса отдельно (set_bb_rate её не трогает). Ch := iio_device_find_channel(FPhy, 'voltage0', 0); if Ch <> nil then iio_channel_attr_write_longlong(Ch, 'rf_bandwidth', Hz); if WasStreaming then StartRX; if WasTxStreaming then StartTX; // пересоздаём TX с новым FTxRatio 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.ApplyTxAtten; var Ch: Piio_channel; begin if FPhy = nil then Exit; Ch := iio_device_find_channel(FPhy, 'voltage0', 1); // TX att — output voltage0 if Ch = nil then Exit; // hardwaregain выходного тракта — отрицательные дБ, шаг 0.25 (0=макс. мощность). if Assigned(iio_channel_attr_write_double) then iio_channel_attr_write_double(Ch, 'hardwaregain', FTxAttDb); 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; procedure TPlutoBackend.StartTX; begin if FTxStreaming or not FConnected or (FTxDev = nil) then Exit; FTxChI := iio_device_find_channel(FTxDev, 'voltage0', 1); // output I FTxChQ := iio_device_find_channel(FTxDev, 'voltage1', 1); // output Q if (FTxChI = nil) or (FTxChQ = nil) then begin FLastError := 'TX I/Q channels not found'; Exit; end; iio_channel_enable(FTxChI); iio_channel_enable(FTxChQ); // Апсемпл-ратио под текущий device-rate (рейты-пресеты кратны 192k → целое). FTxRatio := FSampleRate div TX_SRC_RATE; if FTxRatio < 1 then FTxRatio := 1; FTxPrevI := 0; FTxPrevQ := 0; EnterCriticalSection(FTxLock); FTxFifoHead := 0; FTxFifoTail := 0; FTxFifoCount := 0; LeaveCriticalSection(FTxLock); FTxBuf := iio_device_create_buffer(FTxDev, TX_BUF_PAIRS, False); if FTxBuf = nil then begin FLastError := 'TX create_buffer failed'; Exit; end; ApplyTxAtten; // стартуем с текущей аттенюации (по умолчанию OFF до PTT) FTxStreaming := True; FTxThread := TPlutoTXThread.Create(Self); end; procedure TPlutoBackend.StopTX; begin if FTxThread <> nil then begin FTxThread.Terminate; if (FTxBuf <> nil) and Assigned(iio_buffer_cancel) then iio_buffer_cancel(FTxBuf); // разблокировать push немедленно FTxThread.WaitFor; FreeAndNil(FTxThread); end; FTxStreaming := False; if FTxBuf <> nil then begin iio_buffer_destroy(FTxBuf); FTxBuf := 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); // Full-duplex: оба стрима живут весь Run. TX по умолчанию заглушён макс. // аттенюацией (FTxAttDb=OFF) — реальная мощность включается из контроллера // через SetTxAtten на PTT. if Run then begin StartRX; StartTX; end else begin StopTX; StopRX; end; 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; procedure TPlutoBackend.SetRxGain(ModeIdx: Integer; GainDb: Integer); begin case ModeIdx of 1: FGainMode := 'fast_attack'; 2: FGainMode := 'slow_attack'; 3: FGainMode := 'hybrid'; else FGainMode := 'manual'; end; FGainDb := GainDb; if FConnected then ApplyGain; end; procedure TPlutoBackend.SetTxAtten(Db: Double); begin if Db > 0 then Db := 0; if Db < PLUTO_TX_OFF then Db := PLUTO_TX_OFF; FTxAttDb := Db; if FConnected then ApplyTxAtten; end; procedure TPlutoBackend.SendDUCIQ(const IData, QData: array of Integer); // IData/QData — 24-bit пары @ TX_SRC_RATE (192k) от WDSP через контроллер. // Линейно апсемплим ×FTxRatio, конвертим 24→int16 (>>8, full-scale MSB-aligned), // Q инвертируем (та же конвенция, что и на RX) и кладём в FIFO. Underrun в push- // потоке добивается нулями, overflow (продюсер опередил) — отбрасываем старое. function Clamp16(V: Int64): SmallInt; inline; begin if V > 32767 then Result := 32767 else if V < -32768 then Result := -32768 else Result := SmallInt(V); end; var i, j, t: Integer; ci, cq, oi, oq: Int64; n: Integer; begin if (not FTxStreaming) or (FTxRatio < 1) then Exit; n := Length(IData); if Length(QData) < n then n := Length(QData); EnterCriticalSection(FTxLock); try for i := 0 to n - 1 do begin ci := IData[i]; cq := QData[i]; for j := 1 to FTxRatio do begin // линейная интерполяция prev→cur по FTxRatio точкам (последняя = cur) oi := FTxPrevI + (ci - FTxPrevI) * j div FTxRatio; oq := FTxPrevQ + (cq - FTxPrevQ) * j div FTxRatio; if FTxFifoCount + 2 > FTxFifoCap then begin // overflow: сбрасываем самые старые пары, держим хвост (минимум латентности) FTxFifoHead := (FTxFifoHead + 2) mod FTxFifoCap; Dec(FTxFifoCount, 2); end; t := FTxFifoTail; FTxFifo[t] := Clamp16(oi div 256); // 24→16 бит FTxFifo[t + 1] := Clamp16(-(oq div 256)); // Q инверсия (как RX) FTxFifoTail := (t + 2) mod FTxFifoCap; Inc(FTxFifoCount, 2); end; FTxPrevI := ci; FTxPrevQ := cq; end; finally LeaveCriticalSection(FTxLock); end; end; procedure TPlutoBackend.ClearDUCIQQueue; begin EnterCriticalSection(FTxLock); FTxFifoHead := 0; FTxFifoTail := 0; FTxFifoCount := 0; FTxPrevI := 0; FTxPrevQ := 0; LeaveCriticalSection(FTxLock); end; function TPlutoBackend.ReadTelemetry(out TempC, RSSIdB: Double): Boolean; // Опрос аппаратной телеметрии ad9361-phy (вызывается контроллером, не из RX-потока): // temp0/input — температура чипа в миллиградусах C (longlong); // voltage0/rssi — RSSI входного тракта в дБ (double; строка вида '12.34 dB', // libiio парсит ведущее число). Валиден только во время приёма. var Ch: Piio_channel; raw: clonglong; d: cdouble; begin TempC := TELEMETRY_NONE; RSSIdB := TELEMETRY_NONE; Result := False; if (not FConnected) or (FPhy = nil) or (not IIOLoaded) then Exit; Ch := iio_device_find_channel(FPhy, 'temp0', 0); if (Ch <> nil) and (iio_channel_attr_read_longlong(Ch, 'input', raw) = 0) then begin TempC := raw / 1000.0; Result := True; end; // RSSI имеет смысл только при активном RX-стриме (иначе чтение даёт ошибку/мусор). if FStreaming then begin Ch := iio_device_find_channel(FPhy, 'voltage0', 0); if (Ch <> nil) and Assigned(iio_channel_attr_read_double) and (iio_channel_attr_read_double(Ch, 'rssi', d) = 0) then begin RSSIdB := d; Result := True; end; end; end; end.