diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 3e2795d6..b78481d0 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -80,30 +80,36 @@ #ifdef WITH_RS232_BRIDGE #include "helpers/bridges/RS232Bridge.h" +#define WITH_BRIDGE #endif -#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS -#define REQ_TYPE_KEEP_ALIVE 0x02 -#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 +#ifdef WITH_ESPNOW_BRIDGE +#include "helpers/bridges/ESPNowBridge.h" +#define WITH_BRIDGE +#endif -#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ +#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS +#define REQ_TYPE_KEEP_ALIVE 0x02 +#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 -struct RepeaterStats { - uint16_t batt_milli_volts; - uint16_t curr_tx_queue_len; - int16_t noise_floor; - int16_t last_rssi; - uint32_t n_packets_recv; - uint32_t n_packets_sent; - uint32_t total_air_time_secs; - uint32_t total_up_time_secs; - uint32_t n_sent_flood, n_sent_direct; - uint32_t n_recv_flood, n_recv_direct; - uint16_t err_events; // was 'n_full_events' - int16_t last_snr; // x 4 - uint16_t n_direct_dups, n_flood_dups; - uint32_t total_rx_air_time_secs; -}; +#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ + + struct RepeaterStats { + uint16_t batt_milli_volts; + uint16_t curr_tx_queue_len; + int16_t noise_floor; + int16_t last_rssi; + uint32_t n_packets_recv; + uint32_t n_packets_sent; + uint32_t total_air_time_secs; + uint32_t total_up_time_secs; + uint32_t n_sent_flood, n_sent_direct; + uint32_t n_recv_flood, n_recv_direct; + uint16_t err_events; // was 'n_full_events' + int16_t last_snr; // x 4 + uint16_t n_direct_dups, n_flood_dups; + uint32_t total_rx_air_time_secs; + }; struct ClientInfo { mesh::Identity id; @@ -118,7 +124,7 @@ struct ClientInfo { #define MAX_CLIENTS 32 #endif -#ifdef WITH_RS232_BRIDGE +#ifdef WITH_BRIDGE AbstractBridge* bridge; #endif @@ -308,7 +314,7 @@ protected: } } void logTx(mesh::Packet* pkt, int len) override { -#ifdef WITH_RS232_BRIDGE +#ifdef WITH_BRIDGE bridge->onPacketTransmitted(pkt); #endif if (_logging) { @@ -576,8 +582,14 @@ public: : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), _cli(board, rtc, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) { -#ifdef WITH_RS232_BRIDGE +#ifdef WITH_BRIDGE +#if defined(WITH_RS232_BRIDGE) bridge = new RS232Bridge(WITH_RS232_BRIDGE, _mgr, &rtc); +#elif defined(WITH_ESPNOW_BRIDGE) + bridge = new ESPNowBridge(_mgr, &rtc); +#else +#error "You must choose either RS232 or ESPNow bridge" +#endif #endif memset(known_clients, 0, sizeof(known_clients)); next_local_advert = next_flood_advert = 0; @@ -779,7 +791,7 @@ public: } void loop() { -#ifdef WITH_RS232_BRIDGE +#ifdef WITH_BRIDGE bridge->loop(); #endif @@ -831,7 +843,7 @@ void setup() { Serial.begin(115200); delay(1000); -#ifdef WITH_RS232_BRIDGE +#ifdef WITH_BRIDGE bridge->begin(); #endif diff --git a/src/helpers/bridges/ESPNowBridge.cpp b/src/helpers/bridges/ESPNowBridge.cpp new file mode 100644 index 00000000..a470d521 --- /dev/null +++ b/src/helpers/bridges/ESPNowBridge.cpp @@ -0,0 +1,184 @@ +#include "ESPNowBridge.h" + +#include +#include +#include + +#ifdef WITH_ESPNOW_BRIDGE + +// Static member to handle callbacks +ESPNowBridge *ESPNowBridge::_instance = nullptr; + +// Static callback wrappers +void ESPNowBridge::recv_cb(const uint8_t *mac, const uint8_t *data, int len) { + if (_instance) { + _instance->onDataRecv(mac, data, len); + } +} + +void ESPNowBridge::send_cb(const uint8_t *mac, esp_now_send_status_t status) { + if (_instance) { + _instance->onDataSent(mac, status); + } +} + +// Fletcher16 checksum calculation +static uint16_t fletcher16(const uint8_t *data, size_t len) { + uint16_t sum1 = 0; + uint16_t sum2 = 0; + + while (len--) { + sum1 = (sum1 + *data++) % 255; + sum2 = (sum2 + sum1) % 255; + } + + return (sum2 << 8) | sum1; +} + +ESPNowBridge::ESPNowBridge(mesh::PacketManager *mgr, mesh::RTCClock *rtc) + : _mgr(mgr), _rtc(rtc), _rx_buffer_pos(0) { + _instance = this; +} + +void ESPNowBridge::begin() { + // Initialize WiFi in station mode + WiFi.mode(WIFI_STA); + + // Initialize ESP-NOW + if (esp_now_init() != ESP_OK) { + Serial.printf("%s: ESPNOW BRIDGE: Error initializing ESP-NOW\n", getLogDateTime()); + return; + } + + // Register callbacks + esp_now_register_recv_cb(recv_cb); + esp_now_register_send_cb(send_cb); + + // 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 = 0; + peerInfo.encrypt = false; + + if (esp_now_add_peer(&peerInfo) != ESP_OK) { + Serial.printf("%s: ESPNOW BRIDGE: Failed to add broadcast peer\n", getLogDateTime()); + return; + } +} + +void ESPNowBridge::loop() { + // Nothing to do here - ESP-NOW is callback based +} + +void ESPNowBridge::xorCrypt(uint8_t *data, size_t len) { + size_t keyLen = strlen(_secret); + for (size_t i = 0; i < len; i++) { + data[i] ^= _secret[i % keyLen]; + } +} + +void ESPNowBridge::onDataRecv(const uint8_t *mac, const uint8_t *data, int len) { + // Ignore packets that are too small + if (len < 3) { +#if MESH_PACKET_LOGGING + Serial.printf("%s: ESPNOW BRIDGE: RX packet too small, len=%d\n", getLogDateTime(), len); +#endif + return; + } + + // Check packet header magic + if (data[0] != ESPNOW_HEADER_MAGIC) { +#if MESH_PACKET_LOGGING + Serial.printf("%s: ESPNOW BRIDGE: RX invalid magic 0x%02X\n", getLogDateTime(), data[0]); +#endif + return; + } + + // Make a copy we can decrypt + uint8_t decrypted[MAX_ESPNOW_PACKET_SIZE]; + memcpy(decrypted, data + 1, len - 1); // Skip magic byte + + // Try to decrypt + xorCrypt(decrypted, len - 1); + + // Validate checksum + uint16_t received_checksum = (decrypted[0] << 8) | decrypted[1]; + uint16_t calculated_checksum = fletcher16(decrypted + 2, len - 3); + + if (received_checksum != calculated_checksum) { + // Failed to decrypt - likely from a different network +#if MESH_PACKET_LOGGING + Serial.printf("%s: ESPNOW BRIDGE: RX checksum mismatch, rcv=0x%04X calc=0x%04X\n", getLogDateTime(), + received_checksum, calculated_checksum); +#endif + return; + } + +#if MESH_PACKET_LOGGING + Serial.printf("%s: ESPNOW BRIDGE: RX, len=%d\n", getLogDateTime(), len - 3); +#endif + + // Create mesh packet + mesh::Packet *pkt = _instance->_mgr->allocNew(); + if (!pkt) return; + + if (pkt->readFrom(decrypted + 2, len - 3)) { + _instance->onPacketReceived(pkt); + } else { + _instance->_mgr->free(pkt); + } +} + +void ESPNowBridge::onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) { + // Could add transmission error handling here if needed +} + +void ESPNowBridge::onPacketReceived(mesh::Packet *packet) { + if (!_seen_packets.hasSeen(packet)) { + _mgr->queueInbound(packet, 0); + } else { + _mgr->free(packet); + } +} + +void ESPNowBridge::onPacketTransmitted(mesh::Packet *packet) { + if (!_seen_packets.hasSeen(packet)) { + uint8_t buffer[MAX_ESPNOW_PACKET_SIZE]; + buffer[0] = ESPNOW_HEADER_MAGIC; + + // Write packet to buffer starting after magic byte and checksum + uint16_t len = packet->writeTo(buffer + 3); + + // Calculate and add checksum + uint16_t checksum = fletcher16(buffer + 3, len); + buffer[1] = (checksum >> 8) & 0xFF; + buffer[2] = checksum & 0xFF; + + // Encrypt payload (not including magic byte) + xorCrypt(buffer + 1, len + 2); + + // Broadcast using ESP-NOW + uint8_t broadcastAddress[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; + esp_err_t result = esp_now_send(broadcastAddress, buffer, len + 3); + +#if MESH_PACKET_LOGGING + if (result == ESP_OK) { + Serial.printf("%s: ESPNOW BRIDGE: TX, len=%d\n", getLogDateTime(), len); + } else { + Serial.printf("%s: ESPNOW BRIDGE: TX FAILED!\n", getLogDateTime()); + } +#endif + } +} + +const char *ESPNowBridge::getLogDateTime() { + static char tmp[32]; + uint32_t now = _rtc->getCurrentTime(); + DateTime dt = DateTime(now); + sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(), + dt.year()); + return tmp; +} + +#endif diff --git a/src/helpers/bridges/ESPNowBridge.h b/src/helpers/bridges/ESPNowBridge.h new file mode 100644 index 00000000..7d2dbb0b --- /dev/null +++ b/src/helpers/bridges/ESPNowBridge.h @@ -0,0 +1,170 @@ +#pragma once + +#include "MeshCore.h" +#include "esp_now.h" +#include "helpers/AbstractBridge.h" +#include "helpers/SimpleMeshTables.h" + +#ifdef WITH_ESPNOW_BRIDGE + +#ifndef WITH_ESPNOW_BRIDGE_SECRET +#error WITH_ESPNOW_BRIDGE_SECRET must be defined to use ESPNowBridge +#endif + +/** + * @brief Bridge implementation using ESP-NOW protocol for packet transport + * + * This bridge enables mesh packet transport over ESP-NOW, a connectionless communication + * protocol provided by Espressif that allows ESP32 devices to communicate directly + * without WiFi router infrastructure. + * + * Features: + * - Broadcast-based communication (all bridges receive all packets) + * - Network isolation using XOR encryption with shared secret + * - Duplicate packet detection using SimpleMeshTables tracking + * - Maximum packet size of 250 bytes (ESP-NOW limitation) + * + * Packet Structure: + * [1 byte] Magic Header (0xAB) - Used to identify ESPNowBridge packets + * [2 bytes] Fletcher-16 checksum of encrypted payload (calculated over payload only) + * [n bytes] Encrypted payload containing the mesh packet + * + * The Fletcher-16 checksum is used to validate packet integrity and detect + * corrupted or tampered packets. It's calculated over the encrypted payload + * and provides a simple but effective way to verify packets are both + * uncorrupted and from the same network (since the checksum is calculated + * after encryption). + * + * Configuration: + * - Define WITH_ESPNOW_BRIDGE to enable this bridge + * - Define WITH_ESPNOW_BRIDGE_SECRET with a string to set the network encryption key + * + * Network Isolation: + * Multiple independent mesh networks can coexist by using different + * WITH_ESPNOW_BRIDGE_SECRET values. Packets encrypted with a different key will + * fail the checksum validation and be discarded. + */ +class ESPNowBridge : public AbstractBridge { +private: + static ESPNowBridge *_instance; + static void recv_cb(const uint8_t *mac, const uint8_t *data, int len); + static void send_cb(const uint8_t *mac, esp_now_send_status_t status); + + /** Packet manager for allocating and queuing mesh packets */ + mesh::PacketManager *_mgr; + + /** RTC clock for timestamping debug messages */ + mesh::RTCClock *_rtc; + + /** Tracks seen packets to prevent loops in broadcast communications */ + SimpleMeshTables _seen_packets; + + /** + * Maximum ESP-NOW packet size (250 bytes) + * This is a hardware limitation of ESP-NOW protocol: + * - ESP-NOW header: 20 bytes + * - Max payload: 250 bytes + * Source: ESP-NOW API documentation + */ + static const size_t MAX_ESPNOW_PACKET_SIZE = 250; + + /** + * Magic byte to identify ESPNowBridge packets (0xAB) + */ + static const uint8_t ESPNOW_HEADER_MAGIC = 0xAB; + + /** Buffer for receiving ESP-NOW packets */ + uint8_t _rx_buffer[MAX_ESPNOW_PACKET_SIZE]; + + /** Current position in receive buffer */ + size_t _rx_buffer_pos; + + /** + * Network encryption key from build define + * Must be defined with WITH_ESPNOW_BRIDGE_SECRET + * Used for XOR encryption to isolate different mesh networks + */ + const char *_secret = WITH_ESPNOW_BRIDGE_SECRET; + + /** + * Performs XOR encryption/decryption of data + * + * Uses WITH_ESPNOW_BRIDGE_SECRET as the key in a simple XOR operation. + * The same operation is used for both encryption and decryption. + * While not cryptographically secure, it provides basic network isolation. + * + * @param data Pointer to data to encrypt/decrypt + * @param len Length of data in bytes + */ + void xorCrypt(uint8_t *data, size_t len); + + /** + * ESP-NOW receive callback + * Called by ESP-NOW when a packet is received + * + * @param mac Source MAC address + * @param data Received data + * @param len Length of received data + */ + void onDataRecv(const uint8_t *mac, const uint8_t *data, int len); + + /** + * ESP-NOW send callback + * Called by ESP-NOW after a transmission attempt + * + * @param mac_addr Destination MAC address + * @param status Transmission status + */ + void onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status); + +public: + /** + * Constructs an ESPNowBridge instance + * + * @param mgr PacketManager for allocating and queuing packets + * @param rtc RTCClock for timestamping debug messages + */ + ESPNowBridge(mesh::PacketManager *mgr, mesh::RTCClock *rtc); + + /** + * Initializes the ESP-NOW bridge + * + * - Configures WiFi in station mode + * - Initializes ESP-NOW protocol + * - Registers callbacks + * - Sets up broadcast peer + */ + void begin() override; + + /** + * Main loop handler + * ESP-NOW is callback-based, so this is currently empty + */ + void loop() override; + + /** + * Called when a packet is received via ESP-NOW + * Queues the packet for mesh processing if not seen before + * + * @param packet The received mesh packet + */ + void onPacketReceived(mesh::Packet *packet) override; + + /** + * Called when a packet needs to be transmitted via ESP-NOW + * Encrypts and broadcasts the packet if not seen before + * + * @param packet The mesh packet to transmit + */ + void onPacketTransmitted(mesh::Packet *packet) override; + + /** + * Gets formatted date/time string for logging + * Format: "HH:MM:SS - DD/MM/YYYY U" + * + * @return Formatted date/time string + */ + const char *getLogDateTime(); +}; + +#endif diff --git a/src/helpers/bridges/RS232Bridge.cpp b/src/helpers/bridges/RS232Bridge.cpp index 801bcc5c..5c3b8caa 100644 --- a/src/helpers/bridges/RS232Bridge.cpp +++ b/src/helpers/bridges/RS232Bridge.cpp @@ -4,8 +4,9 @@ #ifdef WITH_RS232_BRIDGE -// Fletcher-16 -// https://en.wikipedia.org/wiki/Fletcher%27s_checksum +// Static Fletcher-16 checksum calculation +// Based on: https://en.wikipedia.org/wiki/Fletcher%27s_checksum +// Used to verify data integrity of received packets inline static uint16_t fletcher16(const uint8_t *bytes, const size_t len) { uint8_t sum1 = 0, sum2 = 0; diff --git a/src/helpers/bridges/RS232Bridge.h b/src/helpers/bridges/RS232Bridge.h index 0e99040f..2adeb503 100644 --- a/src/helpers/bridges/RS232Bridge.h +++ b/src/helpers/bridges/RS232Bridge.h @@ -7,28 +7,87 @@ #ifdef WITH_RS232_BRIDGE /** - * @brief A bridge implementation that uses a serial port to connect two mesh networks. + * @brief Bridge implementation using RS232/UART protocol for packet transport + * + * This bridge enables mesh packet transport over serial/UART connections, + * allowing nodes to communicate over wired serial links. It implements a simple + * packet framing protocol with checksums for reliable transfer. + * + * Features: + * - Point-to-point communication over hardware UART + * - Fletcher-16 checksum for data integrity verification + * - Magic header for packet synchronization + * - Configurable RX/TX pins via build defines + * - Baud rate fixed at 115200 + * + * Packet Structure: + * [2 bytes] Magic Header (0xCAFE) - Used to identify start of packet + * [2 bytes] Fletcher-16 checksum - Data integrity check + * [1 byte] Payload length + * [n bytes] Packet payload + * + * The Fletcher-16 checksum is used to validate packet integrity and detect + * corrupted or malformed packets. It provides error detection capabilities + * suitable for serial communication where noise or timing issues could + * corrupt data. + * + * Configuration: + * - Define WITH_RS232_BRIDGE to enable this bridge + * - Define WITH_RS232_BRIDGE_RX with the RX pin number + * - Define WITH_RS232_BRIDGE_TX with the TX pin number + * + * Platform Support: + * - ESP32 targets + * - NRF52 targets + * - RP2040 targets + * - STM32 targets */ class RS232Bridge : public AbstractBridge { public: /** - * @brief Construct a new Serial Bridge object - * - * @param serial The serial port to use for the bridge. - * @param mgr A pointer to the packet manager. - * @param rtc A pointer to the RTC clock. + * @brief Constructs an RS232Bridge instance + * + * @param serial The hardware serial port to use + * @param mgr PacketManager for allocating and queuing packets + * @param rtc RTCClock for timestamping debug messages */ RS232Bridge(Stream& serial, mesh::PacketManager* mgr, mesh::RTCClock* rtc); + + /** + * Initializes the RS232 bridge + * + * - Configures UART pins based on platform + * - Sets baud rate to 115200 + */ void begin() override; + + /** + * @brief Main loop handler + * Processes incoming serial data and builds packets + */ void loop() override; + + /** + * @brief Called when a packet needs to be transmitted over serial + * Formats and sends the packet with proper framing + * + * @param packet The mesh packet to transmit + */ void onPacketTransmitted(mesh::Packet* packet) override; + + /** + * @brief Called when a complete packet has been received from serial + * Queues the packet for mesh processing if checksum is valid + * + * @param packet The received mesh packet + */ void onPacketReceived(mesh::Packet* packet) override; private: + /** Helper function to get formatted timestamp for logging */ const char* getLogDateTime(); - /** - * @brief The 2-byte magic word used to signify the start of a packet. - */ + + /** Magic number to identify start of RS232 packets (0xCAFE) */ static constexpr uint16_t SERIAL_PKT_MAGIC = 0xCAFE; /** diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index 313b9844..5e9874c9 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -64,29 +64,6 @@ lib_deps = ${LilyGo_TLora_V2_1_1_6.lib_deps} ${esp32_ota.lib_deps} -[env:LilyGo_TLora_V2_1_1_6_repeater_bridge] -extends = LilyGo_TLora_V2_1_1_6 -build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} - + - + - +<../examples/simple_repeater> -build_flags = - ${LilyGo_TLora_V2_1_1_6.build_flags} - -D ADVERT_NAME='"TLora-V2.1-1.6 Bridge"' - -D ADVERT_LAT=0.0 - -D ADVERT_LON=0.0 - -D ADMIN_PASSWORD='"password"' - -D MAX_NEIGHBOURS=8 - -D WITH_RS232_BRIDGE=Serial2 - -D WITH_RS232_BRIDGE_RX=34 - -D WITH_RS232_BRIDGE_TX=25 - -D MESH_PACKET_LOGGING=1 -; -D MESH_DEBUG=1 -; -D CORE_DEBUG_LEVEL=3 -lib_deps = - ${LilyGo_TLora_V2_1_1_6.lib_deps} - ${esp32_ota.lib_deps} - [env:LilyGo_TLora_V2_1_1_6_terminal_chat] extends = LilyGo_TLora_V2_1_1_6 build_flags = @@ -179,3 +156,51 @@ build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} lib_deps = ${LilyGo_TLora_V2_1_1_6.lib_deps} densaugeo/base64 @ ~1.4.0 + +; +; Repeater Bridges +; +[env:LilyGo_TLora_V2_1_1_6_bridge_rs232] +extends = LilyGo_TLora_V2_1_1_6 +build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} + + + + + +<../examples/simple_repeater> +build_flags = + ${LilyGo_TLora_V2_1_1_6.build_flags} + -D ADVERT_NAME='"RS232 Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 + -D WITH_RS232_BRIDGE=Serial2 + -D WITH_RS232_BRIDGE_RX=34 + -D WITH_RS232_BRIDGE_TX=25 + -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +; -D CORE_DEBUG_LEVEL=3 +lib_deps = + ${LilyGo_TLora_V2_1_1_6.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_TLora_V2_1_1_6_bridge_espnow] +extends = LilyGo_TLora_V2_1_1_6 +build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} + + + + + +<../examples/simple_repeater> +build_flags = + ${LilyGo_TLora_V2_1_1_6.build_flags} + -D ADVERT_NAME='"ESPNow Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 + -D WITH_ESPNOW_BRIDGE=1 + -D WITH_ESPNOW_BRIDGE_SECRET='"shared-secret"' + -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +; -D CORE_DEBUG_LEVEL=3 +lib_deps = + ${LilyGo_TLora_V2_1_1_6.lib_deps} + ${esp32_ota.lib_deps} \ No newline at end of file