Add saved repeater trace exception while repeating is disabled

This commit is contained in:
mikecarper committed 2026-10-03 15:48:44 -07:00
1 parent 4bec31b4e1
commit 1ddca12a41
14 files changed
+761 -22

No files matched your search

+5
View File
@@ -96,6 +96,11 @@ jobs:
- name: Verify bridge filter wiring and shorthand permissions
run: python3 -B test/test_transport_filter_wiring.py -v
- name: Verify saved repeat-off trace exception and relay guards
run: |
python3 -B test/test_repeater_trace_repeat.py -v
python3 -B test/test_repeater_trace_forwarding.py -v
- name: Verify MQTT start results and connection-attempt accounting
run: python3 -B test/test_mqtt_transport_results.py -v
+2
View File
@@ -165,6 +165,7 @@ should use command `0x42`. See [Companion radio binary protocol](companion_proto
| System | [`get/set reboot.interval`](#reboot-interval) | Full parser | Yes | Yes | Yes |
| Clock sync | [`get/set clock.sync.*`; `clock.sync.mesh now`](cli_commands.md#estimate-and-correct-infrastructure-node-time-after-startup) | Repeater, sensor, and room server; `clock.sync.internet` needs MQTT repeater | Yes | Yes | Yes |
| Routing | [`get/set repeat`](cli_commands.md#view-or-change-this-nodes-repeat-flag) | Forwarding-capable role | Yes | Yes | Yes |
| Routing | [`get/set repeat.trace`](cli_commands.md#allow-routed-traces-while-repeating-is-disabled-repeater-only) | Repeater only; saved repeat-off trace exception, default off | Yes | Yes | Yes |
| Routing | [`get/set path.hash.mode`](cli_commands.md#view-or-change-this-nodes-advert-path-hash-size) | Role that supports path-hash selection | Yes | Yes | Yes |
| Routing | [`get/set loop.detect`](cli_commands.md#view-or-change-this-nodes-loop-detection) | Repeater | Yes | Yes | Yes |
| Routing | [`get/set txdelay`](cli_commands.md#view-or-change-the-retransmit-delay-factor-for-flood-traffic) | All text CLI roles, including bridges | Yes | Yes | Yes |
@@ -338,6 +339,7 @@ should use command `0x42`. See [Companion radio binary protocol](companion_proto
| System | [`get/set reboot.interval`](#reboot-interval) | Full parser | Yes | Yes | Yes | Yes | Yes |
| Clock sync | [`get/set clock.sync.*`; `clock.sync.mesh now`](cli_commands.md#estimate-and-correct-infrastructure-node-time-after-startup) | Repeater, sensor, and room server; `clock.sync.internet` needs MQTT repeater | Yes | Yes | Yes | Yes | Yes |
| Routing | [`get/set repeat`](cli_commands.md#view-or-change-this-nodes-repeat-flag) | Forwarding-capable role | Yes | Yes | Yes | Yes | Yes |
| Routing | [`get/set repeat.trace`](cli_commands.md#allow-routed-traces-while-repeating-is-disabled-repeater-only) | Repeater only; saved repeat-off trace exception, default off | Yes | Yes | Yes | Yes | Yes |
| Routing | [`get/set path.hash.mode`](cli_commands.md#view-or-change-this-nodes-advert-path-hash-size) | Full parser | Yes | Yes | Yes | Yes | Yes |
| Routing | [`get/set loop.detect`](cli_commands.md#view-or-change-this-nodes-loop-detection) | Repeater, full common parser | Yes | Yes | Yes | Yes | Yes |
| Routing | [`get/set txdelay`](cli_commands.md#view-or-change-the-retransmit-delay-factor-for-flood-traffic) | All text CLI roles, including bridges | Yes | Yes | Yes | Yes | Yes |
+41 -1
View File
@@ -2382,7 +2382,47 @@ The pin number is the Arduino pin number used by that target (the normal GPIO nu
**Parameters:**
- `state`: `on`|`off`
**Default:** `flood.channel.data on`; `flood.channel.data.hops h=all`
**Default:** `repeat on`.
`repeat off` disables relay forwarding, not locally generated traffic or
configuration replies. Repeaters can separately opt in to the routed-trace
exception below.
#### Allow routed traces while repeating is disabled (repeater only)
```text
get repeat.trace
set repeat.trace on
set repeat.trace off
```
Default: `off`, including upgrades from older preference files. The setting is
saved and takes effect without reboot. Failed saves leave the active choice
unchanged. Room Server, Sensor, and Companion do not gain this exception.
To keep ordinary forwarding disabled while allowing routed trace diagnostics:
```text
set repeat off
set repeat.trace on
```
Only direct TRACE packets with a matching next hop may be relayed. Ordinary
messages, adverts, group data, ACKs, and control packets remain subject to
`repeat off`; flood traces remain invalid. Existing trace length, path-capacity,
duplicate, radio-profile, and channel/duty-cycle checks still apply. This is
a diagnostics opt-in, not authenticated-admin-only forwarding: trace packets
retain the protocol's existing authentication behavior.
`get repeat` and the disabled status bit continue to report repeating off.
With `repeat on`, traces use the ordinary forwarding policy regardless of
`repeat.trace`. Changing the exception to off while repeating is off blocks
subsequent relayed trace transmissions, including queued copies and retries;
a trace already on air is allowed to finish. Locally initiated traffic and
management replies are not cancelled.
The dynamic configuration field is `repeat.trace_off` (`0` or `1`). It is
appended to `/com_prefs`; all previously saved field offsets stay unchanged.
---
+3
View File
@@ -260,6 +260,9 @@ settings override these defaults. To toggle only MQTT while keeping
USB logging unchanged, use `set mqtt.enabled off` / `on`. Neither setting
turns LoRa repeating off. Repeater forwarding uses `set repeat off` / `on`
and `get repeat` separately.
Repeaters can opt in to routed trace diagnostics with `set repeat.trace on`
while `repeat` stays off; the saved exception defaults off and does not relay
other payload types. See [repeat-off traces](cli_commands.md#allow-routed-traces-while-repeating-is-disabled-repeater-only).
The selector changes USB master output, not saved `usb.debug` intent. Direct
WiFi MQTT is independent of that USB verbosity preference.
+15 -2
View File
@@ -1022,7 +1022,8 @@ bool MyMesh::floodChannelDataHopApplies(const mesh::Packet* packet) const {
}
bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
if (_prefs.disable_fwd) return false;
if (_prefs.disable_fwd && !(_prefs.trace_when_repeat_off == 1 && packet
&& packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE)) return false;
if (packet->isRouteFlood()) {
if (mesh::isFloodHopLimitExceeded(packet, _prefs.flood_max,
_prefs.flood_max_unscoped,
@@ -1086,6 +1087,16 @@ bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
return true;
}
bool MyMesh::allowPacketTransmit(const mesh::Packet* packet) const {
// Forwarded traces have already appended this node's SNR. A locally
// originated trace starts with an empty SNR path and is not a relay.
// Recheck at egress so a queued relay/retry cannot outlive this opt-in.
if (_prefs.disable_fwd && _prefs.trace_when_repeat_off != 1 && packet
&& packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE
&& packet->path_len > 0) return false;
return mesh::Mesh::allowPacketTransmit(packet);
}
const char *MyMesh::getLogDateTime() {
static char tmp[32];
uint32_t now = getRTCClock()->getCurrentTime();
@@ -1462,7 +1473,9 @@ uint32_t MyMesh::getDirectRetryAttemptStepMillis() const {
}
bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const {
(void)packet;
if (_prefs.disable_fwd && _prefs.trace_when_repeat_off != 1 && packet
&& packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE
&& packet->path_len > 0) return false;
if (!_prefs.direct_retry_enabled) {
return false;
}
+1
View File
@@ -917,6 +917,7 @@ protected:
}
bool allowPacketForward(const mesh::Packet* packet) override;
bool allowPacketTransmit(const mesh::Packet* packet) const override;
const char* getLogDateTime() override;
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
+46 -1
View File
@@ -764,6 +764,7 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
memset(_prefs->extra_sf, 0, sizeof(_prefs->extra_sf));
_prefs->usb_logging_enabled = 1;
_prefs->usb_debug_enabled = 0;
_prefs->trace_when_repeat_off = 0;
#ifdef WITH_RS232_BRIDGE
_prefs->bridge_uart = WITH_RS232_BRIDGE_UART;
#else
@@ -935,9 +936,10 @@ void CommonCLI::syncOtaConfigFromPrefs() {
#endif
void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
// Older or truncated preference images must never inherit debug opt-in
// Older or truncated preference images must never inherit these opt-ins
// from a previous load of a newer image.
_prefs->usb_debug_enabled = 0;
_prefs->trace_when_repeat_off = 0;
#if defined(RP2040_PLATFORM)
File file = fs->open(filename, "r");
#else
@@ -1330,6 +1332,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
if (file.available() >= (int)sizeof(_prefs->usb_debug_enabled)) {
file.read((uint8_t *)&_prefs->usb_debug_enabled,
sizeof(_prefs->usb_debug_enabled));
if (file.available() >= (int)sizeof(_prefs->trace_when_repeat_off)) {
file.read((uint8_t *)&_prefs->trace_when_repeat_off,
sizeof(_prefs->trace_when_repeat_off));
}
}
}
}
@@ -1445,6 +1451,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->usb_logging_enabled = constrain(_prefs->usb_logging_enabled, 0, 1); // boolean
// Only a deliberately saved 1 enables diagnostics; corrupted bytes are OFF.
_prefs->usb_debug_enabled = _prefs->usb_debug_enabled == 1 ? 1 : 0;
// Corrupt or unknown values must not enable a repeat-off exception.
_prefs->trace_when_repeat_off = _prefs->trace_when_repeat_off == 1 ? 1 : 0;
_prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean
if (!directRetryPrefsValid(_prefs)) {
setDefaultDirectRetryPrefs(_prefs);
@@ -1658,6 +1666,7 @@ static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) {
WRITE_COMMON_PREFS(&prefs->primary_radio_preamble); // appended primary tuple field
WRITE_COMMON_PREFS(&prefs->espnow_bridge_enabled); // appended Full ESP-NOW intent
WRITE_COMMON_PREFS(&prefs->usb_debug_enabled); // appended USB debug intent
WRITE_COMMON_PREFS(&prefs->trace_when_repeat_off); // appended repeater trace exception
#undef WRITE_COMMON_PREFS_BYTES
#undef WRITE_COMMON_PREFS
@@ -1671,6 +1680,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
_common_save_result_known = true;
_common_save_succeeded = false;
_prefs->usb_debug_enabled = _prefs->usb_debug_enabled == 1 ? 1 : 0;
_prefs->trace_when_repeat_off = _prefs->trace_when_repeat_off == 1 ? 1 : 0;
}
#ifdef WITH_MQTT_BRIDGE
// Observer builds use a verified temp/backup transaction for common prefs.
@@ -1817,6 +1827,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
file.write((uint8_t *)&_prefs->primary_radio_preamble, sizeof(_prefs->primary_radio_preamble)); // appended
file.write((uint8_t *)&_prefs->espnow_bridge_enabled, sizeof(_prefs->espnow_bridge_enabled)); // appended
file.write((uint8_t *)&_prefs->usb_debug_enabled, sizeof(_prefs->usb_debug_enabled)); // appended
file.write((uint8_t *)&_prefs->trace_when_repeat_off, sizeof(_prefs->trace_when_repeat_off)); // appended
_common_save_succeeded = file.commit();
if (!_common_save_succeeded) {
@@ -3349,6 +3360,29 @@ bool CommonCLI::handleSdCardGetCmd(const char* config, char* reply) {
void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) {
const char* config = &command[4];
if (strncmp(config, "repeat.trace", 12) == 0
&& (config[12] == 0 || config[12] == ' ' || config[12] == '\t')) {
if (strcmp(_callbacks->getRole(), "repeater") != 0) {
strcpy(reply, "Error: repeat.trace is only supported by repeaters");
return;
}
const char* value = config + 12;
while (*value == ' ' || *value == '\t') ++value;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
strcpy(reply, "Error: usage set repeat.trace on|off");
return;
}
const uint8_t previous = _prefs->trace_when_repeat_off;
_prefs->trace_when_repeat_off = strcmp(value, "on") == 0 ? 1 : 0;
if (!trySavePrefs()) {
_prefs->trace_when_repeat_off = previous;
strcpy(reply, "Error: Repeat trace not saved; unchanged");
return;
}
_callbacks->onRetryConfigChanged();
snprintf(reply, 160, "OK - repeat.trace %s (saved)", value);
return;
}
#if defined(NRF52_POWER_MANAGEMENT)
if (mesh::power::handleVoltagePolicyCommand(command, reply, 160)) return;
#endif
@@ -4695,6 +4729,17 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) {
const char* config = &command[4];
if (strncmp(config, "repeat.trace", 12) == 0
&& (config[12] == 0 || config[12] == ' ' || config[12] == '\t')) {
if (strcmp(_callbacks->getRole(), "repeater") != 0) {
strcpy(reply, "Error: repeat.trace is only supported by repeaters");
} else if (strcmp(config, "repeat.trace") != 0) {
strcpy(reply, "Error: usage get repeat.trace");
} else {
snprintf(reply, 160, "> %s", _prefs->trace_when_repeat_off == 1 ? "on" : "off");
}
return;
}
#if defined(NRF52_POWER_MANAGEMENT)
if (mesh::power::handleVoltagePolicyCommand(command, reply, 160)) return;
#endif
+5 -1
View File
@@ -183,8 +183,11 @@ public:
// logging artifact's enabled-at-first-boot behavior.
uint8_t usb_logging_enabled = 1;
// USB diagnostics are opt-in independently of the packet output gate.
// Persisted at the very end of /com_prefs, not beside usb_log.
// Appended after ESP-NOW intent in /com_prefs, not beside usb_log.
uint8_t usb_debug_enabled = 0;
// Repeater-only opt-in for routed trace diagnostics while repeat is off.
// Appended after usb_debug in /com_prefs; existing images stay disabled.
uint8_t trace_when_repeat_off = 0;
// Runtime UART choice for merged RS-232 repeater artifacts. Appended at
// /com_prefs offset 862; single-UART builds keep their compiled port here.
uint8_t bridge_uart = 0;
@@ -377,6 +380,7 @@ private:
protected:
void structure() override {
def("disable", _parent->disable_fwd);
def("trace_off", _parent->trace_when_repeat_off);
def("f_max", _parent->flood_max);
def("f_max_uns", _parent->flood_max_unscoped);
def("f_max_adv", _parent->flood_max_advert);
+12 -8
View File
@@ -573,15 +573,19 @@ bool isUsbLoggingWatchdogUpdateActive() { return false; }
bool handleUsbLoggingWatchdogCommand(const char* command, char* reply,
size_t capacity) {
if (!command || !reply || !capacity) return false;
static const char* const keys[] = {"get usb.watchdog.last", "get usb.watchdog", "set usb.watchdog"};
for (const char* key : keys) {
const size_t length = strlen(key);
if (strncmp(command, key, length)
|| (command[length] && command[length] != ' ' && command[length] != '\t')) continue;
snprintf(reply, capacity, "Error: unsupported");
return true;
const char* suffix;
constexpr size_t prefix_size = sizeof("get usb.watchdog") - 1;
if (strncmp(command, "get usb.watchdog", prefix_size) == 0) {
suffix = command + prefix_size;
if (strncmp(suffix, ".last", 5) == 0) suffix += 5;
} else if (strncmp(command, "set usb.watchdog", prefix_size) == 0) {
suffix = command + prefix_size;
} else {
return false;
}
return false;
if (*suffix && *suffix != ' ' && *suffix != '\t') return false;
snprintf(reply, capacity, "Error: unsupported");
return true;
}
} // namespace mesh
#endif // MESH_USB_CONSOLE_COOPERATIVE
+3 -2
View File
@@ -82,8 +82,9 @@ int main() {
CommonCLI cli;
assert(cli.saveCommonPrefs());
auto original=cli.fs.files["/com_prefs"];
const size_t preamble_offset=original.size()-4;
assert(preamble_offset>=864);
// Appending preferences must not move the established preamble bytes.
const size_t preamble_offset=866;
assert(original.size() >= preamble_offset+2);
assert(original[preamble_offset]==48 && original[preamble_offset+1]==0);
assert(original[preamble_offset+2]==1);
Capture capture; assert(writeCommonPrefsImage(capture,&cli.prefs));
+8 -7
View File
@@ -157,11 +157,11 @@ int main() {
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
assert(cli._common_save_succeeded);
const auto baseline = cli.fs.files["/com_prefs"];
assert(baseline.size() == 870);
const size_t debug_offset = baseline.size() - 1;
const size_t logging_offset = baseline.size() - 7;
const size_t preamble_offset = baseline.size() - 4;
const size_t espnow_offset = baseline.size() - 2;
assert(baseline.size() == 871);
const size_t debug_offset = baseline.size() - 2;
const size_t logging_offset = baseline.size() - 8;
const size_t preamble_offset = baseline.size() - 5;
const size_t espnow_offset = baseline.size() - 3;
assert(baseline[debug_offset] == 0 && baseline[logging_offset] == 1);
assert(baseline[preamble_offset] == 48 && baseline[espnow_offset] == 1);
Capture capture;
@@ -249,10 +249,11 @@ int main() {
assert(cli.prefs.usb_debug_enabled == 0 && cli.fs.files["/com_prefs"][debug_offset] == 0);
}
// Every older/torn length inherits debug OFF, even after loading debug ON.
// Every image without the complete debug byte inherits debug OFF, even
// after loading debug ON; a missing later trace byte cannot erase debug.
auto image = baseline;
image[logging_offset] = 0; image[debug_offset] = 1;
for (size_t size = 0; size < image.size(); ++size) {
for (size_t size = 0; size <= debug_offset; ++size) {
CommonCLI reader;
reader.prefs.usb_debug_enabled = 1;
reader.fs.files["/com_prefs"] = {image.begin(), image.begin() + size};
+222
View File
@@ -0,0 +1,222 @@
"""Execute actual repeater trace gates with the production Packet definition."""
from pathlib import Path
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <cassert>
#include <cstring>
#include <initializer_list>
#include <Packet.h>
#include <helpers/RoutingPolicy.h>
#define LOOP_DETECT_OFF 0
#define LOOP_DETECT_MINIMAL 1
#define LOOP_DETECT_MODERATE 2
#define LOOP_DETECT_STRICT 3
#define FLOOD_CHANNEL_HOPS_ALL 255
const uint8_t max_loop_minimal[] = {1}, max_loop_moderate[] = {2}, max_loop_strict[] = {3};
namespace mesh {
@CONSTRUCTOR@
// The boundary records delegation to the existing OTA/profile transmit veto.
class Mesh {
public:
mutable unsigned base_calls = 0;
bool base_allowed = true;
virtual ~Mesh() = default;
virtual bool allowPacketTransmit(const Packet*) const {
++base_calls;
return base_allowed;
}
};
}
struct SimpleMeshTables {
struct RecentRepeaterInfo { int8_t snr_x4 = 12; } recent;
bool known = false;
const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t*, uint8_t) const {
return known ? &recent : nullptr;
}
};
struct MyMesh : mesh::Mesh {
struct Prefs {
uint8_t disable_fwd = 1, trace_when_repeat_off = 0;
uint8_t flood_max = 64, flood_max_unscoped = 64, flood_max_advert = 64;
uint8_t flood_channel_data_enabled = 1, loop_detect = LOOP_DETECT_OFF;
uint8_t direct_retry_enabled = 1, direct_retry_recent_enabled = 0;
int8_t direct_retry_snr_margin_x4 = 0;
} _prefs;
bool recv_pkt_channel_scope_rejected = false, recv_pkt_regionless_scope_set = false;
bool recv_pkt_channel_scope_bypass = false, looped = false;
const void* recv_pkt_region = this;
unsigned recv_pkt_filter_match_mask = 0;
bool filter_block = false, moderation_block = false, clock_sync_mesh_edge_enabled = false;
unsigned commits = 0, clock_samples = 0;
SimpleMeshTables tables;
bool floodChannelDataHopApplies(const mesh::Packet*) const { return true; }
bool isLooped(const mesh::Packet*, const uint8_t*) { return looped; }
bool shouldBlockFloodPacketForward(const mesh::Packet*, unsigned) { return filter_block; }
bool shouldBlockFloodGroupTextForward(const mesh::Packet*) { return moderation_block; }
void commitFloodPacketFilterRates(const mesh::Packet*, unsigned) { ++commits; }
void recordAcceptedFloodClockSample(const mesh::Packet*) { ++clock_samples; }
const SimpleMeshTables* getTables() const { return &tables; }
int8_t getDirectRetryMinSNRX4() const { return 12; }
bool allowPacketForward(const mesh::Packet*);
bool allowPacketTransmit(const mesh::Packet*) const override;
bool allowDirectRetry(const mesh::Packet*, const uint8_t*, uint8_t) const;
};
@METHODS@
int main() {
MyMesh node;
mesh::Packet p;
// Admission is an exact opt-in, not a general forwarding bypass. Test every
// route/type and invalid stored values with the actual header accessors.
for (uint8_t pref : {uint8_t(0), uint8_t(1), uint8_t(2), uint8_t(255)}) {
node._prefs.trace_when_repeat_off = pref;
for (uint8_t route = 0; route < 4; ++route) {
for (uint8_t type = 0; type < 16; ++type) {
p.header = route | (type << PH_TYPE_SHIFT);
p.path_len = 0;
assert(node.allowPacketForward(&p) ==
(pref == 1 && p.isRouteDirect() && type == PAYLOAD_TYPE_TRACE));
for (uint16_t path : {uint16_t(0), uint16_t(1), uint16_t(MAX_PATH_SIZE)}) {
p.path_len = path;
const bool blocked = pref != 1 && p.isRouteDirect()
&& type == PAYLOAD_TYPE_TRACE && path > 0;
const unsigned before = node.base_calls;
assert(node.allowPacketTransmit(&p) == !blocked);
assert(node.base_calls == before + (blocked ? 0 : 1));
assert(node.allowDirectRetry(&p, nullptr, 0) == !blocked);
}
}
}
}
assert(!node.allowPacketForward(nullptr));
assert(node.allowPacketTransmit(nullptr));
assert(node.allowDirectRetry(nullptr, nullptr, 0));
// repeat on restores the original gate; switching it off retires a relay,
// but never a locally generated trace whose SNR path starts empty.
p.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_TRACE << PH_TYPE_SHIFT);
p.path_len = 1;
node._prefs.trace_when_repeat_off = 0;
node._prefs.disable_fwd = 0;
assert(node.allowPacketForward(&p) && node.allowPacketTransmit(&p));
node._prefs.disable_fwd = 1;
assert(!node.allowPacketForward(&p) && !node.allowPacketTransmit(&p));
node._prefs.trace_when_repeat_off = 1;
assert(node.allowPacketForward(&p) && node.allowPacketTransmit(&p));
node._prefs.trace_when_repeat_off = 0;
assert(!node.allowPacketTransmit(&p) && !node.allowDirectRetry(&p, nullptr, 0));
p.path_len = 0;
assert(node.allowPacketTransmit(&p) && node.allowDirectRetry(&p, nullptr, 0));
// An enabled opt-in still delegates the base hard-drain/profile safety veto.
p.path_len = 1;
node._prefs.trace_when_repeat_off = 1;
node.base_allowed = false;
assert(!node.allowPacketTransmit(&p));
node.base_allowed = true;
node._prefs.direct_retry_enabled = 0;
assert(!node.allowDirectRetry(&p, nullptr, 0));
node._prefs.direct_retry_enabled = 1;
node._prefs.direct_retry_recent_enabled = 1;
const uint8_t hop[] = {0x42};
assert(node.allowDirectRetry(&p, hop, 1)); // unknown remains eligible
node.tables.known = true;
node.tables.recent.snr_x4 = 11;
assert(!node.allowDirectRetry(&p, hop, 1));
node.tables.recent.snr_x4 = 12;
assert(node.allowDirectRetry(&p, hop, 1));
node._prefs.direct_retry_snr_margin_x4 = 1;
assert(!node.allowDirectRetry(&p, hop, 1));
// The opt-in never overrides existing flood-hop/region/scope/loop rules,
// or spends moderation quota for a packet rejected earlier in the chain.
node._prefs.disable_fwd = 0;
p.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_TXT << PH_TYPE_SHIFT);
p.setPathHashSizeAndCount(1, 1);
assert(node.allowPacketForward(&p));
const unsigned committed = node.commits;
node._prefs.flood_max = 0;
assert(!node.allowPacketForward(&p));
node._prefs.flood_max = 64;
node.recv_pkt_channel_scope_rejected = true;
assert(!node.allowPacketForward(&p));
node.recv_pkt_channel_scope_rejected = false;
node.recv_pkt_region = nullptr;
assert(!node.allowPacketForward(&p));
node.recv_pkt_region = &node;
node._prefs.loop_detect = LOOP_DETECT_MINIMAL;
node.looped = true;
assert(!node.allowPacketForward(&p));
node.looped = false;
#if !defined(PORTABLE_MQTT_OBSERVER)
node.filter_block = true;
assert(!node.allowPacketForward(&p));
node.filter_block = false;
node.moderation_block = true;
assert(!node.allowPacketForward(&p));
node.moderation_block = false;
assert(node.commits == committed);
#else
(void)committed;
#endif
assert(node.allowPacketForward(&p));
node._prefs.disable_fwd = 1;
assert(!node.allowPacketForward(&p));
}
'''
class RepeaterTraceForwardingTest(unittest.TestCase):
def test_actual_repeater_methods(self):
compiler = os.environ.get('CXX') or shutil.which('g++') or shutil.which('clang++')
if not compiler:
self.skipTest('a host C++17 compiler is required')
source = (ROOT / 'examples/simple_repeater/MyMesh.cpp').read_text(encoding='utf-8')
methods = '\n'.join(extract_braced(source, signature) for signature in (
'bool MyMesh::allowPacketForward(',
'bool MyMesh::allowPacketTransmit(',
'bool MyMesh::allowDirectRetry('))
packet_source = (ROOT / 'src/Packet.cpp').read_text(encoding='utf-8')
code = HARNESS.replace('@METHODS@', methods).replace(
'@CONSTRUCTOR@', extract_braced(packet_source, 'Packet::Packet()'))
flags = ['-fsanitize=address,undefined', '-fno-sanitize-recover=all',
'-fno-pie', '-no-pie'] if sys.platform.startswith('linux') else []
with tempfile.TemporaryDirectory(prefix='repeater-trace-forwarding-') as directory:
work = Path(directory)
cpp = work / 'test.cpp'
cpp.write_text(code, encoding='utf-8')
for portable in (False, True):
with self.subTest(portable=portable):
binary = work / 'test'
built = subprocess.run([
compiler, '-std=c++17', '-Wall', '-Wextra', '-Werror',
'-Wno-unused-parameter',
'-DMESH_ENABLE_FLOOD_GROUP_MODERATION=1', '-DMESH_ENABLE_CLOCK_SYNC=1',
*(['-DPORTABLE_MQTT_OBSERVER=1'] if portable else []), *flags,
'-I' + str(ROOT / 'src'), str(cpp), '-o', str(binary)],
capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
tested = subprocess.run([str(binary)], capture_output=True,
text=True, timeout=10)
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
def test_repeater_only_override(self):
header = (ROOT / 'examples/simple_repeater/MyMesh.h').read_text(encoding='utf-8')
self.assertIn('bool allowPacketTransmit(const mesh::Packet* packet) const override;', header)
for role in ('simple_room_server', 'simple_sensor', 'companion_radio'):
for path in (ROOT / 'examples' / role).glob('MyMesh.*'):
self.assertNotIn('trace_when_repeat_off', path.read_text(encoding='utf-8'))
if __name__ == '__main__':
unittest.main()
+244
View File
@@ -0,0 +1,244 @@
"""Execute the real repeater trace CLI and appended preference persistence."""
from pathlib import Path
import os
import re
import subprocess
import tempfile
import unittest
from test_common_usb_debug import HARNESS as COMMON_HARNESS
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
TRACE_MAIN = r'''
int main() {
CommonCLI cli;
char reply[160] = {};
assert(cli.prefs.trace_when_repeat_off == 0);
cli.prefs.usb_debug_enabled = 1;
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
assert(cli._common_save_succeeded);
const auto baseline = cli.fs.files["/com_prefs"];
assert(baseline.size() == 871);
const size_t trace_offset = baseline.size() - 1;
const size_t debug_offset = trace_offset - 1;
assert(baseline[trace_offset] == 0 && baseline[debug_offset] == 1);
Capture capture;
assert(writeCommonPrefsImage(capture, &cli.prefs));
assert(capture.bytes == baseline);
cli.get("repeat.trace", reply);
assert(!strcmp(reply, "> off"));
cli.set("repeat.trace on", reply);
assert(!strcmp(reply, "OK - repeat.trace on (saved)"));
assert(cli.prefs.trace_when_repeat_off == 1);
const auto enabled = cli.fs.files["/com_prefs"];
assert(enabled.size() == baseline.size() && enabled[trace_offset] == 1);
assert(std::equal(baseline.begin(), baseline.begin() + trace_offset, enabled.begin()));
Capture enabled_capture;
assert(writeCommonPrefsImage(enabled_capture, &cli.prefs));
assert(enabled_capture.bytes == enabled);
cli.get("repeat.trace", reply);
assert(!strcmp(reply, "> on"));
for (const char* command : {"repeat.trace", "repeat.trace ", "repeat.trace\t",
"repeat.trace true", "repeat.trace 1", "repeat.trace ON", "repeat.trace Off",
"repeat.trace off junk", "repeat.trace off ", "repeat.trace off\t",
"repeat.trace on reboot", "repeat.trace off reboot", "repeat.trace on\n"}) {
const auto before = cli.fs.files["/com_prefs"];
cli.set(command, reply);
assert(!strcmp(reply, "Error: usage set repeat.trace on|off"));
assert(cli.prefs.trace_when_repeat_off == 1 && cli.fs.files["/com_prefs"] == before);
}
for (const char* query : {"repeat.trace ", "repeat.trace\t", "repeat.trace on"}) {
cli.get(query, reply);
assert(!strcmp(reply, "Error: usage get repeat.trace"));
}
for (const char* collision : {"repeat.traces", "repeat.trace_off",
"repeat.tracex on", "repeat.traceback", "repeat", "repeat.trace/on"}) {
cli.set(collision, reply);
assert(!strcmp(reply, "unknown"));
cli.get(collision, reply);
assert(!strcmp(reply, "unknown"));
assert(cli.prefs.trace_when_repeat_off == 1 && cli.fs.files["/com_prefs"] == enabled);
}
cli.set("repeat.trace\t \toff", reply);
assert(!strcmp(reply, "OK - repeat.trace off (saved)"));
assert(cli.prefs.trace_when_repeat_off == 0);
// Both enable and disable must preserve the saved and requested state when
// staging, the new final-byte write, readback or publication fails.
for (uint8_t desired : {1, 0}) {
cli.set(desired ? "repeat.trace off" : "repeat.trace on", reply);
assert(cli.prefs.trace_when_repeat_off == uint8_t(!desired));
const auto committed = cli.fs.files["/com_prefs"];
for (int fault : {0, 1, 2, 3}) {
const unsigned callbacks_before = cli.callbacks.retry_updates;
if (fault == 0) cli.fs.fail_write = true;
if (fault == 1) cli.fs.fail_write_after = trace_offset;
if (fault == 2) cli.fs.fail_read_open = true;
if (fault == 3) cli.fs.fail_rename = 1;
cli.set(desired ? "repeat.trace on" : "repeat.trace off", reply);
assert(!strcmp(reply, "Error: Repeat trace not saved; unchanged"));
assert(cli.prefs.trace_when_repeat_off == uint8_t(!desired));
assert(cli.fs.files["/com_prefs"] == committed);
assert(cli.callbacks.retry_updates == callbacks_before);
cli.fs.fail_write = false; cli.fs.fail_write_after = -1;
cli.fs.fail_read_open = false; cli.fs.fail_rename = 0;
}
cli.set(desired ? "repeat.trace on" : "repeat.trace off", reply);
assert(cli.prefs.trace_when_repeat_off == desired);
Capture saved;
assert(writeCommonPrefsImage(saved, &cli.prefs));
assert(saved.bytes == cli.fs.files["/com_prefs"]);
}
// Sharing CommonCLI must not expose the setting on other firmware roles.
for (const char* role : {"room-server", "sensor", "companion", "observer", "Repeater", ""}) {
cli.callbacks.role = role;
const auto before = cli.fs.files["/com_prefs"];
const uint8_t previous = cli.prefs.trace_when_repeat_off;
cli.get("repeat.trace", reply);
assert(!strcmp(reply, "Error: repeat.trace is only supported by repeaters"));
for (const char* command : {"repeat.trace on", "repeat.trace off"}) {
cli.set(command, reply);
assert(!strcmp(reply, "Error: repeat.trace is only supported by repeaters"));
assert(cli.prefs.trace_when_repeat_off == previous && cli.fs.files["/com_prefs"] == before);
}
}
cli.callbacks.role = "repeater";
// Execute the production RepeatPrefs dynamic schema and committed writers.
RepeatPrefs dynamic(&cli.prefs);
for (uint8_t requested : {1, 0, 2, 255}) {
assert(dynamic.import("trace_off", requested));
assert(cli.prefs.trace_when_repeat_off == requested);
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
assert(cli._common_save_succeeded);
assert(cli.prefs.trace_when_repeat_off == (requested == 1 ? 1 : 0));
assert(cli.fs.files["/com_prefs"][trace_offset] == (requested == 1 ? 1 : 0));
Capture saved;
assert(writeCommonPrefsImage(saved, &cli.prefs));
assert(saved.bytes == cli.fs.files["/com_prefs"]);
}
// Every old or torn image lacks the complete appended preference and must
// reset prior RAM intent to OFF. The old debug byte retains its old offset.
auto image = enabled;
for (size_t size = 0; size < image.size(); ++size) {
CommonCLI reader;
reader.prefs.trace_when_repeat_off = 1;
reader.fs.files["/com_prefs"] = {image.begin(), image.begin() + size};
reader.loadPrefsInt(&reader.fs, "/com_prefs");
assert(reader.prefs.trace_when_repeat_off == 0);
if (size > debug_offset) assert(reader.prefs.usb_debug_enabled == 1);
}
for (uint8_t saved : {0, 1, 2, 255}) {
image[trace_offset] = saved;
CommonCLI reader;
reader.prefs.trace_when_repeat_off = 1;
reader.fs.files["/com_prefs"] = image;
reader.loadPrefsInt(&reader.fs, "/com_prefs");
assert(reader.prefs.trace_when_repeat_off == (saved == 1 ? 1 : 0));
assert(reader.prefs.usb_debug_enabled == 1);
assert(reader.prefs.freq == 910 && reader.prefs.primary_radio_preamble == 48);
}
cli.prefs.trace_when_repeat_off = 1;
cli.loadPrefsInt(&cli.fs, "/missing");
assert(cli.prefs.trace_when_repeat_off == 0);
}
'''
def production_harness(source, header):
"""Reuse the common USB fixture while executing real trace/store methods."""
fields = header.split('class NodePrefs :', 1)[1].split('private:', 1)[0]
fields = '\n'.join(re.findall(
r'^\s*(?:float|double|char|u?int(?:8|16|32)_t)\s+[^;]+;', fields, re.M))
constants = '\n'.join(re.findall(
r'^\s*#define\s+\w+\s+(?:0x[\dA-Fa-f]+|\d+)\s*$', header, re.M))
constants += '\n' + '\n'.join(re.findall(
r'^static const size_t LEGACY_\w+\s*=[^;]+;', source, re.M))
constants += '\n' + re.search(
r'^static constexpr uint8_t DEFAULT_ADVERT_LOC_POLICY\s*=[^;]+;',
header, re.M).group()
helpers = '\n'.join(extract_braced(source, signature) for signature in (
'static bool bwMatches(', 'static bool isValidLoRaBandwidth(',
'static float defaultLoRaBandwidth(', 'static void markDirectRetryPrefsValid(',
'static void applyFloodRetryPreset(', 'static void applyDirectRetryPreset(',
'static void setDefaultDirectRetryPrefs(', 'static bool directRetryPrefsValid('))
methods = '\n'.join(extract_braced(source, signature) for signature in (
'static const char* commonPrefsSaveResultName(',
'bool CommonCLI::recoverCommonPrefsFiles(',
'bool CommonCLI::saveCommonPrefsImageAtomically(',
'void CommonCLI::savePrefs(FILESYSTEM*',
'void CommonCLI::savePrefs(PrefsSaveRouting::Scope scope)',
'bool CommonCLI::trySavePrefs(', 'void CommonCLI::loadPrefsInt('))
setter = extract_braced(source, 'void CommonCLI::handleSetCmd(')
getter = extract_braced(source, 'void CommonCLI::handleGetCmd(')
branch = 'if (strncmp(config, "repeat.trace", 12)'
code = COMMON_HARNESS.split('int main() {', 1)[0].replace(
'void updateAdvertTimer() {}',
'void updateAdvertTimer() {}\n'
' const char* role = "repeater";\n'
' const char* getRole() const { return role; }\n'
' unsigned retry_updates = 0;\n'
' void onRetryConfigChanged() { ++retry_updates; }')
replacements = {
'FIELDS': fields, 'CONSTANTS': constants, 'HELPERS': helpers,
'LEGACY': extract_braced(header, 'struct LegacyObserverTail') + ';',
'SERIALIZER': 'template<typename Writer>\n' + extract_braced(
source, 'static bool writeCommonPrefsImage('),
'STORE': extract_braced(source, 'class CommonPrefsFileStore'),
'METHODS': methods,
'DYNAMIC': extract_braced(header, 'class RepeatPrefs :'),
'SET_DEBUG': extract_braced(setter, branch), 'SET_MASTER': '',
'GET_DEBUG': extract_braced(getter, branch), 'GET_MASTER': '',
}
for key, value in replacements.items():
code = code.replace('@' + key + '@', value)
return code + TRACE_MAIN
class RepeaterTraceRepeatTest(unittest.TestCase):
def test_actual_cli_schema_and_old_new_persistence(self):
source = (ROOT / 'src/helpers/CommonCLI.cpp').read_text()
header = (ROOT / 'src/helpers/CommonCLI.h').read_text()
code = production_harness(source, header)
sanitizer_flags = [] if os.name == 'nt' else [
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
'-fno-pie', '-no-pie']
variants = [('ENABLE_OTA', platform) for platform in (
'NRF52_PLATFORM', 'STM32_PLATFORM', 'ESP32_PLATFORM', 'RP2040_PLATFORM')]
variants += [('ENABLE_OTA', 'ESP32_PLATFORM', 'WITH_MQTT_BRIDGE'),
('NRF52_PLATFORM',), ('ESP32_PLATFORM', 'WITH_MQTT_BRIDGE')]
with tempfile.TemporaryDirectory(prefix='repeater-trace-') as directory:
work = Path(directory)
transaction = (ROOT / 'src/helpers/ContactFileTransaction.h').read_text()
(work / 'ContactFileTransaction.h').write_text(transaction.replace(
'#include "IdentityStore.h"', '#include <helpers/IdentityStore.h>'))
(work / 'test.cpp').write_text(code)
for macros in variants:
with self.subTest(macros=macros):
exe = work / ('test.exe' if os.name == 'nt' else 'test')
built = subprocess.run([
os.environ.get('CXX', 'g++'), '-std=c++17',
*['-D' + macro + '=1' for macro in macros], *sanitizer_flags,
'-I' + str(work), '-I' + str(ROOT / 'test/fixtures/radio_profiles/mocks'),
'-I' + str(ROOT / 'src'), '-I' + str(ROOT / 'src/helpers'),
str(work / 'test.cpp'), '-o', str(exe)],
capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stderr)
tested = subprocess.run([str(exe)], capture_output=True, text=True, timeout=10)
self.assertEqual(tested.returncode, 0, tested.stderr)
def test_outer_load_defaults_before_loading_saved_image(self):
source = (ROOT / 'src/helpers/CommonCLI.cpp').read_text()
load = extract_braced(source, 'void CommonCLI::loadPrefs(FILESYSTEM*')
self.assertIn('_prefs->trace_when_repeat_off = 0;', load)
self.assertLess(load.index('_prefs->trace_when_repeat_off = 0;'),
load.index('loadPrefsInt('))
if __name__ == '__main__':
unittest.main()
+154
View File
@@ -836,6 +836,160 @@ TEST(MeshReceiveHooks, FloodTraceAndControlAreNeverForwarded) {
}
}
// Admission/egress boundary controls deliberately do not reproduce the role
// predicate. The Python extraction suite executes the actual repeater methods;
// these tests exercise the real Mesh/Dispatcher validation and queue ownership.
class TraceForwardingPolicyMesh : public TraceTestMesh {
public:
using TraceTestMesh::TraceTestMesh;
const mesh::Packet* denied = nullptr;
unsigned forwarding_checks = 0;
bool allowPacketForward(const mesh::Packet*) override {
++forwarding_checks;
return forwardFloods;
}
bool allowPacketTransmit(const mesh::Packet* packet) const override {
return packet != denied && mesh::Mesh::allowPacketTransmit(packet);
}
};
static mesh::Packet makeIncomingTrace(const TraceTestMesh& node, uint8_t route,
uint8_t flags = 0, uint8_t hash_size = 1) {
mesh::Packet packet;
packet.header = route | (PAYLOAD_TYPE_TRACE << PH_TYPE_SHIFT);
packet.path_len = 0;
packet.payload_len = 9 + 2 * hash_size;
memset(packet.payload, 0, packet.payload_len);
packet.payload[8] = flags;
memcpy(packet.payload + 9, node.self_id.pub_key, hash_size);
memset(packet.payload + 9 + hash_size, 0x98, hash_size);
packet._snr = 17;
return packet;
}
TEST(TraceForwardingSafety, MatchingDirectHashesAppendOneSnrAndDuplicateIsRejected) {
for (uint8_t route : {ROUTE_TYPE_DIRECT, ROUTE_TYPE_TRANSPORT_DIRECT}) {
for (uint8_t code = 0; code < 4; ++code) {
TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio;
ForwardingTestTables tables; StaticPoolPacketManager manager(12);
TraceForwardingPolicyMesh node(radio, clock, rng, rtc, manager, tables);
node.forwardFloods = true;
memset(node.self_id.pub_key, 0x42, PUB_KEY_SIZE);
mesh::Packet packet = makeIncomingTrace(node, route, code, 1U << code);
mesh::Packet duplicate = packet;
EXPECT_NE(ACTION_RELEASE, node.receivePacket(&packet));
EXPECT_EQ(1, packet.path_len);
EXPECT_EQ(17, packet.path[0]);
EXPECT_EQ(1, tables.mark_seen_calls);
EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&duplicate));
EXPECT_EQ(0, duplicate.path_len);
EXPECT_EQ(1, tables.mark_seen_calls);
}
}
}
TEST(TraceForwardingSafety, MalformedWrongHashAndFullSnrPathNeverReachForwardingGate) {
TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio;
ForwardingTestTables tables; StaticPoolPacketManager manager(12);
TraceForwardingPolicyMesh node(radio, clock, rng, rtc, manager, tables);
node.forwardFloods = true;
memset(node.self_id.pub_key, 0x42, PUB_KEY_SIZE);
const mesh::Packet valid = makeIncomingTrace(node, ROUTE_TYPE_DIRECT);
for (uint8_t length = 0; length < 9; ++length) {
mesh::Packet packet = valid;
packet.payload_len = length;
EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&packet));
}
mesh::Packet wrong = valid;
wrong.payload[9] ^= 0xff;
EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&wrong));
mesh::Packet truncated = valid;
truncated.payload[8] = 3;
truncated.payload_len = 10; // neither an eight-byte nor four-byte hash fits
EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&truncated));
mesh::Packet full = valid;
full.path_len = MAX_PATH_SIZE;
EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&full));
mesh::Packet long_offset = valid;
long_offset.path_len = 33;
long_offset.payload[8] = 3;
long_offset.payload_len = 9 + 160;
memset(long_offset.payload + 9, 0x42, 160);
EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&long_offset)); // 33*8 must not wrap
EXPECT_EQ(0u, node.forwarding_checks);
EXPECT_EQ(0, tables.mark_seen_calls);
}
TEST(TraceForwardingSafety, FinalSnrSlotIsSafeAndDeniedAdmissionDoesNotMarkSeen) {
TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio;
ForwardingTestTables tables; StaticPoolPacketManager manager(12);
TraceForwardingPolicyMesh node(radio, clock, rng, rtc, manager, tables);
memset(node.self_id.pub_key, 0x42, PUB_KEY_SIZE);
mesh::Packet blocked = makeIncomingTrace(node, ROUTE_TYPE_DIRECT);
EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&blocked));
EXPECT_EQ(0, blocked.path_len);
EXPECT_EQ(0, tables.mark_seen_calls);
node.forwardFloods = true;
mesh::Packet last = blocked;
last.path_len = MAX_PATH_SIZE - 1;
last.payload_len = 9 + MAX_PATH_SIZE;
memset(last.payload + 9, 0x42, MAX_PATH_SIZE);
EXPECT_NE(ACTION_RELEASE, node.receivePacket(&last));
EXPECT_EQ(MAX_PATH_SIZE, last.path_len);
EXPECT_EQ(17, last.path[MAX_PATH_SIZE - 1]);
EXPECT_EQ(1, tables.mark_seen_calls);
}
TEST(TraceForwardingSafety, EgressVetoRetiresQueuedRelayAndItsRetryOwner) {
TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio;
ForwardingTestTables tables; StaticPoolPacketManager manager(12);
TraceForwardingPolicyMesh node(radio, clock, rng, rtc, manager, tables);
node.begin();
node.forwardFloods = true;
memset(node.self_id.pub_key, 0x42, PUB_KEY_SIZE);
auto* relay = manager.allocNew(); ASSERT_NE(nullptr, relay);
*relay = makeIncomingTrace(node, ROUTE_TYPE_DIRECT);
ASSERT_NE(ACTION_RELEASE, node.receivePacket(relay));
ASSERT_TRUE(node.sendPacket(relay, 1));
node.denied = relay;
clock.now = 1000; node.loop();
EXPECT_FALSE(radio.sending);
EXPECT_EQ(0, manager.getOutboundTotal());
node.denied = nullptr;
clock.now = 10000; node.loop();
EXPECT_FALSE(radio.sending);
EXPECT_EQ(0, manager.getOutboundTotal());
EXPECT_EQ(12, manager.getFreeCount());
}
TEST(TraceForwardingSafety, AirtimeAlreadyStartedFinishesButQueuedRetryIsRetired) {
TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio;
ForwardingTestTables tables; StaticPoolPacketManager manager(12);
TraceForwardingPolicyMesh node(radio, clock, rng, rtc, manager, tables);
node.begin();
node.forwardFloods = true;
memset(node.self_id.pub_key, 0x42, PUB_KEY_SIZE);
auto* relay = manager.allocNew(); ASSERT_NE(nullptr, relay);
*relay = makeIncomingTrace(node, ROUTE_TYPE_DIRECT);
ASSERT_NE(ACTION_RELEASE, node.receivePacket(relay));
ASSERT_TRUE(node.sendPacket(relay, 1));
clock.now = 1; node.loop(); ASSERT_TRUE(radio.sending);
node.denied = relay;
clock.now = 2; node.loop(); EXPECT_TRUE(radio.sending);
radio.complete = true;
clock.now = 3; node.loop(); EXPECT_FALSE(radio.sending);
ASSERT_EQ(1, manager.getOutboundTotal());
node.denied = manager.getOutboundByIdx(0);
clock.now = 1000; node.loop();
EXPECT_FALSE(radio.sending);
EXPECT_EQ(0, manager.getOutboundTotal());
node.denied = nullptr;
clock.now = 10000; node.loop();
EXPECT_FALSE(radio.sending);
EXPECT_EQ(0, manager.getOutboundTotal());
EXPECT_EQ(12, manager.getFreeCount());
}
static mesh::Packet* makeTrace(TraceTestMesh& node, uint32_t tag, uint32_t auth,
const uint8_t* route, uint8_t route_len) {
mesh::Packet* packet = node.createTrace(tag, auth, 0);