|
| 1 | +/* Simple Server Sent Event (aka EventSource) demo |
| 2 | + Run demo as follows: |
| 3 | + 1. set SSID, password and ports, compile and run program |
| 4 | + you should see (random) updates of sensors A and B |
| 5 | +
|
| 6 | + 2. on the client, register it for the event bus using a REST API call: curl -sS "http://<your ESP IP>:<your port>/rest/events/subscribe" |
| 7 | + on both server and client, you should now see that your client is registered |
| 8 | + the server sends back the location of the event bus (channel) to the client: |
| 9 | + subscription for client IP <your client's IP address>: event bus location: http://<your ESP IP>:<your port + 1>/rest/events |
| 10 | +
|
| 11 | + you will also see that the sensors are ready to broadcast state changes, but the client is not yet listening: |
| 12 | + SSEBroadcastState - client <your client IP>> registered but not listening |
| 13 | +
|
| 14 | + 3. on the client, start listening for events with: curl -sS "http://<your ESP IP>:<your port + 1>/rest/events" |
| 15 | + if all is well, the following is being displayed on the ESP console |
| 16 | + SSEHandler - registered client with IP <your client IP address> is listening... |
| 17 | + broadcast status change to client IP <your client IP>> for sensor[A|B] with new state <some number>> |
| 18 | + every minute you will see on the ESP: SSEKeepAlive - client is still connected |
| 19 | + |
| 20 | + on the client, you should see the SSE messages coming in: |
| 21 | + event: event |
| 22 | + data: { "TYPE":"KEEP-ALIVE" } |
| 23 | + event: event |
| 24 | + data: { "TYPE":"STATE", "sensorB": {"state" : 12408, "prevState": 13502} } |
| 25 | + event: event |
| 26 | + data: { "TYPE":"STATE", "sensorA": {"state" : 17664, "prevState": 49362} } |
| 27 | +
|
| 28 | + 4. on the client, stop listening by hitting control-C |
| 29 | + on the ESP, after maximum one minute, the following message is displayed: SSEKeepAlive - client no longer connected, remove subscription |
| 30 | + if you start listening again after the time expired, the "/rest/events" handle becomes stale and "Handle not found" is returned |
| 31 | + you can also try to start listening again before the KeepAliver timer expires or simply register your client again |
| 32 | +*/ |
| 33 | + |
| 34 | +extern "C" { |
| 35 | +#include "c_types.h" |
| 36 | +} |
| 37 | +#include <ESP8266WiFi.h> |
| 38 | +#include <WiFiClient.h> |
| 39 | +#include <ESP8266WebServer.h> |
| 40 | +#include <ESP8266mDNS.h> |
| 41 | +#include <Ticker.h> |
| 42 | + |
| 43 | +#ifndef STASSID |
| 44 | +//#define STASSID "your-ssid" |
| 45 | +//#define STAPSK "your-password" |
| 46 | +#define STASSID "EThomeZX" |
| 47 | +#define STAPSK "superd0me;0rca" |
| 48 | +#endif |
| 49 | + |
| 50 | +const char* ssid = STASSID; |
| 51 | +const char* password = STAPSK; |
| 52 | +const unsigned int port = 80; |
| 53 | + |
| 54 | +ESP8266WebServer server(port); |
| 55 | +ESP8266WebServer SSEserver(port + 1); |
| 56 | + |
| 57 | +struct SSESubscription { |
| 58 | + uint32_t clientIP; |
| 59 | + WiFiClient client; |
| 60 | + Ticker keepAliveTimer; |
| 61 | +} subscription; // in this simplified example, only one SSE client subscription allowed |
| 62 | + |
| 63 | +unsigned short sensorA = 0, sensorB = 0; //Simulate two sensors |
| 64 | +Ticker update, updateA, updateB; |
| 65 | + |
| 66 | +void notFound(ESP8266WebServer &server) { |
| 67 | + Serial.println(F("Handle not found")); |
| 68 | + String message = "Handle Not Found\n\n"; |
| 69 | + message += "URI: "; |
| 70 | + message += server.uri(); |
| 71 | + message += "\nMethod: "; |
| 72 | + message += (server.method() == HTTP_GET) ? "GET" : "POST"; |
| 73 | + message += "\nArguments: "; |
| 74 | + message += server.args(); |
| 75 | + message += "\n"; |
| 76 | + for (uint8_t i = 0; i < server.args(); i++) { |
| 77 | + message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; |
| 78 | + } |
| 79 | + server.send(404, "text/plain", message); |
| 80 | +} |
| 81 | +void handleNotFound() { notFound(server); } |
| 82 | +void handleSSENotFound() { notFound(SSEserver); } |
| 83 | + |
| 84 | +void SSEBroadcastState(const char *sensorName, unsigned short prevSensorValue, unsigned short sensorValue) { |
| 85 | + if (!subscription.clientIP) return; |
| 86 | + WiFiClient client = subscription.client; |
| 87 | + if (client.connected()) { |
| 88 | + Serial.printf_P(PSTR("broadcast status change to client IP %s for %s with new state %d\n"), |
| 89 | + IPAddress(subscription.clientIP).toString().c_str(), sensorName, sensorValue); |
| 90 | + client.printf_P(PSTR("event: event\ndata: { \"TYPE\":\"STATE\", \"%s\": {\"state\" : %d, \"prevState\": %d} }\n"), |
| 91 | + sensorName, sensorValue, prevSensorValue); |
| 92 | + } else |
| 93 | + Serial.printf_P(PSTR("SSEBroadcastState - client %s registered but not listening\n"), IPAddress(subscription.clientIP).toString().c_str()); |
| 94 | +} |
| 95 | + |
| 96 | +void SSEKeepAlive(SSESubscription *s) { |
| 97 | + SSESubscription &subscription = *s; |
| 98 | + if (!subscription.clientIP) return; |
| 99 | + WiFiClient client = subscription.client; |
| 100 | + if (client.connected()) { |
| 101 | + Serial.println(F("SSEKeepAlive - client is still connected")); |
| 102 | + client.println(F("event: event\ndata: { \"TYPE\":\"KEEP-ALIVE\" }")); |
| 103 | + } else { |
| 104 | + Serial.println(F("SSEKeepAlive - client no longer connected, remove subscription")); |
| 105 | + subscription.keepAliveTimer.detach(); |
| 106 | + client.flush(); |
| 107 | + client.stop(); |
| 108 | + subscription.clientIP = 0; |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +// SSEHandler handles the client connection to the event bus (client event listener) |
| 113 | +// every 60 seconds it sends a keep alive event via Ticker |
| 114 | +void SSEHandler(SSESubscription *s) { |
| 115 | + WiFiClient client = SSEserver.client(); |
| 116 | + SSESubscription &subscription = *s; |
| 117 | + if (subscription.clientIP != uint32_t(client.remoteIP())) { // IP addresses don't match, reject this client |
| 118 | + Serial.printf_P(PSTR("SSEHandler - unregistered client with IP %s tries to listen\n"), SSEserver.client().remoteIP().toString().c_str()); |
| 119 | + return notFound(SSEserver); |
| 120 | + } |
| 121 | + //client.setNoDelay(true); // Any of these will crash the ESP (Soft WDT reset) |
| 122 | + //client.setSync(true); |
| 123 | + Serial.printf_P(PSTR("SSEHandler - registered client with IP %s is listening...\n"), IPAddress(subscription.clientIP).toString().c_str()); |
| 124 | + subscription.client = client; // capture SSE server client connection |
| 125 | + SSEserver.setContentLength(CONTENT_LENGTH_UNKNOWN); // the payload can go on forever |
| 126 | + subscription.keepAliveTimer.attach(30.0, std::bind(SSEKeepAlive, s)); // Refresh time every 30s for demo |
| 127 | +} |
| 128 | + |
| 129 | +// Simulate sensors |
| 130 | +void updateSensor(const char* name, unsigned short *value) { |
| 131 | + unsigned short newVal = (unsigned short)RANDOM_REG32; // (not so good) random value for the sensor |
| 132 | + unsigned short val = *value; |
| 133 | + Serial.printf_P(PSTR("update sensor %s - previous state: %d, new state: %d\n"), name, val, newVal); |
| 134 | + if (val != newVal) SSEBroadcastState(name, newVal, val); // only broadcast if state is different |
| 135 | + *value = newVal; |
| 136 | + update.once(rand() % 20 + 10, std::bind(updateSensor, name, value)); // randomly update sensor A |
| 137 | +} |
| 138 | + |
| 139 | +void handleSubscribe() { |
| 140 | + IPAddress clientIP = server.client().remoteIP(); // get IP address of client |
| 141 | + String SSEurl = F("http://"); |
| 142 | + SSEurl += WiFi.localIP().toString(); |
| 143 | + SSEurl += F(":"); |
| 144 | + SSEurl += port + 1; |
| 145 | + size_t offset = SSEurl.length(); |
| 146 | + SSEurl += F("/rest/events"); |
| 147 | + |
| 148 | + if (subscription.clientIP != (uint32_t) clientIP) { // Allocate new subscription |
| 149 | + subscription = {(uint32_t) clientIP, SSEserver.client(), Ticker()}; |
| 150 | + SSEserver.on(SSEurl.substring(offset), std::bind(SSEHandler, &subscription)); |
| 151 | + } else |
| 152 | + Serial.print(F("reusing ")); |
| 153 | + Serial.printf_P(PSTR("subscription for client IP %s: event bus location: %s\n"), clientIP.toString().c_str(), SSEurl.c_str()); |
| 154 | + server.send_P(200, "text/plain", SSEurl.c_str()); |
| 155 | +} |
| 156 | + |
| 157 | +void startServers() { |
| 158 | + server.on(F("/rest/events/subscribe"), handleSubscribe); |
| 159 | + server.onNotFound(handleNotFound); |
| 160 | + server.begin(); |
| 161 | + Serial.println("HTTP server started"); |
| 162 | + SSEserver.onNotFound(handleSSENotFound); |
| 163 | + //SSEserver.keepCurrentClient(true); // Looks like it is not needed |
| 164 | + SSEserver.begin(); |
| 165 | + Serial.println("HTTP SSE EventSource server started"); |
| 166 | +} |
| 167 | + |
| 168 | +void setup(void) { |
| 169 | + Serial.begin(115200); |
| 170 | + WiFi.mode(WIFI_STA); |
| 171 | + WiFi.begin(ssid, password); |
| 172 | + Serial.println(""); |
| 173 | + while (WiFi.status() != WL_CONNECTED) { // Wait for connection |
| 174 | + delay(500); |
| 175 | + Serial.print("."); |
| 176 | + } |
| 177 | + Serial.printf_P(PSTR("\nConnected to %s with IP address: %s\n"), ssid, WiFi.localIP().toString().c_str()); |
| 178 | + if (MDNS.begin("esp8266")) |
| 179 | + Serial.println("MDNS responder started"); |
| 180 | + |
| 181 | + startServers(); // start web and SSE servers |
| 182 | + updateSensor("sensorA", &sensorA); |
| 183 | + updateSensor("sensorB", &sensorB); |
| 184 | +} |
| 185 | + |
| 186 | +void loop(void) { |
| 187 | + server.handleClient(); |
| 188 | + SSEserver.handleClient(); |
| 189 | + MDNS.update(); |
| 190 | + yield(); |
| 191 | +} |
0 commit comments