Files
uv-k1-k5v3-firmware-custom/App/apps/app_assets.py
T

108 lines
4.6 KiB
Python
Executable File

#!/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: <out.bin> <out.h>
#
# 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 <out.bin> <out.h>")
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))