From fdcc9533b3a1f797680690c429a770daa24968d0 Mon Sep 17 00:00:00 2001 From: Oleg Kalachev Date: Thu, 21 May 2026 10:48:31 +0300 Subject: [PATCH] Implement ESP-NOW support (#40) --- Makefile | 4 ++ README.md | 4 +- docs/img/espnow-connection.jpg | Bin 0 -> 47299 bytes docs/usage.md | 38 ++++++++++-- flix/cli.ino | 8 ++- flix/parameters.ino | 5 +- flix/util.h | 11 ++++ flix/wifi.ino | 81 +++++++++++++++++++------ gazebo/ESP32_NOW_Serial.h | 12 ++++ tools/espnow-proxy/README.md | 3 + tools/espnow-proxy/espnow-proxy.ino | 88 ++++++++++++++++++++++++++++ 11 files changed, 227 insertions(+), 27 deletions(-) create mode 100644 docs/img/espnow-connection.jpg create mode 100644 gazebo/ESP32_NOW_Serial.h create mode 100644 tools/espnow-proxy/README.md create mode 100644 tools/espnow-proxy/espnow-proxy.ino diff --git a/Makefile b/Makefile index 5dfc9e6..de670c4 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,10 @@ dependencies .dependencies: arduino-cli lib install "MAVLink"@2.0.25 touch .dependencies +upload_proxy: .dependencies + arduino-cli compile --fqbn $(BOARD) tools/espnow-proxy + arduino-cli upload --fqbn $(BOARD) -p "$(PORT)" tools/espnow-proxy + gazebo/build cmake: gazebo/CMakeLists.txt mkdir -p gazebo/build cd gazebo/build && cmake .. diff --git a/README.md b/README.md index 649c559..c7da620 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ * Dedicated for education and research. * Made from general-purpose components. * Simple and clean source code in Arduino (<2k lines firmware). -* Connectivity using Wi-Fi and MAVLink protocol. -* Control using USB gamepad, remote control or smartphone. +* Communication using MAVLink protocol over Wi-Fi or ESP-NOW. +* Control with USB gamepad, remote control or smartphone. * Wireless command line interface and analyzing. * Precise simulation with Gazebo. * Python library for scripting and automatic flights. diff --git a/docs/img/espnow-connection.jpg b/docs/img/espnow-connection.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fdd6a0e60c8469bdbb581f1bc0dcffb75afa7e35 GIT binary patch literal 47299 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 Jzkt6>{|%o=b}9e> literal 0 HcmV?d00001 diff --git a/docs/usage.md b/docs/usage.md index 39f9da4..5984a32 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -290,11 +290,8 @@ The Wi-Fi mode is chosen using `WIFI_MODE` parameter in QGroundControl or in the * `0` — Wi-Fi is disabled. * `1` — Access Point mode *(AP)* — the drone creates a Wi-Fi network. -* `2` — Client mode *(STA)* — the drone connects to an existing Wi-Fi network. -* `3` — *ESP-NOW (not implemented yet)*. - -> [!WARNING] -> Tests showed that Client mode may cause **additional delays** in remote control (due to retranslations), so it's generally not recommended. +* `2` — Client mode *(STA)* — the drone connects to an existing Wi-Fi network (may cause additional delays, so generally not recommended). +* `3` — ESP-NOW mode — the drone uses ESP-NOW protocol for communication. The SSID and password are configured using the `ap` and `sta` console commands: @@ -316,6 +313,37 @@ Disabling Wi-Fi: p WIFI_MODE 0 ``` +### Using ESP-NOW + +[ESP-NOW](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/network/esp_now.html) is a low level wireless communication protocol. It can provide lower latency, better reliability, and longer range than Wi-Fi. However, it requires a second ESP32 board to be used as a proxy for the computer. + + + +To setup ESP-NOW communication: + +1. Flash the second ESP32 board with ESP-NOW proxy sketch: [`tools/espnow-proxy/espnow-proxy.ino`](../tools/espnow-proxy/espnow-proxy.ino). Use Arduino IDE or command line: `make upload_proxy`. + +2. Open Serial Monitor or use `make monitor` command. The ESP32 will print its MAC address and generated encryption key, for example: + + ``` + espnow 7a:c8:e3:eb:bf:e9 &PiuSysxP9+$L&5E + ``` + + Run this line as a console command on each drone you want to bind to this proxy board. + +3. Set the `WIFI_MODE` parameter to `3` on the drone: + + ``` + p WIFI_MODE 3 + ``` + +4. Go to the QGroundControl menu ⇒ *Application Settings* ⇒ *Comm Links*, add new link with the following settings: + * Name: ESP32. + * Type: Serial. + * Serial Port: choose the port of the proxy ESP32 board, e. g. `/dev/cu.usbserial-0001`. + * Baud Rate: 115200. +5. Click *Save*. QGroundControl should connect to the drone using ESP-NOW and begin showing the telemetry. + ## Flight log After the flight, you can download the flight log for analysis wirelessly. Use the following command on your computer for that: diff --git a/flix/cli.ino b/flix/cli.ino index 6be66c0..7922f1a 100644 --- a/flix/cli.ino +++ b/flix/cli.ino @@ -10,6 +10,7 @@ extern const int MOTOR_REAR_LEFT, MOTOR_REAR_RIGHT, MOTOR_FRONT_RIGHT, MOTOR_FRONT_LEFT; extern const int RAW, ACRO, STAB, AUTO; +extern const int W_AP, W_STA, W_ESPNOW; extern float t, dt, loopRate; extern uint16_t channels[16]; extern float controlTime; @@ -45,6 +46,7 @@ const char* motd = "wifi - show Wi-Fi info\n" "ap - setup Wi-Fi access point\n" "sta - setup Wi-Fi client mode\n" +"espnow [] - setup ESP-NOW peer\n" "mot - show motor output\n" "log [dump] - print log header [and data]\n" "cr - calibrate RC\n" @@ -143,9 +145,11 @@ void doCommand(String str, bool echo = false) { } else if (command == "wifi") { printWiFiInfo(); } else if (command == "ap") { - configWiFi(true, arg0.c_str(), arg1.c_str()); + configWiFi(W_AP, arg0.c_str(), arg1.c_str()); } else if (command == "sta") { - configWiFi(false, arg0.c_str(), arg1.c_str()); + configWiFi(W_STA, arg0.c_str(), arg1.c_str()); + } else if (command == "espnow") { + configWiFi(W_ESPNOW, arg0.c_str(), arg1.c_str()); } else if (command == "mot") { print("front-right %g front-left %g rear-right %g rear-left %g\n", motors[MOTOR_FRONT_RIGHT], motors[MOTOR_FRONT_LEFT], motors[MOTOR_REAR_RIGHT], motors[MOTOR_REAR_LEFT]); diff --git a/flix/parameters.ino b/flix/parameters.ino index d0d88bd..6533110 100644 --- a/flix/parameters.ino +++ b/flix/parameters.ino @@ -10,7 +10,7 @@ extern int channelZero[16]; extern int channelMax[16]; extern int rollChannel, pitchChannel, throttleChannel, yawChannel, armedChannel, modeChannel; extern int rcRxPin; -extern int wifiMode, udpLocalPort, udpRemotePort; +extern int wifiMode, wifiLongRange, udpLocalPort, udpRemotePort, espnowChannel; extern float rcLossTimeout, descendTime; extern int voltagePin; extern float voltageScale; @@ -112,6 +112,9 @@ Parameter parameters[] = { {"WIFI_MODE", &wifiMode}, {"WIFI_PORT_LOC", &udpLocalPort}, {"WIFI_PORT_REM", &udpRemotePort}, + {"WIFI_LONG_RANGE", &wifiLongRange}, + // espnow + {"ESPNOW_CHANNEL", &espnowChannel}, // mavlink {"MAV_SYS_ID", &mavlinkSysId}, {"MAV_RATE_SLOW", &telemetrySlow.rate}, diff --git a/flix/util.h b/flix/util.h index cda50f3..f721f4a 100644 --- a/flix/util.h +++ b/flix/util.h @@ -6,6 +6,7 @@ #pragma once #include +#include const float ONE_G = 9.80665; extern float t; @@ -46,6 +47,16 @@ void splitString(String& str, String& token0, String& token1, String& token2) { if (token2.c_str() == NULL) token2 = ""; } +// Simplified ESP-NOW Serial without tx buffering and resends +class ESPNOWSerial : public ESP_NOW_Serial_Class { +public: + using ESP_NOW_Serial_Class::ESP_NOW_Serial_Class; + void onSent(bool success) override {} // disable resends + size_t write(const uint8_t *data, size_t len) override { + return ESP_NOW_Peer::send(data, len); // pure send without buffering + } +}; + // Rate limiter class Rate { public: diff --git a/flix/wifi.ino b/flix/wifi.ino index c0aa119..5c87828 100644 --- a/flix/wifi.ino +++ b/flix/wifi.ino @@ -1,82 +1,129 @@ // Copyright (c) 2023 Oleg Kalachev // Repository: https://github.com/okalachev/flix -// Wi-Fi communication +// Wi-Fi and ESP-NOW communication #include #include #include +#include +#include #include "Preferences.h" +#include "util.h" extern Preferences storage; // use the main preferences storage -const int W_DISABLED = 0, W_AP = 1, W_STA = 2; +const int W_DISABLED = 0, W_AP = 1, W_STA = 2, W_ESPNOW = 3; int wifiMode = W_AP; + +int wifiLongRange = 0; int udpLocalPort = 14550; int udpRemotePort = 14550; IPAddress udpRemoteIP = "255.255.255.255"; - WiFiUDP udp; +ESPNOWSerial espnow(NULL, 0, WIFI_IF_AP); +ESPNOWSerial espnowBroadcast(ESP_NOW.BROADCAST_ADDR, 0, WIFI_IF_AP); +int espnowChannel = 6; + void setupWiFi() { print("Setup Wi-Fi\n"); + WiFi.enableLongRange(wifiLongRange); + if (wifiMode == W_AP) { WiFi.softAP(storage.getString("WIFI_AP_SSID", "flix").c_str(), storage.getString("WIFI_AP_PASS", "flixwifi").c_str()); + udp.begin(udpLocalPort); } else if (wifiMode == W_STA) { WiFi.begin(storage.getString("WIFI_STA_SSID", "").c_str(), storage.getString("WIFI_STA_PASS", "").c_str()); - } else { - return; + udp.begin(udpLocalPort); + } else if (wifiMode == W_ESPNOW) { + WiFi.mode(WIFI_AP); + WiFi.setChannel(espnowChannel); + espnow.addr(MacAddress(storage.getString("ESPNOW_PEER_MAC", "FF:FF:FF:FF:FF:FF").c_str())); + String key = storage.getString("ESPNOW_PEER_KEY", ""); + espnow.setKey(key.isEmpty() ? nullptr : (const uint8_t *)key.c_str()); + espnow.begin(); + espnowBroadcast.begin(); } + WiFi.setSleep(false); // disable power save - udp.begin(udpLocalPort); } void sendWiFi(const uint8_t *buf, int len) { + if (espnow) { + espnow.write(buf, len); + static Rate discovery(2); + if (discovery) espnowBroadcast.write((const uint8_t *)"flix", 4); // broadcast message to help finding this device + return; + } + if (WiFi.softAPgetStationNum() == 0 && !WiFi.isConnected()) return; + udp.beginPacket(udpRemoteIP, udpRemotePort); udp.write(buf, len); udp.endPacket(); } int receiveWiFi(uint8_t *buf, int len) { + if (espnow) { + return espnow.read(buf, len); + } + if (WiFi.softAPgetStationNum() == 0 && !WiFi.isConnected()) return 0; + udp.parsePacket(); if (udp.remoteIP()) udpRemoteIP = udp.remoteIP(); return udp.read(buf, len); } void printWiFiInfo() { - if (WiFi.getMode() == WIFI_MODE_AP) { + if (espnow) { + print("Mode: ESP-NOW\n"); + print("ESP-NOW version: %d\n", ESP_NOW.getVersion()); + print("Max packet size: %d\n", ESP_NOW.getMaxDataLen()); + print("MAC: %s\n", WiFi.softAPmacAddress().c_str()); + print("Peer MAC: %s\n", MacAddress(espnow.addr()).toString().c_str()); + print("Encrypted: %d\n", espnow.isEncrypted()); + print("Channel: %d\n", espnow.getChannel()); + } else if (WiFi.getMode() == WIFI_MODE_AP) { print("Mode: Access Point (AP)\n"); print("MAC: %s\n", WiFi.softAPmacAddress().c_str()); print("SSID: %s\n", WiFi.softAPSSID().c_str()); print("Password: ***\n"); + print("Channel: %d\n", WiFi.channel()); print("Clients: %d\n", WiFi.softAPgetStationNum()); print("IP: %s\n", WiFi.softAPIP().toString().c_str()); + print("Remote IP: %s\n", udpRemoteIP.toString().c_str()); } else if (WiFi.getMode() == WIFI_MODE_STA) { print("Mode: Client (STA)\n"); print("Connected: %d\n", WiFi.isConnected()); print("MAC: %s\n", WiFi.macAddress().c_str()); print("SSID: %s\n", WiFi.SSID().c_str()); print("Password: ***\n"); - print("IP: %s\n", WiFi.localIP().toString().c_str()); + print("Channel: %d\n", WiFi.channel()); print("RSSI: %d dBm\n", WiFi.RSSI()); + print("IP: %s\n", WiFi.localIP().toString().c_str()); + print("Remote IP: %s\n", udpRemoteIP.toString().c_str()); } else { print("Mode: Disabled\n"); - return; } - print("Channel: %d\n", WiFi.channel()); - print("Remote IP: %s\n", udpRemoteIP.toString().c_str()); print("MAVLink connected: %d\n", mavlinkConnected); } -void configWiFi(bool ap, const char *ssid, const char *password) { - if (ap) { - storage.putString("WIFI_AP_SSID", ssid); - storage.putString("WIFI_AP_PASS", password); +void configWiFi(int mode, const char *first, const char *second) { + MacAddress mac; + if (mode == W_AP && strlen(first) > 0 && strlen(second) >= 8) { + storage.putString("WIFI_AP_SSID", first); + storage.putString("WIFI_AP_PASS", second); + } else if (mode == W_STA && strlen(first) > 0 && strlen(second) >= 8) { + storage.putString("WIFI_STA_SSID", first); + storage.putString("WIFI_STA_PASS", second); + } else if (mode == W_ESPNOW && mac.fromString(first)) { + storage.putString("ESPNOW_PEER_MAC", first); + storage.putString("ESPNOW_PEER_KEY", strlen(second) == ESP_NOW_KEY_LEN ? second : ""); } else { - storage.putString("WIFI_STA_SSID", ssid); - storage.putString("WIFI_STA_PASS", password); + print("Invalid configuration\n"); + return; } print("✓ Reboot to apply new settings\n"); } diff --git a/gazebo/ESP32_NOW_Serial.h b/gazebo/ESP32_NOW_Serial.h new file mode 100644 index 0000000..1d56c35 --- /dev/null +++ b/gazebo/ESP32_NOW_Serial.h @@ -0,0 +1,12 @@ +// Dummy file for the simulator + +class ESP_NOW_Peer { +protected: + size_t send(const uint8_t *data, int len) { return 0; } +}; + +class ESP_NOW_Serial_Class : public ESP_NOW_Peer { +public: + virtual void onSent(bool success) {}; + virtual size_t write(const uint8_t *data, size_t len) { return 0; }; +}; diff --git a/tools/espnow-proxy/README.md b/tools/espnow-proxy/README.md new file mode 100644 index 0000000..0a83be2 --- /dev/null +++ b/tools/espnow-proxy/README.md @@ -0,0 +1,3 @@ +# ESPNOW-proxy + +Proxy sketch for using ESP-NOW connection with Flix drone. diff --git a/tools/espnow-proxy/espnow-proxy.ino b/tools/espnow-proxy/espnow-proxy.ino new file mode 100644 index 0000000..c42c914 --- /dev/null +++ b/tools/espnow-proxy/espnow-proxy.ino @@ -0,0 +1,88 @@ +// Copyright (c) 2026 Oleg Kalachev +// Repository: https://github.com/okalachev/flix + +// Proxy for ESP-NOW connection + +#include +#include +#include +#include +#include +#include +#include "../../flix/util.h" + +const int CHANNEL = 6; +char key[ESP_NOW_KEY_LEN + 1] = {0}; // with trailing null + +Preferences storage; + +std::vector peers; + +void onNewPeer(const esp_now_recv_info_t *info, const uint8_t *data, int len, void *arg) { + if (len != 4 || memcmp(data, "flix", 4) != 0) return; // check if discovery message + + Serial.printf("New peer: " MACSTR "\n", MAC2STR(info->src_addr)); + ESPNOWSerial *link = new ESPNOWSerial(info->src_addr, CHANNEL, WIFI_IF_AP); + link->begin(); + link->setKey((const uint8_t *)key); + peers.push_back(link); +} + +void setup() { + Serial.begin(115200); + WiFi.mode(WIFI_AP); + WiFi.setSleep(false); + WiFi.setChannel(CHANNEL); + + ESP_NOW.onNewPeer(onNewPeer, NULL); + ESP_NOW.begin(); + + storage.begin("espnow-proxy"); + if (!storage.isKey("key")) { + generateRandomKey(); + storage.putString("key", key); + } + strcpy(key, storage.getString("key").c_str()); + + // Discover the first peer + while (peers.empty()) { + Serial.printf("espnow %s %s\n", WiFi.softAPmacAddress().c_str(), key); + delay(500); + } +} + +void generateRandomKey() { + const char chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*-_+="; + for (int i = 0; i < ESP_NOW_KEY_LEN; i++) { + key[i] = chars[random(0, strlen(chars))]; + } +} + +void loop() { + uint8_t buf[5000]; + + // Send from Serial to ESP-NOW + while (Serial.available() > 0) { + int b = Serial.read(); + if (b < 0) { + break; + } + + mavlink_message_t msg; + mavlink_status_t status; + if (mavlink_parse_char(MAVLINK_COMM_0, (uint8_t)b, &msg, &status)) { + int len = mavlink_msg_to_send_buffer(buf, &msg); + for (ESPNOWSerial *link : peers) { + link->write(buf, len); + } + } + } + + // Send from ESP-NOW to Serial + for (ESPNOWSerial *link : peers) { + int len = link->read(buf, sizeof(buf)); + if (len > 0) { + Serial.write(buf, len); + } + } +}