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. [email protected]



456
457
458
# File 'lib/pwn/ffi/adalm_pluto.rb', line 456

public_class_method def self.authors
  "AUTHOR(S):\n  0day Inc. <[email protected]>\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: }



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
447
448
449
450
451
452
# File 'lib/pwn/ffi/adalm_pluto.rb', line 414

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



462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
# File 'lib/pwn/ffi/adalm_pluto.rb', line 462

public_class_method def self.help
  puts "USAGE:
    # Run available and return its result
    #{self}.available?

    # Run info and return its result
    #{self}.info

    # Run list uris and return its result
    #{self}.list_uris(
      backends: 'optional - backends value consumed by #list_uris'
    )

    # Open a session or connection and return a handle.
    #{self}.open(
      uri: 'required - URI or URL string',
      timeout_ms: 'optional - timeout ms value consumed by #open'
    )

    # Close a session previously returned by #open.
    #{self}.close(
      context: 'optional - context value consumed by #close'
    )

    # Run device info and return its result
    #{self}.device_info(
      context: 'required - context value consumed by #device_info'
    )

    # Run configure and return its result
    #{self}.configure(
      context: 'required - pointer from .open',
      freq_hz: 'required - RX LO frequency Hz',
      rate_hz: 'optional - sample rate (default 2_500_000)',
      bw_hz: 'optional - RF bandwidth (default = rate_hz)',
      gain_db: 'optional - manual gain dB; nil = slow_attack AGC',
      gain_mode: 'optional - slow_attack|fast_attack|manual|hybrid'
    )

    # Run start rx and return its result
    #{self}.start_rx(
      context: 'required - pointer from .open',
      samples: 'optional - samples-per-refill (default 262144)',
      cyclic: 'optional - cyclic buffer (default false)'
    )

    # Run read sync and return its result
    #{self}.read_sync(
      handle: 'required - handle value consumed by #read_sync'
    )

    # Run stop rx and return its result
    #{self}.stop_rx(
      handle: 'optional - handle value consumed by #stop_rx'
    )

    # High-level one-shot: open → configure → start_rx → N refills → stop → close
    #{self}.capture(
      freq_hz: 'required - freq hz value consumed by #capture',
      rate_hz: 'optional - rate hz value consumed by #capture',
      bytes: 'optional - 1_048_576,   # approximate payload size',
      uri: 'optional - URI or URL string',
      gain_db: 'optional - gain db value consumed by #capture',
      timeout_ms: 'optional - timeout ms value consumed by #capture',
      bw_hz: 'optional - bw hz value consumed by #capture',
      gain_mode: 'optional - gain mode value consumed by #capture'
    )

    # Print the AUTHOR(S) string for this module.
    #{self}.authors
  "
  constants.sort
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
377
378
379
380
# 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)

  (h[:read_mutex] ||= Mutex.new).synchronize do
    buf = h[:buffer]
    raise 'ERROR: handle missing :buffer' if buf.nil? || buf.null?
    raise 'ERROR: RX buffer stopping' if h[:stopping]

    nbytes = iio_buffer_refill(buf)
    raise "ERROR: iio_buffer_refill rc=#{nbytes}" if nbytes.negative?
    raise "ERROR: iio_buffer_refill invalid IQ length #{nbytes}" unless (nbytes % 4).zero?

    start = iio_buffer_start(buf)
    # Stock Pluto packs I then Q as sequential int16 scan elements.
    start.read_string(nbytes)
  end
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)



385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/pwn/ffi/adalm_pluto.rb', line 385

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

  (h[:stop_mutex] ||= Mutex.new).synchronize do
    buf = h[:buffer]
    if buf && !buf.null?
      h[:stopping] = true
      iio_buffer_cancel(buf)
      (h[:read_mutex] ||= Mutex.new).synchronize do
        iio_buffer_destroy(buf)
        h[:buffer] = nil
      end
    end
  end
  nil
end