Brian-s-Bluestamp-Engineering-Portfolio

Brian's Self Defined Project

View project on GitHub

Dual Axis Solar Tracker

I built a device that would follow the sun.

A stationary solar panel only points directly at the sun for a moment each day, which means it spends most of its time collecting light at an angle instead of head-on. To close that gap, I built a dual-axis solar tracker: a device that dynamically adjusts the solar panel so its surface stays perpendicular to the sun as it moves across the sky, absorbing more sunlight— and therefore more electricity — out of the same panel.

| Brian C | Gunn High | Electrical Engineering | Incoming Junior |

Brian C


First Milestone

In order to get the Servo Motors to rotate the solar panel to the direction of the sun, I needed 3 core parts. Light dependent resistors (LDRs), Servo Motors, and the algorithm to run it. The LDRs receive electricity from the Arduino, with the resistance against this flow decreasing with greater brightness. As this power flows back to ground, a signal wire read this quantity, and takes it to the Arduino’s analog pin to be read. These values are then outputted to the IDE’s serial monitor, where if-then-else statements in C++ tell the Servo Motors to rotate to the direction of greater light.

int POS = 90;

void setup() {
  Serial.begin(9600);
  panServo.attach(6);
  panServo.write(POS);
  delay(300);
}

void loop() {
  Serial.print(analogRead(A1)); Serial.print(" "); Serial.print(analogRead(A0));
  int Diff = analogRead(A1) - analogRead(A0);
  if (Diff > 50) {
    POS += 3;
  } else if (Diff < -50) {
    POS -= 3;
  }
  panServo.write(POS);
}

You can’t actually command a Servo Motor to move 3 degrees clockwise, or 15 degrees counterclockwise, because It doesn’t remember its current position after you stop powering the Motor! You can only command it to move to a certain degree. Because of these limitations I made it so that the Servo Motor would start at the fixed position of 90 degrees (so that it can go in both directions), and assigned a variable to be always equal to the Servo Position (POS). Everytime the algorithm is repeated, POS changes ±3, thereby rotating the servo Motor.

Screenshot 2026-07-30 at 12 01 06 PM

Second Milestone

For my second milestone, I designed the solar panel mount in Onshape, a CAD program.

Screenshot 2026-07-31 at 9 19 01 AM

By extruding and lofting 2D sketches, I created a hollowed-out frustum to serve as the base, a cylinder with two rectangular beams to be rotated by a servo motor, and a U shape. On top of the base is a hole for the servo motor to snap on. The horn of the Servo is attached to the cylindric base via 2 miniature bolts. A hole is also placed on the rectangular beam, where a servo motor snaps on. The motor tilts the U shape, and therefore, the solar panel up and down. The other beam provides structural support via a bolt and nut.

Final Milestone

I installed the photoresistors to complete the solar tracker. Soldering all these parts onto a small perfboard came with plenty of difficulties– I created a ton of shorts, which required a lot of desoldering.

Each LDR’s resistance drops as more light hits it, so shining light unevenly across the four sensors produces four different voltage readings on the Arduino Analog pins. If the top sensors are reading high volts than the bottom (or the left brighter than the right) by a small deadband, the Arduino nudges the corresponding servo a few degrees in that direction. Running this comparison in a continuous loop lets the mount creep toward the sun in small steps throughout the day, on both the vertical (tilt) and horizontal (pan) axes. The deadband keeps the servos from jittering back and forth chasing tiny, insignificant differences between sensors.

Schematics

Screenshot 2026-07-31 at 10 00 24 AM


Code

#include <Servo.h>




#include <OneWire.h>




#include <DallasTemperature.h>







// --- SERVO DEFINITIONS ---




Servo panServo;




Servo tiltServo;







// --- PIN DEFINITIONS ---




const int LDR_TOP_LEFT     = A1;




const int LDR_BOTTOM_LEFT  = A0;




const int LDR_TOP_RIGHT    = A5;




const int LDR_BOTTOM_RIGHT = A4;




const int TEMP_SENSOR_PIN  = 8;  // DS18B20 data pin (confirmed on D8)




const int RELAY_PIN        = 7;  // Signal pin to relay module's IN terminal







// --- DS18B20 SETUP ---




OneWire oneWire(TEMP_SENSOR_PIN);




DallasTemperature tempSensor(&oneWire);







// --- SAFE BOUNDARIES ---




const int TILT_MIN = 10;




const int TILT_MAX = 150;




const int PAN_MIN  = 0;




const int PAN_MAX  = 177;







// --- TUNING & SPEED SETTINGS ---




int tiltAngle = 90;




int panAngle  = 90;







const int STEP_SIZE  = 3;




const int STEP_DELAY = 30;




const int DEADBAND   = 20;







// --- CALIBRATION MULTIPLICATIVE SCALARS ---




const float SCALE_TL = 1.00;




const float SCALE_BL = 1.00;




const float SCALE_TR = 1.00;




const float SCALE_BR = 1.00;







// --- TEMPERATURE / RELAY (PUMP) SETTINGS ---




const float PUMP_ON_TEMP_C  = 20.0;  // Turn pump ON at/above this temp




const float PUMP_OFF_TEMP_C = 15.0;  // Turn pump OFF at/below this temp (hysteresis)




bool pumpActive = false;







// --- NON-BLOCKING TEMP READ STATE MACHINE ---




bool conversionInProgress = false;




unsigned long conversionStartTime = 0;




const unsigned long CONVERSION_WAIT_MS = 100;




const unsigned long TEMP_REQUEST_INTERVAL = 1000;




unsigned long lastRequestTime = 0;




float lastTempC = -127.0;







void setup() {




Serial.begin(9600);







panServo.write(panAngle);




panServo.attach(6);




delay(300);







tiltServo.write(tiltAngle);




tiltServo.attach(5);




delay(300);







pinMode(RELAY_PIN, OUTPUT);




digitalWrite(RELAY_PIN, LOW);  // Pump off at startup







tempSensor.begin();




tempSensor.setResolution(9);




tempSensor.setWaitForConversion(false);




}







void updatePumpRelay(float tempC) {




if (!pumpActive && tempC >= PUMP_ON_TEMP_C) {




    pumpActive = true;




  } else if (pumpActive && tempC <= PUMP_OFF_TEMP_C) {




    pumpActive = false;




  }




digitalWrite(RELAY_PIN, pumpActive ? HIGH : LOW);




}







void loop() {




int rawTL = analogRead(LDR_TOP_LEFT);




int rawBL = analogRead(LDR_BOTTOM_LEFT);




int rawTR = analogRead(LDR_TOP_RIGHT);




int rawBR = analogRead(LDR_BOTTOM_RIGHT);







int topLeft     = rawTL * SCALE_TL;




int bottomLeft  = rawBL * SCALE_BL;




int topRight    = rawTR * SCALE_TR;




int bottomRight = rawBR * SCALE_BR;







int avgTop    = (topLeft + topRight) / 2;




int avgBottom = (bottomLeft + bottomRight) / 2;




int avgLeft   = (topLeft + bottomLeft) / 2;




int avgRight  = (topRight + bottomRight) / 2;







int vertDiff = avgTop - avgBottom;




if (vertDiff > DEADBAND) {




    tiltAngle += STEP_SIZE;




  } else if (vertDiff < -DEADBAND) {




    tiltAngle -= STEP_SIZE;




  }







int horizDiff = avgLeft - avgRight;




if (horizDiff > DEADBAND) {




    panAngle -= STEP_SIZE;




  } else if (horizDiff < -DEADBAND) {




    panAngle += STEP_SIZE;




  }







  tiltAngle = constrain(tiltAngle, TILT_MIN, TILT_MAX);




  panAngle  = constrain(panAngle, PAN_MIN, PAN_MAX);







tiltServo.write(tiltAngle);




panServo.write(panAngle);







unsigned long now = millis();







if (!conversionInProgress && (now - lastRequestTime >= TEMP_REQUEST_INTERVAL)) {




tempSensor.requestTemperatures();




    conversionInProgress = true;




    conversionStartTime = now;




    lastRequestTime = now;




  }







if (conversionInProgress && (now - conversionStartTime >= CONVERSION_WAIT_MS)) {




    lastTempC = tempSensor.getTempCByIndex(0);




    conversionInProgress = false;




updatePumpRelay(lastTempC);







Serial.print("ADJ -> TL:"); Serial.print(topLeft);




Serial.print(" BL:");       Serial.print(bottomLeft);




Serial.print(" TR:");       Serial.print(topRight);




Serial.print(" BR:");       Serial.print(bottomRight);




Serial.print(" | Tilt:");   Serial.print(tiltAngle);




Serial.print(" Pan:");      Serial.print(panAngle);




Serial.print(" | Temp:");   Serial.print(lastTempC);




Serial.print("C Pump:");    Serial.println(pumpActive ? "ON" : "OFF");




  }







delay(STEP_DELAY);




}

#include <Servo.h>



#include <OneWire.h>



#include <DallasTemperature.h>









// --- SERVO DEFINITIONS ---



Servo panServo;



Servo tiltServo;









// --- PIN DEFINITIONS ---



const int LDR_TOP_LEFT     = A1;



const int LDR_BOTTOM_LEFT  = A0;



const int LDR_TOP_RIGHT    = A5;



const int LDR_BOTTOM_RIGHT = A4;



const int TEMP_SENSOR_PIN  = 8;  // DS18B20 data pin (confirmed on D8)



const int RELAY_PIN        = 7;  // Signal pin to relay module's IN terminal









// --- DS18B20 SETUP ---



OneWire oneWire(TEMP_SENSOR_PIN);



DallasTemperature tempSensor(&oneWire);









// --- SAFE BOUNDARIES ---



const int TILT_MIN = 10;



const int TILT_MAX = 150;



const int PAN_MIN  = 0;



const int PAN_MAX  = 177;









// --- TUNING & SPEED SETTINGS ---



int tiltAngle = 90;



int panAngle  = 90;









const int STEP_SIZE  = 3;



const int STEP_DELAY = 30;



const int DEADBAND   = 20;









// --- CALIBRATION MULTIPLICATIVE SCALARS ---



const float SCALE_TL = 1.00;



const float SCALE_BL = 1.00;



const float SCALE_TR = 1.00;



const float SCALE_BR = 1.00;









// --- TEMPERATURE / RELAY (PUMP) SETTINGS ---



const float PUMP_ON_TEMP_C  = 20.0;  // Turn pump ON at/above this temp



const float PUMP_OFF_TEMP_C = 15.0;  // Turn pump OFF at/below this temp (hysteresis)



bool pumpActive = false;









// --- NON-BLOCKING TEMP READ STATE MACHINE ---



bool conversionInProgress = false;



unsigned long conversionStartTime = 0;



const unsigned long CONVERSION_WAIT_MS = 100;



const unsigned long TEMP_REQUEST_INTERVAL = 1000;



unsigned long lastRequestTime = 0;



float lastTempC = -127.0;









void setup() {



Serial.begin(9600);









panServo.write(panAngle);



panServo.attach(6);



delay(300);









tiltServo.write(tiltAngle);



tiltServo.attach(5);



delay(300);









pinMode(RELAY_PIN, OUTPUT);



digitalWrite(RELAY_PIN, LOW);  // Pump off at startup









tempSensor.begin();



tempSensor.setResolution(9);



tempSensor.setWaitForConversion(false);



}









void updatePumpRelay(float tempC) {



if (!pumpActive && tempC >= PUMP_ON_TEMP_C) {



    pumpActive = true;



  } else if (pumpActive && tempC <= PUMP_OFF_TEMP_C) {



    pumpActive = false;



  }



digitalWrite(RELAY_PIN, pumpActive ? HIGH : LOW);



}









void loop() {



int rawTL = analogRead(LDR_TOP_LEFT);



int rawBL = analogRead(LDR_BOTTOM_LEFT);



int rawTR = analogRead(LDR_TOP_RIGHT);



int rawBR = analogRead(LDR_BOTTOM_RIGHT);









int topLeft     = rawTL * SCALE_TL;



int bottomLeft  = rawBL * SCALE_BL;



int topRight    = rawTR * SCALE_TR;



int bottomRight = rawBR * SCALE_BR;









int avgTop    = (topLeft + topRight) / 2;



int avgBottom = (bottomLeft + bottomRight) / 2;



int avgLeft   = (topLeft + bottomLeft) / 2;



int avgRight  = (topRight + bottomRight) / 2;









int vertDiff = avgTop - avgBottom;



if (vertDiff > DEADBAND) {



    tiltAngle += STEP_SIZE;



  } else if (vertDiff < -DEADBAND) {



    tiltAngle -= STEP_SIZE;



  }









int horizDiff = avgLeft - avgRight;



if (horizDiff > DEADBAND) {



    panAngle -= STEP_SIZE;



  } else if (horizDiff < -DEADBAND) {



    panAngle += STEP_SIZE;



  }









  tiltAngle = constrain(tiltAngle, TILT_MIN, TILT_MAX);



  panAngle  = constrain(panAngle, PAN_MIN, PAN_MAX);









tiltServo.write(tiltAngle);



panServo.write(panAngle);









unsigned long now = millis();









if (!conversionInProgress && (now - lastRequestTime >= TEMP_REQUEST_INTERVAL)) {



tempSensor.requestTemperatures();



    conversionInProgress = true;



    conversionStartTime = now;



    lastRequestTime = now;



  }









if (conversionInProgress && (now - conversionStartTime >= CONVERSION_WAIT_MS)) {



    lastTempC = tempSensor.getTempCByIndex(0);



    conversionInProgress = false;



updatePumpRelay(lastTempC);









Serial.print("ADJ -> TL:"); Serial.print(topLeft);



Serial.print(" BL:");       Serial.print(bottomLeft);



Serial.print(" TR:");       Serial.print(topRight);



Serial.print(" BR:");       Serial.print(bottomRight);



Serial.print(" | Tilt:");   Serial.print(tiltAngle);



Serial.print(" Pan:");      Serial.print(panAngle);



Serial.print(" | Temp:");   Serial.print(lastTempC);



Serial.print("C Pump:");    Serial.println(pumpActive ? "ON" : "OFF");



  }









delay(STEP_DELAY);



}
 


Bill of Materials

Part Note Price Link
Solar Panel Generates electricity via the photoelectric effect $20.95 Link
Arduino A microcontroller that runs code uploaded from your computer $29.95 Link
2× Servo Motors Rotates the panel mount’s base and tilts the panel toward the sun $11.95 Link
4× Light Dependent Resistors Outputs a voltage determined by the amount of light absorbed $0.95 Link
10kΩ Resistors Limits current to protect the electronics $0.75 Link
N-Channel Power MOSFET (30V / 60A) Lets the Arduino switch the pump and fan $2.25 Link
Aluminum Water Cooling Block Absorbs heat from the panel; its microchannels let water pick up that heat $6.29 Link
Liquid Pump Circulates water through the cooling block and radiator loop $24.95 Link
Tubing Carries water through the pump → block → radiator → pump cycle $8.00 Link
Temperature Sensor Measures and reports temperature $9.95 Link
1N4001 Diode (10-pack) Protects the MOSFETs from voltage spikes when the pump/fan switch off $1.50 Link
Heat Sink Thermal Tape Improves heat transfer $4.00 Link
Radiator Cools the circulating water $58.32 Link
Radiator Fan Assists the radiator’s cooling $34.95 Link
G1/4 Thread, OD 4mm Nozzle Screws into the radiator’s threaded ports so tubing can attach $14.39 Link
8mm-to-4mm Adapter Bridges the water block’s 8mm port to the 4mm tubing $6.11 Link