/* GRANDFATHER CLOCK
* David Henshaw -- November 2016 -
November 2016 - basic code
December 2016 - most clock features
January 2017 - additional tweaks and error logic
February 2017 - new stepper motors
March 2017 - chime
*/
#pragma region Includes
#include <Adafruit_NeoPixel.h>
#include <DS1307RTC.h> // http://www.pjrc.com/teensy/td_libs_DS1307RTC.html
#include <TimeLib.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include "avr/pgmspace.h" //for flash memory storage
#include "EEPROM.h" //for eeprom storage
#pragma endregion
#pragma region Menuing system
// Flash Storage: 20 chrs max 12345678901234567890 - these go into flash (program) memory
// prog_char is not valid for new Arduino compiler - now use const char
const char menuItem_0[] PROGMEM = "Chime"; //menu items start here
const char menuItem_1[] PROGMEM = "Secs sleep";
const char menuItem_2[] PROGMEM = "Secs wake";
const char menuItem_3[] PROGMEM = "TickTock sleep";
const char menuItem_4[] PROGMEM = "TickTock wake";
const char menuItem_5[] PROGMEM = "Chime sleep";
const char menuItem_6[] PROGMEM = "Chime wake";
const char menuItem_7[] PROGMEM = "Weekend wake";
const char menuItem_8[] PROGMEM = "Weekend sleep";
const char menuItem_9[] PROGMEM = "Hour";
const char menuItem_10[] PROGMEM = "Time";
const char menuItem_11[] PROGMEM = "Year";
const char menuItem_12[] PROGMEM = "Month";
const char menuItem_13[] PROGMEM = "Day";
const char menuItem_14[] PROGMEM = "Recalibrate";
const char menuItem_15[] PROGMEM = "Turn & push to set"; //status messages from this point on
const char menuItem_16[] PROGMEM = "Silent Mode"; // ** remove if not used
const char menuItem_17[] PROGMEM = "Push to start";
// 20 characters maximum: 12345678901234567890
// Then set up a table to refer to the strings:
// changes from PROGMEM const char *menuTable[] = { to following due to compiler change
const char * const menuTable[] PROGMEM = {
menuItem_0, menuItem_1, menuItem_2, menuItem_3, menuItem_4, menuItem_5, menuItem_6, menuItem_7,
menuItem_8, menuItem_9, menuItem_10, menuItem_11, menuItem_12, menuItem_13, menuItem_14, menuItem_15,
menuItem_16, menuItem_17 };
#pragma endregion
#pragma region Constants
const int stepperHoursStepPin = 20; // Digital Pins via shift registers - stepper motor control - 4 pins start here
const int stepperMinutesStepPin = 16; // See Shift Registers tab in Google Drive
const int stepperCalendarStepPin = 9; //
const int stepperMoonDialStepPin = 5;
const int stepperChimeStepPin = 1;
const int stepperSecondsStepPin = 25;
const int stepperDelay = 3; //delay between step commands ** is this used?
const byte buttonPin = 13; //this is the single button on the control panel
const byte encoderButtonPin = 12; // button on rotary encoder
const byte encoderDelay = 200; // delay used when repeatedly reading encoder dial
const int calendarHallEffectSensor = 11; // the number of the hall effect sensor pin
const int hoursHallEffectSensor = 9;
const int minutesHallEffectSensor = 8;
const int secondsHallEffectSensor = 10;
const int moonDialHallEffectSensor = 14; // 14 = analog pin A0
const int chimeHallEffectSensor = 15; // 15 = analog pin A1
const int maximumSecondsThisHour = 60 * 60; // 60 seconds * 60 minutes
const word maximumSecondsThisHalfDay = 60 * 60 * 12;
const int maximumDaysThisMonth = 31;
const int maximumDaysThisLunarMonth = 59; // sum of days in two lunar months (2 * 29.5)
const int maximumMotorSteps = 1000; // maximum number of allowable turns for any motor
const byte errorCodeMaximumMotorSteps = 1; // error codes
const byte errorCodeRTCStopped = 2;
const byte errorCodeRTCCircuitError = 3;
const byte chimeGong = 0; // shift register index pin used to activate solenoid
const byte tickSolenoid = 13;
const byte tockSolenoid = 14;
const byte onTheHour = 0;
const byte quarterPast = 15;
const byte halfPast = 30;
const byte quarterTo = 45;
const byte neoPixelPin = 7; // data wire for neopixels
const byte powerLight = 0; // NeoPixel LEDs (start at 0 thru 11)
const byte silentModeLight = 1;
const byte RTCLight = 2;
const byte secondHandLight = 3;
const byte minuteHandLight = 4;
const byte hourHandLight = 5;
const byte calendarHandLight = 6;
const byte moonDialLight = 7;
const byte chimeLight = 8;
const byte gongLight = 9;
const byte tickTockLight = 11;
const byte chimeOnLight = 10;
const byte maxMenuItems = 14; // how many menu items there are
const byte maxInformationMessageIndex = 5; // max number of informational messages to toggle between
const byte msgUseDial = 15; // "Turn dial"
const byte msgSilent = 16;
const byte msgPush = 17;
const byte westminsterChimesSetOne = 95; // steps of rotation to play set one of Westminster chimes
const byte westminsterChimesSetTwo = 98; // in general each set requires 64 steps
const byte westminsterChimesSetThree = 103; // then there is "air space" between each set
const byte westminsterChimesSetFour = 105;
const byte westminsterChimesSetFive = 99; // this one ends early to leave room for a mandatory recalibration
// Constants for EEPROM memory (refers to index location in EEPROM where we will store the value) using EEPROM.write(address, value) and EEPROM.read(address, value)
const byte chimeSwitch_EEPROM = 0;
const byte secondsSleepHour_EEPROM = 1;
const byte secondsWakeHour_EEPROM = 2;
const byte chimeSleepHour_EEPROM = 3;
const byte chimeWakeHour_EEPROM = 4;
const byte tickTockWakeHour_EEPROM = 5;
const byte tickTockSleepHour_EEPROM = 6;
const byte weekendSleepHour_EEPROM = 7;
const byte weekendWakeHour_EEPROM = 8;
#define number_of_74hc595s 4 // How many shift registers
#define numOfRegisterPins number_of_74hc595s * 8 // do not touch
#pragma endregion
#pragma region Variables
//boolean clockSleep = false;
//boolean priorClockSleep = false;
boolean chimeSleep = false;
boolean secondsSleep = false;
boolean tickTockSleep = false;
boolean silentMode = false;
boolean tickSound = false;
byte previousSeconds = 0;
byte previousDay = 0;
boolean atSecondHandRecalibrationPoint = false; // flag to determine if second hand has reached recalibration sensor
int maximumStepsThisMinute = 512; // was 407
int maximumStepsThisHour = 513; // motor steps in one hour for the minute hand ** if all are 512, make a single constant!
int maximumStepsThisHalfDay = 512; // motor steps in one half day for the hour hand
int maximumStepsThisMonth = 512; // motor steps in one half day for the calendar hand
int maximumStepsThisLunarMonth = 512; // technically steps for 2 lunar months (moon dial covers two lunar months)
int maximumStepsThisChimingRotation = 512;
int currentStepCountForSecondHand = 0;
int currentStepCountForMinuteHand = 0;
int currentStepCountForHourHand = 0;
int currentStepCountForCalendarHand = 0;
int currentStepCountForMoonDial = 0;
int currentStepCountForChime = 0;
int previousStepCountForHourHand = maximumStepsThisHalfDay;
int previousStepCountForMinuteHand = maximumStepsThisHour;
int previousStepCountForSecondHand = maximumStepsThisMinute;
int previousStepCountForCalendarHand = maximumStepsThisMonth;
int previousStepCountForMoonDial = maximumStepsThisLunarMonth;
int previousStepCountForChime = currentStepCountForChime;
int targetSecondPartStepCountForSecondHand = 0; // used for the second part of the seconds hand swing
byte secondsStepSequenceNumber = 0; // used to keep track of stepping sequence for seconds hand (1-4)
boolean previousMinutesHallEffectSensor = false;
boolean previousHoursHallEffectSensor = false;
boolean previousSecondsHallEffectSensor = false;
boolean previousCalendarHallEffectSensor = false;
boolean previousMoonDialHallEffectSensor = false;
boolean pauseMinuteHand = false;
boolean pauseHourHand = false;
boolean pauseSecondHand = false;
boolean pauseCalendarHand = false;
boolean pauseMoonDial = false;
byte moonDialSegment = 1; // which half of moon dial is visible (1 or 2)
byte currentChimeSegment = 1; // which chime segment do we think we're at? After recalibration, = 1
boolean errorStatus = false; // used to denote any kind of error condition
boolean previousErrorStatus = false;
byte errorCode = 0;
//Variables for shift register
byte SER_Pin = 5; // pin 14 on the 75HC595 Shift Register (Data)
byte RCLK_Pin = 6; // pin 12 on the 75HC595 Shift Register (Latch)
byte SRCLK_Pin = 4; // pin 11 on the 75HC595 Shift Register (Clock)
boolean registers[numOfRegisterPins]; // bit array for the total number of 595 registers
// Variables for rotary encoder
byte encoderPin1 = 2; // these DIGITAL pins can not be changed 2/3 are special pins
byte encoderPin2 = 3; // used for interrupts on rotary encoder
volatile byte lastEncoded = 0; // for interrupt routine - do not change
volatile byte encoderValue = 1; // for interrupt routine - do not change
byte lastEncoderValue = 1;
const byte encoderGap = 2; // size of gap from encoder before we pay attention to it
// Variables for menuing system
char buffer[20]; // max size of message, and max size of lcd screen
byte activeMenuSelection = 0; // Which menu item is active
byte selectedMenuItem = 99;
byte informationMessageIndex = 0;
boolean chimeSwitch = false; // is chime on or off
byte secondsSleepHour;
byte secondsWakeHour;
byte chimeSleepHour;
byte chimeWakeHour;
byte tickTockWakeHour;
byte tickTockSleepHour;
byte weekendSleepHour;
byte weekendWakeHour;
tmElements_t tm; // hours, minutes, seconds, etc. stored in tm.*
int stepperMotorLookup[8] = { B01000, B01100, B00100, B00110, B00010, B00011, B00001, B01001 }; // table used to send bit pattern to stepper motors
#pragma endregion
LiquidCrystal_I2C lcd(0x3F, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE); // Set the LCD I2C address - crazy 0x3F address!!
// Parameter 1 = number of pixels in strip
// Parameter 2 = pin number (most are valid)
// Parameter 3 = pixel type flags, add together as needed:
// NEO_KHZ800 800 KHz bitstream (most NeoPixel products w/WS2812 LEDs)
// NEO_KHZ400 400 KHz (classic 'v1' (not v2) FLORA pixels, WS2811 drivers)
// NEO_GRB Pixels are wired for GRB bitstream (most NeoPixel products)
// NEO_RGB Pixels are wired for RGB bitstream (v1 FLORA pixels, not v2)
Adafruit_NeoPixel strip = Adafruit_NeoPixel(12, neoPixelPin, NEO_GRB + NEO_KHZ800);
#pragma region Colour Codes
// Define the color codes by name to make it easier to call later on ** remove unwanted codes
uint32_t red = strip.Color(200, 0, 0);
//uint32_t brown = strip.Color(102, 51, 0);
//uint32_t orange = strip.Color(204, 102, 0);
uint32_t yellow = strip.Color(255, 255, 0);
uint32_t green = strip.Color(95, 200, 0);
uint32_t dgreen = strip.Color(0, 255, 0);
uint32_t blue = strip.Color(0, 0, 255);
uint32_t white = strip.Color(255, 255, 255);
uint32_t black = strip.Color(0, 0, 0);
#pragma endregion
void setup() {
#pragma region Initialize shift registers
pinMode(SER_Pin, OUTPUT); // Shift Register pin
pinMode(RCLK_Pin, OUTPUT); // Shift Register pin
pinMode(SRCLK_Pin, OUTPUT); // Shift Register pin
clearRegisters(); // reset all register pins
#pragma endregion
#pragma region Hall Effect Sensor Setup
pinMode(hoursHallEffectSensor, INPUT); // initialize the hall effect sensor pins as an input
pinMode(minutesHallEffectSensor, INPUT);
pinMode(secondsHallEffectSensor, INPUT);
pinMode(calendarHallEffectSensor, INPUT);
pinMode(moonDialHallEffectSensor, INPUT);
pinMode(chimeHallEffectSensor, INPUT);
#pragma endregion
#pragma region Initialize buttons
pinMode(buttonPin, INPUT); // set button on control panel to input
digitalWrite(buttonPin, HIGH); // turn on pullup resistors
pinMode(encoderButtonPin, INPUT); // set button on rotary encoder to input
digitalWrite(encoderButtonPin, HIGH); // turn on pullup resistors
#pragma endregion
#pragma region Configure rotary encoder
pinMode(encoderPin1, INPUT); // rotary encoder
pinMode(encoderPin2, INPUT); // interrupt pins
digitalWrite(encoderPin1, HIGH); // turn pullup resistor on
digitalWrite(encoderPin2, HIGH); // turn pullup resistor on
attachInterrupt(0, updateEncoder, CHANGE); // call updateEncoder() when any high/low changed seen
attachInterrupt(1, updateEncoder, CHANGE); // on interrupt 0 (pin 2), or interrupt 1 (pin 3)
#pragma endregion
#pragma region NeoPixel wire setup
strip.begin();
strip.setBrightness(20); // 0 - 255
strip.show(); // Initialize all pixels to 'off'
#pragma endregion
#pragma region RTC Setup
tm.Hour = 17;
tm.Minute = 56;
tm.Second = 04;
tm.Month = 12;
tm.Day = 27;
tm.Year = CalendarYrToTm(2016);
//RTC.write(tm); // and configure the RTC with this info (** remove for production code)
#pragma endregion
#pragma region LCD setup
lcd.begin(20, 4); // initialize the lcd for 20 chars 4 lines, turn on backlight
lcd.backlight(); // turn on the backlight
#pragma endregion
#pragma region Refresh variables from EEPROM
chimeSwitch = EEPROM.read(chimeSwitch_EEPROM);
secondsSleepHour = EEPROM.read(secondsSleepHour_EEPROM);
secondsWakeHour = EEPROM.read(secondsWakeHour_EEPROM);
chimeSleepHour = EEPROM.read(chimeSleepHour_EEPROM);
chimeWakeHour = EEPROM.read(chimeWakeHour_EEPROM);
tickTockSleepHour = EEPROM.read(tickTockSleepHour_EEPROM);
tickTockWakeHour = EEPROM.read(tickTockWakeHour_EEPROM);
weekendSleepHour = EEPROM.read(weekendSleepHour_EEPROM);
weekendWakeHour = EEPROM.read(weekendWakeHour_EEPROM);
#pragma endregion
//recalibrateAllHands(); //** remove comment-out for production code
recalibrateSecondsHand();
RTCCheck(); // check to make sure RTC is working
waitForUserAction();
setStatusLight(powerLight, dgreen); // all systems go!
}
void loop()
{
#pragma region debug code
while (9 > 13) { // enter debug code here
}
#pragma endregion
#pragma region clock code
RTC.read(tm); // find out the time
if (timeHasChanged(tm.Second, previousSeconds)) checkWakeSleepTimers(); // see if it's time to sleep or wake the clock or the chime
updateMainMovement(); // move hands and update display
if ((errorStatus == true) && (previousErrorStatus == false)) { // if we're in an error mode, and weren't the last time round
displayErrorMessage();
} else { // not in error mode
if (chimeSwitch == true && chimeSleep == false
&& silentMode == false
&& tm.Second == 0)
determineChime(tm.Hour, tm.Minute); // we are not in chime sleep and not in clock sleep - check for chimes each 15 mins
if (silentMode == false) secondPartOfSecondsHandMove(tm.Second);// executing this when time hasn't changed - set flag?
}
previousErrorStatus = errorStatus;
//delay(20); // wait a bit until looping; it's only a clock
checkDial(); // update LCD if dial was turned
setStatusLight(RTCLight, black);
if (isEncoderButtonPressed()) menuSystem(activeMenuSelection); // if button pressed, enter menuing system
checkManualSleepButton(); // go to sleep if the button is pressed
#pragma endregion
}
void waitForUserAction() {
// wait for user to push dial before continuing (allows for hands and dials to be adjusted)
printStatusMessage(msgPush); // "push to start" message
do {
setStatusLight(powerLight, blue);
delay(400);
setStatusLight(powerLight, black);
delay(400);
} while (isEncoderButtonPressed() == false); // exit when the button is pressed to set time
delay(encoderDelay); // pause to allow time for button to be released
clearLine(1);
}
void resetLEDs() { // set all LEDs except first two to black - as long as they're not red (error)
for (byte i = 2; i < 12; i++) {
if (strip.getPixelColor(i) != red) setStatusLight(i, black);
}
// set chimeLight to blue if applicable
if (chimeSwitch == true && chimeSleep == false
&& silentMode == false) setStatusLight(chimeLight, blue);
}
void stepOnce(byte stepperMotorPin, int bitStream) // set pins to ULN2003 high in sequence from 1 to 4
{ // thanks http://www.4tronix.co.uk/arduino/Stepper-Motors.php
setRegisterPin(stepperMotorPin + 0, bitRead(stepperMotorLookup[bitStream], 0));
setRegisterPin(stepperMotorPin + 1, bitRead(stepperMotorLookup[bitStream], 1));
setRegisterPin(stepperMotorPin + 2, bitRead(stepperMotorLookup[bitStream], 2));
setRegisterPin(stepperMotorPin + 3, bitRead(stepperMotorLookup[bitStream], 3));
writeRegisters(); // step!
}
void updateMainMovement() {
if (timeHasChanged(tm.Second, previousSeconds)) {
resetLEDs();
setStatusLight(RTCLight, blue);
previousSeconds = tm.Second;
displayTime();
if (tickTockSleep == false && silentMode == false) tickTock();
updateClockFaceHands();
displayInformationMessages();
}
if (dayHasChanged(tm.Day, previousDay)) {
if (tm.Day < previousDay) recalibrateCalendarHand(); // new month has started, so recalibrate to reset
updateCalendarHand(tm.Day);
updateMoonDial(tmYearToCalendar(tm.Year), tm.Month, tm.Day);
previousDay = tm.Day;
// clear out registers so stepper motors don't receive electricity when idle
clearRegisters();
}
#pragma region Automatic recalibration
// every morning, at 5:25:00am, recalibrate hour hand and minute hand
if (tm.Hour == 5 && tm.Minute == 25 && tm.Second == 0) {
recalibrateHoursHand();
recalibrateMinutesHand();
}
// on the 10th of each month, at 5:17:00am, recalibrate calendar hand and lunar dial
if (tm.Day == 10 && tm.Hour == 5 && tm.Minute == 17 && tm.Second == 0) {
recalibrateCalendarHand();
recalibrateMoonDial();
previousDay = 0; // force dial to update both calendar and moon dial
}
#pragma endregion
}
void checkWakeSleepTimers() {
// determine if it's chime wake or sleep mode
if (chimeSleepHour > chimeWakeHour) {
if (tm.Hour >= chimeWakeHour && tm.Hour < chimeSleepHour) {
chimeSleep = false; // we're in wake mode
}
else {
chimeSleep = true; // we're in sleep mode
}
}
else {
if (tm.Hour >= chimeSleepHour && tm.Hour <= chimeWakeHour) {
chimeSleep = true;
}
else {
chimeSleep = false;
}
}
// determine if it's seconds wake or sleep mode
if (secondsSleepHour > secondsWakeHour) {
if (tm.Hour >= secondsWakeHour && tm.Hour < secondsSleepHour) {
secondsSleep = false; // we're in wake mode
}
else {
secondsSleep = true; // we're in sleep mode
}
} else {
if (tm.Hour >= secondsSleepHour && tm.Hour <= secondsWakeHour) {
secondsSleep = true;
}
else {
secondsSleep = false;
}
}
// determine if it's ticker wake or sleep mode
if (tickTockSleepHour > tickTockWakeHour) {
if (tm.Hour >= tickTockWakeHour && tm.Hour < tickTockSleepHour) {
tickTockSleep = false; // we're in wake mode
}
else {
tickTockSleep = true; // we're in sleep mode
}
}
else {
if (tm.Hour >= tickTockSleepHour && tm.Hour <= tickTockWakeHour) {
tickTockSleep = true;
}
else {
tickTockSleep = false;
}
}
// special consideration for weekend hours
if (tm.Wday == 6) { // Friday - stay up late
if ((tm.Hour >= tickTockSleepHour || tm.Hour >= secondsSleepHour || tm.Hour >= chimeSleepHour) && tm.Hour < weekendSleepHour) {
tickTockSleep = false; // we're in wake mode
secondsSleep = false;
chimeSleep = false;
}
}
if (tm.Wday == 7) { // Saturday - easiest rule to implement
if (tm.Hour >= weekendWakeHour && tm.Hour < weekendSleepHour) {
tickTockSleep = false; // we're in wake mode
secondsSleep = false;
chimeSleep = false;
}
}
if (tm.Wday == 1) { // Sunday - wake up early
if ((tm.Hour < tickTockWakeHour || tm.Hour < secondsWakeHour || tm.Hour < chimeWakeHour) && tm.Hour >= weekendWakeHour) {
tickTockSleep = false; // we're in wake mode
secondsSleep = false;
chimeSleep = false;
}
}
}
void checkManualSleepButton() {
if (isManualButtonPressed() == true) {
//clock goes to or from silent mode
silentMode = !silentMode;
if (silentMode == true) {
setStatusLight(silentModeLight, blue);
printStatusMessage(msgSilent); // "turn and push to set" message
recalibrateSecondsHand();
} else {
setStatusLight(silentModeLight, black);
clearLine(1);
}
delay(800); //** debounce
}
}
void displayInformationMessages() {
if (tm.Second % 5 == 0) { // every 5 seconds do this
informationMessageIndex++; // let's display the next info message
clearLine(3);
if (informationMessageIndex > maxInformationMessageIndex) informationMessageIndex = 0; // reset if we went beyond
switch (informationMessageIndex) {
case 0:
displayStepCounts(3, "H", currentStepCountForHourHand, maximumStepsThisHalfDay, previousStepCountForHourHand); // what are current motor step counts?
break;
case 1:
displayStepCounts(3, "M", currentStepCountForMinuteHand, maximumStepsThisHour, previousStepCountForMinuteHand); // what are current motor step counts?
break;
case 2:
displayStepCounts(3, "S", currentStepCountForSecondHand, maximumStepsThisMinute, previousStepCountForSecondHand); // what are current motor step counts?
break;
case 3:
displayStepCounts(3, "C", currentStepCountForCalendarHand, maximumStepsThisMonth, previousStepCountForCalendarHand); // what are current motor step counts?
break;
case 4:
displayStepCounts(3, "L", currentStepCountForMoonDial, maximumStepsThisLunarMonth, previousStepCountForMoonDial); // what are current motor step counts?
lcd.print("D:");
lcd.print(calculateMoonPhase(tmYearToCalendar(tm.Year), tm.Month, tm.Day)); // determine Lunar day (0-29.5));
break;
case 5:
lcd.setCursor(0, 3);
lcd.print("Chime ");
if (chimeSwitch == true && chimeSleep == false) {
printLCDOptionStatus(true); // chime is currently on
}
else {
printLCDOptionStatus(false); // chime is currently off
}
lcd.print(" Set ");
lcd.print(currentChimeSegment);
break;
}
}
}
void displayErrorMessage() {
lcd.setCursor(0, 2);
lcd.print("ERROR ");
lcd.print(errorCode);
lcd.print(" @ ");
printLCDDigits(tm.Hour, "");
printLCDDigits(tm.Minute, ":");
printLCDDigits(tm.Second, ":");
setStatusLight(powerLight, red);
}
void displayStepCounts(byte line, char* type, int current, int maximum, int prior) {
lcd.setCursor(0, line);
lcd.print(type);
lcd.print(":");
lcd.print(current);
lcd.print("/");
lcd.print(maximum);
lcd.print(" (");
displayDeltaBetweenTwoValues(maximum, prior);
lcd.print(") ");
}
void displayDeltaBetweenTwoValues(int valueOne, int valueTwo) {
int difference = valueOne - valueTwo;
if (difference > 0) lcd.print("+");
lcd.print(difference);
}
boolean isManualButtonPressed() {
boolean buttonState = false;
if (digitalRead(buttonPin) == false) buttonState = true; // read button. If currently pressed, return true
return buttonState;
}
boolean isEncoderButtonPressed() {
boolean buttonState = false;
if (digitalRead(encoderButtonPin) == false) {
buttonState = true; // read button. If currently pressed, return true
delay(10); // debounce time
}
return buttonState;
}
boolean dialHasTurned() { // has dial turned at all?
boolean dialHasTurned = false;
if (abs(lastEncoderValue - encoderValue) > encoderGap) dialHasTurned = true;
return dialHasTurned;
}
void checkDial() { // Has the dial been turned on the rotary encoder? set flag if dial was turned or switch set to on
if (dialHasTurned()) { // menu selection has changed
if (encoderValue > lastEncoderValue) { // figure out what the new menu selection is
activeMenuSelection++; // add 1 to menu
if (activeMenuSelection > maxMenuItems) activeMenuSelection = 0;
}
else {
activeMenuSelection--; // subtract 1 from menu
if (activeMenuSelection > 200) activeMenuSelection = maxMenuItems;
}
printLCDActiveMenu(activeMenuSelection);
lastEncoderValue = encoderValue;
}
}
void printLCDActiveMenu(byte menu) { // display the currently-selected menu item
clearLine(1);
getMessage(menu);
lcd.setCursor(0, 1);
lcd.print("> ");
lcd.print(buffer);
}
void getMessage(byte index) { //retrieve eeprom message and put it into buffer
strcpy_P(buffer, (char*)pgm_read_word(&menuTable[index])); // this compiles despite failure in Visual Studio editor
}
void clearLine(byte i) { //clear a specific line
for (byte j = 0; j <20; j++) {
lcd.setCursor(j, i);
lcd.print(" ");
}
}
void updateEncoder() { // interrupt routine called whenever rotary dial is moved
int MSB = digitalRead(encoderPin1); //MSB = most significant bit
int LSB = digitalRead(encoderPin2); //LSB = least significant bit
int encoded = (MSB << 1) | LSB; //converting the 2 pin value to single number
int sum = (lastEncoded << 2) | encoded; //adding it to the previous encoded value
if (sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) encoderValue++;
if (sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) encoderValue--;
lastEncoded = encoded; //store this value for next time
if (encoderValue < 1) encoderValue = 99; //prevent negative numbers on menu
if (encoderValue > 99) encoderValue = 1; //prevent negative numbers on menu
}
void setStatusLight(byte light, uint32_t color) {
strip.setPixelColor(light, color);
strip.show();
}
void recalibrateChime() {
currentStepCountForChime = 0;
setStatusLight(chimeLight, yellow);
// as long as sensor is not seeing black already, do this:
// otherwise since 1 iteration of this loop would be executed, it inadvertently might do an entire recalibration
if (digitalRead(chimeHallEffectSensor) == true) { // true = sensor seeing "white"
do {
stepChimeOnce();
currentStepCountForChime++;
if (motorHasExceededTurnCount(currentStepCountForChime)) {
setStatusLight(chimeLight, red);
break;
}
} while (digitalRead(chimeHallEffectSensor) == true);
//Serial.print("steps pt.1 = ");
//Serial.println(currentStepCountForChime);
}
// now keep going - slower - and get just past the sensor
do {
stepChimeOnce();
delay(100);
currentStepCountForChime++;
if (motorHasExceededTurnCount(currentStepCountForChime)) {
setStatusLight(chimeLight, red);
break;
}
} while (digitalRead(chimeHallEffectSensor) == false);
//Serial.print("steps pt.2 = ");
//Serial.println(currentStepCountForChime);
currentStepCountForChime = 0;
currentChimeSegment = 1; // because we've recalibrated, we must be at chime set 1 of 5
clearRegisters();
}
void recalibrateMoonDial() {
currentStepCountForMoonDial = 0;
setStatusLight(moonDialLight, yellow);
// as long as sensor is not seeing black already, do this:
// otherwise since 1 iteration of this loop would be executed, it inadvertently might do an entire recalibration
if (digitalRead(moonDialHallEffectSensor) == true) { // true = sensor seeing "white"
do {
stepMoonDialOnce();
currentStepCountForMoonDial++;
if (motorHasExceededTurnCount(currentStepCountForMoonDial)) {
setStatusLight(moonDialLight, red);
break;
}
} while (digitalRead(moonDialHallEffectSensor) == true);
//Serial.print("steps pt.1 = ");
//Serial.println(currentStepCountForMoonDial);
}
// now keep going - slower - and get just past the sensor
do {
stepMoonDialOnce();
delay(100);
currentStepCountForMoonDial++;
if (motorHasExceededTurnCount(currentStepCountForMoonDial)) {
setStatusLight(moonDialLight, red);
break;
}
} while (digitalRead(moonDialHallEffectSensor) == false);
//Serial.print("steps pt.2 = ");
//Serial.println(currentStepCountForMoonDial);
currentStepCountForMoonDial = 0;
clearRegisters();
}
void recalibrateCalendarHand() {
currentStepCountForCalendarHand = 0;
setStatusLight(calendarHandLight, yellow);
// as long as sensor is not seeing black already, do this:
// otherwise since 1 iteration of this loop would be executed, it inadvertently might do an entire recalibration
if (digitalRead(calendarHallEffectSensor) == true) { // true = sensor seeing "white"
do {
for(byte i = 0; i <10; i++){ // do 10 steps before checking HE sensor
stepCalendarOnce();
currentStepCountForCalendarHand++;
}
if (motorHasExceededTurnCount(currentStepCountForCalendarHand)) {
setStatusLight(calendarHandLight, red);
break;
}
} while (digitalRead(calendarHallEffectSensor) == true);
//Serial.print("steps pt.1 = ");
//Serial.println(currentStepCountForCalendarHand);
}
// now keep going - slower - and get just past the sensor
do {
stepCalendarOnce();
delay(100);
currentStepCountForCalendarHand++;
if (motorHasExceededTurnCount(currentStepCountForCalendarHand)) {
setStatusLight(calendarHandLight, red);
break;
}
} while (digitalRead(calendarHallEffectSensor) == false);
//Serial.print("steps pt.2 = ");
//Serial.println(currentStepCountForCalendarHand);
currentStepCountForCalendarHand = 0;
clearRegisters();
}
void recalibrateHoursHand() {
currentStepCountForHourHand = 0;
setStatusLight(hourHandLight, yellow);
// as long as sensor is not seeing black already, do this:
// otherwise since 1 iteration of this loop would be executed, it inadvertently might do an entire recalibration
if (digitalRead(hoursHallEffectSensor) == true) { // true = sensor seeing "white"
do {
stepHoursOnce();
currentStepCountForHourHand++;
if (motorHasExceededTurnCount(currentStepCountForHourHand)) {
setStatusLight(hourHandLight, red);
break;
}
} while (digitalRead(hoursHallEffectSensor) == true);
//Serial.print("steps pt.1 = ");
//Serial.println(currentStepCountForHourHand);
}
// now keep going - slower - and get just past the sensor
do {
stepHoursOnce();
delay(100);
currentStepCountForHourHand++;
if (motorHasExceededTurnCount(currentStepCountForHourHand)) {
setStatusLight(hourHandLight, red);
break;
}
} while (digitalRead(hoursHallEffectSensor) == false);
//Serial.print("steps pt.1 = ");
//Serial.println(currentStepCountForHourHand);
currentStepCountForHourHand = 0;
clearRegisters();
writeRegisters();
}
void recalibrateMinutesHand() {
currentStepCountForMinuteHand = 0;
setStatusLight(minuteHandLight, yellow);
// as long as sensor is not seeing black already, do this:
// otherwise since 1 iteration of this loop would be executed, it inadvertently might do an entire recalibration
if (digitalRead(minutesHallEffectSensor) == true) { // true = sensor seeing "white"
do {
stepMinutesOnce();
currentStepCountForMinuteHand++;
if (motorHasExceededTurnCount(currentStepCountForMinuteHand)) {
setStatusLight(minuteHandLight, red);
break;
}
} while (digitalRead(minutesHallEffectSensor) == true); // true = sensor seeing "white"
//Serial.print("steps pt.1 = ");
//Serial.println(currentStepCountForMinuteHand);
}
// now keep going - slower - and get just past the sensor
do {
stepMinutesOnce();
delay(100);
currentStepCountForMinuteHand++;
if (motorHasExceededTurnCount(currentStepCountForMinuteHand)) {
setStatusLight(minuteHandLight, red);
break;
}
} while (digitalRead(minutesHallEffectSensor) == false); // false = sensor seeing "black"
//Serial.print("steps pt.2 = ");
//Serial.println(currentStepCountForMinuteHand);
currentStepCountForMinuteHand = 0;
clearRegisters();
}
void recalibrateSecondsHand() {
currentStepCountForSecondHand = 0;
setStatusLight(secondHandLight, yellow);
// as long as sensor is not seeing black already, do this:
// otherwise since 1 iteration of this loop would be executed, it inadvertently might do an entire recalibration
if (digitalRead(secondsHallEffectSensor) == true) { // true = sensor seeing "white"
do {
stepSecondsOnce();
currentStepCountForSecondHand++;
if (motorHasExceededTurnCount(currentStepCountForSecondHand)) {
setStatusLight(secondHandLight, red);
break;
}
} while (digitalRead(secondsHallEffectSensor) == true); // true = sensor seeing "white"
//Serial.print("steps pt.1 = ");
//Serial.println(currentStepCountForSecondHand);
}
// now keep going - slower - and get just past the sensor
do {
stepSecondsOnce();
delay(5);//**constant
currentStepCountForSecondHand++;
if (motorHasExceededTurnCount(currentStepCountForSecondHand)) { //** need to check if lights stay red
setStatusLight(secondHandLight, red);
break;
}
} while (digitalRead(secondsHallEffectSensor) == false); // false = sensor seeing "black"
//Serial.print("steps pt.2 = ");
//Serial.println(currentStepCountForSecondHand);
currentStepCountForSecondHand = 0;
clearRegisters();
}
boolean motorHasExceededTurnCount(int count) {
boolean error = false;
if (count > maximumMotorSteps) {
error = true;
errorStatus = true;
errorCode = errorCodeMaximumMotorSteps;
}
return error;
}
void stepChimeOnce() {
for (int i = 0; i < 8; i++) // do 8 stepper motor steps
{
stepOnce(stepperChimeStepPin, i);
delay(4);
}
}
void stepMoonDialOnce() {
for (int i = 7; i >= 0; i--) // do 8 stepper motor steps
{
stepOnce(stepperMoonDialStepPin, i);
delay(15);
}
}
void stepSecondsOnce() {
for (int i = 0; i < 8; i++) // do 8 stepper motor steps
{
stepOnce(stepperSecondsStepPin, i);
delay(1);
}
}
void stepCalendarOnce() {
for (int i = 0; i < 8; i++) // do 8 stepper motor steps
{
stepOnce(stepperCalendarStepPin, i);
delay(5);
}
}
void stepHoursOnce() {
for (int i = 0; i < 8; i++) // do 8 stepper motor steps
{
stepOnce(stepperHoursStepPin, i);
delay(5);
}
}
void stepMinutesOnce() {
for (int i = 7; i >= 0; i--) // do 8 stepper motor steps
{
stepOnce(stepperMinutesStepPin, i);
delay(5);
}
}
void clearRegisters() { //set all register pins to LOW
for (int i = numOfRegisterPins - 1; i >= 0; i--) {
registers[i] = LOW;
}
writeRegisters();
}
void writeRegisters() { // Set and display registers - 595's
digitalWrite(RCLK_Pin, LOW);
for (int i = numOfRegisterPins - 1; i >= 0; i--) {
digitalWrite(SRCLK_Pin, LOW);
int val = registers[i];
digitalWrite(SER_Pin, val);
digitalWrite(SRCLK_Pin, HIGH);
}
digitalWrite(RCLK_Pin, HIGH);
}
void setRegisterPin(int index, int value) { //set an individual pin HIGH or LOW
registers[index] = value;
}
void RTCCheck() {
if (RTC.read(tm)) {
setStatusLight(RTCLight, green);
//Serial.println("RTC OK");
/*Serial.print("Time = ");
print2digits(tm.Hour);
Serial.write(':');
print2digits(tm.Minute);
Serial.write(':');
print2digits(tm.Second);
Serial.print(", Date (D/M/Y) = ");
Serial.print(tm.Day);
Serial.write('/');
Serial.print(tm.Month);
Serial.write('/');
Serial.print(tmYearToCalendar(tm.Year));
Serial.println();*/
}
else {
setStatusLight(RTCLight, red);
errorStatus = true;
if (RTC.chipPresent()) {
//Serial.println("RTC stopped");
errorCode = errorCodeRTCStopped;
}
else {
//Serial.println("RTC error");
errorCode = errorCodeRTCCircuitError;
}
}
}
boolean timeHasChanged(byte seconds, byte previousSeconds) {
boolean timeDidChange = false;
if (seconds != previousSeconds) timeDidChange = true;
return timeDidChange;
}
boolean dayHasChanged(byte day, byte previousDay) {
boolean dayDidChange = false;
if (day != previousDay) dayDidChange = true;
return dayDidChange;
}
void updateClockFaceHands() {
if (secondsSleep == false && silentMode == false) updateSecondsHand(tm.Second);
//updateSecondsHand(tm.Second); //** debug code when testing seconds hand (remove)
updateHoursHand(tm.Hour, tm.Minute, tm.Second);
updateMinutesHand(tm.Minute, tm.Second);
}
void updateSecondsHand(byte seconds) {
if(atSecondHandRecalibrationPoint == true) { // sometime very recently we were at recalibration point
//Serial.print("Secs recal @ ");
//Serial.print(tm.Second);
//Serial.print("s : ");
// if seconds = 0, all is good. No need to do anything because we re in sync
// if seconds between 1 and 29: hand is moving too slow. Speed it up for the next minute.
if (tm.Second >= 1 && tm.Second <= 29) {
maximumStepsThisMinute++; //??
//Serial.print("speed up");
}
// if seconds between 30 and 59: hand is moving too fast. Slow it down for the next minute.
if (tm.Second >= 30 && tm.Second <= 59) {
maximumStepsThisMinute--; //??
//Serial.print("slow down");
}
//Serial.print(" to ");
//Serial.println(maximumStepsThisMinute);
atSecondHandRecalibrationPoint = false; // now reset this flag (ensure it doesn t get triggered multiple times in one second?)
}
int target = targetStepCountForSecondHand(seconds, maximumStepsThisMinute); // what step count should we be at in this minute?
if (seconds == 0) currentStepCountForSecondHand = -6 ;
targetSecondPartStepCountForSecondHand = target;
int targetFirstPartStepCountForSecondHand = target;
targetFirstPartStepCountForSecondHand = targetFirstPartStepCountForSecondHand - 2;
byte secondsDelayGap = 38;
if ((target > currentStepCountForSecondHand) && (pauseSecondHand == false)) { // if we need to move the hand...
setStatusLight(secondHandLight, yellow);
while (targetFirstPartStepCountForSecondHand > currentStepCountForSecondHand) { // go slightly past the target**
stepSecondsOnce();
currentStepCountForSecondHand++;
delay(secondsDelayGap);
if (secondsDelayGap > 10) secondsDelayGap = secondsDelayGap - 3; // speed up the movement (makes it look more like a mechanical clock)
// Set a flag when hand is at recalibration point
if(atSecondCalibrationPoint() == true) atSecondHandRecalibrationPoint = true;
}
}
}
void secondPartOfSecondsHandMove(byte second) {
if (targetSecondPartStepCountForSecondHand > currentStepCountForSecondHand) delay(150);
if ((errorStatus == false)) {
while (targetSecondPartStepCountForSecondHand > currentStepCountForSecondHand) { // now fall forward to where we should be
stepSecondsOnce();
currentStepCountForSecondHand++;
delay(30);
// Set a flag when hand is at recalibration point
if (atSecondCalibrationPoint() == true) atSecondHandRecalibrationPoint = true;
}
setStatusLight(secondHandLight, black);
// clear out registers so stepper motors don't receive electricity when idle
clearRegisters();
writeRegisters();
}
}
void updateMinutesHand(byte minutes, byte seconds) {
float secondsElapsed = calculateSecondsOfThisHour(minutes, seconds); // how many seconds into this hour are we?
int target = targetStepCountForMinuteHand(secondsElapsed, maximumStepsThisHour); // what step count should we be at in this hour?
if (target < currentStepCountForMinuteHand) recalibrateMinutesHand(); // in case where time has been put back
if (target > currentStepCountForMinuteHand) { // if we need to move the hand...
setStatusLight(minuteHandLight, yellow);
while (target > currentStepCountForMinuteHand) {
stepMinutesOnce();
currentStepCountForMinuteHand++;
}
}
// clear out registers so stepper motors don't receive electricity when idle
clearRegisters();
if (minutes == 0 && seconds == 0) currentStepCountForMinuteHand = 0; // presume that if target is 0 then hand is already at the top position
}
void updateHoursHand(byte hours, byte minutes, byte seconds) {
float secondsElapsed = calculateSecondsOfThisDay(hours, minutes, seconds); // how many seconds into this 12 hour period are we?
int target = targetStepCountForHourHand(secondsElapsed, maximumStepsThisHalfDay); // what step count should we be at in this hour?
if (target < currentStepCountForHourHand) recalibrateHoursHand(); // in case where time has been put back
if ((target > currentStepCountForHourHand) && (pauseHourHand == false)) {
setStatusLight(hourHandLight, yellow);
while (target > currentStepCountForHourHand) {
stepHoursOnce();
currentStepCountForHourHand++;
}
}
// clear out registers so stepper motors don't receive electricity when idle
clearRegisters();
if ((hours == 12 || hours == 0) && minutes == 0 && seconds == 0) currentStepCountForHourHand = 0; // presume that if target is 0 then hand is already at the top position
}
void updateCalendarHand(byte dayOfMonth) {
float daysElapsed = dayOfMonth; // what day is it?
int target = targetStepCountForCalendarHand(daysElapsed, maximumStepsThisMonth); // what step count should we be at in this day?
if (target == 0) currentStepCountForCalendarHand = 0;
if ((target > currentStepCountForCalendarHand) && (pauseCalendarHand == false)) {
setStatusLight(calendarHandLight, yellow);
while (target > currentStepCountForCalendarHand) {
stepCalendarOnce();
currentStepCountForCalendarHand++;
}
}
// clear out registers so stepper motors don't receive electricity when idle
clearRegisters();
}
void updateMoonDial(int year, int month, int day) {
float lunarAge = calculateMoonPhase(year, month, day); // determine Lunar day (0-29.5)
float moonDialDays = 0;
// if start of lunar month, then flip from moon dial segment 1 > 2 > 1 etc
if (RTCAtBeginningOfLunarMonth(lunarAge)) {
if (moonDialSegment == 1) {
moonDialSegment++; // 1 > 2
}
else {
moonDialSegment--; // 2 > 1
}
}
if (moonDialSegment == 1) {
// we are in the first half of the moon dial being displayed
moonDialDays = lunarAge;
} else {
// we are in the second half of the moon dial being displayed
moonDialDays = lunarAge + 29.5;
}
int target = targetStepCountForMoonDial(moonDialDays, maximumStepsThisLunarMonth); // what step count should we be at in this lunar day?
if (target == 0) currentStepCountForMoonDial = 0;
//if (target < currentStepCountForMoonDial) recalibrateMoonDial();**? needed
/*
Serial.print("Day ");
Serial.print(day);
Serial.print(" - Lunar:");
Serial.print(lunarAge);
Serial.print(" - moonDialDays:");
Serial.print(moonDialDays);
Serial.print(" - step:");
Serial.println(target);
*/
if ((target > currentStepCountForMoonDial) && (pauseMoonDial == false)) {
setStatusLight(moonDialLight, yellow);
while (target > currentStepCountForMoonDial) {
stepMoonDialOnce();
currentStepCountForMoonDial++;
}
}
// clear out registers so stepper motors don't receive electricity when idle
clearRegisters();
}
int targetStepCountForSecondHand(int currentSeconds, int maximumStepsThisMinute) {
float proportionOfSecondsThisMinute = (float)currentSeconds / (float)60.0; // was 60
float targetStepCount = proportionOfSecondsThisMinute * (float)maximumStepsThisMinute;
return targetStepCount;
}
float targetStepCountForMinuteHand(float currentSeconds, float maximumStepsThisHour) {
float proportionOfSecondsThisHour = currentSeconds / maximumSecondsThisHour;
float targetStepCount = proportionOfSecondsThisHour * maximumStepsThisHour;
if (currentSeconds == 0) targetStepCount = maximumStepsThisHour;
return targetStepCount;
}
float targetStepCountForHourHand(float currentSeconds, float maximumStepsThisHalfDay) {
float proportionOfSecondsThisHalfDay = currentSeconds / maximumSecondsThisHalfDay;
float targetStepCount = proportionOfSecondsThisHalfDay * maximumStepsThisHalfDay;
if (currentSeconds == 0 || currentSeconds == maximumSecondsThisHalfDay) targetStepCount = maximumStepsThisHalfDay;
return targetStepCount;
}
float targetStepCountForCalendarHand(float currentDay, float maximumStepsThisMonth) {
float proportionOfDaysThisMonth = (currentDay - 1) / maximumDaysThisMonth; // -1 because first day of month is at step position 0 (not 16)
float targetStepCount = proportionOfDaysThisMonth * maximumStepsThisMonth;
return targetStepCount;
}
float targetStepCountForMoonDial(float currentLunarDay, float maximumStepsThisLunarMonth) {
float proportionOfDaysThisLunarMonth = currentLunarDay / maximumDaysThisLunarMonth;
float targetStepCount = proportionOfDaysThisLunarMonth * maximumStepsThisLunarMonth;
return targetStepCount;
}
int calculateSecondsOfThisHour(byte minutes, byte seconds) {
int secondsElapsed = (minutes * 60) + seconds;
return secondsElapsed;
}
word calculateSecondsOfThisDay(byte hours, byte minutes, byte seconds) {
// hours must be <= 12
if (hours > 11) hours = hours - 12; // if 12 o'clock, don't count seconds for the hour
word secondsElapsed = (hours * 60 * 60) + calculateSecondsOfThisHour(minutes, seconds);
return secondsElapsed;
}
float calculateMoonPhase(int nYear, int nMonth, int nDay) { // calculate the current phase of the moon
// Moon phase, takes three parameters: the year (4 digits), the month and the day.
// The function returns fraction full, float 0-1 (e.g. 0 for new, .25 for crescent, .5 for quarter, .75 for gibbous and 1 for full). [since removed]
// Also calculates phase and age in days. THIS VERSION uses a cosine function to model the illumination fraction.
// Calculated at noon, based on new moon Jan 6, 2000 @18:00, illumination accurate to about 5%, at least for years 1900-2100.
// Modified from post at http://www.nano-reef.com/topic/217305-a-lunar-phase-function-for-the-arduino/
// S. J. Remington 11/2016
// Copied from https://forum.arduino.cc/index.php?topic=438179.15
// Adjustments made to remove things not required (e.g. fraction full)
// You can also check the lunar day at http://www.moongiant.com/phase/today/
float age, phase, days_since;
long YY, MM, K1, K2, K3, JD;
YY = nYear - floor((12 - nMonth) / 10);
MM = nMonth + 9;
if (MM >= 12)
{
MM = MM - 12;
}
K1 = floor(365.25 * (YY + 4712));
K2 = floor(30.6 * MM + 0.5);
K3 = floor(floor((YY / 100) + 49) * 0.75) - 38;
JD = K1 + K2 + nDay + 59; //Julian day
if (JD > 2299160) //1582, Gregorian calendar
{
JD = JD - K3;
}
// Serial.print(" JD "); //Julian Day, checked OK
// Serial.print(JD);
// Serial.print(" ");
days_since = JD - 2451550L; //since noon on Jan. 6, 2000 (new moon @18:00)
phase = (days_since - 0.25) / 29.53059; //0.25 = correct for 6 pm that day
phase -= floor(phase); //phase in cycle
age = phase * 29.53059;
// calculate fraction full
//frac = (1.0 - cos(phase * 2 * PI)) * 0.5;
return age;
}
void determineChime(byte hours, byte minutes) {
switch (minutes){
case onTheHour:
playWestminsterChime(hours, onTheHour);
break;
case quarterPast :
playWestminsterChime(hours, quarterPast);
break;
case halfPast :
playWestminsterChime(hours, halfPast);
break;
case quarterTo :
playWestminsterChime(hours, quarterTo);
break;
}
}
void playWestminsterChime(byte hours, byte minutes) {
// https://en.wikipedia.org/wiki/Westminster_Quarters
// chime and gone timing synchronized with Big Ben at https://www.youtube.com/watch?v=E9wWBjnaEck
switch (minutes) {
case 0: // top of hour.
playChimeSet(4);
playChimeSet(5);
recalibrateChime(); // ensure we are at calibration point (beginning of chime set 1) before starting again
playChimeSet(1);
playChimeSet(2);
chimeDelay(5500); // validated from Big Ben timings https://www.youtube.com/watch?v=E9wWBjnaEck
hitGong(hours);
break;
case 15:
playChimeSet(3);
break;
case 30:
playChimeSet(4);
playChimeSet(5);
recalibrateChime(); // ensure we are at calibration point (beginning of chime set 1) before starting again
break;
case 45:
playChimeSet(1);
playChimeSet(2);
playChimeSet(3);
break;
}
}
void playChimeSet(byte segment) {
switch (segment){
case 1:
rotateChimeCams(westminsterChimesSetOne);
break;
case 2:
rotateChimeCams(westminsterChimesSetTwo);
break;
case 3:
rotateChimeCams(westminsterChimesSetThree);
break;
case 4:
rotateChimeCams(westminsterChimesSetFour);
break;
case 5:
rotateChimeCams(westminsterChimesSetFive);
break;
}
currentChimeSegment++;
if(currentChimeSegment > 4) currentChimeSegment = 1;
}
void rotateChimeCams(int steps) { // rotate motor by # of steps to play the appropriate number of notes
setStatusLight(chimeLight, white);
for(int i = 0; i <= steps; i++) {
stepChimeOnce();
}
clearRegisters();
writeRegisters();
setStatusLight(chimeLight, yellow);
}
void chimeDelay(int milliSeconds) {
// wait for some milliSeconds, and while waiting update clock face
do {
milliSeconds = milliSeconds - 1200; // 1200 because that's how much time delay(500) + seconds hand update takes
delay(500);
// now read RTC and update hour/minute/second hands
RTC.read(tm);
updateSecondsHand(tm.Second);
secondPartOfSecondsHandMove(tm.Second);
} while (milliSeconds > 0);
}
void hitGong(byte hours) {
if (hours > 12) hours = hours - 12; // convert to 12 hour clock if required
for(byte i = 0; i < hours; i++) {
setStatusLight(gongLight, yellow);
setRegisterPin(chimeGong, 1); // pull back solenoid
writeRegisters();
delay(500); // wait for solenoid to engage - don't use chimeDelay because that will also release solenoid
setRegisterPin(chimeGong, 0); // release solenoid
writeRegisters(); // "gong "
setStatusLight(gongLight, black);
chimeDelay(3900); // 4.4 seconds between gongs (- 500ms for the delay above)
}
}
void tickTock() {
tickSound = !tickSound; // tick or tock?
if (tickSound == true) {
setStatusLight(tickTockLight, black);
setRegisterPin(tickSolenoid, 1); // pull back solenoid
writeRegisters(); // "tick "
delay(15);
setRegisterPin(tickSolenoid, 0); // release solenoid
writeRegisters();
}
else {
setStatusLight(tickTockLight, blue);
setRegisterPin(tockSolenoid, 1); // pull back solenoid
writeRegisters(); // "tock "
delay(20);
setRegisterPin(tockSolenoid, 0); // release solenoid
writeRegisters();
}
}
boolean atMoonDialCalibrationPoint() { // are we at Moon Dial Calibration Point?
boolean moonDialCalibrationPoint;
boolean readMoonDialHallEffectSensor = digitalRead(moonDialHallEffectSensor);
if (readMoonDialHallEffectSensor == true && previousMoonDialHallEffectSensor == false) { // sensor is seeing white (true) but was black (false) last time
moonDialCalibrationPoint = true; // we have reached calibration point
}
else {
moonDialCalibrationPoint = false;
}
previousMoonDialHallEffectSensor = readMoonDialHallEffectSensor; // store for next time round
return moonDialCalibrationPoint;
}
boolean atCalendarCalibrationPoint() { // are we at Calendar Calibration Point?
boolean calendarCalibrationPoint;
boolean readCalendarHallEffectSensor = digitalRead(calendarHallEffectSensor);
if (readCalendarHallEffectSensor == true && previousCalendarHallEffectSensor == false) { // sensor is seeing white (true) but was black (false) last time
calendarCalibrationPoint = true; // we have reached calibration point
}
else {
calendarCalibrationPoint = false;
}
previousCalendarHallEffectSensor = readCalendarHallEffectSensor; // store for next time round
return calendarCalibrationPoint;
}
boolean atHourCalibrationPoint() { // are we at Hour Calibration Point?
boolean hourCalibrationPoint;
boolean readHoursHallEffectSensor = digitalRead(hoursHallEffectSensor);
if (readHoursHallEffectSensor == true && previousHoursHallEffectSensor == false) { // sensor is seeing white (true) but was black (false) last time
hourCalibrationPoint = true; // we have reached calibration point
}
else {
hourCalibrationPoint = false;
}
previousHoursHallEffectSensor = readHoursHallEffectSensor; // store for next time round
return hourCalibrationPoint;
}
boolean atMinuteCalibrationPoint() { // are we at Minute Calibration Point?
boolean minuteCalibrationPoint;
boolean readMinutesHallEffectSensor = digitalRead(minutesHallEffectSensor); //** add this to seconds calib point functions
if (readMinutesHallEffectSensor == true && previousMinutesHallEffectSensor == false) { // sensor is seeing white (true) but was black (false) last time
minuteCalibrationPoint = true; // we have reached calibration point
}
else {
minuteCalibrationPoint = false;
}
previousMinutesHallEffectSensor = readMinutesHallEffectSensor; // store for next time round
return minuteCalibrationPoint;
}
boolean atSecondCalibrationPoint() { // are we at Second Calibration Point?
boolean secondCalibrationPoint;
boolean readSecondsHallEffectSensor = digitalRead(secondsHallEffectSensor);
if (readSecondsHallEffectSensor == true && previousSecondsHallEffectSensor == false) { // false=calibration point. black (false) now but was white (true) last time round
secondCalibrationPoint = true; // we have reached calibration point
}
else {
secondCalibrationPoint = false;
}
previousSecondsHallEffectSensor = readSecondsHallEffectSensor; // store for next time round
return secondCalibrationPoint;
}
boolean RTCAtTopOfHour() { // is it x o'clock on the dot? this only happens for one second every hour
boolean topOfHour = false;
if (tm.Minute == 0 && tm.Second == 0) topOfHour = true;
return topOfHour;
}
boolean RTCPriorToTopOfHour() { // are we close to top of hour but not quite there yet?
boolean priorToTopOfHour = false;
if (tm.Minute <= 59) priorToTopOfHour = true;
return priorToTopOfHour;
}
boolean RTCAtTopOfHalfDay() { // is it 12 o'clock on the dot? this only happens for one second every 12 hours
boolean topOfHalfDay = false;
if ((tm.Hour == 0 || tm.Hour == 12) && tm.Minute == 0 && tm.Second == 0) topOfHalfDay = true;
return topOfHalfDay;
}
boolean RTCPriorToTopOfHalfDay() { // are we close to top of half day but not quite there yet?
boolean priorToTopOfHalfDay = false;
if (tm.Minute <= 59) priorToTopOfHalfDay = true; //** check if need to examine hours too
return priorToTopOfHalfDay;
}
boolean RTCPriorToEndOfMonth() { // are we close to top of month but not quite there yet?
boolean priorToEndOfMonth = false;
if (tm.Day <= 31) priorToEndOfMonth = true;
return priorToEndOfMonth;
}
boolean RTCPriorToEndOfLunarMonth(byte day) { // are we close to top of month but not quite there yet?
boolean priorToEndOfLunarMonth = false;
if (day <= maximumDaysThisLunarMonth) priorToEndOfLunarMonth = true;
return priorToEndOfLunarMonth;
}
boolean RTCAtBeginningOfMonth() { // is it 1st of the month? this only happens for one day every month
boolean beginningOfMonth = false;
if (tm.Day == 0) beginningOfMonth = true;
return beginningOfMonth;
}
boolean RTCAtBeginningOfLunarMonth(float day) { // is it 1st day of the lunar month? this only happens for one day every month
boolean beginningOfLunarMonth = false;
if (day < 1) beginningOfLunarMonth = true;
return beginningOfLunarMonth;
}
void displayTime() {
lcd.setCursor(0, 0);
printLCDDigits(tm.Month, "");
printLCDDigits(tm.Day, "/");
printLCDDigits(tmYearToCalendar(tm.Year)-2000, "/"); // tm.Year is from 1970, so this gives us years since 2000 (2 digit)
lcd.setCursor(12, 0);
printLCDDigits(tm.Hour, "");
printLCDDigits(tm.Minute, ":");
printLCDDigits(tm.Second, ":");
//lcd.print(" "); // this solves a problem where a digit sometimes gets printed to the right of the time - clear it out instead
}
void printPositionDigits(int digits) { // utility function for lcd display: prints leading 0's
if (digits < 100) lcd.print('0');
if (digits < 10) lcd.print('0');
lcd.print(digits);
}
void printLCDDigits(int digits, char* precursor) { // utility function for lcd display: prints preceding character and leading 0
lcd.print(precursor);
if (digits < 10) lcd.print('0');
lcd.print(digits);
}
void printStatusMessage(byte i) { //print status message #i on lcd
getMessage(i);
clearLine(1);
lcd.setCursor(0, 1);
lcd.print(buffer);
}
byte addHour(byte hour) {
if (hour >= 23) {
hour = 0;
}
else {
hour++;
}
return hour;
}
byte subtractHour(byte hour) {
if (hour == 0) {
hour = 23;
}
else {
hour--;
}
return hour;
}
void addRTCHour() {
if (tm.Hour >= 23) {
tm.Hour = 0;
}
else {
tm.Hour++;
}
RTC.write(tm);
displayTime();
}
void subtractRTCHour() {
if (tm.Hour == 0) {
tm.Hour = 23;
}
else {
tm.Hour--;
}
RTC.write(tm);
displayTime();
}
void addMinute() {
if (tm.Minute == 59) {
tm.Minute = 0;
if (tm.Hour == 23) {
tm.Hour = 0;
}
else {
tm.Hour++;;
}
}
else {
tm.Minute++;
}
RTC.write(tm);
displayTime();
}
void subtractMinute() {
if (tm.Minute == 0) {
tm.Minute = 59;
if (tm.Hour == 0) {
tm.Hour = 23;
}
else {
tm.Hour--;
}
}
else {
tm.Minute--;
}
RTC.write(tm);
displayTime();
}
void addYear() {
tm.Year++;
RTC.write(tm);
displayTime();
}
void subtractYear() {
tm.Year--;
RTC.write(tm);
displayTime();
}
void addMonth() {
if (tm.Month == 12) {
tm.Month = 1;
}
else {
tm.Month++;
}
RTC.write(tm);
displayTime();
}
void subtractMonth() {
if (tm.Month == 1) {
tm.Month = 12;
}
else {
tm.Month--;
}
RTC.write(tm);
displayTime();
}
void addDay() {
if (tm.Day == 31) {
tm.Day = 1;
}
else {
tm.Day++;
}
RTC.write(tm);
displayTime();
}
void subtractDay() {
if (tm.Day == 1) {
tm.Day = 31;
}
else {
tm.Day--;
}
RTC.write(tm);
displayTime();
}
void menuSystem(byte selectedMenuItem) {
printStatusMessage(msgUseDial); // "turn and push to set" message
clearLine(2);
clearLine(3);
errorStatus = false; // reset error status
switch (selectedMenuItem) {
case 0: // chime on & off
printLCDOptionStatus(chimeSwitch); // true or false equates to On and Off
do {
if (dialHasTurned()) {
if (encoderValue != lastEncoderValue) { // figure out what the new menu selection is
chimeSwitch = !chimeSwitch;
printLCDOptionStatus(chimeSwitch);
}
}
lastEncoderValue = encoderValue;
delay(encoderDelay);
} while (isEncoderButtonPressed() == false); // exit when the button is pressed to set time
EEPROM.write(chimeSwitch_EEPROM, chimeSwitch); // update EEPROM
delay(encoderDelay);
break;
case 1: // set seconds sleep
secondsSleepHour = adjustSetting(secondsSleepHour, secondsSleepHour_EEPROM);
break;
case 2: // set seconds wake
secondsWakeHour = adjustSetting(secondsWakeHour, secondsWakeHour_EEPROM);
break;
case 3: // set ticktock sleep
tickTockSleepHour = adjustSetting(tickTockSleepHour, tickTockSleepHour_EEPROM);
break;
case 4: // set ticktock wake
tickTockWakeHour = adjustSetting(tickTockWakeHour, tickTockWakeHour_EEPROM);
break;
case 5: // set chime sleep time
chimeSleepHour = adjustSetting(chimeSleepHour, chimeSleepHour_EEPROM);
break;
case 6: // set chime wake time
chimeWakeHour = adjustSetting(chimeWakeHour, chimeWakeHour_EEPROM);
break;
case 7: // set weekend wake time
weekendWakeHour = adjustSetting(weekendWakeHour, weekendWakeHour_EEPROM);
break;
case 8: // set weekend sleep time
weekendSleepHour = adjustSetting(weekendSleepHour, weekendSleepHour_EEPROM);
break;
case 9: //set hour (useful for seasonal spring forward / backward)
do {
if (dialHasTurned()) {
if (encoderValue > lastEncoderValue) { // figure out what the new menu selection is
addRTCHour();
}
else {
subtractRTCHour();
}
}
lastEncoderValue = encoderValue;
delay(encoderDelay);
} while (isEncoderButtonPressed() == false); // exit when the button is pressed to set time
delay(encoderDelay); // pause to allow time for button to be released
break;
case 10: //set time
do {
if (dialHasTurned()) {
if (encoderValue > lastEncoderValue) { // figure out what the new menu selection is
addMinute();
}
else {
subtractMinute();
}
}
lastEncoderValue = encoderValue;
delay(encoderDelay);
} while (isEncoderButtonPressed() == false); // exit when the button is pressed to set time
delay(encoderDelay); // pause to allow time for button to be released
break;
case 11: //set year
do {
if (dialHasTurned()) {
if (encoderValue > lastEncoderValue) { // figure out what the new menu selection is
addYear();
}
else {
subtractYear();
}
}
lastEncoderValue = encoderValue;
delay(encoderDelay);
} while (isEncoderButtonPressed() == false); // exit when the button is pressed to set time
delay(encoderDelay); // pause to allow time for button to be released
break;
case 12: //set month
do {
if (dialHasTurned()) {
if (encoderValue > lastEncoderValue) { // figure out what the new menu selection is
addMonth();
}
else {
subtractMonth();
}
}
lastEncoderValue = encoderValue;
delay(encoderDelay);
} while (isEncoderButtonPressed() == false); // exit when the button is pressed to set time
delay(encoderDelay); // pause to allow time for button to be released
break;
case 13: //set day
do {
if (dialHasTurned()) {
if (encoderValue > lastEncoderValue) { // figure out what the new menu selection is
addDay();
}
else {
subtractDay();
}
}
lastEncoderValue = encoderValue;
delay(encoderDelay);
} while (isEncoderButtonPressed() == false); // exit when the button is pressed to set time
delay(encoderDelay); // pause to allow time for button to be released
break;
case 14: //recalibrate
recalibrateAllHands();
previousSeconds = 99; // force update to movement
previousDay = 99; // force update to movement
waitForUserAction();
setStatusLight(powerLight, dgreen); // reset any error light
break;
}
clearLine(3);
clearLine(2);
clearLine(1);
}
void recalibrateAllHands() {
recalibrateCalendarHand();
recalibrateMoonDial();
recalibrateHoursHand();
recalibrateMinutesHand();
recalibrateSecondsHand();
}
byte adjustSetting(byte settingName, byte EEPROMsettingName) {
printLCDOptionHour(settingName);
do {
if (dialHasTurned()) {
if (encoderValue > lastEncoderValue) { // figure out what the new menu selection is
settingName = addHour(settingName);
}
else {
settingName = subtractHour(settingName);
}
printLCDOptionHour(settingName);
}
lastEncoderValue = encoderValue;
delay(encoderDelay);
} while (isEncoderButtonPressed() == false); // exit when the button is pressed to set time
EEPROM.write(EEPROMsettingName, settingName); // update EEPROM
delay(encoderDelay);
return settingName;
}
void printLCDOptionStatus(boolean option) { // print user-friendly on/off message
lcd.setCursor(8, 3);
lcd.print("> O");
if (option == false) {
lcd.print("FF");
}
else {
lcd.print("N ");
}
}
void printLCDOptionHour(byte hour) { // print user-friendly hour
lcd.setCursor(8, 3);
printLCDDigits(hour, "");
lcd.print(" Hrs");
}