Class: Meshtastic::Admin::Firmware::BLE::BlueZ

Inherits:
Bluetooth::BlueZ show all
Defined in:
lib/meshtastic/admin/firmware/ble.rb

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

Constants inherited from Bluetooth::BlueZ

Bluetooth::BlueZ::FROMRADIO_UUID, Bluetooth::BlueZ::SERVICE_UUID, Bluetooth::BlueZ::TORADIO_UUID

Instance Method Summary collapse

Methods inherited from Bluetooth::BlueZ

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.

Raises:

  • (ArgumentError)


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

Raises:

  • (IOError)


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_lockedObject



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

Raises:

  • (IOError)


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