Support ESPNow and improve documentation

This commit is contained in:
João Brázio 2025-09-07 21:39:54 +01:00
parent cb99eb4ae8
commit 5b9d11ac8f
No known key found for this signature in database
GPG key ID: 56A1490716A324DD
6 changed files with 510 additions and 59 deletions

View file

@ -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

View file

@ -0,0 +1,184 @@
#include "ESPNowBridge.h"
#include <RTClib.h>
#include <WiFi.h>
#include <esp_wifi.h>
#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

View file

@ -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

View file

@ -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;

View file

@ -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;
/**

View file

@ -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}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/bridges/RS232Bridge.cpp>
+<../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}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/bridges/RS232Bridge.cpp>
+<../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}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/bridges/ESPNowBridge.cpp>
+<../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}