Stage 1.17.1.5 local release assets and document OTAFIX 2.4.6
Firmware Builder / create-release (push) Canceled after 0s
Firmware Builder / build (push) Canceled after 0s
Firmware Builder / generate-build-matrix (push) Canceled after 0s
Build Repeater Firmwares / build-repeater-firmwares (push) Canceled after 0s

This commit is contained in:
mikecarper committed 2026-09-05 14:04:39 -07:00
1 parent 49dcba5c6e
commit 2630379365
4 files changed
+51 -3

No files matched your search

+6 -3
View File
@@ -163,6 +163,9 @@ Bluetooth DFU updates the Companion; the separate Bluetooth MOTA source
service feeds packages to other mesh nodes.
For OTAFIX installations, use the exact board/storage profile from
[OTAFIX 2.4.5](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/0.11.0-OTAFIX2.4.5).
Its release notes cover the Bluetooth reconnect fix and upgrades from older
bootloaders, including the special UF2-drive precautions for version 2.4.3.
[OTAFIX 2.4.6](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/0.11.0-OTAFIX2.4.6).
It includes the Bluetooth reconnect fix from 2.4.5. Its release notes cover
upgrades from older bootloaders, including the special UF2-drive precautions
for version 2.4.3. The new 64 KiB retained-RAM staging feature applies to
qualified internal-flash nRF52840 receivers; Full Companions continue to
serve host-supplied packages without needing that receiver storage layout.
+21
View File
@@ -43,6 +43,17 @@ offline queue while retaining its MOTA and transport features.
Since 1.17.1.4, this branch also includes USB session and contact-persistence
fixes, automatic nRF52 ExtraFS recovery, improved Indicator setup/display
handling, compressed MOTA transport, and measured ESP32 memory limits.
Qualified internal-flash nRF52840 receivers now reserve 64 KiB of retained
RAM to extend application MOTA staging beyond the available flash prefix.
This handoff requires the exact board's
[OTAFIX 2.4.6 bootloader](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/0.11.0-OTAFIX2.4.6).
Install that bootloader before relying on LoRa updates from these new hybrid
receiver images: they reject application containers larger than one flash
page when the installed bootloader lacks the retained-RAM capability.
QSPI/microSD roles and Full Companions retain their existing storage layouts.
An interrupted hybrid download must be fetched again after an application
restart or power cycle. See the [protocol](../ota_protocol.md) for the exact
admission and reset checks.
## Build using option 3
@@ -73,3 +84,13 @@ infrastructure. Those outputs do not belong in this OTA-required release.
Use a separate output directory when changing the radio or settings profile.
Use a fresh release output directory after changing the source revision or
firmware version so packaging cannot mix old and new binaries.
## Local release staging
After the matrix completes successfully, `scripts/package_cascade_release.py`
groups the qualified images using the previous GitHub release naming. Each
group includes the firmware files, capability manifests, TSV/JSON inventory,
checksums, build notes, and Full Companion directions. The staging directory's
root `FIRMWARE-PICKER-1.17.1.5.html` opens the local downloads; the copies inside
each release group use the planned GitHub download URLs. Staging does not
push commits, create remote tags, or publish a GitHub release.
+17
View File
@@ -161,6 +161,13 @@ 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")
columns = ("artifact_target", "target", "platform", "build_profile",
"ota_update_methods", "files")
(destination / "TARGET-MANIFEST.tsv").write_text(
"\t".join(columns) + "\n" + "".join(
"\t".join(",".join(row.get(key, [])) if isinstance(row.get(key), list)
else str(row.get(key, "")) for key in columns) + "\n"
for row in summaries))
guide = (ROOT / "docs/full_companion_features.md").read_text()
guide = re.sub(r"\]\(([^)]+)\)",
lambda match: "](" + urljoin(source_url + "/docs/", match[1]) + ")", guide)
@@ -178,6 +185,7 @@ def main():
"Firmware and capability checks passed in the build matrix. Hardware update testing across every board was not performed.\n\n"
+ links + "\n")
(args.output / (group["key"] + "-notes.md")).write_text(body)
(destination / "BUILD-NOTES.txt").write_text(body)
picker = ("<!doctype html><meta charset=utf-8><meta name=viewport content='width=device-width'>"
f"<title>MeshCore {html.escape(args.version)} USA Cascade</title>"
"<style>body{font:16px system-ui;margin:2rem}input{font:inherit;padding:.5rem;width:90%;max-width:40rem}table{border-collapse:collapse;width:100%}td,th{text-align:left;padding:.7rem;border-bottom:1px solid #ccc}td:first-child{overflow-wrap:anywhere}a{color:#165acb}</style>"
@@ -188,6 +196,15 @@ def main():
"<table><thead><tr><th>Target</th><th>Profile</th><th>Self-update</th><th>Downloads</th></tr></thead><tbody>"
+ "\n".join(rows) + "</tbody></table>"
"<script>document.getElementById('search').oninput=function(){const q=this.value.toLowerCase();document.querySelectorAll('tbody tr').forEach(r=>r.hidden=!r.textContent.toLowerCase().includes(q))}</script>")
local_picker = picker
for group in groups:
local_picker = local_picker.replace(
f"https://github.com/{args.repo}/releases/download/{group['tag']}/",
group["key"] + "/")
local_picker = local_picker.replace(
f"{source_url}/docs/full_companion_features.md",
groups[0]["key"] + "/FULL-COMPANION-FEATURES.md")
(args.output / f"FIRMWARE-PICKER-{args.version}.html").write_text(local_picker)
for group in groups:
destination = args.output / group["key"]
(destination / f"FIRMWARE-PICKER-{args.version}.html").write_text(picker)
+7
View File
@@ -83,6 +83,13 @@ class ReleaseQualificationTest(unittest.TestCase):
self.assertEqual(plan["groups"][0]["tag"], label + "-deadbeef")
self.assertTrue(plan["groups"][0]["prerelease"])
assets = directory / "stage/companion"
picker = (directory / "stage/FIRMWARE-PICKER-1.17.1.5.html").read_text()
self.assertIn(f'companion/{stem}.bin', picker)
self.assertNotIn("/releases/download/", picker)
self.assertIn("companion/FULL-COMPANION-FEATURES.md", picker)
self.assertIn("/releases/download/", (assets / "FIRMWARE-PICKER-1.17.1.5.html").read_text())
self.assertIn(stem + ".bin", (assets / "TARGET-MANIFEST.tsv").read_text())
self.assertIn("910.525", (assets / "BUILD-NOTES.txt").read_text())
for line in (assets / "SHA256SUMS.txt").read_text().splitlines():
digest, name = line.split(" ", 1)
self.assertEqual(hashlib.sha256((assets / name).read_bytes()).hexdigest(), digest)