#include "ESPNowBridge.h" #include #include #include #ifdef WITH_ESPNOW_BRIDGE #include 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((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((_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((_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(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( (_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((_tx_tail + 1) % TX_QUEUE_DEPTH); _tx_count--; } } } if (!_tx_waiting && _tx_count > 0 && (_tx_queue[_tx_tail].start_retries == 0 || static_cast(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((_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(BRIDGE_PACKET_MAGIC >> 8) || data[1] != static_cast(BRIDGE_PACKET_MAGIC & 0xFF))) { return; } portENTER_CRITICAL(&_rx_mux); if (_rx_count < RX_QUEUE_DEPTH) { memcpy(_rx_buffers[_rx_head], data, static_cast(len)); _rx_lengths[_rx_head] = static_cast(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((_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(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(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