rtlsdr 0.1.0 → 0.1.1
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 +14 -0
- data/Rakefile +226 -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/device.rb
CHANGED
@@ -43,8 +43,22 @@ module RTLSDR
|
|
43
43
|
class Device
|
44
44
|
include Enumerable
|
45
45
|
|
46
|
-
|
47
|
-
|
46
|
+
# @return [Integer] Device index (0-based)
|
47
|
+
attr_reader :index
|
48
|
+
# @return [FFI::Pointer] Internal device handle pointer
|
49
|
+
attr_reader :handle
|
50
|
+
|
51
|
+
# Create a new RTL-SDR device instance
|
52
|
+
#
|
53
|
+
# Opens the specified RTL-SDR device and prepares it for use. The device
|
54
|
+
# will be automatically opened during initialization.
|
55
|
+
#
|
56
|
+
# @param [Integer] index Device index to open (default: 0)
|
57
|
+
# @raise [DeviceNotFoundError] if device doesn't exist
|
58
|
+
# @raise [DeviceOpenError] if device cannot be opened
|
59
|
+
# @example Create device instance
|
60
|
+
# device = RTLSDR::Device.new(0)
|
61
|
+
# puts "Opened: #{device.name}"
|
48
62
|
def initialize(index = 0)
|
49
63
|
@index = index
|
50
64
|
@handle = nil
|
@@ -59,6 +73,16 @@ module RTLSDR
|
|
59
73
|
!@handle.nil?
|
60
74
|
end
|
61
75
|
|
76
|
+
# Close the RTL-SDR device
|
77
|
+
#
|
78
|
+
# Closes the device handle and releases system resources. If async reading
|
79
|
+
# is active, it will be cancelled first. After closing, the device cannot
|
80
|
+
# be used until reopened.
|
81
|
+
#
|
82
|
+
# @return [void]
|
83
|
+
# @example Close device
|
84
|
+
# device.close
|
85
|
+
# puts "Device closed"
|
62
86
|
def close
|
63
87
|
return unless open?
|
64
88
|
|
@@ -68,6 +92,9 @@ module RTLSDR
|
|
68
92
|
check_result(result, "Failed to close device")
|
69
93
|
end
|
70
94
|
|
95
|
+
# Check if the device is closed
|
96
|
+
#
|
97
|
+
# @return [Boolean] true if device is closed, false if open
|
71
98
|
def closed?
|
72
99
|
!open?
|
73
100
|
end
|
@@ -77,6 +104,15 @@ module RTLSDR
|
|
77
104
|
RTLSDR.device_name(@index)
|
78
105
|
end
|
79
106
|
|
107
|
+
# Get USB device strings
|
108
|
+
#
|
109
|
+
# Retrieves the USB manufacturer, product, and serial number strings
|
110
|
+
# for this device. Results are cached after the first call.
|
111
|
+
#
|
112
|
+
# @return [Hash] Hash with :manufacturer, :product, :serial keys
|
113
|
+
# @example Get USB information
|
114
|
+
# usb_info = device.usb_strings
|
115
|
+
# puts "#{usb_info[:manufacturer]} #{usb_info[:product]}"
|
80
116
|
def usb_strings
|
81
117
|
return @usb_strings if @usb_strings
|
82
118
|
|
@@ -94,10 +130,22 @@ module RTLSDR
|
|
94
130
|
}
|
95
131
|
end
|
96
132
|
|
133
|
+
# Get the tuner type constant
|
134
|
+
#
|
135
|
+
# Returns the tuner type as one of the RTLSDR_TUNER_* constants.
|
136
|
+
# The result is cached after the first call.
|
137
|
+
#
|
138
|
+
# @return [Integer] Tuner type constant
|
139
|
+
# @see RTLSDR::FFI::RTLSDR_TUNER_*
|
97
140
|
def tuner_type
|
98
141
|
@tuner_type ||= FFI.rtlsdr_get_tuner_type(@handle)
|
99
142
|
end
|
100
143
|
|
144
|
+
# Get human-readable tuner name
|
145
|
+
#
|
146
|
+
# @return [String] Tuner chip name and manufacturer
|
147
|
+
# @example Get tuner information
|
148
|
+
# puts "Tuner: #{device.tuner_name}"
|
101
149
|
def tuner_name
|
102
150
|
FFI.tuner_type_name(tuner_type)
|
103
151
|
end
|
@@ -108,17 +156,33 @@ module RTLSDR
|
|
108
156
|
check_result(result, "Failed to set center frequency")
|
109
157
|
end
|
110
158
|
|
159
|
+
# Get the current center frequency
|
160
|
+
#
|
161
|
+
# @return [Integer] Center frequency in Hz
|
111
162
|
def center_freq
|
112
163
|
FFI.rtlsdr_get_center_freq(@handle)
|
113
164
|
end
|
165
|
+
# @!method frequency
|
166
|
+
# Alias for {#center_freq}
|
167
|
+
# @return [Integer] Center frequency in Hz
|
114
168
|
alias frequency center_freq
|
169
|
+
# @!method frequency=
|
170
|
+
# Alias for {#center_freq=}
|
115
171
|
alias frequency= center_freq=
|
116
172
|
|
173
|
+
# Set frequency correction in PPM
|
174
|
+
#
|
175
|
+
# @param [Integer] ppm Frequency correction in parts per million
|
176
|
+
# @example Set 15 PPM correction
|
177
|
+
# device.freq_correction = 15
|
117
178
|
def freq_correction=(ppm)
|
118
179
|
result = FFI.rtlsdr_set_freq_correction(@handle, ppm)
|
119
180
|
check_result(result, "Failed to set frequency correction")
|
120
181
|
end
|
121
182
|
|
183
|
+
# Get current frequency correction
|
184
|
+
#
|
185
|
+
# @return [Integer] Frequency correction in PPM
|
122
186
|
def freq_correction
|
123
187
|
FFI.rtlsdr_get_freq_correction(@handle)
|
124
188
|
end
|
@@ -130,6 +194,9 @@ module RTLSDR
|
|
130
194
|
[rtl_freq, tuner_freq]
|
131
195
|
end
|
132
196
|
|
197
|
+
# Get crystal oscillator frequencies
|
198
|
+
#
|
199
|
+
# @return [Array<Integer>] Array of [rtl_freq, tuner_freq] in Hz
|
133
200
|
def xtal_freq
|
134
201
|
rtl_freq_ptr = ::FFI::MemoryPointer.new(:uint32)
|
135
202
|
tuner_freq_ptr = ::FFI::MemoryPointer.new(:uint32)
|
@@ -154,37 +221,67 @@ module RTLSDR
|
|
154
221
|
gains_ptr.read_array_of_int(result)
|
155
222
|
end
|
156
223
|
|
224
|
+
# Set tuner gain in tenths of dB
|
225
|
+
#
|
226
|
+
# @param [Integer] gain Gain in tenths of dB (e.g., 496 = 49.6 dB)
|
227
|
+
# @example Set 40 dB gain
|
228
|
+
# device.tuner_gain = 400
|
157
229
|
def tuner_gain=(gain)
|
158
230
|
result = FFI.rtlsdr_set_tuner_gain(@handle, gain)
|
159
231
|
check_result(result, "Failed to set tuner gain")
|
160
232
|
end
|
161
233
|
|
234
|
+
# Get current tuner gain
|
235
|
+
#
|
236
|
+
# @return [Integer] Current gain in tenths of dB
|
162
237
|
def tuner_gain
|
163
238
|
FFI.rtlsdr_get_tuner_gain(@handle)
|
164
239
|
end
|
240
|
+
# @!method gain
|
241
|
+
# Alias for {#tuner_gain}
|
242
|
+
# @return [Integer] Current gain in tenths of dB
|
165
243
|
alias gain tuner_gain
|
244
|
+
# @!method gain=
|
245
|
+
# Alias for {#tuner_gain=}
|
166
246
|
alias gain= tuner_gain=
|
167
247
|
|
248
|
+
# Set gain mode (manual or automatic)
|
249
|
+
#
|
250
|
+
# @param [Boolean] manual true for manual gain mode, false for automatic
|
168
251
|
def tuner_gain_mode=(manual)
|
169
252
|
mode = manual ? 1 : 0
|
170
253
|
result = FFI.rtlsdr_set_tuner_gain_mode(@handle, mode)
|
171
254
|
check_result(result, "Failed to set gain mode")
|
172
255
|
end
|
173
256
|
|
257
|
+
# Enable manual gain mode
|
258
|
+
#
|
259
|
+
# @return [Boolean] true
|
174
260
|
def manual_gain_mode!
|
175
261
|
self.tuner_gain_mode = true
|
176
262
|
end
|
177
263
|
|
264
|
+
# Enable automatic gain mode
|
265
|
+
#
|
266
|
+
# @return [Boolean] false
|
178
267
|
def auto_gain_mode!
|
179
268
|
self.tuner_gain_mode = false
|
180
269
|
end
|
181
270
|
|
271
|
+
# Set IF gain for specific stage
|
272
|
+
#
|
273
|
+
# @param [Integer] stage IF stage number
|
274
|
+
# @param [Integer] gain Gain value in tenths of dB
|
275
|
+
# @return [Integer] The gain value that was set
|
182
276
|
def set_tuner_if_gain(stage, gain)
|
183
277
|
result = FFI.rtlsdr_set_tuner_if_gain(@handle, stage, gain)
|
184
278
|
check_result(result, "Failed to set IF gain")
|
185
279
|
gain
|
186
280
|
end
|
187
281
|
|
282
|
+
# Set tuner bandwidth
|
283
|
+
#
|
284
|
+
# @param [Integer] bw Bandwidth in Hz
|
188
285
|
def tuner_bandwidth=(bw)
|
189
286
|
result = FFI.rtlsdr_set_tuner_bandwidth(@handle, bw)
|
190
287
|
check_result(result, "Failed to set bandwidth")
|
@@ -196,10 +293,18 @@ module RTLSDR
|
|
196
293
|
check_result(result, "Failed to set sample rate")
|
197
294
|
end
|
198
295
|
|
296
|
+
# Get current sample rate
|
297
|
+
#
|
298
|
+
# @return [Integer] Sample rate in Hz
|
199
299
|
def sample_rate
|
200
300
|
FFI.rtlsdr_get_sample_rate(@handle)
|
201
301
|
end
|
302
|
+
# @!method samp_rate
|
303
|
+
# Alias for {#sample_rate}
|
304
|
+
# @return [Integer] Sample rate in Hz
|
202
305
|
alias samp_rate sample_rate
|
306
|
+
# @!method samp_rate=
|
307
|
+
# Alias for {#sample_rate=}
|
203
308
|
alias samp_rate= sample_rate=
|
204
309
|
|
205
310
|
# Mode control
|
@@ -209,25 +314,40 @@ module RTLSDR
|
|
209
314
|
check_result(result, "Failed to set test mode")
|
210
315
|
end
|
211
316
|
|
317
|
+
# Enable test mode
|
318
|
+
#
|
319
|
+
# @return [Boolean] true
|
212
320
|
def test_mode!
|
213
321
|
self.test_mode = true
|
214
322
|
end
|
215
323
|
|
324
|
+
# Set automatic gain control mode
|
325
|
+
#
|
326
|
+
# @param [Boolean] enabled true to enable AGC, false to disable
|
216
327
|
def agc_mode=(enabled)
|
217
328
|
mode = enabled ? 1 : 0
|
218
329
|
result = FFI.rtlsdr_set_agc_mode(@handle, mode)
|
219
330
|
check_result(result, "Failed to set AGC mode")
|
220
331
|
end
|
221
332
|
|
333
|
+
# Enable automatic gain control
|
334
|
+
#
|
335
|
+
# @return [Boolean] true
|
222
336
|
def agc_mode!
|
223
337
|
self.agc_mode = true
|
224
338
|
end
|
225
339
|
|
340
|
+
# Set direct sampling mode
|
341
|
+
#
|
342
|
+
# @param [Integer] mode Direct sampling mode (0=off, 1=I-ADC, 2=Q-ADC)
|
226
343
|
def direct_sampling=(mode)
|
227
344
|
result = FFI.rtlsdr_set_direct_sampling(@handle, mode)
|
228
345
|
check_result(result, "Failed to set direct sampling")
|
229
346
|
end
|
230
347
|
|
348
|
+
# Get current direct sampling mode
|
349
|
+
#
|
350
|
+
# @return [Integer, nil] Direct sampling mode or nil on error
|
231
351
|
def direct_sampling
|
232
352
|
result = FFI.rtlsdr_get_direct_sampling(@handle)
|
233
353
|
return nil if result.negative?
|
@@ -235,12 +355,18 @@ module RTLSDR
|
|
235
355
|
result
|
236
356
|
end
|
237
357
|
|
358
|
+
# Set offset tuning mode
|
359
|
+
#
|
360
|
+
# @param [Boolean] enabled true to enable offset tuning, false to disable
|
238
361
|
def offset_tuning=(enabled)
|
239
362
|
mode = enabled ? 1 : 0
|
240
363
|
result = FFI.rtlsdr_set_offset_tuning(@handle, mode)
|
241
364
|
check_result(result, "Failed to set offset tuning")
|
242
365
|
end
|
243
366
|
|
367
|
+
# Get current offset tuning mode
|
368
|
+
#
|
369
|
+
# @return [Boolean, nil] true if enabled, false if disabled, nil on error
|
244
370
|
def offset_tuning
|
245
371
|
result = FFI.rtlsdr_get_offset_tuning(@handle)
|
246
372
|
return nil if result.negative?
|
@@ -248,6 +374,9 @@ module RTLSDR
|
|
248
374
|
result == 1
|
249
375
|
end
|
250
376
|
|
377
|
+
# Enable offset tuning
|
378
|
+
#
|
379
|
+
# @return [Boolean] true
|
251
380
|
def offset_tuning!
|
252
381
|
self.offset_tuning = true
|
253
382
|
end
|
@@ -259,10 +388,18 @@ module RTLSDR
|
|
259
388
|
check_result(result, "Failed to set bias tee")
|
260
389
|
end
|
261
390
|
|
391
|
+
# Enable bias tee
|
392
|
+
#
|
393
|
+
# @return [Boolean] true
|
262
394
|
def bias_tee!
|
263
395
|
self.bias_tee = true
|
264
396
|
end
|
265
397
|
|
398
|
+
# Set bias tee GPIO state
|
399
|
+
#
|
400
|
+
# @param [Integer] gpio GPIO pin number
|
401
|
+
# @param [Boolean] enabled true to enable, false to disable
|
402
|
+
# @return [Boolean] The enabled state that was set
|
266
403
|
def set_bias_tee_gpio(gpio, enabled)
|
267
404
|
mode = enabled ? 1 : 0
|
268
405
|
result = FFI.rtlsdr_set_bias_tee_gpio(@handle, gpio, mode)
|
@@ -278,6 +415,11 @@ module RTLSDR
|
|
278
415
|
data_ptr.read_array_of_uint8(length)
|
279
416
|
end
|
280
417
|
|
418
|
+
# Write data to EEPROM
|
419
|
+
#
|
420
|
+
# @param [Array<Integer>] data Array of bytes to write
|
421
|
+
# @param [Integer] offset EEPROM offset address
|
422
|
+
# @return [Integer] Number of bytes written
|
281
423
|
def write_eeprom(data, offset)
|
282
424
|
data_ptr = ::FFI::MemoryPointer.new(:uint8, data.length)
|
283
425
|
data_ptr.write_array_of_uint8(data)
|
@@ -292,6 +434,13 @@ module RTLSDR
|
|
292
434
|
check_result(result, "Failed to reset buffer")
|
293
435
|
end
|
294
436
|
|
437
|
+
# Read raw IQ data synchronously
|
438
|
+
#
|
439
|
+
# Reads raw 8-bit IQ data from the device. The buffer is automatically
|
440
|
+
# reset on the first read to avoid stale data.
|
441
|
+
#
|
442
|
+
# @param [Integer] length Number of bytes to read
|
443
|
+
# @return [Array<Integer>] Array of 8-bit unsigned integers
|
295
444
|
def read_sync(length)
|
296
445
|
# Reset buffer before first read to avoid stale data
|
297
446
|
reset_buffer unless @buffer_reset_done
|
@@ -307,6 +456,16 @@ module RTLSDR
|
|
307
456
|
buffer.read_array_of_uint8(n_read)
|
308
457
|
end
|
309
458
|
|
459
|
+
# Read complex samples synchronously
|
460
|
+
#
|
461
|
+
# Reads the specified number of complex samples from the device and
|
462
|
+
# converts them from raw 8-bit IQ data to Ruby Complex numbers.
|
463
|
+
#
|
464
|
+
# @param [Integer] count Number of complex samples to read (default: 1024)
|
465
|
+
# @return [Array<Complex>] Array of complex samples
|
466
|
+
# @example Read 2048 samples
|
467
|
+
# samples = device.read_samples(2048)
|
468
|
+
# puts "Read #{samples.length} samples"
|
310
469
|
def read_samples(count = 1024)
|
311
470
|
# RTL-SDR outputs 8-bit I/Q samples, so we need 2 bytes per complex sample
|
312
471
|
data = read_sync(count * 2)
|
@@ -322,10 +481,24 @@ module RTLSDR
|
|
322
481
|
samples
|
323
482
|
end
|
324
483
|
|
484
|
+
# Check if asynchronous streaming is active
|
485
|
+
#
|
486
|
+
# @return [Boolean] true if streaming, false otherwise
|
325
487
|
def streaming?
|
326
488
|
@streaming
|
327
489
|
end
|
328
490
|
|
491
|
+
# Read raw IQ data asynchronously
|
492
|
+
#
|
493
|
+
# Starts asynchronous reading of raw 8-bit IQ data. The provided block
|
494
|
+
# will be called for each buffer of data received.
|
495
|
+
#
|
496
|
+
# @param [Integer] buffer_count Number of buffers to use (default: 15)
|
497
|
+
# @param [Integer] buffer_length Length of each buffer in bytes (default: 262144)
|
498
|
+
# @yield [Array<Integer>] Block called with each buffer of raw IQ data
|
499
|
+
# @return [Thread] Thread object running the async operation
|
500
|
+
# @raise [ArgumentError] if no block is provided
|
501
|
+
# @raise [OperationFailedError] if already streaming
|
329
502
|
def read_async(buffer_count: 15, buffer_length: 262_144, &block)
|
330
503
|
raise ArgumentError, "Block required for async reading" unless block_given?
|
331
504
|
raise OperationFailedError, "Already streaming" if streaming?
|
@@ -349,6 +522,16 @@ module RTLSDR
|
|
349
522
|
@async_thread
|
350
523
|
end
|
351
524
|
|
525
|
+
# Read complex samples asynchronously
|
526
|
+
#
|
527
|
+
# Starts asynchronous reading and converts raw IQ data to complex samples.
|
528
|
+
# The provided block will be called for each buffer of complex samples.
|
529
|
+
#
|
530
|
+
# @param [Integer] buffer_count Number of buffers to use (default: 15)
|
531
|
+
# @param [Integer] buffer_length Length of each buffer in bytes (default: 262144)
|
532
|
+
# @yield [Array<Complex>] Block called with each buffer of complex samples
|
533
|
+
# @return [Thread] Thread object running the async operation
|
534
|
+
# @raise [ArgumentError] if no block is provided
|
352
535
|
def read_samples_async(buffer_count: 15, buffer_length: 262_144, &block)
|
353
536
|
raise ArgumentError, "Block required for async reading" unless block_given?
|
354
537
|
|
@@ -365,6 +548,12 @@ module RTLSDR
|
|
365
548
|
end
|
366
549
|
end
|
367
550
|
|
551
|
+
# Cancel asynchronous reading operation
|
552
|
+
#
|
553
|
+
# Stops any active asynchronous reading and cleans up resources.
|
554
|
+
# This method is safe to call from within async callbacks.
|
555
|
+
#
|
556
|
+
# @return [void]
|
368
557
|
def cancel_async
|
369
558
|
return unless streaming?
|
370
559
|
|
@@ -439,6 +628,9 @@ module RTLSDR
|
|
439
628
|
}
|
440
629
|
end
|
441
630
|
|
631
|
+
# Return string representation of device
|
632
|
+
#
|
633
|
+
# @return [String] Human-readable device information
|
442
634
|
def inspect
|
443
635
|
if open?
|
444
636
|
"#<RTLSDR::Device:#{object_id.to_s(16)} index=#{@index} name=\"#{name}\" tuner=\"#{tuner_name}\" freq=#{center_freq}Hz rate=#{sample_rate}Hz>" # rubocop:disable Layout/LineLength
|
@@ -449,6 +641,12 @@ module RTLSDR
|
|
449
641
|
|
450
642
|
private
|
451
643
|
|
644
|
+
# Open the RTL-SDR device handle
|
645
|
+
#
|
646
|
+
# @return [void]
|
647
|
+
# @raise [DeviceNotFoundError] if device doesn't exist
|
648
|
+
# @raise [DeviceOpenError] if device cannot be opened
|
649
|
+
# @private
|
452
650
|
def open_device
|
453
651
|
dev_ptr = ::FFI::MemoryPointer.new(:pointer)
|
454
652
|
result = FFI.rtlsdr_open(dev_ptr, @index)
|
@@ -467,6 +665,13 @@ module RTLSDR
|
|
467
665
|
end
|
468
666
|
end
|
469
667
|
|
668
|
+
# Check FFI function result and raise appropriate error
|
669
|
+
#
|
670
|
+
# @param [Integer] result Return code from FFI function
|
671
|
+
# @param [String] message Error message prefix
|
672
|
+
# @return [void]
|
673
|
+
# @raise [DeviceNotOpenError, InvalidArgumentError, EEPROMError, OperationFailedError]
|
674
|
+
# @private
|
470
675
|
def check_result(result, message)
|
471
676
|
return if result.zero?
|
472
677
|
|
data/lib/rtlsdr/dsp.rb
CHANGED
@@ -33,6 +33,17 @@ module RTLSDR
|
|
33
33
|
# phases = RTLSDR::DSP.phase(filtered)
|
34
34
|
module DSP
|
35
35
|
# Convert raw IQ data to complex samples
|
36
|
+
#
|
37
|
+
# Converts raw 8-bit IQ data from RTL-SDR devices to Ruby Complex numbers.
|
38
|
+
# The RTL-SDR outputs unsigned 8-bit integers centered at 128, which are
|
39
|
+
# converted to floating point values in the range [-1.0, 1.0].
|
40
|
+
#
|
41
|
+
# @param [Array<Integer>] data Array of 8-bit unsigned integers (I, Q, I, Q, ...)
|
42
|
+
# @return [Array<Complex>] Array of Complex numbers representing I+jQ samples
|
43
|
+
# @example Convert device samples
|
44
|
+
# raw_data = [127, 130, 125, 135, 120, 140] # 3 IQ pairs
|
45
|
+
# samples = RTLSDR::DSP.iq_to_complex(raw_data)
|
46
|
+
# # => [(-0.008+0.016i), (-0.024+0.055i), (-0.063+0.094i)]
|
36
47
|
def self.iq_to_complex(data)
|
37
48
|
samples = []
|
38
49
|
(0...data.length).step(2) do |i|
|
@@ -44,6 +55,18 @@ module RTLSDR
|
|
44
55
|
end
|
45
56
|
|
46
57
|
# Calculate power spectral density
|
58
|
+
#
|
59
|
+
# Computes a basic power spectrum from complex samples using windowing.
|
60
|
+
# This applies a Hanning window to reduce spectral leakage and then
|
61
|
+
# calculates the power (magnitude squared) for each sample. A proper
|
62
|
+
# FFT implementation would require an external library.
|
63
|
+
#
|
64
|
+
# @param [Array<Complex>] samples Array of complex samples
|
65
|
+
# @param [Integer] window_size Size of the analysis window (default 1024)
|
66
|
+
# @return [Array<Float>] Power spectrum values
|
67
|
+
# @example Calculate spectrum
|
68
|
+
# spectrum = RTLSDR::DSP.power_spectrum(samples, 512)
|
69
|
+
# max_power = spectrum.max
|
47
70
|
def self.power_spectrum(samples, window_size = 1024)
|
48
71
|
return [] if samples.length < window_size
|
49
72
|
|
@@ -59,7 +82,16 @@ module RTLSDR
|
|
59
82
|
windowed_samples.map { |s| ((s.real**2) + (s.imag**2)) }
|
60
83
|
end
|
61
84
|
|
62
|
-
# Calculate average power
|
85
|
+
# Calculate average power of complex samples
|
86
|
+
#
|
87
|
+
# Computes the mean power (magnitude squared) across all samples.
|
88
|
+
# This is useful for signal strength measurements and AGC calculations.
|
89
|
+
#
|
90
|
+
# @param [Array<Complex>] samples Array of complex samples
|
91
|
+
# @return [Float] Average power value (0.0 if no samples)
|
92
|
+
# @example Measure signal power
|
93
|
+
# power = RTLSDR::DSP.average_power(samples)
|
94
|
+
# power_db = 10 * Math.log10(power + 1e-10)
|
63
95
|
def self.average_power(samples)
|
64
96
|
return 0.0 if samples.empty?
|
65
97
|
|
@@ -67,7 +99,17 @@ module RTLSDR
|
|
67
99
|
total_power / samples.length
|
68
100
|
end
|
69
101
|
|
70
|
-
# Find peak power and frequency bin
|
102
|
+
# Find peak power and frequency bin in spectrum
|
103
|
+
#
|
104
|
+
# Locates the frequency bin with maximum power in a power spectrum.
|
105
|
+
# Returns both the bin index and the power value at that bin.
|
106
|
+
#
|
107
|
+
# @param [Array<Float>] power_spectrum Array of power values
|
108
|
+
# @return [Array<Integer, Float>] [bin_index, peak_power] or [0, 0.0] if empty
|
109
|
+
# @example Find strongest signal
|
110
|
+
# spectrum = RTLSDR::DSP.power_spectrum(samples)
|
111
|
+
# peak_bin, peak_power = RTLSDR::DSP.find_peak(spectrum)
|
112
|
+
# freq_offset = (peak_bin - spectrum.length/2) * sample_rate / spectrum.length
|
71
113
|
def self.find_peak(power_spectrum)
|
72
114
|
return [0, 0.0] if power_spectrum.empty?
|
73
115
|
|
@@ -76,7 +118,17 @@ module RTLSDR
|
|
76
118
|
[max_index, max_power]
|
77
119
|
end
|
78
120
|
|
79
|
-
# DC
|
121
|
+
# Remove DC component using high-pass filter
|
122
|
+
#
|
123
|
+
# Applies a simple first-order high-pass filter to remove DC bias
|
124
|
+
# from the signal. This is useful for RTL-SDR devices which often
|
125
|
+
# have a DC offset in their I/Q samples.
|
126
|
+
#
|
127
|
+
# @param [Array<Complex>] samples Array of complex samples
|
128
|
+
# @param [Float] alpha Filter coefficient (0.995 = ~160Hz cutoff at 2.4MHz sample rate)
|
129
|
+
# @return [Array<Complex>] Filtered samples with DC component removed
|
130
|
+
# @example Remove DC bias
|
131
|
+
# clean_samples = RTLSDR::DSP.remove_dc(samples, 0.99)
|
80
132
|
def self.remove_dc(samples, alpha = 0.995)
|
81
133
|
return samples if samples.empty?
|
82
134
|
|
@@ -87,17 +139,46 @@ module RTLSDR
|
|
87
139
|
filtered
|
88
140
|
end
|
89
141
|
|
90
|
-
#
|
142
|
+
# Extract magnitude from complex samples
|
143
|
+
#
|
144
|
+
# Calculates the magnitude (absolute value) of each complex sample.
|
145
|
+
# This converts I+jQ samples to their envelope/amplitude values.
|
146
|
+
#
|
147
|
+
# @param [Array<Complex>] samples Array of complex samples
|
148
|
+
# @return [Array<Float>] Array of magnitude values
|
149
|
+
# @example Get signal envelope
|
150
|
+
# magnitudes = RTLSDR::DSP.magnitude(samples)
|
151
|
+
# peak_amplitude = magnitudes.max
|
91
152
|
def self.magnitude(samples)
|
92
153
|
samples.map(&:abs)
|
93
154
|
end
|
94
155
|
|
95
|
-
#
|
156
|
+
# Extract phase from complex samples
|
157
|
+
#
|
158
|
+
# Calculates the phase angle (argument) of each complex sample in radians.
|
159
|
+
# The phase represents the angle between the I and Q components.
|
160
|
+
#
|
161
|
+
# @param [Array<Complex>] samples Array of complex samples
|
162
|
+
# @return [Array<Float>] Array of phase values in radians (-π to π)
|
163
|
+
# @example Get phase information
|
164
|
+
# phases = RTLSDR::DSP.phase(samples)
|
165
|
+
# phase_degrees = phases.map { |p| p * 180 / Math::PI }
|
96
166
|
def self.phase(samples)
|
97
167
|
samples.map { |s| Math.atan2(s.imag, s.real) }
|
98
168
|
end
|
99
169
|
|
100
|
-
#
|
170
|
+
# Estimate frequency using zero-crossing detection
|
171
|
+
#
|
172
|
+
# Provides a rough frequency estimate by counting zero crossings in the
|
173
|
+
# magnitude signal. This is a simple method that works reasonably well
|
174
|
+
# for single-tone signals but may be inaccurate for complex signals.
|
175
|
+
#
|
176
|
+
# @param [Array<Complex>] samples Array of complex samples
|
177
|
+
# @param [Integer] sample_rate Sample rate in Hz
|
178
|
+
# @return [Float] Estimated frequency in Hz
|
179
|
+
# @example Estimate carrier frequency
|
180
|
+
# freq_hz = RTLSDR::DSP.estimate_frequency(samples, 2_048_000)
|
181
|
+
# puts "Estimated frequency: #{freq_hz} Hz"
|
101
182
|
def self.estimate_frequency(samples, sample_rate)
|
102
183
|
return 0.0 if samples.length < 2
|
103
184
|
|
data/lib/rtlsdr/errors.rb
CHANGED
@@ -1,12 +1,72 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module RTLSDR
|
4
|
+
# Base error class for all RTL-SDR related exceptions
|
5
|
+
#
|
6
|
+
# This is the parent class for all RTL-SDR specific errors. It extends
|
7
|
+
# Ruby's StandardError and provides a consistent error hierarchy for
|
8
|
+
# the gem. All other RTL-SDR errors inherit from this class.
|
9
|
+
#
|
10
|
+
# @since 0.1.0
|
4
11
|
class Error < StandardError; end
|
12
|
+
|
13
|
+
# Raised when a requested device cannot be found
|
14
|
+
#
|
15
|
+
# This exception is thrown when attempting to open or access a device
|
16
|
+
# by index that doesn't exist, or when a device with a specific serial
|
17
|
+
# number cannot be located.
|
18
|
+
#
|
19
|
+
# @since 0.1.0
|
5
20
|
class DeviceNotFoundError < Error; end
|
21
|
+
|
22
|
+
# Raised when a device cannot be opened
|
23
|
+
#
|
24
|
+
# This exception occurs when a device exists but cannot be opened,
|
25
|
+
# typically because it's already in use by another process or the
|
26
|
+
# user lacks sufficient permissions.
|
27
|
+
#
|
28
|
+
# @since 0.1.0
|
6
29
|
class DeviceOpenError < Error; end
|
30
|
+
|
31
|
+
# Raised when attempting to use a device that hasn't been opened
|
32
|
+
#
|
33
|
+
# This exception is thrown when trying to perform operations on a
|
34
|
+
# device that has been closed or was never properly opened.
|
35
|
+
#
|
36
|
+
# @since 0.1.0
|
7
37
|
class DeviceNotOpenError < Error; end
|
38
|
+
|
39
|
+
# Raised when invalid arguments are passed to device functions
|
40
|
+
#
|
41
|
+
# This exception occurs when function parameters are out of range,
|
42
|
+
# of the wrong type, or otherwise invalid for the requested operation.
|
43
|
+
#
|
44
|
+
# @since 0.1.0
|
8
45
|
class InvalidArgumentError < Error; end
|
46
|
+
|
47
|
+
# Raised when a device operation fails
|
48
|
+
#
|
49
|
+
# This is a general exception for operations that fail at the hardware
|
50
|
+
# or driver level, such as setting frequencies, gains, or sample rates
|
51
|
+
# that the device cannot support.
|
52
|
+
#
|
53
|
+
# @since 0.1.0
|
9
54
|
class OperationFailedError < Error; end
|
55
|
+
|
56
|
+
# Raised when EEPROM operations fail
|
57
|
+
#
|
58
|
+
# This exception occurs when reading from or writing to the device's
|
59
|
+
# EEPROM memory fails, either due to hardware issues or invalid
|
60
|
+
# memory addresses.
|
61
|
+
#
|
62
|
+
# @since 0.1.0
|
10
63
|
class EEPROMError < Error; end
|
64
|
+
|
65
|
+
# Raised when asynchronous callback operations fail
|
66
|
+
#
|
67
|
+
# This exception is thrown when errors occur within the async reading
|
68
|
+
# callback functions, typically during real-time sample processing.
|
69
|
+
#
|
70
|
+
# @since 0.1.0
|
11
71
|
class CallbackError < Error; end
|
12
72
|
end
|