Build a real‑time system telemetry dashboard that shows your PC’s CPU, RAM, and network stats in a dark, single‑page web UI served by a DFRobot FireBeetle ESP32‑P4. Your PC sends newline‑terminated JSON over USB serial; the ESP32 parses it, blinks an onboard LED (pin 3) when new data arrives, and serves a responsive AJAX dashboard that updates every 5 seconds and includes a raw JSON console.
Hardware- DFRobot FireBeetle ESP32‑P4 board (or compatible ESP32‑P4 board
- USB cable (for power, programming, and serial data)
- Optional: small breadboard / jumper wires if you want to wire external LED or buttons (onboard LED uses pin 3)
Display / UI
- No OLED required for the web UI version — everything is in the browser.
Software on PC
- Python 3.8+
- Python packages: pyserial, psutil (install with
pip install pyserial psutil)
Arduino IDE
- Latest Arduino IDE or VS Code with PlatformIO
- ESP32 board support installed (Espressif package)
- Libraries: Arduino_JSON (install via Library Manager)
Network
- Local Wi‑Fi network credentials (SSID and password) for the ESP32 to host the webserver
You 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.
Wiring and hardware setup- Connect the FireBeetle to your PC using the USB cable. This provides power and the serial link for telemetry.
LED
- The sketch uses the onboard LED on pin 3. No extra wiring required unless you want an external LED — then connect LED + resistor to pin 3 and GND.
Notes
- Close Arduino Serial Monitor before running the Python sender (only one app can open the COM port at a time)
- Ensure the FireBeetle is selected as the target board in Arduino IDE and the correct COM port is chosen.
What it does
- Connects to Wi‑Fi using your SSID and password.
- Listens on USB serial (9600 baud) for newline‑terminated JSON from the PC.
- Parses JSON and stores the latest values.
- Serves a dark themed HTML dashboard at
http://<ESP32-IP>/. - Provides
/dataJSON endpoint used by the page’s AJAX to update UI every 5 seconds. - Blinks onboard LED on pin 3 when new data arrives.
Paste and upload this sketch (replace YOUR_SSID and YOUR_PASSWORD):
#include <WiFi.h>
#include <WebServer.h>
#include <Arduino_JSON.h>
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";
WebServer server(80);
String host, os, boot, ramUsed, ramTotal, ip, mac;
double cpu = 0, ramPct = 0;
#define LED_PIN 3 // onboard LED pin
// Serve the main HTML page
void handleRoot() {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ESP32-P4 Telemetry Dashboard</title>
<style>
body{background:#121212;color:#eee;font-family:Arial;text-align:center;}
h1{color:#4cafef;}
.card{background:#1e1e1e;padding:20px;margin:20px;border-radius:10px;box-shadow:0 0 10px rgba(0,0,0,0.5);}
.bar{height:20px;background:#333;border-radius:10px;overflow:hidden;margin-top:10px;}
.fill{height:100%;background:#4caf50;text-align:right;color:#fff;padding-right:5px;}
.gauge{width:120px;height:60px;background:#333;border-radius:120px 120px 0 0;overflow:hidden;margin:20px auto;position:relative;}
.needle{width:4px;height:60px;background:#4cafef;position:absolute;bottom:0;left:58px;transform-origin:bottom center;}
</style>
<script>
async function fetchData(){
const res = await fetch('/data');
const data = await res.json();
document.getElementById('host').innerText = data.host;
document.getElementById('os').innerText = data.os;
document.getElementById('boot').innerText = data.boot_time;
document.getElementById('cpu').innerText = data.cpu_percent + '%';
document.getElementById('cpuFill').style.width = data.cpu_percent + '%';
document.getElementById('cpuNeedle').style.transform = 'rotate('+(data.cpu_percent/100*180)+'deg)';
document.getElementById('ram').innerText = data.ram.used + ' / ' + data.ram.total + ' ('+data.ram.percent+'%)';
document.getElementById('ramFill').style.width = data.ram.percent + '%';
document.getElementById('ramNeedle').style.transform = 'rotate('+(data.ram.percent/100*180)+'deg)';
document.getElementById('ip').innerText = data.network.ip;
document.getElementById('mac').innerText = data.network.mac;
// Show raw JSON in console widget
document.getElementById('console').innerText = JSON.stringify(data, null, 2);
}
setInterval(fetchData, 5000); // auto refresh every 5s
window.onload = fetchData;
</script>
</head>
<body>
<h1>ESP32-P4 Telemetry Dashboard</h1>
<div class="card">
<h2>System Info</h2>
<p><b>Host:</b> <span id="host"></span></p>
<p><b>OS:</b> <span id="os"></span></p>
<p><b>Boot Time:</b> <span id="boot"></span></p></div>
<div class="card">
<h2>CPU Usage</h2>
<div class="gauge"><div id="cpuNeedle" class="needle"></div></div>
<div class="bar"><div id="cpuFill" class="fill">0%</div></div>
<p id="cpu"></p>
</div>
<div class="card">
<h2>RAM Usage</h2>
<div class="gauge"><div id="ramNeedle" class="needle"></div></div>
<div class="bar"><div id="ramFill" class="fill">0%</div></div>
<p id="ram"></p>
</div>
<div class="card">
<h2>Network</h2>
<p><b>IP:</b> <span id="ip"></span></p>
<p><b>MAC:</b> <span id="mac"></span></p>
</div>
<div class="card" style="margin:20px;">
<h2>Raw data console</h2>
<pre id="console" style="background:#000;color:#0f0;padding:10px;border-radius:6px;height:140px;overflow:auto;">{}</pre>
</div>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
}
// Serve JSON data for AJAX
void handleData() {
String json = "{";
json += "\"host\":\"" + host + "\",";
json += "\"os\":\"" + os + "\",";
json += "\"boot_time\":\"" + boot + "\",";
json += "\"cpu_percent\":" + String(cpu) + ",";
json += "\"ram\":{\"used\":\"" + ramUsed + "\",\"total\":\"" + ramTotal + "\",\"percent\":" + String(ramPct) + "},";
json += "\"network\":{\"ip\":\"" + ip + "\",\"mac\":\"" + mac + "\"}";
json += "}";
server.send(200, "application/json", json);
}
void setup() {
Serial.begin(9600);
pinMode(LED_PIN, OUTPUT);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nConnected!");
Serial.print("ESP32 IP: ");
Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/data", handleData);
server.begin();
}
void loop() {
server.handleClient();
if (Serial.available()) {
String msg = Serial.readStringUntil('\n');
JSONVar data = JSON.parse(msg);
if (JSON.typeof(data) != "undefined") {
host = (const char*)data["host"];
os = (const char*)data["os"];
boot = (const char*)data["boot_time"];
cpu = double(data["cpu_percent"]);
ramUsed = (const char*)data["ram"]["used"];
ramTotal = (const char*)data["ram"]["total"];
ramPct = double(data["ram"]["percent"]);
ip = (const char*)data["network"]["ip"];
mac = (const char*)data["network"]["mac"];
// Blink LED when new data arrives
digitalWrite(LED_PIN, HIGH);
delay(100);
digitalWrite(LED_PIN, LOW);
}
}
}Notes
- Replace
YOUR_SSIDandYOUR_PASSWORD. - Keep Serial Monitor closed while Python is running.
What it does
- Collects real system stats using
psutil. - Formats a JSON object and sends it over USB serial to the ESP32 every 5 seconds.
- Newline terminator
\nis required so the ESP32 can usereadStringUntil('\n').
pip install pyserial psutilPython script (run on your PC, update PORT)
import psutil
import platform
import socket
import time
import serial
import json
from datetime import datetime
PORT = "COM8" # Change to your ESP32 port
BAUD = 9600
def get_size(bytes, suffix="B"):
factor = 1024.0
for unit in ["", "K", "M", "G", "T"]:
if bytes < factor:
return f"{bytes:.2f}{unit}{suffix}"
bytes /= factor
def get_ip_mac():
ip, mac = "N/A", "N/A"
for iface, addrs in psutil.net_if_addrs().items():
for addr in addrs:
if addr.family == socket.AF_INET and not addr.address.startswith("127."):
ip = addr.address
if hasattr(addr.family, "name") and addr.family.name in ("AF_LINK", "AF_PACKET"):
mac = addr.address
return ip, mac
try:
ser = serial.Serial(PORT, BAUD, timeout=1)
time.sleep(2)
while True:
uname = platform.uname()
boot_time = datetime.fromtimestamp(psutil.boot_time()).strftime("%Y-%m-%d %H:%M:%S")
cpu = psutil.cpu_percent(interval=None)
ram = psutil.virtual_memory()
disk = psutil.disk_usage('/')
ip, mac = get_ip_mac()
data = {
"host": uname.node,
"os": f"{uname.system} {uname.release}",
"boot_time": boot_time,
"cpu_percent": cpu,
"ram": {
"used": get_size(ram.used),
"total": get_size(ram.total),
"percent": ram.percent
},
"disk": {
"used": get_size(disk.used),
"total": get_size(disk.total),
"percent": disk.percent
},
"network": {
"ip": ip,
"mac": mac
}
}
ser.write((json.dumps(data) + "\n").encode("utf-8"))
print("Sent:", json.dumps(data))
time.sleep(5)
except serial.SerialException as e:
print("Serial error:", e)
except KeyboardInterrupt:
print("Stopped by user.")
finally:
try:
ser.close()
except:
passRunning and verification (step by step)Prepare hardware
- Connect FireBeetle to PC via USB.
- Ensure board drivers are installed and the COM port is visible.
Upload Arduino sketch
- Open Arduino IDE, paste the sketch, set board to FireBeetle ESP32‑P4, update Wi‑Fi credentials, and upload.
- After upload, open Serial Monitor briefly to see Wi‑Fi connection logs, then close it. Note the printed ESP32 IP.
Start Python sender
- Update
PORTto the ESP32 COM port (Windows:COMx, Linux:/dev/ttyUSB0), then run the Python script. - Confirm the script prints “Sent:” messages every 5 seconds.
Open dashboard
- In a browser on the same network, open
http://<ESP32-IP>/(use the IP printed in Serial Monitor). - The page will auto‑fetch
/dataevery 5 seconds and update the UI. - The raw JSON console shows the latest packet.
Verify LED
- Each time the Python script sends a new JSON packet, the onboard LED on pin 3 should blink briefly.
- Add authentication to the web UI (basic token or password).
- Send telemetry over Wi‑Fi (HTTP POST from Python to ESP32) to remove USB dependency.
- Add more metrics: network throughput, GPU stats (via
GPUtil), disk per‑partition details. - Persist logs on an SD card or send to a remote server.
- Add manual controls: buttons on the FireBeetle to change refresh rate or toggle panels.
- Improve visuals: use charting libraries (Chart.js) hosted locally for sparklines and history graphs.
Conclusion:
You now have a complete local telemetry solution: a Python script on your PC collects CPU, RAM, disk, and network stats and streams them as newline‑terminated JSON over USB to your DFRobot FireBeetle ESP32‑P4, which parses the data, blinks its onboard LED on new packets, and serves a dark, single‑page AJAX dashboard that updates every five seconds and includes live gauges and a raw JSON console—giving you real‑time visibility, local control and privacy, and a flexible foundation for adding authentication, Wi‑Fi telemetry, logging, or alerts.




Comments