Mem-Aid - a gadget to help those on the Alzheimers / Dementia journey

Having direct experience of a loved one with short term memory issues, I wanted to build a gadget that would be as simple as possible for them to use. I knew that anything even a little complicated would be too much. It’s 6 weeks in now and going really well. Only one set of tablets missed in the last 3 weeks!

As you can see from the image it’s a case of Read it, Do it, Push the button.

Here’s a vid showing a message being displayed and then dismissed by pushing the button. And in this vid, you’ll see the anatomy of a message

See the full pdf user guide for the Mem-Aid.

There are Google sheets (a blank template messages sheet and an example one) that are linked to in the user guide so you can get your own private copies to work with.

In addition to the parts shown in the schematic, you’ll need a Micro Sd Card USB Reader so you can transfer your prepared messages csv file to the Micro SD Card that goes into the Mem-Aid.

Now for the sketch code. First, be aware that it allows for DST in the UK (British Summer Time and Greenwich Mean Time). I’ve given lots of comments so you can deal with having a different or no DST regime where you are.

Shout with questions - I’m happy to help :smiley:

[Code updated slightly after a clean up of items suggested by a verbose-all compile]

[V2 code released fixing some bugs. Pasted below replacing previous version]

/* The Daily Memory Aid aka "Mem-Aid" V2
  V2 fixes a couple of minor and not-so-minor bugs including a DST-related one
  
An alarm messaging device for those with severe memory
loss / Alzheimers / Dementia.
Note that there is no support for messages set by calendar date
Also note that making any code changes that increase the program memory
use is to be avoided at all costs.
As supplied, the sketch uses 29884 bytes of the available 30720 bytes program 
storage space and 1227 bytes of dynamic memory of the available 2048 bytes
available in the Arduino Nano ATMega328P used for this sketch

// Daylight Savings Time (DST) in UK
// =================================
// When initially setting the time on the RTC, see the instruction in the
// setup() function
// **********************
// In the UK the clocks go forward 1 hour at 1am on the last Sunday in March, and 
// back 1 hour at 2am on the last Sunday in October.
// If the clock time and date is set during the winter, ie before British Summer Time
// starts on the last Sunday of March at 1am, then 1 hour will be added to the RTC 
// when that date / time arrives. It's then in British Summer Time (BST)
// The last Sunday in March dates over the next few years are calculated automatically
// but are repeated here for good measure:
// 2026 29/3, 2027 28/3, 2028 26/3, 2029 25/3, 2030 31/3, 2031 30/3, 2032 28/3
// If the clock time and date is set during British Summer Time, ie before they go 
// back on the last Sunday of October at 2am, then 1 hour will be subtracted from 
// the RTC when that date / time arrives
// The last Sunday in October dates over the next few years are calculated automatically
// but are also repeated here for good measure:
// 2026 25/10, 2027 31/10, 2028 29/10, 2029 28/10, 2030 27/10, 2031 26/10, 2032 31/10
//
// ***********************
// If this is to be used where there's no change in time due to DST then
// see the comments below starting with X. X1 is the first.
// Note that the functions that are used in DST processing are:
// dealWithPossibleBSTOrGMTMismatch(): called only once in setup()
// dealWithPossibleSwitchover(): called twice in loop() 
// getLastSundayInMonth(): called twice in dealWithPossibleBSTOrGMTMismatch()
// and twice in dealWithPossibleSwitchover()
// 

// ***********************
// If this is to be used where there is DST but it's different from the UK
// then the three functions: 
//    dealWithPossibleBSTOrGMTMismatch()
//    dealWithPossibleSwitchover()
//    getLastSundayInMonth()
// will need to be renamed / rewritten accordingly.


This sketch uses hacked and whacked code from AlarmInterrupt.ino
An example on using interrupts with DS3231 alarms.
Jacob Nuernberg
08/22
Library: https://github.com/NorthernWidget/DS3231/tree/master

Hardware setup:
  See the included schematic. Most important:
  Connect DS3231 SQW pin to Arduino interrupt pin 2
  Connect SD Card SS to Arduino Pin 10

Tested on:
- Arduino nano

   Code added to Jacob's to prevent Alarm 2 from interfering with the
   interrupt, by setting A2Minute to a value that can never match the time
   and setting AlarmBits to 0b01100000: alarm "when minutes match".
   Also clear the A2 alarm flag.
David Sparks, September 2022
*/

// When the term "sleeping" is used in the comments, it means that an 
// alarm has been set and the main loop is going round and round waiting
// for the interrupt from the RTC to "wake it up"

// In the comments, mention is made of the BSTGMT flag. It is shorthand for
// saying the value in EEPROM location 0


#include <DS3231.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 20, 4);
#include <EEPROM.h>
// EEPROM memory location 0 is set to 1 when the RTC is in BST and 0 when GMT
// It's initially set when the time is being manually set on the RTC (see setup() function)
// The EEPROM is also used to store the timestamp of the last skipped button pushes 
// with loc 1 holding the last used button skip number. If none have been skipped yet then
// a nonsensical date time will be returned if one is requested
// Only the last numPushButtonSkipsToRecord are recorded.
// loc 2,3,4,5,6,7 - 8,9,10,11,12,13 - 14,15,16,17,18,19 etc hold the buttonpush skips dates/times

#include <DatabaseOnSD.h>
// https://github.com/divinofire/DatabaseOnSD
// Note that the library (DatabaseOnSD.h) needs to be modified so that the chip select pin
// works correctly on an Arduino Nano:
// #define CHIP_SELECT_PIN 4 on line 57 becomes:
// #define CHIP_SELECT_PIN 10

// myRTC interrupt pin
#define CLINT 2

// Set up clock
DS3231 myRTC;

// Variables for use in method parameter lists
byte alarmDay;
byte alarmHour;
byte alarmMinute;
byte alarmSecond;
byte alarmBits;
bool alarmDayIsDay;
bool alarmH12;
bool alarmPM;

// Interrupt signaling byte
volatile byte tick = 0;

const int buttonPin = 4;  // the D number of the pushbutton pin
const auto PRESSED = LOW;
const byte SDChipSelect = 10;
const byte numPushButtonSkipsToRecord = 5;

byte RTCDoW;
byte RTCHour;
byte RTCMinute;
byte RTCDateInMonth;
byte RTCMonth;
byte RTCYear;
bool myH12 = false;
bool myAmPm = false;
bool century;

bool alarmTestHasRun = false;
int activeMessageNum;

MyTable myMsgs("ma-msgs.csv");
int numMessages = myMsgs.countRows() - 1;
// Column nums and names
const byte MessageNumber = 0;
const byte TextLine1 = 1;
const byte TextLine2 = 2;
const byte TextLine3 = 3;
const byte HighPriority = 4;
// Not used const byte DayofWeek = 5;
const byte DoW_D = 6;
// Not used const byte DayFornTh = 7;
const byte nThOrLastDoW_D = 8;
// Not used const byte Which for nTh = 9;
const byte nThLNum_D = 10;
// Not used const byte ShowItChance = 11;
const byte ChanceIn7_D = 12;
// Not used const byte Hour = 13;
const byte Hour_D = 14;
// Not used const byte Minute = 15;
const byte Minute_D = 16;
// Not used const byte FollowOnMessageNum = 17;
const byte FollowOnMId_D = 18;


void ensureCSVProperlyTerminated() {
  // A csv file freshly downloaded from Google Sheets won't be properly
  // terminated with an ending line feed. This function corrects that.
  File myTable;
  char incoming;
  if (!SD.begin(SDChipSelect)) {
    displayFatalErrorAndStop(3, 0, 0, 0);
    while (1);
  }
//  Serial.println("initialization of file is done.");
  myTable = SD.open("ma-msgs.csv", FILE_WRITE);
  delay(100);
  myTable.seek(myTable.size() - 1);
  delay(100);
  incoming = myTable.read();
  if (incoming != '\n') {
    myTable.println("");
    Serial.println(F("added line ending to last line of file"));
    EEPROM.write(1, 0); // reset record of number of pushbutton skips saved
  }
  myTable.close();
}

int getLastSundayInMonth(int myMonthNum){
// X1 If there's no DST where you are, remove this function.
DateTime myDT;
myDT = RTClib::now();
int DateInMonthSundayFound;
  for (int i = 25; i <= 31; i++) {
      // Set a date var to March the "i"th of today's year
      // All global datetime vars were set in setup so can be used here
      if (dow(myDT.year(), myMonthNum, i) == 7) {
      // if it's a Sunday
      DateInMonthSundayFound = i;
      break;
      }
  }
  return DateInMonthSundayFound;
}

void dealWithPossibleBSTOrGMTMismatch () {
// X2 If there's no DST where you are, remove this function.
//
// It could be that after a long power off, the device wakes up with today's date 
// being in BST while the EEPROM flag shows that the RTC's time is actualy GMT
// or vice versa

// This function deals with that and makes sure that the RTC is adjusted if needed
// and the flag in EEPROM is adjusted accordingly

// It gets the dates of the BST->GMT and GMT->BST switchovers for this year
// If the RTC is flagged as using times in BST and it's actually GMT or vice
// versa that has to be corrected, flags updated and RTC updated by 
// adding/subtracting an hour as appropriate.

// This has to be called on initial Power-On before the current date/time is 
// shown on the LCD.
  
DateTime myDT;
DateTime mySwitchoverToBST;
DateTime mySwitchoverToGMT;
myDT = RTClib::now();

  // First, get the dates and times of this year's switchovers
  // GMT->BST switchover is the Sunday on or after 25th March at 1am
  mySwitchoverToBST = DateTime(myDT.year(), 3, getLastSundayInMonth(3), 1, 0, 0);
//  Serial.println(mySwitchoverToBST.unixtime());
  // BST->GMT switchover is the Sunday on or after 25th Oct at 2am
  mySwitchoverToGMT = DateTime(myDT.year(), 10, getLastSundayInMonth(10), 2, 0, 0);

  // With the switchover dates for this year now known, we can check if there's a mismatch between the 
  // current setting of the RTC and its BST/GMT flag and whether its BST or GMT right now.
  // First, check if it's within an hour of the switchover from BST to GMT because if so it will have been
  // set back an hour by the "fall back" rule. If so, the flag will be GMT but the time (as it's been set 
  // back) now indicates it's BST, which it's not.
  if ( (myDT.unixtime() >= mySwitchoverToGMT.unixtime()-3600) && (myDT.unixtime() <= mySwitchoverToGMT.unixtime()) ) {
    return;
  }
  if (myDT.unixtime() >= mySwitchoverToBST.unixtime() && myDT.unixtime() <= mySwitchoverToGMT.unixtime()) {
    // it's BST
    if (EEPROM.read(0) == 1) { // If RTC is flagged as BST
      // all good, nothing to do
    } else { // RTC is flagged as GMT
      EEPROM.write(0, 1); // ..so correct it - 1 means BST
      myDT = myDT.unixtime() + 3600; // add an hour
      myRTC.setEpoch(myDT.unixtime(), true);
      // although not relevant to this sketch, true is the right parameter 
      // to supply when it's local time
    }
  } else {
      // it's GMT
      if (EEPROM.read(0) == 1) { // RTC is flagged as BST
        EEPROM.write(0, 0); // ..so correct it - 0 means GMT
        myDT = myDT.unixtime() - 3600; // subtract an hour
        myRTC.setEpoch(myDT.unixtime(), false);
        // although not relevant to this sketch, false is the right parameter 
        // to supply when it's GMT
      } else {
        // all good, nothing to do
      }
  }
}

void dealWithPossibleSwitchover() {
// X3 If there's no DST where you are, remove this function.
//
// At some point while looping, a switchover to/from BST/GMT wil take place. 
// This function deals with that and makes sure that the RTC is adjusted
// and the flag in EEPROM is set accordingly

// It gets the dates and times of the BST->GMT and GMT->BST switchovers for this year
// and checks them against the current setting of the BSTGMT flag and the RTC. If the 
// switchover time has been reached, the BSTGMT flag has to be changed and the RTC 
// updated by adding/subtracting an hour as appropriate.
  
DateTime myDT;
DateTime mySwitchoverToBST;
DateTime mySwitchoverToGMT;
myDT = RTClib::now();

  // First, get the dates and times of this year's switchovers
  // GMT->BST switchover is the Sunday on or after 25th March at 1am
  mySwitchoverToBST = DateTime(myDT.year(), 3, getLastSundayInMonth(3), 1, 0, 0);
  // BST->GMT switchover is the Sunday on or after 25th Oct at 2am
  mySwitchoverToGMT = DateTime(myDT.year(), 10, getLastSundayInMonth(10), 2, 0, 0);

  // With the switchover dates for this year now known, we can check if there's been a 
  // switchover. We can tell if there has been because there will be a mismatch between the 
  // current setting of the RTC and its BST/GMT flag and whether its BST or GMT right now.
  if (myDT.unixtime() >= mySwitchoverToBST.unixtime() && myDT.unixtime() <= mySwitchoverToGMT.unixtime()) {
    // it's BST
    if (EEPROM.read(0) == 1) { // If RTC is flagged as BST
      // all good, nothing to do
    } else { // RTC is flagged as GMT
      Serial.println(F("changeover to BST detected"));
      EEPROM.write(0, 1); // 1 means BST
      myDT = myDT.unixtime() + 3600; // add an hour
      myRTC.setEpoch(myDT.unixtime(), true);
      // although not relevant to this sketch, true is the right parameter 
      // to supply when it's using local time
      setNextDueAlarm();
      // this has the effect of re-scheduling the existing alarm so it uses adjusted time
    }
  } else {
    // it's GMT
    if (EEPROM.read(0) == 1) { // RTC is flagged as BST
      Serial.println(F("changeover to GMT detected"));
      EEPROM.write(0, 0); // 0 means GMT
      myDT = myDT.unixtime() - 3600; // subtract an hour
      myRTC.setEpoch(myDT.unixtime(), false);
      // although not relevant to this sketch, false is the right parameter 
      // to supply when it's using local time
      setNextDueAlarm();
      // this has the effect of re-scheduling the existing alarm so it uses adjusted time
    } else {
      // all good, nothing to do
    }
  }
}

int dow(int y, int m, int d) {
//gives day of week for a given date Sunday=0 (now 7 with myFix), Saturday=6
  int myFix;
  static int t[] = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 };
  y -= m < 3;
  myFix = (y + y / 4 - y / 100 + y / 400 + t[m - 1] + d) % 7;
  if (myFix == 0) myFix = 7;
  return myFix;
}

void setup() {
// **************************************************************************
// When deploying a Daily Memory Aid, first:
// o Set the time using the Example sketch under 
//   File->Examples->DS3231->DS3231_Set (the DS3231 library needs to have 
//   been loaded first)
// o Set the BSTGMT flag & save-num in EEPROM manually by uncommenting the following
//   three lines, running the sketch and then commenting them out again:
//EEPROM.write(0, 1); // Second parameter: 1 if it's now BST or 0 for GMT. Ignore if no DST where you are.
//EEPROM.write(1, 0); // Pushbutton save number held in loc 1. Initially it'll be 0
//while(1);

long dispStart;
const int fiveSeconds = 5000;
DateTime myIDT; // used for initial date and time display
  myIDT = RTClib::now();

  // Begin I2C communication
  Wire.begin();
  // Begin Serial communication
  Serial.begin(9600);
  while (!Serial);
//  Serial.println(F("Serial started"));
  Serial.println("Serial started");
  // Begin LCD
  lcd.init();
  pinMode(buttonPin, INPUT_PULLUP);

  // myMsgs.printSDstatus();  //[optional] print the initialization status of SD card
/* Uncomment to check msg file contetnt
int numRows, numCols;
  numRows = myMsgs.countRows();
  numCols = myMsgs.countCols();
  for (int i = 0; i < numRows; ++i) {
    for (int j = 0; j < numCols; ++j) {
      Serial.print(j); Serial.print(F(" ")); Serial.println(myMsgs.readCell(i, j));
    }
  }
  while(1);
*/
  ensureCSVProperlyTerminated();

  // if a GMT/BST switchover happened while switched off, deal with it
  // X4 remove the following line if there's no DST where you are
  dealWithPossibleBSTOrGMTMismatch();

  // Establish time now for initial confirmation that all is working and
  // for first use of setNextDueAlarm()
  RTCDoW = myRTC.getDoW();
  RTCHour = myRTC.getHour(myH12, myAmPm);
  RTCMinute = myRTC.getMinute();
  RTCDateInMonth = myRTC.getDate();    // only used in displaying today's day name and date in month
  RTCMonth = myRTC.getMonth(century);  // only for first time run since switched on
  RTCYear = myRTC.getYear();           // only for first time run since switched on


  // Give visual confirmation all is well by displaying key info
  // on the LCD display
  displayDateAndTimeOnLCD(myIDT, 0);
  while (digitalRead(buttonPin) == PRESSED);
  lcdPrint(0, 0, F("Will now test alarm."));
//  lcdPrint(0, 1, F("It'll say ok in 10s"));
//  lcdPrint(0, 2, F("If not, reboot! Hit"));
//  lcdPrint(0, 3, F("button to run test."));

  dispStart = millis();
  while ((digitalRead(buttonPin) != PRESSED) && ((millis() - dispStart) < fiveSeconds));  // auto continue after 5s
  lcd.clear();
//  lcdPrint(2, 1, F("Setting alarm and"));
  lcdPrint(2, 2, F("going to sleep..."));
  delay(2000);
  lcd.clear();
  lcd.noBacklight();
  // attach clock interrupt
  pinMode(CLINT, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(CLINT), isr_TickTock, FALLING);
//  delay(500);
  setNextDueAlarm();
}

void lcdPrint(int r, int c, const __FlashStringHelper *s) {
  lcd.backlight();
  lcd.setCursor(r, c);
  lcd.print(s);
}

void displayDateAndTimeOnLCD(DateTime myDTforDisplay, byte dispLastButtonPushSkip) {
int dateStartPos, timeStartPos, singleDigitPos;
int lastBPRow = 0;
long dispStart;
  if (dispLastButtonPushSkip == 1){
    lastBPRow = 1;
  }
//  Serial.println("Day; "); Serial.println(myDTforDisplay.dayOfTheWeek());
  lcd.backlight();
  //Start with the name of the day
  if (myDTforDisplay.dayOfTheWeek() == 1) {
    lcdPrint(0, lastBPRow, F("MONDAY "));
    dateStartPos = 7;
  }
  if (myDTforDisplay.dayOfTheWeek() == 2) {
    lcdPrint(0, lastBPRow, F("TUESDAY"));
    dateStartPos = 8;
  }
  if (myDTforDisplay.dayOfTheWeek() == 3) {
    lcdPrint(0, lastBPRow, F("WEDNESDAY"));
    dateStartPos = 10;
  }
  if (myDTforDisplay.dayOfTheWeek() == 4) {
    lcdPrint(0, lastBPRow, F("THURSDAY"));
    dateStartPos = 9;
  }
  if (myDTforDisplay.dayOfTheWeek() == 5) {
    lcdPrint(0, lastBPRow, F("FRIDAY"));
    dateStartPos = 7;
  }
  if (myDTforDisplay.dayOfTheWeek() == 6) {
    lcdPrint(0, lastBPRow, F("SATURDAY"));
    dateStartPos = 9;
  }
  if (myDTforDisplay.dayOfTheWeek() == 0) {
    lcdPrint(0, lastBPRow, F("SUNDAY"));
    dateStartPos = 7;
  }
//delay(3000);
  // Display date as obtained from myDTforDisplay
  singleDigitPos = dateStartPos;
  if (myDTforDisplay.day() < 10) {
    lcdPrint(dateStartPos, lastBPRow, F("0"));
    singleDigitPos++;
  }
  lcd.setCursor(singleDigitPos, lastBPRow);
  lcd.print(myDTforDisplay.day());
  lcdPrint(dateStartPos + 2, lastBPRow, F("/"));

  singleDigitPos = dateStartPos;
  if (myDTforDisplay.month() < 10) {
    lcdPrint(dateStartPos + 3, lastBPRow, F("0"));
    singleDigitPos++;
  }
  lcd.setCursor(singleDigitPos + 3, lastBPRow);
  lcd.print(myDTforDisplay.month());
  lcdPrint(dateStartPos + 5, lastBPRow, F("/"));
  lcd.print(myDTforDisplay.year());

  // Display time
  timeStartPos = 12;
  singleDigitPos = timeStartPos;
  if (dispLastButtonPushSkip > 0){
    lastBPRow++;
    lcdPrint(0, 0, F("Button skipped:"));
    lcdPrint(0, 2, F("time was:"));    
    if (dispLastButtonPushSkip == 2) {
      lcd.clear();
      lcdPrint(0, 0, F("Active MNum:  "));
      lcd.setCursor(13, 0);
      lcd.print(activeMessageNum);
      lcdPrint(0, 1, F("time set at")); 
    }
  } else {
    lastBPRow++;
    lcdPrint(0, 1, F("Time is now "));    
  }
  if (myDTforDisplay.hour() < 10) {
    lcdPrint(timeStartPos, lastBPRow, F("0"));
    singleDigitPos++;
  }
  lcd.setCursor(singleDigitPos, lastBPRow);
  lcd.print(myDTforDisplay.hour());
  lcdPrint(timeStartPos + 2, lastBPRow, F(":"));

  singleDigitPos = timeStartPos;
  if (myDTforDisplay.minute() < 10) {
    lcdPrint(timeStartPos + 3, lastBPRow, F("0"));
    singleDigitPos++;
  }
  lcd.setCursor(singleDigitPos + 3, lastBPRow);
  lcd.print(myDTforDisplay.minute());

  // All done...
  lcdPrint(0, 3, F("Hit button to cont.."));
  while (digitalRead(buttonPin) == PRESSED);
//  delay(400);
  dispStart = millis();
  while ((digitalRead(buttonPin) != PRESSED) && ((millis() - dispStart) < 7000));  // auto continue after 7s
//  delay(200);
  lcd.clear();
  lcd.noBacklight();
}

void displayFatalErrorAndStop(int errorCode, int varVal1, int varVal2, int varVal3) {

  lcd.clear();
  if (errorCode == 1) {

    lcdPrint(0, 0, F("Err 1 getMNumForMId"));
/*
    lcdPrint(0, 1, F("Message with given"));
    lcdPrint(0, 2, F("MNum not found. It"));
    lcdPrint(0, 3, F("was "));
    lcd.setCursor(4, 3);
    lcd.print(varVal1);
    if (varVal1 > 9) {
      lcdPrint(7, 3, F(". Reboot!"));
    } else {
      lcdPrint(6, 3, F(". Reboot!"));
    }
*/
  }
  if (errorCode == 2) {
    lcdPrint(0, 0, F("Err 2 matchMessage"));
/*
    lcdPrint(0, 1, F("No match found hour"));
    lcdPrint(0, 2, F("was "));
    lcd.setCursor(4, 2);
    lcd.print(varVal1);
    if (varVal2 > 9) {
      lcdPrint(6, 2, F(", min was "));
      lcd.setCursor(17, 2);
      lcd.print(varVal2);
    } else {
      lcdPrint(5, 2, F(", min was "));
      lcd.setCursor(15, 2);
      lcd.print(varVal2);
    }
    lcdPrint(0, 3, F("DoW was "));
    lcd.setCursor(8, 3);
    lcd.print(varVal3);
    if (varVal3 > 9) {
      lcdPrint(10, 3, F(" Reboot!"));
    } else {
      lcdPrint(9, 3, F(". Reboot!"));
    }
*/
  }
  if (errorCode == 3) {
    lcdPrint(0, 0, F("Err 3 Init SD Card"));
  }

  lcdPrint(0, 1, F("Switch off/on again!"));
  while (1);
}
int matchMessage(byte hour, byte minute, byte DoW) {

  // The message file is searched looking for a match where the passed parameters
  // match a row entry's values.
  // Matches can't be made with Follow-on messages
  // Valid values for DoW parameter are 1 to 7
  // The passed in DoW is irrelevant if the matched message's myDoWNum is 32, ie every day
int alarmTotalMinutes = hour * 60 + minute;
int messageTotalMinutes;
bool matchedSoFar;
int matchedMessageNum = -1;

  for (int i = 1; i <= numMessages; i++) {
    matchedSoFar = false;
    // easiest checks for continuing with matching is if both the hour and the minute match
    // if they don't match, no other testing is needed on this message
    messageTotalMinutes = myMsgs.readCell(i,Hour_D).toInt() * 60 + myMsgs.readCell(i,Minute_D).toInt();
//    if ((myMsgs.readCell(i,Hour_D).toInt() == hour) && (myMsgs.readCell(i,Minute_D).toInt() == minute)) {
    if ( (messageTotalMinutes == (alarmTotalMinutes)) || (messageTotalMinutes == (alarmTotalMinutes - 1)) ) {
      if (myMsgs.readCell(i,DoW_D).toInt() == 32) {
        // When hours and mins are same, Every Day always matches a message
        // no need to continue, return the match
        return i;
      }
      if (myMsgs.readCell(i,DoW_D).toInt() == 33) {
        // if it's an nTh day of month message, check if its date is the same as
        // todays's date (ie the alarm's date)
          if (getNthDoW(myMsgs.readCell(i,nThLNum_D).toInt(), DoW) == RTCDateInMonth) {
          // It is the same, and, as hours and minutes are the same from the earlier test, we have a match
          return i;
        } else {
          // it's an nth in month message but dates don't match
          continue; // get on with testing next message
        }
      }
      // if processing gets to here, the hours and mins match, it's not an everyday message and 
      // it's not an nTh in month message, so all it can possibly be now is a specific day message
      if (myMsgs.readCell(i,DoW_D).toInt() == DoW) {
        // got a match
        return i;
      }
    }
  }
  // if processing gets here, no match has been found with any message
  displayFatalErrorAndStop(2, hour, minute, DoW);
}

int getMNumForMId(int myMId) {
  int matchedMessageNum = -1;
  int myInt;
  for (int i = 1; i <= numMessages; i++) {
    if (myMsgs.readCell(i,MessageNumber).toInt() == myMId) {
      matchedMessageNum = i;
      break;
    }
  }
  if (matchedMessageNum == -1) {
    displayFatalErrorAndStop(1, myMId, 251, 251);
  } else {
    return matchedMessageNum;
  }
}
void displayMessage(int myMessageNum, int &followonMId) {
  lcd.noBacklight();
  lcd.clear();
  lcd.backlight();
  if (RTCDoW == 1) { lcdPrint(3, 0, F("IT'S MONDAY ")); }
  if (RTCDoW == 2) { lcdPrint(2, 0, F("IT'S TUESDAY")); };
  if (RTCDoW == 3) { lcdPrint(0, 0, F("IT'S WEDNESDAY")); };
  if (RTCDoW == 4) { lcdPrint(1, 0, F("IT'S THURSDAY")); };
  if (RTCDoW == 5) { lcdPrint(3, 0, F("IT'S FRIDAY")); };
  if (RTCDoW == 6) { lcdPrint(1, 0, F("IT'S SATURDAY")); };
  if (RTCDoW == 7) { lcdPrint(3, 0, F("IT'S SUNDAY")); };
  lcd.setCursor(15, 0);
  lcd.print(RTCDateInMonth);
  if (RTCDateInMonth < 10) {
    lcd.setCursor(16, 0);
  } else {
    lcd.setCursor(17, 0);
  }
  switch (RTCDateInMonth) {
    case 1:
    case 21:
    case 31:
      lcd.print("st");
      break;
    case 2:
      lcd.print("nd");
      break;
    case 22:
      lcd.print("nd");
      break;
    case 3:
      lcd.print("rd");
      break;
    case 23:
      lcd.print("rd");
      break;
    default:
      lcd.print("th");
  }

  lcd.setCursor(0, 1);
  lcd.print(myMsgs.readCell(myMessageNum,TextLine1));
  lcd.setCursor(0, 2);
  lcd.print(myMsgs.readCell(myMessageNum,TextLine2));
  lcd.setCursor(0, 3);
  lcd.print(myMsgs.readCell(myMessageNum,TextLine3));
  followonMId = myMsgs.readCell(myMessageNum,FollowOnMId_D).toInt();
}
int getMNumOfNextDueAlarm() {
  // To set the next alarm, we need to find the non-followon message
  // that comes next in time after now. It'll never be a follow on message
  // as they're handled within wakeyWakey and are never the subject of an alarm
  int nextDueMessageNum = -1;
  int totalAlarmTimeInMinutes = -1;
  int lowestAlarmTimeInMinutesSoFar = -1;
  int timeNowMinutes = RTCHour * 60 + RTCMinute;
  const int minutesInDay = 24 * 60;
  const int minutesInWeek = minutesInDay * 7;
  int readnThLNum_D, readnThOrLastDoW_D;

  byte myRnd, readDoW_D;
  byte myDayInMonth = 0;
  bool suppressMessage;
  DateTime myDT;
  myDT = RTClib::now();

  randomSeed(analogRead(0));
  for (int i = 1; i <= numMessages; i++) {
    myRnd = random(1, 8);  // gives 1 through 7 as possibilities
    suppressMessage = (myRnd > myMsgs.readCell(i,ChanceIn7_D).toInt());
    // It's easier to think of it in terms of the suppressing a
    // message that could otherwise be set as the next due one.
    // If column 12, ChanceIn7_D, is 7 then this will evaluate
    // to true 0% of the time, ie no chance of suppression
    // if it's 1, suppressMessage will be true 85% of the time, ie
    // 6/7 and very likely to be suppressed
    // and if it's 6 it'll be true 14% of the time, ie hardly likely
    // to be suppressed at all


    // In order to be considered for being the next due message, a message must
    // meet these criteria:
    // 1) It's not a followon message - they're handled by wakeyWakey, which is
    // triggered when an alarm goes off.
    // AND
    // 2) It's not suppressed

    // If it gets past those tests then the following must be true:
    // either:
    // 3) It's a daily message
    // OR
    // 4) It's a specific day message and today is that day
    // OR
    // 5) It's an nTh-day-of-the-month message

    if ((myMsgs.readCell(i,Hour_D).toInt() != 255) && (!suppressMessage)) {
      // it's not a followon message and it's not suppressed, so ok to continue
      
      if (myMsgs.readCell(i,DoW_D).toInt() == 32) {  // it's a daily message, calc total minutes of alarm time
        totalAlarmTimeInMinutes = myMsgs.readCell(i,Hour_D).toInt() * 60;
        totalAlarmTimeInMinutes += myMsgs.readCell(i,Minute_D).toInt();
        if (totalAlarmTimeInMinutes <= timeNowMinutes) {
          // message alarm time is same as or earlier than now
          // so it's not due again until tomorrow
          totalAlarmTimeInMinutes += minutesInDay;
        }
      } else { // must be an unsuppressed (this time), specific-day message because it's not a followon and its not a daily - could also be a nth / last DoW of month
        readDoW_D = myMsgs.readCell(i,DoW_D).toInt();
        if (readDoW_D == 33) {
          readnThLNum_D = myMsgs.readCell(i,nThLNum_D).toInt();
          readnThOrLastDoW_D = myMsgs.readCell(i,nThOrLastDoW_D).toInt();
          myDayInMonth = getNthDoW(readnThLNum_D, readnThOrLastDoW_D);
          // may come back as 255 if it's asked for, say, 5th Monday in month and there isn't one
        }
        if ((myMsgs.readCell(i,DoW_D).toInt() == RTCDoW) || (myDayInMonth == RTCDateInMonth)) {
          // it's for today or it's an nth / last: calc total minutes of alarm time
          totalAlarmTimeInMinutes = myMsgs.readCell(i,Hour_D).toInt() * 60;
          totalAlarmTimeInMinutes += myMsgs.readCell(i,Minute_D).toInt();
          if (totalAlarmTimeInMinutes <= timeNowMinutes) {  // message alarm time is same as or earlier than now
            // it's a specific-day message, same as or earlier than now so
            // it's not due again until next week
            totalAlarmTimeInMinutes += minutesInWeek;
          }
        }
      }
      // totalAlarmTimeInMinutes calculated for this message
      if ((totalAlarmTimeInMinutes < lowestAlarmTimeInMinutesSoFar)
          || (lowestAlarmTimeInMinutesSoFar == -1)) {
        // is it lower than lowest so far? It must be if it's the first time
        lowestAlarmTimeInMinutesSoFar = totalAlarmTimeInMinutes;  // yes, note it
        nextDueMessageNum = i; // ..and the message array index
      }
    }
  }
  Serial.print(F("in getMNumOfNextDueAlarm, is: "));
  Serial.println(nextDueMessageNum);
  return nextDueMessageNum;
}

byte getNthDoW(byte nTh, byte DoW) {
  // nTh 1,2,3,4,5,255 where 255 indicates last of the month
  // DoW passed as 7 = Sun, 1 = Mon
  // Returns the date in the month when the nTh/last DoW occurs
  const long secondsInDay = 86400;
  DateTime testDayDT;

  testDayDT = RTClib::now();
  byte thisMonth = testDayDT.month();
  byte advancingMonth = thisMonth; // need this to allow month 13 to be reached, if needed
  byte numFound = 0;
  if (DoW == 7) DoW = 0; // because .dayOfTheWeek() returns 0 to 6 for Sunday to Saturday
  // whether it's nTh or last in month, always start at 1st of month
  testDayDT = DateTime(testDayDT.year(), testDayDT.month(), 1, testDayDT.hour(), testDayDT.minute(), testDayDT.second());
  if (nTh == 255) { // it's a byte so use 255 as indicator that it's the last in the month needed
    // testDayDT is set to first of current month, so get to next month so we can go back a day to last day of this month
    while (testDayDT.month() - 1 != thisMonth) {
      testDayDT = testDayDT.unixtime() + secondsInDay;
    }
    // now get the DoW requested by counting back a max of 8 days
    for (int i = 1; i <= 8; i++){
      // 8 because on the first time it's going back a day from first 
      // of next month to last day of this month
      testDayDT = testDayDT.unixtime() - secondsInDay;
      if (testDayDT.dayOfTheWeek() == DoW ) {
          break;
      }
    }
  } else {
    for (int i = 1; i <= 7 * nTh; i++){
      // set TesDayDT to i'th of month. If looking for nTh one in the month, maximuum days to look through is 7 * nTh
      // Serial.print("day in month being checked: "); Serial.println(testDayDT.day());
      if (testDayDT.dayOfTheWeek() == DoW ) {
        numFound++;
        if (numFound == nTh) {
          break;
        }
      }
      testDayDT = testDayDT.unixtime() + secondsInDay;
      // if this pushes the testDateDT over to the next month, then flag it and break
      if (testDayDT.month() != thisMonth) {
        return 255;
      }
    }
  }
//  Serial.print("DoW found, day in month is: "); Serial.println(testDayDT.day());
  return testDayDT.day();
}

void setNextDueAlarm() {
// Search through messages looking for 
  DateTime myDT, alarmDT, alarmDate;
  int mNumOfNextDueAlarm;
  bool myH12, myAmPm;

  // Assign parameter values for Alarm 1
  if (alarmTestHasRun) {
    mNumOfNextDueAlarm = getMNumOfNextDueAlarm();
    activeMessageNum = mNumOfNextDueAlarm;
//    Serial.print(F("mNumOfNextDueAlarm is: ")); Serial.println(mNumOfNextDueAlarm);
    alarmHour = myMsgs.readCell(mNumOfNextDueAlarm,Hour_D).toInt();
    alarmMinute = myMsgs.readCell(mNumOfNextDueAlarm,Minute_D).toInt();
    alarmSecond = 0;
    if (myMsgs.readCell(mNumOfNextDueAlarm,DoW_D).toInt() == 32) {  // it's a daily alarm
      alarmDay = 0;
      alarmBits = 0b00001000;  // Alarm 1 every day, time as per above
      alarmDayIsDay = true;
    } else {  // only other possibility is it's a specific-day of week message or a nTh / Last Day of Month
    if (myMsgs.readCell(mNumOfNextDueAlarm,DoW_D).toInt() == 33) {
      alarmDay = myMsgs.readCell(mNumOfNextDueAlarm,nThOrLastDoW_D).toInt();
    } else {
      alarmDay = myMsgs.readCell(mNumOfNextDueAlarm,DoW_D).toInt();
    }
      alarmBits = 0b00000000;  // Alarm 1 on specific day, time as per above
      alarmDayIsDay = true;    // day of the week as opposed to date in the month
    }
    alarmH12 = false;
    alarmPM = false;

    // If it's a daily alarm, then if the alarm's hour and minute are earlier or same as now
    // it means it's already happened today so it's not now due until tomorrow. If later 
    // than now, the daily alarm will be for later today
    alarmDT = RTClib::now(); // Start alarmDT at today as the initial starting point
    myDT = alarmDT; // Start myDT at today as the initial starting point
    if (alarmBits == 0b00001000) {  // it's a daily alarm
      // build a DateTime object for the alarm. Start with now and then change to the
      // alarm specifics but keep it as today
      alarmDT = DateTime(alarmDT.year(), alarmDT.month(), alarmDT.day(), alarmHour, alarmMinute, alarmDT.second());
      if (alarmDT.unixtime() <= myDT.unixtime()) {
        // if the alarm time were today, with those hours and minutes, the alarm time
        // would already have passed so the alarm must be for tomorrow
        alarmDT = alarmDT.unixtime() + 86400; // add a day's worth of seconds
      } else {
        // the alarm is for later today and so is already set
      }
    } else {  // it's a day-specific alarm - including nTh / last DoW in month
      // Set up the alarms initial starting point as now from above and then using 
      // the alarm hour and minute set above
      alarmDT = DateTime(alarmDT.year(), alarmDT.month(), alarmDT.day(), alarmHour, alarmMinute, alarmDT.second());
      // if today's DoW is same as alarm's DoW, the alarm's hour and minute values will
      // indicate if the alarm is earlier or later than right now. If it's earlier then
      // it must be after today. If it's later, then it means it's later today.
      if (myDT.dayOfTheWeek() == alarmDay) { // both DoWs are the same
        // Now get the actual date of the alarm, working it out from specific day set 
        // for the alarm and today's date
        if ( (alarmDT.hour()*60 + alarmDT.minute()) > (myDT.hour() * 60 + myDT.minute()) ) {
          // as the two DoWs are the same, if the alarm's hour and minute are later 
          // than now, the alarm is later today. So its date is today and that is already set.
        } else { // it must be 7 days from now (and earlier than the time now)
        // add 7 days and end the else
          alarmDT = alarmDT.unixtime() + (604800);
        } // else ended
        // }
      } else { // both DoWs are NOT the same
        // the two DoWs are different
        // the alarm is on a later day - work out its date
        // If the alarm DoW is <= today's DoW, then add 7 
        // to the alarm's DoW before subtracting today's DoW from it
        if (alarmDay <= myDT.dayOfTheWeek()) {
          alarmDate = myDT.unixtime() + (((alarmDay + 7) - myDT.dayOfTheWeek()) * 86400);
        } else {
          // just subtract the two DoWs
          alarmDate = myDT.unixtime() + ((alarmDay - myDT.dayOfTheWeek()) * 86400);
        }
        // The alarmDate is now set and its day of the month is ready to be used in setting the alarmDT
        alarmDT = DateTime(alarmDT.year(), alarmDT.month(), alarmDate.day(), alarmDT.hour(), alarmDT.minute(), alarmDT.second());
      } // both DoWs are NOT the same
    } // end of else for day-specific

    // Set alarm 1 for next due message
    myRTC.turnOffAlarm(1);
    myRTC.setA1Time(
      alarmDay, alarmHour, alarmMinute, alarmSecond,
      alarmBits, alarmDayIsDay, alarmH12, alarmPM);
    // enable Alarm 1 interrupts
    myRTC.turnOnAlarm(1);
    // clear Alarm 1 flag
    myRTC.checkIfAlarm(1);
  } else {
    alarmDay = myRTC.getDoW();
    alarmHour = myRTC.getHour(myH12, myAmPm);
    alarmMinute = myRTC.getMinute() + 0;
    alarmSecond = myRTC.getSecond() + 10;
    if (alarmSecond >= 60) {
      alarmSecond-= 60;
      alarmMinute++;
      if (alarmMinute == 60) {
        alarmMinute = 0;
        alarmHour++;
        if (alarmHour >= 24) {
          alarmHour = 0;
          alarmDay++;
          if (alarmDay == 8) {
            alarmDay = 1;
          }
        }
      }
    }
    alarmBits = 0b00000000; // Alarm 1 on specific day, time as per above
    alarmDayIsDay = true; // day of the week as opposed to date in the month
    alarmH12 = false;
    alarmPM = false;    

    // Set alarm 1 for next due message
    myRTC.turnOffAlarm(1);
    myRTC.setA1Time(
    alarmDay, alarmHour, alarmMinute, alarmSecond,
    alarmBits, alarmDayIsDay, alarmH12, alarmPM);
    // enable Alarm 1 interrupts
    myRTC.turnOnAlarm(1);
    // clear Alarm 1 flag
    myRTC.checkIfAlarm(1);
  }
  // When using interrupt with only one of the DS3231 alarms, as in this case,
  // it is advisable to prevent the other alarm entirely,
  // so it will not covertly block the outgoing interrupt signal.

  // Prevent Alarm 2 altogether by assigning a 
  // nonsensical alarm minute value that cannot match the clock time,
  // and an alarmBits value to activate "when minutes match".
  alarmMinute = 0xFF; // a value that will never match the time
  alarmBits = 0b01100000; // Alarm 2 when minutes match, i.e., never
  
  // Upload the parameters to prevent Alarm 2 entirely
  myRTC.setA2Time(
      alarmDay, alarmHour, alarmMinute,
      alarmBits, alarmDayIsDay, alarmH12, alarmPM);
  // disable Alarm 2 interrupt
  myRTC.turnOffAlarm(2);
  // clear Alarm 2 flag
  myRTC.checkIfAlarm(2);

  // NOTE: both of the alarm flags must be clear
  // to enable output of a FALLING interrupt
  
  // Now it's set, can power down
//  Serial.println("going to sleep...");
//  delay(500);
}

void wakeyWakey() {
int myMatchedMessageNum;
int myFollowonMessageId;
long dispStart;
long waitFlashCount;
bool lcdToggler;
const long waitTime = 5L * 60L * 1000L;  // 5mins
const int fiveSeconds = 5000;
const long waitFlashCountCeiling = 200;
const int flashOffPause = 1;
const int flashOnPause = 15;
const long additionalWaitTimeHighPriority = 20L * 60L * 1000L;  // 20 mins
const long additionalWaitTimeNormalPriroty = 9L * 60L * 1000L; // 9 mins
long additionalWaitTime = additionalWaitTimeNormalPriroty;
byte lastUsedPushButtonSkipNumber;
DateTime myDT;
//Serial.println(F("wakeyWakey"));
//delay(500);
  RTCDoW = myRTC.getDoW();
  RTCHour = myRTC.getHour(myH12, myAmPm);
  RTCMinute = myRTC.getMinute();
  RTCDateInMonth = myRTC.getDate();

  if (alarmTestHasRun) {
    myMatchedMessageNum = matchMessage(RTCHour, RTCMinute, RTCDoW);
    // If a message is displayed for too long, it times out (after a period of flashing)
    // and gets treated as though the button was pushed.
    // That means it gets switched off automatically and then the next alarm gets set.
    // Care is needed that it doesn't take too long to time out or the message that would
    // have been the next due one will get missed.
    dispStart = millis();
    displayMessage(myMatchedMessageNum, myFollowonMessageId);
    if (myMsgs.readCell(myMatchedMessageNum,HighPriority) == "Y") additionalWaitTime = additionalWaitTimeHighPriority;
    while ((digitalRead(buttonPin) != PRESSED) && ((millis() - dispStart) < waitTime))
    // unchanged by priority: while not pressed and wait time hasn't elapsed, wait with the
    // display fully on
      ;

    dispStart = millis();
    waitFlashCount = 0;
    lcdToggler = true;
    while ((digitalRead(buttonPin) != PRESSED) && ((millis() - dispStart) < (additionalWaitTime))) {
      // Start flashing the LCD display after twice as long as the initial on time. It'll keep
      // on flashing for twice as long as the normalPriority time - unless it's high priority,
      // when the flashing time will be increased again, this time by additionalWaitTimeHighPriority
      if ((waitFlashCount < waitFlashCountCeiling)) {
        waitFlashCount++;
        if (lcdToggler) {
          delay(flashOnPause);
        } else {
          delay(flashOffPause);
        }
      } else {
        waitFlashCount = 0;
        lcdToggler = !lcdToggler;
        if (lcdToggler) {
          lcd.backlight();
        } else {
          lcd.noBacklight();
        }
      }
    }
    if (digitalRead(buttonPin) != PRESSED) {
      // Get the EEPROM location to write this skipped buttonpush date/time to
      lastUsedPushButtonSkipNumber = 1 + (getlastUsedPushButtonSkipNumber() % numPushButtonSkipsToRecord);
      // max will be 1 less than numPushButtonSkipsToRecord, min 0
      EEPROM.put(lastUsedPushButtonSkipNumber * 6 - 4, RTClib::now()); // 6 bytes for that datatype
      EEPROM.write(1, lastUsedPushButtonSkipNumber);
    }
    while (myFollowonMessageId > 0) {
      lcd.clear();
      lcd.noBacklight();
      delay(850);
      dispStart = millis();
      additionalWaitTime = additionalWaitTimeNormalPriroty;
      myMatchedMessageNum = getMNumForMId(myFollowonMessageId);
      if (myMsgs.readCell(myMatchedMessageNum,HighPriority) == "Y") additionalWaitTime = additionalWaitTimeHighPriority;
      if ((myMsgs.readCell(myMatchedMessageNum,DoW_D).toInt() == RTCDoW) || (myMsgs.readCell(myMatchedMessageNum,DoW_D).toInt() == 32)) {
        // can only be true if it's a specific day that's today OR it's a daily
        displayMessage(myMatchedMessageNum, myFollowonMessageId);
        while ((digitalRead(buttonPin) != PRESSED) && ((millis() - dispStart) < waitTime))
        // unchanged by priority: while not pressed and wait time hasn't elapsed, wait with the
        // display fully on
          ;
        
        dispStart = millis();
        waitFlashCount = 0;
        lcdToggler = true;
        while ((digitalRead(buttonPin) != PRESSED) && ((millis() - dispStart) < (additionalWaitTime))) {
          // Flash the LCD display. It'll be twice as long as the normalPriority time
          // unless it's high priority and then it'll be additionalWaitTimeHighPriority times as long
          waitFlashCount++;
          if ((waitFlashCount < waitFlashCountCeiling)) {
            if (lcdToggler) {
              delay(flashOnPause);
            } else {
              delay(flashOffPause);
            }
          } else {
            waitFlashCount = 0;
            lcdToggler = !lcdToggler;
            if (lcdToggler) {
              lcd.backlight();
            } else {
              lcd.noBacklight();
            }
          }
        }
        if (digitalRead(buttonPin) != PRESSED) {
          // Get the EEPROM location to write this skipped buttonpush date/time to
          lastUsedPushButtonSkipNumber = 1 + (getlastUsedPushButtonSkipNumber() % numPushButtonSkipsToRecord);
          // max will be 1 less than numPushButtonSkipsToRecord, min 0
          EEPROM.put(lastUsedPushButtonSkipNumber * 6 - 4, RTClib::now()); // 6 bytes for this datatype
          EEPROM.write(1, lastUsedPushButtonSkipNumber);
        }
      } else {
        // follow on message found wasn't a specific-day one matching today or a daily one
        // so follow on to the next one, if there is one.
        // If not, myFollowonMessageId will be zero so the while loop will finish
        myFollowonMessageId = myMsgs.readCell(myMatchedMessageNum,FollowOnMId_D).toInt();
      }
    }
  } else {
    alarmTestHasRun = true;
    lcdPrint(0, 0, F("Alarm test has run"));
//    lcdPrint(0, 1, F("ok! Hit button for"));
//    lcdPrint(0, 2, F("normal operation."));
//    lcdPrint(0, 3, F("Display will blank."));
    dispStart = millis();
    while ((digitalRead(buttonPin) != PRESSED) && ((millis() - dispStart) < fiveSeconds))
      // auto continue after 5s
      ;  
  }
  lcd.clear();
  lcd.noBacklight();
  setNextDueAlarm();
}
byte getlastUsedPushButtonSkipNumber() {
byte lastUsedPushButtonSkipNumber;
  // Get the last used push button save number ready for use later if a button push is skipped
  // instead of acknowledging / dismissing a message 
  lastUsedPushButtonSkipNumber = EEPROM.read(1); // get the number of the last push button skip
  if (lastUsedPushButtonSkipNumber == 0) {
    // if it's 0, it's just been set when the time was set in Setup
    // If so or if numPushButtonSkipsToRecord was the last used one, then start again at 1
    lastUsedPushButtonSkipNumber = 1;
  }
  return lastUsedPushButtonSkipNumber;
}


void loop() {
DateTime myIDT;
long dispStart;
const int fiveSeconds = 5000;
const long waitTime = 60L * 1000L;  // 1 minute
int dispButtonPressOrActiveMessage = 1;
int myTimeInMinutes;
byte pushButtonSkipNumber = getlastUsedPushButtonSkipNumber();

  // Allow for display of last skipped button push details and the next
  // due alarm's message number and time
  if (digitalRead(buttonPin) == PRESSED) {
    dispStart = millis();
    while ( (digitalRead(buttonPin) == PRESSED) && (millis() - dispStart < fiveSeconds) )
      // auto continue after 5s
      ;
    if (millis() - dispStart >= fiveSeconds) {
      // buton pressed for long enough
      lcd.backlight();
      delay(300);
      for (int i = 0; i <= numPushButtonSkipsToRecord; i++) {
        EEPROM.get(pushButtonSkipNumber * 6 - 4, myIDT);
        if (i == numPushButtonSkipsToRecord) {
          dispButtonPressOrActiveMessage = 2;
          // hackathon coming - the func to display stuff on the LCD that's coming up 
          // only needs the hour and minute when it's to display the active message
          myIDT = DateTime(myIDT.year(), myIDT.month(), myIDT.day(), myMsgs.readCell(activeMessageNum, Hour_D).toInt(), myMsgs.readCell(activeMessageNum, Minute_D).toInt(), myIDT.second());
        }
        if ( (myIDT.month() == RTCMonth) || (dispButtonPressOrActiveMessage == 2) ) {
          // only show the skipped info if it was skipped this month
          // giving the effect of resetting at the beginning of the month
          // However, do show the stuff if it's for the active message
          displayDateAndTimeOnLCD(myIDT, dispButtonPressOrActiveMessage);
          lcd.noBacklight();
          delay(1000);
          pushButtonSkipNumber = 1 + ((getlastUsedPushButtonSkipNumber() + i ) % numPushButtonSkipsToRecord);
        }
        while (digitalRead(buttonPin) == PRESSED);
      }
    }
  }
  // X5 If there's no DST where you are,
  // remove the following line and the two if statements that follow down to X6.
  myTimeInMinutes = myRTC.getHour(myH12, myAmPm) * 60 + myRTC.getMinute();
  if ( (myRTC.getDoW() == 7) && (EEPROM.read(0) == 0) && (myTimeInMinutes == 60) ) {
    Serial.println("moving from GMT to BST");
    //    only do this if it's a Sunday at 1am (and presently marked as GMT) as that's 
    //    the only day/time when a changeover from GMT to BST can happen in the UK.
    dealWithPossibleSwitchover();
    delay(waitTime); // make sure myTimeInMinutes can't be 60 again next time through the loop()
    // The reasoning is:
    //    So it's presently GMT, and it's a few seconds before 1am when it'll change to
    //    BST (if it's the right date). That means the BSTGMT EEPROM flag will presently 
    //    be set to 0 (ie GMT).
    //    Then this loop executes again and it happens to be a few seconds after  
    //    1am. If it is the changeover date to BST, dealWithPossibleSwitchover() 
    //    will spring the time forward by an hour.
  }
  if ( (myRTC.getDoW() == 7) && (EEPROM.read(0) == 1) && (myTimeInMinutes == 120) ) {
    //    only do this if it's a Sunday at 2am (and presently marked as BST) as that's 
    //    the only day/time when a changeover from BST to GMT can happen in the UK.
    dealWithPossibleSwitchover();
    delay(waitTime); // make sure myTimeInMinutes can't be 120 again next time through the loop()
    // The reasoning is:
    //    So it's presently BST, and it's a few seconds before 2am when it'll change to
    //    GMT (if it's the right date). That means the BSTGMT EEPROM flag will presently 
    //    be set to 1 (ie BST).
    //    Then this loop executes again and it happens to be a few seconds after exactly 
    //    2am. If it is the changeover date to GMT, dealWithPossibleSwitchover() 
    //    will fall the time back by an hour. This would seem to be saying it's BST again
    //    but the logic in dealWithPossibleSwitchover() takes that into account
  }
  // X6 Remove the above (up to X5) if there's no DST where you are

  // if alarm went off, do alarm stuff
  if (tick) {
    tick = 0;
    // Clear Alarm 1 flag
    myRTC.checkIfAlarm(1);
    wakeyWakey();
  }
}

void isr_TickTock() {
  // interrupt signals to loop
  tick = 1;
  return;
}

If you do a compile with ALL warnings you will see a lot of things to clean up.

I see a lot of things for sure but it’s all going over my head for now. More learning required!

I only looked at the first few, most are extra chars like a ; after an include. I am pretty tired now but I will try to check it out tomorrow.

Ah, I saw the verbose but overlooked the dropdown with All in it. Got it now and can do some cleanup as a result. Thanks for pointing it out :smiley:

Done. Ive updated the code in my opening post.

@hightonridley
Isn’t your RTC the one with the controversial charging circuit that can overcharge the battery and cause it leak?

you can trim down your switch case and use the F-Makro for two characters:

switch (RTCDateInMonth) {
    case 1:
	case 21:
	case 31:
      lcd.print(F("st"));
      break;
    case 2:
    case 22:
      lcd.print(F("nd"));
      break;
    case 3:
    case 23:
      lcd.print(F("rd"));
      break;
    default:
      lcd.print(F("th"));
  }

usually the F-Makro doesn't make so much sense with a single character String literal.

lcdPrint(timeStartPos + 2, lastBPRow, F(":"));

if I were you I would prepare another lcdPrint function with a different signature and would write a simple:

lcdPrint(timeStartPos + 2, lastBPRow, ':');

Great suggestions, @noiasca - thanks :)

Edted to add: right now I’m happy to use dynamic memory for those 2 chars. The reason being is that program memory is so tight, I don’t want to consume any more.

I was getting some weird behaviour with it - adding an if statement caused the consumption to go DOWN and the sketch then wouldn’t work. So until I’ve got plenty to play with, I’m not going to do anything to consume more.

I think it’s the same one. I’ve read some old posts, can’t remember exactly where, that reported ocassional issues. I’ve been running a prototype since mid Jan with no problems but it’s something I’ll keep en eye on.

I think the greatest concern meant cutting the charging circuit when using an ordinary CR2032 non-rechargeable battery so that it wasn’t being force-fed any juice.

Overcharging a Li-ion battery can be just as bad.
I think the safe solution was to remove the resistor or diode and use a CR2032 battery.

I’ve researched further and asked over at Stack Exchange for Electronics. I like to think of answers there as authoritative. Here’s a link to my question and the reply that confirms what you suggest, @jim-p :

Use CR2032 after cutting / removing charging circuit

So thanks for raising the issue.

You just insulted me and everyone else here on the forum.
If you think our answers are totally useless why are you here?

I didn’t phrase that very well - so please accept my apologies . What I should have said was that getting a confirmation from there in addition to what you said was enough to convince me.

I’ve seen conflicting advice here about it so it made sense to me to get a second opinion from soemwhere else.

Replaced the code in the opening post with version 2, which fixes a couple of bugs.