From 5f7d03373cc552bcd59e6aba0e5b097613490211 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 17 Sep 2026 23:54:09 -0700 Subject: [PATCH] Fix capacity matrix build regressions --- docs/firmware_memory_budget.md | 5 ++++- examples/companion_radio/ui-new/UITask.cpp | 11 +++++++++++ .../simple_room_server/FloodRuleEngine.cpp | 18 ++++++++++++------ examples/simple_room_server/FloodRuleEngine.h | 10 +++++++++- scripts/check_firmware_ram.py | 18 +++++++++++++++--- src/helpers/rp2040/SerialBLEInterface.cpp | 2 +- variants/heltec_tracker_v2/platformio.ini | 3 +++ variants/lilygo_tbeam_SX1262/platformio.ini | 7 +++++++ variants/lilygo_tbeam_SX1276/platformio.ini | 7 +++++++ variants/rpi_picow/platformio.ini | 4 +++- variants/station_g2/platformio.ini | 3 +++ 11 files changed, 75 insertions(+), 13 deletions(-) diff --git a/docs/firmware_memory_budget.md b/docs/firmware_memory_budget.md index c5f766b8e..348136f1b 100644 --- a/docs/firmware_memory_budget.md +++ b/docs/firmware_memory_budget.md @@ -37,7 +37,10 @@ The [small-screen message layout](v4_pixel5_font_trial.md) retains complete 160-byte messages. Its expanded preview records add 2,816 bytes to the startup allowance and increase the contiguous history allocation budget. The V4 can allocate that history in PSRAM; the guard conservatively reserves -internal capacity so that PSRAM availability cannot hide a RAM shortage. +internal capacity so that PSRAM availability cannot hide a RAM shortage. A +target may replace the generic UI allowance only with +`MESH_COMPANION_SCREEN_STARTUP_BYTES`; a source-side assertion then covers its +actual startup screens plus ESP32 allocator overhead. These are engineering allowances for supported configurations, not measured free heap after boot or a guarantee against every future allocation failure. diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index a6c1ec095..eeb83ed2a 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1757,6 +1757,17 @@ public: } }; +#if defined(MESH_COMPANION_SCREEN_STARTUP_BYTES) +// The target may replace the generic UI allowance with a tighter measured +// bound. Keep the check next to the actual allocations so a future screen +// growth cannot silently invalidate its runtime-RAM budget. ESP32's checked +// heap allocator can consume up to 16 bytes per allocation. +static_assert(sizeof(SplashScreen) + sizeof(HomeScreen) + + sizeof(MsgPreviewScreen) + 3 * 16 + <= MESH_COMPANION_SCREEN_STARTUP_BYTES, + "Increase MESH_COMPANION_SCREEN_STARTUP_BYTES and its RAM budget"); +#endif + void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNodePrefs* node_prefs) { _display = display; _sensors = sensors; diff --git a/examples/simple_room_server/FloodRuleEngine.cpp b/examples/simple_room_server/FloodRuleEngine.cpp index 1dc4274c8..38db9c39a 100644 --- a/examples/simple_room_server/FloodRuleEngine.cpp +++ b/examples/simple_room_server/FloodRuleEngine.cpp @@ -3,6 +3,7 @@ #if MESH_ENABLE_ROOM_FLOOD_RULE_ENGINE #include +#include #include #include @@ -394,8 +395,13 @@ static bool verifyWrittenFile(FILESYSTEM* fs, const char* path, } // namespace -FloodRuleEngine::FloodRuleEngine() : _fs(NULL), _regions(NULL) { - memset(_entries, 0, sizeof(_entries)); +FloodRuleEngine::FloodRuleEngine() + : _fs(NULL), _regions(NULL), + _entries(new (std::nothrow) Entry[RULE_SLOTS]) { + // MyMesh is constructed before setup(), while the internal heap is + // unfragmented. The runtime RAM gate budgets this exact allocation. + if (_entries == NULL) abort(); + memset(_entries, 0, sizeof(Entry) * RULE_SLOTS); } void FloodRuleEngine::begin(FILESYSTEM* fs, RegionMap* regions) { @@ -405,7 +411,7 @@ void FloodRuleEngine::begin(FILESYSTEM* fs, RegionMap* regions) { } void FloodRuleEngine::seedDefaults() { - memset(_entries, 0, sizeof(_entries)); + memset(_entries, 0, sizeof(Entry) * RULE_SLOTS); Entry& entry = _entries[0]; entry.active = true; entry.payload_type = PAYLOAD_TYPE_OTA; @@ -423,7 +429,7 @@ void FloodRuleEngine::load() { enum class FileState : uint8_t { Missing, Valid, Invalid, Unreadable }; auto loadFile = [this](const char* path) -> FileState { - memset(_entries, 0, sizeof(_entries)); + memset(_entries, 0, sizeof(Entry) * RULE_SLOTS); if (!_fs->exists(path)) return FileState::Missing; File file = openRead(_fs, path); if (!file) return FileState::Unreadable; @@ -655,7 +661,7 @@ void FloodRuleEngine::load() { && file.available() == 0; } file.close(); - if (!success) memset(_entries, 0, sizeof(_entries)); + if (!success) memset(_entries, 0, sizeof(Entry) * RULE_SLOTS); return success ? FileState::Valid : FileState::Invalid; }; @@ -1746,7 +1752,7 @@ void FloodRuleEngine::remove(const char* args, char* reply) { if (asciiEqual(selector, "all")) { Entry previous[RULE_SLOTS]; memcpy(previous, _entries, sizeof(previous)); - memset(_entries, 0, sizeof(_entries)); + memset(_entries, 0, sizeof(Entry) * RULE_SLOTS); if (!save()) { memcpy(_entries, previous, sizeof(_entries)); copyString(reply, "Err - unable to save flood filter", 160); diff --git a/examples/simple_room_server/FloodRuleEngine.h b/examples/simple_room_server/FloodRuleEngine.h index b5ab44e03..e1456dd31 100644 --- a/examples/simple_room_server/FloodRuleEngine.h +++ b/examples/simple_room_server/FloodRuleEngine.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -71,9 +72,16 @@ private: bool rate_window_active; }; + // This table is 6,200 bytes. Keep it in the constructor-time heap rather + // than inside the global MyMesh object: classic ESP32 has a small static + // DRAM window but substantially more internal heap at this point in boot. + // Allocation is deliberately all-or-nothing. A node must never fall back + // to silently running without its configured flood rules. + static_assert(sizeof(Entry) == 200, + "Update the flood-rule runtime RAM budget in check_firmware_ram.py"); FILESYSTEM* _fs; RegionMap* _regions; - Entry _entries[RULE_SLOTS]; + Entry* _entries; void seedDefaults(); void load(); diff --git a/scripts/check_firmware_ram.py b/scripts/check_firmware_ram.py index 2ee5232b1..959e90384 100644 --- a/scripts/check_firmware_ram.py +++ b/scripts/check_firmware_ram.py @@ -98,11 +98,22 @@ def requirements(platform, defines, target): engine = integer(defines, "MESH_ENABLE_FLOOD_RULE_ENGINE", int(platform != "STM32_PLATFORM")) slots = integer(defines, "FLOOD_PACKET_FILTER_SLOTS", 63 if engine else 16) parts["flood_filter_table"] = slots * (200 if engine else 40) + 16 + if "room_server" in target.lower() and integer( + defines, "MESH_ENABLE_ROOM_FLOOD_RULE_ENGINE", 0): + # FloodRuleEngine keeps all 31 persisted rules in one startup heap + # allocation. Its C++ assertion pins Entry to the budgeted 200 bytes. + parts["room_flood_rule_table"] = 31 * 200 + 16 if "ENABLE_OTA" in defines and "OTA_HEAP_CONTEXT" in defines: parts["ota_context"] = 16384 + 16 if display and display != "NullDisplayDriver": parts["display_pixels_and_driver"] = display_heap - parts["screen_objects_and_history"] = 8192 if companion else 2048 + screen_budget = integer(defines, "MESH_COMPANION_SCREEN_STARTUP_BYTES", 0) + if companion and screen_budget: + # A target using this override has a source-side static assertion + # that covers its concrete startup screens and allocator overhead. + parts["screen_objects_and_history"] = screen_budget + else: + parts["screen_objects_and_history"] = 8192 if companion else 2048 if companion: # ui-new retains 32 previews in one heap allocation. The baseline # covers 78 bytes per message; budget larger buffers explicitly, @@ -113,7 +124,7 @@ def requirements(platform, defines, target): small_font = integer(defines, "UI_SMALL_MESSAGE_FONT", int(small_display)) default_preview = 161 if small_font else 78 extra = max(0, integer(defines, "UI_MSG_PREVIEW_SIZE", default_preview) - 78) - if extra: + if extra and not screen_budget: parts["expanded_message_previews"] = 32 * ((extra + 7) // 8) * 8 parts["allocation_and_transient_margin"] = 16384 if platform == "ESP32_PLATFORM" else 4096 required = sum(parts.values()) @@ -124,7 +135,8 @@ def requirements(platform, defines, target): largest = max(display_heap, parts["radio_packet_pool"], 8192 if platform == "ESP32_PLATFORM" else 0) if "expanded_message_previews" in parts: largest = max(largest, 8192 + parts["expanded_message_previews"]) - for name in ("client_table", "flood_filter_table", "ota_context"): + for name in ("client_table", "flood_filter_table", "room_flood_rule_table", + "ota_context", "screen_objects_and_history"): largest = max(largest, parts.get(name, 0)) return {"required_heap_bytes": required, "required_contiguous_bytes": largest, "components": parts, "display": display, "full_companion": full} diff --git a/src/helpers/rp2040/SerialBLEInterface.cpp b/src/helpers/rp2040/SerialBLEInterface.cpp index ade4bbc10..c372b8b08 100644 --- a/src/helpers/rp2040/SerialBLEInterface.cpp +++ b/src/helpers/rp2040/SerialBLEInterface.cpp @@ -151,7 +151,7 @@ void SerialBLEInterface::enable() { if (_isEnabled) return; _isEnabled = true; clearBuffers(); - BLE.startAdvertising(true); + BLE.startAdvertising(); } void SerialBLEInterface::disconnect() { diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index f936dee85..0d195a423 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -421,6 +421,9 @@ extends = env:heltec_tracker_v2_companion_radio_wifi_femon board_build.flash_mode = ${esp32_s3_full.board_build.flash_mode} build_flags = ${env:heltec_tracker_v2_companion_radio_wifi_femon.build_flags} + ; Bound by the ui-new source assertion: Splash + Home + full 32-message + ; preview history and ESP32 allocator overhead, without reducing UI state. + -D MESH_COMPANION_SCREEN_STARTUP_BYTES=9808 -D MESHCORE_REQUIRES_COMPANION_RADIO_FULL=1 [env:heltec_tracker_v2_companion_radio_wifi_femoff] diff --git a/variants/lilygo_tbeam_SX1262/platformio.ini b/variants/lilygo_tbeam_SX1262/platformio.ini index 2f5d94e61..8a9cecc1b 100644 --- a/variants/lilygo_tbeam_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_SX1262/platformio.ini @@ -1,8 +1,15 @@ [LilyGo_TBeam_SX1262] extends = esp32_base board = ttgo-t-beam +; MeshCore and its source-built dependencies do not throw C++ exceptions. +; Avoid linking the unused exception runtime and use the supported compact +; ROM formatter so the feature-complete dual-OTA observer remains deployable. +build_unflags = + -fexceptions build_flags = ${esp32_base.build_flags} + -fno-exceptions + -D PORTABLE_ESP32_ROM_NANO_FORMAT=1 -I variants/lilygo_tbeam_SX1262 -D TBEAM_SX1262 -D P_LORA_DIO_0=26 diff --git a/variants/lilygo_tbeam_SX1276/platformio.ini b/variants/lilygo_tbeam_SX1276/platformio.ini index 92ae1843a..caa440390 100644 --- a/variants/lilygo_tbeam_SX1276/platformio.ini +++ b/variants/lilygo_tbeam_SX1276/platformio.ini @@ -1,8 +1,15 @@ [LilyGo_TBeam_SX1276] extends = esp32_base board = ttgo-t-beam +; MeshCore and its source-built dependencies do not throw C++ exceptions. +; Avoid linking the unused exception runtime and use the supported compact +; ROM formatter so the feature-complete dual-OTA observer remains deployable. +build_unflags = + -fexceptions build_flags = ${esp32_base.build_flags} + -fno-exceptions + -D PORTABLE_ESP32_ROM_NANO_FORMAT=1 -I variants/lilygo_tbeam_SX1276 -D TBEAM_SX1276 -D P_LORA_DIO_0=26 diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index 3a0c23c1b..12cd3ca7f 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -122,7 +122,9 @@ build_src_filter = ${rpi_picow.build_src_filter} +<../examples/companion_radio/*.cpp> lib_deps = ${rpi_picow.lib_deps} densaugeo/base64 @ ~1.4.0 -lib_ignore = BLE +lib_ignore = + ${rp2040_base.lib_ignore} + BLE [env:PicoW_terminal_chat] extends = rpi_picow diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index 0a3884ce9..d902668b8 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -49,6 +49,9 @@ build_flags = ; G2 roles remain below each legacy 0x140000 (1.25 MiB) application slot. -D LIGHTWEIGHT_WIFI_OTA=1 -D WEBCONFIG_DISABLED=1 + ; The ESP32-S3 ROM formatter preserves the normal libc ABI while keeping + ; the complete portable LoRa-OTA repeater inside its legacy app slot. + -D PORTABLE_ESP32_ROM_NANO_FORMAT=1 build_src_filter = ${esp32_base.build_src_filter} +<../variants/station_g2> +