From 0ae2aac33441ed7cfcf59204c0c9686b2dbdc918 Mon Sep 17 00:00:00 2001 From: JLP Date: Sun, 29 Mar 2026 09:39:54 -0400 Subject: [PATCH 1/3] ESPNOW Multipeer --- esp/mlrs-espnow-gcs/mlrs-espnow-gcs.ino | 56 ++++------- .../mlrs-wireless-bridge.ino | 96 +++++++++++++++---- 2 files changed, 94 insertions(+), 58 deletions(-) diff --git a/esp/mlrs-espnow-gcs/mlrs-espnow-gcs.ino b/esp/mlrs-espnow-gcs/mlrs-espnow-gcs.ino index 911b3187b..6cc9437a9 100644 --- a/esp/mlrs-espnow-gcs/mlrs-espnow-gcs.ino +++ b/esp/mlrs-espnow-gcs/mlrs-espnow-gcs.ino @@ -8,7 +8,7 @@ // For use with ESP8266, ESP32, ESP32C3 and ESP32S3 modules. // To use USB on ESP32C3 and ESP32S3, 'USB CDC On Boot' must be enabled in Tools. //******************************************************** -// 2. Mar. 2026 +// 29. Mar. 2026 //******************************************************** #ifdef ESP8266 @@ -23,11 +23,9 @@ #define BAUD_RATE 115200 // baudrate for serial connection to GCS -// wifi channel — must match WIFI_CHANNEL on the bridge -// required for MSP systems, e.g. INAV -// MSP uses send/request, so the bridge won't transmit until polled -// for MAVLink systems this can be left commented out; scanning will find the bridge -//#define WIFI_CHANNEL 13 +#define BIND_PHRASE "mlrs.0" // must match the mLRS bind phrase + +//#define LISTEN_ONLY // uncomment to receive only; no serial data sent to bridge, only beacons //#define USE_SERIAL1 // uncomment to use Serial1 instead of USB Serial for ESP32C3 and ESP32S3 //#define TX_PIN 43 // Serial1 TX pin @@ -82,6 +80,11 @@ int espnow_rxbuf_pop(uint8_t* buf, int maxlen) return cnt; } +// beacon: sent periodically so the bridge discovers us even when GCS app is idle +// includes bind phrase so only matching systems pair +#define ESPNOW_BEACON_LEN 11 +const uint8_t espnow_beacon[ESPNOW_BEACON_LEN] = { 'm','L','R','S','-', BIND_PHRASE[0],BIND_PHRASE[1],BIND_PHRASE[2],BIND_PHRASE[3],BIND_PHRASE[4],BIND_PHRASE[5] }; + // mac latch: once a bridge sends us data, we lock to its MAC and register it as a peer volatile bool espnow_latched_mac_available; uint8_t espnow_latched_mac[6]; @@ -102,13 +105,15 @@ void espnow_recv_cb(const esp_now_recv_info_t* info, const uint8_t* data, int le #else const uint8_t* sender_mac = mac; #endif + bool is_beacon = (len == ESPNOW_BEACON_LEN && memcmp(data, "mLRS-", 5) == 0); + if (is_beacon && memcmp(data, espnow_beacon, ESPNOW_BEACON_LEN) != 0) return; // wrong bind phrase if (!espnow_latched_mac_available) { memcpy(espnow_latched_mac, sender_mac, 6); espnow_latched_mac_available = true; } else if (memcmp(sender_mac, espnow_latched_mac, 6) != 0) { return; // ignore other senders } - espnow_rxbuf_push(data, len); + if (!is_beacon) espnow_rxbuf_push(data, len); } uint8_t broadcast_mac[6] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; @@ -118,7 +123,6 @@ const uint8_t scan_channels[] = { 1, 6, 11, 13 }; bool is_connected; -unsigned long is_connected_tlast_ms; bool wifi_initialized; uint8_t buf[250]; @@ -130,11 +134,6 @@ uint8_t buf[250]; // scan channels until we hear from the bridge void scan_for_bridge(void) { - if (espnow_latched_mac_available) { - esp_now_del_peer(espnow_latched_mac); - espnow_latched_mac_available = false; - latched_peer_added = false; - } while (true) { for (int i = 0; i < (int)sizeof(scan_channels); i++) { #ifdef ESP8266 @@ -142,6 +141,8 @@ void scan_for_bridge(void) #else esp_wifi_set_channel(scan_channels[i], WIFI_SECOND_CHAN_NONE); #endif + // beacon on this channel so the bridge discovers us + esp_now_send(broadcast_mac, (uint8_t*)espnow_beacon, ESPNOW_BEACON_LEN); unsigned long t = millis(); while (millis() - t < 500) { @@ -149,7 +150,7 @@ void scan_for_bridge(void) while (SERIAL_PORT.available()) SERIAL_PORT.read(); // dump data while disconnected - if (espnow_rxbuf_head != espnow_rxbuf_tail) { + if (espnow_latched_mac_available) { return; } delay(1); @@ -194,16 +195,7 @@ void setup_wifi(void) esp_now_add_peer(&peer); #endif -#ifdef WIFI_CHANNEL - // fixed channel — skip scanning, required for MSP (send/request) systems -#ifdef ESP8266 - wifi_set_channel(WIFI_CHANNEL); -#else - esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE); -#endif -#else scan_for_bridge(); -#endif } @@ -227,7 +219,6 @@ void setup() latched_peer_added = false; is_connected = false; - is_connected_tlast_ms = 0; wifi_initialized = false; } @@ -241,16 +232,6 @@ void loop() return; } - unsigned long tnow_ms = millis(); - - // connection timeout - if (is_connected && (tnow_ms - is_connected_tlast_ms > 2000)) { - is_connected = false; -#ifndef WIFI_CHANNEL - scan_for_bridge(); -#endif - } - // LED: blink pattern based on connection state if (is_connected) { led_tick_connected(); @@ -262,12 +243,11 @@ void loop() int len = espnow_rxbuf_pop(buf, sizeof(buf)); if (len > 0) { SERIAL_PORT.write(buf, len); - is_connected = true; - is_connected_tlast_ms = tnow_ms; } - // register latched peer for unicast TX + // register latched peer for unicast TX and mark connected if (espnow_latched_mac_available && !latched_peer_added) { + is_connected = true; #ifdef ESP8266 esp_now_add_peer(espnow_latched_mac, ESP_NOW_ROLE_COMBO, 0, NULL, 0); #else @@ -283,10 +263,12 @@ void loop() while (SERIAL_PORT.available() && rlen < (int)sizeof(buf)) { buf[rlen++] = SERIAL_PORT.read(); } +#ifndef LISTEN_ONLY if (rlen > 0) { uint8_t* dest = espnow_latched_mac_available ? espnow_latched_mac : broadcast_mac; esp_now_send(dest, buf, rlen); } +#endif delay(2); } diff --git a/esp/mlrs-wireless-bridge/mlrs-wireless-bridge.ino b/esp/mlrs-wireless-bridge/mlrs-wireless-bridge.ino index 44d0384c9..1707f70a9 100644 --- a/esp/mlrs-wireless-bridge/mlrs-wireless-bridge.ino +++ b/esp/mlrs-wireless-bridge/mlrs-wireless-bridge.ino @@ -7,7 +7,7 @@ // Basic but effective & reliable transparent WiFi or Bluetooth <-> serial bridge. // Minimizes wireless traffic while respecting latency by better packeting algorithm. //******************************************************* -// 21. Mar. 2026 +// 29. Mar. 2026 //*********************************************************/ // inspired by examples from Arduino // NOTES: @@ -63,7 +63,7 @@ Troubleshooting: // (you also need to set the board in the Arduino IDE accordingly) //#define MODULE_MATEK_MTX_DB30 // board: ESP32 PICO-D4 //#define MODULE_ESP82XX_ELRS_TX // board: Generic ESP8266 Module or Generic ESP8285 Module -//#define MODULE_ESP32C3_ELRS_TX // board: ESP32C3 Dev Module +#define MODULE_ESP32C3_ELRS_TX // board: ESP32C3 Dev Module //#define MODULE_ESP32_DEVKITC_V4 // board: ESP32 Dev Module //#define MODULE_NODEMCU_ESP32_WROOM32 // board: ESP32 Dev Module //#define MODULE_ESP32_PICO_KIT // board: ESP32 PICO-D4 @@ -368,9 +368,23 @@ void ble_setup(String device_name) { uint8_t espnow_rxbuf[ESPNOW_RXBUF_SIZE]; volatile uint16_t espnow_rxbuf_head; volatile uint16_t espnow_rxbuf_tail; -volatile bool espnow_gcs_mac_available; // once a GCS sends us data, we lock to its MAC -uint8_t espnow_gcs_mac[6]; -bool espnow_gcs_peer_added; + +// beacon: includes bind phrase so only matching systems pair +#define ESPNOW_BEACON_LEN 11 +uint8_t espnow_beacon[ESPNOW_BEACON_LEN] = { 'm','L','R','S','-', 0,0,0,0,0,0 }; + +// multi-peer: track up to 4 GCS peers +#define ESPNOW_MAX_PEERS 4 + +struct espnow_peer_t { + uint8_t mac[6]; + bool registered; // esp_now_add_peer() done (main loop only) + volatile bool active; // slot occupied (set by callback, read by main loop) + volatile bool confirm_needed; // set by callback, cleared by main loop after sending confirm beacon +}; + +volatile espnow_peer_t espnow_peers[ESPNOW_MAX_PEERS]; + uint8_t espnow_broadcast_mac[6] = { 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF }; void espnow_rxbuf_push(const uint8_t* data, int len) { @@ -392,13 +406,30 @@ int espnow_rxbuf_pop(uint8_t* buf, int maxlen) { } void espnow_recv_callback(const uint8_t* sender_mac, const uint8_t* data, int len) { - if (!espnow_gcs_mac_available) { // accept only from the first sender we hear from - memcpy(espnow_gcs_mac, sender_mac, 6); - espnow_gcs_mac_available = true; - } else { - if (memcmp(sender_mac, espnow_gcs_mac, 6) != 0) return; // ignore other senders + bool is_beacon = (len == ESPNOW_BEACON_LEN && memcmp(data, "mLRS-", 5) == 0); + if (is_beacon && memcmp(data, espnow_beacon, ESPNOW_BEACON_LEN) != 0) return; // wrong bind phrase + // known peer + for (int i = 0; i < ESPNOW_MAX_PEERS; i++) { + if (espnow_peers[i].active && memcmp((void*)espnow_peers[i].mac, sender_mac, 6) == 0) { + if (is_beacon) { + espnow_peers[i].confirm_needed = true; + } else { + espnow_rxbuf_push(data, len); + } + return; + } + } + // unknown — claim first free slot + for (int i = 0; i < ESPNOW_MAX_PEERS; i++) { + if (!espnow_peers[i].active) { + memcpy((void*)espnow_peers[i].mac, sender_mac, 6); + espnow_peers[i].registered = false; + espnow_peers[i].confirm_needed = true; + espnow_peers[i].active = true; // publish last — main loop reads this + if (!is_beacon) espnow_rxbuf_push(data, len); + return; + } } - espnow_rxbuf_push(data, len); } #ifdef ESP8266 @@ -419,10 +450,27 @@ void espnow_add_peer_mac(uint8_t* mac, int wifi_channel) { #endif } +void espnow_register_peers(int wifi_channel) { + for (int i = 0; i < ESPNOW_MAX_PEERS; i++) { + if (!espnow_peers[i].active) continue; + if (!espnow_peers[i].registered) { + espnow_add_peer_mac((uint8_t*)espnow_peers[i].mac, wifi_channel); + espnow_peers[i].registered = true; + } + if (espnow_peers[i].confirm_needed) { + espnow_peers[i].confirm_needed = false; + esp_now_send((uint8_t*)espnow_peers[i].mac, (uint8_t*)espnow_beacon, ESPNOW_BEACON_LEN); + } + } +} + void espnow_setup(int wifi_channel) { espnow_rxbuf_head = espnow_rxbuf_tail = 0; - espnow_gcs_mac_available = false; - espnow_gcs_peer_added = false; + for (int i = 0; i < ESPNOW_MAX_PEERS; i++) { + espnow_peers[i].active = false; + espnow_peers[i].registered = false; + espnow_peers[i].confirm_needed = false; + } WiFi.mode(WIFI_STA); WiFi.disconnect(); #ifdef ESP8266 @@ -448,14 +496,15 @@ void espnow_setup(int wifi_channel) { DBG_PRINTLN("ESP-NOW started"); } -void espnow_send(int wifi_channel, uint8_t* buf, int len) { - if (espnow_gcs_mac_available) { // if gcs seen, send unicast; otherwise broadcast - if (!espnow_gcs_peer_added) { // ensure latched peer is registered - espnow_add_peer_mac(espnow_gcs_mac, wifi_channel); - espnow_gcs_peer_added = true; +void espnow_send(uint8_t* buf, int len) { + bool any = false; + for (int i = 0; i < ESPNOW_MAX_PEERS; i++) { + if (espnow_peers[i].active && espnow_peers[i].registered) { + esp_now_send((uint8_t*)espnow_peers[i].mac, buf, len); + any = true; } - esp_now_send(espnow_gcs_mac, buf, len); - } else { + } + if (!any) { esp_now_send(espnow_broadcast_mac, buf, len); } } @@ -1059,6 +1108,10 @@ class tESPNOWHandler : public tWifiHandler { void Init() { tWifiHandler::Init(); device_name = device_name + " ESPNOW"; + // populate beacon with bind phrase + for (int i = 0; i < 6 && i < (int)g_bindphrase.length(); i++) { + espnow_beacon[5 + i] = g_bindphrase[i]; + } } void wifi_setup() override { @@ -1068,6 +1121,7 @@ class tESPNOWHandler : public tWifiHandler { void Loop(uint8_t* buf, int sizeofbuf) override { if (sizeofbuf > 250) sizeofbuf = 250; // cap at 250 bytes (esp-now max payload) + espnow_register_peers(g_wifichannel); int len = espnow_rxbuf_pop(buf, sizeofbuf); if (len > 0) { SERIAL.write(buf, len); @@ -1077,7 +1131,7 @@ class tESPNOWHandler : public tWifiHandler { } void wifi_write(uint8_t* buf, int len) override { - espnow_send(g_wifichannel, buf, len); + espnow_send(buf, len); } }; tESPNOWHandler espnow_handler; From edec745e098f932fdff06a522eff84e4f00679b6 Mon Sep 17 00:00:00 2001 From: JLP Date: Sun, 29 Mar 2026 10:15:36 -0400 Subject: [PATCH 2/3] No Target --- esp/mlrs-wireless-bridge/mlrs-wireless-bridge.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esp/mlrs-wireless-bridge/mlrs-wireless-bridge.ino b/esp/mlrs-wireless-bridge/mlrs-wireless-bridge.ino index 1707f70a9..fedcc35b6 100644 --- a/esp/mlrs-wireless-bridge/mlrs-wireless-bridge.ino +++ b/esp/mlrs-wireless-bridge/mlrs-wireless-bridge.ino @@ -63,7 +63,7 @@ Troubleshooting: // (you also need to set the board in the Arduino IDE accordingly) //#define MODULE_MATEK_MTX_DB30 // board: ESP32 PICO-D4 //#define MODULE_ESP82XX_ELRS_TX // board: Generic ESP8266 Module or Generic ESP8285 Module -#define MODULE_ESP32C3_ELRS_TX // board: ESP32C3 Dev Module +//#define MODULE_ESP32C3_ELRS_TX // board: ESP32C3 Dev Module //#define MODULE_ESP32_DEVKITC_V4 // board: ESP32 Dev Module //#define MODULE_NODEMCU_ESP32_WROOM32 // board: ESP32 Dev Module //#define MODULE_ESP32_PICO_KIT // board: ESP32 PICO-D4 From cc4b439d0d40ea09d56ea65b8858a12a71e43a02 Mon Sep 17 00:00:00 2001 From: JLP Date: Mon, 30 Mar 2026 11:30:35 -0400 Subject: [PATCH 3/3] Add Power Define --- esp/mlrs-espnow-gcs/mlrs-espnow-gcs.ino | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/esp/mlrs-espnow-gcs/mlrs-espnow-gcs.ino b/esp/mlrs-espnow-gcs/mlrs-espnow-gcs.ino index 6cc9437a9..ab7c29e87 100644 --- a/esp/mlrs-espnow-gcs/mlrs-espnow-gcs.ino +++ b/esp/mlrs-espnow-gcs/mlrs-espnow-gcs.ino @@ -31,6 +31,12 @@ //#define TX_PIN 43 // Serial1 TX pin //#define RX_PIN 44 // Serial1 RX pin +// WiFi transmit power +// Uncomment one of the three power levels: +//#define WIFI_POWER_LOW // lowest power: -1 dBm (ESP32) / 0 dBm (ESP8266) +#define WIFI_POWER_MED // medium power: 5 dBm +//#define WIFI_POWER_MAX // maximum power: 19.5 dBm (ESP32) / 20.5 dBm (ESP8266) + //#define DEVICE_HAS_SINGLE_LED // uncomment for single on/off LED //#define DEVICE_HAS_SINGLE_LED_RGB // uncomment for single RGB (NeoPixel/WS2812) LED //#define LED_IO 8 // LED pin (comment out to disable) @@ -173,12 +179,28 @@ void setup_wifi(void) #ifdef ESP8266 // force 11b only for best reliability wifi_set_phy_mode(PHY_MODE_11B); + // set transmit power + #if defined(WIFI_POWER_MAX) + WiFi.setOutputPower(20.5); + #elif defined(WIFI_POWER_MED) + WiFi.setOutputPower(5); + #else + WiFi.setOutputPower(0); + #endif #else // set country to EU to enable channels 1-13 (default may restrict to 1-11) wifi_country_t country = { .cc = "EU", .schan = 1, .nchan = 13, .policy = WIFI_COUNTRY_POLICY_MANUAL }; esp_wifi_set_country(&country); // force 11b only for best reliability esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_11B); + // set transmit power + #if defined(WIFI_POWER_MAX) + WiFi.setTxPower(WIFI_POWER_19_5dBm); + #elif defined(WIFI_POWER_MED) + WiFi.setTxPower(WIFI_POWER_5dBm); + #else + WiFi.setTxPower(WIFI_POWER_MINUS_1dBm); + #endif #endif esp_now_init();