Skip to content

Commit bc09943

Browse files
committed
Server Sent Events example - issue esp8266#7008
Illustrates the use of SSE using ESP8266WebServer
1 parent 0d38ea7 commit bc09943

File tree

1 file changed

+191
-0
lines changed

1 file changed

+191
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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

Comments
 (0)