// SolarPostion library
// https://github.com/KenWillmott/SolarPosition
#include "SolarPosition.h"
// offset beween local time and UTC time
const int8_t localUtcOffsetHours = -1;
// Brussels latitude, longitude: 50.85033960,4.35171030
// data type double is used to store a 64 bit floating point
const double latitude = 50.85033960; // Observer's latitude
const double longitude = 4.35171030; // Observer's longitude
// create an instance of SolarPosition named 'myPosition'
SolarPosition myPosition(latitude, longitude);
// AccelStepper library
// ********************
// http://www.airspayce.com/mikem/arduino/AccelStepper/
#include <AccelStepper.h>
const uint8_t azimuthStepPin = 6;
const uint8_t azimuthDirPin = 5;
const uint8_t elevationStepPin = 4;
const uint8_t elevationDirPin = 3;
const uint32_t stepsPerRevolution = 3200; // 16x microstepping
// default: the stepper motor moves 1.8 degrees per step (200 steps per revolution)
// gearRatio":"16:1" in diagram.json
// => 1.8/16 = 0.1125 degrees per step
// => 200*16 = 3200 steps per revolution
// 16:1 microstepping with the A4988 is not supported in Wokwi
// https://docs.wokwi.com/parts/wokwi-stepper-motor#simulation-behavior
const uint8_t motorInterfaceType = 1; // Define motor interface type
// create two instances of AccelStepper named azimuthStepper en elevationStepper
AccelStepper azimuthStepper(motorInterfaceType, azimuthStepPin, azimuthDirPin);
AccelStepper elevationStepper(motorInterfaceType, elevationStepPin, elevationDirPin);
/*
#define HALFSTEP 8
#define motorPin1 8 // IN1 on ULN2003 ==> Blue on 28BYJ-48
#define motorPin2 9 // IN2 on ULN2004 ==> Pink on 28BYJ-48
#define motorPin3 10 // IN3 on ULN2003 ==> Yellow on 28BYJ-48
#define motorPin4 11 // IN4 on ULN2003 ==> Orange on 28BYJ-48
int endPoint = 1024; // Move this many steps - 1024 = approx 1/4 turn
// NOTE: The sequence 1-3-2-4 is required for proper sequencing of 28BYJ-48
AccelStepper azimuthStepper(HALFSTEP, motorPin1, motorPin3, motorPin2, motorPin4);
*/
//RTC
//***
// https://www.arduino.cc/reference/en/libraries/rtclib/
#include <RTClib.h>
// create an instance of the RTC
RTC_DS1307 rtc;
// LCD library
//************
// https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library
#include <LiquidCrystal_I2C.h>
const uint8_t I2C_ADDR = 0x27; // IC2 address
const uint8_t LCD_COLUMNS = 20;
const uint8_t LCD_LINES = 4;
// create an instance of LiquidCrystal_I2C named lcd
LiquidCrystal_I2C lcd(I2C_ADDR, LCD_COLUMNS, LCD_LINES);
// Push buttons
//*************
const uint8_t PB_MENU = 8;
const uint8_t PB_INCR = 10;
const uint8_t PB_DECR = 9;
// function prototypes
void dateTimeLcd(const DateTime &lTime, uint8_t blinkPart);
void elevationAzimuthLcd(int16_t elevation, int16_t azimuth,uint8_t blinkPart);
void goLcd(uint8_t blinkPart);
void setup()
{
// set pin mode for push buttons (active low)
pinMode(PB_MENU, INPUT_PULLUP);
pinMode(PB_INCR, INPUT_PULLUP);
pinMode(PB_DECR, INPUT_PULLUP);
// Serial port for debugging
Serial.begin(9600);
// Serial.println("Solar Position");
// is RTC present?
if (! rtc.begin()) {
Serial.println("Couldn't find RTC");
Serial.flush();
abort();
}
// is RTC running?
if (! rtc.isrunning()) {
Serial.println("RTC is NOT running, let's set the time!");
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minutes;
uint8_t seconds;
Serial.print("Year: ");
while (!Serial.available());
year = Serial.parseInt();
if (Serial.read() == '\n') {
Serial.println(year);
}
Serial.println("Month: ");
while (!Serial.available());
month = Serial.parseInt();
if (Serial.read() == '\n') {
Serial.println(month);
}
Serial.println("Day: ");
while (!Serial.available());
day = Serial.parseInt();
if (Serial.read() == '\n') {
Serial.println(day);
}
Serial.println("Hour: ");
while (!Serial.available());
hour = Serial.parseInt();
if (Serial.read() == '\n') {
Serial.println(hour);
}
Serial.println("Minutes: ");
while (!Serial.available());
minutes = Serial.parseInt();
if (Serial.read() == '\n') {
Serial.println(minutes);
}
Serial.println("Seconds: ");
while (!Serial.available());
seconds = Serial.parseInt();
if (Serial.read() == '\n') {
Serial.println(seconds);
}
rtc.adjust(DateTime(year, month, day, hour, minutes, seconds));
}
// set stepper parameters
azimuthStepper.setMaxSpeed(200.0);
azimuthStepper.setAcceleration(200.0);
azimuthStepper.moveTo(0);
azimuthStepper.run();
elevationStepper.setMaxSpeed(200.0);
elevationStepper.setAcceleration(200.0);
elevationStepper.moveTo(0);
elevationStepper.run();
// initialise LCD
lcd.init();
lcd.backlight();
// Finit state machine for set up menu
static uint8_t menu_state = 11;
static uint8_t started = 0;
uint8_t button_menu;
int8_t setHour, setMinute, setSecond;
DateTime localTime;
while(!started) {
button_menu = digitalRead(PB_MENU);
switch(menu_state) {
case 1: // wait for button release
if (button_menu) { // menu button released
// display elevation and azimuth on LCD
elevationAzimuthLcd(0,0,0);
menu_state = 2;
delay(200);
}
break;
case 2: // push incr & decr to adjust hour
// get local time from RTC
localTime = rtc.now();
// display local time and date on LCD with flashing hour
dateTimeLcd(localTime,1);
goLcd(0);
setHour = localTime.hour();
if (!digitalRead(PB_INCR)) { // INCR button pressed
// increase hour
setHour = setHour + 1;
if (setHour >= 24) setHour = 0;
rtc.adjust(DateTime(localTime.year(), localTime.month(), localTime.day(), setHour, localTime.minute(), localTime.second()));
while (!digitalRead(PB_INCR));
}
if (!digitalRead(PB_DECR)) { // DECR button pressed
// decrease hour
setHour = setHour - 1;
if (setHour < 0) setHour = 23;
rtc.adjust(DateTime(localTime.year(), localTime.month(), localTime.day(), setHour, localTime.minute(), localTime.second()));
while (!digitalRead(PB_DECR));
}
if (!button_menu) { // menu button pressed
menu_state = 3;
delay(200);
}
break;
case 3: // wait for button release
if (button_menu) { // menu button released
lcd.setCursor(0,1);
// display elevation and azimuth on LCD
elevationAzimuthLcd(0,0,0);
menu_state = 4;
delay(200);
}
break;
case 4: // push incr & decr to adjust minutes
// get local time from RTC
localTime = rtc.now();
// display local time and date on LCD with flashing minutes
dateTimeLcd(localTime,2);
goLcd(0);
// display elevation and azimuth on LCD
elevationAzimuthLcd(0,0,0);
setMinute = localTime.minute();
if (!digitalRead(PB_INCR)) { // INCR button pressed
// increase minutes
setMinute = setMinute + 1;
if (setMinute >= 60) setMinute = 0;
rtc.adjust(DateTime(localTime.year(), localTime.month(), localTime.day(), localTime.hour(), setMinute, localTime.second()));
while (!digitalRead(PB_INCR));
}
if (!digitalRead(PB_DECR)) { // DECR button pressed
// decrease minutes
setMinute = setMinute - 1;
if (setMinute < 0) setMinute = 59;
rtc.adjust(DateTime(localTime.year(), localTime.month(), localTime.day(), localTime.hour(), setMinute, localTime.second()));
while (!digitalRead(PB_DECR));
}
if (!button_menu) { // menu button pressed
menu_state = 5;
delay(200);
}
break;
case 5: // wait for button release
if (button_menu) { // menu button released
menu_state = 6;
delay(200);
}
break;
case 6: // push incr & decr to adjust seconds
// get local time from RTC
localTime = rtc.now();
// display local time and date on LCD with flashing seconds
dateTimeLcd(localTime,3);
goLcd(0);
// display elevation and azimuth on LCD
elevationAzimuthLcd(0,0,0);
setSecond = localTime.second();
if (!digitalRead(PB_INCR)) { // INCR button pressed
// increase seconds
setSecond = setSecond + 1;
if (setSecond >= 60) setSecond = 0;
rtc.adjust(DateTime(localTime.year(), localTime.month(), localTime.day(), localTime.hour(), localTime.minute(), setSecond));
while (!digitalRead(PB_INCR));
}
if (!digitalRead(PB_DECR)) { // DECR button pressed
// decrease minutes
setSecond = setSecond - 1;
if (setSecond < 0) setSecond = 59;
rtc.adjust(DateTime(localTime.year(), localTime.month(), localTime.day(), localTime.hour(), localTime.minute(), setSecond));
while (!digitalRead(PB_DECR));
}
if (!button_menu) { // menu button pressed
menu_state = 7;
delay(200);
}
break;
case 7: // wait for button release
if (button_menu) { // menu button released
menu_state = 8;
delay(200);
}
break;
case 8: // push incr & decr to adjust elevation steppermotor
localTime = rtc.now();
// display local time and date on LCD with flashing hour
dateTimeLcd(localTime,0);
// display elevation and azimuth on LCD with blinking E:
elevationAzimuthLcd(0,0,1);
goLcd(0);
if (!digitalRead(PB_INCR)) { // INCR button pressed
// Set the target position relative to the current position CW
elevationStepper.move(1.0);
while (elevationStepper.distanceToGo() != 0) {
elevationStepper.run();
}
}
if (!digitalRead(PB_DECR)) { // DECR button pressed
// Set the target position relative to the current position CCW
elevationStepper.move(-1.0);
while (elevationStepper.distanceToGo() != 0) {
elevationStepper.run();
}
}
if (!button_menu) { // menu button pressed
menu_state = 9;
delay(200);
}
break;
case 9: // wait for button release
if (button_menu) { // menu button released
menu_state = 10;
delay(200);
}
break;
case 10: // push incr & decr to adjust azimuth steppermotor
localTime = rtc.now();
// display local time and date on LCD with flashing hour
dateTimeLcd(localTime,0);
// display elevation and azimuth on LCD with blinking A:
elevationAzimuthLcd(0,0,2);
goLcd(0);
if (!digitalRead(PB_INCR)) { // INCR button pressed
// Set the target position relative to the current position CW
azimuthStepper.move(1.0);
while (azimuthStepper.distanceToGo() != 0) {
azimuthStepper.run();
}
}
if (!digitalRead(PB_DECR)) { // DECR button pressed
// Set the target position relative to the current position CCW
azimuthStepper.move(-1.0);
while (azimuthStepper.distanceToGo() != 0) {
azimuthStepper.run();
}
}
if (!button_menu) { // menu button pressed
menu_state = 11;
delay(200);
}
break;
case 11: // wait for button release
if (button_menu) { // menu button released
menu_state = 12;
delay(200);
}
break;
case 12: // push INCR to start tracker
localTime = rtc.now();
// display local time and date on LCD with flashing hour
dateTimeLcd(localTime,0);
goLcd(1);
// display elevation and azimuth on LCD
elevationAzimuthLcd(0,0,0);
if (!digitalRead(PB_INCR)) { // INCR button pressed
// exit the menu and start the tracker
started = 1;
// remove X from display
lcd.setCursor(9,0);
lcd.print(" ");
// set the new 0 position for both stepper motors
azimuthStepper.setCurrentPosition(0);
elevationStepper.setCurrentPosition(0);
}
if (!button_menu) { // menu button pressed
menu_state = 1;
delay(200);
}
break;
} // end switch case
} // end while(1)
} // end setup()
void loop()
{
const uint32_t interval = 10000; // update position steppers every 10s
uint32_t currentMillis;
static uint32_t previousMillis = millis() - interval;
double el,az;
time_t utcTime;
DateTime localTime;
// get local time from RTC
localTime = rtc.now();
// calculate UTC time and convert to time_t
// time_t data type is used to hold a numerical value that signifies the elapsed time in seconds since the epoch
// (epoch: midnight 1 januari 1970)
utcTime = localTime.unixtime() + (localUtcOffsetHours*3600L);
// display local time and date on LCD
dateTimeLcd(localTime,0);
currentMillis = millis();
// execute every 'interval' milliseconds
if (currentMillis - previousMillis > interval) {
previousMillis = millis();
// Azimuth indicates the angle between north and the point where the sun is located,
// measured in degrees in the eastern hemisphere
// (0° at north, 90° at east, 180° at south, 270° at west).
// Elevation Elevation represents the angle between the sun and the horizon,
// measured in degrees from the observer's position.
// get azimuth and elevation
// verify at: https://geotimedate.org/sun/belgium/bru/brussels
az = myPosition.getSolarAzimuth(utcTime);
el = myPosition.getSolarElevation(utcTime);
// display elevation and azimuth on LCD
elevationAzimuthLcd(round(el),round(az),0);
// set target positions steppers
// 360 degrees -> stepsPerRevolution
// 1 degree -> stepsPerRevolution / 360.0
// x degrees -> stepsPerRevolution * x / 360.0
azimuthStepper.moveTo(stepsPerRevolution * az / 360.0);
// only adjust elevationStepper between sunrise and sunset
if (el > 0) {
elevationStepper.moveTo(stepsPerRevolution * el / 360.0);
}
// distanceToGo() is used to determine how far the motor needs to travel
// before reaching the target position (set by moveTo)
while(azimuthStepper.distanceToGo() !=0) {
azimuthStepper.run();
}
while (elevationStepper.distanceToGo() != 0) {
elevationStepper.run();
}
} // end if()
} // end loop()
void dateTimeLcd(const DateTime &lTime, uint8_t blinkPart) {
const uint32_t BLINK_INTERVAL = 500;
static uint8_t isHourVisible = 1;
static uint8_t isMinuteVisible = 1;
static uint8_t isSecondVisible = 1;
static uint8_t isDayVisible = 1;
static uint8_t isMonthVisible = 1;
static uint8_t isYearVisible = 1;
static uint8_t isDayOfWeekVisible = 1;
static uint32_t previousMillis = 0;
// flashing interval
if (millis() - previousMillis >= BLINK_INTERVAL) {
previousMillis = millis();
switch(blinkPart) {
case 1:
isHourVisible = !isHourVisible; // Toggle visibility hour
isMinuteVisible = 1; isSecondVisible = 1;
break;
case 2:
isMinuteVisible = !isMinuteVisible; // Toggle visibility minutes
isHourVisible = 1; isSecondVisible = 1;
break;
case 3:
isSecondVisible = !isSecondVisible; // Toggle visibility seconds
isHourVisible = 1; isMinuteVisible = 1;
break;
default: // all visisble
isHourVisible = 1; isMinuteVisible = 1; isSecondVisible = 1;
}
}
lcd.setCursor(0, 0);
if (isHourVisible) lcd.print(lTime.hour() < 10 ? "0" + String(lTime.hour()) : String(lTime.hour()));
else lcd.print(" ");
lcd.print(":");
if (isMinuteVisible) lcd.print(lTime.minute() < 10 ? "0" + String(lTime.minute()) : String(lTime.minute()));
else lcd.print(" ");
lcd.print(":");
if (isSecondVisible) lcd.print(lTime.second() < 10 ? "0" + String(lTime.second()) : String(lTime.second()));
else lcd.print(" ");
lcd.setCursor(0,1);
if (isDayVisible) lcd.print(lTime.day() < 10 ? "0" + String(lTime.day()) : String(lTime.day()));
else lcd.print(" ");
lcd.print("-");
if (isMonthVisible) lcd.print(lTime.month() < 10 ? "0" + String(lTime.month()) : String(lTime.month()));
else lcd.print(" ");
lcd.print("-");
if (isYearVisible) lcd.print(lTime.year() % 100);
else lcd.print(" ");
lcd.print(" ");
if (isDayOfWeekVisible) lcd.print(lTime.dayOfTheWeek());
else lcd.print(" ");
}
void elevationAzimuthLcd(int16_t elevation, int16_t azimuth, uint8_t blinkPart) {
const uint32_t BLINK_INTERVAL = 500;
static uint8_t isEVisible = 1;
static uint8_t isAVisible = 1;
static uint32_t previousMillis = 0;
// flashing interval
if (millis() - previousMillis >= BLINK_INTERVAL) {
previousMillis = millis();
switch(blinkPart) {
case 1:
isEVisible = !isEVisible; // Toggle visibility E
isAVisible = 1;
break;
case 2:
isAVisible = !isAVisible; // Toggle visibility A
isEVisible = 1;
break;
default: // all visisble
isAVisible = 1; isEVisible = 1;
}
}
lcd.setCursor(11,0);
if (isEVisible) lcd.print("E:");
else lcd.print(" ");
lcd.print(elevation);
lcd.setCursor(11,1);
if (isAVisible) lcd.print("A:");
else lcd.print(" ");
lcd.print(azimuth);
}
void goLcd(uint8_t blinkPart) {
const uint32_t BLINK_INTERVAL = 500;
static uint8_t isGOVisible = 1;
static uint32_t previousMillis = 0;
// flashing interval
if (millis() - previousMillis >= BLINK_INTERVAL) {
previousMillis = millis();
switch(blinkPart) {
case 1:
isGOVisible = !isGOVisible; // Toggle visibility GO
break;
default: // GO visisble
isGOVisible = 1;
}
}
lcd.setCursor(9,0);
if (isGOVisible) lcd.print("X");
else lcd.print(" ");
}Azimuth
Elevation
North ^^^
+
-