rble 0.7.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/CHANGELOG.md +169 -0
- data/LICENSE.txt +21 -0
- data/README.md +514 -0
- data/exe/rble +14 -0
- data/ext/macos_ble/Package.swift +20 -0
- data/ext/macos_ble/Sources/RBLEHelper/BLEManager.swift +783 -0
- data/ext/macos_ble/Sources/RBLEHelper/Protocol.swift +173 -0
- data/ext/macos_ble/Sources/RBLEHelper/main.swift +645 -0
- data/ext/macos_ble/extconf.rb +73 -0
- data/lib/rble/backend/base.rb +181 -0
- data/lib/rble/backend/bluez.rb +1279 -0
- data/lib/rble/backend/corebluetooth.rb +653 -0
- data/lib/rble/backend.rb +193 -0
- data/lib/rble/bluez/adapter.rb +169 -0
- data/lib/rble/bluez/async_call.rb +85 -0
- data/lib/rble/bluez/async_connection_operations.rb +492 -0
- data/lib/rble/bluez/async_gatt_operations.rb +249 -0
- data/lib/rble/bluez/async_introspection.rb +151 -0
- data/lib/rble/bluez/dbus_connection.rb +64 -0
- data/lib/rble/bluez/dbus_session.rb +344 -0
- data/lib/rble/bluez/device.rb +86 -0
- data/lib/rble/bluez/event_loop.rb +153 -0
- data/lib/rble/bluez/gatt_operation_queue.rb +129 -0
- data/lib/rble/bluez/pairing_agent.rb +132 -0
- data/lib/rble/bluez/pairing_session.rb +212 -0
- data/lib/rble/bluez/retry_policy.rb +55 -0
- data/lib/rble/bluez.rb +33 -0
- data/lib/rble/characteristic.rb +237 -0
- data/lib/rble/cli/adapter.rb +88 -0
- data/lib/rble/cli/characteristic_helpers.rb +154 -0
- data/lib/rble/cli/doctor.rb +309 -0
- data/lib/rble/cli/formatters/json.rb +122 -0
- data/lib/rble/cli/formatters/text.rb +157 -0
- data/lib/rble/cli/hex_dump.rb +48 -0
- data/lib/rble/cli/monitor.rb +129 -0
- data/lib/rble/cli/pair.rb +103 -0
- data/lib/rble/cli/paired.rb +22 -0
- data/lib/rble/cli/read.rb +55 -0
- data/lib/rble/cli/scan.rb +88 -0
- data/lib/rble/cli/show.rb +109 -0
- data/lib/rble/cli/status.rb +25 -0
- data/lib/rble/cli/unpair.rb +39 -0
- data/lib/rble/cli/value_parser.rb +211 -0
- data/lib/rble/cli/write.rb +196 -0
- data/lib/rble/cli.rb +152 -0
- data/lib/rble/company_ids.rb +90 -0
- data/lib/rble/connection.rb +539 -0
- data/lib/rble/device.rb +54 -0
- data/lib/rble/errors.rb +317 -0
- data/lib/rble/gatt/uuid_database.rb +395 -0
- data/lib/rble/scanner.rb +219 -0
- data/lib/rble/service.rb +41 -0
- data/lib/rble/tasks/check.rake +154 -0
- data/lib/rble/tasks/integration.rake +242 -0
- data/lib/rble/tasks.rb +8 -0
- data/lib/rble/version.rb +5 -0
- data/lib/rble.rb +62 -0
- metadata +120 -0
data/lib/rble/errors.rb
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RBLE
|
|
4
|
+
# Base error class for all RBLE errors
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when no Bluetooth adapter is found
|
|
8
|
+
class AdapterNotFoundError < Error
|
|
9
|
+
def initialize(msg = 'No Bluetooth adapter found. Ensure Bluetooth hardware is present and enabled.')
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Raised when adapter exists but is powered off or disabled
|
|
15
|
+
class AdapterDisabledError < Error
|
|
16
|
+
def initialize(adapter = nil)
|
|
17
|
+
msg = adapter ? "Bluetooth adapter '#{adapter}' is disabled." : 'Bluetooth adapter is disabled.'
|
|
18
|
+
msg += " Run 'bluetoothctl power on' to enable."
|
|
19
|
+
super(msg)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Raised when operation fails due to insufficient permissions
|
|
24
|
+
class PermissionError < Error
|
|
25
|
+
def initialize(operation = 'access Bluetooth')
|
|
26
|
+
super("Permission denied to #{operation}. " \
|
|
27
|
+
"On Linux: ensure user is in 'bluetooth' group or has appropriate polkit permissions. " \
|
|
28
|
+
"On macOS: grant Bluetooth access in System Preferences > Privacy & Security.")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Raised when subprocess communication fails (macOS backend)
|
|
33
|
+
class SubprocessError < Error
|
|
34
|
+
def initialize(msg = 'Subprocess communication failed. The helper process may have crashed.')
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Raised when Bluetooth is not powered on
|
|
40
|
+
class BluetoothOffError < Error
|
|
41
|
+
def initialize
|
|
42
|
+
super('Bluetooth is not powered on. Enable Bluetooth in system settings.')
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Raised when scan operation fails
|
|
47
|
+
class ScanError < Error; end
|
|
48
|
+
|
|
49
|
+
# Raised when a scan is already in progress
|
|
50
|
+
class ScanInProgressError < ScanError
|
|
51
|
+
def initialize
|
|
52
|
+
super('A scan is already in progress. Stop the current scan before starting a new one.')
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Base class for connection-related errors
|
|
57
|
+
class ConnectionError < Error; end
|
|
58
|
+
|
|
59
|
+
# Raised when connection attempt times out
|
|
60
|
+
class ConnectionTimeoutError < ConnectionError
|
|
61
|
+
def initialize(timeout = 30)
|
|
62
|
+
super("Connection timed out after #{timeout} seconds. " \
|
|
63
|
+
'Ensure device is in range and advertising.')
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Raised when device is no longer available (disappeared between scan and connect)
|
|
68
|
+
class DeviceNotFoundError < ConnectionError
|
|
69
|
+
attr_reader :address, :device_name
|
|
70
|
+
|
|
71
|
+
def initialize(address, device_name: nil)
|
|
72
|
+
@address = address
|
|
73
|
+
@device_name = device_name
|
|
74
|
+
|
|
75
|
+
msg = if device_name
|
|
76
|
+
"Device '#{device_name}' (#{address}) was seen during scan but is no longer available"
|
|
77
|
+
else
|
|
78
|
+
"Device #{address} was seen during scan but is no longer available"
|
|
79
|
+
end
|
|
80
|
+
super(msg)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Raised when operation requires an active connection
|
|
85
|
+
class NotConnectedError < ConnectionError
|
|
86
|
+
def initialize(msg = nil)
|
|
87
|
+
super(msg || 'Connection lost. Create a new connection with RBLE.connect()')
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Raised when attempting to connect to an already-connected device
|
|
92
|
+
class AlreadyConnectedError < ConnectionError
|
|
93
|
+
def initialize
|
|
94
|
+
super('Device is already connected. Disconnect first if you need to reconnect.')
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Raised when connection attempt fails (out of range, device unavailable, etc.)
|
|
99
|
+
class ConnectionFailed < ConnectionError
|
|
100
|
+
attr_reader :address, :reason
|
|
101
|
+
|
|
102
|
+
def initialize(address, reason = nil)
|
|
103
|
+
@address = address
|
|
104
|
+
@reason = reason
|
|
105
|
+
msg = "Connection to #{address} failed"
|
|
106
|
+
msg = "#{msg}: #{reason}" if reason
|
|
107
|
+
super(msg)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Raised when device disconnects during an operation
|
|
112
|
+
class DeviceDisconnected < ConnectionError
|
|
113
|
+
attr_reader :address, :operation
|
|
114
|
+
|
|
115
|
+
def initialize(address = nil, operation = nil)
|
|
116
|
+
@address = address
|
|
117
|
+
@operation = operation
|
|
118
|
+
msg = 'Device disconnected'
|
|
119
|
+
msg = "#{msg} (#{address})" if address
|
|
120
|
+
msg = "#{msg} during #{operation}" if operation
|
|
121
|
+
super(msg)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Raised when an async operation is pending and the session is closed
|
|
126
|
+
class SessionClosedError < ConnectionError
|
|
127
|
+
def initialize
|
|
128
|
+
super('Session closed while operation was pending')
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Raised when BlueZ returns org.bluez.Error.InProgress after retries exhausted
|
|
133
|
+
class OperationInProgressError < ConnectionError
|
|
134
|
+
def initialize(operation = nil)
|
|
135
|
+
msg = operation ? "Operation '#{operation}' still in progress after retries." : 'Operation still in progress after retries.'
|
|
136
|
+
super(msg)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def recovery_hint
|
|
140
|
+
'Wait a moment and retry the operation. If persists, disconnect and reconnect.'
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Raised when adapter is not ready (e.g., not powered, not initialized)
|
|
145
|
+
class AdapterNotReadyError < Error
|
|
146
|
+
def initialize(adapter = nil)
|
|
147
|
+
msg = adapter ? "Adapter '#{adapter}' is not ready." : 'Bluetooth adapter is not ready.'
|
|
148
|
+
super(msg)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def recovery_hint
|
|
152
|
+
"Run 'bluetoothctl power on' to enable the adapter, or wait for initialization to complete."
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Raised when authentication/pairing fails during connection
|
|
157
|
+
class AuthenticationError < ConnectionError
|
|
158
|
+
attr_reader :address
|
|
159
|
+
|
|
160
|
+
def initialize(address = nil)
|
|
161
|
+
@address = address
|
|
162
|
+
msg = address ? "Authentication failed for device #{address}." : 'Authentication failed.'
|
|
163
|
+
super(msg)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def recovery_hint
|
|
167
|
+
'Remove the device pairing and re-pair: bluetoothctl remove <address>, then bluetoothctl pair <address>'
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Raised when connection is aborted (often due to RF interference or device busy)
|
|
172
|
+
class ConnectionAbortedError < ConnectionError
|
|
173
|
+
attr_reader :address
|
|
174
|
+
|
|
175
|
+
def initialize(address = nil, reason = nil)
|
|
176
|
+
@address = address
|
|
177
|
+
msg = address ? "Connection to #{address} was aborted" : 'Connection was aborted'
|
|
178
|
+
msg = "#{msg}: #{reason}" if reason
|
|
179
|
+
super(msg)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def recovery_hint
|
|
183
|
+
'This may be caused by RF interference, device busy, or distance. Move closer to the device and retry.'
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Raised when connection is lost unexpectedly during an operation
|
|
188
|
+
class ConnectionLostError < ConnectionError
|
|
189
|
+
attr_reader :address, :operation
|
|
190
|
+
|
|
191
|
+
def initialize(address = nil, operation = nil)
|
|
192
|
+
@address = address
|
|
193
|
+
@operation = operation
|
|
194
|
+
msg = 'Connection lost'
|
|
195
|
+
msg = "#{msg} to #{address}" if address
|
|
196
|
+
msg = "#{msg} during #{operation}" if operation
|
|
197
|
+
super(msg)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def recovery_hint
|
|
201
|
+
'Reconnect with RBLE.connect(). Check device is still in range and powered.'
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Base class for service discovery errors
|
|
206
|
+
class ServiceDiscoveryError < Error; end
|
|
207
|
+
|
|
208
|
+
# Raised when a requested service UUID is not found on the device
|
|
209
|
+
class ServiceNotFoundError < ServiceDiscoveryError
|
|
210
|
+
def initialize(uuid = nil)
|
|
211
|
+
msg = uuid ? "Service with UUID '#{uuid}' not found on device." : 'Service not found on device.'
|
|
212
|
+
msg += ' Ensure the device supports this service and discovery has completed.'
|
|
213
|
+
super(msg)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Raised when a requested characteristic UUID is not found
|
|
218
|
+
class CharacteristicNotFoundError < ServiceDiscoveryError
|
|
219
|
+
def initialize(uuid = nil)
|
|
220
|
+
msg = uuid ? "Characteristic with UUID '#{uuid}' not found." : 'Characteristic not found.'
|
|
221
|
+
msg += ' Ensure the service contains this characteristic.'
|
|
222
|
+
super(msg)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Base class for GATT operation errors
|
|
227
|
+
class GATTError < Error; end
|
|
228
|
+
|
|
229
|
+
# Raised when a read operation fails
|
|
230
|
+
class ReadError < GATTError
|
|
231
|
+
def initialize(msg = 'Failed to read characteristic value.')
|
|
232
|
+
super
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Raised when a write operation fails
|
|
237
|
+
class WriteError < GATTError
|
|
238
|
+
def initialize(msg = 'Failed to write characteristic value.')
|
|
239
|
+
super
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Raised when notification subscription fails
|
|
244
|
+
class NotifyError < GATTError
|
|
245
|
+
def initialize(msg = 'Failed to enable notifications on characteristic.')
|
|
246
|
+
super
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Raised when subscribing to a characteristic that doesn't support notifications
|
|
251
|
+
class NotifyNotSupported < GATTError
|
|
252
|
+
attr_reader :uuid, :flags
|
|
253
|
+
|
|
254
|
+
def initialize(uuid, flags)
|
|
255
|
+
@uuid = uuid
|
|
256
|
+
@flags = flags
|
|
257
|
+
super("Characteristic #{uuid} does not support notifications. " \
|
|
258
|
+
"Flags: [#{flags.join(', ')}]. " \
|
|
259
|
+
"Characteristic must have 'notify' or 'indicate' flag.")
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Raised when operation is not permitted by the characteristic
|
|
264
|
+
class NotPermittedError < GATTError
|
|
265
|
+
def initialize(operation = 'operation')
|
|
266
|
+
super("#{operation.capitalize} not permitted on this characteristic. " \
|
|
267
|
+
'Check characteristic flags for supported operations.')
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Raised when operation requires pairing or authorization
|
|
272
|
+
class NotAuthorizedError < GATTError
|
|
273
|
+
def initialize(operation = 'operation')
|
|
274
|
+
super("#{operation.capitalize} requires authorization. " \
|
|
275
|
+
'Device may need to be paired first.')
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Raised when characteristic does not support the requested operation
|
|
280
|
+
class NotSupportedError < GATTError
|
|
281
|
+
def initialize(operation = 'operation')
|
|
282
|
+
super("#{operation.capitalize} not supported by this characteristic. " \
|
|
283
|
+
'Check the Flags property for supported operations.')
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Raised when a D-Bus operation times out
|
|
288
|
+
class TimeoutError < Error
|
|
289
|
+
attr_reader :operation, :timeout_value
|
|
290
|
+
|
|
291
|
+
def initialize(operation, timeout_value)
|
|
292
|
+
@operation = operation
|
|
293
|
+
@timeout_value = timeout_value
|
|
294
|
+
super("#{operation} timed out after #{timeout_value}s")
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Base class for backend selection errors
|
|
299
|
+
class BackendUnavailableError < Error
|
|
300
|
+
attr_reader :backend
|
|
301
|
+
|
|
302
|
+
def initialize(backend:, reason:, suggestion: nil)
|
|
303
|
+
@backend = backend
|
|
304
|
+
message = "Backend :#{backend} unavailable: #{reason}"
|
|
305
|
+
message = "#{message}\n\n#{suggestion}" if suggestion
|
|
306
|
+
super(message)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Raised when attempting to change backend after BLE operations have started
|
|
311
|
+
class BackendAlreadySelectedError < Error
|
|
312
|
+
def initialize(msg = 'Cannot change backend after BLE operations have started.')
|
|
313
|
+
super
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
end
|