Add Meshtext Companion channel chat and automatic radio2 setup

This commit is contained in:
mikecarper committed 2026-10-11 02:08:02 -07:00
1 parent 1f1a499482
commit 05907f49e0
52 files changed
+7700 -128

No files matched your search

+18
View File
@@ -11,6 +11,9 @@ import sys
import struct
import zipfile
import zlib
from package_meshtext_license import (CAPABILITY as MESHTEXT_CAPABILITY,
meshtext_evidence, notice_bytes as meshtext_notice_bytes,
notice_record as meshtext_notice_record)
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools" / "mota"))
from motalib import parse_nrf52_layout
@@ -186,6 +189,19 @@ def main() -> int:
checks = []
malformed = False
third_party_notices = []
try:
codec_symbols, chat_symbols = meshtext_evidence(args.image)
if codec_symbols:
stem = args.output.name.removesuffix(".capabilities.json")
third_party_notices.append(meshtext_notice_record(stem, meshtext_notice_bytes()))
if chat_symbols:
checks.append({"capability": MESHTEXT_CAPABILITY,
"evidence": "; ".join(chat_symbols), "present": True,
"source": "linked ELF symbols"})
except (OSError, ValueError) as error:
print(f"capability check: cannot qualify linked Meshtext code: {error}", file=sys.stderr)
malformed = True
application = b""
if args.expect_application:
try:
@@ -245,6 +261,8 @@ def main() -> int:
"ota_update_evidence": ota_evidence,
"verified": not malformed and not missing and (not args.require_ota or ota_verified),
}
if third_party_notices:
manifest["third_party_notices"] = third_party_notices
if ota_verified and args.platform == "NRF52_PLATFORM" and "lora" in ota_methods:
with zipfile.ZipFile(args.dfu_package) as package:
app = json.loads(package.read("manifest.json"))["manifest"]["application"]
+2 -1
View File
@@ -23,7 +23,8 @@ RECIPE_ENVIRONMENT = (
"MESHCORE_NRF52_LEGACY_FLASH_OTA", "MESHCORE_REDUCED_TLS",
)
PACKAGE_SUFFIXES = (".bin", "-merged.bin", ".uf2", ".zip", ".hex",
".capabilities.json", ".memory.json", ".font-license.txt")
".capabilities.json", ".memory.json", ".font-license.txt",
".meshtext-license.txt")
def canonical_digest(value):
+4
View File
@@ -10,6 +10,7 @@ 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
def digest(path):
@@ -43,6 +44,7 @@ def artifact_files(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"))
return [p for p in candidates if p.is_file()]
@@ -50,6 +52,7 @@ def package_report(directory, stem):
stem = Path(stem)
report = validate_build(directory)
validate_artifact_notice(stem)
validate_meshtext_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")
@@ -64,6 +67,7 @@ def package_report(directory, stem):
def validate_package(stem):
stem = Path(stem)
validate_artifact_notice(stem)
validate_meshtext_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"):
+8 -1
View File
@@ -18,6 +18,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
from firmware_memory_manifest import validate_package
from check_nrf52_uf2 import check_uf2
from package_nrf52_font_license import validate_artifact_notice
from package_meshtext_license import validate_artifact_notice as validate_meshtext_notice
ROOT = Path(__file__).resolve().parents[1]
@@ -161,6 +162,9 @@ def collect_artifacts(directory, version):
font_notice = validate_artifact_notice(directory / stem)
if font_notice is not None:
files.append(font_notice)
meshtext_notice = validate_meshtext_notice(directory / stem)
if meshtext_notice is not None:
files.append(meshtext_notice)
manifest["runtime_ram"] = {key: memory[key] for key in (
"passed", "available_internal_bytes", "required_heap_bytes", "elf_sha256")}
accounted.update(files)
@@ -192,8 +196,11 @@ def category(record):
def sha256(path):
digest = hashlib.sha256()
with path.open("rb") as stream:
return hashlib.file_digest(stream, "sha256").hexdigest()
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def completed_matrix_failures(status, allow_partial=False):
@@ -23,6 +23,7 @@ from motalib import ( # noqa: E402
)
from check_esp32_app_size import app_partition_size # noqa: E402
from firmware_memory_manifest import validate_package # noqa: E402
from package_meshtext_license import validate_artifact_notice as validate_meshtext_notice
BOARDS = {
@@ -587,6 +588,9 @@ def package_board(name: str, spec: dict, build_dir: Path, output_dir: Path,
"target-partitions.bin": table,
"capabilities.json": capability_path.read_bytes(),
}
meshtext_notice = validate_meshtext_notice(build_dir / stem)
if meshtext_notice is not None:
files[meshtext_notice.name] = meshtext_notice.read_bytes()
if spec.get("expander_bridge"):
expander_body = (utility_build_dir / spec["expander_bridge"] /
"firmware.bin").read_bytes()
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""Ship the adapted Meshtext MIT notice only for actually linked code."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import sys
from firmware_elf import FirmwareElf
ROOT = Path(__file__).resolve().parents[1]
NOTICE_PATH = ROOT / "licenses/ESPP-Meshtext-MIT.txt"
SIDECAR_SUFFIX = ".meshtext-license.txt"
CAPABILITY = "companion.meshtext.channel_text"
COMPONENT = "espp-meshtext"
def notice_bytes():
data = NOTICE_PATH.read_bytes()
if not valid_notice(data):
raise ValueError("Meshtext MIT source notice is incomplete")
return data
def valid_notice(data):
return (0 < len(data) <= 262144 and all(text in data for text in (
b"Copyright (c) 2022 esp-cpp", b"Permission is hereby granted, free of charge",
b"included in all", b"copies or substantial portions of the Software",
b'THE SOFTWARE IS PROVIDED "AS IS"', b"OTHER DEALINGS IN THE", b"SOFTWARE.")))
def linked_symbols(path):
"""Only allocated executable definitions qualify; debug strings do not."""
if Path(path).read_bytes()[:4] != b"\x7fELF":
return [] # Existing marker-only test fixtures are not linked ELFs.
elf = FirmwareElf(path)
result = []
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 and section[2] & 6 == 6:
result.append(name)
return sorted(result)
def meshtext_evidence(path):
symbols = linked_symbols(path)
# Match the Itanium namespace prefix, not arbitrary names containing MT.
codec = [name for name in symbols if name.startswith("_ZN4mesh2mt")]
required = ("_ZN4mesh2mt10encodeTextE", "_ZN4mesh2mt10decodeTextE",
"_ZN6MyMesh16sendMeshtextTextE", "_ZN6MyMesh19onForeignRadioFrameE",
"_ZN4mesh10Dispatcher22queueForeignRadioFrameE")
selected = []
for operation in required:
matches = [name for name in symbols if name.startswith(operation)]
if not matches:
return codec, []
selected.append(matches[0])
return codec, selected if codec else []
def notice_record(stem, data):
return {"component": COMPONENT, "license": "MIT",
"file": Path(stem).name + SIDECAR_SUFFIX,
"sha256": hashlib.sha256(data).hexdigest()}
def sidecar(stem):
stem = Path(stem)
return stem.parent / (stem.name + SIDECAR_SUFFIX)
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 Meshtext license declarations")
if not records:
if CAPABILITY in manifest.get("capabilities", []):
raise ValueError(f"{stem}: Meshtext capability lacks its MIT notice")
return None
record = records[0]
if (record.get("file") != sidecar(stem).name or record.get("license") != "MIT"
or not re.fullmatch(r"[0-9a-f]{64}", str(record.get("sha256", "")))):
raise ValueError(f"{stem}: invalid Meshtext license 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 an unaffected firmware")
return None
if not output.is_file():
raise ValueError(f"{output}: missing Meshtext MIT notice")
data = output.read_bytes()
if not valid_notice(data) or hashlib.sha256(data).hexdigest() != record["sha256"]:
raise ValueError(f"{output}: incomplete or mismatched Meshtext MIT notice")
return output
def collect_build_notice(directory, stem):
codec, _chat = meshtext_evidence(Path(directory) / "firmware.elf")
record = manifest_record(stem)
if bool(codec) != bool(record):
raise ValueError("Meshtext license declaration does not match the linked ELF")
if not codec:
return validate_artifact_notice(stem)
data = notice_bytes()
if record != notice_record(stem, data):
raise ValueError("Meshtext license declaration does not match this build's source notice")
output = sidecar(stem)
if output.exists():
return validate_artifact_notice(stem)
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"Meshtext license qualification failed: {error}\n")
if __name__ == "__main__":
main()