Module: Meshtastic::Admin::Firmware::BLE

Defined in:
lib/meshtastic/admin/firmware/ble.rb

Overview

ESP32 unified loader only; not the legacy firmware-ota protocol.

Defined Under Namespace

Classes: BlueZ, Stream

Constant Summary collapse

SERVICE_UUID =
'4fafc201-1fb5-459e-8fcc-c5c9c331914b'
OTA_UUID =
'62ec0272-3ec5-11eb-b378-0242ac130005'
TX_UUID =
'62ec0272-3ec5-11eb-b378-0242ac130003'

Class Method Summary collapse

Class Method Details

.authorsObject



185
186
187
# File 'lib/meshtastic/admin/firmware/ble.rb', line 185

public_class_method def self.authors
  Firmware.authors
end

.helpObject



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/meshtastic/admin/firmware/ble.rb', line 189

public_class_method def self.help
  puts "USAGE:
    # Upload using the unified ESP32 BLE protocol.
    #{self}.install(
      backend: 'optional - connected GATT backend; default selected-device BlueZ',
      address: 'optional - required bootloader BLE MAC address when backend is omitted',
      adapter: 'optional - selected BlueZ adapter, default hci0',
      bytes: 'optional - application image bytes, exclusive with firmware',
      firmware: 'optional - application image path, exclusive with bytes',
      timeout: 'optional - whole transfer deadline seconds, default 120'
    )
    # Print the authors of this module.
    #{self}.authors
  "
end

.install(opts = {}) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/meshtastic/admin/firmware/ble.rb', line 12

public_class_method def self.install(opts = {})
  unknown = opts.keys - %i[protocol bytes firmware backend address adapter timeout]
  raise ArgumentError, "Unsupported BLE options: #{unknown.join(', ')}" unless unknown.empty?
  raise ArgumentError, 'protocol must be :unified_ble' unless opts.fetch(:protocol, :unified_ble) == :unified_ble

  timeout = opts.fetch(:timeout, 120)
  raise ArgumentError, 'timeout must be positive finite seconds' unless timeout.is_a?(Numeric) && timeout.real? && timeout.finite? && timeout.positive?

  bytes = Firmware.__send__(:firmware_bytes, opts)
  backend = opts[:backend] || BlueZ.new(address: opts[:address], adapter: opts.fetch(:adapter, 'hci0'), timeout: timeout, service_uuid: SERVICE_UUID).connect
  Timeout.timeout(timeout) do
    backend.subscribe(uuid: TX_UUID)
    stream = Stream.new(backend)
    stream.command("VERSION\n")
    version = stream.line
    raise IOError, "Invalid loader VERSION: #{version}" unless version.match?(/\AOK \d+ \S+ \d+ \S+\z/)

    digest = Digest::SHA256.hexdigest(bytes)
    stream.command("OTA #{bytes.bytesize} #{digest}\n")
    reply = stream.line
    reply = stream.line if reply == 'ERASING'
    raise IOError, "OTA handshake rejected: #{reply}" unless reply == 'OK'

    offset = 0
    while offset < bytes.bytesize
      chunk = bytes.byteslice(offset, 20)
      backend.write(uuid: OTA_UUID, bytes: chunk, response: true)
      offset += chunk.bytesize
      reply = stream.line
      expected = offset == bytes.bytesize ? 'OK' : 'ACK'
      raise IOError, "OTA transfer expected #{expected}, received: #{reply}" unless reply == expected
    end
    { status: :verified, bytes: bytes.bytesize, sha256: digest, loader_version: version.delete_prefix('OK ') }
  end
ensure
  backend&.close
end