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
- The ESP32 acts as a web server, hosting an
HTML page with a drawing canvas.
- Users can draw using a mouse, selecting
between a pencil and an eraser.
- Every drawn pixel is sent to the ESP32 via HTTP
requests.
- 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 ...