Merge remote-tracking branch 'origin/keymindCascade' into keymindCascade
This commit is contained in:
commit
eab29a12cd
15 files changed
+2067
-166
No files matched your search
+7
-5
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<!-- meshcore-hosted-doc-link:start -->
|
||||
<p class="meshcore-hosted-doc-link"><a href="https://mikecarper.github.io/MeshCore/test-results/esp32_psram_storage_2026-10-27/">View this page on MeshCore Docs</a>.</p>
|
||||
<!-- meshcore-hosted-doc-link:end -->
|
||||
|
||||
# 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.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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.<n>|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.<n>|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");
|
||||
|
||||
@@ -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<FloodChannelStorage> 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<uintptr_t>(entries) % alignof(Entry)) abort();
|
||||
_entries = entries;
|
||||
@@ -211,18 +211,25 @@ public:
|
||||
struct Storage { Entry entries[Capacity]; };
|
||||
RepeaterPsramStorage<Storage> _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
|
||||
@@ -1,7 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
|
||||
#include <sdkconfig.h>
|
||||
#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 <sdkconfig.h>
|
||||
#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 <esp_heap_caps.h>
|
||||
@@ -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<typename T> class RepeaterPsramStorage {
|
||||
static_assert(std::is_trivial<T>::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_;
|
||||
}
|
||||
};
|
||||
|
||||
+72
-11
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<uint8_t> snapshot(const MyMesh& mesh) {
|
||||
const auto first = static_cast<const uint8_t*>(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<uint8_t>(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<uint8_t> expected = {'F','C','S','5',255};
|
||||
const auto* start = reinterpret_cast<const uint8_t*>(writer.flood_channel_scopes);
|
||||
const auto* start = reinterpret_cast<const uint8_t*>(writer.floodChannelScopes());
|
||||
expected.insert(expected.end(), start, start + 9180);
|
||||
expected.push_back(32);
|
||||
start = reinterpret_cast<const uint8_t*>(writer.flood_channel_direct_scopes);
|
||||
start = reinterpret_cast<const uint8_t*>(writer.floodChannelDirectScopes());
|
||||
expected.insert(expected.end(), start, start + 1024);
|
||||
CHECK(disk.files["/scopes"] == expected);
|
||||
expected = {'F','C','R','1',255};
|
||||
start = reinterpret_cast<const uint8_t*>(writer.flood_channel_scope_requirements);
|
||||
start = reinterpret_cast<const uint8_t*>(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<uint8_t>(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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user