Keep bridged OTA retries resumable across profile handoffs
Let the mesh timed OTA retry guard own byte-identical requests instead of the permanent bridge FIFO. Bind bridge ingress to its primary profile generation and preserve UART reflection/liveness protections. Verified a LoRa to ESP-NOW to LoRa update with outage recovery and install.
This commit is contained in:
1 parent
bc1b414af0
commit
690ffe5601
11 files changed
+228
-24
No files matched your search
@@ -6,8 +6,25 @@ class AbstractBridge {
|
||||
using PacketFilter = bool (*)(void*, const mesh::Packet*);
|
||||
PacketFilter _packet_filter = nullptr;
|
||||
void* _packet_filter_context = nullptr;
|
||||
const mesh::RadioProfiles* _ingress_profiles = nullptr;
|
||||
|
||||
protected:
|
||||
// Bridges enter the local primary MeshCore network, not whichever tuple
|
||||
// the physical scanner currently visits. Snapshot its generation at queue
|
||||
// admission so a delayed frame cannot acquire a newer temporary lease.
|
||||
bool preparePacketIngress(mesh::Packet* packet) const {
|
||||
if (!packet || (_ingress_profiles
|
||||
&& _ingress_profiles->params(0).protocol != mesh::RadioProfileProtocol::MeshCore)) return false;
|
||||
packet->radio_profile = packet->radio_origin = 0;
|
||||
packet->radio_generation = packet->radio_origin_generation =
|
||||
_ingress_profiles ? _ingress_profiles->generation[0] : 0;
|
||||
packet->radio_bound = false;
|
||||
packet->radio_local = false;
|
||||
packet->tx_radio = mesh::RADIO_TX_AUTO;
|
||||
packet->radio_reply = packet->radio_reply_force = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -21,6 +38,10 @@ protected:
|
||||
public:
|
||||
virtual ~AbstractBridge() {}
|
||||
|
||||
void setIngressProfiles(const mesh::RadioProfiles* profiles) {
|
||||
_ingress_profiles = profiles;
|
||||
}
|
||||
|
||||
void setPacketFilter(PacketFilter filter, void* context) {
|
||||
_packet_filter = filter;
|
||||
_packet_filter_context = context;
|
||||
|
||||
@@ -39,13 +39,17 @@ void BridgeBase::handleReceivedPacket(mesh::Packet *packet) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!allowsPacket(packet)) {
|
||||
if (!preparePacketIngress(packet) || !allowsPacket(packet)) {
|
||||
_mgr->free(packet);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_seen_packets.wasSeen(packet)) {
|
||||
_seen_packets.markSeen(packet);
|
||||
// OTA retries deliberately reuse the same request/data bytes. The mesh
|
||||
// owns their bounded retry/longer-path echo cache and hop ceiling; the
|
||||
// ordinary bridge FIFO has no expiry and would pin a lost burst forever.
|
||||
const bool ota = packet->getPayloadType() == PAYLOAD_TYPE_OTA;
|
||||
if (ota || !_seen_packets.wasSeen(packet)) {
|
||||
if (!ota) _seen_packets.markSeen(packet);
|
||||
// bridge_delay provides a buffer to prevent immediate processing conflicts in the mesh network.
|
||||
_mgr->queueInbound(packet, millis() + _prefs->bridge_delay);
|
||||
} else {
|
||||
|
||||
@@ -110,9 +110,10 @@ protected:
|
||||
* @brief Common packet handling for received packets
|
||||
*
|
||||
* Implements the standard pattern used by all bridges:
|
||||
* - Check if packet was seen before using _seen_packets.wasSeen()
|
||||
* - Queue packet for mesh processing if not seen before
|
||||
* - Free packet if already seen to prevent duplicates
|
||||
* - Bind ingress to the current local primary-profile generation
|
||||
* - Apply transport policy before queueing or marking a packet
|
||||
* - Deduplicate ordinary packets using _seen_packets.wasSeen()
|
||||
* - Leave resumable OTA retries to the mesh's timed retry/echo policy
|
||||
*
|
||||
* @param packet The received mesh packet
|
||||
*/
|
||||
|
||||
@@ -618,7 +618,10 @@ void ESPNowBridge::sendPacket(mesh::Packet *packet) {
|
||||
|
||||
if (!allowsPacket(packet)) return;
|
||||
|
||||
if (!_seen_packets.wasSeen(packet)) {
|
||||
// Core OTA forwarding already bounds repeated requests and rejects longer
|
||||
// downstream echoes. Do not permanently suppress its byte-identical retry.
|
||||
const bool ota = packet->getPayloadType() == PAYLOAD_TYPE_OTA;
|
||||
if (ota || !_seen_packets.wasSeen(packet)) {
|
||||
// Check the serialized size before writing into the ESP-NOW-sized buffer.
|
||||
const int expectedMeshPacketLen = packet->getRawLength();
|
||||
const size_t maxPayload =
|
||||
@@ -670,7 +673,7 @@ void ESPNowBridge::sendPacket(mesh::Packet *packet) {
|
||||
}
|
||||
|
||||
if (encoded && queueTransmitFrames(frames, *packet)) {
|
||||
_seen_packets.markSeen(packet);
|
||||
if (!ota) _seen_packets.markSeen(packet);
|
||||
BRIDGE_DEBUG_PRINTLN("TX %s, len=%d, frames=%u\n",
|
||||
mesh::bridge::espNowFormatName(_active_format),
|
||||
meshPacketLen, (unsigned)frames.count);
|
||||
|
||||
@@ -293,9 +293,10 @@ void RS232Bridge::drainReceivedPackets() {
|
||||
_rx_packet_ready = false;
|
||||
if (valid) {
|
||||
// A cable loopback or replay of our own accepted mesh TX is not peer proof.
|
||||
// Discard here to avoid testing/counting the same duplicate a second time
|
||||
// inside BridgeBase::handleReceivedPacket().
|
||||
if (_seen_packets.wasSeen(pkt)) {
|
||||
// OTA must still reach the mesh's timed retry/echo policy, but it cannot
|
||||
// refresh UART peer liveness merely by reflecting our own transmission.
|
||||
const bool seen = _seen_packets.wasSeen(pkt);
|
||||
if (pkt->getPayloadType() != PAYLOAD_TYPE_OTA && seen) {
|
||||
_mgr->free(pkt);
|
||||
return;
|
||||
}
|
||||
@@ -304,18 +305,20 @@ void RS232Bridge::drainReceivedPackets() {
|
||||
// peer may have replaced the negotiated one. Unarm the safe default rather
|
||||
// than eventually sleeping that quiet legacy link. A later fresh ACK can
|
||||
// immediately restore negotiated capability; force mode remains explicit.
|
||||
if (_have_ack && (!_ack_fresh
|
||||
|| static_cast<uint32_t>(_rx_packet_received_ms - _last_ack)
|
||||
> mesh::bridge::rs232heartbeat::ACTIVE_PING_MS
|
||||
+ mesh::bridge::rs232heartbeat::ACK_MAX_AGE_MS)) {
|
||||
_have_ack = false;
|
||||
_peer_session = 0;
|
||||
if (!seen) {
|
||||
if (_have_ack && (!_ack_fresh
|
||||
|| static_cast<uint32_t>(_rx_packet_received_ms - _last_ack)
|
||||
> mesh::bridge::rs232heartbeat::ACTIVE_PING_MS
|
||||
+ mesh::bridge::rs232heartbeat::ACK_MAX_AGE_MS)) {
|
||||
_have_ack = false;
|
||||
_peer_session = 0;
|
||||
}
|
||||
_legacy_rx = true;
|
||||
_legacy_fresh = true;
|
||||
_last_legacy_rx = _rx_packet_received_ms;
|
||||
_power.notePeer(_rx_packet_received_ms);
|
||||
_partial_suspend_grace = false;
|
||||
}
|
||||
_legacy_rx = true;
|
||||
_legacy_fresh = true;
|
||||
_last_legacy_rx = _rx_packet_received_ms;
|
||||
_power.notePeer(_rx_packet_received_ms);
|
||||
_partial_suspend_grace = false;
|
||||
onPacketReceived(pkt);
|
||||
}
|
||||
else _mgr->free(pkt);
|
||||
@@ -431,7 +434,10 @@ void RS232Bridge::sendPacket(mesh::Packet *packet) {
|
||||
drainTransmit(millis(), true);
|
||||
if (_tx_size != 0) return; // Never interleave mesh traffic with a partial frame.
|
||||
|
||||
if (!_seen_packets.wasSeen(packet)) {
|
||||
// The mesh's timed OTA retry cache, not the permanent bridge FIFO, owns
|
||||
// duplicate suppression for resumable request/data packets.
|
||||
const bool ota = packet->getPayloadType() == PAYLOAD_TYPE_OTA;
|
||||
if (ota || !_seen_packets.wasSeen(packet)) {
|
||||
const int expected = packet->getRawLength();
|
||||
if (expected <= 0 || expected > MAX_TRANS_UNIT + 1) return;
|
||||
uint8_t* buffer = _tx_buffer;
|
||||
@@ -460,6 +466,8 @@ void RS232Bridge::sendPacket(mesh::Packet *packet) {
|
||||
_tx_size = static_cast<uint16_t>(len + SERIAL_OVERHEAD);
|
||||
_tx_pos = 0;
|
||||
_tx_is_ping = false;
|
||||
// Retain an outgoing OTA hash solely to reject reflected data as UART
|
||||
// peer-liveness proof. It never suppresses OTA delivery in either direction.
|
||||
_seen_packets.markSeen(packet);
|
||||
drainTransmit(millis(), true);
|
||||
|
||||
|
||||
Reference in new issue
Block a user