Building an ESP32 OLED Painter with a Web Interface

Introduction

Ever wanted to create your own digital drawing tool? In this project, we'll build an ESP32 OLED Painter, allowing users to draw on a small OLED screen via a simple web interface. The ESP32 hosts a webpage where users can sketch using a mouse, and the drawings will appear in real-time on an OLED display.

Components Required

  • ESP32 board
  • 128x64 SSD1306 OLED display
  • Jumper wires
  • USB cable for programming

How It Works

  1. The ESP32 acts as a web server, hosting an HTML page with a drawing canvas.
  2. Users can draw using a mouse, selecting between a pencil and an eraser.
  3. Every drawn pixel is sent to the ESP32 via HTTP requests.
  4. The ESP32 updates an internal framebuffer, which is displayed on the OLED screen.


Code Breakdown

1. Including Necessary Libraries

#include <WiFi.h>

#include <ESPAsyncWebServer.h>

#include <Adafruit_SSD1306.h>

#include <Adafruit_GFX.h>

  • WiFi.h enables ESP32 to connect to a network.
  • ESPAsyncWebServer.h handles web requests asynchronously.
  • Adafruit_SSD1306.h and Adafruit_GFX.h control the OLED display.

2. Setting Up Wi-Fi Credentials

const char* ssid = "SSID";

const char* password = " PASSWORD ";

Replace these with your own Wi-Fi credentials.

3. Initializing the OLED Display

#define SCREEN_WIDTH 128

#define SCREEN_HEIGHT 64

#define OLED_RESET -1

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

We define screen dimensions and initialize the OLED display.

4. Creating a Web Server

AsyncWebServer server(80);

The ESP32 runs a web server on port 80, serving the drawing webpage.

5. Creating a Framebuffer

uint8_t framebuffer[SCREEN_HEIGHT][SCREEN_WIDTH] = {0};

This 2D array stores the drawn pixels before displaying them.

6. Web Interface (HTML + JavaScript)

The ESP32 serves an interactive webpage that includes a canvas for drawing.

const char webpage[] PROGMEM = R"rawliteral(

<!DOCTYPE html>

<html lang="en">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>ESP32 OLED Painter</title>

    <style>

        body { background: linear-gradient(135deg, #87CEEB, #4682B4); }

        .container { background: rgba(255, 255, 255, 0.9); border-radius: 12px; }

        canvas { border: 1px solid #ddd; background-color: #fff; cursor: crosshair; }

    </style>

</head>

<body>

    <div class="container">

        <h2>ESP32 OLED Painter</h2>

        <button onclick="setTool('pencil')">Pencil</button>

        <button onclick="setTool('eraser')">Eraser</button>

        <button onclick="clearCanvas()">Clear</button>

        <canvas id="canvas" width="128" height="64"></canvas>

    </div>

    <script>

        const canvas = document.getElementById('canvas');

        const ctx = canvas.getContext('2d');

        let isDrawing = false, currentTool = 'pencil';

        function setTool(tool) { currentTool = tool; }

        canvas.addEventListener('mousedown', () => { isDrawing = true; ctx.beginPath(); });

        canvas.addEventListener('mouseup', () => { isDrawing = false; });

        canvas.addEventListener('mousemove', draw);

        function draw(e) {

            if (!isDrawing) return;

            const x = Math.floor(e.offsetX * (canvas.width / canvas.clientWidth));

            const y = Math.floor(e.offsetY * (canvas.height / canvas.clientHeight));

            if (currentTool === 'pencil') {

                ctx.fillRect(x, y, 1, 1);

                fetch(`/draw?x=${x}&y=${y}&state=1`);

            } else {

                ctx.clearRect(x, y, 1, 1);

                fetch(`/draw?x=${x}&y=${y}&state=0`);

            }

        }

        function clearCanvas() { ctx.clearRect(0, 0, canvas.width, canvas.height); fetch('/clear'); }

    </script>

</body>

</html>

)rawliteral";

7. Displaying a Welcome Message

display.setTextSize(2);

display.setTextColor(SSD1306_WHITE);

display.setCursor(20, 20);

display.println("Welcome");

display.display();

delay(3000);

display.clearDisplay();

display.display();

The OLED displays "Welcome to Paint" when the ESP32 starts.

8. Handling Web Requests

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {

    request->send_P(200, "text/html", webpage);

});

This serves the HTML page when a user visits the ESP32’s IP address.

9. Receiving Draw Commands

server.on("/draw", HTTP_GET, [](AsyncWebServerRequest *request) {

    int x = request->getParam("x")->value().toInt();

    int y = request->getParam("y")->value().toInt();

    int state = request->getParam("state")->value().toInt();

    framebuffer[y][x] = state ? 1 : 0;

    request->send(200, "text/plain", "OK");

});

The ESP32 receives x, y coordinates and updates the framebuffer accordingly.

10. Clearing the Display

server.on("/clear", HTTP_GET, [](AsyncWebServerRequest *request) {

    memset(framebuffer, 0, sizeof(framebuffer));

    request->send(200, "text/plain", "Cleared");

});

This clears the screen when the user clicks the Clear button.

11. Updating the OLED Display

void updateDisplay() {

    display.clearDisplay();

    for (int y = 0; y < SCREEN_HEIGHT; y++) {

        for (int x = 0; x < SCREEN_WIDTH; x++) {

            if (framebuffer[y][x]) {

                display.drawPixel(x, y, SSD1306_WHITE);

            }

        }

    }

    display.display();

}

This function continuously refreshes the OLED to show the latest drawings.

12. Main Loop

void loop() {

    static unsigned long lastUpdate = 0;

    if (millis() - lastUpdate > 30) {

        lastUpdate = millis();

        updateDisplay();

    }

}

The loop ensures the OLED display updates every 30ms.

 

Code:

 

#include <WiFi.h>

#include <ESPAsyncWebServer.h>

#include <Adafruit_SSD1306.h>

#include <Adafruit_GFX.h>

 

// Wi-Fi credentials

const char* ssid = "SSID";

const char* password = "PASSWORD";

 

// OLED display settings

#define SCREEN_WIDTH 128

#define SCREEN_HEIGHT 64

#define OLED_RESET -1 // Reset pin (-1 if sharing Arduino reset pin)

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

 

// Create an AsyncWebServer object on port 80

AsyncWebServer server(80);

 

// Framebuffer for the display

uint8_t framebuffer[SCREEN_HEIGHT][SCREEN_WIDTH] = {0};

 

// HTML and JavaScript for the drawing web page

const char webpage[] PROGMEM = R"rawliteral(

<!DOCTYPE html>

<html lang="en">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>ESP32 OLED Painter</title>

    <style>

        body {

            font-family: 'Arial', sans-serif;

            background: linear-gradient(135deg, #87CEEB, #4682B4);

            margin: 0;

            display: flex;

            justify-content: center;

            align-items: center;

            height: 100vh;

            color: #fff;

        }

        .container {

            text-align: center;

            padding: 20px;

            background: rgba(255, 255, 255, 0.9);

            border-radius: 12px;

            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);

            color: #333;

        }

        canvas {

            border: 1px solid #ddd;

            background-color: #fff;

            margin-top: 20px;

            cursor: crosshair;

            image-rendering: pixelated;

        }

        button {

            padding: 10px 20px;

            font-size: 14px;

            margin: 5px;

            border: none;

            border-radius: 8px;

            cursor: pointer;

            background-color: #4CAF50;

            color: white;

        }

        button:hover {

            background-color: #45a049;

        }

    </style>

</head>

<body>

    <div class="container">

        <h2>ESP32 OLED Painter</h2>

        <div>

            <button onclick="setTool('pencil')">Pencil</button>

            <button onclick="setTool('eraser')">Eraser</button>

            <button onclick="clearCanvas()">Clear</button>

        </div>

        <canvas id="canvas" width="128" height="64" style="width: 512px; height: 256px;"></canvas>

    </div>

    <script>

        const canvas = document.getElementById('canvas');

        const ctx = canvas.getContext('2d');

        let isDrawing = false;

        let currentTool = 'pencil';

 

        function setTool(tool) {

            currentTool = tool;

        }

 

        canvas.addEventListener('mousedown', () => { isDrawing = true; ctx.beginPath(); });

        canvas.addEventListener('mouseup', () => { isDrawing = false; });

        canvas.addEventListener('mousemove', draw);

 

        function draw(e) {

            if (!isDrawing) return;

 

            const rect = canvas.getBoundingClientRect();

            const x = Math.floor((e.clientX - rect.left) * (canvas.width / rect.width));

            const y = Math.floor((e.clientY - rect.top) * (canvas.height / rect.height));

 

            if (currentTool === 'pencil') {

                ctx.fillStyle = 'black';

                ctx.fillRect(x, y, 1, 1);

                sendPixelData(x, y, 1);

            } else if (currentTool === 'eraser') {

                ctx.clearRect(x, y, 1, 1);

                sendPixelData(x, y, 0);

            }

        }

 

        function sendPixelData(x, y, state) {

            fetch(`/draw?x=${x}&y=${y}&state=${state}`);

        }

 

        function clearCanvas() {

            ctx.clearRect(0, 0, canvas.width, canvas.height);

            fetch('/clear');

        }

    </script>

</body>

</html>

)rawliteral";

 

void setup() {

  // Initialize Serial Monitor

  Serial.begin(115200);

 

  // Initialize SSD1306 display

  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {

    Serial.println(F("SSD1306 allocation failed"));

    for (;;);

  }

  display.clearDisplay();

  display.display();

 

  // Connect to Wi-Fi

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {

    delay(1000);

    Serial.println("Connecting to WiFi...");

  }

  Serial.println("Connected to WiFi");

  Serial.println(WiFi.localIP());

  display.clearDisplay();

 

  display.setTextSize(2); // Large font size

  display.setTextColor(SSD1306_WHITE);

 

  // Centered message calculations

  int16_t x1, y1;

  uint16_t width, height;

  display.getTextBounds("Welcome", 0, 0, &x1, &y1, &width, &height);

  int16_t xPosWelcome = (SCREEN_WIDTH - width) / 2;

  int16_t yPosWelcome = (SCREEN_HEIGHT - height) / 2 - 10; // Slightly higher for second line

 

  display.setCursor(xPosWelcome, yPosWelcome);

  display.println("Welcome");

 

  display.getTextBounds("to Paint", 0, 0, &x1, &y1, &width, &height);

  int16_t xPosToPaint = (SCREEN_WIDTH - width) / 2;

  int16_t yPosToPaint = (SCREEN_HEIGHT - height) / 2 + 10; // Slightly lower for second line

 

  display.setCursor(xPosToPaint, yPosToPaint);

  display.println("to Paint");

 

  display.display(); // Update the display with the welcome message

  delay(3000); // Show message for 3 seconds

  display.clearDisplay();

  display.display();

 

  // Serve the drawing web page

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {

    request->send_P(200, "text/html", webpage);

  });

 

  // Handle draw requests

  server.on("/draw", HTTP_GET, [](AsyncWebServerRequest *request) {

    if (request->hasParam("x") && request->hasParam("y") && request->hasParam("state")) {

      int x = request->getParam("x")->value().toInt();

      int y = request->getParam("y")->value().toInt();

      int state = request->getParam("state")->value().toInt();

      if (x >= 0 && x < SCREEN_WIDTH && y >= 0 && y < SCREEN_HEIGHT) {

        framebuffer[y][x] = state ? 1 : 0;

      }

    }

    request->send(200, "text/plain", "OK");

  });

 

  // Clear framebuffer

  server.on("/clear", HTTP_GET, [](AsyncWebServerRequest *request) {

    memset(framebuffer, 0, sizeof(framebuffer));

    request->send(200, "text/plain", "Cleared");

  });

 

  // Start server

  server.begin();

}

 

void updateDisplay() {

  display.clearDisplay();

  for (int y = 0; y < SCREEN_HEIGHT; y++) {

    for (int x = 0; x < SCREEN_WIDTH; x++) {

      if (framebuffer[y][x]) {

        display.drawPixel(x, y, SSD1306_WHITE);

      }

    }

  }

  display.display();

}

 

void loop() {

  static unsigned long lastUpdate = 0;

  if (millis() - lastUpdate > 30) { // Update every 30ms

    lastUpdate = millis();

    updateDisplay();

  }

}

This project transforms the ESP32 into a real-time OLED drawing tool accessible from a web browser. It’s a great way to explore web-based microcontroller interaction and IoT applications!




... Thank you ...


Post a Comment (0)
Previous Post Next Post