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,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