Community project

Solar Light Tracker

Arduino
Photo of Solar Light Tracker
Generated with AI

kelleyriverarodriguez1969

Published October 19, 2026

This solar light tracker uses four photoresistors arranged in a 2x2 grid to detect the brightest light source and automatically orient a dual-axis servo bracket toward it. The Arduino Uno reads light levels from each quadrant, compares them, and commands two SG90 servos to pan and tilt the sensor head until all four readings are balanced, keeping the tracker pointed at the sun or brightest available light.

The project runs entirely on solar power, with a 5V/1W panel charging an 18650 Li-ion cell through a TP4056 protection module, then boosting the voltage to a stable 5V rail for the Arduino and servo controller. This guide provides a complete wiring diagram, parts list, Arduino firmware with configurable thresholds and movement limits, and step-by-step assembly instructions from building the light-sensor head through safely testing servo movement.

Wiring diagram

Gather all the parts

QtyComponent
1

5V / 1W Solar Panel

Small 5 V / ~200 mA peak solar panel. Used as the upstream input to a TP4056 (or solar charge controller) for off-grid / outdoor projects.

1

TP4056 Li-Ion/LiPo charger module with protection

TP4056 single-cell Li-Ion/LiPo linear charger module, 5V USB input, 1A charge current (programmable). Common variants ship with DW01 protection. Pair with battery_lipo_storage for the cell.

1

18650 Li-ion Cell

3.7 V, 2500 mAh or higher

18650 lithium-ion cell, nominal 3.7 V, ~2500 mAh. Common for higher-capacity portable / battery-bank style projects; needs a holder and protection / charger circuit.

1

18650 Holder

Use one cell only; do not fit the second cell

Generic holder for one or two removable 18650 Li-ion cells. It is a mechanical/electrical power holder, not a charger or protection circuit; pair with a charger/BMS and regulator appropriate to the cell count.

1

5 V 3 A boost converter module

A step-up power module that turns the single 18650 cell voltage into a steady 5 V supply for the Arduino and servos.

1

PCA9685 PWM Servo Driver

NXP PCA9685-based 16-channel, 12-bit PWM/servo driver module. Controlled over I2C at default address 0x40, with address pins for additional addresses. Outputs 16 independent PWM channels at one shared configurable frequency; servos normally use about 50-60 Hz. VCC is the logic supply and sets the I2C/PWM logic level. V+ is the separate servo/load rail, commonly 5V-6V for servos. OE is active high disable and is pulled low by default on Adafruit-style breakouts.

1

SG90 Servo

Existing pan servo

Micro servo motor (SG90)

1

SG90 Servo

Existing tilt servo

Micro servo motor (SG90)

1

Two-axis pan and tilt servo bracket

A mechanical bracket that holds the two servos so the sensor head can turn left-right and up-down.

1

Photo Cell (CdS Photoresistor)

Top-left sensor

CdS light-dependent resistor. Resistance decreases as light increases. Adafruit product 161 currently specifies approximately 1 kΩ in light and 10 kΩ in dark. Use either symmetric lead in a voltage divider and read the divider with an analog input.

1

Photo Cell (CdS Photoresistor)

Top-right sensor

CdS light-dependent resistor. Resistance decreases as light increases. Adafruit product 161 currently specifies approximately 1 kΩ in light and 10 kΩ in dark. Use either symmetric lead in a voltage divider and read the divider with an analog input.

1

Photo Cell (CdS Photoresistor)

Bottom-left sensor

CdS light-dependent resistor. Resistance decreases as light increases. Adafruit product 161 currently specifies approximately 1 kΩ in light and 10 kΩ in dark. Use either symmetric lead in a voltage divider and read the divider with an analog input.

1

Photo Cell (CdS Photoresistor)

Bottom-right sensor

CdS light-dependent resistor. Resistance decreases as light increases. Adafruit product 161 currently specifies approximately 1 kΩ in light and 10 kΩ in dark. Use either symmetric lead in a voltage divider and read the divider with an analog input.

1

10 kΩ resistor

10 kΩ

A fixed resistor that works with one light sensor to create a voltage the Arduino can measure.

1

10 kΩ resistor

10 kΩ

A fixed resistor that works with one light sensor to create a voltage the Arduino can measure.

1

10 kΩ resistor

10 kΩ

A fixed resistor that works with one light sensor to create a voltage the Arduino can measure.

1

10 kΩ resistor

10 kΩ

A fixed resistor that works with one light sensor to create a voltage the Arduino can measure.

1

1000 µF 10 V electrolytic capacitor

1000 µF, 10 V or higher

A polarized capacitor placed across the 5 V servo supply to reduce voltage dips when the servos start moving.

Assemble it in 7 steps

1. Make the light-sensor head

Mount ldr_tl, ldr_tr, ldr_bl, and ldr_br at the four corners of a small flat plate. Glue or tape two thin, opaque strips between them as a raised cross, so each sensor is slightly shaded when the head is not facing the brightest light. Keep the top-left, top-right, bottom-left, and bottom-right positions exactly as named when viewing the front of the moving head.

  • A cross about 10–20 mm taller than the sensors makes direction differences easier to detect.
  • Photoresistors have no positive or negative leg, so either lead can be used first.

2. Build the four light-reading connections

For each sensor, connect one photoresistor lead to the 5 V rail (power). Connect its other lead to both its matching Arduino analog input and one end of its matching 10 kΩ resistor (signal): ldr_tl to A0, ldr_tr to A1, ldr_bl to A2, and ldr_br to A3. Connect the free end of every 10 kΩ resistor to GND (ground).

  • Each sensor and resistor pair meets in one shared breadboard row; that same row also gets the wire to A0, A1, A2, or A3.
  • Use red wires for 5 V, black wires for GND, and four different colors for the four signal wires.
  • Do not connect an analog-input row directly to both 5 V and GND; the 10 kΩ resistor must sit between that row and GND.

3. Mount the servos and bracket

Fit pan_servo into the fixed part of pan_tilt_bracket, then fit tilt_servo into the moving part. Leave the servo horns loose until the first powered center position, then attach them with the head pointing roughly forward and level. Secure the sensor plate to the tilt section so it moves with both axes.

  • Do not force either bracket axis by hand after the horns are fitted; stripped gears can make a clicking sound.
  • Keep the panel or sensor head light enough for the servos to lift without struggling.

4. Wire the servo controller

Connect pca9685 VCC to the 5 V rail (power), GND to GND (ground), SDA to Arduino A4 (data), and SCL to Arduino A5 (data). Connect pca9685 V+ to the 5 V rail (servo power) and OE to GND (this keeps its outputs enabled). Plug pan_servo into PCA9685 channel 0 and tilt_servo into channel 1: brown or black to GND, red to V+, and orange, yellow, or white to the signal pin.

  • The Uno’s A4 and A5 pins are used as communication wires here; do not use them for the light sensors.
  • Check the small labels beside each three-pin PCA9685 channel before plugging in each servo lead.
  • Make sure the servo plug is not reversed — swapped 5 V and GND can damage a servo or the controller.

5. Build the solar battery supply with power disconnected

With battery_18650 removed from battery_holder, connect solar_panel positive to tp4056 IN+ and solar_panel negative to tp4056 IN-. Connect the holder’s positive and negative leads to tp4056 B+ and B-. Connect tp4056 OUT+ to boost_5v VIN+ and tp4056 OUT- to boost_5v VIN-. Fit only one 18650 cell in battery_holder, observing its marked positive and negative ends.

  • Use a TP4056 board that has separate OUT+ and OUT- pads; these are the protected load outputs.
  • The unused position in the two-cell holder must remain empty.
  • Never fit two cells in the holder or connect its series output to the TP4056 — the charger is for one cell only and can be damaged by a two-cell pack.
  • Keep the bare solar-panel leads apart; touching them together wastes power and can heat thin wires.

6. Set and connect the regulated 5 V rail

Before joining the Arduino or servos, use a multimeter to adjust boost_5v VOUT+ and VOUT- to exactly 5.0 V. Then connect VOUT+ to the shared 5 V rail (power) and VOUT- to the shared GND rail (ground). Connect the Arduino expansion board 5 V pin to that 5 V rail and its GND pin to the GND rail. Place servo_capacitor across the same rails: its long positive lead to 5 V and its striped negative lead to GND.

  • Put the capacitor physically close to the PCA9685 V+ and GND terminals.
  • All grounds must be joined together: boost module, Uno, PCA9685, sensors, and both servos.
  • Do not connect the boost module to the Uno VIN pin; this regulated 5 V supply must go to the Uno 5 V pin.
  • If the boost output is above 5.2 V, stop and adjust it before connecting electronics — excess voltage can damage the Uno and servos.

7. Check movement safely

Place the assembly where both axes have clear space to move. Hold a flashlight or bright lamp to one side of the sensor head; it should take small steps toward that brighter side. If an axis moves away from the light after deployment, change its INVERT_PAN or INVERT_TILT setting in the project code and deploy again.

  • Test indoors with a lamp first, then move outdoors for solar use.
  • The 1 W panel charges slowly; it does not provide enough sunlight power to run two actively loaded servos continuously while also charging the battery.
  • Keep fingers and wires out of the bracket’s moving path so a servo cannot pinch them or pull out a lead.

Review all connections

1. Connections between "solar_panel" and "Arduino"

Functionsolar_panelArduino
power+5V → TP4056 Li-Ion/LiPo charger module with protection IN+EXT
groundGND → TP4056 Li-Ion/LiPo charger module with protection IN-EXT

2. Connections between "battery_18650" and "Arduino"

Functionbattery_18650Arduino
power+V → 18650 Holder BAT+EXT
groundGND → 18650 Holder BAT-EXT

3. Connections between "battery_holder" and "Arduino"

Functionbattery_holderArduino
powerBAT+ → TP4056 Li-Ion/LiPo charger module with protection B+EXT
groundBAT- → TP4056 Li-Ion/LiPo charger module with protection B-EXT

4. Connections between "tp4056" and "Arduino"

Functiontp4056Arduino
powerOUT+ → 5 V 3 A boost converter module VIN+EXT
groundOUT- → 5 V 3 A boost converter module VIN-EXT

5. Connections between "boost_5v" and "Arduino"

Functionboost_5vArduino
powerVOUT+5V
groundVOUT-GND

6. Connections between "pca9685" and "Arduino"

Functionpca9685Arduino
powerVCC5V
powerV+5V
groundGNDGND
i2cSDAGPIO 18
i2cSCLGPIO 19
groundOEGND
digitalPWM0 → SG90 Servo SIGNALEXT
digitalPWM1 → SG90 Servo SIGNALEXT

7. Connections between "pan_servo" and "Arduino"

Functionpan_servoArduino
powerVCC5V
groundGNDGND

8. Connections between "tilt_servo" and "Arduino"

Functiontilt_servoArduino
powerVCC5V
groundGNDGND

9. Connections between "servo_capacitor" and "Arduino"

Functionservo_capacitorArduino
power+5V
ground-GND

10. Connections between "ldr_tl" and "Arduino"

Functionldr_tlArduino
powerTerminal 15V
passiveTerminal 2GPIO 14

11. Connections between "resistor_tl" and "Arduino"

Functionresistor_tlArduino
dataLead 1 → Photo Cell (CdS Photoresistor) Terminal 2EXT
groundLead 2GND

12. Connections between "ldr_tr" and "Arduino"

Functionldr_trArduino
powerTerminal 15V
passiveTerminal 2GPIO 15

13. Connections between "resistor_tr" and "Arduino"

Functionresistor_trArduino
dataLead 1 → Photo Cell (CdS Photoresistor) Terminal 2EXT
groundLead 2GND

14. Connections between "ldr_bl" and "Arduino"

Functionldr_blArduino
powerTerminal 15V
passiveTerminal 2GPIO 16

15. Connections between "resistor_bl" and "Arduino"

Functionresistor_blArduino
dataLead 1 → Photo Cell (CdS Photoresistor) Terminal 2EXT
groundLead 2GND

16. Connections between "ldr_br" and "Arduino"

Functionldr_brArduino
powerTerminal 15V
passiveTerminal 2GPIO 17

17. Connections between "resistor_br" and "Arduino"

Functionresistor_brArduino
dataLead 1 → Photo Cell (CdS Photoresistor) Terminal 2EXT
groundLead 2GND

Deploy the firmware

#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>

const uint8_t LDR_TL_PIN = A0;
const uint8_t LDR_TR_PIN = A1;
const uint8_t LDR_BL_PIN = A2;
const uint8_t LDR_BR_PIN = A3;

const uint8_t PAN_CHANNEL = 0;
const uint8_t TILT_CHANNEL = 1;
const uint16_t SERVO_MIN_PULSE = 110;
const uint16_t SERVO_MAX_PULSE = 510;
const int LIGHT_THRESHOLD = 35;
const int DARK_LEVEL = 45;
const unsigned long SAMPLE_INTERVAL_MS = 250;

// Change either setting to true only if that axis moves away from the brighter side.
const bool INVERT_PAN = false;
const bool INVERT_TILT = false;

const int PAN_MIN = 20;
const int PAN_MAX = 160;
const int TILT_MIN = 30;
const int TILT_MAX = 150;

Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x40);
int panPosition = 90;
int tiltPosition = 90;
unsigned long lastSampleMs = 0;

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

uint16_t angleToPulse(int angle) {
  angle = constrain(angle, 0, 180);
  return map(angle, 0, 180, SERVO_MIN_PULSE, SERVO_MAX_PULSE);
}

void moveServos() {
  pwm.setPWM(PAN_CHANNEL, 0, angleToPulse(panPosition));
  pwm.setPWM(TILT_CHANNEL, 0, angleToPulse(tiltPosition));
}

void setup() {
  Wire.begin();
  pwm.begin();
  pwm.setPWMFreq(50);
  delay(10);
  moveServos();
}

void loop() {
  const unsigned long now = millis();
  if (now - lastSampleMs < SAMPLE_INTERVAL_MS) {
    delay(5);
    return;
  }
  lastSampleMs = now;

  const int topLeft = readAverage(LDR_TL_PIN);
  const int topRight = readAverage(LDR_TR_PIN);
  const int bottomLeft = readAverage(LDR_BL_PIN);
  const int bottomRight = readAverage(LDR_BR_PIN);

  const int totalLight = topLeft + topRight + bottomLeft + bottomRight;
  if (totalLight < DARK_LEVEL * 4) {
    return;
  }

  const int leftLight = (topLeft + bottomLeft) / 2;
  const int rightLight = (topRight + bottomRight) / 2;
  const int topLight = (topLeft + topRight) / 2;
  const int bottomLight = (bottomLeft + bottomRight) / 2;

  const int horizontalDifference = rightLight - leftLight;
  const int verticalDifference = topLight - bottomLight;
  bool changed = false;

  if (abs(horizontalDifference) > LIGHT_THRESHOLD) {
    const int direction = horizontalDifference > 0 ? 1 : -1;
    panPosition = constrain(panPosition + (INVERT_PAN ? -direction : direction), PAN_MIN, PAN_MAX);
    changed = true;
  }

  if (abs(verticalDifference) > LIGHT_THRESHOLD) {
    const int direction = verticalDifference > 0 ? 1 : -1;
    tiltPosition = constrain(tiltPosition + (INVERT_TILT ? -direction : direction), TILT_MIN, TILT_MAX);
    changed = true;
  }

  if (changed) {
    moveServos();
  }
}

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