Project tutorial
Servo-Controlled, Light-Tracking Solar Panel Platform

Servo-Controlled, Light-Tracking Solar Panel Platform © GPL3+

Use an Arduino, two small servo motors with light sensing photo resistors to steer a solar panel towards the sun. UPDATED

  • 12,447 views
  • 10 comments
  • 41 respects

Components and supplies

Adafruit industries ada161 image 75px
Photo resistor
×4
1260 00
Adafruit Flora RGB Neopixel LEDs- Pack of 4
or equivalent RBG multi-color LED
×1
Omron b3f 1000 image 75px
SparkFun Pushbutton switch 12mm
or equivalent normally open push button
×2
10988 01
Temperature Sensor
×1
A000066 iso both
Arduino UNO & Genuino UNO
×1
Rg srv180 a
RobotGeek 180 Degree Robot Servo
or equivalent. I used Futaba S3003 servos
×2
Mfr 25frf52 10k sml
Resistor 10k ohm
×6
Mfr 25fbf52 221r sml
Resistor 221 ohm
I used 220 ohm
×1
Panasonic eca2am101
Capacitor 100 µF
×2
solar panel (80mm x 80mm)
can be bought on Amazon.com or equivalent
×1

Necessary tools and machines

Hand tools and a few small nuts and bolts to create the bracket
Solid base for mounting the "twist" servo motor.
I used a small block of wood
Bendable metal or small sheets of rigid aluminum that can be formed into an "L" shape bracket
I used approx. 3 mm thick aluminum that just under 2 cm wide
Thin, bendable metal to form a bracket to hold solar panel platform and mount it to the "twist" servo
I used thin steel strips bought at hardware store typically used for holding HVAC ducting together. Has mounting holes pre-drilled for ease

Apps and online services

Ide web
Arduino IDE
Microsoft Excel for documentation and notes

About this project

This is another implementation of using two 180 degree servos and four photo-resistors to be able to track a small solar panel to align to the area of most light intensity.

The motors make small movements to try and point the solar panel to the brightest light.

There is also a multi-color LED to indicate if the system is enabled or not. And two push buttons to enable/disable the motors.

There are a few routines that can be used for moving the motors to commanded positions. The commands are via the serial port interface and a series of numeric codes/commands that can be sent to the Arduino Uno.

The independent move commands are programmed in a way to provide (somewhat) smooth motions instead of immediate position commands that cause the mechanical mechanism to move violently.

Code

Q2 codeArduino
//Scott Mangiacotti
//Tucson, Arizona USA
//May 2018
//Q2
//Version 2.0

#include <Servo.h>

//Constants
int const GIVE_BACK_TIME = 125;

//Constants for inputs
int const I_RED_BUTTON_PIN = 4;
int const I_GREEN_BUTTON_PIN = 2;
int const I_LDR1_PIN = A3;  //top left
int const I_LDR2_PIN = A2;  //top right
int const I_LDR3_PIN = A1;  //bottom left
int const I_LDR4_PIN = A0;  //bottom right

//Constants for outputs
int const O_RED_LED_PIN = 9;
int const O_GREEN_LED_PIN = 11;
int const O_BLUE_LED_PIN = 10;
int const O_TWIST_SERVO_PIN = 5;
int const O_TILT_SERVO_PIN = 6;

//Global variables
bool gRunning = false;
bool gTrackToLightEnabled = false;
bool gVerboseDiagMode = false;

Servo gServoTwist;
int gServoTwistPositionCommand; //commanded position
bool gServoTwistMoveIP; //move in progress

Servo gServoTilt;
bool gServoTiltMoveIP; //move in progress
int gServoTiltPositionCommand; //commanded position

int gLDR1;
int gLDR2;
int gLDR3;
int gLDR4;


//Runs once
void setup() 
{
  //Open a serial port
  Serial.begin(9600);

  //Setup digital inputs
  pinMode(I_RED_BUTTON_PIN, INPUT);
  pinMode(I_GREEN_BUTTON_PIN, INPUT);
  
  //Setup digital outputs
  pinMode(O_RED_LED_PIN, OUTPUT);
  pinMode(O_GREEN_LED_PIN, OUTPUT);
  pinMode(O_BLUE_LED_PIN, OUTPUT);

  //Run startup routine
  startup();

}


//Runs continuously
void loop()
{
  //Serial port message receipt processing
  if (Serial.available() > 0)
  {
    int iControlCode;
    iControlCode = Serial.parseInt();
    processSerialMessage(iControlCode);
  }

  //Read green button
  int iGreenButton;
  iGreenButton = digitalRead(I_GREEN_BUTTON_PIN);
  if (iGreenButton == HIGH && gRunning == false)
  {
    enableTracking();
    gTrackToLightEnabled = true;
    
  }

  //Read red button
  int iRedButton;
  iRedButton = digitalRead(I_RED_BUTTON_PIN);
  if (iRedButton == HIGH && gRunning == true)
  {
    disableTracking();
    
  }

  //Read all instrumentation into global variables
  readPhotoResistors();
  averageTopTwoSensors();
  averageBottomTwoSensors();
  
  //Adjust servo positions according to light on photoresistors
  if (gRunning == true)
  {
    if (gTrackToLightEnabled == true)
    {
      //Make small moves of servos based on photo-resitor light levels
      trackToLightSensors();
    }
    else
    {
      //Make small moves based on user commands at serial port. Avoids high velocity moves on mechanism
      smoothMoveTwist();
      smoothMoveTilt();
    }
  }

  //Give a little time back
  delay(GIVE_BACK_TIME);
}


//Turn on servo tracking
void enableTracking()
{
  //Set global variables so other parts of program knows motors are ready to run
  gRunning = true;

  //Attach to the servo motors
  gServoTwist.attach(O_TWIST_SERVO_PIN);
  gServoTilt.attach(O_TILT_SERVO_PIN);

  //Turn on green LED and turn off red LED
  digitalWrite(O_GREEN_LED_PIN, HIGH);
  digitalWrite(O_RED_LED_PIN, LOW);

  //Post results
  Serial.println("servos enabled");
}


//Turn off servo tracking
void disableTracking()
{
  gRunning = false;
  gTrackToLightEnabled = false;

  //Detach from servo motors
  gServoTwist.detach();
  gServoTilt.detach();

  //Clean up move command and move in-process (IP) variables
  gServoTwistPositionCommand = gServoTwist.read();
  gServoTwistMoveIP = false;

  //Clean up move command and move in-process (IP) variables
  gServoTiltPositionCommand = gServoTilt.read();
  gServoTiltMoveIP = false;

  //Turn on red LED, turn off green LED
  digitalWrite(O_RED_LED_PIN, HIGH);
  digitalWrite(O_GREEN_LED_PIN, LOW);

  //Post results
  Serial.println("servos disabled");
}


//Track to light based on photosensor values
void trackToLightSensors()
{
  float fTop;
  float fBottom;
  float fLeft;
  float fRight;

  int iTwistMoveCommand;
  int iTiltMoveCommand;

  int iMoveAmount;

  //Initialize
  //The variable below determines how many degrees of potential motion for both servos
  //per scan of the program. This number in combination with the global constant
  //named 'GIVE_BACK_TIME' determine how aggressive the moves will be.
  iMoveAmount = 5;

  //Get current servo positions
  iTwistMoveCommand = gServoTwist.read();
  iTiltMoveCommand = gServoTilt.read();

  //Get averages
  fTop = averageTopTwoSensors();
  fBottom = averageBottomTwoSensors();
  fLeft = averageLeftTwoSensors();
  fRight = averageRightTwoSensors();

  //Calculate twist move
  if (fLeft > fRight)
  {
    //Move positive
    iTwistMoveCommand += iMoveAmount;

  }
  else if (fRight > fLeft)
  {
    //Move negative
    iTwistMoveCommand -= iMoveAmount;

  }
  else
  {
    //Same. don't move
    
  }

  //Calculate tilt move
  if (fTop > fBottom)
  {
    //Move positive
    iTiltMoveCommand += iMoveAmount;

  }
  else if (fBottom > fTop)
  {
    //Move negative
    iTiltMoveCommand -= iMoveAmount;

  }
  else
  {
    //Same. don't move
    
  }

  //Bounds check twist servo move command
  if (iTwistMoveCommand < 0)
  {
    iTwistMoveCommand = 0;
  }

  if (iTwistMoveCommand > 179)
  {
    iTwistMoveCommand = 179;
  }

  //Bounds check tilt servo move command
  if (iTiltMoveCommand < 45)
  {
    iTiltMoveCommand = 45;
  }

  if (iTiltMoveCommand > 135)
  {
    iTiltMoveCommand = 135;
  }
  
  //Perform moves
  gServoTwist.write(iTwistMoveCommand);
  gServoTilt.write(iTiltMoveCommand);

  //Post results
  if (gVerboseDiagMode == true)
  {
    Serial.println("tl, tr, bl, br, top avg, bottom avg, left avg, right avg, twist move, tilt move: ");
    Serial.print(gLDR1);
    Serial.print(", ");
    Serial.print(gLDR2);
    Serial.print(", ");
    Serial.print(gLDR3);
    Serial.print(", ");
    Serial.print(gLDR4);
    Serial.print(", ");
    Serial.print(fTop);
    Serial.print(", ");
    Serial.print(fBottom);
    Serial.print(", ");
    Serial.print(fLeft);
    Serial.print(", ");
    Serial.print(fRight);
    Serial.print(", ");
    Serial.print(iTwistMoveCommand);
    Serial.print(", ");
    Serial.println(iTiltMoveCommand);
  }
  
}


//read photoresistor values into global variables
void readPhotoResistors()
{
  //Values come in scaled 0-1024
  gLDR1 = analogRead(I_LDR1_PIN);
  gLDR2 = analogRead(I_LDR2_PIN);
  gLDR3 = analogRead(I_LDR3_PIN);
  gLDR4 = analogRead(I_LDR4_PIN);
}


//When the servos are commanded to a position they move at a fast speed.
//Too fast will potentially affect the mechanical structure that holds and moves
//the solar panel and light sensor platform. This routine takes a "move command"
//and makes small incremental moves until the servo is at that desired position.
//This routine is for the base mounted twisting servo motor
void smoothMoveTwist()
{
  int iCurrentPos;
  int iMoveAmountPerScan;
  int iNewMoveCommand;

  //Set move amount per scan in degrees.
  //Combination of this variable and global const 'GIVE_BACK_TIME' determine overall move speed
  iMoveAmountPerScan = 1;

  //Determine current position
  iCurrentPos = gServoTwist.read();

  //Are we at position?
  if (iCurrentPos == gServoTwistPositionCommand)
  {
    gServoTwistMoveIP = false;
    return;
  }
  else
  {
    gServoTwistMoveIP = true;
  }

  //Start off where we are currently at
  iNewMoveCommand = iCurrentPos;
  
  //Determine move amount
  if (iCurrentPos < gServoTwistPositionCommand)
  {
    //Add
    iNewMoveCommand = iNewMoveCommand + iMoveAmountPerScan;
  }
  else
  {
    //Subtract
    iNewMoveCommand = iNewMoveCommand - iMoveAmountPerScan;
  }

  //Clamp if needed
  if (iNewMoveCommand < 0)
  {
    iNewMoveCommand = 0;
  }

  if (iNewMoveCommand > 179)
  {
    iNewMoveCommand = 179;
  }

  //Move
  gServoTwist.write(iNewMoveCommand);

  //Post results
  if (gVerboseDiagMode == true)
  {
    //todo:
    Serial.print("Twist servo move (this move, total):");
    Serial.print(iNewMoveCommand);
    Serial.print(", ");
    Serial.println(gServoTwistPositionCommand);
  }
}


//When the servos are commanded to a position they move at a fast speed.
//Too fast will potentially affect the mechanical structure that holds and moves
//the solar panel and light sensor platform. This routine takes a "move command"
//and makes small incremental moves until the servo is at that desired position.
//This routine is for the bracket mounted tilting servo motor
void smoothMoveTilt()
{
  int iCurrentPos;
  int iMoveAmountPerScan;
  int iNewMoveCommand;

  //Set move amount per scan in degrees.
  //Combination of this variable and global const 'GIVE_BACK_TIME' determine overall move speed
  iMoveAmountPerScan = 1;

  //Determine current position
  iCurrentPos = gServoTilt.read();

  //Are we at position?
  if (iCurrentPos == gServoTiltPositionCommand)
  {
    gServoTiltMoveIP = false;
    return;
  }
  else
  {
    gServoTiltMoveIP = true;
  }

  //Start off where we are currently at
  iNewMoveCommand = iCurrentPos;
  
  //Determine move amount
  if (iCurrentPos < gServoTiltPositionCommand)
  {
    //Add
    iNewMoveCommand = iNewMoveCommand + iMoveAmountPerScan;
  }
  else
  {
    //Subtract
    iNewMoveCommand = iNewMoveCommand - iMoveAmountPerScan;
  }

  //Clamp if needed
  if (iNewMoveCommand < 0)
  {
    iNewMoveCommand = 0;
  }

  if (iNewMoveCommand > 179)
  {
    iNewMoveCommand = 179;
  }

  //Move
  gServoTilt.write(iNewMoveCommand);

  //Post results
  if (gVerboseDiagMode == true)
  {
    //todo:
    Serial.print("Tilt servo move (this move, total):");
    Serial.print(iNewMoveCommand);
    Serial.print(", ");
    Serial.println(gServoTiltPositionCommand);
  }
}


//Take the mathematical average of the two LDR at top of panel
float averageTopTwoSensors()
{
  float fAvg;

  //Math
  fAvg = (gLDR1 + gLDR2) / 2.0;

  return fAvg;
}


//Take the mathematical average of the two LDR at bottom of panel
float averageBottomTwoSensors()
{
  float fAvg;

  //Math
  fAvg = (gLDR3 + gLDR4) / 2.0;

  return fAvg;
}


//Take the mathematical average of the two LDR on left of panel
float averageLeftTwoSensors()
{
  float fAvg;

  //Math
  fAvg = (gLDR1 + gLDR3) / 2.0;

  return fAvg;
}


//Take the mathematical average of the two LDR on right of panel
float averageRightTwoSensors()
{
  float fAvg;

  //Math
  fAvg = (gLDR2 + gLDR4) / 2.0;

  return fAvg;
}


//Process received messages from the serial port interface
//Input parameter iControlCode is the value received from the serial port to be processed
//First two digits are the control command, remaining three are the value to process
void processSerialMessage(int iControlCode)
{
  int iControlCommand;
  int iControlValue;
  
  //Calculate command and value
  iControlCommand = iControlCode / 1000;
  iControlValue = iControlCode % 1000;

  //Report command and value
  Serial.print("control code: ");
  Serial.println(iControlCode);
  
  Serial.print("control command: ");
  Serial.println(iControlCommand);
  
  Serial.print("control value: ");
  Serial.println(iControlValue);

  //Misc command category
  if (iControlCommand == 10)
  {
    if (iControlValue == 0)
    {
      gVerboseDiagMode = true;
      digitalWrite(O_BLUE_LED_PIN, HIGH);
      Serial.println("diagnostics mode started");
    }
    else if (iControlValue == 1)
    {
      gVerboseDiagMode = false;
      digitalWrite(O_BLUE_LED_PIN, LOW);
      Serial.println("diagnostics mode stopped");
    }
    else if (iControlValue == 2)
    {
      reportProductInfo();
    }
    else if (iControlValue == 3)
    {
      //Red LED on
      digitalWrite(O_RED_LED_PIN, HIGH);
      Serial.println("red led on");
    }
    else if (iControlValue == 4)
    {
      //Red LED off
      digitalWrite(O_RED_LED_PIN, LOW);
      Serial.println("red led off");
    }
    else if (iControlValue == 5)
    {
      //Green LED on
      digitalWrite(O_GREEN_LED_PIN, HIGH);
      Serial.println("green led on");
    }
    else if (iControlValue == 6)
    {
      //Green LED off
      digitalWrite(O_GREEN_LED_PIN, LOW);
      Serial.println("green led off");
    }
    else if (iControlValue == 7)
    {
      //Blue LED on
      digitalWrite(O_BLUE_LED_PIN, HIGH);
      Serial.println("blue led on");
    }
    else if (iControlValue == 8)
    {
      //Blue LED off
      digitalWrite(O_BLUE_LED_PIN, LOW);
      Serial.println("blue led off");
    }
    else if (iControlValue == 9)
    {
      //Display LDR1 value
      Serial.print("LDR1 value: ");
      Serial.println(gLDR1);
    }
    else if (iControlValue == 10)
    {
      //Display LDR2 value
      Serial.print("LDR2 value: ");
      Serial.println(gLDR2);
    }
    else if (iControlValue == 11)
    {
      //Display LDR3 value
      Serial.print("LDR3 value: ");
      Serial.println(gLDR3);
    }
    else if (iControlValue == 12)
    {
      //Display LDR4 value
      Serial.print("LDR4 value: ");
      Serial.println(gLDR4);
    }
    else if (iControlValue == 13)
    {
      //Turn on tracking mode
      enableTracking();
    }
    else if (iControlValue == 14)
    {
      //Turn off tracking mode
      disableTracking();
    }
    else if (iControlValue == 19)
    {
      if (gRunning == true && gServoTwistMoveIP == false && gServoTiltMoveIP == false)
      {
        gServoTwistPositionCommand = 90;
        gServoTiltPositionCommand = 90;
        Serial.println("twist and tilt servos commanded to 90 degrees");
      }
    }
    else if (iControlValue == 21)
    {
      if (gRunning == true)
      {
      gTrackToLightEnabled = true;
      Serial.println("Track to light source enabled");
      }
    }
    else if (iControlValue == 22)
    {
      gTrackToLightEnabled = false;
      Serial.println("Track to light source disabled");
    }
    else
    {
      Serial.print("invalid control value: ");
      Serial.println(iControlValue);
    }
  }

  //Servo1 (twist) command category
  if (iControlCommand == 11)
  {
    if (iControlValue >=0 && iControlValue <= 179)
    {
      //Move servo1 to position
      if (gRunning == true && gServoTwistMoveIP == false)
      {
        gServoTwistPositionCommand = iControlValue;
        gServoTwistMoveIP = true;

        Serial.print("Move twist servo command: ");
        Serial.println(gServoTwistPositionCommand);
      }
    }
    else
    {
      Serial.print("invalid control value: ");
      Serial.println(iControlValue);
    }
  }

  //Servo2 (tilt) command category
  if (iControlCommand == 12)
  {
    if (iControlValue >=0 && iControlValue <= 179)
    {
      //Move servo2 to position
      //Move servo1 to position
      if (gRunning == true && gServoTiltMoveIP == false)
      {
        gServoTiltPositionCommand = iControlValue;
        gServoTiltMoveIP = true;
        
        Serial.print("Move tilt servo command: ");
        Serial.println(gServoTiltPositionCommand);
      }
      else
      {
        Serial.print("tilt servo not enabled or move in progress, ");
        Serial.print(gRunning);
        Serial.print(", ");
        Serial.println(gServoTiltMoveIP);
      }
    }
    else
    {
      Serial.print("invalid control value: ");
      Serial.println(iControlValue);
    }
  }

  //End of request string
  Serial.println("-----");
}


//Run a sequence of steps to self-test functions, enable servos and enter light tracking mode
void startup()
{
  int iDelay;

  //Initialize
  iDelay = 500;

  //Display app info
  reportProductInfo();
  delay(iDelay);

  //Turn on red LED
  processSerialMessage(10003);
  delay(iDelay);

  //Turn off red LED, turn on green LED
  processSerialMessage(10004);
  processSerialMessage(10005);
  delay(iDelay);

  //Turn off green LED, turn on blue LED
  processSerialMessage(10006);
  processSerialMessage(10007);
  delay(iDelay);

  //Turn off blue LED, display photo-resistor values (all four)
  processSerialMessage(10008);
  processSerialMessage(10009);
  processSerialMessage(10010);
  processSerialMessage(10011);
  processSerialMessage(10012);
  delay(iDelay);

  //Enable servos
  enableTracking();
  delay(iDelay);

  //Move servos to home position
  processSerialMessage(10019);
  delay(iDelay);

  //Disable servos
  disableTracking();

  //Say good-bye
  Serial.println("startup sequence completed");

}


//Send product information to the serial port
void reportProductInfo()
{
  //Report product and other information to serial port
  Serial.println("q version 2");
  Serial.println("tucson, arizona usa");
  Serial.println("may 2018");
  Serial.print("checksum ");
  Serial.println("58BA-D969-2F82-08FD-2078-2777-396D-E1AA");
}

Custom parts and enclosures

Q2 assembly details
q2_assembly_details_RCbi893qzA.zip

Schematics

Fritzing schematic diagram
q2_schematic_TVp7kakwNa.fzz

Comments

Similar projects you might like

Dual Axis Solar Tracker Panel with Auto and Manual Mode

Project tutorial by Giannis Arvanitakis

  • 7,142 views
  • 6 comments
  • 37 respects

Solar Panel Sun Tracker - Phone Charger

Project tutorial by FIELDING

  • 27,297 views
  • 11 comments
  • 118 respects

Integrated Solar ChargeController, Inverter, PowerBank, Lamp

Project tutorial by Shahariar

  • 7,733 views
  • 16 comments
  • 30 respects

Servo Control Panel

Project tutorial by dancili

  • 1,425 views
  • 2 comments
  • 6 respects

Light Intensity and Solar Panel Energy Detector

Project tutorial by Kutluhan Aktar

  • 1,963 views
  • 3 comments
  • 13 respects

Sun-Tracking Mechanism for 40W solar panel

Project showcase by LIMPINGLIM

  • 2,039 views
  • 1 comment
  • 23 respects
Add projectSign up / Login