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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +169 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +514 -0
  5. data/exe/rble +14 -0
  6. data/ext/macos_ble/Package.swift +20 -0
  7. data/ext/macos_ble/Sources/RBLEHelper/BLEManager.swift +783 -0
  8. data/ext/macos_ble/Sources/RBLEHelper/Protocol.swift +173 -0
  9. data/ext/macos_ble/Sources/RBLEHelper/main.swift +645 -0
  10. data/ext/macos_ble/extconf.rb +73 -0
  11. data/lib/rble/backend/base.rb +181 -0
  12. data/lib/rble/backend/bluez.rb +1279 -0
  13. data/lib/rble/backend/corebluetooth.rb +653 -0
  14. data/lib/rble/backend.rb +193 -0
  15. data/lib/rble/bluez/adapter.rb +169 -0
  16. data/lib/rble/bluez/async_call.rb +85 -0
  17. data/lib/rble/bluez/async_connection_operations.rb +492 -0
  18. data/lib/rble/bluez/async_gatt_operations.rb +249 -0
  19. data/lib/rble/bluez/async_introspection.rb +151 -0
  20. data/lib/rble/bluez/dbus_connection.rb +64 -0
  21. data/lib/rble/bluez/dbus_session.rb +344 -0
  22. data/lib/rble/bluez/device.rb +86 -0
  23. data/lib/rble/bluez/event_loop.rb +153 -0
  24. data/lib/rble/bluez/gatt_operation_queue.rb +129 -0
  25. data/lib/rble/bluez/pairing_agent.rb +132 -0
  26. data/lib/rble/bluez/pairing_session.rb +212 -0
  27. data/lib/rble/bluez/retry_policy.rb +55 -0
  28. data/lib/rble/bluez.rb +33 -0
  29. data/lib/rble/characteristic.rb +237 -0
  30. data/lib/rble/cli/adapter.rb +88 -0
  31. data/lib/rble/cli/characteristic_helpers.rb +154 -0
  32. data/lib/rble/cli/doctor.rb +309 -0
  33. data/lib/rble/cli/formatters/json.rb +122 -0
  34. data/lib/rble/cli/formatters/text.rb +157 -0
  35. data/lib/rble/cli/hex_dump.rb +48 -0
  36. data/lib/rble/cli/monitor.rb +129 -0
  37. data/lib/rble/cli/pair.rb +103 -0
  38. data/lib/rble/cli/paired.rb +22 -0
  39. data/lib/rble/cli/read.rb +55 -0
  40. data/lib/rble/cli/scan.rb +88 -0
  41. data/lib/rble/cli/show.rb +109 -0
  42. data/lib/rble/cli/status.rb +25 -0
  43. data/lib/rble/cli/unpair.rb +39 -0
  44. data/lib/rble/cli/value_parser.rb +211 -0
  45. data/lib/rble/cli/write.rb +196 -0
  46. data/lib/rble/cli.rb +152 -0
  47. data/lib/rble/company_ids.rb +90 -0
  48. data/lib/rble/connection.rb +539 -0
  49. data/lib/rble/device.rb +54 -0
  50. data/lib/rble/errors.rb +317 -0
  51. data/lib/rble/gatt/uuid_database.rb +395 -0
  52. data/lib/rble/scanner.rb +219 -0
  53. data/lib/rble/service.rb +41 -0
  54. data/lib/rble/tasks/check.rake +154 -0
  55. data/lib/rble/tasks/integration.rake +242 -0
  56. data/lib/rble/tasks.rb +8 -0
  57. data/lib/rble/version.rb +5 -0
  58. data/lib/rble.rb +62 -0
  59. metadata +120 -0
@@ -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