Add per-profile TX routing and bridge filtering

Persist Companion TX preferences per contact and channel. Default repeater,
room, and sensor replies to both active TX profiles, with a reply-only force
option for an RX-only secondary profile. Track both reply copies before a
temporary-radio handoff and account for OTA copies under queue backpressure.

Add bridge/crossover filter modes and the third built-in wardriving filter,
plus compact filter CLI syntax, documentation, and CI coverage.

Fix quoted target names being interpreted as slot/key selectors, partial
recovery of invalid saved reply settings, filter suspension during tempradio2,
and unnecessary packet allocation while OTA traffic is throttled.

Validation: 1,477 native tests, 52 Python integration tests, 29 filter UI tests,
and five firmware builds covering ESP32, nRF52, and STM32 passed.
This commit is contained in:
mikecarper
2026-09-15 01:58:43 -07:00
parent 0e0aa8e014
commit 95aca52b4e
60 changed files with 3188 additions and 362 deletions
+28 -2
View File
@@ -142,6 +142,7 @@ void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) {
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->shared_secret, reply_data, len);
bool sent = false;
if (reply) {
reply->radio_reply = true; // asynchronous response to the room subscription
if (client->out_path_len == OUT_PATH_UNKNOWN) {
unsigned long delay_millis = 0;
sent = sendFloodScoped(default_scope, reply, delay_millis,
@@ -278,6 +279,29 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
return 0; // unknown command
}
#if MESH_ENABLE_ROOM_FLOOD_RULE_ENGINE
bool MyMesh::allowTransportPacket(const mesh::Packet* packet, uint8_t context) {
if (!packet) return false;
RegionEntry* incoming_region = NULL;
bool scoped = packet->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD
|| packet->getRouteType() == ROUTE_TYPE_TRANSPORT_DIRECT;
bool incoming_allowed;
if (scoped) {
incoming_region = region_map.findMatch(packet, REGION_DENY_FLOOD);
incoming_allowed = incoming_region != NULL;
} else {
incoming_allowed = (region_map.getWildcard().flags & REGION_DENY_FLOOD) == 0;
}
// Use a local mask: bridge RX/TX callbacks can run inside mesh dispatch,
// where recv_pkt_* still belongs to the ordinary radio forwarding phase.
uint32_t matches = flood_rules.evaluate(packet, isAnyTempRadioActive(),
incoming_allowed, incoming_region, context);
if (flood_rules.shouldBlock(packet, matches, _ms->getMillis())) return false;
flood_rules.commitRates(packet, matches, _ms->getMillis());
return true;
}
#endif
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
#if MESH_PACKET_LOGGING
if (mesh::isUsbLoggingEnabled()) {
@@ -411,7 +435,7 @@ bool MyMesh::evaluateFloodRuleTiming(const mesh::Packet* packet,
mesh::Packet candidate = *packet;
uint32_t match_mask = flood_rules.evaluate(
packet, isTempRadioActive(), incoming_region_allowed, incoming_region);
packet, isAnyTempRadioActive(), incoming_region_allowed, incoming_region);
bool scope_set = false;
return flood_rules.applyScope(&candidate, match_mask, scope_set,
fast_track, false);
@@ -607,7 +631,7 @@ mesh::DispatcherAction MyMesh::onRecvPacket(mesh::Packet* pkt) {
(region_map.getWildcard().flags & REGION_DENY_FLOOD) == 0;
}
recv_pkt_rule_match_mask = flood_rules.evaluate(
pkt, isTempRadioActive(), incoming_region_allowed, incoming_region);
pkt, isAnyTempRadioActive(), incoming_region_allowed, incoming_region);
if (flood_rules.hasRetryRules()) {
pkt->flood_retry_policy = flood_rules.allowsRetry(
recv_pkt_rule_match_mask)
@@ -1410,6 +1434,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
// Set stats sources for automatic stats collection
bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
configureBridgeFilter(bridge);
bridge->begin();
}
}
@@ -2161,6 +2186,7 @@ void MyMesh::onUserGpioTimerCompleted(uint8_t pin, uint8_t state,
client->shared_secret, data,
5 + (size_t)text_len);
if (!packet) return;
packet->radio_reply = true; // delayed GPIO command completion
if (client->out_path_len == OUT_PATH_UNKNOWN) {
sendFlood(packet, SERVER_RESPONSE_DELAY, path_hash_size);
} else {