mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-29 09:13:11 +00:00
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
164 lines
6.2 KiB
Python
164 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Carry Cardputer UI and font notices beside firmware containing that code."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
|
|
from firmware_elf import FirmwareElf
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
UI_NOTICE_PATH = ROOT / "variants/m5stack_cardputer_adv/LICENSE.MultiMote"
|
|
FONT_NOTICE_PATH = ROOT / "variants/m5stack_cardputer_adv/LICENSE.DejaVu"
|
|
SIDECAR_SUFFIX = ".cardputer-license.txt"
|
|
COMPONENT = "cardputer-ui-and-fonts"
|
|
LICENSE = "MIT AND LicenseRef-DejaVu"
|
|
UI_LABEL = b"--- LICENSE.MultiMote ---\n\n"
|
|
FONT_LABEL = b"--- LICENSE.DejaVu ---\n\n"
|
|
|
|
|
|
def valid_ui_notice(data):
|
|
return (0 < len(data) <= 262144 and all(text in data for text in (
|
|
b"Copyright (c) 2026 MultiMote", b"Permission is hereby granted, free of charge",
|
|
b"copies or substantial portions of the Software", b'THE SOFTWARE IS PROVIDED "AS IS"',
|
|
b"LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE",
|
|
b"SOFTWARE.")))
|
|
|
|
|
|
def valid_font_notice(data):
|
|
return (0 < len(data) <= 262144 and all(text in data for text in (
|
|
b"DejaVu changes are in public domain.",
|
|
b"Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.",
|
|
b"The above copyright and trademark notices and this permission notice shall",
|
|
b'are renamed to names not containing either the words "Bitstream" or the word',
|
|
b"THE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND",
|
|
b"Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.",
|
|
b"name of Tavmjong Bah shall not",
|
|
b"AMSFonts (v. 2.2) copyright")))
|
|
|
|
|
|
def valid_notice(data):
|
|
if not (0 < len(data) <= 524288 and data.count(UI_LABEL) == 1
|
|
and data.count(FONT_LABEL) == 1):
|
|
return False
|
|
ui_start = data.index(UI_LABEL) + len(UI_LABEL)
|
|
font_start = data.index(FONT_LABEL)
|
|
return (ui_start < font_start and valid_ui_notice(data[ui_start:font_start])
|
|
and valid_font_notice(data[font_start + len(FONT_LABEL):]))
|
|
|
|
|
|
def notice_bytes():
|
|
ui, fonts = UI_NOTICE_PATH.read_bytes(), FONT_NOTICE_PATH.read_bytes()
|
|
if not valid_ui_notice(ui) or not valid_font_notice(fonts):
|
|
raise ValueError("Cardputer UI/font source notice is incomplete")
|
|
return (b"Cardputer Adv UI and font notices\n\n"
|
|
b"Source: MultiMote/meshcore-cardputer-adv\n\n"
|
|
+ UI_LABEL + ui.rstrip() + b"\n\n" + FONT_LABEL + fonts.rstrip() + b"\n")
|
|
|
|
|
|
def cardputer_evidence(path):
|
|
"""Only allocated definitions qualify, never target names or debug strings."""
|
|
if Path(path).read_bytes()[:4] != b"\x7fELF":
|
|
return []
|
|
elf = FirmwareElf(path)
|
|
evidence = []
|
|
for name, (_address, size, index) in elf.symbols.items():
|
|
if size <= 0 or index >= len(elf.sections):
|
|
continue
|
|
section = elf.sections[index]
|
|
if section[1] == 8 or not section[2] & 2:
|
|
continue
|
|
ui = (section[2] & 4 and name.startswith((
|
|
"_ZN15CardputerUITask", "_ZNK15CardputerUITask")))
|
|
font = re.fullmatch(r"(?:_ZL[0-9]+)?DejaVuSans_[A-Za-z0-9_]+(?:\..+)?", name)
|
|
if ui or font:
|
|
evidence.append(name)
|
|
return sorted(evidence)
|
|
|
|
|
|
def sidecar(stem):
|
|
stem = Path(stem)
|
|
return stem.parent / (stem.name + SIDECAR_SUFFIX)
|
|
|
|
|
|
def notice_record(stem, data):
|
|
return {"component": COMPONENT, "license": LICENSE,
|
|
"file": sidecar(stem).name, "sha256": hashlib.sha256(data).hexdigest()}
|
|
|
|
|
|
def manifest_record(stem):
|
|
stem = Path(stem)
|
|
manifest = json.loads((stem.parent / (stem.name + ".capabilities.json")).read_text())
|
|
records = [record for record in manifest.get("third_party_notices", [])
|
|
if record.get("component") == COMPONENT]
|
|
if len(records) > 1:
|
|
raise ValueError(f"{stem}: duplicate Cardputer UI/font notice declarations")
|
|
if not records:
|
|
return None
|
|
record = records[0]
|
|
if (record.get("file") != sidecar(stem).name or record.get("license") != LICENSE
|
|
or not re.fullmatch(r"[0-9a-f]{64}", str(record.get("sha256", "")))):
|
|
raise ValueError(f"{stem}: invalid Cardputer UI/font notice declaration")
|
|
return record
|
|
|
|
|
|
def validate_artifact_notice(stem):
|
|
record = manifest_record(stem)
|
|
output = sidecar(stem)
|
|
if record is None:
|
|
if output.exists():
|
|
raise ValueError(f"{output}: stale notice for unaffected firmware")
|
|
return None
|
|
if not output.is_file():
|
|
raise ValueError(f"{output}: missing Cardputer UI/font notice")
|
|
data = output.read_bytes()
|
|
if not valid_notice(data) or hashlib.sha256(data).hexdigest() != record["sha256"]:
|
|
raise ValueError(f"{output}: incomplete or mismatched Cardputer UI/font notice")
|
|
return output
|
|
|
|
|
|
def collect_build_notice(directory, stem):
|
|
evidence = cardputer_evidence(Path(directory) / "firmware.elf")
|
|
record = manifest_record(stem)
|
|
if bool(evidence) != bool(record):
|
|
raise ValueError("Cardputer UI/font notice declaration does not match the linked ELF")
|
|
if not evidence:
|
|
return validate_artifact_notice(stem)
|
|
data = notice_bytes()
|
|
if record != notice_record(stem, data):
|
|
raise ValueError("Cardputer UI/font notice declaration does not match the source notice")
|
|
output = sidecar(stem)
|
|
if not output.exists():
|
|
temporary = output.with_name(f".{output.name}.{os.getpid()}.tmp")
|
|
try:
|
|
temporary.write_bytes(data)
|
|
temporary.replace(output)
|
|
finally:
|
|
temporary.unlink(missing_ok=True)
|
|
return validate_artifact_notice(stem)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("action", choices=("collect", "validate"))
|
|
parser.add_argument("path", type=Path)
|
|
parser.add_argument("--stem", type=Path)
|
|
args = parser.parse_args()
|
|
try:
|
|
if args.action == "collect":
|
|
if args.stem is None:
|
|
parser.error("collect requires --stem")
|
|
collect_build_notice(args.path, args.stem)
|
|
else:
|
|
validate_artifact_notice(args.path)
|
|
except (OSError, ValueError, KeyError, TypeError) as error:
|
|
parser.exit(1, f"Cardputer UI/font notice qualification failed: {error}\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|