Automatic Solar Panel Cleaning System using ESP32 | IoT Based Solar Panel Cleaner Project
Solar Panel Cleaning System Using ESP32
This project creates a fully automated solar panel cleaning system powered by solar energy itself. The system features a self-sustaining power loop, WiFi control via smartphone, and automatic timed cleaning cycles. Perfect for maintaining solar panel efficiency!
How the System Works
Battery → XL4015 Buck → 5V ESP32 → Web Server → Phone Control
ESP32 → 5V Relay → 6V Pump → Cleaning
Here's the complete working explanation:
๐ Power Generation & Storage:
- 6V Solar Panel captures sunlight and generates power
- MT3608 Boost Converter increases voltage from 6V to ~9V (required for charging 7.4V battery)
- TP5100 Charging Module safely charges the 2S lithium battery with constant current/constant voltage
- 2S BMS provides protection against overcharge, over-discharge, short circuit, and balances both cells
- 7.4V Li-ion Battery stores energy and becomes the main power source for the system
⚡ Power Distribution:
- From battery, one line goes to XL4015 Buck Converter
- XL4015 steps down voltage to stable 5V for ESP32
- ESP32 runs continuously, hosting a web server for control
๐ฑ Control System:
- ESP32 creates a WiFi access point and hosts a beautiful web interface
- When you open the web page on your phone and press the power button:
- ESP32 sends signal to the 5V Relay Module
- Relay acts as a switch, connecting battery power directly to the 6V Water Pump
- Pump sprays water on the solar panel for the set duration (5-20 seconds)
- After timer ends, relay turns OFF and pump stops
๐ The Complete Loop:
Sunlight → Solar Panel → Boost Converter → Charger → Battery → Buck Converter → ESP32 → Relay → Pump → Cleaning → Sunlight (repeats)
Completely automatic and self-sustaining! The system charges while cleaning, creating an endless cycle.
Materials Required
Core Components:
- ESP32 Development Board - Buy Here
- XL4015 Buck Converter - Buy Here
- MT3608 Boost Converter - Buy Here
- TP5100 Charging Module - Buy Here
- 2S BMS (Battery Protection) - Buy Here
- 7.4V Li-ion Battery - Buy Here
- 6V Solar Panel - Buy Here
- 6V Water Pump with Pipe - Buy Here
- 5V Relay Module - Buy Here
- Jumper Wires - Buy Here
3D Printable Parts
Download the 3D printable enclosure files for the system:
Download 3D FilesPrint these files using JLC3DP's professional 3D printing service:
Order 3D Prints NowPower Management System
| Stage | Component | Input Voltage | Output Voltage | Purpose |
|---|---|---|---|---|
| 1 | Solar Panel | Sunlight | 6V | Primary power source |
| 2 | MT3608 Boost | 6V | 9V | Boost for battery charging |
| 3 | TP5100 Charger | 9V | 8.4V (to battery) | Charges 2S lithium battery |
| 4 | 2S BMS | 8.4V | 7.4V (protected) | Protection & cell balancing |
| 5 | XL4015 Buck | 7.4V | 5V | Powers ESP32 |
| 6 | Relay + Pump | 7.4V (direct) | 6V to pump | Cleaning action |
Step-by-Step Assembly
Power Circuit Assembly:
- Connect 6V solar panel to MT3608 boost converter input
- Adjust MT3608 potentiometer to output 9V (measure with multimeter)
- Connect MT3608 output to TP5100 charger input
- Connect TP5100 output to 2S BMS (B+ and B-)
- Connect 7.4V battery to BMS (B+ and B- for battery, P+ and P- for output)
- From BMS output, connect to XL4015 buck converter input
- Adjust XL4015 to output 5V (for ESP32)
- Add 470ยตF capacitor across power lines for stability
Control Circuit Assembly:
- Connect XL4015 5V output to ESP32 VIN and GND
- Connect relay module VCC to ESP32 5V, GND to GND
- Connect relay IN pin to ESP32 GPIO 23
- Connect 6V pump positive wire to relay COM (common) terminal
- Connect 6V pump negative wire to battery negative (GND)
- Connect battery positive (P+ from BMS) to relay NO (normally open) terminal
- Double-check all connections before powering on
Testing & Calibration:
- Place solar panel in sunlight - check battery voltage rises
- Verify XL4015 output is stable 5V
- Upload ESP32 code with your WiFi credentials
- Connect to ESP32's web interface using the IP address shown in Serial Monitor
- Test pump by pressing the power button on web interface
- Verify auto shutoff after set timer (default 10 seconds)
- Test all timer options: 5, 10, 15, 20 seconds
- Check battery recharging while system operates
๐ฅ Watch the video above for complete assembly and demonstration of the Solar Panel Cleaning System.
Arduino Code (ESP32)
#include <WiFi.h>
#include <WebServer.h>
const char* ssid = "Your Wifi Name";
const char* password = "Your Password";
WebServer server(80);
const int relayPin = 23; // ACTIVE LOW relay
bool pumpState = false;
unsigned long autoOffTime = 10000; // Default 10 seconds (10000ms)
unsigned long pumpStartTime = 0;
// ================= RELAY CONTROL =================
void setPump(bool state) {
pumpState = state;
if (pumpState) {
digitalWrite(relayPin, LOW); // ON (ACTIVE LOW)
pumpStartTime = millis();
Serial.println("Pump ON");
} else {
digitalWrite(relayPin, HIGH); // OFF
Serial.println("Pump OFF");
}
}
// ================= WEB PAGE =================
String webpage() {
return R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=yes">
<title>Solar Pump Controller</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0f172a;
color: white;
font-family: 'Segoe UI', Arial, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.container {
max-width: 600px;
width: 100%;
background: rgba(30, 41, 59, 0.7);
backdrop-filter: blur(10px);
border-radius: 32px;
padding: 24px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
h2 {
font-size: 24px;
font-weight: 600;
background: linear-gradient(135deg, #fff, #94a3b8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.status-badge {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.05);
padding: 8px 16px;
border-radius: 40px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.connection-led {
width: 10px;
height: 10px;
border-radius: 50%;
background: #f44336;
box-shadow: 0 0 10px #f44336;
transition: all 0.3s;
}
.connection-led.online {
background: #4caf50;
box-shadow: 0 0 15px #4caf50;
}
.connection-text {
font-size: 14px;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
}
/* Status Cards */
.status-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 24px;
}
.status-card {
background: rgba(15, 23, 42, 0.6);
border-radius: 20px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.05);
text-align: left;
}
.status-label {
font-size: 13px;
color: #94a3b8;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 1px;
}
.status-value {
font-size: 28px;
font-weight: 700;
color: #fff;
}
.status-value.on {
color: #4caf50;
text-shadow: 0 0 15px #4caf50;
}
.status-value.off {
color: #f44336;
text-shadow: 0 0 15px #f44336;
}
/* ===== YOUR BUTTON DESIGN ===== */
.power-wrapper {
display: flex;
justify-content: center;
margin: 30px 0;
cursor: pointer;
user-select: none;
}
.power-btn {
width: 220px;
height: 220px;
border-radius: 50%;
position: relative;
cursor: pointer;
transition: all 0.15s ease;
background: radial-gradient(circle at 30% 30%, #444, #111 70%);
border: 16px solid #c0c0c0;
box-shadow:
inset 0 10px 18px rgba(255,255,255,0.1),
inset 0 -12px 25px rgba(0,0,0,0.8),
0 15px 35px rgba(0,0,0,0.7);
}
.power-btn:active {
transform: translateY(8px) scale(0.96);
}
/* Symbol */
.symbol {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
width: 110px;
height: 110px;
position: relative;
color: #aaa;
transition: 0.3s;
}
.power-line {
position: absolute;
top: 5px;
left: 50%;
transform: translateX(-50%);
width: 18px;
height: 50px;
background: currentColor;
border-radius: 12px;
}
.power-circle {
position: absolute;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 80px;
border: 18px solid currentColor;
border-top: 18px solid transparent;
border-radius: 50%;
box-sizing: border-box;
}
.led {
width: 70px;
height: 10px;
border-radius: 6px;
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: red;
box-shadow: 0 0 12px red;
transition: 0.3s;
z-index: 2;
}
/* ON State */
.power-btn.on .symbol {
color: #00ff88;
text-shadow: 0 0 25px #00ff88;
}
.power-btn.on .led {
background: #00ff88;
box-shadow: 0 0 25px #00ff88;
}
/* Timer Section */
.timer-section {
background: rgba(15, 23, 42, 0.6);
border-radius: 24px;
padding: 24px;
margin: 24px 0;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.timer-title {
font-size: 14px;
color: #94a3b8;
margin-bottom: 16px;
text-transform: uppercase;
letter-spacing: 1px;
}
.timer-buttons {
display: flex;
gap: 12px;
justify-content: space-between;
flex-wrap: wrap;
}
.timer-btn {
flex: 1;
min-width: 70px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
color: white;
padding: 14px 8px;
border-radius: 16px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.timer-btn.active {
background: rgba(33, 150, 243, 0.15);
border-color: #2196f3;
box-shadow: 0 0 20px rgba(33, 150, 243, 0.3);
color: #2196f3;
}
.timer-sec {
font-size: 11px;
color: rgba(255, 255, 255, 0.5);
font-weight: 400;
}
.timer-btn.active .timer-sec {
color: #90caf9;
}
/* Timer Display */
.timer-display {
background: linear-gradient(145deg, #0f172a, #1e293b);
border-radius: 24px;
padding: 24px;
margin: 24px 0;
border: 1px solid rgba(255, 255, 255, 0.05);
text-align: center;
}
.timer-label {
font-size: 14px;
color: #94a3b8;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 2px;
}
.timer-value {
font-size: 64px;
font-weight: 700;
color: #2196f3;
font-family: monospace;
line-height: 1;
text-shadow: 0 0 30px rgba(33, 150, 243, 0.5);
}
.timer-unit {
font-size: 18px;
color: rgba(255, 255, 255, 0.5);
margin-left: 5px;
}
.progress-container {
width: 100%;
height: 8px;
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
margin-top: 20px;
overflow: hidden;
}
.progress-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #2196f3, #64b5f6);
border-radius: 4px;
transition: width 0.1s linear;
}
/* Disabled state for timer buttons */
.timer-btn.disabled {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
@media (max-width: 480px) {
.container { padding: 16px; }
.power-btn { width: 180px; height: 180px; border-width: 12px; }
.symbol { width: 90px; height: 90px; }
.power-line { width: 14px; height: 40px; }
.power-circle { width: 65px; height: 65px; border-width: 14px; }
.timer-value { font-size: 48px; }
.timer-btn { min-width: 60px; font-size: 16px; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>⚡ Solar Panel Cleaning System </h2>
<div class="status-badge">
<span class="connection-led" id="connLed"></span>
<span class="connection-text" id="connText">Connecting...</span>
</div>
</div>
<!-- Status Cards -->
<div class="status-grid">
<div class="status-card">
<div class="status-label">Pump Status</div>
<div class="status-value off" id="pumpStatus">OFF</div>
</div>
<div class="status-card">
<div class="status-label">System</div>
<div class="status-value" id="systemStatus">Standby</div>
</div>
</div>
<!-- YOUR POWER BUTTON -->
<div class="power-wrapper" onclick="togglePump()">
<div id="powerBtn" class="power-btn">
<div class="led"></div>
<div class="symbol">
<div class="power-line"></div>
<div class="power-circle"></div>
</div>
</div>
</div>
<!-- Timer Selection -->
<div class="timer-section">
<div class="timer-title">⏰ Auto Shutdown Timer</div>
<div class="timer-buttons">
<button class="timer-btn" onclick="setTimer(5)" id="timer5">5<span class="timer-sec">SEC</span></button>
<button class="timer-btn active" onclick="setTimer(10)" id="timer10">10<span class="timer-sec">SEC</span></button>
<button class="timer-btn" onclick="setTimer(15)" id="timer15">15<span class="timer-sec">SEC</span></button>
<button class="timer-btn" onclick="setTimer(20)" id="timer20">20<span class="timer-sec">SEC</span></button>
</div>
</div>
<!-- Timer Display -->
<div class="timer-display" id="timerDisplay">
<div class="timer-label">Time Remaining</div>
<div>
<span class="timer-value" id="timerValue">10</span>
<span class="timer-unit">seconds</span>
</div>
<div class="progress-container">
<div class="progress-fill" id="progressFill"></div>
</div>
</div>
</div>
<script>
// ==================== STATE MANAGEMENT ====================
let isPumpOn = false;
let remainingTime = 0;
let totalTime = 10; // in seconds
let timerInterval = null;
let commandInProgress = false;
// ==================== TOGGLE PUMP ====================
function togglePump() {
if (commandInProgress) return;
commandInProgress = true;
fetch('/toggle')
.then(() => {
console.log('Toggle command sent');
setTimeout(() => { commandInProgress = false; }, 300);
})
.catch(error => {
console.error('Error:', error);
commandInProgress = false;
});
}
// ==================== SET TIMER ====================
function setTimer(seconds) {
if (isPumpOn) {
alert('Please turn OFF the pump first');
return;
}
// Convert seconds to milliseconds for server
let timeMs = seconds * 1000;
// Update UI
totalTime = seconds;
document.getElementById('timerValue').textContent = seconds;
// Update active button
document.querySelectorAll('.timer-btn').forEach(btn => {
btn.classList.remove('active');
});
document.getElementById('timer' + seconds).classList.add('active');
// Send to server
fetch('/setTimer?time=' + timeMs);
}
// ==================== UPDATE STATUS ====================
function updateStatus() {
fetch('/status')
.then(res => res.json())
.then(data => {
// Update connection status
document.getElementById('connLed').className = 'connection-led online';
document.getElementById('connText').innerHTML = 'Online';
// Get button element
let btn = document.getElementById('powerBtn');
let pumpStatusEl = document.getElementById('pumpStatus');
let systemStatusEl = document.getElementById('systemStatus');
// Update pump state
isPumpOn = data.pump;
if (data.pump) {
btn.classList.add('on');
pumpStatusEl.innerHTML = 'RUNNING';
pumpStatusEl.className = 'status-value on';
systemStatusEl.innerHTML = 'ACTIVE';
systemStatusEl.className = 'status-value on';
// Calculate remaining time
if (data.remainingTime !== undefined) {
remainingTime = data.remainingTime;
document.getElementById('timerValue').textContent = remainingTime;
// Update progress bar
let progress = (remainingTime / totalTime) * 100;
document.getElementById('progressFill').style.width = progress + '%';
}
// Disable timer buttons
document.querySelectorAll('.timer-btn').forEach(btn => {
btn.classList.add('disabled');
});
} else {
btn.classList.remove('on');
pumpStatusEl.innerHTML = 'OFF';
pumpStatusEl.className = 'status-value off';
systemStatusEl.innerHTML = 'STANDBY';
systemStatusEl.className = 'status-value';
// Reset timer display
document.getElementById('timerValue').textContent = totalTime;
document.getElementById('progressFill').style.width = '0%';
// Enable timer buttons
document.querySelectorAll('.timer-btn').forEach(btn => {
btn.classList.remove('disabled');
});
}
})
.catch(() => {
// Connection error
document.getElementById('connLed').className = 'connection-led';
document.getElementById('connText').innerHTML = 'Offline';
document.getElementById('connLed').style.background = '#f44336';
document.getElementById('connLed').style.boxShadow = '0 0 15px #f44336';
});
}
// ==================== LOCAL COUNTDOWN ====================
function startLocalCountdown() {
if (timerInterval) clearInterval(timerInterval);
timerInterval = setInterval(() => {
if (isPumpOn && remainingTime > 0) {
remainingTime--;
document.getElementById('timerValue').textContent = remainingTime;
let progress = (remainingTime / totalTime) * 100;
document.getElementById('progressFill').style.width = progress + '%';
// Visual effect for last 3 seconds
if (remainingTime <= 3) {
document.getElementById('timerValue').style.color = '#ff9800';
} else {
document.getElementById('timerValue').style.color = '#2196f3';
}
}
}, 1000);
}
// ==================== INITIALIZE ====================
function initialize() {
// Start status updates
updateStatus();
setInterval(updateStatus, 1000);
// Start local countdown
startLocalCountdown();
// Set initial timer to 10 seconds
setTimer(10);
}
// Start everything
initialize();
</script>
</body>
</html>
)rawliteral";
}
// ==================== ROUTES ====================
void handleRoot() {
server.send(200, "text/html", webpage());
}
void handleToggle() {
setPump(!pumpState);
server.send(200, "text/plain", "OK");
}
void handleStatus() {
// Calculate remaining time in seconds
int remainingSeconds = 0;
if (pumpState) {
unsigned long elapsed = millis() - pumpStartTime;
if (elapsed < autoOffTime) {
remainingSeconds = (autoOffTime - elapsed) / 1000;
}
}
String json = "{";
json += "\"pump\":" + String(pumpState ? "true" : "false") + ",";
json += "\"remainingTime\":" + String(remainingSeconds);
json += "}";
server.send(200, "application/json", json);
}
void handleTimer() {
if (server.hasArg("time")) {
autoOffTime = server.arg("time").toInt();
Serial.print("Timer set to: ");
Serial.print(autoOffTime / 1000);
Serial.println(" seconds");
}
server.send(200, "text/plain", "OK");
}
// ==================== SETUP ====================
void setup() {
Serial.begin(115200);
Serial.println("\nStarting Solar Pump Controller...");
pinMode(relayPin, OUTPUT);
digitalWrite(relayPin, HIGH); // SAFE OFF (active LOW)
// Connect to WiFi
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 30) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\n✅ WiFi Connected!");
Serial.print("๐ก IP Address: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\n❌ WiFi Failed!");
}
// Setup routes
server.on("/", handleRoot);
server.on("/toggle", handleToggle);
server.on("/status", handleStatus);
server.on("/setTimer", handleTimer);
server.begin();
Serial.println("๐ Server Started!");
}
// ==================== LOOP ====================
void loop() {
server.handleClient();
// Auto OFF timer check
if (pumpState && (millis() - pumpStartTime >= autoOffTime)) {
setPump(false);
Serial.println("⏰ Auto OFF");
}
}
Code Features:
- Beautiful Web Interface: Custom-designed power button with status display
- Adjustable Timer: 5, 10, 15, or 20 second cleaning cycles
- Real-time Updates: Live countdown with progress bar
- Connection Status: Visual LED indicator for WiFi connection
- Mobile Responsive: Works perfectly on smartphones
- Safety Features: Active LOW relay for failsafe operation
- Auto Shutdown: Pump turns off automatically after timer expires
Setup Instructions:
- Change
ssidandpasswordto your WiFi credentials - Upload code to ESP32 using Arduino IDE (install ESP32 board support first)
- Open Serial Monitor to see the IP address after connection
- Connect to the same WiFi network on your phone
- Open browser and enter the ESP32's IP address
- Control the pump from the beautiful web interface!
Wiring Connections
ESP32 Pin Connections:
| Component | ESP32 Pin | Notes |
|---|---|---|
| Relay Module IN | GPIO 23 | Control signal (ACTIVE LOW) |
| Relay VCC | 5V | From XL4015 buck converter |
| Relay GND | GND | Common ground |
| ESP32 VIN | XL4015 5V | Power input |
| ESP32 GND | XL4015 GND | Power ground |
Power System Connections:
| From | To | Voltage |
|---|---|---|
| Solar Panel + | MT3608 IN+ | 6V |
| Solar Panel - | MT3608 IN- | GND |
| MT3608 OUT+ | TP5100 IN+ | 9V |
| MT3608 OUT- | TP5100 IN- | GND |
| TP5100 B+ | BMS B+ | Battery + |
| TP5100 B- | BMS B- | Battery - |
| BMS P+ | XL4015 IN+ / Relay NO | 7.4V |
| BMS P- | XL4015 IN- / Pump - | GND |
| XL4015 OUT+ | ESP32 VIN / Relay VCC | 5V |
| Relay COM | Pump + | Switched 7.4V |
Applications
☀️ Residential Solar Panels
Maintain efficiency with regular cleaning
๐ญ Solar Farms
Scalable design for multiple panels
๐ RVs & Boats
Off-grid solar maintenance
๐ฑ Greenhouses
Keep solar-powered systems clean
๐ง Note: This system is completely self-powered! The solar panel charges the battery while also powering the cleaning system. In full sunlight, the battery will remain charged even with regular cleaning cycles. For best results, adjust the timer based on your panel's dirt accumulation rate.
Why Choose This Design?
- ✅ Self-Sustaining: Runs entirely on solar power, no external electricity needed
- ✅ WiFi Control: Convenient smartphone interface from anywhere
- ✅ Efficiency Boost: Clean panels can produce 15-25% more energy
- ✅ Battery Protection: Multiple protection layers (BMS, chargers, converters)
- ✅ Customizable: Adjustable timers for different cleaning needs
- ✅ Scalable: Can be adapted for larger panels and pumps
Comments
Post a Comment