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,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'characteristic_helpers'
|
|
4
|
+
|
|
5
|
+
module RBLE
|
|
6
|
+
module CLI
|
|
7
|
+
class Monitor
|
|
8
|
+
include CharacteristicHelpers
|
|
9
|
+
|
|
10
|
+
def initialize(options)
|
|
11
|
+
@options = options
|
|
12
|
+
@formatter = options["json"] ? Formatters::Json.new : Formatters::Text.new
|
|
13
|
+
@address = options["address"]
|
|
14
|
+
@characteristic_uuid = options["characteristic"]
|
|
15
|
+
@timeout = options["timeout"] || 10
|
|
16
|
+
@reconnect = options["reconnect"] || false
|
|
17
|
+
@stop = false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def execute
|
|
21
|
+
if @reconnect
|
|
22
|
+
reconnect_loop
|
|
23
|
+
else
|
|
24
|
+
single_session
|
|
25
|
+
end
|
|
26
|
+
rescue RBLE::DeviceNotFoundError => e
|
|
27
|
+
$stderr.puts "Error: #{e.message}"
|
|
28
|
+
$stderr.puts "Run `rble scan` first to discover nearby devices."
|
|
29
|
+
exit 1
|
|
30
|
+
rescue RBLE::Error => e
|
|
31
|
+
$stderr.puts "Error: #{e.message}"
|
|
32
|
+
exit 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Run a single monitor session: connect, subscribe, stream until stop or disconnect.
|
|
38
|
+
def single_session
|
|
39
|
+
connection = connect_and_discover(@address, timeout: @timeout)
|
|
40
|
+
|
|
41
|
+
begin
|
|
42
|
+
char = find_characteristic(connection, @characteristic_uuid)
|
|
43
|
+
|
|
44
|
+
unless char.subscribable?
|
|
45
|
+
$stderr.puts "Error: Characteristic #{@characteristic_uuid} does not support notifications."
|
|
46
|
+
$stderr.puts "Supported: #{char.flags.join(', ')}"
|
|
47
|
+
exit 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
stream_notifications(connection, char)
|
|
51
|
+
ensure
|
|
52
|
+
connection.disconnect if connection.connected?
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Reconnect loop: retry single_session on connection errors until @stop is set.
|
|
57
|
+
def reconnect_loop
|
|
58
|
+
loop do
|
|
59
|
+
begin
|
|
60
|
+
single_session
|
|
61
|
+
rescue RBLE::DeviceNotFoundError
|
|
62
|
+
raise # No retry for device not found
|
|
63
|
+
rescue RBLE::ConnectionTimeoutError, RBLE::ConnectionFailed, RBLE::Error => e
|
|
64
|
+
break if @stop
|
|
65
|
+
|
|
66
|
+
$stderr.puts "Error: #{e.message}"
|
|
67
|
+
$stderr.puts "Reconnecting in 2s..."
|
|
68
|
+
interruptible_sleep(2)
|
|
69
|
+
next
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
break if @stop
|
|
73
|
+
|
|
74
|
+
$stderr.puts "Device disconnected."
|
|
75
|
+
$stderr.puts "Reconnecting in 2s..."
|
|
76
|
+
interruptible_sleep(2)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Sleep in short intervals, checking @stop flag for responsiveness
|
|
81
|
+
# @param seconds [Numeric] Total sleep duration
|
|
82
|
+
def interruptible_sleep(seconds)
|
|
83
|
+
intervals = (seconds / 0.1).ceil
|
|
84
|
+
intervals.times do
|
|
85
|
+
break if @stop
|
|
86
|
+
sleep 0.1
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Subscribe and stream notifications until @stop flag or disconnect.
|
|
91
|
+
def stream_notifications(connection, char)
|
|
92
|
+
name = resolve_char_name(char.uuid)
|
|
93
|
+
char_display_name = RBLE::GATT::UUIDDatabase.resolve(char.uuid, type: :characteristic)
|
|
94
|
+
|
|
95
|
+
$stderr.puts "Subscribing to #{name}..."
|
|
96
|
+
|
|
97
|
+
char.subscribe do |value|
|
|
98
|
+
timestamp = Time.now
|
|
99
|
+
formatted = format_value(char.uuid, value)
|
|
100
|
+
@formatter.monitor_value(
|
|
101
|
+
address: @address,
|
|
102
|
+
uuid: char.uuid,
|
|
103
|
+
name: char_display_name,
|
|
104
|
+
raw: value,
|
|
105
|
+
formatted: formatted,
|
|
106
|
+
timestamp: timestamp
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
$stderr.puts "Subscribed, waiting for notifications..."
|
|
111
|
+
|
|
112
|
+
prev_handler = trap("INT") { @stop = true }
|
|
113
|
+
|
|
114
|
+
begin
|
|
115
|
+
while !@stop && connection.connected?
|
|
116
|
+
sleep 0.1
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
unless connection.connected?
|
|
120
|
+
$stderr.puts "Device disconnected." unless @stop
|
|
121
|
+
end
|
|
122
|
+
ensure
|
|
123
|
+
trap("INT", prev_handler || "DEFAULT")
|
|
124
|
+
char.unsubscribe if connection.connected?
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RBLE
|
|
4
|
+
module CLI
|
|
5
|
+
# IO handler for interactive terminal prompts during pairing
|
|
6
|
+
class TerminalIOHandler
|
|
7
|
+
def display(msg)
|
|
8
|
+
$stderr.puts msg
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def prompt(msg)
|
|
12
|
+
$stderr.print msg
|
|
13
|
+
$stdin.gets&.strip
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def confirm(msg)
|
|
17
|
+
$stderr.print msg
|
|
18
|
+
response = $stdin.gets&.strip&.downcase
|
|
19
|
+
response == 'y' || response == 'yes'
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Null IO handler for JSON mode (no interactive prompts on stdout)
|
|
24
|
+
class NullIOHandler
|
|
25
|
+
def display(_msg); end
|
|
26
|
+
|
|
27
|
+
def prompt(_msg)
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def confirm(_msg)
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class Pair
|
|
37
|
+
SECURITY_CAPABILITY_MAP = {
|
|
38
|
+
"low" => "NoInputNoOutput",
|
|
39
|
+
"medium" => "DisplayYesNo",
|
|
40
|
+
"high" => "KeyboardDisplay"
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
def initialize(options)
|
|
44
|
+
@options = options
|
|
45
|
+
@formatter = options["json"] ? Formatters::Json.new : Formatters::Text.new
|
|
46
|
+
@address = options["address"]
|
|
47
|
+
@timeout = options["timeout"] || 30
|
|
48
|
+
@security = options["security"]
|
|
49
|
+
@force = options["force"] || false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def execute
|
|
53
|
+
if @security && !SECURITY_CAPABILITY_MAP.key?(@security)
|
|
54
|
+
$stderr.puts "Error: Invalid security level '#{@security}'. Use: #{SECURITY_CAPABILITY_MAP.keys.join(', ')}"
|
|
55
|
+
exit 1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
backend = RBLE::Backend.for_platform
|
|
59
|
+
device_path = backend.device_path_for_address(@address)
|
|
60
|
+
|
|
61
|
+
if device_path.nil?
|
|
62
|
+
$stderr.puts "Error: Device #{@address} not found."
|
|
63
|
+
$stderr.puts "Run `rble scan` first to discover nearby devices."
|
|
64
|
+
exit 1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
io_handler = @options["json"] ? NullIOHandler.new : TerminalIOHandler.new
|
|
68
|
+
capability = SECURITY_CAPABILITY_MAP[@security]
|
|
69
|
+
|
|
70
|
+
$stderr.puts "Pairing with #{@address}..."
|
|
71
|
+
result = backend.pair_device(
|
|
72
|
+
device_path,
|
|
73
|
+
io_handler: io_handler,
|
|
74
|
+
force: @force,
|
|
75
|
+
capability: capability,
|
|
76
|
+
timeout: @timeout
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
case result
|
|
80
|
+
when :paired
|
|
81
|
+
$stderr.puts "Bonded successfully"
|
|
82
|
+
@formatter.pair_result(success: true, address: @address, message: "Bonded successfully")
|
|
83
|
+
when :already_paired
|
|
84
|
+
$stderr.puts "Device already paired"
|
|
85
|
+
@formatter.pair_result(success: true, address: @address, message: "Already paired")
|
|
86
|
+
end
|
|
87
|
+
rescue RBLE::DeviceNotFoundError => e
|
|
88
|
+
$stderr.puts "Error: #{e.message}"
|
|
89
|
+
$stderr.puts "Run `rble scan` first to discover nearby devices."
|
|
90
|
+
exit 1
|
|
91
|
+
rescue RBLE::AuthenticationError
|
|
92
|
+
$stderr.puts "Error: Pairing rejected by device. Ensure device is in pairing mode and try again."
|
|
93
|
+
exit 1
|
|
94
|
+
rescue RBLE::ConnectionFailed
|
|
95
|
+
$stderr.puts "Error: Connection failed. Device may be out of range."
|
|
96
|
+
exit 1
|
|
97
|
+
rescue RBLE::Error => e
|
|
98
|
+
$stderr.puts "Error: #{e.message}"
|
|
99
|
+
exit 1
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RBLE
|
|
4
|
+
module CLI
|
|
5
|
+
class Paired
|
|
6
|
+
def initialize(options)
|
|
7
|
+
@options = options
|
|
8
|
+
@formatter = options["json"] ? Formatters::Json.new : Formatters::Text.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def execute
|
|
12
|
+
backend = RBLE::Backend.for_platform
|
|
13
|
+
devices = backend.bonded_devices
|
|
14
|
+
|
|
15
|
+
@formatter.paired_list(devices)
|
|
16
|
+
rescue RBLE::Error => e
|
|
17
|
+
$stderr.puts "Error: #{e.message}"
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'characteristic_helpers'
|
|
4
|
+
|
|
5
|
+
module RBLE
|
|
6
|
+
module CLI
|
|
7
|
+
class Read
|
|
8
|
+
include CharacteristicHelpers
|
|
9
|
+
|
|
10
|
+
def initialize(options)
|
|
11
|
+
@options = options
|
|
12
|
+
@formatter = options["json"] ? Formatters::Json.new : Formatters::Text.new
|
|
13
|
+
@address = options["address"]
|
|
14
|
+
@characteristic_uuid = options["characteristic"]
|
|
15
|
+
@timeout = options["timeout"] || 10
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def execute
|
|
19
|
+
connection = connect_and_discover(@address, timeout: @timeout)
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
char = find_characteristic(connection, @characteristic_uuid)
|
|
23
|
+
|
|
24
|
+
unless char.readable?
|
|
25
|
+
$stderr.puts "Error: Characteristic #{@characteristic_uuid} does not support read."
|
|
26
|
+
$stderr.puts "Supported: #{char.flags.join(', ')}"
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
name = resolve_char_name(char.uuid)
|
|
31
|
+
$stderr.puts "Reading #{name}..."
|
|
32
|
+
raw_value = char.read
|
|
33
|
+
formatted = format_value(char.uuid, raw_value)
|
|
34
|
+
|
|
35
|
+
@formatter.read_value(
|
|
36
|
+
address: @address,
|
|
37
|
+
uuid: char.uuid,
|
|
38
|
+
name: RBLE::GATT::UUIDDatabase.resolve(char.uuid, type: :characteristic),
|
|
39
|
+
raw: raw_value,
|
|
40
|
+
formatted: formatted
|
|
41
|
+
)
|
|
42
|
+
ensure
|
|
43
|
+
connection.disconnect if connection.connected?
|
|
44
|
+
end
|
|
45
|
+
rescue RBLE::DeviceNotFoundError => e
|
|
46
|
+
$stderr.puts "Error: #{e.message}"
|
|
47
|
+
$stderr.puts "Run `rble scan` first to discover nearby devices."
|
|
48
|
+
exit 1
|
|
49
|
+
rescue RBLE::Error => e
|
|
50
|
+
$stderr.puts "Error: #{e.message}"
|
|
51
|
+
exit 1
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module RBLE
|
|
6
|
+
module CLI
|
|
7
|
+
class Scan
|
|
8
|
+
def initialize(options)
|
|
9
|
+
@options = options
|
|
10
|
+
@formatter = options["json"] ? Formatters::Json.new : Formatters::Text.new
|
|
11
|
+
@advertisement_count = 0
|
|
12
|
+
@seen_addresses = Set.new
|
|
13
|
+
@start_time = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def execute
|
|
17
|
+
scanner = RBLE::Scanner.new(
|
|
18
|
+
timeout: @options["timeout"],
|
|
19
|
+
active: !@options["passive"],
|
|
20
|
+
allow_duplicates: true
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
prev_handler = trap("INT") { scanner.stop }
|
|
24
|
+
|
|
25
|
+
@advertisement_count = 0
|
|
26
|
+
@seen_addresses = Set.new
|
|
27
|
+
@start_time = Time.now
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
scanner.start do |device|
|
|
31
|
+
next unless matches_filters?(device)
|
|
32
|
+
|
|
33
|
+
@advertisement_count += 1
|
|
34
|
+
is_new = @seen_addresses.add?(device.address)
|
|
35
|
+
next if @options["unique"] && !is_new
|
|
36
|
+
|
|
37
|
+
@formatter.device(device)
|
|
38
|
+
end
|
|
39
|
+
rescue RBLE::Error => e
|
|
40
|
+
handle_error(e)
|
|
41
|
+
ensure
|
|
42
|
+
trap("INT", prev_handler || "DEFAULT")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
duration = (Time.now - @start_time).round(1)
|
|
46
|
+
device_count = @seen_addresses.size
|
|
47
|
+
$stderr.puts "Discovered #{device_count} device#{'s' unless device_count == 1} " \
|
|
48
|
+
"(#{@advertisement_count} advertisement#{'s' unless @advertisement_count == 1}) " \
|
|
49
|
+
"in #{duration}s"
|
|
50
|
+
|
|
51
|
+
if @options["timeout"] && @advertisement_count.zero? && has_filters?
|
|
52
|
+
$stderr.puts "No devices found matching filters"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def matches_filters?(device)
|
|
59
|
+
if @options["name"]
|
|
60
|
+
return false unless device.name
|
|
61
|
+
return false unless device.name.downcase.include?(@options["name"].downcase)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if @options["rssi"]
|
|
65
|
+
return false unless device.rssi
|
|
66
|
+
return false unless device.rssi >= @options["rssi"]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def has_filters?
|
|
73
|
+
@options["name"] || @options["rssi"]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def handle_error(error)
|
|
77
|
+
$stderr.puts "Error: #{error.message}"
|
|
78
|
+
|
|
79
|
+
if @options["verbose"]
|
|
80
|
+
$stderr.puts error.recovery_hint if error.respond_to?(:recovery_hint)
|
|
81
|
+
$stderr.puts error.backtrace.join("\n") if error.backtrace
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
exit 1
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rble/gatt/uuid_database'
|
|
4
|
+
require_relative 'characteristic_helpers'
|
|
5
|
+
|
|
6
|
+
module RBLE
|
|
7
|
+
module CLI
|
|
8
|
+
class Show
|
|
9
|
+
include CharacteristicHelpers
|
|
10
|
+
|
|
11
|
+
def initialize(options)
|
|
12
|
+
@options = options
|
|
13
|
+
@formatter = options["json"] ? Formatters::Json.new : Formatters::Text.new
|
|
14
|
+
@address = options["address"]
|
|
15
|
+
@timeout = options["timeout"] || 10
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def execute
|
|
19
|
+
backend = RBLE::Backend.for_platform
|
|
20
|
+
connection = connect_with_retry(@address, timeout: @timeout)
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
$stderr.puts "Discovering services..."
|
|
24
|
+
connection.discover_services
|
|
25
|
+
services = connection.services
|
|
26
|
+
|
|
27
|
+
$stderr.puts "Found #{services.size} service#{'s' unless services.size == 1}"
|
|
28
|
+
|
|
29
|
+
tree_data = build_tree_data(@address, connection, services, backend)
|
|
30
|
+
@formatter.show_tree(tree_data)
|
|
31
|
+
ensure
|
|
32
|
+
connection.disconnect if connection.connected?
|
|
33
|
+
end
|
|
34
|
+
rescue RBLE::DeviceNotFoundError => e
|
|
35
|
+
$stderr.puts "Error: #{e.message}"
|
|
36
|
+
$stderr.puts "Run `rble scan` first to discover nearby devices."
|
|
37
|
+
exit 1
|
|
38
|
+
rescue RBLE::Error => e
|
|
39
|
+
$stderr.puts "Error: #{e.message}"
|
|
40
|
+
exit 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def build_tree_data(address, connection, services, backend)
|
|
46
|
+
device_name = begin
|
|
47
|
+
backend.device_name(connection.device_path)
|
|
48
|
+
rescue StandardError
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
service_data = services.map do |service|
|
|
53
|
+
resolved_service = RBLE::GATT::UUIDDatabase.resolve(service.uuid, type: :service)
|
|
54
|
+
|
|
55
|
+
chars = service.characteristics.map do |char|
|
|
56
|
+
resolved_char = RBLE::GATT::UUIDDatabase.resolve(char.uuid, type: :characteristic)
|
|
57
|
+
|
|
58
|
+
descriptors = extract_descriptors(char)
|
|
59
|
+
|
|
60
|
+
{
|
|
61
|
+
uuid: char.uuid,
|
|
62
|
+
resolved_name: resolved_char,
|
|
63
|
+
properties: char.flags,
|
|
64
|
+
handle: extract_handle(char.path),
|
|
65
|
+
descriptors: descriptors
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
uuid: service.uuid,
|
|
71
|
+
resolved_name: resolved_service,
|
|
72
|
+
primary: service.primary,
|
|
73
|
+
characteristics: chars
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
total_chars = service_data.sum { |s| s[:characteristics].size }
|
|
78
|
+
|
|
79
|
+
{
|
|
80
|
+
address: address,
|
|
81
|
+
name: device_name,
|
|
82
|
+
services: service_data,
|
|
83
|
+
total_characteristics: total_chars
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def extract_descriptors(char)
|
|
88
|
+
return [] unless char.respond_to?(:descriptors) && char.descriptors
|
|
89
|
+
|
|
90
|
+
char.descriptors.map do |desc|
|
|
91
|
+
resolved_desc = RBLE::GATT::UUIDDatabase.resolve(desc[:uuid], type: :descriptor)
|
|
92
|
+
{
|
|
93
|
+
uuid: desc[:uuid],
|
|
94
|
+
resolved_name: resolved_desc,
|
|
95
|
+
handle: extract_handle(desc[:path])
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def extract_handle(path)
|
|
101
|
+
return nil unless path
|
|
102
|
+
|
|
103
|
+
if path =~ /([0-9a-fA-F]{4})\z/
|
|
104
|
+
"0x#{Regexp.last_match(1).upcase}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RBLE
|
|
4
|
+
module CLI
|
|
5
|
+
class Status
|
|
6
|
+
def initialize(options)
|
|
7
|
+
@options = options
|
|
8
|
+
@formatter = options["json"] ? Formatters::Json.new : Formatters::Text.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def execute
|
|
12
|
+
backend = Backend.for_platform
|
|
13
|
+
adapter_name = @options["adapter"] || backend.default_adapter_name
|
|
14
|
+
info = backend.adapter_info(adapter_name)
|
|
15
|
+
@formatter.status(info)
|
|
16
|
+
rescue RBLE::AdapterNotFoundError
|
|
17
|
+
$stderr.puts "No Bluetooth adapter found. Run `rble doctor` to diagnose."
|
|
18
|
+
exit 1
|
|
19
|
+
rescue RBLE::Error => e
|
|
20
|
+
$stderr.puts "Error: #{e.message}"
|
|
21
|
+
exit 1
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RBLE
|
|
4
|
+
module CLI
|
|
5
|
+
class Unpair
|
|
6
|
+
def initialize(options)
|
|
7
|
+
@options = options
|
|
8
|
+
@formatter = options["json"] ? Formatters::Json.new : Formatters::Text.new
|
|
9
|
+
@address = options["address"]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def execute
|
|
13
|
+
backend = RBLE::Backend.for_platform
|
|
14
|
+
device_path = backend.device_path_for_address(@address)
|
|
15
|
+
|
|
16
|
+
if device_path.nil?
|
|
17
|
+
$stderr.puts "Device not paired"
|
|
18
|
+
@formatter.pair_result(success: true, address: @address, message: "Not paired")
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
$stderr.puts "Removing pairing for #{@address}..."
|
|
23
|
+
result = backend.unpair_device(device_path)
|
|
24
|
+
|
|
25
|
+
case result
|
|
26
|
+
when :unpaired
|
|
27
|
+
$stderr.puts "Pairing removed"
|
|
28
|
+
@formatter.pair_result(success: true, address: @address, message: "Pairing removed")
|
|
29
|
+
when :not_paired
|
|
30
|
+
$stderr.puts "Device not paired"
|
|
31
|
+
@formatter.pair_result(success: true, address: @address, message: "Not paired")
|
|
32
|
+
end
|
|
33
|
+
rescue RBLE::Error => e
|
|
34
|
+
$stderr.puts "Error: #{e.message}"
|
|
35
|
+
exit 1
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|