Post

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:

  1. 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.”
  2. Start a New Project:
    • Name: esp32_web_server.
    • Board: “Espressif ESP32-S3-DevkitC-1-N8”.
    • Framework: “Arduino”.
    • Click Finish.

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() and handleSerialData().

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:

  1. Constructor (EmbeddedWS::EmbeddedWS):
    • Initializes the AsyncWebServer on port 80 and the WebSocket at the path /ws.
  2. begin() Method:
    • Mounts the LittleFS filesystem to serve files from flash memory.
    • Serves static files from LittleFS and sets index.html as the default.
    • Attaches the WebSocket handler using ws.onEvent() to listen for events.
    • Starts the server and prints a confirmation message.
  3. handleSerialData() Method:
    • Checks for incoming data from the serial monitor.
    • Sends any received data to all connected WebSocket clients.
  4. 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.

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.
This post is licensed under CC BY 4.0 by the author.