diff --git a/docs/companion_contact_cache.md b/docs/companion_contact_cache.md index b1343a50..f5015f17 100644 --- a/docs/companion_contact_cache.md +++ b/docs/companion_contact_cache.md @@ -140,6 +140,14 @@ OUTPUT_DIR=.releases/contact-cache-v3 bash build.sh build-firmware \ The firmware RAM guard remains enabled. A successful linked budget is distinct from live free heap and physical Bluetooth/LoRa qualification on each board. +The smaller NimBLE applications let the CT62, XIAO C3 and Generic ESP-NOW +trials use two 1.5 MiB WiFi OTA slots on 4 MiB flash. Their 896 KiB SPIFFS and +coredump stay at the same addresses as the ordinary `huge_app.csv` Full +layout. Install the trial's merged image over USB first to install its +partition table. Subsequent WiFi updates must fit the new slots. An +application-only update from the ordinary single-slot Full layout cannot +change that layout or enable WiFi OTA. + ## Regression checks ```sh diff --git a/platformio.nimble.ini b/platformio.nimble.ini index c5a1fee3..ad922702 100644 --- a/platformio.nimble.ini +++ b/platformio.nimble.ini @@ -77,6 +77,7 @@ lib_ignore = ${companion_nimble_trial.lib_ignore} [env:Heltec_ct62_ram_trial_companion_radio_full_nimble] extends = env:Heltec_ct62_companion_radio_ble +board_build.full_partitions = variants/dual_ota_1536k.csv build_flags = ${env:Heltec_ct62_companion_radio_ble.build_flags} ${companion_nimble_ram_trial.build_flags} @@ -87,6 +88,7 @@ lib_ignore = ${companion_nimble_trial.lib_ignore} [env:Xiao_C3_ram_trial_companion_radio_full_nimble] extends = env:Xiao_C3_companion_radio_wifi +board_build.full_partitions = variants/dual_ota_1536k.csv build_flags = ${env:Xiao_C3_companion_radio_wifi.build_flags} ${companion_nimble_ram_trial.build_flags} @@ -97,6 +99,7 @@ lib_ignore = ${companion_nimble_trial.lib_ignore} [env:Generic_ESPNOW_ram_trial_companion_radio_full_nimble] extends = env:Generic_ESPNOW_comp_radio_usb +board_build.full_partitions = variants/dual_ota_1536k.csv build_flags = ${env:Generic_ESPNOW_comp_radio_usb.build_flags} ${companion_nimble_ram_trial.build_flags} diff --git a/scripts/esp32_full_partition.py b/scripts/esp32_full_partition.py index 153976d4..9a36f683 100644 --- a/scripts/esp32_full_partition.py +++ b/scripts/esp32_full_partition.py @@ -12,6 +12,10 @@ PRESERVED_FULL_PARTITION_TABLES = { "variants/sensecap_indicator-espnow/dual_ota_6400k_preserve_spiffs.csv", } +# Explicitly selected by the smaller NimBLE capacity trials. The normal +# 4 MiB Full Companion still needs huge_app.csv for its larger application. +OPT_IN_FULL_PARTITION_TABLES = {"variants/dual_ota_1536k.csv"} + def parse_flash_size(value): text = str(value).strip().upper() @@ -39,10 +43,14 @@ if os.environ.get("MESHCORE_ESP32_FULL_BUILD") == "1": flash_size = parse_flash_size(board.get("upload.flash_size", "4MB")) requested_partitions = os.environ.get( "MESHCORE_ESP32_FULL_PARTITION_TABLE", "" - ).strip() + ).strip() or str(board.get("build.full_partitions", "")).strip() required_partitions = preserved_full_partition_table( requested_partitions ) + if requested_partitions in OPT_IN_FULL_PARTITION_TABLES: + if flash_size < 4 * 1024 * 1024: + raise ValueError("Full dual-OTA trial requires at least 4 MiB flash") + required_partitions = requested_partitions if requested_partitions and not required_partitions: raise ValueError( "unsupported required Full partition table: " diff --git a/test/test_esp32_full_partition.py b/test/test_esp32_full_partition.py index c10ffc28..423c94d2 100644 --- a/test/test_esp32_full_partition.py +++ b/test/test_esp32_full_partition.py @@ -170,6 +170,32 @@ class Esp32FullPartitionTest(unittest.TestCase): ) self.assertEqual(board.values["build.partitions"], "huge_app.csv") + def test_explicit_small_full_trial_uses_dual_ota_with_large_spiffs(self): + table = "variants/dual_ota_1536k.csv" + board, _output = apply_policy( + { + "upload.flash_size": "4MB", + "build.mcu": "esp32c3", + "build.partitions": "huge_app.csv", + "build.full_partitions": table, + } + ) + self.assertEqual(board.values["build.partitions"], table) + # Retain the huge_app SPIFFS/coredump placement and enough storage for + # two complete 350-contact snapshots, rather than the 64 KiB table. + partitions = (ROOT / table).read_text() + self.assertIn("spiffs, data, spiffs, 0x310000,0xE0000", partitions) + self.assertIn("app1, app, ota_1, 0x190000,0x180000", partitions) + + def test_explicit_small_full_trial_rejects_insufficient_flash(self): + with self.assertRaisesRegex(ValueError, "at least 4 MiB"): + apply_policy( + { + "upload.flash_size": "2MB", + "build.full_partitions": "variants/dual_ota_1536k.csv", + } + ) + def test_tbeam_1w_factory_layout_keeps_precedence(self): board, _output = apply_policy( {