SolarTracer – Solar Azimuth & Elevation

Ideas sometimes come from surprising places. This weekend we hosted some of Sian’s (my daughter) friends, a couple of whom were interested in my obsession with time and making clocks. Looking at my Graphic LCD Analogue Clock one puzzled over solar azimuth and elevation. As I described the sun’s trajectory across the sky an interesting way to demonstrate this movement came to mind and voila, the SolarTracer, an indoor sundial. From the perspective of someone at the centre of the base, the SolarTracer tracks the position of the sun – its azimuth (East to West) and its elevation to the horizon – in real-time thoughout the day.

Solar Tracer displaying the sun's azimuth and elevation

SolarTracer displaying the sun’s azimuth and elevation. In this image, taken around solar noon, the azimuth was 183 degrees (S) and the elevation was 65 degrees from the horizontal. North is to the right. You can see the shadow cast by the illuminated LED that represents the sun’s position in the sky.

UPDATE [150528]:  I made a second SolarTracer to move the azimuth point of rotation to the centre of the base. The azimuth is controlled by a servo motor that moves from East (90º) to West  (270º) over the course of the day. The sun’s elevation is displayed using a strip of 15 LEDs arranged on the inside of an arc that spans from 0º – 90º to the horizontal.

SolarTracer: second build

SolarTracer: SECOND BUILD using a servo motor for azimuth and a strip of 15 LEDs on a quarter circle for elevation

Apart from showing real time solar position, the SolarTracer provides a useful tool to describe the passage of the sun across the sky and concepts such as solar noon, the summer and winter solstices and spring and fall equinoxes. Eminently hackable and a great conversation piece!

According to Wikipedia, the position of the sun “in the sky is a function of both time and the geographic coordinates of the observer on the surface of the Earth. As the Earth moves around the Sun during the course of the year, the Sun appears to move with respect to the fixed stars on the celestial sphere, along a path called the “ecliptic”. The Earth’s rotation about its axis causes the fixed stars to move in the sky in a way that depends on the observer’s geographic latitude. The time when a given fixed star crosses the observer’s meridian depends on the geographic longitude. To find the Sun’s position for a given observer at a given time, one may therefore proceed in three steps: 1) calculate the Sun’s position in the ecliptic coordinate system, 2) convert to the equatorial coordinate system, and 3) convert to the horizontal coordinate system, for the observer’s local time and position.”

The Arduino software performs these latter three steps using the sundata.h library that calculates both elevation and azimuth angles in real time, once the current location (latitude and longitude) and time offset to GMT is known; the latter using the simple DST algorithm discussed before. Initially,

BUILD 1: The SolarTracer consists of a base onto which a semicircular strip of 36 addressable LEDs is mounted. The angle of the LED strip to the base is controlled by a stepper motor. An Arduino Nano with a real-time clock and stepper motor driver controls the angle of the strip (solar elevation) and the LED which is illuminated (solar azimuth). it is important to determine the number of steps the stepper motor takes to raise the strip through 90º – in my case it was 520.

SolarTracer

SolarTracer

The elevation angle is continuously calculated and the stepper motor instructed to move the appropriate number of steps and direction to follow changes. Similarly, the azimuth angle – from East (90º) to West  (270º) – is continuously calculated and mapped to the appropriate LED of the strip. The 36 LED strip was cut from a longer length such that each LED represent 5º of azimuth.

Here’s the Arduino code for SolarTracer BUILD 1 … Enjoy!

[codesyntax lang=”php” title=”SolarTracer: Build 1 Arduino Code” blockstate=”collapsed”]

//**************************************************************************************************
//                                        SOLAR TRACER
//                                   Adrian Jones, May 2015
//
//**************************************************************************************************

// Build 1
//   r1 150315 - initial build
// 
//********************************************************************************************
#define build 1
#define revision 1
//********************************************************************************************

#include <avr/pgmspace.h>                // for memory storage in program space

// LED strip
#include "FastLED.h"
#define DATA_PIN 9                       // LED strip data transfer pin
#define LED_COUNT 36          
CRGB leds[LED_COUNT];
byte bright = 255, sat = 0, hue = 0;     // colour of azimuth LED

// Stepper motor driver
#include <Stepper.h>
#define STEPS 200                        // number of steps of stepper motor
Stepper stepper(STEPS, 4, 6, 5, 7);      // create an instance of the stepper class
const byte sspeed = 30;                  // speed of movement
const byte steppins[] = {4,5,6,7};       // all stepper pins 
#define tsteps 520                       // number of steps from 0 - 90 degrees

// Sun Position library
#include <sundata.h>
#define LONGITUDE -75.64316
#define LATITUDE  45.66167
sundata vdm(0,0,0);

// cardinal directions
const char ca0[]  PROGMEM = "N  ";
const char ca1[]  PROGMEM = "NNE";
const char ca2[]  PROGMEM = "NE ";
const char ca3[]  PROGMEM = "ENE";
const char ca4[]  PROGMEM = "E  ";
const char ca5[]  PROGMEM = "ESE";
const char ca6[]  PROGMEM = "SE ";
const char ca7[]  PROGMEM = "SSE";
const char ca8[]  PROGMEM = "S  ";
const char ca9[]  PROGMEM = "SSW";
const char caA[]  PROGMEM = "SW ";
const char caB[]  PROGMEM = "WSW";
const char caC[]  PROGMEM = "W  ";
const char caD[]  PROGMEM = "WNW";
const char caE[]  PROGMEM = "NW  ";
const char caF[]  PROGMEM = "NNW";
const char * const cas[] PROGMEM = { ca0,ca1,ca2,ca3,ca4,ca5,ca6,ca7,ca8,ca9,caA,caB,caC,caD,caE,caF };

char cb[10];                 // character buffer for character strings. Set dimension to length of longest string
float el_deg, az_deg, sunup, sundn;
int new_el, old_el = 0, az = 0, old_az = 0;
boolean nighttime;
static int TZ;


// RTC
#include <Wire.h>
#include "RTClib.h"
RTC_DS1307 RTC;
int hr, mn, se, osec=-1, omin=-1, ohr=-1, dy, ody=0, mo, yr, dw;

// eeprom storage
#include <EEPROM.h>               // for dst data storage
int dst;                          // dst setting                      
int *set[] = { &dst };            // values to set/recover from EEPROM


//*****************************************************************************************//
//                                      Initial Setup
//*****************************************************************************************//
void setup() {
  
  Serial.begin(57600);
  Serial.println(F("SOLAR TRACER by Adrian Jones"));
  Serial.print(F("Build ")); Serial.print(build); Serial.print(F(".")); Serial.println(revision);
  Serial.print(F("Free RAM: "));  Serial.print(freeRam()); Serial.println(F("B"));
  Serial.println(F("***************************"));

  FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, LED_COUNT);   // create instance of LED Strip

  stepper.setSpeed(sspeed);                                   // set speed of stepper motor 
  
  Wire.begin();
  RTC.begin();                                                // start RTC
  getTime();                                                  // get current time

  if (! RTC.isrunning() || (String(__DATE__).lastIndexOf(String(yr)) == -1)) {
    RTC.adjust(DateTime(__DATE__, __TIME__));                // adjust if year is off
    Serial.print(F("RTC reset to ")); Serial.print(__DATE__); Serial.print(F(" ")); Serial.println(__TIME__);
  } 
   
  // RTC.adjust(DateTime(__DATE__, __TIME__));          // reset to system time
  // RTC.adjust(DateTime(2015, 1, 18, 1, 59, 50 ));     // test
  
  TZ = (IsDST)?-4:-5;                                       // adjust GMT offset based on DST  
  vdm = sundata(LATITUDE, LONGITUDE, TZ);                   // create sundata instance for current location
  restoreSettings();                                        // restore EEPROM settings 
 }
 
//*****************************************************************************************//
//                                         MAIN LOOP
//*****************************************************************************************//
void loop() {
  getTime();                                         // get current time
  doDSTAdjust();                                     // adjust dst if necessary
  if(se != osec) {                                   // every second...
    vdm.time(yr, mo, dy, hr, mn, se);                // current year, month, day, hour, minutes and seconds
    vdm.calculations();                              // update calculations for current time
    el_deg = vdm.elevation_deg();                    // store sun's elevation in x.xx degrees
    az_deg = vdm.azimuth_deg();                      // store sun's azimuth in x.xx degrees
    sunup  = vdm.sunrise_time();                     // store sunrise time in decimal form
    sundn  = vdm.sunset_time();                      // store sunset time in decimal form
    nighttime = (el_deg<0.0)? true:false;            // has the sun set ?
  
    new_el = max(0, int(el_deg*tsteps/90.0));        // translate elevation to #steps from horizontal
    doNewStep(old_el,new_el);                        // move to new elevation
    old_el = new_el;                                 // update elevation 

    az = max(0, (az_deg-90)/5);                      // translate azimuth to East (0) to West (35) LED
    az = LED_COUNT - min(az,LED_COUNT-1) - 1;        // adjust to suit orientation of strip
    leds[old_az] = CHSV(hue, sat, 0);                // light appropriate LED
    leds[az] = (nighttime)? CHSV(hue, sat, 0) : CHSV(hue, sat, bright); 
    FastLED.show();                                  // show it
    old_az=az;                                       // update azimuth
    
    doSerial();                                      // print everything out  

    osec=se; omin=mn; ohr=hr%12;                     // update time
  }
  delay(100);
}

//*****************************************************************************************//
//                                      MAIN LOOP
//*****************************************************************************************//

// doNewStep: moves stepper (os - ns) "steps" in appropriate direction 
void doNewStep(int os, int ns) {
  if(ns == os) return;                              // no steps to move so do nothing
  int dir = (ns > os)?1:-1;                         // set direction (+ve is greater elevation)
  for(int x = 0; x < abs(ns-os); x++) {             // set number of steps 
    stepper.step(dir);                              // move
  } 
}

void clearStepper() { for(byte x=0; x<sizeof(steppins); x++) { digitalWrite(steppins[x],LOW); } }

void doSerial() {
    Serial.print(yr-2000); Serial.print(F("/"));
    if(mo<10) Serial.print(F("0")); Serial.print(mo); Serial.print(F("/"));
    if(dy<10) Serial.print(F("0")); Serial.print(dy); Serial.print(F(" "));
    if(hr<10) Serial.print(F("0")); Serial.print(hr); Serial.print(F(":"));
    if(mn<10) Serial.print(F("0")); Serial.print(mn); Serial.print(F(":"));
    if(se<10) Serial.print(F("0")); Serial.print(se); 

    Serial.print(F("\tE: ")); Serial.print(el_deg); Serial.print(F(" deg. [")); Serial.print(new_el); Serial.print(F("]")); 
    Serial.print(F("\tA: ")); Serial.print(az_deg); Serial.print(F(" deg. ")); Serial.print(getCardinal(az_deg)); Serial.print(F(" [")); Serial.print(az); Serial.print(F("]"));
//    Serial.print(F(" right ascension = ")); Serial.print(Sky[0], 5); Serial.print(F(" declination = ")); Serial.print(Sky[1], 5); 
    Serial.println(); 
}


// ********************************************************************************** //
//                                      OPERATION ROUTINES
// ********************************************************************************** //
// FREERAM: Returns the number of bytes currently free in RAM  
int freeRam(void) {
  extern int  __bss_end, *__brkval; 
  int free_memory; 
  if((int)__brkval == 0) {
    free_memory = ((int)&free_memory) - ((int)&__bss_end); 
  } 
  else {
    free_memory = ((int)&free_memory) - ((int)__brkval); 
  }
  return free_memory; 
}


/* ********************************************************************************** //
//                                       TIME ROUTINES
// ********************************************************************************** //
   In most of Canada Daylight Saving Time begins at 2:00 a.m. local time on the second Sunday in March. 
   On the first Sunday in November areas on DST return to Standard Time at 2:00 a.m. local time. 
*/

// IsDST returns true if DST, false otherwise
boolean IsDST() {
  if (mo < 3 || mo > 11) { return false; }                // January, February, and December are out.
  if (mo > 3 && mo < 11) { return true;  }                // April to October are in
  int previousSunday = dy - dw;                               
  if (mo == 3) { return previousSunday >= 8; }            // In March, we are DST if our previous Sunday was on or after the 8th.
  return previousSunday <= 0;                             // In November we must be before the first Sunday to be DST. That means the previous Sunday must be before the 1st.
}

// getTime obtains current time from RTC
void getTime() {
  DateTime now = RTC.now();
  hr = now.hour(); if(hr==0) hr=24;                      // adjust to 1-24
  mn = now.minute();
  se = now.second();
  yr = now.year();
  mo = now.month();
  dy = now.day();
  dw = now.dayOfWeek();
}

// doDSTAdjust increments (or decrements) by one hour when entering (or leaving) DST
void doDSTAdjust() {
  if(dst == IsDST()) return;                    // if prior setting is same as DST setting, do nothing
  if(hr != 2) return;                           // do nothing until 2pm

  DateTime now = RTC.now();                     // get time
  if(IsDST() && !dst) {
    DateTime newTime (now.unixtime() + 3600);   // add one hour
    RTC.adjust(newTime);                        
  }
  if(!IsDST() && dst) {
    DateTime newTime (now.unixtime() - 3600);   // subtract one hour
    RTC.adjust(newTime);
  }
  dst = IsDST(); 
  saveSettings();                // save change to DST
}

// ********************************************************************************** //
//                         MOON PHASE AND MOON RISE/ SET ROUTINES
// ********************************************************************************** //

// moonphase:  the phase day (0 to 29, where 0=new moon, 15=full etc.): see http://www.ben-daglish.net/moon.shtml
int moonphase() {
  long lp = 2551443;                                   // 2551223 secs / lunar cycle. new scaling for 0-26 (i.e. 27 unique) bitmaps
  DateTime now = RTC.now();
  DateTime new_moon (1970, 1, 7, 20, 35, 0);
  long ph = (now.unixtime() - new_moon.unixtime()) % lp;  // scales to 0-29 days
  ph = floor (ph/3600L);                                    
  ph = map(ph, 0.0, 29*24.0, 0, 26);                      // map to 0-26
  return ph;  
}

// getCardinal: converts the headsing (in degrees) to a text-based cardinal heading ("ENE", for example)
String getCardinal( float h ) { 
  h += 11.25; if (h > 360.0) h = h-360.0;
  strcpy_P(cb, (char*) pgm_read_word(&(cas[(int) (h / 22.5)]) ) );
  return cb;  
}

// ********************************************************************************** //
//                                       EEPROM ROUTINES
// ********************************************************************************** //
// EEPROM save, restore and set/save defaults
void saveSettings()    { for(int addr = 0; addr < sizeof(set)/2; addr++) { EEPROM.write(addr, *set[addr]); } }    // save
void restoreSettings() { for(int addr = 0; addr < sizeof(set)/2; addr++) { *set[addr] = EEPROM.read(addr); } }   // recover EEPROM value

[/codesyntax]

BUILD 2: The second SolarTracer consists of a base in which a centrally-mounted servo motor is located. This servo attaches to a tower that supports an arc of plastic, to which is attached a strip of 15 addressable LEDs. The angular position of the LED strip is controlled by the servo motor that swings from 0-180 which represents the sun’s azimuth from East (90º) to West  (270º). The sun’s elevation is mapper to the strip of15 LEDs such that LED0 represents 0º (horizontal) and LED14 represents 90º (vertical) relative to the horizontal. Both the elevation and azimuth angles are continuously calculated and mapped to the LED strip and servo moto respectively.

SolarTracer: second build

SolarTracer: second build with centrally mounted servo motor

Here’s the Arduino code for SolarTracer BUILD 2.

[codesyntax lang=”php” title=”SolarTracer: Build 2 Arduino Code” blockstate=”collapsed”]

//**************************************************************************************************
//                                        SOLAR TRACER
//                                   Adrian Jones, May 2015
//
//**************************************************************************************************

// Build 1
//   r1 150315 - initial build
// 
//********************************************************************************************
#define build 2
#define revision 1
//********************************************************************************************

#include <avr/pgmspace.h>                // for memory storage in program space

// LED strip
#include "FastLED.h"
#define LED_PIN 4                        // LED strip data transfer pin
#define LED_COUNT 15          
CRGB leds[LED_COUNT];
byte bright = 255, sat = 0, hue = 0;     // colour of elevation LED

#include <Servo.h>
#define SERVO_PIN 6                      // servo pin
Servo servo;                             // create servo object to control a servo

// Sun Position library
#include <sundata.h>
#define LONGITUDE -75.64316
#define LATITUDE  45.66167
sundata vdm(0,0,0);

// cardinal directions
const char ca0[]  PROGMEM = "N  ";
const char ca1[]  PROGMEM = "NNE";
const char ca2[]  PROGMEM = "NE ";
const char ca3[]  PROGMEM = "ENE";
const char ca4[]  PROGMEM = "E  ";
const char ca5[]  PROGMEM = "ESE";
const char ca6[]  PROGMEM = "SE ";
const char ca7[]  PROGMEM = "SSE";
const char ca8[]  PROGMEM = "S  ";
const char ca9[]  PROGMEM = "SSW";
const char caA[]  PROGMEM = "SW ";
const char caB[]  PROGMEM = "WSW";
const char caC[]  PROGMEM = "W  ";
const char caD[]  PROGMEM = "WNW";
const char caE[]  PROGMEM = "NW  ";
const char caF[]  PROGMEM = "NNW";
const char * const cas[] PROGMEM = { ca0,ca1,ca2,ca3,ca4,ca5,ca6,ca7,ca8,ca9,caA,caB,caC,caD,caE,caF };

char cb[10];                 // character buffer for character strings. Set dimension to length of longest string
float el_deg, az_deg, sunup, sundn;
int new_el, old_el = 0, new_az = 0, old_az = 0;
boolean nighttime;
static int TZ;


// RTC
#include <Wire.h>
#include "RTClib.h"
RTC_DS1307 RTC;
int hr, mn, se, osec=-1, omin=-1, ohr=-1, dy, ody=0, mo, yr, dw;

// eeprom storage
#include <EEPROM.h>               // for dst data storage
int dst;                          // dst setting                      
int *set[] = { &dst };            // values to set/recover from EEPROM


//*****************************************************************************************//
//                                      Initial Setup
//*****************************************************************************************//
void setup() {
  
  Serial.begin(57600);
  Serial.println(F("SOLAR TRACER by Adrian Jones"));
  Serial.print(F("Build ")); Serial.print(build); Serial.print(F(".")); Serial.println(revision);
  Serial.print(F("Free RAM: "));  Serial.print(freeRam()); Serial.println(F("B"));
  Serial.println(F("****************************"));

  FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, LED_COUNT);   // create instance of LED Strip

  servo.attach(SERVO_PIN);                                   // attaches the servo on pin 9 to the servo object 
  
  Wire.begin();
  RTC.begin();                                               // start RTC
  getTime();                                                 // get current time

  if (! RTC.isrunning() || (String(__DATE__).lastIndexOf(String(yr)) == -1)) {
    RTC.adjust(DateTime(__DATE__, __TIME__));                // adjust if year is off
    Serial.print(F("RTC reset to ")); Serial.print(__DATE__); Serial.print(F(" ")); Serial.println(__TIME__);
  } 
   
  // RTC.adjust(DateTime(__DATE__, __TIME__));          // reset to system time
  // RTC.adjust(DateTime(2015, 1, 18, 1, 59, 50 ));     // test
  
  TZ = (IsDST)?-4:-5;                                        // adjust GMT offset based on DST  
  vdm = sundata(LATITUDE, LONGITUDE, TZ);                    // create sundata instance for current location
  restoreSettings();                                         // restore EEPROM settings
  doServoSetUp();
 }
 
//*****************************************************************************************//
//                                       MAIN LOOP
//*****************************************************************************************//
void loop() {
  getTime();                                         // get current time
  doDSTAdjust();                                     // adjust dst if necessary
  if(se != osec) {                                   // every second...
    vdm.time(yr, mo, dy, hr, mn, se);                // current year, month, day, hour, minutes and seconds
    vdm.calculations();                              // update calculations for current time
    el_deg = vdm.elevation_deg();                    // store sun's elevation in x.xx degrees
    az_deg = vdm.azimuth_deg();                      // store sun's azimuth in x.xx degrees
    sunup  = vdm.sunrise_time();                     // store sunrise time in decimal form
    sundn  = vdm.sunset_time();                      // store sunset time in decimal form
    nighttime = (el_deg<0.0)? true:false;            // has the sun set ?
  
    new_az = constrain(int(270-az_deg),0,179);       // translate azimuth to East (0) to West (180)
    if(abs(new_az-old_az) > 0) {
      if(!servo.attached()) servo.attach(SERVO_PIN);
      delay(10);
      servo.write(new_az);
      delay(200);
      servo.detach();
    }
    old_az = new_az;    
        
    new_el = max(0, (LED_COUNT-1)-int((el_deg+3)/6)); // translate elevation to LED number (0-90 deg. -> LED 14-0)
    leds[old_el] = CHSV(hue, sat, 0);                // light appropriate LED
    leds[new_el] = (nighttime)? CHSV(hue, sat, 0) : CHSV(hue, sat, bright);     
    old_el = new_el;                                 // update elevation 
    FastLED.show();                                  // show it
    
    doSerial();                                      // print everything out  

    osec=se; omin=mn; ohr=hr%12;                     // update time
  }
  delay(100);
}

//*****************************************************************************************//
//                                      SUBROUTINES
//*****************************************************************************************//

void doServoSetUp() {
  servo.write(90);                                  // point SOUTH
  delay(5000);  
}

void doSerial() {
    Serial.print(yr-2000); Serial.print(F("/"));
    if(mo<10) Serial.print(F("0")); Serial.print(mo); Serial.print(F("/"));
    if(dy<10) Serial.print(F("0")); Serial.print(dy); Serial.print(F(" "));
    if(hr<10) Serial.print(F("0")); Serial.print(hr); Serial.print(F(":"));
    if(mn<10) Serial.print(F("0")); Serial.print(mn); Serial.print(F(":"));
    if(se<10) Serial.print(F("0")); Serial.print(se); 

    Serial.print(F("\tE: ")); Serial.print(el_deg); Serial.print(F(" deg. [")); Serial.print(new_el); Serial.print(F("]")); 
    Serial.print(F("\tA: ")); Serial.print(az_deg); Serial.print(F(" deg. ")); Serial.print(getCardinal(az_deg)); Serial.print(F(" [")); Serial.print(servo.read()); Serial.print(F("]"));
//    Serial.print(F(" right ascension = ")); Serial.print(Sky[0], 5); Serial.print(F(" declination = ")); Serial.print(Sky[1], 5); 
    Serial.println(); 
}


// ********************************************************************************** //
//                                      OPERATION ROUTINES
// ********************************************************************************** //
// FREERAM: Returns the number of bytes currently free in RAM  
int freeRam(void) {
  extern int  __bss_end, *__brkval; 
  int free_memory; 
  if((int)__brkval == 0) {
    free_memory = ((int)&free_memory) - ((int)&__bss_end); 
  } 
  else {
    free_memory = ((int)&free_memory) - ((int)__brkval); 
  }
  return free_memory; 
}


/* ********************************************************************************** //
//                                       TIME ROUTINES
// ********************************************************************************** //
   In most of Canada Daylight Saving Time begins at 2:00 a.m. local time on the second Sunday in March. 
   On the first Sunday in November areas on DST return to Standard Time at 2:00 a.m. local time. 
*/

// IsDST returns true if DST, false otherwise
boolean IsDST() {
  if (mo < 3 || mo > 11) { return false; }                // January, February, and December are out.
  if (mo > 3 && mo < 11) { return true;  }                // April to October are in
  int previousSunday = dy - dw;                               
  if (mo == 3) { return previousSunday >= 8; }            // In March, we are DST if our previous Sunday was on or after the 8th.
  return previousSunday <= 0;                             // In November we must be before the first Sunday to be DST. That means the previous Sunday must be before the 1st.
}

// getTime obtains current time from RTC
void getTime() {
  DateTime now = RTC.now();
  hr = now.hour(); if(hr==0) hr=24;                      // adjust to 1-24
  mn = now.minute();
  se = now.second();
  yr = now.year();
  mo = now.month();
  dy = now.day();
  dw = now.dayOfWeek();
}

// doDSTAdjust increments (or decrements) by one hour when entering (or leaving) DST
void doDSTAdjust() {
  if(dst == IsDST()) return;                    // if prior setting is same as DST setting, do nothing
  if(hr != 2) return;                           // do nothing until 2pm

  DateTime now = RTC.now();                     // get time
  if(IsDST() && !dst) {
    DateTime newTime (now.unixtime() + 3600);   // add one hour
    RTC.adjust(newTime);                        
  }
  if(!IsDST() && dst) {
    DateTime newTime (now.unixtime() - 3600);   // subtract one hour
    RTC.adjust(newTime);
  }
  dst = IsDST(); 
  saveSettings();                // save change to DST
}

// ********************************************************************************** //
//                         MOON PHASE AND MOON RISE/ SET ROUTINES
// ********************************************************************************** //

// moonphase:  the phase day (0 to 29, where 0=new moon, 15=full etc.): see http://www.ben-daglish.net/moon.shtml
int moonphase() {
  long lp = 2551443;                                   // 2551223 secs / lunar cycle. new scaling for 0-26 (i.e. 27 unique) bitmaps
  DateTime now = RTC.now();
  DateTime new_moon (1970, 1, 7, 20, 35, 0);
  long ph = (now.unixtime() - new_moon.unixtime()) % lp;  // scales to 0-29 days
  ph = floor (ph/3600L);                                    
  ph = map(ph, 0.0, 29*24.0, 0, 26);                      // map to 0-26
  return ph;  
}

// getCardinal: converts the headsing (in degrees) to a text-based cardinal heading ("ENE", for example)
String getCardinal( float h ) { 
  h += 11.25; if (h > 360.0) h = h-360.0;
  strcpy_P(cb, (char*) pgm_read_word(&(cas[(int) (h / 22.5)]) ) );
  return cb;  
}

// ********************************************************************************** //
//                                       EEPROM ROUTINES
// ********************************************************************************** //
// EEPROM save, restore and set/save defaults
void saveSettings()    { for(int addr = 0; addr < sizeof(set)/2; addr++) { EEPROM.write(addr, *set[addr]); } }    // save
void restoreSettings() { for(int addr = 0; addr < sizeof(set)/2; addr++) { *set[addr] = EEPROM.read(addr); } }   // recover EEPROM value

[/codesyntax]

The ideal base for an indoor sundial!

Come on... leave a comment...