/*
----------------------------------------------------------------------------
PortableOLEDClock
----------------------------------------------------------------------------
This sketch is for LCDs with PCF8574 or MCP23008 chip based backpacks
WARNING:
The Teensy LC processor specifoes 3.3 V only but tests on pins 5
(which is labeled "20MA"),18,19 show no current draw at 5V.
Pin lowBattPort is connected to the "LBO" (Battery voltage < 3.5) is specified in the
"Adafruit Power Boost Basic Charger"as Open Drain.
However is has a pull-up to battery voltage.
Pins 18 & 19 are I2C. The PCF8574 has 3300 ohm pullups to Vcc.
John Saunders 10/20/2023
----------------------------------------------------------------------------
LiquidCrystal compability:
Since hd44780 is LiquidCrystal API compatible, most existing LiquidCrystal
sketches should work with hd44780 hd44780_I2Cexp i/o class once the
includes are changed to use hd44780 and the lcd object constructor is
changed to use the hd44780_I2Cexp i/o class.
*/
#include <Wire.h>
#include <hd44780.h> // main hd44780 header
#include <hd44780ioClass/hd44780_I2Cexp.h> // i2c expander i/o class header
#include <Adafruit_AHTX0.h>
#include <DS1307RTC.h> //DS 1307RTC
#include <TimeLib.h>
tmElements_t tm;
Adafruit_AHTX0 aht;
hd44780_I2Cexp lcd(0x27 ); // declare lcd object: auto locate & auto config expander chip
// OLED geometry
const int LCD_COLS = 20;
const int LCD_ROWS = 4;
#define battPort A0
#define chargePort A1
#define keepalivePort 10
#define triggerPort 11
#define lowBattPort 16
#define encoderPort1 3
#define encoderPort2 2
#define timeoutView 15
#define timeoutSet 30
uint8_t runCountMax;
uint8_t runCount = runCountMax;
const float vRef = 3.292; //Actual measurement
int encCtr = 0;
int tickLow, tickHigh;
bool buttFlag;
//------------------Setup and Loop -----------------------
void setup()
{
int status;
pinMode(23, OUTPUT);
pinMode(keepalivePort, OUTPUT);
pinMode(lowBattPort, INPUT_PULLUP);
pinMode(encoderPort1, INPUT);
pinMode(encoderPort2, INPUT);
pinMode(triggerPort, INPUT);
digitalWrite(keepalivePort, LOW);
pulseKeepalive();
encCtr = 0;
buttFlag = false;
runCountMax = timeoutView;
runCount = runCountMax;
Serial.begin(9600);
delay(300);
//Serial.println(millis());
// digitalWrite(keepalivePort, LOW);
// initialize LCD with number of columns and rows:
// hd44780 returns a status from begin() that can be used
// to determine if initalization failed.
// the actual status codes are defined in <hd44780.h>
status = lcd.begin(LCD_COLS, LCD_ROWS);
if (status) // non zero status means it was unsuccesful
{
// hd44780 has a fatalError() routine that blinks an led if possible
// begin() failed so blink error code using the onboard LED if possible
//Serial.println("LCD init failure");
hd44780::fatalError(status); // does not return
}
aht.begin();
// initalization was successful
//Serial.println("Initialization complete");
// Print a message to the LCD
lcd.createChar(0, degSym);
lcd.home(); //UNO 0.6 ms TeensyLC .5
lcd.clear();
displayIntro();
RTC.read(tm);
pulseKeepalive();
delay(2000);
lcd.clear();
attachInterrupt(digitalPinToInterrupt(triggerPort), buttHandler, RISING);
pulseKeepalive();
}
void loop() {
sensors_event_t humidity, temp;
static int pageID = 0;
static int adjID = 0;
static int prevPageID = 0;
byte nowSec;
static byte prevSec = 100;
if (pageID < 2) {
runCountMax = timeoutView;
}
else {
runCountMax = timeoutSet;
}
RTC.read(tm);
nowSec = tm.Second;
if (nowSec != prevSec) {
pulseKeepalive(); //Keep in active mode
prevSec = nowSec;
//Refresh Measurements
aht.getEvent(&humidity, &temp); // populate temp and humidity objects with fresh data
meas.tmp = temp.temperature;
meas.hum = humidity.relative_humidity;
meas.batt = getBatteryVolt();
meas.chg = getChargeVolt();
if ((runCount > 0) && (meas.chg < 2.5)) {
runCount--;
}
if (runCount == 0) { //exit to Standby Mode
lcd.clear();
delay(5000); //Allows the Standalive Module 3-sec timer to expire
}
}
if (pageID != prevPageID) {
}
lcd.setCursor(0, 0);
switch (pageID) {
case 0:
if (pageID != prevPageID) {
encCtr = pageID;
runCount = runCountMax;
prevPageID = pageID;
lcd.clear();
}
displayTime(0);
displayDate(1);
displayBattery(2);
lcd.setCursor(0, 3);
lcd.print("Press to shutdown");
if (encTick(0, 1)) {
pageID = encCtr;
}
if (buttFlag) { //exit to Standby Mode
lcd.clear();
delay(5000); //Allows the Standalive Module 3-sec timer to expire
}
break;
case 1:
if (pageID != prevPageID) {
encCtr = pageID;
runCount = runCountMax;
prevPageID = pageID;
lcd.clear();
}
displayTemperatureC(0);
displayTemperatureF(1);
displayHumidity(2);
lcd.setCursor(0, 3);
lcd.print("Press for Settings");
if (encTick(0, 1)) {
pageID = encCtr;
}
if (buttFlag) {
pageID = 2;
buttFlag = false;
lcd.clear();
}
break;
case 2:
if (encTick(0, 6)) {
adjID = encCtr;
}
else {
encCtr = adjID;
}
if (adjVals[adjID].page == 0) {
lcd.setCursor(0, 0);
lcd.print("Return to Time page");
lcd.setCursor(0, 1);
lcd.print("From settings edit");
lcd.setCursor(0, 2);
lcd.print("Charge Volts = ");
lcd.print(meas.chg);
lcd.setCursor(0, 3);
lcd.print("Press button to exit");
if (buttFlag) {
pageID = 0;
buttFlag = false;
lcd.clear();
}
}
else {
lcd.setCursor(0, 0);
lcd.print("Adjustment Selection");
lcd.setCursor(0, 1);
lcd.print("Dial to pick item");
displayItem(2, adjID);
lcd.print("Edit item = ");
lcd.print(adjVals[adjID].desc);
lcd.setCursor(0, 3);
lcd.print("Press button io edit");
if (buttFlag) {
pageID = adjVals[adjID].page;
lcd.clear();
buttFlag = false;
}
}
break;
default:
if (pageID != prevPageID) {
encCtr = getAdjVal(adjID);
runCount = runCountMax;
prevPageID = pageID;
lcd.clear();
}
lcd.setCursor(0, 0);
if (adjVals[adjID].hdr == 0) {
displayTime(0);
}
if (adjVals[adjID].hdr == 1) {
displayDate(0);
}
if (encTick(adjVals[adjID].LL, adjVals[adjID].HL)) {
putAdjVal(adjID, encCtr);
RTC.write(tm);
runCount = runCountMax;
pulseKeepalive();
}
displayAdj(1,adjID);
lcd.setCursor(0, 2);
lcd.print("Dial to edit value");
lcd.setCursor(0, 3);
lcd.print("Press to return");
if (buttFlag) {
pageID = 2;
buttFlag = false;
lcd.clear();
}
break;
}
delay(1);
}
struct measurements_t {
float tmp;
float hum;
float batt;
float chg;
};
measurements_t meas = {80.4, 45.0, 3.78, 5.02 };
struct adjustments_t {
char desc[7]; //Goes on second line
uint8_t LL; //encTick lower limit
uint8_t HL; //encTick upper limit
byte hdr; //top line;0=time,1=date
byte page; //Page to jump to
};
const adjustments_t adjVals[7] = {
{"Hour", 0, 50, 0, 3}, {"Minute", 0, 59, 0, 4}, {"Day", 1, 7, 0, 5},
{"Date", 1, 31, 1, 6}, {"Month", 1, 12, 1, 7}, {"Year", 0, 55, 1, 8},
{"Return", 0, 0, 1, 0}
};
uint8_t getAdjVal(byte iD) {
uint8_t retVal;
switch (iD) {
case 0:
retVal = tm.Hour;
break;
case 1:
retVal = tm.Minute;
break;
case 2:
retVal = tm.Wday;
break;
case 3:
retVal = tm.Day;
break;
case 4:
retVal = tm.Month;
break;
case 5:
retVal = tm.Year;
if (retVal > 95) {
retVal -= 96;
}
break;
default:
retVal = 0;
break;
}
return retVal;
}
void putAdjVal(byte iD, uint8_t newVal) {
switch (iD) {
case 0:
tm.Hour = newVal;
break;
case 1:
tm.Minute = newVal;
break;
case 2:
tm.Wday = newVal;
break;
case 3:
tm.Day = newVal;
break;
case 4:
tm.Month = newVal;
break;
case 5:
tm.Year = newVal;
break;
}
}
char lineBuff[25];
//------------------ Control Functions -----------------------
void buttHandler(void) { //Called by triggerPort interrupt
buttFlag = true;
runCount = runCountMax;
}
inline void pulseKeepalive(void) { //Resets the 3-sec timout in the Keepalive Module
digitalWrite(keepalivePort, HIGH);
delay(2);
digitalWrite(keepalivePort, LOW);
}
void trace() {
digitalWrite(23, HIGH);
delay(300);
digitalWrite(23, LOW);
}
I spent a lot of time unsuccessfully using sprintf() for temperature, humidity, battery and charging before I read a blog post informing me that the Arduino IDE does not support sprintf() for floating point.
//------------------ Display Functions -----------------------
const char *wDays[7] = {
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
};
const char *months[12] = {
"January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"
};
const char *settings[7] = {
"Setting is off", "Minutes", "Hours", "Day of Week", "Date", "Month", "Year"
};
byte degSym[8] = {
B11100,
B10100,
B11100,
B00000,
B00000,
B00000,
B00000,
};
float getBatteryVolt() {
analogReference(DEFAULT);
int battCount = analogRead(battPort);
float battVolt = (vRef * battCount) / 661;
return battVolt;
}
float getChargeVolt(void) {
analogReference(DEFAULT);
int chargeCount = analogRead(chargePort);
float chargeVolt = (vRef * chargeCount) / 512;
return chargeVolt;
}
void displayTime(byte row) {
int numChars;
char lineBuf[] = " ";
RTC.read(tm);
numChars = sprintf(lineBuf, "%02d:%02d:%02d %s", (int)tm.Hour, (int)tm.Minute, (int)tm.Second, wDays[tm.Wday - 1]);
lineBuf[numChars] = ' ';
lineBuf[20] = 0;
lcd.setCursor(0, row);
lcd.print(lineBuf);
}
void displayDate(byte row) {
int numChars;
char lineBuf[] = " ";
RTC.read(tm);
uint8_t y2k = tm.Year;
if(y2k > 95) {
y2k -= 96;
}
numChars = sprintf(lineBuf, "%s %02u 20%02u ", months[tm.Month - 1], tm.Day, y2k);
lineBuf[numChars] = ' ';
lineBuf[20] = 0;
lcd.setCursor(0, row);
lcd.print(lineBuf);
}
void displayItem(byte row, byte id) {
int numChars;
char lineBuf[] = " ";
numChars = sprintf(lineBuf, "%s %s", "Edit item =", adjVals[id].desc);
lineBuf[numChars] = ' ';
lineBuf[20] = 0;
lcd.setCursor(0, row);
lcd.print(lineBuf);
}
void displayAdj(byte row, byte id) {
int numChars;
char lineBuf[] = " ";
numChars = sprintf(lineBuf, "%s %s", "Editing ", adjVals[id].desc);
lineBuf[numChars] = ' ';
lineBuf[20] = 0;
lcd.setCursor(0, row);
lcd.print(lineBuf);
}
void displayBattery(byte row) {
lcd.setCursor(0, row);
lcd.print("Battery = ");
lcd.print(meas.batt, 2 );
lcd.print(" V ");
}
void displayTemperatureC(byte row) {
lcd.setCursor(0, row);
lcd.print("Temperature = ");
lcd.print(meas.tmp, 1);
lcd.print((char)byte(0));
lcd.print('C');
}
void displayTemperatureF(byte row) {
float tempVal;
tempVal = ((9.0 * meas.tmp) / 5.0) + 32.0;
lcd.setCursor(0, row);
lcd.print("Temperature = ");
lcd.print(tempVal, 1);
lcd.print((char)byte(0));
lcd.print('F');
}
void displayHumidity(byte row) {
lcd.setCursor(0, row);
lcd.print("Humidity = ");
lcd.print(meas.hum, 1);
lcd.print("% ");
}
void displayIntro(void) {
lcd.setCursor(0, 0);
lcd.print("Portable OLED Clock");
lcd.setCursor(2, 1);
lcd.print("This was designed");
lcd.setCursor(4, 2);
lcd.print("and made by");
lcd.setCursor(0, 3);
lcd.print("John Saunders age 89");
}
I invented this algorithm. It is very resistant to glitches in the input. This is a full-step decoder. If you add 0x1E and 0x2D you get a half-step decoder.
//------------------ Encoder Functions -----------------------
/* This solution is different. It uses a byte as a software shift register.
For each input shange the contents are shifted right 2, and bits 6 & 7 replaced by the new encoder reading.
The byte can be represented as a hex number. As the knob is rotated this number repeats each 4 readings.
The numbers are unique and are different for the other direction. One per direction is picked to change the counter.
*/
bool encTick(uint8_t lLimit, uint8_t uLimit) {
static uint8_t prevEncVal;
static uint8_t encSr;
uint8_t encVal;
bool retVal = false;
encVal = 0;
if (digitalRead(encoderPort1) == LOW) {
bitSet(encVal, 6);
}
if (digitalRead(encoderPort2) == LOW) {
bitSet(encVal, 7);
}
if (encVal != prevEncVal) {
prevEncVal = encVal;
encSr = encSr >> 2;
encSr |= encVal;
if (encSr == 0xE1) {
if (encCtr < uLimit) {
encCtr++;
}
else {
encCtr = lLimit;
}
retVal = true;
}
if (encSr == 0xD2) {
if (encCtr > lLimit) {
encCtr--;
}
else {
encCtr = uLimit;
}
retVal = true;
}
}
return retVal;
}