/********** Tide Clock **********/
/* If you do not want the moon phase display,
comment or suppress the next line */
#define moonDisplayActive
#include <U8g2lib.h>
#include <RTClib.h>
#include <Fsm.h>
#include <EEPROM.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C
u8g2(U8G2_R0);
int displayWidth, displayHeight;
int HMiddle, VMiddle;
const int plusButtonPin=2;
const int minusButtonPin=3;
const int modeButtonPin=4;
const int selectButtonPin=5;
const int RTC_CLK=8;
const int RTC_DAT=7;
const int RTC_RST=6;
DS1302 rtc(RTC_RST, RTC_CLK, RTC_DAT);
unsigned long tideSeconds=0;
const unsigned long tidePeriod=44714;
/* Number of seconds of the moon
half period i. e. 12 h 25 mn 14 s */
DateTime current;
DateTime highTideRef;
int year, month, day, hour, minute;
enum
{
EEPROMId0,
EEPROMId1,
EEPROMId2,
EEPROMYear,
EEPROMMonth,
EEPROMDay,
EEPROMHour,
EEPROMMinute
};
enum
{
selYear, selMonth, selDay,
selHour, selMinute
};
int selected;
bool plusPressed=false,
minusPressed=false,
buttonPressed=false;
volatile bool modePressed=false,
selectPressed=false;
/*
Objects of the class stopWatch are used to
perform time dependant actions without
blocking the program by delay functions.
*/
class stopWatch
{
unsigned long previousTime, currentTime;
public :
unsigned long elapsed;
void init();
void now();
stopWatch();
};
void stopWatch::init()
{
currentTime=millis();
previousTime=currentTime;
}
void stopWatch::now()
{
currentTime=millis();
if (currentTime<previousTime)
elapsed=0xFFFFFFFFul-previousTime+
currentTime;
else
elapsed=currentTime-previousTime;
}
stopWatch::stopWatch()
{
currentTime=millis();
previousTime=currentTime;
}
/***** Functions to manage pushbuttons *******/
/*
The Mode and Select pushbuttons are treated
by interrupts because they are used by
single presses.
The + and - pushbuttons are treated by
program because they can be held down to
repeat their actions.
*/
void interruptRoutineReadMode(void)
{
noInterrupts();
delayMicroseconds(10000);
if(digitalRead(modeButtonPin)==LOW)
modePressed=true;
interrupts();
}
void interruptRoutineReadSelect(void)
{
noInterrupts();
delayMicroseconds(10000);
if(digitalRead(selectButtonPin)==LOW)
selectPressed=true;
interrupts();
}
void buttonsManagement(void)
{
const int debounce=20;
const int repeatPeriod=300;
const int beginRepeatTime=1000;
static stopWatch plusButtonStopWatch,
minusButtonStopWatch;
static stopWatch plusRepeatStopWatch,
minusRepeatStopWatch;
int plusButton, minusButton;
static int nbPlusButton=0,
nbMinusButton=0;
plusButton=digitalRead(plusButtonPin);
minusButton=digitalRead(minusButtonPin);
if(plusButton==HIGH)
{
plusButtonStopWatch.init();
nbPlusButton=0;
}
else
{
plusButtonStopWatch.now();
switch(nbPlusButton)
{
case 0 :
if(plusButtonStopWatch.elapsed>
debounce)
{
plusPressed=true;
buttonPressed=true;
nbPlusButton=1;
}
break;
case 1 :
if(plusButtonStopWatch.elapsed>
beginRepeatTime)
{
plusPressed=true;
nbPlusButton=2;
plusRepeatStopWatch.init();
}
break;
default :
plusRepeatStopWatch.now();
if(plusRepeatStopWatch.elapsed>
repeatPeriod)
{
plusPressed=true;
plusRepeatStopWatch.init();
}
break;
}
}
if(minusButton==HIGH)
{
minusButtonStopWatch.init();
nbMinusButton=0;
}
else
{
minusButtonStopWatch.now();
switch(nbMinusButton)
{
case 0 :
if(minusButtonStopWatch.elapsed>
debounce)
{
minusPressed=true;
buttonPressed=true;
nbMinusButton=1;
}
break;
case 1 :
if(minusButtonStopWatch.elapsed>
beginRepeatTime)
{
minusPressed=true;
nbMinusButton=2;
minusRepeatStopWatch.init();
}
break;
default :
minusRepeatStopWatch.now();
if(minusRepeatStopWatch.elapsed>
repeatPeriod)
{
minusPressed=true;
minusRepeatStopWatch.init();
}
break;
}
}
}
/********* Display functions *********/
/*
Display of round tide clock
*/
void backgroundDisplay(void)
{
float angle, arrowAngle;
float radius;
int leftRight;
// Central dot:
u8g2.drawDisc(HMiddle, VMiddle, 1);
// Graduations:
radius=min(displayWidth, displayHeight)/2-1;
for(int i=0; i<12; i++)
{
angle=PI/6*i;
u8g2.drawLine(HMiddle+radius*sin(angle),
VMiddle-radius*cos(angle),
HMiddle+(radius-4)*sin(angle),
VMiddle-(radius-4)*cos(angle));
}
/* Arrow to show the rotation direction. It is
drawn on the opposite side of the hand: */
radius=min(displayWidth, displayHeight)*0.24;
if(tideSeconds>tidePeriod/2)
leftRight=1;
else
leftRight=-1;
for(int i=-10; i<=10; i++)
{
u8g2.drawPixel(HMiddle+
radius*sin(leftRight*PI/2+i*PI/80),
VMiddle-
radius*cos(leftRight*PI/2+i*PI/80));
}
arrowAngle=leftRight*PI/2+PI/8;
u8g2.drawLine(HMiddle+radius*sin(arrowAngle),
VMiddle-radius*cos(arrowAngle),
HMiddle+radius*sin(arrowAngle)+
leftRight*4,
VMiddle-radius*cos(arrowAngle)-
leftRight*2);
u8g2.drawLine(HMiddle+radius*sin(arrowAngle),
VMiddle-radius*cos(arrowAngle),
HMiddle+radius*sin(arrowAngle)-
leftRight*2,
VMiddle-radius*cos(arrowAngle)-
leftRight*3);
// Texts "high" and "low":
u8g2.setFont(u8g2_font_5x7_mf);
u8g2.setCursor(HMiddle-
u8g2.getStrWidth("high")/2,
15);
u8g2.print("high");
u8g2.setCursor(HMiddle-
u8g2.getStrWidth("low")/2,
displayHeight-9);
u8g2.print("low");
}
void handDisplay(void)
{
float handAngle;
int handLength=min(HMiddle, VMiddle)-8;
handAngle=2*PI*tideSeconds/tidePeriod;
u8g2.drawCircle(HMiddle+sin(handAngle)*6,
VMiddle-cos(handAngle)*6, 4);
u8g2.drawCircle(HMiddle+sin(handAngle)*13,
VMiddle-cos(handAngle)*13, 2);
u8g2.drawLine(HMiddle+sin(handAngle)*16,
VMiddle-cos(handAngle)*16,
HMiddle+sin(handAngle)*handLength,
VMiddle-cos(handAngle)*handLength);
}
void tideClockDisplay(void)
{
current=rtc.now();
tideSeconds=(current.unixtime()-
highTideRef.unixtime())%
tidePeriod;
u8g2.clearBuffer();
backgroundDisplay();
handDisplay();
u8g2.sendBuffer();
Serial.println(tideSeconds);
}
/*
Display of daily tide. A sine curve shows
the evolution of the tide along the current
day, and a vertical line shows the tide
at the current time.
*/
void dayTideDisplay(void)
{
current=rtc.now();
DateTime currentDay(current.year(),
current.month(),
current.day(),
0, 0, 0);
/* Rounded display width and offset are used
to display the daily clock on a reduced
and centered screen in order to have
regular hour graduations.
*/
int rdDisplayWidth=24*(displayWidth/24);
int offs=(displayWidth-rdDisplayWidth)/2;
u8g2.clearBuffer();
// Sinus curve:
for(int i=0; i<rdDisplayWidth; i++)
{
tideSeconds=((currentDay+((long)i*86400/
rdDisplayWidth)).unixtime()-
highTideRef.unixtime())%
tidePeriod;
u8g2.drawPixel(i+offs,
(1-cos(2*PI*tideSeconds/tidePeriod))*
(displayHeight-15)/2);
}
// Vertical line:
int x=((long)rdDisplayWidth*
((current.hour()*60+
current.minute())))/1440+offs;
for(int i=0; i<displayHeight-15; i++)
u8g2.drawPixel(x, i);
// Time indications:
u8g2.setFont(u8g2_font_5x7_mf);
u8g2.setCursor(rdDisplayWidth/6-
u8g2.getStrWidth("4h")/2+
offs,
displayHeight);
u8g2.print("4h");
u8g2.setCursor(rdDisplayWidth/2-
u8g2.getStrWidth("12h")/2+
offs,
displayHeight);
u8g2.print("12h");
u8g2.setCursor(rdDisplayWidth*5/6-
u8g2.getStrWidth("20h")/2+
offs,
displayHeight);
u8g2.print("20h");
for(int i=0; i<24; i++)
u8g2. drawLine(i*rdDisplayWidth/24+offs,
displayHeight-9,
i*rdDisplayWidth/24+offs,
displayHeight-11);
for(int i=4; i<24; i+=8)
u8g2.drawPixel(i*rdDisplayWidth/24+offs,
displayHeight-12);
u8g2.sendBuffer();
}
void myDrawDisc(int xCenter, int yCenter,
int R)
{
for(int x=0; x<displayWidth; x++)
for(int y=0; y<displayHeight; y++)
if(((long)x-(long)xCenter)*
((long)x-(long)xCenter)+
((long)y-(long)yCenter)*
((long)y-(long)yCenter)<=
(long)R*(long)R)
u8g2.drawPixel(x, y);
}
/* Moon display */
void moonDisplay()
{
/* first new moon since 01/01/2000 */
DateTime orgNewMoon(2000, 1, 6, 18, 14, 0);
/* Moon period : 29 days, 12 hours,
44 minutes, 2,9 seconds
ie 2551443 seconds*/
const unsigned long moonPeriod=2551443;
unsigned long moonSeconds, moonPixelsDelay;
int moonRadius=min(displayWidth,
displayHeight)/2-2;
int shadowRadius;
int shadowDistance; //from moon center
current=rtc.now();
moonSeconds=(current.unixtime()-
orgNewMoon.unixtime())%
moonPeriod;
moonPixelsDelay=4*moonRadius*moonSeconds/
moonPeriod;
u8g2.clearBuffer();
if(moonPixelsDelay<=moonRadius)
// new moon -> first quarter
{
u8g2.setDrawColor(1);
shadowDistance=moonRadius-moonPixelsDelay;
shadowRadius=moonRadius/
sin(PI-2*atan2(moonRadius,
shadowDistance));
u8g2.drawDisc(HMiddle, VMiddle,
moonRadius);
u8g2.setDrawColor(0);
if(shadowDistance>0)
myDrawDisc(HMiddle+shadowDistance-
shadowRadius, VMiddle,
shadowRadius);
else
u8g2.drawBox(0, 0, displayWidth/2,
displayHeight);
}
else if(moonPixelsDelay<=2*moonRadius)
// first quarter -> full moon
{
u8g2.setDrawColor(1);
shadowDistance=moonPixelsDelay-moonRadius;
shadowRadius=moonRadius/
sin(PI-2*atan2(moonRadius,
shadowDistance));
if(shadowDistance>0)
{
myDrawDisc(HMiddle-shadowDistance+
shadowRadius, VMiddle,
shadowRadius);
u8g2.setDrawColor(0);
for(int x=0; x<displayWidth; x++)
for(int y=0; y<displayHeight; y++)
if((x-HMiddle)*(x-HMiddle)+
(y-VMiddle)*(y-VMiddle)>
moonRadius*moonRadius)
u8g2.drawPixel(x,y);
}
else
u8g2.drawDisc(HMiddle, VMiddle,
moonRadius,
U8G2_DRAW_UPPER_RIGHT |
U8G2_DRAW_LOWER_RIGHT);
}
else if(moonPixelsDelay<=3*moonRadius)
// full moon -> last quarter
{
u8g2.setDrawColor(1);
shadowDistance=3*moonRadius-
moonPixelsDelay;
shadowRadius=moonRadius/
sin(PI-2*atan2(moonRadius,
shadowDistance));
if(shadowDistance>0)
{
myDrawDisc(HMiddle+shadowDistance-
shadowRadius, VMiddle,
shadowRadius);
u8g2.setDrawColor(0);
for(int x=0; x<displayWidth; x++)
for(int y=0; y<displayHeight; y++)
if((x-HMiddle)*(x-HMiddle)+
(y-VMiddle)*(y-VMiddle)>
moonRadius*moonRadius)
u8g2.drawPixel(x,y);
}
else
u8g2.drawDisc(HMiddle, VMiddle,
moonRadius,
U8G2_DRAW_UPPER_LEFT |
U8G2_DRAW_LOWER_LEFT);
}
else
// last quarter -> new moon
{
u8g2.setDrawColor(1);
shadowDistance=moonPixelsDelay-
3*moonRadius;
shadowRadius=moonRadius/
sin(PI-2*atan2(moonRadius,
shadowDistance));
u8g2.drawDisc(HMiddle, VMiddle,
moonRadius);
u8g2.setDrawColor(0);
if(shadowDistance>0)
myDrawDisc(HMiddle-shadowDistance+
shadowRadius, VMiddle,
shadowRadius);
else
u8g2.drawBox(HMiddle, 0,
displayWidth/2, displayHeight);
}
u8g2.setDrawColor(1);
u8g2.drawCircle(HMiddle, VMiddle,
moonRadius);
u8g2.sendBuffer();
}
/*
This function is used to display the date
and the time for the current time and for
the reference high tide time. One of the
5 displayed values is highlighted, the one
that is selected.
*/
void dateTimeAdjustDisplay(void)
{
u8g2.setFont(u8g2_font_9x15_mf);
u8g2.setCursor(
(displayWidth-u8g2.getStrWidth
("2021/07/26"))/2, 37);
u8g2.setDrawColor(1);
if(selected==selYear) u8g2.setDrawColor(0);
u8g2.print(year);
u8g2.setDrawColor(1);
u8g2.print("/");
if(selected==selMonth) u8g2.setDrawColor(0);
if(month<10) u8g2.print("0");
u8g2.print(month);
u8g2.setDrawColor(1);
u8g2.print("/");
if(selected==selDay) u8g2.setDrawColor(0);
if(day<10) u8g2.print("0");
u8g2.print(day);
u8g2.setDrawColor(1);
u8g2.setCursor(
(displayWidth-u8g2.getStrWidth
("08:39"))/2, 60);
if(selected==selHour) u8g2.setDrawColor(0);
if(hour<10) u8g2.print("0");
u8g2.print(hour);
u8g2.setDrawColor(1);
u8g2.print(":");
if(selected==selMinute) u8g2.setDrawColor(0);
if(minute<10) u8g2.print("0");
u8g2.print(minute);
u8g2.setDrawColor(1);
}
void currentTimeDisplay(void)
{
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_7x13_mf);
u8g2.setCursor(
(displayWidth-u8g2.getStrWidth
("Current time"))/2, 13);
u8g2.print("Current time");
dateTimeAdjustDisplay();
u8g2.sendBuffer();
}
void highTideRefDisplay(void)
{
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_7x13_mf);
u8g2.setCursor(
(displayWidth-u8g2.getStrWidth
("Ref. high tide"))/2, 13);
u8g2.print("Ref. high tide");
dateTimeAdjustDisplay();
u8g2.sendBuffer();
}
/*
This function is called to adjust the 5
values of the current time or of the
reference high tide time with the +/-
pushbuttons. Several tests are made to
prevent from displaying an impossible date
such as february the 30th.
*/
void dateTimeAdjust(void)
{
int numberOfDays;
buttonsManagement();
switch(selected)
{
case selYear :
if(plusPressed && year<2100)
year++;
if(minusPressed && year>2000)
year--;
break;
case selMonth :
if(plusPressed)
{
month++;
if(month>12) month=1;
}
if(minusPressed)
{
month--;
if(month<1) month=12;
}
if(plusPressed || minusPressed)
{
if ((month==4 || month==6 ||
month==9 ||month==11) &&
day==31)
day=30;
if (month==2 && day>28)
day=28;
}
break;
case selDay :
numberOfDays=31;
if(plusPressed) day++;
if(minusPressed) day--;
if(plusPressed || minusPressed)
{
if (month==4 || month==6 ||
month==9 || month == 11)
numberOfDays=30;
if (month==2)
if (year%4!=0 ||
(year%100==0 && year%400!=0))
numberOfDays=28;
else
numberOfDays=29;
if(day<1) day=numberOfDays;
if(day>numberOfDays) day=1;
}
break;
case selHour :
if(plusPressed) ++hour%=24;
if(minusPressed)
if(hour>0) hour--;
else hour=23;
break;
case selMinute :
if(plusPressed) ++minute%=60;
if(minusPressed)
if(minute>0) minute--;
else minute=59;
break;
}
plusPressed=false;
minusPressed=false;
}
/*
Functions called by the Finite State Machine
to adjust current time and reference high
tide time.
*/
void currentTimeSetEnter(void)
{
selected=selYear;
current=rtc.now();
year=current.year();
month=current.month();
day=current.day();
hour=current.hour();
minute=current.minute();
currentTimeDisplay();
buttonPressed=false;
}
void currentTimeSet(void)
{
dateTimeAdjust();
currentTimeDisplay();
}
void currentTimeSetExit(void)
{
/* The RTC is set to the displayed time only
if the + or - pushbutton has been pressed.
Otherwise, the RTC is still up to date.
*/
if(buttonPressed)
rtc.adjust(DateTime(year, month, day,
hour, minute, 0));
}
void highTideRefSetEnter(void)
{
selected=selYear;
year=highTideRef.year();
month=highTideRef.month();
day=highTideRef.day();
hour=highTideRef.hour();
minute=highTideRef.minute();
highTideRefDisplay();
}
void highTideRefSet(void)
{
dateTimeAdjust();
highTideRefDisplay();
}
void highTideRefSetExit(void)
{
highTideRef.setyear(year);
highTideRef.setmonth(month);
highTideRef.setday(day);
highTideRef.sethour(hour);
highTideRef.setminute(minute);
highTideRef.setsecond(0);
EEPROM.update(EEPROMYear, year%100);
EEPROM.update(EEPROMMonth, month);
EEPROM.update(EEPROMDay, day);
EEPROM.update(EEPROMHour, hour);
EEPROM.update(EEPROMMinute, minute);
}
void readEEPROM(void)
{
if(EEPROM.read(EEPROMId0)==byte('t') &&
EEPROM.read(EEPROMId1)==byte('i') &&
EEPROM.read(EEPROMId2)==byte('d'))
{
highTideRef.setyear(EEPROM.read
(EEPROMYear));
highTideRef.setmonth(EEPROM.read
(EEPROMMonth));
highTideRef.setday(EEPROM.read
(EEPROMDay));
highTideRef.sethour(EEPROM.read
(EEPROMHour));
highTideRef.setminute(EEPROM.read
(EEPROMMinute));
}
else
{
EEPROM.update(EEPROMId0, 't');
EEPROM.update(EEPROMId1, 'i');
EEPROM.update(EEPROMId2, 'd');
highTideRef.setyear(2021);
EEPROM.update(EEPROMYear,
highTideRef.year()%100);
highTideRef.setmonth(7);
EEPROM.update(EEPROMMonth,
highTideRef.month());
highTideRef.setday(26);
EEPROM.update(EEPROMDay,
highTideRef.day());
highTideRef.sethour(8);
EEPROM.update(EEPROMHour,
highTideRef.hour());
highTideRef.setminute(39);
EEPROM.update(EEPROMYear,
highTideRef.minute());
}
}
/*
A Finite State Machine is used to navigate
between the 5 display modes thanks to the
Mode pushbutton.
*/
State tideClockDisplayState(&tideClockDisplay,
NULL, NULL);
State dayTideDisplayState(&dayTideDisplay,
NULL, NULL);
State moonDisplayState(&moonDisplay,
NULL, NULL);
State currentTimeSetState(¤tTimeSetEnter,
¤tTimeSet,
¤tTimeSetExit);
State higtTideRefSetState(&highTideRefSetEnter,
&highTideRefSet,
&highTideRefSetExit);
Fsm modeFsm(&tideClockDisplayState);
void setTransitions(void)
{
modeFsm.add_transition(
&tideClockDisplayState,
&dayTideDisplayState,
1, NULL);
#ifdef moonDisplayActive
modeFsm.add_transition(
&dayTideDisplayState,
&moonDisplayState,
1, NULL);
modeFsm.add_transition(
&moonDisplayState,
¤tTimeSetState,
1, NULL);
#else
modeFsm.add_transition(
&dayTideDisplayState,
¤tTimeSetState,
1, NULL);
#endif
modeFsm.add_transition(
¤tTimeSetState,
&higtTideRefSetState,
1, NULL);
modeFsm.add_transition(
&higtTideRefSetState,
&tideClockDisplayState,
1, NULL);
/* These two transitions are used to refresh
the two clock displays every 10 seconds.
*/
modeFsm.add_timed_transition(
&tideClockDisplayState,
&tideClockDisplayState,
10000,
NULL);
modeFsm.add_timed_transition(
&dayTideDisplayState,
&dayTideDisplayState,
10000,
NULL);
modeFsm.add_timed_transition(
&moonDisplayState,
&moonDisplayState,
10000,
NULL);
}
void setup()
{
u8g2.begin();
displayWidth=u8g2.getDisplayWidth();
displayHeight=u8g2.getDisplayHeight();
HMiddle=displayWidth/2;
VMiddle=displayHeight/2;
Serial.begin(9600);
rtc.begin();
if (!rtc.isrunning())
{
Serial.println("RTC is NOT running!");
// following line sets the RTC to the
// date & time this sketch was compiled
rtc.adjust(DateTime(__DATE__, __TIME__));
}
pinMode(plusButtonPin, INPUT_PULLUP);
pinMode(minusButtonPin, INPUT_PULLUP);
pinMode(modeButtonPin, INPUT_PULLUP);
pinMode(selectButtonPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt
(modeButtonPin),
interruptRoutineReadMode,FALLING);
attachInterrupt(digitalPinToInterrupt
(selectButtonPin),
interruptRoutineReadSelect, FALLING);
/*
If Mode and Select pushbuttons are pressed
simultaneously during power on, the
reference high tide time is reset to
its default value.
*/
if(digitalRead(modeButtonPin)==LOW &&
digitalRead(selectButtonPin)==LOW)
{
EEPROM.update(EEPROMId0, 0xFF);
EEPROM.update(EEPROMId1, 0xFF);
EEPROM.update(EEPROMId2, 0xFF);
}
readEEPROM();
setTransitions();
}
void loop()
{
int event=0;
noInterrupts();
if(modePressed)
{
event=1;
modePressed=false;
}
if(selectPressed)
{
++selected%=5;
selectPressed=false;
}
interrupts();
modeFsm.run_machine();
modeFsm.trigger(event);
}
Comments