rtlsdr 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/rtlsdr/dsp.rb ADDED
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RTLSDR
4
+ # Digital Signal Processing utilities for RTL-SDR
5
+ #
6
+ # The DSP module provides essential signal processing functions for working
7
+ # with RTL-SDR sample data. It includes utilities for converting raw IQ data
8
+ # to complex samples, calculating power spectra, performing filtering
9
+ # operations, and extracting signal characteristics.
10
+ #
11
+ # All methods are designed to work with Ruby's Complex number type and
12
+ # standard Array collections, making them easy to integrate into Ruby
13
+ # applications and pipelines.
14
+ #
15
+ # Features:
16
+ # * IQ data conversion to complex samples
17
+ # * Power spectrum analysis with windowing
18
+ # * Peak detection and frequency estimation
19
+ # * DC removal and filtering
20
+ # * Magnitude and phase extraction
21
+ # * Average power calculation
22
+ #
23
+ # @example Basic signal analysis
24
+ # raw_data = device.read_sync(2048)
25
+ # samples = RTLSDR::DSP.iq_to_complex(raw_data)
26
+ # power = RTLSDR::DSP.average_power(samples)
27
+ # spectrum = RTLSDR::DSP.power_spectrum(samples)
28
+ # peak_idx, peak_power = RTLSDR::DSP.find_peak(spectrum)
29
+ #
30
+ # @example Signal conditioning
31
+ # filtered = RTLSDR::DSP.remove_dc(samples)
32
+ # magnitudes = RTLSDR::DSP.magnitude(filtered)
33
+ # phases = RTLSDR::DSP.phase(filtered)
34
+ module DSP
35
+ # Convert raw IQ data to complex samples
36
+ def self.iq_to_complex(data)
37
+ samples = []
38
+ (0...data.length).step(2) do |i|
39
+ i_sample = (data[i] - 128) / 128.0
40
+ q_sample = (data[i + 1] - 128) / 128.0
41
+ samples << Complex(i_sample, q_sample)
42
+ end
43
+ samples
44
+ end
45
+
46
+ # Calculate power spectral density
47
+ def self.power_spectrum(samples, window_size = 1024)
48
+ return [] if samples.length < window_size
49
+
50
+ windowed_samples = samples.take(window_size)
51
+
52
+ # Apply Hanning window
53
+ windowed_samples = windowed_samples.each_with_index.map do |sample, i|
54
+ window_factor = 0.5 * (1 - Math.cos(2 * Math::PI * i / (window_size - 1)))
55
+ sample * window_factor
56
+ end
57
+
58
+ # Simple magnitude calculation (real FFT would require external library)
59
+ windowed_samples.map { |s| ((s.real**2) + (s.imag**2)) }
60
+ end
61
+
62
+ # Calculate average power
63
+ def self.average_power(samples)
64
+ return 0.0 if samples.empty?
65
+
66
+ total_power = samples.reduce(0.0) { |sum, sample| sum + sample.abs2 }
67
+ total_power / samples.length
68
+ end
69
+
70
+ # Find peak power and frequency bin
71
+ def self.find_peak(power_spectrum)
72
+ return [0, 0.0] if power_spectrum.empty?
73
+
74
+ max_power = power_spectrum.max
75
+ max_index = power_spectrum.index(max_power)
76
+ [max_index, max_power]
77
+ end
78
+
79
+ # DC removal (high-pass filter)
80
+ def self.remove_dc(samples, alpha = 0.995)
81
+ return samples if samples.empty?
82
+
83
+ filtered = [samples.first]
84
+ (1...samples.length).each do |i|
85
+ filtered[i] = samples[i] - samples[i - 1] + (alpha * filtered[i - 1])
86
+ end
87
+ filtered
88
+ end
89
+
90
+ # Simple magnitude detection
91
+ def self.magnitude(samples)
92
+ samples.map(&:abs)
93
+ end
94
+
95
+ # Phase detection
96
+ def self.phase(samples)
97
+ samples.map { |s| Math.atan2(s.imag, s.real) }
98
+ end
99
+
100
+ # Frequency estimation using zero crossings
101
+ def self.estimate_frequency(samples, sample_rate)
102
+ return 0.0 if samples.length < 2
103
+
104
+ magnitudes = magnitude(samples)
105
+ zero_crossings = 0
106
+
107
+ (1...magnitudes.length).each do |i|
108
+ if (magnitudes[i - 1] >= 0 && magnitudes[i].negative?) ||
109
+ (magnitudes[i - 1].negative? && magnitudes[i] >= 0)
110
+ zero_crossings += 1
111
+ end
112
+ end
113
+
114
+ # Frequency = (zero crossings / 2) / time_duration
115
+ time_duration = samples.length.to_f / sample_rate
116
+ (zero_crossings / 2.0) / time_duration
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RTLSDR
4
+ class Error < StandardError; end
5
+ class DeviceNotFoundError < Error; end
6
+ class DeviceOpenError < Error; end
7
+ class DeviceNotOpenError < Error; end
8
+ class InvalidArgumentError < Error; end
9
+ class OperationFailedError < Error; end
10
+ class EEPROMError < Error; end
11
+ class CallbackError < Error; end
12
+ end
data/lib/rtlsdr/ffi.rb ADDED
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ module RTLSDR
6
+ # Low-level FFI bindings to librtlsdr
7
+ #
8
+ # The FFI module provides direct 1:1 bindings to the librtlsdr C library,
9
+ # exposing all native functions with their original signatures and behaviors.
10
+ # This module handles library loading from multiple common locations and
11
+ # defines all necessary data types, constants, and function prototypes.
12
+ #
13
+ # This is the foundation layer that the high-level Device class is built upon,
14
+ # but can also be used directly for applications that need complete control
15
+ # over the C API or want to implement custom abstractions.
16
+ #
17
+ # Features:
18
+ # * Complete librtlsdr API coverage
19
+ # * Automatic library discovery and loading
20
+ # * Proper FFI type definitions and callbacks
21
+ # * Tuner type constants and helper functions
22
+ # * Memory management support for pointers
23
+ #
24
+ # @example Direct FFI usage
25
+ # device_count = RTLSDR::FFI.rtlsdr_get_device_count
26
+ # device_ptr = FFI::MemoryPointer.new(:pointer)
27
+ # result = RTLSDR::FFI.rtlsdr_open(device_ptr, 0)
28
+ # handle = device_ptr.read_pointer
29
+ # RTLSDR::FFI.rtlsdr_set_center_freq(handle, 100_000_000)
30
+ #
31
+ # @note Most users should use the high-level RTLSDR::Device class instead
32
+ # of calling these FFI functions directly.
33
+ module FFI
34
+ extend ::FFI::Library
35
+
36
+ # Try to load the library from various locations
37
+ begin
38
+ ffi_lib "rtlsdr"
39
+ rescue LoadError
40
+ begin
41
+ ffi_lib "./librtlsdr/build/install/lib/librtlsdr.so"
42
+ rescue LoadError
43
+ begin
44
+ ffi_lib "/usr/local/lib/librtlsdr.so"
45
+ rescue LoadError
46
+ begin
47
+ ffi_lib "/usr/lib/librtlsdr.so"
48
+ rescue LoadError # rubocop:disable Metrics/BlockNesting
49
+ raise LoadError,
50
+ "Could not find librtlsdr. Make sure it's installed or built in librtlsdr/build/install/lib/"
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ # Opaque device pointer
57
+ typedef :pointer, :rtlsdr_dev_t
58
+
59
+ # Tuner types enum
60
+ RTLSDR_TUNER_UNKNOWN = 0
61
+ RTLSDR_TUNER_E4000 = 1
62
+ RTLSDR_TUNER_FC0012 = 2
63
+ RTLSDR_TUNER_FC0013 = 3
64
+ RTLSDR_TUNER_FC2580 = 4
65
+ RTLSDR_TUNER_R820T = 5
66
+ RTLSDR_TUNER_R828D = 6
67
+
68
+ # Callback for async reading
69
+ callback :rtlsdr_read_async_cb_t, %i[pointer uint32 pointer], :void
70
+
71
+ # Device enumeration functions
72
+ attach_function :rtlsdr_get_device_count, [], :uint32
73
+ attach_function :rtlsdr_get_device_name, [:uint32], :string
74
+ attach_function :rtlsdr_get_device_usb_strings, %i[uint32 pointer pointer pointer], :int
75
+ attach_function :rtlsdr_get_index_by_serial, [:string], :int
76
+
77
+ # Device control functions
78
+ attach_function :rtlsdr_open, %i[pointer uint32], :int
79
+ attach_function :rtlsdr_close, [:rtlsdr_dev_t], :int
80
+
81
+ # Configuration functions
82
+ attach_function :rtlsdr_set_xtal_freq, %i[rtlsdr_dev_t uint32 uint32], :int
83
+ attach_function :rtlsdr_get_xtal_freq, %i[rtlsdr_dev_t pointer pointer], :int
84
+ attach_function :rtlsdr_get_usb_strings, %i[rtlsdr_dev_t pointer pointer pointer], :int
85
+ attach_function :rtlsdr_write_eeprom, %i[rtlsdr_dev_t pointer uint8 uint16], :int
86
+ attach_function :rtlsdr_read_eeprom, %i[rtlsdr_dev_t pointer uint8 uint16], :int
87
+
88
+ # Frequency control
89
+ attach_function :rtlsdr_set_center_freq, %i[rtlsdr_dev_t uint32], :int
90
+ attach_function :rtlsdr_get_center_freq, [:rtlsdr_dev_t], :uint32
91
+ attach_function :rtlsdr_set_freq_correction, %i[rtlsdr_dev_t int], :int
92
+ attach_function :rtlsdr_get_freq_correction, [:rtlsdr_dev_t], :int
93
+
94
+ # Tuner functions
95
+ attach_function :rtlsdr_get_tuner_type, [:rtlsdr_dev_t], :int
96
+ attach_function :rtlsdr_get_tuner_gains, %i[rtlsdr_dev_t pointer], :int
97
+ attach_function :rtlsdr_set_tuner_gain, %i[rtlsdr_dev_t int], :int
98
+ attach_function :rtlsdr_set_tuner_bandwidth, %i[rtlsdr_dev_t uint32], :int
99
+ attach_function :rtlsdr_get_tuner_gain, [:rtlsdr_dev_t], :int
100
+ attach_function :rtlsdr_set_tuner_if_gain, %i[rtlsdr_dev_t int int], :int
101
+ attach_function :rtlsdr_set_tuner_gain_mode, %i[rtlsdr_dev_t int], :int
102
+
103
+ # Sample rate and mode functions
104
+ attach_function :rtlsdr_set_sample_rate, %i[rtlsdr_dev_t uint32], :int
105
+ attach_function :rtlsdr_get_sample_rate, [:rtlsdr_dev_t], :uint32
106
+ attach_function :rtlsdr_set_testmode, %i[rtlsdr_dev_t int], :int
107
+ attach_function :rtlsdr_set_agc_mode, %i[rtlsdr_dev_t int], :int
108
+ attach_function :rtlsdr_set_direct_sampling, %i[rtlsdr_dev_t int], :int
109
+ attach_function :rtlsdr_get_direct_sampling, [:rtlsdr_dev_t], :int
110
+ attach_function :rtlsdr_set_offset_tuning, %i[rtlsdr_dev_t int], :int
111
+ attach_function :rtlsdr_get_offset_tuning, [:rtlsdr_dev_t], :int
112
+
113
+ # Streaming functions
114
+ attach_function :rtlsdr_reset_buffer, [:rtlsdr_dev_t], :int
115
+ attach_function :rtlsdr_read_sync, %i[rtlsdr_dev_t pointer int pointer], :int
116
+ attach_function :rtlsdr_wait_async, %i[rtlsdr_dev_t rtlsdr_read_async_cb_t pointer], :int
117
+ attach_function :rtlsdr_read_async, %i[rtlsdr_dev_t rtlsdr_read_async_cb_t pointer uint32 uint32], :int
118
+ attach_function :rtlsdr_cancel_async, [:rtlsdr_dev_t], :int
119
+
120
+ # Bias tee functions
121
+ attach_function :rtlsdr_set_bias_tee, %i[rtlsdr_dev_t int], :int
122
+ attach_function :rtlsdr_set_bias_tee_gpio, %i[rtlsdr_dev_t int int], :int
123
+
124
+ # Helper function to get tuner type name
125
+ def self.tuner_type_name(tuner_type)
126
+ case tuner_type
127
+ when RTLSDR_TUNER_UNKNOWN then "Unknown"
128
+ when RTLSDR_TUNER_E4000 then "Elonics E4000"
129
+ when RTLSDR_TUNER_FC0012 then "Fitipower FC0012"
130
+ when RTLSDR_TUNER_FC0013 then "Fitipower FC0013"
131
+ when RTLSDR_TUNER_FC2580 then "FCI FC2580"
132
+ when RTLSDR_TUNER_R820T then "Rafael Micro R820T"
133
+ when RTLSDR_TUNER_R828D then "Rafael Micro R828D"
134
+ else "Unknown (#{tuner_type})"
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RTLSDR
4
+ # Frequency scanning and spectrum analysis
5
+ #
6
+ # The Scanner class provides high-level frequency scanning capabilities for
7
+ # RTL-SDR devices. It automates the process of sweeping across frequency
8
+ # ranges, collecting samples, and analyzing signal characteristics. This is
9
+ # particularly useful for spectrum analysis, signal hunting, and surveillance
10
+ # applications.
11
+ #
12
+ # Features:
13
+ # * Configurable frequency range and step size
14
+ # * Adjustable dwell time per frequency
15
+ # * Synchronous and asynchronous scanning modes
16
+ # * Peak detection with power thresholds
17
+ # * Power sweep analysis
18
+ # * Real-time result callbacks
19
+ # * Thread-safe scanning control
20
+ #
21
+ # @example Basic frequency scan
22
+ # scanner = RTLSDR::Scanner.new(
23
+ # device,
24
+ # start_freq: 88_000_000, # 88 MHz
25
+ # end_freq: 108_000_000, # 108 MHz
26
+ # step_size: 100_000, # 100 kHz steps
27
+ # dwell_time: 0.1 # 100ms per frequency
28
+ # )
29
+ #
30
+ # scanner.scan do |result|
31
+ # puts "#{result[:frequency] / 1e6} MHz: #{result[:power]} dBm"
32
+ # end
33
+ #
34
+ # @example Find strong signals
35
+ # peaks = scanner.find_peaks(threshold: -60)
36
+ # peaks.each do |peak|
37
+ # puts "Strong signal at #{peak[:frequency] / 1e6} MHz"
38
+ # end
39
+ class Scanner
40
+ attr_reader :device, :start_freq, :end_freq, :step_size, :dwell_time
41
+
42
+ def initialize(device, start_freq:, end_freq:, step_size: 1_000_000, dwell_time: 0.1)
43
+ @device = device
44
+ @start_freq = start_freq
45
+ @end_freq = end_freq
46
+ @step_size = step_size
47
+ @dwell_time = dwell_time
48
+ @scanning = false
49
+ end
50
+
51
+ def frequencies
52
+ (@start_freq..@end_freq).step(@step_size).to_a
53
+ end
54
+
55
+ def frequency_count
56
+ ((end_freq - start_freq) / step_size).to_i + 1
57
+ end
58
+
59
+ # Perform a sweep scan
60
+ def scan(samples_per_freq: 1024, &block)
61
+ raise ArgumentError, "Block required for scan" unless block_given?
62
+
63
+ @scanning = true
64
+ results = {}
65
+
66
+ frequencies.each do |freq|
67
+ break unless @scanning
68
+
69
+ @device.center_freq = freq
70
+ sleep(@dwell_time)
71
+
72
+ samples = @device.read_samples(samples_per_freq)
73
+ power = DSP.average_power(samples)
74
+
75
+ result = {
76
+ frequency: freq,
77
+ power: power,
78
+ samples: samples,
79
+ timestamp: Time.now
80
+ }
81
+
82
+ results[freq] = result
83
+ block.call(result)
84
+ end
85
+
86
+ @scanning = false
87
+ results
88
+ end
89
+
90
+ # Async sweep scan
91
+ def scan_async(samples_per_freq: 1024, &block)
92
+ raise ArgumentError, "Block required for async scan" unless block_given?
93
+
94
+ Thread.new do
95
+ scan(samples_per_freq: samples_per_freq, &block)
96
+ end
97
+ end
98
+
99
+ # Find peaks in the spectrum
100
+ def find_peaks(threshold: -60, samples_per_freq: 1024)
101
+ peaks = []
102
+
103
+ scan(samples_per_freq: samples_per_freq) do |result|
104
+ power_db = 10 * Math.log10(result[:power] + 1e-10)
105
+ if power_db > threshold
106
+ peaks << {
107
+ frequency: result[:frequency],
108
+ power: result[:power],
109
+ power_db: power_db,
110
+ timestamp: result[:timestamp]
111
+ }
112
+ end
113
+ end
114
+
115
+ peaks.sort_by { |peak| -peak[:power] }
116
+ end
117
+
118
+ # Power sweep - returns array of [frequency, power] pairs
119
+ def power_sweep(samples_per_freq: 1024)
120
+ results = []
121
+
122
+ scan(samples_per_freq: samples_per_freq) do |result|
123
+ power_db = 10 * Math.log10(result[:power] + 1e-10)
124
+ results << [result[:frequency], power_db]
125
+ end
126
+
127
+ results
128
+ end
129
+
130
+ def stop
131
+ @scanning = false
132
+ end
133
+
134
+ def scanning?
135
+ @scanning
136
+ end
137
+
138
+ # Configure scan parameters
139
+ def configure(start_freq: nil, end_freq: nil, step_size: nil, dwell_time: nil)
140
+ @start_freq = start_freq if start_freq
141
+ @end_freq = end_freq if end_freq
142
+ @step_size = step_size if step_size
143
+ @dwell_time = dwell_time if dwell_time
144
+ self
145
+ end
146
+
147
+ def inspect
148
+ "#<RTLSDR::Scanner #{@start_freq / 1e6}MHz-#{@end_freq / 1e6}MHz step=#{@step_size / 1e6}MHz dwell=#{@dwell_time}s>" # rubocop:disable Layout/LineLength
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RTLSDR
4
+ VERSION = "0.1.0"
5
+ end
data/lib/rtlsdr.rb ADDED
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rtlsdr/version"
4
+ require_relative "rtlsdr/ffi"
5
+ require_relative "rtlsdr/device"
6
+ require_relative "rtlsdr/errors"
7
+ require_relative "rtlsdr/dsp"
8
+ require_relative "rtlsdr/scanner"
9
+
10
+ # Ruby bindings for RTL-SDR (Software Defined Radio) devices
11
+ #
12
+ # RTLSDR provides a complete Ruby interface to RTL-SDR USB dongles, enabling
13
+ # software-defined radio applications. It offers both low-level FFI bindings
14
+ # that map directly to the librtlsdr C API and high-level Ruby classes with
15
+ # idiomatic methods and DSLs.
16
+ #
17
+ # Features:
18
+ # * Device enumeration and control
19
+ # * Frequency, gain, and sample rate configuration
20
+ # * Synchronous and asynchronous sample reading
21
+ # * Signal processing utilities (DSP)
22
+ # * Frequency scanning and spectrum analysis
23
+ # * EEPROM reading/writing and bias tee control
24
+ #
25
+ # @example Basic usage
26
+ # device = RTLSDR.open(0)
27
+ # device.sample_rate = 2_048_000
28
+ # device.center_freq = 100_000_000
29
+ # device.gain = 496
30
+ # samples = device.read_samples(1024)
31
+ # device.close
32
+ #
33
+ # @example List all devices
34
+ # RTLSDR.devices.each do |dev|
35
+ # puts "#{dev[:index]}: #{dev[:name]}"
36
+ # end
37
+ module RTLSDR
38
+ class << self
39
+ # Get the number of connected RTL-SDR devices
40
+ def device_count
41
+ FFI.rtlsdr_get_device_count
42
+ end
43
+
44
+ # Get the name of a device by index
45
+ def device_name(index)
46
+ FFI.rtlsdr_get_device_name(index)
47
+ end
48
+
49
+ # Get USB strings for a device by index
50
+ def device_usb_strings(index)
51
+ manufact = " " * 256
52
+ product = " " * 256
53
+ serial = " " * 256
54
+
55
+ result = FFI.rtlsdr_get_device_usb_strings(index, manufact, product, serial)
56
+ return nil if result != 0
57
+
58
+ {
59
+ manufacturer: manufact.strip,
60
+ product: product.strip,
61
+ serial: serial.strip
62
+ }
63
+ end
64
+
65
+ # Find device index by serial number
66
+ def find_device_by_serial(serial)
67
+ result = FFI.rtlsdr_get_index_by_serial(serial)
68
+ return nil if result.negative?
69
+
70
+ result
71
+ end
72
+
73
+ # Open a device and return a Device instance
74
+ def open(index = 0)
75
+ Device.new(index)
76
+ end
77
+
78
+ # List all available devices
79
+ def devices
80
+ (0...device_count).map do |i|
81
+ {
82
+ index: i,
83
+ name: device_name(i),
84
+ usb_strings: device_usb_strings(i)
85
+ }
86
+ end
87
+ end
88
+ end
89
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rtlsdr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - joshfng
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ffi
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.15'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.15'
26
+ description: Ruby bindings for librtlsdr - turn RTL2832 based DVB dongles into SDR
27
+ receivers
28
+ email:
29
+ - me@joshfrye.dev
30
+ executables:
31
+ - rtlsdr
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".rspec"
36
+ - ".rubocop.yml"
37
+ - ".ruby-version"
38
+ - CHANGELOG.md
39
+ - LICENSE.txt
40
+ - README.md
41
+ - Rakefile
42
+ - examples/basic_usage.rb
43
+ - examples/spectrum_analyzer.rb
44
+ - exe/rtlsdr
45
+ - lib/rtlsdr.rb
46
+ - lib/rtlsdr/device.rb
47
+ - lib/rtlsdr/dsp.rb
48
+ - lib/rtlsdr/errors.rb
49
+ - lib/rtlsdr/ffi.rb
50
+ - lib/rtlsdr/scanner.rb
51
+ - lib/rtlsdr/version.rb
52
+ homepage: https://github.com/joshfng/rtlsdr-ruby
53
+ licenses:
54
+ - MIT
55
+ metadata:
56
+ allowed_push_host: https://rubygems.org
57
+ rubygems_mfa_required: 'true'
58
+ homepage_uri: https://github.com/joshfng/rtlsdr-ruby
59
+ source_code_uri: https://github.com/joshfng/rtlsdr-ruby
60
+ changelog_uri: https://github.com/joshfng/rtlsdr-ruby/blob/main/CHANGELOG.md
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 3.3.0
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.6.7
76
+ specification_version: 4
77
+ summary: Ruby bindings for librtlsdr
78
+ test_files: []