#!/usr/bin/env python3 # Shared builder for overlay-app read-only assets (served by api->asset_read). # # Each app has a gen_assets.py that declares its texts and data with this # module, then calls Assets.main(): it writes the asset blob packed into the # .app by pack_app.py, and a C header of offsets the app includes. The header # is generated before compiling, so code and data can never drift apart. # # a = Assets("SNAKE") # a.text("T_PAUSE", "PAUSE") # NUL-terminated string # a.table("T_STATE", ["TX", "RX"]) # fixed-stride strings (+ _STRIDE) # a.u16("SPR_HEAD", [0x6566, 0x0FD2]) # packed little-endian data # a.main() # argv: # # Texts come first, so their offsets stay small (cheap immediates in Thumb # code). TEXT_MAX is the longest text including its NUL: the size of the # app's shared text buffer, read in one asset_read by T(). import os, re, struct, sys HERE = os.path.dirname(os.path.abspath(__file__)) def _cdefine(header, name): pat = re.compile(r"^\s*#define\s+" + re.escape(name) + r"\s+(0[xX][0-9a-fA-F]+|\d+)[uUlL]*\b") with open(os.path.join(HERE, header)) as f: for line in f: m = pat.match(line) if m: return int(m.group(1), 0) sys.exit(f"{header}: #define {name} not found") ASSET_MAX = _cdefine("app_overlay.h", "APP_ASSET_MAX") class Assets: def __init__(self, guard): self.guard = guard self.texts = [] # (name, bytes) self.data = [] # (name, bytes) self.extra = [] # (name, value) plain constants self.names = set() def _name(self, name): if name in self.names or not re.fullmatch(r"[A-Z][A-Z0-9_]*", name): sys.exit(f"asset name {name!r} is invalid or duplicated") self.names.add(name) def text(self, name, s): self._name(name) self.texts.append((name, s.encode("ascii") + b"\x00")) def table(self, name, strings, stride=None): """Fixed-stride string table: entry i is at name + i * name_STRIDE.""" self._name(name) width = stride or max(len(s) for s in strings) + 1 if any(len(s) + 1 > width for s in strings): sys.exit(f"{name}: an entry does not fit the {width} B stride") blob = b"".join(s.encode("ascii").ljust(width, b"\x00") for s in strings) self.texts.append((name, blob)) self.const(name + "_STRIDE", width) def const(self, name, value): self._name(name) self.extra.append((name, value)) def raw(self, name, payload): self._name(name) self.data.append((name, bytes(payload))) def u8(self, name, values): self.raw(name, struct.pack(f"<{len(values)}B", *values)) def i8(self, name, values): self.raw(name, struct.pack(f"<{len(values)}b", *values)) def u16(self, name, values): self.raw(name, struct.pack(f"<{len(values)}H", *values)) def i16(self, name, values): self.raw(name, struct.pack(f"<{len(values)}h", *values)) def u32(self, name, values): self.raw(name, struct.pack(f"<{len(values)}I", *values)) def build(self): blob = bytearray() lines = [] text_max = 1 strides = dict(self.extra) for name, payload in self.texts: lines.append((name, len(blob), len(payload))) text_max = max(text_max, strides.get(name + "_STRIDE", len(payload))) blob += payload for name, payload in self.data: # asset_read copies bytes: no alignment needed lines.append((name, len(blob), len(payload))) blob += payload if len(blob) > ASSET_MAX: sys.exit(f"assets {len(blob)} B exceed the {ASSET_MAX} B asset area") return bytes(blob), lines, text_max def main(self): if len(sys.argv) != 3: sys.exit("usage: gen_assets.py ") blob, lines, text_max = self.build() with open(sys.argv[1], "wb") as f: f.write(blob) guard = self.guard.upper() + "_ASSETS_H" out = [f"/* Generated by gen_assets.py - do not edit ({len(blob)} B of assets). */", f"#ifndef {guard}", f"#define {guard}", "", f"#define TEXT_MAX {text_max}u /* longest text incl. NUL */"] for name, off, size in lines: out.append(f"#define {name} {off}u") out.append(f"#define {name}_LEN {size}u") for name, value in self.extra: out.append(f"#define {name} {value}u") out += ["", f"#endif /* {guard} */", ""] with open(sys.argv[2], "w") as f: f.write("\n".join(out))