├── keywords.txt ├── library.properties ├── Dusk2Dawn.h ├── examples └── example │ └── example.ino ├── README.md └── Dusk2Dawn.cpp /keywords.txt: -------------------------------------------------------------------------------- 1 | Dusk2Dawn KEYWORD1 2 | sunrise KEYWORD2 3 | sunset KEYWORD2 4 | min2str KEYWORD2 5 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=Dusk2Dawn 2 | version=1.0.1 3 | author=DM Kishi 4 | maintainer=DM Kishi 5 | sentence=Get time of sunrise and sunset. 6 | paragraph=Given some basic data, such as geographic coordinates and a date, an estimate time of apparent sunrise or sunset is returned in minutes elapsed since midnight. This is a paired down port of the NOAA Solar Calculator. 7 | category=Other 8 | url=https://github.com/dmkishi/Dusk2Dawn 9 | architectures=* 10 | includes=Dusk2Dawn.h 11 | -------------------------------------------------------------------------------- /Dusk2Dawn.h: -------------------------------------------------------------------------------- 1 | /* Dusk2Dawn.h 2 | * Get time of sunrise and sunset. 3 | * Created by DM Kishi on 2017-02-01. 4 | * 5 | */ 6 | 7 | #ifndef Dusk2Dawn_h 8 | #define Dusk2Dawn_h 9 | 10 | #include "Arduino.h" 11 | #include 12 | 13 | class Dusk2Dawn { 14 | public: 15 | Dusk2Dawn(float, float, float); 16 | int sunrise(int, int, int, bool); 17 | int sunset(int, int, int, bool); 18 | static bool min2str(char*, int); 19 | private: 20 | float _latitude, _longitude; 21 | int _timezone; 22 | int sunriseSet(bool, int, int, int, bool); 23 | float sunriseSetUTC(bool, float, float, float); 24 | float equationOfTime(float); 25 | float meanObliquityOfEcliptic(float); 26 | float eccentricityEarthOrbit(float); 27 | float sunDeclination(float); 28 | float sunApparentLong(float); 29 | float sunTrueLong(float); 30 | float sunEqOfCenter(float); 31 | float hourAngleSunrise(float, float); 32 | float obliquityCorrection(float); 33 | float geomMeanLongSun(float); 34 | float geomMeanAnomalySun(float); 35 | float jDay(int, int, int); 36 | float fractionOfCentury(float); 37 | float radToDeg(float); 38 | float degToRad(float); 39 | static bool zeroPadTime(char*, byte); 40 | }; 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /examples/example/example.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | 4 | void setup() { 5 | Serial.begin (9600); 6 | 7 | /* Multiple instances can be created. Arguments are longitude, latitude, and 8 | * time zone offset in hours from UTC. 9 | * 10 | * The first two must be in decimal degrees (DD), e.g. 10.001, versus the 11 | * more common degrees, minutes, and seconds format (DMS), e.g. 10° 00′ 3.6″. 12 | * The time zone offset can be expressed in float in the few cases where the 13 | * the zones are offset by 30 or 45 minutes, e.g. "5.75" for Nepal Standard 14 | * Time. 15 | * 16 | * HINT: An easy way to find the longitude and latitude for any location is 17 | * to find the spot in Google Maps, right click the place on the map, and 18 | * select "What's here?". At the bottom, you’ll see a card with the 19 | * coordinates. 20 | */ 21 | Dusk2Dawn losAngeles(34.0522, -118.2437, -8); 22 | Dusk2Dawn antarctica(-77.85, 166.6667, 12); 23 | 24 | 25 | /* Available methods are sunrise() and sunset(). Arguments are year, month, 26 | * day, and if Daylight Saving Time is in effect. 27 | */ 28 | int laSunrise = losAngeles.sunrise(2017, 12, 31, false); 29 | int laSunset = losAngeles.sunset(2017, 12, 31, false); 30 | int antSunrise = antarctica.sunrise(2017, 6, 30, true); 31 | 32 | 33 | /* Time is returned in minutes elapsed since midnight. If no sunrises or 34 | * sunsets are expected, a "-1" is returned. 35 | */ 36 | Serial.println(laSunrise); // 418 37 | Serial.println(laSunset); // 1004 38 | Serial.println(antSunrise); // -1 39 | 40 | 41 | /* A static method converts the returned time to a 24-hour clock format. 42 | * Arguments are a character array and time in minutes. 43 | */ 44 | char time[6]; 45 | Dusk2Dawn::min2str(time, laSunrise); 46 | Serial.println(time); // 06:58 47 | 48 | 49 | /* Alternatively, the array could be initialized with a dummy. This may be 50 | * easier to remember. 51 | */ 52 | char time2[] = "00:00"; 53 | Dusk2Dawn::min2str(time2, laSunset); 54 | Serial.println(time2); // 16:53 55 | 56 | 57 | /* Do some calculations with the minutes, then convert to time. 58 | */ 59 | int laSolarNoon = laSunrise + (laSunset - laSunrise) / 2; 60 | char time3[] = "00:00"; 61 | Dusk2Dawn::min2str(time3, laSolarNoon); 62 | Serial.println(time3); // 11:56 63 | 64 | 65 | /* In case of an error, an error message is given. The static method also 66 | * returns a false boolean value for error handling purposes. 67 | */ 68 | char time4[] = "00:00"; 69 | bool response = Dusk2Dawn::min2str(time4, antSunrise); 70 | if (response == false) { 71 | Serial.println(time4); // "ERROR" 72 | Serial.println("Uh oh!"); 73 | } 74 | } 75 | 76 | 77 | void loop() { 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dusk2Dawn 2 | 3 | Minimal Arduino library for sunrise and sunset time. 4 | 5 | Given some basic data, such as geographic coordinates and a date, an estimate time of [*apparent* sunrise or sunset](https://www.esrl.noaa.gov/gmd/grad/solcalc/glossary.html#apparentsunrise) is returned in **minutes elapsed since midnight**. 6 | 7 | **WARNING**: This is an unauthorized port of [NOAA's Solar Calculator](https://www.esrl.noaa.gov/gmd/grad/solcalc/), 8 | hence the package will remain unlicensed. *Use at your own risk!* 9 | 10 | ## Installation 11 | ### From the Library Manager 12 | 1. Launch the Arduino IDE and navigate to *Sketch → Include Library → Manage Libraries*. 13 | 2. In the library manager, scroll to *Dusk2Dawn* or enter the name into the search field. 14 | 3. Click on the library, then click on the Install button. 15 | 16 | ### From the ZIP file 17 | 1. Download the [ZIP file](https://github.com/dmkishi/Dusk2Dawn/archive/master.zip). 18 | 2. Launch the Arduino IDE and navigate to *Sketch → Include Library → Add .ZIP Library...*. From the prompt, select the ZIP just downloaded. 19 | 20 | ## Usage 21 | ```C++ 22 | /* Multiple instances can be created. Arguments are longitude, latitude, and 23 | * time zone offset in hours from UTC. 24 | * 25 | * The first two must be in decimal degrees (DD), e.g. 10.001, versus the 26 | * more common degrees, minutes, and seconds format (DMS), e.g. 10° 00′ 3.6″. 27 | * The time zone offset can be expressed in float in the few cases where the 28 | * the zones are offset by 30 or 45 minutes, e.g. "5.75" for Nepal Standard 29 | * Time. 30 | * 31 | * HINT: An easy way to find the longitude and latitude for any location is 32 | * to find the spot in Google Maps, right click the place on the map, and 33 | * select "What's here?". At the bottom, you’ll see a card with the 34 | * coordinates. 35 | */ 36 | Dusk2Dawn losAngeles(34.0522, -118.2437, -8); 37 | Dusk2Dawn antarctica(-77.85, 166.6667, 12); 38 | 39 | 40 | /* Available methods are sunrise() and sunset(). Arguments are year, month, 41 | * day, and if Daylight Saving Time is in effect. 42 | */ 43 | int laSunrise = losAngeles.sunrise(2017, 12, 31, false); 44 | int laSunset = losAngeles.sunset(2017, 12, 31, false); 45 | int antSunrise = antarctica.sunrise(2017, 6, 30, true); 46 | 47 | 48 | /* Time is returned in minutes elapsed since midnight. If no sunrises or 49 | * sunsets are expected, a "-1" is returned. 50 | */ 51 | Serial.println(laSunrise); // 418 52 | Serial.println(laSunset); // 1004 53 | Serial.println(antSunrise); // -1 54 | 55 | 56 | /* A static method converts the returned time to a 24-hour clock format. 57 | * Arguments are a character array and time in minutes. 58 | */ 59 | char time[6]; 60 | Dusk2Dawn::min2str(time, laSunrise); 61 | Serial.println(time); // 06:58 62 | 63 | 64 | /* Alternatively, the array could be initialized with a dummy. This may be 65 | * easier to remember. 66 | */ 67 | char time2[] = "00:00"; 68 | Dusk2Dawn::min2str(time2, laSunset); 69 | Serial.println(time2); // 16:53 70 | 71 | 72 | /* Do some calculations with the minutes, then convert to time. 73 | */ 74 | int laSolarNoon = laSunrise + (laSunset - laSunrise) / 2; 75 | char time3[] = "00:00"; 76 | Dusk2Dawn::min2str(time3, laSolarNoon); 77 | Serial.println(time3); // 11:56 78 | 79 | 80 | /* In case of an error, an error message is given. The static method also 81 | * returns a false boolean value for error handling purposes. 82 | */ 83 | char time4[] = "00:00"; 84 | bool response = Dusk2Dawn::min2str(time4, antSunrise); 85 | if (response == false) { 86 | Serial.println(time4); // "ERROR" 87 | Serial.println("Uh oh!"); 88 | } 89 | ``` 90 | 91 | ## History 92 | - **2019-6-18**: Add license warning. 93 | - **2017-2-9**: Bug fix. 94 | - **2017-2-3**: Released. 95 | -------------------------------------------------------------------------------- /Dusk2Dawn.cpp: -------------------------------------------------------------------------------- 1 | /* Dusk2Dawn.cpp 2 | * Get time of sunrise and sunset. 3 | * Created by DM Kishi on 2017-02-01. 4 | * 5 | */ 6 | 7 | #include "Arduino.h" 8 | #include 9 | #include "Dusk2Dawn.h" 10 | 11 | 12 | 13 | /******************************************************************************/ 14 | /* PUBLIC */ 15 | /******************************************************************************/ 16 | /* Though most time zones are offset by whole hours, there are a few zones 17 | * offset by 30 or 45 minutes, so the argument must be declared as a float. 18 | */ 19 | Dusk2Dawn::Dusk2Dawn(float latitude, float longitude, float timezone) { 20 | _latitude = latitude; 21 | _longitude = longitude; 22 | _timezone = timezone; 23 | } 24 | 25 | 26 | int Dusk2Dawn::sunrise(int y, int m, int d, bool isDST) { 27 | return sunriseSet(true, y, m, d, isDST); 28 | } 29 | 30 | 31 | int Dusk2Dawn::sunset(int y, int m, int d, bool isDST) { 32 | return sunriseSet(false, y, m, d, isDST); 33 | } 34 | 35 | 36 | /* Convert minutes elapsed since midnight, the figure returned by the public 37 | * methods sunrise() and sunset(), to a 24-hour clock format, e.g. "23:00". 38 | * 39 | * This is done by filling a passed character array, which must be of length 6, 40 | * e.g. "12:34\0". In case of an error, the array is written as "ERROR" (which 41 | * is coincidently the same length as the clock format.) This is much friendlier 42 | * and obvious than having to check the function return, which is still provided 43 | * for error handling purposes. 44 | * 45 | * This function is provided as a static method so that returned minutes can be 46 | * worked on before requesting a formatted time. For instance, given the time of 47 | * sunrise and sunset, the solar noon can be calculated, at which point it can 48 | * be converted to a 24-hour clock format. 49 | * 50 | * String classes are avoided to keep memory use to a minimum. 51 | */ 52 | bool Dusk2Dawn::min2str(char *str, int minutes) { 53 | bool isError = false; 54 | 55 | if (minutes < 0 || minutes >= 1440) { 56 | isError = true; 57 | } 58 | 59 | float floatHour = minutes / 60.0; 60 | float floatMinute = 60.0 * (floatHour - floor(floatHour)); 61 | byte byteHour = (byte) floatHour; 62 | byte byteMinute = (byte) floatMinute; 63 | 64 | if (byteMinute > 59) { 65 | byteHour += 1; 66 | byteMinute = 0; 67 | } 68 | 69 | char strHour[] = "00"; 70 | char strMinute[] = "00"; 71 | 72 | // In case of an error, keep passing it down. 73 | isError = isError ? isError : !zeroPadTime(strHour, byteHour); 74 | isError = isError ? isError : !zeroPadTime(strMinute, byteMinute); 75 | 76 | // This is fugly but I can't think of a better way.... 77 | if (!isError) { 78 | str[0] = strHour[0]; 79 | str[1] = strHour[1]; 80 | str[2] = ':'; 81 | str[3] = strMinute[0]; 82 | str[4] = strMinute[1]; 83 | str[5] = '\0'; 84 | } else { 85 | str[0] = 'E'; 86 | str[1] = 'R'; 87 | str[2] = 'R'; 88 | str[3] = 'O'; 89 | str[4] = 'R'; 90 | str[5] = '\0'; 91 | } 92 | 93 | return !isError; 94 | } 95 | 96 | 97 | /******************************************************************************/ 98 | /* PRIVATE */ 99 | /******************************************************************************/ 100 | int Dusk2Dawn::sunriseSet(bool isRise, int y, int m, int d, bool isDST) { 101 | float jday, newJday, timeUTC, newTimeUTC; 102 | int timeLocal; 103 | 104 | jday = jDay(y, m, d); 105 | timeUTC = sunriseSetUTC(isRise, jday, _latitude, _longitude); 106 | 107 | // Advance the calculated time by a fraction of itself. I've no idea what the 108 | // purpose of this is. 109 | newJday = jday + timeUTC / (60 * 24); 110 | newTimeUTC = sunriseSetUTC(isRise, newJday, _latitude, _longitude); 111 | 112 | if (!isnan(newTimeUTC)) { 113 | timeLocal = (int) round(newTimeUTC + (_timezone * 60)); 114 | timeLocal += (isDST) ? 60 : 0; 115 | } else { 116 | // There is no sunrise or sunset, e.g. it's in the (ant)arctic. 117 | timeLocal = -1; 118 | } 119 | 120 | return timeLocal; 121 | } 122 | 123 | 124 | float Dusk2Dawn::sunriseSetUTC(bool isRise, float jday, float latitude, float longitude) { 125 | float t = fractionOfCentury(jday); 126 | float eqTime = equationOfTime(t); 127 | float solarDec = sunDeclination(t); 128 | float hourAngle = hourAngleSunrise(latitude, solarDec); 129 | 130 | hourAngle = isRise ? hourAngle : -hourAngle; 131 | float delta = longitude + radToDeg(hourAngle); 132 | float timeUTC = 720 - (4 * delta) - eqTime; // in minutes 133 | return timeUTC; 134 | } 135 | 136 | 137 | /* ---------------------------- EQUATION OF TIME ---------------------------- */ 138 | /* The difference between mean solar time (as shown by clocks) and apparent 139 | * solar time (indicated by sundials), which varies with the time of year. 140 | */ 141 | float Dusk2Dawn::equationOfTime(float t) { 142 | float epsilon = obliquityCorrection(t); 143 | float l0 = geomMeanLongSun(t); 144 | float e = eccentricityEarthOrbit(t); 145 | float m = geomMeanAnomalySun(t); 146 | 147 | float y = tan(degToRad(epsilon) / 2); 148 | y *= y; 149 | 150 | float sin2l0 = sin(2.0 * degToRad(l0)); 151 | float sinm = sin(degToRad(m)); 152 | float cos2l0 = cos(2.0 * degToRad(l0)); 153 | float sin4l0 = sin(4.0 * degToRad(l0)); 154 | float sin2m = sin(2.0 * degToRad(m)); 155 | 156 | float Etime = y * sin2l0 - 2.0 * e * sinm + 4.0 * e * y * sinm * cos2l0 - 0.5 * y * y * sin4l0 - 1.25 * e * e * sin2m; 157 | return radToDeg(Etime) * 4.0; // in minutes of time 158 | } 159 | 160 | 161 | /* Obliquity of the ecliptic is the term used by astronomers for the inclination 162 | * of Earth's equator with respect to the ecliptic, or of Earth's rotation axis 163 | * to a perpendicular to the ecliptic. 164 | */ 165 | float Dusk2Dawn::meanObliquityOfEcliptic(float t) { 166 | float seconds = 21.448 - t * (46.8150 + t * (0.00059 - t * 0.001813)); 167 | float e0 = 23 + (26 + (seconds / 60)) / 60; 168 | return e0; // in degrees 169 | } 170 | 171 | 172 | float Dusk2Dawn::eccentricityEarthOrbit(float t) { 173 | float e = 0.016708634 - t * (0.000042037 + 0.0000001267 * t); 174 | return e; // unitless 175 | } 176 | 177 | 178 | /* --------------------------- SOLAR DECLINATION ---------------------------- */ 179 | float Dusk2Dawn::sunDeclination(float t) { 180 | float e = obliquityCorrection(t); 181 | float lambda = sunApparentLong(t); 182 | 183 | float sint = sin(degToRad(e)) * sin(degToRad(lambda)); 184 | float theta = radToDeg(asin(sint)); 185 | return theta; // in degrees 186 | } 187 | 188 | 189 | float Dusk2Dawn::sunApparentLong(float t) { 190 | float o = sunTrueLong(t); 191 | float omega = 125.04 - 1934.136 * t; 192 | float lambda = o - 0.00569 - 0.00478 * sin(degToRad(omega)); 193 | return lambda; // in degrees 194 | } 195 | 196 | 197 | float Dusk2Dawn::sunTrueLong(float t) { 198 | float l0 = geomMeanLongSun(t); 199 | float c = sunEqOfCenter(t); 200 | float O = l0 + c; 201 | return O; // in degrees 202 | } 203 | 204 | 205 | float Dusk2Dawn::sunEqOfCenter(float t) { 206 | float m = geomMeanAnomalySun(t); 207 | float mrad = degToRad(m); 208 | float sinm = sin(mrad); 209 | float sin2m = sin(mrad * 2); 210 | float sin3m = sin(mrad * 3); 211 | float C = sinm * (1.914602 - t * (0.004817 + 0.000014 * t)) + sin2m * (0.019993 - 0.000101 * t) + sin3m * 0.000289; 212 | return C; // in degrees 213 | } 214 | 215 | 216 | /* ------------------------------- HOUR ANGLE ------------------------------- */ 217 | float Dusk2Dawn::hourAngleSunrise(float lat, float solarDec) { 218 | float latRad = degToRad(lat); 219 | float sdRad = degToRad(solarDec); 220 | float HAarg = (cos(degToRad(90.833)) / (cos(latRad) * cos(sdRad)) - tan(latRad) * tan(sdRad)); 221 | float HA = acos(HAarg); 222 | return HA; // in radians (for sunset, use -HA) 223 | } 224 | 225 | 226 | /* ---------------------------- SHARED FUNCTIONS ---------------------------- */ 227 | float Dusk2Dawn::obliquityCorrection(float t) { 228 | float e0 = meanObliquityOfEcliptic(t); 229 | float omega = 125.04 - 1934.136 * t; 230 | float e = e0 + 0.00256 * cos(degToRad(omega)); 231 | return e; // in degrees 232 | } 233 | 234 | 235 | float Dusk2Dawn::geomMeanLongSun(float t) { 236 | float L0 = 280.46646 + t * (36000.76983 + t * 0.0003032); 237 | while (L0 > 360) { 238 | L0 -= 360; 239 | } 240 | while (L0 < 0) { 241 | L0 += 360; 242 | } 243 | return L0; // in degrees 244 | } 245 | 246 | 247 | float Dusk2Dawn::geomMeanAnomalySun(float t) { 248 | float M = 357.52911 + t * (35999.05029 - 0.0001537 * t); 249 | return M; // in degrees 250 | } 251 | 252 | 253 | /* --------------------------- UTILITY FUNCTIONS ---------------------------- */ 254 | /* Convert Gregorian calendar date to Julian Day. 255 | */ 256 | float Dusk2Dawn::jDay(int year, int month, int day) { 257 | if (month <= 2) { 258 | year -= 1; 259 | month += 12; 260 | } 261 | 262 | int A = floor(year/100); 263 | int B = 2 - A + floor(A/4); 264 | return floor(365.25 * (year + 4716)) + floor(30.6001 * (month + 1)) + 265 | day + B - 1524.5; 266 | } 267 | 268 | 269 | /* Return fraction of time elapsed this century, AD 2000–2100. 270 | * 271 | * NOTE: 2,451,545 was the Julian day starting at noon UTC on 1 January AD 2000. 272 | * 36,525 is a Julian century. 273 | */ 274 | float Dusk2Dawn::fractionOfCentury(float jd) { 275 | return (jd - 2451545) / 36525; 276 | } 277 | 278 | 279 | float Dusk2Dawn::radToDeg(float rad) { 280 | return 180 * rad / PI; 281 | } 282 | 283 | 284 | float Dusk2Dawn::degToRad(float deg) { 285 | return PI * deg / 180; 286 | } 287 | 288 | 289 | /* Zero-pad a component of time, e.g. 1 → "01", 24 → "24". 290 | * 291 | * NOTE: Supports integers of up to only two digits. 292 | */ 293 | bool Dusk2Dawn::zeroPadTime(char *str, byte timeComponent) { 294 | if (timeComponent >= 100) { return false; } 295 | 296 | str[0] = (floor(timeComponent / 10)) + '0'; 297 | str[1] = (timeComponent % 10) + '0'; 298 | return true; 299 | } 300 | --------------------------------------------------------------------------------