Module: PWN::FFI::AdalmPluto

Extended by:
Library
Defined in:
lib/pwn/ffi/adalm_pluto.rb

Overview

Thin libiio binding specialised for the ADALM-PLUTO (AD9363).

Control-plane + blocking RX of interleaved CS16 I/Q so PWN::SDR::Decoder::* can pull MHz-rate complex samples without GQRX's 48 kHz audio tap and without shelling out to iio_*. libad9361 is used only for optional bb_rate helpers when present.

If libiio is missing .available? is false and callers fall back to RTLSdr / HackRF / SoapySDR / pure-Ruby detector paths.

Default URI tries USB (local) first: "ip:192.168.2.1" is the stock Ethernet/USB-gadget address of an unconfigured Pluto.

Defined Under Namespace

Modules: Ad9361

Constant Summary collapse

DEFAULT_URI =
'ip:192.168.2.1'
PHY_NAME =
'ad9361-phy'
RX_NAME =
'cf-ad9361-lpc'
RX_LO_NAME =

Stock Pluto RX LO channel

'altvoltage0'

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.load_errorObject (readonly)

Returns the value of attribute load_error.



32
33
34
# File 'lib/pwn/ffi/adalm_pluto.rb', line 32

def load_error
  @load_error
end

Class Method Details

.authorsObject

Author(s)

0day Inc. support@0dayinc.com



450
451
452
# File 'lib/pwn/ffi/adalm_pluto.rb', line 450

public_class_method def self.authors
  "AUTHOR(S):\n  0day Inc. <support@0dayinc.com>\n"
end

.available?Boolean

Supported Method Parameters

PWN::FFI::AdalmPluto.available?

Returns:

  • (Boolean)


143
144
145
146
147
# File 'lib/pwn/ffi/adalm_pluto.rb', line 143

public_class_method def self.available?
  !@load_error && respond_to?(:iio_create_default_context, true)
rescue StandardError
  false
end

.capture(opts = {}) ⇒ Object

High-level one-shot: open → configure → start_rx → N refills → stop → close.

Supported Method Parameters

iq = PWN::FFI::AdalmPluto.capture( freq_hz: 'required', rate_hz: 2_500_000, bytes: 1_048_576, # approximate payload size uri: nil, gain_db: nil ) Returns { iq_cs16: String, rate_hz:, freq_hz:, samples: }



408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/pwn/ffi/adalm_pluto.rb', line 408

public_class_method def self.capture(opts = {})
  raise 'ERROR: :freq_hz required' unless opts[:freq_hz]

  rate  = (opts[:rate_hz] || 2_500_000).to_i
  want  = (opts[:bytes] || 1_048_576).to_i
  ctx   = self.open(uri: opts[:uri], timeout_ms: opts[:timeout_ms])
  handle = nil
  begin
    configure(
      context: ctx,
      freq_hz: opts[:freq_hz],
      rate_hz: rate,
      bw_hz: opts[:bw_hz],
      gain_db: opts[:gain_db],
      gain_mode: opts[:gain_mode]
    )
    # each sample = 4 bytes (I s16 + Q s16)
    nsamps = (want / 4).clamp(4_096, 1_048_576)
    handle = start_rx(context: ctx, samples: nsamps)
    chunks = []
    got = 0
    while got < want
      chunk = read_sync(handle: handle)
      break if chunk.bytesize.zero?

      chunks << chunk
      got += chunk.bytesize
    end
    {
      iq_cs16: chunks.join,
      rate_hz: rate,
      freq_hz: opts[:freq_hz].to_i,
      samples: got / 4
    }
  ensure
    stop_rx(handle: handle) if handle
    close(context: ctx)
  end
end

.close(opts = {}) ⇒ Object

Supported Method Parameters

PWN::FFI::AdalmPluto.close(context: pointer)



235
236
237
238
239
240
241
# File 'lib/pwn/ffi/adalm_pluto.rb', line 235

public_class_method def self.close(opts = {})
  ctx = opts[:context]
  return unless ctx && !ctx.null?

  iio_context_destroy(ctx)
  nil
end

.configure(opts = {}) ⇒ Object

rubocop:disable Naming/PredicateMethod



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/pwn/ffi/adalm_pluto.rb', line 276

public_class_method def self.configure(opts = {}) # rubocop:disable Naming/PredicateMethod
  ctx = opts[:context]
  raise 'ERROR: :context required' if ctx.nil? || ctx.null?

  freq = opts[:freq_hz].to_i
  rate = (opts[:rate_hz] || 2_500_000).to_i
  bw   = (opts[:bw_hz] || rate).to_i
  raise 'ERROR: :freq_hz required' if freq <= 0

  phy = iio_context_find_device(ctx, PHY_NAME)
  raise "ERROR: phy device #{PHY_NAME.inspect} not found" if phy.null?

  # RX LO
  lo = iio_device_find_channel(phy, RX_LO_NAME, true) # output channel
  if lo.null?
    # some firmwares expose "RX_LO"
    lo = iio_device_find_channel(phy, 'RX_LO', true)
  end
  raise 'ERROR: RX LO channel not found' if lo.null?

  check_ssize!(iio_channel_attr_write_longlong(lo, 'frequency', freq), 'RX LO frequency')

  # Sampling frequency + RF bandwidth on voltage0 (RX)
  rx_chn = iio_device_find_channel(phy, 'voltage0', false)
  raise 'ERROR: phy voltage0 (RX) not found' if rx_chn.null?

  check_ssize!(iio_channel_attr_write_longlong(rx_chn, 'sampling_frequency', rate), 'sampling_frequency')
  check_ssize!(iio_channel_attr_write_longlong(rx_chn, 'rf_bandwidth', bw), 'rf_bandwidth')

  gain_mode = opts[:gain_mode]
  if opts.key?(:gain_db) && !opts[:gain_db].nil?
    gain_mode ||= 'manual'
    check_ssize!(iio_channel_attr_write(rx_chn, 'gain_control_mode', gain_mode), 'gain_control_mode')
    check_ssize!(iio_channel_attr_write_longlong(rx_chn, 'hardwaregain', opts[:gain_db].to_i), 'hardwaregain')
  else
    gain_mode ||= 'slow_attack'
    check_ssize!(iio_channel_attr_write(rx_chn, 'gain_control_mode', gain_mode), 'gain_control_mode')
  end

  # Prefer libad9361 bb_rate when linked (programs FIR + HB chain)
  if Ad9361.load_error.nil? && Ad9361.respond_to?(:ad9361_set_bb_rate, true)
    begin
      Ad9361.ad9361_set_bb_rate(phy, rate)
    rescue StandardError
      nil
    end
  end

  true
end

.device_info(opts = {}) ⇒ Object

Supported Method Parameters

meta = PWN::FFI::AdalmPluto.device_info(context: pointer) Returns { name:, description:, devices: [name:, ...] }



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/pwn/ffi/adalm_pluto.rb', line 247

public_class_method def self.device_info(opts = {})
  ctx = opts[:context]
  raise 'ERROR: :context required' if ctx.nil? || ctx.null?

  n = iio_context_get_devices_count(ctx)
  devices = Array.new(n) do |i|
    d = iio_context_get_device(ctx, i)
    {
      id: iio_device_get_id(d).to_s,
      name: (iio_device_get_name(d) || '').to_s
    }
  end
  {
    name: iio_context_get_name(ctx).to_s,
    description: iio_context_get_description(ctx).to_s,
    devices: devices
  }
end

.helpObject

Display Usage for this Module



456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
# File 'lib/pwn/ffi/adalm_pluto.rb', line 456

public_class_method def self.help
  puts "USAGE:
    #{self}.available?
    #{self}.info
    #{self}.list_uris
    ctx = #{self}.open(uri: 'ip:192.168.2.1')   # or nil for default
    #{self}.device_info(context: ctx)
    #{self}.configure(context: ctx, freq_hz:, rate_hz: 2_500_000, gain_db: nil)
    h = #{self}.start_rx(context: ctx, samples: 262_144)
    iq = #{self}.read_sync(handle: h)            # cs16le interleaved String
    #{self}.stop_rx(handle: h)
    #{self}.close(context: ctx)

    # one-shot
    cap = #{self}.capture(freq_hz: 1090e6, rate_hz: 2_500_000, bytes: 1_048_576)

    #{self}.authors
  "
end

.infoObject

Supported Method Parameters

info = PWN::FFI::AdalmPluto.info Returns { available:, major:, minor:, git_tag:, ad9361: }



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/pwn/ffi/adalm_pluto.rb', line 153

public_class_method def self.info
  return { available: false, error: @load_error&.message } unless available?

  maj = PubFFI::MemoryPointer.new(:uint)
  min = PubFFI::MemoryPointer.new(:uint)
  # git_tag is a caller-owned char[8] buffer (NOT char**).
  git = PubFFI::MemoryPointer.new(:char, 8)
  git.put_bytes(0, "\0" * 8)
  iio_library_get_version(maj, min, git)
  tag = git.read_string
  {
    available: true,
    major: maj.read_uint,
    minor: min.read_uint,
    git_tag: tag,
    ad9361: Ad9361.load_error.nil? && Ad9361.respond_to?(:ad9361_set_bb_rate, true)
  }
end

.list_uris(opts = {}) ⇒ Object

Supported Method Parameters

uris = PWN::FFI::AdalmPluto.list_uris(backends: 'usb,ip,local') Returns Array of { uri:, description: }



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/pwn/ffi/adalm_pluto.rb', line 176

public_class_method def self.list_uris(opts = {})
  raise 'ERROR: libiio not available' unless available?

  backends = (opts[:backends] || 'usb,ip,local').to_s
  scan = iio_create_scan_context(backends, 0)
  return [] if scan.null?

  info_ptr = PubFFI::MemoryPointer.new(:pointer)
  info_ptr.write_pointer(PubFFI::Pointer::NULL)
  begin
    n = iio_scan_context_get_info_list(scan, info_ptr)
    return [] if n <= 0

    base = info_ptr.read_pointer
    return [] if base.null?

    Array.new(n) do |i|
      entry = base.get_pointer(i * PubFFI::Pointer.size)
      next { uri: '', description: '' } if entry.null?

      {
        uri: iio_context_info_get_uri(entry).to_s,
        description: iio_context_info_get_description(entry).to_s
      }
    end
  ensure
    base = info_ptr.read_pointer
    iio_context_info_list_free(base) if base && !base.null?
    iio_scan_context_destroy(scan)
  end
rescue StandardError
  []
end

.open(opts = {}) ⇒ Object

Supported Method Parameters

ctx = PWN::FFI::AdalmPluto.open(uri: 'ip:192.168.2.1') Returns opaque iio_context* pointer. ALWAYS pair with .close.



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/pwn/ffi/adalm_pluto.rb', line 214

public_class_method def self.open(opts = {})
  raise 'ERROR: libiio not available' unless available?

  uri = opts[:uri]
  ctx =
    if uri.to_s.empty?
      # try default USB/local first, then stock IP
      c = iio_create_default_context
      (c.null? ? iio_create_context_from_uri(DEFAULT_URI) : c)
    else
      iio_create_context_from_uri(uri.to_s)
    end
  raise "ERROR: iio_create_context failed for #{uri.inspect}" if ctx.null?

  iio_context_set_timeout(ctx, (opts[:timeout_ms] || 5_000).to_i)
  ctx
end

.read_sync(opts = {}) ⇒ Object

Supported Method Parameters

iq_cs16 = PWN::FFI::AdalmPluto.read_sync(handle: hash_from_start_rx) Returns binary String of interleaved little-endian signed-16 I/Q (I0,Q0,I1,Q1,…) — same contract as a cs16 capture file.



363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/pwn/ffi/adalm_pluto.rb', line 363

public_class_method def self.read_sync(opts = {})
  h = opts[:handle]
  raise 'ERROR: :handle required' unless h.is_a?(Hash)

  buf = h[:buffer]
  raise 'ERROR: handle missing :buffer' if buf.nil? || buf.null?

  nbytes = iio_buffer_refill(buf)
  raise "ERROR: iio_buffer_refill rc=#{nbytes}" if nbytes.negative?

  start = iio_buffer_start(buf)
  # Pluto packs I then Q as sequential int16 scan elements; step covers both.
  start.read_string(nbytes)
end

.start_rx(opts = {}) ⇒ Object

Supported Method Parameters

handle = PWN::FFI::AdalmPluto.start_rx( context: 'required - pointer from .open', samples: 'optional - samples-per-refill (default 262144)', cyclic: 'optional - cyclic buffer (default false)' ) Returns { buffer:, rx:, i_chn:, q_chn:, samples: } — pair with .stop_rx.



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/pwn/ffi/adalm_pluto.rb', line 335

public_class_method def self.start_rx(opts = {})
  ctx = opts[:context]
  raise 'ERROR: :context required' if ctx.nil? || ctx.null?

  nsamps = (opts[:samples] || 262_144).to_i
  rx = iio_context_find_device(ctx, RX_NAME)
  raise "ERROR: RX streaming device #{RX_NAME.inspect} not found" if rx.null?

  # Enable voltage0 (I) + voltage1 (Q) scan elements
  i_chn = iio_device_find_channel(rx, 'voltage0', false)
  q_chn = iio_device_find_channel(rx, 'voltage1', false)
  raise 'ERROR: RX I/Q channels not found' if i_chn.null? || q_chn.null?

  iio_channel_enable(i_chn)
  iio_channel_enable(q_chn)

  buf = iio_device_create_buffer(rx, nsamps, opts[:cyclic] ? true : false)
  raise 'ERROR: iio_device_create_buffer failed' if buf.null?

  iio_buffer_set_blocking_mode(buf, true)
  { buffer: buf, rx: rx, i_chn: i_chn, q_chn: q_chn, samples: nsamps }
end

.stop_rx(opts = {}) ⇒ Object

Supported Method Parameters

PWN::FFI::AdalmPluto.stop_rx(handle: hash_from_start_rx)



381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/pwn/ffi/adalm_pluto.rb', line 381

public_class_method def self.stop_rx(opts = {})
  h = opts[:handle]
  return unless h.is_a?(Hash)

  buf = h[:buffer]
  if buf && !buf.null?
    begin
      iio_buffer_cancel(buf)
    rescue StandardError
      nil
    end
    iio_buffer_destroy(buf)
  end
  nil
end