Module: Meshtastic::Admin::Firmware::Hex

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

Overview

Strict Intel HEX validation and explicitly selected nRF52840 SWD programming.

Constant Summary collapse

MAX_IMAGE =
16 * 1024 * 1024

Class Method Summary collapse

Class Method Details

.authors ⇒ Object



179
180
181
# File 'lib/meshtastic/admin/firmware/hex.rb', line 179

public_class_method def self.authors
  'Meshtastic Ruby contributors'
end

.help ⇒ Object



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/meshtastic/admin/firmware/hex.rb', line 183

public_class_method def self.help
  puts <<~HELP
    Validate Intel HEX without accessing hardware.
    #{self}.validate(
      bytes: 'required - complete Intel HEX content',
      expected_chip: 'required - exact supported symbol :nrf52840'
    )
    Program and verify explicitly selected nRF52840 hardware (destructive).
    #{self}.install(
      protocol: 'required - explicitly :swd only',
      firmware: 'optional - path to HEX file, exclusive with bytes',
      bytes: 'optional - Intel HEX content, exclusive with firmware',
      expected_chip: 'required - exact supported symbol :nrf52840',
      expected_target: 'required - configured OpenOCD target name, e.g. nrf52.cpu',
      openocd: 'required - absolute path to trusted installed executable',
      interface_config: 'required - absolute path to trusted probe Tcl configuration',
      target_config: 'required - absolute path to trusted nRF52 Tcl configuration',
      timeout: 'optional - positive execution limit in seconds, default 120'
    )
    List the module contributors.
    #{self}.authors
  HELP
end

.install(opts = {}) ⇒ Object

Raises:

  • (ArgumentError)


74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/meshtastic/admin/firmware/hex.rb', line 74

public_class_method def self.install(opts = {})
  allowed = %i[protocol firmware bytes expected_chip expected_target openocd interface_config target_config timeout]
  raise ArgumentError, 'Unsupported HEX install options' unless (opts.keys - allowed).empty?
  raise ArgumentError, 'protocol must be explicitly :swd' unless opts[:protocol] == :swd
  raise ArgumentError, 'Supply exactly one of firmware or bytes' unless opts.key?(:firmware) ^ opts.key?(:bytes)

  bytes = image_bytes(opts.merge({}))
   = validate(bytes: bytes, expected_chip: opts[:expected_chip])
  target = opts[:expected_target]
  raise ArgumentError, 'expected_target must be an explicit OpenOCD target name' unless target.is_a?(String) && target.match?(/\A[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}\z/)

  executable = trusted_file(path: opts[:openocd], executable: true)
  interface = trusted_file(path: opts[:interface_config])
  config = trusted_file(path: opts[:target_config])
  timeout = opts.fetch(:timeout, 120)
  raise ArgumentError, 'timeout must be finite and positive (at most 3600 seconds)' unless timeout.is_a?(Numeric) && timeout.finite? && timeout.positive? && timeout <= 3600

  token = "MESHTASTIC_HEX_OK_#{SecureRandom.hex(16)}"
  Tempfile.create(['meshtastic-', '.hex']) do |image|
    image.binmode
    image.write(bytes)
    image.flush
    path = tcl_word(value: image.path)
    script = <<~TCL
      if {[catch {
        init
        targets #{tcl_word(value: target)}
        reset init
        halt
        if {[lindex [read_memory 0x10000100 32 1] 0] != 0x52840} {error "nRF52840 FICR PART mismatch"}
        if {[lindex [read_memory 0x10000010 32 1] 0] != 4096 || [lindex [read_memory 0x10000014 32 1] 0] != 256} {error "nRF52840 flash geometry mismatch"}
        flash write_image erase #{path} 0 ihex
        verify_image #{path} 0 ihex
        reset run
      } failure]} {
        echo $failure
        shutdown error
      } else {
        echo #{token}
        shutdown
      }
    TCL
    args = [executable, '-c', 'gdb_port disabled; telnet_port disabled; tcl_port disabled',
            '-f', interface, '-c', 'transport select swd', '-f', config, '-c', script]
    run_programmer(args: args, timeout: timeout, token: token)
  end
  .merge(status: :verified, protocol: :swd, format: :hex, expected_chip: opts[:expected_chip],
                 expected_target: target, bytes: bytes.bytesize, sha256: Digest::SHA256.hexdigest(bytes),
                 flash_verified: true, reboot_verified: false)
end

.validate(opts = {}) ⇒ Object

Raises:

  • (ArgumentError)


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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/meshtastic/admin/firmware/hex.rb', line 15

public_class_method def self.validate(opts = {})
  raise ArgumentError, 'Only explicit expected_chip: :nrf52840 is supported' unless opts[:expected_chip] == :nrf52840

  bytes = opts[:bytes]
  raise ArgumentError, 'HEX bytes must be a nonempty String of at most 16 MiB' unless bytes.is_a?(String) && bytes.bytesize.between?(1, MAX_IMAGE)

  base = 0
  eof = false
  start = nil
  ranges = []
  bytes.b.each_line do |raw|
    line = raw.delete_suffix("\n").delete_suffix("\r")
    raise ArgumentError, 'Invalid HEX record or data after EOF' if eof || !line.match?(/\A:(?:[0-9a-fA-F]{2}){5,260}\z/)

    fields = [line[1..]].pack('H*').bytes
    count, high, low, type = fields.first(4)
    address = (high << 8) | low
    data = fields[4, count]
    raise ArgumentError, 'HEX length or checksum mismatch' unless fields.length == count + 5 && fields.sum.nobits?(255)

    case type
    when 0
      raise ArgumentError, 'Empty data or record crosses 64 KiB boundary' unless count.positive? && address + count <= 0x10000

      first = base + address
      last = first + count
      raise ArgumentError, 'HEX address outside nRF52840 flash/UICR' unless (first >= 0 && last <= 0x100000) || (first >= 0x10001000 && last <= 0x10002000)

      ranges << [first, last]
    when 1
      raise ArgumentError, 'Malformed EOF record' unless count.zero? && address.zero?

      eof = true
    when 2, 4
      raise ArgumentError, 'Malformed extended address record' unless count == 2 && address.zero?

      base = data.pack('C*').unpack1('n') << (type == 2 ? 4 : 16)
    when 3, 5
      raise ArgumentError, 'Malformed or repeated start address record' unless count == 4 && address.zero? && start.nil?

      start = if type == 3
                segment, offset = data.pack('C*').unpack('n2')
                (segment << 4) + offset
              else
                data.pack('C*').unpack1('N')
              end
      raise ArgumentError, 'Start address outside flash' unless start < 0x100000
    else
      raise ArgumentError, "Unsupported HEX record type #{type}"
    end
  end
  raise ArgumentError, 'HEX requires EOF and nonempty data' unless eof && !ranges.empty?

  ranges.sort_by!(&:first)
  raise ArgumentError, 'Overlapping HEX data records' if ranges.each_cons(2).any? { |left, right| left.last > right.first }

  { data_bytes: ranges.sum { |first, last| last - first }, ranges: ranges, start_address: start }
end