Class: Meshtastic::Admin::Firmware::BLE::BlueZ
Overview
Shared backend for unified OTA and Nordic DFU. All bus I/O stays on
the caller thread: synchronous replies also dispatch queued signals.
No discovery, pairing, address inference, or cross-device UUID lookup.
Constant Summary
Bluetooth::BlueZ::FROMRADIO_UUID, Bluetooth::BlueZ::SERVICE_UUID, Bluetooth::BlueZ::TORADIO_UUID
Instance Method Summary
collapse
authors, #bounded, #call, #check_adapter, #close, #close_locked, #connect, help, #managed_objects, #properties, #read, #release_bus, scan, #scan_adapter
Constructor Details
#initialize(address:, service_uuid:, adapter: 'hci0', timeout: 15) ⇒ BlueZ
Returns a new instance of BlueZ.
54
55
56
57
58
59
60
|
# File 'lib/meshtastic/admin/firmware/ble.rb', line 54
def initialize(address:, service_uuid:, adapter: 'hci0', timeout: 15)
super(address: address, adapter: adapter, timeout: timeout)
raise ArgumentError, 'Invalid GATT service UUID' unless service_uuid.is_a?(String) && /\A[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\z/i.match?(service_uuid)
@service_uuid = service_uuid
@notifications = []
end
|
Instance Method Details
#characteristic(uuid) ⇒ Object
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
# File 'lib/meshtastic/admin/firmware/ble.rb', line 87
def characteristic(uuid)
objects = managed_objects
services = objects.filter_map do |path, interfaces|
props = interfaces['org.bluez.GattService1']
path if props && props['Device'] == @device_path && props['UUID']&.casecmp?(@service_uuid)
end
matches = objects.filter_map do |path, interfaces|
props = interfaces['org.bluez.GattCharacteristic1']
path if props && services.include?(props['Service']) && props['UUID']&.casecmp?(uuid)
end
raise IOError, "Expected exactly one GATT characteristic #{uuid} on selected device" unless matches.size == 1
matches.first
end
|
#connect_locked ⇒ Object
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
|
# File 'lib/meshtastic/admin/firmware/ble.rb', line 62
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, 'Selected bootloader address not known to BlueZ; discover it explicitly first' unless @device_path
@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']
raise IOError, 'Bootloader ServicesResolved timed out' if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
sleep 0.05
end
self
rescue StandardError
close_locked
raise
end
|
#connected! ⇒ Object
102
103
104
|
# File 'lib/meshtastic/admin/firmware/ble.rb', line 102
def connected!
raise IOError, 'Bootloader disconnected' unless @bus && properties(@device_path, 'org.bluez.Device1')['Connected']
end
|
#notification(timeout:) ⇒ Object
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
# File 'lib/meshtastic/admin/firmware/ble.rb', line 132
def notification(timeout:)
@mutex.synchronize do
Timeout.timeout(timeout) do
loop do
return @notifications.shift unless @notifications.empty?
raise IOError, 'Bootloader bus closed' unless @bus
message = @bus.message_queue.pop
@bus.process(message)
end
end
end
rescue Timeout::Error
release_bus
raise IOError, 'Bootloader notification timed out; transfer not retried'
end
|
#subscribe(uuid:) ⇒ Object
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
# File 'lib/meshtastic/admin/firmware/ble.rb', line 115
def subscribe(uuid:)
@mutex.synchronize do
connected!
path = characteristic(uuid)
rule = "type='signal',sender='org.bluez',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='#{path}'"
bounded do
@bus.add_match(rule) do |message|
interface, changed = message.params
next unless message.path == path && interface == 'org.bluez.GattCharacteristic1' && changed.key?('Value')
@notifications << { uuid: uuid, bytes: changed.fetch('Value').pack('C*') }
end
end
call(path, 'org.bluez.GattCharacteristic1', 'StartNotify')
end
end
|
#write(uuid:, bytes:, response: true) ⇒ Object
106
107
108
109
110
111
112
113
|
# File 'lib/meshtastic/admin/firmware/ble.rb', line 106
def write(uuid:, bytes:, response: true)
@mutex.synchronize do
connected!
path = characteristic(uuid)
call(path, 'org.bluez.GattCharacteristic1', 'WriteValue', ['ay', bytes.bytes], ['a{sv}', { 'type' => ['s', response ? 'request' : 'command'] }])
bytes.bytesize
end
end
|