Project 1: Building a Real-Time Text Messaging Web UI with an ESP32-S3 Embedded Web Server
Welcome to Project 1 of my series on creating real-time web interfaces with the ESP32-S3! We’ll learn to build web servers that communicate with hardware using C++ and Object-Oriented Programming (OOP). In this first post, we’ll create a basic web interface that enables text message exchanges between a web UI and the serial monitor of an ESP32-S3 board.
Project Overview
We’ll set up an ESP32-S3 microcontroller to act as an embedded web server. The server will host a webpage with a text input field and a button. You can enter text and send it to the ESP32 via WebSocket by clicking the button. The ESP32 will display the received text on its serial monitor and can also send data back to the web UI.
Getting Started
1. Setting Up the ESP32-S3 with PlatformIO
To start, we’ll use the PlatformIO extension in Visual Studio Code to create a new project. The ESP32-S3 will be programmed to:
- Set up an Access Point (AP) for direct device connections.
- Start an embedded web server using the
ESPAsyncWebServer
library. - Manage WebSocket communication between the web UI and the serial monitor.
Here’s how to set up your project:
- Open PlatformIO in Visual Studio Code:
- Launch Visual Studio Code.
- Click on the PlatformIO icon in the left sidebar.
- Under “PROJECT TASKS,” click “Create New Project.”
- Start a New Project:
- Name:
esp32_web_server
. - Board: “Espressif ESP32-S3-DevkitC-1-N8”.
- Framework: “Arduino”.
- Click Finish.
- Name:
2. Key Components of the Code
Let’s explore the core parts of our code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "EmbeddedWS.h"
// Access Point credentials
const char* ap_ssid = "ESP32_AP";
const char* ap_password = "123456789";
EmbeddedWS embeddedwebserver;
void setup() {
Serial.begin(115200);
// Set up the Access Point
WiFi.softAP(ap_ssid, ap_password);
Serial.print("AP IP address: ");
Serial.println(WiFi.softAPIP());
// Initialize and start WebSocket
embeddedwebserver.begin();
}
void loop() {
// Continuously check for serial data and send it via WebSocket
embeddedwebserver.handleSerialData();
}
Explanation:
- WiFi Access Point Setup:
WiFi.softAP()
creates a local WiFi network for device connections, enabling communication between the ESP32 and clients. - Serial Communication:
Serial.begin(115200)
initializes serial communication for debugging. - Object-Oriented Approach: The
EmbeddedWS
class encapsulates all functionalities related to the web server, such as starting the server and handling serial data.
3. Using Object-Oriented Programming (OOP) in Embedded Systems
We define a class called EmbeddedWS
in EmbeddedWS.h
and EmbeddedWS.cpp
. This class handles the server setup, file serving, and WebSocket communication. The use of OOP allows us to:
- Encapsulate web server functionality in a single class.
- Promote reusability with methods like
begin()
andhandleSerialData()
.
Here is the EmbeddedWS
class definition:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef EMBEDDEDWS_H
#define EMBEDDEDWS_H
#include <ESPAsyncWebServer.h>
#include <LittleFS.h>
class EmbeddedWS {
public:
EmbeddedWS(); // Constructor
void begin(); // Method to start the server
void handleSerialData(); // Method to send serial data
private:
AsyncWebServer server; // Server instance
AsyncWebSocket ws; // WebSocket instance
};
#endif // EMBEDDEDWS_H
Explanation:
- Constructor: Initializes the web server and WebSocket on port 80.
begin()
Method: Sets up the file system (LittleFS
), serves static files, and configures WebSocket event handlers.handleSerialData()
Method: Checks for incoming serial data and sends it to all connected WebSocket clients.
4. EmbeddedWS.cpp: Implementation of the EmbeddedWS Class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include "EmbeddedWS.h"
// Constructor initializes the server on port 80 and WebSocket on "/ws"
EmbeddedWS::EmbeddedWS()
: server(80), ws("/ws") {}
// Method to start the server and set up WebSocket
void EmbeddedWS::begin() {
// Initialize LittleFS
if (!LittleFS.begin()) {
Serial.println("An error occurred while mounting LittleFS");
return;
}
Serial.println("LittleFS mounted successfully");
// Serve static files from LittleFS
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
// Add WebSocket event handlers
ws.onEvent([this](AsyncWebSocket *server, AsyncWebSocketClient *client,
AwsEventType type, void *arg, uint8_t *data, size_t len) {
handleWebSocketEvent(server, client, type, arg, data, len);
});
// Attach WebSocket to the server
server.addHandler(&ws);
// Start the server
server.begin();
Serial.println("Server started");
}
// Method to handle incoming serial data and send it over WebSocket
void EmbeddedWS::handleSerialData() {
if (Serial.available() > 0) {
String serialData = Serial.readStringUntil('\n'); // Read serial input
ws.textAll(serialData); // Send to all connected WebSocket clients
}
}
// Function to handle WebSocket events
void EmbeddedWS::handleWebSocketEvent(AsyncWebSocket *server,
AsyncWebSocketClient *client,
AwsEventType type,
void *arg,
uint8_t *data,
size_t len) {
if (type == WS_EVT_CONNECT) {
Serial.printf("Client connected: %u\n", client->id());
} else if (type == WS_EVT_DISCONNECT) {
Serial.printf("Client disconnected: %u\n", client->id());
} else if (type == WS_EVT_DATA) {
// Handle incoming text data
String msg = "";
for (size_t i = 0; i < len; i++) {
msg += (char) data[i];
}
Serial.println("Message received: " + msg);
// Echo the message back to all clients
ws.textAll(msg);
}
}
Explanation of EmbeddedWS.cpp
:
- Constructor (
EmbeddedWS::EmbeddedWS
):- Initializes the
AsyncWebServer
on port 80 and the WebSocket at the path/ws
.
- Initializes the
begin()
Method:- Mounts the
LittleFS
filesystem to serve files from flash memory. - Serves static files from
LittleFS
and setsindex.html
as the default. - Attaches the WebSocket handler using
ws.onEvent()
to listen for events. - Starts the server and prints a confirmation message.
- Mounts the
handleSerialData()
Method:- Checks for incoming data from the serial monitor.
- Sends any received data to all connected WebSocket clients.
handleWebSocketEvent()
Method:- Manages different WebSocket events:
WS_EVT_CONNECT
: Logs a message when a client connects.WS_EVT_DISCONNECT
: Logs a message when a client disconnects.WS_EVT_DATA
: Handles incoming text data, displays it on the serial monitor, and sends it back to all connected clients.
- Manages different WebSocket events:
5. WebSocket Communication via JavaScript
The JavaScript code in index.html
handles the WebSocket connection:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!DOCTYPE html>
<html>
<head>
<title>ESP32 Web Server</title>
<style>
/* Styling for the web interface */
</style>
</head>
<body>
<h1>ESP32 Web Server</h1>
<input type="text" id="textInput" placeholder="Enter text here">
<button onclick="sendMessage()">Send</button>
<div id="serialOutput"></div>
<script>
const ws = new WebSocket('ws://192.168.4.1/ws');
ws.onopen = () => {
console.log('WebSocket connection established');
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('WebSocket connection closed');
};
ws.onmessage = (event) => {
const serialOutput = document.getElementById('serialOutput');
serialOutput.textContent += event.data + '\n';
serialOutput.scrollTop = serialOutput.scrollHeight;
};
function sendMessage() {
const text = document.getElementById('textInput').value;
if (
ws.readyState === WebSocket.OPEN) {
ws.send(text);
console.log('Message sent:', text);
} else {
console.error('WebSocket is not open');
}
}
</script>
</body>
</html>
Explanation:
- WebSocket Connection: Connects to the WebSocket at
ws://192.168.4.1/ws
. - Message Handling: Displays received messages in the
serialOutput
div. - Send Functionality: Sends messages entered in the text field to the ESP32 server.