ESP-NOW packet modification

- include up to 5 segments in 1st packet
- header contains total number of packets (instead of segments)

web server code reorganise
This commit is contained in:
Blaz Kristan 2024-02-15 20:40:55 +01:00
parent 2d30535b69
commit 95e2e574b8
4 changed files with 189 additions and 169 deletions

View File

@ -253,6 +253,7 @@ void setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w);
void refreshNodeList();
void sendSysInfoUDP();
#ifndef WLED_DISABLE_ESPNOW
void espNowSentCB(uint8_t* address, uint8_t status);
void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rssi, bool broadcast);
#endif
@ -428,15 +429,10 @@ void handleSerial();
void updateBaudRate(uint32_t rate);
//wled_server.cpp
bool isIp(String str);
void createEditHandler(bool enable);
bool captivePortal(AsyncWebServerRequest *request);
void initServer();
void serveIndex(AsyncWebServerRequest* request);
String msgProcessor(const String& var);
void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl="", byte optionT=255);
void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t error);
String dmxProcessor(const String& var);
void serveSettings(AsyncWebServerRequest* request, bool post = false);
void serveSettingsJS(AsyncWebServerRequest* request);
@ -447,7 +443,6 @@ void sendDataWs(AsyncWebSocketClient * client = nullptr);
//xml.cpp
void XML_response(AsyncWebServerRequest *request, char* dest = nullptr);
void URL_response(AsyncWebServerRequest *request);
void getSettingsJS(byte subPage, char* dest);
#endif

View File

@ -13,7 +13,7 @@
typedef struct PartialEspNowPacket {
uint8_t magic;
uint8_t packet;
uint8_t segs;
uint8_t noOfPackets;
uint8_t data[247];
} partial_packet_t;
@ -151,20 +151,32 @@ void notify(byte callMode, bool followUp)
#ifndef WLED_DISABLE_ESPNOW
if (enableESPNow && useESPNowSync && statusESPNow == ESP_NOW_STATE_ON) {
partial_packet_t buffer = {'W', 0, (uint8_t)s, {0}};
partial_packet_t buffer = {'W', 0, 1, {0}};
// send global data
DEBUG_PRINTLN(F("ESP-NOW sending first packet."));
memcpy(buffer.data, udpOut, 41);
auto err = quickEspNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast<const uint8_t*>(&buffer), 41+3);
if (!err) {
// send segment data
DEBUG_PRINTLN(F("ESP-NOW sending first packet."));
const size_t bufferSize = sizeof(buffer.data)/sizeof(uint8_t);
size_t packetSize = 41;
size_t s0 = 0;
memcpy(buffer.data, udpOut, packetSize);
// stuff as many segments in first packet as possible (normally up to 5)
for (size_t i = 0; packetSize < bufferSize && i < s; i++) {
memcpy(buffer.data + packetSize, &udpOut[41+i*UDP_SEG_SIZE], UDP_SEG_SIZE);
packetSize += UDP_SEG_SIZE;
s0++;
}
if (s > s0) buffer.noOfPackets += 1 + ((s - s0) * UDP_SEG_SIZE) / bufferSize; // set number of packets
auto err = quickEspNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast<const uint8_t*>(&buffer), packetSize+3);
if (!err && s0 < s) {
// send rest of the segments
buffer.packet++;
size_t packetSize = 0;
int32_t err = 0;
for (size_t i = 0; i < s; i++) {
packetSize = 0;
// WARNING: this will only work for up to 3 messages (~17 segments) as QuickESPNOW only has a ring buffer capable of holding 3 queued messages
// to work around that limitation it is mandatory to utilize onDataSent() callback which should reduce number queued messages
// and wait until at least one space is available in the buffer
for (size_t i = s0; i < s; i++) {
memcpy(buffer.data + packetSize, &udpOut[41+i*UDP_SEG_SIZE], UDP_SEG_SIZE);
packetSize += UDP_SEG_SIZE;
if (packetSize + UDP_SEG_SIZE < sizeof(buffer.data)/sizeof(uint8_t)) continue;
if (packetSize + UDP_SEG_SIZE < bufferSize) continue;
DEBUG_PRINTF("ESP-NOW sending packet: %d (%d)\n", (int)buffer.packet, packetSize+3);
err = quickEspNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast<const uint8_t*>(&buffer), packetSize+3);
buffer.packet++;
@ -175,9 +187,9 @@ void notify(byte callMode, bool followUp)
DEBUG_PRINTF("ESP-NOW sending last packet: %d (%d)\n", (int)buffer.packet, packetSize+3);
err = quickEspNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast<const uint8_t*>(&buffer), packetSize+3);
}
if (err) {
DEBUG_PRINTLN(F("ESP-NOW sending packet failed."));
}
}
if (err) {
DEBUG_PRINTLN(F("ESP-NOW sending packet failed."));
}
}
if (udpConnected)
@ -942,6 +954,11 @@ uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, uint8
}
#ifndef WLED_DISABLE_ESPNOW
// ESP-NOW message sent callback function
void espNowSentCB(uint8_t* address, uint8_t status) {
DEBUG_PRINTF("Message sent to " MACSTR ", status: %d\n", MAC2STR(address), status);
}
// ESP-NOW message receive callback function
void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rssi, bool broadcast) {
sprintf_P(last_signal_src, PSTR("%02x%02x%02x%02x%02x%02x"), address[0], address[1], address[2], address[3], address[4], address[5]);
@ -970,19 +987,35 @@ void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rs
}
static uint8_t *udpIn = nullptr;
static uint8_t packetsReceived = 0; // bitfield (max 5 packets ATM)
static uint8_t packetsReceived = 0;
static uint8_t segsReceived = 0;
static unsigned long lastProcessed = 0;
if (buffer->packet == 0) {
if (udpIn == nullptr) udpIn = (uint8_t *)malloc(WLEDPACKETSIZE); // we cannot use stack as we are in callback
DEBUG_PRINTLN(F("ESP-NOW inited UDP buffer."));
memcpy(udpIn, buffer->data, len-3); // global data (41 bytes)
packetsReceived |= 0x01 << buffer->packet;
segsReceived = 0;
return;
} else if (((len-3)/UDP_SEG_SIZE)*UDP_SEG_SIZE != (len-3)) {
DEBUG_PRINTF("ESP-NOW incorrect packet size: %d (%d) [%d]\n", (int)buffer->packet, (int)len-3, (int)UDP_SEG_SIZE);
packetsReceived = 0; // it will increment later (this is to make sure we start counting packets correctly)
if (udpIn == nullptr) {
udpIn = (uint8_t *)malloc(WLEDPACKETSIZE); // we cannot use stack as we are in callback
if (!udpIn) return; // memory alocation failed
DEBUG_PRINTLN(F("ESP-NOW inited UDP buffer."));
}
memcpy(udpIn, buffer->data, len-3); // global data (41 bytes + up to 5 segments)
segsReceived = (len - 3 - 41) / UDP_SEG_SIZE;
} else if (buffer->packet == packetsReceived && udpIn && ((len - 3) / UDP_SEG_SIZE) * UDP_SEG_SIZE == (len-3)) {
// we received a packet full of segments
if (segsReceived >= MAX_NUM_SEGMENTS) {
// we are already past max segments, just ignore
DEBUG_PRINTLN(F("ESP-NOW received segments past maximum."));
len = 3;
} else if ((segsReceived + ((len - 3) / UDP_SEG_SIZE)) >= MAX_NUM_SEGMENTS) {
len = ((MAX_NUM_SEGMENTS - segsReceived) * UDP_SEG_SIZE) + 3; // we have reached max number of segments
}
if (len > 3) {
memcpy(udpIn + 41 + (segsReceived * UDP_SEG_SIZE), buffer->data, len-3);
segsReceived += (len - 3) / UDP_SEG_SIZE;
}
} else {
// any out of order packet or incorrectly sized packet or if we have no UDP buffer will abort
DEBUG_PRINTF("ESP-NOW incorrect packet: %d (%d) [%d]\n", (int)buffer->packet, (int)len-3, (int)UDP_SEG_SIZE);
if (udpIn) free(udpIn);
udpIn = nullptr;
packetsReceived = 0;
@ -991,15 +1024,9 @@ void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rs
}
if (!udpIn) return;
// TODO add verification if segsReceived > MAX_NUM_SEGMENTS or WLEDPACKETSIZE
memcpy(udpIn+41+segsReceived, buffer->data, len-3);
packetsReceived |= 0x01 << buffer->packet;
segsReceived += (len-3)/UDP_SEG_SIZE;
DEBUG_PRINTF("ESP-NOW packet received: %d (%d) [%d]\n", (int)buffer->packet, (int)len-3, (int)segsReceived);
if (segsReceived == buffer->segs) {
packetsReceived++;
DEBUG_PRINTF("ESP-NOW packet received: %d (%d/%d) s:[%d/%d]\n", (int)buffer->packet, (int)packetsReceived, (int)buffer->noOfPackets, (int)segsReceived, MAX_NUM_SEGMENTS);
if (packetsReceived >= buffer->noOfPackets) {
// last packet received
if (millis() - lastProcessed > 250) {
DEBUG_PRINTLN(F("ESP-NOW processing complete message."));

View File

@ -247,6 +247,9 @@ void WLED::loop()
}
#endif
DEBUG_PRINT(F("Wifi state: ")); DEBUG_PRINTLN(WiFi.status());
#ifndef WLED_DISABLE_ESPNOW
DEBUG_PRINT(F("ESP-NOW state: ")); DEBUG_PRINTLN(statusESPNow);
#endif
if (WiFi.status() != lastWifiState) {
wifiStateChangedTime = millis();
@ -834,7 +837,8 @@ void WLED::initConnection()
#ifndef WLED_DISABLE_ESPNOW
if (enableESPNow) {
quickEspNow.onDataRcvd(espNowReceiveCB);
quickEspNow.onDataSent(espNowSentCB); // see udp.cpp
quickEspNow.onDataRcvd(espNowReceiveCB); // see udp.cpp
bool espNowOK;
if (apActive) {
DEBUG_PRINTLN(F("ESP-NOW initing in AP mode."));

View File

@ -11,14 +11,6 @@
#endif
#include "html_cpal.h"
/*
* Integrated HTTP web server page declarations
*/
bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest* request, int code, uint16_t eTagSuffix = 0);
void setStaticContentCacheHeaders(AsyncWebServerResponse *response, int code, uint16_t eTagSuffix = 0);
void handleStaticContent(AsyncWebServerRequest *request, const String &path, int code, const String &contentType, const uint8_t *content, size_t len, bool gzip = true, uint16_t eTagSuffix = 0);
// define flash strings once (saves flash memory)
static const char s_redirecting[] PROGMEM = "Redirecting...";
static const char s_content_enc[] PROGMEM = "Content-Encoding";
@ -33,7 +25,7 @@ static const char s_plain[] PROGMEM = "text/plain";
static const char s_css[] PROGMEM = "text/css";
//Is this an IP?
bool isIp(String str) {
static bool isIp(String str) {
for (size_t i = 0; i < str.length(); i++) {
int c = str.charAt(i);
if (c != '.' && (c < '0' || c > '9')) {
@ -43,7 +35,128 @@ bool isIp(String str) {
return true;
}
void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) {
/*
* Integrated HTTP web server page declarations
*/
static void generateEtag(char *etag, uint16_t eTagSuffix) {
sprintf_P(etag, PSTR("%7d-%02x-%04x"), VERSION, cacheInvalidate, eTagSuffix);
}
static void setStaticContentCacheHeaders(AsyncWebServerResponse *response, int code, uint16_t eTagSuffix = 0) {
// Only send ETag for 200 (OK) responses
if (code != 200) return;
// https://medium.com/@codebyamir/a-web-developers-guide-to-browser-caching-cc41f3b73e7c
#ifndef WLED_DEBUG
// this header name is misleading, "no-cache" will not disable cache,
// it just revalidates on every load using the "If-None-Match" header with the last ETag value
response->addHeader(F("Cache-Control"), F("no-cache"));
#else
response->addHeader(F("Cache-Control"), F("no-store,max-age=0")); // prevent caching if debug build
#endif
char etag[32];
generateEtag(etag, eTagSuffix);
response->addHeader(F("ETag"), etag);
}
static bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest *request, int code, uint16_t eTagSuffix = 0) {
// Only send 304 (Not Modified) if response code is 200 (OK)
if (code != 200) return false;
AsyncWebHeader *header = request->getHeader(F("If-None-Match"));
char etag[32];
generateEtag(etag, eTagSuffix);
if (header && header->value() == etag) {
AsyncWebServerResponse *response = request->beginResponse(304);
setStaticContentCacheHeaders(response, code, eTagSuffix);
request->send(response);
return true;
}
return false;
}
/**
* Handles the request for a static file.
* If the file was found in the filesystem, it will be sent to the client.
* Otherwise it will be checked if the browser cached the file and if so, a 304 response will be sent.
* If the file was not found in the filesystem and not in the browser cache, the request will be handled as a 200 response with the content of the page.
*
* @param request The request object
* @param path If a file with this path exists in the filesystem, it will be sent to the client. Set to "" to skip this check.
* @param code The HTTP status code
* @param contentType The content type of the web page
* @param content Content of the web page
* @param len Length of the content
* @param gzip Optional. Defaults to true. If false, the gzip header will not be added.
* @param eTagSuffix Optional. Defaults to 0. A suffix that will be added to the ETag header. This can be used to invalidate the cache for a specific page.
*/
static void handleStaticContent(AsyncWebServerRequest *request, const String &path, int code, const String &contentType, const uint8_t *content, size_t len, bool gzip = true, uint16_t eTagSuffix = 0) {
if (path != "" && handleFileRead(request, path)) return;
if (handleIfNoneMatchCacheHeader(request, code, eTagSuffix)) return;
AsyncWebServerResponse *response = request->beginResponse_P(code, contentType, content, len);
if (gzip) response->addHeader(FPSTR(s_content_enc), F("gzip"));
setStaticContentCacheHeaders(response, code, eTagSuffix);
request->send(response);
}
#ifdef WLED_ENABLE_DMX
static String dmxProcessor(const String& var)
{
String mapJS;
if (var == F("DMXVARS")) {
mapJS += F("\nCN=");
mapJS += String(DMXChannels);
mapJS += F(";\nCS=");
mapJS += String(DMXStart);
mapJS += F(";\nCG=");
mapJS += String(DMXGap);
mapJS += F(";\nLC=");
mapJS += String(strip.getLengthTotal());
mapJS += F(";\nvar CH=[");
for (int i=0; i<15; i++) {
mapJS += String(DMXFixtureMap[i]) + ',';
}
mapJS += F("0];");
}
return mapJS;
}
#endif
static String msgProcessor(const String& var)
{
if (var == "MSG") {
String messageBody = messageHead;
messageBody += F("</h2>");
messageBody += messageSub;
uint32_t optt = optionType;
if (optt < 60) //redirect to settings after optionType seconds
{
messageBody += F("<script>setTimeout(RS,");
messageBody +=String(optt*1000);
messageBody += F(")</script>");
} else if (optt < 120) //redirect back after optionType-60 seconds, unused
{
//messageBody += "<script>setTimeout(B," + String((optt-60)*1000) + ")</script>";
} else if (optt < 180) //reload parent after optionType-120 seconds
{
messageBody += F("<script>setTimeout(RP,");
messageBody += String((optt-120)*1000);
messageBody += F(")</script>");
} else if (optt == 253)
{
messageBody += F("<br><br><form action=/settings><button class=\"bt\" type=submit>Back</button></form>"); //button to settings
} else if (optt == 254)
{
messageBody += F("<br><br><button type=\"button\" class=\"bt\" onclick=\"B()\">Back</button>");
}
return messageBody;
}
return String();
}
static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) {
if (!correctPIN) {
if (final) request->send(401, FPSTR(s_plain), FPSTR(s_unlock_cfg));
return;
@ -96,7 +209,7 @@ void createEditHandler(bool enable) {
}
}
bool captivePortal(AsyncWebServerRequest *request)
static bool captivePortal(AsyncWebServerRequest *request)
{
if (!apActive) return false; //only serve captive in AP mode
if (!request->hasHeader("Host")) return false;
@ -368,102 +481,6 @@ void initServer()
});
}
void generateEtag(char *etag, uint16_t eTagSuffix) {
sprintf_P(etag, PSTR("%7d-%02x-%04x"), VERSION, cacheInvalidate, eTagSuffix);
}
bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest *request, int code, uint16_t eTagSuffix) {
// Only send 304 (Not Modified) if response code is 200 (OK)
if (code != 200) return false;
AsyncWebHeader *header = request->getHeader(F("If-None-Match"));
char etag[32];
generateEtag(etag, eTagSuffix);
if (header && header->value() == etag) {
AsyncWebServerResponse *response = request->beginResponse(304);
setStaticContentCacheHeaders(response, code, eTagSuffix);
request->send(response);
return true;
}
return false;
}
void setStaticContentCacheHeaders(AsyncWebServerResponse *response, int code, uint16_t eTagSuffix) {
// Only send ETag for 200 (OK) responses
if (code != 200) return;
// https://medium.com/@codebyamir/a-web-developers-guide-to-browser-caching-cc41f3b73e7c
#ifndef WLED_DEBUG
// this header name is misleading, "no-cache" will not disable cache,
// it just revalidates on every load using the "If-None-Match" header with the last ETag value
response->addHeader(F("Cache-Control"), F("no-cache"));
#else
response->addHeader(F("Cache-Control"), F("no-store,max-age=0")); // prevent caching if debug build
#endif
char etag[32];
generateEtag(etag, eTagSuffix);
response->addHeader(F("ETag"), etag);
}
/**
* Handels the request for a static file.
* If the file was found in the filesystem, it will be sent to the client.
* Otherwise it will be checked if the browser cached the file and if so, a 304 response will be sent.
* If the file was not found in the filesystem and not in the browser cache, the request will be handled as a 200 response with the content of the page.
*
* @param request The request object
* @param path If a file with this path exists in the filesystem, it will be sent to the client. Set to "" to skip this check.
* @param code The HTTP status code
* @param contentType The content type of the web page
* @param content Content of the web page
* @param len Length of the content
* @param gzip Optional. Defaults to true. If false, the gzip header will not be added.
* @param eTagSuffix Optional. Defaults to 0. A suffix that will be added to the ETag header. This can be used to invalidate the cache for a specific page.
*/
void handleStaticContent(AsyncWebServerRequest *request, const String &path, int code, const String &contentType, const uint8_t *content, size_t len, bool gzip, uint16_t eTagSuffix) {
if (path != "" && handleFileRead(request, path)) return;
if (handleIfNoneMatchCacheHeader(request, code, eTagSuffix)) return;
AsyncWebServerResponse *response = request->beginResponse_P(code, contentType, content, len);
if (gzip) response->addHeader(FPSTR(s_content_enc), F("gzip"));
setStaticContentCacheHeaders(response, code, eTagSuffix);
request->send(response);
}
String msgProcessor(const String& var)
{
if (var == "MSG") {
String messageBody = messageHead;
messageBody += F("</h2>");
messageBody += messageSub;
uint32_t optt = optionType;
if (optt < 60) //redirect to settings after optionType seconds
{
messageBody += F("<script>setTimeout(RS,");
messageBody +=String(optt*1000);
messageBody += F(")</script>");
} else if (optt < 120) //redirect back after optionType-60 seconds, unused
{
//messageBody += "<script>setTimeout(B," + String((optt-60)*1000) + ")</script>";
} else if (optt < 180) //reload parent after optionType-120 seconds
{
messageBody += F("<script>setTimeout(RP,");
messageBody += String((optt-120)*1000);
messageBody += F(")</script>");
} else if (optt == 253)
{
messageBody += F("<br><br><form action=/settings><button class=\"bt\" type=submit>Back</button></form>"); //button to settings
} else if (optt == 254)
{
messageBody += F("<br><br><button type=\"button\" class=\"bt\" onclick=\"B()\">Back</button>");
}
return messageBody;
}
return String();
}
void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl, byte optionT)
{
@ -487,29 +504,6 @@ void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t erro
request->send(response);
}
#ifdef WLED_ENABLE_DMX
String dmxProcessor(const String& var)
{
String mapJS;
if (var == F("DMXVARS")) {
mapJS += F("\nCN=");
mapJS += String(DMXChannels);
mapJS += F(";\nCS=");
mapJS += String(DMXStart);
mapJS += F(";\nCG=");
mapJS += String(DMXGap);
mapJS += F(";\nLC=");
mapJS += String(strip.getLengthTotal());
mapJS += F(";\nvar CH=[");
for (int i=0; i<15; i++) {
mapJS += String(DMXFixtureMap[i]) + ',';
}
mapJS += F("0];");
}
return mapJS;
}
#endif
void serveSettingsJS(AsyncWebServerRequest* request)
{