From f7b5c40c3b5a4b83558cbbdd87c4ac9b0abb7093 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Sun, 18 Oct 2026 23:48:07 -0700 Subject: [PATCH] Fit RAK4631 Full OTA image by reusing regulator recovery code --- .github/workflows/run-unit-tests.yml | 4 ++-- src/helpers/RegulatorRecovery.h | 14 ++++++-------- src/helpers/RegulatorSettings.cpp | 10 ++++------ test/test_companion_ota_includes.py | 1 + test/test_regulator_recovery.py | 19 +++++++++++++++++++ 5 files changed, 32 insertions(+), 16 deletions(-) diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index f0714725d..a9da465e2 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -19,7 +19,7 @@ jobs: - name: Qualify RAK4631 Full Companion with linked Meshtext support run: >- bash build.sh build-firmware RAK_4631_companion_radio_full - --firmware-version v1.17.1.10-halo-keymind-cascade-dev + --firmware-version v1.18.0.1-halo-keymind-cascade-dev --radio-preset usa-cascade-fixed --profile cascade --require-ota - name: Verify linked Companion crypto, retained Reader and ARM stack use @@ -33,7 +33,7 @@ jobs: # This configuration has no Meshtext/Ethernet application callbacks. run: >- bash build.sh build-firmware Xiao_nrf52_companion_radio_full - --firmware-version v1.17.1.10-halo-keymind-cascade-dev + --firmware-version v1.18.0.1-halo-keymind-cascade-dev --radio-preset usa-cascade-fixed --profile cascade --require-ota - name: Qualify unified T114 Full Companion with automatic screen detection diff --git a/src/helpers/RegulatorRecovery.h b/src/helpers/RegulatorRecovery.h index 984f45d6c..657cd9792 100644 --- a/src/helpers/RegulatorRecovery.h +++ b/src/helpers/RegulatorRecovery.h @@ -2,6 +2,7 @@ #include #include +#include "PersistentStoreFormat.h" namespace mesh { namespace power { @@ -143,14 +144,11 @@ class RecoveryPolicy { static uint32_t elapsed(uint32_t now, uint32_t then) { return now - then; } static uint32_t checksum(const RecoveryRecord& record) { - uint32_t crc = 0xffffffff; - const uint8_t* bytes = reinterpret_cast(&record); - for (size_t i = 0; i < offsetof(RecoveryRecord, crc); ++i) { - crc ^= bytes[i]; - for (uint8_t bit = 0; bit < 8; ++bit) - crc = (crc >> 1) ^ (0xedb88320u & (0u - (crc & 1u))); - } - return ~crc; + // Retained records use the same IEEE polynomial as persistent storage, + // with a final complement. Reuse its already-linked implementation while + // preserving the record layout and checksum across firmware updates. + return ~storage::updateCRC32(0xffffffffUL, + reinterpret_cast(&record), offsetof(RecoveryRecord, crc)); } static bool isValid(const RecoveryRecord& record) { diff --git a/src/helpers/RegulatorSettings.cpp b/src/helpers/RegulatorSettings.cpp index 39448df31..0ef1755b1 100644 --- a/src/helpers/RegulatorSettings.cpp +++ b/src/helpers/RegulatorSettings.cpp @@ -300,12 +300,10 @@ bool handleRegulatorCommand(const char* command, char* reply, size_t capacity) { } void markRegulatorCleanReset() { - if (initialized) recovery().markCleanReset(); - else { - RecoveryPolicy policy(platformRecoveryRecord()); - policy.begin(ResetKind::Intentional, 0); - policy.markCleanReset(); - } + // Early resets still initialize the retained record, using the same policy + // object as normal startup instead of constructing another on the stack. + if (!initialized) recovery().begin(ResetKind::Intentional, 0); + recovery().markCleanReset(); } void resumeRegulatorRecovery() { diff --git a/test/test_companion_ota_includes.py b/test/test_companion_ota_includes.py index b94bcd658..8edd6f2a1 100644 --- a/test/test_companion_ota_includes.py +++ b/test/test_companion_ota_includes.py @@ -25,6 +25,7 @@ class CompanionOtaIncludeTest(unittest.TestCase): with tempfile.TemporaryDirectory(prefix="meshcore-companion-includes-") as temp: directory = Path(temp) for header in ( + "helpers/RegulatorSettings.h", "helpers/ui/StartupScreen.h", "helpers/ui/DisplayPowerSettings.h", "helpers/UsbLoggingWatchdog.h", "helpers/UsbLoggingClientActivity.h", "Mesh.h", "helpers/BluetoothMac.h", "CompanionBluetooth.h", diff --git a/test/test_regulator_recovery.py b/test/test_regulator_recovery.py index c717894ba..c24c2c99e 100644 --- a/test/test_regulator_recovery.py +++ b/test/test_regulator_recovery.py @@ -212,6 +212,24 @@ int main(int argc, char** argv) { // A sample apparently newer than the last heartbeat is conservative until // that heartbeat catches up; subtraction cannot turn it into a fresh age. { RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); assertCounts(p, 0, 0); } + } else if (scenario == 16) { + // Frozen retained record with two MCU crashes and one radio crash. Its CRC + // and the fresh-record CRC below were independently calculated by zlib. + const uint8_t legacy[] = { + 0x52, 0x47, 0x50, 0x52, 0x01, 0x00, 0x20, 0x00, + 0xbc, 0xcc, 0x5b, 0x07, 0x15, 0xcd, 0x5b, 0x07, + 0x0c, 0xcd, 0x5b, 0x07, 0x03, 0x00, 0x02, 0x01, + 0x4b, 0x05, 0x00, 0x00, 0x6b, 0x64, 0x72, 0xa2, + }; + static_assert(sizeof(legacy) == sizeof(r), "frozen retained format"); + std::memcpy(&r, legacy, sizeof(r)); + { RecoveryPolicy p(r); p.begin(ResetKind::Crash, 0); + assertCounts(p, 3, 2, RECOVERY_MCU); } + // Losing the final complement, seed or payload extent must fail this + // compatibility check even if newly written records remain self-consistent. + std::memset(&r, 0, sizeof(r)); + initial(r, RECOVERY_DOMAINS, 75, true, 123456789); + assert(r.crc == 0x1d384636u); } else assert(false); std::cout << "PASS: regulator recovery " << scenario << '\n'; } @@ -256,6 +274,7 @@ class RegulatorRecoveryTests(unittest.TestCase): "a boot is disarmed until the actual modes are selected", "saved and retained fallback masks merge", "future sample is ineligible until heartbeat catches up", + "retained CRC remains compatible with independently encoded records", ) for number, scenario in enumerate(scenarios): with self.subTest(scenario=scenario):