A complete step‑by‑step guide to build a web‑controlled OLED display: the FireBeetle ESP32‑P4 hosts a webserver where you edit content and immediately update a connected 0.96" SSD1306 OLED over I2C. The project includes a dark single‑page web UI, AJAX updates, and an LED indicator that blinks when the ESP32 receives new commands.
This project turns your FireBeetle ESP32‑P4 into an OLED controller you can operate from any browser on the same Wi‑Fi network. The web UI lets you edit multiple pages (title and two text lines), preview the JSON state, save pages, and push a page to the OLED instantly. The ESP32 receives updates via AJAX, redraws the OLED, and blinks the onboard LED on pin 3 when new content arrives.
Get PCBs for Your Projects ManufacturedYou must check out PCBWAY for ordering PCBs online for cheap!
You get 10 good-quality PCBs manufactured and shipped to your doorstep for cheap. You will also get a discount on shipping on your first order. Upload your Gerber files onto PCBWAY to get them manufactured with good quality and quick turnaround time. PCBWay now could provide a complete product solution, from design to enclosure production. Check out their online Gerber viewer function. With reward points, you can get free stuff from their gift shop. Also, check out this useful blog on PCBWay Plugin for KiCad from here. Using this plugin, you can directly order PCBs in just one click after completing your design in KiCad.
Components and softwareHardware
- DFRobot FireBeetle ESP32‑P4 board
- DFRobot 0.96" SSD1306 I2C OLED (128×64)
- USB cable for power and programming
- Optional: jumper wires, external LED and resistor (if you prefer external indicator)
Software
- Arduino IDE or PlatformIO with ESP32 board support
- Libraries: Adafruit GFX, Adafruit SSD1306, Arduino_JSON
Network
- Local Wi‑Fi SSID and password
Connections
- OLED VCC → 3.3V on FireBeetle
- OLED GND → GND
- OLED SDA → SDA pin on FireBeetle
- OLED SCL → SCL pin on FireBeetle
I2C address
- Typical address: 0x3C. If initialization fails, try 0x3D or call
Wire.begin(SDA_PIN, SCL_PIN)with board pins.
Notes: Use the USB cable for programming and optional serial debug output. Close Arduino Serial Monitor before using any serial‑based tools that need the COM port.
Firmware: features and behavior- Connects to Wi‑Fi and starts an HTTP server on port 80.
- Serves a single‑page web UI for editing four OLED pages.
Endpoints:
GET /→ web UIGET /state→ current pages and active page as JSONPOST /update→ update a page (JSON body)GET /show?page=N→ display page N immediately on OLED
On receiving an update or show command the ESP32:
- Updates internal state
- Redraws the OLED with the selected page content
- Blinks onboard LED on pin 3 briefly
Replace YOUR_SSID and YOUR_PASSWORD before uploading. Install the listed libraries, then paste and upload this sketch.
// Web OLED controller with in-browser preview for FireBeetle ESP32-P4
#include <WiFi.h>
#include <WebServer.h>
#include <Arduino_JSON.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDR 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";
WebServer server(80);
#define LED_PIN 3
struct OledPage {
String title;
String line1;
String line2;
int textSize;
};
OledPage pages[4];
int currentPage = 0;
void drawPage(int p) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0,0);
display.println(pages[p].title);
display.drawLine(0, 12, SCREEN_WIDTH-1, 12, SSD1306_WHITE);
display.setTextSize(pages[p].textSize);
display.setCursor(0, 18);
display.println(pages[p].line1);
display.setCursor(0, 34);
display.println(pages[p].line2);
display.display();
}
void handleRoot() {
String page = R"rawliteral(
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>ESP32 OLED Controller</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body{background:#0f1720;color:#e6eef6;font-family:Arial;margin:0;padding:12px}
h1{color:#58a6ff;margin:0 0 12px 0}
.grid{display:flex;gap:12px;flex-wrap:wrap}
.card{background:#111827;padding:12px;border-radius:8px;flex:1;min-width:260px}
label{display:block;color:#94a3b8;font-size:13px;margin-top:8px}
input[type=text], select{width:100%;padding:8px;margin-top:6px;border-radius:6px;border:1px solid #23303a;background:#0b1116;color:#e6eef6}
button{margin-top:10px;padding:8px 12px;border-radius:6px;border:none;background:#4cc9f0;color:#021018;font-weight:700}
.preview{background:#000;padding:8px;border-radius:6px;color:#0f0;font-family:monospace;height:120px;overflow:auto}
.pages{display:flex;gap:6px;margin-top:8px}
.pages button{flex:1}
.canvas-wrap{background:#000;padding:8px;border-radius:8px;display:flex;flex-direction:column;align-items:center}
canvas{image-rendering:pixelated;border:1px solid #23303a;background:#000}
.hint{color:#94a3b8;font-size:12px;margin-top:6px}
</style>
</head>
<body>
<h1>ESP32 OLED Controller</h1>
<div class="grid">
<div class="card">
<h3>Edit Page</h3>
<label>Page</label>
<select id="pageSelect">
<option value="0">Page 0</option>
<option value="1">Page 1</option>
<option value="2">Page 2</option>
<option value="3">Page 3</option>
</select>
<label>Title</label>
<input id="title" type="text" maxlength="24">
<label>Line 1</label>
<input id="line1" type="text" maxlength="32">
<label>Line 2</label>
<input id="line2" type="text" maxlength="32">
<label>Text size</label>
<select id="textSize">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<div class="pages">
<button id="saveBtn">Save</button>
<button id="showBtn">Show</button>
</div>
</div>
<div class="card">
<h3>OLED Preview</h3>
<div class="canvas-wrap">
<!-- canvas uses native OLED resolution but scaled via CSS -->
<canvas id="oledPreview" width="128" height="64" style="width:384px;height:192px"></canvas>
<div class="hint">Preview is pixel-accurate (scaled). Click Show to push to physical OLED.</div>
</div>
<h3 style="margin-top:12px">Console</h3>
<div id="console" class="preview" style="height:80px"></div>
</div>
</div>
<script>
// map textSize to pixel font sizes for canvas
const fontMap = {1: 8, 2: 12, 3: 16};
async function loadState(){
const r = await fetch('/state');
const s = await r.json();
document.getElementById('console').innerText = 'Current page: ' + s.currentPage;
const sel = document.getElementById('pageSelect').value;
const p = s.pages[sel];
document.getElementById('title').value = p.title;
document.getElementById('line1').value = p.line1;
document.getElementById('line2').value = p.line2;
document.getElementById('textSize').value = p.textSize;
renderPreview(parseInt(sel), p);
}
function renderPreview(pageIndex, pageObj){
const canvas = document.getElementById('oledPreview');
const ctx = canvas.getContext('2d');
// clear (OLED black)
ctx.fillStyle = '#000';
ctx.fillRect(0,0,canvas.width,canvas.height);
// Title (small)
ctx.fillStyle = '#fff';
ctx.font = '10px monospace';
ctx.textBaseline = 'top';
ctx.fillText(pageObj.title.substring(0,20), 0, 0);
// divider line
ctx.fillStyle = '#fff';
ctx.fillRect(0, 12, canvas.width, 1);
// body lines using textSize mapping
const size = pageObj.textSize in fontMap ? fontMap[pageObj.textSize] : 8;
ctx.fillStyle = '#fff';
ctx.font = size + 'px monospace';
// line1 at y=18, line2 at y=18 + size + 4
ctx.fillText(pageObj.line1.substring(0,20), 0, 18);
ctx.fillText(pageObj.line2.substring(0,20), 0, 18 + size + 4);
}
// update preview when page selection changes
document.getElementById('pageSelect').addEventListener('change', async ()=>{
const idx = document.getElementById('pageSelect').value;
const r = await fetch('/state');
const s = await r.json();
const p = s.pages[idx];
document.getElementById('title').value = p.title;
document.getElementById('line1').value = p.line1;
document.getElementById('line2').value = p.line2;
document.getElementById('textSize').value = p.textSize;
renderPreview(parseInt(idx), p);
});
document.getElementById('saveBtn').addEventListener('click', async ()=>{
const p = document.getElementById('pageSelect').value;
const payload = {
page: parseInt(p),
title: document.getElementById('title').value,
line1: document.getElementById('line1').value,
line2: document.getElementById('line2').value,
textSize: parseInt(document.getElementById('textSize').value)
};
const res = await fetch('/update', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(payload)
});
const txt = await res.text();
document.getElementById('console').innerText = txt;
renderPreview(parseInt(p), payload);
});
document.getElementById('showBtn').addEventListener('click', async ()=>{
const p = parseInt(document.getElementById('pageSelect').value);
await fetch('/show?page=' + p);
document.getElementById('console').innerText = 'Displayed page ' + p;
});
// initial load
window.onload = loadState;
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", page);
}
void handleState() {
JSONVar root;
root["currentPage"] = currentPage;
JSONVar arr = JSONVar();
for (int i=0;i<4;i++){
JSONVar p;
p["title"] = pages[i].title;
p["line1"] = pages[i].line1;
p["line2"] = pages[i].line2;
p["textSize"] = pages[i].textSize;
arr[i] = p;
}
root["pages"] = arr;
String out = JSON.stringify(root);
server.send(200, "application/json", out);
}
void handleUpdate() {
if (server.hasArg("plain") == false) {
server.send(400, "text/plain", "Bad Request");
return;
}
String body = server.arg("plain");
JSONVar data = JSON.parse(body);
if (JSON.typeof(data) == "undefined") {
server.send(400, "text/plain", "Invalid JSON");
return;
}
int p = (int)data["page"];
if (p < 0 || p > 3) p = 0;
pages[p].title = (const char*)data["title"];
pages[p].line1 = (const char*)data["line1"];
pages[p].line2 = (const char*)data["line2"];
pages[p].textSize = (int)data["textSize"];
server.send(200, "text/plain", "Saved page " + String(p));
}
void handleShow() {
if (!server.hasArg("page")) {
server.send(400, "text/plain", "Missing page");
return;
}
int p = server.arg("page").toInt();
if (p < 0 || p > 3) p = 0;
currentPage = p;
drawPage(currentPage);
server.send(200, "text/plain", "Displayed page " + String(p));
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
for (int i=0;i<4;i++){
pages[i].title = "Page " + String(i);
pages[i].line1 = "Line1";
pages[i].line2 = "Line2";
pages[i].textSize = 1;
}
Wire.begin();
if(!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
Serial.println("SSD1306 allocation failed");
while(true);
}
display.clearDisplay();
drawPage(currentPage);
WiFi.begin(ssid, password);
Serial.print("Connecting WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(400);
Serial.print(".");
}
Serial.println();
Serial.print("IP: ");
Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/state", HTTP_GET, handleState);
server.on("/update", HTTP_POST, handleUpdate);
server.on("/show", HTTP_GET, handleShow);
server.begin();
}
void loop() {
server.handleClient();
if (Serial.available()) {
String s = Serial.readStringUntil('\n');
if (s.length() > 2) {
digitalWrite(LED_PIN, HIGH);
delay(80);
digitalWrite(LED_PIN, LOW);
}
}
}How to use- Install required libraries in Arduino IDE.
- Replace Wi‑Fi credentials and upload the sketch to the FireBeetle.
- Open Serial Monitor briefly to read the ESP32 IP, then close it.
- Open a browser and navigate to
http://<ESP32-IP>/. - Select a page, edit Title/Line1/Line2, click Save to store the page, and Show to immediately display it on the OLED. The preview and console reflect current state.
- Persist pages across reboots using Preferences or SPIFFS.
- Add custom fonts or bitmap icons for richer OLED layouts.
- Add authentication or a simple token to secure the web UI.
- Add image upload and conversion to monochrome bitmaps for icons.
You now have a working DFRobot FireBeetle ESP32‑P4 OLED Controller: the board hosts a Wi‑Fi webserver with a dark single‑page UI that lets you edit and preview multiple OLED pages, push updates instantly to the 0.96" SSD1306 over I2C, and signals new commands by blinking the onboard LED—giving you a simple, local, real‑time way to control and customize the display with room to add persistence, security, or richer graphics later.







Comments