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
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Extension configuration for macOS BLE helper
|
|
4
|
+
# This runs during `gem install` to compile the Swift CLI
|
|
5
|
+
|
|
6
|
+
require 'mkmf'
|
|
7
|
+
|
|
8
|
+
# Create a dummy Makefile - actual build happens below
|
|
9
|
+
# We need a Makefile for RubyGems to consider the extension "built"
|
|
10
|
+
File.write('Makefile', <<~MAKEFILE)
|
|
11
|
+
.PHONY: all install clean
|
|
12
|
+
|
|
13
|
+
all:
|
|
14
|
+
\t@echo "Swift build completed during extconf.rb"
|
|
15
|
+
|
|
16
|
+
install:
|
|
17
|
+
\t@echo "No install step needed"
|
|
18
|
+
|
|
19
|
+
clean:
|
|
20
|
+
\t@echo "Run 'swift package clean' to clean Swift build"
|
|
21
|
+
MAKEFILE
|
|
22
|
+
|
|
23
|
+
# Only build on macOS
|
|
24
|
+
unless RUBY_PLATFORM.include?('darwin')
|
|
25
|
+
puts 'Skipping macOS BLE helper build (not on macOS)'
|
|
26
|
+
exit 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Check for Swift
|
|
30
|
+
swift_path = find_executable('swift')
|
|
31
|
+
unless swift_path
|
|
32
|
+
warn <<~MSG
|
|
33
|
+
|
|
34
|
+
WARNING: Swift compiler not found.
|
|
35
|
+
The macOS BLE helper requires Swift to compile.
|
|
36
|
+
Install Xcode or Xcode Command Line Tools:
|
|
37
|
+
xcode-select --install
|
|
38
|
+
|
|
39
|
+
You can build manually later with:
|
|
40
|
+
cd #{Dir.pwd} && swift build -c release
|
|
41
|
+
|
|
42
|
+
MSG
|
|
43
|
+
exit 0 # Don't fail the gem install, just warn
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Build the Swift CLI
|
|
47
|
+
puts 'Building macOS BLE helper (Swift CLI)...'
|
|
48
|
+
puts " Source: #{Dir.pwd}"
|
|
49
|
+
|
|
50
|
+
# Run swift build in release mode
|
|
51
|
+
success = system('swift build -c release')
|
|
52
|
+
|
|
53
|
+
if success
|
|
54
|
+
binary = File.join(Dir.pwd, '.build', 'release', 'RBLEHelper')
|
|
55
|
+
if File.exist?(binary)
|
|
56
|
+
puts " Built: #{binary}"
|
|
57
|
+
puts " Size: #{File.size(binary)} bytes"
|
|
58
|
+
else
|
|
59
|
+
warn " WARNING: Build succeeded but binary not found at #{binary}"
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
warn <<~MSG
|
|
63
|
+
|
|
64
|
+
WARNING: Swift build failed.
|
|
65
|
+
You can try building manually:
|
|
66
|
+
cd #{Dir.pwd} && swift build -c release
|
|
67
|
+
|
|
68
|
+
Check that Xcode Command Line Tools are installed:
|
|
69
|
+
xcode-select --install
|
|
70
|
+
|
|
71
|
+
MSG
|
|
72
|
+
# Don't fail gem install - user can build manually
|
|
73
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RBLE
|
|
4
|
+
module Backend
|
|
5
|
+
# Abstract base class for platform-specific BLE backends.
|
|
6
|
+
# Subclasses must implement all public methods.
|
|
7
|
+
class Base
|
|
8
|
+
# Start scanning for BLE devices
|
|
9
|
+
#
|
|
10
|
+
# @param service_uuids [Array<String>, nil] Filter by service UUIDs (nil = all devices)
|
|
11
|
+
# @param allow_duplicates [Boolean] If true, callback fires on every advertisement
|
|
12
|
+
# @param adapter [String, nil] Specific adapter to use (nil = default)
|
|
13
|
+
# @yield [Device] Called when a device is discovered
|
|
14
|
+
# @return [void]
|
|
15
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
16
|
+
def start_scan(service_uuids: nil, allow_duplicates: false, adapter: nil, active: true, &block)
|
|
17
|
+
raise NotImplementedError, "#{self.class}#start_scan must be implemented"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Stop the current scan
|
|
21
|
+
#
|
|
22
|
+
# @return [void]
|
|
23
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
24
|
+
def stop_scan
|
|
25
|
+
raise NotImplementedError, "#{self.class}#stop_scan must be implemented"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Check if a scan is currently running
|
|
29
|
+
#
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
32
|
+
def scanning?
|
|
33
|
+
raise NotImplementedError, "#{self.class}#scanning? must be implemented"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# List available Bluetooth adapters
|
|
37
|
+
#
|
|
38
|
+
# @return [Array<Hash>] Array of adapter info hashes with :name, :address, :powered keys
|
|
39
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
40
|
+
def adapters
|
|
41
|
+
raise NotImplementedError, "#{self.class}#adapters must be implemented"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get the default adapter name
|
|
45
|
+
#
|
|
46
|
+
# @return [String, nil] Default adapter name or nil if none available
|
|
47
|
+
def default_adapter
|
|
48
|
+
adapters.first&.dig(:name)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Connection methods (implement in subclass)
|
|
52
|
+
|
|
53
|
+
# Connect to a BLE device
|
|
54
|
+
#
|
|
55
|
+
# @param device_path [String] D-Bus device path
|
|
56
|
+
# @param timeout [Numeric] Connection timeout in seconds
|
|
57
|
+
# @return [Boolean] true on successful connection
|
|
58
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
59
|
+
def connect_device(device_path, timeout: 30)
|
|
60
|
+
raise NotImplementedError, "#{self.class}#connect_device must be implemented"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Disconnect from a BLE device
|
|
64
|
+
#
|
|
65
|
+
# @param device_path [String] D-Bus device path
|
|
66
|
+
# @return [void]
|
|
67
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
68
|
+
def disconnect_device(device_path)
|
|
69
|
+
raise NotImplementedError, "#{self.class}#disconnect_device must be implemented"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Discover GATT services on a connected device
|
|
73
|
+
#
|
|
74
|
+
# @param device_path [String] D-Bus device path
|
|
75
|
+
# @param connection [Connection, nil] Connection for D-Bus session routing (BlueZ only)
|
|
76
|
+
# @param timeout [Numeric] Discovery timeout in seconds
|
|
77
|
+
# @return [Array<Service>] Discovered services
|
|
78
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
79
|
+
def discover_services(device_path, connection: nil, timeout: 30)
|
|
80
|
+
raise NotImplementedError, "#{self.class}#discover_services must be implemented"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Get the device D-Bus path for a given MAC address
|
|
84
|
+
#
|
|
85
|
+
# @param address [String] Device MAC address
|
|
86
|
+
# @param adapter [String, nil] Specific adapter to use
|
|
87
|
+
# @return [String, nil] Device path or nil if not found
|
|
88
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
89
|
+
def device_path_for_address(address, adapter: nil)
|
|
90
|
+
raise NotImplementedError, "#{self.class}#device_path_for_address must be implemented"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# GATT operations (implement in subclass)
|
|
94
|
+
|
|
95
|
+
# Read a characteristic value
|
|
96
|
+
#
|
|
97
|
+
# @param char_path [String] D-Bus characteristic path
|
|
98
|
+
# @param connection [Connection, nil] Connection for D-Bus session routing (BlueZ only)
|
|
99
|
+
# @param timeout [Numeric] Read timeout in seconds
|
|
100
|
+
# @return [String] Binary string (ASCII-8BIT encoding)
|
|
101
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
102
|
+
def read_characteristic(char_path, connection: nil, timeout: 30)
|
|
103
|
+
raise NotImplementedError, "#{self.class}#read_characteristic must be implemented"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Write a value to a characteristic
|
|
107
|
+
#
|
|
108
|
+
# @param char_path [String] D-Bus characteristic path
|
|
109
|
+
# @param data [String, Array<Integer>] Data to write
|
|
110
|
+
# @param connection [Connection, nil] Connection for D-Bus session routing (BlueZ only)
|
|
111
|
+
# @param response [Boolean] Wait for write response (true = write with response)
|
|
112
|
+
# @param timeout [Numeric] Write timeout in seconds
|
|
113
|
+
# @return [Boolean] true on success
|
|
114
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
115
|
+
def write_characteristic(char_path, data, connection: nil, response: true, timeout: 30)
|
|
116
|
+
raise NotImplementedError, "#{self.class}#write_characteristic must be implemented"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Subscribe to characteristic notifications
|
|
120
|
+
#
|
|
121
|
+
# @param char_path [String] D-Bus characteristic path
|
|
122
|
+
# @param connection [Connection, nil] Connection for D-Bus session routing (BlueZ only)
|
|
123
|
+
# @yield [String] Called with value (binary string) on each notification
|
|
124
|
+
# @return [Boolean] true on success
|
|
125
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
126
|
+
def subscribe_characteristic(char_path, connection: nil, &)
|
|
127
|
+
raise NotImplementedError, "#{self.class}#subscribe_characteristic must be implemented"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Unsubscribe from characteristic notifications
|
|
131
|
+
#
|
|
132
|
+
# @param char_path [String] D-Bus characteristic path
|
|
133
|
+
# @param connection [Connection, nil] Connection for D-Bus session routing (BlueZ only)
|
|
134
|
+
# @return [Boolean] true on success
|
|
135
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
136
|
+
def unsubscribe_characteristic(char_path, connection: nil)
|
|
137
|
+
raise NotImplementedError, "#{self.class}#unsubscribe_characteristic must be implemented"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Get active subscriptions for a connection
|
|
141
|
+
#
|
|
142
|
+
# @param connection [Connection] Connection to query
|
|
143
|
+
# @return [Array<Hash>] Subscription info hashes with :uuid and :path
|
|
144
|
+
def subscriptions_for_connection(connection)
|
|
145
|
+
[]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Pairing operations (implement in subclass)
|
|
149
|
+
|
|
150
|
+
# Pair with a BLE device
|
|
151
|
+
#
|
|
152
|
+
# @param device_path [String] D-Bus device path
|
|
153
|
+
# @param io_handler [#display, #prompt, #confirm] IO handler for user interaction
|
|
154
|
+
# @param force [Boolean] Skip interactive prompts (auto-accept)
|
|
155
|
+
# @param capability [String, nil] Security capability level ("low", "medium", "high", or nil for default)
|
|
156
|
+
# @param timeout [Numeric] Pairing timeout in seconds
|
|
157
|
+
# @return [Symbol] :paired on success, :already_paired if already paired
|
|
158
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
159
|
+
def pair_device(device_path, io_handler:, force: false, capability: nil, timeout: 30)
|
|
160
|
+
raise NotImplementedError, "#{self.class}#pair_device must be implemented"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Remove pairing bond from a device
|
|
164
|
+
#
|
|
165
|
+
# @param device_path [String] D-Bus device path
|
|
166
|
+
# @return [Symbol] :unpaired on success, :not_paired if not paired
|
|
167
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
168
|
+
def unpair_device(device_path)
|
|
169
|
+
raise NotImplementedError, "#{self.class}#unpair_device must be implemented"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# List all bonded/paired devices
|
|
173
|
+
#
|
|
174
|
+
# @return [Array<Hash>] Array of device info hashes
|
|
175
|
+
# @raise [NotImplementedError] if not implemented by subclass
|
|
176
|
+
def bonded_devices
|
|
177
|
+
raise NotImplementedError, "#{self.class}#bonded_devices must be implemented"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|