rsdeck: dual-boot launcher, RNode mode, build pipeline

This commit is contained in:
DeFiDude committed 2026-06-10 18:38:49 -06:00
1 parent 16dedbfa3e
commit a2dee44b07
112 files changed
+20450 -3

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Check that launcher, Standalone, and RNode app images fit their OTA slots."""
from __future__ import annotations
import argparse
from pathlib import Path
def parse_int(value: str) -> int:
return int(value, 0)
def image_size(path: Path) -> int:
if not path.exists():
raise FileNotFoundError(path)
return path.stat().st_size
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--launcher", required=True, type=Path)
parser.add_argument("--launcher-slot-size", default="0x100000", type=parse_int)
parser.add_argument("--standalone", required=True, type=Path)
parser.add_argument("--standalone-slot-size", default="0x400000", type=parse_int)
parser.add_argument("--rnode", required=True, type=Path)
parser.add_argument("--rnode-slot-size", default="0x300000", type=parse_int)
args = parser.parse_args()
failed = False
checks = (
("Launcher", args.launcher, args.launcher_slot_size),
("Standalone", args.standalone, args.standalone_slot_size),
("RNode", args.rnode, args.rnode_slot_size),
)
for name, path, slot_size in checks:
size = image_size(path)
margin = slot_size - size
print(f"{name}: {size} bytes, slot {slot_size} bytes, margin {margin} bytes")
if margin < 0:
failed = True
if failed:
print("error: at least one image is too large for its OTA slot")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""Build a merged rsDeck image with launcher, Standalone, and RNode."""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
LAUNCHER_OFFSET = "0x10000"
STANDALONE_OFFSET = "0x110000"
RNODE_OFFSET = "0x510000"
def parse_int(value: str) -> int:
return int(value, 0)
def require_file(path: Path, label: str) -> None:
if not path.exists():
raise FileNotFoundError(f"{label} not found: {path}")
if not path.is_file():
raise FileNotFoundError(f"{label} is not a file: {path}")
def check_slot(path: Path, label: str, slot_size: int) -> None:
size = path.stat().st_size
if size > slot_size:
raise ValueError(f"{label} image is {size} bytes, exceeds slot size {slot_size}")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--bootloader", required=True, type=Path)
parser.add_argument("--partitions", required=True, type=Path)
parser.add_argument("--boot-app0", required=True, type=Path)
parser.add_argument("--launcher", required=True, type=Path)
parser.add_argument("--standalone", required=True, type=Path)
parser.add_argument("--rnode", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--launcher-slot-size", default="0x100000", type=parse_int)
parser.add_argument("--standalone-slot-size", default="0x400000", type=parse_int)
parser.add_argument("--rnode-slot-size", default="0x300000", type=parse_int)
args = parser.parse_args()
for label, path in (
("bootloader", args.bootloader),
("partition table", args.partitions),
("boot_app0", args.boot_app0),
("launcher app", args.launcher),
("Standalone app", args.standalone),
("RNode app", args.rnode),
):
require_file(path, label)
check_slot(args.launcher, "Launcher", args.launcher_slot_size)
check_slot(args.standalone, "Standalone", args.standalone_slot_size)
check_slot(args.rnode, "RNode", args.rnode_slot_size)
args.output.parent.mkdir(parents=True, exist_ok=True)
cmd = [
sys.executable,
"-m",
"esptool",
"--chip",
"esp32s3",
"merge-bin",
"--flash-mode",
"qio",
"--flash-size",
"16MB",
"--output",
str(args.output),
"0x0000",
str(args.bootloader),
"0x8000",
str(args.partitions),
"0xe000",
str(args.boot_app0),
LAUNCHER_OFFSET,
str(args.launcher),
STANDALONE_OFFSET,
str(args.standalone),
RNODE_OFFSET,
str(args.rnode),
]
subprocess.run(cmd, check=True)
print(f"dual firmware image written to {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Package a merged ESP32-S3 image with a web-flasher manifest."""
from __future__ import annotations
import argparse
import json
import zipfile
from pathlib import Path
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--image", required=True, type=Path)
parser.add_argument("--name", required=True)
parser.add_argument("--output", required=True, type=Path)
args = parser.parse_args()
if not args.image.is_file():
raise FileNotFoundError(args.image)
image_name = f"{args.name}.bin"
manifest = {
"chipFamily": "ESP32-S3",
"flashSize": "16MB",
"flashMode": "qio",
"flashFreq": "80m",
"parts": [
{"path": image_name, "offset": "0x0000"},
],
}
args.output.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(args.output, "w", compression=zipfile.ZIP_DEFLATED) as zf:
zf.write(args.image, image_name)
zf.writestr("manifest.json", json.dumps(manifest, indent=2) + "\n")
print(f"firmware package written to {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())