Files
Kaj SchittecatandClaude Opus 5.5 d3a9cf1453 touch: radio tools redesign, Music app, Store update badge, triage fixes
Spectrum: stat tiles (peak, floor, our channel, busy), plot and waterfall drawn
at screen size (the lv_chart and zoomed waterfall were the page's render cost),
dark-to-yellow waterfall, peak hold, our-channel marker, Band/Channel and
Live/1 min/10 min chips. RF Monitor 1.4 (tiles, filled chart, typed feed, packet
detail card; the old type table did not match MeshCore payload types) and
Airtime 1.6 (big figure, tiles, duty line, rx/tx split).

Lua SDK: ui.panel, bold labels and a size-24 figure font, colors.warn/border,
app.on_audio (fires with the screen off), read-only wada.sd.list and sd: audio on
the SD_MMC boards (P4, Tanmatsu, Wio L2), caps panel/audio_events/epaper, a
"music" icon and an "audio" Store requirement. button:set() no longer writes
label fields into the button object (heap corruption, every earlier firmware).
The SDK page's input table described events the firmware never sent.

Music 1.0 (Store app): plays MP3/WAV from the card's Music folder, shuffle,
repeat, keeps going through the list with the screen off. Verified on the
T-Display P4 end to end.

Store: the update check also fetches the app catalog and badges the Store tile
with the number of installed apps that have a newer version. Baked Lua apps are
raw-deflated and inflated by the S3 ROM (14.6 KB back on the V4); the V4 language
table keeps only texts of apps it can run.

Fixes: the SD-card notice no longer fires when the card mounts fine (#667); the
Control Center clock keeps time while open (#666); M9 keyboard brightness (#664);
the battery widget's number no longer sits on a ring too small for it (Discord);
M9 shortcuts doc follows #669's composer order. New texts in all 14 languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:07:11 +02:00

174 lines
7.7 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Bake the catalog's Lua apps into the firmware as seeded built-ins.
The Lua host already supports this: luaAppLaunchFile(id, title, embedded, len)
tries <data>/apps/<id>.lua first and falls back to an embedded source when the
file is not there. That file-first order is what makes a seeded app updatable —
download a newer version from the Store and the file simply wins.
This generates src/ui-touch/lua_builtin.h from deploy/apps/, i.e. exactly the
sources the Store serves, so a board that cannot reach the Store still ships
with the apps rather than an empty drawer.
It used to read out/firmware/apps/ instead. Nothing writes that directory --
deploy-apps.sh rsyncs deploy/apps/ straight to the VPS -- so it was a stale
mirror, and the "same sources the Store serves" guarantee was false: a bumped
app shipped to the Store while the baked-in copy stayed on whatever version was
mirrored last (#257).
Run from the repo root: python3 scripts/build/gen-lua-builtin.py
"""
import json, os, re, sys, zlib
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
APPS = os.path.join(ROOT, 'deploy/apps') # canonical; what deploy-apps.sh publishes
OUT = os.path.join(ROOT, 'src/ui-touch/lua_builtin.h')
DELIM = 'WADALUA' # raw-string delimiter: keeps Lua source verbatim
_LONG_OPEN = re.compile(r'\[(=*)\[')
def strip_lua(code):
"""Drop comments and indentation from a Lua chunk before it is baked in.
The baked copy is flash on the boards with the least of it, and the Store's
copy keeps every comment, so only the image loses them. Every newline stays,
so a line number in an app's error message still points at the Store source.
Strings and long strings are copied as they are; a comment turns into the
whitespace the Lua lexer would have seen. The result compiles to the same
bytecode, line info included (checked when this was written).
"""
out = []
i, n = 0, len(code)
line_start = True
while i < n:
c = code[i]
if c == '\n':
while out and out[-1] in (' ', '\t'):
out.pop() # trailing blanks
out.append('\n')
i += 1
line_start = True
continue
if line_start and c in ' \t':
i += 1 # indentation
continue
line_start = False
if code.startswith('--', i):
m = _LONG_OPEN.match(code, i + 2)
if m: # --[[ long comment ]]
close = ']' + m.group(1) + ']'
j = code.find(close, m.end())
if j < 0:
raise ValueError('unterminated long comment')
nl = code.count('\n', i, j)
if nl:
while out and out[-1] in (' ', '\t'):
out.pop()
out.append('\n' * nl if nl else ' ')
line_start = nl > 0
i = j + len(close)
continue
j = code.find('\n', i) # -- line comment
i = n if j < 0 else j
continue
if c in '"\'': # short string, escapes included
j = i + 1
while j < n and code[j] != c:
j += 2 if code[j] == '\\' else 1
out.append(code[i:j + 1])
i = j + 1
continue
if c == '[':
m = _LONG_OPEN.match(code, i)
if m: # [[ long string ]]
close = ']' + m.group(1) + ']'
j = code.find(close, m.end())
if j < 0:
raise ValueError('unterminated long string')
out.append(code[i:j + len(close)])
i = j + len(close)
continue
out.append(c)
i += 1
return ''.join(out)
def main():
cat_path = os.path.join(APPS, 'apps.json')
if not os.path.exists(cat_path):
sys.exit('no catalog at ' + cat_path)
cat = json.load(open(cat_path, encoding='utf-8'))['apps']
entries = []
for app in cat:
aid, ver, name = app['id'], app['ver'], app['name']
# "seed": false keeps an app out of the image while still publishing it to
# the Store. Seeding is not free: it is flash on the boards that have the
# least of it, which are exactly the CAP_BUILTIN_LUA_APPS boards. An app
# that is large, or that needs hardware those boards do not have, earns its
# place rather than getting it by being in the catalog.
if app.get('seed') is False:
print(' skip %s: seed=false in apps.json' % aid)
continue
src = os.path.join(APPS, aid, ver, aid + '.lua')
if not os.path.exists(src):
print(' skip %s: no %s' % (aid, os.path.relpath(src, ROOT)))
continue
code = strip_lua(open(src, encoding='utf-8').read())
if (')' + DELIM + '"') in code:
sys.exit('%s contains the raw-string delimiter' % aid)
# An app that needs the extended SDK cannot run on a board without it, so it
# is baked in only where it can (CAP_LUA_SDK_EXT, every board but the 2 MB
# Heltec V4, the one with the least flash to give).
entries.append((aid, name, ver, code, app.get('requires') == 'sdk_ext'))
if not entries:
sys.exit('no app sources found under ' + APPS)
# Stored as raw deflate, decompressed on launch by the ESP32-S3 ROM's tinfl
# (the only board that bakes apps in is the 2 MB V4, an S3). Plain text cost
# the V4 ~14 KB of flash more than this, which is the room it needed.
w = ['// Generated by scripts/build/gen-lua-builtin.py — DO NOT EDIT.',
'// Source: deploy/apps/ (the same .lua the Lua Store serves), comments and',
'// indentation stripped, then raw-deflated. luaBuiltinSrc() inflates one on launch.',
'// Seeded built-ins: a downloaded <data>/apps/<id>.lua always wins.',
'#pragma once', '#include <stdint.h>', '',
'struct LuaBuiltinApp { const char* id; const char* name; const char* ver;',
' const uint8_t* z; uint32_t zlen; uint32_t len; };', '']
zs = {}
for aid, name, ver, code, ext in entries:
raw = code.encode('utf-8')
c = zlib.compressobj(9, zlib.DEFLATED, -15)
z = c.compress(raw) + c.flush()
zs[aid] = (z, len(raw))
if ext: w.append('#if CAP_LUA_SDK_EXT')
w.append('static const uint8_t kLuaZ_%s[%d] = { // %d B of Lua' % (aid, len(z), len(raw)))
for i in range(0, len(z), 20):
w.append(' ' + ','.join('0x%02x' % b for b in z[i:i + 20]) + ',')
w.append('};')
if ext: w.append('#endif')
w.append('')
w.append('static const LuaBuiltinApp kLuaBuiltin[] = {')
for aid, name, ver, _, ext in entries:
if ext: w.append('#if CAP_LUA_SDK_EXT')
z, n = zs[aid]
w.append(' { "%s", "%s", "%s", kLuaZ_%s, %d, %d },' % (aid, name, ver, aid, len(z), n))
if ext: w.append('#endif')
w.append('};')
w.append('static const int kLuaBuiltinCount = (int)(sizeof(kLuaBuiltin)/sizeof(kLuaBuiltin[0]));')
w.append('')
open(OUT, 'w', encoding='utf-8').write('\n'.join(w))
total = sum(len(e[3]) for e in entries)
print('%s: %d apps, %d bytes of Lua, %d deflated' %
(os.path.relpath(OUT, ROOT), len(entries), total, sum(len(z) for z, _ in zs.values())))
for aid, _, ver, code, ext in entries:
print(' %-10s v%-4s %6d B -> %5d B%s' % (aid, ver, len(code), len(zs[aid][0]),
' (extended SDK only)' if ext else ''))
if __name__ == '__main__':
main()