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:
1 parent
e9a2326c4f
commit
a07f39c6dd
60 files changed
+3188
-362
No files matched your search
+58
-8
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -118,6 +118,8 @@ void RS232Bridge::sendPacket(mesh::Packet *packet) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!allowsPacket(packet)) return;
|
||||
|
||||
if (!_seen_packets.wasSeen(packet)) {
|
||||
_seen_packets.markSeen(packet);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user