Arduino Nano W5500 Ethernet MQTT Based 8 Channel Opto-Isolated I/O Board

Figured I’d shared these boards I designed which form the basis of the I/O side of my Home Assistant installation. I wanted a board which provided simple I/O to control switches and relays using MQTT and connected via Ethernet. There just wasn’t anything out there that fit all my requirements (the big one being the ability to run my own firmware) so I designed these.

The board uses an Arduino Nano as the controller. A socket is provided to plug in one of the small W5500 modules commonly available on eBay/Aliexpress. The socket just breaks out the SPI bus with additional power and ground.

At the top left is a small power supply section for an optional 7805 linear regulator if the board is to be powered from a non-5V source.

The real utility are the input terminals on the right side. There are 8 active-high opto-isolated inputs which can be connected to any on/off input such as a button, switch, door contact, motion sensor, relay contacts, etc. The firmware loops through all the inputs every 50 mS and then simply reports their state back via MQTT if the state has changed. Additionally it sends a hold time in mS if the state changes again within a pre-determined timeout. In this way it supports button press duration detection for different actions based on long, short, etc. press. Being opto-isolate, then have 1000V isolation in case of surges and are very noise immune. No “ghost switching”.

At the bottom are 8 digital outputs, some of them supporting PWM, to drive relays. If I need to control something else such as a WS2812 LED strip, then that just gets compiled into the firmware and uses one of the output pins.Yeah, the Ethernet card sort of covers them so I just bend them out a few degrees. Also the serial port is broken out (to the left of the output pins) to provide debugging serial output, and serial input from things such as RFID readers.

I use one or more of these per room to provide all the basic I/O needed, the supplement with things such as other sensors RGB bulbs as required. So far, these boards have been incredibly reliable. I’ve never needed to reboot one and telemetry reports uptimes of 280+ days on the boards I haven’t reset for one reason or another (adding lights, etc.)

As I am slowly gutting every room of my house, each room gets a master control box where all conduits lead that contains one or more of these boards.

Seems that 95% of home automation comes down to taking a user input from a button or switch, then controlling one or more relays. :slight_smile: So most of my control runs through these boards.

But, I’m bumping into limits in memory using the Arduino Nano. Start hanging sensors off of them like BMP280 (or…ugh…DHT22) and an RFID reader and the poor Nano is out of RAM and almost out of program space. So I’ve designed a new board, which is another topic entirely. :slight_smile:

2 Likes

Do you mind sharing your code ? I do have something very similar with some Arduino Uno and w5500 shield !

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:

  1. 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.
  2. 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.
  3. Code is button/switch agnostic. It’s up to you to decide how to use the MQTT outputs via automations.
  4. Don’t use ENC28J60 Ethernet chipsets. They suck. Plus the libraries needed use too much RAM for this code.
  5. 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.

Thank you very much, I will try to adapt it to my code as I have a very similar setup this board https://www.amazon.ca/-/fr/Keyestudio-Ethernet-Shield-pour-Arduino/dp/B079G4WDGW/ref=sxts_sxwds-bia-wc-drs-ajax1_0?cv_ct_cx=w5500&dchild=1&keywords=w5500&pd_rd_i=B079G4WDGW&pd_rd_r=2e6aca41-87b4-4507-8eb9-34a2056a0317&pd_rd_w=2Fhmv&pd_rd_wg=0Cl16&pf_rd_p=cfd4fdec-168d-4847-841c-70aebccfd5db&pf_rd_r=TZ6A9MXQ0SYNB71Q9498&psc=1&qid=1602523205&sr=1-1-776e1b88-8838-403c-9ba3-fc5054845b7e
And it lack memory or my code is poorly optimize (or both XD)
As I already have buy a few of them (I should have wait)
I’m trying everything I know like Progmem but still having a few issues.

Again ! Thank you

I use those as well both commercially and personally because they are great compact package with the Atmega 328 and W5500 Ethernet.

I tried to make my code as memory friendly as possible by doing things like dynamically generating MQTT topics out of PROGMEM. It will definitely run on the Atmega328 w/W5500 leaving about 1K of free memory. I’m actually going to rewrite the MQTT topic generation in a future version to use sprintf_P and perform the generation in a function to save a few more bytes of RAM and eliminate the copy-paste code.

Can you keep me in the loop ? I’m really attacking this project since all the wiring in my new(old) house are in all the light switch
But I’m struggling to make this work because of the memory issue (my code is badly coded or the chip doesn’t have enough space :p)
As soon as I arrive at home I will show you what I got and what I am trying to do!

Looking forward to see the new code (any GitHub that I can take a look?)

Here is my code
Its reading my 4 dht22 every 10 secondes (and probably stalling my code …)
It.wait for button input (also 4, 2.room main switch and 2 closet)
And publish the information on the mqtt broker
It also subscribe to light topic and relay topic
To turn on the light and the electrical heating in each room

It seams that I am at the limit of the mcu … I’m using 78% ram and 88% flash (reported by platforming)
The button are not always working. And I would like to have the topic real name … And not what you see something like
Chambre/Alexis/main/light/status

#include <OneButton.h>

#include <DHT.h>
#include <PubSubClient.h>
#include <Ethernet.h>

//Configuration //
//#define Enable_Dhcp  true                 // true/false
//IPAddress ip(192, 168, 1, 105);           //Static Adress if Enable_Dhcp = false

//Static Mac Address
static uint8_t mac[] = { 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0x01 };  // s/a
#define DHTTYPE DHT22
#define DHTPIN0  A0 //alexis main dht22
#define DHTPIN1  A1 //alexis closet dht22
#define DHTPIN2  A2 //samuel main dht22
#define DHTPIN3  A3 //samuel Closet dht22
OneButton button1(A4, false, false); //Alexis main light switch
OneButton button2(2, false); //Alexis closet light switch
OneButton button3(A5, false); //Samuel main light switch
OneButton button4(3, false); //Samuel closet light switch

//pinout A0-A5  2-9
const int sendDhtInfo = 10000;    // Dht22 will report every X milliseconds.
unsigned long lastSend = 0;
const char* dhtPublish[] = { "c/a/clim/main/", "c/a/clim/closet/", "c/s/clim/main/", "c/s/clim/closet/" };

const int output_pin[6] = { 4, 5, 6, 7, 8, 9 }; //Relay Pinout turn on/off light et chauffage
//4=l main a
//5=l garde-robe a
//6=chauffage a
//7=l main s
//8=l garde-robe s
//9=chauffage s
const char* subscribeRelay[] = { "c/a/l/main/status/", "c/a/l/closet/status/",
                                 "c/a/heat/main/status/", "c/s/l/closet/status/",
                                 "c/s/l/main/status/", "c/s/l/closet/status/"
                               };

const char* inputPublish[] = { "c/a/l/main/simple/", "c/a/l/main/double/", "c/a/l/main/long/",
                               "c/a/l/closet/simple/","c/a/l/closet/double/","c/a/l/closet/long/",
                               "c/s/l/main/simple/", "c/s/l/main/double/", "c/s/l/main/long/",
                               "c/s/l/closet/simple/","c/s/l/closet/double/","c/s/l/closet/long/" 
                               };

// MQTT Settings //
const char* broker = "ubuntu.jaune.lan"; // MQTT broker
//#define mqttUser "USERNAME"         //Username for MQTT Broker
//#define mqttPassword "PASS"       //Password for MQTT Broker

DHT dht[] = { { DHTPIN0, DHTTYPE }, { DHTPIN1, DHTTYPE }, { DHTPIN2, DHTTYPE }, { DHTPIN3, DHTTYPE } }; // { DHTPIN4, DHTTYPE }, { DHTPIN5, DHTTYPE } };

EthernetClient ethclient;
PubSubClient client(ethclient);

void callback(char* topic, byte* payload, unsigned int length)
{
  Serial.println("Callback");
  Serial.println(topic);
  byte output_number = payload[0] - '0';

  for (int i = 0; i < sizeof(subscribeRelay) / sizeof(subscribeRelay[0]); i++)
  {
    int strcomparison = strcmp(topic, subscribeRelay[i]);
    if (strcomparison == 0)
    {
      if (output_number == 1)// || ((char)payload[0] == '1'))
      {
        digitalWrite(output_pin[i], HIGH);
      }
      if (output_number == 0)
      {
        digitalWrite(output_pin[i], LOW);
      }
    }

  }
}

void reconnect() {
  while (!client.connected()) {
    char clientBuffer[40];

    String clientString = "ip : "+ String(Ethernet.localIP()[0]) + "." + String(Ethernet.localIP()[1]) + "." + String(Ethernet.localIP()[2]) + "." + String(Ethernet.localIP()[3]);
    clientString.toCharArray(clientBuffer, clientString.length() + 1);
    if (client.connect(clientBuffer)) 
    {
      client.publish("arduino/chambre/ip/", clientBuffer);

      for (int i = 0; i < sizeof(subscribeRelay) / sizeof(subscribeRelay[0]); i++)
      {
        client.subscribe(subscribeRelay[i]);
      }
    }
  }
}
void setup()
{
  Serial.begin(115200);
  // if (Enable_Dhcp == true)
  // {
    while (!Ethernet.begin(mac))  { }

  enable_and_reset_all_outputs(); //Reset and Set all pin on OUTPUT mode

  client.setServer(broker, 1883);
  client.setCallback(callback);
  for (int i = 0; i < sizeof(dhtPublish) / sizeof(dhtPublish[0]); i++)
    {
      dht[i].begin();
    }

  button1.attachClick(click1);
  button1.attachDoubleClick(doubleclick1);
  //button1.attachLongPressStart(longPressStart1);
  //button1.attachLongPressStop(longPressStop1);
  //button1.attachDuringLongPress(longPress1);

  button2.attachClick(click2);
  button2.attachDoubleClick(doubleclick2);
  //button2.attachLongPressStart(longPressStart2);

   button3.attachClick(click3);
   button3.attachDoubleClick(doubleclick3);
   //button3.attachLongPressStart(longPressStart3);

   button4.attachClick(click4);
   button4.attachDoubleClick(doubleclick4);
   //button4.attachLongPressStart(longPressStart4);

}

void loop()
{
  if (!client.connected())
  {
    reconnect();
  }

  if (millis() - lastSend > sendDhtInfo)
  {
    readDHT();
    lastSend = millis();
  }
  
  button1.tick();
  button2.tick();
  button3.tick();
  button4.tick();

  client.loop();
}

void readDHT()
{
  for (int i = 0; i < sizeof(dhtPublish) / sizeof(dhtPublish[0]); i++)
  {
    float temperature = dht[i].readTemperature();
    float humidity = dht[i].readHumidity();
    //float heatindex;
    if(isnan(humidity) || isnan(temperature))
    {
      temperature = 100.0f;
      humidity = 100.0f;
     // heatindex = 100.0f;
    }
    else
    {
    // heatindex = dht[i].computeHeatIndex(temperature,humidity,false);
    }

    String payload = "{";
    payload += "\"temperature\":"; payload += String(temperature).c_str(); payload += ",";
    payload += "\"humidity\":"; payload += String(humidity).c_str(); payload += ",";
   // payload += "\"heatindex\":"; payload += String(heatindex).c_str();
    payload += "}";

    // Send payload
    char attributes[100];
    payload.toCharArray(attributes, (payload.length() + 1));
    client.publish(dhtPublish[i], attributes);

  }
}

void enable_and_reset_all_outputs()
{
  for (int i = 0; i < sizeof(subscribeRelay) / sizeof(subscribeRelay[0]); i++)
  {
    pinMode(output_pin[i], OUTPUT);
    digitalWrite(output_pin[i], LOW);
  }
}


//Boutton1
void click1() 
{
  client.publish("c/a/l/main/simple/", "1");
} 
void doubleclick1() 
{
  client.publish("c/a/l/main/double/", "1");
} 
void longPressStart1() 
{
  client.publish("c/a/l/main/long/", "1");
} 


//Boutton2
void click2() 
{
  client.publish("c/a/l/closet/simple/", "1");
} 
void doubleclick2() 
{
  client.publish("c/a/l/closet/double/", "1");
} 
void longPressStart2() 
{
  client.publish("c/a/l/closet/long/", "1");
} 

//Buton3
void click3() 
{
  client.publish("c/s/l/closet/simple/", "1");
} 
void doubleclick3() 
{
  client.publish("c/s/l/closet/double/", "1");
} 
void longPressStart3() 
{
  client.publish("c/s/l/closet/long/", "1");
} 

//buton4
void click4() 
{
  client.publish("c/s/l/closet/simple/", "1");
} 
void doubleclick4() 
{
  client.publish("c/s/l/closet/double/", "1");
} 
void longPressStart4() 
{
  client.publish("c/s/l/closet/long/", "1");
} 

After a quick glance, I’ve found several issues which will account for your memory use button responsiveness issues:

  • Anytime you explicitly define char arrays in quotes like “this is my array!”, that data needs to be copied to RAM anytime the processor needs it. So each one of those cstrings in subscribeRelay, inputPublish, etc. are taking up lots of memory when you access the variable. You can save this by placing them into PROGMEM and using the _P equivelants of functions to manipulate them (see my code)
  • Same thing everytime you do a Serial.println(“Insert my cstring char array here”) . That cstring needs to be moved to RAM first. Use the F("") macro to print these cstrings out of PROGMEM like Serial.println(F(“This string is being printed right from PROGMEM to the serial port so it can be as long as I want and it won’t eat RAM!!!”))
  • Never, ever, use String. The String class makes it easier to work with characters as a string rather than a cstring, but it completely fragments memory, leaving memory unusable for other operations, eventually running the processor out. Move these to cstrings and then use functions like strcmp, strcat to manipulate (see my code)
  • You definitely don’t need to read 4 DHT sensors every 10 seconds and publish the data. It can take up to about 750mS to read one DHT sensor, and they are so slow that in 10 seconds you won’t see much of a response. Depending on what you are measuring (a room, for example) it might not be possible for it to radically change temperature in 10 seconds unless something crazy happens like an explosion or a door is opened wide to a windy cold exterior. You could read one sensor per minute over a 4 minute period and publish as each is read.
  • It is definitely the DHT sensors read that is causing your button issues. You call readDHT() which can take up to a few seconds for all those sensors, so button.tick() which happens after the blocking readDHT() is missing button presses. It needs to be called in loop() as often as possible
  • Anytime you client.publish(“string/here/character/array”) everything between the quotes needs to be copied to RAM. As above, store these in PROGMEM and use publish_P
  • Instead of defining all the topics as seperate cstrings/strings, dynamically generate them like I do in my code.

is there a way for a arduino uno+ethernet shield +4 relays!???

Look at my code posted Sept 20: Arduino Nano W5500 Ethernet MQTT Based 8 Channel Opto-Isolated I/O Board .

This will run on an UNO with an Ethernet shield.

You just need to adjust the InputPins, RelayPins and NumOfInputs / NumOfRelays for your application.

this happens!!!no good for arduino uno!?

Please post the error message, not a picture of it.

error
to compile arduino uno

In your picture, I see two Adafruit Circuit Playground libraries included in the sketch.

These are not part of my code.

Start with a totally blank sketch and paste my code in.

Then make the changes for your pin numbers and appropriate Ethernet shield (uncomment the correct library).

ok,i manage to put the sketch to arduino!!!and now where can i see it?is there a interface!!!or the home assistant can find it!?

The program will obtain an IP address via DHCP. If successful, it will connect to the MQTT server defined here:

//MQTT related variables
const char MQTTServer[] = "192.168.107.11";

It will subscribe to the topics /(BoardTopic)/r(relay number)/

Publish a 1 to /(BoardTopic)/r(relay number)/ to turn the relay ON, publish 0 to turn it OFF

When a relay is changed the state will be reported as /(BoardTopic)/r(relay number)_s/

(relay number) represents the position in the RelayPins[] array.

When you change one of the InputPins[], it will publish /BoardTopic/s(input number)/ with a payload of 1 for high, 0 for low.

Additionally you will get /BoardTopic/s(input number)/held/ when the input changes state again to show hold time in mS the input was held. There is a hold time timeout of 4 seconds. After 4 seconds, no held time will be published.

There is no user interface. All interaction takes place via MQTT and configuration at time of compile.

In Home Assistant, you configure your entities manually.

For example, here is a relay output configured as a light:

light:
  - platform: mqtt
    name: "Shop Light South"
    state_topic: "/shop/r7_s/"
    command_topic: "/shop/r7/"
    payload_on: "1"
    payload_off: "0"
    qos: 0
    retain: true

Here is an input as a sensor which exposes hold time:

sensor shopbuttons:
 - platform: mqtt
   state_topic: "/shop/s0/held/"
   name: "Shop Button 0"

Here is an automation which toggles on a bunch of lights for a held state of 1 - 2.2 seconds:

- id: '888a82222900846484947332344'
  alias: Toggle All Shop Lights 1 Sec Press Fab Light Button (Entry Timer Idle)
  trigger:
  - platform: mqtt
    topic: /shop/s4/held/
  condition:
  - condition: numeric_state
    entity_id: sensor.shop_button_4
    above: 1000
    below: 2200  
  action:
  - data:
      entity_id:
        - light.shop_fab_light
        - light.shop_light_north
        - light.shop_light_south
    service: light.toggle

Every minute telemetry with uptime, IP, free memory, version is sent out on /BoardTopic/tele/ encoded in JSON.

Lots of debug information is available via the serial port at 9600 baud.

1 Like

Will you post your code on GitHub ?

I will not be posting my code on GitHub as I only host my content on my website (with the exception of YouTube and forum posts). I don’t believe in large monopolies (ie. Instructables, GitHub, etc.) gobbling up content. Currently I am working on a large writeup for my website which covers my entire automation setup including this code. It’s a big project, and has taken a while but is about 85% done.

However, for the time being if you look at the post from Sept 2020 ( Arduino Nano W5500 Ethernet MQTT Based 8 Channel Opto-Isolated I/O Board - #3 by AaronCake ) you will find an older stable version of this code that allows MQTT control of inputs and outputs.

My code uploads successfully However in the serial monitor the arduino hangs on:

ETHERNET: Begin…

I have waited over 5 minutes and it just hangs.

Any suggestions?

Cheers
Jake

Are you using the correct library for your Ethernet module? Ethernet2 for W5500, Ethernet for W5100.

Does your Ethernet module work? Try using the DHCP Printer example that comes with the Arduino IDE.

Do you see a link light on the Ethernet module?

Assuming your DHCP server is healthy and you have a link, if everything is working obtaining a DHCP address should be a second or two at most.