rtlsdr 0.1.0 → 0.1.2
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 +4 -4
- data/.yardconfig +42 -0
- data/.yardopts +17 -0
- data/CHANGELOG.md +23 -0
- data/Rakefile +250 -1
- data/doc_config.rb +25 -0
- data/lib/rtlsdr/device.rb +207 -2
- data/lib/rtlsdr/dsp.rb +87 -6
- data/lib/rtlsdr/errors.rb +60 -0
- data/lib/rtlsdr/ffi.rb +38 -3
- data/lib/rtlsdr/scanner.rb +114 -6
- data/lib/rtlsdr/version.rb +12 -1
- data/lib/rtlsdr.rb +53 -2
- metadata +15 -3
data/lib/rtlsdr/ffi.rb
CHANGED
@@ -30,6 +30,7 @@ module RTLSDR
|
|
30
30
|
#
|
31
31
|
# @note Most users should use the high-level RTLSDR::Device class instead
|
32
32
|
# of calling these FFI functions directly.
|
33
|
+
# @since 0.1.0
|
33
34
|
module FFI
|
34
35
|
extend ::FFI::Library
|
35
36
|
|
@@ -56,26 +57,54 @@ module RTLSDR
|
|
56
57
|
# Opaque device pointer
|
57
58
|
typedef :pointer, :rtlsdr_dev_t
|
58
59
|
|
59
|
-
# Tuner types enum
|
60
|
+
# Tuner types enum constants
|
61
|
+
#
|
62
|
+
# These constants identify the different tuner chips that can be found
|
63
|
+
# in RTL-SDR devices. Each tuner has different characteristics and
|
64
|
+
# supported frequency ranges.
|
65
|
+
|
66
|
+
# Unknown or unsupported tuner type
|
60
67
|
RTLSDR_TUNER_UNKNOWN = 0
|
68
|
+
# Elonics E4000 tuner (52 - 2200 MHz with gaps)
|
61
69
|
RTLSDR_TUNER_E4000 = 1
|
70
|
+
# Fitipower FC0012 tuner (22 - 948 MHz)
|
62
71
|
RTLSDR_TUNER_FC0012 = 2
|
72
|
+
# Fitipower FC0013 tuner (22 - 1100 MHz)
|
63
73
|
RTLSDR_TUNER_FC0013 = 3
|
74
|
+
# FCI FC2580 tuner (146 - 308 MHz and 438 - 924 MHz)
|
64
75
|
RTLSDR_TUNER_FC2580 = 4
|
76
|
+
# Rafael Micro R820T tuner (24 - 1766 MHz)
|
65
77
|
RTLSDR_TUNER_R820T = 5
|
78
|
+
# Rafael Micro R828D tuner (24 - 1766 MHz)
|
66
79
|
RTLSDR_TUNER_R828D = 6
|
67
80
|
|
68
|
-
# Callback for
|
81
|
+
# Callback function type for asynchronous reading
|
82
|
+
#
|
83
|
+
# This callback is called by the C library when new sample data is available
|
84
|
+
# during asynchronous reading operations. The callback receives a pointer to
|
85
|
+
# the buffer data, the length of the buffer, and a user context pointer.
|
69
86
|
callback :rtlsdr_read_async_cb_t, %i[pointer uint32 pointer], :void
|
70
87
|
|
71
88
|
# Device enumeration functions
|
89
|
+
|
90
|
+
# Get the number of available RTL-SDR devices
|
72
91
|
attach_function :rtlsdr_get_device_count, [], :uint32
|
92
|
+
|
93
|
+
# Get the name of an RTL-SDR device by index
|
73
94
|
attach_function :rtlsdr_get_device_name, [:uint32], :string
|
95
|
+
|
96
|
+
# Get USB device strings (manufacturer, product, serial)
|
74
97
|
attach_function :rtlsdr_get_device_usb_strings, %i[uint32 pointer pointer pointer], :int
|
98
|
+
|
99
|
+
# Find device index by serial number
|
75
100
|
attach_function :rtlsdr_get_index_by_serial, [:string], :int
|
76
101
|
|
77
102
|
# Device control functions
|
103
|
+
|
104
|
+
# Open an RTL-SDR device
|
78
105
|
attach_function :rtlsdr_open, %i[pointer uint32], :int
|
106
|
+
|
107
|
+
# Close an RTL-SDR device
|
79
108
|
attach_function :rtlsdr_close, [:rtlsdr_dev_t], :int
|
80
109
|
|
81
110
|
# Configuration functions
|
@@ -121,7 +150,13 @@ module RTLSDR
|
|
121
150
|
attach_function :rtlsdr_set_bias_tee, %i[rtlsdr_dev_t int], :int
|
122
151
|
attach_function :rtlsdr_set_bias_tee_gpio, %i[rtlsdr_dev_t int int], :int
|
123
152
|
|
124
|
-
#
|
153
|
+
# Convert tuner type constant to human-readable name
|
154
|
+
#
|
155
|
+
# @param [Integer] tuner_type One of the RTLSDR_TUNER_* constants
|
156
|
+
# @return [String] Human-readable tuner name with chip details
|
157
|
+
# @example Get tuner name
|
158
|
+
# RTLSDR::FFI.tuner_type_name(RTLSDR::FFI::RTLSDR_TUNER_R820T)
|
159
|
+
# # => "Rafael Micro R820T"
|
125
160
|
def self.tuner_type_name(tuner_type)
|
126
161
|
case tuner_type
|
127
162
|
when RTLSDR_TUNER_UNKNOWN then "Unknown"
|
data/lib/rtlsdr/scanner.rb
CHANGED
@@ -37,8 +37,32 @@ module RTLSDR
|
|
37
37
|
# puts "Strong signal at #{peak[:frequency] / 1e6} MHz"
|
38
38
|
# end
|
39
39
|
class Scanner
|
40
|
-
|
40
|
+
# @return [RTLSDR::Device] The RTL-SDR device being used for scanning
|
41
|
+
attr_reader :device
|
42
|
+
# @return [Integer] Starting frequency in Hz
|
43
|
+
attr_reader :start_freq
|
44
|
+
# @return [Integer] Ending frequency in Hz
|
45
|
+
attr_reader :end_freq
|
46
|
+
# @return [Integer] Frequency step size in Hz
|
47
|
+
attr_reader :step_size
|
48
|
+
# @return [Float] Time to dwell on each frequency in seconds
|
49
|
+
attr_reader :dwell_time
|
41
50
|
|
51
|
+
# Create a new frequency scanner
|
52
|
+
#
|
53
|
+
# @param [RTLSDR::Device] device RTL-SDR device to use for scanning
|
54
|
+
# @param [Integer] start_freq Starting frequency in Hz
|
55
|
+
# @param [Integer] end_freq Ending frequency in Hz
|
56
|
+
# @param [Integer] step_size Frequency step size in Hz (default: 1 MHz)
|
57
|
+
# @param [Float] dwell_time Time to spend on each frequency in seconds (default: 0.1s)
|
58
|
+
# @example Create FM band scanner
|
59
|
+
# scanner = RTLSDR::Scanner.new(
|
60
|
+
# device,
|
61
|
+
# start_freq: 88_000_000, # 88 MHz
|
62
|
+
# end_freq: 108_000_000, # 108 MHz
|
63
|
+
# step_size: 200_000, # 200 kHz
|
64
|
+
# dwell_time: 0.05 # 50ms per frequency
|
65
|
+
# )
|
42
66
|
def initialize(device, start_freq:, end_freq:, step_size: 1_000_000, dwell_time: 0.1)
|
43
67
|
@device = device
|
44
68
|
@start_freq = start_freq
|
@@ -48,15 +72,38 @@ module RTLSDR
|
|
48
72
|
@scanning = false
|
49
73
|
end
|
50
74
|
|
75
|
+
# Get array of all frequencies to be scanned
|
76
|
+
#
|
77
|
+
# @return [Array<Integer>] Array of frequencies in Hz
|
78
|
+
# @example Get frequency list
|
79
|
+
# freqs = scanner.frequencies
|
80
|
+
# puts "Will scan #{freqs.length} frequencies"
|
51
81
|
def frequencies
|
52
82
|
(@start_freq..@end_freq).step(@step_size).to_a
|
53
83
|
end
|
54
84
|
|
85
|
+
# Get total number of frequencies to be scanned
|
86
|
+
#
|
87
|
+
# @return [Integer] Number of frequency steps
|
55
88
|
def frequency_count
|
56
89
|
((end_freq - start_freq) / step_size).to_i + 1
|
57
90
|
end
|
58
91
|
|
59
|
-
# Perform a sweep scan
|
92
|
+
# Perform a frequency sweep scan
|
93
|
+
#
|
94
|
+
# Scans through all frequencies in the configured range, collecting samples
|
95
|
+
# and calling the provided block for each frequency. The block receives a
|
96
|
+
# hash with frequency, power, samples, and timestamp information.
|
97
|
+
#
|
98
|
+
# @param [Integer] samples_per_freq Number of samples to collect per frequency
|
99
|
+
# @yield [Hash] Block called for each frequency with scan results
|
100
|
+
# @yieldparam result [Hash] Scan result containing :frequency, :power, :samples, :timestamp
|
101
|
+
# @return [Hash] Hash of all results keyed by frequency
|
102
|
+
# @example Scan and log results
|
103
|
+
# results = scanner.scan(samples_per_freq: 2048) do |result|
|
104
|
+
# power_db = 10 * Math.log10(result[:power] + 1e-10)
|
105
|
+
# puts "#{result[:frequency]/1e6} MHz: #{power_db.round(1)} dB"
|
106
|
+
# end
|
60
107
|
def scan(samples_per_freq: 1024, &block)
|
61
108
|
raise ArgumentError, "Block required for scan" unless block_given?
|
62
109
|
|
@@ -87,7 +134,20 @@ module RTLSDR
|
|
87
134
|
results
|
88
135
|
end
|
89
136
|
|
90
|
-
#
|
137
|
+
# Perform asynchronous frequency sweep scan
|
138
|
+
#
|
139
|
+
# Same as {#scan} but runs in a separate thread, allowing the calling
|
140
|
+
# thread to continue execution while scanning proceeds in the background.
|
141
|
+
#
|
142
|
+
# @param [Integer] samples_per_freq Number of samples to collect per frequency
|
143
|
+
# @yield [Hash] Block called for each frequency with scan results
|
144
|
+
# @return [Thread] Thread object running the scan
|
145
|
+
# @example Background scanning
|
146
|
+
# scan_thread = scanner.scan_async do |result|
|
147
|
+
# # Process results in background
|
148
|
+
# end
|
149
|
+
# # Do other work...
|
150
|
+
# scan_thread.join # Wait for completion
|
91
151
|
def scan_async(samples_per_freq: 1024, &block)
|
92
152
|
raise ArgumentError, "Block required for async scan" unless block_given?
|
93
153
|
|
@@ -96,7 +156,20 @@ module RTLSDR
|
|
96
156
|
end
|
97
157
|
end
|
98
158
|
|
99
|
-
# Find peaks
|
159
|
+
# Find signal peaks above a power threshold
|
160
|
+
#
|
161
|
+
# Scans the frequency range and returns all frequencies where the signal
|
162
|
+
# power exceeds the specified threshold. Results are sorted by power
|
163
|
+
# in descending order (strongest signals first).
|
164
|
+
#
|
165
|
+
# @param [Float] threshold Power threshold in dB (default: -60 dB)
|
166
|
+
# @param [Integer] samples_per_freq Number of samples per frequency
|
167
|
+
# @return [Array<Hash>] Array of peak information hashes
|
168
|
+
# @example Find strong signals
|
169
|
+
# peaks = scanner.find_peaks(threshold: -50, samples_per_freq: 4096)
|
170
|
+
# peaks.each do |peak|
|
171
|
+
# puts "#{peak[:frequency]/1e6} MHz: #{peak[:power_db]} dB"
|
172
|
+
# end
|
100
173
|
def find_peaks(threshold: -60, samples_per_freq: 1024)
|
101
174
|
peaks = []
|
102
175
|
|
@@ -115,7 +188,19 @@ module RTLSDR
|
|
115
188
|
peaks.sort_by { |peak| -peak[:power] }
|
116
189
|
end
|
117
190
|
|
118
|
-
#
|
191
|
+
# Perform a power sweep across the frequency range
|
192
|
+
#
|
193
|
+
# Scans all frequencies and returns an array of [frequency, power_db] pairs.
|
194
|
+
# This is useful for generating spectrum plots or finding the overall
|
195
|
+
# power distribution across a frequency band.
|
196
|
+
#
|
197
|
+
# @param [Integer] samples_per_freq Number of samples per frequency
|
198
|
+
# @return [Array<Array>] Array of [frequency_hz, power_db] pairs
|
199
|
+
# @example Generate spectrum data
|
200
|
+
# spectrum_data = scanner.power_sweep(samples_per_freq: 2048)
|
201
|
+
# spectrum_data.each do |freq, power|
|
202
|
+
# puts "#{freq/1e6} MHz: #{power.round(1)} dB"
|
203
|
+
# end
|
119
204
|
def power_sweep(samples_per_freq: 1024)
|
120
205
|
results = []
|
121
206
|
|
@@ -127,15 +212,35 @@ module RTLSDR
|
|
127
212
|
results
|
128
213
|
end
|
129
214
|
|
215
|
+
# Stop the current scan operation
|
216
|
+
#
|
217
|
+
# Sets the scanning flag to false, which will cause any active scan
|
218
|
+
# to terminate after the current frequency step completes.
|
219
|
+
#
|
220
|
+
# @return [Boolean] false (new scanning state)
|
130
221
|
def stop
|
131
222
|
@scanning = false
|
132
223
|
end
|
133
224
|
|
225
|
+
# Check if a scan is currently in progress
|
226
|
+
#
|
227
|
+
# @return [Boolean] true if scanning, false otherwise
|
134
228
|
def scanning?
|
135
229
|
@scanning
|
136
230
|
end
|
137
231
|
|
138
|
-
#
|
232
|
+
# Update scan configuration parameters
|
233
|
+
#
|
234
|
+
# Allows modification of scan parameters after the scanner has been created.
|
235
|
+
# Only non-nil parameters will be updated.
|
236
|
+
#
|
237
|
+
# @param [Integer, nil] start_freq New starting frequency in Hz
|
238
|
+
# @param [Integer, nil] end_freq New ending frequency in Hz
|
239
|
+
# @param [Integer, nil] step_size New step size in Hz
|
240
|
+
# @param [Float, nil] dwell_time New dwell time in seconds
|
241
|
+
# @return [Scanner] self for method chaining
|
242
|
+
# @example Reconfigure scanner
|
243
|
+
# scanner.configure(start_freq: 400_000_000, step_size: 25_000)
|
139
244
|
def configure(start_freq: nil, end_freq: nil, step_size: nil, dwell_time: nil)
|
140
245
|
@start_freq = start_freq if start_freq
|
141
246
|
@end_freq = end_freq if end_freq
|
@@ -144,6 +249,9 @@ module RTLSDR
|
|
144
249
|
self
|
145
250
|
end
|
146
251
|
|
252
|
+
# Return string representation of scanner
|
253
|
+
#
|
254
|
+
# @return [String] Human-readable scanner configuration
|
147
255
|
def inspect
|
148
256
|
"#<RTLSDR::Scanner #{@start_freq / 1e6}MHz-#{@end_freq / 1e6}MHz step=#{@step_size / 1e6}MHz dwell=#{@dwell_time}s>" # rubocop:disable Layout/LineLength
|
149
257
|
end
|
data/lib/rtlsdr/version.rb
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module RTLSDR
|
4
|
-
|
4
|
+
# Current version of the RTL-SDR Ruby gem
|
5
|
+
#
|
6
|
+
# This constant defines the semantic version of the Ruby bindings.
|
7
|
+
# The version follows semantic versioning (semver) conventions:
|
8
|
+
# MAJOR.MINOR.PATCH where:
|
9
|
+
# - MAJOR: Backwards-incompatible API changes
|
10
|
+
# - MINOR: New features, backwards-compatible
|
11
|
+
# - PATCH: Bug fixes, backwards-compatible
|
12
|
+
#
|
13
|
+
# @return [String] Current gem version
|
14
|
+
# @since 0.1.0
|
15
|
+
VERSION = "0.1.2"
|
5
16
|
end
|
data/lib/rtlsdr.rb
CHANGED
@@ -37,16 +37,37 @@ require_relative "rtlsdr/scanner"
|
|
37
37
|
module RTLSDR
|
38
38
|
class << self
|
39
39
|
# Get the number of connected RTL-SDR devices
|
40
|
+
#
|
41
|
+
# @return [Integer] Number of RTL-SDR devices found
|
42
|
+
# @example Check for devices
|
43
|
+
# if RTLSDR.device_count > 0
|
44
|
+
# puts "Found #{RTLSDR.device_count} RTL-SDR devices"
|
45
|
+
# end
|
40
46
|
def device_count
|
41
47
|
FFI.rtlsdr_get_device_count
|
42
48
|
end
|
43
49
|
|
44
50
|
# Get the name of a device by index
|
51
|
+
#
|
52
|
+
# @param [Integer] index Device index (0-based)
|
53
|
+
# @return [String] Device name string
|
54
|
+
# @example Get device name
|
55
|
+
# name = RTLSDR.device_name(0)
|
56
|
+
# puts "Device 0: #{name}"
|
45
57
|
def device_name(index)
|
46
58
|
FFI.rtlsdr_get_device_name(index)
|
47
59
|
end
|
48
60
|
|
49
|
-
# Get USB strings for a device by index
|
61
|
+
# Get USB device strings for a device by index
|
62
|
+
#
|
63
|
+
# Retrieves the USB manufacturer, product, and serial number strings
|
64
|
+
# for the specified device.
|
65
|
+
#
|
66
|
+
# @param [Integer] index Device index (0-based)
|
67
|
+
# @return [Hash, nil] Hash with :manufacturer, :product, :serial keys, or nil on error
|
68
|
+
# @example Get USB info
|
69
|
+
# usb_info = RTLSDR.device_usb_strings(0)
|
70
|
+
# puts "#{usb_info[:manufacturer]} #{usb_info[:product]}"
|
50
71
|
def device_usb_strings(index)
|
51
72
|
manufact = " " * 256
|
52
73
|
product = " " * 256
|
@@ -63,6 +84,15 @@ module RTLSDR
|
|
63
84
|
end
|
64
85
|
|
65
86
|
# Find device index by serial number
|
87
|
+
#
|
88
|
+
# Searches for a device with the specified serial number and returns
|
89
|
+
# its index if found.
|
90
|
+
#
|
91
|
+
# @param [String] serial Serial number to search for
|
92
|
+
# @return [Integer, nil] Device index if found, nil otherwise
|
93
|
+
# @example Find device by serial
|
94
|
+
# index = RTLSDR.find_device_by_serial("00000001")
|
95
|
+
# device = RTLSDR.open(index) if index
|
66
96
|
def find_device_by_serial(serial)
|
67
97
|
result = FFI.rtlsdr_get_index_by_serial(serial)
|
68
98
|
return nil if result.negative?
|
@@ -71,11 +101,32 @@ module RTLSDR
|
|
71
101
|
end
|
72
102
|
|
73
103
|
# Open a device and return a Device instance
|
104
|
+
#
|
105
|
+
# Creates and returns a new Device instance for the specified device index.
|
106
|
+
# The device will be automatically opened and ready for use.
|
107
|
+
#
|
108
|
+
# @param [Integer] index Device index to open (default: 0)
|
109
|
+
# @return [RTLSDR::Device] Device instance
|
110
|
+
# @raise [DeviceNotFoundError] if device doesn't exist
|
111
|
+
# @raise [DeviceOpenError] if device cannot be opened
|
112
|
+
# @example Open first device
|
113
|
+
# device = RTLSDR.open(0)
|
114
|
+
# device.center_freq = 100_000_000
|
74
115
|
def open(index = 0)
|
75
116
|
Device.new(index)
|
76
117
|
end
|
77
118
|
|
78
|
-
# List all available devices
|
119
|
+
# List all available devices with their information
|
120
|
+
#
|
121
|
+
# Returns an array of hashes containing information about all connected
|
122
|
+
# RTL-SDR devices, including index, name, and USB strings.
|
123
|
+
#
|
124
|
+
# @return [Array<Hash>] Array of device information hashes
|
125
|
+
# @example List all devices
|
126
|
+
# RTLSDR.devices.each do |device|
|
127
|
+
# puts "#{device[:index]}: #{device[:name]}"
|
128
|
+
# puts " #{device[:usb_strings][:manufacturer]} #{device[:usb_strings][:product]}"
|
129
|
+
# end
|
79
130
|
def devices
|
80
131
|
(0...device_count).map do |i|
|
81
132
|
{
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rtlsdr
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- joshfng
|
@@ -30,15 +30,21 @@ email:
|
|
30
30
|
executables:
|
31
31
|
- rtlsdr
|
32
32
|
extensions: []
|
33
|
-
extra_rdoc_files:
|
33
|
+
extra_rdoc_files:
|
34
|
+
- CHANGELOG.md
|
35
|
+
- LICENSE.txt
|
36
|
+
- README.md
|
34
37
|
files:
|
35
38
|
- ".rspec"
|
36
39
|
- ".rubocop.yml"
|
37
40
|
- ".ruby-version"
|
41
|
+
- ".yardconfig"
|
42
|
+
- ".yardopts"
|
38
43
|
- CHANGELOG.md
|
39
44
|
- LICENSE.txt
|
40
45
|
- README.md
|
41
46
|
- Rakefile
|
47
|
+
- doc_config.rb
|
42
48
|
- examples/basic_usage.rb
|
43
49
|
- examples/spectrum_analyzer.rb
|
44
50
|
- exe/rtlsdr
|
@@ -58,7 +64,13 @@ metadata:
|
|
58
64
|
homepage_uri: https://github.com/joshfng/rtlsdr-ruby
|
59
65
|
source_code_uri: https://github.com/joshfng/rtlsdr-ruby
|
60
66
|
changelog_uri: https://github.com/joshfng/rtlsdr-ruby/blob/main/CHANGELOG.md
|
61
|
-
|
67
|
+
documentation_uri: https://rubydoc.info/gems/rtlsdr
|
68
|
+
bug_tracker_uri: https://github.com/joshfng/rtlsdr-ruby/issues
|
69
|
+
rdoc_options:
|
70
|
+
- "--main"
|
71
|
+
- README.md
|
72
|
+
- "--line-numbers"
|
73
|
+
- "--all"
|
62
74
|
require_paths:
|
63
75
|
- lib
|
64
76
|
required_ruby_version: !ruby/object:Gem::Requirement
|