diff --git a/examples/AsyncResponseStream/AsyncResponseStream.ino b/examples/AsyncResponseStream/AsyncResponseStream.ino new file mode 100644 index 0000000..62fa799 --- /dev/null +++ b/examples/AsyncResponseStream/AsyncResponseStream.ino @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +#include +#ifdef ESP32 +#include +#include +#elif defined(ESP8266) +#include +#include +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#include +#include +#endif + +#include + +static AsyncWebServer server(80); + +void setup() { + Serial.begin(115200); + +#ifndef CONFIG_IDF_TARGET_ESP32H2 + WiFi.mode(WIFI_AP); + WiFi.softAP("esp-captive"); +#endif + + // Shows how to use AsyncResponseStream. + // The internal buffer will be allocated and data appended to it, + // until the response is sent, then this buffer is read and committed on the network. + // + // curl -v http://192.168.4.1/ + // + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncResponseStream *response = request->beginResponseStream("plain/text", 40 * 1024); + for (int i = 0; i < 32 * 1024; i++) { + response->write('a'); + } + request->send(response); + }); + + server.begin(); +} + +void loop() { + delay(100); +} diff --git a/examples/PerfTests/PerfTests.ino b/examples/PerfTests/PerfTests.ino index 047c7e3..6467d2c 100644 --- a/examples/PerfTests/PerfTests.ino +++ b/examples/PerfTests/PerfTests.ino @@ -103,7 +103,7 @@ void setup() { // curl -v -X POST -H "Content-Type: application/json" -d '{"game": "test"}' http://192.168.4.1/delay // server.onNotFound([](AsyncWebServerRequest *request) { - requests++; + requests = requests + 1; if (request->url() == "/delay") { request->send(200, "application/json", "{\"status\":\"OK\"}"); } else { @@ -125,7 +125,7 @@ void setup() { server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { // need to cast to uint8_t* // if you do not, the const char* will be copied in a temporary String buffer - requests++; + requests = requests + 1; request->send(200, "text/html", (uint8_t *)htmlContent, htmlContentLength); }); @@ -143,7 +143,7 @@ void setup() { // time curl -N -v -G -d 'd=2000' -d 'l=10000' http://192.168.4.1/slow.html --output - // server.on("/slow.html", HTTP_GET, [](AsyncWebServerRequest *request) { - requests++; + requests = requests + 1; uint32_t d = request->getParam("d")->value().toInt(); uint32_t l = request->getParam("l")->value().toInt(); Serial.printf("d = %" PRIu32 ", l = %" PRIu32 "\n", d, l); diff --git a/examples/WebSocketEasy/WebSocketEasy.ino b/examples/WebSocketEasy/WebSocketEasy.ino new file mode 100644 index 0000000..12b03ce --- /dev/null +++ b/examples/WebSocketEasy/WebSocketEasy.ino @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +// +// WebSocket example using the easy to use AsyncWebSocketMessageHandler handler that only supports unfragmented messages +// + +#include +#ifdef ESP32 +#include +#include +#elif defined(ESP8266) +#include +#include +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#include +#include +#endif + +#include + +static AsyncWebServer server(80); + +// create an easy-to-use handler +static AsyncWebSocketMessageHandler wsHandler; + +// add it to the websocket server +static AsyncWebSocket ws("/ws", wsHandler.eventHandler()); + +// alternatively you can do as usual: +// +// static AsyncWebSocket ws("/ws"); +// ws.onEvent(wsHandler.eventHandler()); + +static const char *htmlContent PROGMEM = R"( + + + + WebSocket + + +

WebSocket Example

+ <>Open your browser console!

+ + + + + + )"; +static const size_t htmlContentLength = strlen_P(htmlContent); + +void setup() { + Serial.begin(115200); + +#ifndef CONFIG_IDF_TARGET_ESP32H2 + WiFi.mode(WIFI_AP); + WiFi.softAP("esp-captive"); +#endif + + // serves root html page + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/html", (const uint8_t *)htmlContent, htmlContentLength); + }); + + wsHandler.onConnect([](AsyncWebSocket *server, AsyncWebSocketClient *client) { + Serial.printf("Client %" PRIu32 " connected\n", client->id()); + server->textAll("New client: " + String(client->id())); + }); + + wsHandler.onDisconnect([](AsyncWebSocket *server, uint32_t clientId) { + Serial.printf("Client %" PRIu32 " disconnected\n", clientId); + server->textAll("Client " + String(clientId) + " disconnected"); + }); + + wsHandler.onError([](AsyncWebSocket *server, AsyncWebSocketClient *client, uint16_t errorCode, const char *reason, size_t len) { + Serial.printf("Client %" PRIu32 " error: %" PRIu16 ": %s\n", client->id(), errorCode, reason); + }); + + wsHandler.onMessage([](AsyncWebSocket *server, AsyncWebSocketClient *client, const uint8_t *data, size_t len) { + Serial.printf("Client %" PRIu32 " data: %s\n", client->id(), (const char *)data); + }); + + wsHandler.onFragment([](AsyncWebSocket *server, AsyncWebSocketClient *client, const AwsFrameInfo *frameInfo, const uint8_t *data, size_t len) { + Serial.printf("Client %" PRIu32 " fragment %" PRIu32 ": %s\n", client->id(), frameInfo->num, (const char *)data); + }); + + server.addHandler(&ws); + server.begin(); +} + +static uint32_t lastWS = 0; +static uint32_t deltaWS = 2000; + +void loop() { + uint32_t now = millis(); + + if (now - lastWS >= deltaWS) { + ws.cleanupClients(); + ws.printfAll("now: %" PRIu32 "\n", now); + lastWS = millis(); +#ifdef ESP32 + Serial.printf("Free heap: %" PRIu32 "\n", ESP.getFreeHeap()); +#endif + } +} diff --git a/library.json b/library.json index 70d5044..bd9b884 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "ESPAsyncWebServer", - "version": "3.7.4", + "version": "3.7.5", "description": "Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040. Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static File serving, URL Rewrite, URL Redirect, etc.", "keywords": "http,async,websocket,webserver", "homepage": "https://github.com/ESP32Async/ESPAsyncWebServer", diff --git a/library.properties b/library.properties index 7894456..4a1b210 100644 --- a/library.properties +++ b/library.properties @@ -1,6 +1,6 @@ name=ESP Async WebServer includes=ESPAsyncWebServer.h -version=3.7.4 +version=3.7.5 author=ESP32Async maintainer=ESP32Async sentence=Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040 diff --git a/pioarduino_examples/IncreaseMaxSockets/platformio.ini b/pioarduino_examples/IncreaseMaxSockets/platformio.ini index 20291d1..08d7a50 100644 --- a/pioarduino_examples/IncreaseMaxSockets/platformio.ini +++ b/pioarduino_examples/IncreaseMaxSockets/platformio.ini @@ -1,6 +1,6 @@ [env] framework = arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20/platform-espressif32.zip build_flags = -Og -Wall -Wextra diff --git a/platformio.ini b/platformio.ini index 025d011..a0129d4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,6 +1,7 @@ [platformio] default_envs = arduino-2, arduino-3, esp8266, raspberrypi lib_dir = . +; src_dir = examples/AsyncResponseStream ; src_dir = examples/Auth ; src_dir = examples/CaptivePortal ; src_dir = examples/CatchAllHandler @@ -32,10 +33,11 @@ src_dir = examples/PerfTests ; src_dir = examples/Templates ; src_dir = examples/Upload ; src_dir = examples/WebSocket +; src_dir = examples/WebSocketEasy [env] framework = arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20/platform-espressif32.zip board = esp32dev build_flags = -Og @@ -48,6 +50,7 @@ build_flags = -D CONFIG_ASYNC_TCP_QUEUE_SIZE=64 -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 -D CONFIG_ASYNC_TCP_STACK_SIZE=4096 + ; -D CONFIG_ASYNC_TCP_USE_WDT=0 upload_protocol = esptool monitor_speed = 115200 monitor_filters = esp32_exception_decoder, log2file @@ -66,7 +69,7 @@ platform = espressif32@6.10.0 [env:arduino-3] [env:arduino-3-latest] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20-rc1/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20-rc2/platform-espressif32.zip [env:arduino-3-no-json] lib_deps = @@ -115,7 +118,7 @@ board = ${sysenv.PIO_BOARD} board = ${sysenv.PIO_BOARD} [env:ci-arduino-3-latest] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20-rc1/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20-rc2/platform-espressif32.zip board = ${sysenv.PIO_BOARD} [env:ci-arduino-3-no-json] diff --git a/src/AsyncWebServerVersion.h b/src/AsyncWebServerVersion.h index ecab8a7..4f3fdc4 100644 --- a/src/AsyncWebServerVersion.h +++ b/src/AsyncWebServerVersion.h @@ -12,7 +12,7 @@ extern "C" { /** Minor version number (x.X.x) */ #define ASYNCWEBSERVER_VERSION_MINOR 7 /** Patch version number (x.x.X) */ -#define ASYNCWEBSERVER_VERSION_PATCH 4 +#define ASYNCWEBSERVER_VERSION_PATCH 5 /** * Macro to convert version number into an integer diff --git a/src/AsyncWebSocket.h b/src/AsyncWebSocket.h index 1963e75..2318834 100644 --- a/src/AsyncWebSocket.h +++ b/src/AsyncWebSocket.h @@ -291,7 +291,7 @@ private: String _url; std::list _clients; uint32_t _cNextId; - AwsEventHandler _eventHandler{nullptr}; + AwsEventHandler _eventHandler; AwsHandshakeHandler _handshakeHandler; bool _enabled; #ifdef ESP32 @@ -305,8 +305,8 @@ public: PARTIALLY_ENQUEUED = 2, } SendStatus; - explicit AsyncWebSocket(const char *url) : _url(url), _cNextId(1), _enabled(true) {} - AsyncWebSocket(const String &url) : _url(url), _cNextId(1), _enabled(true) {} + explicit AsyncWebSocket(const char *url, AwsEventHandler handler = nullptr) : _url(url), _cNextId(1), _eventHandler(handler), _enabled(true) {} + AsyncWebSocket(const String &url, AwsEventHandler handler = nullptr) : _url(url), _cNextId(1), _eventHandler(handler), _enabled(true) {} ~AsyncWebSocket(){}; const char *url() const { return _url.c_str(); @@ -413,4 +413,86 @@ public: } }; +class AsyncWebSocketMessageHandler { +public: + AwsEventHandler eventHandler() const { + return _handler; + } + + void onConnect(std::function onConnect) { + _onConnect = onConnect; + } + + void onDisconnect(std::function onDisconnect) { + _onDisconnect = onDisconnect; + } + + /** + * Error callback + * @param reason null-terminated string + * @param len length of the string + */ + void onError(std::function onError) { + _onError = onError; + } + + /** + * Complete message callback + * @param data pointer to the data (binary or null-terminated string). This handler expects the user to know which data type he uses. + */ + void onMessage(std::function onMessage) { + _onMessage = onMessage; + } + + /** + * Fragmented message callback + * @param data pointer to the data (binary or null-terminated string), will be null-terminated. This handler expects the user to know which data type he uses. + */ + // clang-format off + void onFragment(std::function onFragment) { + _onFragment = onFragment; + } + // clang-format on + +private: + // clang-format off + std::function _onConnect; + std::function _onError; + std::function _onMessage; + std::function _onFragment; + std::function _onDisconnect; + // clang-format on + + // this handler is meant to only support 1-frame messages (== unfragmented messages) + AwsEventHandler _handler = [this](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { + if (type == WS_EVT_CONNECT) { + if (_onConnect) { + _onConnect(server, client); + } + } else if (type == WS_EVT_DISCONNECT) { + if (_onDisconnect) { + _onDisconnect(server, client->id()); + } + } else if (type == WS_EVT_ERROR) { + if (_onError) { + _onError(server, client, *((uint16_t *)arg), (const char *)data, len); + } + } else if (type == WS_EVT_DATA) { + AwsFrameInfo *info = (AwsFrameInfo *)arg; + if (info->opcode == WS_TEXT) { + data[len] = 0; + } + if (info->final && info->index == 0 && info->len == len) { + if (_onMessage) { + _onMessage(server, client, data, len); + } + } else { + if (_onFragment) { + _onFragment(server, client, info, data, len); + } + } + } + }; +}; + #endif /* ASYNCWEBSOCKET_H_ */ diff --git a/src/WebResponseImpl.h b/src/WebResponseImpl.h index 80dbca8..6408625 100644 --- a/src/WebResponseImpl.h +++ b/src/WebResponseImpl.h @@ -10,7 +10,7 @@ #undef max #endif #include "literals.h" -#include +#include #include #include @@ -157,7 +157,7 @@ public: class AsyncResponseStream : public AsyncAbstractResponse, public Print { private: - StreamString _content; + std::unique_ptr _content; public: AsyncResponseStream(const char *contentType, size_t bufferSize); @@ -168,6 +168,12 @@ public: size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final; size_t write(const uint8_t *data, size_t len); size_t write(uint8_t data); + /** + * @brief Returns the number of bytes available in the stream. + */ + size_t available() const { + return _content->available(); + } using Print::write; }; diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp index f5d4653..97981bd 100644 --- a/src/WebResponses.cpp +++ b/src/WebResponses.cpp @@ -820,7 +820,9 @@ AsyncResponseStream::AsyncResponseStream(const char *contentType, size_t bufferS _code = 200; _contentLength = 0; _contentType = contentType; - if (!_content.reserve(bufferSize)) { + // internal buffer will be null on allocation failure + _content = std::unique_ptr(new cbuf(bufferSize)); + if (_content->size() != bufferSize) { #ifdef ESP32 log_e("Failed to allocate"); #endif @@ -828,14 +830,18 @@ AsyncResponseStream::AsyncResponseStream(const char *contentType, size_t bufferS } size_t AsyncResponseStream::_fillBuffer(uint8_t *buf, size_t maxLen) { - return _content.readBytes((char *)buf, maxLen); + return _content->read((char *)buf, maxLen); } size_t AsyncResponseStream::write(const uint8_t *data, size_t len) { if (_started()) { return 0; } - size_t written = _content.write(data, len); + if (len > _content->room()) { + size_t needed = len - _content->room(); + _content->resizeAdd(needed); + } + size_t written = _content->write((const char *)data, len); _contentLength += written; return written; }