True "push" is done with WebSockets, which are two-way. For that, you'll probably use additional libraries on both the server and client. You can get a lot of the same effect by -- not "pulling" but -- polling at regular intervals. There's even something in between: long polling, where the client makes a request, and the server will hold onto that request and not reply immediately. It waits until there is something to say, or some longer time limit has been reached. That way, it can simulate a push.
But basic polling is easy to implement, and may be sufficient. Modern browser APIs help. Here's an example web page
<!DOCTYPE html>
<title>Polling demo</title>
<h1>Uptime</h1>
<p id="info">one moment...</p>
<script>
setInterval(() => {
fetch("/latest")
.then(response => response.json())
.then(value => {
document.querySelector("#info").innerText = value;
})
.catch(console.error);
}, 3500);
</script>
setInterval runs a function periodically
- that function calls
fetch for a path, which returns a Promise of a Response
- which is handled by
then, which calls json on it, which returns a Promise of some JavaScript value that was returned as JSON
- which is handled by another
then, which uses the web page DOM to set the <p> text to that response value
- any exception that happens along the way will be printed in the Developer Tools Console
On the web server side, you need to distinguish request paths. Some of the more advanced web server libraries have functions for this, but you can also handle it manually. Here's a modified loop of the WiFi101 example WiFiWebServer, along with a new helper function
void contentLengthClose(Client &client, const String &content) {
client.print("Content-Length: ");
client.println(content.length() + 2); // for CR+LF
client.println("Connection: close");
client.println();
client.println(content);
}
void loop() {
// listen for incoming clients
WiFiClient client = server.available();
if (client) {
Serial.println("new client");
String startLine;
String method;
bool firstLine = true;
// an HTTP request ends with a blank line
bool currentLineIsBlank = true;
while (client.connected()) {
if (client.available()) {
char c = client.read();
Serial.write(c);
// Accumulate first line
if (firstLine) {
startLine += c;
}
// if you've gotten to the end of the line (received a newline
// character) and the line is blank, the HTTP request has ended,
// so you can send a reply
if (c == '\n' && currentLineIsBlank) {
if (method == "") {
client.println("HTTP/1.1 405 Method Not Allowed");
client.println("Content-Type: text/plain");
contentLengthClose(client, startLine);
break;
}
if (startLine == "/latest") {
String info(millis());
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: application/json");
contentLengthClose(client, info);
break;
}
if (startLine != "/") {
client.println("HTTP/1.1 404 Not Found");
client.println("Connection: close");
client.println();
break;
}
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
contentLengthClose(client, home_page);
break;
}
if (c == '\n') {
if (firstLine) {
firstLine = false;
// parse e.g.
// GET /something HTTP/1.1
int i = startLine.lastIndexOf("\x20HTTP/"); // space before HTTP
if (i >= 0) {
startLine.remove(i);
i = startLine.indexOf('\x20');
if (startLine.startsWith("GET\x20")) {
method = "GET";
startLine.remove(0, ++i);
startLine.trim(); // should now be "/something"
}
}
}
// you're starting a new line
currentLineIsBlank = true;
} else if (c != '\r') {
// you've gotten a character on the current line
currentLineIsBlank = false;
}
}
}
// give the web browser time to receive the data
delay(1);
// close the connection:
client.stop();
Serial.println("client disconnected");
}
}
There's code to parse the first line of the request, aka the start-line, primarily to extract the path. But while we're at it, might as well also check the method or verb, to make sure it is one we support. In this case, we only do GET. If the request is something else, we return the appropriate HTTP response, 405
If the request is for that fetch path, then we return some JSON. In this case, it's just a number. If it was a string, you'd have to put quotes around it, but a number requires nothing more.
Modifying the original function, I left (a little bit of) the existing code that returned the HTML page. So first, if the request is for anything other than the root path /, we return 404. If not, then the home page is now saved as String using a raw string
const String home_page = R"~~~(<!DOCTYPE html>
<title>Polling demo</title>
<h1>Uptime</h1>
<p id="info">one moment...</p>
<script>
setInterval(() => {
fetch("/latest")
.then(response => response.json())
.then(value => {
document.querySelector("#info").innerText = value;
})
.catch(console.error);
}, 3500);
</script>
)~~~";
If you've been following along, you might think, "In this demo, the value is a single number, which is valid JSON. For example, 1234. This goes "over the wire" as four bytes, also 1234, which is then parsed as JSON, returning the number. That means in the function above, value is a number. Can you assign that to innerText? Yes, JavaScript will "helpfully" do those types of conversions for you automatically.