From d4c8890eae00fe50ef8c2b0095e6984832cb2476 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Sat, 5 Sep 2026 09:08:42 -0700 Subject: [PATCH] Match Halo Keymind Cascade dev release naming and include WM1110 DFU packages --- create-uf2.py | 10 +++++++- docs/releases/1.17.1.5.md | 17 +++++++++---- scripts/package_cascade_release.py | 34 ++++++++++++++++++------- test/test_cascade_release_package.py | 38 ++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 15 deletions(-) diff --git a/create-uf2.py b/create-uf2.py index 10ec0ed6..9388ce99 100644 --- a/create-uf2.py +++ b/create-uf2.py @@ -28,4 +28,12 @@ env.AddCustomTarget( title="Create UF2 file", description="Use uf2conv to convert hex binary into uf2", always_build=True, -) \ No newline at end of file +) + +# Debug-probe upload defaults (for example WM1110's J-Link) must not omit the +# application DFU ZIP from releases. Depend on the HEX builder so all its +# post-actions, including EndF, finish before the DFU package is generated. +# This does not change the board's selected upload protocol. +if env.subst("$UPLOAD_PROTOCOL") != "nrfutil": + dfu_package = env.PackageDfu("$BUILD_DIR/${PROGNAME}", firmware_hex) + env.Alias("buildprog", dfu_package) diff --git a/docs/releases/1.17.1.5.md b/docs/releases/1.17.1.5.md index 43c35798..e0288da5 100644 --- a/docs/releases/1.17.1.5.md +++ b/docs/releases/1.17.1.5.md @@ -1,4 +1,10 @@ -# MeshCore 1.17.1.5 — USA Cascade +# MeshCore 1.17.1.5 Dev — USA Cascade + +The firmware identifier and main release tag follow +`v1.17.1.5-halo-keymind-cascade-dev-`, matching the 1.17.1.4 naming +convention. Supplemental pages retain the `repeater-room-`, `utility-`, +`logging-`, `lora-ota-`, and `full-profiles-` prefixes where those outputs +exist. All pages are development prereleases. This release uses the USA/Canada radio preset and Cascade runtime defaults. The release build records the resolved frequency, bandwidth, spreading factor, @@ -34,15 +40,16 @@ handling, compressed MOTA transport, and measured ESP32 memory limits. The equivalent reproducible command is: ```bash -OUTPUT_DIR=.releases/1.17.1.5-usa-cascade \ +OUTPUT_DIR=.releases/1.17.1.5-halo-keymind-cascade-dev \ bash build.sh build-firmwares-logging-matrix \ - --firmware-version v1.17.1.5 \ + --firmware-version v1.17.1.5-halo-keymind-cascade-dev \ --radio-preset usa-cascadia --profile cascade \ --require-ota --skip-kiss --resume --background ``` -Or run `bash build.sh`, select option 3, set version `v1.17.1.5`, and retain -USA Cascadia/Cascade. Option 3 enables the infrastructure OTA requirement by +Or run `bash build.sh`, select option 3, set version +`v1.17.1.5-halo-keymind-cascade-dev`, and retain USA Cascadia/Cascade. +Option 3 enables the infrastructure OTA requirement by default. It merges supported runtime USB logging into canonical artifacts and builds the expanded ESP32 profiles. It runs one PlatformIO process at a time. diff --git a/scripts/package_cascade_release.py b/scripts/package_cascade_release.py index dde0b0e9..6f0cea20 100644 --- a/scripts/package_cascade_release.py +++ b/scripts/package_cascade_release.py @@ -8,8 +8,9 @@ import hashlib import html import json from pathlib import Path +import re import shutil -from urllib.parse import quote +from urllib.parse import quote, urljoin ROOT = Path(__file__).resolve().parents[1] @@ -79,7 +80,11 @@ def category(record): return "full-profiles" if "lora_ota" in manifest["target"].lower(): return "lora-ota" - return "infrastructure" + if "logging" in manifest["artifact_target"].lower(): + return "logging" + if any(role in manifest["target"].lower() for role in ("sensor", "terminal")): + return "utility" + return "repeater-room" def sha256(path): @@ -102,18 +107,22 @@ def main(): output_directory = Path(status["working_directory"]) / status["output_directory"] if output_directory.resolve() != args.input.resolve(): raise ValueError("build status belongs to another output directory") - if status.get("source_commit") != args.commit or status.get("firmware_version") != "v" + args.version: + firmware_label = f"v{args.version}-halo-keymind-cascade-dev" + if status.get("source_commit") != args.commit or status.get("firmware_version") != firmware_label: raise ValueError("build status belongs to another source revision or version") if status.get("firmware_profile") != "cascade": raise ValueError("matrix did not use Cascade runtime defaults") radio = {key: status["radio_" + key] for key in ("frequency", "bandwidth", "sf", "cr")} if args.output.exists() and any(args.output.iterdir()): raise ValueError("staging directory must be empty; existing releases are never overwritten") - version = f"v{args.version}-{args.commit[:8]}" + version = f"{firmware_label}-{args.commit[:8]}" records = collect_artifacts(args.input, version) - base_tag = f"v{args.version}-usa-cascade-{args.commit[:8]}" + base_tag = version groups = [] - for name in ("companion", "infrastructure", "lora-ota", "full-profiles"): + titles = {"companion": "Companion builds", "repeater-room": "Repeater and Room Server builds", + "utility": "Sensor and Terminal utilities", "logging": "USB packet logging builds", + "lora-ota": "LoRa OTA builds", "full-profiles": "Expanded FULL ESP32 profiles"} + for name in titles: current = [] count = 0 chunks = [] @@ -128,7 +137,10 @@ def main(): for index, chunk in enumerate(chunks): key = name + (f"-{index + 1}" if index else "") tag = base_tag if key == "companion" else f"{key}-{base_tag}" - groups.append({"key": key, "tag": tag, "records": chunk}) + title = f"MeshCore {args.version} Dev - {titles[name]}" + if index: + title += f" (part {index + 1})" + groups.append({"key": key, "tag": tag, "title": title, "prerelease": True, "records": chunk}) links = "\n".join(f"- [{g['key']}](https://github.com/{args.repo}/releases/tag/{g['tag']})" for g in groups) source_url = f"https://github.com/{args.repo}/blob/{args.commit}" rows = [] @@ -149,11 +161,15 @@ def main(): summaries.append({**manifest, "files": [path.name for path in record["files"]]}) rows.append(f"{html.escape(manifest['artifact_target'])}{html.escape(manifest['build_profile'])}{html.escape(methods)}{' · '.join(file_links)}") (destination / "TARGET-MANIFEST.json").write_text(json.dumps(summaries, indent=2) + "\n") - shutil.copy2(ROOT / "docs/full_companion_features.md", destination / "FULL-COMPANION-FEATURES.md") + guide = (ROOT / "docs/full_companion_features.md").read_text() + guide = re.sub(r"\]\(([^)]+)\)", + lambda match: "](" + urljoin(source_url + "/docs/", match[1]) + ")", guide) + (destination / "FULL-COMPANION-FEATURES.md").write_text(guide) exclusions = args.input / "ota-excluded-targets.txt" if exclusions.is_file(): shutil.copy2(exclusions, destination / exclusions.name) - body = (f"# MeshCore {args.version} — USA Cascade\n\n" + body = (f"> **Development prerelease.** Firmware identifier: **{base_tag}**.\n\n" + f"# MeshCore {args.version} Dev — USA Cascade\n\n" f"Source: `{args.commit}`. USA/Canada: **{radio['frequency']} MHz, BW{radio['bandwidth']}, SF{radio['sf']}, CR{radio['cr']}**; Cascade defaults.\n\n" f"This page contains **{len(summaries)} qualified firmware profiles**. Full Companions send MOTA and normally update over USB. Infrastructure has a verified wireless update path; see each capability manifest.\n\n" f"[Feature on/off and update directions]({source_url}/docs/full_companion_features.md) · " diff --git a/test/test_cascade_release_package.py b/test/test_cascade_release_package.py index ef8509fb..42745761 100644 --- a/test/test_cascade_release_package.py +++ b/test/test_cascade_release_package.py @@ -4,6 +4,9 @@ import json from pathlib import Path import tempfile import unittest +import subprocess +import sys +import hashlib ROOT = Path(__file__).resolve().parents[1] spec = importlib.util.spec_from_file_location("package_cascade_release", ROOT / "scripts/package_cascade_release.py") @@ -49,6 +52,41 @@ class ReleaseQualificationTest(unittest.TestCase): with self.assertRaisesRegex(ValueError, "pair incomplete"): package.collect_artifacts(directory, "v1.0-deadbeef") + def test_stages_named_prerelease_and_checksums_only_after_completion(self): + with tempfile.TemporaryDirectory() as temp: + directory = Path(temp) + inputs = directory / "input" + inputs.mkdir() + commit = "deadbeef" + "0" * 32 + label = "v1.17.1.5-halo-keymind-cascade-dev" + stem = "test_companion_radio_usb-" + label + "-deadbeef" + manifest = self.manifest("test_companion_radio_usb", ota_update_verified=False, + artifact_target="test_companion_radio_usb", build_profile="standard") + (inputs / (stem + ".capabilities.json")).write_text(json.dumps(manifest)) + (inputs / (stem + ".bin")).write_bytes(b"test application") + (inputs / (stem + "-merged.bin")).write_bytes(b"test merged image") + status = directory / "status" + settings = (f"exit_code=0\nworking_directory={directory}\noutput_directory=input\n" + f"source_commit={commit}\nfirmware_version={label}\nfirmware_profile=cascade\n" + "radio_frequency=910.525\nradio_bandwidth=62.5\nradio_sf=7\nradio_cr=5\n") + status.write_text("state=running\n" + settings) + command = [sys.executable, str(ROOT / "scripts/package_cascade_release.py"), + "--input", str(inputs), "--output", str(directory / "stage"), + "--build-status", str(status), "--commit", commit] + result = subprocess.run(command, capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertFalse((directory / "stage").exists()) + status.write_text("state=completed\n" + settings) + result = subprocess.run(command, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + plan = json.loads((directory / "stage/release-plan.json").read_text()) + self.assertEqual(plan["groups"][0]["tag"], label + "-deadbeef") + self.assertTrue(plan["groups"][0]["prerelease"]) + assets = directory / "stage/companion" + for line in (assets / "SHA256SUMS.txt").read_text().splitlines(): + digest, name = line.split(" ", 1) + self.assertEqual(hashlib.sha256((assets / name).read_bytes()).hexdigest(), digest) + if __name__ == "__main__": unittest.main()