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

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.