PCINT Review → ESP-01 Hardware → AT Commands → Mini Projects
Quick warm-up exercises on pin change interrupts from last week
Hardware overview, pinout, wiring to Arduino Uno
Communicating with the ESP-01 via serial AT commands
Choose your project: WiFi Chat Terminal or WiFi LED Controller
| Group | Port | Pins | Vector | PCICR Bit | Mask |
|---|---|---|---|---|---|
| 0 | B | D8–D13 | PCINT0_vect | PCIE0 | PCMSK0 |
| 1 | C | A0–A5 | PCINT1_vect | PCIE1 | PCMSK1 |
| 2 | D | D0–D7 | PCINT2_vect | PCIE2 | PCMSK2 |
// Generic PCINT setup pattern:
// 1. Enable group in PCICR
PCICR |= (1 << PCIEx);
// 2. Enable specific pin in mask
PCMSKx |= (1 << PCINTn);
// 3. Write the ISR
ISR(PCINTx_vect) {
// Check which pin changed
if (PIND & (1 << PDn)) {
// Rising edge (pin went HIGH)
} else {
// Falling edge (pin went LOW)
}
}
// 4. Enable global interrupts
sei();
You want a pin change interrupt on analog pin A2. Fill in each blank:
Configure a pin change interrupt on digital pin D10. Fill in each blank:
Configure a pin change interrupt on digital pin D4. Fill in each blank:
| Chip | ESP8266EX |
| WiFi | 802.11 b/g/n |
| Voltage | 3.3V |
| Current (peak) | ~170 mA |
| Flash | 1 MB |
| GPIO Pins | 2 (GPIO0, GPIO2) |
| UART | TX, RX |
| Baud Rate | 9600 or 115200 (varies) |
| Pin | Name | Function |
|---|---|---|
| 1 | VCC | 3.3V power supply |
| 2 | GND | Ground |
| 3 | TX | Transmit data (to Arduino RX) |
| 4 | RX | Receive data (from Arduino TX) |
| 5 | RST | Reset (active LOW) |
| 6 | CH_PD / EN | Chip enable (pull HIGH) |
| 7 | GPIO0 | General purpose I/O |
| 8 | GPIO2 | General purpose I/O |
| ESP-01 Pin | → | Arduino / Power | Notes |
|---|---|---|---|
| VCC | → | 3.3V | Use Arduino 3.3V output |
| GND | → | GND | Common ground |
| TX | → | Pin 2 (SoftwareSerial RX) | Direct connection OK |
| RX | → | Pin 3 via voltage divider | 5V → 3.3V needed! |
| CH_PD / EN | → | 3.3V | Must be HIGH to run |
| RST | → | (leave unconnected or 3.3V) | Optional |
| GPIO0 | → | (leave unconnected) | HIGH = normal mode |
| GPIO2 | → | (leave unconnected) | Must be HIGH at boot |
Arduino Uno R3 TX is 5V but ESP-01 RX only accepts 3.3V. Use a resistor divider (1kΩ + 2kΩ):
V_out = 5V × 2kΩ / (1kΩ + 2kΩ) = 3.3V
SoftwareSerial library creates a second serial port.
SoftwareSerial is a built-in library that ships with the Arduino IDE. You do not need to install it from the Library Manager. Simply write #include <SoftwareSerial.h> at the top of your sketch and it's ready to use.
Upload this sketch to verify your ESP-01 is responding:
AT and press EnterAT → OK#include <SoftwareSerial.h>
// ESP-01: TX → Pin 2, RX → Pin 3
SoftwareSerial esp(2, 3);
void setup() {
Serial.begin(9600); // USB serial
esp.begin(9600); // ESP-01 baud rate
Serial.println("ESP-01 Test Ready");
Serial.println("Type AT commands:");
}
void loop() {
// Forward: Serial Monitor → ESP-01
if (Serial.available()) {
esp.write(Serial.read());
}
// Forward: ESP-01 → Serial Monitor
if (esp.available()) {
Serial.write(esp.read());
}
}
Some ESP-01 modules ship with 9600 baud as default, while others come set to 115200. If you use the wrong baud rate, you'll get garbage characters in the Serial Monitor.
| Serial Monitor Output | Meaning |
|---|---|
ESP responding at 9600 | Already at 9600 ✅ — you're good! |
ESP responding at 115200 | Needs to be changed → next slide |
No response at either rate | Check wiring & CH_PD |
SoftwareSerial on Arduino is unreliable at 115200 — it drops characters and corrupts data. At 9600 baud, it works perfectly with AT commands.
#include <SoftwareSerial.h>
SoftwareSerial esp(2, 3);
void setup() {
Serial.begin(9600);
Serial.println("=== ESP Baud Rate Detector ===");
Serial.println();
// Try 9600 first
Serial.print("Trying 9600... ");
esp.begin(9600);
delay(1000);
esp.println("AT");
delay(1000);
if (esp.find("OK")) {
Serial.println("ESP responding at 9600");
Serial.println("You're all set!");
return;
}
// Try 115200
Serial.print("Trying 115200...");
esp.begin(115200);
delay(1000);
esp.println("AT");
delay(1000);
if (esp.find("OK")) {
Serial.println("ESP responding at 115200");
Serial.println(">> Change it to 9600!");
Serial.println(">> See next slide.");
return;
}
Serial.println();
Serial.println("No response at either rate.");
Serial.println("Check your wiring!");
}
void loop() { }
If your ESP is at 115200, use this sketch to permanently change it to 9600:
| Parameter | Value | Meaning |
|---|---|---|
| Baud rate | 9600 | New speed |
| Data bits | 8 | Standard |
| Stop bits | 1 | Standard |
| Parity | 0 | None |
| Flow control | 0 | Disabled |
esp.begin(9600) in all your sketches.
#include <SoftwareSerial.h>
SoftwareSerial esp(2, 3);
void setup() {
Serial.begin(9600);
Serial.println("=== Change ESP to 9600 ===");
Serial.println();
// Connect at current speed (115200)
esp.begin(115200);
delay(1000);
// Send the baud rate change command
Serial.println("Sending AT+UART_DEF=9600...");
esp.println("AT+UART_DEF=9600,8,1,0,0");
delay(2000);
// Now reconnect at 9600 to verify
esp.begin(9600);
delay(1000);
// Test with AT
esp.println("AT");
delay(1000);
String response = "";
while (esp.available()) {
response += (char)esp.read();
}
Serial.println("Response: " + response);
if (response.indexOf("OK") != -1) {
Serial.println();
Serial.println("Baud rate changed to 9600!");
Serial.println("You're all set!");
} else {
Serial.println("Something went wrong.");
Serial.println("Try uploading again.");
}
}
void loop() { }
| Command | Description | Example Response |
|---|---|---|
AT | Test connection | OK |
AT+RST | Reset module | ready |
AT+GMR | Firmware version | AT version: ... |
AT+CWMODE=1 | Set Station mode | OK |
AT+CWMODE=2 | Set AP mode | OK |
AT+CWMODE=3 | Set Station + AP | OK |
AT+CWJAP="ssid","pass" | Connect to WiFi | WIFI CONNECTED |
AT+CIFSR | Get IP address | +CIFSR:STAIP,"..." |
AT+CIPMUX=1 | Enable multiple connections | OK |
AT+CIPSERVER=1,80 | Start TCP server on port 80 | OK |
\r\n (carriage return + newline)Try typing AT commands below. The simulator responds like a real ESP-01:
192.168.1.42. Other devices on the same network can communicate with it.
Before connecting your ESP-01, you need to know the exact SSID (network name) and password of the NZSE Wi-Fi. Here's how to find it:
AT+CWLAP via Serial Monitor. The ESP will list all visible networks:
The format is: (encryption, "SSID", signal_strength, ...)
"NZSE-Student" is different from "nzse-student"netsh wlan show profile name="SSID" key=clear (Windows CMD)Writing AT command code gets repetitive. This helper function sends a command and waits for a response:
sendATCommand("AT+CWMODE=2", 1000);sendATCommand("AT+CWSAP=...", 3000);timeout ms, and prints the ESP's response to Serial Monitor.
String sendATCommand(String cmd, int timeout) {
String response = "";
esp.println(cmd); // Send command to ESP
long int time = millis();
while ((millis() - time) < timeout) {
while (esp.available()) {
char c = esp.read();
response += c;
}
}
Serial.println(response); // Debug output
return response;
}
// Example usage in setup():
void setup() {
Serial.begin(9600);
esp.begin(9600);
sendATCommand("AT", 1000);
sendATCommand("AT+CWMODE=2", 1000);
sendATCommand("AT+CWSAP=\"MyESP\","
"\"12345678\",5,3", 3000);
sendATCommand("AT+CIPMUX=1", 1000);
sendATCommand("AT+CIPSERVER=1,80", 1000);
sendATCommand("AT+CIFSR", 1000);
}
Replace YOUR_SSID and YOUR_PASSWORD with the actual values from the previous slide:
FAIL → Wrong SSID or passwordDISCONNECT → Out of range or network fullAT+RST)#include <SoftwareSerial.h>
SoftwareSerial esp(2, 3);
// ====== CHANGE THESE ======
String ssid = "YOUR_SSID"; // e.g. "NZSE-Student"
String password = "YOUR_PASSWORD"; // ask instructor
// ==========================
void setup() {
Serial.begin(9600);
esp.begin(9600);
delay(2000);
Serial.println("Connecting to NZSE Wi-Fi...");
// 1. Station mode
sendATCommand("AT+CWMODE=1", 2000);
// 2. Connect to the network
String joinCmd = "AT+CWJAP=\"" + ssid + "\",\""
+ password + "\"";
String resp = sendATCommand(joinCmd, 10000);
if (resp.indexOf("OK") != -1) {
Serial.println("=== WiFi Connected! ===");
// 3. Show assigned IP
sendATCommand("AT+CIFSR", 2000);
} else {
Serial.println("Connection failed!");
Serial.println(resp);
}
}
void loop() {
// pass-through for debugging
if (esp.available()) Serial.write(esp.read());
if (Serial.available()) esp.write(Serial.read());
}
Once connected to NZSE Wi-Fi, verify the connection actually works by fetching a real web page. We'll request example.com:
HTTP/1.1 200 OK — you are connected to the internet through NZSE Wi-Fi!
This sketch connects to NZSE Wi-Fi and then fetches a page from example.com to prove internet connectivity:
AT+CIPSEND needs the exact number of bytes. Our request string is:
GET / HTTP/1.1\r\n
Host: example.com\r\n
\r\n
That's 18 + 21 + 4 = 43 bytes (counting each \r\n as 2 bytes).
Connecting to WiFi...=== WiFi Connected! ===Fetching example.com...HTTP/1.1 200 OK...<h1>Example Domain</h1>...
#include <SoftwareSerial.h>
SoftwareSerial esp(2, 3);
String ssid = "YOUR_SSID";
String password = "YOUR_PASSWORD";
void setup() {
Serial.begin(9600);
esp.begin(9600);
delay(2000);
// Connect to Wi-Fi
sendATCommand("AT+CWMODE=1", 2000);
String join = "AT+CWJAP=\"" + ssid + "\",\""
+ password + "\"";
String r = sendATCommand(join, 10000);
if (r.indexOf("OK") == -1) {
Serial.println("WiFi FAILED"); return;
}
Serial.println("=== WiFi Connected! ===");
sendATCommand("AT+CIFSR", 2000);
// Open TCP connection to example.com
Serial.println("Fetching example.com...");
sendATCommand(
"AT+CIPSTART=\"TCP\",\"example.com\",80",
5000);
// HTTP GET request (43 bytes)
String http = "GET / HTTP/1.1\r\n"
"Host: example.com\r\n\r\n";
sendATCommand("AT+CIPSEND=" +
String(http.length()), 2000);
sendATCommand(http, 5000);
}
void loop() {
if (esp.available()) Serial.write(esp.read());
}
In AP mode, the ESP-01 creates its own WiFi network. No router needed — devices connect directly to it.
Once the ESP-01 is on a network, it communicates using TCP connections — the same protocol that powers every website. Think of it like making a phone call:
| CIPMUX=1 | → Allow multiple callers |
| CIPSERVER=1,80 | → Open the phone line |
| +IPD | → Phone rings, caller speaks |
| CIPSEND | → You reply to the caller |
| CIPCLOSE | → Hang up |
+IPD,0,... → first client+IPD,1,... → second client+IPD,2,... → third clientAT+CIPSEND=0,len
AT+CIPSERVER=1,80.
When a client sends data to the ESP, you receive a +IPD notification. Let's break down the format:
| Part | Meaning | Value |
|---|---|---|
| +IPD | Incoming data marker | Always the same |
| 0 | Connection ID | Which client sent it |
| 14 | Data length (bytes) | "Hello Arduino!" = 14 chars |
| Hello Arduino! | The actual data | What the client sent |
Sending data is a two-step process. First you tell the ESP how many bytes, then you send the actual data:
→ "I want to send 5 bytes to connection 0"
After the ESP replies with >, type exactly that many bytes:
ERROR. If you send fewer, the ESP waits forever. Count carefully!
| Data to Send | Bytes | AT+CIPSEND |
|---|---|---|
Hello | 5 | AT+CIPSEND=0,5 |
ON | 2 | AT+CIPSEND=0,2 |
Temp: 23.5C | 11 | AT+CIPSEND=0,11 |
<h1>Hi</h1> | 10 | AT+CIPSEND=0,10 |
String.length() to count bytes automatically:String msg = "LED is ON";
"AT+CIPSEND=0," + String(msg.length())
Here's a complete exchange between a browser and the ESP from start to finish. Follow each step:
+IPD (read request) → CIPSEND (send reply) → CIPCLOSE (done)
In Arduino, you need to read the +IPD data and extract the connection ID and the request URL. Here's how:
esp.available() | → Returns how many bytes are waiting to be read from the ESP. Returns 0 if nothing has arrived yet. |
esp.read() | → Reads one byte (character) from the ESP buffer and removes it. Returns -1 if nothing available. |
esp.find("text") | → Reads and discards bytes until it finds the exact text. Returns true if found, false on timeout. |
esp.readString() | → Reads all available bytes into a String. Waits until timeout (default 1 second). |
void loop() {
if (esp.available()) {
// Wait for +IPD
if (esp.find("+IPD,")) {
// Read connection ID (single digit)
int connId = esp.read() - '0';
// Skip to the actual data (find ":")
esp.find(":");
// Read the full request
String request = "";
long start = millis();
while (millis() - start < 500) {
if (esp.available()) {
char c = esp.read();
request += c;
}
}
Serial.println("Client " + String(connId)
+ " says: " + request);
// Check what was requested
if (request.indexOf("/led/on") != -1) {
digitalWrite(13, HIGH);
sendResponse(connId, "LED ON!");
}
else if (request.indexOf("/led/off") != -1) {
digitalWrite(13, LOW);
sendResponse(connId, "LED OFF!");
}
else {
sendResponse(connId, "Hello from ESP!");
}
}
}
}
Let's break down the key lines from the previous slide so you understand exactly what each one does:
int connId = esp.read() - '0';
This converts an ASCII character to an integer. Here's how:
| ESP sends the character | '0' | ASCII value = 48 |
| Or it might send | '1' | ASCII value = 49 |
| Or | '2' | ASCII value = 50 |
esp.read() returns the ASCII value (e.g. 48).
Subtracting '0' (which is 48) gives the actual number:
if (esp.find("+IPD,"))
The ESP is constantly sending data. esp.find() reads and throws away every byte until it spots the exact text +IPD,. Once found, the next byte in the buffer is the connection ID character.
esp.find(":")
After reading the connection ID, there's still ,412: before the actual data. esp.find(":") skips past everything up to and including the colon:
request.indexOf("/led/on") != -1
indexOf() searches a String for a substring. Returns the position (0, 1, 2...) if found, or -1 if not found. So != -1 means "this text was found in the request".
while (millis() - start < 500) loop
Reads bytes one at a time for up to 500ms. This gives the ESP enough time to finish transmitting the full request. Without a timeout, the loop would hang forever waiting for more data.
This function wraps the CIPSEND + CIPCLOSE pattern so you can send a reply in one line:
sendResponse(0, "LED is ON!");sendResponse(1, "<h1>Hi!</h1>");AT+CIPSEND=id,len> promptString page = "<html><body>";
page += "<h1>Temperature</h1>";
page += "<p>" + String(temp) + " C</p>";
page += "</body></html>";
sendResponse(connId, page);
void sendResponse(int connId, String content) {
// Build a simple HTTP response
String httpResp = "HTTP/1.1 200 OK\r\n";
httpResp += "Content-Type: text/html\r\n";
httpResp += "Connection: close\r\n\r\n";
httpResp += content;
// Tell ESP how many bytes we're sending
String cipSend = "AT+CIPSEND=";
cipSend += String(connId);
cipSend += ",";
cipSend += String(httpResp.length());
esp.println(cipSend);
// Wait for the ">" prompt
long start = millis();
while (millis() - start < 2000) {
if (esp.available() && esp.read() == '>') {
break;
}
}
// Send the actual data
esp.print(httpResp);
delay(500);
// Close the connection
esp.println("AT+CIPCLOSE=" + String(connId));
delay(300);
}
// Now in loop() you just write:
// sendResponse(connId, "LED is ON!");
// and it handles everything!
Two Arduinos with ESP-01 modules talk to each other over WiFi. Type a message in Serial Monitor on one — it appears on the other!
ESP-01 creates a hotspot and serves a web page. Open it on your phone to toggle LEDs on the Arduino — and see sensor readings live!
| Project A: Chat | Project B: LED | |
|---|---|---|
| WiFi Mode | Station (join network) | AP (create hotspot) |
| Hardware | 2× Arduino + 2× ESP-01 | 1× Arduino + 1× ESP-01 + LEDs |
| Communication | TCP socket | HTTP web server |
| Client | Serial Monitor | Phone/laptop browser |
| Direction | Two-way messaging | Browser → Arduino + readback |
| Skills | TCP, string parsing | HTML, HTTP, web serving |
| Difficulty | ⭐⭐⭐ | ⭐⭐⭐ |
One Arduino acts as a TCP Server, the other as a TCP Client. Both connect to the same WiFi network.
This Arduino acts as the chat server. It listens for incoming connections and messages.
+IPD to extract messagessendATCommand() helper// PROJECT A — CHAT SERVER
#include <SoftwareSerial.h>
SoftwareSerial esp(2, 3);
String sendATCommand(String cmd, int t) {
String r = "";
esp.println(cmd);
long m = millis();
while (millis() - m < t) {
while (esp.available()) r += (char)esp.read();
}
Serial.println(r);
return r;
}
void setup() {
Serial.begin(9600);
esp.begin(9600);
delay(2000);
sendATCommand("AT+CWMODE=1", 1000);
sendATCommand("AT+CWJAP=\"LabNet\","
"\"password\"", 5000);
delay(3000);
sendATCommand("AT+CIPMUX=1", 1000);
sendATCommand("AT+CIPSERVER=1,333", 1000);
sendATCommand("AT+CIFSR", 1000);
Serial.println("--- Chat Server Ready ---");
}
void loop() {
// Receive from ESP
if (esp.available()) {
String data = esp.readString();
// Check for incoming message
if (data.indexOf("+IPD") != -1) {
int start = data.indexOf(':') + 1;
String msg = data.substring(start);
msg.trim();
Serial.print("Friend: ");
Serial.println(msg);
}
}
// Send from Serial Monitor
if (Serial.available()) {
String msg = Serial.readStringUntil('\n');
msg.trim();
if (msg.length() > 0) {
esp.println("AT+CIPSEND=0," +
String(msg.length()));
delay(100);
esp.println(msg);
Serial.print("You: ");
Serial.println(msg);
}
}
}
This Arduino acts as the chat client. It connects to the server and can send/receive messages.
SERVER_IP to match the IP from the server's AT+CIFSR output!
SERVER_IP in Client code// PROJECT A — CHAT CLIENT
#include <SoftwareSerial.h>
SoftwareSerial esp(2, 3);
// *** CHANGE THIS to your server's IP ***
#define SERVER_IP "192.168.1.50"
#define SERVER_PORT "333"
String sendATCommand(String cmd, int t) {
String r = "";
esp.println(cmd);
long m = millis();
while (millis() - m < t) {
while (esp.available()) r += (char)esp.read();
}
Serial.println(r);
return r;
}
void setup() {
Serial.begin(9600);
esp.begin(9600);
delay(2000);
sendATCommand("AT+CWMODE=1", 1000);
sendATCommand("AT+CWJAP=\"LabNet\","
"\"password\"", 5000);
delay(3000);
// Connect to the chat server
String con = "AT+CIPSTART=\"TCP\",\""
SERVER_IP "\"," SERVER_PORT;
sendATCommand(con, 3000);
Serial.println("--- Chat Client Ready ---");
}
void loop() {
// Receive from ESP
if (esp.available()) {
String data = esp.readString();
if (data.indexOf("+IPD") != -1) {
int start = data.indexOf(':') + 1;
String msg = data.substring(start);
msg.trim();
Serial.print("Friend: ");
Serial.println(msg);
}
}
// Send from Serial Monitor
if (Serial.available()) {
String msg = Serial.readStringUntil('\n');
msg.trim();
if (msg.length() > 0) {
esp.println("AT+CIPSEND=" +
String(msg.length()));
delay(100);
esp.println(msg);
Serial.print("You: ");
Serial.println(msg);
}
}
}
The ESP-01 creates a WiFi hotspot and serves a webpage. You open it on your phone to control LEDs on the Arduino.
This code sets up an AP, serves an HTML page with LED controls, and parses incoming HTTP requests.
GET /LED1=ON HTTP/1.1LED1=ON or LED1=OFF and toggles the pin.
<meta refresh>// PROJECT B — WiFi LED Controller
#include <SoftwareSerial.h>
SoftwareSerial esp(2, 3);
const int LED1 = 12;
const int LED2 = 13;
bool led1State = false;
bool led2State = false;
String sendATCommand(String cmd, int t) {
String r = "";
esp.println(cmd);
long m = millis();
while (millis() - m < t) {
while (esp.available()) r += (char)esp.read();
}
Serial.println(r);
return r;
}
void setup() {
Serial.begin(9600);
esp.begin(9600);
pinMode(LED1, OUTPUT);
pinMode(LED2, OUTPUT);
delay(2000);
sendATCommand("AT+CWMODE=2", 1000);
sendATCommand("AT+CWSAP=\"Arduino_WiFi\","
"\"12345678\",5,3", 3000);
sendATCommand("AT+CIPMUX=1", 1000);
sendATCommand("AT+CIPSERVER=1,80", 1000);
sendATCommand("AT+CIFSR", 1000);
Serial.println("--- LED Server Ready ---");
Serial.println("Connect to 'Arduino_WiFi'");
Serial.println("Browse http://192.168.4.1");
}
void loop() {
if (esp.available()) {
String req = esp.readString();
if (req.indexOf("+IPD") != -1) {
// Get connection ID
int idStart = req.indexOf("+IPD,") + 5;
int id = req.substring(idStart,
idStart + 1).toInt();
// Parse LED commands
if (req.indexOf("LED1=ON") != -1)
{ led1State = true; digitalWrite(LED1, HIGH); }
if (req.indexOf("LED1=OFF") != -1)
{ led1State = false; digitalWrite(LED1, LOW); }
if (req.indexOf("LED2=ON") != -1)
{ led2State = true; digitalWrite(LED2, HIGH); }
if (req.indexOf("LED2=OFF") != -1)
{ led2State = false; digitalWrite(LED2, LOW); }
// Build HTML page
String html = "<!DOCTYPE html><html>"
"<head><meta name='viewport' "
"content='width=device-width'>"
"<title>LED Control</title>"
"<style>body{font-family:sans-serif;"
"text-align:center;padding:20px}"
".btn{display:inline-block;padding:15px 30px;"
"margin:8px;font-size:18px;border:none;"
"border-radius:10px;cursor:pointer;color:#fff}"
".on{background:#28a745}"
".off{background:#dc3545}"
"</style></head><body>"
"<h1>Arduino LED Control</h1>"
"<h2>LED 1: " +
String(led1State ? "ON" : "OFF") +
"</h2>"
"<a href='/LED1=ON' class='btn on'>"
"LED1 ON</a>"
"<a href='/LED1=OFF' class='btn off'>"
"LED1 OFF</a><br>"
"<h2>LED 2: " +
String(led2State ? "ON" : "OFF") +
"</h2>"
"<a href='/LED2=ON' class='btn on'>"
"LED2 ON</a>"
"<a href='/LED2=OFF' class='btn off'>"
"LED2 OFF</a></body></html>";
// Send response
esp.println("AT+CIPSEND=" +
String(id) + "," +
String(html.length()));
delay(100);
esp.print(html);
delay(300);
esp.println("AT+CIPCLOSE=" + String(id));
}
}
}
The problem with the Arduino IDE: as your project grows, stuffing all your HTML inside C++ strings becomes messy and hard to maintain. Let's fix that.
WiFi_LED_ControllerPlatformIO creates a proper folder structure. Here's how we'll organize the LED controller project:
src/ — Your main C++ code (replaces .ino)include/ — Header files (.h) for HTML, CSS, and configplatformio.ini — Board & library settings (auto-generated)String html = "<!DOCTYPE html><html>"
"<head><style>body{font-family:"
"sans-serif}..." // 🤮 messy!
// main.cpp
#include "html_page.h"
// Just use: buildHtmlPage(led1, led2)
main.cpp not .ino#include <Arduino.h> at the topsetup(), loop(), digitalWrite(), etc.This header stores all Wi-Fi and pin settings in one place. If you change your SSID, password, or pin numbers, you only update this one file.
#ifndef?#ifndef NAME → "if not already defined"#define NAME → "mark as defined"...your code...#endif → "end of guard"
include/ folder in VS Code → New File → wifi_config.h
// include/wifi_config.h
#ifndef WIFI_CONFIG_H
#define WIFI_CONFIG_H
// ==============================
// Wi-Fi Access Point Settings
// ==============================
#define AP_SSID "Arduino_WiFi"
#define AP_PASSWORD "12345678"
#define AP_CHANNEL 5
#define AP_ENCRYPT 3 // WPA2_PSK
// ==============================
// Pin Assignments
// ==============================
#define LED1_PIN 12
#define LED2_PIN 13
// ==============================
// ESP-01 Serial Settings
// ==============================
#define ESP_RX_PIN 2
#define ESP_TX_PIN 3
#define ESP_BAUD 9600
#endif // WIFI_CONFIG_H
The CSS styles go into their own header file. This keeps the styling completely separate from the HTML structure and the C++ logic.
R"rawliteral(...)rawliteral" let you write HTML/CSS exactly as-is — no escaping quotes or special characters needed!
include/html_style.h
// include/html_style.h
#ifndef HTML_STYLE_H
#define HTML_STYLE_H
const char CSS_STYLE[] PROGMEM = R"rawliteral(
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
padding: 20px;
background: #f0f0f0;
}
h1 {
color: #2d3436;
font-size: 24px;
}
h2 { color: #636e72; }
.btn {
display: inline-block;
padding: 15px 30px;
margin: 8px;
font-size: 18px;
border: none;
border-radius: 10px;
cursor: pointer;
color: #fff;
text-decoration: none;
}
.on { background: #28a745; }
.off { background: #dc3545; }
</style>
)rawliteral";
#endif // HTML_STYLE_H
This header contains a function that builds the HTML page. It takes the LED states as parameters and returns the complete HTML string.
html_style.hled1 and led2 boolean statesStringString page = buildHtmlPage(led1State, led2State);include/html_page.h
// include/html_page.h
#ifndef HTML_PAGE_H
#define HTML_PAGE_H
#include <Arduino.h>
#include "html_style.h"
String buildHtmlPage(bool led1, bool led2) {
String page = "<!DOCTYPE html><html><head>";
page += "<meta name='viewport' "
"content='width=device-width'>";
page += "<title>LED Control</title>";
// Add CSS from html_style.h
page += CSS_STYLE;
page += "</head><body>";
page += "<h1>Arduino LED Control</h1>";
// LED 1 section
page += "<h2>LED 1: ";
page += (led1 ? "ON" : "OFF");
page += "</h2>";
page += "<a href='/LED1=ON' class='btn on'>"
"LED1 ON</a>";
page += "<a href='/LED1=OFF' class='btn off'>"
"LED1 OFF</a><br>";
// LED 2 section
page += "<h2>LED 2: ";
page += (led2 ? "ON" : "OFF");
page += "</h2>";
page += "<a href='/LED2=ON' class='btn on'>"
"LED2 ON</a>";
page += "<a href='/LED2=OFF' class='btn off'>"
"LED2 OFF</a>";
page += "</body></html>";
return page;
}
#endif // HTML_PAGE_H
The main file is now clean and focused — it only handles the Arduino logic. All HTML and config are imported from headers.
| Before: | ~90 lines, all in one file |
| After: | ~65 lines in main.cpp |
| + clean HTML in html_page.h | |
| + clean CSS in html_style.h | |
| + config in wifi_config.h |
#include <Arduino.h> is requiredsetup()#include for config and HTML// src/main.cpp
#include <Arduino.h>
#include <SoftwareSerial.h>
#include "wifi_config.h"
#include "html_page.h"
SoftwareSerial esp(ESP_RX_PIN, ESP_TX_PIN);
bool led1State = false;
bool led2State = false;
// Forward declaration
String sendATCommand(String cmd, int timeout);
void setup() {
Serial.begin(9600);
esp.begin(ESP_BAUD);
pinMode(LED1_PIN, OUTPUT);
pinMode(LED2_PIN, OUTPUT);
delay(2000);
// Set up Access Point
sendATCommand("AT+CWMODE=2", 1000);
String sapCmd = "AT+CWSAP=\"" + String(AP_SSID)
+ "\",\"" + String(AP_PASSWORD) + "\","
+ String(AP_CHANNEL) + "," + String(AP_ENCRYPT);
sendATCommand(sapCmd, 3000);
sendATCommand("AT+CIPMUX=1", 1000);
sendATCommand("AT+CIPSERVER=1,80", 1000);
sendATCommand("AT+CIFSR", 1000);
Serial.println("--- LED Server Ready ---");
}
void loop() {
if (esp.available()) {
String req = esp.readString();
if (req.indexOf("+IPD") != -1) {
int idStart = req.indexOf("+IPD,") + 5;
int id = req.substring(idStart,
idStart + 1).toInt();
// Parse LED commands
if (req.indexOf("LED1=ON") != -1)
{ led1State = true;
digitalWrite(LED1_PIN, HIGH); }
if (req.indexOf("LED1=OFF") != -1)
{ led1State = false;
digitalWrite(LED1_PIN, LOW); }
if (req.indexOf("LED2=ON") != -1)
{ led2State = true;
digitalWrite(LED2_PIN, HIGH); }
if (req.indexOf("LED2=OFF") != -1)
{ led2State = false;
digitalWrite(LED2_PIN, LOW); }
// Build page from html_page.h
String html = buildHtmlPage(
led1State, led2State);
esp.println("AT+CIPSEND=" + String(id)
+ "," + String(html.length()));
delay(100);
esp.print(html);
delay(300);
esp.println("AT+CIPCLOSE=" + String(id));
}
}
}
String sendATCommand(String cmd, int t) {
String r = "";
esp.println(cmd);
long m = millis();
while (millis() - m < t)
while (esp.available())
r += (char)esp.read();
Serial.println(r);
return r;
}
Follow these steps to create the WiFi LED Controller project using PlatformIO:
WiFi_LED_Controller, board = Arduino Unoinclude/wifi_config.h — copy the config codeinclude/html_style.h — copy the CSS codeinclude/html_page.h — copy the page buildersrc/main.cpp — copy the main codeArduino_WiFihttp://192.168.4.1 in the browser#include <Arduino.h> in main.cppsrc/ instead of include/#ifndef / #endif).ino extension instead of .cpphtml_scripts.h for JavaScript (auto-refresh)WiFi_LED_Controller folder and submit it. Your instructor will check that the code is properly organized across the 4 files.
Test your understanding of AT commands and ESP-01 configuration:
AT+CWJAP — Join WiFiAT+CWSAP — Configure APAT+CIFSR — Get IPAT+CIPMUX — Multi-connAT+CIPSERVER — Start serverAT+CIPSEND — Send data
| 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 |