| Part | Topic |
|---|---|
| 1 | Quick Recap from Week 8 — interactive review |
| 2 | Meet the DHT11 — temperature & humidity sensor |
| 3 | Combining ESP-01 + DHT11 on one Arduino |
| 4 | 🌐 Project A: Serve a live web page on the LAN |
| 5 | ☁️ Project B: Push readings to ThingSpeak |
| 6 | 🚀 Stretch goals — pick a challenge |
| 7 | Common pitfalls & summary |
RX line (5V → 3.3V)CH_PD high; ESP is a 3.3V moduleAT+UART_DEFAT+CWMODE=1 + AT+CWJAPsendATCommand() helper to keep code cleanWIFI GOT IP, and read the assigned IP from AT+CIFSR.
#include <SoftwareSerial.h>
SoftwareSerial esp(2, 3); // RX, TX
String sendATCommand(String cmd, int timeout) {
String response = "";
esp.println(cmd);
long t = millis();
while ((millis() - t) < timeout) {
while (esp.available()) {
response += (char)esp.read();
}
}
Serial.println(response);
return response;
}
void setup() {
Serial.begin(9600);
esp.begin(9600); // ALWAYS 9600 with SoftwareSerial!
delay(2000);
sendATCommand("AT", 1000);
sendATCommand("AT+CWMODE=1", 1000);
sendATCommand("AT+CWJAP=\"NZSE-Student\","
"\"password\"", 10000);
sendATCommand("AT+CIFSR", 2000);
}
void loop() {}
Fill in each blank with the correct AT command or value. Press Enter or click Check.
Connecting to Wi-Fi is fun, but an IoT device only matters when it's measuring something real.
Today we add a DHT11 temperature & humidity sensor and publish its readings two different ways.
ESP-01 hosts its own web page.
Open the IP in any browser on the same Wi-Fi → see live readings.
Push readings to ThingSpeak.
Get an automatic chart from anywhere in the world.
A cheap, beginner-friendly digital sensor that measures both temperature and relative humidity on a single data pin.
| Spec | Value |
|---|---|
| Temperature range | 0 – 50 °C (±2 °C) |
| Humidity range | 20 – 90 % RH (±5 %) |
| Supply voltage | 3.3 V – 5 V |
| Sample rate | 1 reading every ~2 s |
| Protocol | Single-wire, 40-bit frame + checksum |
| Cost | ≈ $1–$3 |
NaN. We'll use a non-blocking 5-second timer in our project.
DHTTYPE in the code and you're done.
Two flavours you'll see in the lab:
DATA and VCC.| DHT11 Pin | Wire | Arduino Uno |
|---|---|---|
| VCC | red | 5V |
| DATA | yellow | D4 |
| NC (4-pin only) | — | not connected |
| GND | black | GND |
3-pin breakouts already include the pull-up resistor.
The DHT11 single-wire protocol involves microsecond timing. Don't write that yourself — use Adafruit's library.
"DHT sensor library" → install the one by AdafruitAdafruit Unified Sensor — say yesAdd to platformio.ini:
[env:uno]
platform = atmelavr
board = uno
framework = arduino
monitor_speed = 9600
lib_deps =
adafruit/DHT sensor library@^1.4.6
adafruit/Adafruit Unified Sensor@^1.1.14
Adafruit Unified Sensor → compiler error Adafruit_Sensor.h: No such file or directoryBefore we touch the ESP-01 again, prove the DHT11 is wired correctly with this minimal sketch:
DHT11 smoke test
Humidity: 56.0 % Temperature: 24.3 *C
Humidity: 56.0 % Temperature: 24.4 *C
Humidity: 57.0 % Temperature: 24.4 *C
NaN? Check the data wire is on D4 (not D2/D3) and that VCC is on 5V — the most common cause is loose breadboard contact.
#include <DHT.h>
#define DHTPIN 4 // DATA pin → Arduino D4
#define DHTTYPE DHT11 // sensor model
DHT dht(DHTPIN, DHTTYPE);
void setup() {
Serial.begin(9600);
Serial.println("DHT11 smoke test");
dht.begin();
}
void loop() {
// DHT11 needs >= 2 s between reads
delay(2000);
float h = dht.readHumidity();
float t = dht.readTemperature(); // °C
// Library returns NaN on bad reads — always check!
if (isnan(h) || isnan(t)) {
Serial.println("Sensor read failed");
return;
}
Serial.print("Humidity: "); Serial.print(h);
Serial.print(" % Temperature: "); Serial.print(t);
Serial.println(" *C");
}
Reconnect the ESP-01 from week 8 and add the DHT11. Here is the full pin budget:
| Arduino Pin | Goes to | Notes |
|---|---|---|
| D2 | ESP-01 TX | Arduino RX (SoftwareSerial) |
| D3 | ESP-01 RX | Through 1k/2k voltage divider (5V → 3.3V) |
| D4 | DHT11 DATA | New this week |
| 3.3V | ESP VCC + CH_PD | Use external supply if Uno's 3.3V brown-outs |
| 5V | DHT11 VCC | DHT11 is 5V tolerant |
| GND | Common ground | ESP & DHT & Arduino all share GND |
If we just delay(5000) between sensor reads, the ESP-01 stops responding for 5 seconds — incoming HTTP requests get dropped.
Use the millis() pattern instead so the loop keeps spinning fast enough to service the ESP serial buffer.
loop(), ask: "is it time yet?"g_temp / g_hum.SAMPLE_MS ≥ 2000. Faster than that and the DHT11 returns NaN.
#include <SoftwareSerial.h>
#include <DHT.h>
SoftwareSerial esp(2, 3);
#define DHTPIN 4
DHT dht(DHTPIN, DHT11);
float g_temp = 0.0;
float g_hum = 0.0;
const unsigned long SAMPLE_MS = 5000;
unsigned long lastSample = 0;
void readSensor() {
float h = dht.readHumidity();
float t = dht.readTemperature();
if (!isnan(h) && !isnan(t)) {
g_hum = h;
g_temp = t;
Serial.print("DHT T="); Serial.print(t);
Serial.print("C H="); Serial.print(h);
Serial.println("%");
}
}
void loop() {
unsigned long now = millis();
if (now - lastSample >= SAMPLE_MS) {
lastSample = now;
readSensor();
}
// ... handle incoming ESP data here ...
}
Everything that happens once, in order:
void setup() {
Serial.begin(9600);
Serial.println(F("Weather Station booting..."));
// 1. Sensor
dht.begin();
delay(500);
// 2. ESP-01 serial link
esp.begin(9600);
esp.setTimeout(2000);
delay(2000); // ESP boot delay
// 3. Sanity check
sendATCommand("AT", 1000);
sendATCommand("AT+CWMODE=1", 1000);
// 4. Join the Wi-Fi
String join = "AT+CWJAP=\"" + ssid + "\",\""
+ password + "\"";
String r = sendATCommand(join, 10000);
if (r.indexOf("OK") == -1) {
Serial.println(F("WiFi join FAILED."));
while (1); // halt
}
// 5. Show our IP
sendATCommand("AT+CIFSR", 2000);
// 6. Project A: open a TCP server
sendATCommand("AT+CIPMUX=1", 1000);
sendATCommand("AT+CIPSERVER=1,80", 1000);
Serial.println(F("Server ready on port 80"));
}
The ESP-01 becomes a tiny HTTP server. Anyone on the same Wi-Fi can open http://<ESP-IP> in a browser and the Arduino replies with a freshly-built HTML page showing the latest DHT11 reading.
+IPD,<id>,<len>:<data>+IPD:+IPD,0,328:GET / HTTP/1.1\r\nHost: 192.168.1.42…After joining Wi-Fi, two more commands turn the ESP into a TCP server:
| Command | Why |
|---|---|
AT+CIPMUX=1 | Allow several simultaneous connections (required before CIPSERVER). |
AT+CIPSERVER=1,80 | 1 = start server. 80 = port (HTTP). |
// In loop() — non-blocking
if (esp.available()) {
if (esp.find("+IPD,")) {
int id = esp.read() - '0'; // ASCII trick
esp.find(":"); // skip the length
// Read whatever the browser sent
String req = esp.readStringUntil('\r');
Serial.print("Request: ");
Serial.println(req);
// Build & send the page
sendPage(id);
// Close the connection
esp.print("AT+CIPCLOSE=");
esp.println(id);
}
}
esp.read() - '0' converts the digit character '0'..'9' into its integer value 0..9 — handy for parsing the connection ID.
A valid HTTP response has three parts:
HTTP/1.1 200 OKContent-Type: text/html + a blank line\r\n (CRLF) — a single \n won't work.AT+CIPSEND=id,len needs the exact byte count of what follows. Compute page.length(); off-by-one and the ESP hangs waiting for more bytes.<meta http-equiv="refresh" content="5"> tells the browser to reload every 5 seconds — instant live dashboard, zero JavaScript.
String buildPage() {
String s;
s += F("<!DOCTYPE html><html><head>");
s += F("<meta http-equiv='refresh' content='5'>");
s += F("<title>Weather</title>");
s += F("<style>body{font-family:sans-serif;"
"text-align:center;background:#222;color:#eee}"
"h1{font-size:4em;margin:20px}</style>");
s += F("</head><body>");
s += F("<h1>🌡️ ");
s += String(g_temp, 1); s += F(" °C</h1>");
s += F("<h1>💧 ");
s += String(g_hum, 0); s += F(" %</h1>");
s += F("<p>Auto-refresh every 5 s</p>");
s += F("</body></html>");
return s;
}
void sendPage(int id) {
String body = buildPage();
String hdr = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"Connection: close\r\n\r\n";
String full = hdr + body;
esp.print("AT+CIPSEND=");
esp.print(id); esp.print(",");
esp.println(full.length()); // <— byte count!
delay(50);
if (esp.find(">")) { // wait for ESP prompt
esp.print(full); // raw response
}
}
Glue it all together. The whole weather-station server fits in < 100 lines.
ssid + password.+CIFSR:STAIP,….+IPD// ── 1. Globals ──────────────────────────
#include <SoftwareSerial.h>
#include <DHT.h>
SoftwareSerial esp(2, 3);
DHT dht(4, DHT11);
String ssid = "NZSE-Student";
String password = "ASK_INSTRUCTOR";
float g_temp = 0, g_hum = 0;
unsigned long lastSample = 0;
String sendATCommand(String cmd, int t) {
String r; esp.println(cmd);
long start = millis();
while (millis() - start < t) {
while (esp.available()) r += (char)esp.read();
}
Serial.println(r);
return r;
}
// ── 2. setup() ──────────────────────────
void setup() {
Serial.begin(9600); esp.begin(9600); dht.begin();
delay(2000);
sendATCommand("AT", 1000);
sendATCommand("AT+CWMODE=1", 1000);
sendATCommand("AT+CWJAP=\""+ssid+"\",\""+password+"\"", 10000);
sendATCommand("AT+CIFSR", 2000);
sendATCommand("AT+CIPMUX=1", 1000);
sendATCommand("AT+CIPSERVER=1,80", 1000);
}
// ── 3. loop() ───────────────────────────
void loop() {
if (millis() - lastSample >= 5000) {
lastSample = millis();
float h = dht.readHumidity(), t = dht.readTemperature();
if (!isnan(h) && !isnan(t)) { g_hum = h; g_temp = t; }
}
if (esp.available() && esp.find("+IPD,")) {
int id = esp.read() - '0';
esp.find(":");
esp.readStringUntil('\r'); // discard
sendPage(id);
esp.print("AT+CIPCLOSE="); esp.println(id);
}
}
// ── 4. buildPage() / sendPage() above ───
↑ what your phone shows when it browses to the ESP IP
| Symptom | Likely cause |
|---|---|
| Browser hangs forever | CIPSEND length is wrong → ESP waits for more bytes. Compute full.length() after concatenation. |
| "Connection reset" | Forgot AT+CIPCLOSE, or sent before the > prompt. Add esp.find(">"). |
Page shows nan °C | DHT not wired correctly, or readSensor() ran before dht.begin(). |
No +IPD ever appears | Forgot AT+CIPMUX=1 before CIPSERVER — the order matters. |
| ESP keeps resetting | 3.3V brown-out. Use a separate supply for the ESP. |
| Page is mojibake | Wrong Content-Type or missing blank line between headers & body. |
A free MathWorks service that stores time-series data sent over plain HTTP and draws it as live charts. Perfect for IoT learning — no app, no server, no JavaScript.
thingspeak.com (uses a MathWorks account).Temperature.Humidity.ABC123XYZQRS9876 that proves "this device is allowed to write to my channel".
api.thingspeak.com:80 — perfect for learning. (For real production: use the ESP8266 as a stand-alone microcontroller; that's lecture 9.)
secrets.h and add that file to .gitignore.
ThingSpeak accepts a single HTTP GET request that bundles every reading into the URL:
GET /update?api_key=YOUR_KEY&field1=24.3&field2=56 HTTP/1.1
Host: api.thingspeak.com
Connection: close
Note the blank line at the end — HTTP requires \r\n\r\n after the headers to mark "end of request".
| Field | Where it goes |
|---|---|
api_key | From ThingSpeak — your Write API key |
field1 | Temperature (any number) |
field2 | Humidity |
| extra | Add &field3=… for more sensors later |
0 means "rate-limited or invalid key".
A single helper that does the whole "open → measure length → send → close" dance.
postToThingSpeak(g_temp, g_hum);
millis() timer in loop():
if (millis() - lastPost >= 20000) {
lastPost = millis();
postToThingSpeak(g_temp, g_hum);
}
→ one update every 20 s, well under the 15 s rate limit.
const String API_KEY = "YOUR_WRITE_API_KEY";
const String HOST = "api.thingspeak.com";
void postToThingSpeak(float t, float h) {
// 1. Build the request string
String req = "GET /update?api_key=" + API_KEY;
req += "&field1=" + String(t, 1);
req += "&field2=" + String(h, 0);
req += " HTTP/1.1\r\n";
req += "Host: " + HOST + "\r\n";
req += "Connection: close\r\n\r\n";
// 2. Open TCP connection to ThingSpeak
esp.print("AT+CIPSTART=\"TCP\",\""); esp.print(HOST);
esp.println("\",80");
if (!esp.find("OK")) { Serial.println("CIPSTART fail"); return; }
// 3. Tell the ESP exactly how many bytes follow
esp.print("AT+CIPSEND=");
esp.println(req.length());
if (!esp.find(">")) { Serial.println("No > prompt"); return; }
// 4. Send it!
esp.print(req);
// 5. Wait briefly for the reply, then close
delay(1500);
while (esp.available()) Serial.write(esp.read());
esp.println("AT+CIPCLOSE");
Serial.print("Posted T="); Serial.print(t);
Serial.print(" H="); Serial.println(h);
}
Open your channel page on thingspeak.com — the chart updates automatically. Each postToThingSpeak() call adds a new point.
Add a relay or LED on D5. When the DHT11 reads above 28 °C, fire it on; below 26 °C, off. Hysteresis prevents flapping.
You'll practise: digitalWrite, hysteresis logic, combining sensor + actuator.
When temperature crosses a threshold, send a Telegram message to your phone via the Bot API. Use CIPSTART to api.telegram.org.
You'll practise: outbound HTTP, URL escaping, BotFather setup, chat IDs.
/bot<TOKEN>/sendMessage?chat_id=<ID>&text=Temp+high
Pair with a classmate. Each Uno posts to a different field on the same ThingSpeak channel — Room A on field 1, Room B on field 2. Compare them on one chart.
You'll practise: sharing API keys, coordinating timing, channel design.
| Symptom | Cause | Fix |
|---|---|---|
DHT returns nan | Reading faster than 2 s, or wiring loose | Use millis() with SAMPLE_MS >= 2000; reseat wires |
| Browser hangs on Project A | CIPSEND length wrong | Use full.length() after concatenation |
| Garbage chars in Serial Monitor | ESP at 115200, not 9600 | Run baud rate detector + AT+UART_DEF=9600,8,1,0,0 (week 8) |
| +IPD never appears | Forgot AT+CIPMUX=1 | Send CIPMUX=1 before CIPSERVER=1,80 |
| ESP keeps resetting | 3.3V brown-out under TX load | Use a separate 3.3V supply; tie GNDs |
ThingSpeak returns 0 | Wrong API key, or rate-limited | Re-copy Write key; ensure ≥ 15 s between posts |
| HTTP response is mojibake | Missing CRLF or blank header line | Every line ends \r\n; blank line between headers and body |
| Wi-Fi never connects | SSID has spaces / wrong case | Quote exactly: AT+CWJAP="My WiFi","pw" |
Today you turned the ESP-01 from "thing that joins WiFi" into a complete IoT weather station.
isnan()CIPMUX=1 + CIPSERVER=1,80+IPD,id,len:CIPSTART → CIPSEND → write → closeGET /update?api_key=…Lab: Build Project A end-to-end. Stretch goals are optional — but extras count toward Assignment 2.
| Command | What it does |
|---|---|
AT | Test — responds OK |
AT+RST | Reset the ESP module |
AT+CWMODE=1 | Station mode (join WiFi) |
AT+CWMODE=2 | AP mode (create hotspot) |
AT+CWMODE=3 | Both Station + AP |
AT+CWJAP="ssid","pw" | Connect to a WiFi network |
AT+CWSAP="name","pw",ch,enc | Configure AP (name, password, channel, encryption) |
AT+CIFSR | Get IP address |
AT+CIPMUX=1 | Enable multiple connections |
AT+CIPSERVER=1,80 | Start TCP server on port 80 |
AT+CIPSEND=id,len | Send len bytes on connection id |
AT+CIPCLOSE=id | Close connection id |
| Pattern | Meaning |
|---|---|
+IPD,id,len:data | Received len bytes from connection id |
OK | Command succeeded |
ERROR | Command failed |
SEND OK | Data sent successfully |
> | Ready for data (after CIPSEND) |
| Function | What it does |
|---|---|
esp.available() | Returns number of bytes waiting to be read |
esp.read() | Reads one byte (as int). Use - '0' to convert ASCII digit |
esp.readString() | Reads all available data as a String (blocks until timeout) |
esp.find("text") | Reads until "text" found → true, or timeout → false. Consumes data! |
esp.println("cmd") | Sends command + newline to ESP |
esp.print(data) | Sends data without newline |
esp.write(byte) | Sends a single raw byte |
esp.setTimeout(ms) | Sets timeout for find/readString (default 1000ms) |
| Pattern | Use |
|---|---|
int id = esp.read() - '0'; | Get connection ID as integer (ASCII trick) |
if (esp.find("+IPD,")) | Wait for incoming client request |
String s = esp.readString(); | Read full request after +IPD detected |
s.indexOf("/led/on") | Check if request URL contains a path |
while(millis()-t < 3000) | Non-blocking timeout loop |