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 committed 2026-09-15 01:58:43 -07:00
1 parent e9a2326c4f
commit a07f39c6dd
60 files changed
+3188 -362

No files matched your search

+58 -8
View File
@@ -123,7 +123,8 @@ void Dispatcher::restoreOutboundTxOverrides() {
bool Dispatcher::startOutboundTransmit() {
if (outbound == NULL) return false;
if (!isPacketRadioCurrent(outbound)
|| _radio->prepareTransmitProfile(outbound->radio_profile) != RadioParamApplyResult::APPLIED) return false;
|| _radio->prepareTransmitProfile(outbound->radio_profile,
outbound->radio_reply && outbound->radio_reply_force) != RadioParamApplyResult::APPLIED) return false;
int len = 0;
uint8_t raw[MAX_TRANS_UNIT];
@@ -354,7 +355,8 @@ void Dispatcher::loop() {
// keep the retry waiting forever on an unread RxDone interrupt.
if (isDualRadioActive()) checkRecv();
if (!millisHasNowPassed(outbound_radio_retry_at)) return;
const auto prepared = _radio->prepareTransmitProfile(outbound->radio_profile);
const auto prepared = _radio->prepareTransmitProfile(outbound->radio_profile,
outbound->radio_reply && outbound->radio_reply_force);
if (prepared == RadioParamApplyResult::BUSY) {
outbound_radio_retry_at = futureMillis(10);
return;
@@ -544,6 +546,8 @@ bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) {
pkt->radio_profile = pkt->radio_origin = _radio->receiveProfile();
pkt->radio_bound = false;
pkt->radio_local = false;
pkt->tx_radio = RADIO_TX_AUTO;
pkt->radio_reply = pkt->radio_reply_force = false;
pkt->radio_generation = pkt->radio_origin_generation = _radio->receiveProfileGeneration();
pkt->header = raw[i++];
if (pkt->getPayloadVer() > PAYLOAD_VER_1) {
@@ -737,7 +741,8 @@ void Dispatcher::checkSend() {
// Waiting for airtime credit must leave the receive scanner free to run.
// Only retune once this queue entry is actually eligible to transmit.
if (pending && _radio->profiles()) {
const auto prepared = _radio->prepareTransmitProfile(pending->radio_profile);
const auto prepared = _radio->prepareTransmitProfile(pending->radio_profile,
pending->radio_reply && pending->radio_reply_force);
if (prepared == RadioParamApplyResult::BUSY) return;
if (prepared == RadioParamApplyResult::FAILED) {
outbound = _mgr->getNextOutbound(now);
@@ -822,6 +827,9 @@ Packet* Dispatcher::obtainNewPacket() {
pkt->radio_generation = pkt->radio_origin_generation = receive_context_active ? receive_context_generation : 0;
pkt->radio_bound = false;
pkt->radio_local = !receive_context_active;
pkt->tx_radio = RADIO_TX_AUTO;
pkt->radio_reply = receive_context_active;
pkt->radio_reply_force = false;
}
return pkt;
}
@@ -841,21 +849,37 @@ bool Dispatcher::queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t
}
auto* profiles = _radio->profiles();
if (packet->radio_profile > 1) return false;
if (profiles == nullptr || packet->radio_bound) {
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));
}
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));
}
if (!packet->radio_local && packet->radio_generation
&& packet->radio_generation != profiles->generation[packet->radio_profile]) return false;
if (packet->radio_reply && packet->tx_radio == RADIO_TX_AUTO) {
packet->tx_radio = profiles->reply_tx;
packet->radio_reply_force = profiles->reply_force;
}
// Locally generated OTA traffic uses the temporary update profile. Replies
// created while processing RX inherit that request's profile instead.
// Keep that origin in RX-only mode too: isolation must drop a disallowed
// transmission instead of silently sending it on the normal channel.
if (packet->radio_local && packet->getPayloadType() == PAYLOAD_TYPE_OTA
if (packet->tx_radio == RADIO_TX_AUTO && packet->radio_local && packet->getPayloadType() == PAYLOAD_TYPE_OTA
&& profiles->secondary_temporary && !profiles->primary_temporary) {
packet->radio_profile = 1;
}
const uint8_t origin = packet->radio_profile;
const uint8_t mask = profiles->transmitMask(origin);
uint8_t mask = getTransmitProfileMask(packet);
const uint8_t cross_mask = (uint8_t)(1U << (origin ^ 1));
if ((mask & cross_mask) && !allowRadioProfileCross(packet)) {
mask &= (uint8_t)~cross_mask;
}
if (!mask) return false;
packet->radio_origin = origin;
packet->radio_origin_generation = profiles->generation[origin];
@@ -886,15 +910,41 @@ bool Dispatcher::queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t
return true;
}
uint8_t Dispatcher::getTransmitProfileMask(const Packet* packet) const {
const auto* profiles = _radio->profiles();
if (!profiles) return packet->tx_radio == RADIO_TX_AUTO ? 1 : explicitRadioTxMask(packet->tx_radio, false);
if (packet->radio_profile > 1) return 0;
const bool reply = packet->radio_reply && !packet->radio_bound && packet->tx_radio == RADIO_TX_AUTO;
const uint8_t policy = reply ? profiles->reply_tx : packet->tx_radio;
const bool force = packet->radio_reply && (reply ? profiles->reply_force : packet->radio_reply_force);
uint8_t origin = packet->radio_profile;
if (policy == RADIO_TX_AUTO && packet->radio_local && packet->getPayloadType() == PAYLOAD_TYPE_OTA
&& profiles->secondary_temporary && !profiles->primary_temporary) origin = 1;
return policy == RADIO_TX_AUTO ? profiles->transmitMask(origin)
: explicitRadioTxMask(policy, profiles->canTransmit(1, force));
}
uint32_t Dispatcher::getTransmitAirtime(const Packet* packet) const {
const uint8_t mask = getTransmitProfileMask(packet);
uint32_t total = 0;
for (uint8_t profile = 0; profile < 2; ++profile) {
if (mask & (1U << profile))
total += _radio->getProfileAirtime(profile, packet->getRawLength(), packet->tx_cr);
}
return total;
}
bool Dispatcher::isPacketRadioCurrent(const Packet* packet) const {
const auto* p = _radio->profiles();
if (!p || !packet->radio_bound) return true;
const uint8_t target = packet->radio_profile;
const uint8_t origin = packet->radio_origin;
return target < 2 && origin < 2 && p->canTransmit(target)
const bool force = packet->radio_reply && packet->radio_reply_force;
return target < 2 && origin < 2 && p->canTransmit(target, force)
&& packet->radio_generation == p->generation[target]
&& packet->radio_origin_generation == p->generation[origin]
&& (origin == target || p->canCross());
&& (packet->tx_radio == RADIO_TX_AUTO ? (origin == target || p->canCross())
: (explicitRadioTxMask(packet->tx_radio, p->canTransmit(1, force)) & (1U << target)) != 0);
}
bool Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) {
+10
View File
@@ -52,6 +52,10 @@ public:
virtual RadioParamApplyResult prepareTransmitProfile(uint8_t profile) {
return profile == 0 ? RadioParamApplyResult::APPLIED : RadioParamApplyResult::FAILED;
}
virtual RadioParamApplyResult prepareTransmitProfile(uint8_t profile, bool reply_rx_override) {
(void)reply_rx_override;
return prepareTransmitProfile(profile);
}
virtual uint16_t profilePreamble(uint8_t) const { return 0; }
virtual uint32_t getProfileAirtime(uint8_t, int len_bytes, uint8_t = 0) {
return getEstAirtimeFor(len_bytes);
@@ -379,6 +383,9 @@ protected:
virtual void onSendComplete(Packet* packet) { }
virtual void onSendFail(Packet* packet) { }
virtual void onRadioProfileCopyQueued(Packet* packet, const Packet* original, uint8_t priority) { }
// Policy gate for the other profile only; same-profile TX remains eligible.
// Called before admitting copies (including bound retry packets) to a queue.
virtual bool allowRadioProfileCross(const Packet* packet) { return true; }
virtual const char* getLogDateTime() { return ""; }
virtual float getAirtimeBudgetFactor() const;
@@ -421,6 +428,9 @@ protected:
void setRadioAvailable(bool available);
bool isRadioAvailable() const { return radio_available; }
bool isPacketRadioCurrent(const Packet* packet) const;
// Planned fan-out before transport filters; also bounds OTA queue admission.
uint8_t getTransmitProfileMask(const Packet* packet) const;
uint32_t getTransmitAirtime(const Packet* packet) const;
public:
void begin();
+26 -6
View File
@@ -192,13 +192,26 @@ static int queuedPacedOtaResponses(PacketManager* manager) {
bool Mesh::otaSendAdapter(void* ctx, const uint8_t* msg, uint16_t len, bool /*flood*/) {
Mesh* m = (Mesh*)ctx;
if (!m->isAnyTempRadioActive()) return false;
if (isPacedOtaResponse(msg, len)
&& (queuedPacedOtaResponses(m->_mgr) >= OTA_EGRESS_QUEUE_CREDIT
|| m->_mgr->getFreeCount() <= OTA_EGRESS_MIN_FREE)) {
const bool paced = isPacedOtaResponse(msg, len);
const int queued = paced ? queuedPacedOtaResponses(m->_mgr) : 0;
// Routine OTA backpressure must not attempt an allocation, which would
// otherwise report a packet-pool error when the receive reserve is in use.
if (paced && (queued >= OTA_EGRESS_QUEUE_CREDIT
|| m->_mgr->getFreeCount() <= OTA_EGRESS_MIN_FREE)) return false;
Packet* p = m->createOtaPacket(msg, len);
if (!p) return false;
p->radio_reply = len && ota::ota_is_response_message(msg[0]);
const uint8_t mask = m->getTransmitProfileMask(p);
const int copies = (mask & 1 ? 1 : 0) + (mask & 2 ? 1 : 0);
// p already occupies one slot. Reserve the other copy as well, keeping the
// receive reserve and OTA credit intact when replies use both profiles.
if (!copies || (paced
&& (queued + copies > OTA_EGRESS_QUEUE_CREDIT
|| m->_mgr->getFreeCount() < OTA_EGRESS_MIN_FREE + copies - 1))) {
m->releasePacket(p);
return false;
}
Packet* p = m->createOtaPacket(msg, len);
return p && m->sendOtaFlood(p);
return m->sendOtaFlood(p);
}
// Runtime OTA flood reach (`ota config hops`, persisted in NodePrefs): accept packets up to N hops away and
@@ -1315,6 +1328,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
delay_millis += getDirectRetransmitDelay(packet) + 300;
auto a1 = createMultiAck(packet->payload, packet->payload_len, extra);
if (a1) {
a1->radio_reply = false; // relayed ACK, not a local response
a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len);
a1->header &= ~PH_ROUTE_MASK;
a1->header |= ROUTE_TYPE_DIRECT;
@@ -1326,6 +1340,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
auto a2 = createAck(packet->payload, packet->payload_len);
if (a2) {
a2->radio_reply = false; // relayed ACK, not a local response
a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len);
a2->header &= ~PH_ROUTE_MASK;
a2->header |= ROUTE_TYPE_DIRECT;
@@ -2640,7 +2655,9 @@ Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, siz
Packet* Mesh::createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len) {
uint8_t dest_hash[PATH_HASH_SIZE];
dest.copyHashTo(dest_hash);
return createPathReturn(dest_hash, secret, path, path_len, extra_type, extra, extra_len);
Packet* packet = createPathReturn(dest_hash, secret, path, path_len, extra_type, extra, extra_len);
if (packet) packet->tx_radio = getContactTxRadio(dest);
return packet;
}
Packet* Mesh::createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len) {
@@ -2696,6 +2713,7 @@ Packet* Mesh::createDatagram(uint8_t type, const Identity& dest, const uint8_t*
return NULL;
}
packet->header = (type << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
packet->tx_radio = getContactTxRadio(dest);
int len = 0;
len += dest.copyHashTo(&packet->payload[len]); // dest hash
@@ -2720,6 +2738,7 @@ Packet* Mesh::createAnonDatagram(uint8_t type, const LocalIdentity& sender, cons
return NULL;
}
packet->header = (type << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
packet->tx_radio = getContactTxRadio(dest);
int len = 0;
if (type == PAYLOAD_TYPE_ANON_REQ) {
@@ -2745,6 +2764,7 @@ Packet* Mesh::createGroupDatagram(uint8_t type, const GroupChannel& channel, con
return NULL;
}
packet->header = (type << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
packet->tx_radio = channel.tx_radio;
int len = 0;
memcpy(&packet->payload[len], channel.hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE;
+4
View File
@@ -60,6 +60,7 @@ class GroupChannel {
public:
uint8_t hash[PATH_HASH_SIZE];
uint8_t secret[PUB_KEY_SIZE];
uint8_t tx_radio = RADIO_TX_AUTO; // local setting, not part of the channel key
};
/**
@@ -396,6 +397,9 @@ protected:
* \brief Called exactly once whenever an active flood-retry slot is released.
*/
virtual void onFloodRetrySlotReleased(const uint8_t* retry_key, uint8_t radio_profile) { }
// Resolve local TX routing while the complete peer identity is available.
// Packet destination hashes are too short to identify a contact safely.
virtual uint8_t getContactTxRadio(const Identity& peer) const { return RADIO_TX_AUTO; }
/**
* \returns number of extra (Direct) ACK transmissions wanted.
+4
View File
@@ -15,6 +15,8 @@ Packet::Packet() {
radio_generation = radio_origin_generation = 0;
radio_profile = radio_origin = 0;
radio_bound = false; radio_local = true;
tx_radio = RADIO_TX_AUTO;
radio_reply = radio_reply_force = false;
}
bool Packet::isValidPathLen(uint8_t path_len) {
@@ -81,6 +83,8 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) {
radio_profile = radio_origin = 0;
radio_bound = false; radio_local = true;
header = src[i++];
tx_radio = RADIO_TX_AUTO;
radio_reply = radio_reply_force = false;
if (getPayloadVer() > PAYLOAD_VER_1) return false;
if (hasTransportCodes()) {
+4
View File
@@ -1,6 +1,7 @@
#pragma once
#include <MeshCore.h>
#include <RadioTxPolicy.h>
namespace mesh {
@@ -65,6 +66,9 @@ public:
uint8_t radio_origin = 0;
bool radio_bound = false; // retries retain one profile and never fan out again
bool radio_local = true;
uint8_t tx_radio = RADIO_TX_AUTO; // local policy, never sent over the air
bool radio_reply = false; // locally generated response, never a forwarded packet
bool radio_reply_force = false; // captured reply-only RX-mode override
/**
* \brief calculate the hash of payload + type
+8 -2
View File
@@ -3,6 +3,7 @@
#include <stdint.h>
#include <stddef.h>
#include <math.h>
#include "RadioTxPolicy.h"
namespace mesh {
@@ -62,6 +63,10 @@ class RadioProfiles {
bool primary_temporary = false;
bool secondary_temporary = false;
RadioCrossMode cross = RadioCrossMode::Auto;
// Infrastructure opts into BOTH at startup; Companions keep their own
// contact/channel policy. Force applies only to locally generated replies.
uint8_t reply_tx = RADIO_TX_AUTO;
bool reply_force = false;
uint32_t generation[2] = {1, 1};
uint32_t switches = 0;
uint32_t rx_packets[2] = {};
@@ -70,8 +75,9 @@ class RadioProfiles {
uint32_t longest_switch_us = 0;
bool enabled() const { return secondary.mode != RadioProfileMode::Off; }
bool canTransmit(uint8_t profile) const {
return profile == 0 || (profile == 1 && secondary.mode == RadioProfileMode::RxTx);
bool canTransmit(uint8_t profile, bool reply_rx_override = false) const {
return profile == 0 || (profile == 1 && (secondary.mode == RadioProfileMode::RxTx
|| (reply_rx_override && secondary.mode == RadioProfileMode::Rx)));
}
bool canCross() const {
return cross == RadioCrossMode::On || (cross == RadioCrossMode::Auto
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <stdint.h>
#include <string.h>
namespace mesh {
// Local outbound choice. AUTO follows the radio2.cross policy; explicit masks
// choose profiles independently of cross. Mode permissions are enforced
// separately, including the infrastructure reply-only RX override.
enum RadioTxPolicy : uint8_t {
RADIO_TX_AUTO = 0,
RADIO_TX_PRIMARY = 1,
RADIO_TX_SECONDARY = 2,
RADIO_TX_BOTH = 3,
RADIO_TX_OFF = 4,
};
inline const char* radioTxPolicyName(uint8_t policy) {
switch (policy) {
case RADIO_TX_PRIMARY: return "radio";
case RADIO_TX_SECONDARY: return "radio2";
case RADIO_TX_BOTH: return "both";
case RADIO_TX_OFF: return "off";
default: return "auto";
}
}
inline bool parseRadioTxPolicy(const char* text, uint8_t& policy) {
for (uint8_t value = RADIO_TX_AUTO; value <= RADIO_TX_OFF; ++value) {
if (!strcmp(text, radioTxPolicyName(value))) { policy = value; return true; }
}
return false;
}
// Use a tagged value in previously reserved storage bytes. Zero and historical
// unrecognised bytes retain the old automatic behavior; record sizes stay fixed.
inline uint8_t encodeRadioTxPolicy(uint8_t policy) {
return policy >= RADIO_TX_PRIMARY && policy <= RADIO_TX_OFF ? 0xa0 | policy : 0;
}
inline uint8_t decodeRadioTxPolicy(uint8_t stored) {
return stored >= 0xa1 && stored <= 0xa4 ? stored & 7 : RADIO_TX_AUTO;
}
inline uint8_t explicitRadioTxMask(uint8_t policy, bool secondary_tx) {
return policy >= RADIO_TX_PRIMARY && policy <= RADIO_TX_BOTH
? policy & (secondary_tx ? 3 : 1) : 0;
}
} // namespace mesh
+17
View File
@@ -3,9 +3,26 @@
#include <Mesh.h>
class AbstractBridge {
using PacketFilter = bool (*)(void*, const mesh::Packet*);
PacketFilter _packet_filter = nullptr;
void* _packet_filter_context = nullptr;
protected:
// Called on the mesh loop, before copying/marking/queuing a packet. Never
// invoke the policy from a WiFi callback or the MQTT publishing task.
bool allowsPacket(const mesh::Packet* packet) const {
return packet && (!_packet_filter
|| _packet_filter(_packet_filter_context, packet));
}
public:
virtual ~AbstractBridge() {}
void setPacketFilter(PacketFilter filter, void* context) {
_packet_filter = filter;
_packet_filter_context = context;
}
/**
* @brief Initializes the bridge.
*/
+32 -19
View File
@@ -84,20 +84,36 @@ mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, doubl
return createAdvert(self_id, app_data, app_data_len);
}
uint8_t BaseChatMesh::getContactTxRadio(const mesh::Identity& peer) const {
for (int i = MAX_ANON_CONTACTS; i < num_contacts; ++i) {
if (peer.matches(contacts[i].id)) return contacts[i].tx_radio;
}
return mesh::RADIO_TX_AUTO;
}
void BaseChatMesh::sendAckTo(const ContactInfo& dest, const uint8_t* ack_hash, uint8_t ack_len) {
if (dest.out_path_len == OUT_PATH_UNKNOWN) {
mesh::Packet* ack = createAck(ack_hash, ack_len);
if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY);
if (ack) {
ack->tx_radio = getContactTxRadio(dest.id);
sendFloodScoped(dest, ack, TXT_ACK_DELAY);
}
} else {
uint32_t d = TXT_ACK_DELAY;
if (getExtraAckTransmitCount() > 0) {
mesh::Packet* a1 = createMultiAck(ack_hash, ack_len, 1);
if (a1) sendDirect(a1, dest.getPath(), dest.out_path_len, d);
if (a1) {
a1->tx_radio = getContactTxRadio(dest.id);
sendDirect(a1, dest.getPath(), dest.out_path_len, d);
}
d += 300;
}
mesh::Packet* a2 = createAck(ack_hash, ack_len);
if (a2) sendDirect(a2, dest.getPath(), dest.out_path_len, d);
if (a2) {
a2->tx_radio = getContactTxRadio(dest.id);
sendDirect(a2, dest.getPath(), dest.out_path_len, d);
}
}
}
@@ -567,7 +583,7 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp,
est_timeout = 0;
return MSG_SEND_FAILED;
}
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
uint32_t t = getTransmitAirtime(pkt);
int rc;
bool sent;
@@ -628,7 +644,7 @@ int BaseChatMesh::sendCommandData(const ContactInfo& recipient,
return MSG_SEND_FAILED;
}
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
uint32_t t = getTransmitAirtime(pkt);
const uint32_t base_timeout = recipient.out_path_len == OUT_PATH_UNKNOWN
? calcFloodTimeoutMillisFor(t)
: calcDirectTimeoutMillisFor(t, recipient.out_path_len);
@@ -669,8 +685,7 @@ bool BaseChatMesh::sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& chan
auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, channel, temp, 5 + prefix_len + text_len);
if (pkt) {
sendFloodScoped(channel, pkt);
return true;
return sendFloodScoped(channel, pkt);
}
return false;
}
@@ -698,12 +713,10 @@ bool BaseChatMesh::sendGroupData(mesh::GroupChannel& channel, uint8_t* path, uin
}
if (path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(channel, pkt);
return sendFloodScoped(channel, pkt);
} else {
sendDirect(pkt, path, path_len);
return sendDirect(pkt, path, path_len);
}
return true;
}
bool BaseChatMesh::shareContactZeroHop(const ContactInfo& contact) {
@@ -763,9 +776,9 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password,
pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.getSharedSecret(self_id), temp, tlen);
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
uint32_t t = getTransmitAirtime(pkt);
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
if (!sendFloodScoped(recipient, pkt)) return MSG_SEND_FAILED;
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
@@ -788,9 +801,9 @@ int BaseChatMesh::sendAnonReq(const ContactInfo& recipient, const uint8_t* data,
pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + len);
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
uint32_t t = getTransmitAirtime(pkt);
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
if (!sendFloodScoped(recipient, pkt)) return MSG_SEND_FAILED;
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
@@ -815,9 +828,9 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + data_len);
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
uint32_t t = getTransmitAirtime(pkt);
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
if (!sendFloodScoped(recipient, pkt)) return MSG_SEND_FAILED;
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
@@ -842,9 +855,9 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, u
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, sizeof(temp));
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
uint32_t t = getTransmitAirtime(pkt);
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
if (!sendFloodScoped(recipient, pkt)) return MSG_SEND_FAILED;
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
+1
View File
@@ -173,6 +173,7 @@ protected:
virtual bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) { return false; }
// Mesh overrides
uint8_t getContactTxRadio(const mesh::Identity& peer) const override;
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override;
int searchPeersByHash(const uint8_t* hash) override;
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
+1 -1
View File
@@ -733,7 +733,7 @@ static void formatSnrDbX4Short(char* dest, size_t dest_len, int16_t snr_x4) {
}
void CommonCLI::loadPrefs(FILESYSTEM* fs) {
_radio_profiles.begin(fs, _callbacks->getProfileRadio(), _rtc);
_radio_profiles.begin(fs, _callbacks->getProfileRadio(), _rtc, true);
const bool display_settings_loaded = mesh::ui::loadDisplayPowerSettings(fs, false);
(void)display_settings_loaded;
bool is_fresh_install = false;
+82
View File
@@ -0,0 +1,82 @@
#pragma once
#include <RadioTxPolicy.h>
#include <stddef.h>
#include <stdlib.h>
namespace mesh { namespace companion {
struct TxRouteCommand {
bool matched = false;
bool valid = false;
bool channel = false;
bool set = false;
bool quoted = false;
char target[70] = {};
uint8_t policy = RADIO_TX_AUTO;
};
inline bool txRouteSpace(char c) { return c == ' ' || c == '\t'; }
inline TxRouteCommand parseTxRouteCommand(const char* text) {
TxRouteCommand result;
if (!text) return result;
if (!strncmp(text, "get ", 4)) result.set = false;
else if (!strncmp(text, "set ", 4)) result.set = true;
else return result;
text += 4;
const char* family = nullptr;
if (!strncmp(text, "tx.user", 7)) family = "tx.user";
else if (!strncmp(text, "tx.channel", 10)) { family = "tx.channel"; result.channel = true; }
else return result;
text += strlen(family);
if (*text && !txRouteSpace(*text)) return result;
result.matched = true;
while (txRouteSpace(*text)) ++text;
const char* end = text + strlen(text);
while (end > text && txRouteSpace(end[-1])) --end;
if (result.set) {
const char* mode = end;
while (mode > text && !txRouteSpace(mode[-1])) --mode;
char value[8];
const size_t length = end - mode;
if (mode == text || length >= sizeof(value)) return result;
memcpy(value, mode, length); value[length] = 0;
if (!parseRadioTxPolicy(value, result.policy)) return result;
end = mode;
while (end > text && txRouteSpace(end[-1])) --end;
}
if (end > text && *text == '"') {
if (end - text < 2 || end[-1] != '"') return result;
result.quoted = true;
++text; --end;
}
const size_t length = end - text;
if (length >= sizeof(result.target) || ((result.set || result.quoted) && !length)) return result;
if (memchr(text, '"', length) || memchr(text, '\r', length) || memchr(text, '\n', length)) return result;
memcpy(result.target, text, length);
result.target[length] = 0;
result.valid = true;
return result;
}
// Explicit key: avoids confusing a contact named like hexadecimal with a key.
inline bool parseTxRouteKey(const char* text, uint8_t key[32], size_t& bytes) {
const size_t length = strlen(text);
if (length < 12 || length > 64 || (length & 1)) return false;
bytes = length / 2;
for (size_t i = 0; i < bytes; ++i) {
uint8_t value = 0;
for (unsigned n = 0; n < 2; ++n) {
char c = text[2 * i + n];
if (c >= 'A' && c <= 'F') c += 'a' - 'A';
if (c >= '0' && c <= '9') value = (value << 4) | (c - '0');
else if (c >= 'a' && c <= 'f') value = (value << 4) | (c - 'a' + 10);
else return false;
}
key[i] = value;
}
return true;
}
} } // namespace mesh::companion
+5 -1
View File
@@ -18,7 +18,9 @@ struct ContactInfo {
uint8_t type = 0; // one of ADV_TYPE_*
uint8_t flags = 0;
uint8_t out_path_len = 0;
mutable bool shared_secret_valid = false;
// Both fit in the original cache-valid byte; no extra RAM per contact.
mutable uint8_t shared_secret_valid : 1;
uint8_t tx_radio : 3;
#if MESH_CONTACT_CACHE
mesh::ContactPathRef path_ref;
#else
@@ -36,6 +38,8 @@ struct ContactInfo {
mutable uint16_t storage_slot = mesh::storage::CONTACT_SLOT_NONE;
#endif
ContactInfo() : shared_secret_valid(false), tx_radio(mesh::RADIO_TX_AUTO) {}
// The pointer is a short-lived borrow. Copy bytes for a queued operation;
// another contact-cache access may replace the resident entry.
const uint8_t* getPath() const {
+73
View File
@@ -87,6 +87,79 @@ inline bool decodeStoredRuleChannel(uint8_t stored, uint8_t& key_len,
return channelKeyLengthSupported(key_len);
}
// FPF7's active byte keeps bit 0 as the active flag. Bits 1/2 select bridge
// and radio crossover admission. A zero mode retains legacy radio forwarding.
// Row length and all channel-key encodings remain unchanged.
enum RuleMode : uint8_t {
RULE_MODE_RADIO = 0,
RULE_MODE_BRIDGE = 2,
RULE_MODE_CROSS = 4,
};
inline uint8_t encodeStoredRuleActive(bool active, uint8_t modes) {
return active ? (uint8_t)(1 | modes) : 0;
}
inline bool decodeStoredRuleActive(uint8_t stored, bool& active, uint8_t& modes) {
if ((stored & ~7U) != 0 || (stored != 0 && (stored & 1U) == 0)) return false;
active = (stored & 1U) != 0;
modes = stored & 6U;
return true;
}
inline const char* ruleModeName(uint8_t modes) {
switch (modes) {
case RULE_MODE_RADIO: return "radio";
case RULE_MODE_BRIDGE: return "bridge";
case RULE_MODE_CROSS: return "cross";
default: return "bridge,cross";
}
}
inline const char* compactRuleModeName(uint8_t modes) {
switch (modes) {
case RULE_MODE_RADIO: return "r";
case RULE_MODE_BRIDGE: return "b";
case RULE_MODE_CROSS: return "c";
default: return "bc";
}
}
inline bool parseRuleModes(const char* text, uint8_t& modes) {
if (!text) return false;
// Fold only the bounded selector, leaving public channel names untouched.
char value[16];
size_t len = strlen(text);
if (len >= sizeof(value)) return false;
for (size_t i = 0; i <= len; i++) {
char c = text[i];
value[i] = c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c;
}
if (strcmp(value, "radio") == 0 || strcmp(value, "r") == 0) modes = RULE_MODE_RADIO;
else if (strcmp(value, "bridge") == 0 || strcmp(value, "b") == 0) modes = RULE_MODE_BRIDGE;
else if (strcmp(value, "cross") == 0 || strcmp(value, "c") == 0) modes = RULE_MODE_CROSS;
else if (strcmp(value, "bridge,cross") == 0 || strcmp(value, "cross,bridge") == 0
|| strcmp(value, "bc") == 0 || strcmp(value, "cb") == 0
|| strcmp(value, "b,c") == 0 || strcmp(value, "c,b") == 0)
modes = RULE_MODE_BRIDGE | RULE_MODE_CROSS;
else return false;
return true;
}
// These are separate admission phases. A transport rule cannot suppress
// ordinary LoRa forwarding, nor can a radio stop rule bypass a transport deny.
inline bool ruleModeMatches(uint8_t modes, uint8_t context,
const mesh::Packet* packet) {
return packet && (modes == RULE_MODE_RADIO ? context == RULE_MODE_RADIO
: (modes & context) != 0)
&& (context != RULE_MODE_RADIO || packet->isRouteFlood());
}
inline bool transportActionsSupported(uint8_t modes, bool scope_action,
bool retry_action, bool slow_timing) {
return modes == RULE_MODE_RADIO || (!scope_action && !retry_action && !slow_timing);
}
inline int channelHashNibble(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
+115
View File
@@ -0,0 +1,115 @@
#pragma once
#include <stdarg.h>
#include <stdio.h>
#include "FloodFilterPolicy.h"
namespace FloodRuleCLI {
inline bool isChannelReference(const char* text) {
return FloodFilterPolicy::channelHashPrefixEqual(text, "key:");
}
// Keep the existing non-secret key:XXXXXXXX display usable when editing or
// copying rules on this node. Never resolve a fingerprint collision to an
// arbitrary key, and never widen it into an unauthenticated channel hash.
template<typename Entry>
bool copyChannelReference(const char* text, const Entry* entries, unsigned count,
uint8_t& key_len, uint8_t& channel_hash,
uint8_t* secret, char* name, size_t name_len) {
if (!isChannelReference(text) || !entries) return false;
const Entry* found = nullptr;
for (unsigned i = 0; i < count; i++) {
const auto& entry = entries[i];
if (!entry.active || !isChannelReference(entry.channel_name)
|| !FloodFilterPolicy::channelRequiresAuthentication(entry.channel_key_len)
|| strlen(text) != strlen(entry.channel_name)
|| !FloodFilterPolicy::channelHashPrefixEqual(text, entry.channel_name)) continue;
if (found && !FloodFilterPolicy::sameChannelKey(found->channel_key_len,
found->channel_secret, entry.channel_key_len, entry.channel_secret)) return false;
found = &entry;
}
if (!found) return false;
key_len = found->channel_key_len;
channel_hash = found->channel_hash;
memcpy(secret, found->channel_secret, sizeof(found->channel_secret));
snprintf(name, name_len, "%s", found->channel_name);
return true;
}
// No temporary expansion buffer: compact and descriptive commands feed the
// same parser. This writer emits a complete, pasteable command or an error,
// never a truncated command whose missing suffix could change its meaning.
class CommandWriter {
char* _out;
size_t _capacity;
size_t _used = 0;
bool _complete = true;
public:
CommandWriter(char* out, size_t capacity) : _out(out), _capacity(capacity) {}
void append(const char* format, ...) {
if (!_complete || !_out || _used >= _capacity) { _complete = false; return; }
va_list args;
va_start(args, format);
int length = vsnprintf(_out + _used, _capacity - _used, format, args);
va_end(args);
if (length < 0 || (size_t)length >= _capacity - _used) _complete = false;
else _used += (size_t)length;
}
bool finish() {
if (!_complete && _out && _capacity) {
snprintf(_out, _capacity, "Err - compact command exceeds reply size");
}
return _complete;
}
};
// Generalized repeater and room-server rows have the same named fields.
// Defaults are omitted only when the setter reconstructs exactly that value.
template<typename Entry>
bool formatCompact(char* out, size_t capacity, unsigned slot, const Entry& entry) {
using namespace FloodFilterPolicy;
CommandWriter text(out, capacity);
text.append("set fr.%u ", slot);
if (entry.payload_type == 0xFF) text.append("any");
else text.append("%u", (unsigned)entry.payload_type);
if (entry.transport_modes != RULE_MODE_RADIO)
text.append(" m=%s", compactRuleModeName(entry.transport_modes));
if (entry.min_hops != 0 || entry.max_hops != 63) {
if (entry.min_hops == entry.max_hops) text.append(" %u", (unsigned)entry.min_hops);
else if (entry.max_hops == 63) text.append(" %u+", (unsigned)entry.min_hops);
else text.append(" %u-%u", (unsigned)entry.min_hops, (unsigned)entry.max_hops);
}
if (entry.channel_key_len != 0) text.append(" c=%s", entry.channel_name);
if (entry.match_blacklisted_path) text.append(" p=bl");
else if (entry.path_hops) {
text.append(" p=");
for (unsigned hop = 0; hop < entry.path_hops; hop++) {
if (hop) text.append(",");
for (unsigned byte = 0; byte < entry.path_hash_size; byte++)
text.append("%02X", entry.path[hop * entry.path_hash_size + byte]);
}
}
switch (entry.incoming_scope_kind) {
case RULE_IN_NONE: text.append(" i=n"); break;
case RULE_IN_SCOPED: text.append(" i=s"); break;
case RULE_IN_ALLOWED: text.append(" i=a"); break;
case RULE_IN_UNKNOWN: text.append(" i=u"); break;
case RULE_IN_SCOPE: text.append(" i=s:%s", entry.incoming_scope_name); break;
case RULE_IN_REGION: text.append(" i=r:%s", entry.incoming_scope_name); break;
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);
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");
if (entry.scope_uses_slow_timing || entry.suspend_on_temp_radio) {
text.append(" f=%s%s%s", entry.scope_uses_slow_timing ? "s" : "",
entry.suspend_on_temp_radio ? "t" : "", entry.retry_on_match ? "r" : "");
} else if (entry.retry_on_match) text.append(" r");
return text.finish();
}
} // namespace FloodRuleCLI
+51 -2
View File
@@ -44,6 +44,9 @@ unsigned split(char* s, char* parts[], unsigned maximum) {
bool parsePreamble(const char* s, uint16_t& result) {
return cli::parseRadioPreamble(s, result);
}
bool validReplySetting(uint8_t value) {
return (value >= 0xb0 && value <= 0xb4) || value == 0xba || value == 0xbb;
}
}
bool RadioProfileCLI::readImage(const char* path, uint8_t* bytes, size_t size) {
@@ -92,6 +95,7 @@ bool RadioProfileCLI::save(const RadioProfileConfig& config, uint16_t preamble,
memcpy(image + 14, &config.params.preamble, 2);
memcpy(image + 16, &preamble, 2);
image[18] = (uint8_t)cross;
image[19] = reply_setting_;
const uint32_t crc = checksum(image, ImageSize - 4);
memcpy(image + ImageSize - 4, &crc, 4);
if (!writeImage(TempPath, image, sizeof(image))) return false;
@@ -107,8 +111,13 @@ bool RadioProfileCLI::save(const RadioProfileConfig& config, uint16_t preamble,
return true;
}
void RadioProfileCLI::begin(FILESYSTEM* fs, Radio* radio, RTCClock* rtc) {
void RadioProfileCLI::begin(FILESYSTEM* fs, Radio* radio, RTCClock* rtc, bool infrastructure_replies) {
fs_ = fs; radio_ = radio; rtc_ = rtc; last_ms_ = millis();
infrastructure_replies_ = infrastructure_replies;
if (radio_ && radio_->profiles()) {
radio_->profiles()->reply_tx = infrastructure_replies ? RADIO_TX_BOTH : RADIO_TX_AUTO;
radio_->profiles()->reply_force = false;
}
if (!fs_ || !radio_ || !radio_->profiles()) return;
uint8_t bytes[ImageSize];
bool loaded = readImage(ImagePath, bytes, sizeof(bytes));
@@ -127,10 +136,12 @@ void RadioProfileCLI::begin(FILESYSTEM* fs, Radio* radio, RTCClock* rtc) {
memcpy(&saved_.params.preamble, bytes + 14, 2);
memcpy(&primary_preamble_, bytes + 16, 2);
cross_ = (RadioCrossMode)bytes[18];
reply_setting_ = validReplySetting(bytes[19]) ? bytes[19] : 0;
if ((uint8_t)saved_.mode > 2 || (uint8_t)cross_ > 2
|| (saved_.mode != RadioProfileMode::Off && !radio_->validateProfile(saved_.params))
|| (primary_preamble_ && (primary_preamble_ < 8 || primary_preamble_ > RadioProfiles::MaxPreamble))) {
hold_ = true; saved_ = {}; primary_preamble_ = 0; cross_ = RadioCrossMode::Auto;
reply_setting_ = 0; // reject the entire image, including its reply override
return;
}
stagePrimary(primary_preamble_, false);
@@ -157,6 +168,10 @@ bool RadioProfileCLI::acceptsPrimary(float freq, float bw, uint8_t sf, uint8_t c
void RadioProfileCLI::publish() {
if (!radio_ || !radio_->profiles()) return;
radio_->profiles()->cross = cross_;
radio_->profiles()->reply_tx = infrastructure_replies_
? (validReplySetting(reply_setting_) ? reply_setting_ & 7 : RADIO_TX_BOTH) : RADIO_TX_AUTO;
radio_->profiles()->reply_force = infrastructure_replies_
&& validReplySetting(reply_setting_) && (reply_setting_ & 8);
if (publish_pending_ && (int32_t)(millis() - publish_after_ms_) < 0) return;
publish_pending_ = false;
radio_->profiles()->setSecondary(temp_active_ ? temporary_ : saved_, temp_active_);
@@ -292,12 +307,46 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity)
const bool scheduled = !strcmp(key, "radioat2") || !strcmp(key, "tempradioat2");
const bool scheduled_temp = !strcmp(key, "tempradioat2");
const bool crossing = !strcmp(key, "radio2.cross");
const bool replies = infrastructure_replies_ && !strcmp(key, "tx.reply");
const bool status = !strcmp(key, "radio2.status");
const bool scan = !strcmp(key, "radio2.scan");
const bool timing = !strcmp(key, "radio2.timing") || !strcmp(key, "radio.timing");
if (!base && !temporary && !scheduled && !crossing && !status && !scan && !timing) return false;
if (!base && !temporary && !scheduled && !crossing && !replies && !status && !scan && !timing) return false;
if (!radio_ || !radio_->profiles()) { snprintf(reply, capacity, "Error: radio profiles unsupported"); return true; }
if (command == text && *args && verb == Get && (temporary || base)) verb = Set;
if (replies) {
const auto* p = radio_->profiles();
if (verb == Get && !*args) {
snprintf(reply, capacity, "> %s%s%s", radioTxPolicyName(p->reply_tx),
p->reply_force ? " force" : "",
(p->reply_tx == RADIO_TX_BOTH || p->reply_tx == RADIO_TX_SECONDARY)
&& !p->canTransmit(1, p->reply_force) ? "; radio2 TX unavailable" : "");
return true;
}
char mode[16];
const size_t length = strcspn(args, " \t");
uint8_t policy = RADIO_TX_AUTO;
const char* suffix = args + length;
while (*suffix == ' ' || *suffix == '\t') ++suffix;
const bool force = !strcmp(suffix, "force");
if (length < sizeof(mode)) { memcpy(mode, args, length); mode[length] = 0; }
if (verb != Set || length >= sizeof(mode) || !parseRadioTxPolicy(mode, policy)
|| (*suffix && !force)
|| (force && policy != RADIO_TX_BOTH && policy != RADIO_TX_SECONDARY)) {
snprintf(reply, capacity, "Error: use get/set tx.reply auto|radio|radio2|both|off [force for radio2/both]");
return true;
}
const uint8_t previous = reply_setting_;
reply_setting_ = 0xb0 | policy | (force ? 8 : 0);
if (!save(saved_, primary_preamble_, cross_)) {
reply_setting_ = previous;
snprintf(reply, capacity, "Error: settings could not be saved");
} else {
publish();
snprintf(reply, capacity, "OK - tx.reply=%s%s", radioTxPolicyName(policy), force ? " force" : "");
}
return true;
}
if (timing) {
if (verb != Get || *args) { snprintf(reply, capacity, "Error: radio timing is read-only"); return true; }
const auto& p = *radio_->profiles();
+3 -1
View File
@@ -25,6 +25,8 @@ class RadioProfileCLI {
RadioProfileConfig pending_temporary_;
uint16_t primary_preamble_ = 0;
RadioCrossMode cross_ = RadioCrossMode::Auto;
uint8_t reply_setting_ = 0; // reserved image byte: 0 = role default
bool infrastructure_replies_ = false;
Schedule schedules_[8];
uint32_t temp_remaining_ms_ = 0;
uint32_t temp_start_ms_ = 0;
@@ -41,7 +43,7 @@ class RadioProfileCLI {
void formatConfig(char* reply, size_t capacity, const RadioProfileConfig& config,
bool temporary, uint32_t remaining_ms) const;
public:
void begin(FILESYSTEM* fs, Radio* radio, RTCClock* rtc);
void begin(FILESYSTEM* fs, Radio* radio, RTCClock* rtc, bool infrastructure_replies = false);
void loop();
bool handle(const char* command, char* reply, size_t capacity = 160);
uint16_t primaryPreamble() const { return primary_preamble_; }
+36 -14
View File
@@ -5,26 +5,48 @@ namespace mesh {
// A parameterized TempRadio command changes the modulation used to carry its
// own remote-CLI reply. A wall-clock delay cannot prove that the reply has
// left a busy outbound queue, so keep the scheduled handoff blocked by the
// exact packet object until Dispatcher reports TX completion. This class is
// exact packet objects on both profiles until Dispatcher reports TX completion.
// At least one successful TX permits the handoff, after both copies drain. This class is
// deliberately pointer-only: packet ownership stays with Dispatcher.
class TempRadioReplyBarrier {
const void* _packet;
const void* _packet = nullptr;
const void* _copy = nullptr;
bool _armed = false;
bool _succeeded = false;
public:
TempRadioReplyBarrier() : _packet(nullptr) {}
void arm(const void* packet) { _packet = packet; }
void clear() { _packet = nullptr; }
bool waiting() const { return _packet != nullptr; }
bool complete(const void* packet) {
if (_packet == nullptr || packet != _packet) return false;
_packet = nullptr;
bool remove(const void* packet) {
if (!_armed || packet == nullptr) return false;
if (packet == _packet) _packet = nullptr;
else if (packet == _copy) _copy = nullptr;
else return false;
return true;
}
bool fail(const void* packet) { return complete(packet); }
public:
// Collect the Dispatcher-created second copy during queue admission. Retry
// bookkeeping can call onSendFail before admission; it must not resolve this.
void prepare(const void* packet) { clear(); _packet = packet; }
void trackCopy(const void* original, const void* copy) {
if (original && original == _packet && copy != original) _copy = copy;
}
void arm(const void* packet) {
if (packet != _packet) prepare(packet);
_armed = packet != nullptr;
}
void clear() { _packet = _copy = nullptr; _armed = _succeeded = false; }
bool waiting() const { return _armed && (_packet || _copy); }
bool complete(const void* packet) {
if (!remove(packet)) return false;
_succeeded = true;
return true;
}
// Cancel the transition only when every admitted copy has failed.
bool fail(const void* packet) {
return remove(packet) && !waiting() && !_succeeded;
}
};
} // namespace mesh
+5
View File
@@ -39,6 +39,11 @@ void BridgeBase::handleReceivedPacket(mesh::Packet *packet) {
return;
}
if (!allowsPacket(packet)) {
_mgr->free(packet);
return;
}
if (!_seen_packets.wasSeen(packet)) {
_seen_packets.markSeen(packet);
// bridge_delay provides a buffer to prevent immediate processing conflicts in the mesh network.
+2
View File
@@ -545,6 +545,8 @@ void ESPNowBridge::sendPacket(mesh::Packet *packet) {
return;
}
if (!allowsPacket(packet)) return;
if (!_seen_packets.wasSeen(packet)) {
// Check the serialized size before writing into the ESP-NOW-sized buffer.
const int expectedMeshPacketLen = packet->getRawLength();
+10
View File
@@ -3214,6 +3214,11 @@ void MQTTBridge::onPacketReceived(mesh::Packet *packet) {
return;
}
if (!allowsPacket(packet)) {
_filtered_packets++;
return;
}
// Queue packet for transmission
queuePacket(packet, false);
}
@@ -3237,6 +3242,11 @@ void MQTTBridge::sendPacket(mesh::Packet *packet) {
return;
}
if (!allowsPacket(packet)) {
_filtered_packets++;
return;
}
// Queue packet for transmission
queuePacket(packet, true);
}
+2
View File
@@ -118,6 +118,8 @@ void RS232Bridge::sendPacket(mesh::Packet *packet) {
return;
}
if (!allowsPacket(packet)) return;
if (!_seen_packets.wasSeen(packet)) {
_seen_packets.markSeen(packet);
+6
View File
@@ -100,6 +100,12 @@ enum OtaMsgType : uint8_t {
OTA_LEAVES = 0x0B, // a fragment of the leaves[] array (for host-side seed leaf-diff)
};
// Locally served replies may be emitted long after RX, outside its call stack.
inline bool ota_is_response_message(uint8_t type) {
return type == OTA_HAVE || type == OTA_MANIFEST || type == OTA_DATA
|| type == OTA_PROOF || type == OTA_LEAVES;
}
// Discovery is periodic background traffic; every packet that advances an active fetch is primary
// traffic. Keep this classification in the wire-format layer so origins and opaque relay-only builds make
// the same queueing decision without needing an OtaManager.
+5 -1
View File
@@ -303,7 +303,11 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) {
}
mesh::RadioParamApplyResult RadioLibWrapper::prepareTransmitProfile(uint8_t profile) {
if (!_profiles.canTransmit(profile)) return mesh::RadioParamApplyResult::FAILED;
return prepareTransmitProfile(profile, false);
}
mesh::RadioParamApplyResult RadioLibWrapper::prepareTransmitProfile(uint8_t profile, bool reply_rx_override) {
if (!_profiles.canTransmit(profile, reply_rx_override)) return mesh::RadioParamApplyResult::FAILED;
if (_profiles.enabled() && (isChipBusy() || isPacketPendingOrReceiving())) {
return mesh::RadioParamApplyResult::BUSY;
}
+1
View File
@@ -210,6 +210,7 @@ public:
uint8_t receiveProfile() const override { return _active_profile; }
uint32_t receiveProfileGeneration() const override { return _profile_generation; }
mesh::RadioParamApplyResult prepareTransmitProfile(uint8_t profile) override;
mesh::RadioParamApplyResult prepareTransmitProfile(uint8_t profile, bool reply_rx_override) override;
mesh::RadioParamApplyResult trySetPrimaryParams(const mesh::RadioProfileParams& params,
bool temporary, const uint32_t* timings = nullptr) override;
uint16_t profilePreamble(uint8_t profile) const override {