Skip to content

Commit c448f60

Browse files
authored
Merge pull request #480 from gibix/feat/ota-pack
tools: sketch: build .ota update files
2 parents 66ac8bb + 1aeefdf commit c448f60

2 files changed

Lines changed: 248 additions & 0 deletions

File tree

tools/zephyr-sketch-tool/main.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,23 @@ func main() {
2020
var force = flag.Bool("force", false, "Ignore safety checks and overwrite the header")
2121
var add_header = flag.Bool("add_header", false, "Add space for the header to the file")
2222

23+
// OTA mode: produce .ota update files from a mangled sketch + loader.
24+
var ota = flag.Bool("ota", false, "OTA mode: produce .ota update files")
25+
var otaLoader = flag.String("ota-loader", "", "[ota] loader binary path")
26+
var otaSketch = flag.String("ota-sketch", "", "[ota] sketch binary path (the mangled -zsk.bin artifact)")
27+
var otaOffset = flag.String("ota-offset", "", "[ota] sketch offset in merged binary (hex)")
28+
var otaMagic = flag.String("ota-magic", "", "[ota] board magic number (hex, 32-bit)")
29+
2330
flag.Parse()
31+
32+
if *ota {
33+
if err := runOTA(*otaLoader, *otaSketch, *otaOffset, *otaMagic, *otaSketch); err != nil {
34+
fmt.Printf("OTA error: %v\n", err)
35+
os.Exit(1)
36+
}
37+
return
38+
}
39+
2440
if flag.NArg() != 1 {
2541
fmt.Printf("Usage: %s [flags] <filename>\n", os.Args[0])
2642
flag.PrintDefaults()

tools/zephyr-sketch-tool/ota.go

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
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

Comments
 (0)