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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +34 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +397 -0
- data/Rakefile +33 -0
- data/examples/basic_usage.rb +120 -0
- data/examples/spectrum_analyzer.rb +175 -0
- data/exe/rtlsdr +4 -0
- data/lib/rtlsdr/device.rb +482 -0
- data/lib/rtlsdr/dsp.rb +119 -0
- data/lib/rtlsdr/errors.rb +12 -0
- data/lib/rtlsdr/ffi.rb +138 -0
- data/lib/rtlsdr/scanner.rb +151 -0
- data/lib/rtlsdr/version.rb +5 -0
- data/lib/rtlsdr.rb +89 -0
- metadata +78 -0
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
|
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: []
|