diff --git a/build.sh b/build.sh index 06cb43fd..41621b8b 100755 --- a/build.sh +++ b/build.sh @@ -63,6 +63,7 @@ SUPPORTED_PLATFORM_PATTERN='ESP32_PLATFORM|NRF52_PLATFORM|STM32_PLATFORM|RP2040_ OUTPUT_DIR="out" ESP32_LORA_OTA_APP_LIMIT=$((0x150000 - 0x10000)) ESP32_FULL_MAX_NEIGHBOURS=254 +ESP32_FULL_DRAM_LIMITED_MAX_NEIGHBOURS=50 FALLBACK_VERSION_PREFIX="dev" FALLBACK_VERSION_DATE_FORMAT='+%Y-%m-%d-%H-%M' @@ -82,8 +83,8 @@ Commands: build-firmware : Build the firmware for the given build target. build-firmwares: Build all firmwares for all targets. build-firmwares-logging-matrix: Build all firmwares in standard, logging, MQTT, FULL ESP32 MQTT, and FULL ESP32 logging (no MQTT) profiles, logging each target under out/build-logs/ and continuing after failures. - build-full-esp32-firmwares: Build only feature-complete ESP32 MQTT profiles with 254 neighbors, LoRa OTA, and expanded dual-OTA partitions. - build-full-esp32-logging-firmwares: Build only feature-complete ESP32 profiles with 254 neighbors, logging, MQTT disabled, LoRa OTA, and expanded dual-OTA partitions. + build-full-esp32-firmwares: Build only feature-complete ESP32 MQTT profiles with up to 254 neighbors, LoRa OTA, and expanded dual-OTA partitions. + build-full-esp32-logging-firmwares: Build only feature-complete ESP32 profiles with up to 254 neighbors, logging, MQTT disabled, LoRa OTA, and expanded dual-OTA partitions. build-matching-firmwares : Build all firmwares for build targets containing the string given for . build-companion-firmwares: Build all companion firmwares for all build targets. build-repeater-firmwares: Build all repeater firmwares for all build targets. @@ -1738,6 +1739,16 @@ requires_esp32_companion_full_ota_fallback() { esac } +requires_esp32_full_dram_limited_neighbors() { + # These classic ESP32 MQTT observers already declare a 50-entry table because + # their GPS, TLS, and OTA state leave little internal DRAM. FULL restores flash- + # limited features, but must not override that explicit RAM safety limit. + case "${1,,}" in + tbeam_sx1262_repeater_observer_mqtt|tbeam_sx1262_room_server_observer_mqtt|tbeam_sx1276_repeater_observer_mqtt|tbeam_sx1276_room_server_observer_mqtt) return 0 ;; + *) return 1 ;; + esac +} + is_lora_ota_build() { local env_name=$1 local env_name_lc=${env_name,,} @@ -1879,6 +1890,7 @@ apply_esp32_lora_ota_size_profile() { apply_esp32_full_size_profile() { local env_name=$1 + local max_neighbours=$ESP32_FULL_MAX_NEIGHBOURS if [ "$ESP32_FULL_BUILD" != "1" ] || ! supports_esp32_full_build "$env_name"; then return 0 @@ -1890,9 +1902,15 @@ apply_esp32_full_size_profile() { export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -UWEBCONFIG_DISABLED -DWIFI_OTA_SEEDER=1" # Keep ordinary builds at their board-defined neighbor capacity. FULL builds - # use the largest table supported by the one-byte neighbor discovery indexes. - append_platformio_build_unflags "-DMAX_NEIGHBOURS=50 -DMAX_NEIGHBOURS=8" - export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMAX_NEIGHBOURS=${ESP32_FULL_MAX_NEIGHBOURS}" + # normally use the largest table supported by the one-byte neighbor discovery + # indexes. Explicitly DRAM-limited classic ESP32 observers retain 50 entries. + if requires_esp32_full_dram_limited_neighbors "$env_name"; then + max_neighbours=$ESP32_FULL_DRAM_LIMITED_MAX_NEIGHBOURS + append_platformio_build_unflags "-DMAX_NEIGHBOURS=8" + else + append_platformio_build_unflags "-DMAX_NEIGHBOURS=50 -DMAX_NEIGHBOURS=8" + fi + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMAX_NEIGHBOURS=${max_neighbours}" # Restore the full ElegantOTA implementation only when the target already # declares its dependency. Some ESP32-C6 targets intentionally have no @@ -2847,14 +2865,14 @@ run_full_esp32_profile() { fi if [ "$logging_mode" = "on" ]; then - echo "${profile_label}: building ${#full_targets[@]} feature-complete ESP32 target(s) with ${ESP32_FULL_MAX_NEIGHBOURS} neighbors, logging on, MQTT off, and expanded dual-OTA partitions." + echo "${profile_label}: building ${#full_targets[@]} feature-complete ESP32 target(s) with up to ${ESP32_FULL_MAX_NEIGHBOURS} neighbors (target DRAM limits apply), logging on, MQTT off, and expanded dual-OTA partitions." echo "FULL logging artifacts exclude MQTT, include LoRa OTA, and use filename form: name-full-logging-ota-version." MESHDEBUG_OVERRIDE="on" PACKET_LOGGING_OVERRIDE="on" MQTT_BRIDGE_OVERRIDE="off" FIRMWARE_FILENAME_INFIX="full-logging" else - echo "${profile_label}: building ${#full_targets[@]} feature-complete ESP32 MQTT target(s) with ${ESP32_FULL_MAX_NEIGHBOURS} neighbors, logging off, and expanded dual-OTA partitions." + echo "${profile_label}: building ${#full_targets[@]} feature-complete ESP32 MQTT target(s) with up to ${ESP32_FULL_MAX_NEIGHBOURS} neighbors (target DRAM limits apply), logging off, and expanded dual-OTA partitions." echo "FULL artifacts include MQTT and LoRa OTA and use filename form: name-full-ota-version." MESHDEBUG_OVERRIDE="off" PACKET_LOGGING_OVERRIDE="off" diff --git a/docs/WiFi.md b/docs/WiFi.md index ea591733..35656e99 100644 --- a/docs/WiFi.md +++ b/docs/WiFi.md @@ -335,15 +335,16 @@ role. | Standard | Uses the selected target's role. Ordinary portable ESP32 repeater/room-server artifacts omit WebConfig to fit the legacy app slot. Explicit MQTT and WiFi-companion targets still use WiFi. | | Logging | Enables USB/debug packet logging and disables the MQTT bridge. Logging output is not an MQTT uplink. | | MQTT | Builds explicit MQTT observer or WiFi-companion-MQTT targets with USB packet logging off. | -| FULL ESP32 | Uses the board's MQTT target with logging off, expanded dual-OTA partitions, 254 neighbors, LoRa OTA, and full-size ESP32 features such as WebConfig where supported. | -| FULL ESP32 logging | Uses the board's non-MQTT target with debug and packet logging enabled, expanded dual-OTA partitions, 254 neighbors, and LoRa OTA. | +| FULL ESP32 | Uses the board's MQTT target with logging off, expanded dual-OTA partitions, up to 254 neighbors, LoRa OTA, and full-size ESP32 features such as WebConfig where supported. Classic T-Beam MQTT observers retain their 50-entry table because they have no PSRAM. | +| FULL ESP32 logging | Uses the board's non-MQTT target with debug and packet logging enabled, expanded dual-OTA partitions, up to 254 neighbors, and LoRa OTA. | | LoRa-OTA no-external-sensors | A lean repeater image with no MQTT; ESP32 builds retain the compact on-demand browser WiFi uploader and 254 neighbors. | The interactive Option 1 **FULL everything** choice selects the FULL logging profile: logging is enabled and MQTT is disabled. The standalone FULL ESP32 profile and Profile 4 of the five-profile matrix use the matching MQTT target -instead. Both FULL profiles include LoRa OTA, WebConfig where supported, 254 -neighbors, and expanded dual-OTA partitions. +instead. Both FULL profiles include LoRa OTA, WebConfig where supported, up to +254 neighbors, and expanded dual-OTA partitions. Target-specific internal-DRAM +limits still apply. FULL images change the ESP32 partition layout. Flash the matching `*-merged.bin` once when installing that layout. A partition-layout change can diff --git a/src/helpers/ota/OtaManager.cpp b/src/helpers/ota/OtaManager.cpp index 72341841..f6bbf573 100644 --- a/src/helpers/ota/OtaManager.cpp +++ b/src/helpers/ota/OtaManager.cpp @@ -10,6 +10,19 @@ namespace mesh { namespace ota { +OtaManager::~OtaManager() { + free(_catalog_heap); +} + +bool OtaManager::expandCatalog() { + if (_catalog_heap || OTA_INLINE_CATALOG >= OTA_MAX_CATALOG) return false; + CatRow* expanded = static_cast(malloc(sizeof(CatRow) * OTA_MAX_CATALOG)); + if (!expanded) return false; + memcpy(expanded, _catalog_inline, sizeof(CatRow) * _n_cat); + _catalog_heap = expanded; + return true; +} + uint8_t* OtaManager::ensureSourceLeaves() { #if defined(ESP32_PLATFORM) if (!_src_leaves) { @@ -539,25 +552,27 @@ void OtaManager::handleHave(const uint8_t* m, uint16_t n) { uint32_t target = rd_u32le(row + 4), fwver = rd_u32le(row + 8); uint8_t codec = row[12], flags = row[13]; uint32_t have_count = rd_u16le(row + 14); // this source's progress + CatRow* catalog = catalogData(); int slot = -1, lru = 0; // upsert into the catalog (dedup by mid) for (int i = 0; i < _n_cat; i++) { - if (memcmp(_catalog[i].mid, mid, 4) == 0) { slot = i; break; } - if (_catalog[i].last_ms < _catalog[lru].last_ms) lru = i; + if (memcmp(catalog[i].mid, mid, 4) == 0) { slot = i; break; } + if (catalog[i].last_ms < catalog[lru].last_ms) lru = i; } if (slot < 0) { - slot = (_n_cat < OTA_MAX_CATALOG) ? _n_cat++ : lru; - _catalog[slot] = CatRow{}; - memcpy(_catalog[slot].mid, mid, 4); - memcpy(_catalog[slot].seeders[0], hv.seeder_id, 4); - _catalog[slot].n_seeders = 1; + if (_n_cat >= catalogCapacity() && expandCatalog()) catalog = catalogData(); + slot = (_n_cat < catalogCapacity()) ? _n_cat++ : lru; + catalog[slot] = CatRow{}; + memcpy(catalog[slot].mid, mid, 4); + memcpy(catalog[slot].seeders[0], hv.seeder_id, 4); + catalog[slot].n_seeders = 1; } else { - CatRow& cc = _catalog[slot]; // count DISTINCT sources (no double-count) + CatRow& cc = catalog[slot]; // count DISTINCT sources (no double-count) bool known = false; for (uint8_t k = 0; k < cc.n_seeders; k++) if (memcmp(cc.seeders[k], hv.seeder_id, 4) == 0) { known = true; break; } if (!known && cc.n_seeders < OTA_CAT_SEEDERS) memcpy(cc.seeders[cc.n_seeders++], hv.seeder_id, 4); } - CatRow& c = _catalog[slot]; + CatRow& c = catalog[slot]; c.target_id = target; c.fw_version = fwver; c.codec = codec; c.flags = flags; c.last_ms = _now_ms; if (have_count > c.have_max) c.have_max = have_count; // best-known progress among sources if (wantRow(mid, target, codec, flags)) startFetch(mid, target); @@ -566,9 +581,10 @@ void OtaManager::handleHave(const uint8_t* m, uint16_t n) { void OtaManager::deferCatalog(const uint8_t mid[4], uint32_t until_ms) { if (!mid) return; + CatRow* catalog = catalogData(); for (uint8_t i = 0; i < _n_cat; i++) { - if (memcmp(_catalog[i].mid, mid, 4) == 0) { - _catalog[i].retry_after_ms = until_ms; + if (memcmp(catalog[i].mid, mid, 4) == 0) { + catalog[i].retry_after_ms = until_ms; return; } } diff --git a/src/helpers/ota/OtaManager.h b/src/helpers/ota/OtaManager.h index c26c2e91..6db9420c 100644 --- a/src/helpers/ota/OtaManager.h +++ b/src/helpers/ota/OtaManager.h @@ -93,6 +93,16 @@ typedef bool (*ServeReadFn)(void* ctx, uint32_t off, uint8_t* buf, uint32_t len) #ifndef OTA_MAX_CATALOG #define OTA_MAX_CATALOG 255 // protocol maximum; large SD seeders must remain browsable remotely #endif +#ifndef OTA_INLINE_CATALOG +#define OTA_INLINE_CATALOG 12 // keep the common case static; expand to OTA_MAX_CATALOG only on demand +#endif +#if OTA_INLINE_CATALOG < 1 +#error "OTA_INLINE_CATALOG must be at least 1" +#endif +#if OTA_INLINE_CATALOG > OTA_MAX_CATALOG +#undef OTA_INLINE_CATALOG +#define OTA_INLINE_CATALOG OTA_MAX_CATALOG +#endif #ifndef OTA_QUERY_MIN_MS #define OTA_QUERY_MIN_MS 300 // min delay before sending a catalog query (overhear-suppression window) #endif @@ -128,6 +138,11 @@ typedef bool (*ServeReadFn)(void* ctx, uint32_t off, uint8_t* buf, uint32_t len) class OtaManager { public: + OtaManager() = default; + ~OtaManager(); + OtaManager(const OtaManager&) = delete; + OtaManager& operator=(const OtaManager&) = delete; + // PAUSED: a folder-destination write failed mid-transfer (the seeder link dropped). Progress is held on // the host; the manager stops requesting and does NOT fall back to RAM/flash. resumeStaged() (called on // reconnect) re-STATs the host file, recomputes which blocks are missing, and resumes. @@ -319,7 +334,7 @@ public: uint32_t retry_after_ms; // per-image archive retry deadline (0 = eligible) }; uint8_t catalogCount() const { return _n_cat; } - const CatRow* catalogRow(uint8_t i) const { return i < _n_cat ? &_catalog[i] : nullptr; } + const CatRow* catalogRow(uint8_t i) const { return i < _n_cat ? &catalogData()[i] : nullptr; } uint8_t sourceCount() const { return _n_src; } // distinct OTA sources (beacon senders) heard void deferCatalog(const uint8_t mid[4], uint32_t until_ms); bool catalogReady(const CatRow* row, uint32_t now_ms) const { @@ -327,6 +342,11 @@ public: } private: + CatRow* catalogData() { return _catalog_heap ? _catalog_heap : _catalog_inline; } + const CatRow* catalogData() const { return _catalog_heap ? _catalog_heap : _catalog_inline; } + uint16_t catalogCapacity() const { return _catalog_heap ? OTA_MAX_CATALOG : OTA_INLINE_CATALOG; } + bool expandCatalog(); + void emit(const uint8_t* b, uint16_t n, bool flood) { if (_send && n) _send(_ctx, b, n, flood); } void handleAdv(const uint8_t* m, uint16_t n); // beacon -> sources table (+ query if interested) void handleQuery(const uint8_t* m, uint16_t n); // serve: reply OTA_HAVE catalog @@ -464,7 +484,8 @@ private: }; Source _sources[OTA_MAX_SOURCES]; uint8_t _n_src = 0; - CatRow _catalog[OTA_MAX_CATALOG]; + CatRow _catalog_inline[OTA_INLINE_CATALOG]; + CatRow* _catalog_heap = nullptr; uint8_t _n_cat = 0; uint32_t _now_ms = 0; // coarse clock (fed by set_clock; for ages/LRU/jitter) };