My project was somehow inspired by this blog post: Self-adjusting clock with e-display

 

Idea

SunPathClock intro - whole setup

 

Nowadays GNSS receivers a so cheap that you don't only use them for their actual task, positioning, but also for more trivial tasks like giving the time. This was done in the above mentioned project. The only downside of this is that the receiver somehow needs to receive signals from the satellites. So in basements or other covered places this won't work. But with more sensitive receivers this gets less a concern.

 

Additionally when there is already a GNSS receiver in the system you could also use it's positioning feature. One goal would be to show the times for sunrise and sunset. Another goal is to draw the position of the sun at the location of the clock. These dates highly depend on the position. So this is a perfect fit. And this is what I am going to show in my project.

 

Hardware

 

My project is based on an Arduino MKR WiFi 1010.

As GNSS receiver I use the Adafruit Ultimate GPS Breakout (https://www.adafruit.com/product/746 ). This is based on the MTK3339 chipset.

As display I used my ArduHMI shield which I already introduced here: NFC-Badge - Update your badge with your smartphone - Design data of the HMI shield . The latest version of the shield has also an Arduino MKR connector. So it can be directly plugged on top of the Arduino MKR WiFi. The data of the HMI shield is available on github: https://github.com/generationmake/ArduHMIShield

The project is powered by an USB power bank.

 

Software

Libraries

 

To simplify development I use some Arduino libraries:

 

107-Arduino-NMEA-Parser (https://github.com/107-systems/107-Arduino-NMEA-Parser ) is used to decode the NMEA string from the GNSS receiver. The library is interrupt based and available on the Arduino Library Manager.

 

DogGraphicDisplay (https://github.com/generationmake/DogGraphicDisplay ) is used to control the display.

 

SolarPostion (https://github.com/KenWillmott/SolarPosition ) calculates the position of the sun based on your position and time.

 

The source code and formulas to calculate sunrise and sunset was found on this forum post: https://www.arduinoforum.de/arduino-Thread-Sonnenaufgang-untergang-f%C3%BCr-Steuerungen-berechnen

I modified the code to get some useful functions and get all the outputs in the main loop.

 

Additionally the Arduino Time Library (https://github.com/PaulStoffregen/Time ) was used to glue everything together.

 

Functions

 

After startup the sketch waits until it gets a time fix from the GNSS receiver. The sketch consists of only two different screens. You can switch from one screen to the other by pressing the up or down button on the ArduHMI shield.

 

SunPathClock - first screen

 

The first screen shows time and date on the top center. Additionally it prints the time of sunrise and sunset of that day.

 

SunPathClock - second screen

 

The second screen also shows date and time, now at the bottom center. Additionally it draws the postion of the sun as graphic. During the run of the day the sun will pass from left to right. additionally the numeric values of the postion of the sun are printed on the top right.

 

The following is a time laps video which shows the path of the sun during one day. Sorry for the bad quality.

 

 

Source code

 

This is the complete source code of the sketch:

 

/* 
 *  Arduino Sketch SunPathClock for Arduino MKR WiFi 1010
 */
#include <DogGraphicDisplay.h>
#include <ArduinoNmeaParser.h>
#include <SolarPosition.h>
#include "dense_numbers_8.h"
#include "ubuntumono_b_16.h"

// define some values used by the panel and buttons
#define btnRIGHT  0
#define btnUP     1
#define btnDOWN   2
#define btnLEFT   3
#define btnSELECT 4
#define btnNONE   5

void onRmcUpdate(nmea::RmcData const);

ArduinoNmeaParser parser(onRmcUpdate, NULL);
DogGraphicDisplay DOG;
volatile time_t global_timestamp=0;
volatile bool flag_rmc=0;
nmea::RmcData global_rmc;

/* ---------------- functions to control HMI ------------------ */
// read the buttons
int read_LCD_buttons()
{
  int adc_key_in = analogRead(2);      // read the value from the sensor 
  if (adc_key_in > 1000) return btnNONE; // We make this the 1st option for speed reasons since it will be the most likely result
  if (adc_key_in < 50)   return btnRIGHT;  
  if (adc_key_in < 250)  return btnUP; 
  if (adc_key_in < 450)  return btnDOWN; 
  if (adc_key_in < 650)  return btnLEFT; 
  if (adc_key_in < 850)  return btnSELECT;  

  return btnNONE;  // when all others fail, return this...
}

/* ---------------- functions to calculate sunset and sunrise ------------------ */
// code from https://www.arduinoforum.de/arduino-Thread-Sonnenaufgang-untergang-f%C3%BCr-Steuerungen-berechnen
// formulars were taken from http://lexikon.astronomie.info/zeitgleichung/
// subfunction to compute declination of sun
float sundeclination(int T) {
    // declination of sun in Radians
    // Formula 2008 by Arnold(at)Barmettler.com, fit to 20 years of average declinations (2008-2017)
    return 0.409526325277017*sin(0.0169060504029192*(T-80.0856919827619));
}

// subfunction to compute timedifference    
float timedifference(float Declination, float B) {
    // time of half sunpath. from sunrise to highest position
    return 12.0*acos((sin(-(50.0/60.0)*PI/180.0) - sin(B)*sin(Declination)) / (cos(B)*cos(Declination)))/PI;
}

// subfunction to compute equation of time
float equationoftime(int T) {
    return -0.170869921174742*sin(0.0336997028793971 * T + 0.465419984181394) - 0.129890681040717*sin(0.0178674832556871*T - 0.167936777524864);
}

// subfunction to compute sunrise
float compute_sunrise(int T, float B) {
    float DK = sundeclination(T);
    return 12 - timedifference(DK, B) - equationoftime(T);
}

// subfunction to compute sunset
float compute_sunset(int T, float B) {
    float DK = sundeclination(T);
    return 12 + timedifference(DK, B) - equationoftime(T);
}
// subfunction to compute day of year
int getdayofyear(time_t t)
{
  static int monthlength[]={31,28,31,30,31,30,31,31,30,31,30,31};
  int count=0;
  for(int i=0; i<(month(t)-1); i++) count+=monthlength[i];
  count+=day(t);
  return count;
}

// subfunction to compute it all  
void compute_sunset_sunrise(time_t t, float lon, float lat, float &sunrise, float &sunset) {
  int T=getdayofyear(t);
  float lat_rad = lat*M_PI/180.0;  // lat in radians
    
  // compute sunrise and sunset
  sunrise = compute_sunrise(T, lat_rad);    // sunrise at lon 0 degree
  sunset = compute_sunset(T, lat_rad);  // sunset at lon 0 degree
    
  sunrise    = sunrise   - lon /15.0; // sunrise at desired lon
  sunset  = sunset - lon /15.0; // sunset at desired lon
}

/* ---------------- functions to format and print time and date ------------------ */
char * totime(float x)
{
  static char timestr[]="00:00";
  timestr[0]=((unsigned int)(x/10.0)%10)+48;
  timestr[1]=((unsigned int)(x)%10)+48;
  unsigned int y=(unsigned int)x;
  x=x-(float)y;
  x=x*60.0;
  timestr[3]=((unsigned int)(x/10.0)%10)+48;
  timestr[4]=((unsigned int)(x)%10)+48;
  return timestr;
}
char * totimestrt(time_t t)
{
  tmElements_t someTime;
  breakTime(t, someTime);

  static char timestr[]="00:00:00";
  timestr[0]=((someTime.Hour/10)%10)+48;
  timestr[1]=((someTime.Hour)%10)+48;
  timestr[3]=((someTime.Minute/10)%10)+48;
  timestr[4]=((someTime.Minute)%10)+48;
  timestr[6]=((someTime.Second/10)%10)+48;
  timestr[7]=((someTime.Second)%10)+48;
  return timestr;
}
char * todatestrt(time_t t)
{
  tmElements_t someTime;
  breakTime(t, someTime);

  static char timestr[]="00.00.0000";
  timestr[0]=((someTime.Day/10)%10)+48;
  timestr[1]=((someTime.Day)%10)+48;
  timestr[3]=((someTime.Month/10)%10)+48;
  timestr[4]=((someTime.Month)%10)+48;
  timestr[6]=((tmYearToCalendar(someTime.Year)/1000)%10)+48;
  timestr[7]=((tmYearToCalendar(someTime.Year)/100)%10)+48;
  timestr[8]=((tmYearToCalendar(someTime.Year)/10)%10)+48;
  timestr[9]=((tmYearToCalendar(someTime.Year))%10)+48;
  return timestr;
}

/* ---------------- arduino functions ------------------ */
void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
//  while(!Serial);
  Serial.println("SunPathClock");
  Serial1.begin(9600);

  // set Time clock to Jan. 1, 2000
  setTime(SECS_YR_2000);

  // set the Time library time service as the time provider
  SolarPosition::setTimeProvider(now);

  pinMode(A6, OUTPUT);   // set backlight pin to output
  digitalWrite(A6, HIGH);

  DOG.begin(6,0,0, 0, 1,DOGM128);
  DOG.clear();
  DOG.createCanvas(128, 64, 0, 0, 1);  // Canvas in buffered mode

  DOG.string(0,3,UBUNTUMONO_B_16,"SunPathClock",ALIGN_CENTER); // print "SunPathClock" in line 3, centered
  DOG.string(0,5,UBUNTUMONO_B_16,"data not valid",ALIGN_CENTER); // print "not valid" in line 5 
}

void loop() {
  // put your main code here, to run repeatedly:
  static bool display_screen=0;
  char buf[30];
  float lon=11.50;
  float lat=48.10;

  float sunrise=0;
  float sunset=0;

  while (Serial1.available()) {
    parser.encode((char)Serial1.read());
  }
  int lcd_key = read_LCD_buttons();  // read the buttons
  switch (lcd_key)               // depending on which button was pushed, we perform an action
  {
    case btnUP:               // up
      {
        display_screen=1;
        break;
      }
    case btnDOWN:               // down
      {
        display_screen=0;
        break;
      }
  }
  if(flag_rmc)
  {
    flag_rmc=0;
    global_timestamp = nmea::toPosixTimestamp(global_rmc.date, global_rmc.time_utc);
    setTime(global_timestamp);

    SolarPosition sunpos(lat, lon); 
    if (global_rmc.is_valid)
    {
      lon=global_rmc.longitude;
      lat=global_rmc.latitude;
    }

    compute_sunset_sunrise(global_timestamp, lon, lat, sunrise, sunset);

    DOG.clear();
    if(display_screen==0)
    {
      DOG.string(0,2,UBUNTUMONO_B_16,totimestrt(global_timestamp+3600), ALIGN_CENTER); // print time
      DOG.string(0,0,UBUNTUMONO_B_16,todatestrt(global_timestamp+3600), ALIGN_CENTER); // print date
  
      DOG.string(0,4,UBUNTUMONO_B_16,"sunrise",ALIGN_LEFT);
      DOG.string(0,6,UBUNTUMONO_B_16,totime(sunrise+1),ALIGN_LEFT);
      DOG.string(0,4,UBUNTUMONO_B_16,"sunset",ALIGN_RIGHT);
      DOG.string(0,6,UBUNTUMONO_B_16,totime(sunset+1),ALIGN_RIGHT);
    }
    else
    {
      DOG.clearCanvas();
      DOG.drawLine(0,32,128,32); // draw horizon
      DOG.drawCircle(128*(sunpos.getSolarAzimuth()/360.0), 32-40*(sunpos.getSolarElevation()/180.0), 5, true); // draw sun
      DOG.flushCanvas();
      String azimuth_str(sunpos.getSolarAzimuth());
      DOG.string(0,0,DENSE_NUMBERS_8,azimuth_str.c_str(),ALIGN_RIGHT);
      String ele_str(sunpos.getSolarElevation());
      DOG.string(0,1,DENSE_NUMBERS_8,ele_str.c_str(),ALIGN_RIGHT);
      DOG.string(0,6,DENSE_NUMBERS_8,totimestrt(global_timestamp+3600), ALIGN_CENTER); // print time
      DOG.string(0,7,DENSE_NUMBERS_8,todatestrt(global_timestamp+3600), ALIGN_CENTER); // print date
    }
  }
}

/* ---------------- interrupt functions ------------------ */
void onRmcUpdate(nmea::RmcData const rmc)
{
  global_rmc=rmc;
  flag_rmc=1;
}


Holder

(update 2021-02-25)

In the last days I constructed a holder so that the SunPathClock can sit comfortably on the desk. Additionally I also constructed a protection frame for the display. The display has a glass substrate and may be easily damaged. If the display falls only sligthly on one edge and then the glass cracks and the display is destroyed. I already destroyed several of them. The frame hopefully reduces this risc.

 

The holder was produced with a 3D printer. The production data for the holder is available on the github repository for the shield: https://github.com/generationmake/ArduHMIShield/

 

SunPathClock with holder - front rightSunPathClock with holder - front leftSunPathClock with holder - back side