How to control 3 LED's using SunriseSunset data from SolarCalculator library

Very first Arduino project so I need a lot of help.

Hardware

  • Arduino Uno R4 Minima
  • DS3231 (RTC)
  • 270 Ohm resistors
  • 3-3.2V, 20mA LED

Goal
I'm trying to turn on 1 LED during the day, another during golden hour (sunrise~20min after sunrise & 20min before sunset~sunset) for a traffic light type art piece I'm making.

So Far
I've uploaded the "ds3231" example code from RTClib...

// Date and time functions using a DS3231 RTC connected via I2C and Wire lib
#include "RTClib.h"

RTC_DS3231 rtc;

char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};

void setup () {
  Serial.begin(57600);

#ifndef ESP8266
  while (!Serial); // wait for serial port to connect. Needed for native USB
#endif

  if (! rtc.begin()) {
    Serial.println("Couldn't find RTC");
    Serial.flush();
    while (1) delay(10);
  }

  if (rtc.lostPower()) {
    Serial.println("RTC lost power, let's set the time!");
    // When time needs to be set on a new device, or after a power loss, the
    // following line sets the RTC to the date & time this sketch was compiled
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
    // This line sets the RTC with an explicit date & time, for example to set
    // January 21, 2014 at 3am you would call:
    // rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0));
  }

  // When time needs to be re-set on a previously configured device, the
  // following line sets the RTC to the date & time this sketch was compiled
  // rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  // This line sets the RTC with an explicit date & time, for example to set
  // January 21, 2014 at 3am you would call:
  // rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0));
}

void loop () {
    DateTime now = rtc.now();

    Serial.print(now.year(), DEC);
    Serial.print('/');
    Serial.print(now.month(), DEC);
    Serial.print('/');
    Serial.print(now.day(), DEC);
    Serial.print(" (");
    Serial.print(daysOfTheWeek[now.dayOfTheWeek()]);
    Serial.print(") ");
    Serial.print(now.hour(), DEC);
    Serial.print(':');
    Serial.print(now.minute(), DEC);
    Serial.print(':');
    Serial.print(now.second(), DEC);
    Serial.println();

    Serial.print(" since midnight 1/1/1970 = ");
    Serial.print(now.unixtime());
    Serial.print("s = ");
    Serial.print(now.unixtime() / 86400L);
    Serial.println("d");

and gotten the correct time output.

I plan
on using the "SunriseSunset" example code from the SolarCalculator library to get the times for sunrise and sunset.

#include <SolarCalculator.h>

void setup()
{
  Serial.begin(9600);

  // Date
  int year = 2000;
  int month = 1;
  int day = 1;

  // Location
  double latitude = 42.36;
  double longitude = -71.058;
  int utc_offset = -5;

  double transit, sunrise, sunset;

  // Calculate the times of sunrise, transit, and sunset, in hours (UTC)
  calcSunriseSunset(year, month, day, latitude, longitude, transit, sunrise, sunset);

  // Get the approximate times (minimum program size) (iterations = 0)
  //calcSunriseSunset(year, month, day, latitude, longitude, transit, sunrise, sunset, SUNRISESET_STD_ALTITUDE, 0);

  // Print results
  char str[6];
  Serial.println(hoursToString(sunrise + utc_offset, str));
  Serial.println(hoursToString(transit + utc_offset, str));
  Serial.println(hoursToString(sunset + utc_offset, str));
}

void loop()
{
}

// Rounded HH:mm format
char* hoursToString(double h, char* str)
{
  int m = int(round(h * 60));
  int hr = (m / 60) % 24;
  int mn = m % 60;

  str[0] = (hr / 10) % 10 + '0';
  str[1] = (hr % 10) + '0';
  str[2] = ':';
  str[3] = (mn / 10) % 10 + '0';
  str[4] = (mn % 10) + '0';
  str[5] = '\0';
  return str;
}

My question's are...

  1. How do I use the date I'm getting from my RTC for the SunriseSunset code? I see places in the code for manually inputting date but I'm not sure how to automatically use the date I have on my RTC for this part of the code.

  2. I'm thinking from there the code would be something like

void loop(){
  if(current time == sunrise + 20min){ // time from RTC = sunrise calculated by sunrisesunset code + 20 minutes
    digitalWrite(13,HIGH); // first LED for daytime on
   
  }
  else{ 
    digitalWrite(13,LOW);// turn the LED off
  }

but I'm unsure of the specifics, like how to keep the LED on from the point it is sunrise+20min till it is Sunset-20min and not just turn on the LED for that 1 minute of time where sunrise+20 is true.

Any amount of guidance would be greatly appreciated. I know only how to carve wood.

Welcome! You will want to learn about state machines, I suggest you start here:

Think of your program as having four states: Night, Sunrise, Day, Sunset. Your code need simply decide which state it is in based on the time information you’re already aware of, and then execute one of four states.

I suggest you read up, then make an attempt, and come back to us when you have questions.

Making your LEDs turn on and off with the RTC is like setting an alarm on a clock. Have a look at the "alarm" example from the library you are using: RTClib/examples/DS3231_alarm/DS3231_alarm.ino at master · adafruit/RTClib · GitHub

look this over

  • it shows how the time in hours:mins is more conveniently handled in terms of mins for an entire day
  • it shows how the current, sunrise and sunset can be compared in terms of minute to recognize the various "windows" of time throughout the day
  • it uses a simple function to simulate the use of an RTC to get the time, hour:mins. You posted a similar function to get the sunrise/sunset times
  • a print to indicates which LED is turned on.
char s [90];

// -----------------------------------------------------------------------------
struct DateTime {
    unsigned hour;
    unsigned mins;
};

DateTime getTime ()
{
    DateTime time;

    unsigned long mins5 = 5 *millis () /200;   // 25 mins /sec
    time.mins =     mins5 %60;
    time.hour = 1 +(mins5 /60) %24;

    return time;
}

// -----------------------------------------------------------------------------
enum { LedDay, LedNight, LedGolden };

const char *LedStr [] = { "Day", "Night", "Golden" };

int ledIdxLst;

void turnOnLed (
    int ledIdx )
{
    if (ledIdxLst != ledIdx)  {
        ledIdxLst  = ledIdx;

        sprintf (s, "    turnOnLed: %d %s", ledIdx, LedStr [ledIdx]);
        Serial.println (s);
    }
}

// -----------------------------------------------------------------------------
DateTime dtSunrise = {  7, 30 };
DateTime dtSunset  = { 16, 10 };

void loop ()
{
    unsigned sunriseMins = 60*dtSunrise.hour +dtSunrise.mins;
    unsigned sunsetMins  = 60*dtSunset .hour +dtSunset .mins;

    DateTime now = getTime ();
    unsigned mins = 60*now.hour + now.mins;

    sprintf (s, " %2d:%02d %2d:%02d %2d:%02d", now.hour, now.mins,
        dtSunrise.hour, dtSunrise.mins, dtSunset.hour, dtSunset.mins );
    Serial.println (s);

    if (mins < sunriseMins -20)
        turnOnLed (LedNight);
    else if (mins < sunriseMins)
        turnOnLed (LedGolden);
    else if (mins < sunsetMins -20)
        turnOnLed (LedDay);
    else if (mins < sunsetMins)
        turnOnLed (LedGolden);
    else
        turnOnLed (LedNight);

    delay (250);
}


// -----------------------------------------------------------------------------
void setup ()
{
    Serial.begin (9600);
}

When your "alarm time" arrives, call the Sunrise/Sunset function.

#include <RTClib.h> // the library
RTC_DS1307 rtc; // create an object/instance to control
int secOld, secNew; // variables to store "alarms"

void setup () {
  Serial.begin(115200); // start serial comms
  rtc.begin(); // start rtc
  pinMode(LED_BUILTIN, OUTPUT); // confiure led pin for output
}

void loop () {
  DateTime now = rtc.now(); // read time
  secNew = now.second(); // store "seconds"

  if (secOld != secNew) { // compare old "alarm" (seconds) with new "alarm" (seconds)
    secOld = secNew; // store new "alarm" start
    sunRiseSet(); // call a function
  }
}

void sunRiseSet() {
  digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // flip the LED_BUILTIN state
  Serial.print(digitalRead(LED_BUILTIN)); // see it in the Serial Monitor
}

In my clocks I gently dim the LEDs around dusk/dawn.
So not time related, but solar angle related.
Which of course happens slower in winter than in summer.
Solar angle can be found this way.
calcHorizontalCoordinates(now, latitude, longitude, az, el);
Then I map elevation to PWM values for the LED.
I get 'now' from NTP, not from a RTC.
Maybe this works for a RTC.
calcHorizontalCoordinates(rtc.now(), latitude, longitude, az, el);
Leo..

Hi @Wawa !

I assume rtc.unixtime() should do it. It calculates and returns the RTC’s time data converted to UTC

 /* 32-bit times as seconds since 1970-01-01. */

uint32_t unixtime(void) const;

which is one of the formats accepted by the SolarCalculator library

void calcHorizontalCoordinates(unsigned long utc, double latitude, double longitude, double& azimuth, double& elevation);

void calcHorizontalCoordinates(int year, int month, int day, int hour, int minute, int second, double latitude, double longitude, double& azimuth, double& elevation);

Thank you all for the suggestions. I've been googling line by line the example sketches to try to make sense of this language.

There was another example sketch in SolarCalculator that used TimeLib; making it easier to figure out how to use the time from my RTC for the calc function. Leaving the prints for now just to check accuracy.

#include <SolarCalculator.h>
#include <TimeLib.h>
#include <Wire.h>
#include <RTClib.h>

RTC_DS3231 rtc;

time_t getRTCsync() {
  return rtc.now().unixtime();
}

// Location
double latitude = 42.36;
double longitude = -71.058;
int utc_offset = -5;

void setup()
{
  Serial.begin(9600);
  Wire.begin();
  rtc.begin();

  if (!rtc.begin()) { /* error */ }

  setSyncProvider(getRTCsync);
  setSyncInterval(3600);

  double transit, sunrise, sunset;  // Event times, in hours (UTC)
  double eq;                        // Equation of time, in minutes
  double ra, dec, r;                // Equatorial coordinates, in degrees and AUs
  double az, el;                    // Horizontal coordinates, in degrees

  // Set system time to rtc time
  setTime(getRTCsync() - utc_offset * 3600L);

  // Set time manually (hr, min, sec, day, mo, yr)
  //setTime(0, 0, 0, 1, 1, 2000);

  // Get current time
  time_t utc = now();

  calcEquationOfTime(utc, eq);
  calcEquatorialCoordinates(utc, ra, dec, r);
  calcHorizontalCoordinates(utc, latitude, longitude, az, el);
  calcSunriseSunset(utc, latitude, longitude, transit, sunrise, sunset);

  // Print results
  Serial.print(F("Sunrise: "));
  printSunTime24h(sunrise + utc_offset);
  Serial.print(F("Transit: "));
  printSunTime24h(transit + utc_offset);
  Serial.print(F("Sunset:  "));
  printSunTime24h(sunset + utc_offset);
}

void loop()
{
}

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);
}

Because the event times are given as hours(UTC) and the system time is set to seconds(Unix) I'm having trouble wrapping my mind around how I'll compare the two for an if statement. Probably convert both to minutes like gcjr suggested?

Sunrise transition (different light colour) takes longer in winter than in summer.
To me it makes more sense to make the light dependent on solar elevation, not on times before or after sunrise.

For elevation you only need UTC and lat/long.
The line I gave you returns sun elevation angle, which you can map to LED colour.
Say red fading in between -4 and +1 degree and green/blue between any degree of your choice.
I usually multiply angle first by 100 and the cast to int.
Then say 3.456 degrees becomes 346, and can be mapped easier to a PWM value.
Example:

int sun value = el * 100; // cast to int x 100
int redValue = map(now, -400, 100, 0, 255); // red fade-in between -4 degree and +1 degree
```
Leo..

I've got some Sonoff devices that control lighting based around sunset/sunrise times.
Most of the time that's OK.
But this year in the UK, with the exceptionally poor weather and thick cloud cover, they really could have done with a light level input as well.
Looking ahead, a lot more of our day to day living is going to have to take more account of our changing planet.

if you bring your time to an integer (for example as HHMM calculated like hour*100 + minute) you can compare them quite easily.

here I have a demo sketch
time - Wokwi ESP32, STM32, Arduino Simulator

I honestly skipped over the sun elevation angle calculations cause I didn't know what they could be used for, but what you're saying does seem to make more sense than just an arbitrary time frame after sunrise to determine golden hour. Quick google says 0 to 6 degrees above the horizon is golden hour so that could be the way to go.

Your lamp project could be done easier with a small ESP32 board.
I would use the XIAO ESP32-C3 for this.
It can get the time off the internet with one line of code. No RTC needed.
The whole project would be the size of a matchbox.
Leo..

Just personal preference to not use wifi/bluetooth.

Seems like I've gotten it working the way I'd wanted.

#include <SolarCalculator.h>
#include <TimeLib.h>
#include <Wire.h>
#include <RTClib.h>

RTC_DS3231 rtc;

time_t getRTCsync() {
  return rtc.now().unixtime();
}

// Location
double latitude = 42.36;
double longitude = -71.058;
int utc_offset = -5;

int day_led = 12;
int gold_led = 10;
int night_led = 8;

unsigned long previousMillis;
const long interval = 60000UL;

void setup()
{
  Serial.begin(9600);
  delay(6000);
  Wire.begin();
  rtc.begin();

  pinMode(day_led, OUTPUT);
  pinMode(gold_led, OUTPUT);
  pinMode(night_led, OUTPUT);

  if (!rtc.begin()) { /* error */ }

  setSyncProvider(getRTCsync);
  setSyncInterval(3600);


  // Set system time to rtc time
  setTime(getRTCsync() - utc_offset * 3600L);

  
}

void loop()
{
  time_t utc = now();

  double transit, sunrise, sunset;  // Event times, in hours (UTC)
  double eq;                        // Equation of time, in minutes
  double ra, dec, r;                // Equatorial coordinates, in degrees and AUs
  double az, el;                    // Horizontal coordinates, in degrees
  
  calcHorizontalCoordinates(utc, latitude, longitude, az, el);

  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval)
  {
    previousMillis = currentMillis;

   if (el > 6){
    digitalWrite(day_led,HIGH);
    Serial.println("day");
   }
   else if(el <= 6 && el >= -4){
    digitalWrite(gold_led,HIGH);
    Serial.println("gold");
   }
   else if(el < -4){
    digitalWrite(night_led,HIGH);
    Serial.println("night");
   }
   else {
    Serial.println("IM NOT OK");
   }
  }
}

Moved el calc function to loop, millis delay to loop every minute, simple if/ if else to light LED depending on elevation.

It really was much simpler and more accurate to use sun elevation angle so thank you Wawa and thank you all for the suggestions.

Security?

In my clock projects I fire up WiFi on power-up and/or just long enough to update NTP.
Then I put WiFi to sleep for half a day (maybe 2 sec drift).
A sunrise lamp can have it's WiFi cut off for several days and still be accurate enough.
Leo..