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

.authorsObject



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

public_class_method def self.authors
  'Meshtastic Ruby contributors'
end

.helpObject



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