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
data/README.md ADDED
@@ -0,0 +1,514 @@
1
+ # RBLE
2
+
3
+ Reliable BLE communication for Ruby — scanning, connections, and GATT operations on Linux and macOS. Includes a standalone `rble` CLI tool.
4
+
5
+ ## Features
6
+
7
+ - **CLI Tool** - `rble` command for scanning, inspecting, reading, writing, and monitoring BLE devices from the terminal
8
+ - **Device Scanning** - Discover BLE devices with filtering by service UUID, name, RSSI
9
+ - **GATT Connections** - Connect to devices and discover services/characteristics
10
+ - **Read/Write** - Read and write characteristic values with timeout support
11
+ - **Notifications** - Subscribe to characteristic value changes with smart formatting
12
+ - **Cross-Platform** - Works on Linux (BlueZ/D-Bus) and macOS (CoreBluetooth)
13
+ - **Disconnect Detection** - Callbacks for connection state changes and disconnections
14
+ - **GATT UUID Database** - 175 human-readable names for standard BLE services and characteristics
15
+
16
+ ## Requirements
17
+
18
+ - **Ruby** 3.2 or higher
19
+ - **Linux**: BlueZ 5.50+ with D-Bus
20
+ - **macOS**: macOS 12 (Monterey) or higher, Xcode command-line tools
21
+
22
+ ## Installation
23
+
24
+ Add to your Gemfile:
25
+
26
+ ```ruby
27
+ gem 'rble'
28
+ ```
29
+
30
+ Then run:
31
+
32
+ ```bash
33
+ bundle install
34
+ ```
35
+
36
+ ### macOS
37
+
38
+ On macOS, a Swift helper binary is automatically compiled during gem installation. This requires Xcode Command Line Tools:
39
+
40
+ ```bash
41
+ xcode-select --install
42
+ ```
43
+
44
+ If the automatic build fails, you can build manually:
45
+
46
+ ```bash
47
+ cd $(bundle info rble --path)/ext/macos_ble
48
+ swift build -c release
49
+ ```
50
+
51
+ ### Linux
52
+
53
+ On Linux, the BlueZ backend requires the `ruby-dbus` gem. If using Bundler, add it to your Gemfile:
54
+
55
+ ```ruby
56
+ install_if -> { RUBY_PLATFORM.include?('linux') } do
57
+ gem 'ruby-dbus', '~> 0.25'
58
+ end
59
+ ```
60
+
61
+ For standalone CLI installation:
62
+
63
+ ```bash
64
+ gem install ruby-dbus
65
+ ```
66
+
67
+ ### Linux Permissions
68
+
69
+ On Linux, ensure your user has permission to access Bluetooth. Either:
70
+
71
+ 1. Add your user to the `bluetooth` group:
72
+ ```bash
73
+ sudo usermod -aG bluetooth $USER
74
+ # Log out and back in for changes to take effect
75
+ ```
76
+
77
+ 2. Or run with appropriate permissions via polkit.
78
+
79
+ ## Quick Start
80
+
81
+ Verify your setup works by scanning for nearby devices:
82
+
83
+ ```ruby
84
+ require 'rble'
85
+
86
+ RBLE.scan(timeout: 5) do |device|
87
+ puts "Found: #{device.name || 'Unknown'} (#{device.address})"
88
+ end
89
+ ```
90
+
91
+ ## CLI Tool
92
+
93
+ RBLE includes a standalone command-line tool for BLE operations without writing Ruby code:
94
+
95
+ ```bash
96
+ # Discover nearby devices
97
+ rble scan
98
+ rble scan --timeout 30 --name Polar --rssi -70
99
+
100
+ # Check Bluetooth health
101
+ rble status
102
+ rble doctor
103
+
104
+ # Inspect a device's GATT services
105
+ rble show AA:BB:CC:DD:EE:FF
106
+
107
+ # Read/write characteristics
108
+ rble read AA:BB:CC:DD:EE:FF 2a19 # Battery Level → "95%"
109
+ rble write AA:BB:CC:DD:EE:FF 2a00 "MyDevice"
110
+
111
+ # Stream notifications
112
+ rble monitor AA:BB:CC:DD:EE:FF 2a37 # Heart Rate → "72 bpm"
113
+
114
+ # Manage adapter
115
+ rble adapter list
116
+ rble adapter power on
117
+
118
+ # Pairing
119
+ rble pair AA:BB:CC:DD:EE:FF
120
+ rble paired
121
+ rble unpair AA:BB:CC:DD:EE:FF
122
+ ```
123
+
124
+ All commands support `--json` for structured NDJSON output and `--help` for usage details.
125
+
126
+ ## Rake Tasks
127
+
128
+ RBLE provides rake tasks for system checks and integration testing. Add to your Rakefile:
129
+
130
+ ```ruby
131
+ require 'rble/tasks'
132
+ ```
133
+
134
+ Then run:
135
+
136
+ ```bash
137
+ # Check system BLE readiness (permissions, adapter, helper binary)
138
+ rake rble:check
139
+
140
+ # Run integration test with real BLE hardware
141
+ rake test:integration
142
+ ```
143
+
144
+ The `rble:check` task verifies your system is correctly configured for BLE operations and provides actionable suggestions for any issues found.
145
+
146
+ ## Usage Examples
147
+
148
+ ### Scanning for Devices
149
+
150
+ Basic scan with timeout:
151
+
152
+ ```ruby
153
+ require 'rble'
154
+
155
+ # Scan for 10 seconds, callback for each unique device
156
+ RBLE.scan(timeout: 10) do |device|
157
+ puts "#{device.name || 'Unknown'} - #{device.address}"
158
+ puts " RSSI: #{device.rssi} dBm"
159
+ puts " Services: #{device.service_uuids.join(', ')}" unless device.service_uuids.empty?
160
+ end
161
+ ```
162
+
163
+ Filter by service UUID (e.g., Heart Rate service):
164
+
165
+ ```ruby
166
+ RBLE.scan(timeout: 10, service_uuids: ['180d']) do |device|
167
+ puts "Heart rate monitor: #{device.name}"
168
+ end
169
+ ```
170
+
171
+ Continuous RSSI monitoring (for beacon-style applications):
172
+
173
+ ```ruby
174
+ RBLE.scan(timeout: 60, allow_duplicates: true) do |device|
175
+ puts "#{device.address}: RSSI #{device.rssi} dBm"
176
+ end
177
+ ```
178
+
179
+ Manual stop control:
180
+
181
+ ```ruby
182
+ scanner = RBLE.scan do |device|
183
+ puts device.name
184
+ scanner.stop if device.name == "MyDevice"
185
+ end
186
+ ```
187
+
188
+ ### Finding a Specific Device
189
+
190
+ Find a device by address (stops scanning when found):
191
+
192
+ ```ruby
193
+ device = RBLE.find_device("AA:BB:CC:DD:EE:FF", timeout: 10)
194
+
195
+ if device
196
+ puts "Found: #{device.name}"
197
+ else
198
+ puts "Device not found"
199
+ end
200
+ ```
201
+
202
+ ### Connecting and Discovering Services
203
+
204
+ ```ruby
205
+ # First, find the device to ensure it's advertising
206
+ device = RBLE.find_device("AA:BB:CC:DD:EE:FF", timeout: 10)
207
+ raise "Device not found" unless device
208
+
209
+ # Connect to the device
210
+ connection = RBLE.connect(device.address, timeout: 30)
211
+ puts "Connected!"
212
+
213
+ # Discover GATT services
214
+ services = connection.discover_services
215
+ puts "Found #{services.length} services:"
216
+
217
+ services.each do |service|
218
+ puts " Service: #{service.short_uuid}"
219
+ service.characteristics.each do |char|
220
+ puts " Characteristic: #{char.short_uuid} [#{char.flags.join(', ')}]"
221
+ end
222
+ end
223
+
224
+ # Always disconnect when done
225
+ connection.disconnect
226
+ ```
227
+
228
+ ### Reading a Characteristic
229
+
230
+ ```ruby
231
+ device = RBLE.find_device("AA:BB:CC:DD:EE:FF", timeout: 10)
232
+ raise "Device not found" unless device
233
+
234
+ connection = RBLE.connect(device.address)
235
+ connection.discover_services
236
+
237
+ # Get Device Information service (standard UUID: 0x180a)
238
+ device_info = connection.service('180a')
239
+
240
+ # Read Model Number characteristic (standard UUID: 0x2a24)
241
+ model_char = device_info.characteristic('2a24')
242
+ model_number = model_char.read
243
+ puts "Model: #{model_number}"
244
+
245
+ # Read as byte array instead
246
+ bytes = model_char.read_bytes
247
+ puts "Bytes: #{bytes.inspect}"
248
+
249
+ connection.disconnect
250
+ ```
251
+
252
+ ### Writing a Characteristic
253
+
254
+ ```ruby
255
+ device = RBLE.find_device("AA:BB:CC:DD:EE:FF", timeout: 10)
256
+ raise "Device not found" unless device
257
+
258
+ connection = RBLE.connect(device.address)
259
+ connection.discover_services
260
+
261
+ # Find your service and characteristic
262
+ service = connection.service('your-service-uuid')
263
+ char = service.characteristic('your-characteristic-uuid')
264
+
265
+ # Write with response (waits for acknowledgment)
266
+ char.write([0x01, 0x02, 0x03])
267
+
268
+ # Write without response (faster, no acknowledgment)
269
+ char.write([0x01, 0x02, 0x03], response: false)
270
+
271
+ # Write a string (converted to bytes)
272
+ char.write("hello".bytes)
273
+
274
+ connection.disconnect
275
+ ```
276
+
277
+ ### Subscribing to Notifications
278
+
279
+ ```ruby
280
+ device = RBLE.find_device("AA:BB:CC:DD:EE:FF", timeout: 10)
281
+ raise "Device not found" unless device
282
+
283
+ connection = RBLE.connect(device.address)
284
+ connection.discover_services
285
+
286
+ # Subscribe to Heart Rate Measurement notifications
287
+ hr_service = connection.service('180d')
288
+ hr_measurement = hr_service.characteristic('2a37')
289
+
290
+ hr_measurement.subscribe do |value|
291
+ # Heart Rate Measurement format: first byte contains flags,
292
+ # heart rate value follows (8-bit or 16-bit based on flags)
293
+ bytes = value.bytes
294
+ flags = bytes[0]
295
+ hr_value = (flags & 0x01) == 0 ? bytes[1] : (bytes[1] | (bytes[2] << 8))
296
+ puts "Heart Rate: #{hr_value} BPM"
297
+ end
298
+
299
+ # Keep the connection open to receive notifications
300
+ puts "Listening for heart rate updates... Press Ctrl+C to stop"
301
+ sleep(60)
302
+
303
+ # Clean up
304
+ hr_measurement.unsubscribe
305
+ connection.disconnect
306
+ ```
307
+
308
+ ### Handling Disconnection Events
309
+
310
+ ```ruby
311
+ device = RBLE.find_device("AA:BB:CC:DD:EE:FF", timeout: 10)
312
+ raise "Device not found" unless device
313
+
314
+ connection = RBLE.connect(device.address)
315
+
316
+ # Register disconnect callback before doing other operations
317
+ connection.on_disconnect do |reason|
318
+ puts "Disconnected! Reason: #{reason}"
319
+ # reason is a symbol: :user_requested, :link_loss, :timeout, :remote_disconnect, etc.
320
+ end
321
+
322
+ # Optional: track all state changes
323
+ connection.on_state_change do |old_state, new_state|
324
+ puts "Connection state: #{old_state} -> #{new_state}"
325
+ end
326
+
327
+ connection.discover_services
328
+ # ... use the connection ...
329
+
330
+ # When you disconnect intentionally, callback receives :user_requested
331
+ connection.disconnect
332
+ ```
333
+
334
+ ## API Overview
335
+
336
+ ### RBLE Module Methods
337
+
338
+ - `RBLE.scan(timeout:, service_uuids:, allow_duplicates:, adapter:) { |device| }` - Scan for devices
339
+ - `RBLE.find_device(address, timeout:, adapter:)` - Find a specific device by address
340
+ - `RBLE.connect(address, timeout:, adapter:)` - Connect to a device
341
+ - `RBLE.adapters` - List available Bluetooth adapters
342
+
343
+ ### Device
344
+
345
+ Immutable snapshot of a discovered device:
346
+ - `address` - MAC address (Linux) or UUID (macOS)
347
+ - `name` - Device name from advertisement
348
+ - `rssi` - Signal strength in dBm
349
+ - `service_uuids` - Advertised service UUIDs
350
+ - `manufacturer_data` - Company ID to byte array mapping
351
+ - `tx_power` - Transmit power level
352
+
353
+ ### Connection
354
+
355
+ Active connection to a device:
356
+ - `discover_services` - Discover GATT services
357
+ - `services` - Get discovered services
358
+ - `service(uuid)` - Find a service by UUID
359
+ - `connected?` - Check connection state
360
+ - `on_disconnect { |reason| }` - Register disconnect callback
361
+ - `on_state_change { |old, new| }` - Register state change callback
362
+ - `disconnect` - Close the connection
363
+
364
+ ### Service
365
+
366
+ GATT service with characteristics:
367
+ - `uuid` - Full 128-bit UUID
368
+ - `short_uuid` - Short UUID for standard services (e.g., "180d")
369
+ - `characteristic(uuid)` - Find a characteristic by UUID
370
+ - `characteristics` - All characteristics in this service
371
+
372
+ ### ActiveCharacteristic
373
+
374
+ Characteristic with read/write/subscribe operations:
375
+ - `uuid` / `short_uuid` - Characteristic UUID
376
+ - `flags` - Capability flags ("read", "write", "notify", etc.)
377
+ - `readable?` / `writable?` / `subscribable?` - Check capabilities
378
+ - `read` - Read value as binary string
379
+ - `read_bytes` - Read value as byte array
380
+ - `write(data, response:)` - Write value
381
+ - `subscribe { |value| }` - Subscribe to notifications
382
+ - `unsubscribe` - Stop receiving notifications
383
+
384
+ ## Platform Notes
385
+
386
+ ### Linux (BlueZ)
387
+
388
+ **Requirements:**
389
+ - BlueZ 5.50 or higher
390
+ - D-Bus system bus access
391
+
392
+ **Adapter Selection:**
393
+
394
+ On systems with multiple Bluetooth adapters, specify which to use:
395
+
396
+ ```ruby
397
+ RBLE.scan(adapter: 'hci1') { |d| puts d.name }
398
+ RBLE.connect(address, adapter: 'hci1')
399
+ ```
400
+
401
+ List available adapters:
402
+
403
+ ```ruby
404
+ RBLE.adapters.each do |adapter|
405
+ puts "#{adapter[:name]}: #{adapter[:address]} (powered: #{adapter[:powered]})"
406
+ end
407
+ ```
408
+
409
+ **Permissions:**
410
+
411
+ If you see permission errors, ensure your user is in the `bluetooth` group:
412
+
413
+ ```bash
414
+ sudo usermod -aG bluetooth $USER
415
+ # Log out and log back in
416
+ ```
417
+
418
+ Or configure polkit for your application.
419
+
420
+ ### macOS (CoreBluetooth)
421
+
422
+ **Requirements:**
423
+ - macOS 12 (Monterey) or higher
424
+ - Xcode command-line tools (`xcode-select --install`)
425
+
426
+ **Build the Helper:**
427
+
428
+ The macOS backend uses a Swift helper binary. Build it with:
429
+
430
+ ```bash
431
+ bundle exec rake build:macos
432
+ ```
433
+
434
+ **Device Addresses:**
435
+
436
+ On macOS, CoreBluetooth does not expose actual MAC addresses. Instead, devices are identified by a system-assigned UUID that may change:
437
+
438
+ ```ruby
439
+ # macOS device addresses look like UUIDs
440
+ RBLE.find_device("E621E1F8-C36C-495A-93FC-0C247A3E6E5F")
441
+ ```
442
+
443
+ **Bluetooth Permissions:**
444
+
445
+ The first time your application uses Bluetooth, macOS will prompt for permission. Grant access in System Preferences > Privacy & Security > Bluetooth.
446
+
447
+ ## Error Handling
448
+
449
+ RBLE raises specific exceptions for different error conditions:
450
+
451
+ ```ruby
452
+ begin
453
+ connection = RBLE.connect(address, timeout: 10)
454
+ connection.discover_services
455
+ # ...
456
+ rescue RBLE::AdapterNotFoundError
457
+ puts "No Bluetooth adapter found"
458
+ rescue RBLE::AdapterDisabledError
459
+ puts "Bluetooth is turned off"
460
+ rescue RBLE::ConnectionTimeoutError
461
+ puts "Could not connect - device may be out of range"
462
+ rescue RBLE::NotConnectedError
463
+ puts "Connection lost during operation"
464
+ rescue RBLE::ServiceNotFoundError => e
465
+ puts "Service not available: #{e.message}"
466
+ rescue RBLE::CharacteristicNotFoundError => e
467
+ puts "Characteristic not available: #{e.message}"
468
+ rescue RBLE::NotSupportedError => e
469
+ puts "Operation not supported: #{e.message}"
470
+ rescue RBLE::ReadError, RBLE::WriteError => e
471
+ puts "GATT operation failed: #{e.message}"
472
+ end
473
+ ```
474
+
475
+ ### Common Errors
476
+
477
+ | Error | Meaning |
478
+ |-------|---------|
479
+ | `AdapterNotFoundError` | No Bluetooth hardware detected |
480
+ | `AdapterDisabledError` | Bluetooth is turned off |
481
+ | `PermissionError` | Missing Bluetooth permissions |
482
+ | `ConnectionTimeoutError` | Device not responding (out of range, not advertising) |
483
+ | `NotConnectedError` | Connection was lost |
484
+ | `ServiceNotFoundError` | Requested service UUID not on device |
485
+ | `CharacteristicNotFoundError` | Requested characteristic not in service |
486
+ | `NotSupportedError` | Characteristic doesn't support the operation |
487
+ | `ReadError` / `WriteError` | GATT operation failed |
488
+
489
+ ## Development
490
+
491
+ ### Running Tests
492
+
493
+ ```bash
494
+ bundle install
495
+ bundle exec rspec
496
+ ```
497
+
498
+ ### Building the macOS Helper
499
+
500
+ ```bash
501
+ bundle exec rake build:macos
502
+ ```
503
+
504
+ This compiles the Swift helper from `ext/macos_ble/`.
505
+
506
+ ### Code Style
507
+
508
+ ```bash
509
+ bundle exec rubocop
510
+ ```
511
+
512
+ ## License
513
+
514
+ MIT License. See [LICENSE.txt](LICENSE.txt) for details.
data/exe/rble ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Prevent broken pipe crash when piped to head/less
5
+ trap(:PIPE, "SYSTEM_DEFAULT")
6
+
7
+ # Flush immediately for streaming output
8
+ $stdout.sync = true
9
+ $stderr.sync = true
10
+
11
+ require "rble/cli"
12
+
13
+ Thor::Base.shell = Thor::Shell::Basic
14
+ RBLE::CLI::Main.start(ARGV)
@@ -0,0 +1,20 @@
1
+ // swift-tools-version: 5.9
2
+ // The swift-tools-version declares the minimum version of Swift required to build this package.
3
+
4
+ import PackageDescription
5
+
6
+ let package = Package(
7
+ name: "RBLEHelper",
8
+ platforms: [
9
+ .macOS(.v10_15)
10
+ ],
11
+ products: [
12
+ .executable(name: "RBLEHelper", targets: ["RBLEHelper"])
13
+ ],
14
+ targets: [
15
+ .executableTarget(
16
+ name: "RBLEHelper",
17
+ path: "Sources/RBLEHelper"
18
+ )
19
+ ]
20
+ )