Files
MeshCore/src/helpers/bridges/ESPNowBridge.cpp
T
mikecarper 690ffe5601 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.
2026-10-15 21:05:23 -07:00

691 lines
24 KiB
C++

#include "ESPNowBridge.h"
#include <helpers/WirelessControl.h>
#include <esp_wifi.h>
#include <helpers/esp32/WiFiRadioPolicy.h>
#ifdef WITH_ESPNOW_BRIDGE
#include <WiFi.h>
static void stopBridgeWiFiIfUnused() {
if (!(mesh::wireless::control().enabled() & mesh::wireless::WiFi)) {
// WebConfig/OTA may have adopted an initially IDF-only bridge driver.
// Shut down through Arduino first so its private initialized/started
// caches cannot outlive the SDK driver and break the next WiFi startup.
// In the pinned Arduino 2/3 cores, channel() is gated by initialized but
// getMode() is also gated by started. A failed facade start can therefore
// report OFF despite retaining initialized state. Complete that start
// before stopping it, without allocating a facade for a pure IDF owner.
if (WiFi.getMode() == WIFI_OFF && WiFi.channel() > 0) {
if (!WiFi.mode(WIFI_STA)) return;
}
if (!WiFi.mode(WIFI_OFF)) return;
// Arduino OFF is a no-op when the bridge alone initialized the SDK.
// Probe the SDK separately rather than trusting the facade's mode/cache.
wifi_mode_t mode = WIFI_MODE_NULL;
if (esp_wifi_get_mode(&mode) == ESP_OK) {
esp_wifi_stop();
esp_wifi_deinit();
}
}
}
// Static member to handle callbacks
ESPNowBridge *ESPNowBridge::_instance = nullptr;
// Static callback wrappers
#if ESP_IDF_VERSION_MAJOR >= 5
void ESPNowBridge::recv_cb(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
const uint8_t *mac = info != nullptr ? info->src_addr : nullptr;
if (_instance) {
_instance->queueReceivedFrame(mac, data, len);
}
}
#else
void ESPNowBridge::recv_cb(const uint8_t *mac, const uint8_t *data, int len) {
if (_instance) {
_instance->queueReceivedFrame(mac, data, len);
}
}
#endif
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
void ESPNowBridge::send_cb(const esp_now_send_info_t *info,
esp_now_send_status_t status) {
const uint8_t *mac = info != nullptr ? info->des_addr : nullptr;
#else
void ESPNowBridge::send_cb(const uint8_t *mac, esp_now_send_status_t status) {
#endif
if (_instance) {
_instance->onDataSent(mac, status);
}
}
ESPNowBridge::ESPNowBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc)
: BridgeBase(prefs, mgr, rtc), _rx_head(0), _rx_tail(0), _rx_count(0),
_rx_dropped(0), _rx_dropped_reported(0),
_tx_head(0), _tx_tail(0), _tx_count(0), _tx_waiting(false),
_tx_callback_done(false), _tx_callback_status(ESP_NOW_SEND_SUCCESS),
_tx_dropped(0), _tx_dropped_reported(0),
_active_format(mesh::bridge::ESPNOW_FORMAT_WRAPPED) {
_instance = this;
}
void ESPNowBridge::begin() {
if (mesh::wireless::control().blocked(mesh::wireless::EspNow)) return;
BRIDGE_DEBUG_PRINTLN("Initializing...\n");
if (_initialized) return;
if (_sdk_teardown_failed) {
end();
if (_sdk_teardown_failed) return;
}
_active_format = mesh::bridge::isValidEspNowFormat(_prefs->bridge_format)
? _prefs->bridge_format : mesh::bridge::ESPNOW_FORMAT_WRAPPED;
const bool raw_format = _active_format == mesh::bridge::ESPNOW_FORMAT_RAW;
if (raw_format && mesh::wifi::checkLongRangeRadioStart() != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("ESP-NOW LR start deferred while a WiFi AP is active\n");
return;
}
portENTER_CRITICAL(&_rx_mux);
_rx_head = 0;
_rx_tail = 0;
_rx_count = 0;
_rx_dropped = 0;
portEXIT_CRITICAL(&_rx_mux);
_rx_dropped_reported = 0;
_raw_reassembler.reset();
portENTER_CRITICAL(&_tx_mux);
_tx_head = 0;
_tx_tail = 0;
_tx_count = 0;
_tx_waiting = false;
_tx_callback_done = false;
_tx_callback_status = ESP_NOW_SEND_SUCCESS;
_tx_dropped = 0;
portEXIT_CRITICAL(&_tx_mux);
_tx_dropped_reported = 0;
_tx_started_at = _tx_retry_at = 0;
if (!mesh::bridge::isValidEspNowBridgeChannel(
_prefs->bridge_channel, _active_format)) {
BRIDGE_DEBUG_PRINTLN("ESP-NOW channel must be 1-13\n");
return;
}
// ESP-NOW only needs the ESP-IDF station interface. Avoid Arduino's WiFi
// facade here: it pulls DHCP, DNS, scanning, AP, and event plumbing that an
// ESP-NOW-only bridge never uses.
const bool shared_wifi = (mesh::wireless::control().enabled() & mesh::wireless::WiFi) != 0;
if (shared_wifi) {
uint8_t channel = 0;
wifi_second_chan_t secondary = WIFI_SECOND_CHAN_NONE;
if (esp_wifi_get_channel(&channel, &secondary) != ESP_OK || channel != _prefs->bridge_channel) {
BRIDGE_DEBUG_PRINTLN("ESP-NOW bridge channel must match the active WiFi channel\n");
return;
}
}
wifi_init_config_t wifi_config = WIFI_INIT_CONFIG_DEFAULT();
if (!shared_wifi && (esp_wifi_init(&wifi_config) != ESP_OK ||
esp_wifi_set_storage(WIFI_STORAGE_RAM) != ESP_OK ||
esp_wifi_set_mode(WIFI_MODE_STA) != ESP_OK ||
esp_wifi_start() != ESP_OK)) {
stopBridgeWiFiIfUnused();
return;
}
if (raw_format) {
// Primary ESPNOWRadio nodes use Espressif's LR PHY. Retain B/G/N receive
// support as well, but force this peer's outgoing frames to LR below.
const uint8_t protocols = WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G
| WIFI_PROTOCOL_11N | WIFI_PROTOCOL_LR;
if (mesh::wifi::applyStationProtocolMask(protocols, true) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error enabling ESP-NOW LR protocol\n");
stopBridgeWiFiIfUnused();
return;
}
}
// Set Wi-Fi channel
if (!shared_wifi && esp_wifi_set_channel(_prefs->bridge_channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error setting WIFI channel to %d\n", _prefs->bridge_channel);
stopBridgeWiFiIfUnused();
return;
}
// Initialize ESP-NOW
if (esp_now_init() != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error initializing ESP-NOW\n");
stopBridgeWiFiIfUnused();
return;
}
// Register callbacks
if (esp_now_register_recv_cb(recv_cb) != ESP_OK || esp_now_register_send_cb(send_cb) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error registering ESP-NOW callbacks\n");
esp_now_register_recv_cb(nullptr);
esp_now_register_send_cb(nullptr);
esp_now_deinit();
stopBridgeWiFiIfUnused();
return;
}
// Add broadcast peer
esp_now_peer_info_t peerInfo = {};
memset(&peerInfo, 0, sizeof(peerInfo));
memset(peerInfo.peer_addr, 0xFF, ESP_NOW_ETH_ALEN); // Broadcast address
peerInfo.channel = _prefs->bridge_channel;
peerInfo.ifidx = WIFI_IF_STA;
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Failed to add broadcast peer\n");
esp_now_register_recv_cb(nullptr);
esp_now_register_send_cb(nullptr);
esp_now_deinit();
stopBridgeWiFiIfUnused();
return;
}
if (raw_format) {
esp_err_t rate_result;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 0)
esp_now_rate_config_t rate_config = {};
rate_config.phymode = WIFI_PHY_MODE_LR;
rate_config.rate = WIFI_PHY_RATE_LORA_250K;
rate_config.ersu = false;
rate_config.dcm = false;
rate_result = esp_now_set_peer_rate_config(peerInfo.peer_addr, &rate_config);
#else
rate_result = esp_wifi_config_espnow_rate(
WIFI_IF_STA, WIFI_PHY_RATE_LORA_250K);
#endif
if (rate_result != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error configuring ESP-NOW LR peer rate\n");
esp_now_del_peer(peerInfo.peer_addr);
esp_now_register_recv_cb(nullptr);
esp_now_register_send_cb(nullptr);
esp_now_deinit();
stopBridgeWiFiIfUnused();
return;
}
}
// Update bridge state
_initialized = true;
mesh::wifi::setLongRangeOwner(mesh::wifi::kLongRangeBridgeOwner, raw_format);
mesh::wifi::bridgeEspNowChannel().store(_prefs->bridge_channel);
}
void ESPNowBridge::end() {
if (!_initialized && !_sdk_teardown_failed) return;
BRIDGE_DEBUG_PRINTLN("Stopping...\n");
// Remove broadcast peer
uint8_t broadcastAddress[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
if (esp_now_del_peer(broadcastAddress) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error removing broadcast peer\n");
}
// Unregister callbacks
esp_now_register_recv_cb(nullptr);
esp_now_register_send_cb(nullptr);
// Deinitialize ESP-NOW
_sdk_teardown_failed = esp_now_deinit() != ESP_OK;
if (_sdk_teardown_failed) {
BRIDGE_DEBUG_PRINTLN("Error deinitializing ESP-NOW\n");
}
// Infrastructure WiFi may still own this shared radio.
mesh::wifi::bridgeEspNowChannel().store(0);
mesh::wifi::setLongRangeOwner(mesh::wifi::kLongRangeBridgeOwner, false);
stopBridgeWiFiIfUnused();
portENTER_CRITICAL(&_rx_mux);
_rx_head = 0;
_rx_tail = 0;
_rx_count = 0;
portEXIT_CRITICAL(&_rx_mux);
_raw_reassembler.reset();
// queueTransmitFrames() marks packets seen at admission so the same bridge
// loop cannot enqueue duplicates. Undo that mark for entries which never
// reached esp_now_send() before an explicit bridge stop/restart.
uint8_t tx_index = _tx_tail;
for (uint8_t i = 0; i < _tx_count; ++i) {
const QueuedTransmit& queued_tx = _tx_queue[tx_index];
if (!queued_tx.started) _seen_packets.clear(&queued_tx.packet);
tx_index = static_cast<uint8_t>((tx_index + 1) % TX_QUEUE_DEPTH);
}
portENTER_CRITICAL(&_tx_mux);
_tx_head = 0;
_tx_tail = 0;
_tx_count = 0;
_tx_waiting = false;
_tx_callback_done = false;
portEXIT_CRITICAL(&_tx_mux);
// Update bridge state
_initialized = false;
}
void ESPNowBridge::loop() {
if (!_initialized) return;
pumpTransmitQueue();
if (!_initialized) return;
uint8_t frame[MAX_ESPNOW_PACKET_SIZE];
uint8_t source[mesh::espnow::ESPNOW_RAW_SOURCE_MAC_SIZE];
for (uint8_t processed = 0; processed < RX_QUEUE_DEPTH; ++processed) {
// Reserve a mesh slot before removing a callback frame. Even a completing
// raw fragment must remain queued until the reconstructed packet can live.
mesh::Packet* packet = _mgr->allocNew();
if (packet == nullptr) break;
size_t frame_len = 0;
uint32_t dropped = 0;
portENTER_CRITICAL(&_rx_mux);
dropped = _rx_dropped;
if (_rx_count > 0) {
frame_len = _rx_lengths[_rx_tail];
if (frame_len > 0 && frame_len <= sizeof(frame)) {
memcpy(frame, _rx_buffers[_rx_tail], frame_len);
memcpy(source, _rx_sources[_rx_tail], sizeof(source));
} else {
frame_len = 0;
}
_rx_tail = static_cast<uint8_t>((_rx_tail + 1) % RX_QUEUE_DEPTH);
_rx_count--;
}
portEXIT_CRITICAL(&_rx_mux);
if (dropped != _rx_dropped_reported) {
BRIDGE_DEBUG_PRINTLN("RX queue full, dropped=%lu\n",
(unsigned long)dropped);
_rx_dropped_reported = dropped;
}
if (frame_len == 0) { _mgr->free(packet); break; }
if (!processReceivedFrame(packet, source, frame, frame_len)) {
_mgr->free(packet);
}
}
}
bool ESPNowBridge::queueTransmitFrames(
const mesh::espnow::ESPNowRawFrames& frames,
const mesh::Packet& packet) {
if (frames.count == 0 || frames.count > mesh::espnow::ESPNOW_RAW_FRAGMENT_COUNT) {
return false;
}
for (uint8_t i = 0; i < frames.count; ++i) {
if (frames.lengths[i] == 0
|| frames.lengths[i] > MAX_ESPNOW_PACKET_SIZE) {
return false;
}
}
bool queued = false;
portENTER_CRITICAL(&_tx_mux);
if (_tx_count < TX_QUEUE_DEPTH) {
QueuedTransmit& queued_tx = _tx_queue[_tx_head];
queued_tx.frames = frames;
queued_tx.packet = packet;
queued_tx.next_frame = 0;
queued_tx.started = false;
queued_tx.start_retries = 0;
_tx_head = static_cast<uint8_t>((_tx_head + 1) % TX_QUEUE_DEPTH);
_tx_count++;
queued = true;
} else {
_tx_dropped++;
}
portEXIT_CRITICAL(&_tx_mux);
return queued;
}
void ESPNowBridge::pumpTransmitQueue() {
uint8_t frame[MAX_ESPNOW_PACKET_SIZE];
uint16_t frame_len = 0;
bool completed = false;
int completed_status = ESP_NOW_SEND_SUCCESS;
bool clear_unsent_seen = false;
mesh::Packet unsent_packet;
uint32_t dropped = 0;
const uint32_t now = millis();
bool timed_out = false;
portENTER_CRITICAL(&_tx_mux);
timed_out = _tx_waiting && !_tx_callback_done
&& static_cast<uint32_t>(now - _tx_started_at) >= TX_CALLBACK_TIMEOUT_MS;
portEXIT_CRITICAL(&_tx_mux);
if (timed_out) {
// A completion carries no frame identity. Continuing after timeout could
// mistake a late callback for the next fragment. Tear down that SDK session
// before accepting another frame; never transmit an orphan sibling.
BRIDGE_DEBUG_PRINTLN("TX callback timeout; restarting ESP-NOW\n");
end();
// Fail closed if the old session could still own an accepted frame. A
// later explicit begin retries teardown before accepting new traffic.
if (!_sdk_teardown_failed) begin();
return;
}
portENTER_CRITICAL(&_tx_mux);
dropped = _tx_dropped;
if (_tx_waiting && _tx_callback_done) {
completed = true;
completed_status = _tx_callback_status;
_tx_waiting = false;
_tx_callback_done = false;
if (_tx_count > 0) {
QueuedTransmit& queued_tx = _tx_queue[_tx_tail];
if (completed_status == ESP_NOW_SEND_SUCCESS) {
queued_tx.next_frame++;
queued_tx.start_retries = 0;
if (queued_tx.next_frame >= queued_tx.frames.count) {
_tx_tail = static_cast<uint8_t>(
(_tx_tail + 1) % TX_QUEUE_DEPTH);
_tx_count--;
}
} else if (queued_tx.start_retries < TX_MAX_START_RETRIES) {
// This completion is definite, unlike a missing callback. Retry the
// same frame within the shared start/callback budget; its sibling
// cannot be submitted until this frame succeeds.
++queued_tx.start_retries;
_tx_retry_at = now + TX_RETRY_DELAY_MS;
} else {
_tx_tail = static_cast<uint8_t>((_tx_tail + 1) % TX_QUEUE_DEPTH);
_tx_count--;
}
}
}
if (!_tx_waiting && _tx_count > 0
&& (_tx_queue[_tx_tail].start_retries == 0
|| static_cast<int32_t>(now - _tx_retry_at) >= 0)) {
QueuedTransmit& queued_tx = _tx_queue[_tx_tail];
if (queued_tx.next_frame < queued_tx.frames.count) {
frame_len = queued_tx.frames.lengths[queued_tx.next_frame];
if (frame_len > 0 && frame_len <= sizeof(frame)) {
memcpy(frame, queued_tx.frames.data[queued_tx.next_frame], frame_len);
} else {
frame_len = 0;
}
}
if (frame_len > 0) {
_tx_waiting = true;
_tx_callback_done = false;
_tx_started_at = now;
_tx_retry_at = 0;
}
}
portEXIT_CRITICAL(&_tx_mux);
if (dropped != _tx_dropped_reported) {
BRIDGE_DEBUG_PRINTLN("TX queue full, dropped=%lu\n",
(unsigned long)dropped);
_tx_dropped_reported = dropped;
}
if (completed && completed_status != ESP_NOW_SEND_SUCCESS) {
BRIDGE_DEBUG_PRINTLN("TX callback failed, status=%d\n",
completed_status);
}
if (frame_len == 0) return;
static const uint8_t broadcast_address[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
const esp_err_t result = esp_now_send(
broadcast_address, frame, frame_len);
if (result != ESP_OK) {
portENTER_CRITICAL(&_tx_mux);
_tx_waiting = false;
_tx_callback_done = false;
if (_tx_count > 0) {
QueuedTransmit& queued_tx = _tx_queue[_tx_tail];
if (result == ESP_ERR_ESPNOW_NO_MEM
&& queued_tx.start_retries < TX_MAX_START_RETRIES) {
++queued_tx.start_retries;
_tx_retry_at = now + TX_RETRY_DELAY_MS;
} else {
if (!queued_tx.started) {
unsent_packet = queued_tx.packet;
clear_unsent_seen = true;
}
_tx_tail = static_cast<uint8_t>((_tx_tail + 1) % TX_QUEUE_DEPTH);
_tx_count--;
_tx_retry_at = 0;
}
}
portEXIT_CRITICAL(&_tx_mux);
// Admission marks the packet to suppress duplicate queue entries. Restore
// the prior behavior when even its first esp_now_send() cannot start.
if (clear_unsent_seen) _seen_packets.clear(&unsent_packet);
BRIDGE_DEBUG_PRINTLN("TX failed to start, error=%d\n", (int)result);
} else {
portENTER_CRITICAL(&_tx_mux);
if (_tx_count > 0) _tx_queue[_tx_tail].started = true;
portEXIT_CRITICAL(&_tx_mux);
}
}
bool ESPNowBridge::xorCrypt(uint8_t *data, size_t len) {
size_t keyLen = 0;
while (keyLen < sizeof(_prefs->bridge_secret) && _prefs->bridge_secret[keyLen] != 0) keyLen++;
if (keyLen == 0 || keyLen == sizeof(_prefs->bridge_secret)) {
BRIDGE_DEBUG_PRINTLN("Invalid empty or unterminated bridge secret\n");
return false;
}
for (size_t i = 0; i < len; i++) {
data[i] ^= _prefs->bridge_secret[i % keyLen];
}
return true;
}
void ESPNowBridge::receiveMeshPacket(mesh::Packet* pkt, const uint8_t *data, size_t len) {
if (pkt->readFrom(data, len)) {
onPacketReceived(pkt);
} else {
_mgr->free(pkt);
}
}
void ESPNowBridge::queueReceivedFrame(const uint8_t *mac, const uint8_t *data,
int len) {
if (data == nullptr || len <= 0 || len > MAX_ESPNOW_PACKET_SIZE) {
return;
}
// Wrapped deployments share the WiFi channel with unrelated ESP-NOW users.
// Reject obvious foreign traffic before it can occupy a queue slot.
if (_active_format == mesh::bridge::ESPNOW_FORMAT_WRAPPED
&& (len < BRIDGE_MAGIC_SIZE
|| data[0] != static_cast<uint8_t>(BRIDGE_PACKET_MAGIC >> 8)
|| data[1] != static_cast<uint8_t>(BRIDGE_PACKET_MAGIC & 0xFF))) {
return;
}
portENTER_CRITICAL(&_rx_mux);
if (_rx_count < RX_QUEUE_DEPTH) {
memcpy(_rx_buffers[_rx_head], data, static_cast<size_t>(len));
_rx_lengths[_rx_head] = static_cast<uint16_t>(len);
if (mac != nullptr) {
memcpy(_rx_sources[_rx_head], mac,
mesh::espnow::ESPNOW_RAW_SOURCE_MAC_SIZE);
} else {
memset(_rx_sources[_rx_head], 0,
mesh::espnow::ESPNOW_RAW_SOURCE_MAC_SIZE);
}
_rx_head = static_cast<uint8_t>((_rx_head + 1) % RX_QUEUE_DEPTH);
_rx_count++;
} else {
_rx_dropped++;
}
portEXIT_CRITICAL(&_rx_mux);
}
bool ESPNowBridge::processReceivedFrame(mesh::Packet* packet_slot, const uint8_t *mac,
const uint8_t *data, size_t len) {
if (data == nullptr || len == 0 || len > MAX_ESPNOW_PACKET_SIZE) {
return false;
}
if (_active_format == mesh::bridge::ESPNOW_FORMAT_RAW) {
uint8_t packet[MAX_TRANS_UNIT];
size_t packet_len = 0;
const mesh::espnow::ESPNowRawReassemblyResult result =
_raw_reassembler.acceptFrame(
mac, data, len, millis(), packet, sizeof(packet), packet_len);
if (result == mesh::espnow::ESPNowRawReassemblyResult::PASSTHROUGH
|| result ==
mesh::espnow::ESPNowRawReassemblyResult::PACKET_COMPLETE) {
BRIDGE_DEBUG_PRINTLN("RX raw, payload_len=%u%s\n",
(unsigned)packet_len,
result == mesh::espnow::ESPNowRawReassemblyResult::PACKET_COMPLETE
? " (reassembled)" : "");
receiveMeshPacket(packet_slot, packet, packet_len);
return true;
} else if (result == mesh::espnow::ESPNowRawReassemblyResult::REJECTED
|| result ==
mesh::espnow::ESPNowRawReassemblyResult::OUTPUT_TOO_SMALL) {
BRIDGE_DEBUG_PRINTLN("RX raw fragment rejected, result=%u len=%u\n",
(unsigned)result, (unsigned)len);
}
return false;
}
// Wrapped mode is strict: ignore packets too small to contain its header
// and checksum rather than trying to auto-detect raw MeshCore traffic.
if (len < (BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE)) {
BRIDGE_DEBUG_PRINTLN("RX packet too small, len=%u\n", (unsigned)len);
return false;
}
// Check packet header magic
uint16_t received_magic = (data[0] << 8) | data[1];
if (received_magic != BRIDGE_PACKET_MAGIC) {
BRIDGE_DEBUG_PRINTLN("RX invalid magic 0x%04X\n", received_magic);
return false;
}
// Make a copy we can decrypt
uint8_t decrypted[MAX_ESPNOW_PACKET_SIZE];
const size_t encryptedDataLen = len - BRIDGE_MAGIC_SIZE;
memcpy(decrypted, data + BRIDGE_MAGIC_SIZE, encryptedDataLen);
// Try to decrypt (checksum + payload)
if (!xorCrypt(decrypted, encryptedDataLen)) return false;
// Validate checksum
uint16_t received_checksum = (decrypted[0] << 8) | decrypted[1];
const size_t payloadLen = encryptedDataLen - BRIDGE_CHECKSUM_SIZE;
if (!validateChecksum(decrypted + BRIDGE_CHECKSUM_SIZE, payloadLen, received_checksum)) {
// Failed to decrypt - likely from a different network
BRIDGE_DEBUG_PRINTLN("RX checksum mismatch, rcv=0x%04X\n", received_checksum);
return false;
}
BRIDGE_DEBUG_PRINTLN("RX, payload_len=%u\n", (unsigned)payloadLen);
receiveMeshPacket(packet_slot, decrypted + BRIDGE_CHECKSUM_SIZE, payloadLen);
return true;
}
void ESPNowBridge::onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
(void)mac_addr;
portENTER_CRITICAL(&_tx_mux);
if (_tx_waiting) {
_tx_callback_status = static_cast<int>(status);
_tx_callback_done = true;
}
portEXIT_CRITICAL(&_tx_mux);
}
void ESPNowBridge::sendPacket(mesh::Packet *packet) {
// Guard against uninitialized state
if (_initialized == false) {
return;
}
// First validate the packet pointer
if (!packet) {
BRIDGE_DEBUG_PRINTLN("TX invalid packet pointer\n");
return;
}
if (!allowsPacket(packet)) return;
// 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 =
mesh::bridge::espNowMaxMeshPacketSize(_active_format);
if (expectedMeshPacketLen < 0
|| (size_t)expectedMeshPacketLen > maxPayload) {
BRIDGE_DEBUG_PRINTLN("TX packet too large (payload=%d, max=%u)\n",
expectedMeshPacketLen, (unsigned)maxPayload);
return;
}
uint8_t buffer[MAX_TRANS_UNIT];
uint16_t meshPacketLen = packet->writeTo(buffer);
if (meshPacketLen != (uint16_t)expectedMeshPacketLen) {
BRIDGE_DEBUG_PRINTLN("TX packet length mismatch (actual=%d, expected=%d)\n", meshPacketLen,
expectedMeshPacketLen);
return;
}
size_t totalPacketSize = meshPacketLen;
if (_active_format != mesh::bridge::ESPNOW_FORMAT_RAW) {
const size_t packetOffset = BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE;
memmove(buffer + packetOffset, buffer, meshPacketLen);
// Write the legacy magic and encrypted checksum/payload wrapper.
buffer[0] = (BRIDGE_PACKET_MAGIC >> 8) & 0xFF;
buffer[1] = BRIDGE_PACKET_MAGIC & 0xFF;
const uint16_t checksum =
fletcher16(buffer + packetOffset, meshPacketLen);
buffer[2] = (checksum >> 8) & 0xFF;
buffer[3] = checksum & 0xFF;
if (!xorCrypt(buffer + BRIDGE_MAGIC_SIZE,
meshPacketLen + BRIDGE_CHECKSUM_SIZE)) {
return;
}
totalPacketSize = packetOffset + meshPacketLen;
}
mesh::espnow::ESPNowRawFrames frames;
bool encoded = false;
if (_active_format == mesh::bridge::ESPNOW_FORMAT_RAW) {
encoded = mesh::espnow::encodeEspNowRawFrames(
buffer, totalPacketSize, frames);
} else if (totalPacketSize <= MAX_ESPNOW_PACKET_SIZE) {
frames.count = 1;
frames.lengths[0] = static_cast<uint16_t>(totalPacketSize);
memcpy(frames.data[0], buffer, totalPacketSize);
encoded = true;
}
if (encoded && queueTransmitFrames(frames, *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);
} else {
BRIDGE_DEBUG_PRINTLN("TX queue/encode failed!\n");
}
}
}
void ESPNowBridge::onPacketReceived(mesh::Packet *packet) {
handleReceivedPacket(packet);
}
#endif