Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 41 additions & 37 deletions esp/mlrs-espnow-gcs/mlrs-espnow-gcs.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,16 +23,20 @@

#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
//#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)
Expand Down Expand Up @@ -82,6 +86,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];
Expand All @@ -102,13 +111,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 };
Expand All @@ -118,7 +129,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];

Expand All @@ -130,26 +140,23 @@ 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
wifi_set_channel(scan_channels[i]);
#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) {
led_tick_scanning();

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);
Expand All @@ -172,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();
Expand All @@ -194,16 +217,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
}


Expand All @@ -227,7 +241,6 @@ void setup()
latched_peer_added = false;

is_connected = false;
is_connected_tlast_ms = 0;
wifi_initialized = false;
}

Expand All @@ -241,16 +254,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();
Expand All @@ -262,12 +265,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
Expand All @@ -283,10 +285,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);
}
94 changes: 74 additions & 20 deletions esp/mlrs-wireless-bridge/mlrs-wireless-bridge.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand All @@ -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;
Expand Down