My project was somehow inspired by this blog post: Self-adjusting clock with e-display
Idea
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.
The first screen shows time and date on the top center. Additionally it prints the time of sunrise and sunset of that day.
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/
Top Comments