Qualify RAK repeater RAM and retire stale channel-busy waits
This commit is contained in:
1 parent
e34e83e6b4
commit
adb11e7b41
18 files changed
+535
-110
No files matched your search
@@ -64,6 +64,16 @@ jobs:
|
||||
bash build.sh build-firmware Heltec_t114_repeater_lora_ota_no_external_sensors
|
||||
--firmware-version v1.18.0.1-halo-keymind-cascade-dev
|
||||
--radio-preset usa-cascade-fixed --profile cascade --require-ota
|
||||
- name: Qualify both RAK4631 Repeater sensor and LoRa OTA profiles
|
||||
run: >-
|
||||
bash build.sh build-firmware RAK_4631_repeater
|
||||
--build-profile auto --firmware-version v1.18.0.1-halo-keymind-cascade-dev
|
||||
--radio-preset usa-cascade-fixed --profile cascade --require-ota
|
||||
- name: Qualify both RAK3401 Room Server sensor and LoRa OTA profiles
|
||||
run: >-
|
||||
bash build.sh build-firmware RAK_3401_room_server
|
||||
--build-profile auto --firmware-version v1.18.0.1-halo-keymind-cascade-dev
|
||||
--radio-preset usa-cascade-fixed --profile cascade --require-ota
|
||||
- name: Compile optional RAK15002 SD archive (no hardware qualification)
|
||||
run: pio run -e RAK_4631_repeater_rak15002_sd_seeder_only
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ MQTT reserve the same 49,152-byte WiFi allowance even without `WIFI_SSID` or
|
||||
`WIFI_OTA_SEEDER`: saved runtime credentials can activate WiFi. Multiple WiFi
|
||||
features count that allowance once; PSRAM-backed TLS does not remove it.
|
||||
ESP32 images with
|
||||
RS-232 reserve another 6,656 bytes even when UART defaults off: 2,560 bytes for
|
||||
RS-232 reserve another 7,168 bytes even when UART defaults off: 3,072 bytes for
|
||||
the source-bounded bridge object and allocation metadata, plus an unchanged
|
||||
4 KiB for the UART driver and buffers. This
|
||||
keeps runtime UART enablement in the budget beside MQTT and ESP-NOW.
|
||||
@@ -68,10 +68,11 @@ The old 75,396-byte linked capacity therefore failed correctly; the check is
|
||||
not bypassed or relaxed to accept it. Full Companion's independent 72 KiB
|
||||
floor remains in place.
|
||||
|
||||
ESP32 SSD1306 uses a source-bounded 2 KiB
|
||||
allowance for its fixed 1,024-byte framebuffer, driver and metadata; other
|
||||
OLED and ordinary non-ESP32 allowances remain unchanged. nRF52 Full with that
|
||||
color framebuffer must have at least 72 KiB available before startup allocations.
|
||||
ESP32 and nRF52 SSD1306 use a source-bounded 2 KiB allowance for the same
|
||||
fixed 1,024-byte framebuffer, embedded driver and allocation metadata. The
|
||||
OLED auto-detection backend reuses that one buffer across SSD1306/SH1106
|
||||
selection and retries. Other display allowances remain unchanged. nRF52 Full
|
||||
with that color framebuffer must have at least 72 KiB available before startup allocations.
|
||||
Headless and OLED devices use their own smaller totals. The JSON lists each
|
||||
component and checks the largest available region against the largest planned
|
||||
single allocation.
|
||||
@@ -162,7 +163,7 @@ physical boot/soak or maximum TLS/WSS/outbox-load qualification. It also does
|
||||
not qualify an independently built compact portable-slot recipe.
|
||||
|
||||
The experimental RAK4631 combined Ethernet/UART2/adaptive-storage config adds
|
||||
an explicit 2,560-byte allowance for the UART bridge and its TX semaphore.
|
||||
an explicit 3,072-byte allowance for the UART bridge and its TX semaphore.
|
||||
Its Ethernet worker reserves 1,024 stack words (4,096 bytes) plus 256 bytes for
|
||||
task metadata and allocator overhead. Its bounded TCP transmit queue allocates
|
||||
512 bytes only while Ethernet is enabled; another 64 bytes cover allocator
|
||||
@@ -197,8 +198,9 @@ for allocation overhead still leaves 836 bytes within the allowance.
|
||||
A compile-time assertion
|
||||
covers framebuffer, complete driver object and allocator overhead within that
|
||||
bound. This corrects a padded allocation estimate; it does not free actual RAM.
|
||||
Other images retain their existing display budgets. These are local
|
||||
test configs, not a qualification of a published or physically tested image.
|
||||
The same source assertion and bound now cover ordinary nRF52 SSD1306 images
|
||||
as well; other display drivers keep their existing budgets. These are linked
|
||||
capacity checks, not qualification of a published or physically tested image.
|
||||
|
||||
## Shared RAM reductions
|
||||
|
||||
@@ -211,6 +213,7 @@ stored settings, OTA block sizes and source-image limits.
|
||||
| Repeater ACL session state | Pure repeater builds on all platforms | 36 bytes per client on 32-bit targets; 1,152 bytes for 32 clients |
|
||||
| Compact recent-repeater records | Repeaters with recent history enabled | 12 to 9 bytes per record; 1,536 bytes for 512 records |
|
||||
| Compact flood-rule flags and aligned counters | Generalized repeater and room rule engines | 200 to 192 bytes per rule; 504 bytes for 63 repeater rules or 248 bytes for 31 room rules |
|
||||
| One tagged flood-rule target name | Generalized repeater engine on all platforms | 332 to 300 bytes per extended rule; 2,016 constructor-time heap bytes for all 63 slots |
|
||||
| Streaming Merkle proof generation | All OTA roles and platforms | Removes full-leaf-table proof scratch; compressed output needs only one logical block |
|
||||
| Source buffers allocated on demand | All OTA roles and platforms | Leaves allocate only the selected image's digest count; unused leaves/output return to the heap |
|
||||
|
||||
@@ -225,6 +228,14 @@ loop; network command handlers marshal their work to that loop. These byte
|
||||
operations do not provide synchronization for hypothetical concurrent users.
|
||||
Flood flags remain internal; FPF files serialize each field explicitly and
|
||||
never expose compiler bitfield layout. Multibyte counters remain aligned.
|
||||
Repeater rules store either the direct scope name or target region name in
|
||||
one 32-byte array because the CLI and stored-policy validator already reject
|
||||
both actions together. A spare flag bit identifies the name. FPF6 retains its
|
||||
single scope field. FPF7/8 retain both fixed-width name fields; the inactive field
|
||||
is emitted as 32 zero bytes. Load reads and validates both fields before
|
||||
selecting the shared name, retaining maximum name lengths and refusing a
|
||||
malformed two-target policy. Room-engine rows and on-disk formats are unchanged.
|
||||
|
||||
Rule deduplication compares configured fields explicitly, so neither live
|
||||
rate-window counters nor struct padding can make different rules share a slot.
|
||||
|
||||
|
||||
@@ -6487,6 +6487,8 @@ bool MyMesh::loadFloodPacketFilters() {
|
||||
for (int i = 0; success && i < count; i++) {
|
||||
auto& entry = i < flood_packet_filter_slots ? loaded[i] : overflow_entry;
|
||||
if (i >= flood_packet_filter_slots) memset(&entry, 0, sizeof(entry));
|
||||
char stored_scope_name[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN] = {};
|
||||
char stored_region_name[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN] = {};
|
||||
uint8_t active = 0;
|
||||
uint8_t suspend_on_temp_radio = 0;
|
||||
uint8_t match_blacklisted_path = 0;
|
||||
@@ -6503,11 +6505,11 @@ bool MyMesh::loadFloodPacketFilters() {
|
||||
success = success && file.read(&suspend_on_temp_radio,
|
||||
sizeof(suspend_on_temp_radio))
|
||||
== sizeof(suspend_on_temp_radio);
|
||||
success = success && file.read((uint8_t*)entry.scope_name,
|
||||
sizeof(entry.scope_name))
|
||||
== sizeof(entry.scope_name);
|
||||
success = success && file.read((uint8_t*)stored_scope_name,
|
||||
sizeof(stored_scope_name))
|
||||
== sizeof(stored_scope_name);
|
||||
success = success
|
||||
&& memchr(entry.scope_name, 0, sizeof(entry.scope_name)) != NULL;
|
||||
&& memchr(stored_scope_name, 0, sizeof(stored_scope_name)) != NULL;
|
||||
success = success && file.read(&match_blacklisted_path,
|
||||
sizeof(match_blacklisted_path))
|
||||
== sizeof(match_blacklisted_path);
|
||||
@@ -6559,9 +6561,9 @@ bool MyMesh::loadFloodPacketFilters() {
|
||||
sizeof(entry.rate_per_minute))
|
||||
== sizeof(entry.rate_per_minute);
|
||||
success = success
|
||||
&& file.read((uint8_t*)entry.target_region_name,
|
||||
sizeof(entry.target_region_name))
|
||||
== sizeof(entry.target_region_name);
|
||||
&& file.read((uint8_t*)stored_region_name,
|
||||
sizeof(stored_region_name))
|
||||
== sizeof(stored_region_name);
|
||||
success = success
|
||||
&& file.read(&entry.priority, sizeof(entry.priority))
|
||||
== sizeof(entry.priority);
|
||||
@@ -6575,7 +6577,7 @@ bool MyMesh::loadFloodPacketFilters() {
|
||||
entry.incoming_scope_kind = scope_requires_region_match
|
||||
? FloodFilterPolicy::RULE_IN_ALLOWED
|
||||
: FloodFilterPolicy::RULE_IN_ANY;
|
||||
drop_on_match = entry.scope_name[0] == 0 ? 1 : 0;
|
||||
drop_on_match = stored_scope_name[0] == 0 ? 1 : 0;
|
||||
}
|
||||
if (success && version_7) {
|
||||
success = FloodFilterPolicy::decodeStoredRuleChannel(
|
||||
@@ -6604,8 +6606,8 @@ bool MyMesh::loadFloodPacketFilters() {
|
||||
success = false;
|
||||
}
|
||||
success = success && FloodFilterPolicy::transportActionsSupported(
|
||||
entry.transport_modes, entry.scope_name[0] != 0
|
||||
|| entry.target_region_name[0] != 0,
|
||||
entry.transport_modes, stored_scope_name[0] != 0
|
||||
|| stored_region_name[0] != 0,
|
||||
entry.retry_on_match, entry.scope_uses_slow_timing);
|
||||
if (!success) break;
|
||||
if (!entry.active) {
|
||||
@@ -6613,12 +6615,12 @@ bool MyMesh::loadFloodPacketFilters() {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool direct_target = entry.scope_name[0] != 0;
|
||||
bool direct_target = stored_scope_name[0] != 0;
|
||||
bool target_name_terminated = memchr(
|
||||
entry.target_region_name, 0,
|
||||
sizeof(entry.target_region_name)) != NULL;
|
||||
stored_region_name, 0,
|
||||
sizeof(stored_region_name)) != NULL;
|
||||
bool region_target = target_name_terminated
|
||||
&& entry.target_region_name[0] != 0;
|
||||
&& stored_region_name[0] != 0;
|
||||
bool input_name_terminated = memchr(
|
||||
entry.incoming_scope_name, 0,
|
||||
sizeof(entry.incoming_scope_name)) != NULL;
|
||||
@@ -6674,6 +6676,12 @@ bool MyMesh::loadFloodPacketFilters() {
|
||||
|| entry.payload_type == PAYLOAD_TYPE_GRP_DATA;
|
||||
}
|
||||
|
||||
// Keep malformed two-target records invalid; never silently select one.
|
||||
if (target_name_terminated && !(direct_target && region_target)) {
|
||||
entry.target_is_region = region_target;
|
||||
memcpy(entry.target_name, region_target ? stored_region_name : stored_scope_name,
|
||||
sizeof(entry.target_name));
|
||||
}
|
||||
bool path_valid = FloodRuleExtensions::validRuleFields(entry);
|
||||
bool action_valid = entry.drop_on_match || direct_target
|
||||
|| region_target || entry.rate_limit_enabled
|
||||
@@ -6683,11 +6691,11 @@ bool MyMesh::loadFloodPacketFilters() {
|
||||
&& entry.min_hops <= entry.max_hops
|
||||
&& entry.max_hops <= FLOOD_PACKET_FILTER_MAX_HOPS
|
||||
&& (!direct_target
|
||||
|| isValidStoredFloodFilterScopeName(entry.scope_name))
|
||||
|| isValidStoredFloodFilterScopeName(stored_scope_name))
|
||||
&& target_name_terminated
|
||||
&& (!region_target
|
||||
|| isValidStoredFloodRuleRegionName(
|
||||
entry.target_region_name))
|
||||
stored_region_name))
|
||||
&& !(direct_target && region_target)
|
||||
&& !(entry.drop_on_match && (direct_target || region_target))
|
||||
&& !(entry.drop_on_match && entry.rate_limit_enabled)
|
||||
@@ -6701,6 +6709,11 @@ bool MyMesh::loadFloodPacketFilters() {
|
||||
&& action_valid)) {
|
||||
success = false;
|
||||
}
|
||||
if (success) {
|
||||
entry.target_is_region = region_target;
|
||||
memcpy(entry.target_name, region_target ? stored_region_name : stored_scope_name,
|
||||
sizeof(entry.target_name));
|
||||
}
|
||||
if (success && i >= flood_packet_filter_slots) active_overflow = true;
|
||||
}
|
||||
|
||||
@@ -6928,7 +6941,7 @@ bool MyMesh::isFloodChannelDataRule(
|
||||
&& valid_hops
|
||||
&& entry.max_hops == FLOOD_PACKET_FILTER_MAX_HOPS
|
||||
&& !entry.suspend_on_temp_radio
|
||||
&& entry.scope_name[0] == 0
|
||||
&& entry.scopeName()[0] == 0
|
||||
&& !entry.match_blacklisted_path
|
||||
&& !entry.scope_uses_slow_timing
|
||||
&& entry.incoming_scope_kind == FloodFilterPolicy::RULE_IN_ANY
|
||||
@@ -6937,7 +6950,7 @@ bool MyMesh::isFloodChannelDataRule(
|
||||
&& entry.channel_name[0] == 0
|
||||
&& entry.path_hash_size == 0
|
||||
&& entry.path_hops == 0
|
||||
&& entry.target_region_name[0] == 0
|
||||
&& entry.regionName()[0] == 0
|
||||
&& entry.drop_on_match
|
||||
&& !entry.rate_limit_enabled
|
||||
&& entry.priority == 0
|
||||
@@ -7218,7 +7231,7 @@ bool MyMesh::migrateLegacyFloodChannelBlocks() {
|
||||
&& entry.min_hops == min_hops
|
||||
&& entry.max_hops == FLOOD_PACKET_FILTER_MAX_HOPS
|
||||
&& !entry.suspend_on_temp_radio
|
||||
&& entry.scope_name[0] == 0
|
||||
&& entry.scopeName()[0] == 0
|
||||
&& !entry.match_blacklisted_path
|
||||
&& !entry.scope_uses_slow_timing
|
||||
&& entry.incoming_scope_kind == FloodFilterPolicy::RULE_IN_ANY
|
||||
@@ -7226,7 +7239,7 @@ bool MyMesh::migrateLegacyFloodChannelBlocks() {
|
||||
&& entry.channel_key_len == row.key_len
|
||||
&& memcmp(entry.channel_secret, row.secret, row.key_len) == 0
|
||||
&& entry.path_hash_size == 0 && entry.path_hops == 0
|
||||
&& entry.target_region_name[0] == 0
|
||||
&& entry.regionName()[0] == 0
|
||||
&& entry.drop_on_match && !entry.rate_limit_enabled
|
||||
&& !entry.stop_on_match;
|
||||
if (duplicate) break;
|
||||
@@ -7323,7 +7336,7 @@ bool MyMesh::saveFloodPacketFilters(bool empty_scope_phase,
|
||||
&& writeExact(&entry.min_hops, sizeof(entry.min_hops))
|
||||
&& writeExact(&entry.max_hops, sizeof(entry.max_hops))
|
||||
&& writeExact(&suspend_on_temp_radio, sizeof(suspend_on_temp_radio))
|
||||
&& writeExact(entry.scope_name, sizeof(entry.scope_name))
|
||||
&& writeExact(entry.scopeName(), sizeof(entry.scopeName()))
|
||||
&& writeExact(&match_blacklisted_path,
|
||||
sizeof(match_blacklisted_path))
|
||||
&& writeExact(&scope_requires_region_match,
|
||||
@@ -7343,8 +7356,8 @@ bool MyMesh::saveFloodPacketFilters(bool empty_scope_phase,
|
||||
&& writeExact(&drop_on_match, sizeof(drop_on_match))
|
||||
&& writeExact(&rate_limit_enabled, sizeof(rate_limit_enabled))
|
||||
&& writeExact(&entry.rate_per_minute, sizeof(entry.rate_per_minute))
|
||||
&& writeExact(entry.target_region_name,
|
||||
sizeof(entry.target_region_name))
|
||||
&& writeExact(entry.regionName(),
|
||||
sizeof(entry.regionName()))
|
||||
&& writeExact(&entry.priority, sizeof(entry.priority))
|
||||
&& writeExact(&stop_on_match, sizeof(stop_on_match));
|
||||
if (success && version_8) {
|
||||
@@ -7460,11 +7473,11 @@ bool MyMesh::loadFloodPacketFilters() {
|
||||
success = success && file.read(&suspend_on_temp_radio,
|
||||
sizeof(suspend_on_temp_radio))
|
||||
== sizeof(suspend_on_temp_radio);
|
||||
success = success && file.read((uint8_t*)loaded[i].scope_name,
|
||||
sizeof(loaded[i].scope_name))
|
||||
== sizeof(loaded[i].scope_name);
|
||||
success = success && file.read((uint8_t*)loaded[i].scopeName(),
|
||||
sizeof(loaded[i].scopeName()))
|
||||
== sizeof(loaded[i].scopeName());
|
||||
success = success
|
||||
&& memchr(loaded[i].scope_name, 0, sizeof(loaded[i].scope_name)) != NULL;
|
||||
&& memchr(loaded[i].scopeName(), 0, sizeof(loaded[i].scopeName())) != NULL;
|
||||
success = success && file.read(&match_blacklisted_path,
|
||||
sizeof(match_blacklisted_path))
|
||||
== sizeof(match_blacklisted_path);
|
||||
@@ -7483,7 +7496,7 @@ bool MyMesh::loadFloodPacketFilters() {
|
||||
|| match_blacklisted_path > 1 || scope_requires_region_match > 1
|
||||
|| scope_uses_slow_timing > 1
|
||||
|| (!loaded[i].active
|
||||
&& (loaded[i].suspend_on_temp_radio || loaded[i].scope_name[0] != 0
|
||||
&& (loaded[i].suspend_on_temp_radio || loaded[i].scopeName()[0] != 0
|
||||
|| loaded[i].match_blacklisted_path
|
||||
|| loaded[i].scope_requires_region_match
|
||||
|| loaded[i].scope_uses_slow_timing)))) {
|
||||
@@ -7494,17 +7507,17 @@ bool MyMesh::loadFloodPacketFilters() {
|
||||
|| loaded[i].payload_type == FLOOD_PACKET_FILTER_ANY_TYPE)
|
||||
&& loaded[i].min_hops <= loaded[i].max_hops
|
||||
&& loaded[i].max_hops <= FLOOD_PACKET_FILTER_MAX_HOPS
|
||||
&& (loaded[i].scope_name[0] == 0
|
||||
|| isValidStoredFloodFilterScopeName(loaded[i].scope_name))
|
||||
&& (loaded[i].scopeName()[0] == 0
|
||||
|| isValidStoredFloodFilterScopeName(loaded[i].scopeName()))
|
||||
&& (!loaded[i].scope_requires_region_match
|
||||
|| loaded[i].scope_name[0] != 0)
|
||||
|| loaded[i].scopeName()[0] != 0)
|
||||
&& (!loaded[i].scope_uses_slow_timing
|
||||
|| loaded[i].scope_name[0] != 0))) {
|
||||
|| loaded[i].scopeName()[0] != 0))) {
|
||||
success = false;
|
||||
}
|
||||
if (success && loaded[i].scope_name[0] != 0) {
|
||||
StrHelper::strzcpy(loaded[i].scope_name, loaded[i].scope_name,
|
||||
sizeof(loaded[i].scope_name));
|
||||
if (success && loaded[i].scopeName()[0] != 0) {
|
||||
StrHelper::strzcpy(loaded[i].scopeName(), loaded[i].scopeName(),
|
||||
sizeof(loaded[i].scopeName()));
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
@@ -7544,8 +7557,8 @@ bool MyMesh::saveFloodPacketFilters(bool empty_scope_phase,
|
||||
success = success && file.write(&entry.min_hops, sizeof(entry.min_hops)) == sizeof(entry.min_hops);
|
||||
success = success && file.write(&entry.max_hops, sizeof(entry.max_hops)) == sizeof(entry.max_hops);
|
||||
success = success && file.write(&suspend_on_temp_radio, sizeof(suspend_on_temp_radio)) == sizeof(suspend_on_temp_radio);
|
||||
success = success && file.write((const uint8_t*)entry.scope_name,
|
||||
sizeof(entry.scope_name)) == sizeof(entry.scope_name);
|
||||
success = success && file.write((const uint8_t*)entry.scopeName(),
|
||||
sizeof(entry.scopeName())) == sizeof(entry.scopeName());
|
||||
success = success && file.write(&match_blacklisted_path,
|
||||
sizeof(match_blacklisted_path))
|
||||
== sizeof(match_blacklisted_path);
|
||||
@@ -7704,14 +7717,14 @@ uint64_t MyMesh::applyFloodPacketFilterStop(uint64_t match_mask) {
|
||||
specificities[i] = FloodFilterPolicy::channelMatcherSpecificity(
|
||||
entry.channel_key_len);
|
||||
bool region_usable = true;
|
||||
if (entry.target_region_name[0] != 0) {
|
||||
if (entry.regionName()[0] != 0) {
|
||||
TransportKey scope;
|
||||
const char* canonical_name = NULL;
|
||||
region_usable = resolveFloodPacketFilterTargetRegion(
|
||||
entry.target_region_name, scope, canonical_name);
|
||||
entry.regionName(), scope, canonical_name);
|
||||
}
|
||||
stop_flags[i] = FloodFilterPolicy::stopActionApplies(
|
||||
entry.stop_on_match, entry.target_region_name[0] != 0,
|
||||
entry.stop_on_match, entry.regionName()[0] != 0,
|
||||
region_usable) ? 1 : 0;
|
||||
}
|
||||
return FloodFilterPolicy::truncateRulesAtStop(
|
||||
@@ -7792,18 +7805,18 @@ bool MyMesh::applyFloodPacketFilterScope(mesh::Packet* packet,
|
||||
if (i < 0) break;
|
||||
visited |= (uint64_t)1U << i;
|
||||
const auto& entry = flood_packet_filters[i];
|
||||
if (entry.scope_name[0] == 0
|
||||
&& entry.target_region_name[0] == 0) {
|
||||
if (entry.scopeName()[0] == 0
|
||||
&& entry.regionName()[0] == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TransportKey scope;
|
||||
const char* target_name = entry.scope_name;
|
||||
if (entry.scope_name[0] != 0) {
|
||||
deriveFloodFilterScopeKey(entry.scope_name, scope);
|
||||
const char* target_name = entry.scopeName();
|
||||
if (entry.scopeName()[0] != 0) {
|
||||
deriveFloodFilterScopeKey(entry.scopeName(), scope);
|
||||
} else {
|
||||
if (!resolveFloodPacketFilterTargetRegion(
|
||||
entry.target_region_name, scope, target_name)) {
|
||||
entry.regionName(), scope, target_name)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -7927,10 +7940,10 @@ bool MyMesh::applyFloodPacketFilterScope(mesh::Packet* packet,
|
||||
for (int i = 0; i < flood_packet_filter_slots; i++) {
|
||||
const auto& entry = flood_packet_filters[i];
|
||||
if ((match_mask & ((uint64_t)1U << i)) == 0
|
||||
|| entry.scope_name[0] == 0) continue;
|
||||
|| entry.scopeName()[0] == 0) continue;
|
||||
|
||||
TransportKey scope;
|
||||
deriveFloodFilterScopeKey(entry.scope_name, scope);
|
||||
deriveFloodFilterScopeKey(entry.scopeName(), scope);
|
||||
bool scope_changed = FloodFilterPolicy::setTransportScope(
|
||||
packet, scope.calcTransportCode(packet));
|
||||
scope_set = true;
|
||||
@@ -7939,7 +7952,7 @@ bool MyMesh::applyFloodPacketFilterScope(mesh::Packet* packet,
|
||||
if (scope_changed && log_change) {
|
||||
MESH_DEBUG_PRINTLN("flood.filter set scope slot=%d type=%d hops=%d scope=%s tx=%s",
|
||||
i + 1, packet->getPayloadType(),
|
||||
packet->getPathHashCount(), entry.scope_name,
|
||||
packet->getPathHashCount(), entry.scopeName(),
|
||||
entry.scope_uses_slow_timing ? "slow" : "fast");
|
||||
}
|
||||
return scope_changed;
|
||||
@@ -7955,7 +7968,7 @@ bool MyMesh::shouldBlockFloodPacketForward(const mesh::Packet* packet,
|
||||
for (int i = 0; i < flood_packet_filter_slots; i++) {
|
||||
const auto& entry = flood_packet_filters[i];
|
||||
if ((match_mask & ((uint64_t)1U << i)) != 0
|
||||
&& entry.scope_name[0] == 0) {
|
||||
&& entry.scopeName()[0] == 0) {
|
||||
MESH_DEBUG_PRINTLN("allowPacketForward: flood.filter matched slot=%d type=%d hops=%d range=%d-%d",
|
||||
i + 1, type, hops, entry.min_hops,
|
||||
entry.max_hops);
|
||||
@@ -8043,13 +8056,13 @@ void MyMesh::formatFloodPacketFilterDetail(int index, char* reply, size_t reply_
|
||||
action[0] = 0;
|
||||
if (entry.drop_on_match) {
|
||||
strcpy(action, "drop");
|
||||
} else if (entry.scope_name[0] != 0) {
|
||||
snprintf(action, sizeof(action), "scope=%s", entry.scope_name);
|
||||
} else if (entry.target_region_name[0] != 0) {
|
||||
} else if (entry.scopeName()[0] != 0) {
|
||||
snprintf(action, sizeof(action), "scope=%s", entry.scopeName());
|
||||
} else if (entry.regionName()[0] != 0) {
|
||||
snprintf(action, sizeof(action), "region=%s%s",
|
||||
entry.target_region_name,
|
||||
entry.regionName(),
|
||||
floodRuleRegionNamePresent(
|
||||
region_map, entry.target_region_name) ? "" : "?");
|
||||
region_map, entry.regionName()) ? "" : "?");
|
||||
}
|
||||
rate[0] = 0;
|
||||
if (entry.rate_limit_enabled) {
|
||||
@@ -8133,11 +8146,11 @@ void MyMesh::formatFloodPacketFilters(const char* args, char* reply, bool compac
|
||||
formatFloodFilterHopSpec(hops, sizeof(hops), entry.min_hops, entry.max_hops);
|
||||
if (entry.drop_on_match) {
|
||||
strcpy(target, "!drop");
|
||||
} else if (entry.scope_name[0] != 0) {
|
||||
snprintf(target, sizeof(target), ">%s", entry.scope_name);
|
||||
} else if (entry.target_region_name[0] != 0) {
|
||||
} else if (entry.scopeName()[0] != 0) {
|
||||
snprintf(target, sizeof(target), ">%s", entry.scopeName());
|
||||
} else if (entry.regionName()[0] != 0) {
|
||||
snprintf(target, sizeof(target), ">r:%s",
|
||||
entry.target_region_name);
|
||||
entry.regionName());
|
||||
} else {
|
||||
target[0] = 0;
|
||||
}
|
||||
@@ -8681,8 +8694,10 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
|
||||
candidate.path_hash_size = path_hash_size;
|
||||
candidate.path_hops = path_hops;
|
||||
memcpy(candidate.path, path, sizeof(candidate.path));
|
||||
StrHelper::strzcpy(candidate.target_region_name, target_region_name,
|
||||
sizeof(candidate.target_region_name));
|
||||
candidate.target_is_region = target_region_name[0] != 0;
|
||||
StrHelper::strzcpy(candidate.target_name,
|
||||
candidate.target_is_region ? target_region_name : scope_name,
|
||||
sizeof(candidate.target_name));
|
||||
candidate.drop_on_match = drop_on_match;
|
||||
candidate.rate_limit_enabled = rate_limit_enabled;
|
||||
candidate.rate_per_minute = rate_per_minute;
|
||||
@@ -8691,8 +8706,6 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
|
||||
candidate.retry_on_match = retry_on_match;
|
||||
candidate.transport_modes = transport_modes;
|
||||
candidate.extra = extra;
|
||||
StrHelper::strzcpy(candidate.scope_name, scope_name,
|
||||
sizeof(candidate.scope_name));
|
||||
if (!FloodRuleExtensions::validRuleFields(candidate)) {
|
||||
strcpy(reply, "Err - content needs a keyed group-text channel; scope/retry needs flood route");
|
||||
return;
|
||||
@@ -8758,8 +8771,8 @@ void MyMesh::formatFloodPacketFilterDetail(int index, char* reply, size_t reply_
|
||||
char hops[12];
|
||||
formatFloodFilterHopSpec(hops, sizeof(hops), entry.min_hops, entry.max_hops);
|
||||
const char* suspension = entry.suspend_on_temp_radio ? " suspend=tempradio" : "";
|
||||
const char* scope_prefix = entry.scope_name[0] ? " scope=" : "";
|
||||
const char* scope_name = entry.scope_name[0] ? entry.scope_name : "";
|
||||
const char* scope_prefix = entry.scopeName()[0] ? " scope=" : "";
|
||||
const char* scope_name = entry.scopeName()[0] ? entry.scopeName() : "";
|
||||
const char* path_match = entry.match_blacklisted_path ? " path=blacklist" : "";
|
||||
const char* region_requirement =
|
||||
entry.scope_requires_region_match ? " require=region" : "";
|
||||
@@ -8805,8 +8818,8 @@ void MyMesh::formatFloodPacketFilters(const char* args, char* reply, bool compac
|
||||
snprintf(item, sizeof(item), " %d=%s@%s%s%s%s%s%s%s",
|
||||
i + 1, floodFilterPayloadTypeName(entry.payload_type), hops,
|
||||
entry.match_blacklisted_path ? "?blacklist" : "",
|
||||
entry.scope_name[0] ? ">" : "",
|
||||
entry.scope_name,
|
||||
entry.scopeName()[0] ? ">" : "",
|
||||
entry.scopeName(),
|
||||
entry.scope_requires_region_match ? "!region" : "",
|
||||
entry.scope_uses_slow_timing ? "~slow" : "",
|
||||
entry.suspend_on_temp_radio ? "~tempradio" : "");
|
||||
@@ -8967,7 +8980,7 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
|
||||
&& entry.suspend_on_temp_radio == suspend_on_temp_radio
|
||||
&& entry.match_blacklisted_path == match_blacklisted_path
|
||||
&& entry.scope_requires_region_match == scope_requires_region_match
|
||||
&& strcmp(entry.scope_name, scope_name) == 0) {
|
||||
&& strcmp(entry.scopeName(), scope_name) == 0) {
|
||||
slot = i;
|
||||
break;
|
||||
}
|
||||
@@ -8996,7 +9009,7 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
|
||||
entry.match_blacklisted_path = match_blacklisted_path;
|
||||
entry.scope_requires_region_match = scope_requires_region_match;
|
||||
entry.scope_uses_slow_timing = scope_uses_slow_timing;
|
||||
StrHelper::strzcpy(entry.scope_name, scope_name, sizeof(entry.scope_name));
|
||||
StrHelper::strzcpy(entry.scopeName(), scope_name, sizeof(entry.scopeName()));
|
||||
if (!saveFloodPacketFilters()) {
|
||||
entry = previous;
|
||||
strcpy(reply, "Err - unable to save flood filter");
|
||||
|
||||
@@ -421,7 +421,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
};
|
||||
struct FloodPacketFilterEntry {
|
||||
#if MESH_ENABLE_FLOOD_RULE_ENGINE
|
||||
// Keep multibyte counters naturally aligned. These seven independent
|
||||
// Keep multibyte counters naturally aligned. These eight independent
|
||||
// flags share a byte; their bit order is never persisted (FPF7 serializes
|
||||
// each field explicitly). active/retry remain addressable for the stored
|
||||
// rule decoders. Rule mutations run on the mesh loop.
|
||||
@@ -435,12 +435,22 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
bool rate_limit_enabled : 1;
|
||||
bool stop_on_match : 1;
|
||||
bool rate_window_active : 1;
|
||||
bool target_is_region : 1;
|
||||
bool active;
|
||||
bool retry_on_match;
|
||||
uint8_t payload_type;
|
||||
uint8_t min_hops;
|
||||
uint8_t max_hops;
|
||||
char scope_name[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN];
|
||||
// scope= and region= cannot coexist; retain their full name capacity.
|
||||
char target_name[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN];
|
||||
const char (&scopeName() const)[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN] {
|
||||
static const char empty[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN] = {};
|
||||
return target_is_region ? empty : target_name;
|
||||
}
|
||||
const char (®ionName() const)[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN] {
|
||||
static const char empty[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN] = {};
|
||||
return target_is_region ? target_name : empty;
|
||||
}
|
||||
uint8_t incoming_scope_kind;
|
||||
char incoming_scope_name[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN];
|
||||
uint8_t channel_key_len;
|
||||
@@ -450,7 +460,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
uint8_t path_hash_size;
|
||||
uint8_t path_hops;
|
||||
uint8_t path[FLOOD_PACKET_FILTER_PATH_PREFIX_BYTES_MAX];
|
||||
char target_region_name[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN];
|
||||
uint8_t priority;
|
||||
uint8_t transport_modes;
|
||||
FloodRuleExtensions::Fields extra;
|
||||
@@ -461,6 +470,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
uint8_t max_hops;
|
||||
bool suspend_on_temp_radio;
|
||||
char scope_name[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN];
|
||||
char (&scopeName())[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN] { return scope_name; }
|
||||
const char (&scopeName() const)[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN] { return scope_name; }
|
||||
bool match_blacklisted_path;
|
||||
bool scope_requires_region_match;
|
||||
bool scope_uses_slow_timing;
|
||||
@@ -507,7 +518,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
};
|
||||
mutable FloodRetryBridgeState flood_retry_bridge_states[TOTAL_FLOOD_RETRY_SLOTS];
|
||||
FloodRetryBridgeReachability flood_retry_bridge_reachability[FLOOD_RETRY_BRIDGE_BUCKETS + 1];
|
||||
static_assert(sizeof(FloodPacketFilterEntry) <= (MESH_ENABLE_FLOOD_RULE_ENGINE ? 336 : 40),
|
||||
static_assert(sizeof(FloodPacketFilterEntry) <= (MESH_ENABLE_FLOOD_RULE_ENGINE ? 304 : 40),
|
||||
"Update the flood-table runtime RAM budget in check_firmware_ram.py");
|
||||
// The rule table is the single largest member of this object. It is heap
|
||||
// allocated in the constructor (before setup(), while the heap is still
|
||||
|
||||
@@ -197,7 +197,7 @@ def requirements(platform, defines, target):
|
||||
if combined_rak_ethernet and (platform != "NRF52_PLATFORM"
|
||||
or not {"RAK_4631", "ETHERNET_ENABLED", "OTA_RAK_AUTO_STORE"}.issubset(defines)):
|
||||
raise ValueError("combined RAK Ethernet budget requires RAK4631 adaptive nRF52 storage")
|
||||
if (combined_rak_ethernet or platform == "ESP32_PLATFORM") and display == "SSD1306Display":
|
||||
if platform in {"ESP32_PLATFORM", "NRF52_PLATFORM"} and display == "SSD1306Display":
|
||||
# The fixed 128x64 driver allocates one 1024-byte framebuffer and
|
||||
# reuses it after power cycling. Its linked source assertion binds
|
||||
# framebuffer + complete driver object + allocator overhead to 2 KiB.
|
||||
@@ -308,7 +308,7 @@ def requirements(platform, defines, target):
|
||||
if repeater:
|
||||
engine = integer(defines, "MESH_ENABLE_FLOOD_RULE_ENGINE", int(platform != "STM32_PLATFORM"))
|
||||
slots = integer(defines, "FLOOD_PACKET_FILTER_SLOTS", 63 if engine else 16)
|
||||
parts["flood_filter_table"] = slots * (336 if engine else 40) + 16
|
||||
parts["flood_filter_table"] = slots * (304 if engine else 40) + 16
|
||||
if "room_server" in target.lower() and integer(
|
||||
defines, "MESH_ENABLE_ROOM_FLOOD_RULE_ENGINE", 0):
|
||||
# FloodRuleEngine keeps all 31 persisted rules in one startup heap
|
||||
|
||||
+32
-4
@@ -96,6 +96,8 @@ void Dispatcher::begin() {
|
||||
n_recv_flood = n_recv_direct = 0;
|
||||
last_meshcore_recv_millis = 0;
|
||||
_err_flags = 0;
|
||||
cad_busy_start = 0;
|
||||
profile_cad_busy[0] = profile_cad_busy[1] = 0;
|
||||
outbound_radio_retry_at = 0;
|
||||
outbound_radio_prepare_deadline = 0;
|
||||
outbound_radio_retry_pending = false;
|
||||
@@ -329,6 +331,7 @@ bool Dispatcher::queueDirectOtaRadioFrame(const uint8_t* message, uint16_t lengt
|
||||
const uint16_t encoded = ota::ota_direct_wire_encode(
|
||||
foreign_radio_bytes, sizeof(foreign_radio_bytes), message, length);
|
||||
if (!encoded) return false;
|
||||
retireUnusedCADWait(profile);
|
||||
foreign_radio_length = encoded;
|
||||
foreign_radio_profile = profile;
|
||||
foreign_radio_protocol = RadioProfileProtocol::MeshCore;
|
||||
@@ -353,6 +356,7 @@ bool Dispatcher::queueForeignRadioFrame(const uint8_t* bytes, size_t length,
|
||||
|| !profiles || profile != 1 || !profiles->enabled() || !profiles->canTransmit(profile)
|
||||
|| profiles->params(profile).protocol != protocol
|
||||
|| generation != profiles->generation[profile]) return false;
|
||||
retireUnusedCADWait(profile);
|
||||
memcpy(foreign_radio_bytes, bytes, length);
|
||||
foreign_radio_length = (uint16_t)length;
|
||||
foreign_radio_profile = profile;
|
||||
@@ -1382,6 +1386,30 @@ void Dispatcher::releasePacket(Packet* packet) {
|
||||
_mgr->free(packet);
|
||||
}
|
||||
|
||||
void Dispatcher::retireUnusedCADWait(uint8_t profile) {
|
||||
if (profile_cad_busy[profile] && !(outbound && outbound->radio_profile == profile)
|
||||
#if MESH_ENABLE_FOREIGN_RADIO
|
||||
&& !(hasForeignRadioFrame() && foreign_radio_profile == profile)
|
||||
#endif
|
||||
) {
|
||||
// Cancellation/echo suppression can retire the last packet for a profile
|
||||
// without observing a free channel. Its old age must not force a later,
|
||||
// unrelated packet to transmit immediately with a false timeout. Check
|
||||
// only on enqueue with an aged wait, never during the idle/sleep loop.
|
||||
bool waiting = false;
|
||||
for (int i = 0, count = _mgr->getOutboundTotal(); i < count; ++i) {
|
||||
const auto* queued = _mgr->getOutboundByIdx(i);
|
||||
if (queued && queued->radio_profile == profile) { waiting = true; break; }
|
||||
}
|
||||
if (!waiting) profile_cad_busy[profile] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool Dispatcher::queueTransmitCandidate(Packet* packet, uint8_t priority, uint32_t scheduled_for) {
|
||||
retireUnusedCADWait(packet->radio_profile);
|
||||
return _mgr->queueOutbound(packet, priority, scheduled_for);
|
||||
}
|
||||
|
||||
bool Dispatcher::queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) {
|
||||
if (packet->violatesRoutePolicy()) {
|
||||
return false;
|
||||
@@ -1399,13 +1427,13 @@ bool Dispatcher::queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t
|
||||
if (profiles == nullptr) {
|
||||
if (packet->tx_radio != RADIO_TX_AUTO
|
||||
&& !(explicitRadioTxMask(packet->tx_radio, false) & 1)) return false;
|
||||
return _mgr->queueOutbound(packet, priority, futureMillis(delay_millis));
|
||||
return queueTransmitCandidate(packet, priority, futureMillis(delay_millis));
|
||||
}
|
||||
if (packet->radio_bound) {
|
||||
if (!isPacketRadioCurrent(packet)) return false;
|
||||
if (profiles && packet->radio_profile != packet->radio_origin
|
||||
&& !allowRadioProfileCross(packet)) return false;
|
||||
return _mgr->queueOutbound(packet, priority, futureMillis(delay_millis));
|
||||
return queueTransmitCandidate(packet, priority, futureMillis(delay_millis));
|
||||
}
|
||||
if (!packet->radio_local && packet->radio_generation
|
||||
&& packet->radio_generation != profiles->generation[packet->radio_profile]) return false;
|
||||
@@ -1436,7 +1464,7 @@ bool Dispatcher::queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t
|
||||
}
|
||||
packet->radio_generation = profiles->generation[packet->radio_profile];
|
||||
packet->radio_bound = true;
|
||||
if (!_mgr->queueOutbound(packet, priority, futureMillis(delay_millis))) return false;
|
||||
if (!queueTransmitCandidate(packet, priority, futureMillis(delay_millis))) return false;
|
||||
if (packet->radio_profile != origin) onRadioProfileCopyQueued(packet, nullptr, priority);
|
||||
const uint8_t other = packet->radio_profile ^ 1;
|
||||
if (mask & (1U << other)) {
|
||||
@@ -1445,7 +1473,7 @@ bool Dispatcher::queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t
|
||||
*copy = *packet;
|
||||
copy->radio_profile = other;
|
||||
copy->radio_generation = profiles->generation[other];
|
||||
if (_mgr->queueOutbound(copy, priority, futureMillis(delay_millis))) {
|
||||
if (queueTransmitCandidate(copy, priority, futureMillis(delay_millis))) {
|
||||
onRadioProfileCopyQueued(copy, packet, priority);
|
||||
onTracePacketQueuedForSend(copy);
|
||||
} else {
|
||||
|
||||
@@ -723,6 +723,8 @@ public:
|
||||
unsigned long futureMillis(int millis_from_now) const;
|
||||
|
||||
private:
|
||||
void retireUnusedCADWait(uint8_t profile);
|
||||
bool queueTransmitCandidate(Packet* packet, uint8_t priority, uint32_t scheduled_for);
|
||||
// Keep the frequently accessed core fields at compact ARM load offsets.
|
||||
AGCResetScheduler agc_reset;
|
||||
uint32_t receive_context_generation = 0;
|
||||
|
||||
@@ -68,7 +68,7 @@ bool sameRuleConfiguration(const Entry& left, const Entry& right) {
|
||||
&& left.min_hops == right.min_hops
|
||||
&& left.max_hops == right.max_hops
|
||||
&& left.suspend_on_temp_radio == right.suspend_on_temp_radio
|
||||
&& memcmp(left.scope_name, right.scope_name, sizeof(left.scope_name)) == 0
|
||||
&& memcmp(FloodRuleTargets::scopeName(left), FloodRuleTargets::scopeName(right), sizeof(FloodRuleTargets::scopeName(left))) == 0
|
||||
&& left.match_blacklisted_path == right.match_blacklisted_path
|
||||
&& left.scope_uses_slow_timing == right.scope_uses_slow_timing
|
||||
&& left.incoming_scope_kind == right.incoming_scope_kind
|
||||
@@ -82,8 +82,8 @@ bool sameRuleConfiguration(const Entry& left, const Entry& right) {
|
||||
&& left.path_hash_size == right.path_hash_size
|
||||
&& left.path_hops == right.path_hops
|
||||
&& memcmp(left.path, right.path, sizeof(left.path)) == 0
|
||||
&& memcmp(left.target_region_name, right.target_region_name,
|
||||
sizeof(left.target_region_name)) == 0
|
||||
&& memcmp(FloodRuleTargets::regionName(left), FloodRuleTargets::regionName(right),
|
||||
sizeof(FloodRuleTargets::regionName(left))) == 0
|
||||
&& left.drop_on_match == right.drop_on_match
|
||||
&& left.rate_limit_enabled == right.rate_limit_enabled
|
||||
&& left.rate_per_minute == right.rate_per_minute
|
||||
|
||||
@@ -117,8 +117,8 @@ bool formatCompact(char* out, size_t capacity, unsigned slot, const Entry& entry
|
||||
default: break;
|
||||
}
|
||||
if (entry.drop_on_match) text.append(" d");
|
||||
else if (entry.scope_name[0]) text.append(" s=%s", entry.scope_name);
|
||||
else if (entry.target_region_name[0]) text.append(" r=%s", entry.target_region_name);
|
||||
else if (FloodRuleTargets::scopeName(entry)[0]) text.append(" s=%s", FloodRuleTargets::scopeName(entry));
|
||||
else if (FloodRuleTargets::regionName(entry)[0]) text.append(" r=%s", FloodRuleTargets::regionName(entry));
|
||||
if (entry.rate_limit_enabled) text.append(" q=%u", (unsigned)entry.rate_per_minute);
|
||||
if (entry.priority) text.append(" pri=%u", (unsigned)entry.priority);
|
||||
if (entry.stop_on_match) text.append(" s");
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <Packet.h>
|
||||
#include "FloodRuleTargets.h"
|
||||
#include "TxtDataHelpers.h"
|
||||
#include "FloodTextPattern.h"
|
||||
#include "FloodGroupPlainText.h"
|
||||
@@ -80,7 +81,7 @@ inline bool validRuleFields(const Entry& entry) {
|
||||
const bool content_valid = !contentRequested(entry.extra)
|
||||
|| ((entry.channel_key_len == 16 || entry.channel_key_len == 32)
|
||||
&& (entry.payload_type == 0xFF || entry.payload_type == PAYLOAD_TYPE_GRP_TXT));
|
||||
const bool flood_actions = entry.scope_name[0] || entry.target_region_name[0]
|
||||
const bool flood_actions = FloodRuleTargets::scopeName(entry)[0] || FloodRuleTargets::regionName(entry)[0]
|
||||
|| entry.retry_on_match || entry.scope_uses_slow_timing;
|
||||
return path_valid && content_valid && (!flood_actions
|
||||
|| entry.extra.route == LegacyRoute || entry.extra.route == FloodRoute);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
// Rule targets are mutually exclusive. The repeater stores one tagged name;
|
||||
// the room engine and portable policy users retain their existing fields.
|
||||
// Preserve array references so explicit FPF7 serialization keeps both fixed
|
||||
// width fields, including the zero-filled inactive name.
|
||||
namespace FloodRuleTargets {
|
||||
template<typename Entry>
|
||||
auto scopeNameImpl(const Entry& entry, int) -> decltype(entry.scopeName()) {
|
||||
return entry.scopeName();
|
||||
}
|
||||
template<typename Entry>
|
||||
auto scopeNameImpl(const Entry& entry, long) -> decltype((entry.scope_name)) {
|
||||
return entry.scope_name;
|
||||
}
|
||||
template<typename Entry>
|
||||
auto regionNameImpl(const Entry& entry, int) -> decltype(entry.regionName()) {
|
||||
return entry.regionName();
|
||||
}
|
||||
template<typename Entry>
|
||||
auto regionNameImpl(const Entry& entry, long) -> decltype((entry.target_region_name)) {
|
||||
return entry.target_region_name;
|
||||
}
|
||||
template<typename Entry>
|
||||
auto scopeName(const Entry& entry) -> decltype(scopeNameImpl(entry, 0)) {
|
||||
return scopeNameImpl(entry, 0);
|
||||
}
|
||||
template<typename Entry>
|
||||
auto regionName(const Entry& entry) -> decltype(regionNameImpl(entry, 0)) {
|
||||
return regionNameImpl(entry, 0);
|
||||
}
|
||||
} // namespace FloodRuleTargets
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "OLEDControllerSettings.h"
|
||||
#endif
|
||||
|
||||
#if defined(ESP32_PLATFORM) || (defined(NRF52_PLATFORM) && defined(RAK4631_COMBINED_ETHERNET))
|
||||
#if defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)
|
||||
// Adafruit_SSD1306::begin allocates WIDTH * ((HEIGHT + 7) / 8) once and
|
||||
// reuses that framebuffer on later begin calls. The driver itself is embedded
|
||||
// in the linked global display, not allocated on the heap. Even including
|
||||
|
||||
+50
-10
@@ -438,7 +438,7 @@ int main() {
|
||||
"ENABLE_OTA": 1, "OTA_HEAP_CONTEXT": 1,
|
||||
}, "GEPRC_Linkflow_900_repeater")
|
||||
self.assertEqual(policy["components"]["client_table"], 32 * 320 + 16)
|
||||
self.assertEqual(policy["components"]["flood_filter_table"], 63 * 336 + 16)
|
||||
self.assertEqual(policy["components"]["flood_filter_table"], 63 * 304 + 16)
|
||||
self.assertEqual(policy["components"]["ota_context"], 16384 + 16)
|
||||
self.assertGreaterEqual(policy["required_contiguous_bytes"], 16384 + 16)
|
||||
reduced = ram.requirements("STM32_PLATFORM", {
|
||||
@@ -681,9 +681,9 @@ int main() {
|
||||
self.assertNotIn("ota_encoder_workspace", components)
|
||||
self.assertNotIn("ota_self_source_scratch", components)
|
||||
self.assertEqual(components["display_pixels_and_driver"], 2048)
|
||||
self.assertEqual(combined["required_heap_bytes"] - legacy["required_heap_bytes"], 3072 - 2048)
|
||||
self.assertEqual(combined["required_heap_bytes"] - legacy["required_heap_bytes"], 3072)
|
||||
self.assertEqual(components["loop_and_callback_stacks"], 8192 + 3072)
|
||||
self.assertEqual(components["flood_filter_table"], 63 * 336 + 16)
|
||||
self.assertEqual(components["flood_filter_table"], 63 * 304 + 16)
|
||||
# An alternate port selects the same bridge instead of allocating two.
|
||||
alternate = ram.requirements("NRF52_PLATFORM", {
|
||||
**defines, "RAK4631_COMBINED_ETHERNET": 1,
|
||||
@@ -794,7 +794,7 @@ int main() {
|
||||
alias = ram.requirements("ESP32_PLATFORM", {"MESH_CLIENT_REPEATER_ONLY": 1},
|
||||
"custom_companion_alias")
|
||||
self.assertEqual(alias["components"]["client_table"], 32 * 284 + 16)
|
||||
self.assertEqual(alias["components"]["flood_filter_table"], 63 * 336 + 16)
|
||||
self.assertEqual(alias["components"]["flood_filter_table"], 63 * 304 + 16)
|
||||
self.assertEqual(alias["components"]["neighbor_history"], 2048 * 9 + 16)
|
||||
self.assertEqual(alias["components"]["radio_packet_pool"], 10240)
|
||||
|
||||
@@ -838,7 +838,46 @@ int main() {
|
||||
self.assertEqual(legacy["required_contiguous_bytes"],
|
||||
selected["required_contiguous_bytes"])
|
||||
|
||||
def test_combined_rak_oled_bound_does_not_change_other_display_budgets(self):
|
||||
def test_rak_repeater_compact_rule_and_oled_budgets_keep_every_other_reserve(self):
|
||||
flags = {
|
||||
"ENABLE_OTA": 1, "OTA_RAK_AUTO_STORE": 1, "OTA_QSPI_STORE": 1,
|
||||
"OTA_RAK15002_SD_STORE": 1, "OTA_FLASH_STORE": 1,
|
||||
"MESHCORE_OTA_DEVICE_DEFLATE": 1, "MESH_CLIENT_REPEATER_ONLY": 1,
|
||||
"DISPLAY_CLASS": "SSD1306Display",
|
||||
}
|
||||
selected = ram.requirements("NRF52_PLATFORM", flags, "RAK_4631_repeater")
|
||||
self.assertEqual(selected["components"], {
|
||||
"loop_and_callback_stacks": 11264,
|
||||
"core_usb_filesystems_sensors": 12288,
|
||||
"sd_fat_volume_and_files": 2048,
|
||||
"radio_packet_pool": 10240,
|
||||
"client_table": 9104,
|
||||
"flood_filter_table": 63 * 304 + 16,
|
||||
"ota_generic_source_buffers": 3616,
|
||||
"ota_encoder_workspace": 1040,
|
||||
"ota_self_source_scratch": 4128,
|
||||
"display_pixels_and_driver": 2048,
|
||||
"screen_objects_and_history": 2048,
|
||||
"allocation_and_transient_margin": 4096,
|
||||
})
|
||||
self.assertEqual(selected["required_heap_bytes"], 81088)
|
||||
self.assertEqual(selected["required_contiguous_bytes"], 19168)
|
||||
# The fresh Full baseline's 81,668 bytes passed only after the real
|
||||
# 32-byte per-row reduction and same-driver OLED accounting proof.
|
||||
# Neither a one-byte deficit nor the old failed capacity can bypass
|
||||
# the existing real-ELF gate.
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
path = Path(temporary) / "firmware.elf"
|
||||
for available, expected in ((81087, False), (81088, True), (81668, True)):
|
||||
with self.subTest(available=available):
|
||||
elf32(path, {"__HeapBase": (0x20010000, 0),
|
||||
"__HeapLimit": (0x20010000 + available, 0)})
|
||||
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
||||
passed = ram.check_firmware(path, "NRF52_PLATFORM", "nrf52840",
|
||||
flags, "RAK_4631_repeater") == 0
|
||||
self.assertEqual(passed, expected)
|
||||
|
||||
def test_nrf52_oled_bound_matches_same_source_guard_without_changing_other_displays(self):
|
||||
defines = {
|
||||
"RAK_4631": 1, "ETHERNET_ENABLED": 1, "OTA_RAK_AUTO_STORE": 1,
|
||||
"RAK4631_COMBINED_ETHERNET": 1,
|
||||
@@ -853,7 +892,7 @@ int main() {
|
||||
ordinary = ram.requirements("NRF52_PLATFORM", {
|
||||
"DISPLAY_CLASS": "SSD1306Display",
|
||||
}, "RAK_4631_repeater")
|
||||
self.assertEqual(ordinary["components"]["display_pixels_and_driver"], 4096)
|
||||
self.assertEqual(ordinary["components"]["display_pixels_and_driver"], 2048)
|
||||
|
||||
def test_esp32_fixed_oled_and_uart_bounds_preserve_full_runtime_reserves(self):
|
||||
defines = {
|
||||
@@ -866,13 +905,13 @@ int main() {
|
||||
}
|
||||
full = ram.requirements("ESP32_PLATFORM", defines, "Heltec_v3_repeater_observer_mqtt")
|
||||
parts = full["components"]
|
||||
self.assertEqual(full["required_heap_bytes"], 203920)
|
||||
self.assertEqual(full["required_heap_bytes"], 201904)
|
||||
self.assertEqual(parts["display_pixels_and_driver"], 2048)
|
||||
self.assertEqual(parts["uart_bridge_and_driver"], 3072 + 4096)
|
||||
self.assertEqual(parts["wireless_stacks"], 49152)
|
||||
self.assertEqual(parts["mqtt_startup_allowance"], 24576)
|
||||
self.assertEqual(parts["allocation_and_transient_margin"], 16384)
|
||||
self.assertEqual(full["required_contiguous_bytes"], 21184)
|
||||
self.assertEqual(full["required_contiguous_bytes"], 19168)
|
||||
for driver, expected in (("SH1106Display", 2048), ("SH1107Display", 4096),
|
||||
("ST7735Display", 25602)):
|
||||
with self.subTest(driver=driver):
|
||||
@@ -883,7 +922,7 @@ int main() {
|
||||
self.assertEqual(other["components"], {
|
||||
**parts, "display_pixels_and_driver": expected,
|
||||
})
|
||||
self.assertEqual(other["required_contiguous_bytes"], max(21184, expected))
|
||||
self.assertEqual(other["required_contiguous_bytes"], max(19168, expected))
|
||||
|
||||
def test_sh1106_full_mqtt_release_profile_is_linked_in_ci(self):
|
||||
workflow = (ROOT / ".github/workflows/run-unit-tests.yml").read_text()
|
||||
@@ -933,7 +972,8 @@ int main() {
|
||||
"-DWITH_RS232_BRIDGE=1", "-DWITH_UDP_BRIDGE=1", f"-DOBJECT_BYTES={size}",
|
||||
f"-DFRAME_BYTES={frame}", *["-D" + flag for flag in platform],
|
||||
], input=stand_in + prefix, text=True, capture_output=True)
|
||||
enforced = "ESP32=1" in platform or (
|
||||
enforced = "ESP32=1" in platform or ("NRF52_PLATFORM=1" in platform
|
||||
and path.endswith("SSD1306Display.cpp")) or (
|
||||
"RAK4631_COMBINED_ETHERNET=1" in platform
|
||||
and not path.endswith(("SH1106Display.cpp", "UDPBridge.cpp")))
|
||||
self.assertEqual(result.returncode == 0, bounded or not enforced,
|
||||
|
||||
@@ -93,10 +93,17 @@ static const char RULE_TEMP_FILE[] = "/flood_filter.tmp";
|
||||
static const char RULE_BACKUP_FILE[] = "/flood_filter.bak";
|
||||
'''
|
||||
SCENARIOS = r'''
|
||||
template<class Entry>
|
||||
auto setScope(Entry& row, const char* name, int) -> decltype(row.target_name, void()) {
|
||||
row.target_is_region = false;
|
||||
std::strcpy(row.target_name, name);
|
||||
}
|
||||
template<class Entry>
|
||||
void setScope(Entry& row, const char* name, long) { std::strcpy(row.scope_name,name); }
|
||||
template<class Entry> void canonicalRule(Entry& row) {
|
||||
row.active=true;row.payload_type=PAYLOAD_TYPE_ADVERT;
|
||||
row.min_hops=2;row.max_hops=63;row.suspend_on_temp_radio=true;
|
||||
std::strcpy(row.scope_name,"#Town");row.match_blacklisted_path=true;
|
||||
setScope(row,"#Town",0);row.match_blacklisted_path=true;
|
||||
row.scope_uses_slow_timing=true;row.incoming_scope_kind=FloodFilterPolicy::RULE_IN_ALLOWED;
|
||||
row.path_hash_size=3;row.rate_limit_enabled=true;row.rate_per_minute=1234;
|
||||
row.priority=7;row.stop_on_match=true;row.retry_on_match=true;
|
||||
@@ -216,6 +223,33 @@ int main() {
|
||||
assert(writer.saveFloodPacketFilters());MyMesh drop(fs,63);assert(drop.loadFloodPacketFilters());
|
||||
assert(drop.storage[0].active && drop.storage[0].drop_on_match && !drop.storage[0].retry_on_match);
|
||||
}
|
||||
{
|
||||
MemoryFS fs;MyMesh writer(fs,63);auto& row=writer.storage[62];
|
||||
row.active=true;row.payload_type=PAYLOAD_TYPE_ADVERT;row.max_hops=63;
|
||||
row.target_is_region=true;
|
||||
std::strcpy(row.target_name,"1234567890123456789012345678901");
|
||||
assert(writer.saveFloodPacketFilters());
|
||||
const auto image=fs.get(FLOOD_PACKET_FILTER_FILE);
|
||||
MyMesh reader(fs,63);assert(reader.loadFloodPacketFilters());
|
||||
assert(reader.storage[62].active && reader.storage[62].target_is_region);
|
||||
assert(!std::strcmp(reader.storage[62].regionName(), row.target_name));
|
||||
assert(reader.storage[62].scopeName()[0]==0);
|
||||
assert(reader.saveFloodPacketFilters() && fs.get(FLOOD_PACKET_FILTER_FILE)==image);
|
||||
// The stored region field starts at byte 153 of a 187-byte FPF7 row.
|
||||
// A direct scope and region target in one old-format row is invalid;
|
||||
// compaction must never silently choose one.
|
||||
const size_t stride=(image.size()-5-9)/63;
|
||||
assert(stride==187);
|
||||
for(bool terminate : {true,false}) {
|
||||
auto malformed=image;
|
||||
const size_t scope=5+62*stride+5;
|
||||
malformed[scope]='#';malformed[scope+1]='X';
|
||||
if(!terminate)std::fill_n(malformed.begin()+scope,32,'X');
|
||||
MemoryFS bad;bad.put(FLOOD_PACKET_FILTER_FILE,malformed);
|
||||
MyMesh boot(bad,63);assert(boot.loadFloodPacketFilters());
|
||||
assert(boot.seeded==1 && !boot.storage[62].active);
|
||||
}
|
||||
}
|
||||
const auto large=fixture(63,62),small=fixture(4,2);
|
||||
// Older writers stored every slot, including inactive trailing entries.
|
||||
// Build the exact production wire format and replace the final active row
|
||||
|
||||
@@ -24,8 +24,8 @@ static constexpr uint8_t NAME_LEN = 32, PATH_PREFIX_BYTES_MAX = 9;
|
||||
'''
|
||||
|
||||
SCENARIOS = r'''
|
||||
static_assert(sizeof(RepeaterEntry) == 332 && sizeof(RoomEntry) == 332,
|
||||
"extended rows retain the reviewed 332-byte RAM bound");
|
||||
static_assert(sizeof(RepeaterEntry) == 300 && sizeof(RoomEntry) == 332,
|
||||
"compact repeater rows save 32 bytes; room layout stays unchanged");
|
||||
static_assert(sizeof(LegacyEntry) == 40, "retain legacy STM32 rule layout");
|
||||
static_assert(alignof(RepeaterEntry) >= alignof(uint32_t));
|
||||
static_assert(alignof(RoomEntry) >= alignof(uint32_t));
|
||||
@@ -37,6 +37,16 @@ static_assert(alignof(RoomEntry) >= alignof(uint32_t));
|
||||
CHECK_ALIGNED(RepeaterEntry);
|
||||
CHECK_ALIGNED(RoomEntry);
|
||||
|
||||
template<class Entry>
|
||||
auto setTargetByte(Entry& e, bool region, size_t offset, char value, int)
|
||||
-> decltype(e.target_name, void()) {
|
||||
e.target_is_region = region;
|
||||
e.target_name[offset] = value;
|
||||
}
|
||||
template<class Entry>
|
||||
void setTargetByte(Entry& e, bool region, size_t offset, char value, long) {
|
||||
(region ? e.target_region_name : e.scope_name)[offset] = value;
|
||||
}
|
||||
template<class Entry> void setFlags(Entry& e, unsigned flags) {
|
||||
e.active = flags & 1;
|
||||
e.suspend_on_temp_radio = flags & 2;
|
||||
@@ -108,7 +118,7 @@ template<class Entry> void exerciseConfigurationEquality() {
|
||||
case 2: changed.min_hops = 1; break;
|
||||
case 3: changed.max_hops = 1; break;
|
||||
case 4: changed.suspend_on_temp_radio = true; break;
|
||||
case 5: changed.scope_name[sizeof(changed.scope_name)-1] = 1; break;
|
||||
case 5: setTargetByte(changed, false, 31, 1, 0); break;
|
||||
case 6: changed.match_blacklisted_path = true; break;
|
||||
case 7: changed.scope_uses_slow_timing = true; break;
|
||||
case 8: changed.incoming_scope_kind = 1; break;
|
||||
@@ -120,7 +130,7 @@ template<class Entry> void exerciseConfigurationEquality() {
|
||||
case 14: changed.path_hash_size = 1; break;
|
||||
case 15: changed.path_hops = 1; break;
|
||||
case 16: changed.path[sizeof(changed.path)-1] = 1; break;
|
||||
case 17: changed.target_region_name[sizeof(changed.target_region_name)-1] = 1; break;
|
||||
case 17: setTargetByte(changed, true, 31, 1, 0); break;
|
||||
case 18: changed.drop_on_match = true; break;
|
||||
case 19: changed.rate_limit_enabled = false; break;
|
||||
case 20: changed.rate_per_minute = 6; break;
|
||||
@@ -128,7 +138,7 @@ template<class Entry> void exerciseConfigurationEquality() {
|
||||
case 22: changed.stop_on_match = true; break;
|
||||
case 23: changed.retry_on_match = true; break;
|
||||
case 24: changed.transport_modes = 2; break;
|
||||
case 25: changed.scope_name[0] = '#'; break;
|
||||
case 25: setTargetByte(changed, false, 0, '#', 0); break;
|
||||
}
|
||||
assert(!FloodFilterPolicy::sameRuleConfiguration(original, changed));
|
||||
assert(!FloodFilterPolicy::sameRuleConfiguration(changed, original));
|
||||
@@ -171,6 +181,25 @@ template<class Rules, class Entry> void exerciseProductionSlotSelection() {
|
||||
assert(rules.select(candidate, 3) == 3); // Explicit index remains authoritative.
|
||||
}
|
||||
int main() {
|
||||
// Every old flag combination can coexist with either target kind. Neither
|
||||
// the tag nor name views may corrupt independent flags or leak a stale name.
|
||||
for (unsigned flags = 0; flags < 512; ++flags) {
|
||||
for (unsigned region = 0; region < 2; ++region) {
|
||||
RepeaterEntry e{};
|
||||
setFlags(e, flags);
|
||||
e.target_is_region = region;
|
||||
std::strcpy(e.target_name, "1234567890123456789012345678901");
|
||||
assert(flagsOf(e) == flags);
|
||||
const auto& selected = region ? e.regionName() : e.scopeName();
|
||||
const auto& inactive = region ? e.scopeName() : e.regionName();
|
||||
assert(!std::strcmp(selected, e.target_name));
|
||||
for (char byte : inactive) assert(byte == 0);
|
||||
RepeaterEntry copied = e;
|
||||
assert(copied.target_is_region == region);
|
||||
assert(!std::memcmp(copied.target_name, e.target_name, 32));
|
||||
assert(flagsOf(copied) == flags);
|
||||
}
|
||||
}
|
||||
exerciseFlags<RepeaterEntry>();
|
||||
exerciseFlags<RoomEntry>();
|
||||
exerciseConfigurationEquality<RepeaterEntry>();
|
||||
|
||||
@@ -139,6 +139,12 @@ int main() {
|
||||
{ Fixture f;f.radio.receiving=true;assert(f.d.queue(bytes));f.tick();f.tick(201);
|
||||
assert(f.radio.sends==0 && f.d.hasOutbound());f.radio.receiving=false;f.tick(201);assert(f.radio.sends==1);
|
||||
}
|
||||
{ Fixture f;f.radio.receiving=true;assert(f.d.queue(bytes));f.tick();
|
||||
// The empty MeshCore queue must not retire a foreign frame's live CAD
|
||||
// wait. Its existing bounded escape still records sustained contention.
|
||||
f.tick(201);assert(!f.radio.sends && f.d.hasOutbound());
|
||||
f.tick(3800);assert(f.radio.sends==1 && (f.d.getErrFlags() & ERR_EVENT_CAD_TIMEOUT));
|
||||
}
|
||||
{ Fixture f;auto* p=newPacket(f.d);f.radio.p.cross=RadioCrossMode::On;
|
||||
assert(!f.radio.p.canCross() && f.d.getTransmitProfileMask(p)==1);
|
||||
assert(f.d.sendPacket(p,0));assert(f.manager.getOutboundTotal()==1);f.tick();
|
||||
|
||||
@@ -286,6 +286,30 @@ int main() {
|
||||
assert(f.d.getTotalAirTime()==1151 && !f.d.getNumSentDirect() && f.d.done.empty());
|
||||
}
|
||||
}
|
||||
'''
|
||||
self.compile_run(code, ["-DENABLE_OTA", "-DMESH_ENABLE_MESHTEXT_CHAT=0",
|
||||
"-DMESH_ENABLE_ETHERNET_TUNNEL=0"], dispatcher=True)
|
||||
|
||||
def test_channel_wait_retirement_preserves_direct_ota_owner(self):
|
||||
code = dispatcher_harness() + r'''
|
||||
int main() {
|
||||
{ Fixture f;directProfile(f);f.radio.receiving=true;
|
||||
auto* old=newPacket(f.d,RADIO_TX_SECONDARY);assert(f.d.sendPacket(old,0));
|
||||
f.tick();assert(!f.radio.sends && f.manager.getOutboundTotal()==1);
|
||||
f.manager.free(f.manager.removeOutboundByIdx(0));
|
||||
f.clock.now=10000;assert(enqueue(f));f.tick();
|
||||
// New raw/direct ownership cannot inherit a cancelled MeshCore wait.
|
||||
assert(!f.radio.sends && !(f.d.getErrFlags() & ERR_EVENT_CAD_TIMEOUT));
|
||||
}
|
||||
{ Fixture f;directProfile(f);f.radio.receiving=true;assert(enqueue(f));
|
||||
f.tick();assert(!f.radio.sends && !f.manager.getOutboundTotal());
|
||||
f.clock.now=10000;auto* next=newPacket(f.d,RADIO_TX_SECONDARY);
|
||||
assert(f.d.sendPacket(next,0));f.tick();
|
||||
// Conversely, a still-owned direct frame keeps its original ceiling even
|
||||
// when another queued packet arrives for the same profile.
|
||||
assert(f.radio.sends==1 && (f.d.getErrFlags() & ERR_EVENT_CAD_TIMEOUT));
|
||||
}
|
||||
}
|
||||
'''
|
||||
self.compile_run(code, ["-DENABLE_OTA", "-DMESH_ENABLE_MESHTEXT_CHAT=0",
|
||||
"-DMESH_ENABLE_ETHERNET_TUNNEL=0"], dispatcher=True)
|
||||
|
||||
@@ -1317,6 +1317,190 @@ TEST(Dispatcher, GrowingQueueShortensCADBusyCeiling) {
|
||||
EXPECT_TRUE(dispatcher.getErrFlags() & ERR_EVENT_CAD_TIMEOUT);
|
||||
}
|
||||
|
||||
TEST(Dispatcher, CancelledBusyQueueDoesNotAgeLaterChannelWait) {
|
||||
RxReservePacketManager manager(8, 1);
|
||||
TestClock clock;
|
||||
clock.now = 100;
|
||||
TestRadio radio;
|
||||
radio.receiving = true;
|
||||
TestDispatcher dispatcher(radio, clock, manager);
|
||||
dispatcher.begin();
|
||||
|
||||
auto queuePacket = [&]() {
|
||||
mesh::Packet* packet = dispatcher.obtainNewPacket();
|
||||
EXPECT_NE(packet, nullptr);
|
||||
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
|
||||
packet->payload[0] = 0x40;
|
||||
packet->payload_len = 1;
|
||||
EXPECT_TRUE(dispatcher.sendPacket(packet, 0));
|
||||
};
|
||||
queuePacket();
|
||||
clock.now = 101;
|
||||
dispatcher.loop();
|
||||
EXPECT_EQ(0, radio.send_starts);
|
||||
|
||||
// Echo suppression, cancellation and stale-work expiry can retire the last
|
||||
// queued packet without a successful send or a free-channel observation.
|
||||
manager.free(manager.removeOutboundByIdx(0));
|
||||
clock.now = 302;
|
||||
dispatcher.loop();
|
||||
EXPECT_EQ(0, manager.getOutboundTotal());
|
||||
|
||||
clock.now = 10000;
|
||||
queuePacket();
|
||||
dispatcher.loop();
|
||||
EXPECT_EQ(0, radio.send_starts);
|
||||
EXPECT_EQ(0, dispatcher.getErrFlags() & ERR_EVENT_CAD_TIMEOUT);
|
||||
|
||||
// A genuinely continuous busy channel still reaches the original ceiling.
|
||||
clock.now = 14001;
|
||||
dispatcher.loop();
|
||||
EXPECT_EQ(1, radio.send_starts);
|
||||
EXPECT_TRUE(dispatcher.getErrFlags() & ERR_EVENT_CAD_TIMEOUT);
|
||||
}
|
||||
|
||||
TEST(Dispatcher, BeginStartsFreshCADBusyObservation) {
|
||||
RxReservePacketManager manager(8, 1);
|
||||
TestClock clock;
|
||||
clock.now = 100;
|
||||
TestRadio radio;
|
||||
radio.receiving = true;
|
||||
TestDispatcher dispatcher(radio, clock, manager);
|
||||
dispatcher.begin();
|
||||
|
||||
mesh::Packet* packet = dispatcher.obtainNewPacket();
|
||||
ASSERT_NE(packet, nullptr);
|
||||
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
|
||||
packet->payload[0] = 0x40;
|
||||
packet->payload_len = 1;
|
||||
ASSERT_TRUE(dispatcher.sendPacket(packet, 0));
|
||||
clock.now = 101;
|
||||
dispatcher.loop();
|
||||
EXPECT_EQ(0, radio.send_starts);
|
||||
|
||||
clock.now = 10000;
|
||||
dispatcher.begin();
|
||||
dispatcher.loop();
|
||||
EXPECT_EQ(0, radio.send_starts);
|
||||
EXPECT_EQ(0, dispatcher.getErrFlags() & ERR_EVENT_CAD_TIMEOUT);
|
||||
clock.now = 14001;
|
||||
dispatcher.loop();
|
||||
EXPECT_EQ(1, radio.send_starts);
|
||||
EXPECT_TRUE(dispatcher.getErrFlags() & ERR_EVENT_CAD_TIMEOUT);
|
||||
}
|
||||
|
||||
TEST(Dispatcher, ImmediateReplacementStartsFreshCADBusyObservation) {
|
||||
RxReservePacketManager manager(8, 1);
|
||||
TestClock clock;
|
||||
clock.now = 100;
|
||||
TestRadio radio;
|
||||
radio.receiving = true;
|
||||
TestDispatcher dispatcher(radio, clock, manager);
|
||||
dispatcher.begin();
|
||||
|
||||
auto queuePacket = [&]() {
|
||||
mesh::Packet* packet = dispatcher.obtainNewPacket();
|
||||
EXPECT_NE(packet, nullptr);
|
||||
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
|
||||
packet->payload[0] = 0x40;
|
||||
packet->payload_len = 1;
|
||||
EXPECT_TRUE(dispatcher.sendPacket(packet, 0));
|
||||
};
|
||||
queuePacket();
|
||||
clock.now = 101;
|
||||
dispatcher.loop();
|
||||
EXPECT_EQ(0, radio.send_starts);
|
||||
|
||||
// There need not be an intervening Dispatcher loop with an empty queue.
|
||||
// A cancellation callback can enqueue the replacement immediately.
|
||||
clock.now = 10000;
|
||||
manager.free(manager.removeOutboundByIdx(0));
|
||||
queuePacket();
|
||||
dispatcher.loop();
|
||||
EXPECT_EQ(0, radio.send_starts);
|
||||
EXPECT_EQ(0, dispatcher.getErrFlags() & ERR_EVENT_CAD_TIMEOUT);
|
||||
}
|
||||
|
||||
TEST(Dispatcher, OtherProfilesQueuedWorkDoesNotPreserveRetiredCADWait) {
|
||||
class DualProfileRadio : public TestRadio {
|
||||
public:
|
||||
mesh::RadioProfiles config;
|
||||
DualProfileRadio() {
|
||||
config.secondary.params = config.primary;
|
||||
config.secondary.mode = mesh::RadioProfileMode::RxTx;
|
||||
}
|
||||
mesh::RadioProfiles* profiles() override { return &config; }
|
||||
const mesh::RadioProfiles* profiles() const override { return &config; }
|
||||
mesh::RadioParamApplyResult prepareTransmitProfile(uint8_t) override {
|
||||
return mesh::RadioParamApplyResult::APPLIED;
|
||||
}
|
||||
} radio;
|
||||
RxReservePacketManager manager(8, 1);
|
||||
TestClock clock;
|
||||
clock.now = 100;
|
||||
radio.receiving = true;
|
||||
TestDispatcher dispatcher(radio, clock, manager);
|
||||
dispatcher.begin();
|
||||
|
||||
auto queuePacket = [&](uint8_t policy, uint32_t delay = 0) {
|
||||
mesh::Packet* packet = dispatcher.obtainNewPacket();
|
||||
EXPECT_NE(packet, nullptr);
|
||||
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
|
||||
packet->payload[0] = 0x40;
|
||||
packet->payload_len = 1;
|
||||
packet->tx_radio = policy;
|
||||
EXPECT_TRUE(dispatcher.sendPacket(packet, 0, delay));
|
||||
};
|
||||
queuePacket(mesh::RADIO_TX_SECONDARY);
|
||||
clock.now = 101;
|
||||
dispatcher.loop();
|
||||
EXPECT_EQ(0, radio.send_starts);
|
||||
manager.free(manager.removeOutboundByIdx(0));
|
||||
|
||||
// Future work on primary keeps the whole queue nonempty, but cannot own
|
||||
// the secondary profile's previous, cancelled channel wait.
|
||||
queuePacket(mesh::RADIO_TX_PRIMARY, 100000);
|
||||
clock.now = 10000;
|
||||
queuePacket(mesh::RADIO_TX_SECONDARY);
|
||||
dispatcher.loop();
|
||||
EXPECT_EQ(0, radio.send_starts);
|
||||
EXPECT_EQ(0, dispatcher.getErrFlags() & ERR_EVENT_CAD_TIMEOUT);
|
||||
}
|
||||
|
||||
TEST(Dispatcher, NewQueuedWorkPreservesRetainedRetryCADWait) {
|
||||
RxReservePacketManager manager(8, 1);
|
||||
TestClock clock;
|
||||
clock.now = 100;
|
||||
TestRadio radio;
|
||||
radio.start_success = false;
|
||||
TestDispatcher dispatcher(radio, clock, manager);
|
||||
dispatcher.begin();
|
||||
auto queuePacket = [&]() {
|
||||
mesh::Packet* packet = dispatcher.obtainNewPacket();
|
||||
EXPECT_NE(packet, nullptr);
|
||||
packet->header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
|
||||
packet->payload[0] = 0x40;
|
||||
packet->payload_len = 1;
|
||||
EXPECT_TRUE(dispatcher.sendPacket(packet, 0));
|
||||
};
|
||||
queuePacket();
|
||||
clock.now = 101;
|
||||
dispatcher.loop();
|
||||
EXPECT_EQ(1, radio.send_starts);
|
||||
EXPECT_EQ(0, manager.getOutboundTotal());
|
||||
|
||||
radio.receiving = true;
|
||||
clock.now = 302;
|
||||
dispatcher.loop();
|
||||
EXPECT_EQ(1, radio.send_starts);
|
||||
clock.now = 5000;
|
||||
queuePacket();
|
||||
radio.start_success = true;
|
||||
dispatcher.loop();
|
||||
EXPECT_EQ(2, radio.send_starts);
|
||||
EXPECT_TRUE(dispatcher.getErrFlags() & ERR_EVENT_CAD_TIMEOUT);
|
||||
}
|
||||
|
||||
TEST(Dispatcher, SilentRadioEscalatesFromSoftToHardRecovery) {
|
||||
RxReservePacketManager manager(8, 4);
|
||||
TestClock clock;
|
||||
|
||||
Reference in new issue
Block a user