// Arduino astronomic heliostat
// Points mirror according to calculated sun position, no need for sensors.
// Device must be initially programmed (externally) with current location
// and current time, then it will become standalone.
// PELCO-D version
// V. 0.0.1 - 13/mar/2024
// V. 0.0.2 - 14/mar/2024 - Cleaned up code, for porting to PelcoD
// V. 0.0.3 - Replaced steppers by MAX485 for PelcoD
// V 0.0.10: with servos
// Arduino Uno / ESP32 pinout:
////// RTC pins (automatically managed by RTC library):
// Arduino --> ESP32
// SDA: A4 --> D21
// SCL: A5 --> D22
// Power: 5V
#include <DS1307RTC.h> // https://github.com/PaulStoffregen/DS1307RTC
#include <SolarCalculator.h>
#include <TimeLib.h>
#include <ESP32Servo.h>
////// Mode switch:
const byte modePin = 23; // Arduino = 9, ESP32 = D23
////// Output LEDs:
const byte waitPin = 19; // Arduino = 10, ESP32 = 19
const byte readyPin = 18; // Arduino = 7, ESP32 = 18
////// Input joystick:
const byte joyVertPin = 13; // Arduino = A0, ESP32 = 13
//const byte joyHorizPin = 12; // Arduino = A1, ESP32 = 12
const int MAXJOY = 4096;
const byte joyRightPin = 14;
const byte joyLeftPin = 12;
const byte joyUpPin = 33;
const byte joyDownPin = 25;
int SIMULATED_TIME_FOR_DEBUGGING = 0;
int SIMULATED_STEP = 360l;
const int DEBUG_FLAG = 1;
Servo servoAlt;
Servo servoAz;
// Location
double latitude = 41.89366; // debug: must be customizable
double longitude = 12.50436; // debug: must be customizable
// Target
double targetAzimuth = 180;
double targetAltitude = 10;
const byte MODE_SETUP = LOW;
const byte MODE_TARGET = HIGH;
time_t utc;
// Wokwi sets the default RTC to match the local computer's system time,
// if your local computer's time is not UTC, you need an offset
const int localUtcOffsetHours = 1;// Adjust RTC from default Wokwi local time setting to UTC:
// number of decimal digits to print
const uint8_t digits = 3;
// program begins
void setup() {
Serial.begin(115200);
Serial.println("\tHeliostat calculator");
servoAlt.attach(26);
servoAz.attach(27);
pinMode(modePin, INPUT_PULLUP); // HIGH = enlighten target; LOW = SETUP
pinMode(waitPin, OUTPUT);
pinMode(readyPin, OUTPUT);
digitalWrite(waitPin, LOW);
digitalWrite(readyPin, LOW);
pinMode(joyVertPin, INPUT);
//pinMode(joyHorizPin, INPUT);
pinMode(joyRightPin, INPUT);
pinMode(joyLeftPin, INPUT);
pinMode(joyUpPin, INPUT);
pinMode(joyDownPin, INPUT);
setTime(07, 00, 00, 13, 3, 2024); // Set dummy onboard time without RTC
RTC.set(now()); // Set dummy RTC time
/////////// Debug: time must be set by user //////////
setTime((compileTime())); // Set real onboard time without RTC to compile time, which is UTC
RTC.set((compileTime())); // Set real RTC time C to compile time, which is UTC
// Change onboard time from UTC to local
if (localUtcOffsetHours != 0 ) {
unsigned long newTime = RTC.get() + 3600L * localUtcOffsetHours;
RTC.set(newTime);
setTime(newTime);
}
}
void loop() {
double az, el; // Horizontal coordinates, in degrees
double finalAz;
double finalAlt;
const uint32_t moveInterval = 2000l; // DEBUG
static uint32_t lastMoveTime = -1; //-(moveInterval -1000);
uint32_t now = millis();
if (digitalRead(modePin) == MODE_SETUP) {
//Serial.println("========= CONTINUOUS SETUP POINTING ============");
// Realtime update, more energy consumption
lastMoveTime = now + moveInterval + 1000; // force never updating to sun position
//double joyH = analogRead(joyHorizPin);
double joyV = analogRead(joyVertPin);
if (digitalRead(joyRightPin) == HIGH) {
Serial.println("RIGHT, AZ+ CCW");
targetAzimuth += 10;
// if (targetAzimuth > 180) targetAzimuth = 180;
delay(100);
}
if (digitalRead(joyLeftPin) == HIGH){
Serial.println("LEFT, AZ- CW");
targetAzimuth -= 10;
//if (targetAzimuth < -180) targetAzimuth = -180;
delay(100);
}
if (digitalRead(joyDownPin) == HIGH){
Serial.println("DOWN, Alt-");
targetAltitude -= 10;
//if (targetAltitude < -180) targetAltitude = -180;
delay(100);
}
if (digitalRead(joyUpPin) == HIGH){
Serial.println("UP, Alt+");
targetAltitude += 10;
//if (targetAltitude > 180) targetAltitude = 180;
delay(100);
}
} else { // end of setup mode
// Interval update, low energy.
/*Serial.print("Updating every ");
Serial.print(moveInterval);
Serial.print(", elapsed = ");
Serial.println(now - lastMoveTime);*/
}
if ((now - lastMoveTime >= moveInterval) /*|| (digitalRead(modePin) == MODE_SETUP) */){
digitalWrite(waitPin, HIGH); // "Please wait, pointing..."
digitalWrite(readyPin, LOW);
lastMoveTime = now; // += moveInterval;
// Serial.println();
time_t sysTime = RTC.get();
//printTime(sysTime, false);
if (localUtcOffsetHours != 0 ) { // make local/UTC adjustments
unsigned long newTime = sysTime - 3600L * localUtcOffsetHours;
utc = newTime + SIMULATED_TIME_FOR_DEBUGGING * DEBUG_FLAG;
SIMULATED_TIME_FOR_DEBUGGING = SIMULATED_TIME_FOR_DEBUGGING + SIMULATED_STEP;
}
calculate(utc, az, el);
// if (digitalRead(modePin) == MODE_TARGET) {
finalAz = targetAzimuth + (az - targetAzimuth) / 2;
finalAlt = targetAltitude + (el - targetAltitude) / 2;
// } else {
finalAz = targetAzimuth + (az - targetAzimuth) / 2;
finalAlt = targetAltitude + (el - targetAltitude) / 2;
// }
printTime(utc, true);
Serial.print(F("S/T/M Az/El:"));
Serial.print(az);
Serial.print(F("°/"));
Serial.print(el);
Serial.print(F("°|"));
Serial.print(targetAzimuth);
Serial.print(F("°/"));
Serial.print(targetAltitude);
Serial.print(F("°|"));
Serial.print(finalAz);
Serial.print(F("°/"));
Serial.print(finalAlt);
Serial.println(F("°"));
//digitalWrite(waitPin, HIGH);
//digitalWrite(readyPin, LOW);
/////////////////////////////////////////////////////
///////////////////// Point mirror /////////////////
/////////////////////////////////////////////////////
if (finalAz>0) {
pointMirror(-finalAlt+90, finalAz-90);
}
//////////////////////////////////////////////////////
//////////////////////////////////////////////////////
digitalWrite(waitPin, LOW); // "Pointing completed"
digitalWrite(readyPin, HIGH);
//Serial.println("Position updated.");
}
delay(100);
}
// Print a time to serial
//
void printTime(time_t t, boolean utc)
{
if (!utc) {
if (localUtcOffsetHours != 0 ) { // make local/UTC adjustments
unsigned long newTime = t + 3600L * localUtcOffsetHours;
t = newTime;
}
}
tmElements_t someTime;
breakTime(t, someTime);
if (someTime.Hour < 10)Serial.print('0');
Serial.print(someTime.Hour);
Serial.print(F(":"));
if (someTime.Minute < 10)Serial.print('0');
Serial.print(someTime.Minute);
Serial.print(F(":"));
if (someTime.Second < 10)Serial.print('0');
Serial.print(someTime.Second);
Serial.print(utc ? F(" UTC on "): F(" Local on "));
Serial.print(dayStr(someTime.Wday));
Serial.print(F(", "));
Serial.print(monthStr(someTime.Month));
Serial.print(F(" "));
Serial.print(someTime.Day);
Serial.print(F(", "));
Serial.println(tmYearToCalendar(someTime.Year));
}
double degreesToHours(double deg)
{
return deg / 15;
}
// Code from JChristensen/Timezone Clock example
time_t compileTime()
{
const uint8_t COMPILE_TIME_DELAY = 8;
const char *compDate = __DATE__, *compTime = __TIME__, *months = "JanFebMarAprMayJunJulAugSepOctNovDec";
char chMon[4], *m;
tmElements_t tm;
strncpy(chMon, compDate, 3);
chMon[3] = '\0';
m = strstr(months, chMon);
tm.Month = ((m - months) / 3 + 1);
tm.Day = atoi(compDate + 4);
tm.Year = atoi(compDate + 7) - 1970;
tm.Hour = atoi(compTime);
tm.Minute = atoi(compTime + 3);
tm.Second = atoi(compTime + 6);
time_t t = makeTime(tm);
return t + COMPILE_TIME_DELAY;
}
void printSunTime24h(double hours)
{
int m = int(round(hours * 60));
int hr = (m / 60) % 24;
int mn = m % 60;
printDigits(hr);
Serial.print(':');
printDigits(mn);
Serial.println();
}
void printDigits(int digits)
{
if (digits < 10)
Serial.print('0');
Serial.print(digits);
}
void calculate(time_t utc, double &az, double &el) {
calcHorizontalCoordinates(utc, latitude, longitude, az, el);
}
time_t toUtc(time_t local)
{
return local - localUtcOffsetHours * 3600L;
}
void pointMirror(double alt, double az) {
servoAlt.write( alt);
servoAz.write( az);
}AZ-
AZ+
Alt+
Alt-
45°
-45°
Azimuth
Altitude
PLEASE WAIT...
READY
Operating
Setup
0°
45°
90°
135°
180°
Servo
Mirror
90°
135°
180°
225°
270°