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,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :rble do
4
+ desc 'Check system BLE readiness'
5
+ task :check do
6
+ require_relative '../../rble'
7
+
8
+ checker = RBLE::SystemChecker.new
9
+ checker.run
10
+ exit(checker.success? ? 0 : 1)
11
+ end
12
+ end
13
+
14
+ module RBLE
15
+ # System BLE readiness checker
16
+ #
17
+ # Validates that the system is properly configured for BLE operations.
18
+ # Performs platform-specific checks and provides actionable fix suggestions.
19
+ #
20
+ # @api private
21
+ class SystemChecker
22
+ def initialize
23
+ @checks_run = 0
24
+ @failures = []
25
+ @suggestions = {}
26
+ end
27
+
28
+ # Run all platform-specific checks
29
+ def run
30
+ puts "RBLE System Check"
31
+ puts "=" * 40
32
+ puts
33
+
34
+ case RUBY_PLATFORM
35
+ when /linux/
36
+ run_linux_checks
37
+ when /darwin/
38
+ run_macos_checks
39
+ else
40
+ puts "[FAIL] Unsupported platform: #{RUBY_PLATFORM}"
41
+ @failures << "Unsupported platform"
42
+ return
43
+ end
44
+
45
+ print_summary
46
+ print_suggestions unless @failures.empty?
47
+ end
48
+
49
+ # Check if all checks passed
50
+ # @return [Boolean]
51
+ def success?
52
+ @failures.empty?
53
+ end
54
+
55
+ private
56
+
57
+ # Run a single check with standardized output
58
+ #
59
+ # @param name [String] Check name
60
+ # @param suggestion [String, nil] Fix suggestion if check fails
61
+ # @yield Block that returns truthy/falsy result
62
+ def check(name, suggestion: nil)
63
+ @checks_run += 1
64
+ result = yield
65
+ if result
66
+ puts "[OK] #{name}"
67
+ true
68
+ else
69
+ puts "[FAIL] #{name}"
70
+ @failures << name
71
+ @suggestions[name] = suggestion if suggestion
72
+ false
73
+ end
74
+ rescue StandardError => e
75
+ puts "[FAIL] #{name}: #{e.message}"
76
+ @failures << name
77
+ @suggestions[name] = suggestion if suggestion
78
+ false
79
+ end
80
+
81
+ # Linux-specific BLE readiness checks
82
+ def run_linux_checks
83
+ puts "Platform: Linux"
84
+ puts
85
+
86
+ check("BlueZ D-Bus service",
87
+ suggestion: "Install BlueZ: sudo apt install bluez") do
88
+ system("busctl list 2>/dev/null | grep -q org.bluez")
89
+ end
90
+
91
+ check("bluetoothd running",
92
+ suggestion: "Start Bluetooth: sudo systemctl start bluetooth") do
93
+ system("systemctl is-active --quiet bluetooth 2>/dev/null") ||
94
+ system("pgrep -x bluetoothd >/dev/null 2>&1")
95
+ end
96
+
97
+ check("Bluetooth adapter found",
98
+ suggestion: "No adapter found. Connect USB adapter or enable built-in.") do
99
+ RBLE.adapters.any?
100
+ end
101
+
102
+ check("Bluetooth adapter powered",
103
+ suggestion: "Enable adapter: bluetoothctl power on") do
104
+ RBLE.adapters.any? { |a| a[:powered] }
105
+ end
106
+ end
107
+
108
+ # macOS-specific BLE readiness checks
109
+ def run_macos_checks
110
+ puts "Platform: macOS"
111
+ puts
112
+
113
+ # Determine helper path relative to gem root
114
+ helper_path = File.expand_path('../../../ext/macos_ble/.build/release/RBLEHelper', __dir__)
115
+
116
+ helper_exists = check("Helper binary exists",
117
+ suggestion: "Build helper: rake build:macos") do
118
+ File.exist?(helper_path)
119
+ end
120
+
121
+ helper_executable = check("Helper binary executable",
122
+ suggestion: "Fix permissions: chmod +x #{helper_path}") do
123
+ File.executable?(helper_path)
124
+ end
125
+
126
+ # Only try RBLE operations if helper exists and is executable
127
+ # This avoids triggering CoreBluetooth permission prompts unnecessarily
128
+ if helper_exists && helper_executable
129
+ check("Bluetooth powered",
130
+ suggestion: "Enable Bluetooth in System Settings > Bluetooth") do
131
+ RBLE.adapters.any? { |a| a[:powered] }
132
+ end
133
+ end
134
+ end
135
+
136
+ # Print summary of check results
137
+ def print_summary
138
+ puts
139
+ passed = @checks_run - @failures.length
140
+ puts "#{passed}/#{@checks_run} checks passed"
141
+ end
142
+
143
+ # Print grouped fix suggestions for failures
144
+ def print_suggestions
145
+ puts
146
+ puts "Suggestions:"
147
+ @failures.each do |name|
148
+ if (suggestion = @suggestions[name])
149
+ puts " - #{name}: #{suggestion}"
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :test do
4
+ desc 'Run integration test with real BLE hardware'
5
+ task :integration do
6
+ require_relative '../../rble'
7
+
8
+ test = RBLE::IntegrationTest.new
9
+ test.run
10
+ exit(test.success? ? 0 : 1)
11
+ end
12
+ end
13
+
14
+ module RBLE
15
+ # Dynamic integration test for BLE hardware
16
+ #
17
+ # Scans for devices, then tries each device until finding one with
18
+ # a readable characteristic. Prioritizes devices advertising Device
19
+ # Information Service (0x180A) for reliability.
20
+ #
21
+ # @api private
22
+ class IntegrationTest
23
+ DEVICE_INFO_SERVICE = '180a'
24
+ MANUFACTURER_NAME = '2a29'
25
+ MODEL_NUMBER = '2a24'
26
+ SCAN_TIMEOUT = 5
27
+ CONNECTION_TIMEOUT = 10
28
+
29
+ def initialize
30
+ @success = false
31
+ @connection = nil
32
+ @results = []
33
+ @devices = []
34
+ end
35
+
36
+ # Run the integration test
37
+ def run
38
+ scan_for_devices
39
+ return if @devices.empty?
40
+
41
+ try_devices_until_success
42
+ rescue StandardError => e
43
+ puts "Error: #{e.message}"
44
+ @results << "Error: #{e.class} - #{e.message}"
45
+ ensure
46
+ cleanup
47
+ print_summary
48
+ end
49
+
50
+ # Check if test passed
51
+ # @return [Boolean]
52
+ def success?
53
+ @success
54
+ end
55
+
56
+ private
57
+
58
+ def scan_for_devices
59
+ puts "Scanning for BLE devices (#{SCAN_TIMEOUT}s timeout)..."
60
+
61
+ scanner = Scanner.new(timeout: SCAN_TIMEOUT)
62
+ scanner.start do |device|
63
+ # Deduplicate by address
64
+ unless @devices.any? { |d| d.address == device.address }
65
+ @devices << device
66
+ end
67
+ end
68
+
69
+ if @devices.empty?
70
+ puts
71
+ puts "No BLE devices found."
72
+ puts "Suggestions:"
73
+ puts " - Ensure BLE devices are nearby and advertising"
74
+ puts " - Check that Bluetooth adapter is enabled (rake rble:check)"
75
+ puts " - Try increasing scan time or moving closer to devices"
76
+ @results << "No devices found"
77
+ return
78
+ end
79
+
80
+ puts "Found #{@devices.length} device(s)"
81
+ @results << "Scanned and found #{@devices.length} device(s)"
82
+
83
+ # Sort: prefer devices with Device Information Service
84
+ @devices.sort_by! do |d|
85
+ has_device_info = d.service_uuids.any? { |uuid| uuid.downcase.include?(DEVICE_INFO_SERVICE) }
86
+ has_device_info ? 0 : 1
87
+ end
88
+ end
89
+
90
+ def try_devices_until_success
91
+ @devices.each_with_index do |device, index|
92
+ device_name = device.name || 'unnamed'
93
+ puts
94
+ puts "Trying device #{index + 1}/#{@devices.length}: #{device_name} (#{device.address})"
95
+
96
+ result = try_device(device)
97
+ if result == :success
98
+ @success = true
99
+ return
100
+ end
101
+ # Continue to next device on :no_readable or :connection_failed
102
+ end
103
+
104
+ puts
105
+ puts "Tried all #{@devices.length} device(s), none had readable characteristics"
106
+ @results << "No device with readable characteristics found"
107
+ end
108
+
109
+ # Try a single device: connect, discover, read
110
+ # Returns :success, :no_readable, or :connection_failed
111
+ def try_device(device)
112
+ # Connect
113
+ puts " Connecting..."
114
+ begin
115
+ @connection = RBLE.connect(device.address, timeout: CONNECTION_TIMEOUT)
116
+ puts " Connected!"
117
+ rescue ConnectionTimeoutError
118
+ puts " Connection timed out, skipping..."
119
+ return :connection_failed
120
+ rescue ConnectionError => e
121
+ puts " Connection failed: #{e.message}, skipping..."
122
+ return :connection_failed
123
+ end
124
+
125
+ # Discover services
126
+ puts " Discovering services..."
127
+ services = @connection.discover_services
128
+ char_count = services.sum { |s| s.characteristics.length }
129
+ puts " Found #{services.length} services, #{char_count} characteristics"
130
+
131
+ # Find and read a characteristic
132
+ char = find_readable_characteristic(services)
133
+ unless char
134
+ puts " No readable characteristics, trying next device..."
135
+ disconnect_quietly
136
+ return :no_readable
137
+ end
138
+
139
+ # Read the characteristic
140
+ puts " Reading #{char_name(char)} (#{char.short_uuid})..."
141
+ value = char.read
142
+ display = format_value(value)
143
+ puts " Value: #{display.inspect}"
144
+
145
+ # Record success
146
+ device_name = device.name || 'unnamed'
147
+ @results << "Connected to #{device_name}"
148
+ @results << "Discovered #{services.length} services"
149
+ truncated = display.length > 50 ? "#{display[0..47]}..." : display
150
+ @results << "Read characteristic #{char.short_uuid}: #{truncated}"
151
+
152
+ :success
153
+ rescue StandardError => e
154
+ puts " Error: #{e.message}, trying next device..."
155
+ disconnect_quietly
156
+ :connection_failed
157
+ end
158
+
159
+ def find_readable_characteristic(services)
160
+ # First try Device Information Service
161
+ device_info = services.find { |s| s.short_uuid == DEVICE_INFO_SERVICE }
162
+
163
+ if device_info
164
+ # Prefer Manufacturer Name, then Model Number
165
+ [MANUFACTURER_NAME, MODEL_NUMBER].each do |uuid|
166
+ char = device_info.characteristics.find { |c| c.short_uuid == uuid && c.readable? }
167
+ return char if char
168
+ end
169
+
170
+ # Try any readable char in Device Info service
171
+ char = device_info.characteristics.find(&:readable?)
172
+ return char if char
173
+ end
174
+
175
+ # Fallback: any readable characteristic from any service
176
+ services.each do |service|
177
+ char = service.characteristics.find(&:readable?)
178
+ return char if char
179
+ end
180
+
181
+ nil
182
+ end
183
+
184
+ def format_value(value)
185
+ if value.bytes.all? { |b| (b >= 32 && b < 127) || b == 0 }
186
+ # Remove null terminators and clean up
187
+ value.force_encoding('UTF-8').gsub("\x00", '')
188
+ else
189
+ # Display as hex for binary data
190
+ value.bytes.map { |b| format('%02x', b) }.join(' ')
191
+ end
192
+ end
193
+
194
+ def char_name(char)
195
+ case char.short_uuid
196
+ when MANUFACTURER_NAME then 'Manufacturer Name'
197
+ when MODEL_NUMBER then 'Model Number'
198
+ when '2a25' then 'Serial Number'
199
+ when '2a27' then 'Hardware Revision'
200
+ when '2a26' then 'Firmware Revision'
201
+ when '2a28' then 'Software Revision'
202
+ else 'Characteristic'
203
+ end
204
+ end
205
+
206
+ def disconnect_quietly
207
+ return unless @connection
208
+
209
+ @connection.disconnect if @connection.connected?
210
+ rescue StandardError
211
+ # Ignore disconnect errors
212
+ ensure
213
+ @connection = nil
214
+ end
215
+
216
+ def cleanup
217
+ return unless @connection
218
+
219
+ begin
220
+ if @connection.connected?
221
+ puts
222
+ puts "Disconnecting..."
223
+ @connection.disconnect
224
+ puts "Disconnected"
225
+ end
226
+ rescue StandardError => e
227
+ puts "Disconnect warning: #{e.message}"
228
+ end
229
+ end
230
+
231
+ def print_summary
232
+ puts
233
+ puts '=' * 40
234
+ if @success
235
+ puts 'Integration test PASSED'
236
+ else
237
+ puts 'Integration test FAILED'
238
+ end
239
+ @results.each { |r| puts " - #{r}" }
240
+ end
241
+ end
242
+ end
data/lib/rble/tasks.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Load all RBLE rake tasks
4
+ # Usage in Rakefile: require 'rble/tasks'
5
+
6
+ Dir.glob(File.join(__dir__, 'tasks', '*.rake')).each do |task_file|
7
+ load task_file
8
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBLE
4
+ VERSION = '0.7.0'
5
+ end
data/lib/rble.rb ADDED
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rble/version'
4
+ require_relative 'rble/errors'
5
+ require_relative 'rble/device'
6
+ require_relative 'rble/service'
7
+ require_relative 'rble/characteristic'
8
+ require_relative 'rble/backend'
9
+ require_relative 'rble/scanner'
10
+ require_relative 'rble/connection'
11
+
12
+ module RBLE
13
+ class << self
14
+ # Logger for debug output
15
+ # Set to a Logger instance with debug level to see notification flow
16
+ # @example
17
+ # RBLE.logger = Logger.new(STDOUT)
18
+ # RBLE.logger.level = Logger::DEBUG
19
+ attr_accessor :logger
20
+
21
+ # Enable trace-level output for connection timing and D-Bus call flow
22
+ # @example
23
+ # RBLE.trace = true
24
+ attr_accessor :trace
25
+
26
+ # Global warning flag. Defaults to true.
27
+ # Set to false to suppress feature-level warnings from RBLE.
28
+ # @example
29
+ # RBLE.warnings = false
30
+ attr_writer :warnings
31
+
32
+ def warnings
33
+ @warnings.nil? ? true : @warnings
34
+ end
35
+
36
+ # Output a warning with [RBLE] prefix, respecting RBLE.warnings flag.
37
+ # @param message [String] Warning message
38
+ def rble_warn(message)
39
+ return unless warnings
40
+
41
+ warn "[RBLE] #{message}"
42
+ end
43
+ end
44
+
45
+ # Backend selection API - delegates to RBLE::Backend
46
+
47
+ def self.backend
48
+ Backend.backend
49
+ end
50
+
51
+ def self.backend=(value)
52
+ Backend.backend = value
53
+ end
54
+
55
+ def self.available_backends
56
+ Backend.available_backends
57
+ end
58
+
59
+ def self.backend_info
60
+ Backend.backend_info
61
+ end
62
+ end
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rble
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.7.0
5
+ platform: ruby
6
+ authors:
7
+ - Jonas Tehler
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: thor
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.3'
26
+ description: Reliable BLE communication for Ruby - scanning, connections, GATT operations
27
+ on Linux (BlueZ/D-Bus) and macOS (CoreBluetooth). Includes standalone rble CLI tool.
28
+ email:
29
+ - jonas@tehler.se
30
+ executables:
31
+ - rble
32
+ extensions:
33
+ - ext/macos_ble/extconf.rb
34
+ extra_rdoc_files: []
35
+ files:
36
+ - CHANGELOG.md
37
+ - LICENSE.txt
38
+ - README.md
39
+ - exe/rble
40
+ - ext/macos_ble/Package.swift
41
+ - ext/macos_ble/Sources/RBLEHelper/BLEManager.swift
42
+ - ext/macos_ble/Sources/RBLEHelper/Protocol.swift
43
+ - ext/macos_ble/Sources/RBLEHelper/main.swift
44
+ - ext/macos_ble/extconf.rb
45
+ - lib/rble.rb
46
+ - lib/rble/backend.rb
47
+ - lib/rble/backend/base.rb
48
+ - lib/rble/backend/bluez.rb
49
+ - lib/rble/backend/corebluetooth.rb
50
+ - lib/rble/bluez.rb
51
+ - lib/rble/bluez/adapter.rb
52
+ - lib/rble/bluez/async_call.rb
53
+ - lib/rble/bluez/async_connection_operations.rb
54
+ - lib/rble/bluez/async_gatt_operations.rb
55
+ - lib/rble/bluez/async_introspection.rb
56
+ - lib/rble/bluez/dbus_connection.rb
57
+ - lib/rble/bluez/dbus_session.rb
58
+ - lib/rble/bluez/device.rb
59
+ - lib/rble/bluez/event_loop.rb
60
+ - lib/rble/bluez/gatt_operation_queue.rb
61
+ - lib/rble/bluez/pairing_agent.rb
62
+ - lib/rble/bluez/pairing_session.rb
63
+ - lib/rble/bluez/retry_policy.rb
64
+ - lib/rble/characteristic.rb
65
+ - lib/rble/cli.rb
66
+ - lib/rble/cli/adapter.rb
67
+ - lib/rble/cli/characteristic_helpers.rb
68
+ - lib/rble/cli/doctor.rb
69
+ - lib/rble/cli/formatters/json.rb
70
+ - lib/rble/cli/formatters/text.rb
71
+ - lib/rble/cli/hex_dump.rb
72
+ - lib/rble/cli/monitor.rb
73
+ - lib/rble/cli/pair.rb
74
+ - lib/rble/cli/paired.rb
75
+ - lib/rble/cli/read.rb
76
+ - lib/rble/cli/scan.rb
77
+ - lib/rble/cli/show.rb
78
+ - lib/rble/cli/status.rb
79
+ - lib/rble/cli/unpair.rb
80
+ - lib/rble/cli/value_parser.rb
81
+ - lib/rble/cli/write.rb
82
+ - lib/rble/company_ids.rb
83
+ - lib/rble/connection.rb
84
+ - lib/rble/device.rb
85
+ - lib/rble/errors.rb
86
+ - lib/rble/gatt/uuid_database.rb
87
+ - lib/rble/scanner.rb
88
+ - lib/rble/service.rb
89
+ - lib/rble/tasks.rb
90
+ - lib/rble/tasks/check.rake
91
+ - lib/rble/tasks/integration.rake
92
+ - lib/rble/version.rb
93
+ homepage: https://github.com/jegt/rble
94
+ licenses:
95
+ - MIT
96
+ metadata:
97
+ homepage_uri: https://github.com/jegt/rble
98
+ source_code_uri: https://github.com/jegt/rble/tree/main
99
+ changelog_uri: https://github.com/jegt/rble/blob/main/CHANGELOG.md
100
+ documentation_uri: https://github.com/jegt/rble/blob/main/README.md
101
+ bug_tracker_uri: https://github.com/jegt/rble/issues
102
+ rubygems_mfa_required: 'true'
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '3.2'
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ requirements: []
117
+ rubygems_version: 4.0.7
118
+ specification_version: 4
119
+ summary: BLE communication library for Ruby with CoreBluetooth and BlueZ backends
120
+ test_files: []