From 2630379365cf47cb7eec146b7d1b57b354efe251 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Sat, 5 Sep 2026 14:04:39 -0700 Subject: [PATCH] Stage 1.17.1.5 local release assets and document OTAFIX 2.4.6 --- docs/full_companion_features.md | 9 ++++++--- docs/releases/1.17.1.5.md | 21 +++++++++++++++++++++ scripts/package_cascade_release.py | 17 +++++++++++++++++ test/test_cascade_release_package.py | 7 +++++++ 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/docs/full_companion_features.md b/docs/full_companion_features.md index 3722d331..c249ae2c 100644 --- a/docs/full_companion_features.md +++ b/docs/full_companion_features.md @@ -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. diff --git a/docs/releases/1.17.1.5.md b/docs/releases/1.17.1.5.md index f7975b86..ffbdee56 100644 --- a/docs/releases/1.17.1.5.md +++ b/docs/releases/1.17.1.5.md @@ -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. diff --git a/scripts/package_cascade_release.py b/scripts/package_cascade_release.py index 6f0cea20..20454b67 100644 --- a/scripts/package_cascade_release.py +++ b/scripts/package_cascade_release.py @@ -161,6 +161,13 @@ 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") + 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 = ("" f"MeshCore {html.escape(args.version)} USA Cascade" "" @@ -188,6 +196,15 @@ def main(): "" + "\n".join(rows) + "
TargetProfileSelf-updateDownloads
" "") + 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) diff --git a/test/test_cascade_release_package.py b/test/test_cascade_release_package.py index 42745761..0d71082a 100644 --- a/test/test_cascade_release_package.py +++ b/test/test_cascade_release_package.py @@ -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)