If you are using one ESP32 and one UNO R4 WIFI, please refer to the wiring tutorial below.
https://wiki.elegoo.com/en/oshw-getting-started-&-kits/simpleremote
After completing this project, you will have a dual ESP32 system that can chat bidirectionally via WiFi TCP protocol:
Simple analogy: It's like two people chatting on WeChat — once the two ESP32 boards establish a WiFi TCP connection, type a message in either side's serial monitor and press Enter, the other side receives it instantly.
ESP32_Server, starts TCP server listening on port 8080192.168.4.1:8080Sent: and Received: markersESP32_Dual_Board_WiFi_TCP) on your computer in an easily accessible location (e.g., desktop).ino file:
ESP32_TCP_Server/ESP32_TCP_Server.ino → Flash to Server boardESP32_TCP_Client/ESP32_TCP_Client.ino → Flash to Client board💡 Default configuration works out of the box! If you want to customize hotspot name and password, you need to modify both files simultaneously.
Modify Server Side (ESP32_TCP_Server.ino):
const char* ap_ssid = "ESP32_Server"; // ⚠️ Change to your desired hotspot name
const char* ap_password = "12345678"; // ⚠️ Change to your desired password (min 8 chars)
const uint16_t serverPort = 8080; // TCP port
Modify Client Side (ESP32_TCP_Client.ino):
const char* serverSSID = "ESP32_Server"; // ⚠️ Must match Server side!
const char* serverPassword = "12345678"; // ⚠️ Must match Server side!
const char* serverIP = "192.168.4.1"; // Server's AP IP, usually no need to change
const uint16_t serverPort = 8080; // ⚠️ Must match Server side!
⚠️ Important! If you change the hotspot name or password, both sides must be modified simultaneously, otherwise the Client cannot connect to the Server!
File → Open → select ESP32_TCP_Server/ESP32_TCP_Server.inoTools → Board → select your ESP32 board (e.g., ESP32 Dev Module)Tools → Port → select the COM port for ServerUpload button (arrow icon) to start flashingHard resetting via RTS pin... indicates successFile → Open → select ESP32_TCP_Client/ESP32_TCP_Client.inoTools → Board → select your ESP32 boardTools → Port → select the COM port for ClientUpload button to start flashingAfter successful flashing, let's see what the ESP32 is doing through the "Serial Monitor":
Ctrl+Shift+M)========================================
ESP32 TCP Server — Starting up...
========================================
[SERVER] Creating AP hotspot: ESP32_Server
[SERVER] AP IP Address: 192.168.4.1
[SERVER] TCP Server started on port: 8080
[SERVER] Waiting for client connection...
[SERVER] Type a message and press Enter to send.
AP IP Address: 192.168.4.1 → WiFi hotspot created successfully ✔TCP Server started on port: 8080 → TCP server started successfully ✔========================================
ESP32 TCP Client — Starting up...
========================================
[CLIENT] Connecting to AP: ESP32_Server
.....
[CLIENT] WiFi connected!
[CLIENT] Client IP: 192.168.4.2
[CLIENT] Server IP: 192.168.4.1
[CLIENT] Connecting to TCP server... CONNECTED!
[CLIENT] Ready to chat. Type a message and press Enter.
[CLIENT] Server says: [Server] Client connected! Ready to chat.
WiFi connected! → WiFi connection successful ✔CONNECTED! → TCP connection successful ✔Server says: [Server] Client connected! Ready to chat. → Received server welcome message ✔Type a message in Server's serial port, press Enter to send:
[Server] Hello Client! How are you?
[SERVER] Sent: Hello Client! How are you?
Client's serial port will receive:
[CLIENT] Received: Hello Client! How are you?
Type a message in Client's serial port, press Enter to send:
[CLIENT] Sent: I'm great! Thanks for asking!
Server's serial port will receive:
[SERVER] Received: I'm great! Thanks for asking!
| LED Location | Status | Meaning |
|---|---|---|
| Server GPIO2 LED | Off | Waiting for client connection |
| Server GPIO2 LED | On | Client connected |
| Client GPIO2 LED | Off | Not connected to server |
| Client GPIO2 LED | On | Connected to server |
Unplug the Client's USB cable (or press EN button to reset), Server will display:
----------------------------------------
[SERVER] <<< Client disconnected.
[SERVER] Waiting for next client...
----------------------------------------
Reconnect the Client, it will automatically reconnect and resume communication.
💡 Beginners look here first: 90% of issues are listed below. If your situation is not in the list, go back and check all wiring and configuration from scratch.
A: This is the most common beginner error, almost 100% caused by incorrect baud rate.
A: The most likely cause is incorrect line ending setting in the serial monitor.
A: Check the following in order:
[SERVER] TCP Server started on port: 8080ESP32_Server hotspotserverSSID and serverPassword in Client code match the Server sideesp32_server and ESP32_Server are differentA: Check the following in order:
[SERVER] TCP Server started on port: 8080serverIP in Client code should be 192.168.4.1 (Server's AP default IP)serverPort on both sides must be consistent, default is 8080A: Check one by one:
[CLIENT] Sent: ...A: Check one by one:
CONNECTED! and Ready to chat[SERVER] Sent: ...[CLIENT] Received: ...A: Check the following:
LED_PIN = 2 (GPIO2)LED_PIN = 2 (GPIO2)A: Check the following in order:
[CLIENT] Attempting to reconnect...3000 valueA: Check the following:
ESP32_TCP_Server.inoESP32_TCP_Client.ino| Problem Symptom | Most Likely Cause | Fastest Solution |
|---|---|---|
| Serial garbled / blank | Incorrect baud rate | Change to 115200, press EN to reset |
| No response after input | Line ending setting wrong | Change to "Newline" or "Both NL & CR" |
| Client cannot connect WiFi | Wrong password / weak signal | Check SSID and password, move boards closer |
| TCP connection fails | Wrong Server IP/port | Confirm serverIP = 192.168.4.1, ports match |
| Cannot receive messages | Line ending / connection broken | Check line ending setting, reconnect |
| LED not on | Wrong GPIO definition | Confirm LED_PIN = 2 |
| Changes not taking effect | Not re-flashed | Re-upload code |
| Reconnect not working | Server not running | Confirm Server is running |
💡 After completing the experiment, understanding the principles behind it will yield twice the result with half the effort! This step is "knowing the what, and more importantly, the why".
This project uses a dual-board architecture, with two ESP32 boards taking on different roles:
| Device | Role | Main Responsibilities | Analogy |
|---|---|---|---|
| ESP32 Server | Hotspot + TCP Server | Create WiFi, listen on port, receive messages, send messages | Chat server (waiting for connections, sending/receiving messages) |
| ESP32 Client | TCP Client | Connect to hotspot, establish TCP, send messages, receive messages | Chat client (actively connecting, sending/receiving messages) |
Module Relationship Diagram:
ESP32 Server (.ino) ESP32 Client (.ino)
├── WiFi.softAP() Create hotspot ├── WiFi.begin() Connect to hotspot
├── WiFiServer(8080) TCP Server ├── WiFiClient.connect() TCP Client
├── server.available() Wait for conn ├── Serial.available() Read serial input
├── Serial.available() Read serial ├── client.println() Send message
├── client.println() Send message ├── client.readStringUntil() Receive
└── client.readStringUntil() Receive └── Auto-reconnect mechanism
Why use dual-board architecture?
| Advantage | Explanation |
|---|---|
| Learn communication | Intuitive understanding of TCP client-server model |
| Independent testing | Debug Server and Client code separately |
| Scalable | Easy to add more clients later |
| Real-world scenario | Simulate real IoT device communication patterns |
TCP (Transmission Control Protocol) is a connection-oriented reliable transport protocol with the following characteristics:
| Feature | Description | Implementation in This Project |
|---|---|---|
| Connection-oriented | Must establish connection before communicating | Client must first connect() to Server |
| Reliable transmission | Guarantees no data loss or duplication | TCP protocol itself guarantees reliable message delivery |
| Ordering | Data arrives in send order | Messages arrive at the other end in order |
| Flow control | Prevents sender from sending too fast | Automatically handled by TCP stack |
| Congestion control | Prevents network overload | Automatically handled by TCP stack |
UDP's shortcomings:
📌 Why choose TCP? Because we need to ensure reliable message delivery. If using UDP, important messages might be lost, while TCP ensures every message can be received by the other party through connection mechanism and retransmission.
WiFi Mode Comparison:
| Mode | Description | Used in This Project |
|---|---|---|
| STA Mode | Act as client connecting to router | Client uses this mode to connect to Server's hotspot |
| AP Mode | Act as hotspot connected by other devices | Server uses this mode to create hotspot |
| STA+AP Mode | Simultaneously act as client and hotspot | ESP32 supports it but not needed in this project |
Why Server uses AP mode instead of STA mode?
This is a very important concept in embedded programming — non-blocking programming.
❌ Problem with delay(): The program "gets stuck" here, unable to do anything else during that time.
✅ Benefit of millis(): millis() returns "how many milliseconds have elapsed since power-on". We use it to determine "is it time yet?" and continue doing other things if not.
Non-blocking main loop in Client:
void loop() {
// 1. Check connection status (non-blocking)
if (!clientConnected || !client.connected()) {
// Disconnect and reconnect logic...
return;
}
// 2. Read serial input and send (non-blocking)
if (Serial.available()) {
// Read and send if input exists
}
// 3. Read messages from other end (non-blocking)
if (client.available()) {
// Display if message exists
}
delay(10); // Short delay, does not affect responsiveness
}
// Each loop checks all conditions without being stuck by any one
Non-blocking listening in Server:
void loop() {
// 1. Check new connections (non-blocking)
if (!clientConnected) {
WiFiClient newClient = server.available();
if (newClient) { /* Handle new connection */ }
}
// 2. Read serial input and send (non-blocking)
if (Serial.available()) {
// Read and send if input exists
}
// 3. Read messages from other end (non-blocking)
if (client.available()) {
// Display if message exists
}
delay(10);
}
📌 Key Concepts:
Serial.available()checks for serial data, reads if available, skips if notclient.available()checks for TCP data, reads if available, skips if notserver.available()checks for new connections, accepts if available, skips if not- All these checks are non-blocking and will not get stuck in the main loop
This project ensures reliable message transmission through the following mechanisms:
| Mechanism | Implementation | Description |
|---|---|---|
| Connection confirmation | client.connect() return value |
Only start sending messages after successful connection |
| TCP reliable transmission | TCP stack automatic handling | Automatic retransmission on message loss, guaranteed reliable delivery |
| Timeout reconnection | millis() + timing judgment |
Attempts to reconnect every 3 seconds after disconnection |
| Real-time bidirectional communication | Serial.available() + client.available() |
Both ends send and receive independently without blocking |
The core of this project is serial-input-driven bidirectional chat, working as follows:
| Step | Description |
|---|---|
| 1. User types text in serial monitor | Ends with newline character (requires line ending setting) |
2. ESP32 detects via Serial.available() |
Checks if new serial data is available |
3. Reads via Serial.readStringUntil('\n') |
Reads until newline is encountered |
4. Sends via TCP client.println() |
Sends to other party, auto-adds newline |
5. Other party receives via client.available() |
Checks if new TCP data is available |
6. Reads via client.readStringUntil('\n') |
Reads complete message |
7. Displays via Serial.println() |
Displays on serial monitor |
⚠️ Important: The serial monitor's line ending setting must be correct, otherwise the ESP32 cannot recognize message completion. Recommended setting is "Newline" or "Both NL & CR".
💡 Before looking at specific code, learn about what libraries and functions this project uses, making it easier to read the code later.
| Library Name | Source | Function Description |
|---|---|---|
WiFi.h |
ESP32 Core Built-in | ESP32-specific WiFi library, provides AP/STA mode, TCP server/client, etc. |
| Function/Command | Meaning and Purpose |
|---|---|
WiFi.mode(WIFI_AP) |
Set WiFi to Hotspot Mode. ESP32 itself becomes a WiFi hotspot |
WiFi.mode(WIFI_STA) |
Set WiFi to Station Mode. ESP32 acts as client connecting to other hotspots |
WiFi.softAP(ssid, pass) |
Create AP hotspot. Pass in hotspot name and password |
WiFi.softAPIP() |
Get IP address in AP mode. Returns 192.168.4.1 by default |
WiFi.begin(ssid, pass) |
Start WiFi connection. Connect to specified hotspot |
WiFi.status() |
Query current connection status. WL_CONNECTED means connected |
WiFi.disconnect() |
Disconnect WiFi connection |
WiFi.localIP() |
Get IP address in STA mode |
| Function/Command | Meaning and Purpose |
|---|---|
WiFiServer server(port) |
Create TCP server instance. Specify listen port (default 8080) |
server.begin() |
Start TCP server. Start listening on port, accept client connections |
server.available() |
Non-blocking check for new connections. Returns WiFiClient object when client connects |
client.remoteIP() |
Get client IP address |
client.println(data) |
Send data to client. Auto-adds newline |
client.readStringUntil('\n') |
Read data from client. Reads until newline |
client.available() |
Check if data can be read |
client.connected() |
Check if connection is still valid |
client.stop() |
Close client connection |
| Function/Command | Meaning and Purpose |
|---|---|
WiFiClient client |
Create TCP client instance |
client.connect(ip, port) |
Connect to TCP server. Returns true for success |
client.println(data) |
Send data to server. Auto-adds newline |
client.readStringUntil('\n') |
Read data from server |
client.available() |
Check if data can be read |
client.connected() |
Check if connection is still valid |
client.stop() |
Close TCP connection |
| Function/Command | Meaning and Purpose |
|---|---|
pinMode(pin, OUTPUT) |
Configure pin as output mode. Used for LED control |
digitalWrite(pin, HIGH/LOW) |
Set pin output voltage. HIGH = 3.3V, LOW = 0V |
millis() |
Return milliseconds since boot. Used for non-blocking timing |
delay(ms) |
Delay specified milliseconds. ⚠️ Blocks program, only used in initialization |
Serial.begin(115200) |
Initialize serial communication. Baud rate 115200 |
Serial.available() |
Check if serial data can be read. Non-blocking, returns 0 or 1 |
Serial.readStringUntil('\n') |
Read string from serial port. Reads until newline |
Serial.print() / Serial.println() |
Print information to serial port. Used for debug output |
💡 Server is responsible for creating WiFi hotspot and TCP server, waiting for Client connection and handling bidirectional chat.
#include <WiFi.h>
// ==================== Configuration ====================
const char* ap_ssid = "ESP32_Server"; // Hotspot name
const char* ap_password = "12345678"; // Hotspot password (min 8 chars)
const uint16_t serverPort = 8080; // TCP server port
// ==================== LED ====================
#define LED_PIN 2 // Onboard LED pin
// ==================== TCP Server ====================
WiFiServer server(serverPort); // Create TCP server instance
WiFiClient client; // Persistent client object
bool clientConnected = false; // Connection status flag
Running Logic: Include WiFi library, define hotspot info, LED pin, TCP server instance, and status variables.
Key Command Analysis:
const char* ap_password = "12345678": Password must be at least 8 characters, fewer will cause softAP() to silently failWiFiServer server(8080): Create TCP server object, listening on port 8080WiFiClient client: Persistent client object, used across multiple calls in loop() to maintain connectionclientConnected: Connection status flag, controls whether to process messagesvoid setup() {
Serial.begin(115200);
delay(1000);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
Serial.println();
Serial.println("========================================");
Serial.println(" ESP32 TCP Server — Starting up...");
Serial.println("========================================");
// Step 1: Create WiFi hotspot
Serial.print("[SERVER] Creating AP hotspot: ");
Serial.println(ap_ssid);
WiFi.mode(WIFI_AP);
WiFi.softAP(ap_ssid, ap_password);
delay(1000);
Serial.print("[SERVER] AP IP Address: ");
Serial.println(WiFi.softAPIP());
// Step 2: Start TCP server
server.begin();
Serial.print("[SERVER] TCP Server started on port: ");
Serial.println(serverPort);
Serial.println("[SERVER] Waiting for client connection...");
Serial.println();
Serial.println("[SERVER] Type a message and press Enter to send.");
Serial.println();
}
Running Logic: After power-on, sequentially completes 5 tasks:
Key Command Analysis:
WiFi.mode(WIFI_AP): Switch to AP mode, ESP32 becomes WiFi hotspotWiFi.softAP(ap_ssid, ap_password): Create hotspot, Client can search and connectWiFi.softAPIP(): Get IP address in AP mode (default 192.168.4.1)server.begin(): Start TCP server, begin listening on portvoid loop() {
// Check for incoming client connections (non-blocking)
if (!clientConnected) {
WiFiClient newClient = server.available();
if (newClient) {
client = newClient;
clientConnected = true;
digitalWrite(LED_PIN, HIGH);
Serial.println("----------------------------------------");
Serial.println("[SERVER] >>> New client connected!");
Serial.print("[SERVER] Client IP: ");
Serial.println(client.remoteIP());
// Send welcome message to client
client.println("[Server] Client connected! Ready to chat.");
Serial.println("[SERVER] Ready to chat. Type a message and press Enter.");
Serial.println("----------------------------------------");
}
}
// When connected, handle bidirectional communication
if (clientConnected) {
// Check if client disconnected
if (!client.connected()) {
clientConnected = false;
digitalWrite(LED_PIN, LOW);
client.stop();
Serial.println("----------------------------------------");
Serial.println("[SERVER] <<< Client disconnected.");
Serial.println("[SERVER] Waiting for next client...");
Serial.println("----------------------------------------");
return;
}
// 1. Read from serial input and send to client
if (Serial.available()) {
String msg = Serial.readStringUntil('\n');
msg.trim();
if (msg.length() > 0) {
client.println(msg);
Serial.print("[SERVER] Sent: ");
Serial.println(msg);
}
}
// 2. Read from client and display
if (client.available()) {
String msg = client.readStringUntil('\n');
if (msg.length() > 0) {
msg.trim();
Serial.print("[SERVER] Received: ");
Serial.println(msg);
}
}
}
delay(10); // Small delay to avoid busy loop
}
Running Logic:
Key Command Analysis:
server.available(): Non-blocking check for new connections. Returns empty object immediately when no connectionSerial.available(): Non-blocking check for serial input. Returns 1 when data existsSerial.readStringUntil('\n'): Read serial input ending with newlineclient.println(msg): Send message to client via TCPclient.available(): Non-blocking check for TCP data availableclient.readStringUntil('\n'): Read message sent by clientmsg.trim(): Remove leading/trailing whitespace characters (newlines, spaces, etc.)client.stop(): Close client connection, release resourcesdelay(10): Short delay to avoid CPU overload, does not affect main loop responsiveness💡 Client is responsible for connecting to Server's WiFi hotspot, establishing TCP connection, and performing bidirectional chat.
#include <WiFi.h>
// ==================== Configuration ====================
const char* serverSSID = "ESP32_Server"; // Server's hotspot name
const char* serverPassword = "12345678"; // Server's hotspot password
const char* serverIP = "192.168.4.1"; // Server's AP IP address
const uint16_t serverPort = 8080; // TCP server port
// ==================== Pins ====================
#define LED_PIN 2 // Status LED pin
// ==================== State ====================
WiFiClient client;
bool clientConnected = false;
unsigned long lastReconnectAttempt = 0;
Running Logic: Define Server connection info, pins, and state variables.
Key Command Analysis:
serverIP = "192.168.4.1": Server AP mode default IP, usually no need to modifyclientConnected: Connection status flag, used to control LED and reconnect logiclastReconnectAttempt: Record last reconnect time, used to implement 3-second reconnect intervalvoid connectToServer() {
Serial.print("[CLIENT] Connecting to AP: ");
Serial.println(serverSSID);
WiFi.mode(WIFI_STA);
WiFi.begin(serverSSID, serverPassword);
// Wait for WiFi connection (timeout: 15 seconds)
int timeout = 0;
while (WiFi.status() != WL_CONNECTED && timeout < 30) {
delay(500);
Serial.print(".");
timeout++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println();
Serial.println("[CLIENT] WiFi connected!");
Serial.print("[CLIENT] Client IP: ");
Serial.println(WiFi.localIP());
Serial.print("[CLIENT] Server IP: ");
Serial.println(serverIP);
// Connect to TCP server
Serial.print("[CLIENT] Connecting to TCP server...");
if (client.connect(serverIP, serverPort)) {
clientConnected = true;
digitalWrite(LED_PIN, HIGH);
Serial.println(" CONNECTED!");
Serial.println("[CLIENT] Ready to chat. Type a message and press Enter.");
// Read welcome message from server
delay(500);
while (client.available()) {
String line = client.readStringUntil('\n');
if (line.length() > 0) {
Serial.print("[CLIENT] Server says: ");
Serial.println(line);
}
}
} else {
Serial.println(" FAILED!");
clientConnected = false;
}
} else {
Serial.println();
Serial.println("[CLIENT] WiFi connection FAILED. Will retry...");
}
}
Running Logic:
Key Command Analysis:
WiFi.mode(WIFI_STA): Switch to STA mode, connect to hotspot as clientWiFi.begin(serverSSID, serverPassword): Start connecting to specified WiFi hotspottimeout < 30: Timeout counter, 30 × 0.5 seconds = 15 seconds. Timeout mechanism prevents infinite loopclient.connect(serverIP, serverPort): Establish TCP connection. Returns true for successclient.available(): Check if data can be read, used to read welcome messagevoid setup() {
Serial.begin(115200);
delay(1000);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
Serial.println();
Serial.println("========================================");
Serial.println(" ESP32 TCP Client — Starting up...");
Serial.println("========================================");
connectToServer();
lastReconnectAttempt = millis();
}
Running Logic:
Key Command Analysis:
connectToServer(): Connect immediately at startup, don't wait for loop()lastReconnectAttempt = millis(): Initialize reconnect timing start pointvoid loop() {
// Check if still connected
if (!clientConnected || !client.connected()) {
clientConnected = false;
digitalWrite(LED_PIN, LOW);
// Attempt reconnect every 3 seconds
if (millis() - lastReconnectAttempt > 3000) {
lastReconnectAttempt = millis();
Serial.println("[CLIENT] Attempting to reconnect...");
client.stop();
WiFi.disconnect();
connectToServer();
}
return;
}
// 1. Read from serial input and send to server
if (Serial.available()) {
String msg = Serial.readStringUntil('\n');
msg.trim();
if (msg.length() > 0) {
client.println(msg);
Serial.print("[CLIENT] Sent: ");
Serial.println(msg);
}
}
// 2. Read from server and display
if (client.available()) {
String msg = client.readStringUntil('\n');
if (msg.length() > 0) {
msg.trim();
Serial.print("[CLIENT] Received: ");
Serial.println(msg);
}
}
delay(10); // Small delay to avoid busy loop
}
Running Logic:
Key Command Analysis:
!clientConnected || !client.connected(): Double check connection statusmillis() - lastReconnectAttempt > 3000: Reconnect interval 3 secondsSerial.available(): Non-blocking check for serial inputSerial.readStringUntil('\n'): Read serial input ending with newlineclient.println(msg): Send message to server via TCPclient.available(): Non-blocking check for TCP data availableclient.readStringUntil('\n'): Read message sent by servermsg.trim(): Remove leading/trailing whitespace charactersreturn: Return immediately when disconnected, don't execute subsequent codeServer Side (ESP32_TCP_Server.ino):
const char* ap_ssid = "Your Hotspot Name";
const char* ap_password = "Your Password"; // min 8 chars
Client Side (ESP32_TCP_Client.ino):
const char* serverSSID = "Your Hotspot Name";
const char* serverPassword = "Your Password";
⚠️ Both sides must be modified simultaneously!
Keep the serverPort variable consistent on both sides. Common port numbers:
8080 (default) - Common HTTP alternate port80 - HTTP default port8888 - Another common port💡 Port Range: 1~65535, but recommended to use ports above 1024 (1~1023 are system reserved ports).
Find in ESP32_TCP_Client.ino:
if (millis() - lastReconnectAttempt > 3000) { // 3000ms = 3 seconds
Change to your desired reconnect interval (in milliseconds), e.g.:
1000 = 1 second (faster reconnection)5000 = 5 seconds (slower reconnection)10000 = 10 seconds