Arduino Playground is read-only starting December 31st, 2018. For more info please look at this Forum Post

// Identifiers
  #define programName "ZsuTT "     // set to name of program, acronym of "Zeitschaltuhr(MQ)TT"  
  #define programVersion "v2.5"    // set to version of program                                            
  #define MQto "o/8rd001"          // set to name of MQTT output queue incl. unique device ID.  Keep it short!
//                ^^^^^^-----------------------------------------------------^^^^^^^^^^^^^^^^                   
// 
/************************************************************************/
/*  ↄ⃝ Copyleft 2017 under GNU GPL v.3 and the EUPL, whichever may apply */
/*                                             mýið - myith@tutanota.de	*/
/*                                                                      */
/*      Please use, share, modify, improve, redistribute, inspire       */
/************************************************************************/ 
/*

This program provides the functionality of a TIMER SWITCH.  The present 
version also provides status messaging via MQTT.      

The program is known to function well on the 
 - Arduino UNO Wifi (firmware ESP8266 v0.0.3), together with 
 - Seeedstudio's Relay Shield v3.0

ZsuTT provides functionality similar to a mechanical timer switch, but may 
be managed and monitored remotely over WiFi. To enter commands use the 
Arduino Wifi console or any program providing simple TCP access on port 23
such as netcat. Execution feedback is given via MQTT MQ topics whose names 
are parametrized and thus can be changed to suit. In order to use MQTT it
is necessary to have an MQTT broker running where the Arduino can reach 
it. On the Arduino MQTT needs to be activated via the Connectivity panel 
(same connection as the console). It also needs to be told where to find 
the broker and on which port to reach it. A broker that works well with 
this program is Mosquitto but there may be others working well also.

ZsuTT can be used without MQTT.  However in that case I recommend to
activate the verbose option, so that appropriate feedback may be provided
via the console connection instead. 

The timer is only as accurate as the hardware on which it is running. 
Since the Arduino UNO does not have a Real Time Clock (RTC), slight time
variations are bound to occur.  These can be compensated by indicating how
many seconds ± the timer clock shall speed up or slow down every 24 hours.  
I found that aberrations tend to be consistent by Hardware instance and  
steady compensation thus makes sense. I also found it useful to 
periodically issue a programmatic 'settime' command from a system with an 
RTC just to keep the Arduino aligned.  

The central element of the execution logic is the Timer Settings Table. It
can host as many entries as available memory allows. The <tableSIZE> 
program parameter defines the number of entries.  9 entries are adequate 
for my uses which is why this is the defined default. The Settings Table 
is scanned several times per second.  Changes to that Table take effect 
immediately and do not need a trigger (i.e. the program is not event driven
as by alarms, but state driven).       


The following commands  may be used to remotely interact with the timer:   

  <showtime>  No parameters: returns actual system time of device. 
              System time is set to midnight at startup and increased from there.
  <settime HH:MM:SS>  Parameters must have indicated format.  Sets system time
              to desired value.  Returns ack or ERR.  Lends itself to automated
              time sync via netcat or similar.  
  <adjust ±n> Positive number accelerates, negative number slows down SW clock 
              by n seconds within 24 hours.  This command calibrates hardware 
              to clock speed, as the Arduino UNO does not have a reliable time 
              provider built in.  
  <#on&off HH:MM HH:MM> where # is the number of the relay addressed (1...4). 
              Normally open contact (NOC) is closed during the times indicated 
              (from... to).  Multiple entries for the same relay and overlapping 
              times are permissible.  NOC closed (=relay on) settings prevail.
  <#everyh MM:SS MM:SS> where # is the number of the relay addressed (1...4).
              This command repeats EVERY HOUR. NOC is closed during the indicated   
              period every hour. Multiple entries and overlapping times are 
              possible. NOC closed (=relay on) settings prevail.
  <#everym SS SS> where # is the number of the relay addressed (1...4). This   
              command repeats EVERY MINUTE. NOC is closed (=relay ON) during the 
              indicated period every minute. Multiple entries and overlapping 
              times are possible. NOC closed (=relay on) settings prevail. 
  <#manon>    where # is the number of the relay addressed (1...4).  Manually 
              overrides NOC settings to closed (=relay ON), just like the 
              physical "on" button on a mechanical timer. 
  <#manoff>   where # is the number of the relay addressed (1...4).  Counters
              #manon setting and reverts to timer setting (not necessarily 
              to "off").
  <#name>     assigns meaningful name to relay number #; max 8 chars; this name 
              used as MQ topic; if no name is given, relay number is used
  <clearall>  removes all entries from the timer setting teable.  All timer 
              settings then need to be reentered.
  <showall>   Shows all timer settings, including manual overrides and time
              adjustments (calibrations).
  <remove n>  Removes entry <n> from the timer settings table as visible via 
              showall command
  <verbose [on|off]> allows to choose the amount of feedback provided by the 
              application.  If set to off, messages regarding relay change 
              and time adjustment events will be suppressed. Default is off.

/************************************************************************/


#include <UnoWiFiDevEd.h>


//settable parameters
  #define tableSIZE 9                                                    // specifies number of entries in the Timer Settings Table


//constants
  #define CONNECTOR "mqtt"                                               // never change connector name 

  #define infoTxt01 F("valid commands are:")                             // adjust infotext when commands are changed
  #define infoTxt02 F("  showtime\r\n  settime  HH:MM:SS\r\n  adjust ±n (+ accelerates, - slows down by n secs/24h")
  #define infoTxt03 F("  #on&off  HH:MM HH:MM (on...off)\r\n  #everyh MM:SS MM:SS (on...off)\r\n  #everym SS SS (on...off)\r\n  #manon")
  #define infoTxt04 F("  #manoff\r\n  #name (relay name, max. 8 chars)\r\n  clearall\r\n  showall\r\n  remove n (n is table entry)\r\n  verbose [on|off]")
  #define infoTxt05 F("where #=relay number")

  const char inputErr[] = "*ERR: input!";                                // terse error message used repeatedly
  const char MQrelayStatus[] = "/relay/";                                // MQ topic name: needs slash at the end: not at bottom of hierarchy 
  const char MQinfo[] = "/info" ;                                        // MQ topic name: no slash at the end: is at bottom of hierarchy


//common variables
  unsigned long daySecs  = 0;                                            // daily seconds count that drives the timer's action 
  unsigned long previousMillis = 0;                                      // needed to monitor progress against internal clock
  long nthSecond = 0;                                                    // every so many seconds a second needs to be added(+) or dropped(-) from the daySec count 
  long actionNthSecond = 0;                                              // used for counting down the "nthSecond"
  char addOrSkipSecond[2] = "\0";


//when filled with data from the incoming message, these workfields tell us the following:
  char  MQdir[2];        // placeholder for future development           // - whether this is command addressed to a specific system (MQ only)
  char  MQsys[8];        // placeholder for future development           // - the name of the system affected                        (MQ only)
  char  MQcommand[9];                                                    // - what needs to be done
  char  MQdata[21];                                                      // - parameters for the stuff that needs to be done 


//Settings Table (array) that drives the relay setting
  byte HHfrom[tableSIZE];
  byte MMfrom[tableSIZE];
  byte SSfrom[tableSIZE];
  byte HHto[tableSIZE];
  byte MMto[tableSIZE];
  byte SSto[tableSIZE];
  byte rNum[tableSIZE];

//other general variables and flags
  boolean manuallySetToOn[4] = {false,false,false,false};                  // used to override the clock setting to always on
  boolean verboseModeEnabled = false ;                                     // verbose mode shows messages for any swich of state
                                                                           // by default disabled since changes are written to MQ
//functional names assigned to relays
  char relayNames[36];                                                     // alternate names for each of the four relays of max lenght 8 at position 0,9,18,27
                                                                           // xxxxxxxx0xxxxxxxx0xxxxxxxx0xxxxxxxx0
                                                                           // |        |        |        |                                                       
                                                                           // 012345678901234567890123456789012345
                                                                           //           1         2         3 


void setup() 
/*************************************************************************/
/*  HOUSEKEEPING: One Time Through at the beginning of the programm      */
/*************************************************************************/   
{
Wifi.begin();                                                            // initialise WiFi comms

pinMode(7, OUTPUT); digitalWrite(7,LOW);                                 // declare usage mode of pins used for relays
pinMode(6, OUTPUT); digitalWrite(6,LOW);                                 // and initialize to LOW (i.e. all relais are off)
pinMode(5, OUTPUT); digitalWrite(5,LOW);                                 //   
pinMode(4, OUTPUT); digitalWrite(4,LOW);                                 //


clearTimerSettingsTable();                                               // assign empty values to settings table (i.e. clear it) 

relayNamesDefaultValues();                                               // assign default values to relay names (before initial load!)

initialLoad();                                                           // Strictly Optional: Load precoded initial values into the timer
                                                                         // settings table (default values in case of a restart after failure)        
                                                                         // --> alternatively they can be loated programmatically 
                                                                         //     via the console (port 23) connection

delay(5000);                                                             // allow the system to finish getting its comms organised (min 4. secs)
                                                                         // before sending the first MQ message

//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//+ format and write MQ message confirming startup of system 
//+ format is:  "HH:MM:SS READY <program name> <program version>"  
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

char humanReadableSystemTimeNow[9] = "";                                 // allocate memory space for time string 
formatDaySecTime(humanReadableSystemTimeNow);                            // have function deliver time string in above memoryspace
                                                                         // 
char MQoutMsg[26];                                                       // allocate memory space for payload data
strcpy (MQoutMsg,"READY ");                                              ///
strcat (MQoutMsg,humanReadableSystemTimeNow);                            ///  | assemble 
strcat (MQoutMsg," ");                                                   ///  | payload 
strcat (MQoutMsg,programName);                                           ///  | data
strcat (MQoutMsg,programVersion);                                        ///
                                                                         //
char MQtopic[20];                                                        // allocate memory space and assemble topic string  
strcpy(MQtopic,MQto); strcat(MQtopic,MQinfo);                            // format is e.g.:  "o/8rd000/info"
                                                                         //
Ciao.write(CONNECTOR, MQtopic, MQoutMsg);                                // push to queue


}
/**********************************************************************************************************************************/
/*  HOUSEKEEPING END                                                                                                              */
/**********************************************************************************************************************************/  



void loop()
/**********************************************************************************************************************************/
/*  MAINLINE: forever loop                                                                                                        */
/**********************************************************************************************************************************/  
{  

updateDaySecs();                                                         // update the counter of daily elapsed seconds

processTimerSettings();                                                  // set the relays as required by the timer settings

lookForInstructions();                                                   // look for commands an instructions from the Console 

}
/**********************************************************************************************************************************/
/*  MAINLINE END                                                                                                                 */
/**********************************************************************************************************************************/  


void relayNamesDefaultValues()
/*************************************************************************/
/**          ASSIGN DEFAULT NAMES TO RELAY REFERENCES                   **/
/**                                                                     **/
/** These names will be the lowest level name of the MQ topics when     **/
/** status feedback is given.  By default they are just 01, 02, 03, 04. **/
/** In order to make these names more recognizable, the #name command   **/
/** (where # = 1...4) can be used to assign functional names to the     **/
/** relays so as to make the relay action more recognizable.            **/
/**                                                                     **/
/*************************************************************************/
{

for (int pos=1; pos<5; pos++)                                            // assign default values to the four substrings contained within "relayNames" 
  {                                                                      
    char* relayName = relayNames+(pos-1)*9;                              // "relayName" is a template placed in position 0,9,18,27 over the string "relayNames"
    char defaultVal[3] = "";                                             // field defined will accept formatted positioning reference (01, 02, 03, or 04) = 2 chars + end of string
    sprintf(defaultVal,"%02d",pos);                                      // format numerical int value of positioning reference into two char field with leading zero 
    strncpy(relayName, defaultVal, 8);                                   // copy formatted positioning reference to the positioned template overlay "relayName"
  }

}



void initialLoad() 
/*************************************************************************/
/**          SET DEFAULT VALUES AT TIME OF STARTUP OR RESTART           **/
/**                                                                     **/
/** Leave blank if starting with a  blank slate is ok.  Otherwise       **/
/** initialize here.                                                    **/
/**                                                                     **/
/*************************************************************************/ 
{


// strcpy (MQcommand,"1"); strcpy (MQdata,"06:30 20:30"); setEveryDay(atoi(MQcommand));  //relay 1  -  12V lamps
// strcpy (MQcommand,"4"); strcpy (MQdata,"06:29 20:29"); setEveryDay(atoi(MQcommand));  //relay 4  - 220V lamps
// strcpy (MQcommand,"3"); strcpy (MQdata,"02:00 02:10"); setEveryHour(atoi(MQcommand)); //relay 3  - pump
// strcpy (MQcommand,"3"); strcpy (MQdata,"17:00 17:10"); setEveryHour(atoi(MQcommand)); //relay 3  - pump
// strcpy (MQcommand,"3"); strcpy (MQdata,"32:00 32:10"); setEveryHour(atoi(MQcommand)); //relay 3  - pump
// strcpy (MQcommand,"3"); strcpy (MQdata,"47:00 47:10"); setEveryHour(atoi(MQcommand)); //relay 3  - pump

// strcpy (MQdata,"-20"); setSecAdjustmentInterval();

}


void lookForInstructions()
/*************************************************************************/
/**   LOOK FOR INPUT FROM THE CONSOLE AND PROCESS IT IF THERE IS ANY    **/
/**                                                                     **/
/** When this function is called it looks whether there are bytes       **/
/** waiting at Wifi.read. If there are, it assembles the incoming       **/
/** information, breaks it down into MQcommand & MQdata, and calls the  **/
/** code required to process the stated action.  If the input is not    **/  
/** recognized, it is discarded and an explanatory message is issued    **/
/**                                                                     **/
/*************************************************************************/ 
{

char inMsg[41] = "";                                                     // set area for incoming data to empty

int consoleByte = Wifi.read();                                           // look what is in the input queue from the console

if (consoleByte != -1)                                                   // if it is not a null byte, than there is info coming
  {
  strcpy (inMsg,"@");                                                    // plunk "@" sign,   
  strcat (inMsg, " ") ;                                                  // and one more blank at the beginning of a character string and start
  int charPos = 9;                                                       // filling from position 9 in the format of a targetted MQ message
  inMsg[charPos]=consoleByte;                                            // (i.e. as if it had come through a bus channel with a target header) 

  do                                                                     // do the following until carriage return is received
    {                                                                    // or receiving string is filled with more than 39 character 
    consoleByte = Wifi.read();                                           // get the next byte from the console
    if (consoleByte != -1)                                               // if this byte is not a null character,  
      {                                                                  // then 
      charPos = charPos+1;                                               // increase position for placement within the target string 
      inMsg[charPos]=consoleByte;                                        // and put the newly received byte in the calculated target position of the string  

      }
    }  
      while (consoleByte != '\n' && charPos < 40);                       // do as long no EOL char is received and the position 

                                                                         // in the target string is less than 40  

  charPos = charPos+1;                                                   // for good measure add end of string character 
  inMsg[charPos]="\0"; 

  }



//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//+ If there is an input message, then break it down ito into components: 
//+      - MQdir  ('@' for directed, blank for broadcast) - where applicable
//+      - MQsys  (source of message) - where applicable
//+      - MQcommand  (what needs to be done)
//+      - MQdata     (info that it needed to do what needs to be done)
//+ 

if (strlen(inMsg) > 0)                                                   // if in fact there is a message, then process it                                                     
  {                                                                            

//MQdir -- fixed length & location
  MQdir[0] = inMsg[0];                                                   // MQdir indicates direction; directed or outgoing; first character only


//MQsys -- fixed length & location   
  char* templateMQsys = inMsg+1;                                         // seven chars starting after the single "direction" characters
  strncpy (MQsys, templateMQsys, 7);                                     // move these characters to MQsys


//MQcommand & MQdata -- variable length, data is separated from command by a blank
  char* templateRemainder = inMsg+9;                                     // the remainder of the record contains the command plus data where applicable                                                                           

  char * pch;                                                            // define datafield to store memory location
  pch=strchr(templateRemainder,'\n');                                    // returns absolute memory location of first line feed character after begin of templateRemainder
  int fieldEnd = pch-templateRemainder;                                  // calculate relative length of whole input field 
  templateRemainder[fieldEnd] = '\0';                                    // replace LF char with string delimiter; now we have a regular bounded string 


  pch=strchr(templateRemainder,' ');                                     // is there a blank separating the action and data?
  if (pch == NULL)                                                       // if there is no blank after the action separating the data 
    {
    strncpy (MQcommand, templateRemainder, 8);                           // then the whole input up to a max of eight characters is an action command 
    strncpy (MQdata, "", 1);                                             // and the MQdata field is empty
    }     
   else                                                                  // on the other hand, if there is a blank delimiter
    {
    strncpy (MQcommand, templateRemainder, pch-templateRemainder);       // then only the part up to the delimiter is the action field
    MQcommand[pch-templateRemainder] = '\0' ;                            // terminate target string 
    char* templateData = pch;                                            // and the part after the delimiter
    strncpy (MQdata, templateData+1, 20);                                // is data that goes with the action  
    }


  for(int i = 0; MQcommand[i]; i++)                                      // "normalize" MQcommand to lower case
    {                                                                    // so that it can be better matched to 
    MQcommand[i] = tolower(MQcommand[i]);                                // commandstrings in the text
    }                                                                    //



//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//+ Send a copy of the preprocessed and broken down instructions received 
//+ to the MQ output
//
  char humanReadableSystemTimeNow[9] = "";                               // allocate memory space for time string                                                                
  formatDaySecTime(humanReadableSystemTimeNow);                          // have function deliver time string in above memoryspace

//write MQ messages 1     

  char MQoutMsg[35];                                                     // write input provided to MQ
  strcpy (MQoutMsg,humanReadableSystemTimeNow); 
  strcat (MQoutMsg," >");                                                ///          | 
  strcat (MQoutMsg,MQcommand);                                           ///          |       
  if (strlen(MQdata) > 0)                                                ///          |    data  
    {                                                                    ///          |    from  
    strcat (MQoutMsg," \"");                                             ///          |    input 
    strncat (MQoutMsg,MQdata,12);                                        ///          |
    strcat (MQoutMsg,"\"");                                              ///          |
    }                                                                    ///          |

  char MQtopic[15]; strcpy(MQtopic,MQto); strcat(MQtopic,MQinfo);        // topic of message, e.g.:  "o/info"

  Ciao.write(CONNECTOR, MQtopic, MQoutMsg);                              // push to queue


//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//+ Continue processing the message: depending on the content of the action 
//+ field just received either set appropriate data fields or call required 
//+ subroutine / function
//+

  boolean matchFound=false;                                              // flag to keep track of whether action could be matched 

  if (strstr(MQcommand,"showtime") != 0 )      {showSystemTime();  matchFound=true;}                   // show system time
  else if (strstr(MQcommand,"settime") != 0 )  {setSystemTime();  matchFound=true;}                    // set system time 
  else if (strstr(MQcommand,"showall") != 0)  {showAllSettings(); matchFound=true;}                    // show timer settings

  else if (strstr(MQcommand,"manon")  !=0) {manualOverride(atoi(MQcommand),true);  matchFound=true;}   // manually ON (override timer)
  else if (strstr(MQcommand,"manoff") !=0) {manualOverride(atoi(MQcommand),false); matchFound=true;}   // manually OFF (timer prevails)

  else if (strstr(MQcommand,"on&off") !=0) {setEveryDay(atoi(MQcommand)); matchFound=true; }           // define on and off times for relay

  else if (strstr(MQcommand,"everym") !=0) {setEveryMinute(atoi(MQcommand)); matchFound=true; }        // every minute: Relay # and seconds on & off 

  else if (strstr(MQcommand,"everyh") !=0) {setEveryHour(atoi(MQcommand)); matchFound=true; }          // every hourt: Relay # and mins & secs on & off 

  else if (strstr(MQcommand,"clearall") !=0)   {clearSettings(); matchFound=true;}                     // removes all entries from the settings table

  else if (strstr(MQcommand,"verbose") !=0)   {setVerboseOnOff(); matchFound=true;}                    // set chattyness  

  else if (strstr(MQcommand,"adjust") !=0)   {setSecAdjustmentInterval(); matchFound=true;}            // set number of seconds by which to correct the clock every day 

  else if (strstr(MQcommand,"remove") !=0)   {removeRowFromTimerSettingsTable(); matchFound=true;}     // remove indicated row from Timer Settings Table

  else if (strstr(MQcommand,"name") !=0)   {assigneNameToRelais(atoi(MQcommand)); matchFound=true;}    // assign functional name to relay



  else if (matchFound==false)                                            // if action could not be matched, then do the following:
    {

    Wifi.print(F("Command \""));                                         // provide feedback to console                                            
    Wifi.print(MQcommand);                                               // including the command part of the input field   
    Wifi.println(F("\" not recognized"));                                //    
    Wifi.println(infoTxt01);                                             // print infotext with valid commands
    Wifi.println(infoTxt02);
    Wifi.println(infoTxt03);
    Wifi.println(infoTxt04);
    Wifi.println(infoTxt05); 

    char MQtopic[15]; strcpy(MQtopic,MQto); strcat(MQtopic,MQinfo);      // topic of message, e.g.:  "o/info"
    Ciao.write(CONNECTOR, MQtopic, inputErr);                            // Err msg to queue

    }

  }

}

void assigneNameToRelais(int relaisNumber)
/*************************************************************************/
/** ASSIGNE FUNCTIONAL NAMES TO RELAYS                                  **/
/**                                                                     **/ 
/** These names will be used as lowest level MQTT topic names where     **/
/** the relays' status changes are reported                             **/
/**                                                                     **/ 
/*************************************************************************/
{
char humanReadableSystemTimeNow[9] = "";                                 // allocate memory space for time string                                                                
formatDaySecTime(humanReadableSystemTimeNow);                            // have function deliver time string in above memoryspace

if (! ((relaisNumber == 1) || (relaisNumber == 2)   ||                   // if relay number passed is not 1 or 2 or 3 or 4 
       (relaisNumber == 3) || (relaisNumber == 4) )  )                   // print error message and do not process data
  {
  Wifi.println(F("  ERR: First char not a valid relay number"));

  char MQtopic[20]; strcpy(MQtopic,MQto); strcat(MQtopic,MQinfo);        //format is e.g.:  "o/8rd000/info"
  Ciao.write(CONNECTOR, MQtopic, inputErr);                              // Err msg to queue
  }
else  
  {
  char* relayName = relayNames+(relaisNumber-1)*9;                        // position "relayName" template in position 0,9,18, or 27 over the string "relayNames", 
                                                                          // depending on the relay being addressed

  char prevName[9];                                                       // store old name for use in info message prepared later
  strncpy(prevName, relayName, 8);                                        //

  strncpy(relayName, MQdata, 8);                                          // copy max 8 characters from MQdata to the positioned template overlay "relayName" 

  Wifi.print(humanReadableSystemTimeNow);                                 // write info message
  Wifi.print(F("  Name of relais #"));
  Wifi.print(relaisNumber);
  Wifi.print(F(" changed from \""));
  Wifi.print(prevName);
  Wifi.print(F("\" to \""));
  Wifi.print(relayName);
  Wifi.println(F("\""));

  }


}

void removeRowFromTimerSettingsTable()
/*************************************************************************/
/** REMOVE INDICATED ROW FROM TIMER SETTINGS TABLE                      **/
/*************************************************************************/
{

char humanReadableSystemTimeNow[9] = "";                                  // allocate memory space for time string                                                                
formatDaySecTime(humanReadableSystemTimeNow);                             // have function deliver time string in above memoryspace

if  ( (atoi(MQdata) >= tableSIZE) || (atoi(MQdata) < 0) || (isdigit(MQdata[0]) ==false))
  {
  Wifi.println(F("  ERR: Not a valid table row"));                        // Err msg to stdout

  char MQtopic[20]; strcpy(MQtopic,MQto); strcat(MQtopic,MQinfo);         //format is e.g.:  "o/8rd000/info"
  Ciao.write(CONNECTOR, MQtopic, inputErr);                               // Err msg to queue
  }
else
  { 


  clearRowInTimerSettingsTable(atoi(MQdata));
  char printChunk[4] = ""; 
  sprintf(printChunk,"%02d",atoi(MQdata)); 

  Wifi.print(humanReadableSystemTimeNow); 
  Wifi.print(F(" Row "));
  Wifi.print(printChunk);
  Wifi.println(F(" cleared"));


  }
}


void setSecAdjustmentInterval()
/*************************************************************************/
/** SET INTERVAL FOR ADDING OR SKIPPING A SECOND                        **/
/**                                                                     **/
/** This feature is designed to compensate time deviations that HW      **/
/** devices not having a builtin clock might incur. The requested       **/   
/** adjustment will be applied gradually over a period of 24hrs.        **/
/**                                                                     **/
/** At the end of this code the following values will be set:           **/
/**  - nthSecond: indicating how often adjustments will need to be made **/ 
/**  - addOrSkipSecond: indicating whether adjustment is up or down     **/
/**                                                                     **/ 
/*************************************************************************/
{

char humanReadableSystemTimeNow[9] = "";                                  // allocate memory space for time string                                                                
formatDaySecTime(humanReadableSystemTimeNow);                             // have function deliver time string in above memoryspace


if (MQdata[0] == '0')                                                     // if input for adjust command is zero                                                     
  {                                                                       // then no adjustment should be made
  strcpy (addOrSkipSecond,"\0");                                          // the threeway "skip" flag is set to empty 
  nthSecond = 0;                                                          // and the nth-Seconds field is set to zero because there is no nth Second

  Wifi.print(humanReadableSystemTimeNow);   
  Wifi.println (F(" No adjustment! "));

  }
else                                                                      // if the input is not zero
  {                                                                       // then try to convert MQdata to a long integer
  char * pEnd;
  long int adjustBy = strtol(MQdata,&pEnd,10);                            // if conversion fails, data value will be zero  

  if (adjustBy == 0) 
    { 
    Wifi.println (F("  ERR: invalid input"));
    char MQtopic[20]; strcpy(MQtopic,MQto); strcat(MQtopic,MQinfo);       // format is e.g.:  "o/8rd000/info"
    Ciao.write(CONNECTOR, MQtopic, inputErr);                             // Err msg to queue
    }                                                                                            
  else 
    {
    nthSecond = 86400L/adjustBy;                                           // calculate nth second when action needs to happen; 
                                                                           // zerodevide not an issue, as this was covered above    
    actionNthSecond = 0 ;                                                  // reset action counter used to count down seconds

    if (adjustBy > 0)                                                      // set the direction of the adjustment in "addOrSkipSecond"
      {                                                                    // and make sure that "adjustBy" and "nthSecond" values are 
      strcpy (addOrSkipSecond,"+");                                        // always positive   
      }                                                                    //
    else                                                                   // 
      {                                                                    //
      strcpy (addOrSkipSecond,"-");                                        //
      adjustBy = adjustBy * -1;                                            //
      nthSecond = nthSecond * -1;                                          //
      }

    Wifi.print(humanReadableSystemTimeNow);                                // print info message 
    Wifi.print (F(" Adjusting clock by "));
    Wifi.print (atoi(MQdata)); 
    Wifi.print (F(" secs/24h: ")); 

    if (strcmp(addOrSkipSecond, "+") == 0)
      {
      Wifi.print (F("add"));
      }
    else
      { 
      Wifi.print (F("skip"));   
      }
    Wifi.print (F(" 1 sec every "));
    Wifi.print (nthSecond);
    Wifi.println (F(" secs"));

    }
  }
}




void setVerboseOnOff()
/*************************************************************************/
/** MANUALLY SET CHATTINESS LEVEL; default is Verbose = false           **/
/*************************************************************************/
{

for(int i = 0; MQdata[i]; i++)                                             // "normalize" MQdata to lower case
  {                                                                        //
  MQdata[i] = tolower(MQdata[i]);                                          //
  }                                                                        //

char humanReadableSystemTimeNow[9] = "";                                   // allocate memory space for time string                                                                
formatDaySecTime(humanReadableSystemTimeNow);                              // have function deliver time string in above memoryspace

if (strstr(MQdata,"on") != 0)                                              // switch ON
  {
  verboseModeEnabled = true ;  
  Wifi.print(humanReadableSystemTimeNow);                                  // Print info message 
  Wifi.println(F("  Verbose mode is on" ));                                //
  }
else if (strstr(MQdata,"off") != 0)                                        // switch OFF
  {
  verboseModeEnabled = false;
    Wifi.print(humanReadableSystemTimeNow);                                // Print info message 
    Wifi.println(F("  Verbose mode is OFF" ));                             //
  }
else                                                                       // clueless! 
  {    
    Wifi.print(humanReadableSystemTimeNow);                                // Print info message 
    Wifi.println(F("  Verbosity instructions unclear" ));                  //
  }  
}



void clearSettings()
/*************************************************************************/
/** REMOVE ALL ENTRIES FROM SETTINGS TABLE AND MANUAL OVERRIDE          **/
/*************************************************************************/
{
clearTimerSettingsTable();

manuallySetToOn[0]=false; manuallySetToOn[1]=false;                        // clear manual overrides of clock settings 
manuallySetToOn[2]=false; manuallySetToOn[3]=false;                        // 


char humanReadableSystemTimeNow[9] = "";                                   // allocate memory space for time string                                                                
formatDaySecTime(humanReadableSystemTimeNow);                              // have function deliver time string in above memoryspace

Wifi.print(humanReadableSystemTimeNow);                                    // Print confirmation message 
Wifi.println(F("  All timer settings cleared" ));                          //

}


void showAllSettings()
/*************************************************************************/
/** SHOW CONTENT OF SETTINGS TABLE AND OTHER PARAMETERS                 **/
/*************************************************************************/
{
char printChunk[4] = "";                                                   // define field to format variables into   

char humanReadableSystemTimeNow[9] = "";                                   // Allocate memory space for time string                                                                
formatDaySecTime(humanReadableSystemTimeNow);                              // call function and have it deliver time string in above memoryspace

Wifi.println("");                                                          // insert blank line at the beginning of the block

for (int n = 0; n < tableSIZE; n++)                                        // step through the table row by row, analyze, format, and print
  {

  sprintf(printChunk,"%02d: ",n);                                          // stage row number of table and place in front of next print line 
  Wifi.print(printChunk);


  if (rNum[n] !=255)                                                       // 255 is initialisation value, meaning there is no entry
    {
    if (HHfrom[n]>23 && MMfrom[n]>60)                                      // format for seconds repeating every minute
      {
      Wifi.print(F("relay "));Wifi.print(rNum[n]);
      Wifi.print(F(": every min ON at " ));
      sprintf(printChunk,"%02d",SSfrom[n]);Wifi.print(printChunk); 
      Wifi.print(F("s and OFF at ")); 
      sprintf(printChunk,"%02d",SSto[n]);Wifi.print(printChunk);  
      Wifi.println("s");  
      }
    else if (HHfrom[n]>23)                                                 // format for minute and seconds repeating every hour (Stundentakt)
      {
      Wifi.print(F("relay "));Wifi.print(rNum[n]);
      Wifi.print(F(": every hour ON at " )); 
      sprintf(printChunk,"%02d",MMfrom[n]);Wifi.print(printChunk);  Wifi.print(":");
      sprintf(printChunk,"%02d",SSfrom[n]);Wifi.print(printChunk); 
      Wifi.print(F("mins, OFF at "));
      sprintf(printChunk,"%02d",MMto[n]);Wifi.print(printChunk);  Wifi.print(":");
      sprintf(printChunk,"%02d",SSto[n]);Wifi.print(printChunk); 
      Wifi.println("mins");
      }
    else                                                                   // standard from-to format HH and MM only 
      {
      Wifi.print(F("relay "));Wifi.print(rNum[n]);
      Wifi.print(F(": ON at ")); 
      sprintf(printChunk,"%02d",HHfrom[n]);Wifi.print(printChunk);Wifi.print(":");
      sprintf(printChunk,"%02d",MMfrom[n]);Wifi.print(printChunk);
      Wifi.print(F("hrs, OFF at "));
      sprintf(printChunk,"%02d",HHto[n]);Wifi.print(printChunk);Wifi.print(":");
      sprintf(printChunk,"%02d",MMto[n]);Wifi.print(printChunk);
      Wifi.println("hrs");

      }
    } 
  else
    {
    Wifi.println(F("no setting in this position"));    
    }
  }

Wifi.print(F("Manual override active for relay: ")) ;                      // show whether there are any active manual overrides    

bool noOverride = true;                                                    //


for (byte n=0;n<4;n++)                                                     //
  {                                                                        //


  if (manuallySetToOn[n]==true)                                            // 
    {                                                                      //
    noOverride=false;                                                      //
    Wifi.print(n+1); Wifi.print(" ");                                      //


    }                                                                      //
  }                                                                        //
if (noOverride==true) Wifi.print(F("*** NONE ***"));                       //
Wifi.println("");                                                          


Wifi.print(F("Adjustment to internal clock: "));                           // show whether clock adjustments have been set up

int adjustmentSecs;
if (strcmp(addOrSkipSecond, "\0") == 0)                                    //
  {                                                                        // 
    Wifi.println (F("*** NONE ***"));                                      // 
  }                                                                        //
else                                                                       //
  {                                                                        //     
  adjustmentSecs = 86400 / nthSecond;                                      //
  if (strcmp(addOrSkipSecond, "-") == 0)                                   //
    {                                                                      //
    adjustmentSecs = adjustmentSecs * -1;                                  //
    }                                                                      //
  Wifi.print (adjustmentSecs);                                             //
  Wifi.println (F(" secs/24hrs"));                                         //

  }

for (int pos=1; pos<5; pos++)                                              // assign default values to the four substrings contained within "relayNames" 
  {                                                                      
    char* relayName = relayNames+(pos-1)*9;                                // "relayName" is a template placed in position 0,9,18,27 over the string "relayNames"
                                                                           // and contains the functional name of the referenced relay

    Wifi.print (F("Relay #")); Wifi.print(pos);                            //  |
    Wifi.print (F(" is currently named \"")); Wifi.print (relayName);      //  |  Print info: 4 lines
    Wifi.println ("\"");                                                   //  |
  }


Wifi.print (F("Program name and version: "));  
Wifi.print (programName); 
Wifi.println (programVersion);

}




void manualOverride (int relaisNumber, bool yes)
/*************************************************************************/
/** MANUAL OVERRIDE TO KEEP A RELAY PERMANATELY ON (NOC = CLOSED)       **/
/** - When the override is engaged for a given relay, timer settings    **/         
/**     will be ignored and the normally open circuit will be closed    **/
/** - When the override is disengaged, control reverts to timer         **/
/*************************************************************************/ 
{

char humanReadableSystemTimeNow[9] = "";                                   // Allocate memory space for time string       
formatDaySecTime(humanReadableSystemTimeNow);                              // call function and have it deliver time string in above memoryspace



if (! ((relaisNumber == 1) || (relaisNumber == 2)   ||                     // if relay number passed is not 1 or 2 or 3 or 4 
       (relaisNumber == 3) || (relaisNumber == 4) )  )                     // print error message and do not process data  
  {

  Wifi.println(F("  ERR: First char not a valid relay number"));         

  char MQtopic[20]; strcpy(MQtopic,MQto); strcat(MQtopic,MQinfo);          //format is e.g.:  "o/8rd000/info"
  Ciao.write(CONNECTOR, MQtopic, inputErr);

  }
else  
  {
  if (yes==true)
    {
    manuallySetToOn[relaisNumber-1]=true; 
    }
  else
    {
    manuallySetToOn[relaisNumber-1]=false;  
    }
  }

}



void clearTimerSettingsTable()
/*************************************************************************/
/** Clears and initialize Timer Settings Table                          **/       
/*************************************************************************/ 
{
for (int n = 0; n < tableSIZE; n++)                                        // reset Timer Settings Table
  { 
  clearRowInTimerSettingsTable(n);
  }
}



void clearRowInTimerSettingsTable(int tableRow)
/*************************************************************************/
/** Clears and initialize row in Timer Settings Table                   **/       
/*************************************************************************/ 
{
HHfrom[tableRow]=255; MMfrom[tableRow]=255; SSfrom[tableRow]=255;          // initialize row in Timer Settings Table
  HHto[tableRow]=255;   MMto[tableRow]=255;   SSto[tableRow]=255;
  rNum[tableRow]=255;
}




void updateDaySecs()
/*************************************************************************/
/** Update counter of daily elapsed seconds (this counter drives the    **/
/** timer's activities.                                                 **/           
/*************************************************************************/ 
{
if (((millis() - previousMillis) >= 1000)  ||                              // if more than a second has elapsed since last time we passed here, or
    (millis() < previousMillis))                                           // the builtin millisecond counter has turned over since that time,                                                                  
  {                                                                        // then do the following:
  previousMillis = millis();                                               //   - store content of current milliseconds counter for future use  
  daySecs=daySecs+1;                                                       //   - add 1 to this program's daySecs counter 
                                                                           //  
  if (daySecs > 86400) daySecs = 1;                                        // reset daySecs counter before hitting 24:00:00 hrs

  if (addOrSkipSecond[0] != '\0')
    { 
//=== debug  Wifi.print("addOrSkipSecond[0]=");Wifi.println(addOrSkipSecond[0]);
//=== debug  Wifi.print("actionNthSecond=");Wifi.println(actionNthSecond);
    if (actionNthSecond == 0)
      {
      char humanReadableSystemTimeNow[9] = "";                             // Allocate memory space for time string                                                                
      formatDaySecTime(humanReadableSystemTimeNow);                        // call function and have it deliver time string in above memoryspace

//=== debug  Wifi.print("nthSecond=");Wifi.println(nthSecond);
      actionNthSecond = nthSecond-1;
      if (addOrSkipSecond[0]=='+') 
        {
        daySecs=daySecs+1;

        char MQoutMsg[40];                                                 // write input provided to MQ
        strcpy (MQoutMsg,humanReadableSystemTimeNow);                      // time when sent  
        strcat (MQoutMsg," +1sec");                                        //  

        char MQtopic[20]; strcpy(MQtopic,MQto); strcat(MQtopic,MQinfo);    // format is e.g.:  "o/8rd000/info" 
        Ciao.write(CONNECTOR, MQtopic, MQoutMsg);                          // push to queue  

        if (verboseModeEnabled==true)                                      // write message only if verbose mode enabled
          {
          Wifi.print(humanReadableSystemTimeNow);                              
          Wifi.println(F(" Time adjusted: Jumped ahead one second" ));  
          }
        }
      else if (addOrSkipSecond[0]=='-') 
        {
        daySecs=daySecs-1;  

        char MQoutMsg[40];                                                 // write input provided to MQ
        strcpy (MQoutMsg,humanReadableSystemTimeNow);                      // time when sent  
        strcat (MQoutMsg," -1sec");                                        // 

        char MQtopic[20]; strcpy(MQtopic,MQto); strcat(MQtopic,MQinfo);    // format is e.g.:  "o/8rd000/info" 
        Ciao.write(CONNECTOR, MQtopic, MQoutMsg);                          // push to queue  

        if (verboseModeEnabled==true)                                      // write message only if verbose mode enabled
          {
          Wifi.print(humanReadableSystemTimeNow);                              
          Wifi.println(F(" Time adjusted: Held back one second" ));  
          }        
        }           
      }
    else
      { 
      actionNthSecond = actionNthSecond - 1;
      }
    } 

  }   // done processing elapsed decond   
}



void processTimerSettings()
/*************************************************************************/
/**  - walk through the action table                                    **/
/**  - find out what needs to be happening at this time (daySecs)       **/
/**  - verify whether it is happening                                   **/
/**  - if it is not happening, then make it happen                      **/            
/*************************************************************************/ 
{
boolean targetRstatus[4] = {false,false,false,false};                      // used to compose target relais status for each loop (default is off, pos. 0 is relais 1)


byte hh =  daySecs / 3600;                                                 //
byte mm = (daySecs - (hh*3600L))/60;                                       // calculate Hrs, Mins, Secs from actual day second count
byte ss = (daySecs - (hh*3600L) - (mm*60));                                // the "L" indicates that the math is done in long variables


for (int n = 0; n < tableSIZE; n++)                                        // walk settings of TABLE ENTRIES and decide which relais needs to be on just now
  {                                                                     
  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  //+ SET RELAY STATUS BASED ON TIME BOUNDARIES STORED IN TABLE
  //+
  //+ If current time is between the time boundaries stated in the table entry
  //+ being explored, then set target status of specified relais to on (=true) 
  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  unsigned long loBound = SSfrom[n] + (MMfrom[n]*60L) + (HHfrom[n]*3600L); // calculate low boundary of table entry
  unsigned long hiBound = SSto[n] + (MMto[n]*60L) + (HHto[n]*3600L);       // calculate high boundary of table entry

  if ((daySecs > loBound) && (daySecs < hiBound))                          // if current time is within table entry boundaries 
    {                                                                      // then set the relais associated with that table entry
    targetRstatus[rNum[n]-1] = true;                                       // to true (=ON) 
    }



  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  //+ PROCESS SETTINGS OF SECONDS RECURRING BY THE MINUTE 
  //+ 
  //+ If both the starting hour and the starting minute specified in the table
  //+ exceed max. value, then check whether the current second count is within
  //+ the boundaries specified in the table.  If so, then set the target state
  //+ of the specified relais to on (=true) 
  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


  if ((HHfrom[n]>23 && HHfrom[n]!=255) &&                                  // if from-hours (HHfrom) and from-minutes (MMfrom) exceed max value 
      (MMfrom[n]>60 && MMfrom[n]!=255))                                    // (but are not equal to 255 which is initialisation value)
    {                                                                      // then there is an entry for switching a relais every minute   
    if (SSto[n] > SSfrom[n])                                               // this is the regular situation with no boundary wrapping 
      {    
      if ((ss>=SSfrom[n]) && (ss<=SSto[n]))                                // if current time is between boundary times 
        {
        targetRstatus[rNum[n]-1] = true;                                   // than the relay ought to be "on" 
        }
      }
    else                                                                   // if the seconds when to stop are lower than the seconds when to start 
      {                                                                    // we need to ask a different question:
      if ((ss>=SSfrom[n]) || (ss<=SSto[n]))                                // is the start time less than now OR is the stop time greater that nor        
        {
        targetRstatus[rNum[n]-1] = true;                                   // than the relay ought to be "on" 
        }
      }
    }




  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  //+ PROCESS SETTINGS OF MINUTES & SECONDS RECURRING BY THE HOUR
  //+  
  //+ If only the starting hour specified in the table is out of bound, check
  //+ whether the current minute and second counts are within the boundaries 
  //+ specified in the table.  If so, set the target state of the associated 
  //+ relais to on (=true) 
  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  if ((HHfrom[n]>23 && HHfrom[n]!=255) &&                                  // if only from-hours (HHfrom) are out of range 
     !(MMfrom[n]>60 && MMfrom[n]!=255))                                    // (but not 255 which is initialisation value) 
    {                                                                      // then we need to process Stundentakt
    int calcActual= (mm * 60) + ss;
    int calcFrom = (MMfrom[n] * 60)  + SSfrom[n];
    int calcTo   = (MMto[n]   * 60)  + SSto[n];  

    if (calcTo > calcFrom)                                                 // no wrapping involved
      {
      if ((calcActual>=calcFrom) && (calcActual<=calcTo))                  // if current time is between boundayry times 
        {
        targetRstatus[rNum[n]-1] = true;                                   // than the relay ought to be "on" 
        }
      } 
    else
      {
      if ((calcActual>=calcFrom) || (calcActual<=calcTo))                  // is the start time less than now OR is the stop time greater that nor        
        {
        targetRstatus[rNum[n]-1] = true;                                   // than the relay ought to be "on" 
        }
      } 
    }    


  }     //end of "for" statement                                           // DONE walking time settings table entries.  Table "targetRstatus" now set. 



//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//+ SWITCH RELAIS ON AND OFF SO AS TO REFLECT SETTING IN targetRstatus TABLE
//+
//+ Table "targetRstatus" shows which relays ought to be ON (true) and which  
//+ which ought to be OFF (false) at this very moment.    
//+
//+ Position 0 through 3 show status of relay 1 through 4 respectively. 
//+ It is a boolean table.  True means relay is on.  False means it is off. 
//+ Relais 1 through 4 are addressed by pins 7 through 4 in that order.    
//+
//+       index position targetRstatus table  0  1  2  3    n
//+   shows required setting of relay number  1  2  3  4
//+                which is addressed by pin  7  6  5  4    x
//+
//+ Hence index position [n] indicates the status of pin (n-7)*-1. Conversly
//+ the status of pin x is reflected by index position [7-x]
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


for (byte pin = 7; pin >= 4; pin--)                                        // enact settings of targetRstatus table for each of the four pins
  {  
  if ((targetRstatus[7-pin] == true) ||                                    // pin should be set to high (relais ON)  
      (manuallySetToOn[7-pin] == true)) 
    {
    if (digitalRead(pin) != HIGH)                                          // if pin is not already set to HIGH
      {    
      digitalWrite(pin,HIGH);                                              // then set it to HIGH, and let the world know about the switch

      char humanReadableSystemTimeNow[9] = "";                             // allocate memory space for time string                                                                
      formatDaySecTime(humanReadableSystemTimeNow);                        // have function deliver time string in above memoryspace
      byte numRelay = 7-pin+1;                                             // calculate relay number based on pin processed

//write verbose message      
      if (verboseModeEnabled==true)                                        // write Console message only if verbose mode enabled
        {
        char* relayName = relayNames+(numRelay-1)*9;                       // "relayName" is a template placed in position 0,9,18,27 over the string "relayNames"
        Wifi.print(humanReadableSystemTimeNow);                            // 
        Wifi.print(F("  Relay \""));                                       // and send a message 
        Wifi.print(relayName);   
        Wifi.println(F("\" on"));                                          //
        }

//write MQ message for "on"  
      char MQoutMsg[15];                                                   // payload of message is:  time followed by "on"     
      strcpy (MQoutMsg,humanReadableSystemTimeNow);  
      strcat (MQoutMsg," on");                                      

      char MQtopic[25];   strcpy(MQtopic,MQto);                            // topic of message, e.g.:  "tackle/8rd000/relay/02"
      strcat(MQtopic,MQrelayStatus);                                       //
      char* relayName = relayNames+(numRelay-1)*9;                         // position template overlay on name associated with the relay number being processed
      strcat (MQtopic,relayName);                                          // copy relay name to string describing MQ topic
      Ciao.write(CONNECTOR, MQtopic, MQoutMsg);                            // push to queue


      }
    }
  else                                                                     // target status is false and pin should be low (relais OFF)
    { 
    if (digitalRead(pin) != LOW)                                           // if pin is not already set to LOW
      {  
      digitalWrite(pin,LOW);                                               // then set it to LOW, and let the world know about the switch

      char humanReadableSystemTimeNow[9] = "";                             // allocate memory space for time string                                                                
      formatDaySecTime(humanReadableSystemTimeNow);                        // have function deliver time string in above memoryspace

      byte numRelay = 7-pin+1;

//write verbose message
      if (verboseModeEnabled==true)                                        // write message only if verbose mode enabled
        {
        char* relayName = relayNames+(numRelay-1)*9;                       // "relayName" is a template placed in position 0,9,18,27 over the string "relayNames"
        Wifi.print(humanReadableSystemTimeNow);                            // 
        Wifi.print(F("  Relay \"" ));                                      // and send a message 
        Wifi.print(relayName);                                             //
        Wifi.println(F("\" off"));                                         //
        }

//write MQ message for "off"  
      char MQoutMsg[15];                                                   // payload of message is:  time followed by "off"     
      strcpy (MQoutMsg,humanReadableSystemTimeNow);  
      strcat (MQoutMsg," off");                                      

      char MQtopic[25];   strcpy(MQtopic,MQto);                            // topic of message, e.g.:  "tackle/8rd000/relay/02"
      strcat(MQtopic,MQrelayStatus);                                       //
      char* relayName = relayNames+(numRelay-1)*9;                         // position template overlay on name associated with the relay number being processed
      strcat (MQtopic,relayName);                                          // copy relay name to string describing MQ topic

      Ciao.write(CONNECTOR, MQtopic, MQoutMsg);                            // push to queue
      }
    }
  }     //end of "for" statement       

}






void setEveryDay (int relaisNumber)
/*************************************************************************/
/** Insert relais switching time into the status table.  Only           **/
/** granularity up to the minute is currently possible, but could be    **/
/** upgraded to seconds if necessary.  The table allows for that        **/   
/*************************************************************************/   
{
char* timeOnHH = MQdata;                                                   // template overlay on incoming data
char* timeOnMM = MQdata+3;
char* timeOffHH = MQdata+6 ;
char* timeOffMM = MQdata+9;

char HHandMMon[6] = "";                                                    // formatted on and off times for printing     
char HHandMMoff[6]= "";                                                    //

char humanReadableSystemTimeNow[9] = "";                                   // allocate memory space for time string                                                                
formatDaySecTime(humanReadableSystemTimeNow);                              // have function deliver time string in above memoryspace


if (! ((relaisNumber == 1) || (relaisNumber == 2)   ||                     // if relay number passed is not 1 or 2 or 3 or 4 
       (relaisNumber == 3) || (relaisNumber == 4) )  )                     // print error message and do not process data
  {                                                                        //
  Wifi.println(F("  ERR: First char not a valid relay number"));           //

  char MQtopic[20]; strcpy(MQtopic,MQto); strcat(MQtopic,MQinfo);          //format is e.g.:  "o/8rd000/info"
  Ciao.write(CONNECTOR, MQtopic, inputErr);                                // Err msg to queue


  }
else  
  {


  byte tRow = 0;                                                           // row counter of table being inspected; start with zero
  byte freeSlot = tableSIZE;                                               // memorize free slot here if found; otherwise default is tableSize
  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  //+ Find free slot in action table. Return number of free row in "freeSlot".
  //+ When freeSlot=tableSIZE (the initialisation value) none are left.
  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  while (tRow < tableSIZE)                                                 // walk the table looking for a free row until running out of rows
    {   
    if (rNum[tRow] == 255)                                                 // 255 is initialised value: current row is unused; hence let's stop looking
    {
      freeSlot=tRow;                                                       // memorize the row that is free 
      tRow=tableSIZE;                                                      // set row counter to tablesize so as to break out from the loop 
      }
    else                                                                   // if the row inspected is already in use
      {                                                                    // increase the row counter and keep looking 
      tRow= tRow+1;                                                        // until running out of rows
      }                                                                    //
    }


  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  //+ Fill the free row with data entered and send ack message or advise that 
  //+ there are now more slots left for recording switching times. 
  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  if (freeSlot<tableSIZE)
    {  
    rNum[freeSlot] = relaisNumber;
    HHfrom[freeSlot] = atoi(timeOnHH);                                     //  
    MMfrom[freeSlot] = atoi(timeOnMM);                                     //
    SSfrom[freeSlot] = 0 ;                                                 // load data into the identified table row
    HHto[freeSlot]   = atoi(timeOffHH);                                    //
    MMto[freeSlot]   = atoi(timeOffMM);                                    //
    SSto[freeSlot]   = 0 ;                                                 //

    sprintf(HHandMMon, "%02d:%02d", HHfrom[freeSlot],  MMfrom[freeSlot] ); // format fields for ack message from table entries
    sprintf(HHandMMoff,"%02d:%02d", HHto[freeSlot],    MMto[freeSlot]   ); // (and not from data entered)

    Wifi.print(humanReadableSystemTimeNow);                                // write info message
    Wifi.print(F("  Slot #"));                                             //
    Wifi.print(freeSlot);                                                  //
    Wifi.print(F(": relay #"));                                            //
    Wifi.print(rNum[freeSlot]);                                            //
    Wifi.print(F(" ON at "));                                              //
    Wifi.print(HHandMMon);                                                 //
    Wifi.print(F(" and OFF at "));                                         //
    Wifi.print(HHandMMoff);                                                // 
    Wifi.println("");                                                      // and don't forget the carriage return
    }
  else
    {
    Wifi.print(humanReadableSystemTimeNow);                                // write this if table was full    
    Wifi.println(F("  All slots taken"));                                  //
    }
  }
}


void setEveryMinute(int relaisNumber)
/*************************************************************************/
/** Insert in table events that have a periodicty of a minute           **/   
/*************************************************************************/ 
{
char* timeOnSS = MQdata;                                                   // template overlay on incoming data
char* timeOffSS = MQdata+3;

char humanReadableSystemTimeNow[9] = "";                                   // allocate memory space for time string                                                                
formatDaySecTime(humanReadableSystemTimeNow);                              // have function deliver time string in above memoryspace

if (! ((relaisNumber == 1) || (relaisNumber == 2)   ||                     // if relay number passed is not 1 or 2 or 3 or 4 
       (relaisNumber == 3) || (relaisNumber == 4) )  )                     // print error message and do not process data
  {
  Wifi.println(F("  ERR: First char not a valid relay number"));

  char MQtopic[20]; strcpy(MQtopic,MQto); strcat(MQtopic,MQinfo);          //format is e.g.:  "o/8rd000/info"
  Ciao.write(CONNECTOR, MQtopic, inputErr);                                // Err msg to queue
  }
else  
  {


  byte tRow = 0;                                                           // row counter of table being inspected; start with zero
  byte freeSlot = tableSIZE;                                               // memorize free slot here if found; otherwise default is tableSize
  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  //+ Find free slot in action table. Return number of free row in "freeSlot".
  //+ When freeSlot=tableSIZE (the initialisation value) none are left.
  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  while (tRow < tableSIZE)                                                 // walk the table looking for a free row until running out of rows
    {   
    if (rNum[tRow] == 255)                                                 // 255 is initialised value: current row is unused; hence let's stop looking
    {
      freeSlot=tRow;                                                       // memorize the row that is free 
      tRow=tableSIZE;                                                      // set row counter to tablesize so as to break out from the loop 
      }
    else                                                                   // if the row inspected is already in use
      {                                                                    // increase the row counter and keep looking 
      tRow= tRow+1;                                                        // until running out of rows
      }                                                                    //
    }


  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  //+ Fill the free row with data entered and send ack message or advise that 
  //+ there are no more slots left for recording switching times. 
  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  if (freeSlot<tableSIZE)
    {  
    rNum[freeSlot] = relaisNumber;
    HHfrom[freeSlot] = 99;                                                 //  
    MMfrom[freeSlot] = 99;                                                 //
    SSfrom[freeSlot] = atoi(timeOnSS);                                     // load data into the identified table row
    HHto[freeSlot]   = 99;                                                 //
    MMto[freeSlot]   = 99;                                                 //
    SSto[freeSlot]   = atoi(timeOffSS) ;                                   //


    Wifi.print(humanReadableSystemTimeNow);                                // write info message
    Wifi.print(F("  Slot #"));                                             //
    Wifi.print(freeSlot);                                                  //
    Wifi.print(F(": relay #"));                                            //
    Wifi.print(rNum[freeSlot]);                                            //
    Wifi.print(F(" every minute ON at "));                                 //
    Wifi.print(SSfrom[freeSlot]);                                          //
    Wifi.print(F("s and OFF at "));                                        //
    Wifi.print(SSto[freeSlot]);                                            // 
    Wifi.println(F("s"));                                                  // and don't forget the carriage return
    }
  else
    {
    Wifi.print(humanReadableSystemTimeNow);                                // print this if table is full    
    Wifi.println(F("  All slots taken"));                                  //
    }
  }

}

void setEveryHour(int relaisNumber)
/*************************************************************************/
/** Insert in table events that have a periodicty of a minute           **/   
/*************************************************************************/ 
{
char* timeOnMM= MQdata;                                                    // template overlay on incoming data
char* timeOnSS = MQdata+3;
char* timeOffMM = MQdata+6 ;
char* timeOffSS = MQdata+9;

char MMandSSon[6] = "";                                                    // formatted on and off times for printing     
char MMandSSoff[6]= "";                                                    //

char humanReadableSystemTimeNow[9] = "";                                   // allocate memory space for time string                                                                
formatDaySecTime(humanReadableSystemTimeNow);                              // have function deliver time string in above memoryspace


if (! ((relaisNumber == 1) || (relaisNumber == 2)   ||                     // if relay number passed is not 1 or 2 or 3 or 4 
       (relaisNumber == 3) || (relaisNumber == 4) )  )                     // print error message and do not process data
  {                                                                        //
  Wifi.println(F("  ERR: First char not a valid relay number"));           //

  char MQoutMsg[40];                                                       // write input provided to MQ

  char MQtopic[20]; strcpy(MQtopic,MQto); strcat(MQtopic,MQinfo);          //format is e.g.:  "o/8rd000/info"
  Ciao.write(CONNECTOR, MQtopic, inputErr);                                // Err msg to queue
  }
else  
  {

  byte tRow = 0;                                                           // row counter of table being inspected; start with zero
  byte freeSlot = tableSIZE;                                               // memorize free slot here if found; otherwise default is tableSize
  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  //+ Find free slot in action table. Return number of free row in "freeSlot".
  //+ When freeSlot=tableSIZE (the initialisation value) none are left.
  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  while (tRow < tableSIZE)                                                 // walk the table looking for a free row until running out of rows
    {   
    if (rNum[tRow] == 255)                                                 // 255 is initialised value: current row is unused; hence let's stop looking
    {
      freeSlot=tRow;                                                       // memorize the row that is free 
      tRow=tableSIZE;                                                      // set row counter to tablesize so as to break out from the loop 
      }
    else                                                                   // if the row inspected is already in use
      {                                                                    // increase the row counter and keep looking 
      tRow= tRow+1;                                                        // until running out of rows
      }                                                                    //
    }


  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  //+ Fill the free row with data entered and send ack message or advise that 
  //+ there are no more slots left for recording switching times. 
  //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

  if (freeSlot<tableSIZE)
    {  
    rNum[freeSlot] = relaisNumber;
    HHfrom[freeSlot] = 99;                                                 //  
    MMfrom[freeSlot] = atoi(timeOnMM);                                     //
    SSfrom[freeSlot] = atoi(timeOnSS);                                     // load data into the identified table row
    HHto[freeSlot]   = 99;                                                 //
    MMto[freeSlot]   = atoi(timeOffMM);                                    //
    SSto[freeSlot]   = atoi(timeOffSS) ;                                   //


    sprintf(MMandSSon, "%02d:%02d", MMfrom[freeSlot],  SSfrom[freeSlot] ); // format fields for ack message from table entries
    sprintf(MMandSSoff,"%02d:%02d", MMto[freeSlot],    SSto[freeSlot]   ); // (and not from data entered)

    Wifi.print(humanReadableSystemTimeNow);                                // write info message
    Wifi.print(F("  Slot #"));                                             //
    Wifi.print(freeSlot);                                                  //
    Wifi.print(F(": relay #"));                                            //
    Wifi.print(rNum[freeSlot]);                                            //
    Wifi.print(F(" every hour ON at "));                                   //
    Wifi.print(MMandSSon);                                                 //
    Wifi.print(F(" and OFF at "));                                         //
    Wifi.print(MMandSSoff);                                                // 
    Wifi.println(F(""));                                                   // and don't forget the carriage return
    }
  else
    {
    Wifi.print(humanReadableSystemTimeNow);                                // print this if table is full    
    Wifi.println(F("  All slots taken"));                                  //
    }
  }

}





void setSystemTime() 
/*************************************************************************/
/* Set System time to value received on tickle message and give feedback */
/*                                                                       */
/*************************************************************************/


{
char* HHour = MQdata;                                                     // template overlay to incoming data
char* MMin = MQdata+3;
char* SSec = MQdata+6;

char humanReadableSystemTimeNow[9] = "";                                  // allocate memory space for time string                                                                
formatDaySecTime(humanReadableSystemTimeNow);                             // have function deliver time string in above memoryspace

if ((isDigit(HHour[0]) == true) &&  (isDigit(HHour[1]) == true) &&        // verify that all relevant characters are numeric
    (isDigit (MMin[0]) == true) &&  (isDigit (MMin[1]) == true) &&        // reject if they aren't
    (isDigit (SSec[0]) == true) &&  (isDigit (SSec[1]) == true))          //
  {
  int hh = atoi(HHour);                                                   // convert incoming numeric string values to integers
  int mm = atoi(MMin);                                                    //
  int ss = atoi(SSec);                                                    //

  if ((hh <= 24) && (mm <= 60) && (ss <= 60)) 
    {
    daySecs = ss + (mm*60L) + (hh*3600L);                                 // update daySec value

    Wifi.print(humanReadableSystemTimeNow);                               // send ack
    Wifi.println F((" time set!"));                                       // 

    } 
  else
    {
    Wifi.println (F("Out of range"));

    char MQtopic[20]; strcpy(MQtopic,MQto); strcat(MQtopic,MQinfo);       //format is e.g.:  "o/8rd000/info"
    Ciao.write(CONNECTOR, MQtopic, inputErr);                             // Err msg to queue
    }
  } 
else
  {

  Wifi.print ("\"");
  Wifi.print (MQdata);
  Wifi.print ("\" ");
  Wifi.println (F("not hh:mm:ss format"));

  char MQtopic[20]; strcpy(MQtopic,MQto); strcat(MQtopic,MQinfo);        //format is e.g.:  "o/8rd000/info" 
  Ciao.write(CONNECTOR, MQtopic, inputErr);                              // Err msg to queue

  }
}




void showSystemTime() 
/*************************************************************************/
/*  Write hh:mm:ss formatted time to INFO output                         */
/*                                                                       */
/*************************************************************************/

{
char humanReadableSystemTimeNow[9] = "";                                 // allocate memory space for time string                                                                
formatDaySecTime(humanReadableSystemTimeNow);                            // have function deliver time string in above memoryspace

Wifi.print(humanReadableSystemTimeNow);                                  // write info message
Wifi.println(F(" is reference time") );                                      
}



void formatDaySecTime (char humanReadableTime[])
/*************************************************************************/
/*  Return daySecTime formatted as hh:mm:ss to caller                    */
/*************************************************************************/  
{

byte daySecTime[2];          // 3 bytes: hh|mm|ss                        // allocate memory space for simple time string      
getSimpleDaySecTime(daySecTime);                                         // have function deliver time string in above memoryspace

sprintf(humanReadableTime,"%02d:%02d:%02d",                              // format the time as human readable hh:mm:ss 
daySecTime[0], daySecTime[1], daySecTime[2]);                            // and place into string

humanReadableTime[9]="\0";                                               // for good measure place field delimiter at end of array

return ;                                                                 // done: there is no more to it
}


void getSimpleDaySecTime (byte simpleDaySecTime[])
/*************************************************************************/
/*  Return daySecTime formatted as three consecutive bytes in            */
/*  a byte array                                                         */
/*************************************************************************/  
{

simpleDaySecTime[0] =  daySecs / 3600;                                   //
simpleDaySecTime[1] = (daySecs - (simpleDaySecTime[0]*3600L))/60;        // calculate Hrs, Mins, Secs from actual day second count
simpleDaySecTime[2] = (daySecs - (simpleDaySecTime[0]*3600L) -           // the "L" indicates that the math is done in long variables
(simpleDaySecTime[1]*60));                                               //

return;                                                                  // done: there is no more to it

}