Module: PWN::FFI::HackRF

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

Overview

Thin libhackrf binding for inventory / RX of raw I/Q.

Intentionally control-plane first: init/open/tune/rate/gains + one-shot sync-style helpers used by Extrospection probe_rf and by wideband PWN::SDR::Decoder::* modules that need real I/Q (not GQRX audio). Streaming callbacks stay opt-in — Ruby GC must not run on the libusb transfer thread, so call sites that need continuous RX should buffer into a Queue from a dedicated thread.

Defined Under Namespace

Classes: Transfer

Constant Summary collapse

HACKRF_SUCCESS =
0

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.load_errorObject (readonly)

Returns the value of attribute load_error.



30
31
32
# File 'lib/pwn/ffi/hack_rf.rb', line 30

def load_error
  @load_error
end

Class Method Details

.authorsObject

Author(s)

0day Inc. support@0dayinc.com



277
278
279
# File 'lib/pwn/ffi/hack_rf.rb', line 277

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

.available?Boolean

Supported Method Parameters

PWN::FFI::HackRF.available?

Returns:

  • (Boolean)


63
64
65
66
67
# File 'lib/pwn/ffi/hack_rf.rb', line 63

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

.capture(opts = {}) ⇒ Object

Supported Method Parameters

data = PWN::FFI::HackRF.capture( freq_hz: 'required - center frequency Hz', rate_hz: 'optional - sample rate (default 10e6)', samples: 'optional - I/Q sample pairs to capture (default 262_144)', lna_gain: 'optional', vga_gain: 'optional', amp: 'optional', serial: 'optional' ) One-shot open/configure/start_rx/read/stop_rx/close. Returns String of interleaved cs8 I/Q.



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/pwn/ffi/hack_rf.rb', line 252

public_class_method def self.capture(opts = {})
  want = (opts[:samples] || 262_144).to_i * 2
  dev  = self.open(serial: opts[:serial])
  configure(
    device: dev,
    freq_hz: opts[:freq_hz],
    rate_hz: opts[:rate_hz] || 10_000_000,
    lna_gain: opts[:lna_gain], vga_gain: opts[:vga_gain], amp: opts[:amp]
  )
  rx  = start_rx(device: dev)
  buf = +''
  while buf.bytesize < want
    chunk = read_sync(handle: rx, timeout: 2.0)
    break unless chunk

    buf << chunk
  end
  buf[0, want]
ensure
  stop_rx(handle: rx) if defined?(rx) && rx
  close(device: dev)  if defined?(dev) && dev
end

.close(opts = {}) ⇒ Object

Supported Method Parameters

PWN::FFI::HackRF.close(device: pointer)



105
106
107
108
109
110
111
112
# File 'lib/pwn/ffi/hack_rf.rb', line 105

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

  check!(hackrf_close(dev))
  hackrf_exit
  nil
end

.configure(opts = {}) ⇒ Object

rubocop:disable Naming/PredicateMethod



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/pwn/ffi/hack_rf.rb', line 125

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

  freq = opts[:freq_hz].to_i
  rate = (opts[:rate_hz] || 10_000_000).to_f
  lna  = (opts[:lna_gain] || 16).to_i
  vga  = (opts[:vga_gain] || 20).to_i
  amp  = opts[:amp] ? 1 : 0

  check!(hackrf_set_freq(dev, freq))
  check!(hackrf_set_sample_rate(dev, rate))
  check!(hackrf_set_lna_gain(dev, lna))
  check!(hackrf_set_vga_gain(dev, vga))
  check!(hackrf_set_amp_enable(dev, amp))
  check!(hackrf_set_baseband_filter_bandwidth(dev, opts[:bb_bw_hz].to_i)) if opts[:bb_bw_hz]
  true
end

.device_info(opts = {}) ⇒ Object

Supported Method Parameters

meta = PWN::FFI::HackRF.device_info(device: pointer) Returns { board_id:, board_name:, version: }



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/pwn/ffi/hack_rf.rb', line 148

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

  bid_ptr = PubFFI::MemoryPointer.new(:uint8)
  check!(hackrf_board_id_read(dev, bid_ptr))
  bid = bid_ptr.read_uint8
  ver_buf = PubFFI::MemoryPointer.new(:char, 255)
  check!(hackrf_version_string_read(dev, ver_buf, 255))
  {
    board_id: bid,
    board_name: hackrf_board_id_name(bid).to_s,
    version: ver_buf.read_string
  }
end

.helpObject

Display Usage for this Module



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/pwn/ffi/hack_rf.rb', line 283

public_class_method def self.help
  puts "USAGE:
    #{self}.available?
    #{self}.info
    dev = #{self}.open(serial: nil)
    #{self}.configure(device: dev, freq_hz:, rate_hz: 10e6, lna_gain: 16, vga_gain: 20, amp: false)
    rx = #{self}.start_rx(device: dev)
    data = #{self}.read_sync(handle: rx)   # cs8 interleaved I/Q
    #{self}.stop_rx(handle: rx)
    data = #{self}.capture(freq_hz:, rate_hz: 10e6, samples: 262_144)
    #{self}.device_info(device: dev)
    #{self}.close(device: dev)

    #{self}.authors
  "
end

.infoObject

Supported Method Parameters

info = PWN::FFI::HackRF.info Returns Hash with library_version / library_release / available.



73
74
75
76
77
78
79
80
81
# File 'lib/pwn/ffi/hack_rf.rb', line 73

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

  {
    available: true,
    library_version: hackrf_library_version.to_s,
    library_release: hackrf_library_release.to_s
  }
end

.open(opts = {}) ⇒ Object

Supported Method Parameters

dev = PWN::FFI::HackRF.open(serial: nil) Returns FFI::Pointer (opaque hackrf_device*) or raises.



87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/pwn/ffi/hack_rf.rb', line 87

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

  check!(hackrf_init)
  dev_ptr = PubFFI::MemoryPointer.new(:pointer)
  serial = opts[:serial]
  rc = if serial
         hackrf_open_by_serial(serial.to_s, dev_ptr)
       else
         hackrf_open(dev_ptr)
       end
  check!(rc)
  dev_ptr.read_pointer
end

.read_sync(opts = {}) ⇒ Object

Supported Method Parameters

data = PWN::FFI::HackRF.read_sync( handle: 'required - handle from .start_rx', timeout: 'optional - seconds to wait for a chunk (default 1.0)' ) Returns String of interleaved cs8 I/Q, or nil on timeout.



211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/pwn/ffi/hack_rf.rb', line 211

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

  q  = h[:queue]
  to = (opts[:timeout] || 1.0).to_f
  deadline = Time.now + to
  while q.empty?
    return nil if Time.now > deadline

    sleep 0.005
  end
  q.pop
end

.start_rx(opts = {}) ⇒ Object

Supported Method Parameters

rx = PWN::FFI::HackRF.start_rx( device: 'required - pointer from .open', max_queue: 'optional - max buffered chunks (default 64)' ) Returns { device:, queue:, callback: } handle for read_sync/stop_rx. libhackrf runs the callback on its own libusb thread; FFI acquires the GVL for us, so keep the callback body to a bare byte copy.



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/pwn/ffi/hack_rf.rb', line 182

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

  dev = opts[:device]
  raise 'ERROR: :device required' if dev.nil? || dev.null?

  max_q = (opts[:max_queue] || 64).to_i
  queue = Queue.new
  cb = PubFFI::Function.new(:int, [:pointer]) do |xfer_ptr|
    begin
      xfer = Transfer.new(xfer_ptr)
      len  = xfer[:valid_length].to_i
      queue.push(xfer[:buffer].read_bytes(len)) if len.positive? && queue.size < max_q
    rescue StandardError
      nil
    end
    0
  end
  check!(hackrf_start_rx(dev, cb, nil))
  { device: dev, queue: queue, callback: cb }
end

.stop_rx(opts = {}) ⇒ Object

Supported Method Parameters

PWN::FFI::HackRF.stop_rx(handle: rx)



229
230
231
232
233
234
235
236
237
238
239
# File 'lib/pwn/ffi/hack_rf.rb', line 229

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

  hackrf_stop_rx(h[:device])
  h[:queue]&.clear
  h[:callback] = nil
  nil
rescue StandardError
  nil
end