/*
LauraClock.ino. Program for the long 14-segment clock for Laura to verify clock adjusting
The clock Display has 6, 4-character 14-segmeny yellow modules from SparkFun.
Each module is so;dered to an "Backpack" from SparkFun.which provides I2C connectivity.
The display is powered via a DC-DC converter with nominal 9-12V input.
The display is connected to the SparkFun "MiniPro", 5V 16MHZ Atmel P238.
This is powered from the same 7-12V input tp its Raw input.
Contro is by an incremental encoder with push-button.
It has a RTC clock module with a DS1307. It used 2 rechargarbable batteries fo backup.
There are also 4 analog inputs:a 1.2V reference diode, a temperature sensor,
a photocell and an external input from an augio jack.
John Saunders 10/27/2022
*/
// Libraries:
#include <EncoderStepCounter.h> //Better than Encoder
#include "SegDisp.h" //For the Spark Fun 4-character display with I2C backpack
#include <Wire.h> //For I2C
#include "RTClib.h" //For the old RTC with a DS1307
#include <EEPROM.h>
//Hardware definitions
#define NUMDISP 6 //The display has 6 modules with addresses 70 - 75
#define rtcAddr 0x68
#define secPort 2 //The one-Hz output of the DS1307
#define buttPort 3 //Pressing the incremental encoder knob to ground
#define lightPort A0 //A CDS phtocell embedded in the box botton, conected to Vcc
#define voltPort A1 //A 3mm audio jack at the botton of the base with 2:1 divider 2k ohm
#define tempPort A2 //A LM335 sticking out of the back of the box
#define refPort A3 //A 1.2V LM385 to avoid dependind on Vcc for ADVC reference
#define ENCODER_USE_INTERRUPTS
#define ENTRYLEN 30
// Instanstaning devices
DS1307 RTC;
EncoderStepCounter controlKnob(4, 5);
// Global variables:
bool buttFlag = false; //Button pressed state, down = true
bool secFlag;
volatile byte darkCount = 0;
volatile byte dotAddr;
int dispTemp;
byte nextEvent;
byte currWD;
byte numEvents = 19;
uint8_t darkThreshold; //Photocell output, less is dark, stored in RTC address 11
uint8_t messageSpeed; // In units of 100 milliseconds, stored in RTC address 10
uint8_t darkCountMax; //Seconds of display on after button pressed, stored in RTC address 12
//Constants:
const byte measSampleCount = 25; //Number of samples for measuring temperature and external voltage
const float refVal = 1.233; //LM385
const float zeroC = 2731.5; //Convert LM335 deg K to Deg C
const float zeroF = 4596.7; //Convert LM335 deg K to Deg F
const int mainPosMax = 6;
const int mainPosMin = 1;
const int adjPosMax = 17;
const int adjPosMin = 7;
const int numMsgs = 25;
//Strings:
#define welcomeMesgLen 4
struct msg_t {
String msg;
byte len;
};
const struct msg_t messages[numMsgs] = {
{"Hello, I am Walnut Clock", 24}, {"John Saunders made me ", 24}, // 0-1 main
{"I live at 40641 178 St E", 24}, {"Lake Los Angeles, CA ", 24}, // 2-3 main
{"LIGHT=", 6}, {"Press for Clock Adjust", 22}, {"Press to review dates", 21}, // 4-6 main
{"minutes", 7}, {"hours", 5}, {"date", 4}, {"month", 5}, {"year", 4}, // 0-4 adjust, add 7
{"Press for Temp in deg", 21}, {"Press for", 9}, {"Hour Mode", 9}, // 5-7 adjust, add 7
{"Press to exit adjust", 20}, {"Press to Adjust ", 16}, {"Press to store", 14}, // 8-10 adjust, add 7
{"Dial to display dates ", 21}, {"on-time", 7}, {"Darkness", 8}, // 11-14 adjust, add 7
{"Interval", 8}, {" ", 1}, {"Dare to be wise", 15}, {"Go by the other road", 20}
};
struct event_t {
char nme[25];
uint8_t typ;
uint8_t mnth;
uint8_t dte;
uint16_t yr;
};
const char *eventItem[] = {"Anniversary" , "Birthday"};
event_t event;
struct edit_t {
uint8_t msgInx;
uint8_t addr;
uint8_t editMin;
uint8_t editMax;
};
const struct edit_t edits[] = {
// exit Degree select Hour Mode
{8, 0, 0, 1}, {15, 8, 0, 1}, {15, 2, 0, 0x40},
// minutes hours date month year Dark On Time Dark Threshold Line Delay
{0, 1, 0, 59}, {1, 2, 0, 23}, {2, 4, 1, 31}, {3, 5, 1, 12}, {4, 6, 22, 60}, {12, 12, 5, 100}, {13, 11, 5, 100}, {14, 10, 4, 80}
};
const char weekdays[] = {"SUNMONTUEWEDTHUFRISAT"};
const char months[] = {"JANFEBMARAPRMAYJUNJLYAUGSEPOCTNOVDEC"};
const uint16_t monthDays[13] = {
// J F M A M J J A S O N D
0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
};
// ------------------- Interrupt service Routines -----------------------
void showTime(void) { //Called by the one-Hz interrupt
if (dotAddr < 23) {
dotAddr++;
}
else {
dotAddr = 0;
}
if (darkCount > 0) {
darkCount--;
}
secFlag = true;
}
void setButt(void) { //Called by the push-button
buttFlag = true;
}
// ------------------- Utilities -----------------------
uint8_t bcd2bin (uint8_t bcdval) {
return bcdval - 6 * (bcdval >> 4);
}
uint8_t bin2bcd (uint8_t binval) {
return binval + 6 * (binval / 10);
}
uint16_t daysInYear(uint8_t mnth, uint8_t dte) {
return monthDays[mnth] + dte;
}
byte getNextEvent(byte mnth, byte dte) {
int currDays, eventDays, inx;
currDays = daysInYear(mnth, dte);
for (inx = 0; inx < numEvents; inx++) {
EEPROM.get((inx * ENTRYLEN), event);
eventDays = daysInYear(event.mnth, event.dte);
if (eventDays >= currDays) {
break;
}
}
return inx;
}
nt getTemperature(void) { //LM335
static float tempAcc = 0.0;
static byte sampleCount = 0;
int refCount, tempCount;
int degVal;
uint8_t bcdVal;
if (sampleCount == 0) {
bcdVal = RTC.read(8);
if (bcdVal == 'C') {
degVal = int((1000 * tempAcc / measSampleCount) - zeroC);
}
else {
degVal = int((1800 * tempAcc / measSampleCount) - zeroF);
}
tempAcc = 0;
}
refCount = analogRead(refPort);
tempCount = analogRead(tempPort) ;
tempAcc += (refVal * tempCount / refCount);
sampleCount++;
if (sampleCount >= measSampleCount) {
sampleCount = 0;
}
return degVal;
}
bool setDarkFlag(void) { //Photocell
int lightCount = analogRead(lightPort);
if ((lightCount < darkThreshold) && (darkCount == 0)) {
return true;
}
else {
return false;
}
}
// ------------------- Display Functions -----------------------
//Up to 9999, blank leadimg zeroes,insert decimal points after last 3 digits
byte dispNum(byte addr, int pos, bool blz = false, byte dp = 0) {
byte c;
bool plz = false;
bool setDp = false;
if (pos < 0) {
pos = -pos;
SegDisp_drawAscii(addr++, 0x2D, false);
}
if (pos > 9999) {
pos = pos % 10000;
}
c = (pos / 1000) + 0x30;
if ((blz == true) && (c == 0x30)) {
plz = true;
}
else {
SegDisp_drawAscii(addr++, c, false);
plz = false;
}
pos = pos % 1000;
c = (pos / 100) + 0x30;
if ((blz == true) && (c == 0x30) && (plz == true)) {
plz = true;
}
else {
if (dp == 2) {
setDp = true;
}
SegDisp_drawAscii(addr++, c, setDp);
plz = false;
setDp = false;
}
pos = pos % 100;
c = (pos / 10) + 0x30;
if ((blz == true) && (c == 0x30) && (plz == true)) {
plz = true;
SegDisp_drawAscii(addr++, 0x20, false);
}
else {
if (dp == 1) {
setDp = true;
}
SegDisp_drawAscii(addr++, c, setDp);
setDp = false;
}
c = (pos % 10) + 0x30;
if (dp == 3) {
setDp = true;
}
SegDisp_drawAscii(addr++, c, setDp);
SegDisp_writeAlpha();
return addr;
}
void dispWeekday(byte p, byte wd) {
char c;
byte indx;
byte addr = p;
for (byte i = 0; i < 3; i++) {
indx = (3 * wd ) + i;
c = weekdays[indx];
SegDisp_drawAscii(addr++, c, false);
}
}
uint8_t dispMonth(byte p, byte mth) {
char c;
byte indx;
byte addr = p;
for (byte i = 0; i < 3; i++) {
indx = (3 * (mth - 1)) + i;
c = months[indx];
SegDisp_drawAscii(addr++, c, false);
}
return addr;
}
void dispWelcome(void) {
char c;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < messages[i].len; j++) {
c = messages[i].msg.charAt(j);
SegDisp_drawAscii( j, c, false);
}
SegDisp_writeAlpha();
delay(100 * messageSpeed );
}
}
int dispMsg(byte addr, byte inx) {
char c;
for (int i = 0; i < messages[inx].len; i++) {
c = messages[inx].msg.charAt(i);
SegDisp_drawAscii(addr++, c, buttFlag);
}
return addr;
}
int dispName(byte addr) {
char c;
byte pos = 0;
do {
c = event.nme[pos++];
SegDisp_drawAscii(addr++, c, false);
} while (c > 0x1F);
SegDisp_drawAscii((addr - 1), 0x20, false);
return addr;
}
int dispType(uint8_t addr, byte index) {
char c;
byte pos = 0;
do {
c = eventItem[index][pos++];
SegDisp_drawAscii(addr++, c, false);
} while (c > 0x1F);
SegDisp_drawAscii((addr - 1), 0x20, false);
return addr;
}
void dispDark(void) {
static byte oldDotAddr;
if (dotAddr > 0) {
oldDotAddr = dotAddr - 1;
}
else {
oldDotAddr = 23;
}
SegDisp_drawAscii(oldDotAddr, 0x20, false);
SegDisp_drawAscii(dotAddr, 0x20, true);
SegDisp_writeAlpha();
}
// ------------------- Analog Measurement Functions -----------------------
int dispExtVolt(byte addr) { //Bottom Read Audio Jack
float voltAcc = 0.0;
int refCount, voltCount;
int voltVal;
for (int i = 0; i < measSampleCount; i ++ ) {
refCount = analogRead(refPort);
voltCount = analogRead(voltPort) ;
voltAcc += (refVal * voltCount / refCount);
delayMicroseconds(200);
}
voltVal = int((198 * voltAcc / measSampleCount));
addr = dispNum(addr, voltVal, true, 2);
SegDisp_drawAscii(addr++, 0x56, false);
return addr;
}
void dispEvent(bool line) {
uint8_t addr;
if (line) {
addr = dispType(0, event.typ);
addr = dispMonth(addr, event.mnth);
addr = dispNum(addr, event.dte, true, 0);
SegDisp_drawAscii(addr++, 0x20, false);
addr = dispNum(addr, event.yr);
}
else {
dispName(0);
}
SegDisp_writeAlpha();
}
// ------------------- Group Functions -----------------------
int dispMain(void) {
static int mainPos = mainPosMin; //Encoder output
static int oldMainPos = mainPosMax + 1;
uint8_t addr, bcdVal, wd, mm, dd, newGroup = 0;
int temp, inx;
static bool line = 0;
static int intervalCount = 0;
if (oldMainPos >= mainPosMax) {
controlKnob.setPosition(mainPos);
}
controlKnob.tick();
mainPos = controlKnob.getPosition();
if (mainPos > mainPosMax) {
mainPos = mainPosMin;
controlKnob.setPosition(mainPosMin);
}
if (mainPos < mainPosMin) {
mainPos = mainPosMax;
controlKnob.setPosition(mainPosMax);
}
DateTime now = RTC.now();
if (mainPos != oldMainPos) {
SegDisp_setAll(0);
oldMainPos = mainPos;
EEPROM.get((nextEvent * ENTRYLEN), event);
}
inx = mainPos - mainPosMin;
switch (inx) {
case 0:
bcdVal = RTC.read(2);
if (bcdVal > 0x2F) {
bcdVal = bcdVal & 0x1F;
}
if (bcdVal < 0x10) {
SegDisp_drawAscii(0, 0x20, false); // Blank a leading zero
}
else {
SegDisp_drawAscii(0, ((bcdVal / 16) + 0x30), false);
}
SegDisp_drawAscii(1, ((bcdVal & 0x0F) + 0x30), true);
addr = dispNum(2, now.minute(), true, 3);
addr = dispNum(addr, now.second(), true);
wd = now.dayOfWeek();
dispWeekday(8, wd);
mm = now.month();
dispMonth(12, mm);
dd = now.day();
dispNum(16, dd , true);
dispNum(19, now.year(), false);
if (wd != currWD) {
nextEvent = getNextEvent(mm, dd);
currWD = wd;
}
if (buttFlag) {
buttFlag = false;
SegDisp_setAll(0);
dispMsg(0, 23);
SegDisp_writeAlpha();
delay(messageSpeed * 100);
SegDisp_setAll(0);
newGroup = 0;
}
break;
case 1:
addr = dispNum(0, dispTemp, true, 1);
SegDisp_drawAscii(addr++, 10, false);
bcdVal = RTC.read(8);
SegDisp_drawAscii(addr++, bcdVal, false);
addr++;
addr = dispMsg(addr, 4);
temp = analogRead(lightPort);
addr = dispNum(addr++, temp, true, 0);
addr++;
addr = dispExtVolt(addr);
if (buttFlag) {
buttFlag = false;
SegDisp_setAll(0);
dispMsg(0, 24);
SegDisp_writeAlpha();
delay(messageSpeed * 100);
SegDisp_setAll(0);
newGroup = 0;
}
break;
case 2:
addr = dispMsg(0, 5);
if (buttFlag) {
SegDisp_setAll(0);
dispMsg(0, 16);
newGroup = 1;
buttFlag = false;
}
break;
case 3:
addr = dispMsg(0, 6);
if (buttFlag) {
newGroup = 2;
buttFlag = false;
}
break;
case 4:
dispDark();
break;
case 5:
intervalCount++;
delay(50);
if (intervalCount > messageSpeed) {
intervalCount = 0;
line = !line;
SegDisp_setAll(0);
}
dispEvent(line);
if (buttFlag) {
newGroup = 0;
buttFlag = 0;
}
break;
}
SegDisp_writeAlpha();
oldMainPos = mainPos;
while (digitalRead(buttPort) == 0);
return newGroup;
}
uint8_t adjClock(uint8_t index) { // input is to edits[], returns new value
uint8_t itemAddr = edits[index].addr;
static uint8_t editPos;
uint8_t editPosMin = edits[index].editMin;
uint8_t editPosMax = edits[index].editMax;
uint8_t editVal = RTC.read(itemAddr);
while (digitalRead(buttPort) == 0);
editPos = bcd2bin(editVal);
controlKnob.setPosition(editPos);
while (digitalRead(buttPort) == 1) {
controlKnob.tick();
editPos = controlKnob.getPosition();
if (editPos > editPosMax) {
editPos = editPosMin;
controlKnob.setPosition(editPosMin);
}
if (editPos < editPosMin) {
editPos = editPosMax;
controlKnob.setPosition(editPosMax);
}
dispNum(16, editPos, true, 0);
SegDisp_writeAlpha();
};
editVal = bin2bcd(editPos);
SegDisp_drawAscii(23, (itemAddr + 0x30), false);
SegDisp_writeAlpha();
while (digitalRead(buttPort) == 0);
return editVal;
}
int dispAdjust(void) {
static int adjPos; //Encoder output, in the display
static int oldAdjPos = adjPosMax + 1;
uint8_t addr; //Of the display
uint8_t bcdVal, newVal, inx; //of the messages table relative to adjPosMin
byte newGroup = 1;
adjPos = 1;
controlKnob.tick();
if (oldAdjPos > adjPosMax) {
controlKnob.setPosition(adjPos);
}
adjPos = controlKnob.getPosition();
if (adjPos > adjPosMax) {
adjPos = adjPosMin;
controlKnob.setPosition(adjPosMin);
}
if (adjPos < adjPosMin) {
adjPos = adjPosMax;
controlKnob.setPosition(adjPosMax);
}
if (adjPos != oldAdjPos) {
SegDisp_setAll(0);
oldAdjPos = adjPos;
}
inx = adjPos - adjPosMin;
switch (inx) {
case 0:
addr = dispMsg(addr, (adjPosMin + 8));
if (buttFlag) { // exit
newGroup = 0;
buttFlag = false;
}
break;
case 1: //temperature mode selection
addr = dispMsg(0, (adjPosMin + 5));
bcdVal = RTC.read(8);
if (bcdVal == 'C') {
newVal = 'F';
}
else {
newVal = 'C';
}
SegDisp_drawAscii(addr++, newVal, false);
if (buttFlag) {
RTC.write(8, newVal);
buttFlag = false;
newGroup = 0;
}
break;
case 2:
addr = dispMsg(0, (adjPosMin + 6)); //12 or 24 hour seletion
addr++;
bcdVal = RTC.read(2);
if (bcdVal > 0x2F) { //12-hour mode, go to 24
addr = dispNum(addr, 24, true);
newVal = bitClear(bcdVal, 6);
}
else {
addr = dispNum(addr, 12, true);
newVal = bitSet(bcdVal, 6);;
}
if (buttFlag) {
RTC.write(2, newVal);
buttFlag = false;
newGroup = 0;
}
addr += 2;
addr = dispMsg(addr, (adjPosMin + 7));
break;
case 3: case 4: case 5: case 6: case 7: case 8: case 9: case 10: //Time and date adjustment
addr = dispMsg(0, (adjPosMin + 9));
addr = dispMsg(addr, edits[inx].msgInx + 7);
if (buttFlag) {
buttFlag = false;
SegDisp_setAll(0);
addr = dispMsg(0, (adjPosMin + 10));
newVal = adjClock(inx);
newGroup = 0;
RTC.write(edits[inx].addr, newVal);
}
break;
}
SegDisp_writeAlpha();
while (digitalRead(buttPort) == 0);
return newGroup;
}
int dispDates(void) {
static int eventPos = nextEvent; //Encoder output, in the display
static int prevEventPos;
byte inx;
uint16_t eepromAddr;
byte newGroup = 2;
byte numSteps = 2 * numEvents + 1;
do {
controlKnob.tick();
eventPos = controlKnob.getPosition();
if (eventPos > numSteps) {
eventPos = 0;
controlKnob.setPosition(0);
}
if (eventPos < 0) {
eventPos = numSteps;
controlKnob.setPosition(numSteps);
}
inx = eventPos / 2;
eepromAddr = inx * ENTRYLEN;
if (eventPos != prevEventPos) {
prevEventPos = eventPos;
SegDisp_setAll(0);
SegDisp_drawAscii(23, 0X20, (bitRead(inx, 0) == 0));
if (bitRead(eventPos, 0) == 0) {
EEPROM.get(eepromAddr, event);
dispEvent(false);
}
else {
dispEvent(true);
}
}
if (buttFlag) { // exit
newGroup = 0;
buttFlag = false;
break;
}
} while (digitalRead(buttPort) == 1);
return newGroup;
}
// ------------------- Control Functions -----------------------
void setup() {
Wire.begin();
RTC.begin();
SegDisp_init();
controlKnob.begin();
pinMode(buttPort, INPUT_PULLUP); //Others have resistors
RTC.write(7, 0x10); //Enable 1 Hz square wave
numEvents = RTC.read(9);
messageSpeed = RTC.read(10);
darkThreshold = RTC.read(11);
darkCountMax = RTC.read(12);
attachInterrupt(digitalPinToInterrupt(secPort), showTime, RISING);
attachInterrupt(digitalPinToInterrupt(buttPort), setButt, FALLING);
dispWelcome();
dispTemp = 720;
currWD = 0;
darkCount = darkCountMax;
dotAddr = 0;
buttFlag = false;
}
void loop() {
static byte group = 0; //0=main,1-adjust,2-review,3=dark
static byte prevGroup = 9;
dispTemp = getTemperature();
if (setDarkFlag()) {
group = 3;
}
switch (group) {
case 0:
group = dispMain();
break;
case 1:
group = dispAdjust();
break;
case 2:
dispDates();
group = 0;
break;
case 3:
if (!setDarkFlag()) {
group = 0;
}
else {
dispDark();
}
if (buttFlag) {
darkCount = darkCountMax;
buttFlag = false;
}
break;
}
if (prevGroup != group) {
SegDisp_setAll(0);
prevGroup = group;
}
while (digitalRead(buttPort) == 0);
}