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.
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/rtlsdr"
5
+
6
+ # Simple spectrum analyzer example
7
+ class SpectrumAnalyzer
8
+ def initialize(device_index = 0)
9
+ @device = RTLSDR.open(device_index)
10
+ puts "Opened device: #{@device.inspect}"
11
+
12
+ # Configure device
13
+ @device.configure(
14
+ frequency: 100_000_000, # 100 MHz
15
+ sample_rate: 2_048_000, # 2.048 MHz
16
+ gain: 400, # 40.0 dB
17
+ agc_mode: false
18
+ )
19
+
20
+ puts "Device info:"
21
+ puts " Tuner: #{@device.tuner_name}"
22
+ puts " Center frequency: #{@device.center_freq / 1e6} MHz"
23
+ puts " Sample rate: #{@device.sample_rate / 1e6} MHz"
24
+ puts " Gain: #{@device.tuner_gain / 10.0} dB"
25
+ puts " Available gains: #{@device.tuner_gains.map { |g| g / 10.0 }} dB"
26
+ end
27
+
28
+ def analyze_current_frequency(samples_count = 8192)
29
+ puts "\nAnalyzing #{samples_count} samples at #{@device.center_freq / 1e6} MHz..."
30
+
31
+ samples = @device.read_samples(samples_count)
32
+
33
+ avg_power = RTLSDR::DSP.average_power(samples)
34
+ power_db = 10 * Math.log10(avg_power + 1e-10)
35
+
36
+ magnitude = RTLSDR::DSP.magnitude(samples)
37
+ RTLSDR::DSP.phase(samples)
38
+
39
+ puts " Average power: #{power_db.round(2)} dB"
40
+ puts " Peak magnitude: #{magnitude.max.round(4)}"
41
+ puts " Sample count: #{samples.length}"
42
+
43
+ # Estimate frequency content
44
+ freq_est = RTLSDR::DSP.estimate_frequency(samples, @device.sample_rate)
45
+ puts " Estimated frequency offset: #{freq_est.round(2)} Hz"
46
+
47
+ samples
48
+ end
49
+
50
+ def frequency_sweep(start_freq, end_freq, step = 1_000_000)
51
+ scanner = RTLSDR::Scanner.new(@device,
52
+ start_freq: start_freq,
53
+ end_freq: end_freq,
54
+ step_size: step,
55
+ dwell_time: 0.05)
56
+
57
+ puts "\nPerforming frequency sweep from #{start_freq / 1e6} MHz to #{end_freq / 1e6} MHz"
58
+ puts "Step size: #{step / 1e6} MHz"
59
+ puts "Frequencies to scan: #{scanner.frequency_count}"
60
+
61
+ results = scanner.power_sweep(samples_per_freq: 2048)
62
+
63
+ puts "\nSweep results (top 10 strongest signals):"
64
+ results.sort_by { |_freq, power| -power }.first(10).each do |freq, power_db|
65
+ puts " #{(freq / 1e6).round(3)} MHz: #{power_db.round(2)} dB"
66
+ end
67
+
68
+ results
69
+ end
70
+
71
+ def find_active_frequencies(start_freq, end_freq, threshold = -50)
72
+ scanner = RTLSDR::Scanner.new(@device,
73
+ start_freq: start_freq,
74
+ end_freq: end_freq,
75
+ step_size: 500_000)
76
+
77
+ puts "\nScanning for active frequencies above #{threshold} dB..."
78
+ peaks = scanner.find_peaks(threshold: threshold, samples_per_freq: 4096)
79
+
80
+ if peaks.empty?
81
+ puts "No signals found above #{threshold} dB threshold"
82
+ else
83
+ puts "Found #{peaks.length} active frequencies:"
84
+ peaks.each do |peak|
85
+ puts " #{(peak[:frequency] / 1e6).round(3)} MHz: #{peak[:power_db].round(2)} dB"
86
+ end
87
+ end
88
+
89
+ peaks
90
+ end
91
+
92
+ def monitor_frequency(frequency = 100 * 1e6, duration = 1)
93
+ puts "\nMonitoring #{frequency / 1e6} MHz for #{duration} seconds..."
94
+
95
+ start_time = Time.now
96
+ sample_count = 0
97
+ monitoring = true
98
+
99
+ # Start async reading in a separate thread
100
+ monitoring_thread = @device.read_samples_async(buffer_count: 8, buffer_length: 32_768) do |samples|
101
+ next unless monitoring
102
+
103
+ sample_count += samples.length
104
+ elapsed = Time.now - start_time
105
+
106
+ if elapsed >= duration
107
+ monitoring = false
108
+ next
109
+ end
110
+
111
+ avg_power = RTLSDR::DSP.average_power(samples)
112
+ power_db = 10 * Math.log10(avg_power + 1e-10)
113
+
114
+ print "\rTime: #{elapsed.round(1)}s, Samples: #{sample_count}, Power: #{power_db.round(2)} dB"
115
+ $stdout.flush
116
+ end
117
+
118
+ # Wait for completion or timeout
119
+ start_wait = Time.now
120
+ sleep(0.1) while monitoring && (Time.now - start_wait) < (duration + 1.0)
121
+
122
+ # Cancel if still running
123
+ if @device.streaming?
124
+ @device.cancel_async
125
+ monitoring_thread&.join(1.0) # Wait up to 1 second for clean shutdown
126
+ end
127
+
128
+ puts "\nMonitoring complete. Total samples: #{sample_count}"
129
+ end
130
+
131
+ def close
132
+ @device&.close
133
+ end
134
+ end
135
+
136
+ # Main execution
137
+ if __FILE__ == $PROGRAM_NAME
138
+ begin
139
+ puts "RTL-SDR Spectrum Analyzer Example"
140
+ puts "================================="
141
+
142
+ # Check for devices
143
+ puts "Available RTL-SDR devices:"
144
+ RTLSDR.devices.each_with_index do |device, i|
145
+ puts " #{i}: #{device[:name]} (#{device[:usb_strings][:manufacturer]} #{device[:usb_strings][:product]})"
146
+ end
147
+
148
+ if RTLSDR.device_count.zero?
149
+ puts "No RTL-SDR devices found!"
150
+ exit 1
151
+ end
152
+
153
+ analyzer = SpectrumAnalyzer.new(0)
154
+
155
+ analyzer.analyze_current_frequency(4096)
156
+
157
+ analyzer.frequency_sweep(88_000_000, 108_000_000, 200_000)
158
+
159
+ frequencies = analyzer.find_active_frequencies(88_000_000, 108_000_000, -60)
160
+
161
+ frequencies.each do |freq|
162
+ analyzer.monitor_frequency(freq[:frequency], 1)
163
+ end
164
+
165
+ analyzer.monitor_frequency(5)
166
+ rescue RTLSDR::Error => e
167
+ puts "RTL-SDR Error: #{e.message}"
168
+ exit 1
169
+ rescue Interrupt
170
+ puts "\nInterrupted by user"
171
+ ensure
172
+ analyzer&.close
173
+ puts "Device closed."
174
+ end
175
+ end
data/exe/rtlsdr ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rtlsdr"
@@ -0,0 +1,482 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ module RTLSDR
6
+ # High-level interface to RTL-SDR devices
7
+ #
8
+ # The Device class provides a Ruby-idiomatic interface to RTL-SDR dongles,
9
+ # wrapping the low-level librtlsdr C API with convenient methods and automatic
10
+ # resource management. It supports both synchronous and asynchronous sample
11
+ # reading, comprehensive device configuration, and implements Enumerable for
12
+ # streaming operations.
13
+ #
14
+ # Features:
15
+ # * Automatic device lifecycle management (open/close)
16
+ # * Frequency, gain, and sample rate control with validation
17
+ # * Multiple gain modes (manual, automatic, AGC)
18
+ # * Synchronous and asynchronous sample reading
19
+ # * IQ sample conversion to Ruby Complex numbers
20
+ # * EEPROM access and bias tee control
21
+ # * Enumerable interface for continuous streaming
22
+ # * Comprehensive error handling with custom exceptions
23
+ #
24
+ # @example Basic device setup
25
+ # device = RTLSDR::Device.new(0)
26
+ # device.configure(
27
+ # frequency: 100_000_000, # 100 MHz
28
+ # sample_rate: 2_048_000, # 2.048 MSPS
29
+ # gain: 496 # 49.6 dB
30
+ # )
31
+ #
32
+ # @example Streaming samples
33
+ # device.each(samples_per_read: 1024) do |samples|
34
+ # power = RTLSDR::DSP.average_power(samples)
35
+ # puts "Average power: #{power}"
36
+ # end
37
+ #
38
+ # @example Asynchronous reading
39
+ # device.read_samples_async do |samples|
40
+ # # Process samples in real-time
41
+ # spectrum = RTLSDR::DSP.power_spectrum(samples)
42
+ # end
43
+ class Device
44
+ include Enumerable
45
+
46
+ attr_reader :index, :handle
47
+
48
+ def initialize(index = 0)
49
+ @index = index
50
+ @handle = nil
51
+ @streaming = false
52
+ @async_thread = nil
53
+ @buffer_reset_done = false
54
+ open_device
55
+ end
56
+
57
+ # Device lifecycle
58
+ def open?
59
+ !@handle.nil?
60
+ end
61
+
62
+ def close
63
+ return unless open?
64
+
65
+ cancel_async if streaming?
66
+ result = FFI.rtlsdr_close(@handle)
67
+ @handle = nil
68
+ check_result(result, "Failed to close device")
69
+ end
70
+
71
+ def closed?
72
+ !open?
73
+ end
74
+
75
+ # Device information
76
+ def name
77
+ RTLSDR.device_name(@index)
78
+ end
79
+
80
+ def usb_strings
81
+ return @usb_strings if @usb_strings
82
+
83
+ manufact = " " * 256
84
+ product = " " * 256
85
+ serial = " " * 256
86
+
87
+ result = FFI.rtlsdr_get_usb_strings(@handle, manufact, product, serial)
88
+ check_result(result, "Failed to get USB strings")
89
+
90
+ @usb_strings = {
91
+ manufacturer: manufact.strip,
92
+ product: product.strip,
93
+ serial: serial.strip
94
+ }
95
+ end
96
+
97
+ def tuner_type
98
+ @tuner_type ||= FFI.rtlsdr_get_tuner_type(@handle)
99
+ end
100
+
101
+ def tuner_name
102
+ FFI.tuner_type_name(tuner_type)
103
+ end
104
+
105
+ # Frequency control
106
+ def center_freq=(freq)
107
+ result = FFI.rtlsdr_set_center_freq(@handle, freq)
108
+ check_result(result, "Failed to set center frequency")
109
+ end
110
+
111
+ def center_freq
112
+ FFI.rtlsdr_get_center_freq(@handle)
113
+ end
114
+ alias frequency center_freq
115
+ alias frequency= center_freq=
116
+
117
+ def freq_correction=(ppm)
118
+ result = FFI.rtlsdr_set_freq_correction(@handle, ppm)
119
+ check_result(result, "Failed to set frequency correction")
120
+ end
121
+
122
+ def freq_correction
123
+ FFI.rtlsdr_get_freq_correction(@handle)
124
+ end
125
+
126
+ # Crystal oscillator frequencies
127
+ def set_xtal_freq(rtl_freq, tuner_freq)
128
+ result = FFI.rtlsdr_set_xtal_freq(@handle, rtl_freq, tuner_freq)
129
+ check_result(result, "Failed to set crystal frequencies")
130
+ [rtl_freq, tuner_freq]
131
+ end
132
+
133
+ def xtal_freq
134
+ rtl_freq_ptr = ::FFI::MemoryPointer.new(:uint32)
135
+ tuner_freq_ptr = ::FFI::MemoryPointer.new(:uint32)
136
+
137
+ result = FFI.rtlsdr_get_xtal_freq(@handle, rtl_freq_ptr, tuner_freq_ptr)
138
+ check_result(result, "Failed to get crystal frequencies")
139
+
140
+ [rtl_freq_ptr.read_uint32, tuner_freq_ptr.read_uint32]
141
+ end
142
+
143
+ # Gain control
144
+ def tuner_gains
145
+ # First call to get count
146
+ count = FFI.rtlsdr_get_tuner_gains(@handle, nil)
147
+ return [] if count <= 0
148
+
149
+ # Second call to get actual gains
150
+ gains_ptr = ::FFI::MemoryPointer.new(:int, count)
151
+ result = FFI.rtlsdr_get_tuner_gains(@handle, gains_ptr)
152
+ return [] if result <= 0
153
+
154
+ gains_ptr.read_array_of_int(result)
155
+ end
156
+
157
+ def tuner_gain=(gain)
158
+ result = FFI.rtlsdr_set_tuner_gain(@handle, gain)
159
+ check_result(result, "Failed to set tuner gain")
160
+ end
161
+
162
+ def tuner_gain
163
+ FFI.rtlsdr_get_tuner_gain(@handle)
164
+ end
165
+ alias gain tuner_gain
166
+ alias gain= tuner_gain=
167
+
168
+ def tuner_gain_mode=(manual)
169
+ mode = manual ? 1 : 0
170
+ result = FFI.rtlsdr_set_tuner_gain_mode(@handle, mode)
171
+ check_result(result, "Failed to set gain mode")
172
+ end
173
+
174
+ def manual_gain_mode!
175
+ self.tuner_gain_mode = true
176
+ end
177
+
178
+ def auto_gain_mode!
179
+ self.tuner_gain_mode = false
180
+ end
181
+
182
+ def set_tuner_if_gain(stage, gain)
183
+ result = FFI.rtlsdr_set_tuner_if_gain(@handle, stage, gain)
184
+ check_result(result, "Failed to set IF gain")
185
+ gain
186
+ end
187
+
188
+ def tuner_bandwidth=(bw)
189
+ result = FFI.rtlsdr_set_tuner_bandwidth(@handle, bw)
190
+ check_result(result, "Failed to set bandwidth")
191
+ end
192
+
193
+ # Sample rate control
194
+ def sample_rate=(rate)
195
+ result = FFI.rtlsdr_set_sample_rate(@handle, rate)
196
+ check_result(result, "Failed to set sample rate")
197
+ end
198
+
199
+ def sample_rate
200
+ FFI.rtlsdr_get_sample_rate(@handle)
201
+ end
202
+ alias samp_rate sample_rate
203
+ alias samp_rate= sample_rate=
204
+
205
+ # Mode control
206
+ def test_mode=(enabled)
207
+ mode = enabled ? 1 : 0
208
+ result = FFI.rtlsdr_set_testmode(@handle, mode)
209
+ check_result(result, "Failed to set test mode")
210
+ end
211
+
212
+ def test_mode!
213
+ self.test_mode = true
214
+ end
215
+
216
+ def agc_mode=(enabled)
217
+ mode = enabled ? 1 : 0
218
+ result = FFI.rtlsdr_set_agc_mode(@handle, mode)
219
+ check_result(result, "Failed to set AGC mode")
220
+ end
221
+
222
+ def agc_mode!
223
+ self.agc_mode = true
224
+ end
225
+
226
+ def direct_sampling=(mode)
227
+ result = FFI.rtlsdr_set_direct_sampling(@handle, mode)
228
+ check_result(result, "Failed to set direct sampling")
229
+ end
230
+
231
+ def direct_sampling
232
+ result = FFI.rtlsdr_get_direct_sampling(@handle)
233
+ return nil if result.negative?
234
+
235
+ result
236
+ end
237
+
238
+ def offset_tuning=(enabled)
239
+ mode = enabled ? 1 : 0
240
+ result = FFI.rtlsdr_set_offset_tuning(@handle, mode)
241
+ check_result(result, "Failed to set offset tuning")
242
+ end
243
+
244
+ def offset_tuning
245
+ result = FFI.rtlsdr_get_offset_tuning(@handle)
246
+ return nil if result.negative?
247
+
248
+ result == 1
249
+ end
250
+
251
+ def offset_tuning!
252
+ self.offset_tuning = true
253
+ end
254
+
255
+ # Bias tee control
256
+ def bias_tee=(enabled)
257
+ mode = enabled ? 1 : 0
258
+ result = FFI.rtlsdr_set_bias_tee(@handle, mode)
259
+ check_result(result, "Failed to set bias tee")
260
+ end
261
+
262
+ def bias_tee!
263
+ self.bias_tee = true
264
+ end
265
+
266
+ def set_bias_tee_gpio(gpio, enabled)
267
+ mode = enabled ? 1 : 0
268
+ result = FFI.rtlsdr_set_bias_tee_gpio(@handle, gpio, mode)
269
+ check_result(result, "Failed to set bias tee GPIO")
270
+ enabled
271
+ end
272
+
273
+ # EEPROM access
274
+ def read_eeprom(offset, length)
275
+ data_ptr = ::FFI::MemoryPointer.new(:uint8, length)
276
+ result = FFI.rtlsdr_read_eeprom(@handle, data_ptr, offset, length)
277
+ check_result(result, "Failed to read EEPROM")
278
+ data_ptr.read_array_of_uint8(length)
279
+ end
280
+
281
+ def write_eeprom(data, offset)
282
+ data_ptr = ::FFI::MemoryPointer.new(:uint8, data.length)
283
+ data_ptr.write_array_of_uint8(data)
284
+ result = FFI.rtlsdr_write_eeprom(@handle, data_ptr, offset, data.length)
285
+ check_result(result, "Failed to write EEPROM")
286
+ data.length
287
+ end
288
+
289
+ # Streaming control
290
+ def reset_buffer
291
+ result = FFI.rtlsdr_reset_buffer(@handle)
292
+ check_result(result, "Failed to reset buffer")
293
+ end
294
+
295
+ def read_sync(length)
296
+ # Reset buffer before first read to avoid stale data
297
+ reset_buffer unless @buffer_reset_done
298
+ @buffer_reset_done = true
299
+
300
+ buffer = ::FFI::MemoryPointer.new(:uint8, length)
301
+ n_read_ptr = ::FFI::MemoryPointer.new(:int)
302
+
303
+ result = FFI.rtlsdr_read_sync(@handle, buffer, length, n_read_ptr)
304
+ check_result(result, "Failed to read synchronously")
305
+
306
+ n_read = n_read_ptr.read_int
307
+ buffer.read_array_of_uint8(n_read)
308
+ end
309
+
310
+ def read_samples(count = 1024)
311
+ # RTL-SDR outputs 8-bit I/Q samples, so we need 2 bytes per complex sample
312
+ data = read_sync(count * 2)
313
+
314
+ # Convert to complex numbers (I + jQ)
315
+ samples = []
316
+ (0...data.length).step(2) do |i|
317
+ i_sample = (data[i] - 128) / 128.0 # Convert to -1.0 to 1.0 range
318
+ q_sample = (data[i + 1] - 128) / 128.0 # Convert to -1.0 to 1.0 range
319
+ samples << Complex(i_sample, q_sample)
320
+ end
321
+
322
+ samples
323
+ end
324
+
325
+ def streaming?
326
+ @streaming
327
+ end
328
+
329
+ def read_async(buffer_count: 15, buffer_length: 262_144, &block)
330
+ raise ArgumentError, "Block required for async reading" unless block_given?
331
+ raise OperationFailedError, "Already streaming" if streaming?
332
+
333
+ @streaming = true
334
+ @async_callback = proc do |buf_ptr, len, _ctx|
335
+ data = buf_ptr.read_array_of_uint8(len)
336
+ block.call(data)
337
+ rescue StandardError => e
338
+ puts "Error in async callback: #{e.message}"
339
+ cancel_async
340
+ end
341
+
342
+ @async_thread = Thread.new do
343
+ result = FFI.rtlsdr_read_async(@handle, @async_callback, nil, buffer_count, buffer_length)
344
+ @streaming = false
345
+ # Don't raise error for cancellation (-1) or timeout (-5)
346
+ check_result(result, "Async read failed") unless [-1, -5].include?(result)
347
+ end
348
+
349
+ @async_thread
350
+ end
351
+
352
+ def read_samples_async(buffer_count: 15, buffer_length: 262_144, &block)
353
+ raise ArgumentError, "Block required for async reading" unless block_given?
354
+
355
+ read_async(buffer_count: buffer_count, buffer_length: buffer_length) do |data|
356
+ # Convert to complex samples
357
+ samples = []
358
+ (0...data.length).step(2) do |i|
359
+ i_sample = (data[i] - 128) / 128.0
360
+ q_sample = (data[i + 1] - 128) / 128.0
361
+ samples << Complex(i_sample, q_sample)
362
+ end
363
+
364
+ block.call(samples)
365
+ end
366
+ end
367
+
368
+ def cancel_async
369
+ return unless streaming?
370
+
371
+ result = FFI.rtlsdr_cancel_async(@handle)
372
+ @streaming = false
373
+
374
+ # Only join if we're not calling from within the async thread itself
375
+ if @async_thread && @async_thread != Thread.current
376
+ @async_thread.join(1) # Wait up to 1 second for thread to finish
377
+ end
378
+
379
+ @async_thread = nil
380
+ @async_callback = nil
381
+
382
+ check_result(result, "Failed to cancel async operation")
383
+ end
384
+
385
+ # Enumerable interface for reading samples
386
+ def each(samples_per_read: 1024)
387
+ return enum_for(:each, samples_per_read: samples_per_read) unless block_given?
388
+
389
+ loop do
390
+ samples = read_samples(samples_per_read)
391
+ yield samples
392
+ rescue StandardError => e
393
+ break if e.is_a?(Interrupt)
394
+
395
+ raise
396
+ end
397
+ end
398
+
399
+ # Configuration shortcuts
400
+ def configure(frequency: nil, sample_rate: nil, gain: nil, **options)
401
+ self.center_freq = frequency if frequency
402
+ self.sample_rate = sample_rate if sample_rate
403
+
404
+ if gain
405
+ manual_gain_mode!
406
+ self.tuner_gain = gain
407
+ end
408
+
409
+ options.each do |key, value|
410
+ case key
411
+ when :freq_correction then self.freq_correction = value
412
+ when :bandwidth then self.tuner_bandwidth = value
413
+ when :agc_mode then self.agc_mode = value
414
+ when :test_mode then self.test_mode = value
415
+ when :bias_tee then self.bias_tee = value
416
+ when :direct_sampling then self.direct_sampling = value
417
+ when :offset_tuning then self.offset_tuning = value
418
+ end
419
+ end
420
+
421
+ self
422
+ end
423
+
424
+ # Device info as hash
425
+ def info
426
+ {
427
+ index: @index,
428
+ name: name,
429
+ usb_strings: usb_strings,
430
+ tuner_type: tuner_type,
431
+ tuner_name: tuner_name,
432
+ center_freq: center_freq,
433
+ sample_rate: sample_rate,
434
+ tuner_gain: tuner_gain,
435
+ tuner_gains: tuner_gains,
436
+ freq_correction: freq_correction,
437
+ direct_sampling: direct_sampling,
438
+ offset_tuning: offset_tuning
439
+ }
440
+ end
441
+
442
+ def inspect
443
+ if open?
444
+ "#<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
445
+ else
446
+ "#<RTLSDR::Device:#{object_id.to_s(16)} index=#{@index} closed>"
447
+ end
448
+ end
449
+
450
+ private
451
+
452
+ def open_device
453
+ dev_ptr = ::FFI::MemoryPointer.new(:pointer)
454
+ result = FFI.rtlsdr_open(dev_ptr, @index)
455
+
456
+ case result
457
+ when 0
458
+ @handle = dev_ptr.read_pointer
459
+ when -1
460
+ raise DeviceNotFoundError, "Device #{@index} not found"
461
+ when -2
462
+ raise DeviceOpenError, "Device #{@index} already in use"
463
+ when -3
464
+ raise DeviceOpenError, "Device #{@index} cannot be opened"
465
+ else
466
+ raise DeviceOpenError, "Failed to open device #{@index}: error #{result}"
467
+ end
468
+ end
469
+
470
+ def check_result(result, message)
471
+ return if result.zero?
472
+
473
+ error_msg = "#{message}: error #{result}"
474
+ case result
475
+ when -1 then raise DeviceNotOpenError, error_msg
476
+ when -2 then raise InvalidArgumentError, error_msg
477
+ when -3 then raise EEPROMError, error_msg
478
+ else raise OperationFailedError, error_msg
479
+ end
480
+ end
481
+ end
482
+ end