Generating PDF…

Preparing…
← Back

WiFi Communication with ESP8266 ESP-01

DE6417 — Microcontrollers 2 · Week 8

PCINT Review → ESP-01 Hardware → AT Commands → Mini Projects

Today's Agenda

🔄 Part 1: PCINT Review

Quick warm-up exercises on pin change interrupts from last week

📡 Part 2: ESP8266 ESP-01

Hardware overview, pinout, wiring to Arduino Uno

⌨️ Part 3: AT Commands

Communicating with the ESP-01 via serial AT commands

🚀 Part 4: Mini Projects

Choose your project: WiFi Chat Terminal or WiFi LED Controller

Part 1: PCINT Review

Quick warm-up from Week 7

PCINT Quick Recap

GroupPortPinsVectorPCICR BitMask
0BD8–D13PCINT0_vectPCIE0PCMSK0
1CA0–A5PCINT1_vectPCIE1PCMSK1
2DD0–D7PCINT2_vectPCIE2PCMSK2
Remember: PCINT triggers on both edges (rising AND falling). You must check the pin state inside the ISR to determine the direction.
// 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();

Review Exercise: PCINT for Pin A2

You want a pin change interrupt on analog pin A2. Fill in each blank:

Port
PCINT
Group
PCICR |= (1 << );
|= (1 << );
ISR( ) { ... }
if ( & (1 << ))
Pin Mapping Cheat Sheet

Port C (Analog):
A0 = PC0 = PCINT8
A1 = PC1 = PCINT9
A2 = PC2 = PCINT10
A3 = PC3 = PCINT11
A4 = PC4 = PCINT12
A5 = PC5 = PCINT13
Formula:
PCINT# = 8 + analog pin number
e.g. A2 → PCINT(8+2) = PCINT10

Review Exercise 2: PCINT for Pin D10

Configure a pin change interrupt on digital pin D10. Fill in each blank:

Port
PCINT
Group
PCICR |= (1 << );
|= (1 << );
ISR( ) { ... }
if ( & (1 << ))
&= ~(1 << PB2);   |= (1 << PB2);
Pin Mapping Cheat Sheet

Port B (Digital 8–13):
D8 = PB0 = PCINT0
D9 = PB1 = PCINT1
D10 = PB2 = PCINT2
D11 = PB3 = PCINT3
D12 = PB4 = PCINT4
D13 = PB5 = PCINT5
Formula:
Port B: PCINT# = digital pin − 8
e.g. D10 → PCINT(10−8) = PCINT2

Group 0 covers all of Port B.

Review Exercise 3: PCINT for Pin D4

Configure a pin change interrupt on digital pin D4. Fill in each blank:

Port
PCINT
Group
PCICR |= (1 << );
|= (1 << );
ISR( ) { ... }
if ( & (1 << ))
if (!( & (1 << ))) { /* falling */ }
Pin Mapping Cheat Sheet

Port D (Digital 0–7):
D0 = PD0 = PCINT16
D1 = PD1 = PCINT17
D2 = PD2 = PCINT18
D3 = PD3 = PCINT19
D4 = PD4 = PCINT20
D5 = PD5 = PCINT21
D6 = PD6 = PCINT22
D7 = PD7 = PCINT23
Formula:
Port D: PCINT# = 16 + pin number
e.g. D4 → PCINT(16+4) = PCINT20

Group 2 covers all of Port D.
Falling edge detection:
Since PCINT fires on both edges, negate the pin read to check for LOW (falling edge).

Part 2: ESP8266 ESP-01

Adding WiFi to your Arduino

What is the ESP-01?

  • WiFi module based on the ESP8266 chip by Espressif
  • Supports 802.11 b/g/n (2.4 GHz)
  • Controlled via AT commands over serial (UART)
  • Operating voltage: 3.3V (NOT 5V tolerant!)
  • Can act as Station (connect to WiFi) or Access Point (create WiFi)
  • Only 8 pins — simple and compact
⚠️ Critical: The ESP-01 runs on 3.3V. Connecting it directly to Arduino's 5V pins will damage it. Use a voltage divider or level shifter on the TX→RX line.
📋 ESP-01 Specifications
ChipESP8266EX
WiFi802.11 b/g/n
Voltage3.3V
Current (peak)~170 mA
Flash1 MB
GPIO Pins2 (GPIO0, GPIO2)
UARTTX, RX
Baud Rate9600 or 115200 (varies)

ESP-01 Pinout

PinNameFunction
1VCC3.3V power supply
2GNDGround
3TXTransmit data (to Arduino RX)
4RXReceive data (from Arduino TX)
5RSTReset (active LOW)
6CH_PD / ENChip enable (pull HIGH)
7GPIO0General purpose I/O
8GPIO2General purpose I/O
ESP-01 Module — Top View
GND
ESP8266
ESP-01 802.11 b/g/n ⚡ 3.3V ONLY
VCC (3.3V)
GPIO2
RST
GPIO0
CH_PD / EN
RX
TX
Power Ground TX RX GPIO Enable Reset

Wiring ESP-01 to Arduino Uno

ESP-01 PinArduino / PowerNotes
VCC3.3VUse Arduino 3.3V output
GNDGNDCommon ground
TXPin 2 (SoftwareSerial RX)Direct connection OK
RXPin 3 via voltage divider5V → 3.3V needed!
CH_PD / EN3.3VMust 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
⚠️ Voltage Divider for RX

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

Arduino Uno R3 3.3V 5V GND D2 (RX) D3 (TX) ESP-01 VCC GND CH_PD TX RX direct 1kΩ 2kΩ GND 5V out 3.3V Power TX/RX RX/TX (divider)
💡 Why SoftwareSerial?
We use pins 2 & 3 instead of hardware serial (pins 0 & 1) so that the USB serial monitor stays available for debugging. The SoftwareSerial library creates a second serial port.
📦 Do I need to install SoftwareSerial?
No! 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.

First Connection Test

Upload this sketch to verify your ESP-01 is responding:

  1. Wire up the ESP-01 as shown on the previous slide
  2. Upload the code (right)
  3. Open Serial Monitor at 9600 baud
  4. Set line ending to "Both NL & CR"
  5. Type AT and press Enter
  6. You should see OK
✅ Expected output:
ATOK
If you see garbage text, your ESP may be at 115200 — see the next slide to detect & fix it.
Troubleshooting:
• No response → Check wiring & CH_PD is HIGH
• Garbled text → ESP is probably at 115200, use the baud rate detector (next slide)
• "busy" → ESP is still booting, wait 2 sec
#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());
  }
}

⚠️ Baud Rate Problem

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.

How do you know which one yours is?
Upload the baud rate detector sketch (right) — it tries both speeds and tells you which one works.

What to look for

Serial Monitor OutputMeaning
ESP responding at 9600Already at 9600 ✅ — you're good!
ESP responding at 115200Needs to be changed → next slide
No response at either rateCheck wiring & CH_PD
💡 Why 9600?
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() { }

Changing Baud Rate to 9600

If your ESP is at 115200, use this sketch to permanently change it to 9600:

✅ AT+UART_DEF=9600,8,1,0,0
This command changes the default baud rate. It is saved to flash — the ESP remembers it even after power off.

Command Breakdown

ParameterValueMeaning
Baud rate9600New speed
Data bits8Standard
Stop bits1Standard
Parity0None
Flow control0Disabled
⚠️ After running this sketch:
1. Upload the Baud Rate Detector again to confirm it now responds at 9600.
2. From now on, use 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() { }

Part 3: AT Commands

Talking to the ESP-01

Essential AT Commands

CommandDescriptionExample Response
ATTest connectionOK
AT+RSTReset moduleready
AT+GMRFirmware versionAT version: ...
AT+CWMODE=1Set Station modeOK
AT+CWMODE=2Set AP modeOK
AT+CWMODE=3Set Station + APOK
AT+CWJAP="ssid","pass"Connect to WiFiWIFI CONNECTED
AT+CIFSRGet IP address+CIFSR:STAIP,"..."
AT+CIPMUX=1Enable multiple connectionsOK
AT+CIPSERVER=1,80Start TCP server on port 80OK
WiFi Modes Explained:
  • Mode 1 (Station): ESP connects to an existing WiFi network — like your phone connecting to home WiFi
  • Mode 2 (AP): ESP creates its own WiFi hotspot — other devices connect to it
  • Mode 3 (Both): Does both simultaneously
📝 Important Notes:
  • Every command ends with \r\n (carriage return + newline)
  • Strings must use double quotes inside AT commands
  • Commands are case-sensitive

Interactive: AT Command Simulator

Try typing AT commands below. The simulator responds like a real ESP-01:

Connecting to WiFi (Station Mode)

AT+CWMODE=1
Set Station mode
AT+CWJAP="SSID","PASS"
Connect to network
Wait for WIFI CONNECTED
+ WIFI GOT IP
AT+CIFSR
Get assigned IP
AT+CWMODE=1
OK
AT+CWJAP="LabWiFi","password123"
WIFI CONNECTED
WIFI GOT IP
OK
AT+CIFSR
+CIFSR:STAIP,"192.168.1.42"
+CIFSR:STAMAC,"aa:bb:cc:dd:ee:ff"
OK
✅ You're on the network!
The ESP now has IP 192.168.1.42. Other devices on the same network can communicate with it.

Finding the NZSE Wi-Fi SSID

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:

📱 Option 1 — Check Your Phone or Laptop
  1. Open Wi-Fi Settings on your phone or laptop
  2. Look at the network you are currently connected to
  3. The network name shown is your SSID
📡 Option 2 — Scan with the ESP-01
Send the command AT+CWLAP via Serial Monitor. The ESP will list all visible networks:
AT+CWLAP
+CWLAP:(3,"NZSE-Student",-45,...)
+CWLAP:(3,"NZSE-Staff",-52,...)
+CWLAP:(0,"Guest-WiFi",-70,...)
OK

The format is: (encryption, "SSID", signal_strength, ...)

⚠️ SSID Tips
  • The SSID is case-sensitive"NZSE-Student" is different from "nzse-student"
  • Copy the name exactly as it appears in the scan results
  • If you see multiple NZSE networks, use the one you normally connect to on your laptop
🔑 Getting the Password
  • The Wi-Fi password is usually provided by your instructor at the start of the lab session
  • On Windows: Settings → Network → Wi-Fi → your network → Properties → "Show password"
  • On macOS: System Settings → Wi-Fi → click (i) next to the network → "Show password"
  • You can also check Keychain Access (macOS) or netsh wlan show profile name="SSID" key=clear (Windows CMD)

Helper: sendATCommand()

Writing AT command code gets repetitive. This helper function sends a command and waits for a response:

Usage:
sendATCommand("AT+CWMODE=2", 1000);
sendATCommand("AT+CWSAP=...", 3000);

The function sends the command, waits up to timeout ms, and prints the ESP's response to Serial Monitor.
💡 Why this matters:
Without this helper, you'd need to repeat the send-wait-read pattern for every single AT command. This keeps your code clean.
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);
}

Connecting to NZSE Wi-Fi

Replace YOUR_SSID and YOUR_PASSWORD with the actual values from the previous slide:

1. Set Station Mode
AT+CWMODE=1
2. Connect
AT+CWJAP="SSID","PASS"
3. Wait ~5-10 seconds
for WIFI CONNECTED
4. Check IP
AT+CIFSR
⚠️ Common Errors:
  • FAIL → Wrong SSID or password
  • DISCONNECT → Out of range or network full
  • No response → ESP may need a reset (AT+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());
}

Verifying: Send a GET Request

Once connected to NZSE Wi-Fi, verify the connection actually works by fetching a real web page. We'll request example.com:

Already connected
WIFI GOT IP
AT+CIPSTART="TCP","example.com",80
Open TCP connection
AT+CIPSEND=length
Tell ESP how many bytes to send
Send HTTP request text
GET / HTTP/1.1\r\nHost: example.com\r\n\r\n
Read response
HTML from example.com
// Open TCP to example.com on port 80
AT+CIPSTART="TCP","example.com",80
CONNECT
OK
// Tell ESP we'll send 43 bytes
AT+CIPSEND=43
OK
> ← ready for data
// Send the HTTP GET request
GET / HTTP/1.1\r\nHost: example.com\r\n\r\n
SEND OK
// Response arrives from the server
+IPD,1256:HTTP/1.1 200 OK
...
<h1>Example Domain</h1>
...
✅ If you see HTTP/1.1 200 OK — you are connected to the internet through NZSE Wi-Fi!

GET Request — Full Code

This sketch connects to NZSE Wi-Fi and then fetches a page from example.com to prove internet connectivity:

📝 How the byte count works:
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).
✅ Expected Serial Monitor Output:
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());
}

Creating a Hotspot (AP Mode)

In AP mode, the ESP-01 creates its own WiFi network. No router needed — devices connect directly to it.

AT+CWMODE=2
Set AP mode
AT+CWSAP="MyESP","pass1234",5,3
Name, Password, Channel, Encryption
AT+CIFSR
Default IP: 192.168.4.1
Encryption types: 0=Open, 2=WPA_PSK, 3=WPA2_PSK, 4=WPA_WPA2_PSK
AT+CWMODE=2
OK
AT+CWSAP="Arduino_WiFi","12345678",5,3
OK
AT+CIFSR
+CIFSR:APIP,"192.168.4.1"
+CIFSR:APMAC,"aa:bb:cc:dd:ee:ff"
OK
// Now connect your phone/laptop to "Arduino_WiFi"
// and browse to http://192.168.4.1

Sending & Receiving Data

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:

1. Enable multiple calls
AT+CIPMUX=1
2. Start listening
AT+CIPSERVER=1,80
3. Someone connects
+IPD,id,len:data
4. Send reply
AT+CIPSEND=id,len → data
5. Hang up
AT+CIPCLOSE=id
📞 Phone Call Analogy:
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
🔢 Connection IDs
Each client that connects gets a number (0, 1, 2...). When 3 phones connect at once:
  • +IPD,0,... → first client
  • +IPD,1,... → second client
  • +IPD,2,... → third client
You reply to each using their ID: AT+CIPSEND=0,len
Port 80 is the standard HTTP port. When you type a URL in a browser, it connects to port 80 by default. That's why we use AT+CIPSERVER=1,80.

Understanding +IPD (Incoming Data)

When a client sends data to the ESP, you receive a +IPD notification. Let's break down the format:

+IPD,0,14:Hello Arduino!
PartMeaningValue
+IPDIncoming data markerAlways the same
0Connection IDWhich client sent it
14Data length (bytes)"Hello Arduino!" = 14 chars
Hello Arduino!The actual dataWhat the client sent

Real-World Examples

// Browser visits your ESP's IP address:
+IPD,0,402:GET / HTTP/1.1\r\nHost: 192...
// Browser clicks a link to /led/on:
+IPD,1,410:GET /led/on HTTP/1.1\r\n...
// Simple TCP client sends "ON":
+IPD,0,2:ON
// Another client sends temperature request:
+IPD,2,8:GET_TEMP
💡 Key Insight:
When a browser connects, the data is a full HTTP request (GET / HTTP/1.1 ...). When a simple TCP app connects, it's just raw text. Your Arduino code needs to parse this data to decide what to do.

AT+CIPSEND — Sending a Reply

Sending data is a two-step process. First you tell the ESP how many bytes, then you send the actual data:

Step 1: Declare the length

AT+CIPSEND=0,5

→ "I want to send 5 bytes to connection 0"

Step 2: Send the data

After the ESP replies with >, type exactly that many bytes:

Hello
⚠️ Byte count must be exact!
If you say 5 bytes but send 10, you get ERROR. If you send fewer, the ESP waits forever. Count carefully!

Counting Bytes — Examples

Data to SendBytesAT+CIPSEND
Hello5AT+CIPSEND=0,5
ON2AT+CIPSEND=0,2
Temp: 23.5C11AT+CIPSEND=0,11
<h1>Hi</h1>10AT+CIPSEND=0,10
// Example: Send "LED is ON" to client 0
AT+CIPSEND=0,9
OK
> ← ESP is ready for data
LED is ON
SEND OK
// Example: Send HTML to client 1
AT+CIPSEND=1,20
OK
>
<h1>Hello WiFi!</h1>
SEND OK
💡 In Arduino code:
Use String.length() to count bytes automatically:
String msg = "LED is ON";
"AT+CIPSEND=0," + String(msg.length())

Full Conversation Example

Here's a complete exchange between a browser and the ESP from start to finish. Follow each step:

// 1. Setup: allow multiple connections
AT+CIPMUX=1
OK
// 2. Start server on port 80
AT+CIPSERVER=1,80
OK
// 3. A browser connects! (phone/laptop)
0,CONNECT
+IPD,0,398:GET / HTTP/1.1
Host: 192.168.4.1
// 4. Send a web page back (46 bytes)
AT+CIPSEND=0,46
OK
>
<html><body><h1>Hello from ESP!</h1></body></html>
SEND OK
// 5. Close the connection
AT+CIPCLOSE=0
OK
0,CLOSED

What the Browser Sees

🔒 192.168.4.1

Hello from ESP!

Another Example: LED Control

// User clicks "Turn ON" button
+IPD,0,412:GET /led/on HTTP/1.1
// Arduino reads "/led/on" → turns on LED
// Then sends confirmation page
AT+CIPSEND=0,31
>
<h1>LED is now ON</h1>
SEND OK
AT+CIPCLOSE=0
🔄 The pattern is always the same:
+IPD (read request) → CIPSEND (send reply) → CIPCLOSE (done)

Parsing +IPD in Code

In Arduino, you need to read the +IPD data and extract the connection ID and the request URL. Here's how:

What we need to extract:
+IPD,0,412:GET /led/on HTTP/1.1
  • 0 → Connection ID (for CIPSEND)
  • /led/on → What the user requested
ESP Serial Functions Used:
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!");
      }
    }
  }
}

Code Deep Dive

Let's break down the key lines from the previous slide so you understand exactly what each one does:

Line: 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:

48 - 48 = 0  →  connId = 0
49 - 48 = 1  →  connId = 1
50 - 48 = 2  →  connId = 2
Line: 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.

+IPD,0,412:GET /led/on
↑ discarded   ↑ next read
Line: 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:

0,412:GET /led/on HTTP/1.1
↑ discarded     ↑ what we read next
Line: 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".

The 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.

Helper: sendResponse()

This function wraps the CIPSEND + CIPCLOSE pattern so you can send a reply in one line:

Usage:
sendResponse(0, "LED is ON!");
sendResponse(1, "<h1>Hi!</h1>");

It automatically:
  1. Counts the bytes for you
  2. Sends AT+CIPSEND=id,len
  3. Waits for > prompt
  4. Sends the data
  5. Closes the connection
💡 Sending HTML pages:
You can send full HTML, not just text:
String 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!

Part 4: Mini Projects

Choose your project!

Choose Your Project

💬 Project A: WiFi Chat Terminal

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!

Station Mode TCP Client/Server Two-Way

🎮 Project B: WiFi LED Controller

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!

AP Mode Web Server Phone Control

Comparison

Project A: ChatProject B: LED
WiFi ModeStation (join network)AP (create hotspot)
Hardware2× Arduino + 2× ESP-011× Arduino + 1× ESP-01 + LEDs
CommunicationTCP socketHTTP web server
ClientSerial MonitorPhone/laptop browser
DirectionTwo-way messagingBrowser → Arduino + readback
SkillsTCP, string parsingHTML, HTTP, web serving
Difficulty⭐⭐⭐⭐⭐⭐
🤝 Team up or Solo!
Project A works best with a partner (two boards). Project B can be done solo. Pick what excites you!

Project A: Chat Architecture

One Arduino acts as a TCP Server, the other as a TCP Client. Both connect to the same WiFi network.

🖥️
Arduino A
TCP Server
Listens on port 333
💻
Arduino B
TCP Client
Connects to Server IP
Both join same WiFi network
Server starts listening (port 333)
Client connects to Server's IP
💬 Send messages back and forth!

Server Setup Steps

// 1. Station mode
AT+CWMODE=1OK
// 2. Join WiFi
AT+CWJAP="LabNet","password"
WIFI CONNECTED
// 3. Get IP (note this down!)
AT+CIFSR192.168.1.50
// 4. Enable multi-connection
AT+CIPMUX=1OK
// 5. Start TCP server
AT+CIPSERVER=1,333OK

Client Setup Steps

// 1. Station mode + Join WiFi (same as above)
AT+CWMODE=1OK
AT+CWJAP="LabNet","password"
// 2. Connect to server
AT+CIPSTART="TCP","192.168.1.50",333
CONNECT
OK
// 3. Send a message (11 bytes)
AT+CIPSEND=11
> Hello there
SEND OK

Project A: Server Code

This Arduino acts as the chat server. It listens for incoming connections and messages.

Key Concepts:
  • Parses +IPD to extract messages
  • Sends typed messages back to the client
  • Uses sendATCommand() helper
📝 Change these values:
• WiFi SSID & password
• Port number (333 default)
// 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);
    }
  }
}

Project A: Client Code

This Arduino acts as the chat client. It connects to the server and can send/receive messages.

⚠️ Important:
Change SERVER_IP to match the IP from the server's AT+CIFSR output!
🧪 Testing:
  1. Upload Server code to Arduino A
  2. Note the server's IP address
  3. Set SERVER_IP in Client code
  4. Upload Client code to Arduino B
  5. Open two Serial Monitors!
  6. Type & send messages
// 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);
    }
  }
}

Project B: LED Controller

The ESP-01 creates a WiFi hotspot and serves a webpage. You open it on your phone to control LEDs on the Arduino.

📡
Arduino + ESP-01
WiFi AP + Web Server
192.168.4.1
📶
📱
Your Phone
Connects to ESP WiFi
Opens browser
Hardware needed:
  • 1× Arduino Uno + ESP-01
  • 2× LEDs (e.g. red on D12, green on D13)
  • 2× 220Ω resistors
  • Your phone or laptop

How It Works

ESP-01 starts as WiFi hotspot
"Arduino_WiFi"
Phone connects to "Arduino_WiFi"
Phone opens http://192.168.4.1
Arduino sends HTML page with buttons
Phone taps button → GET /LED1=ON
Arduino reads URL, toggles LED ✓

Project B: Full Code

This code sets up an AP, serves an HTML page with LED controls, and parses incoming HTTP requests.

How the URL parsing works:
When you click a button on the webpage, the browser sends:
GET /LED1=ON HTTP/1.1

Arduino checks if the request contains LED1=ON or LED1=OFF and toggles the pin.
💡 Try adding:
  • A third LED
  • Potentiometer reading on the page
  • Auto-refresh with <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));
    }
  }
}

Better Workflow: PlatformIO + VS Code

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.

⚠️ The Problem with Arduino IDE
  • All code lives in one .ino file
  • HTML is buried inside C++ string concatenation
  • No syntax highlighting for the HTML
  • Difficult to debug or change the page layout
  • No folder structure — everything is flat
✅ The PlatformIO Solution
  • Write HTML in separate .h files with proper syntax
  • Organize code into multiple files
  • Use VS Code with autocomplete, IntelliSense, and linting
  • Professional project structure

Installing PlatformIO

  1. Open VS Code
  2. Go to Extensions (Ctrl+Shift+X / Cmd+Shift+X)
  3. Search for "PlatformIO IDE"
  4. Click Install — wait for it to finish
  5. Restart VS Code when prompted

Creating a New Project

  1. Click the PlatformIO Home icon (alien head 🐝) in the sidebar
  2. Click "New Project"
  3. Name: WiFi_LED_Controller
  4. Board: Arduino Uno
  5. Framework: Arduino
  6. Click Finish
💡 First time? PlatformIO will download the Arduino toolchain automatically. This may take a few minutes on slow connections.

Project Structure

PlatformIO creates a proper folder structure. Here's how we'll organize the LED controller project:

WiFi_LED_Controller/
├── platformio.ini
├── src/
│   └── main.cpp  ← Arduino code
└── include/
    ├── html_page.h  ← HTML template
    ├── html_style.h  ← CSS styles
    └── wifi_config.h ← Wi-Fi settings
📁 Folder purposes:
  • src/ — Your main C++ code (replaces .ino)
  • include/ — Header files (.h) for HTML, CSS, and config
  • platformio.ini — Board & library settings (auto-generated)

Why Separate Files?

Before (Arduino IDE) — one file:
String html = "<!DOCTYPE html><html>"
  "<head><style>body{font-family:"
  "sans-serif}..."  // 🤮 messy!
After (PlatformIO) — clean separation:
// main.cpp
#include "html_page.h"
// Just use: buildHtmlPage(led1, led2)
📝 Key Difference: .ino → .cpp
  • PlatformIO uses main.cpp not .ino
  • You must add #include <Arduino.h> at the top
  • Functions must be declared before use (or use header files)
  • Everything else works the same — setup(), loop(), digitalWrite(), etc.

File 1: wifi_config.h

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.

💡 What is #ifndef?
This is called an include guard. It prevents the file from being included twice, which would cause "redefinition" errors.

Pattern:
#ifndef NAME → "if not already defined"
#define NAME → "mark as defined"
...your code...
#endif → "end of guard"
📝 Create this file:
Right-click the 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

File 2: html_style.h

The CSS styles go into their own header file. This keeps the styling completely separate from the HTML structure and the C++ logic.

✅ Benefits:
  • Change the look without touching any C++ code
  • Easy to experiment with colors, sizes, layouts
  • Less clutter in your main code
⚠️ Why raw strings?
C++ raw strings R"rawliteral(...)rawliteral" let you write HTML/CSS exactly as-is — no escaping quotes or special characters needed!
📝 Create this file:
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

File 3: html_page.h

This header contains a function that builds the HTML page. It takes the LED states as parameters and returns the complete HTML string.

💡 How it works:
  1. Includes the CSS from html_style.h
  2. Takes led1 and led2 boolean states
  3. Builds HTML with correct ON/OFF labels
  4. Returns complete page as a String
Usage in main.cpp:
String page = buildHtmlPage(led1State, led2State);
That's it! One clean line instead of 20+ lines of string concatenation.
📝 Create: 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

File 4: main.cpp

The main file is now clean and focused — it only handles the Arduino logic. All HTML and config are imported from headers.

✅ Compare:
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
💡 Key differences from .ino:
  • #include <Arduino.h> is required
  • Helper functions declared before setup()
  • Uses #include for config and HTML
📤 Upload: Click the arrow button in the PlatformIO toolbar (bottom of VS Code), or press Ctrl+Alt+U.
// 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;
}

Your Task: Build It!

Follow these steps to create the WiFi LED Controller project using PlatformIO:

Step-by-Step Instructions:
  1. Install PlatformIO extension in VS Code
  2. Create a new project: WiFi_LED_Controller, board = Arduino Uno
  3. Create include/wifi_config.h — copy the config code
  4. Create include/html_style.h — copy the CSS code
  5. Create include/html_page.h — copy the page builder
  6. Edit src/main.cpp — copy the main code
  7. Wire up your ESP-01 + 2 LEDs
  8. Click Upload (→ arrow in PlatformIO toolbar)
  9. Connect your phone to Arduino_WiFi
  10. Open http://192.168.4.1 in the browser
⚠️ Common PlatformIO Mistakes:
  • Forgetting #include <Arduino.h> in main.cpp
  • Putting .h files in src/ instead of include/
  • Missing include guards (#ifndef / #endif)
  • Using .ino extension instead of .cpp
🏆 Challenge Extensions:
  • Add a third LED — update all 4 files
  • Add an analog sensor reading to the page
  • Create a html_scripts.h for JavaScript (auto-refresh)
  • Change the CSS to use a dark theme
📤 Submitting:
Zip your entire WiFi_LED_Controller folder and submit it. Your instructor will check that the code is properly organized across the 4 files.

Exercise: AT Command Knowledge

Test your understanding of AT commands and ESP-01 configuration:

AT+CWMODE=
AT+CIPMUX=
Arduino TX is but ESP needs
Quick Reference

Modes:
1 = Station, 2 = AP, 3 = Both

Key Commands:
AT+CWJAP — Join WiFi
AT+CWSAP — Configure AP
AT+CIFSR — Get IP
AT+CIPMUX — Multi-conn
AT+CIPSERVER — Start server
AT+CIPSEND — Send data

Summary: Key Takeaways

✓ ESP-01 Basics
  • 3.3V module — use voltage divider!
  • Controlled via AT commands over UART
  • SoftwareSerial keeps USB debug free
  • CH_PD must be pulled HIGH
📡 WiFi Modes
  • Station: join existing network
  • AP: create your own hotspot
  • Both work for different use cases
⌨️ AT Command Flow
  1. Set mode (CWMODE)
  2. Connect/configure (CWJAP/CWSAP)
  3. Get IP (CIFSR)
  4. Enable multi-conn (CIPMUX)
  5. Start server (CIPSERVER)
🚀 Projects
  • Chat: TCP client/server, station mode
  • LED Control: Web server, AP mode
  • Both use the sendATCommand() helper

📋 ESP-01 Quick Reference

⚡ AT Commands

CommandWhat it does
ATTest — responds OK
AT+RSTReset the ESP module
AT+CWMODE=1Station mode (join WiFi)
AT+CWMODE=2AP mode (create hotspot)
AT+CWMODE=3Both Station + AP
AT+CWJAP="ssid","pw"Connect to a WiFi network
AT+CWSAP="name","pw",ch,encConfigure AP (name, password, channel, encryption)
AT+CIFSRGet IP address
AT+CIPMUX=1Enable multiple connections
AT+CIPSERVER=1,80Start TCP server on port 80
AT+CIPSEND=id,lenSend len bytes on connection id
AT+CIPCLOSE=idClose connection id

📨 Incoming Data Format

PatternMeaning
+IPD,id,len:dataReceived len bytes from connection id
OKCommand succeeded
ERRORCommand failed
SEND OKData sent successfully
>Ready for data (after CIPSEND)

🔧 ESP Serial Functions (Arduino)

FunctionWhat 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)

💡 Common Patterns

PatternUse
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