Community project

Solar Panel Tracking Cleaner

ESP32
Photo of Solar Panel Tracking Cleaner
Generated with AI

Ngtfury dev

Published October 11, 2026

This project combines solar tracking with automated panel cleaning to maximize energy generation. The system uses four light-dependent resistors to track the sun's position across the sky, adjusting azimuth and elevation servos to keep the panel optimally oriented. When dust accumulation reduces output below a threshold detected by the INA219 current sensor, a motorized cleaner automatically traverses the panel surface.

Builders will receive a complete wiring diagram, parts list, and step-by-step assembly instructions covering mechanical mounting, light-sensing divider construction, low-voltage sensor integration, and power distribution. The ESP32 firmware handles dual-axis sun tracking, dust detection via current monitoring, temperature sensing with the BME280 and DS18B20, and automated cleaning cycles with end-switch safety limits.

Wiring diagram

Wiring diagram for Solar Panel Tracking Cleaner

Gather all the parts

QtyComponent
1

Adafruit INA219 High-Side DC Current Sensor

INA219 high-side current and bus-voltage monitor breakout. It is powered from 3.3V or 5V and communicates over I2C. Route the measured load current through VIN+ and VIN-; those shunt terminals are part of the power path, not MCU GPIO.

1

BME280

Bosch BME280 environmental sensor on the exact Adafruit 2652 breakout. Supports I2C via SCK/SCL and SDI/SDA, optional SDO address select, and SPI pins when needed.

1

DS18B20

Digital temperature sensor using OneWire protocol

1

MG996R Servo Motor

High-torque digital metal gear RC servo motor by TowerPro. Operating voltage 4.8V-7.2V (nominal 5V-6V external supply). Standard RC servo control: 50 Hz signal with roughly 500-2500 us pulse width for 0-180 degree rotation. Three-wire interface: Brown = GND, Red = VCC, Orange = signal. Stall current can reach about 2.5A at 6V, so use a dedicated external servo supply and common ground with the controller. Add bulk capacitance across the servo rail when driving one or more servos. Torque: 9.4 kg.cm (4.8V) / 11 kg.cm (6V).

1

MG996R Servo Motor

High-torque digital metal gear RC servo motor by TowerPro. Operating voltage 4.8V-7.2V (nominal 5V-6V external supply). Standard RC servo control: 50 Hz signal with roughly 500-2500 us pulse width for 0-180 degree rotation. Three-wire interface: Brown = GND, Red = VCC, Orange = signal. Stall current can reach about 2.5A at 6V, so use a dedicated external servo supply and common ground with the controller. Add bulk capacitance across the servo rail when driving one or more servos. Torque: 9.4 kg.cm (4.8V) / 11 kg.cm (6V).

1

TB6612FNG dual motor-driver breakout

A dual H-bridge motor-driver breakout that reverses and speed-controls a suitably small 12 V brush motor.

1

6V 4W Solar Panel

Polycrystalline or monocrystalline solar panel rated 4W peak output at 6V nominal. Typical small-panel values are Vmp about 6V and Imp about 667mA; open-circuit voltage is higher than the nominal working voltage and must stay within the charger module input rating. Two-wire output (V+ and GND). Outdoor/weatherproof rating depends on the exact panel. Directly compatible with CN3791 MPPT solar charger modules only when that module's input-voltage rating is not exceeded. No firmware library required — purely a passive power source.

1

GL5528 cadmium-sulfide photoresistor

A light-sensitive resistor that provides the north-west light reading for panel alignment.

1

GL5528 cadmium-sulfide photoresistor

A light-sensitive resistor that provides the north-east light reading for panel alignment.

1

GL5528 cadmium-sulfide photoresistor

A light-sensitive resistor that provides the south-west light reading for panel alignment.

1

GL5528 cadmium-sulfide photoresistor

A light-sensitive resistor that provides the south-east light reading for panel alignment.

1

0–3.3 V panel-condition sensor input

A protected analog-input connector for a separately selected dust or panel-condition sensor with a 0–3.3 V output.

1

Micro Switch - Premium Zippy 3-Terminal

Higher-quality 3-terminal bump-actuator microswitch for arcade controls and light mechatronics, rated 30 VDC / 250 mA max. Common, normally-open, and normally-closed contacts.

1

Micro Switch - Premium Zippy 3-Terminal

Higher-quality 3-terminal bump-actuator microswitch for arcade controls and light mechatronics, rated 30 VDC / 250 mA max. Common, normally-open, and normally-closed contacts.

1

10 kΩ resistor

10 kΩ

The fixed lower resistor that makes the north-west LDR into a safe 3.3 V light-sensing divider.

1

10 kΩ resistor

10 kΩ

The fixed lower resistor that makes the north-east LDR into a safe 3.3 V light-sensing divider.

1

10 kΩ resistor

10 kΩ

The fixed lower resistor that makes the south-west LDR into a safe 3.3 V light-sensing divider.

1

10 kΩ resistor

10 kΩ

The fixed lower resistor that makes the south-east LDR into a safe 3.3 V light-sensing divider.

1

4.7 kΩ resistor

4.7 kΩ

The pull-up resistor that holds the DS18B20 data wire at a valid 3.3 V logic level.

1

12 V regulated DC power supply

12 V, ≥10 A

A regulated 12 V DC supply rated for at least 10 A that powers the brush branch and the buck converters.

1

6 V 8 A synchronous buck converter

6 V, ≥8 A

A regulated buck converter that supplies both high-current tracking servos from the 12 V supply.

1

5 V 2 A buck converter

5 V, ≥2 A

A regulated buck converter that provides clean 5 V for the ESP32 VIN pin from the 12 V source.

1

12 V brushed DC geared motor

12 V, must be ≤1.0 A continuous and ≤3.2 A short peak

The geared motor that moves the cleaning brush or wiper across the panel.

1

Electrolytic capacitor

2200 µF, 10 V or higher

A bulk capacitor that absorbs brief servo current surges on the regulated 6 V rail.

1

Solar-panel monitored-output terminal

A two-way terminal that carries the separately monitored solar-panel output to a charger or correctly rated test load.

Assemble it in 7 steps

1. Mount the moving parts

With all power unplugged, mount the two MG996R servos on the azimuth and elevation brackets. Fit the panel and brush mechanics so neither servo nor brush can hit a hard stop before the matching limit switch is pressed.

  • Do not connect servo power while you are fitting arms or linkages — a moving servo can pinch fingers or damage the mechanism.

2. Make the four light-sensing dividers

Place the four GL5528 sensors around a small cross-shaped shade at the panel centre: north-west, north-east, south-west, and south-east. For each one, connect one LDR leg to 3V3 and its other leg to both its matching 10 kΩ resistor and its ESP32 input. Connect the free resistor leg to GND.

  • Keep the four LDRs equally exposed except for the small centre shade; this lets unequal light readings show which way the panel should turn.

3. Wire the low-voltage sensors

Connect INA219 VCC and BME280 VCC to 3V3 (power), and both GND pins to GND (ground). Connect both SDA pins to GPIO21 (data) and both SCL pins to GPIO22 (clock). Connect BME280 SDO to GND so it answers at address 0x76. Connect DS18B20 VCC to 3V3 (power), GND to GND (ground), and DATA to GPIO4 (signal). Put the 4.7 kΩ resistor between DATA and 3V3. Connect the 0–3.3 V dust-sensor output to GPIO27 (signal) and its ground to GND.

  • Never connect a 5 V dust-sensor output directly to GPIO27 — an ESP32 input is 3.3 V only. Use a verified 0–3.3 V sensor or a correctly calculated divider.

4. Wire the control signals and end switches

Connect the azimuth servo orange signal wire to GPIO25 (signal) and the elevation servo orange signal wire to GPIO26 (signal). Connect TB6612 AIN1 to GPIO16 (direction), AIN2 to GPIO17 (direction), PWMA to GPIO18 (speed signal), and STBY to GPIO19 (enable). Connect each limit-switch COM terminal to GND (ground); connect the home switch NO terminal to GPIO32 (signal) and the far switch NO terminal to GPIO33 (signal). Leave each NC terminal unconnected.

  • The program treats a pressed switch as LOW because the switch joins its GPIO to ground.

5. Wire the power branches

Feed the 12 V supply into the two buck converters. Set and measure the servo converter at exactly 6.0 V before connecting the servos. Connect its VOUT+ to both servo red wires (power), VOUT- to both servo brown wires (ground), and fit the 2200 µF capacitor across the same 6 V rails with its + stripe-free lead on VOUT+ and its marked − lead on VOUT−. Set the logic converter to exactly 5.0 V and connect it to ESP32 VIN (power) and GND (ground). Join all supply negatives at the common GND point. Connect TB6612 VM to fused 12 V, VCC to 3V3, GND to common GND, and A01/A02 to the two brush-motor leads.

  • Do not power either MG996R from the ESP32 5 V or 3.3 V pin — current surges can reset or damage the board.
  • The TB6612 is only safe if the 12 V brush motor is rated at or below 1.0 A continuous current. Do not connect a higher-current motor to it.

6. Connect the solar monitor separately

Connect the solar panel positive lead to INA219 VIN+ and INA219 VIN− to the monitored PV+ terminal. Connect the panel negative lead and PV− terminal to the common low-current ground reference. Connect the PV+ and PV− terminal only to a correctly rated charger or test load; it is not the ESP32 power input.

  • Do not connect the small 6 V solar panel straight to the ESP32 VIN pin or to the 12 V motor rail — sunlight voltage changes and can damage equipment.

7. Check before applying power

With a meter and the 12 V supply still unplugged, check that there is no short between each positive rail and GND. Confirm the logic buck is 5.0 V and the servo buck is 6.0 V. Press each limit switch and confirm it mechanically stops the brush at the correct end. Then power the ESP32 over USB first, connect the 12 V supply, and observe the system with the brush lifted clear of the panel for its first test.

  • If the brush travels toward the wrong end, unplug 12 V and swap the two motor leads at A01/A02 or reverse the direction logic — do not let it force against the frame.

Review all connections

1. Connections between "ina219_1" and "ESP32"

Functionina219_1ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22
dataVIN+ → 6V 4W Solar Panel V+EXT
dataVIN- → Solar-panel monitored-output terminal PV+EXT

2. Connections between "bme280_1" and "ESP32"

Functionbme280_1ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22
groundSDOGND

3. Connections between "ds18b20_1" and "ESP32"

Functionds18b20_1ESP32
powerVCC3V3
groundGNDGND
dataDATAGPIO 4

4. Connections between "ds18b20_pullup" and "ESP32"

Functionds18b20_pullupESP32
powerA3V3
dataB → DS18B20 DATAEXT

5. Connections between "tracker_servo_azimuth" and "ESP32"

Functiontracker_servo_azimuthESP32
powerVCC → 6 V 8 A synchronous buck converter VOUT+EXT
groundGND → 6 V 8 A synchronous buck converter VOUT-EXT
pwmSIGGPIO 25

6. Connections between "tracker_servo_elevation" and "ESP32"

Functiontracker_servo_elevationESP32
powerVCC → 6 V 8 A synchronous buck converter VOUT+EXT
groundGND → 6 V 8 A synchronous buck converter VOUT-EXT
pwmSIGGPIO 26

7. Connections between "servo_bulk_cap" and "ESP32"

Functionservo_bulk_capESP32
power+ → 6 V 8 A synchronous buck converter VOUT+EXT
ground- → 6 V 8 A synchronous buck converter VOUT-EXT

8. Connections between "tb6612_1" and "ESP32"

Functiontb6612_1ESP32
powerVM → 12 V regulated DC power supply +12VEXT
powerVCC3V3
groundGNDGND
digitalAIN1GPIO 16
digitalAIN2GPIO 17
pwmPWMAGPIO 18
digitalBIN1GPIO 23
digitalBIN2GPIO 13
pwmPWMBGPIO 14
digitalSTBYGPIO 19
powerA01 → 12 V brushed DC geared motor M+EXT
powerA02 → 12 V brushed DC geared motor M-EXT

9. Connections between "cleaner_limit_home" and "ESP32"

Functioncleaner_limit_homeESP32
groundCOMGND
digitalNOGPIO 32

10. Connections between "cleaner_limit_far" and "ESP32"

Functioncleaner_limit_farESP32
groundCOMGND
digitalNOGPIO 33

11. Connections between "ldr_nw" and "ESP32"

Functionldr_nwESP32
powerLDR+3V3
analogLDR-GPIO 36

12. Connections between "ldr_resistor_nw" and "ESP32"

Functionldr_resistor_nwESP32
groundBGND
analogA → GL5528 cadmium-sulfide photoresistor LDR-EXT

13. Connections between "ldr_ne" and "ESP32"

Functionldr_neESP32
powerLDR+3V3
analogLDR-GPIO 39

14. Connections between "ldr_resistor_ne" and "ESP32"

Functionldr_resistor_neESP32
groundBGND
analogA → GL5528 cadmium-sulfide photoresistor LDR-EXT

15. Connections between "ldr_sw" and "ESP32"

Functionldr_swESP32
powerLDR+3V3
analogLDR-GPIO 34

16. Connections between "ldr_resistor_sw" and "ESP32"

Functionldr_resistor_swESP32
groundBGND
analogA → GL5528 cadmium-sulfide photoresistor LDR-EXT

17. Connections between "ldr_se" and "ESP32"

Functionldr_seESP32
powerLDR+3V3
analogLDR-GPIO 35

18. Connections between "ldr_resistor_se" and "ESP32"

Functionldr_resistor_seESP32
groundBGND
analogA → GL5528 cadmium-sulfide photoresistor LDR-EXT

19. Connections between "dust_input" and "ESP32"

Functiondust_inputESP32
power3V33V3
groundGNDGND
analogOUTGPIO 27

20. Connections between "supply_12v" and "ESP32"

Functionsupply_12vESP32
groundGNDGND

21. Connections between "buck_servo_6v" and "ESP32"

Functionbuck_servo_6vESP32
powerVIN+ → 12 V regulated DC power supply +12VEXT
groundVIN- → 12 V regulated DC power supply GNDEXT
groundVOUT-GND

22. Connections between "buck_logic_5v" and "ESP32"

Functionbuck_logic_5vESP32
powerVIN+ → 12 V regulated DC power supply +12VEXT
groundVIN- → 12 V regulated DC power supply GNDEXT
powerVOUT+VIN
groundVOUT-GND

23. Connections between "solar_panel_1" and "ESP32"

Functionsolar_panel_1ESP32
groundGNDGND

24. Connections between "pv_monitored_output" and "ESP32"

Functionpv_monitored_outputESP32
groundPV-GND

Deploy the firmware

#include <Wire.h>
#include <Adafruit_INA219.h>
#include <Adafruit_BME280.h>
#include <soc/gpio_struct.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <ESP32Servo.h>

#define DS18B20_PIN 4
#define AZIMUTH_SERVO_PIN 25
#define ELEVATION_SERVO_PIN 26
#define TB_AIN1_PIN 16
#define TB_AIN2_PIN 17
#define TB_PWMA_PIN 18
#define TB_STBY_PIN 19
#define CLEANER_HOME_PIN 32
#define CLEANER_FAR_PIN 33
#define LDR_NW_PIN 36
#define LDR_NE_PIN 39
#define LDR_SW_PIN 34
#define LDR_SE_PIN 35
#define DUST_PIN 27

const int SERVO_MIN_DEG = 15;
const int SERVO_MAX_DEG = 165;
const int TRACK_DEADBAND = 120;
const int DUST_CLEAN_THRESHOLD = 2500;
const uint32_t SENSOR_PERIOD_MS = 2000;
const uint32_t CLEAN_TIMEOUT_MS = 30000;

Adafruit_INA219 ina219;
Adafruit_BME280 bme;
OneWire oneWire(DS18B20_PIN);
DallasTemperature panelTemperature(&oneWire);
Servo azimuthServo;
Servo elevationServo;

int azimuthDeg = 90;
int elevationDeg = 90;
uint32_t lastSampleMs = 0;
uint32_t cleanerStartMs = 0;
bool cleanerRunning = false;
bool cleanerReturning = false;

int readAverage(int pin) {
  uint32_t sum = 0;
  for (int i = 0; i < 8; ++i) {
    sum += analogRead(pin);
    delay(2);
  }
  return sum / 8;
}

void stopCleaner() {
  ledcWrite(TB_PWMA_PIN, 0);
  digitalWrite(TB_AIN1_PIN, LOW);
  digitalWrite(TB_AIN2_PIN, LOW);
  cleanerRunning = false;
  cleanerReturning = false;
}

void runCleanerTowardFarEnd() {
  digitalWrite(TB_STBY_PIN, HIGH);
  digitalWrite(TB_AIN1_PIN, HIGH);
  digitalWrite(TB_AIN2_PIN, LOW);
  ledcWrite(TB_PWMA_PIN, 180);
  cleanerStartMs = millis();
  cleanerRunning = true;
  cleanerReturning = false;
}

void returnCleanerHome() {
  digitalWrite(TB_AIN1_PIN, LOW);
  digitalWrite(TB_AIN2_PIN, HIGH);
  ledcWrite(TB_PWMA_PIN, 180);
  cleanerStartMs = millis();
  cleanerRunning = true;
  cleanerReturning = true;
}

void updateTracker(int nw, int ne, int sw, int se) {
  int left = nw + sw;
  int right = ne + se;
  int top = nw + ne;
  int bottom = sw + se;

  if (abs(left - right) > TRACK_DEADBAND) azimuthDeg += (right > left) ? 1 : -1;
  if (abs(top - bottom) > TRACK_DEADBAND) elevationDeg += (top > bottom) ? 1 : -1;
  azimuthDeg = constrain(azimuthDeg, SERVO_MIN_DEG, SERVO_MAX_DEG);
  elevationDeg = constrain(elevationDeg, SERVO_MIN_DEG, SERVO_MAX_DEG);
  azimuthServo.write(azimuthDeg);
  elevationServo.write(elevationDeg);
}

void reportMaintenance(int ldrMean, int dust, float busVoltage, float currentmA, float panelC, float airC, float humidity) {
  bool lowSolarOutput = ldrMean > 1800 && busVoltage < 4.0F;
  bool dirtyPanel = dust > DUST_CLEAN_THRESHOLD;
  bool hotPanel = panelC > 75.0F;
  Serial.printf("LDR=%d dust=%d PV=%.2fV %.0fmA panel=%.1fC air=%.1fC RH=%.1f%%\n", ldrMean, dust, busVoltage, currentmA, panelC, airC, humidity);
  if (lowSolarOutput) Serial.println("MAINTENANCE: panel voltage is low despite bright light; inspect panel, wiring, and charger/load.");
  if (dirtyPanel) Serial.println("MAINTENANCE: dust input exceeds the cleaning threshold.");
  if (hotPanel) Serial.println("MAINTENANCE: panel temperature is high; inspect ventilation and compare output trend.");
  if (dirtyPanel && !cleanerRunning && digitalRead(CLEANER_FAR_PIN) == HIGH) runCleanerTowardFarEnd();
}

void setup() {
  Serial.begin(115200);
  analogReadResolution(12);
  Wire.begin(21, 22);
  pinMode(CLEANER_HOME_PIN, INPUT_PULLUP);
  pinMode(CLEANER_FAR_PIN, INPUT_PULLUP);
  pinMode(TB_AIN1_PIN, OUTPUT);
  pinMode(TB_AIN2_PIN, OUTPUT);
  pinMode(TB_STBY_PIN, OUTPUT);
  digitalWrite(TB_STBY_PIN, HIGH);
  ledcAttach(TB_PWMA_PIN, 20000, 8);
  stopCleaner();
  azimuthServo.setPeriodHertz(50);
  elevationServo.setPeriodHertz(50);
  azimuthServo.attach(AZIMUTH_SERVO_PIN, 500, 2500);
  elevationServo.attach(ELEVATION_SERVO_PIN, 500, 2500);
  azimuthServo.write(azimuthDeg);
  elevationServo.write(elevationDeg);
  panelTemperature.begin();
  if (!ina219.begin()) Serial.println("INA219 not found; check its 3.3V, GND, SDA, and SCL wires.");
  if (!bme.begin(0x76)) Serial.println("BME280 not found; check its 3.3V, GND, SDA, SCL, and SDO-to-GND wires.");
}

void loop() {
  if (cleanerRunning) {
    if (!cleanerReturning && digitalRead(CLEANER_FAR_PIN) == LOW) {
      returnCleanerHome();
      Serial.println("Cleaner reached the far limit and is returning home.");
    } else if (cleanerReturning && digitalRead(CLEANER_HOME_PIN) == LOW) {
      stopCleaner();
      Serial.println("Cleaner returned to the home limit.");
    } else if (millis() - cleanerStartMs > CLEAN_TIMEOUT_MS) {
      stopCleaner();
      Serial.println("Cleaner stopped because its travel timeout expired.");
    }
  }
  if (millis() - lastSampleMs < SENSOR_PERIOD_MS) return;
  lastSampleMs = millis();
  int nw = readAverage(LDR_NW_PIN);
  int ne = readAverage(LDR_NE_PIN);
  int sw = readAverage(LDR_SW_PIN);
  int se = readAverage(LDR_SE_PIN);
  int dust = readAverage(DUST_PIN);
  updateTracker(nw, ne, sw, se);
  panelTemperature.requestTemperatures();
  float panelC = panelTemperature.getTempCByIndex(0);
  float airC = bme.readTemperature();
  float humidity = bme.readHumidity();
  float busVoltage = ina219.getBusVoltage_V();
  float currentmA = ina219.getCurrent_mA();
  reportMaintenance((nw + ne + sw + se) / 4, dust, busVoltage, currentmA, panelC, airC, humidity);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik