6f2d618b8d
The http table copies crosspoint-reader's signatures exactly -- get/head/delete/post/ patch returning (body|nil, status), download taking maxBytes/expectedSize/sha256, the same 50000 byte body cap and the same -1 for a request that never left the device -- so a script that talks to a server runs on either firmware. docs/lua-api-parity.md records that, and every other place the two APIs agree, differ for a reason, or differ because nobody noticed. Two crosspoint behaviours are deliberately not copied. It reinterprets a string in argument 2 of a GET as a request body, which turns a mistyped headers table into a silent protocol error. More seriously it calls setInsecure() on every request, so TLS is encrypted but unauthenticated on the very path a firmware update would use; this verifies against the root bundle already sitting in the framework, and the emulator confirms expired.badssl.com is refused while a wrong sha256 deletes the file. Downloading exposed two failures worth naming. A 2KB read buffer on the stack tripped the loop task's canary because a TLS handshake had already spent it, and the hand-rolled read loop spun forever on a stream that stopped producing -- HTTPClient's own writeToStream handles both, so the loop is gone and the loop task gets 16KB. scripts/gen_lua_stubs.py generates stubs/esp32lcd.lua in the same LuaLS format crosspoint uses, reading annotations off the luaL_Reg tables so a module's docs sit with its registration. make test runs --check, which crosspoint's copy never wired up.
161 lines
5.7 KiB
Python
161 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate the LuaLS stub for the firmware's Lua API.
|
|
|
|
Adapted from crosspoint-reader's scripts/gen_lua_stubs.py. The emitted format is
|
|
deliberately identical so an editor configured for one repo works for the other; the
|
|
parser differs because this firmware registers bindings as annotated luaL_Reg tables
|
|
rather than addFunction() calls, which keeps a module's documentation contiguous.
|
|
|
|
python3 scripts/gen_lua_stubs.py # write the stub
|
|
python3 scripts/gen_lua_stubs.py --check # exit 1 if it is stale
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
SOURCES = sorted((ROOT / "src/lua/bindings").glob("*.cpp"))
|
|
OUTPUT = ROOT / "stubs/esp32lcd.lua"
|
|
|
|
TYPES = {"int": "integer", "bool": "boolean"}
|
|
|
|
ENTRY = re.compile(r'^\s*\{"(?P<name>\w+)",\s*\w+\}')
|
|
TABLE = re.compile(r"static const luaL_Reg (?P<var>\w+)\[\]")
|
|
BIND = re.compile(r'luaL_newlib\(L,\s*(?P<var>\w+)\);\s*\n\s*lua_setglobal\(L,\s*"(?P<global>\w+)"\)')
|
|
DOC = re.compile(r"^\s*//\s*---\s?(?P<text>.*)$")
|
|
PARAM = re.compile(r"^\s*//\s*@param\s+(?P<name>\w+)\s+(?P<type>\S+)(?:\s+(?P<desc>.*))?$")
|
|
RETURN = re.compile(r"^\s*//\s*@return\s+(?P<type>\S+)(?:\s+(?P<desc>.*))?$")
|
|
TAG = re.compile(r"^\s*//\s*@(?P<tag>\w+)")
|
|
|
|
|
|
def lua_type(raw):
|
|
"""`int|nil` becomes `integer?`, matching how LuaLS spells optionality."""
|
|
optional = raw.endswith("|nil")
|
|
if optional:
|
|
raw = raw[: -len("|nil")]
|
|
mapped = TYPES.get(raw, raw)
|
|
return f"{mapped}?" if optional else mapped
|
|
|
|
|
|
def parse(path):
|
|
"""Collect {var: [function, ...]} and {var: global name} from one file."""
|
|
text = path.read_text()
|
|
tables, bindings = {}, {}
|
|
|
|
for match in BIND.finditer(text):
|
|
bindings[match.group("var")] = match.group("global")
|
|
|
|
lines = text.splitlines()
|
|
var, pending = None, {"doc": [], "params": [], "returns": []}
|
|
|
|
def reset():
|
|
return {"doc": [], "params": [], "returns": []}
|
|
|
|
for line in lines:
|
|
table = TABLE.search(line)
|
|
if table:
|
|
var = table.group("var")
|
|
tables.setdefault(var, [])
|
|
pending = reset()
|
|
continue
|
|
if var is None:
|
|
continue
|
|
|
|
doc = DOC.match(line)
|
|
if doc:
|
|
pending["doc"].append(doc.group("text").strip())
|
|
continue
|
|
param = PARAM.match(line)
|
|
if param:
|
|
pending["params"].append(
|
|
(param.group("name"), lua_type(param.group("type")), (param.group("desc") or "").strip())
|
|
)
|
|
continue
|
|
ret = RETURN.match(line)
|
|
if ret:
|
|
pending["returns"].append((lua_type(ret.group("type")), (ret.group("desc") or "").strip()))
|
|
continue
|
|
tag = TAG.match(line)
|
|
if tag:
|
|
raise SystemExit(f"{path.name}: unknown tag @{tag.group('tag')}")
|
|
|
|
entry = ENTRY.match(line)
|
|
if entry:
|
|
name = entry.group("name")
|
|
if not pending["doc"]:
|
|
raise SystemExit(f"{path.name}: {name} is registered without a description")
|
|
tables[var].append((name, pending))
|
|
pending = reset()
|
|
continue
|
|
if "};" in line:
|
|
var = None
|
|
|
|
return tables, bindings
|
|
|
|
|
|
def render(modules):
|
|
out = [
|
|
"---@meta",
|
|
"",
|
|
"-- Generated by scripts/gen_lua_stubs.py. Do not edit.",
|
|
"-- Point your editor's Lua language server at this file to get completion for the",
|
|
"-- firmware API inside sdcard/apps and sdcard/lib.",
|
|
"",
|
|
]
|
|
for global_name in sorted(modules):
|
|
functions = modules[global_name]
|
|
out.append(f"---@class {global_name}lib")
|
|
out.append(f"{global_name} = {{}}")
|
|
out.append("")
|
|
for name, meta in functions:
|
|
for line in meta["doc"]:
|
|
out.append(f"--- {line}" if line else "---")
|
|
for param, type_, desc in meta["params"]:
|
|
out.append(f"---@param {param} {type_}{(' ' + desc) if desc else ''}")
|
|
for type_, desc in meta["returns"]:
|
|
out.append(f"---@return {type_}{(' ' + desc) if desc else ''}")
|
|
args = ", ".join(param for param, _, _ in meta["params"])
|
|
out.append(f"function {global_name}.{name}({args}) end")
|
|
out.append("")
|
|
out.extend(
|
|
[
|
|
"-- Callbacks an app may define as globals:",
|
|
"-- setup() once, before the first draw",
|
|
"-- draw() every 33 ms while the app runs",
|
|
"-- on_tick() every TICK_MS, when that global is set",
|
|
"-- on_touch_down(x, y) finger down",
|
|
"-- on_touch_up(x, y) finger up",
|
|
"-- on_touch(x, y) tap, fired on release like a click",
|
|
"",
|
|
]
|
|
)
|
|
return "\n".join(out)
|
|
|
|
|
|
def main():
|
|
modules = {}
|
|
for path in SOURCES:
|
|
tables, bindings = parse(path)
|
|
for var, functions in tables.items():
|
|
if var not in bindings:
|
|
continue # a table that is never installed as a global documents nothing
|
|
modules.setdefault(bindings[var], []).extend(functions)
|
|
|
|
if not modules:
|
|
raise SystemExit("no annotated bindings found")
|
|
|
|
rendered = render(modules)
|
|
if "--check" in sys.argv:
|
|
current = OUTPUT.read_text() if OUTPUT.exists() else ""
|
|
if current != rendered:
|
|
raise SystemExit(f"{OUTPUT.relative_to(ROOT)} is stale; run scripts/gen_lua_stubs.py")
|
|
return
|
|
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
|
OUTPUT.write_text(rendered)
|
|
print(f"wrote {OUTPUT.relative_to(ROOT)}: {sum(len(f) for f in modules.values())} functions")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|