From cdd1ced67a837d6c560d517a8e4149f1a9169037 Mon Sep 17 00:00:00 2001 From: Me No Dev Date: Tue, 28 Jun 2016 21:51:48 +0300 Subject: [PATCH] Add EventSource plugin --- README.md | 35 +++++- src/AsyncEventSource.cpp | 250 +++++++++++++++++++++++++++++++++++++++ src/AsyncEventSource.h | 87 ++++++++++++++ src/ESPAsyncWebServer.h | 1 + 4 files changed, 367 insertions(+), 6 deletions(-) create mode 100644 src/AsyncEventSource.cpp create mode 100644 src/AsyncEventSource.h diff --git a/README.md b/README.md index 11c75e7..5e52ec1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ To use this library you need to have the latest git versions of either [ESP8266] - Easily extendible to handle any type of content - Supports Continue 100 - Async WebSocket plugin offering different locations without extra servers or ports +- Async EventSource (ServerSideEvents) plugin to send events to the browser +- URL Rewrite plugin for conditional and permanent url rewrites +- ServeStatic plugin that supports cache, Last-Modified, default index and more ## Important things to remember - This is fully asynchronous server and as such does not run on the loop thread. @@ -65,13 +68,13 @@ To use this library you need to have the latest git versions of either [ESP8266] - Two filter callbacks are provided: ```ON_AP_FILTER``` to execute the rewrite when request is made to the AP interface, ```ON_SAT_FILTER``` to execute the rewrite when request is made to the STA interface. - The ```canHandle``` method is used for handler specific control on whether the requests can be handled - and for declaring any interesting headers that the ```Request``` should parse. Decision can be based on request + and for declaring any interesting headers that the ```Request``` should parse. Decision can be based on request method, request url, http version, request host/port/target host and get parameters - Once a ```Handler``` is attached to given ```Request``` (```canHandle``` returned true) that ```Handler``` takes care to receive any file/data upload and attach a ```Response``` once the ```Request``` has been fully parsed -- ```Handlers``` are evaluated in the order they are attached to the server. The ```canHandle``` is called only - if the ```Filter``` that was set to the ```Handler``` return true. +- ```Handlers``` are evaluated in the order they are attached to the server. The ```canHandle``` is called only + if the ```Filter``` that was set to the ```Handler``` return true. - The first ```Handler``` that can handle the request is selected, not further ```Filter``` and ```canHandle``` are called. ### Responses and how do they work @@ -199,6 +202,14 @@ void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_ ``` ## Responses +### Redirect to another URL +```cpp +//to local url +request->redirect("/login"); + +//to external url +request->redirect("http://esp8266.com"); +``` ### Basic response with HTTP Code ```cpp @@ -417,7 +428,7 @@ request->send(response); ## Serving static files In addition to serving files from SPIFFS as described above, the server provide a dedicated handler that optimize the -performance of serving files from SPIFFS - ```AsyncStaticWebHandler```. Use ```server.serveStatic()``` function to +performance of serving files from SPIFFS - ```AsyncStaticWebHandler```. Use ```server.serveStatic()``` function to initialize and add a new instance of ```AsyncStaticWebHandler``` to the server. The Handler will not handle the request if the file does not exists, e.g. the server will continue to look for another handler that can handle the request. @@ -656,6 +667,10 @@ const uint8_t flash_binary[] PROGMEM = { 0x01, 0x02, 0x03, 0x04 }; client->binary(flash_binary, 4); ``` +## Async Event Source Plugin +The server includes EventSource (ServerSideEvents) plugin which can be used to send short text events to the browser. +Difference between EventSource and WebSockets is that EventSource is single direction, text-only protocol. + ## Setting up the server ```cpp @@ -664,6 +679,7 @@ client->binary(flash_binary, 4); AsyncWebServer server(80); AsyncWebSocket ws("/ws"); // access at ws://[esp ip]/ws +AsyncEventSource events("/events"); // event source (server side events) const char* ssid = "your-ssid"; const char* password = "your-pass"; @@ -700,6 +716,9 @@ void setup(){ ws.onEvent(onEvent); server.addHandler(&ws); + // attach AsyncEventSource + server.addHandler(&events); + // respond to GET requests on URL /heap server.on("/heap", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(200, "text/plain", String(ESP.getFreeHeap())); @@ -735,7 +754,11 @@ void setup(){ server.begin(); } -void loop(){} +void loop(){ + static char temp[128]; + sprintf(temp, "Seconds since boot: %u", millis()/1000); + events.send(temp, "time"); //send event "time" +} ``` ### Methods for controlling websocket connections @@ -750,7 +773,7 @@ void loop(){} ws.enable(true); ``` -Example of OTA code +Example of OTA code ```arduino // OTA callbacks diff --git a/src/AsyncEventSource.cpp b/src/AsyncEventSource.cpp new file mode 100644 index 0000000..4275e9f --- /dev/null +++ b/src/AsyncEventSource.cpp @@ -0,0 +1,250 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#include "Arduino.h" +#include "AsyncEventSource.h" + +// Client + +AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, AsyncEventSource *server){ + _client = request->client(); + _server = server; + next = NULL; + //_client->onError([](void *r, AsyncClient* c, int8_t error){ ((AsyncEventSourceClient*)(r))->_onError(error); }, this); + //_client->onAck([](void *r, AsyncClient* c, size_t len, uint32_t time){ ((AsyncEventSourceClient*)(r))->_onAck(len, time); }, this); + //_client->onPoll([](void *r, AsyncClient* c){ ((AsyncEventSourceClient*)(r))->_onPoll(); }, this); + _client->onError(NULL, NULL); + _client->onAck(NULL, NULL); + _client->onPoll(NULL, NULL); + _client->onData(NULL, NULL); + _client->onTimeout([](void *r, AsyncClient* c, uint32_t time){ ((AsyncEventSourceClient*)(r))->_onTimeout(time); }, this); + _client->onDisconnect([](void *r, AsyncClient* c){ ((AsyncEventSourceClient*)(r))->_onDisconnect(); }, this); + _server->_addClient(this); + delete request; +} + +AsyncEventSourceClient::~AsyncEventSourceClient(){ + close(); +} + +//void AsyncEventSourceClient::_onAck(size_t len, uint32_t time){} +//void AsyncEventSourceClient::_onPoll(){} +//void AsyncEventSourceClient::_onError(int8_t){} +void AsyncEventSourceClient::_onTimeout(uint32_t time){ + _client->close(true); +} + +void AsyncEventSourceClient::_onDisconnect(){ + AsyncClient* cl = _client; + _client = NULL; + cl->free(); + delete cl; + _server->_handleDisconnect(this); +} + +void AsyncEventSourceClient::close(){ + if(_client != NULL) + _client->close(true); +} + +void AsyncEventSourceClient::send(const char * message, size_t len){ + if(!_client->canSend()){ + return; + } + if(_client->space() < len){ + return; + } + _client->write(message, len); +} + + +// Handler + +AsyncEventSource::AsyncEventSource(String url):_url(url){} + +AsyncEventSource::~AsyncEventSource(){ + close(); +} + +void AsyncEventSource::_addClient(AsyncEventSourceClient * client){ + if(_clients == NULL){ + _clients = client; + return; + } + AsyncEventSourceClient * c = _clients; + while(c->next != NULL) c = c->next; + c->next = client; +} + +void AsyncEventSource::_handleDisconnect(AsyncEventSourceClient * client){ + if(_clients == NULL){ + return; + } + if(_clients == client){ + _clients = client->next; + delete client; + return; + } + AsyncEventSourceClient * c = _clients; + while(c->next != NULL && c->next != client) c = c->next; + if(c->next == NULL){ + return; + } + c->next = client->next; + delete client; +} + +void AsyncEventSource::close(){ + AsyncEventSourceClient * c = _clients; + while(c != NULL){ + if(c->connected()) + c->close(); + c = c->next; + } +} + +void AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect){ + if(_clients == NULL) + return; + + String ev = ""; + + if(reconnect){ + ev += "retry: "; + ev += String(reconnect); + ev += "\r\n"; + } + + if(id){ + ev += "id: "; + ev += String(reconnect); + ev += "\r\n"; + } + + if(event != NULL){ + ev += "event: "; + ev += String(event); + ev += "\r\n"; + } + + if(message != NULL){ + size_t messageLen = strlen(message); + char * lineStart = (char *)message; + char * lineEnd; + do { + char * nextN = strchr(lineStart, '\n'); + char * nextR = strchr(lineStart, '\r'); + if(nextN == NULL && nextR == NULL){ + //last line + size_t llen = ((char *)message + messageLen) - lineStart; + char * ldata = (char *)malloc(llen+1); + if(ldata != NULL){ + memcpy(ldata, lineStart, llen); + ldata[llen] = 0; + ev += "data: "; + ev += ldata; + ev += "\r\n\r\n"; + free(ldata); + } + lineStart = (char *)message + messageLen; + } else { + char * nextLine = NULL; + if(nextN != NULL && nextR != NULL){ + if(nextR < nextN){ + lineEnd = nextR; + if(nextN == (nextR + 1)) + nextLine = nextN + 1; + else + nextLine = nextR + 1; + } else { + lineEnd = nextN; + if(nextR == (nextN + 1)) + nextLine = nextR + 1; + else + nextLine = nextN + 1; + } + } else if(nextN != NULL){ + lineEnd = nextN; + nextLine = nextN + 1; + } else { + lineEnd = nextR; + nextLine = nextR + 1; + } + + size_t llen = lineEnd - lineStart; + char * ldata = (char *)malloc(llen+1); + if(ldata != NULL){ + memcpy(ldata, lineStart, llen); + ldata[llen] = 0; + ev += "data: "; + ev += ldata; + ev += "\r\n"; + free(ldata); + } + lineStart = nextLine; + if(lineStart == ((char *)message + messageLen)) + ev += "\r\n"; + } + } while(lineStart < ((char *)message + messageLen)); + } + + //os_printf("EVENT_SOURCE:\n%s", ev.c_str()); + + AsyncEventSourceClient * c = _clients; + while(c != NULL){ + if(c->connected()) + c->send(ev.c_str(), ev.length()); + c = c->next; + } + ev = String(); +} + +bool AsyncEventSource::canHandle(AsyncWebServerRequest *request){ + if(request->method() != HTTP_GET || !request->url().equals(_url)) + return false; + return true; +} + +void AsyncEventSource::handleRequest(AsyncWebServerRequest *request){ + request->send(new AsyncEventSourceResponse(this)); +} + +// Response + +AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncEventSource *server){ + _server = server; + _code = 200; + _contentType = "text/event-stream"; + _sendContentLength = false; + addHeader("Cache-Control", "no-cache"); + addHeader("Connection","keep-alive"); +} + +void AsyncEventSourceResponse::_respond(AsyncWebServerRequest *request){ + String out = _assembleHead(request->version()); + request->client()->write(out.c_str(), _headLength); + _state = RESPONSE_WAIT_ACK; +} + +size_t AsyncEventSourceResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time){ + if(len){ + new AsyncEventSourceClient(request, _server); + } + return 0; +} diff --git a/src/AsyncEventSource.h b/src/AsyncEventSource.h new file mode 100644 index 0000000..bb5cdcb --- /dev/null +++ b/src/AsyncEventSource.h @@ -0,0 +1,87 @@ +/* + Asynchronous WebServer library for Espressif MCUs + + Copyright (c) 2016 Hristo Gochkov. All rights reserved. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ +#ifndef ASYNCEVENTSOURCE_H_ +#define ASYNCEVENTSOURCE_H_ + +#include +#include +#include + +class AsyncEventSource; +class AsyncEventSourceResponse; +class AsyncEventSourceClient; + +class AsyncEventSourceClient { + private: + AsyncClient *_client; + AsyncEventSource *_server; + + public: + AsyncEventSourceClient * next; + + AsyncEventSourceClient(AsyncWebServerRequest *request, AsyncEventSource *server); + ~AsyncEventSourceClient(); + + AsyncClient* client(){ return _client; } + void close(); + void send(const char * message, size_t len); + bool connected(){ return (_client != NULL) && _client->connected(); } + + //system callbacks (do not call) + //void _onAck(size_t len, uint32_t time); + //void _onError(int8_t); + //void _onPoll(); + void _onTimeout(uint32_t time); + void _onDisconnect(); +}; + +class AsyncEventSource: public AsyncWebHandler { + private: + String _url; + AsyncEventSourceClient * _clients; + uint32_t _cNextId; + public: + AsyncEventSource(String url); + ~AsyncEventSource(); + + const char * url(){ return _url.c_str(); } + void close(); + void send(const char *message, const char *event=NULL, uint32_t id=0, uint32_t reconnect=0); + + //system callbacks (do not call) + void _addClient(AsyncEventSourceClient * client); + void _handleDisconnect(AsyncEventSourceClient * client); + bool canHandle(AsyncWebServerRequest *request); + void handleRequest(AsyncWebServerRequest *request); +}; + +class AsyncEventSourceResponse: public AsyncWebServerResponse { + private: + String _content; + AsyncEventSource *_server; + public: + AsyncEventSourceResponse(AsyncEventSource *server); + void _respond(AsyncWebServerRequest *request); + size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time); + bool _sourceValid(){ return true; } +}; + + +#endif /* ASYNCEVENTSOURCE_H_ */ diff --git a/src/ESPAsyncWebServer.h b/src/ESPAsyncWebServer.h index 7da11f9..6c57ead 100644 --- a/src/ESPAsyncWebServer.h +++ b/src/ESPAsyncWebServer.h @@ -378,5 +378,6 @@ class AsyncWebServer { #include "WebResponseImpl.h" #include "WebHandlerImpl.h" #include "AsyncWebSocket.h" +#include "AsyncEventSource.h" #endif /* _AsyncWebServer_H_ */