#include <WiFi.
h>
#include <WebServer.h>
#define VOLTAGE_PIN 32
#define CURRENT_PIN 34
#define RELAY_PIN 5
#define HOLD_LOW_SWITCH_PIN 2 // Manual override (Force LOW)
float sensorMaxVoltage = 25.0;
int adcMax = 4095;
float refVoltage = 3.3;
float ACS_Zero = 0;
float ACS_Sensitivity = 0.100;
WebServer server(80);
const char MAIN_page[] PROGMEM = R"=====(
<!DOCTYPE html>
<html>
<head>
<title>ESP32 Power Monitor</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: Arial, sans-serif;
background: #121212;
color: #E5E5E5;
text-align: center;
margin: 0;
padding: 0;
.container {
width: 100%;
margin: 0 auto;
padding-bottom: 40px;
/* TOP CARD */
.card {
width: 350px;
margin: 25px auto;
padding: 24px;
background: #1E1E1E;
border-radius: 18px;
box-shadow: 0 6px 18px rgba(0,0,0,0.55);
.valueBox {
font-size: 26px;
margin: 10px;
color: #FFFFFF;
/* SWITCH */
.toggle {
width: 62px;
height: 34px;
position: relative;
display: inline-block;
.toggle input { display:none; }
.slider {
position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0;
background:#3A3A3A; border-radius:34px; transition: .3s;
.slider:before {
position:absolute; content:""; height:26px; width:26px;
left:4px; bottom:4px;
background:white; border-radius:50%; transition:.3s;
input:checked + .slider { background:#0A84FF; }
input:checked + .slider:before { transform: translateX(26px); }
/* GRAPH ROW — default desktop mode */
.graphRow {
display: flex;
flex-wrap: wrap;
gap: 20px;
width: 100%;
justify-content: space-between;
padding: 10px 20px;
box-sizing: border-box;
/* GRAPH CARD — desktop = 1/3 width */
.graphCard {
background: #1E1E1E;
border-radius: 18px;
padding: 16px;
flex: 1;
min-width: 260px;
max-width: 33%;
box-shadow: 0 6px 18px rgba(0,0,0,0.45);
.smallLabel {
margin-bottom: 10px;
font-size: 15px;
color: #BFBFBF;
/* CANVAS */
canvas {
width: 100%;
height: 180px;
background: #121212;
border-radius: 12px;
box-shadow: inset 0 4px 8px rgba(0,0,0,0.6);
/* MOBILE MODE (<768px) → stack vertically */
@media (max-width: 768px) {
.graphRow {
flex-direction: column;
padding: 10px 12px;
}
.graphCard {
max-width: 100%;
width: 100%;
/* SMALL PHONES (<480px) */
@media (max-width: 480px) {
.graphCard {
padding: 14px;
canvas {
height: 160px;
</style>
<script>
// DATA ARRAYS
const maxPoints = 40;
let voltHistory = new Array(maxPoints).fill(0);
let currHistory = new Array(maxPoints).fill(0);
let powerHistory = new Array(maxPoints).fill(0);
// ------------------------------------------------------
// DRAW SIMPLE LINE CHART
// ------------------------------------------------------
function drawLineChart(canvasId, data, color, unit) {
const canvas = [Link](canvasId);
const ctx = [Link]('2d');
const w = [Link];
const h = [Link];
[Link](0,0,w,h);
[Link] = '#121212';
[Link](0,0,w,h);
// GRID
[Link] = '#222';
[Link] = 1;
for (let i=0;i<=4;i++) {
let y = i*(h/4);
[Link]();
[Link](0,y);
[Link](w,y);
[Link]();
// RANGE
let maxVal = [Link](...data);
let minVal = [Link](...data);
if (maxVal === minVal) { maxVal += 1; minVal -= 1; }
const step = w / ([Link] - 1);
// LINE
[Link]();
[Link] = 2.4;
[Link] = color;
for (let i=0;i<[Link];i++){
const x = i * step;
const y = h - ((data[i] - minVal) / (maxVal - minVal)) * h;
if (i === 0) [Link](x,y);
else [Link](x,y);
[Link]();
// POINTS
[Link] = color;
for (let i=0;i<[Link];i++){
const x = i * step;
const y = h - ((data[i] - minVal) / (maxVal - minVal)) * h;
[Link]();
[Link](x,y,2,0,[Link]*2);
[Link]();
// LABELS
[Link] = '#BFBFBF';
[Link] = '12px Arial';
[Link](unit + " max: " + [Link](2), 5, 12);
[Link]("min: " + [Link](2), 5, h - 5);
// ------------------------------------------------------
// UPDATE UI
// ------------------------------------------------------
function updateUI(v, c, p, relayOn) {
[Link]("volt").innerText = [Link](2);
[Link]("curr").innerText = [Link](2);
[Link]("pow").innerText = [Link](2);
[Link]("relayState").checked = !!relayOn;
[Link](); [Link](); [Link]();
[Link](v); [Link](c); [Link](p);
drawLineChart("voltCanvas", voltHistory, "#0A84FF", "V");
drawLineChart("currCanvas", currHistory, "#00D17A", "A");
drawLineChart("powerCanvas", powerHistory, "#FFCE44", "W");
// ------------------------------------------------------
// FETCH JSON FROM ESP32
// ------------------------------------------------------
function fetchValues() {
fetch("/values")
.then(r => [Link]())
.then(data => {
updateUI(
parseFloat([Link]),
parseFloat([Link]),
parseFloat([Link]),
[Link] == 1
);
});
function toggleRelay() {
fetch("/toggle").then(() => setTimeout(fetchValues, 200));
// ------------------------------------------------------
// PAGE LOAD
// ------------------------------------------------------
[Link] = function() {
const ids = ["voltCanvas","currCanvas","powerCanvas"];
// Auto-size canvases to actual displayed width
[Link](id => {
let c = [Link](id);
let displayWidth = [Link];
[Link] = displayWidth;
[Link] = 180;
});
fetchValues();
setInterval(fetchValues, 1000);
};
</script>
</head>
<body>
<div class="container">
<div class="card">
<h2>ESP32 Power Monitor</h2>
<div class="valueBox">Voltage: <span id="volt">--</span> V</div>
<div class="valueBox">Current: <span id="curr">--</span> A</div>
<div class="valueBox">Power: <span id="pow">--</span> W</div>
<h3>Relay Control</h3>
<label class="toggle">
<input type="checkbox" id="relayState" onclick="toggleRelay()">
<span class="slider"></span>
</label>
</div>
<!-- RESPONSIVE FULL-WIDTH GRAPH ROW -->
<div class="graphRow">
<div class="graphCard">
<div class="smallLabel">Voltage (V)</div>
<canvas id="voltCanvas"></canvas>
</div>
<div class="graphCard">
<div class="smallLabel">Current (A)</div>
<canvas id="currCanvas"></canvas>
</div>
<div class="graphCard">
<div class="smallLabel">Power (W)</div>
<canvas id="powerCanvas"></canvas>
</div>
</div>
</div>
</body>
</html>
)=====";
// ---------- AVERAGE ADC ----------
int readADC(int pin) {
long sum = 0;
for (int i = 0; i < 100; i++) {
sum += analogRead(pin);
delayMicroseconds(200);
return sum / 100;
// ---------- CALIBRATION ----------
void calibrateACS() {
long sum = 0;
for (int i = 0; i < 500; i++) {
sum += analogRead(CURRENT_PIN);
delay(2);
float raw = sum / 500.0;
ACS_Zero = (raw * refVoltage) / adcMax;
// ---------- HOLD-LOW SWITCH ----------
bool checkHoldLowSwitch() {
if (digitalRead(HOLD_LOW_SWITCH_PIN) == HIGH) {
digitalWrite(RELAY_PIN, LOW); // force LOW (ON)
return true;
} else {
return false;
// ---------- AUTO-OFF WHEN CURRENT = 0 ----------
void handleAutoCurrentOff(float current) {
// placeholder - disabled by default
// if (current <= 0.01) {
// digitalWrite(RELAY_PIN, HIGH); // OFF
// }
// ========= WEB HANDLERS =========
void handleRoot() {
server.send_P(200, "text/html", MAIN_page);
void handleValues() {
// Read voltage
int rawVolt = readADC(VOLTAGE_PIN);
float voltageOut = (rawVolt * refVoltage) / adcMax;
float voltage = voltageOut * (sensorMaxVoltage / refVoltage);
// Read current
int rawCurrent = readADC(CURRENT_PIN);
float voltageACS = (rawCurrent * refVoltage) / adcMax;
float current = (voltageACS - ACS_Zero) / ACS_Sensitivity;
if (current < 0.01) current = 0;
float power = voltage * current;
// Apply logic
if (!checkHoldLowSwitch()) {
handleAutoCurrentOff(current);
int relayPhysical = digitalRead(RELAY_PIN);
// We define relayOn = 1 if physical pin is LOW (assuming LOW energizes relay)
int relayOn = (relayPhysical == LOW) ? 1 : 0;
String json = "{";
json += "\"voltage\":" + String(voltage, 2) + ",";
json += "\"current\":" + String(current, 2) + ",";
json += "\"power\":" + String(power, 2) + ",";
json += "\"relay\":" + String(relayOn);
json += "}";
[Link](200, "application/json", json);
void handleToggle() {
int state = digitalRead(RELAY_PIN);
// toggle: if currently HIGH (OFF) -> set LOW (ON), else set HIGH (OFF)
if (state == HIGH) digitalWrite(RELAY_PIN, LOW); else digitalWrite(RELAY_PIN, HIGH);
[Link](200, "text/plain", "OK");
// ---------- SETUP ----------
void setup() {
[Link](115200);
delay(1000);
pinMode(RELAY_PIN, OUTPUT);
pinMode(HOLD_LOW_SWITCH_PIN, INPUT_PULLDOWN);
digitalWrite(RELAY_PIN, HIGH); // Relay OFF initially
calibrateACS();
// ---- WiFi ----
[Link]("Starting WiFi Access Point...");
[Link]("ESP32-PowerMonitor", "12345678"); // You can change both
[Link]("AP IP Address: ");
[Link]([Link]());
// Web routes
[Link]("/", handleRoot);
[Link]("/values", handleValues);
[Link]("/toggle", handleToggle);
[Link]();
// ---------- LOOP ----------
void loop() {
[Link]();