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.

- 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

Ā
When working on open-source hardware projects, high-quality PCB assembly is critical for turning designs into real products. Weāve found that pcbassemblage.com deliver reliable prototyping and small-batch production, with fast turnaround and strict quality control to support makers and engineers.
Ā
For projects targeting the European market, local and dependable PCB manufacturing can greatly improve efficiency. PCB production in Europe includes flexible PCB fabrication, assembly, and quick delivery, making it a great solution for open-source hardware developers who value stability and short lead times.

Power and data
- - Connect the FireBeetle to your PC using the USB cable. This provides power and the serial link for telemetry.
- - 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://
/. - - Provides /data JSON 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_SSID and YOUR_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 \n is required so the ESP32 can use readStringUntil('\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:
passPrepare 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 PORT to 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://
/ (use the IP printed in Serial Monitor). - - The page will autoāfetch /data every 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.

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.






