From 61b584f94653b420678f6a1451c3f4c284ce9fce Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 27 Oct 2026 14:05:01 -0700 Subject: [PATCH] Extend PSRAM offload to supported ESP32 boards after startup --- docs/WiFi.md | 12 +- docs/research/firmware_memory_budget.md | 19 +- .../esp32_psram_storage_2026-10-27.md | 96 + .../esp32_psram_storage_2026-10-27.safe.json | 1635 +++++++++++++++++ examples/simple_repeater/MyMesh.cpp | 165 +- examples/simple_repeater/MyMesh.h | 56 +- scripts/check_firmware_ram.py | 9 +- src/helpers/ESP32WsTransportFix.cpp | 4 +- src/helpers/FloodAdvertLimiter.h | 15 +- src/helpers/RepeaterPsramStorage.h | 29 +- test/test_firmware_ram.py | 83 +- test/test_flood_advert_psram.py | 11 +- test/test_flood_policy_capacity.py | 4 + test/test_repeater_scope_psram.py | 78 +- test/test_ws_transport_psram.py | 17 +- 15 files changed, 2067 insertions(+), 166 deletions(-) create mode 100644 docs/test-results/esp32_psram_storage_2026-10-27.md create mode 100644 docs/test-results/esp32_psram_storage_2026-10-27.safe.json diff --git a/docs/WiFi.md b/docs/WiFi.md index 38ab3b4f0..7155e501d 100644 --- a/docs/WiFi.md +++ b/docs/WiFi.md @@ -886,11 +886,13 @@ send retries only that broker every 30 seconds; successful brokers keep their normal status interval. Each slot has a stable, distinct client ID, including when two slots point at the same broker. -G3 also places the SDK's two padded WebSocket buffers per initialized MQTT -client in PSRAM, saving 2,050 internal bytes per client when PSRAM allocation -succeeds. G3 repeaters move their complete scope tables and advert-limit -history there too. Capacities, saved formats, task stacks and DMA reserves -remain unchanged; safe internal fallback is retained. +PSRAM-enabled ESP32 builds also place the SDK's two padded WebSocket buffers +per initialized MQTT client in PSRAM, saving 2,050 internal bytes per client when PSRAM allocation +succeeds. ESP32-S3 repeaters with PSRAM move their complete scope tables and +advert-limit history there too, allocating after QSPI/OPI PSRAM initialization. +Capacities, saved formats, task stacks and DMA reserves remain unchanged; +safe internal fallback is retained. Classic ESP32 T-Beam keeps its existing +table storage while receiving the MQTT buffer change. Outbox diagnostics are cached by the network owner, so `get mqtt.stats` does not wait for an SDK mutex held by a network write. `rec:N/write` records a diff --git a/docs/research/firmware_memory_budget.md b/docs/research/firmware_memory_budget.md index 817c8b80e..bda0f12d2 100644 --- a/docs/research/firmware_memory_budget.md +++ b/docs/research/firmware_memory_budget.md @@ -190,22 +190,29 @@ other tasks. Available linked internal heap cannot establish how much DMA-capable heap remains or how it is fragmented after boot. Only runtime observations can establish that. -G3 repeater builds also place their channel/scope tables and full advert-source -history in PSRAM. Their capacities and persisted formats remain unchanged. +ESP32-S3 repeater builds with PSRAM also place their channel/scope tables and +full advert-source history there. Their capacities and persisted formats +remain unchanged. The RAM check still reserves internal fallback for both complete tables; moving them out of BSS does not relax startup admission or the DMA reserve. If neither heap can safely hold a complete table, startup reports the failed allocation and stops rather than running with reduced filtering protection. -Other targets keep their existing storage. `MESH_REPEATER_PSRAM_TABLES=0` -restores inline storage for a custom G3 build. +Allocation occurs in `MyMesh::begin`, after Arduino initializes either QSPI or +OPI PSRAM. Global constructors retain only the owners' pointers. Other targets +keep their existing storage. `MESH_REPEATER_PSRAM_TABLES=0` restores inline +storage for a custom build. The checker resolves the actual board MCU so the +complete fallback cannot disappear from a non-G3 S3's budget. -G3's IDF 4 WebSocket wrapper also prefers PSRAM for its two padded 1,025-byte -SDK buffers per initialized MQTT client, including clients using plain MQTT. +The IDF 4 WebSocket wrapper on any PSRAM-enabled ESP32 build also prefers PSRAM +for its two padded 1,025-byte SDK buffers per initialized MQTT client, including +clients using plain MQTT. Five initialized clients move 10,250 bytes of payload from internal RAM. Transport objects, task stacks, DMA buffers and radio packet queues keep their existing placement. The existing extra-byte overflow fix and guarded internal fallback remain in force. These figures describe allocation placement, not a measurement of running free heap or qualification of five simultaneous brokers. +See the [cross-board checks](../test-results/esp32_psram_storage_2026-10-27.md) +for covered families and the distinction between linked and physical results. Maximum-load qualification still needs physical boot and sustained reconnect, publish and configuration-change tests with the selected number of brokers, diff --git a/docs/test-results/esp32_psram_storage_2026-10-27.md b/docs/test-results/esp32_psram_storage_2026-10-27.md new file mode 100644 index 000000000..940ec1c73 --- /dev/null +++ b/docs/test-results/esp32_psram_storage_2026-10-27.md @@ -0,0 +1,96 @@ + + + + +# ESP32 PSRAM offload, 2026-10-27 + +V4, G2 Full and T-Deck Full each free **36,792 bytes of internal RAM** by moving +their scope tables and advert history to PSRAM. MQTT buffers can save another +**2,050 bytes per initialized client**. Features, capacities and saved formats +remain unchanged. No board was flashed during these checks. + +The [measured evidence](esp32_psram_storage_2026-10-27.safe.json) binds source +hashes, matched builds, capabilities and the two existing packaging failures. +The paired measurements isolate these PSRAM changes on baseline `74c03cb30`. +Concurrent Companion PIN and startup-screen changes were integrated afterward; +a further G2 Full build at `6a5a04b1b` passes with the same RAM result and +15/15 capability checks. It fits its OTA slots at 2,125,736 application bytes. + +## Enabled by default + +ESP32-S3 repeater builds with `BOARD_HAS_PSRAM` move complete scope tables and +advert history to PSRAM. Existing explicit RTC/IRAM advert backends retain +priority. This covers these configured board families and their repeater aliases: + +| Board families | PSRAM interface | +| --- | --- | +| Station G2/G3, T-Deck, T-ETH Elite, T-Beam 1W | OPI | +| V4 R8, RC32, E213, E290, T190 | OPI | +| W12, Wio Tracker L2, XIAO ESP32-S3/WIO, ThinkNode M7/M9 | OPI | +| Original SenseCAP Indicator | OPI | +| Standard V4, T3-S3 SX1262/SX1276, EoRa-S3 | QSPI | +| T-Beam S3 Supreme, Waveshare S3 LR1121, Indicator N16R2 | QSPI | + +The IDF 4 WebSocket wrapper also prefers PSRAM on all PSRAM-enabled ESP32 +builds that initialize MQTT clients, including companions, room servers and +classic T-Beam. It retains both 1,025-byte padded buffers per initialized +client, including plain MQTT clients. IDF 5 remains a passthrough. + +Classic T-Beam keeps its established table placement. Its qualified Full +MQTT profiles already store all 17,920 advert-history bytes in separate +word-accessible IRAM; only 3,162 scope-table bytes remain. This extension +retains that backend and its cache-workaround flags. Boards without compiled +PSRAM support retain existing allocation paths. + +## Startup and fallback + +QSPI PSRAM initializes in `initArduino()`, after global constructors. The +tables now allocate in `MyMesh::begin`, before routing and persistence start, +on both QSPI and OPI boards. Constructors retain only pointers. Getters never +allocate or move storage, and return complete array references so `sizeof`, +saved bytes, rollback and const access remain correct. + +Internal fallback must preserve the 16 KiB DMA reserve and hold each complete +table; otherwise startup reports failure and stops. The RAM check resolves +the actual board MCU and retains both full fallback allowances. Packet pools, +task stacks, ISR state and DMA buffers retain their existing placement. +`MESH_REPEATER_PSRAM_TABLES=0` restores inline table storage for custom builds. + +## Matched Full builds + +| Target | Internal heap before | Internal heap after | Gain | Capability checks | +| --- | ---: | ---: | ---: | ---: | +| V4 | 203,456 | 240,248 | 36,792 | 16/16 | +| G2 | 200,328 | 237,120 | 36,792 | 15/15 | +| T-Deck | 223,960 | 260,752 | 36,792 | 14/14 | + +All three pass compilation, RAM admission, dual-slot OTA fit and artifact +checks with identical capability lists and no declared reductions. The RAM +model still reserves 36,828 bytes for complete internal fallback, so its +conservative admission margins remain similar. These are linked capacity +measurements, not running free-heap or maximum broker-load measurements. + +Two packaging failures were reproduced before and after these changes: + +- G2 standard exceeds its old 1,310,720-byte slot: linked program size falls + from 1,375,509 to 1,374,093 bytes but still fails. G2 Full passes separately. +- Classic T-Beam Full MQTT compiles and passes RAM admission, but fails its + existing dual-slot OTA fit check. This image was not qualified or flashed. + +Builds used `build.sh build-firmware TARGET --full-exact` with +`--firmware-version v1.18.0.2-psram-test --profile cascade +--radio-preset usa-cascade-fixed --require-ota`; G2 legacy testing substituted +`--standard`. PlatformIO processes ran one at a time. + +## Software coverage + +- All 65 RAM-policy tests pass, including real-MCU selection and unchanged + fallback, reserve and contiguous-block thresholds. +- Scope storage, advert storage and existing capacity/RTC/IRAM tests pass + (23 methods), including constructor-no-allocation, QSPI startup, full + persisted-byte comparisons, rollback, lifetime and allocation failures. +- WebSocket tests pass three methods and 32 scenarios over seven platform + configurations, with padding, cleanup, reserve and IDF 4/5 coverage. + +Relevant native storage/transport tests run under ASan/UBSan. Physical boot, +RF timing, persistence and sustained MQTT load still need hardware validation. diff --git a/docs/test-results/esp32_psram_storage_2026-10-27.safe.json b/docs/test-results/esp32_psram_storage_2026-10-27.safe.json new file mode 100644 index 000000000..f3c5a719d --- /dev/null +++ b/docs/test-results/esp32_psram_storage_2026-10-27.safe.json @@ -0,0 +1,1635 @@ +{ + "schema_version": 1, + "baseline_commit": "74c03cb30f76551d8eedbe2b78ec98345c514d09", + "physical_validation_performed": false, + "maximum_runtime_load_qualified": false, + "websocket_payload_bytes_per_initialized_client": 2050, + "websocket_payload_bytes_for_five_initialized_clients": 10250, + "psram_repeater_table_policy": "ESP32-S3 + BOARD_HAS_PSRAM + qualified repeater; deferred begin; existing RTC/IRAM backends retained", + "tests": { + "ram_policy_methods_passed": 65, + "scope_advert_capacity_rtc_iram_methods_passed": 23, + "ws_methods_passed": 3, + "ws_scenarios_passed": 32, + "ws_platform_configurations": 7, + "native_storage_transport_sanitizers": "ASan/UBSan", + "pio_execution": "single process; RAM config hook ran separately from builds" + }, + "matched_builds": { + "heltec_v4_repeater": { + "baseline": { + "memory": { + "schema_version": 1, + "target": "heltec_v4_repeater", + "platform": "ESP32_PLATFORM", + "mcu": "esp32s3", + "elf_sha256": "26a554d320b8d14d67d1eab59c74f6c83b286b2ff8b73f64deeccf261b1a39a5", + "passed": true, + "available_internal_bytes": 203456, + "largest_internal_region_bytes": 170688, + "required_heap_bytes": 201904, + "required_contiguous_bytes": 19168, + "components": { + "display_i2c_spi_and_probe": 4096, + "core_tasks_usb_filesystems": 24576, + "wireless_stacks": 49152, + "mqtt_startup_allowance": 24576, + "udp_bridge_and_socket": 5120, + "ota_source_scratch": 8192, + "browser_terminal_session": 2048, + "neighbor_history": 18448, + "host_kiss_codec": 2048, + "radio_packet_pool": 10240, + "client_table": 9104, + "flood_filter_table": 19168, + "ota_generic_source_buffers": 3616, + "ota_encoder_workspace": 1040, + "display_pixels_and_driver": 2048, + "screen_objects_and_history": 2048, + "allocation_and_transient_margin": 16384 + }, + "display": "HeltecV4Display", + "full_companion": false, + "mqtt": { + "budget_scope": "startup minimum with runtime connection admission", + "startup_allowance_bytes": 24576, + "startup_active_slot_allowance": 2, + "runtime_slot_count": 6, + "maximum_active_slots": 5, + "maximum_active_slots_without_psram": 2, + "worker_stack_bytes": 8192, + "client_task_stack_bytes": 6144, + "client_task_priority": 5, + "client_rx_tx_buffer_bytes": 1792, + "maximum_slot_stack_buffer_allowance_bytes": 48384, + "cold_start_allocation_allowance_bytes": 16384, + "cold_start_dma_admission_free_bytes": 32768, + "cold_start_dma_admission_contiguous_bytes": 8192, + "additional_broker_cold_start_dma_admission_contiguous_bytes": 20480, + "restart_allocation_allowance_bytes": 8192, + "restart_dma_admission_free_bytes": 24576, + "restart_dma_admission_contiguous_bytes": 8192, + "additional_broker_restart_dma_admission_contiguous_bytes": 12288, + "dma_admission_free_bytes": 16384, + "dma_admission_contiguous_bytes": 4096, + "publish_contiguous_bytes": 4096, + "publish_contiguous_without_psram_bytes": 8000, + "publish_contiguous_with_psram_bytes": 4096, + "tls_psram_admission_contiguous_bytes": 16000, + "additional_broker_cold_start_tls_psram_admission_contiguous_bytes": 32384, + "additional_broker_restart_tls_psram_admission_contiguous_bytes": 24192, + "additional_broker_tls_psram_admission_contiguous_bytes": 16000, + "tls_internal_admission_free_bytes": 61440, + "tls_record_allocation_bytes": 17408, + "additional_broker_cold_start_tls_internal_admission_contiguous_bytes": 41792, + "additional_broker_restart_tls_internal_admission_contiguous_bytes": 33600, + "tls_internal_admission_contiguous_bytes": 17408, + "additional_broker_tls_internal_admission_contiguous_bytes": 25408, + "maximum_runtime_load_qualified": false, + "physical_validation_performed": false, + "limitations": "Maximum TLS/WSS/outbox workload is not covered by the startup allowance; runtime free DMA heap and fragmentation must satisfy connection admission; additional brokers reserve publication headroom beside startup allocations." + }, + "regions": [ + { + "start": 1070333008, + "end": 1070503696 + }, + { + "start": 1070530560, + "end": 1070563328 + } + ], + "scope": "Linked capacity before runtime allocation; physical boot/load/soak validation remains required.", + "artifact_target": "heltec_v4_repeater", + "pio_environment": "heltec_v4_repeater_observer_mqtt", + "files": { + "heltec_v4_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3.bin": "785fe1fc26492aff14c10dadf27f52ce104bf50e5f5f6ab0595a4812530a5de8", + "heltec_v4_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3-merged.bin": "efa39aa3bd0a17aa5885f81dc6b7a37522be7dca6a6275e501c81b0e2b0516ee", + "heltec_v4_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3.capabilities.json": "75a95825edb208e5b2f600ad65798da382e3e59d4c237965b767afb0d730319e", + "heltec_v4_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3.meshtext-license.txt": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + }, + "capabilities": { + "artifact_target": "heltec_v4_repeater", + "build_profile": "full", + "build_recipe": { + "schema_version": 1, + "sha256": "f435455067a794665ec3cf09a1277c7b30526fc5ed53376512ff9ecbafb17750" + }, + "capabilities": [ + "bridge.espnow", + "bridge.udp", + "profile.full", + "release.full.same_partition_successor", + "radio2.kiss.host", + "radio2.kiss.usb", + "radio2.kiss.tcp", + "ota.update.wifi", + "ota.update.lora", + "cli.retry_preset", + "telemetry.history", + "ota.cli", + "web.webconfig", + "logging.usb.packets", + "logging.usb.control", + "logging.usb.output", + "display.heltec_v4.unified", + "bridge.udp.configuration" + ], + "ota_update_evidence": "firmware fits both OTA application slots; otadata present", + "ota_update_methods": [ + "wifi", + "lora" + ], + "ota_update_verified": true, + "platform": "ESP32_PLATFORM", + "platformio_env": "heltec_v4_repeater_observer_mqtt", + "reductions": [], + "schema_version": 2, + "target": "heltec_v4_repeater", + "third_party_notices": [ + { + "component": "espp-meshtext", + "file": "heltec_v4_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3.meshtext-license.txt", + "license": "MIT", + "sha256": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + ], + "verification": [ + { + "capability": "radio2.kiss.host", + "evidence": "_ZN4mesh8hostkiss5Codec4feedEh; _ZN4mesh8hostkiss5Codec11queueOutputEhPKhj; _ZN4mesh10Dispatcher22queueForeignRadioFrameEPKhjhjNS_20RadioProfileProtocolE", + "present": true, + "source": "linked ELF symbols" + }, + { + "capability": "radio2.kiss.usb", + "evidence": "Error: use set kiss on|off; Error: use set kiss.transport usb|tcp; USB: send KISS FEND to start", + "present": true, + "source": "packaged application" + }, + { + "capability": "radio2.kiss.tcp", + "evidence": "station TCP 5004", + "present": true, + "source": "packaged application" + }, + { + "capability": "bridge.espnow", + "evidence": "_ZN12ESPNowBridge5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "bridge.udp", + "evidence": "_ZN9UDPBridge5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.wifi", + "evidence": "Started: http://%s/update", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.lora", + "evidence": "image_hash MISMATCH after decode", + "present": true, + "source": "linked image" + }, + { + "capability": "cli.retry_preset", + "evidence": "retry.preset", + "present": true, + "source": "linked image" + }, + { + "capability": "telemetry.history", + "evidence": "telemetry.volt.i2c", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.cli", + "evidence": "OTA: status", + "present": true, + "source": "linked image" + }, + { + "capability": "web.webconfig", + "evidence": "start webconfig", + "present": true, + "source": "linked image" + }, + { + "capability": "logging.usb.packets", + "evidence": "%s: %s, len=%d (type=%d, route=%s, payload_len=%d)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.control", + "evidence": "OK - USB logging %s (saved)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.output", + "evidence": "OK - logging.output %s (saved)", + "present": true, + "source": "packaged application" + }, + { + "capability": "display.heltec_v4.unified", + "evidence": "meshcore.capability.display.heltec_v4.unified.v1", + "present": true, + "source": "packaged application" + }, + { + "capability": "bridge.udp.configuration", + "evidence": "udp.enabled", + "present": true, + "source": "packaged application" + } + ], + "verified": true + }, + "application_bytes": 2158824, + "application_slot_bytes": 6553600 + }, + "final": { + "memory": { + "schema_version": 1, + "target": "heltec_v4_repeater", + "platform": "ESP32_PLATFORM", + "mcu": "esp32s3", + "elf_sha256": "f03df26d6ed65237095dbd0513323b19291eaa172ea52b62c5f324d5a1101943", + "passed": true, + "available_internal_bytes": 240248, + "largest_internal_region_bytes": 207480, + "required_heap_bytes": 238732, + "required_contiguous_bytes": 19168, + "components": { + "display_i2c_spi_and_probe": 4096, + "core_tasks_usb_filesystems": 24576, + "wireless_stacks": 49152, + "mqtt_startup_allowance": 24576, + "udp_bridge_and_socket": 5120, + "ota_source_scratch": 8192, + "browser_terminal_session": 2048, + "neighbor_history": 18448, + "host_kiss_codec": 2048, + "radio_packet_pool": 10240, + "client_table": 9104, + "flood_filter_table": 19168, + "repeater_scope_storage_fallback": 18892, + "repeater_advert_storage_fallback": 17936, + "ota_generic_source_buffers": 3616, + "ota_encoder_workspace": 1040, + "display_pixels_and_driver": 2048, + "screen_objects_and_history": 2048, + "allocation_and_transient_margin": 16384 + }, + "display": "HeltecV4Display", + "full_companion": false, + "mqtt": { + "budget_scope": "startup minimum with runtime connection admission", + "startup_allowance_bytes": 24576, + "startup_active_slot_allowance": 2, + "runtime_slot_count": 6, + "maximum_active_slots": 5, + "maximum_active_slots_without_psram": 2, + "worker_stack_bytes": 8192, + "client_task_stack_bytes": 6144, + "client_task_priority": 5, + "client_rx_tx_buffer_bytes": 1792, + "maximum_slot_stack_buffer_allowance_bytes": 48384, + "cold_start_allocation_allowance_bytes": 16384, + "cold_start_dma_admission_free_bytes": 32768, + "cold_start_dma_admission_contiguous_bytes": 8192, + "additional_broker_cold_start_dma_admission_contiguous_bytes": 20480, + "restart_allocation_allowance_bytes": 8192, + "restart_dma_admission_free_bytes": 24576, + "restart_dma_admission_contiguous_bytes": 8192, + "additional_broker_restart_dma_admission_contiguous_bytes": 12288, + "dma_admission_free_bytes": 16384, + "dma_admission_contiguous_bytes": 4096, + "publish_contiguous_bytes": 4096, + "publish_contiguous_without_psram_bytes": 8000, + "publish_contiguous_with_psram_bytes": 4096, + "tls_psram_admission_contiguous_bytes": 16000, + "additional_broker_cold_start_tls_psram_admission_contiguous_bytes": 32384, + "additional_broker_restart_tls_psram_admission_contiguous_bytes": 24192, + "additional_broker_tls_psram_admission_contiguous_bytes": 16000, + "tls_internal_admission_free_bytes": 61440, + "tls_record_allocation_bytes": 17408, + "additional_broker_cold_start_tls_internal_admission_contiguous_bytes": 41792, + "additional_broker_restart_tls_internal_admission_contiguous_bytes": 33600, + "tls_internal_admission_contiguous_bytes": 17408, + "additional_broker_tls_internal_admission_contiguous_bytes": 25408, + "maximum_runtime_load_qualified": false, + "physical_validation_performed": false, + "limitations": "Maximum TLS/WSS/outbox workload is not covered by the startup allowance; runtime free DMA heap and fragmentation must satisfy connection admission; additional brokers reserve publication headroom beside startup allocations." + }, + "regions": [ + { + "start": 1070296216, + "end": 1070503696 + }, + { + "start": 1070530560, + "end": 1070563328 + } + ], + "scope": "Linked capacity before runtime allocation; physical boot/load/soak validation remains required.", + "artifact_target": "heltec_v4_repeater", + "pio_environment": "heltec_v4_repeater_observer_mqtt", + "files": { + "heltec_v4_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3.bin": "2262c682c4e1bafe1703a26cd97f3cf5aa18d5f0ebeb15ad8b352552f83cc128", + "heltec_v4_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3-merged.bin": "e2fad3e879b28d32dcb8983cf2c024dd2455475dedde125a461d2acbf09a61b8", + "heltec_v4_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3.capabilities.json": "b365e7b7864f49ab2888690b2b5d7e044a42d25450d2f3e1ff1ca1200e2b46da", + "heltec_v4_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3.meshtext-license.txt": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + }, + "capabilities": { + "artifact_target": "heltec_v4_repeater", + "build_profile": "full", + "build_recipe": { + "schema_version": 1, + "sha256": "b9db24a63c5eac6316114b8a05e3c09c3f9f0d01d9e2e29648b9ba5c57a96d64" + }, + "capabilities": [ + "bridge.espnow", + "bridge.udp", + "profile.full", + "release.full.same_partition_successor", + "radio2.kiss.host", + "radio2.kiss.usb", + "radio2.kiss.tcp", + "ota.update.wifi", + "ota.update.lora", + "cli.retry_preset", + "telemetry.history", + "ota.cli", + "web.webconfig", + "logging.usb.packets", + "logging.usb.control", + "logging.usb.output", + "display.heltec_v4.unified", + "bridge.udp.configuration" + ], + "ota_update_evidence": "firmware fits both OTA application slots; otadata present", + "ota_update_methods": [ + "wifi", + "lora" + ], + "ota_update_verified": true, + "platform": "ESP32_PLATFORM", + "platformio_env": "heltec_v4_repeater_observer_mqtt", + "reductions": [], + "schema_version": 2, + "target": "heltec_v4_repeater", + "third_party_notices": [ + { + "component": "espp-meshtext", + "file": "heltec_v4_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3.meshtext-license.txt", + "license": "MIT", + "sha256": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + ], + "verification": [ + { + "capability": "radio2.kiss.host", + "evidence": "_ZN4mesh8hostkiss5Codec4feedEh; _ZN4mesh8hostkiss5Codec11queueOutputEhPKhj; _ZN4mesh10Dispatcher22queueForeignRadioFrameEPKhjhjNS_20RadioProfileProtocolE", + "present": true, + "source": "linked ELF symbols" + }, + { + "capability": "radio2.kiss.usb", + "evidence": "Error: use set kiss on|off; Error: use set kiss.transport usb|tcp; USB: send KISS FEND to start", + "present": true, + "source": "packaged application" + }, + { + "capability": "radio2.kiss.tcp", + "evidence": "station TCP 5004", + "present": true, + "source": "packaged application" + }, + { + "capability": "bridge.espnow", + "evidence": "_ZN12ESPNowBridge5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "bridge.udp", + "evidence": "_ZN9UDPBridge5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.wifi", + "evidence": "Started: http://%s/update", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.lora", + "evidence": "image_hash MISMATCH after decode", + "present": true, + "source": "linked image" + }, + { + "capability": "cli.retry_preset", + "evidence": "retry.preset", + "present": true, + "source": "linked image" + }, + { + "capability": "telemetry.history", + "evidence": "telemetry.volt.i2c", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.cli", + "evidence": "OTA: status", + "present": true, + "source": "linked image" + }, + { + "capability": "web.webconfig", + "evidence": "start webconfig", + "present": true, + "source": "linked image" + }, + { + "capability": "logging.usb.packets", + "evidence": "%s: %s, len=%d (type=%d, route=%s, payload_len=%d)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.control", + "evidence": "OK - USB logging %s (saved)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.output", + "evidence": "OK - logging.output %s (saved)", + "present": true, + "source": "packaged application" + }, + { + "capability": "display.heltec_v4.unified", + "evidence": "meshcore.capability.display.heltec_v4.unified.v1", + "present": true, + "source": "packaged application" + }, + { + "capability": "bridge.udp.configuration", + "evidence": "udp.enabled", + "present": true, + "source": "packaged application" + } + ], + "verified": true + }, + "application_bytes": 2158216, + "application_slot_bytes": 6553600 + }, + "linked_internal_bytes_freed": 36792 + }, + "LilyGo_TDeck_repeater": { + "baseline": { + "memory": { + "schema_version": 1, + "target": "LilyGo_TDeck_repeater", + "platform": "ESP32_PLATFORM", + "mcu": "esp32s3", + "elf_sha256": "7a81e26e7ca6db8241ded72c7f25c171cfddf867dcf04018b17bec2841314fc8", + "passed": true, + "available_internal_bytes": 223960, + "largest_internal_region_bytes": 191192, + "required_heap_bytes": 171184, + "required_contiguous_bytes": 19168, + "components": { + "core_tasks_usb_filesystems": 24576, + "wireless_stacks": 49152, + "udp_bridge_and_socket": 5120, + "ota_source_scratch": 8192, + "browser_terminal_session": 2048, + "neighbor_history": 18448, + "host_kiss_codec": 2048, + "radio_packet_pool": 10240, + "client_table": 9104, + "flood_filter_table": 19168, + "ota_generic_source_buffers": 3616, + "ota_encoder_workspace": 1040, + "display_pixels_and_driver": 0, + "screen_objects_and_history": 2048, + "allocation_and_transient_margin": 16384 + }, + "display": "ST7789LCDDisplay", + "full_companion": false, + "regions": [ + { + "start": 1070312504, + "end": 1070503696 + }, + { + "start": 1070530560, + "end": 1070563328 + } + ], + "scope": "Linked capacity before runtime allocation; physical boot/load/soak validation remains required.", + "artifact_target": "LilyGo_TDeck_repeater", + "pio_environment": "LilyGo_TDeck_repeater", + "files": { + "LilyGo_TDeck_repeater-full-logging-ota-v1.18.0.2-psram-test-74c03cb3.bin": "58645d4a0221c47aced270ebd2d36334e5eff5f47f5443cea3eec552d72a6d48", + "LilyGo_TDeck_repeater-full-logging-ota-v1.18.0.2-psram-test-74c03cb3-merged.bin": "1af4afdf12e53e194515a6193f6080701515e644365dc829f075597c562f7b1f", + "LilyGo_TDeck_repeater-full-logging-ota-v1.18.0.2-psram-test-74c03cb3.capabilities.json": "6e2aa96cfc2baa32aedc22a48b34fa37392a6acbea79ec77433dbe4a965dfb02", + "LilyGo_TDeck_repeater-full-logging-ota-v1.18.0.2-psram-test-74c03cb3.meshtext-license.txt": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + }, + "capabilities": { + "artifact_target": "LilyGo_TDeck_repeater", + "build_profile": "full", + "build_recipe": { + "schema_version": 1, + "sha256": "a0fdec3a295e5d8a1e55aa25f7a6461c0c7d944b026d4b94c6e2d399711f9ede" + }, + "capabilities": [ + "bridge.espnow", + "bridge.udp", + "profile.full", + "release.full.same_partition_successor", + "radio2.kiss.host", + "radio2.kiss.usb", + "radio2.kiss.tcp", + "ota.update.wifi", + "ota.update.lora", + "cli.retry_preset", + "telemetry.history", + "ota.cli", + "web.webconfig", + "logging.usb.packets", + "logging.usb.control", + "bridge.udp.configuration" + ], + "ota_update_evidence": "firmware fits both OTA application slots; otadata present", + "ota_update_methods": [ + "wifi", + "lora" + ], + "ota_update_verified": true, + "platform": "ESP32_PLATFORM", + "platformio_env": "LilyGo_TDeck_repeater", + "reductions": [], + "schema_version": 2, + "target": "LilyGo_TDeck_repeater", + "third_party_notices": [ + { + "component": "espp-meshtext", + "file": "LilyGo_TDeck_repeater-full-logging-ota-v1.18.0.2-psram-test-74c03cb3.meshtext-license.txt", + "license": "MIT", + "sha256": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + ], + "verification": [ + { + "capability": "radio2.kiss.host", + "evidence": "_ZN4mesh8hostkiss5Codec4feedEh; _ZN4mesh8hostkiss5Codec11queueOutputEhPKhj; _ZN4mesh10Dispatcher22queueForeignRadioFrameEPKhjhjNS_20RadioProfileProtocolE", + "present": true, + "source": "linked ELF symbols" + }, + { + "capability": "radio2.kiss.usb", + "evidence": "Error: use set kiss on|off; Error: use set kiss.transport usb|tcp; USB: send KISS FEND to start", + "present": true, + "source": "packaged application" + }, + { + "capability": "radio2.kiss.tcp", + "evidence": "station TCP 5004", + "present": true, + "source": "packaged application" + }, + { + "capability": "bridge.espnow", + "evidence": "_ZN12ESPNowBridge5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "bridge.udp", + "evidence": "_ZN9UDPBridge5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.wifi", + "evidence": "Started: http://%s/update", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.lora", + "evidence": "image_hash MISMATCH after decode", + "present": true, + "source": "linked image" + }, + { + "capability": "cli.retry_preset", + "evidence": "retry.preset", + "present": true, + "source": "linked image" + }, + { + "capability": "telemetry.history", + "evidence": "telemetry.volt.i2c", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.cli", + "evidence": "OTA: status", + "present": true, + "source": "linked image" + }, + { + "capability": "web.webconfig", + "evidence": "start webconfig", + "present": true, + "source": "linked image" + }, + { + "capability": "logging.usb.packets", + "evidence": "%s: %s, len=%d (type=%d, route=%s, payload_len=%d)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.control", + "evidence": "OK - USB logging %s (saved)", + "present": true, + "source": "packaged application" + }, + { + "capability": "bridge.udp.configuration", + "evidence": "udp.enabled", + "present": true, + "source": "packaged application" + } + ], + "verified": true + }, + "application_bytes": 1749000, + "application_slot_bytes": 6553600 + }, + "final": { + "memory": { + "schema_version": 1, + "target": "LilyGo_TDeck_repeater", + "platform": "ESP32_PLATFORM", + "mcu": "esp32s3", + "elf_sha256": "1b380ad615448e5b24992a4f78cb1e59a123e271bd6ac6ef0b8203579e620068", + "passed": true, + "available_internal_bytes": 260752, + "largest_internal_region_bytes": 227984, + "required_heap_bytes": 208012, + "required_contiguous_bytes": 19168, + "components": { + "core_tasks_usb_filesystems": 24576, + "wireless_stacks": 49152, + "udp_bridge_and_socket": 5120, + "ota_source_scratch": 8192, + "browser_terminal_session": 2048, + "neighbor_history": 18448, + "host_kiss_codec": 2048, + "radio_packet_pool": 10240, + "client_table": 9104, + "flood_filter_table": 19168, + "repeater_scope_storage_fallback": 18892, + "repeater_advert_storage_fallback": 17936, + "ota_generic_source_buffers": 3616, + "ota_encoder_workspace": 1040, + "display_pixels_and_driver": 0, + "screen_objects_and_history": 2048, + "allocation_and_transient_margin": 16384 + }, + "display": "ST7789LCDDisplay", + "full_companion": false, + "regions": [ + { + "start": 1070275712, + "end": 1070503696 + }, + { + "start": 1070530560, + "end": 1070563328 + } + ], + "scope": "Linked capacity before runtime allocation; physical boot/load/soak validation remains required.", + "artifact_target": "LilyGo_TDeck_repeater", + "pio_environment": "LilyGo_TDeck_repeater", + "files": { + "LilyGo_TDeck_repeater-full-logging-ota-v1.18.0.2-psram-test-74c03cb3.bin": "aacc9f578fc6e6185061f4939e4f6f58c3f7553c0de3b87c8e95b4f231a9f845", + "LilyGo_TDeck_repeater-full-logging-ota-v1.18.0.2-psram-test-74c03cb3-merged.bin": "5741fb5c0f9b744f2895a3db5e207d873c172192ccc6ab473d3a7f5da4d88d4d", + "LilyGo_TDeck_repeater-full-logging-ota-v1.18.0.2-psram-test-74c03cb3.capabilities.json": "f1628e243c28a4619597f6236a6b1ea593885e581c4949a9cbac084aecf6f891", + "LilyGo_TDeck_repeater-full-logging-ota-v1.18.0.2-psram-test-74c03cb3.meshtext-license.txt": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + }, + "capabilities": { + "artifact_target": "LilyGo_TDeck_repeater", + "build_profile": "full", + "build_recipe": { + "schema_version": 1, + "sha256": "4637703ef279754b9752767c3d6cdc1c00ae852c9c2c2cf34b85f42a3d24b028" + }, + "capabilities": [ + "bridge.espnow", + "bridge.udp", + "profile.full", + "release.full.same_partition_successor", + "radio2.kiss.host", + "radio2.kiss.usb", + "radio2.kiss.tcp", + "ota.update.wifi", + "ota.update.lora", + "cli.retry_preset", + "telemetry.history", + "ota.cli", + "web.webconfig", + "logging.usb.packets", + "logging.usb.control", + "bridge.udp.configuration" + ], + "ota_update_evidence": "firmware fits both OTA application slots; otadata present", + "ota_update_methods": [ + "wifi", + "lora" + ], + "ota_update_verified": true, + "platform": "ESP32_PLATFORM", + "platformio_env": "LilyGo_TDeck_repeater", + "reductions": [], + "schema_version": 2, + "target": "LilyGo_TDeck_repeater", + "third_party_notices": [ + { + "component": "espp-meshtext", + "file": "LilyGo_TDeck_repeater-full-logging-ota-v1.18.0.2-psram-test-74c03cb3.meshtext-license.txt", + "license": "MIT", + "sha256": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + ], + "verification": [ + { + "capability": "radio2.kiss.host", + "evidence": "_ZN4mesh8hostkiss5Codec4feedEh; _ZN4mesh8hostkiss5Codec11queueOutputEhPKhj; _ZN4mesh10Dispatcher22queueForeignRadioFrameEPKhjhjNS_20RadioProfileProtocolE", + "present": true, + "source": "linked ELF symbols" + }, + { + "capability": "radio2.kiss.usb", + "evidence": "Error: use set kiss on|off; Error: use set kiss.transport usb|tcp; USB: send KISS FEND to start", + "present": true, + "source": "packaged application" + }, + { + "capability": "radio2.kiss.tcp", + "evidence": "station TCP 5004", + "present": true, + "source": "packaged application" + }, + { + "capability": "bridge.espnow", + "evidence": "_ZN12ESPNowBridge5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "bridge.udp", + "evidence": "_ZN9UDPBridge5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.wifi", + "evidence": "Started: http://%s/update", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.lora", + "evidence": "image_hash MISMATCH after decode", + "present": true, + "source": "linked image" + }, + { + "capability": "cli.retry_preset", + "evidence": "retry.preset", + "present": true, + "source": "linked image" + }, + { + "capability": "telemetry.history", + "evidence": "telemetry.volt.i2c", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.cli", + "evidence": "OTA: status", + "present": true, + "source": "linked image" + }, + { + "capability": "web.webconfig", + "evidence": "start webconfig", + "present": true, + "source": "linked image" + }, + { + "capability": "logging.usb.packets", + "evidence": "%s: %s, len=%d (type=%d, route=%s, payload_len=%d)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.control", + "evidence": "OK - USB logging %s (saved)", + "present": true, + "source": "packaged application" + }, + { + "capability": "bridge.udp.configuration", + "evidence": "udp.enabled", + "present": true, + "source": "packaged application" + } + ], + "verified": true + }, + "application_bytes": 1747624, + "application_slot_bytes": 6553600 + }, + "linked_internal_bytes_freed": 36792 + }, + "Station_G2_full": { + "baseline": { + "memory": { + "schema_version": 1, + "target": "Station_G2_repeater", + "platform": "ESP32_PLATFORM", + "mcu": "esp32s3", + "elf_sha256": "e9ef328f902caeb6bb46e922c2d058f159fe8ef27775688f837bf4bf29a983e3", + "passed": true, + "available_internal_bytes": 200328, + "largest_internal_region_bytes": 167560, + "required_heap_bytes": 197808, + "required_contiguous_bytes": 19168, + "components": { + "core_tasks_usb_filesystems": 24576, + "wireless_stacks": 49152, + "mqtt_startup_allowance": 24576, + "udp_bridge_and_socket": 5120, + "ota_source_scratch": 8192, + "browser_terminal_session": 2048, + "neighbor_history": 18448, + "host_kiss_codec": 2048, + "radio_packet_pool": 10240, + "client_table": 9104, + "flood_filter_table": 19168, + "ota_generic_source_buffers": 3616, + "ota_encoder_workspace": 1040, + "display_pixels_and_driver": 2048, + "screen_objects_and_history": 2048, + "allocation_and_transient_margin": 16384 + }, + "display": "SH1106Display", + "full_companion": false, + "mqtt": { + "budget_scope": "startup minimum with runtime connection admission", + "startup_allowance_bytes": 24576, + "startup_active_slot_allowance": 2, + "runtime_slot_count": 6, + "maximum_active_slots": 5, + "maximum_active_slots_without_psram": 2, + "worker_stack_bytes": 8192, + "client_task_stack_bytes": 6144, + "client_task_priority": 5, + "client_rx_tx_buffer_bytes": 1792, + "maximum_slot_stack_buffer_allowance_bytes": 48384, + "cold_start_allocation_allowance_bytes": 16384, + "cold_start_dma_admission_free_bytes": 32768, + "cold_start_dma_admission_contiguous_bytes": 8192, + "additional_broker_cold_start_dma_admission_contiguous_bytes": 20480, + "restart_allocation_allowance_bytes": 8192, + "restart_dma_admission_free_bytes": 24576, + "restart_dma_admission_contiguous_bytes": 8192, + "additional_broker_restart_dma_admission_contiguous_bytes": 12288, + "dma_admission_free_bytes": 16384, + "dma_admission_contiguous_bytes": 4096, + "publish_contiguous_bytes": 4096, + "publish_contiguous_without_psram_bytes": 8000, + "publish_contiguous_with_psram_bytes": 4096, + "tls_psram_admission_contiguous_bytes": 16000, + "additional_broker_cold_start_tls_psram_admission_contiguous_bytes": 32384, + "additional_broker_restart_tls_psram_admission_contiguous_bytes": 24192, + "additional_broker_tls_psram_admission_contiguous_bytes": 16000, + "tls_internal_admission_free_bytes": 61440, + "tls_record_allocation_bytes": 17408, + "additional_broker_cold_start_tls_internal_admission_contiguous_bytes": 41792, + "additional_broker_restart_tls_internal_admission_contiguous_bytes": 33600, + "tls_internal_admission_contiguous_bytes": 17408, + "additional_broker_tls_internal_admission_contiguous_bytes": 25408, + "maximum_runtime_load_qualified": false, + "physical_validation_performed": false, + "limitations": "Maximum TLS/WSS/outbox workload is not covered by the startup allowance; runtime free DMA heap and fragmentation must satisfy connection admission; additional brokers reserve publication headroom beside startup allocations." + }, + "regions": [ + { + "start": 1070336136, + "end": 1070503696 + }, + { + "start": 1070530560, + "end": 1070563328 + } + ], + "scope": "Linked capacity before runtime allocation; physical boot/load/soak validation remains required.", + "artifact_target": "Station_G2_repeater", + "pio_environment": "Station_G2_repeater_observer_mqtt", + "files": { + "Station_G2_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3.bin": "5c85a2296472ca98896f19c6f03896edcefb05940eabfc0ff371300ae7b48e5c", + "Station_G2_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3-merged.bin": "85db924b667bc5d6a68c13a71db3cc9383d60698e662822f42d40c30e67a9075", + "Station_G2_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3.capabilities.json": "9f6ad5ae8ad0f1f7867b4e6747802c9fd96c85a0381d5512b5d21faf4b373cf9", + "Station_G2_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3.meshtext-license.txt": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + }, + "capabilities": { + "artifact_target": "Station_G2_repeater", + "build_profile": "full", + "build_recipe": { + "schema_version": 1, + "sha256": "097d01b27ee89898eba7aeafad8ec3aee1f2e28faabe2b615760230323aa9207" + }, + "capabilities": [ + "bridge.espnow", + "bridge.udp", + "profile.full", + "release.full.partition_migration_ota", + "radio2.kiss.host", + "radio2.kiss.usb", + "radio2.kiss.tcp", + "ota.update.wifi", + "ota.update.lora", + "cli.retry_preset", + "telemetry.history", + "ota.cli", + "web.webconfig", + "logging.usb.packets", + "logging.usb.control", + "logging.usb.output", + "bridge.udp.configuration" + ], + "ota_update_evidence": "firmware fits both OTA application slots; otadata present", + "ota_update_methods": [ + "wifi", + "lora" + ], + "ota_update_verified": true, + "platform": "ESP32_PLATFORM", + "platformio_env": "Station_G2_repeater_observer_mqtt", + "reductions": [], + "schema_version": 2, + "target": "Station_G2_repeater", + "third_party_notices": [ + { + "component": "espp-meshtext", + "file": "Station_G2_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3.meshtext-license.txt", + "license": "MIT", + "sha256": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + ], + "verification": [ + { + "capability": "radio2.kiss.host", + "evidence": "_ZN4mesh8hostkiss5Codec4feedEh; _ZN4mesh8hostkiss5Codec11queueOutputEhPKhj; _ZN4mesh10Dispatcher22queueForeignRadioFrameEPKhjhjNS_20RadioProfileProtocolE", + "present": true, + "source": "linked ELF symbols" + }, + { + "capability": "radio2.kiss.usb", + "evidence": "Error: use set kiss on|off; Error: use set kiss.transport usb|tcp; USB: send KISS FEND to start", + "present": true, + "source": "packaged application" + }, + { + "capability": "radio2.kiss.tcp", + "evidence": "station TCP 5004", + "present": true, + "source": "packaged application" + }, + { + "capability": "bridge.espnow", + "evidence": "_ZN12ESPNowBridge5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "bridge.udp", + "evidence": "_ZN9UDPBridge5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.wifi", + "evidence": "Started: http://%s/update", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.lora", + "evidence": "image_hash MISMATCH after decode", + "present": true, + "source": "linked image" + }, + { + "capability": "cli.retry_preset", + "evidence": "retry.preset", + "present": true, + "source": "linked image" + }, + { + "capability": "telemetry.history", + "evidence": "telemetry.volt.i2c", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.cli", + "evidence": "OTA: status", + "present": true, + "source": "linked image" + }, + { + "capability": "web.webconfig", + "evidence": "start webconfig", + "present": true, + "source": "linked image" + }, + { + "capability": "logging.usb.packets", + "evidence": "%s: %s, len=%d (type=%d, route=%s, payload_len=%d)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.control", + "evidence": "OK - USB logging %s (saved)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.output", + "evidence": "OK - logging.output %s (saved)", + "present": true, + "source": "packaged application" + }, + { + "capability": "bridge.udp.configuration", + "evidence": "udp.enabled", + "present": true, + "source": "packaged application" + } + ], + "verified": true + }, + "application_bytes": 2125896, + "application_slot_bytes": 6553600 + }, + "final": { + "memory": { + "schema_version": 1, + "target": "Station_G2_repeater", + "platform": "ESP32_PLATFORM", + "mcu": "esp32s3", + "elf_sha256": "61910fcb243d37fa99771fe2c7cca5c3fb28e077abcb98e4f14b62215ab21460", + "passed": true, + "available_internal_bytes": 237120, + "largest_internal_region_bytes": 204352, + "required_heap_bytes": 234636, + "required_contiguous_bytes": 19168, + "components": { + "core_tasks_usb_filesystems": 24576, + "wireless_stacks": 49152, + "mqtt_startup_allowance": 24576, + "udp_bridge_and_socket": 5120, + "ota_source_scratch": 8192, + "browser_terminal_session": 2048, + "neighbor_history": 18448, + "host_kiss_codec": 2048, + "radio_packet_pool": 10240, + "client_table": 9104, + "flood_filter_table": 19168, + "repeater_scope_storage_fallback": 18892, + "repeater_advert_storage_fallback": 17936, + "ota_generic_source_buffers": 3616, + "ota_encoder_workspace": 1040, + "display_pixels_and_driver": 2048, + "screen_objects_and_history": 2048, + "allocation_and_transient_margin": 16384 + }, + "display": "SH1106Display", + "full_companion": false, + "mqtt": { + "budget_scope": "startup minimum with runtime connection admission", + "startup_allowance_bytes": 24576, + "startup_active_slot_allowance": 2, + "runtime_slot_count": 6, + "maximum_active_slots": 5, + "maximum_active_slots_without_psram": 2, + "worker_stack_bytes": 8192, + "client_task_stack_bytes": 6144, + "client_task_priority": 5, + "client_rx_tx_buffer_bytes": 1792, + "maximum_slot_stack_buffer_allowance_bytes": 48384, + "cold_start_allocation_allowance_bytes": 16384, + "cold_start_dma_admission_free_bytes": 32768, + "cold_start_dma_admission_contiguous_bytes": 8192, + "additional_broker_cold_start_dma_admission_contiguous_bytes": 20480, + "restart_allocation_allowance_bytes": 8192, + "restart_dma_admission_free_bytes": 24576, + "restart_dma_admission_contiguous_bytes": 8192, + "additional_broker_restart_dma_admission_contiguous_bytes": 12288, + "dma_admission_free_bytes": 16384, + "dma_admission_contiguous_bytes": 4096, + "publish_contiguous_bytes": 4096, + "publish_contiguous_without_psram_bytes": 8000, + "publish_contiguous_with_psram_bytes": 4096, + "tls_psram_admission_contiguous_bytes": 16000, + "additional_broker_cold_start_tls_psram_admission_contiguous_bytes": 32384, + "additional_broker_restart_tls_psram_admission_contiguous_bytes": 24192, + "additional_broker_tls_psram_admission_contiguous_bytes": 16000, + "tls_internal_admission_free_bytes": 61440, + "tls_record_allocation_bytes": 17408, + "additional_broker_cold_start_tls_internal_admission_contiguous_bytes": 41792, + "additional_broker_restart_tls_internal_admission_contiguous_bytes": 33600, + "tls_internal_admission_contiguous_bytes": 17408, + "additional_broker_tls_internal_admission_contiguous_bytes": 25408, + "maximum_runtime_load_qualified": false, + "physical_validation_performed": false, + "limitations": "Maximum TLS/WSS/outbox workload is not covered by the startup allowance; runtime free DMA heap and fragmentation must satisfy connection admission; additional brokers reserve publication headroom beside startup allocations." + }, + "regions": [ + { + "start": 1070299344, + "end": 1070503696 + }, + { + "start": 1070530560, + "end": 1070563328 + } + ], + "scope": "Linked capacity before runtime allocation; physical boot/load/soak validation remains required.", + "artifact_target": "Station_G2_repeater", + "pio_environment": "Station_G2_repeater_observer_mqtt", + "files": { + "Station_G2_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3.bin": "181cd48ddcb3f59d6248dc8d0eb56d24e078cb1570d2c3849982e994944de2db", + "Station_G2_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3-merged.bin": "2333bffd21cfba9499af5e09693cb942056827a088cae8b1bb451824a7eee33a", + "Station_G2_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3.capabilities.json": "0d0947fa3b07da87f70b1935386e7d17d873906318e2abcd08192b2e3629a956", + "Station_G2_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3.meshtext-license.txt": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + }, + "capabilities": { + "artifact_target": "Station_G2_repeater", + "build_profile": "full", + "build_recipe": { + "schema_version": 1, + "sha256": "fc24f56be0d6bca5ed94476d117ef22613bcbdde132356a70a88cc4c9a00b697" + }, + "capabilities": [ + "bridge.espnow", + "bridge.udp", + "profile.full", + "release.full.partition_migration_ota", + "radio2.kiss.host", + "radio2.kiss.usb", + "radio2.kiss.tcp", + "ota.update.wifi", + "ota.update.lora", + "cli.retry_preset", + "telemetry.history", + "ota.cli", + "web.webconfig", + "logging.usb.packets", + "logging.usb.control", + "logging.usb.output", + "bridge.udp.configuration" + ], + "ota_update_evidence": "firmware fits both OTA application slots; otadata present", + "ota_update_methods": [ + "wifi", + "lora" + ], + "ota_update_verified": true, + "platform": "ESP32_PLATFORM", + "platformio_env": "Station_G2_repeater_observer_mqtt", + "reductions": [], + "schema_version": 2, + "target": "Station_G2_repeater", + "third_party_notices": [ + { + "component": "espp-meshtext", + "file": "Station_G2_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-74c03cb3.meshtext-license.txt", + "license": "MIT", + "sha256": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + ], + "verification": [ + { + "capability": "radio2.kiss.host", + "evidence": "_ZN4mesh8hostkiss5Codec4feedEh; _ZN4mesh8hostkiss5Codec11queueOutputEhPKhj; _ZN4mesh10Dispatcher22queueForeignRadioFrameEPKhjhjNS_20RadioProfileProtocolE", + "present": true, + "source": "linked ELF symbols" + }, + { + "capability": "radio2.kiss.usb", + "evidence": "Error: use set kiss on|off; Error: use set kiss.transport usb|tcp; USB: send KISS FEND to start", + "present": true, + "source": "packaged application" + }, + { + "capability": "radio2.kiss.tcp", + "evidence": "station TCP 5004", + "present": true, + "source": "packaged application" + }, + { + "capability": "bridge.espnow", + "evidence": "_ZN12ESPNowBridge5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "bridge.udp", + "evidence": "_ZN9UDPBridge5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.wifi", + "evidence": "Started: http://%s/update", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.lora", + "evidence": "image_hash MISMATCH after decode", + "present": true, + "source": "linked image" + }, + { + "capability": "cli.retry_preset", + "evidence": "retry.preset", + "present": true, + "source": "linked image" + }, + { + "capability": "telemetry.history", + "evidence": "telemetry.volt.i2c", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.cli", + "evidence": "OTA: status", + "present": true, + "source": "linked image" + }, + { + "capability": "web.webconfig", + "evidence": "start webconfig", + "present": true, + "source": "linked image" + }, + { + "capability": "logging.usb.packets", + "evidence": "%s: %s, len=%d (type=%d, route=%s, payload_len=%d)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.control", + "evidence": "OK - USB logging %s (saved)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.output", + "evidence": "OK - logging.output %s (saved)", + "present": true, + "source": "packaged application" + }, + { + "capability": "bridge.udp.configuration", + "evidence": "udp.enabled", + "present": true, + "source": "packaged application" + } + ], + "verified": true + }, + "application_bytes": 2125160, + "application_slot_bytes": 6553600 + }, + "linked_internal_bytes_freed": 36792 + } + }, + "existing_failures": { + "Station_G2_repeater": { + "baseline": { + "log_sha256": "62693f3b03281ef3d0531d08492854aaba0c9c482199a58779f17a7f06833459", + "linked_program_bytes": 1375509, + "application_slot_bytes": 1310720, + "result": "preexisting legacy flash-slot overflow" + }, + "final": { + "log_sha256": "a12c2d92958176a080a1413fac999290636d2e8b053400ff3d990683f81b0f13", + "linked_program_bytes": 1374093, + "application_slot_bytes": 1310720, + "result": "preexisting legacy flash-slot overflow" + } + }, + "Tbeam_SX1262_repeater_observer_mqtt": { + "baseline": { + "log_sha256": "ac25a522c11462e2bec8e198a477e378828c59f5aa207ea90abde87cd903e884", + "result": "compile and RAM admission passed; existing dual-slot OTA-fit check failed" + }, + "final": { + "log_sha256": "1f1b2485a341b88a1b31e57c5acc199d4d17a59b6877cbb836410126d6dd53b3", + "result": "compile and RAM admission passed; existing dual-slot OTA-fit check failed" + } + } + }, + "source_sha256": { + "src/helpers/RepeaterPsramStorage.h": "9286a1aea6f7fa6da30c1d0788717d82f2479edf45b35181d5d6bdb22c5b211b", + "src/helpers/FloodAdvertLimiter.h": "72dda9cafa0b5f599b4858829c5b9e9bf9439b97bcbeece92d20514a9acdf829", + "src/helpers/ESP32WsTransportFix.cpp": "1a946f2c6061118e55995b38387cfc2e3462d9bcb1576764989edd68cf99ab2c", + "examples/simple_repeater/MyMesh.h": "7b95cee5e9e148bbbf76aea8394b0a075047aac74064795fedb1256cb66c5712", + "examples/simple_repeater/MyMesh.cpp": "9c6fcd42f6e1ce17dec0209e83d7ae6ec71dbf4c9bdaae17c77924f396e70f77", + "scripts/check_firmware_ram.py": "833e842048fbe72463da547d52e1f8f291ea248142f2abf6735f596d3be601ed", + "test/test_firmware_ram.py": "1402904d4d4907a0743b6d09c3d0c72534efc14ff7f288c4c118b04cf454cb09", + "test/test_repeater_scope_psram.py": "c5974097405f89db5a529513a6c52d517de458c5acbd5fa0efe193fd2936ccf4", + "test/test_flood_advert_psram.py": "839d97c99215ba8c2e26b490398968049de668e386f4e1c3d4e8d2541d19fb5b", + "test/test_ws_transport_psram.py": "1a41b4efb069e604275c132eda286e5e2f34d3257f5d7ff3fd28f7e6fb653425" + }, + "limitations": [ + "No board flashed.", + "Linked capacity is not running free heap.", + "Physical startup, RF timing, persistence and broker-load qualification were not performed." + ], + "post_integration": { + "tested_commit": "6a5a04b1b881022f59f6489c67b62aed4629fea0", + "target": "Station_G2_repeater", + "memory": { + "schema_version": 1, + "target": "Station_G2_repeater", + "platform": "ESP32_PLATFORM", + "mcu": "esp32s3", + "elf_sha256": "fb5e4cfbf7fe68af87abbaa7ae06cfe43abedb8ff1450179020f34750f24e4f1", + "passed": true, + "available_internal_bytes": 237120, + "largest_internal_region_bytes": 204352, + "required_heap_bytes": 234636, + "required_contiguous_bytes": 19168, + "components": { + "core_tasks_usb_filesystems": 24576, + "wireless_stacks": 49152, + "mqtt_startup_allowance": 24576, + "udp_bridge_and_socket": 5120, + "ota_source_scratch": 8192, + "browser_terminal_session": 2048, + "neighbor_history": 18448, + "host_kiss_codec": 2048, + "radio_packet_pool": 10240, + "client_table": 9104, + "flood_filter_table": 19168, + "repeater_scope_storage_fallback": 18892, + "repeater_advert_storage_fallback": 17936, + "ota_generic_source_buffers": 3616, + "ota_encoder_workspace": 1040, + "display_pixels_and_driver": 2048, + "screen_objects_and_history": 2048, + "allocation_and_transient_margin": 16384 + }, + "display": "SH1106Display", + "full_companion": false, + "mqtt": { + "budget_scope": "startup minimum with runtime connection admission", + "startup_allowance_bytes": 24576, + "startup_active_slot_allowance": 2, + "runtime_slot_count": 6, + "maximum_active_slots": 5, + "maximum_active_slots_without_psram": 2, + "worker_stack_bytes": 8192, + "client_task_stack_bytes": 6144, + "client_task_priority": 5, + "client_rx_tx_buffer_bytes": 1792, + "maximum_slot_stack_buffer_allowance_bytes": 48384, + "cold_start_allocation_allowance_bytes": 16384, + "cold_start_dma_admission_free_bytes": 32768, + "cold_start_dma_admission_contiguous_bytes": 8192, + "additional_broker_cold_start_dma_admission_contiguous_bytes": 20480, + "restart_allocation_allowance_bytes": 8192, + "restart_dma_admission_free_bytes": 24576, + "restart_dma_admission_contiguous_bytes": 8192, + "additional_broker_restart_dma_admission_contiguous_bytes": 12288, + "dma_admission_free_bytes": 16384, + "dma_admission_contiguous_bytes": 4096, + "publish_contiguous_bytes": 4096, + "publish_contiguous_without_psram_bytes": 8000, + "publish_contiguous_with_psram_bytes": 4096, + "tls_psram_admission_contiguous_bytes": 16000, + "additional_broker_cold_start_tls_psram_admission_contiguous_bytes": 32384, + "additional_broker_restart_tls_psram_admission_contiguous_bytes": 24192, + "additional_broker_tls_psram_admission_contiguous_bytes": 16000, + "tls_internal_admission_free_bytes": 61440, + "tls_record_allocation_bytes": 17408, + "additional_broker_cold_start_tls_internal_admission_contiguous_bytes": 41792, + "additional_broker_restart_tls_internal_admission_contiguous_bytes": 33600, + "tls_internal_admission_contiguous_bytes": 17408, + "additional_broker_tls_internal_admission_contiguous_bytes": 25408, + "maximum_runtime_load_qualified": false, + "physical_validation_performed": false, + "limitations": "Maximum TLS/WSS/outbox workload is not covered by the startup allowance; runtime free DMA heap and fragmentation must satisfy connection admission; additional brokers reserve publication headroom beside startup allocations." + }, + "regions": [ + { + "start": 1070299344, + "end": 1070503696 + }, + { + "start": 1070530560, + "end": 1070563328 + } + ], + "scope": "Linked capacity before runtime allocation; physical boot/load/soak validation remains required.", + "artifact_target": "Station_G2_repeater", + "pio_environment": "Station_G2_repeater_observer_mqtt", + "files": { + "Station_G2_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-6a5a04b1.bin": "d9233e7d7282a7f31f1cb23c8c807a59a84ad0fcdca31536b2e5aac8c9749566", + "Station_G2_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-6a5a04b1-merged.bin": "22a230d3267af891acc36f0b33c9e05a74b074390191c2c9a73789aaa7d5dd30", + "Station_G2_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-6a5a04b1.capabilities.json": "fe8e50e019a13549e74e4be6f60a5c351a723d92dbfa90496752413a954a7864", + "Station_G2_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-6a5a04b1.meshtext-license.txt": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + }, + "capabilities": { + "artifact_target": "Station_G2_repeater", + "build_profile": "full", + "build_recipe": { + "schema_version": 1, + "sha256": "3f316af418f394d506a1af9ad4f5746d3ffaf91a4b240e9f0c6e2c5e4c5677f4" + }, + "capabilities": [ + "bridge.espnow", + "bridge.udp", + "profile.full", + "release.full.partition_migration_ota", + "radio2.kiss.host", + "radio2.kiss.usb", + "radio2.kiss.tcp", + "ota.update.wifi", + "ota.update.lora", + "cli.retry_preset", + "telemetry.history", + "ota.cli", + "web.webconfig", + "logging.usb.packets", + "logging.usb.control", + "logging.usb.output", + "bridge.udp.configuration" + ], + "ota_update_evidence": "firmware fits both OTA application slots; otadata present", + "ota_update_methods": [ + "wifi", + "lora" + ], + "ota_update_verified": true, + "platform": "ESP32_PLATFORM", + "platformio_env": "Station_G2_repeater_observer_mqtt", + "reductions": [], + "schema_version": 2, + "target": "Station_G2_repeater", + "third_party_notices": [ + { + "component": "espp-meshtext", + "file": "Station_G2_repeater-full-usb-wifi-ota-v1.18.0.2-psram-test-6a5a04b1.meshtext-license.txt", + "license": "MIT", + "sha256": "c37d0a453a2491514bac33ed835c1877f207390f134ae439a97b06b004e236d2" + } + ], + "verification": [ + { + "capability": "radio2.kiss.host", + "evidence": "_ZN4mesh8hostkiss5Codec4feedEh; _ZN4mesh8hostkiss5Codec11queueOutputEhPKhj; _ZN4mesh10Dispatcher22queueForeignRadioFrameEPKhjhjNS_20RadioProfileProtocolE", + "present": true, + "source": "linked ELF symbols" + }, + { + "capability": "radio2.kiss.usb", + "evidence": "Error: use set kiss on|off; Error: use set kiss.transport usb|tcp; USB: send KISS FEND to start", + "present": true, + "source": "packaged application" + }, + { + "capability": "radio2.kiss.tcp", + "evidence": "station TCP 5004", + "present": true, + "source": "packaged application" + }, + { + "capability": "bridge.espnow", + "evidence": "_ZN12ESPNowBridge5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "bridge.udp", + "evidence": "_ZN9UDPBridge5beginEv", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.wifi", + "evidence": "Started: http://%s/update", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.update.lora", + "evidence": "image_hash MISMATCH after decode", + "present": true, + "source": "linked image" + }, + { + "capability": "cli.retry_preset", + "evidence": "retry.preset", + "present": true, + "source": "linked image" + }, + { + "capability": "telemetry.history", + "evidence": "telemetry.volt.i2c", + "present": true, + "source": "linked image" + }, + { + "capability": "ota.cli", + "evidence": "OTA: status", + "present": true, + "source": "linked image" + }, + { + "capability": "web.webconfig", + "evidence": "start webconfig", + "present": true, + "source": "linked image" + }, + { + "capability": "logging.usb.packets", + "evidence": "%s: %s, len=%d (type=%d, route=%s, payload_len=%d)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.control", + "evidence": "OK - USB logging %s (saved)", + "present": true, + "source": "packaged application" + }, + { + "capability": "logging.usb.output", + "evidence": "OK - logging.output %s (saved)", + "present": true, + "source": "packaged application" + }, + { + "capability": "bridge.udp.configuration", + "evidence": "udp.enabled", + "present": true, + "source": "packaged application" + } + ], + "verified": true + }, + "application_bytes": 2125736, + "note": "Combined with upstream Companion PIN and startup-screen changes; isolated PSRAM source hashes unchanged." + } +} diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index e99837958..4fed902fb 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -3707,11 +3707,6 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc : mesh::Mesh(radio, ms, rng, rtc, *createObserverPacketManager(32), tables), region_map(key_store), temp_map(key_store), _cli(board, rtc, sensors, region_map, acl, &_prefs, this), -#if MESH_REPEATER_PSRAM_TABLES - flood_channel_scopes(flood_channel_storage.require("channel scope tables").scopes), - flood_channel_direct_scopes(flood_channel_storage.require("channel scope tables").direct), - flood_channel_scope_requirements(flood_channel_storage.require("channel scope tables").requirements), -#endif telemetry(MAX_PACKET_PAYLOAD - 4), discover_limiter(4, 120), // max 4 every 2 minutes anon_limiter(4, 180) // max 4 every 3 minutes @@ -3796,11 +3791,13 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc if (flood_packet_filters) memset(flood_packet_filters, 0, sizeof(FloodPacketFilterEntry) * flood_packet_filter_slots); flood_packet_filter_blacklist_count = 0; memset(flood_packet_filter_blacklist, 0, sizeof(flood_packet_filter_blacklist)); +#if !MESH_REPEATER_PSRAM_TABLES memset(flood_channel_scopes, 0, sizeof(flood_channel_scopes)); memset(flood_channel_direct_scopes, 0, sizeof(flood_channel_direct_scopes)); memset(flood_channel_scope_requirements, 0, sizeof(flood_channel_scope_requirements)); +#endif #if MESH_ENABLE_FLOOD_GROUP_MODERATION memset(flood_group_moderation, 0, sizeof(flood_group_moderation)); #endif @@ -3998,6 +3995,14 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc // OTA mesh-integration (receive/begin/loop) is centralized in mesh::Mesh - no per-example wiring. void MyMesh::begin(FILESYSTEM *fs) { +#if MESH_REPEATER_PSRAM_TABLES + // initArduino() has now initialized either QSPI or OPI PSRAM. Allocate + // complete tables before any routing or persistence can obtain an address. + if (!flood_channel_storage.begin("channel scope tables")) abort(); +#ifndef PORTABLE_MQTT_OBSERVER + if (!flood_advert_limiter.begin()) abort(); +#endif +#endif #if MESH_ENABLE_FLOOD_RULE_ENGINE flood_filter_probability_seed = getRNG()->nextInt(0, UINT32_MAX); #endif @@ -6651,9 +6656,9 @@ bool MyMesh::loadFloodPacketFilters() { if (flood_packet_filters) memset(flood_packet_filters, 0, sizeof(FloodPacketFilterEntry) * flood_packet_filter_slots); flood_channel_data_rule_slot = 0xFF; flood_channel_data_rule_max_hops = FLOOD_CHANNEL_HOPS_ALL; - memset(flood_channel_scopes, 0, sizeof(flood_channel_scopes)); - memset(flood_channel_direct_scopes, 0, - sizeof(flood_channel_direct_scopes)); + memset(floodChannelScopes(), 0, sizeof(floodChannelScopes())); + memset(floodChannelDirectScopes(), 0, + sizeof(floodChannelDirectScopes())); flood_packet_filter_blacklist_count = 0; memset(flood_packet_filter_blacklist, 0, sizeof(flood_packet_filter_blacklist)); @@ -6957,7 +6962,7 @@ bool MyMesh::loadFloodPacketFilters() { size_t retained_scope_bytes = (size_t)retained_scope * sizeof(FloodChannelScopeEntry); success = success - && file.read((uint8_t*)flood_channel_scopes, retained_scope_bytes) + && file.read((uint8_t*)floodChannelScopes(), retained_scope_bytes) == retained_scope_bytes; FloodChannelScopeEntry discarded_scope; for (uint16_t i = retained_scope; success && i < scope_count; i++) { @@ -6981,13 +6986,13 @@ bool MyMesh::loadFloodPacketFilters() { success = false; } if (success && i < retained_direct) { - memcpy(flood_channel_direct_scopes[i], direct_name, + memcpy(floodChannelDirectScopes()[i], direct_name, sizeof(direct_name)); } } for (uint16_t i = 0; success && i < retained_scope; i++) { - auto& entry = flood_channel_scopes[i]; + auto& entry = floodChannelScopes()[i]; uint8_t selector = FloodFilterPolicy::channelScopeMatchSelectorValue(entry.selector); bool direct_target = @@ -6996,7 +7001,7 @@ bool MyMesh::loadFloodPacketFilters() { memset(&entry, 0, sizeof(entry)); } else if (direct_target && (entry.target_id > retained_direct - || flood_channel_direct_scopes[entry.target_id - 1][0] == 0)) { + || floodChannelDirectScopes()[entry.target_id - 1][0] == 0)) { success = false; } else if (selector <= FLOOD_CHANNEL_SCOPE_OTHER_ANY) { entry.channel_hash = 0; @@ -7051,9 +7056,9 @@ bool MyMesh::loadFloodPacketFilters() { file.close(); if (!success) { if (flood_packet_filters) memset(flood_packet_filters, 0, sizeof(FloodPacketFilterEntry) * flood_packet_filter_slots); - memset(flood_channel_scopes, 0, sizeof(flood_channel_scopes)); - memset(flood_channel_direct_scopes, 0, - sizeof(flood_channel_direct_scopes)); + memset(floodChannelScopes(), 0, sizeof(floodChannelScopes())); + memset(floodChannelDirectScopes(), 0, + sizeof(floodChannelDirectScopes())); flood_packet_filter_blacklist_count = 0; memset(flood_packet_filter_blacklist, 0, sizeof(flood_packet_filter_blacklist)); @@ -7577,12 +7582,12 @@ bool MyMesh::saveFloodPacketFilters(bool empty_scope_phase, uint8_t direct_count = 0; if (!empty_scope_phase) { for (uint16_t i = 0; i < FLOOD_CHANNEL_SCOPE_SLOTS; i++) { - if (flood_channel_scopes[i].target_id != 0) { + if (floodChannelScopes()[i].target_id != 0) { scope_count = (uint8_t)(i + 1U); } } for (uint16_t i = 0; i < FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS; i++) { - if (flood_channel_direct_scopes[i][0] != 0) { + if (floodChannelDirectScopes()[i][0] != 0) { direct_count = (uint8_t)(i + 1U); } } @@ -7594,12 +7599,12 @@ bool MyMesh::saveFloodPacketFilters(bool empty_scope_phase, sizeof(flood_channel_data_rule_max_hops)) && writeExact(&scope_count, sizeof(scope_count)); if (success && scope_count != 0) { - success = writeExact(flood_channel_scopes, + success = writeExact(floodChannelScopes(), scope_count * sizeof(FloodChannelScopeEntry)); } success = success && writeExact(&direct_count, sizeof(direct_count)); if (success && direct_count != 0) { - success = writeExact(flood_channel_direct_scopes, + success = writeExact(floodChannelDirectScopes(), direct_count * FLOOD_PACKET_FILTER_SCOPE_NAME_LEN); } @@ -9304,9 +9309,9 @@ static const char* wildcardFloodChannelScopeName(uint8_t selector) { bool MyMesh::loadFloodChannelScopes() { static_assert(sizeof(FloodChannelScopeEntry) == 36, "channel scope persistence requires 36-byte entries"); - memset(flood_channel_scopes, 0, sizeof(flood_channel_scopes)); - memset(flood_channel_direct_scopes, 0, - sizeof(flood_channel_direct_scopes)); + memset(floodChannelScopes(), 0, sizeof(floodChannelScopes())); + memset(floodChannelDirectScopes(), 0, + sizeof(floodChannelDirectScopes())); if (_fs == NULL || !_fs->exists(FLOOD_CHANNEL_SCOPE_FILE)) return true; File file = openFloodSettingsRead(_fs, FLOOD_CHANNEL_SCOPE_FILE); if (!file) return false; @@ -9323,7 +9328,7 @@ bool MyMesh::loadFloodChannelScopes() { && file.read(&count, sizeof(count)) == sizeof(count); uint8_t retained = count < FLOOD_CHANNEL_SCOPE_SLOTS ? count : FLOOD_CHANNEL_SCOPE_SLOTS; size_t retained_bytes = (size_t)retained * sizeof(FloodChannelScopeEntry); - success = success && file.read((uint8_t*)flood_channel_scopes, retained_bytes) == retained_bytes; + success = success && file.read((uint8_t*)floodChannelScopes(), retained_bytes) == retained_bytes; FloodChannelScopeEntry discarded; for (int i = retained; success && i < count; i++) { success = file.read((uint8_t*)&discarded, sizeof(discarded)) == sizeof(discarded); @@ -9338,10 +9343,10 @@ bool MyMesh::loadFloodChannelScopes() { size_t retained_direct_bytes = (size_t)retained_direct * FLOOD_PACKET_FILTER_SCOPE_NAME_LEN; success = success - && file.read((uint8_t*)flood_channel_direct_scopes, + && file.read((uint8_t*)floodChannelDirectScopes(), retained_direct_bytes) == retained_direct_bytes; for (int i = 0; success && i < retained_direct; i++) { - char* name = flood_channel_direct_scopes[i]; + char* name = floodChannelDirectScopes()[i]; if (name[0] != 0 && !isValidStoredFloodFilterScopeName(name)) { success = false; } @@ -9349,7 +9354,7 @@ bool MyMesh::loadFloodChannelScopes() { } for (int i = 0; success && i < retained; i++) { - auto& entry = flood_channel_scopes[i]; + auto& entry = floodChannelScopes()[i]; if ((version_1 && (FloodFilterPolicy::scopeUsesSlowTiming(entry.selector) || FloodFilterPolicy::scopeRequiresPath(entry.selector))) @@ -9370,7 +9375,7 @@ bool MyMesh::loadFloodChannelScopes() { } else if (direct_target && (!version_5 || entry.target_id > FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS - || flood_channel_direct_scopes[entry.target_id - 1][0] == 0)) { + || floodChannelDirectScopes()[entry.target_id - 1][0] == 0)) { success = false; break; } else if (selector <= FLOOD_CHANNEL_SCOPE_OTHER_ANY) { @@ -9390,9 +9395,9 @@ bool MyMesh::loadFloodChannelScopes() { // A malformed table is inert; corrupt storage must never assign a scope. if (!success) { - memset(flood_channel_scopes, 0, sizeof(flood_channel_scopes)); - memset(flood_channel_direct_scopes, 0, - sizeof(flood_channel_direct_scopes)); + memset(floodChannelScopes(), 0, sizeof(floodChannelScopes())); + memset(floodChannelDirectScopes(), 0, + sizeof(floodChannelDirectScopes())); } return success; } @@ -9410,17 +9415,17 @@ bool MyMesh::saveFloodChannelScopes(bool empty_table) { bool success = file.write(magic, sizeof(magic)) == sizeof(magic) && file.write(&count, sizeof(count)) == sizeof(count); if (success && count != 0) { - success = file.write((const uint8_t*)flood_channel_scopes, - sizeof(flood_channel_scopes)) == sizeof(flood_channel_scopes); + success = file.write((const uint8_t*)floodChannelScopes(), + sizeof(floodChannelScopes())) == sizeof(floodChannelScopes()); } uint8_t direct_count = empty_table ? 0 : FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS; success = success && file.write(&direct_count, sizeof(direct_count)) == sizeof(direct_count); if (success && direct_count != 0) { - success = file.write((const uint8_t*)flood_channel_direct_scopes, - sizeof(flood_channel_direct_scopes)) - == sizeof(flood_channel_direct_scopes); + success = file.write((const uint8_t*)floodChannelDirectScopes(), + sizeof(floodChannelDirectScopes())) + == sizeof(floodChannelDirectScopes()); } file.close(); if (!success || !_fs->rename(FLOOD_CHANNEL_SCOPE_TEMP_FILE, FLOOD_CHANNEL_SCOPE_FILE)) { @@ -9432,8 +9437,8 @@ bool MyMesh::saveFloodChannelScopes(bool empty_table) { } void MyMesh::loadFloodChannelScopeRequirements() { - memset(flood_channel_scope_requirements, 0, - sizeof(flood_channel_scope_requirements)); + memset(floodChannelScopeRequirements(), 0, + sizeof(floodChannelScopeRequirements())); if (_fs == NULL || !_fs->exists(FLOOD_CHANNEL_SCOPE_REQUIRE_FILE)) return; File file = @@ -9450,7 +9455,7 @@ void MyMesh::loadFloodChannelScopeRequirements() { size_t retained_bytes = (size_t)retained * sizeof(FloodChannelScopeRequireEntry); success = success - && file.read((uint8_t*)flood_channel_scope_requirements, retained_bytes) + && file.read((uint8_t*)floodChannelScopeRequirements(), retained_bytes) == retained_bytes; FloodChannelScopeRequireEntry discarded; for (int i = retained; success && i < count; i++) { @@ -9458,7 +9463,7 @@ void MyMesh::loadFloodChannelScopeRequirements() { == sizeof(discarded); } for (int i = 0; success && i < retained; i++) { - auto& entry = flood_channel_scope_requirements[i]; + auto& entry = floodChannelScopeRequirements()[i]; if (entry.key_len == 0) { memset(&entry, 0, sizeof(entry)); } else if (entry.key_len == CIPHER_KEY_SIZE @@ -9476,8 +9481,8 @@ void MyMesh::loadFloodChannelScopeRequirements() { file.close(); if (!success) { - memset(flood_channel_scope_requirements, 0, - sizeof(flood_channel_scope_requirements)); + memset(floodChannelScopeRequirements(), 0, + sizeof(floodChannelScopeRequirements())); } } @@ -9500,9 +9505,9 @@ bool MyMesh::saveFloodChannelScopeRequirements(bool empty_table) { } } else if (success) { success = file.write( - (const uint8_t*)flood_channel_scope_requirements, - sizeof(flood_channel_scope_requirements)) - == sizeof(flood_channel_scope_requirements); + (const uint8_t*)floodChannelScopeRequirements(), + sizeof(floodChannelScopeRequirements())) + == sizeof(floodChannelScopeRequirements()); } file.close(); if (!success || !_fs->rename(FLOOD_CHANNEL_SCOPE_REQUIRE_TEMP_FILE, @@ -9528,7 +9533,7 @@ bool MyMesh::findFloodChannelScopeRequirementMatch( % CIPHER_BLOCK_SIZE) == 0; for (int i = 0; i < FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS; i++) { - const auto& entry = flood_channel_scope_requirements[i]; + const auto& entry = floodChannelScopeRequirements()[i]; if (entry.key_len == 0) continue; table_active = true; if (!valid_layout || packet->payload[0] != entry.channel_hash) continue; @@ -9558,7 +9563,7 @@ bool MyMesh::applyFloodChannelScopeTarget(mesh::Packet* packet, bool direct_target = FloodFilterPolicy::channelScopeUsesDirectTarget(entry.selector); if (direct_target) { - target_name = flood_channel_direct_scopes[entry.target_id - 1]; + target_name = floodChannelDirectScopes()[entry.target_id - 1]; deriveFloodFilterScopeKey(target_name, scope); regionless_scope_set = true; } else { @@ -9644,7 +9649,7 @@ bool MyMesh::applyFloodChannelScope(mesh::Packet* packet, bool& fast_track, uint8_t data[MAX_PACKET_PAYLOAD]; for (int path_pass = 1; path_pass >= 0; path_pass--) { for (int i = 0; i < FLOOD_CHANNEL_SCOPE_SLOTS; i++) { - const auto& entry = flood_channel_scopes[i]; + const auto& entry = floodChannelScopes()[i]; if (entry.target_id == 0 || !isExactFloodChannelScopeSelector(entry.selector) || packet->payload[0] != entry.channel_hash || !matches_path_pass(entry.selector, path_pass != 0)) { @@ -9665,7 +9670,7 @@ bool MyMesh::applyFloodChannelScope(mesh::Packet* packet, bool& fast_track, uint8_t wildcard = wildcardFloodChannelScopeSelector(type); for (int path_pass = 1; path_pass >= 0; path_pass--) { for (int i = 0; i < FLOOD_CHANNEL_SCOPE_SLOTS; i++) { - const auto& entry = flood_channel_scopes[i]; + const auto& entry = floodChannelScopes()[i]; if (entry.target_id != 0 && FloodFilterPolicy::channelScopeMatchSelectorValue(entry.selector) == wildcard @@ -9819,7 +9824,7 @@ void MyMesh::formatFloodChannelScopeDetail(int index, char* reply, size_t reply_ snprintf(reply, reply_len, "Err - scope slot must be 1-%d", FLOOD_CHANNEL_SCOPE_SLOTS); return; } - const auto& entry = flood_channel_scopes[index]; + const auto& entry = floodChannelScopes()[index]; if (entry.target_id == 0) { snprintf(reply, reply_len, "> %d empty", index + 1); return; @@ -9856,7 +9861,7 @@ void MyMesh::formatFloodChannelScopeDetail(int index, char* reply, size_t reply_ if (FloodFilterPolicy::channelScopeUsesDirectTarget(entry.selector)) { snprintf(reply, reply_len, "> %d match=%s%s scope=%s%s", index + 1, channel, path, - flood_channel_direct_scopes[entry.target_id - 1], timing); + floodChannelDirectScopes()[entry.target_id - 1], timing); return; } @@ -9888,7 +9893,7 @@ void MyMesh::formatFloodChannelScopes(const char* args, char* reply) { int active = 0; for (int i = 0; i < FLOOD_CHANNEL_SCOPE_SLOTS; i++) { - if (flood_channel_scopes[i].target_id != 0) active++; + if (floodChannelScopes()[i].target_id != 0) active++; } #if defined(STM32_PLATFORM) snprintf(reply, 160, "> %d/%d", @@ -10016,7 +10021,7 @@ void MyMesh::setFloodChannelScope(const char* args, char* reply) { int matching_slot = -1; int free_slot = -1; for (int i = 0; i < FLOOD_CHANNEL_SCOPE_SLOTS; i++) { - const auto& entry = flood_channel_scopes[i]; + const auto& entry = floodChannelScopes()[i]; if (entry.target_id == 0) { if (free_slot < 0) free_slot = i; } else if (FloodFilterPolicy::channelScopeMatchSelectorValue( @@ -10042,7 +10047,7 @@ void MyMesh::setFloodChannelScope(const char* args, char* reply) { if (direct_target) { int reusable_direct_scope_slot = -1; for (int i = 0; i < FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS; i++) { - if (strcmp(flood_channel_direct_scopes[i], direct_scope_name) == 0) { + if (strcmp(floodChannelDirectScopes()[i], direct_scope_name) == 0) { direct_scope_slot = i; break; } @@ -10050,7 +10055,7 @@ void MyMesh::setFloodChannelScope(const char* args, char* reply) { bool referenced = false; for (int row = 0; row < FLOOD_CHANNEL_SCOPE_SLOTS; row++) { if (row == slot) continue; - const auto& entry = flood_channel_scopes[row]; + const auto& entry = floodChannelScopes()[row]; if (entry.target_id == i + 1 && FloodFilterPolicy::channelScopeUsesDirectTarget( entry.selector)) { @@ -10070,15 +10075,15 @@ void MyMesh::setFloodChannelScope(const char* args, char* reply) { strcpy(reply, "Err - scope table full"); return; } - if (strcmp(flood_channel_direct_scopes[direct_scope_slot], + if (strcmp(floodChannelDirectScopes()[direct_scope_slot], direct_scope_name) != 0) { changed_direct_scope_slot = direct_scope_slot; memcpy(previous_direct_scope, - flood_channel_direct_scopes[direct_scope_slot], + floodChannelDirectScopes()[direct_scope_slot], sizeof(previous_direct_scope)); - StrHelper::strzcpy(flood_channel_direct_scopes[direct_scope_slot], + StrHelper::strzcpy(floodChannelDirectScopes()[direct_scope_slot], direct_scope_name, - sizeof(flood_channel_direct_scopes[direct_scope_slot])); + sizeof(floodChannelDirectScopes()[direct_scope_slot])); } } @@ -10086,8 +10091,8 @@ void MyMesh::setFloodChannelScope(const char* args, char* reply) { FloodFilterPolicy::encodeChannelScopeTargetSelector(selector, direct_target); - FloodChannelScopeEntry previous = flood_channel_scopes[slot]; - auto& entry = flood_channel_scopes[slot]; + FloodChannelScopeEntry previous = floodChannelScopes()[slot]; + auto& entry = floodChannelScopes()[slot]; memset(&entry, 0, sizeof(entry)); entry.target_id = direct_target ? direct_scope_slot + 1 : region->id; entry.selector = @@ -10100,7 +10105,7 @@ void MyMesh::setFloodChannelScope(const char* args, char* reply) { if (!saveFloodChannelScopes()) { entry = previous; if (changed_direct_scope_slot >= 0) { - memcpy(flood_channel_direct_scopes[changed_direct_scope_slot], + memcpy(floodChannelDirectScopes()[changed_direct_scope_slot], previous_direct_scope, sizeof(previous_direct_scope)); } strcpy(reply, "Err - save failed"); @@ -10115,9 +10120,9 @@ void MyMesh::deleteFloodChannelScope(const char* args, char* reply) { if (!saveFloodChannelScopes(true)) { strcpy(reply, "Err - save failed"); } else { - memset(flood_channel_scopes, 0, sizeof(flood_channel_scopes)); - memset(flood_channel_direct_scopes, 0, - sizeof(flood_channel_direct_scopes)); + memset(floodChannelScopes(), 0, sizeof(floodChannelScopes())); + memset(floodChannelDirectScopes(), 0, + sizeof(floodChannelDirectScopes())); strcpy(reply, "OK"); } return; @@ -10129,14 +10134,14 @@ void MyMesh::deleteFloodChannelScope(const char* args, char* reply) { strcpy(reply, "Err - use: del flood.channel.scope.|all"); return; } - if (flood_channel_scopes[index].target_id == 0) { + if (floodChannelScopes()[index].target_id == 0) { strcpy(reply, "Err - empty scope slot"); return; } - FloodChannelScopeEntry previous = flood_channel_scopes[index]; - memset(&flood_channel_scopes[index], 0, sizeof(flood_channel_scopes[index])); + FloodChannelScopeEntry previous = floodChannelScopes()[index]; + memset(&floodChannelScopes()[index], 0, sizeof(floodChannelScopes()[index])); if (!saveFloodChannelScopes()) { - flood_channel_scopes[index] = previous; + floodChannelScopes()[index] = previous; strcpy(reply, "Err - save failed"); } else { strcpy(reply, "OK"); @@ -10150,7 +10155,7 @@ void MyMesh::formatFloodChannelScopeRequirementDetail( FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS); return; } - const auto& entry = flood_channel_scope_requirements[index]; + const auto& entry = floodChannelScopeRequirements()[index]; if (entry.key_len == 0) { snprintf(reply, reply_len, "> %d empty", index + 1); return; @@ -10184,7 +10189,7 @@ void MyMesh::formatFloodChannelScopeRequirements(const char* args, int active = 0; for (int i = 0; i < FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS; i++) { - if (flood_channel_scope_requirements[i].key_len != 0) active++; + if (floodChannelScopeRequirements()[i].key_len != 0) active++; } snprintf(reply, 160, "> %d/%d active; listed=require allowed scope, others=bypass", @@ -10221,7 +10226,7 @@ void MyMesh::setFloodChannelScopeRequirement(const char* args, char* reply) { int matching_slot = -1; int free_slot = -1; for (int i = 0; i < FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS; i++) { - const auto& entry = flood_channel_scope_requirements[i]; + const auto& entry = floodChannelScopeRequirements()[i]; if (entry.key_len == 0) { if (free_slot < 0) free_slot = i; } else if (entry.key_len == key_len @@ -10239,8 +10244,8 @@ void MyMesh::setFloodChannelScopeRequirement(const char* args, char* reply) { } FloodChannelScopeRequireEntry previous = - flood_channel_scope_requirements[slot]; - auto& entry = flood_channel_scope_requirements[slot]; + floodChannelScopeRequirements()[slot]; + auto& entry = floodChannelScopeRequirements()[slot]; memset(&entry, 0, sizeof(entry)); entry.key_len = key_len; entry.channel_hash = hash_prefix[0]; @@ -10265,8 +10270,8 @@ void MyMesh::deleteFloodChannelScopeRequirement(const char* args, if (!saveFloodChannelScopeRequirements(true)) { strcpy(reply, "Err - save failed"); } else { - memset(flood_channel_scope_requirements, 0, - sizeof(flood_channel_scope_requirements)); + memset(floodChannelScopeRequirements(), 0, + sizeof(floodChannelScopeRequirements())); strcpy(reply, "OK"); } return; @@ -10278,17 +10283,17 @@ void MyMesh::deleteFloodChannelScopeRequirement(const char* args, strcpy(reply, "Err - use: del flood.channel.scope.require.|all"); return; } - if (flood_channel_scope_requirements[index].key_len == 0) { + if (floodChannelScopeRequirements()[index].key_len == 0) { strcpy(reply, "Err - empty require slot"); return; } FloodChannelScopeRequireEntry previous = - flood_channel_scope_requirements[index]; - memset(&flood_channel_scope_requirements[index], 0, - sizeof(flood_channel_scope_requirements[index])); + floodChannelScopeRequirements()[index]; + memset(&floodChannelScopeRequirements()[index], 0, + sizeof(floodChannelScopeRequirements()[index])); if (!saveFloodChannelScopeRequirements()) { - flood_channel_scope_requirements[index] = previous; + floodChannelScopeRequirements()[index] = previous; strcpy(reply, "Err - save failed"); } else { strcpy(reply, "OK"); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 6e6ee587c..8448b7e62 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -560,8 +560,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks uint8_t flood_packet_filter_blacklist[FLOOD_PACKET_FILTER_BLACKLIST_MAX] [FLOOD_PACKET_FILTER_PATH_ID_SIZE]; #if MESH_REPEATER_PSRAM_TABLES - // Keep real array references: persistence, resets and rollback use sizeof - // these complete tables, and routing retains addresses into them. + // Only the pointer exists during global construction. Array-reference + // accessors preserve sizeof for persistence, resets and rollback. struct FloodChannelStorage { FloodChannelScopeEntry scopes[FLOOD_CHANNEL_SCOPE_SLOTS]; char direct[FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS][FLOOD_PACKET_FILTER_SCOPE_NAME_LEN]; @@ -575,11 +575,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks + FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS * 34U + 2U, "Update the repeater PSRAM fallback budget in check_firmware_ram.py"); mesh::RepeaterPsramStorage flood_channel_storage; - FloodChannelScopeEntry (&flood_channel_scopes)[FLOOD_CHANNEL_SCOPE_SLOTS]; - char (&flood_channel_direct_scopes)[FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS] - [FLOOD_PACKET_FILTER_SCOPE_NAME_LEN]; - FloodChannelScopeRequireEntry - (&flood_channel_scope_requirements)[FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS]; #else FloodChannelScopeEntry flood_channel_scopes[FLOOD_CHANNEL_SCOPE_SLOTS]; char flood_channel_direct_scopes[FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS] @@ -587,6 +582,53 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks FloodChannelScopeRequireEntry flood_channel_scope_requirements[FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS]; #endif + using FloodChannelScopes = FloodChannelScopeEntry[FLOOD_CHANNEL_SCOPE_SLOTS]; + using FloodChannelDirectScopes = char[FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS] + [FLOOD_PACKET_FILTER_SCOPE_NAME_LEN]; + using FloodChannelScopeRequirements = + FloodChannelScopeRequireEntry[FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS]; + FloodChannelScopes& floodChannelScopes() { +#if MESH_REPEATER_PSRAM_TABLES + return flood_channel_storage.require().scopes; +#else + return flood_channel_scopes; +#endif + } + const FloodChannelScopes& floodChannelScopes() const { +#if MESH_REPEATER_PSRAM_TABLES + return flood_channel_storage.require().scopes; +#else + return flood_channel_scopes; +#endif + } + FloodChannelDirectScopes& floodChannelDirectScopes() { +#if MESH_REPEATER_PSRAM_TABLES + return flood_channel_storage.require().direct; +#else + return flood_channel_direct_scopes; +#endif + } + const FloodChannelDirectScopes& floodChannelDirectScopes() const { +#if MESH_REPEATER_PSRAM_TABLES + return flood_channel_storage.require().direct; +#else + return flood_channel_direct_scopes; +#endif + } + FloodChannelScopeRequirements& floodChannelScopeRequirements() { +#if MESH_REPEATER_PSRAM_TABLES + return flood_channel_storage.require().requirements; +#else + return flood_channel_scope_requirements; +#endif + } + const FloodChannelScopeRequirements& floodChannelScopeRequirements() const { +#if MESH_REPEATER_PSRAM_TABLES + return flood_channel_storage.require().requirements; +#else + return flood_channel_scope_requirements; +#endif + } #if MESH_ENABLE_FLOOD_GROUP_MODERATION FloodGroupModerationEntry flood_group_moderation[FLOOD_GROUP_MODERATION_SLOTS]; #endif diff --git a/scripts/check_firmware_ram.py b/scripts/check_firmware_ram.py index 8624b6e5a..c9bda1121 100644 --- a/scripts/check_firmware_ram.py +++ b/scripts/check_firmware_ram.py @@ -355,7 +355,7 @@ def requirements(platform, defines, target): slots = integer(defines, "FLOOD_PACKET_FILTER_SLOTS", 63 if engine else 16) parts["flood_filter_table"] = slots * (304 if engine else 40) + 16 psram_tables_default = int(platform == "ESP32_PLATFORM" and qualified_repeater - and "STATION_G3_ESP32" in defines + and integer(defines, "CONFIG_IDF_TARGET_ESP32S3", 0) and "BOARD_HAS_PSRAM" in defines) if platform == "ESP32_PLATFORM" and integer( defines, "MESH_REPEATER_PSRAM_TABLES", psram_tables_default): @@ -605,7 +605,12 @@ def heap_regions(elf, platform, mcu): def check_firmware(elf_path, platform, mcu, defines, target, output=None): elf = FirmwareElf(elf_path) if platform == "ESP32_PLATFORM": - defines = {**defines, "MESH_RAM_DEFAULT_RECENT_REPEATERS": 256 if str(mcu).lower() == "esp32" else 2048} + # SDK target macros are absent from SCons CPPDEFINES. Resolve them + # from the actual board MCU so source-selected PSRAM tables cannot + # disappear from the fallback budget on a non-G3 ESP32-S3. + target_mcu = str(mcu).lower() + defines = {**defines, "CONFIG_IDF_TARGET_ESP32S3": int(target_mcu == "esp32s3"), + "MESH_RAM_DEFAULT_RECENT_REPEATERS": 256 if target_mcu == "esp32" else 2048} policy = requirements(platform, defines, target) regions = heap_regions(elf, platform, mcu) available = sum(end - start for start, end, _ in regions) diff --git a/src/helpers/ESP32WsTransportFix.cpp b/src/helpers/ESP32WsTransportFix.cpp index a4702cae4..888955db6 100644 --- a/src/helpers/ESP32WsTransportFix.cpp +++ b/src/helpers/ESP32WsTransportFix.cpp @@ -67,7 +67,7 @@ #include "esp_transport.h" #include "esp_transport_ws.h" -#if defined(STATION_G3_ESP32) && defined(BOARD_HAS_PSRAM) +#if defined(BOARD_HAS_PSRAM) #include "esp_heap_caps.h" #include "MQTTConnectionAdmission.h" #endif @@ -109,7 +109,7 @@ esp_transport_handle_t __wrap_esp_transport_ws_init(esp_transport_handle_t paren // The buffer is untouched at this point (allocated moments ago inside // __real_esp_transport_ws_init), so a swap is safe. const size_t bytes = CONFIG_WS_BUFFER_SIZE + 1; -#if defined(STATION_G3_ESP32) && defined(BOARD_HAS_PSRAM) +#if defined(BOARD_HAS_PSRAM) // Upgrade request/response text is CPU-only. Retain this padded buffer // until the SDK destroys the transport; its ordinary free accepts PSRAM. char* padded = (char*)heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); diff --git a/src/helpers/FloodAdvertLimiter.h b/src/helpers/FloodAdvertLimiter.h index abe3ad1fa..9f2f95f07 100644 --- a/src/helpers/FloodAdvertLimiter.h +++ b/src/helpers/FloodAdvertLimiter.h @@ -91,8 +91,8 @@ protected: #else FloodAdvertLimiter(Entry* entries, size_t capacity) : _entries(entries), _capacity(capacity) {} #if MESH_REPEATER_PSRAM_TABLES - // The derived storage owner is initialized after this base. Bind its full - // table before reset() or any limiter operation can inspect the entries. + // Bind the complete table from begin(), after Arduino initializes PSRAM, + // before reset() or any limiter operation can inspect the entries. void bindStorage(Entry* entries) { if (!entries || reinterpret_cast(entries) % alignof(Entry)) abort(); _entries = entries; @@ -211,18 +211,25 @@ public: struct Storage { Entry entries[Capacity]; }; RepeaterPsramStorage _storage; public: - StaticFloodAdvertLimiter() : FloodAdvertLimiter(nullptr, Capacity) { + StaticFloodAdvertLimiter() : FloodAdvertLimiter(nullptr, Capacity) {} + bool begin() { + if (_storage.data()) return true; // Exhaustion must stop startup rather than disabling advert protection // or reducing its source/hash history. No full internal array is retained. - if (!_storage.begin("advert source history")) abort(); + if (!_storage.begin("advert source history")) return false; bindStorage(_storage.data()->entries); reset(); + return true; } #else Entry _storage[Capacity]; public: StaticFloodAdvertLimiter() : FloodAdvertLimiter(_storage, Capacity) { reset(); } #endif +#if !MESH_REPEATER_PSRAM_TABLES || FLOOD_ADVERT_IRAM_WORD_STORAGE \ + || FLOOD_ADVERT_RTC_FAST_SLOTS || FLOOD_ADVERT_RTC_SLOW_SLOTS + bool begin() { return true; } +#endif }; } // namespace mesh diff --git a/src/helpers/RepeaterPsramStorage.h b/src/helpers/RepeaterPsramStorage.h index 5993f1c88..c975b0245 100644 --- a/src/helpers/RepeaterPsramStorage.h +++ b/src/helpers/RepeaterPsramStorage.h @@ -1,7 +1,12 @@ #pragma once +#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM) +#include +#endif + #ifndef MESH_REPEATER_PSRAM_TABLES - #if defined(STATION_G3_ESP32) && defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM) \ + #if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM) \ + && defined(CONFIG_IDF_TARGET_ESP32S3) && CONFIG_IDF_TARGET_ESP32S3 \ && defined(MESH_CLIENT_REPEATER_ONLY) && MESH_CLIENT_REPEATER_ONLY #define MESH_REPEATER_PSRAM_TABLES 1 #else @@ -17,12 +22,14 @@ #if !defined(ESP32_PLATFORM) || !defined(BOARD_HAS_PSRAM) #error "Repeater PSRAM tables require an ESP32 target with PSRAM support" #endif +#if !defined(MESH_CLIENT_REPEATER_ONLY) || !MESH_CLIENT_REPEATER_ONLY +#error "Repeater PSRAM tables require the repeater startup path" +#endif #include #if !defined(CONFIG_IDF_TARGET_ESP32S3) || !CONFIG_IDF_TARGET_ESP32S3 \ - || !defined(CONFIG_SPIRAM_BOOT_INIT) || !CONFIG_SPIRAM_BOOT_INIT \ || !defined(CONFIG_SPIRAM_USE_MALLOC) || !CONFIG_SPIRAM_USE_MALLOC -#error "Repeater PSRAM tables require ESP32-S3 with boot-initialized PSRAM heaps" +#error "Repeater PSRAM tables require ESP32-S3 with PSRAM heaps" #endif #include @@ -34,8 +41,8 @@ namespace mesh { // Own only CPU data here. ISR state, locks, task stacks and DMA buffers retain -// their existing internal storage. G3's pinned SDK initializes PSRAM-backed -// heaps before global constructors, so tables can keep their startup lifetime. +// their existing internal storage. Allocate from begin(), after initArduino() +// initializes both QSPI and OPI PSRAM. Global constructors own only a pointer. template class RepeaterPsramStorage { static_assert(std::is_trivial::value, "Repeater PSRAM tables require zero-initialized trivial data"); @@ -67,10 +74,14 @@ public: T* data() { return value_; } const T* data() const { return value_; } - T& require(const char* label) { - // Used only while binding the owner's references during construction. - // Allocation never moves after pointers escape to routing or persistence. - if (!begin(label)) abort(); + T& require() { + // Never allocate through a getter: initialization must precede routing + // and persistence, and allocation never moves after pointers escape. + if (!value_) abort(); + return *value_; + } + const T& require() const { + if (!value_) abort(); return *value_; } }; diff --git a/test/test_firmware_ram.py b/test/test_firmware_ram.py index 6578b768e..10c8e49ca 100644 --- a/test/test_firmware_ram.py +++ b/test/test_firmware_ram.py @@ -453,8 +453,8 @@ int main() { sensor = ram.requirements("NRF52_PLATFORM", {}, "t114_sensor") self.assertEqual(sensor["components"]["client_table"], 32 * 320 + 16) - def test_g3_psram_tables_reserve_full_internal_fallback_without_cutting_other_owners(self): - flags = {"STATION_G3_ESP32": 1, "BOARD_HAS_PSRAM": 1, + def test_s3_psram_tables_reserve_full_internal_fallback_without_cutting_other_owners(self): + flags = {"CONFIG_IDF_TARGET_ESP32S3": 1, "BOARD_HAS_PSRAM": 1, "MESH_CLIENT_REPEATER_ONLY": 1, "WITH_MQTT_BRIDGE": 1, "DISPLAY_CLASS": "SH1106Display", "ADMIN_PASSWORD": "test", "ENABLE_OTA": 1, "WITH_UDP_BRIDGE": 1, "WITH_RS232_BRIDGE": "Serial2"} @@ -475,11 +475,11 @@ int main() { self.assertEqual(ram.requirements("ESP32_PLATFORM", { **flags, "MESH_REPEATER_PSRAM_TABLES": 1}, target), selected) - def test_g3_psram_table_defaults_follow_board_psram_platform_and_source_role_gates(self): - flags = {"STATION_G3_ESP32": 1, "BOARD_HAS_PSRAM": 1, + def test_psram_table_defaults_follow_s3_psram_platform_and_source_role_gates(self): + flags = {"CONFIG_IDF_TARGET_ESP32S3": 1, "BOARD_HAS_PSRAM": 1, "MESH_CLIENT_REPEATER_ONLY": 1} cases = [ - ("ESP32_PLATFORM", {k: v for k, v in flags.items() if k != "STATION_G3_ESP32"}, + ("ESP32_PLATFORM", {k: v for k, v in flags.items() if k != "CONFIG_IDF_TARGET_ESP32S3"}, "other_s3_repeater"), ("ESP32_PLATFORM", {k: v for k, v in flags.items() if k != "BOARD_HAS_PSRAM"}, "Station_G3_ESP32_repeater"), @@ -493,6 +493,10 @@ int main() { ("ESP32_PLATFORM", {**flags, "MESH_CLIENT_REPEATER_ONLY": 0}, "Station_G3_ESP32_room_server"), ] + for mcu in ("ESP32", "ESP32S2", "ESP32C3", "ESP32C6"): + cases.append(("ESP32_PLATFORM", { + **flags, "CONFIG_IDF_TARGET_ESP32S3": 0, + "CONFIG_IDF_TARGET_" + mcu: 1}, "other_repeater")) for platform in ("NRF52_PLATFORM", "RP2040_PLATFORM", "STM32_PLATFORM"): cases.extend((platform, {**flags, **extra}, "other_repeater") for extra in ({}, {"MESH_REPEATER_PSRAM_TABLES": 1})) @@ -508,6 +512,30 @@ int main() { self.assertIn("repeater_scope_storage_fallback", renamed["components"]) self.assertIn("repeater_advert_storage_fallback", renamed["components"]) + def test_psram_repeater_table_defaults_cover_opi_and_qspi_families_without_a_g3_marker(self): + flags = {"BOARD_HAS_PSRAM": 1, "MESH_CLIENT_REPEATER_ONLY": 1, + "CONFIG_IDF_TARGET_ESP32S3": 1, "CONFIG_SPIRAM_USE_MALLOC": 1} + families = ( + ("LilyGo_TDeck_repeater", {"CONFIG_SPIRAM_MODE_OCT": 1, + "CONFIG_SPIRAM_BOOT_INIT": 1}), + ("Station_G2_repeater", {"CONFIG_SPIRAM_MODE_OCT": 1, + "CONFIG_SPIRAM_BOOT_INIT": 1}), + ("heltec_v4_repeater", {"CONFIG_SPIRAM_MODE_QUAD": 1}), + ("SenseCapIndicator-LoRa-N16R2_repeater", {"CONFIG_SPIRAM_MODE_QUAD": 1}), + ) + for target, extra in families: + with self.subTest(target=target): + defines = {**flags, **extra} + self.assertNotIn("STATION_G3_ESP32", defines) + selected = ram.requirements("ESP32_PLATFORM", defines, target) + original = ram.requirements("ESP32_PLATFORM", { + **defines, "MESH_REPEATER_PSRAM_TABLES": 0}, target) + self.assertEqual(selected["components"], { + **original["components"], "repeater_scope_storage_fallback": 18892, + "repeater_advert_storage_fallback": 17936}) + self.assertEqual(selected["required_heap_bytes"] - original["required_heap_bytes"], + 36828) + def test_explicit_psram_repeater_table_opt_in_and_capacity_overrides_are_fully_budgeted(self): flags = {"BOARD_HAS_PSRAM": 1, "MESH_CLIENT_REPEATER_ONLY": 1, "MESH_REPEATER_PSRAM_TABLES": 1} @@ -537,7 +565,7 @@ int main() { original["required_heap_bytes"] + scope + advert) def test_psram_advert_budget_excludes_portable_iram_and_rtc_storage_only(self): - flags = {"STATION_G3_ESP32": 1, "BOARD_HAS_PSRAM": 1, + flags = {"CONFIG_IDF_TARGET_ESP32S3": 1, "BOARD_HAS_PSRAM": 1, "MESH_CLIENT_REPEATER_ONLY": 1} target = "Station_G3_ESP32_repeater" original = ram.requirements("ESP32_PLATFORM", flags, target) @@ -560,7 +588,7 @@ int main() { **flags, **disabled_backends}, target), original) def test_each_psram_table_fallback_can_set_the_contiguous_allocation_bound(self): - flags = {"STATION_G3_ESP32": 1, "BOARD_HAS_PSRAM": 1, + flags = {"CONFIG_IDF_TARGET_ESP32S3": 1, "BOARD_HAS_PSRAM": 1, "MESH_CLIENT_REPEATER_ONLY": 1, "MAX_CLIENTS": 1, "FLOOD_PACKET_FILTER_SLOTS": 1, "MAX_RECENT_REPEATERS": 0} for extra, name in (({}, "repeater_scope_storage_fallback"), @@ -571,13 +599,14 @@ int main() { self.assertEqual(policy["required_contiguous_bytes"], policy["components"][name]) self.assertGreater(policy["required_contiguous_bytes"], 16384) - def test_g3_psram_fallback_elf_gate_rejects_old_budget_and_enforces_exact_total(self): - flags = {"STATION_G3_ESP32": 1, "BOARD_HAS_PSRAM": 1, + def test_s3_psram_fallback_elf_gate_rejects_old_budget_and_enforces_exact_total(self): + flags = {"BOARD_HAS_PSRAM": 1, "MESH_CLIENT_REPEATER_ONLY": 1, "WITH_MQTT_BRIDGE": 1} - target = "Station_G3_ESP32_repeater" + target = "heltec_v4_repeater" previous = {**flags, "MESH_REPEATER_PSRAM_TABLES": 0} old = ram.requirements("ESP32_PLATFORM", previous, target)["required_heap_bytes"] - required = ram.requirements("ESP32_PLATFORM", flags, target)["required_heap_bytes"] + required = ram.requirements("ESP32_PLATFORM", { + **flags, "CONFIG_IDF_TARGET_ESP32S3": 1}, target)["required_heap_bytes"] with tempfile.TemporaryDirectory() as temporary: path = Path(temporary) / "firmware.elf" report = Path(temporary) / "firmware.memory.json" @@ -596,6 +625,38 @@ int main() { self.assertEqual(result["components"]["repeater_scope_storage_fallback"], 18892) self.assertEqual(result["components"]["repeater_advert_storage_fallback"], 17936) + def test_psram_table_elf_policy_uses_authoritative_mcu_over_missing_or_stale_sdk_macros(self): + flags = {"BOARD_HAS_PSRAM": 1, "MESH_CLIENT_REPEATER_ONLY": 1} + target = "board_repeater" + with tempfile.TemporaryDirectory() as temporary: + path = Path(temporary) / "firmware.elf" + report = Path(temporary) / "firmware.memory.json" + for mcu, stale_macro in (("esp32s3", None), ("esp32s3", 0), + ("esp32", None), ("esp32", 1)): + with self.subTest(mcu=mcu, stale_macro=stale_macro), contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): + defines = dict(flags) + if stale_macro is not None: + defines["CONFIG_IDF_TARGET_ESP32S3"] = stale_macro + expected = ram.requirements("ESP32_PLATFORM", { + **defines, "CONFIG_IDF_TARGET_ESP32S3": int(mcu == "esp32s3"), + "MESH_RAM_DEFAULT_RECENT_REPEATERS": 256 if mcu == "esp32" else 2048, + }, target) + capacity = expected["required_heap_bytes"] + # Classic ESP32 reclaims the second 64 KiB ROM-stack + # region. The S3 excludes it; both exclude the first 16 KiB. + length = capacity + 0x4000 - (0x10000 if mcu == "esp32" else 0) + esp_fixture(path, modern=True, internal_length=length) + status = ram.check_firmware(path, "ESP32_PLATFORM", mcu, + defines, target, report) + result = json.loads(report.read_text()) + self.assertEqual(status, 0) + self.assertTrue(result["passed"]) + self.assertEqual(result["available_internal_bytes"], capacity) + self.assertEqual(result["required_heap_bytes"], capacity) + self.assertEqual(result["components"], expected["components"]) + for name in ("repeater_scope_storage_fallback", "repeater_advert_storage_fallback"): + self.assertEqual(name in result["components"], mcu == "esp32s3") + def test_browser_terminal_reserves_internal_session_and_psram_aware_scrollback(self): defines = {"ENABLE_USB_INTERFACE": 1, "WIFI_SSID": "", "DISPLAY_CLASS": "SSD1306Display"} base = ram.requirements("ESP32_PLATFORM", {**defines, "WEBCONFIG_DISABLED": 1}, "v4_companion") diff --git a/test/test_flood_advert_psram.py b/test/test_flood_advert_psram.py index 50c34969a..4ab3a5169 100644 --- a/test/test_flood_advert_psram.py +++ b/test/test_flood_advert_psram.py @@ -115,7 +115,6 @@ class FloodAdvertPsramTests(unittest.TestCase): (cls.work / "esp_heap_caps.h").write_text(HEAP) (cls.work / "sdkconfig.h").write_text( "#define CONFIG_IDF_TARGET_ESP32S3 1\n" - "#define CONFIG_SPIRAM_BOOT_INIT 1\n" "#define CONFIG_SPIRAM_USE_MALLOC 1\n") cls.compiler = [os.environ.get("CXX", "g++")] cls.binaries = {} @@ -132,6 +131,16 @@ class FloodAdvertPsramTests(unittest.TestCase): source = self.work / (name + ".cpp") binary = self.work / name code = HARNESS.replace("@BANKS@", "").replace("int main(", "int traceMain(") + code = code.replace( + "#define MAKE_LIMITER(name) mesh::StaticFloodAdvertLimiter<> name\n", + "#define MAKE_LIMITER(name) \\\n" + " const unsigned name##_calls = heap_test::heap().external_calls + heap_test::heap().internal_calls; \\\n" + " mesh::StaticFloodAdvertLimiter<> name; \\\n" + " CHECK(heap_test::heap().external_calls + heap_test::heap().internal_calls == name##_calls); \\\n" + " CHECK(name.begin()); \\\n" + " const unsigned name##_initialized = heap_test::heap().external_calls + heap_test::heap().internal_calls; \\\n" + " CHECK(name.begin()); \\\n" + " CHECK(heap_test::heap().external_calls + heap_test::heap().internal_calls == name##_initialized)\n") # Release builds of the limiter are exercised while harness assertions # stay active. The trace also checks exact full-key prefix collisions. code = code.rstrip()[:-1] + " return 0;\n}\n" diff --git a/test/test_flood_policy_capacity.py b/test/test_flood_policy_capacity.py index ad98aa0c1..5851e28df 100644 --- a/test/test_flood_policy_capacity.py +++ b/test/test_flood_policy_capacity.py @@ -66,6 +66,10 @@ public: uint8_t flood_channel_data_rule_max_hops = FLOOD_CHANNEL_HOPS_ALL; FloodChannelScopeEntry flood_channel_scopes[FLOOD_CHANNEL_SCOPE_SLOTS]{}; char flood_channel_direct_scopes[FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS][32]{}; + auto floodChannelScopes() -> decltype(flood_channel_scopes)& { return flood_channel_scopes; } + auto floodChannelScopes() const -> const decltype(flood_channel_scopes)& { return flood_channel_scopes; } + auto floodChannelDirectScopes() -> decltype(flood_channel_direct_scopes)& { return flood_channel_direct_scopes; } + auto floodChannelDirectScopes() const -> const decltype(flood_channel_direct_scopes)& { return flood_channel_direct_scopes; } uint8_t flood_packet_filter_blacklist_count = 0; uint8_t flood_packet_filter_blacklist[18][3]{}; unsigned seeded = 0; diff --git a/test/test_repeater_scope_psram.py b/test/test_repeater_scope_psram.py index 6a6b1b776..d73261959 100644 --- a/test/test_repeater_scope_psram.py +++ b/test/test_repeater_scope_psram.py @@ -13,7 +13,6 @@ from test_flood_advert_psram import HEAP ROOT = Path(__file__).resolve().parents[1] SDK = ("#define CONFIG_IDF_TARGET_ESP32S3 1\n" - "#define CONFIG_SPIRAM_BOOT_INIT 1\n" "#define CONFIG_SPIRAM_USE_MALLOC 1\n") DEFINES = ["ESP32_PLATFORM=1", "BOARD_HAS_PSRAM=1", "STATION_G3_ESP32=1", "MESH_CLIENT_REPEATER_ONLY=1"] @@ -112,19 +111,19 @@ static std::vector snapshot(const MyMesh& mesh) { const auto first = static_cast(data); bytes.insert(bytes.end(), first, first + size); }; - add(mesh.flood_channel_scopes, sizeof(mesh.flood_channel_scopes)); - add(mesh.flood_channel_direct_scopes, sizeof(mesh.flood_channel_direct_scopes)); - add(mesh.flood_channel_scope_requirements, sizeof(mesh.flood_channel_scope_requirements)); + add(mesh.floodChannelScopes(), sizeof(mesh.floodChannelScopes())); + add(mesh.floodChannelDirectScopes(), sizeof(mesh.floodChannelDirectScopes())); + add(mesh.floodChannelScopeRequirements(), sizeof(mesh.floodChannelScopeRequirements())); return bytes; } static void populate(MyMesh& mesh) { for (unsigned i = 0; i < 32; ++i) - std::snprintf(mesh.flood_channel_direct_scopes[i], 32, "#Scope%u", i); + std::snprintf(mesh.floodChannelDirectScopes()[i], 32, "#Scope%u", i); for (unsigned i = 0; i < 255; ++i) { - auto& row = mesh.flood_channel_scopes[i]; + auto& row = mesh.floodChannelScopes()[i]; row.target_id = i % 32 + 1; row.selector = FloodFilterPolicy::encodeChannelScopeTargetSelector(0, true); - auto& requirement = mesh.flood_channel_scope_requirements[i]; + auto& requirement = mesh.floodChannelScopeRequirements()[i]; requirement.key_len = 32; std::memset(requirement.secret, uint8_t(i + 1), sizeof(requirement.secret)); } @@ -132,10 +131,13 @@ static void populate(MyMesh& mesh) { static void exercise(const char* mode) { MemoryFS disk; { + const unsigned constructor_calls = heap_test::heap().external_calls + heap_test::heap().internal_calls; MyMesh writer(disk); - static_assert(sizeof(writer.flood_channel_scopes) == 255 * 36, "all scope slots"); - static_assert(sizeof(writer.flood_channel_direct_scopes) == 32 * 32, "all direct names"); - static_assert(sizeof(writer.flood_channel_scope_requirements) == 255 * 34, "all requirements"); + CHECK(heap_test::heap().external_calls + heap_test::heap().internal_calls == constructor_calls); + writer.begin(); + static_assert(sizeof(writer.floodChannelScopes()) == 255 * 36, "all scope slots"); + static_assert(sizeof(writer.floodChannelDirectScopes()) == 32 * 32, "all direct names"); + static_assert(sizeof(writer.floodChannelScopeRequirements()) == 255 * 34, "all requirements"); #if MESH_REPEATER_PSRAM_TABLES static_assert(sizeof(MyMesh) < 256, "no inline fallback arrays"); auto& heap = heap_test::heap(); @@ -153,24 +155,26 @@ static void exercise(const char* mode) { (void)mode; #endif CHECK(snapshot(writer) == std::vector(18874, 0)); - const void* scopes = writer.flood_channel_scopes; - const void* direct = writer.flood_channel_direct_scopes; - const void* require = writer.flood_channel_scope_requirements; + const void* scopes = writer.floodChannelScopes(); + const void* direct = writer.floodChannelDirectScopes(); + const void* require = writer.floodChannelScopeRequirements(); populate(writer); const auto initial = snapshot(writer); + writer.begin(); + CHECK(snapshot(writer) == initial); CHECK(writer.saveFloodChannelScopes()); CHECK(writer.saveFloodChannelScopeRequirements()); CHECK(disk.files["/scopes"].size() == 10210); CHECK(disk.files["/require"].size() == 8675); std::vector expected = {'F','C','S','5',255}; - const auto* start = reinterpret_cast(writer.flood_channel_scopes); + const auto* start = reinterpret_cast(writer.floodChannelScopes()); expected.insert(expected.end(), start, start + 9180); expected.push_back(32); - start = reinterpret_cast(writer.flood_channel_direct_scopes); + start = reinterpret_cast(writer.floodChannelDirectScopes()); expected.insert(expected.end(), start, start + 1024); CHECK(disk.files["/scopes"] == expected); expected = {'F','C','R','1',255}; - start = reinterpret_cast(writer.flood_channel_scope_requirements); + start = reinterpret_cast(writer.floodChannelScopeRequirements()); expected.insert(expected.end(), start, start + 8670); CHECK(disk.files["/require"] == expected); const auto saved = disk.files; @@ -191,9 +195,9 @@ static void exercise(const char* mode) { CHECK(snapshot(writer) == initial && disk.files == saved); } disk.fail_rename = disk.short_write = false; - std::memset(writer.flood_channel_scopes, 0xa5, sizeof(writer.flood_channel_scopes)); - std::memset(writer.flood_channel_direct_scopes, 0xa5, sizeof(writer.flood_channel_direct_scopes)); - std::memset(writer.flood_channel_scope_requirements, 0xa5, sizeof(writer.flood_channel_scope_requirements)); + std::memset(writer.floodChannelScopes(), 0xa5, sizeof(writer.floodChannelScopes())); + std::memset(writer.floodChannelDirectScopes(), 0xa5, sizeof(writer.floodChannelDirectScopes())); + std::memset(writer.floodChannelScopeRequirements(), 0xa5, sizeof(writer.floodChannelScopeRequirements())); CHECK(writer.loadFloodChannelScopes()); writer.loadFloodChannelScopeRequirements(); CHECK(snapshot(writer) == initial); @@ -206,17 +210,17 @@ static void exercise(const char* mode) { CHECK(writer.loadFloodChannelScopes()); writer.loadFloodChannelScopeRequirements(); CHECK(snapshot(writer) == initial); - CHECK(scopes == writer.flood_channel_scopes && direct == writer.flood_channel_direct_scopes - && require == writer.flood_channel_scope_requirements); + CHECK(scopes == writer.floodChannelScopes() && direct == writer.floodChannelDirectScopes() + && require == writer.floodChannelScopeRequirements()); // Compare the complete production file payloads between inline and PSRAM runs. for (const char* path : {"/scopes", "/require"}) { const auto& bytes = disk.files[path]; CHECK(std::fwrite(bytes.data(), 1, bytes.size(), stdout) == bytes.size()); } writer.deleteFloodChannelScope(".255", reply); - CHECK(!std::strcmp(reply, "OK") && writer.flood_channel_scopes[254].target_id == 0); + CHECK(!std::strcmp(reply, "OK") && writer.floodChannelScopes()[254].target_id == 0); writer.deleteFloodChannelScopeRequirement(".255", reply); - CHECK(!std::strcmp(reply, "OK") && writer.flood_channel_scope_requirements[254].key_len == 0); + CHECK(!std::strcmp(reply, "OK") && writer.floodChannelScopeRequirements()[254].key_len == 0); writer.deleteFloodChannelScope("all", reply); CHECK(!std::strcmp(reply, "OK")); writer.deleteFloodChannelScopeRequirement("all", reply); @@ -225,6 +229,8 @@ static void exercise(const char* mode) { } CHECK(heap_test::heap().blocks.empty()); MyMesh recreated(disk); + CHECK(heap_test::heap().blocks.empty()); + recreated.begin(); CHECK(snapshot(recreated) == std::vector(18874, 0)); } int main(int argc, char** argv) { @@ -260,10 +266,11 @@ def production_program(): start = header.rfind("#if MESH_REPEATER_PSRAM_TABLES", 0, location) declarations = header[start:header.index("#if MESH_ENABLE_FLOOD_GROUP_MODERATION", location)] constructor = source.index("MyMesh::MyMesh(") - start = source.index("#if MESH_REPEATER_PSRAM_TABLES", constructor) - binders = source[start:source.index("#endif", start) + len("#endif")] - start = source.index(" memset(flood_channel_scopes, 0, sizeof(flood_channel_scopes));", constructor) + start = source.index("#if !MESH_REPEATER_PSRAM_TABLES\n memset(flood_channel_scopes", constructor) resets = source[start:source.index("#if MESH_ENABLE_FLOOD_GROUP_MODERATION", start)] + begin_start = source.index("void MyMesh::begin(FILESYSTEM *fs) {") + begin_start = source.index("{", begin_start) + 1 + begin = source[begin_start:source.index("#if MESH_ENABLE_FLOOD_RULE_ENGINE", begin_start)] methods = [ "bool MyMesh::loadFloodChannelScopes(", "bool MyMesh::saveFloodChannelScopes(", "void MyMesh::loadFloodChannelScopeRequirements(", @@ -277,12 +284,13 @@ def production_program(): "static bool isValidStoredFloodFilterScopeName(", "static bool isExactFloodChannelScopeSelector(", ] - owner = "class MyMesh {\npublic:\nMemoryFS* _fs;\n" + owner = ("class MyMesh {\npublic:\nMemoryFS* _fs;\n" + "struct Limiter { bool begin() { return true; } } flood_advert_limiter;\n") for name in ("FloodChannelScopeEntry", "FloodChannelScopeRequireEntry"): owner += body(header, "struct " + name + " {") + ";\n" - owner += declarations + "\nint end_marker;\n" - owner += "explicit MyMesh(MemoryFS& fs) : _fs(&fs),\n" + binders - owner += "\nend_marker(0) {\n" + resets + "}\n" + owner += declarations + owner += "explicit MyMesh(MemoryFS& fs) : _fs(&fs) {\n" + resets + "}\n" + owner += "void begin() {\n" + begin + "}\n" owner += ("bool loadFloodChannelScopes();\n" "bool saveFloodChannelScopes(bool empty_table=false);\n" "void loadFloodChannelScopeRequirements();\n" @@ -353,11 +361,17 @@ class RepeaterScopePsramTests(unittest.TestCase): cases = [([], 0, True, SDK), (["ESP32_PLATFORM=1", "STATION_G3_ESP32=1"], 0, True, SDK), (["ESP32_PLATFORM=1", "BOARD_HAS_PSRAM=1"], 0, True, SDK), (DEFINES[:-1], 0, True, SDK), + ([define for define in DEFINES if not define.startswith("STATION_G3")], 1, True, SDK), (DEFINES, 1, True, SDK), (DEFINES + ["MESH_REPEATER_PSRAM_TABLES=0"], 0, True, ""), (["MESH_REPEATER_PSRAM_TABLES=1"], 1, False, SDK), + (DEFINES[:-1] + ["MESH_REPEATER_PSRAM_TABLES=1"], 1, False, SDK), + (DEFINES[:-1] + ["MESH_CLIENT_REPEATER_ONLY=0", "MESH_REPEATER_PSRAM_TABLES=1"], + 1, False, SDK), (DEFINES + ["MESH_REPEATER_PSRAM_TABLES=2"], 2, False, SDK), - (DEFINES, 1, False, SDK.replace("CONFIG_IDF_TARGET_ESP32S3 1", "CONFIG_IDF_TARGET_ESP32 1")), - (DEFINES, 1, False, SDK.replace("CONFIG_SPIRAM_BOOT_INIT 1", "CONFIG_SPIRAM_BOOT_INIT 0")), + (DEFINES, 0, True, SDK.replace("CONFIG_IDF_TARGET_ESP32S3 1", "CONFIG_IDF_TARGET_ESP32 1")), + (DEFINES + ["MESH_REPEATER_PSRAM_TABLES=1"], 1, False, + SDK.replace("CONFIG_IDF_TARGET_ESP32S3 1", "CONFIG_IDF_TARGET_ESP32 1")), + (DEFINES, 1, True, SDK + "#define CONFIG_SPIRAM_BOOT_INIT 1\n"), (DEFINES, 1, False, SDK.replace("CONFIG_SPIRAM_USE_MALLOC 1", "CONFIG_SPIRAM_USE_MALLOC 0"))] for index, (defines, expected, success, config) in enumerate(cases): with self.subTest(index=index, defines=defines): diff --git a/test/test_ws_transport_psram.py b/test/test_ws_transport_psram.py index 717025d74..d18f3843d 100644 --- a/test/test_ws_transport_psram.py +++ b/test/test_ws_transport_psram.py @@ -280,7 +280,9 @@ class WsTransportPsramTest(unittest.TestCase): cls.binaries = {} variants = { "g3_psram": (4, ["STATION_G3_ESP32", "BOARD_HAS_PSRAM"]), - "other_psram": (4, ["BOARD_HAS_PSRAM"]), + "generic_psram": (4, ["BOARD_HAS_PSRAM"]), + "classic_psram": (4, ["CONFIG_IDF_TARGET_ESP32", "BOARD_HAS_PSRAM"]), + "s3_psram": (4, ["CONFIG_IDF_TARGET_ESP32S3", "BOARD_HAS_PSRAM"]), "g3_internal": (4, ["STATION_G3_ESP32"]), "other_internal": (4, []), "idf5": (5, ["STATION_G3_ESP32", "BOARD_HAS_PSRAM"]), @@ -306,14 +308,15 @@ class WsTransportPsramTest(unittest.TestCase): capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) - def test_g3_psram_and_guarded_failure_lifecycles(self): - for scenario in ["external_success", "internal_boundary", "reserve_denied", - "largest_denied", "both_fail", "sdk_failure"]: - with self.subTest(scenario=scenario): - self.run_scenario("g3_psram", scenario) + def test_board_agnostic_psram_and_guarded_failure_lifecycles(self): + for variant in ["g3_psram", "generic_psram", "classic_psram", "s3_psram"]: + for scenario in ["external_success", "internal_boundary", "reserve_denied", + "largest_denied", "both_fail", "sdk_failure"]: + with self.subTest(variant=variant, scenario=scenario): + self.run_scenario(variant, scenario) def test_other_target_allocation_defaults_and_failure_cleanup(self): - for variant in ["other_psram", "g3_internal", "other_internal"]: + for variant in ["g3_internal", "other_internal"]: for scenario in ["generic_success", "generic_failure", "sdk_failure"]: with self.subTest(variant=variant, scenario=scenario): self.run_scenario(variant, scenario)