I had a question like this before, but now i rewrited my code and tried this again.
My goal is to calculate the Sun rise and the Sun set times based on the microcontroller's location. For this to work, I'm using a public API which will tell me the Longitude and Latitude data based on the device's public IP. The controller's time is synced using NTP and an external RTC module.
I have created a wrapper class for this calculations so it can be used by other components of my program.
The problem is that the calculated times are shifting upwards. When the system first calculates the times it is minus one hour from the actual times of my location. If i calculate it every 5 sec it will eventually reach the real times, after like 10 min or so.
Here is a sample calculation of just a couple of seconds
**********
[SUN_TIMES] - Millis: 5326
[SUN_TIMES] - SunRise: 4:39
[SUN_TIMES] - SunSet: 18:32
**********
**********
[SUN_TIMES] - Millis: 10335
[SUN_TIMES] - SunRise: 4:39
[SUN_TIMES] - SunSet: 18:32
**********
**********
[SUN_TIMES] - Millis: 15335
[SUN_TIMES] - SunRise: 4:39
[SUN_TIMES] - SunSet: 18:32
**********
**********
[SUN_TIMES] - Millis: 20335
[SUN_TIMES] - SunRise: 4:39
[SUN_TIMES] - SunSet: 18:33
**********
**********
[SUN_TIMES] - Millis: 25335
[SUN_TIMES] - SunRise: 4:39
[SUN_TIMES] - SunSet: 18:33
**********
**********
[SUN_TIMES] - Millis: 30335
[SUN_TIMES] - SunRise: 4:39
[SUN_TIMES] - SunSet: 18:33
**********
**********
[SUN_TIMES] - Millis: 35335
[SUN_TIMES] - SunRise: 4:40
[SUN_TIMES] - SunSet: 18:33
**********
**********
[SUN_TIMES] - Millis: 40335
[SUN_TIMES] - SunRise: 4:40
[SUN_TIMES] - SunSet: 18:33
**********
**********
[SUN_TIMES] - Millis: 45342
[SUN_TIMES] - SunRise: 4:40
[SUN_TIMES] - SunSet: 18:33
**********
**********
[SUN_TIMES] - Millis: 50341
[SUN_TIMES] - SunRise: 4:40
[SUN_TIMES] - SunSet: 18:33
**********
**********
[SUN_TIMES] - Millis: 55345
[SUN_TIMES] - SunRise: 4:40
[SUN_TIMES] - SunSet: 18:33
**********
**********
[SUN_TIMES] - Millis: 60346
[SUN_TIMES] - SunRise: 4:40
[SUN_TIMES] - SunSet: 18:33
**********
**********
[SUN_TIMES] - Millis: 65346
[SUN_TIMES] - SunRise: 4:40
[SUN_TIMES] - SunSet: 18:33
**********
**********
[SUN_TIMES] - Millis: 70346
[SUN_TIMES] - SunRise: 4:40
[SUN_TIMES] - SunSet: 18:33
**********
**********
[SUN_TIMES] - Millis: 75346
[SUN_TIMES] - SunRise: 4:40
[SUN_TIMES] - SunSet: 18:33
**********
As you can see it started on 5326 millisec, calculated 18:32 PM for the sun set. On 75346 millisec it shifted one minute. It's about 70 seconds and more then one minute offset. And if i left it it will goes upper and upper as time passes. What did i miss?
My current sun rise is 5:38AM and the set is 19:30
At 857999 millis the calculation prints the following:
**********
[SUN_TIMES] - Millis: 857999
[SUN_TIMES] - SunRise: 4:53
[SUN_TIMES] - SunSet: 18:47
**********
sunTimes.h
#include <Arduino.h>
#include <ArduinoJson.h>
#include <utilities/globals.h>
#include <utilities/networkSystem.h>
#include <utilities/fileSystem.h>
#include <utilities/timeSystem.h>
#include <HTTPClient.h>
/*
these includes mostly for the http request response processing.
Like JSON for arduino and things like that. It does not important for the
calculation
*/
#define SUN_TIMES_DEBUG true
#define GEO_RESP_SIZE 1500
class sunTimes {
private:
const char* ipStackURL = "http://api.ipstack.com/check?access_key=0f4ce7d93a2ed67e7435d4227cc0e931&fields=main";
boolean gotGeoData = false;
// Various helper functions for the calculation.
// These calculations adapted from the following open source C project
// Link by Mike Chirico http://souptonuts.sourceforge.net/code/sunrise.c.html
double degToRad(double angleDeg);
double radToDeg(double angleRad);
double calcMeanObliquityOfEcliptic(double t);
double calcGeomMeanLongSun(double t);
double calcObliquityCorrection(double t);
double calcEccentricityEarthOrbit(double t);
double calcGeomMeanAnomalySun(double t);
double calcEquationOfTime(double t);
double calcTimeJulianCent(double jd);
double calcSunTrueLong(double t);
double calcSunApparentLong(double t);
double calcSunDeclination(double t);
double calcHourAngleSunrise(double lat, double solarDec);
double calcHourAngleSunset(double lat, double solarDec);
double calcJD(int year, int month, int day);
double calcJDFromJulianCent(double t);
double calcSunEqOfCenter(double t);
double calcSunriseUTC(double JD, double latitude, double longitude);
double calcSunsetUTC(double JD, double latitude, double longitude);
public:
void getGeoData();
void calcSunTimes();
};
sunTimes.cpp
#include <utilities/sunTimes.h>
void sunTimes::getGeoData(){
// First check if we already got geolocation data.
// No need to get it again.
if( fileSys.config.gotGeoData ){ return; }
HTTPClient http;
http.begin( ipStackURL );
int httpResponseCode = http.GET();
// Check the HTTP response code.
if (httpResponseCode <= 0) {
#if SUN_TIMES_DEBUG
Serial.printf("[SUN_TIMES] - Server not reachable. Error: %d\n",httpResponseCode);
#endif
return;
}
// Try to deserialize the response stream. It should be a JSON string.
SpiRamJsonDocument doc(GEO_RESP_SIZE);
DeserializationError error = deserializeJson(doc,http.getString());
http.end();
if(error){
#if SUN_TIMES_DEBUG
Serial.printf("[SUN_TIMES] - Can't deserialize geo response. Error: %s\n", error.c_str() );
#endif
return;
}
// Check the deserialized response message.
if( doc["success"] == false ){
const char* errorMSG = doc["error"]["info"].as<const char*>();
#if SUN_TIMES_DEBUG
Serial.printf("[SUN_TIMES] - Response error: %s\n", errorMSG);
#endif
return;
}
// Get the data from the JSON response.
fileSys.config.publicIP.fromString(doc["ip"].as<const char*>());
strncpy(fileSys.config.city, doc["city"], sizeof(fileSys.config.city));
fileSys.config.latitude = doc["latitude"].as<float>();
fileSys.config.longitude = doc["longitude"].as<float>();
#if SUN_TIMES_DEBUG
Serial.printf("[SUN_TIMES] - Latitude: %.6f\n",fileSys.config.latitude);
Serial.printf("[SUN_TIMES] - Longitude: %.6f\n",fileSys.config.longitude);
#endif
fileSys.config.gotGeoData = true;
calcSunTimes();
}
void sunTimes::calcSunTimes(){
// First we need to check if we already got latitude data.
if( !fileSys.config.gotGeoData ){ return; }
time_t seconds;
time_t tseconds;
struct tm *ptm = NULL;
struct tm tm;
float JD = calcJD( hsh_timeSystem.getYear(),
hsh_timeSystem.getMonth(),
hsh_timeSystem.getDayOfMonth());
tm.tm_year = hsh_timeSystem.getYear() - 1900;
tm.tm_mon = hsh_timeSystem.getMonth() - 1;
tm.tm_mday = hsh_timeSystem.getDayOfMonth();
tm.tm_hour = hsh_timeSystem.getHour();
tm.tm_min = hsh_timeSystem.getMinute();
tm.tm_sec = hsh_timeSystem.getSecond();
seconds = mktime(&tm);
int delta;
ptm = gmtime(&seconds);
delta = ptm->tm_hour;
// Calc sunRise
tseconds = seconds;
seconds = seconds + calcSunriseUTC(JD, fileSys.config.latitude, -fileSys.config.longitude) * 60;
seconds = seconds - delta * 3600;
ptm = gmtime(&seconds);
// Is winter time.
int offsetToAdd = 1;
if( fileSys.config.dst ){ offsetToAdd = 0; }
int calculatedYear = ptm->tm_year + 1900;
if( calculatedYear == hsh_timeSystem.getYear() ){
fileSys.config.sunRiseHour = ptm->tm_hour + offsetToAdd;
fileSys.config.sunRiseMinute = ptm->tm_min;
}
// Calc sunSet
seconds = tseconds;
seconds += calcSunsetUTC(JD, fileSys.config.latitude, -fileSys.config.longitude) * 60;
seconds = seconds - delta * 3600;
ptm = gmtime(&seconds);
calculatedYear = ptm->tm_year + 1900;
if( calculatedYear == hsh_timeSystem.getYear() ){
fileSys.config.sunSetHour = ptm->tm_hour + offsetToAdd;
fileSys.config.sunSetMinute = ptm->tm_min;
}
#if SUN_TIMES_DEBUG
Serial.println("*********** ***********");
Serial.printf("[SUN_TIMES] - Millis: %lu\n",millis() );
Serial.printf("[SUN_TIMES] - SunRise: %d:%d\n",fileSys.config.sunRiseHour,fileSys.config.sunRiseMinute);
Serial.printf("[SUN_TIMES] - SunSet: %d:%d\n",fileSys.config.sunSetHour,fileSys.config.sunSetMinute);
Serial.println("*********** ***********");
#endif
// Make the config file with the new data.
fileSys.makeConfig();
}
sunTimesCalc.cpp
// Various helper functions for the calculation.
// These calculations adapted from the following open source C project
// Link by Mike Chirico http://souptonuts.sourceforge.net/code/sunrise.c.html
#include <utilities/sunTimes.h>
/* Convert degree angle to radians */
double sunTimes::degToRad(double angleDeg) {
return (PI * angleDeg / 180.0);
}
double sunTimes::radToDeg(double angleRad) {
return (180.0 * angleRad / PI);
}
double sunTimes::calcMeanObliquityOfEcliptic(double t) {
double seconds = 21.448 - t * (46.8150 + t * (0.00059 - t * (0.001813)));
double e0 = 23.0 + (26.0 + (seconds / 60.0)) / 60.0;
return e0; // in degrees
}
double sunTimes::calcGeomMeanLongSun(double t) {
double L = 280.46646 + t * (36000.76983 + 0.0003032 * t);
while ((int)L > 360) {
L -= 360.0;
}
while (L < 0) {
L += 360.0;
}
return L; // in degrees
}
double sunTimes::calcObliquityCorrection(double t) {
double e0 = calcMeanObliquityOfEcliptic(t);
double omega = 125.04 - 1934.136 * t;
double e = e0 + 0.00256 * cos(degToRad(omega));
return e; // in degrees
}
double sunTimes::calcEccentricityEarthOrbit(double t) {
double e = 0.016708634 - t * (0.000042037 + 0.0000001267 * t);
return e; // unitless
}
double sunTimes::calcGeomMeanAnomalySun(double t) {
double M = 357.52911 + t * (35999.05029 - 0.0001537 * t);
return M; // in degrees
}
double sunTimes::calcEquationOfTime(double t) {
double epsilon = calcObliquityCorrection(t);
double l0 = calcGeomMeanLongSun(t);
double e = calcEccentricityEarthOrbit(t);
double m = calcGeomMeanAnomalySun(t);
double y = tan(degToRad(epsilon) / 2.0);
y *= y;
double sin2l0 = sin(2.0 * degToRad(l0));
double sinm = sin(degToRad(m));
double cos2l0 = cos(2.0 * degToRad(l0));
double sin4l0 = sin(4.0 * degToRad(l0));
double sin2m = sin(2.0 * degToRad(m));
double Etime = y * sin2l0 - 2.0 * e * sinm + 4.0 * e * y * sinm * cos2l0 - 0.5 * y * y * sin4l0 - 1.25 * e * e * sin2m;
return radToDeg(Etime) * 4.0; // in minutes of time
}
double sunTimes::calcTimeJulianCent(double jd) {
double T = (jd - 2451545.0) / 36525.0;
return T;
}
double sunTimes::calcSunTrueLong(double t) {
double l0 = calcGeomMeanLongSun(t);
double c = calcSunEqOfCenter(t);
double O = l0 + c;
return O; // in degrees
}
double sunTimes::calcSunApparentLong(double t) {
double o = calcSunTrueLong(t);
double omega = 125.04 - 1934.136 * t;
double lambda = o - 0.00569 - 0.00478 * sin(degToRad(omega));
return lambda; // in degrees
}
double sunTimes::calcSunDeclination(double t) {
double e = calcObliquityCorrection(t);
double lambda = calcSunApparentLong(t);
double sint = sin(degToRad(e)) * sin(degToRad(lambda));
double theta = radToDeg(asin(sint));
return theta; // in degrees
}
double sunTimes::calcHourAngleSunrise(double lat, double solarDec) {
double latRad = degToRad(lat);
double sdRad = degToRad(solarDec);
double HA = (acos(cos(degToRad(90.833)) / (cos(latRad) * cos(sdRad)) - tan(latRad) * tan(sdRad)));
return HA; // in radians
}
double sunTimes::calcHourAngleSunset(double lat, double solarDec) {
double latRad = degToRad(lat);
double sdRad = degToRad(solarDec);
double HA = (acos(cos(degToRad(90.833)) / (cos(latRad) * cos(sdRad)) - tan(latRad) * tan(sdRad)));
return -HA; // in radians
}
double sunTimes::calcJD(int year, int month, int day) {
if (month <= 2) {
year -= 1;
month += 12;
}
int A = floor(year / 100);
int B = 2 - A + floor(A / 4);
double JD = floor(365.25 * (year + 4716)) + floor(30.6001 * (month + 1)) + day + B - 1524.5;
return JD;
}
double sunTimes::calcJDFromJulianCent(double t) {
double JD = t * 36525.0 + 2451545.0;
return JD;
}
double sunTimes::calcSunEqOfCenter(double t) {
double m = calcGeomMeanAnomalySun(t);
double mrad = degToRad(m);
double sinm = sin(mrad);
double sin2m = sin(mrad + mrad);
double sin3m = sin(mrad + mrad + mrad);
double C = sinm * (1.914602 - t * (0.004817 + 0.000014 * t)) + sin2m * (0.019993 - 0.000101 * t) + sin3m * 0.000289;
return C; // in degrees
}
double sunTimes::calcSunriseUTC(double JD, double latitude, double longitude) {
double t = calcTimeJulianCent(JD);
double eqTime = calcEquationOfTime(t);
double solarDec = calcSunDeclination(t);
double hourAngle = calcHourAngleSunrise(latitude, solarDec);
double delta = longitude - radToDeg(hourAngle);
double timeDiff = 4 * delta; // in minutes of time
double timeUTC = 720 + timeDiff - eqTime; // in minutes
double newt = calcTimeJulianCent(calcJDFromJulianCent(t) + timeUTC / 1440.0);
eqTime = calcEquationOfTime(newt);
solarDec = calcSunDeclination(newt);
hourAngle = calcHourAngleSunrise(latitude, solarDec);
delta = longitude - radToDeg(hourAngle);
timeDiff = 4 * delta;
timeUTC = 720 + timeDiff - eqTime;
return timeUTC;
}
double sunTimes::calcSunsetUTC(double JD, double latitude, double longitude) {
double t = calcTimeJulianCent(JD);
double eqTime = calcEquationOfTime(t);
double solarDec = calcSunDeclination(t);
double hourAngle = calcHourAngleSunset(latitude, solarDec);
double delta = longitude - radToDeg(hourAngle);
double timeDiff = 4 * delta; // in minutes of time
double timeUTC = 720 + timeDiff - eqTime; // in minutes
double newt = calcTimeJulianCent(calcJDFromJulianCent(t) + timeUTC / 1440.0);
eqTime = calcEquationOfTime(newt);
solarDec = calcSunDeclination(newt);
hourAngle = calcHourAngleSunset(latitude, solarDec);
delta = longitude - radToDeg(hourAngle);
timeDiff = 4 * delta;
cc timeUTC = 720 + timeDiff - eqTime;
return timeUTC;
}
File system is used to store the calculated data and the longitude and latitude because of the limited API requests.
If you comment out the file system related things it should work.
#include <utilities/sunTimes.h>
sunTimes sunSys;
long lastSunTest = 0;
char ssid[32] = "Network";
char password[32] = "password";
#define WIFI_TIMEOUT 5000
#define WIFI_DEBUG true
/*
This function will wait for a wifi to connect.
If the connection takes more then WIFI_TIMEOUT it will return
false, otherwise returns true.
*/
boolean connectWifi(){
WiFi.begin(ssid, password);
long wifiConnStartMS = millis();
#if WIFI_DEBUG
Serial.printf("\nConnecting to: %s...",ssid);
#endif
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(200);
if( millis() - wifiConnStartMS >= WIFI_TIMEOUT ){
#if WIFI_DEBUG
Serial.println("\nTimeout on wifi connection. Wrong credentials?");
#endif
return false;
}
}
#if WIFI_DEBUG
Serial.printf("\nWiFi Connected to: %s\n",ssid);
#endif
return true;
}
void setup() {
Serial.begin(115200);
if( connectWifi() ){
sunSys.getGeoData();
}
}
void loop() {
if( millis() - lastSunTest >= 5000 ){
lastSunTest = millis();
sunSys.calcSunTimes();
}
}
I can confirm that the latitude and longitude which the API gave me is correct. The time is also correct.
Sorry for the long question, i know every detail matters and I'm desperate.
EDIT: I let the calculation run every 5 sec. Suddenly when it reached the correct suntimes it shifted back one hour. And started all over again.
Here is a sample:
**********
[SUN_TIMES] - Millis: 3403180
[SUN_TIMES] - SunRise: 5:36
[SUN_TIMES] - SunSet: 19:29
**********
**********
[SUN_TIMES] - Millis: 3408181
[SUN_TIMES] - SunRise: 5:36
[SUN_TIMES] - SunSet: 19:29
**********
**********
[SUN_TIMES] - Millis: 3413181
[SUN_TIMES] - SunRise: 4:36
[SUN_TIMES] - SunSet: 18:29
**********
**********
[SUN_TIMES] - Millis: 3418182
[SUN_TIMES] - SunRise: 4:36
[SUN_TIMES] - SunSet: 18:29
**********
**********
[SUN_TIMES] - Millis: 3423183
[SUN_TIMES] - SunRise: 4:36
[SUN_TIMES] - SunSet: 18:29
**********
As you can see on 3413181 millis it shifted back which is roughly 56 minute.
EDIT 2 : I have found an other way to compute this. This way it isn't shifting anywhere on it's own, but it is plus one hour for some reason. I did not wanted to paste the code here so i created files on pastebin.com
EDIT3: I created an ESP32 simulation to demonstrate the problem. Just adjust the time in the t_Config struct and let it run. You can check it out at this link: https://wokwi.com/projects/340791620186145362
I have found an open source library to address my issue. I could not figure out what was my code problem so i started to search for libraries. I did not want to use library, i rather use my own with inspired code, but i had no choise.
Here is the library ( thanks for the author ) link.
My final calculation function simplified to this:
void sunTimes::calcSunTimes(){
Dusk2Dawn cycle( fileSys.config.latitude, fileSys.config.longitude, fileSys.config.timeZone );
int year = hsh_timeSystem.getYear(),
month = hsh_timeSystem.getMonth(),
day = hsh_timeSystem.getDayOfMonth();
int sunRise = cycle.sunrise(year, month, day, fileSys.config.dst);
int sunSet = cycle.sunset(year, month, day, fileSys.config.dst);
fileSys.config.sunRiseHour = cycle.getHour_fromMin(sunRise);
fileSys.config.sunRiseMinute = cycle.getMin_fromMin(sunRise);
fileSys.config.sunSetHour = cycle.getHour_fromMin(sunSet);
fileSys.config.sunSetMinute = cycle.getMin_fromMin(sunSet);
fileSys.makeConfig();
#if SUN_TIMES_DEBUG
Serial.println("*********** ***********");
Serial.printf("[SUN_TIMES] - Millis: %lu\n",millis() );
Serial.printf("[SUN_TIMES] - SunRise: %02d:%02d\n",fileSys.config.sunRiseHour,fileSys.config.sunRiseMinute);
Serial.printf("[SUN_TIMES] - SunSet: %02d:%02d\n",fileSys.config.sunSetHour,fileSys.config.sunSetMinute);
Serial.println("*********** ***********");
#endif
}
In the following days i will compare my code to this lib's code and i will check whats went wrong. ( I suspect that it was the timeZone ) Thank you if you read my question.