No problem! Here’s the last version of the code I made specifically for this board. Later versions are more targeted toward my Atmega 1284 based board, using compiler directives to enable/disable features based on the processor it is being loaded to.
/*
Relay and Switch/Button MQTT Controller
Aaron Cake
http://www.aaroncake.net
Runs on Nano, Uno, ESP8266 etc.
Works with ENC28J60, W5100, W5500, ESP WiFi, etc. Any standard Arduino Ethernet library.
Requires PubSubClient found here: https://github.com/knolleary/pubsubclient
Requires MemoryFree found here: https://github.com/McNeight/MemoryFree/
Ethernet: Pins D10, D11, D12, D13 used for Ethernet SPI
Version 1: Intital version with basic functionality
Version 1.5: -change pin init to switch pins off right after pinMode to avoid sequential relay clicking
-change digitalRead in switch state reading loop to only read pin at start of loop and assign to varialble instead of multiple digitalReads
-change switch/button/input state publish to publish state of input instead of just "t" for toggle
-add uptime counter
-add minute heartbeat signal published to /board/tele with uptime, IP, firmware version, free memory
Version 1.75: -pins A6 and A7 on Nano are analog input only! digitalread doesn't work. Create function bool ReadPinState(pin) to wrap
digitalread/analogread and output boolean while handling A6 and A7
Version 1.80: -increase size of HeartBeatPublishString to 100 characters because previous 75 was causing overruns
with long IP addresses leading to weird uptime values from overwritten memory
Version 1.85: -move output pin init to first thing in setup() and write RELAY_OFF to pin before setting pinmode
-this avoids active low relays from activating on boot
Version 1.90 -move strings to PROGMEM to save a crap load of memory
-change JSON generation to sprintf_Pinstead of all the concatinations
Version 2:
-long/short press detection
-rename "switch" variables to "input"
-move more stuff to PROGMEM
-clean up unneeded variables
Too add:
-add RFID reader capability to serial receive
-move build topic to function
-eventually-add web interface for status and on/off control
-DHT sensor?
-rewrite build topic to be function using p_sprintf instead of all the strcats
*/
//set BOARD_TYPE preprocessor directive based on CPU type (Arduino Nano, 1284 board, etc.)
//Atmega168 and Atmega328 (Arduino Nano, Uno)
#if defined(__AVR_ATmega168__) ||defined(__AVR_ATmega168P__) ||defined(__AVR_ATmega328P__)
#define BOARD_TYPE 168328
// Atmega1284 (My custom board)
#elif defined(__AVR_ATmega1284__) || defined(__AVR_ATmega1284P__)
#define BOARD_TYPE 1284
#endif
#include <MemoryFree.h> //library with free memory function
#include <avr/wdt.h> //watch dog timer
#include <PubSubClient.h> //MQTT client
#include <SPI.h> //SPI library for Ethernet, etc. SPI bus
//Ethernet library for ENC28J60
//#include <UIPEthernet.h>
//library for W5500
#include <Ethernet2.h>
//library for W5100
//#include <Ethernet.h>
//function prototype for MQTT callback
void MQTTCallback(char* topic, byte* payload, unsigned int length);
//MQTT sever address
//#define MQTT_SERVER "192.168.107.11"
// define these because some relay boards are active LOW, some active HIGH
#define RELAY_ON LOW
#define RELAY_OFF HIGH
//firmware version
const float FirmwareVersion = 2.0;
const int NumOfInputs = 7; //number of switches above. Starts at zero (0)
const int NumOfRelays = 7; //number of relays above. Starts at zero (0)
//if compiling on Atmega1284
#if BOARD_TYPE == 1284
const byte InputPins[] = { 3,4,5,8,9,28,29,30 }; //my 1284 W5100 board
const byte RelayPins[] = { A0,A1,A2,A3,A4,A5,A6,A7 }; //my 1284 W5100 board
#endif
//if compiling on Atmega168/Atmega328
#if BOARD_TYPE == 168328
const byte InputPins[] = { A0,A1,A2,A3,A4,A5,A6,A7 }; //use analog inputs as switch/button inputs. Pins A0 - A7
const byte RelayPins[] = { 2,3,4,5,6,7,8,9 }; //use digital pins as relay outputs. Pins 0 - 9
#endif
byte LastInputState[] = { LOW,LOW,LOW,LOW,LOW,LOW,LOW,LOW }; //array of bytes to hold last switch state, init to zero
char SwitchStatePaylod[2]; //need a char to hold switch state payload for MQTT publish
//variables and constants for switch check timer
unsigned long PreviousMillis = 0; //the last time the switch check ran
const int InputCheckInterval = 100; //the interval to check switches, milliseconds
//const int SwitchIgnoreTime = 1500; //time period after a switch/button is toggled to ignore another toggle so buttons won't double toggle
//unsigned long SwitchPreviousToggleMillis[] = { 0,0,0,0,0,0,0,0 }; //array for last time switches were toggled. Num of elements must match NumOfSwitches
int InputHoldCounts[] = { 0,0,0,0,0,0,0,0 }; //array to keep track of number of SwitCheckInterval(s) switch is held for. 0 indicates first time through
int InputHoldTime = 0; //integer to store number of mS switch held
const int MaxInputHoldTime = 4000; //maximum amount of time switch is allowed to be held before it is considered a switch and hold ignored
//MQTT related variables
const char MQTTServer[] = "192.168.107.11"; //MQTT server
const char BoardTopic[] PROGMEM = "/tst1284brd/"; // MQTT topic this board should use. Will be used as prefix for communication. ie. BoardTopic + "/outlet1/"
char const* MQTTClientName = "tst1284brd"; //MQTT client name. Keep short, don't waste RAM
const char TelemetryTopic[] PROGMEM = "tele/"; //Suffix added to board topic for telemetry heatbeat
const char BackSlash[] PROGMEM = "/"; //Backslash needed often for strcopy. Save memory by having it once
const char RelayPrefix[] PROGMEM = "r"; //Prefix denoting Relay when building MQTT topic
const char SwitchPrefix[] PROGMEM = "s"; //Prefix denoting Switch when building MQTT topic
const char StateSuffix[] PROGMEM = "_s"; //suffix to ad to topic state message..._s for _state
const char HeldSuffix[] PROGMEM = "held/"; //suffix to add to topic indicating input held
//variables for building topic string for subscription and comparison
char CountString[2];
char BuiltTopic[32];
char BuiltTopicState[35];
char SwitchHeldTimeString[17]; //have to convert the integer SwitchHeldTime to string in order to publish
//uptime counter and heartbeat variables
int UptimeSeconds = 0; //uptime counter seconds, 0-60
int UptimeMinutes = 0; //uptime counter minutes, 0-60
int UptimeHours = 0; //uptime counter hours, 0 - 24
int UptimeDays = 0; //uptime counter days, 0 - maxint...32,000 day uptime? Unlikely
unsigned long UptimePreviousMillis = 0; //the last time the uptime counter updated
bool SendHeartbeat = false; //flag to send the heartbeat. Set true in uptime loop to send a heartbeat
//char IP[16];
char HeartBeatPublishString[100]; //char array to hold heartbeat for MQTT publish, in JSON
//*************************************************
// Make sure to assign a unique MAC to each board!
//*************************************************
//Used:
uint8_t mac[6] = { 0x03, 0xAA, 0x03, 0xFE, 0x01, 0xEE };
//Initialize ethernet client as ethClient
EthernetClient ethClient;
//initialize PubSubClient as "MQTTClient"
PubSubClient MQTTClient(MQTTServer, 1883, MQTTCallback, ethClient);
//function to use the watch dog timer to reboot the processor immediately
void Reboot() {
wdt_disable(); //disable any existing watchdogs
wdt_enable(WDTO_15MS); //enable with a 15MS timeout
for (;;); //loop forever so watchdog will reset processor
}
//function to reset the watchdog timer. Call often
void PetTheDog() {
wdt_reset(); //reset the watchdog timer.
}
//on Nano, A6 and A7 are analog input only. digitalRead doesn't work. So this wrapper function is used to read the input pins
//and outputs a HIGH or LOW while handling the analog pins
int ReadPinState(int PinToRead) {
if (PinToRead == A6 || PinToRead == A7) { //if we are reading from the special analog pins A6 and A7 (dedicated ADC/DAC)
//Serial.println(analogRead(PinToRead));
if (analogRead(PinToRead) < 500) { //pin high reads about 1023, pin low reads about 10
return LOW; //so just return the correct value based on analog read
}
else {
return HIGH;
}
}
else {
return digitalRead(PinToRead); //otherwise just digitalRead and pass the value to the function
}
}
void setup() {
//wdt_enable(WDTO_8S); //enable the watchdog timer for 8 seconds
PetTheDog(); //pet the watchdog to reset the timer
//initialize all the pins used
//loop through and set all the rely outputs as outputs, then turn them off
for (int i = 0; i <= NumOfRelays; i++) {
digitalWrite(RelayPins[i], RELAY_OFF); //some relays are active low so before any pin setting, write low to the pin to make sure they don't activate
pinMode(RelayPins[i], OUTPUT); //set the pin modes for relay pins to output
digitalWrite(RelayPins[i], RELAY_OFF); //turn them all off
PetTheDog(); //pet the watchdog to reset the timer
}
//open the serial port for debugging
Serial.begin(9600);
delay(100);
Serial.println(F("BOOT..."));
// loop through and set all the switch/button inputs as inputs, then read each one current status and fill the array
for (int i = 0; i <= NumOfInputs; i++) {
pinMode(InputPins[i], INPUT);
Serial.print(F("PIN: Set input: "));
Serial.println(InputPins[i]);
LastInputState[i] = ReadPinState(InputPins[i]); //now read current pin state and update element in LastSwitchState. Because it could have rebooted
Serial.print(F("PIN: Read input: "));
Serial.print(InputPins[i]);
Serial.print(F(", State: "));
Serial.println(LastInputState[i]);
PetTheDog(); //pet the watchdog to reset the timer
}
// start the Ethernet and obtain an address via DHCP
Serial.println(F("ETHERNET: Begin..."));
if (Ethernet.begin(mac) == 0) {
Serial.println(F("ETHERNET: FAIL. No DHCP."));
delay(5000);
//Reboot();
}
Serial.print(F("ETHERNET: Success. IP Addresss: "));
Serial.println(Ethernet.localIP());
//attempt to connect to MQTT broker
ConnectToMQTTBroker();
//wait a bit before starting the main loop
PetTheDog(); //pet the watchdog to reset the timer
delay(2000);
}
//function to manage the uptime
void UptimeCounter() {
UptimeSeconds++; //increase uptime seconds by 1
if (UptimeSeconds >= 60) { //if seconds are greater than 60 that's a minute
UptimeMinutes++; //increase minutes by 1
UptimeSeconds = 0; //reset seconds to zero
SendHeartbeat = true; //set the flag to send the heartbeat publish
}
if (UptimeMinutes >= 60) { //if minutes > 60 that's an hour
UptimeHours++; //increase hour count
UptimeMinutes = 0; //reset minutes to zero
}
if (UptimeHours >= 24) {
UptimeDays++;
UptimeHours = 0;
}
if (SendHeartbeat == true) { //if the heartbeat flag is set (on minute changeover), send heartbeat
Heartbeat();
SendHeartbeat = false; //set the flag false to it doesn't run until next minute
}
//Serial.print(F("Uptime (DD:HH:MM:SS): ")); //print out uptime every time function called
//Serial.print(UptimeDays);
//Serial.print(F(":"));
//Serial.print(UptimeHours);
//Serial.print(F(":"));
//Serial.print(UptimeMinutes);
//Serial.print(F(":"));
//Serial.println(UptimeSeconds);
}
//function to generate heartbeat ping on topic /boardname/tele/
void Heartbeat() {
//ArduinoJSON exists as a library to generate and serialise JSON. Not doing anything complicated so can do it in less memory manually
//Bunch of sprintfs/strcats to build the heartbeat string.
//create an uptime in ISO 8601 format:
/*P is the duration designator(for period) placed at the start of the duration representation.
Y is the year designator that follows the value for the number of years.
M is the month designator that follows the value for the number of months.
W is the week designator that follows the value for the number of weeks.
D is the day designator that follows the value for the number of days.
T is the time designator that precedes the time components of the representation.
H is the hour designator that follows the value for the number of hours.
M is the minute designator that follows the value for the number of minutes.
S is the second designator that follows the value for the number of seconds.
For example, "P3Y6M4DT12H30M5S" represents a duration
of "three years, six months, four days, twelve hours, thirty minutes, and five seconds".*/
//generate JSON string with sprintf_p . Format string stored in PROGMEM
sprintf_P(HeartBeatPublishString, PSTR("{\"ip\":\"%d.%d.%d.%d\",\"uptime\":\"P%dDT%dH%dM%dS\",\"ver\":\"%d.%02d\",\"freemem\":\"%d\"}"),
Ethernet.localIP()[0], Ethernet.localIP()[1], Ethernet.localIP()[2], Ethernet.localIP()[3],
UptimeDays, UptimeHours, UptimeMinutes, UptimeSeconds,
(int)FirmwareVersion, (int)(FirmwareVersion * 100) % 100, //sprintf_p doesn't do floats, so need to convert it to two ints to retain decimal
(int)freeMemory());
Serial.print(F("HEARTBEAT: ")); //print it out to the serial for debugging
Serial.println(HeartBeatPublishString);
//build the MQTT topic to publish
strcpy_P(BuiltTopic, BoardTopic);
strcat_P(BuiltTopic, TelemetryTopic); //add the telemetry topic to board topic...ie. /board/tele/
Serial.print(F("MQTT: Heartbeat publish: "));
Serial.println(BuiltTopic);
MQTTClient.publish(BuiltTopic, HeartBeatPublishString);
}
void loop() {
PetTheDog(); //pet the watchdog to reset the timer
unsigned long CurrentMillis = millis(); //get the current time count
byte SwitchStateRead = 0; //hold the switch state read in the loop for processing, avoids multiple digitalreads
//reconnect if connection is lost
if (!MQTTClient.connected()) { ConnectToMQTTBroker(); }
//maintain MQTT connection
MQTTClient.loop();
if (CurrentMillis - UptimePreviousMillis >= 1000) { //if 1 second (1000ms) has passed since last run, increase the uptime
UptimePreviousMillis = CurrentMillis; //update the time uptime was last updated
UptimeCounter(); //run the uptime function
}
//if the difference between the current time and last time is bigger than the interval, time to check the switches
if (CurrentMillis - PreviousMillis >= InputCheckInterval) {
PreviousMillis = CurrentMillis; //update the time the check last ran
//Serial.print(F("Check switches: "));
for (int i = 0; i <= NumOfInputs; i++) { //loop through all the switch/button inputs
PetTheDog(); //pet the watchdog to reset the timer
//Serial.println(SwitchPins[i]);
SwitchStateRead = ReadPinState(InputPins[i]); //read from the input and assign to SwitchStateReadTemp
if (SwitchStateRead != LastInputState[i]) { //if the input is different then it was last time, a switch/button has been toggled/pressed
//if (CurrentMillis - SwitchPreviousToggleMillis[i] >= SwitchIgnoreTime) { //if the switch hasn't togged within SwitchIgnoreTime
//SwitchPreviousToggleMillis[i] = CurrentMillis; //update the last time the switch toggled
//LastSwitchState[i] = SwitchStateRead; //update last switch state
if (InputHoldCounts[i] == 0) {
Serial.print(F("PIN: Switch chnaged: "));
Serial.println(InputPins[i]);
}
InputHoldCounts[i]++; //add 1 to hold count, counting number of cycles switch is held for
InputHoldTime = (InputCheckInterval * InputHoldCounts[i]); //multiply the interval at which switch is checked by number of checks to obtain mS switch was held for
//If the hold count is lager than the max configured hold count, it is a switch or another input with long lasting states
//Can zero out the counter and set the last state to the current state so hold count not sent and ready for next state change
if (InputHoldTime >= MaxInputHoldTime) {
InputHoldCounts[i] = 0; //zero out held counts
LastInputState[i] = SwitchStateRead; //set the last state to current state to prepare for next state change
Serial.print(F("PIN: Hold ingored. Max time exceeded: "));
Serial.println(InputPins[i]);
}
//if HoldCount is 1, first time the loop has gone through so publish the state
if (InputHoldCounts[i] == 1) {
//pin state has changed, so publish state immediately
//build the topic string to publish
sprintf_P(CountString, PSTR("%d"), i); //convert i to char
strcpy_P(BuiltTopic, BoardTopic); //do a strcopy to place BoardTopic in SubTopic
strcat_P(BuiltTopic, SwitchPrefix); //add the switch prefix (probably s)
strcat(BuiltTopic, CountString); // concatinate the i value after switch prefix. ex. /s1
strcat_P(BuiltTopic, BackSlash); //and add the backslash
sprintf_P(SwitchStatePaylod, PSTR("%d"), SwitchStateRead); //need to convert switch state byte reading to char for MQTT publish
MQTTClient.publish(BuiltTopic, SwitchStatePaylod); //publish a T for Toggle
Serial.print(F("MQTT: Published: "));
Serial.print(BuiltTopic);
Serial.println(SwitchStateRead);
}
}
//the switch has been released if SwitchStateRead is equal to where it has started and some number of SwitchHoldCounts has happened
if ((SwitchStateRead == LastInputState[i]) && (InputHoldCounts[i] >= 1)) {
//publish the switch state
//build the topic string to publish
sprintf_P(CountString, PSTR("%d"), i); //convert i to char
strcpy_P(BuiltTopic, BoardTopic); //do a strcopy to place BoardTopic in SubTopic
strcat_P(BuiltTopic, SwitchPrefix); //add the switch prefix (probably s)
strcat(BuiltTopic, CountString); // concatinate the i value after switch prefix. ex. /s1
strcat_P(BuiltTopic, BackSlash); //and add the backslash
sprintf_P(SwitchStatePaylod, PSTR("%d"), SwitchStateRead); //need to convert switch state byte reading to char for MQTT publish
MQTTClient.publish(BuiltTopic, SwitchStatePaylod); //publish a T for Toggle
Serial.print(F("MQTT: Published: "));
Serial.print(BuiltTopic);
Serial.println(SwitchStateRead);
//now publish the inverval for how long the switch was toggled
sprintf_P(SwitchHeldTimeString, PSTR("%d"), InputHoldTime); //convert it to cstring
strcat_P(BuiltTopic, HeldSuffix); //add "held" to MQTT topic to indicate switch hold time
MQTTClient.publish(BuiltTopic, SwitchHeldTimeString); //publish the held time via MQTT
Serial.print(F("MQTT: Published: "));
Serial.print(BuiltTopic);
Serial.println(SwitchHeldTimeString);
//clean up variables
InputHoldCounts[i] = 0; //zero out switch hold counts
}
}
}
}
void MQTTCallback(char* topic, byte* payload, unsigned int length) {
PetTheDog(); //pet the watchdog to reset the timer
//MQTTCallback is fired whenever PubSubClient (MQTTClient) receives an MQTT message from MQTT_SERVER
//Note: the "topic" value gets overwritten everytime it receives confirmation (callback) message from MQTT
//Print out some debugging info
Serial.print(F("MQTT: MQTT callback. Topic: "));
Serial.println(topic);
//loop through the array of relays and build the topic to look for
for (int i = 0; i <= NumOfRelays; i++) {
PetTheDog(); //pet the watchdog to reset the timer
sprintf_P(CountString, PSTR("%d"), i); //convert i to char
strcpy_P(BuiltTopic, BoardTopic); //do a strcopy to place BoardTopic in BuiltTopic
strcat_P(BuiltTopic, RelayPrefix); //add the relay prefix (probably r)
strcat(BuiltTopic, CountString); // concatinate the i value after prefix. ex. /r1
strcpy(BuiltTopicState, BuiltTopic); //copy Built topic into BuiltTopic State
strcat_P(BuiltTopic, BackSlash); //and add the backslash
strcat_P(BuiltTopicState, StateSuffix); //add the status/state suffix (ex. "_s") the end. ex. /board/topic1_s
strcat_P(BuiltTopicState, BackSlash); //and add the backslash
Serial.print(F("Checking topic: "));
Serial.println(BuiltTopic);
// if the topic is found, strcmp returns 0
if (strcmp(topic, BuiltTopic) == 0) { //found the topic so position in RelayPins array is known
Serial.println(F("PIN: Changing output for topic: "));
Serial.print(BuiltTopic);
if (payload[0] == '1') { //payload is 1 so need to turn the output ON
digitalWrite(RelayPins[i], RELAY_ON); //turn on the output
Serial.print(F("PIN: Set Output: "));
Serial.print(RelayPins[i]);
Serial.print(F(", State: "));
Serial.println(RELAY_ON);
MQTTClient.publish(BuiltTopicState, "1"); //publish payload 1 indicating RelayPin[i] is on
Serial.print(F("MQTT: Published state: "));
Serial.println(BuiltTopicState);
}
if (payload[0] == '0') { //payload is 0 so need to turn the output OFF
digitalWrite(RelayPins[i], RELAY_OFF); //turn off the output
Serial.print(F("PIN: Set Output: "));
Serial.print(RelayPins[i]);
Serial.print(F(", State: "));
Serial.println(RELAY_OFF);
MQTTClient.publish(BuiltTopicState, "0"); //publish payload 0 indicating RelayPin[i] is on
Serial.print(F("MQTT: Published state: "));
Serial.println(BuiltTopicState);
}
}
}
}
void ConnectToMQTTBroker() {
PetTheDog(); //pet the watchdog to reset the timer
// Loop until connected to MQTT broker
while (!MQTTClient.connected()) {
PetTheDog(); //pet the watchdog to reset the timer
Serial.print(F("MQTT: "));
Serial.print(MQTTServer);
Serial.println(F(": Attempting MQTT Connection..."));
//if connected, subscribe to the relay topics as BoardTopic/relay number
if (MQTTClient.connect((char*)MQTTClientName)) {
Serial.println(F("MQTT: Connected."));
//loop through and subscribe to relays
for (int i = 0; i <= NumOfRelays; i++) {
sprintf_P(CountString, PSTR("%d"), i); //convert i to char.....
strcpy_P(BuiltTopic, BoardTopic); //do a strcopy to place BoardTopic in SubTopic
strcat_P(BuiltTopic, RelayPrefix); //add the relay prefix (probably r)
strcat(BuiltTopic, CountString); // concatinate the i value onto SubTopic
strcat_P(BuiltTopic, BackSlash); //and add the backslash
MQTTClient.subscribe(BuiltTopic); //subscribe to the topic.
Serial.print(F("MQTT: Subscribe: "));
Serial.println(BuiltTopic);
PetTheDog(); //pet the watchdog to reset the timer
}
}
//otherwise print failed for debugging
else {
PetTheDog(); //pet the watchdog to reset the timer
Serial.print(F("MQTT: Subscribe failed. Retry in 5 seconds. Error: "));
Serial.println(MQTTClient.state());
delay(5000);
}
}
}
A few notes:
- I know there is repeated code for build the MQTT topic. It actually takes less RAM to use the copy & paste code vs. a function. However I’m going to switch to a function as a few bytes aren’t an issue, especially on my new 1284 board with 16K of RAM.
- Enable the watchdog function at your peril because lots of Arduinos (especially Nano clones) have a flawed bootloader which puts the processor into a loop if triggered. To fix, load the newest Optiboot.
- Code is button/switch agnostic. It’s up to you to decide how to use the MQTT outputs via automations.
- Don’t use ENC28J60 Ethernet chipsets. They suck. Plus the libraries needed use too much RAM for this code.
- Yes, I publish in the MQTT callback. This is not recommended in the PubSubClient documentation because of variable re-use in the library. Hasn’t caused issues so far, but fixing in later versions.