Help Needed with Azimuth and Elevation Code for Servo Motor Project

Hello Arduino community,

I am currently working on a project using a servo motor and 4 LDRs. However, I am stuck on how to incorporate azimuth and elevation into my code. I am hoping someone can assist me in finding a solution to my problem.

My project involves using the servo motor to track the position of a light source. I have 4 LDRs set up, each positioned at a 90-degree angle from one another. The LDRs are used to detect the intensity of light coming from the source, and the servo motor moves to the corresponding position to keep the light source in the center of the LDRs.

The issue I am facing is that I do not know how to incorporate azimuth and elevation into my code. I have read through various resources, but I am still struggling to understand how to use these values in my project.

If anyone has experience working with azimuth and elevation in Arduino projects or knows of any helpful resources, I would greatly appreciate your input. Additionally, if you have any suggestions for code implementation, that would be very helpful.

Thank you in advance for your help!

#include <Servo.h>
 //defining servo
Servo servohori;
int servoh=0;
int servohLimitHigh=160; //avg position max
int servohLimitLow=20; //avg position min

Servo servoverti;
int servov=0;
int servovLimitHigh=160; //avg position max
int servovLimitLow=20; //avg position min

//Assigning LDRs

int ldrtopl=2;//top left LDR green
int ldrtopr=1;//top right LDR red
int ldrbotl=3;//bottom left LDR yellow
int ldrbotr=0;//bottom right LDR orange

void setup()
{
  servohori.attach(10);
  servohori.write(0);
  servoverti.attach(9);
  servoverti.write(0);
  delay(500);
}

void loop()
{
  servoh=servohori.read();
  servov=servoverti.read();
  //capturing analog values of each LDR
  int topl=analogRead(ldrtopl);
  int topr=analogRead(ldrtopr);
  int botl=analogRead(ldrbotl);
  int botr=analogRead(ldrbotr);
  //calculating average
  int avgtop=(topl+topr)/2;
  int avgbot=(botl+botr)/2;
  int avgleft=(topl+botl)/2;
  int avgright=(topr+botr)/2;

  if(avgtop<avgbot)
  {
    servoverti.write(servov +1);
    if(servov>servovLimitHigh)
    {
      servov=servovLimitHigh;
    }
    delay(10);
  }

   else if(avgbot<avgtop)
  {
    servoverti.write(servov -1);
    if(servov<servovLimitLow)
    {
      servov=servovLimitLow;
    }
    delay(10);
  }

  else
  {
    servoverti.write(servov);
  }

   if(avgleft>avgright)
  {
    servohori.write(servoh +1);
    if(servoh>servohLimitHigh)
    {
      servoh=servohLimitHigh;
    }
    delay(10);
  }

  else if(avgright>avgleft)
  {
    servohori.write(servoh -1);
    if(servoh<servohLimitLow)
    {
      servoh=servohLimitLow;
    }
    delay(10);
  }

  else
  {
    servohori.write(servoh);
  }
  delay(50);
}

Here is my test code I used to develop a solar tracking system.

/*
   Project, use solar cells to generate power
   2/2/2020

*/
// https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/esp_timer.html
///esp_timer_get_time(void) //gets time in uSeconds like Arduino Micros. see above link
//////// http://www.iotsharing.com/2017/09/how-to-use-arduino-esp32-can-interface.html
#include "sdkconfig.h"
#include "esp_system.h" //This inclusion configures the peripherals in the ESP system.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "freertos/event_groups.h"
// #include "esp32/ulp.h"
// #include "driver/rtc_io.h"
#include <SimpleKalmanFilter.h>
#include <driver/adc.h>
#include "esp_sleep.h"
#include "driver/mcpwm.h"
const int TaskCore1 = 1;
const int TaskCore0 = 0;
const int TaskStack20K = 20000;
const int Priority3 = 3;
const int Priority4 = 4;
const int SerialDataBits = 115200;
volatile bool EnableTracking = true;
////
//
////
void setup()
{
  Serial.begin( 115200 );
  // https://dl.espressif.com/doc/esp-idf/latest/api-reference/peripherals/adc.html
  // set up A:D channels
  log_i( "Setup A:D" );
  adc_power_on( );
  vTaskDelay( 1 );
  adc1_config_width(ADC_WIDTH_12Bit);
  // ADC1 channel 0 is GPIO36 (ESP32), GPIO1 (ESP32-S2)
  adc1_config_channel_atten(ADC1_CHANNEL_0 , ADC_ATTEN_DB_11); // azimuth 0
  //  // ADC1_CHANNEL_3  ADC1 channel 3 is GPIO39 (ESP32)
  adc1_config_channel_atten(ADC1_CHANNEL_3, ADC_ATTEN_DB_11); // azimuth 1
  //  // ADC1 channel 5 is GPIO33
  adc1_config_channel_atten(ADC1_CHANNEL_5, ADC_ATTEN_DB_11); // altitude 1
  //  // ADC1 channel 6 is GPIO34
  adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11); // altitude 0
  //  // adc for light dark detection, ADC1 channel 7 is GPIO35 (ESP32)
  adc1_config_channel_atten(ADC1_CHANNEL_7, ADC_ATTEN_DB_11);
  // adc for solar cell voltage detection, ADC1 channel 4 is GPIO33 (ESP32)
  adc1_config_channel_atten(ADC1_CHANNEL_4, ADC_ATTEN_DB_11);
  //
  log_i( "A:D setup complete" );
  // control for relay to supply power to the servos
  pinMode( 5, OUTPUT );
  vTaskDelay(1);
  log_i( "setup MCPWM" );
  //
  mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM1A, GPIO_NUM_4 ); // Azimuth
  mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM0A, GPIO_NUM_12 ); // Altitude servo
  //
  mcpwm_config_t pwm_config = {};
  pwm_config.frequency = 50;    //frequency = 50Hz, i.e. for every servo motor time period should be 20ms
  pwm_config.cmpr_a = 0;    //duty cycle of PWMxA = 0
  pwm_config.cmpr_b = 0;    //duty cycle of PWMxb = 0
  pwm_config.counter_mode = MCPWM_UP_COUNTER;
  pwm_config.duty_mode = MCPWM_DUTY_MODE_0;
  log_i( "MCPWM complete" );
  //
  //
  mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_0, &pwm_config);    //Configure PWM0A timer 0 with above settings
  mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_1, &pwm_config);    //Configure PWM0A timer 1 with above settings
  //  ////
  fMoveAltitude( 1525 );
  fMoveAzimuth( 1500 );
  ////
  xTaskCreatePinnedToCore( TrackSun, "TrackSun", TaskStack20K, NULL, Priority3, NULL, TaskCore1 ); // assigned to core
  xTaskCreatePinnedToCore( fDaylight, "fDaylight", TaskStack20K, NULL, Priority3, NULL, TaskCore0 ); // assigned to core
  ////  // esp_sleep_enable_timer_wakeup( (60000000 * 2) ); // set timer to wake up once a minute 60000000uS * 2

}
//
void fDaylight( void * pvParameters )
{
  int lightLevel = 0;
  log_i( "Start up fDaylight" );
  while (1)
  {
    // sufficent voltage at the solar cells to do the thing?
    log_i( "Solar Cell: %d", adc1_get_raw(ADC1_CHANNEL_4) );
    if (adc1_get_raw(ADC1_CHANNEL_4) > 1600 )
    {
      lightLevel = adc1_get_raw(ADC1_CHANNEL_7);
      if ( adc1_get_raw(ADC1_CHANNEL_7) >= 300 )
      {
        log_i( "Light Level: %d", lightLevel );
        EnableTracking = true;
        digitalWrite( 5, LOW ); // power to relay module/servos
        vTaskDelay( 1 );
      } else {
        // if light level to low park boom
        fMoveAltitude( 1475 );
        fMoveAzimuth( 1900 );
        //     put esp32 into deep sleep
        digitalWrite( 5, HIGH ); // deenergize servo motors
        EnableTracking = false;
        esp_sleep_enable_timer_wakeup( (60000000 * 5) ); // set timer to wake up once a minute 60000000uS
        esp_deep_sleep_start();
      }
    } else {
              digitalWrite( 5, HIGH ); // deenergize servo motors
        EnableTracking = false;
        esp_sleep_enable_timer_wakeup( (60000000 * 5) ); // set timer to wake up once a minute 60000000uS
        esp_deep_sleep_start();
    }
    vTaskDelay( 1000 );
  } // while(1)
} // void fDaylight( void * pvParameters )
////
/**
   @brief Use this function to calcute pulse width for per degree rotation
   @param  degree_of_rotation the angle in degree to which servo has to rotate
   @return
       - calculated pulse width
*/
//static uint32_t servo_per_degree_init(uint32_t degree_of_rotation)
//{
//  const int SERVO_MIN_PULSEWIDTH = 500; //Minimum pulse width in microsecond
//const int SERVO_MAX_PULSEWIDTH 2500 //Maximum pulse width in microsecond
//const int SERVO_MAX_DEGREE 180 //Maximum angle in degree upto which servo can rotate
//  uint32_t cal_pulsewidth = 0;
//  cal_pulsewidth = (SERVO_MIN_PULSEWIDTH + (((SERVO_MAX_PULSEWIDTH - SERVO_MIN_PULSEWIDTH) * (degree_of_rotation)) / (SERVO_MAX_DEGREE)));
//  return cal_pulsewidth;
//}
////
void mcpwm_gpio_initialize(void)
{
  mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM1A, GPIO_NUM_4 );
  mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM0A, GPIO_NUM_12 );
}
////
// void TrackSun( int Altitude, int Azimuth )
void TrackSun( void * pvParameters )
{
  log_i( "Startup TrackSun" );
  int64_t EndTime = esp_timer_get_time();
  int64_t StartTime = esp_timer_get_time(); //gets time in uSeconds like Arduino Micros
  int Altitude = 1500;
  int Azimuth = 900;
  int maxAltitudeRange = 2144;
  int minAltitudeRange = 900;
  int maxAzimuthRange = 2144;
  int minAzimuthRange = 900;
  float AltitudeThreashold = 10.0f;
  float AzimuthThreashold = 10.0f;
  float kalmanThreshold = 80.0f;
  SimpleKalmanFilter kfAltitude0( AltitudeThreshold, kalmanThreshold, .01 ); // kalman filter Altitude 0
  SimpleKalmanFilter kfAltitude1( AltitudeThreshold, kalmanThreshold, .01 ); // kalman filter Altitude 1
  SimpleKalmanFilter kfAzimuth0( AzimuthThreshold, kalmanThreshold, .01 ); // kalman filter Azimuth 0
  SimpleKalmanFilter kfAzimuth1( AzimuthThreshold, kalmanThreshold, .01 ); // kalman filter Azimuth 1
  float filteredAltitude_0 = 0.0f;
  float filteredAltitude_1 = 0.0f;
  float filteredAzimuth_0 = 0.0f;
  float filteredAzimuth_1 = 0.0f;
  uint64_t AzimuthEndTime = esp_timer_get_time();
  uint64_t AzimuthStartTime = esp_timer_get_time(); //gets time in uSeconds like Arduino Micros,but rolls over after 207 years
  uint64_t AltitudeEndTime = esp_timer_get_time();
  uint64_t AltitudeStartTime = esp_timer_get_time(); //gets time in uSeconds like Arduino Micros,
  int StartupCount = 0;
  int TorqueAmmount = 5;
  while (1)
  {

    if ( EnableTracking )
    {
      //Altitude
      AltitudeEndTime = esp_timer_get_time() - AltitudeStartTime; // produce elapsed time for the simpleKalmanFilter
      kfAltitude0.setProcessNoise( float(AltitudeEndTime) / 1000000.0f );
      kfAltitude1.setProcessNoise( float(AltitudeEndTime) / 1000000.0f );
      // Serial.println( AltitudeEndTime,6 );
      filteredAltitude_0 = kfAltitude0.updateEstimate( (float)adc1_get_raw(ADC1_CHANNEL_6) );
      filteredAltitude_1 = kfAltitude1.updateEstimate( (float)adc1_get_raw(ADC1_CHANNEL_5) );
      //      Serial.print( filteredAltitude_0 );
      //      Serial.print( ", " );
      //      Serial.print( filteredAltitude_1 );
      //      Serial.print( ", " );
      //      Serial.println();
      if ( (filteredAltitude_0 > filteredAltitude_1) && (abs(filteredAltitude_0 - filteredAltitude_1) > AltitudeThreshold))
      {
        Altitude -= TorqueAmmount;
        // log_i( "> Alt %d", Altitude );
        if ( Altitude < minAltitudeRange )
        {
          Altitude = 900;
        }
        fMoveAltitude( Altitude );
        AltitudeStartTime = esp_timer_get_time();
      }
      if ( (filteredAltitude_0 < filteredAltitude_1) && (abs(filteredAltitude_0 - filteredAltitude_1) > AltitudeThreshold) )
      {
        Altitude += TorqueAmmount;
        if ( Altitude >= maxAltitudeRange )
        {
          Altitude = 900;
        }
        fMoveAltitude( Altitude );
        // log_i( "< Alt %d", Altitude );
        AltitudeStartTime = esp_timer_get_time();
      }
      //// AZIMUTH
      AzimuthEndTime = esp_timer_get_time() - AzimuthStartTime; // produce elasped time for the simpleKalmanFilter
      //      Serial.print( (float)adc1_get_raw(ADC1_CHANNEL_3) );
      //      Serial.print( "," );
      //      Serial.print( (float)adc1_get_raw(ADC1_CHANNEL_0) );
      //      Serial.print( "," );
      kfAzimuth0.setProcessNoise( float(AzimuthEndTime) / 1000000.0f ); //convert time of process to uS, update SimpleKalmanFilter q
      kfAzimuth1.setProcessNoise( float(AzimuthEndTime) / 1000000.0f ); //convert time of process to uS, update SimpleKalmanFilter q
      filteredAzimuth_0 = kfAzimuth0.updateEstimate( (float)adc1_get_raw(ADC1_CHANNEL_3) );
      filteredAzimuth_1 = kfAzimuth1.updateEstimate( (float)adc1_get_raw(ADC1_CHANNEL_0) );
      //      Serial.print( filteredAzimuth_0 );
      //      Serial.print( ", " );
      //      Serial.print( filteredAzimuth_1 );
      //      Serial.println();
      if ( (filteredAzimuth_0 > filteredAzimuth_1) && (abs(filteredAzimuth_0 - filteredAzimuth_1)) > AzimuthThreashold )
      {
        Azimuth += TorqueAmmount;
        if ( Azimuth >= maxAzimuthRange )
        {
          Azimuth = 900;
        }
        // log_i( "> Az %d", Azimuth );
        fMoveAzimuth( Azimuth );
        AzimuthStartTime = esp_timer_get_time();
      }
      //    //
      if ( (filteredAzimuth_0 < filteredAzimuth_1) && (abs(filteredAzimuth_1 - filteredAzimuth_0)) > AzimuthThreashold )
      {
        // log_i( "< Az %d", Azimuth );
        Azimuth -= TorqueAmmount; // make new position to torque to
        if ( (Azimuth >= maxAzimuthRange) || (Azimuth <= minAzimuthRange) )
        {
          Azimuth = 900;
        }
        fMoveAzimuth( Azimuth );
        AzimuthStartTime = esp_timer_get_time();
      } //azmiuth end
      if ( StartupCount < 500 )
      {
        ++StartupCount;
      } else {
        TorqueAmmount = 1;
        // esp_sleep_enable_timer_wakeup( (60000000 / 30) ); // set timer to wake up
        // esp_sleep_enable_timer_wakeup( (60000000 / 20) ); // set timer to wake up
        // esp_light_sleep_start();
      }
    }
     vTaskDelay( 65 );
  } // while(1)
} //void TrackSun()
////
void fMoveAltitude( int MoveTo )
{
  mcpwm_set_duty_in_us(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, MoveTo);
  vTaskDelay( 12 );
}
//////
void fMoveAzimuth( int MoveTo )
{
  mcpwm_set_duty_in_us(MCPWM_UNIT_0, MCPWM_TIMER_1, MCPWM_OPR_A, MoveTo);
  vTaskDelay( 12 );
}
////
void loop() {}
////

You'll have to translate it to work with a non-ESP32 MCU but the basic principle is there. Look at the task TrackSun().

I used photodiodes set at each end and slightly below the surface of the solar array. I found photodiodes work way better than LDR's.

In the end I found that using a solar tracking library that calculates the suns location given a position and torquing the solar cells once every 6 minutes to a new position was more energy saving then a continuous tracker. I only apply power to the motors when they need torquing instead of the motors being a continuous power draw.

Just rename the "h"/"hori" variables "azimuth" and the "v"/"verti" variables "elevation".

Use a local clock with a calendar to get azimuth and elevation angles.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.