Files
MeshCore/scripts/firmware_memory_manifest.py
T
mikecarper 64a068506b Merge PR #26: add M5Stack Cardputer-Adv support
Integrate mjgodat's keyboard/display Companion, USB Companion, Repeater and
Room Server support for Cardputer-Adv with Cap LoRa-1262.

Fix UI retry dispatch and ACK ownership, packet/storage bounds, numeric
input validation, sparse channel selection, optional-display shutdown,
keyboard layout limits, reserved GPIOs and Windows cross-drive includes.
Retain all 350 contacts and queue capacity using the existing contact cache;
account for the custom UI's runtime allocations in the RAM admission gate.
Carry the MultiMote and DejaVu notices with firmware packages and add CI
coverage for all four board roles plus the shared T-Deck Companion.

Validation: all four Cardputer roles and the T-Deck BLE Companion build;
Cardputer sanitizer, UI, layout, ownership, licensing and storage tests;
65 RAM-policy tests and 14 contact-cache tests. PlatformIO ran serially.

PR: https://github.com/mikecarper/MeshCore/pull/26
2026-10-27 22:25:35 -07:00

107 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Keep a passing RAM check attached to exactly the firmware it qualified."""
import argparse
import hashlib
import json
from pathlib import Path
import re
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent))
from package_nrf52_font_license import validate_artifact_notice
from package_meshtext_license import validate_artifact_notice as validate_meshtext_notice
from package_pocket_gui_license import validate_artifact_notice as validate_pocket_gui_notice
from package_cardputer_license import validate_artifact_notice as validate_cardputer_notice
def digest(path):
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def passing_report(path):
report = json.loads(Path(path).read_text())
fields = ("available_internal_bytes", "required_heap_bytes",
"largest_internal_region_bytes", "required_contiguous_bytes")
if not all(type(report.get(key)) is int and report[key] > 0 for key in fields):
raise ValueError("incomplete runtime RAM qualification")
if (report.get("schema_version") != 1 or report.get("passed") is not True
or report["available_internal_bytes"] < report["required_heap_bytes"]
or report["largest_internal_region_bytes"] < report["required_contiguous_bytes"]
or not re.fullmatch(r"[0-9a-f]{64}", str(report.get("elf_sha256", "")))):
raise ValueError("missing or failing runtime RAM qualification")
return report
def validate_build(directory):
directory = Path(directory)
report = passing_report(directory / "firmware.memory.json")
if report["elf_sha256"] != digest(directory / "firmware.elf"):
raise ValueError("runtime RAM report belongs to a different ELF")
return report
def artifact_files(stem):
stem = Path(stem)
candidates = [stem.parent / (stem.name + suffix)
for suffix in (".bin", "-merged.bin", ".uf2", ".zip", ".hex", ".capabilities.json",
".font-license.txt")]
candidates.append(stem.parent / (stem.name + ".meshtext-license.txt"))
candidates.append(stem.parent / (stem.name + ".pocket-gui-license.txt"))
candidates.append(stem.parent / (stem.name + ".cardputer-license.txt"))
return [p for p in candidates if p.is_file()]
def package_report(directory, stem):
stem = Path(stem)
report = validate_build(directory)
validate_artifact_notice(stem)
validate_meshtext_notice(stem)
validate_pocket_gui_notice(stem)
validate_cardputer_notice(stem)
files = artifact_files(stem)
if len(files) < 2 or not any(p.suffix in (".bin", ".uf2", ".zip", ".hex") for p in files):
raise ValueError("cannot qualify an empty firmware package")
manifest = json.loads((stem.parent / (stem.name + ".capabilities.json")).read_text())
report.update(target=manifest["target"], artifact_target=manifest["artifact_target"],
pio_environment=report["target"],
files={p.name: digest(p) for p in files})
path = stem.parent / (stem.name + ".memory.json")
path.write_text(json.dumps(report, indent=2) + "\n")
def validate_package(stem):
stem = Path(stem)
validate_artifact_notice(stem)
validate_meshtext_notice(stem)
validate_pocket_gui_notice(stem)
validate_cardputer_notice(stem)
report = passing_report(stem.parent / (stem.name + ".memory.json"))
actual = {p.name: digest(p) for p in artifact_files(stem)}
if len(actual) < 2 or actual != report.get("files"):
raise ValueError("firmware package changed after its runtime RAM check")
return report
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("action", choices=("validate-build", "package", "validate-package"))
parser.add_argument("path", type=Path)
parser.add_argument("--stem", type=Path)
args = parser.parse_args()
try:
if args.action == "validate-build":
validate_build(args.path)
elif args.action == "validate-package":
validate_package(args.path)
elif args.stem:
package_report(args.path, args.stem)
else:
parser.error("package requires --stem")
except (OSError, ValueError, KeyError) as error:
parser.exit(1, f"Runtime RAM qualification failed: {error}\n")
if __name__ == "__main__":
main()