A reading on the page
Building the number into the page means the reader has to press F5 to see a new one. Splitting the page from the number is four extra lines, and it is the split every dashboard on this board is built on.
Watch what crosses the wire on each update, and what the reader sees while it does.
Two routes, not one
The first version everyone writes builds the number into the HTML:
"<h1>Light: " + String(analogRead(LDR)) + "</h1>"
It works, and it forces a full reload for every new value. Split it instead:
/returns the page. It is a constant, it never changes, and it can be served straight out of flash./lightreturns three or four characters of plain text. No tags, no headers you did not need, no layout.
The page then asks for /light on a timer and rewrites one element. About 130
bytes cross the wire instead of 500, and nothing blanks.
Content-Type is the instruction
text/html tells the browser to parse it; text/plain tells it not to. Send a
bare number as text/html and it still works — until the day a reading contains
a character the parser cares about. Say what a response is.
Polling has a ceiling
Once a second for a temperature is sensible. Ten times a second for a graph is ten new TCP connections a second, and each one costs a handshake to carry four bytes. That is the point where the connection should stay open instead: WebSocket dashboards is the comparison, and Async web server is the library that serves both.
The code
Two routes. `/` is the page and never changes; `/light` is the reading and nothing else. The script in the page asks for `/light` once a second and writes the answer into a span.
#include <WiFi.h>
#include <WebServer.h>
const int LDR = 33; // ADC1, so it survives Wi-Fi
WebServer server(80);
const char PAGE[] PROGMEM = R"html(
<!doctype html><meta charset=utf-8>
<h1>Light</h1><h2 id=v>--</h2>
<script>
setInterval(() => fetch('/light')
.then(r => r.text())
.then(t => v.textContent = t), 1000);
</script>
)html";
void setup() {
Serial.begin(115200);
pinMode(LDR, INPUT);
WiFi.begin("your-network", "your-password");
while (WiFi.status() != WL_CONNECTED) delay(250);
Serial.println(WiFi.localIP());
server.on("/", HTTP_GET, [] {
server.send_P(200, "text/html", PAGE);
});
server.on("/light", HTTP_GET, [] {
server.sendHeader("Cache-Control", "no-store");
server.send(200, "text/plain", String(analogRead(LDR)));
});
server.begin();
}
void loop() {
server.handleClient();
}GPIO 33 is an ADC1 pin. On the classic ESP32 the ADC2 pins - 0, 2, 4, 12-15, 25-27 - stop returning anything once the radio is on, and this sketch turns the radio on.
The same two routes, written out longhand. Note that the page is sent as text/html and the reading as text/plain - the browser decides what to do with a response from that header, not from the contents.
import network, socket
from machine import ADC, Pin
ldr = ADC(Pin(33))
ldr.atten(ADC.ATTN_11DB) # full 0-3.3 V range
PAGE = """<!doctype html><meta charset=utf-8>
<h1>Light</h1><h2 id=v>--</h2>
<script>
setInterval(() => fetch('/light')
.then(r => r.text())
.then(t => v.textContent = t), 1000);
</script>"""
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect('your-network', 'your-password')
while not wlan.isconnected():
pass
print(wlan.ifconfig()[0])
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('0.0.0.0', 80))
s.listen(2)
while True:
conn, _ = s.accept()
path = conn.recv(1024).decode().split(' ')[1]
if path == '/light':
head = 'HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nCache-Control: no-store\r\n\r\n'
body = str(ldr.read())
else:
head = 'HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n'
body = PAGE
conn.send(head)
conn.send(body)
conn.close()Keep the page in a module-level constant, not built per request. A string rebuilt on every fetch fragments the heap and the board dies after a few hours rather than immediately.
When it does not work
The browser is serving /light from its cache. Send a Cache-Control no-store header on that route, or append a changing query parameter to the fetch. It looks like a frozen sensor and it is a frozen response.
You are on an ADC2 pin with the radio running. On the classic ESP32 the second ADC block shares hardware with Wi-Fi and the radio wins. Move the sensor to GPIO 32-39.
The fetch is failing, not the sensor. Open the browser console - it will name the reason - and check /light on its own by visiting it directly. If that path returns the number, the page is the problem; if it does not, the route is.
Each tab polls once a second, and this server answers strictly one request at a time. Two tabs is two requests a second queued against each other, plus whatever else the browser asks for. That is the wall the async server exists to remove.
The page can read the board now. The next one lets the board be told something.
Switching a pin from the page →Edit this page — content/esp32/web-server-sensor-readings.mdx
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.