|
| 1 | +// Copyright (c) Arduino s.r.l. and/or its affiliated companies |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +// OTA-mode file generation. A single runOTA() call produces four artifacts |
| 5 | +// derived from the same inputs, sharing the OTA header |
| 6 | +// (len + crc32 + magic + version + payload): |
| 7 | +// |
| 8 | +// <output> .ota — sketch only, uncompressed |
| 9 | +// <output> .lzss.ota — sketch only, LZSS-compressed |
| 10 | +// <output> -bundle.ota — loader + 0xFF padding + sketch, uncompressed |
| 11 | +// <output> -bundle.lzss.ota — loader + 0xFF padding + sketch, LZSS-compressed |
| 12 | + |
| 13 | +package main |
| 14 | + |
| 15 | +import ( |
| 16 | + "encoding/binary" |
| 17 | + "fmt" |
| 18 | + "hash/crc32" |
| 19 | + "os" |
| 20 | + "strconv" |
| 21 | + "strings" |
| 22 | +) |
| 23 | + |
| 24 | +// LZSS parameters matching the Arduino OTA decoder |
| 25 | +const ( |
| 26 | + lzssEI = 11 |
| 27 | + lzssEJ = 4 |
| 28 | + lzssN = 1 << lzssEI // ring buffer size = 2048 |
| 29 | + lzssF = (1 << lzssEJ) + 1 // max match length = 17 |
| 30 | + lzssMask = lzssN - 1 |
| 31 | +) |
| 32 | + |
| 33 | +func runOTA(loaderPath, sketchPath, offsetStr, magicStr, outputBase string) error { |
| 34 | + // The hook runs unconditionally for every board, but only OTA-enabled |
| 35 | + // boards set build.ota.magic. An empty magic means "not configured" — |
| 36 | + // silently skip so the uniform recipe is safe for non-OTA boards. |
| 37 | + if magicStr == "" { |
| 38 | + return nil |
| 39 | + } |
| 40 | + if loaderPath == "" || sketchPath == "" || offsetStr == "" { |
| 41 | + return fmt.Errorf("ota: -ota-loader, -ota-sketch and -ota-offset are all required when -ota-magic is set") |
| 42 | + } |
| 43 | + |
| 44 | + magic, err := parseHex(magicStr) |
| 45 | + if err != nil { |
| 46 | + return fmt.Errorf("parse magic: %w", err) |
| 47 | + } |
| 48 | + |
| 49 | + offset, err := parseHex(offsetStr) |
| 50 | + if err != nil { |
| 51 | + return fmt.Errorf("parse offset: %w", err) |
| 52 | + } |
| 53 | + |
| 54 | + loader, err := os.ReadFile(loaderPath) |
| 55 | + if err != nil { |
| 56 | + return fmt.Errorf("read loader: %w", err) |
| 57 | + } |
| 58 | + |
| 59 | + sketch, err := os.ReadFile(sketchPath) |
| 60 | + if err != nil { |
| 61 | + return fmt.Errorf("read sketch: %w", err) |
| 62 | + } |
| 63 | + |
| 64 | + if int64(len(loader)) > offset { |
| 65 | + return fmt.Errorf("loader (%d bytes) exceeds offset (0x%X)", len(loader), offset) |
| 66 | + } |
| 67 | + |
| 68 | + merged := append(padFF(loader, int(offset)), sketch...) |
| 69 | + |
| 70 | + // Tolerate callers passing either "<base>" or "<base>.ota" — strip the |
| 71 | + // trailing ".ota" so the four derivatives don't end up as "<base>.ota.ota". |
| 72 | + base := strings.TrimSuffix(outputBase, ".ota") |
| 73 | + |
| 74 | + fmt.Printf("Inputs: loader %d bytes, sketch %d bytes, merged %d bytes\n", |
| 75 | + len(loader), len(sketch), len(merged)) |
| 76 | + |
| 77 | + variants := []struct { |
| 78 | + suffix string |
| 79 | + payload []byte |
| 80 | + compress bool |
| 81 | + label string |
| 82 | + }{ |
| 83 | + {".ota", sketch, false, "sketch"}, |
| 84 | + {".lzss.ota", sketch, true, "sketch (LZSS)"}, |
| 85 | + {"-bundle.ota", merged, false, "loader+sketch"}, |
| 86 | + {"-bundle.lzss.ota", merged, true, "loader+sketch (LZSS)"}, |
| 87 | + } |
| 88 | + |
| 89 | + for _, v := range variants { |
| 90 | + path := base + v.suffix |
| 91 | + out := buildOTA(uint32(magic), v.payload, v.compress) |
| 92 | + if err := os.WriteFile(path, out, 0644); err != nil { |
| 93 | + return fmt.Errorf("write %s: %w", path, err) |
| 94 | + } |
| 95 | + fmt.Printf("OTA: %s (%d bytes, %s)\n", path, len(out), v.label) |
| 96 | + } |
| 97 | + return nil |
| 98 | +} |
| 99 | + |
| 100 | +// buildOTA wraps the payload (optionally LZSS-encoded) in the OTA file |
| 101 | +// envelope: len(4) | crc32(4) | magic(4) | version(8) | payload. |
| 102 | +func buildOTA(magic uint32, raw []byte, compress bool) []byte { |
| 103 | + payload := raw |
| 104 | + if compress { |
| 105 | + payload = lzssEncode(raw) |
| 106 | + } |
| 107 | + |
| 108 | + var version [8]byte |
| 109 | + version[0] = 1 |
| 110 | + if compress { |
| 111 | + version[0] |= 0x40 |
| 112 | + } |
| 113 | + |
| 114 | + body := make([]byte, 0, 4+8+len(payload)) |
| 115 | + body = binary.LittleEndian.AppendUint32(body, magic) |
| 116 | + body = append(body, version[:]...) |
| 117 | + body = append(body, payload...) |
| 118 | + |
| 119 | + crcVal := crc32.ChecksumIEEE(body) |
| 120 | + |
| 121 | + out := make([]byte, 0, 8+len(body)) |
| 122 | + out = binary.LittleEndian.AppendUint32(out, uint32(len(body))) |
| 123 | + out = binary.LittleEndian.AppendUint32(out, crcVal) |
| 124 | + out = append(out, body...) |
| 125 | + return out |
| 126 | +} |
| 127 | + |
| 128 | +// padFF pads data with 0xFF up to size. |
| 129 | +func padFF(data []byte, size int) []byte { |
| 130 | + buf := make([]byte, size) |
| 131 | + copy(buf, data) |
| 132 | + for i := len(data); i < size; i++ { |
| 133 | + buf[i] = 0xFF |
| 134 | + } |
| 135 | + return buf |
| 136 | +} |
| 137 | + |
| 138 | +// --- LZSS Encoder --- |
| 139 | + |
| 140 | +type bitWriter struct { |
| 141 | + data []byte |
| 142 | + accum uint32 |
| 143 | + nbits int |
| 144 | +} |
| 145 | + |
| 146 | +func (w *bitWriter) write(value, bits int) { |
| 147 | + w.accum = (w.accum << uint(bits)) | uint32(value&((1<<uint(bits))-1)) |
| 148 | + w.nbits += bits |
| 149 | + for w.nbits >= 8 { |
| 150 | + w.nbits -= 8 |
| 151 | + w.data = append(w.data, byte(w.accum>>uint(w.nbits))) |
| 152 | + w.accum &= (1 << uint(w.nbits)) - 1 |
| 153 | + } |
| 154 | +} |
| 155 | + |
| 156 | +func (w *bitWriter) flush() { |
| 157 | + if w.nbits > 0 { |
| 158 | + w.data = append(w.data, byte(w.accum<<uint(8-w.nbits))) |
| 159 | + w.nbits = 0 |
| 160 | + w.accum = 0 |
| 161 | + } |
| 162 | +} |
| 163 | + |
| 164 | +func lzssEncode(input []byte) []byte { |
| 165 | + var ring [lzssN]byte |
| 166 | + for i := range ring { |
| 167 | + ring[i] = ' ' |
| 168 | + } |
| 169 | + |
| 170 | + w := &bitWriter{} |
| 171 | + r := lzssN - lzssF |
| 172 | + pos := 0 |
| 173 | + |
| 174 | + for pos < len(input) { |
| 175 | + bestLen := 1 |
| 176 | + bestPos := 0 |
| 177 | + |
| 178 | + maxLen := lzssF |
| 179 | + if rem := len(input) - pos; rem < maxLen { |
| 180 | + maxLen = rem |
| 181 | + } |
| 182 | + |
| 183 | + for i := 0; i < lzssN; i++ { |
| 184 | + if ring[i] != input[pos] { |
| 185 | + continue |
| 186 | + } |
| 187 | + safeDist := (r - i) & lzssMask |
| 188 | + if safeDist == 0 { |
| 189 | + safeDist = lzssN |
| 190 | + } |
| 191 | + safeLen := maxLen |
| 192 | + if safeDist < safeLen { |
| 193 | + safeLen = safeDist |
| 194 | + } |
| 195 | + matchLen := 1 |
| 196 | + for matchLen < safeLen && ring[(i+matchLen)&lzssMask] == input[pos+matchLen] { |
| 197 | + matchLen++ |
| 198 | + } |
| 199 | + if matchLen > bestLen { |
| 200 | + bestLen = matchLen |
| 201 | + bestPos = i |
| 202 | + } |
| 203 | + } |
| 204 | + |
| 205 | + if bestLen >= 2 { |
| 206 | + w.write(0, 1) |
| 207 | + w.write(bestPos, lzssEI) |
| 208 | + w.write(bestLen-2, lzssEJ) |
| 209 | + } else { |
| 210 | + bestLen = 1 |
| 211 | + w.write(1, 1) |
| 212 | + w.write(int(input[pos]), 8) |
| 213 | + } |
| 214 | + |
| 215 | + for k := 0; k < bestLen; k++ { |
| 216 | + ring[r] = input[pos+k] |
| 217 | + r = (r + 1) & lzssMask |
| 218 | + } |
| 219 | + pos += bestLen |
| 220 | + } |
| 221 | + |
| 222 | + w.flush() |
| 223 | + return w.data |
| 224 | +} |
| 225 | + |
| 226 | +func parseHex(s string) (int64, error) { |
| 227 | + s = strings.TrimSpace(s) |
| 228 | + if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") { |
| 229 | + return strconv.ParseInt(s[2:], 16, 64) |
| 230 | + } |
| 231 | + return strconv.ParseInt(s, 0, 64) |
| 232 | +} |
0 commit comments