// 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 }