Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server Sent Events example - issue #7008 #7012

Merged
merged 14 commits into from
May 16, 2020
Merged
2 changes: 1 addition & 1 deletion libraries/ESP8266SdFat
Submodule ESP8266SdFat updated 1 files
+7 −1 src/SysCall.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/* Multi-client Server Sent Event (aka EventSource) demo
Run demo as follows:
1. set SSID, password and ports, compile and run program
you should see (random) updates of sensors A and B

2. on the client(s), register it for the event bus using a REST API call: curl -sS "http://<your ESP IP>:<your port>/rest/events/subscribe"
on both server and client, you should now see that your client is registered
the server sends back the location of the event bus (channel) to the client:
subscription for client IP <your client's IP address>: event bus location: http://<your ESP IP>:<your port>/rest/events/<channel>

you will also see that the sensors are ready to broadcast state changes, but the client is not yet listening:
SSEBroadcastState - client <your client IP>> registered but not listening

3. on the client(s), start listening for events with: curl -sS "http://<your ESP IP>:<your port>/rest/events/<channel>"
if all is well, the following is being displayed on the ESP console
SSEHandler - registered client with IP <your client IP address> is listening...
broadcast status change to client IP <your client IP>> for sensor[A|B] with new state <some number>>
every minute you will see on the ESP: SSEKeepAlive - client is still connected

on the client, you should see the SSE messages coming in:
event: event
data: { "TYPE":"KEEP-ALIVE" }
event: event
data: { "TYPE":"STATE", "sensorB": {"state" : 12408, "prevState": 13502} }
event: event
data: { "TYPE":"STATE", "sensorA": {"state" : 17664, "prevState": 49362} }

4. on the client, stop listening by hitting control-C
on the ESP, after maximum one minute, the following message is displayed: SSEKeepAlive - client no longer connected, remove subscription
if you start listening again after the time expired, the "/rest/events" handle becomes stale and "Handle not found" is returned
you can also try to start listening again before the KeepAliver timer expires or simply register your client again
*/

extern "C" {
#include "c_types.h"
}
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <Ticker.h>

#ifndef STASSID
#define STASSID "your-ssid"
#define STAPSK "your-password"
#endif

const char* ssid = STASSID;
const char* password = STAPSK;
const unsigned int port = 80;

ESP8266WebServer server(port);

#define SSE_MAX_CHANNELS 8 // in this simplified example, only eight SSE clients subscription allowed
struct SSESubscription {
uint32_t clientIP;
earlephilhower marked this conversation as resolved.
Show resolved Hide resolved
WiFiClient client;
Ticker keepAliveTimer;
} subscription[SSE_MAX_CHANNELS];
uint8_t subscriptionCount = 0;

unsigned short sensorA = 0, sensorB = 0; //Simulate two sensors
Ticker update;

void handleNotFound() {
Serial.println(F("Handle not found"));
String message = "Handle Not Found\n\n";
message += "URI: ";
message += server.uri();
message += "\nMethod: ";
message += (server.method() == HTTP_GET) ? "GET" : "POST";
message += "\nArguments: ";
message += server.args();
message += "\n";
for (uint8_t i = 0; i < server.args(); i++) {
message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
}
server.send(404, "text/plain", message);
}

void SSEKeepAlive() {
for (uint8_t i = 0; i < SSE_MAX_CHANNELS; i++) {
if (!(subscription[i].clientIP)) continue;
WiFiClient client = subscription[i].client;
if (client.connected()) {
Serial.printf_P(PSTR("SSEKeepAlive - client is still listening on channel %d\n"), i);
client.println(F("event: event\ndata: { \"TYPE\":\"KEEP-ALIVE\" }\n")); // Extra newline required by SSE standard
} else {
Serial.printf_P(PSTR("SSEKeepAlive - client not listening on channel %d, remove subscription\n"), i);
subscription[i].keepAliveTimer.detach();
client.flush();
client.stop();
subscription[i].clientIP = 0;
subscriptionCount--;
}
}
}

// SSEHandler handles the client connection to the event bus (client event listener)
// every 60 seconds it sends a keep alive event via Ticker
void SSEHandler(uint8_t channel) {
WiFiClient client = server.client();
SSESubscription &s = subscription[channel];
if (s.clientIP != uint32_t(client.remoteIP())) { // IP addresses don't match, reject this client
earlephilhower marked this conversation as resolved.
Show resolved Hide resolved
Serial.printf_P(PSTR("SSEHandler - unregistered client with IP %s tries to listen\n"), server.client().remoteIP().toString().c_str());
return handleNotFound();
}
client.setNoDelay(true);
client.setSync(true);
Serial.printf_P(PSTR("SSEHandler - registered client with IP %s is listening\n"), IPAddress(s.clientIP).toString().c_str());
s.client = client; // capture SSE server client connection
server.setContentLength(CONTENT_LENGTH_UNKNOWN); // the payload can go on forever
server.sendContent_P(PSTR("HTTP/1.1 200 OK\nContent-Type: text/event-stream;\nConnection: keep-alive\nCache-Control: no-cache\nAccess-Control-Allow-Origin: *\n\n"));
s.keepAliveTimer.attach_scheduled(30.0, SSEKeepAlive); // Refresh time every 30s for demo
}

void handleAll() {
const char *uri = server.uri().c_str();
if (strncmp_P(uri, PSTR("/rest/events/"), sizeof("/rest/events"))) return handleNotFound();
uri += sizeof("/rest/events");
earlephilhower marked this conversation as resolved.
Show resolved Hide resolved
unsigned int channel = atoi(uri);
if (channel < SSE_MAX_CHANNELS)
return SSEHandler(channel);
handleNotFound();
};

void SSEBroadcastState(const char *sensorName, unsigned short prevSensorValue, unsigned short sensorValue) {
for (uint8_t i = 0; i < SSE_MAX_CHANNELS; i++) {
if (!(subscription[i].clientIP)) continue;
WiFiClient client = subscription[i].client;
earlephilhower marked this conversation as resolved.
Show resolved Hide resolved
String IPaddrstr = IPAddress(subscription[i].clientIP).toString();
if (client.connected()) {
Serial.printf_P(PSTR("broadcast status change to client IP %s on channel %d for %s with new state %d\n"),
IPaddrstr.c_str(), i, sensorName, sensorValue);
client.printf_P(PSTR("event: event\ndata: {\"TYPE\":\"STATE\", \"%s\":{\"state\":%d, \"prevState\":%d}}\n\n"),
sensorName, sensorValue, prevSensorValue);
} else
Serial.printf_P(PSTR("SSEBroadcastState - client %s registered on channel %d but not listening\n"), IPaddrstr.c_str(), i);
}
}

// Simulate sensors
void updateSensor(const char* name, unsigned short *value) {
earlephilhower marked this conversation as resolved.
Show resolved Hide resolved
unsigned short newVal = (unsigned short)RANDOM_REG32; // (not so good) random value for the sensor
unsigned short val = *value;
Serial.printf_P(PSTR("update sensor %s - previous state: %d, new state: %d\n"), name, val, newVal);
if (val != newVal) SSEBroadcastState(name, val, newVal); // only broadcast if state is different
*value = newVal;
update.once(rand() % 20 + 10, std::bind(updateSensor, name, value)); // randomly update sensor A
}

void handleSubscribe() {
uint8_t channel;
IPAddress clientIP = server.client().remoteIP(); // get IP address of client
String SSEurl = F("http://");
SSEurl += WiFi.localIP().toString();
SSEurl += F(":");
SSEurl += port;
size_t offset = SSEurl.length();
SSEurl += F("/rest/events/");

if (subscriptionCount == SSE_MAX_CHANNELS - 1) return handleNotFound(); // We ran out of channels
++subscriptionCount;
for (channel = 0; channel < SSE_MAX_CHANNELS; channel++) // Find first free slot
if (!subscription[channel].clientIP) break;
subscription[channel] = {(uint32_t) clientIP, server.client()};
SSEurl += channel;
Serial.printf_P(PSTR("Allocated channel %d, on uri %s\n"), channel, SSEurl.substring(offset).c_str());
//server.on(SSEurl.substring(offset), std::bind(SSEHandler, &(subscription[channel])));
Serial.printf_P(PSTR("subscription for client IP %s: event bus location: %s\n"), clientIP.toString().c_str(), SSEurl.c_str());
server.send_P(200, "text/plain", SSEurl.c_str());
}

void startServers() {
server.on(F("/rest/events/subscribe"), handleSubscribe);
server.onNotFound(handleAll);
server.begin();
Serial.println("HTTP server and SSE EventSource started");
}

void setup(void) {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.println("");
while (WiFi.status() != WL_CONNECTED) { // Wait for connection
delay(500);
Serial.print(".");
}
Serial.printf_P(PSTR("\nConnected to %s with IP address: %s\n"), ssid, WiFi.localIP().toString().c_str());
if (MDNS.begin("esp8266"))
Serial.println("MDNS responder started");

startServers(); // start web and SSE servers
updateSensor("sensorA", &sensorA);
updateSensor("sensorB", &sensorB);
}

void loop(void) {
server.handleClient();
MDNS.update();
yield();
}
2 changes: 1 addition & 1 deletion libraries/LittleFS/lib/littlefs
2 changes: 1 addition & 1 deletion tools/esptool
Submodule esptool updated 45 files
+12 −7 .travis.yml
+17 −1 .travis_setup_build_env.sh
+0 −14 README.md
+99 −179 espefuse.py
+23 −68 espsecure.py
+154 −396 esptool.py
+11 −5 flasher_stub/Makefile
+1 −1 flasher_stub/compare_stubs.py
+0 −130 flasher_stub/include/soc_support.h
+0 −44 flasher_stub/include/stub_commands.h
+0 −43 flasher_stub/include/stub_write_flash.h
+0 −26 flasher_stub/miniz.h
+0 −3 flasher_stub/rom_32.ld
+0 −0 flasher_stub/rom_8266.ld
+22 −11 flasher_stub/rom_functions.h
+1 −1 flasher_stub/run_tests_with_stub.sh
+5 −1 flasher_stub/slip.h
+47 −0 flasher_stub/soc_support.h
+0 −0 flasher_stub/stub_32.ld
+1 −1 flasher_stub/stub_8266.ld
+1 −2 flasher_stub/stub_commands.c
+25 −0 flasher_stub/stub_commands.h
+32 −27 flasher_stub/stub_flasher.c
+5 −5 flasher_stub/stub_flasher.h
+18 −63 flasher_stub/stub_write_flash.c
+20 −0 flasher_stub/stub_write_flash.h
+1 −1 flasher_stub/wrap_stub.py
+1 −1 setup.cfg
+ test/elf2image/esp32-app-template.elf
+ test/images/aes_key.bin
+ test/images/esp8266_deepsleep.bin
+ test/images/helloworld-esp32_edit.bin
+ test/images/image_header_only.bin
+0 −1 test/images/one_kb_all_ef.bin
+ test/secure_images/bootloader-encrypted-conf0.bin
+ test/secure_images/bootloader-encrypted-conf3.bin
+ test/secure_images/bootloader-encrypted-conf9.bin
+ test/secure_images/bootloader-encrypted-confc.bin
+ test/secure_images/bootloader-encrypted.bin
+0 −1 test/secure_images/ef-flashencryption-key.bin
+ test/secure_images/hello-world-signed-encrypted.bin
+ test/secure_images/hello-world-signed.bin
+11 −34 test/test_espsecure.py
+21 −162 test/test_esptool.py
+11 −17 test/test_imagegen.py
2 changes: 1 addition & 1 deletion tools/sdk/lwip2/builder