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/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
|
+
)
|