Match Halo Keymind Cascade dev release naming and include WM1110 DFU packages

This commit is contained in:
mikecarper committed 2026-09-05 09:08:42 -07:00
1 parent 02020513f4
commit d4c8890eae
4 files changed
+83 -14

No files matched your search

+8
View File
@@ -29,3 +29,11 @@ env.AddCustomTarget(
description="Use uf2conv to convert hex binary into uf2",
always_build=True,
)
# 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)
+12 -5
View File
@@ -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-<commit>`, 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.
+25 -9
View File
@@ -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"<tr><td>{html.escape(manifest['artifact_target'])}</td><td>{html.escape(manifest['build_profile'])}</td><td>{html.escape(methods)}</td><td>{' · '.join(file_links)}</td></tr>")
(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) · "
+38
View File
@@ -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()