Class: Meshtastic::Bluetooth::BlueZ
- Inherits:
-
Object
- Object
- Meshtastic::Bluetooth::BlueZ
show all
- Defined in:
- lib/meshtastic/bluetooth/bluez.rb
Overview
Linux BlueZ transport for raw Meshtastic protobuf messages.
Constant Summary
collapse
- SERVICE_UUID =
'6ba1b218-15a8-461f-9fa8-5dcae273eafd'
- TORADIO_UUID =
'f75c76d2-129e-4dad-a1dd-7866124401e7'
- FROMRADIO_UUID =
'2c55e69e-4993-11ed-b878-0242ac120002'
Class Method Summary
collapse
Instance Method Summary
collapse
Constructor Details
#initialize(address:, adapter: 'hci0', timeout: 15) ⇒ BlueZ
Returns a new instance of BlueZ.
176
177
178
179
180
181
182
183
184
185
186
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 176
def initialize(address:, adapter: 'hci0', timeout: 15)
raise ArgumentError, 'Invalid Bluetooth address' unless address.is_a?(String) && /\A(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}\z/.match?(address)
raise ArgumentError, 'Invalid Bluetooth adapter (expected hciN)' unless adapter.is_a?(String) && /\Ahci\d+\z/.match?(adapter)
raise ArgumentError, 'Invalid Bluetooth timeout (expected positive finite seconds)' unless timeout.is_a?(Numeric) && timeout.real? && timeout.finite? && timeout.positive?
@address = address
@adapter = adapter
@timeout = timeout
@mutex = Mutex.new
end
|
Class Method Details
.authors ⇒ Object
188
189
190
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 188
public_class_method def self.authors
"AUTHOR(S):\n 0day Inc. <[email protected]>\n "
end
|
.help ⇒ Object
192
193
194
195
196
197
198
199
200
201
202
203
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 192
public_class_method def self.help
puts "USAGE:
# Scan BlueZ for nearby Meshtastic BLE advertisements.
#{self}.scan(
adapter: 'optional - BlueZ adapter name such as hci0',
timeout: 'optional - discovery duration in seconds (default: 5)'
)
# Print the AUTHOR(S) string for this module.
#{self}.authors
"
end
|
.scan(opts = {}) ⇒ Object
15
16
17
18
19
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 15
public_class_method def self.scan(opts = {})
adapter = opts[:adapter] || 'hci0'
timeout = opts[:timeout] || 5
new(address: '00:00:00:00:00:00', adapter: adapter, timeout: timeout).scan_adapter
end
|
Instance Method Details
#bounded ⇒ Object
ruby-dbus 0.25 has no method timeout API. An interrupted bus must never
be reused because its pending reply bookkeeping may be inconsistent.
169
170
171
172
173
174
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 169
def bounded(&)
Timeout.timeout(@timeout, &)
rescue Timeout::Error
release_bus
raise IOError, 'Bluetooth D-Bus operation timed out'
end
|
#call(path, interface, member, *parameters) ⇒ Object
157
158
159
160
161
162
163
164
165
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 157
def call(path, interface, member, *parameters)
message = DBus::Message.new(DBus::Message::METHOD_CALL)
message.destination = 'org.bluez'
message.path = path
message.interface = interface
message.member = member
parameters.each { |type, value| message.add_param(type, value) }
bounded { @bus.send_sync_or_async(message) }
end
|
#characteristic(uuid) ⇒ Object
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 120
def characteristic(uuid)
objects = managed_objects
service = objects.find do |_path, interfaces|
properties = interfaces['org.bluez.GattService1']
properties && properties['Device'] == @device_path && properties['UUID']&.casecmp?(SERVICE_UUID)
end&.first
raise IOError, 'Meshtastic GATT service not found on selected device' unless service
match = objects.find do |_path, interfaces|
properties = interfaces['org.bluez.GattCharacteristic1']
properties && properties['Service'] == service && properties['UUID']&.casecmp?(uuid)
end&.first
raise IOError, "Meshtastic GATT characteristic #{uuid} not found" unless match
match
end
|
#check_adapter(objects) ⇒ Object
137
138
139
140
141
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 137
def check_adapter(objects)
adapter = objects.dig("/org/bluez/#{@adapter}", 'org.bluez.Adapter1')
raise IOError, "Bluetooth adapter #{@adapter} not found" unless adapter
raise IOError, "Bluetooth adapter #{@adapter} is not powered" unless adapter['Powered']
end
|
#close ⇒ Object
54
55
56
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 54
def close
@mutex.synchronize { close_locked }
end
|
#close_locked ⇒ Object
58
59
60
61
62
63
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 58
def close_locked
call(@device_path, 'org.bluez.Device1', 'Disconnect') if @bus && @disconnect_needed
ensure
@disconnect_needed = false
release_bus
end
|
#connect ⇒ Object
21
22
23
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 21
def connect
@mutex.synchronize { connect_locked }
end
|
#connect_locked ⇒ Object
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 25
def connect_locked
@bus = DBus::ASystemBus.allocate
bounded { @bus.__send__(:initialize) }
objects = managed_objects
check_adapter(objects)
@device_path = objects.find do |_path, interfaces|
device = interfaces['org.bluez.Device1']
device && device['Address']&.casecmp?(@address) && device['Adapter'] == "/org/bluez/#{@adapter}"
end&.first
raise IOError, "Bluetooth device #{@address} not found on #{@adapter}; scan first" unless @device_path
raise IOError, "Bluetooth device is not Paired. Run: bluetoothctl pair #{@address} (enter the PIN shown by your radio), then retry." unless properties(@device_path, 'org.bluez.Device1')['Paired']
@disconnect_needed = true
call(@device_path, 'org.bluez.Device1', 'Connect')
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout
until properties(@device_path, 'org.bluez.Device1')['ServicesResolved']
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
raise IOError, 'Bluetooth ServicesResolved timed out' unless remaining.positive?
sleep [0.1, remaining].min
end
@to_radio = characteristic(TORADIO_UUID)
@from_radio = characteristic(FROMRADIO_UUID)
self
rescue StandardError
close_locked
raise
end
|
#connected! ⇒ Object
99
100
101
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 99
def connected!
raise IOError, 'Bluetooth device disconnected' unless @bus && @from_radio && properties(@device_path, 'org.bluez.Device1')['Connected']
end
|
#managed_objects ⇒ Object
153
154
155
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 153
def managed_objects
call('/', 'org.freedesktop.DBus.ObjectManager', 'GetManagedObjects').first
end
|
#properties(path, interface) ⇒ Object
149
150
151
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 149
def properties(path, interface)
call(path, 'org.freedesktop.DBus.Properties', 'GetAll', ['s', interface]).first
end
|
#read ⇒ Object
103
104
105
106
107
108
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 103
def read
@mutex.synchronize do
connected!
call(@from_radio, 'org.bluez.GattCharacteristic1', 'ReadValue', ['a{sv}', {}]).first.pack('C*')
end
end
|
#release_bus ⇒ Object
143
144
145
146
147
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 143
def release_bus
socket = @bus&.message_queue&.socket
socket.close if socket && !socket.closed?
@bus = nil
end
|
#scan_adapter ⇒ Object
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 65
def scan_adapter
started = false
@mutex.synchronize do
@bus = DBus::ASystemBus.allocate
bounded { @bus.__send__(:initialize) }
objects = managed_objects
check_adapter(objects)
adapter_path = "/org/bluez/#{@adapter}"
begin
call(adapter_path, 'org.bluez.Adapter1', 'StartDiscovery')
started = true
rescue DBus::Error
nil
end
sleep @timeout
managed_objects.filter_map do |_path, interfaces|
device = interfaces['org.bluez.Device1']
next unless device && device['Adapter'] == adapter_path
next unless Array(device['UUIDs']).any? { |uuid| uuid.to_s.casecmp?(SERVICE_UUID) }
{ address: device['Address'], name: device['Name'], paired: device['Paired'] }
end
ensure
if @bus && started
begin
call("/org/bluez/#{@adapter}", 'org.bluez.Adapter1', 'StopDiscovery')
rescue DBus::Error
nil
end
end
release_bus
end
end
|
#write(bytes) ⇒ Object
110
111
112
113
114
115
116
117
118
|
# File 'lib/meshtastic/bluetooth/bluez.rb', line 110
def write(bytes)
@mutex.synchronize do
connected!
call(@to_radio, 'org.bluez.GattCharacteristic1', 'WriteValue', ['ay', bytes.bytes], ['a{sv}', { 'type' => %w[s request] }])
bytes.bytesize
end
end
|