Select registered OTA slots and verify LoRa staging geometry

This commit is contained in:
mikecarper committed 2026-09-23 23:33:29 -07:00
1 parent f8a1e2ff96
commit 6976877f04
3 files changed
+36 -33

No files matched your search

+13 -26
View File
@@ -152,14 +152,6 @@ esp_partition_t rawPartition(uint32_t address, uint32_t size, const char* label)
return part;
}
esp_partition_t rawAppPartition(uint32_t address, uint32_t size,
esp_partition_subtype_t subtype, const char* label) {
esp_partition_t part = rawPartition(address, size, label);
part.type = ESP_PARTITION_TYPE_APP;
part.subtype = subtype;
return part;
}
bool rangesOverlap(uint32_t first_address, uint32_t first_size,
uint32_t second_address, uint32_t second_size) {
const uint64_t first_end = static_cast<uint64_t>(first_address) + first_size;
@@ -398,7 +390,7 @@ bool stageResumeSlot(uint8_t slot) {
return saved;
}
bool resumeLegacyOtaReceiver(const migration::TargetPlan& plan) {
bool resumeLegacyOtaReceiver() {
Preferences migration_nvs;
if (!migration_nvs.begin(kMigrationNvsNamespace, false)) {
strcpy(status_text, "Could not read old LoRa application slot");
@@ -410,12 +402,12 @@ bool resumeLegacyOtaReceiver(const migration::TargetPlan& plan) {
strcpy(status_text, "Expanded layout ready; no LoRa resume record");
return false;
}
const esp_partition_t old_app = slot == 0
? rawAppPartition(plan.layout.app0_address, plan.layout.app0_size,
ESP_PARTITION_SUBTYPE_APP_OTA_0, "app0")
: rawAppPartition(plan.layout.app1_address, plan.layout.app1_size,
ESP_PARTITION_SUBTYPE_APP_OTA_1, "app1");
const esp_err_t selected = esp_ota_set_boot_partition(&old_app);
const esp_partition_t* old_app = esp_partition_find_first(
ESP_PARTITION_TYPE_APP,
slot == 0 ? ESP_PARTITION_SUBTYPE_APP_OTA_0 : ESP_PARTITION_SUBTYPE_APP_OTA_1,
nullptr);
const esp_err_t selected = old_app
? esp_ota_set_boot_partition(old_app) : ESP_ERR_NOT_FOUND;
if (selected != ESP_OK) {
snprintf(status_text, sizeof(status_text), "Could not resume old LoRa app: %s",
errName(selected));
@@ -569,20 +561,16 @@ void runMigration() {
}
#endif
// The OTA-select data records a slot identity. Select a descriptor for the
// bridge's expanded slot before replacing the table so the next boot runs
// the bridge at the correct address under the new layout.
// The OTA-select data records a slot identity. Select the bridge's currently
// registered legacy slot; after the table change, that slot identity points
// at the verified bridge copy under the expanded layout.
#if defined(MESHCORE_MIGRATION_RESUME_OTA)
const bool bridge_in_app0 = resume.bridge_slot == 0;
#else
const bool bridge_in_app0 = true;
#endif
const esp_partition_t target_bridge = bridge_in_app0
? rawAppPartition(plan->layout.app0_address, plan->layout.app0_size,
ESP_PARTITION_SUBTYPE_APP_OTA_0, "app0")
: rawAppPartition(plan->layout.app1_address, plan->layout.app1_size,
ESP_PARTITION_SUBTYPE_APP_OTA_1, "app1");
const esp_err_t select_result = esp_ota_set_boot_partition(&target_bridge);
const esp_err_t select_result = esp_ota_set_boot_partition(
bridge_in_app0 ? refs.app0 : refs.app1);
if (select_result != ESP_OK) {
snprintf(status_text, sizeof(status_text), "Could not select bridge slot: %s",
errName(select_result));
@@ -700,8 +688,7 @@ void setup() {
if (restoreStagedIdentity()) {
strcpy(status_text, "Expanded layout ready");
#if defined(MESHCORE_MIGRATION_RESUME_OTA)
const migration::TargetPlan* plan = migration::targetForFlash(flash_bytes);
if (plan) resumeLegacyOtaReceiver(*plan);
resumeLegacyOtaReceiver();
#endif
}
} else if (findPartitions(refs, geometry)
+20 -4
View File
@@ -61,13 +61,17 @@ def partition_entries(table: bytes) -> dict[str, tuple[int, int]]:
return entries
def mota_full(image: bytes, identity: FwIdent) -> bytes:
def mota_full(image: bytes, identity: FwIdent, block_size: int) -> bytes:
# Device-side seeders keep at most 1024 Merkle leaves in 4 KiB scratch.
# The bridge fits 1 KiB blocks; a roughly 2 MiB Full image needs 2 KiB.
if (len(image) + block_size - 1) // block_size > 1024:
raise ValueError("mOTA image exceeds the device seeder's 1024-block limit")
manifest = build_manifest(
target_id=identity.target_id,
fw_version=identity.fw_version,
image_size=len(image),
payload=image,
block_size=1024,
block_size=block_size,
image_hash=hashlib.sha256(image).digest(),
codec_id=CODEC_FULL,
is_full=True,
@@ -80,6 +84,16 @@ def mota_full(image: bytes, identity: FwIdent) -> bytes:
return package
def check_esp32_stage(package: bytes, image: bytes, slot_bytes: int) -> None:
# Mirror the ESP32 FULL staging geometry: the payload is written at the
# start of the inactive app, with metadata in aligned sectors at its end.
metadata_bytes = len(package) - len(image) - 5
metadata_flush = (metadata_bytes + 5 + 4095) & ~4095
if (len(package) > slot_bytes or metadata_flush > 65536
or len(image) > slot_bytes - metadata_flush):
raise ValueError("mOTA image cannot be staged in the inactive ESP32 slot")
def readme(board: str, spec: dict, version: str, source: str) -> str:
return f"""# {board} in-place partition migration
@@ -166,12 +180,14 @@ def package_board(name: str, spec: dict, build_dir: Path, output_dir: Path,
files = {
"wifi-bridge.bin": wifi_bridge,
"lora-bridge.mota": mota_full(lora_bridge, bridge_ident),
"lora-bridge.mota": mota_full(lora_bridge, bridge_ident, 1024),
"full-application.bin": full_image,
"full-application.mota": mota_full(full_image, full_ident),
"full-application.mota": mota_full(full_image, full_ident, 2048),
"target-partitions.bin": table,
"capabilities.json": capability_path.read_bytes(),
}
check_esp32_stage(files["lora-bridge.mota"], lora_bridge, LEGACY_SLOT_BYTES)
check_esp32_stage(files["full-application.mota"], full_image, slot_bytes)
files["README.md"] = readme(name, spec, version, source).encode()
manifest = {
"board": name,
+3 -3
View File
@@ -54,13 +54,13 @@ class HeltecV4PartitionMigratorTest(unittest.TestCase):
migration_body = source[source.index("void runMigration()") :]
self.assertLess(migration_body.index("stageLegacyIdentity()"),
migration_body.index("copyAndVerify(*running"))
self.assertLess(migration_body.index("esp_ota_set_boot_partition(&target_bridge)"),
self.assertLess(migration_body.index("esp_ota_set_boot_partition(\n bridge_in_app0 ? refs.app0 : refs.app1)"),
migration_body.index("publishExpandedPartitionTable(*plan)"))
setup_body = source[source.index("void setup()") :]
self.assertIn("restoreStagedIdentity()", setup_body)
self.assertIn("resumeLegacyOtaReceiver(*plan)", setup_body)
self.assertIn("resumeLegacyOtaReceiver()", setup_body)
self.assertLess(setup_body.index("restoreStagedIdentity()"),
setup_body.index("resumeLegacyOtaReceiver(*plan)"))
setup_body.index("resumeLegacyOtaReceiver()"))
self.assertIn("validLegacyOtaReceiver(*old_receiver)", migration_body)
self.assertIn("copyAndVerify(*refs.app1", migration_body)
self.assertIn("stageResumeSlot(resume.resume_slot)", migration_body)