Portable OLED Clock Arduino Sketch

Definitions and Variables

/*

  ----------------------------------------------------------------------------

  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

//------------------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);

}

Control Function

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);

}

Display Functions

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");

}

Encoder 

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;

}