mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
TX LO (altvoltage1) is now powered down on RX and enabled on PTT, instead of staying on continuously. Removes the TX carrier leakage that showed up at the VFO frequency in the RX spectrum (and persisted after MOX), and stops idle TX-LO radiation. ApplyTxLO sets powerdown from FTransmitting, which also recovers the LO if external software (e.g. SDR Console) left it powered down. RX LO (altvoltage0) is kept on — receive is continuous; QO-100 full-duplex still works (RX LO separate). Trade-off: re-enabling the TX LO re-runs the AD9361 synth calibration (a few ms), so the first syllable on SSB may be slightly clipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
946 lines
37 KiB
ObjectPascal
946 lines
37 KiB
ObjectPascal
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 (целочисленный апсемпл ×L)
|
||
FTxBufPairs: Integer; // размер TX-буфера в IQ-парах (по sample-rate, см. TxBufPairsForRate)
|
||
// Полифазный FIR-интерполятор: FTxCoef[p*TAPS + k] = h[p + k*L] (фаза p, отвод k).
|
||
// Линия задержки FTxDlI/Q хранит последние TAPS входных сэмплов между вызовами.
|
||
FTxCoef: array of Double;
|
||
FTxDlI: array of Double;
|
||
FTxDlQ: array of Double;
|
||
// 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 SetTxLOPowerdown(PoweredOn: Boolean); // TX LO вкл/выкл (по PTT)
|
||
procedure ApplySampleRate(Hz: Integer);
|
||
procedure BuildTxResampler; // (пере)строить полифазные коэффициенты под FTxRatio
|
||
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-буфера масштабируется от sample-rate (как SoapyPlutoSDR):
|
||
// round(rate/60) → следующая степень 2. Фикс 4096 «недостаточно для full-duplex
|
||
// выше 1 Msps — сэмплы дропаются» (комментарий драйвера). Минимум — TX_BUF_MIN.
|
||
TX_BUF_MIN = 4096; // нижняя граница (~7 мс @ 576k)
|
||
TX_BUF_MAX = 65536; // верхняя граница (защита от гигантских буферов)
|
||
// Полифазный анти-имидж интерполятор 192k→device-rate (вместо линейной интерполяции,
|
||
// которая оставляла образы на кратных 192k с подавлением только sinc²).
|
||
TX_FIR_TAPS = 16; // отводов на фазу (групп-задержка TAPS/2 входных сэмплов)
|
||
TX_FIR_CUT = 40000; // частота среза анти-имидж ФНЧ, Гц (покрывает SSB/NFM/AM/data)
|
||
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 TxBufPairsForRate(Rate: Integer): Integer;
|
||
// Размер TX-буфера в IQ-парах под device-rate (как SoapyPlutoSDR): round(rate/60)
|
||
// округлённое вверх до степени 2, с клампом [TX_BUF_MIN..TX_BUF_MAX].
|
||
// 576k→16384, 1536k→32768, 3072k→65536.
|
||
var
|
||
Target, P: Integer;
|
||
begin
|
||
Target := (Rate + 30) div 60; // round(rate/60)
|
||
P := TX_BUF_MIN;
|
||
while P < Target do P := P shl 1; // следующая степень 2 ≥ Target
|
||
if P < TX_BUF_MIN then P := TX_BUF_MIN;
|
||
if P > TX_BUF_MAX then P := TX_BUF_MAX;
|
||
Result := P;
|
||
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;
|
||
FTxAttDb := PLUTO_TX_OFF; // на старте TX выключен (макс. аттенюация)
|
||
// FIFO на ~4 TX-буфера запаса под джиттер продюсера/консьюмера. Окончательный
|
||
// размер выставляется в StartTX по фактическому device-rate (TxBufPairsForRate).
|
||
FTxBufPairs := TxBufPairsForRate(FSampleRate);
|
||
FTxFifoCap := FTxBufPairs * 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
|
||
begin
|
||
iio_channel_attr_write(Ch, 'powerdown', '0'); // вернуть LO, если внешнее ПО его погасило
|
||
iio_channel_attr_write_longlong(Ch, 'frequency', Round(Hz));
|
||
end;
|
||
end;
|
||
|
||
procedure TPlutoBackend.SetTxLOPowerdown(PoweredOn: Boolean);
|
||
// TX LO (altvoltage1) вкл/выкл. Гасим на приёме → нет утечки несущей TX на частоте
|
||
// VFO в RX-спектре; включаем на PTT. Также возвращает LO, если стороннее ПО его
|
||
// погасило. RX LO (altvoltage0) не трогаем — приём непрерывен.
|
||
var Ch: Piio_channel;
|
||
begin
|
||
if FPhy = nil then Exit;
|
||
Ch := iio_device_find_channel(FPhy, 'altvoltage1', 1);
|
||
if Ch = nil then Exit;
|
||
if PoweredOn then iio_channel_attr_write(Ch, 'powerdown', '0')
|
||
else iio_channel_attr_write(Ch, 'powerdown', '1');
|
||
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));
|
||
// powerdown TX LO по состоянию передачи (на приёме гасим — убирает спайк-утечку).
|
||
SetTxLOPowerdown(FTransmitting);
|
||
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.BuildTxResampler;
|
||
// Полифазный FIR-интерполятор ×L (L=FTxRatio): прототип — оконный sinc-ФНЧ длиной
|
||
// TAPS*L с частотой среза TX_FIR_CUT, нормированный так, что DC-усиление любой фазы
|
||
// ≈ 1 (полная сумма = L). Раскладка в фазы: coef[p*TAPS+k] = proto[p + k*L].
|
||
var
|
||
L, M, n, p, k: Integer;
|
||
fc, x, s, w, sum, scale: Double;
|
||
proto: array of Double;
|
||
begin
|
||
L := FTxRatio;
|
||
if L < 1 then L := 1;
|
||
M := TX_FIR_TAPS * L;
|
||
SetLength(proto, M);
|
||
fc := TX_FIR_CUT / FSampleRate; // нормированный срез (циклов/сэмпл на выходе)
|
||
if fc > 0.49 then fc := 0.49;
|
||
sum := 0;
|
||
for n := 0 to M - 1 do
|
||
begin
|
||
x := n - (M - 1) / 2.0;
|
||
if Abs(x) < 1e-9 then s := 2.0 * fc
|
||
else s := Sin(2.0 * Pi * fc * x) / (Pi * x);
|
||
// окно Блэкмана (~ −58 дБ боковые)
|
||
w := 0.42 - 0.5 * Cos(2.0 * Pi * n / (M - 1)) + 0.08 * Cos(4.0 * Pi * n / (M - 1));
|
||
proto[n] := s * w;
|
||
sum := sum + proto[n];
|
||
end;
|
||
if sum = 0 then sum := 1;
|
||
scale := L / sum; // DC-усиление на фазу ≈ 1
|
||
SetLength(FTxCoef, M);
|
||
for p := 0 to L - 1 do
|
||
for k := 0 to TX_FIR_TAPS - 1 do
|
||
FTxCoef[p * TX_FIR_TAPS + k] := proto[p + k * L] * scale;
|
||
// линия задержки на TAPS входных сэмплов (сброс)
|
||
SetLength(FTxDlI, TX_FIR_TAPS);
|
||
SetLength(FTxDlQ, TX_FIR_TAPS);
|
||
for k := 0 to TX_FIR_TAPS - 1 do begin FTxDlI[k] := 0; FTxDlQ[k] := 0; end;
|
||
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;
|
||
BuildTxResampler; // полифазные коэффициенты + сброс линии задержки под FTxRatio
|
||
// Размер push-буфера под текущий rate + FIFO на ~4 таких буфера запаса.
|
||
FTxBufPairs := TxBufPairsForRate(FSampleRate);
|
||
EnterCriticalSection(FTxLock);
|
||
FTxFifoCap := FTxBufPairs * 2 * 4;
|
||
if Length(FTxFifo) <> FTxFifoCap then SetLength(FTxFifo, FTxFifoCap);
|
||
FTxFifoHead := 0; FTxFifoTail := 0; FTxFifoCount := 0;
|
||
LeaveCriticalSection(FTxLock);
|
||
FTxBuf := iio_device_create_buffer(FTxDev, FTxBufPairs, 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;
|
||
// TX LO включаем на передаче, гасим на приёме (нет утечки несущей в RX-спектр).
|
||
// TX-стрим живёт непрерывно (full-duplex); реальная мощность — через аттенюацию.
|
||
if FConnected then SetTxLOPowerdown(Active);
|
||
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 через контроллер.
|
||
// Полифазный FIR-интерполятор ×FTxRatio (анти-имидж ФНЧ, см. BuildTxResampler):
|
||
// на каждый входной сэмпл выдаём L выходных, конвертим 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, p, k, t, L, taps, base, n: Integer;
|
||
ai, aq: Double;
|
||
begin
|
||
if (not FTxStreaming) or (FTxRatio < 1) or (Length(FTxCoef) = 0) then Exit;
|
||
n := Length(IData);
|
||
if Length(QData) < n then n := Length(QData);
|
||
L := FTxRatio;
|
||
taps := TX_FIR_TAPS;
|
||
EnterCriticalSection(FTxLock);
|
||
try
|
||
for i := 0 to n - 1 do
|
||
begin
|
||
// сдвигаем линию задержки (FTxDl[0] = новейший входной сэмпл)
|
||
for k := taps - 1 downto 1 do
|
||
begin FTxDlI[k] := FTxDlI[k - 1]; FTxDlQ[k] := FTxDlQ[k - 1]; end;
|
||
FTxDlI[0] := IData[i]; FTxDlQ[0] := QData[i];
|
||
// L выходных сэмплов — фазы p = 0..L-1, каждая = свёртка с подфильтром
|
||
for p := 0 to L - 1 do
|
||
begin
|
||
ai := 0; aq := 0;
|
||
base := p * taps;
|
||
for k := 0 to taps - 1 do
|
||
begin
|
||
ai := ai + FTxCoef[base + k] * FTxDlI[k];
|
||
aq := aq + FTxCoef[base + k] * FTxDlQ[k];
|
||
end;
|
||
if FTxFifoCount + 2 > FTxFifoCap then
|
||
begin
|
||
// overflow: сбрасываем самые старые пары, держим хвост (минимум латентности)
|
||
FTxFifoHead := (FTxFifoHead + 2) mod FTxFifoCap;
|
||
Dec(FTxFifoCount, 2);
|
||
end;
|
||
t := FTxFifoTail;
|
||
FTxFifo[t] := Clamp16(Round(ai / 256.0)); // 24→16 бит
|
||
FTxFifo[t + 1] := Clamp16(-Round(aq / 256.0)); // Q инверсия (как RX)
|
||
FTxFifoTail := (t + 2) mod FTxFifoCap;
|
||
Inc(FTxFifoCount, 2);
|
||
end;
|
||
end;
|
||
finally
|
||
LeaveCriticalSection(FTxLock);
|
||
end;
|
||
end;
|
||
|
||
procedure TPlutoBackend.ClearDUCIQQueue;
|
||
var k: Integer;
|
||
begin
|
||
EnterCriticalSection(FTxLock);
|
||
FTxFifoHead := 0; FTxFifoTail := 0; FTxFifoCount := 0;
|
||
for k := 0 to Length(FTxDlI) - 1 do begin FTxDlI[k] := 0; FTxDlQ[k] := 0; end;
|
||
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.
|