With some very useful pointers from Makuna (who wrote NeoPixelBus) and a lot of trial and error, I have my word clock working on my Oak. It also uses NTP to set the RTC, and adjusts for daylight saving.
I am having intermittent borked flashes, which mean I have to ground P1 to get to safe mode.
My code:
#include <NeoPixelBus.h>
#include <NeoPixelAnimator.h>
#include <NeoPixelBrightnessBus.h>
#include <Wire.h>
#include <TimeLib.h>
#include "RTClib.h"
#include <BH1750.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
BH1750 lightMeter(0x23);
RTC_DS3231 rtc;
//LED pin = GPIO 3 for ESP8266, = pin 3 for Oak
typedef ColumnMajorAlternating270Layout MyPanelLayout;
const uint8_t PanelWidth = 16;
const uint8_t PanelHEIGHT = 6;
const uint16_t PixelCount = PanelWidth * PanelHEIGHT;
NeoTopology<MyPanelLayout> topo(PanelWidth, PanelHEIGHT);
NeoPixelBus<NeoGrbFeature, Neo800KbpsMethod> strip(PixelCount);
RgbColor red(128, 0, 0);
RgbColor green(0, 128, 0);
RgbColor blue(0, 0, 128);
RgbColor white(128);
RgbColor black(0);
const uint16_t left = 0;
const uint16_t right = PanelWidth - 1;
const uint16_t top = 0;
const uint16_t bottom = PanelHEIGHT - 1;
const uint8_t AnimationChannels = PixelCount;
NeoPixelAnimator animations(AnimationChannels);
uint16_t effectState = 0;
struct MyAnimationState {
RgbColor StartingColor;
RgbColor EndingColor;
};
MyAnimationState animationState[AnimationChannels];
struct wordLocation { //define a data structure. Every wordLocation object will have .row, .firstCol, .lastCol
const uint8_t row;
const uint8_t firstCol;
const uint8_t lastCol;
byte state;
};
//ENUM acts as index for array. Using NEAR in code, will get replaced by 0
enum Word {
NEAR,
PAST1,
EXACTLY,
A1,
QUARTER,
TWENTY,
MTEN,
A2,
MFIVE,
I,
HALF,
TO,
PAST2,
TEN,
FOUR,
THREE,
EIGHT,
SEVEN,
NINE,
ELEVEN,
TWO,
FIVE,
ONE,
SIX,
TWELVE,
Word_COUNT // the last one will represent how many words total
};
Word Word; // <-- the actual instance
wordLocation wordLocations[Word_COUNT] = {
//{.row, .firstCol, .lastCol, .state},
{0, 0, 3, 0}, //NEAR
{0, 4, 7, 0}, //past1
{0, 8, 14, 0}, //exactly
{0, 15, 15, 0}, //a1
{1, 0, 6, 0}, //quarter
{1, 7, 12, 0}, //twenty
{1, 13, 15, 0}, //mTEN
{2, 0, 0, 0}, //a2
{2, 1, 4, 0}, //mFIVE
{2, 5, 5, 0}, //i
{2, 6, 9, 0}, //half
{2, 10, 11, 0}, //to
{2, 12, 15, 0}, //past2
{3, 0, 2, 0},//TEN,
{3, 3, 6, 0},//FOUR,
{3, 7, 11, 0},//THREE,
{3, 11, 15, 0},//EIGHT,
{4, 0, 4, 0},//SEVEN,
{4, 4, 7, 0},//NINE,
{4, 7, 12, 0},//ELEVEN,
{4, 13, 15, 0},//TWO,
{5, 0, 3, 0},//FIVE,
{5, 4, 6, 0},//ONE,
{5, 7, 9, 0},// SIX,
{5, 10, 15, 0} // TWELVE, //25 total words
};
const int timeZone = 10; // AEST
char timeServer[] = "0.au.pool.ntp.org";
WiFiUDP Udp;
const unsigned int localPort = 8888; // local port to lisTEN for UDP packets
const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message
byte packetBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming & outgoing packets
//variables
long lastMillis = 0;
long serialMillis = 0;
int mytimemonth;
int mytimeday;
int mytimehr;
int mytimemin;
int mytimesec;
void setup() {
Serial.begin(115200); //Begin serial communcation
// init RTC
Wire.begin();
rtc.begin();
Serial.println("Power on");
Serial.println("Set RTC to 01/01/2010 at 01:01:01 for debugging");
rtc.adjust(DateTime(2010, 1, 1, 01, 01, 01)); //set RTC to 01/01/2010 at 01:01:01
Serial.print("RTC epoch time is: ");
DateTime now = rtc.now();
lightMeter.begin(BH1750_CONTINUOUS_HIGH_RES_MODE);
Serial.println(F("BH1750 enable CONTINUOUS_HIGH_RES_MODE"));
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.print("Local IP: ");
Serial.println(WiFi.localIP());
Serial.print("Starting UDP... ");
Udp.begin(localPort);
Serial.print("local port: ");
Serial.println(Udp.localPort());
Serial.println("Getting NTP time");
getNTPtime(); //get NTP time and set RTC to UTC
setSyncInterval(1); //Arduino/RTC Sync interval to 1 second for intial sync
setSyncProvider(localTime);
setSyncInterval(300); //Arduino/RTC Sync interval to 300 seconds
strip.Begin();
strip.Show();
}
unsigned long getNTPtime()
{
memset(packetBuffer, 0, NTP_PACKET_SIZE); //Set all bytes in the buffer to 0
packetBuffer[0] = 0b11100011; //LI, Version, Mode
packetBuffer[1] = 0; //Stratum, or type of clock
packetBuffer[2] = 6; //Polling Interval
packetBuffer[3] = 0xEC; //Peer Clock Precision
packetBuffer[12] = 49;
packetBuffer[13] = 0x4E;
packetBuffer[14] = 49;
packetBuffer[15] = 52;
Udp.beginPacket(timeServer, 123); //NTP requests are to port 123
Udp.write(packetBuffer, NTP_PACKET_SIZE);
Udp.endPacket();
//Wait to see if a reply is available
delay(500); //Adjust this delay for time server (effects accuracy, use shortest delay possible)
if (Udp.parsePacket())
{
Udp.read(packetBuffer, NTP_PACKET_SIZE);
unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
unsigned long secsSince1900 = highWord << 16 | lowWord;
const unsigned long SEVENTY_YEARS = 2208988800UL; //Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
unsigned long epoch = secsSince1900 - SEVENTY_YEARS; //Subtract SEVENty years
Serial.print("NTP: ");
Serial.println(epoch);
rtc.adjust(DateTime(epoch));
}
return 0;
}
int dstOffset (unsigned long unixTime)
{
//Receives unix epoch time and returns seconds of offset for local DST
//Code idea from doughboy @ "http://forum.arduino.cc/index.php?PHPSESSID=uoj11hu5j72556mk3gh0ba5ok3&topic=197637.0"
//Get epoch times @ "http://www.epochconverter.com/" for testing
//DST update wont be reflected until the next time sync
time_t t = unixTime; //take unixTime variable passed and put it in time_t type variable
tmElements_t te;
te.Year = year(t)-1970;
te.Month = 10;
te.Day = 1;
te.Hour = 0;
te.Minute = 0;
te.Second = 0;
time_t dstStart,dstEnd, current;
// Serial.println(time_t);
dstStart = makeTime(te);
Serial.print("dstStart ");
Serial.println(dstStart);
dstStart = nextSunday(dstStart); //Once, first Sunday in Oct
dstStart += 2*SECS_PER_HOUR; //2AM
te.Year = year(t)-1970+1;
te.Month=4;
dstEnd = makeTime(te);
dstEnd = nextSunday(dstEnd); //First Sunday in April
dstEnd += SECS_PER_HOUR; //1AM
Serial.print("dstEnd ");
Serial.println(dstEnd);
if (t>=dstStart && t<dstEnd) return (3600); //Add one hours worth of seconds - DST in effect
if (t>=dstStart && t<dstEnd) Serial.println("DST on");
else return (0); //NonDST
}
time_t localTime(){
time_t local = rtc.now().unixtime();
Serial.print("Epoch: ");
Serial.println(local);
unsigned long offset;
offset = timeZone * 3600;
Serial.print("AEST Offset: ");
Serial.println(offset);
local = local + offset; //add timezone offset to RTC time
Serial.print("Epoch + offset : ");
Serial.println(local);
local = local + dstOffset(local); //Adjust for DST
Serial.print("DST: ");
Serial.println(local);
return local; //return value adjusted for timezone and then DST
}
void displayWord(int WORD, int wState) { //function to display word, effect if just turned on, no effect (stay on) otherwise
if (wState == ON) {
if (wordLocations[WORD].state == 0) { //do the following when word first displayed
fadeIn(WORD); //fade word in
wordLocations[WORD].state = 1; //set state to show word has been displayed
} else { //do the following if word already on
//LEDs will stay at prior state if no command sent, leave this else empty if just want word to stay on as is
fadeIn(WORD); //calling this here will fade the word from existing colour to a new colour
// Just show word without animation
//animationState[WORD].StartingColor = strip.GetPixelColor(topo.Map(wordLocations[WORD].firstCol,wordLocations[WORD].row));
//animationState[WORD].StartingColor = white;
//animations.StartAnimation(WORD, 0, showWordUpdate);
}
} else { //if word to be turned off, do:
animations.StartAnimation(WORD, 0, hideWordUpdate);
wordLocations[WORD].state = 0; //word hidden -> reset state
}
}
void hideWordUpdate(const AnimationParam& param) { //simple hide word
for (uint16_t pixel = wordLocations[param.index].firstCol; pixel <= wordLocations[param.index].lastCol; pixel++)
{
strip.SetPixelColor(topo.Map(pixel,wordLocations[param.index].row), black);
}
}
void showWordUpdate(const AnimationParam& param) { //simple show word, no effect
for (uint16_t pixel = wordLocations[param.index].firstCol; pixel <= wordLocations[param.index].lastCol; pixel++)
{
strip.SetPixelColor(topo.Map(pixel,wordLocations[param.index].row), animationState[param.index].StartingColor);
}
}
void fadeIn(int WORD) {
RgbColor target = HslColor(random(360) / 360.0f, 1.0f, 0.25f);
uint16_t time = random(800, 2000);
animationState[WORD].StartingColor = strip.GetPixelColor(topo.Map(wordLocations[WORD].firstCol,wordLocations[WORD].row));
animationState[WORD].EndingColor = target;
animations.StartAnimation(WORD, 5000, BlendAnimUpdate);
}
void BlendAnimUpdate(const AnimationParam& param) {
RgbColor updatedColor = RgbColor::LinearBlend(
animationState[param.index].StartingColor,
animationState[param.index].EndingColor,
param.progress);
for (uint16_t pixel = wordLocations[param.index].firstCol; pixel <= wordLocations[param.index].lastCol; pixel++)
{
strip.SetPixelColor(topo.Map(pixel,wordLocations[param.index].row), updatedColor);
}
}
void displayClock() {
Serial.println("displayClock");
if ((mytimemin== 0)|(mytimemin== 5)|(mytimemin== 10)|(mytimemin== 15)|(mytimemin== 20)
| (mytimemin== 25)|(mytimemin == 30)|(mytimemin == 35)|(mytimemin == 40)
| (mytimemin == 45)|(mytimemin == 50)|(mytimemin == 55)) {
displayWord(EXACTLY, ON);
} else {
displayWord(EXACTLY, OFF);
}
if ((mytimemin == 1)|(mytimemin == 2)|(mytimemin == 6)|(mytimemin == 7)|(mytimemin == 11)
| (mytimemin == 12)|(mytimemin == 16)|(mytimemin == 17)|(mytimemin == 21)|(mytimemin == 22)
| (mytimemin == 26)|(mytimemin == 27)|(mytimemin == 31)|(mytimemin == 32)|(mytimemin == 36)
| (mytimemin == 37)|(mytimemin == 41)|(mytimemin == 42)|(mytimemin == 46)|(mytimemin == 47)
| (mytimemin == 51)|(mytimemin == 52)|(mytimemin == 56)|(mytimemin == 57)) {
displayWord(PAST1, ON);
} else {
displayWord(PAST1, OFF);
}
if ((mytimemin == 3)|(mytimemin == 4)|(mytimemin == 8)|(mytimemin == 9)|(mytimemin == 13)
| (mytimemin == 14)|(mytimemin == 18)|(mytimemin == 19)|(mytimemin == 23)|(mytimemin == 24)
| (mytimemin == 28)|(mytimemin == 29)|(mytimemin == 33)|(mytimemin == 34)|(mytimemin == 38)
| (mytimemin == 39)|(mytimemin == 43)|(mytimemin == 44)|(mytimemin == 48)|(mytimemin == 49)
| (mytimemin == 53)|(mytimemin == 54)|(mytimemin == 58)|(mytimemin == 59)) {
displayWord(NEAR, ON);
} else {
displayWord(NEAR, OFF);
}
//minutes
if(mytimemin<3){
displayWord(QUARTER, OFF);
displayWord(TWENTY, OFF);
displayWord(MTEN, OFF);
displayWord(MFIVE, OFF);
displayWord(HALF, OFF);
displayWord(TO, OFF);
displayWord(PAST2, OFF);
}
if(mytimemin>2 && mytimemin<8){
displayWord(QUARTER, OFF);
displayWord(TWENTY, OFF);
displayWord(MTEN, OFF);
displayWord(MFIVE, ON);
displayWord(HALF, OFF);
displayWord(TO, OFF);
displayWord(PAST2, ON);
}
if(mytimemin>7 && mytimemin<13){
displayWord(QUARTER, OFF);
displayWord(TWENTY, OFF);
displayWord(MTEN, ON);
displayWord(MFIVE, OFF);
displayWord(HALF, OFF);
displayWord(TO, OFF);
displayWord(PAST2, ON);
}
if(mytimemin>12 && mytimemin<18){
displayWord(QUARTER, ON);
displayWord(TWENTY, OFF);
displayWord(MTEN, OFF);
displayWord(MFIVE, OFF);
displayWord(HALF, OFF);
displayWord(TO, OFF);
displayWord(PAST2, ON);
}
if(mytimemin>17 && mytimemin<23){
displayWord(QUARTER, OFF);
displayWord(TWENTY, ON);
displayWord(MTEN, OFF);
displayWord(MFIVE, OFF);
displayWord(HALF, OFF);
displayWord(TO, OFF);
displayWord(PAST2, ON);
}
if(mytimemin>22 && mytimemin<28){
displayWord(QUARTER, OFF);
displayWord(TWENTY, ON);
displayWord(MTEN, OFF);
displayWord(MFIVE, ON);
displayWord(HALF, OFF);
displayWord(TO, OFF);
displayWord(PAST2, ON);
}
if(mytimemin>27 && mytimemin<33){
displayWord(QUARTER, OFF);
displayWord(TWENTY, OFF);
displayWord(MTEN, OFF);
displayWord(MFIVE, OFF);
displayWord(HALF, ON);
displayWord(TO, OFF);
displayWord(PAST2, ON);
}
if(mytimemin>32 && mytimemin<38){
displayWord(QUARTER, OFF);
displayWord(TWENTY, ON);
displayWord(MTEN, OFF);
displayWord(MFIVE, ON);
displayWord(HALF, OFF);
displayWord(TO, ON);
displayWord(PAST2, OFF);
}
if(mytimemin>37 && mytimemin<43){
displayWord(QUARTER, OFF);
displayWord(TWENTY, ON);
displayWord(MTEN, OFF);
displayWord(MFIVE, OFF);
displayWord(HALF, OFF);
displayWord(TO, ON);
displayWord(PAST2, OFF);
}
if(mytimemin>42 && mytimemin<48){
displayWord(QUARTER, ON);
displayWord(TWENTY, OFF);
displayWord(MTEN, OFF);
displayWord(MFIVE, OFF);
displayWord(HALF, OFF);
displayWord(TO, ON);
displayWord(PAST2, OFF);
}
if(mytimemin>47 && mytimemin<53){
displayWord(QUARTER, OFF);
displayWord(TWENTY, OFF);
displayWord(MTEN, ON);
displayWord(MFIVE, OFF);
displayWord(HALF, OFF);
displayWord(TO, ON);
displayWord(PAST2, OFF);
}
if(mytimemin>52 && mytimemin<58){
displayWord(QUARTER, OFF);
displayWord(TWENTY, OFF);
displayWord(MTEN, OFF);
displayWord(MFIVE, ON);
displayWord(HALF, OFF);
displayWord(TO, ON);
displayWord(PAST2, OFF);
}
//hours
if(mytimehr==1||mytimehr==13){
if(mytimemin>32){
displayWord(ONE, OFF);
displayWord(TWO, ON);
displayWord(THREE, OFF);
}
else
{
displayWord(ONE, ON);
displayWord(TWO, OFF);
displayWord(TWELVE, OFF);
}
}
if(mytimehr==2||mytimehr==14){
if(mytimemin>32){
displayWord(TWO,OFF);
displayWord(THREE,ON);
displayWord(FOUR,OFF);
}
else
{
displayWord(ONE,OFF);
displayWord(TWO,ON);
displayWord(THREE,OFF);
}
}
if(mytimehr==3||mytimehr==15){
if(mytimemin>32){
displayWord(THREE,OFF);
displayWord(FOUR,ON);
displayWord(FIVE,OFF);
}
else
{
displayWord(TWO,OFF);
displayWord(THREE,ON);
displayWord(FOUR,OFF);
}
}
if(mytimehr==4||mytimehr==16){
if(mytimemin>32){
displayWord(FOUR,OFF);
displayWord(FIVE,ON);
displayWord(SIX,OFF);
}
else
{
displayWord(THREE,OFF);
displayWord(FOUR,ON);
displayWord(FIVE,OFF);
}
}
if(mytimehr==5||mytimehr==17){
if(mytimemin>32){
displayWord(FIVE,OFF);
displayWord(SIX,ON);
displayWord(SEVEN,OFF);
}
else
{
displayWord(FOUR,OFF);
displayWord(FIVE,ON);
displayWord(SIX,OFF);
}
}
if(mytimehr==6||mytimehr==18){
if(mytimemin>32){
displayWord(SIX,OFF);
displayWord(SEVEN,ON);
displayWord(EIGHT,OFF);
}
else
{
displayWord(FIVE,OFF);
displayWord(SIX,ON);
displayWord(SEVEN,OFF);
}
}
if(mytimehr==7||mytimehr==19){
if(mytimemin>32){
displayWord(SEVEN,OFF);
displayWord(EIGHT,ON);
displayWord(NINE,OFF);
}
else
{
displayWord(SIX,OFF);
displayWord(SEVEN,ON);
displayWord(EIGHT,OFF);
}
}
if(mytimehr==8||mytimehr==20){
if(mytimemin>32){
displayWord(EIGHT,OFF);
displayWord(NINE,ON);
displayWord(TEN,OFF);
}
else
{
displayWord(SEVEN,OFF);
displayWord(EIGHT,ON);
displayWord(NINE,OFF);
}
}
if(mytimehr==9||mytimehr==21){
if(mytimemin>32){
displayWord(NINE, OFF);
displayWord(TEN,ON);
displayWord(ELEVEN,OFF);
}
else
{
displayWord(EIGHT, OFF);
displayWord(NINE, ON);
displayWord(TEN, OFF);
}
}
if(mytimehr==10||mytimehr==22){
if(mytimemin>32){
displayWord(TEN, OFF);
displayWord(ELEVEN, ON);
displayWord(TWELVE, OFF);
}
else
{
displayWord(NINE, OFF);
displayWord(TEN, ON);
displayWord(ELEVEN, OFF);
}
}
if(mytimehr==11||mytimehr==23){
if(mytimemin>32){
displayWord(ONE, OFF);
displayWord(ELEVEN, OFF);
displayWord(TWELVE, ON);
}
else
{
displayWord(TEN, OFF);
displayWord(ELEVEN, ON);
displayWord(TWELVE, OFF);
}
}
if(mytimehr==12||mytimehr==0){
if(mytimemin>32){
displayWord(ONE, ON);
displayWord(TWO, OFF);
displayWord(TWELVE, OFF);
}
else
{
displayWord(ONE, OFF);
displayWord(TWO, OFF);
displayWord(TWELVE, ON);
}
}
}
unsigned long minuteCheck = 0;
void loop (){
now();
mytimemonth=month();
Serial.print(mytimemonth);
Serial.print("/");
mytimeday=day();
Serial.print(mytimeday);
Serial.print(", ");
mytimehr=hour();
Serial.print(mytimehr);
Serial.print(":");
mytimemin=minute();
Serial.print(mytimemin);
Serial.print(":");
mytimesec=second();
Serial.println(mytimesec);
minuteCheck = millis() / 1000;
if (minuteCheck % 60 == 0) { // i.e if it's fully divisible by 60
displayClock();
}
animations.UpdateAnimations();
strip.Show();
}
}