Clockwise from top: USB3 externally-powered hub, RS-485 dongle, RS-485 to TTL serial PCB and ESP-12E ESP8266 NodeMCU module |
Modbus TCP/IP to RS-485 passthrough gateway for RM25.
Part 1 describes an ESP-12E Modbus RTU Master using RS-485 interface. It can read and write RS-485 Modbus devices but it uses the ESP-12E debug serial port to do so. This works if the ESP-12E itself is the host controller, but Modbus masters usually have a lot more horsepower.
To be really useful, we can also make the ESP-12E a TCP/IP Modbus Slave. It still can work as a host, but this will make it a Modbus TCP/IP to RS485 "passthrough" gateway. A real Modbus host, say a desktop or Industrial PC can then orchestrate a whole bunch of Modbus devices to control a whole buildings' services.
A real TCP/IP to RS-485 Modbus gateway is some RM680 |
Of course the ESP-12E is not as powerful as a regular Modbus TCP/IP to RS485 passthrough but at RM25 it is an ideal way of retrofitting IoT functionality to Modbus devices.
If we replace the desktop with a cloud-based server we can scale |
Then I expanded the sketch in Part 1:
#include <ESP8266WiFi.h>
#include <ModbusSlaveTCP.h>
const char* ssid = "YourAccessPoint";
const char* pass = "StrongPassword";
IPAddress staticIP(10,0,0,100);
IPAddress gateway(10,0,0,1);
IPAddress subnet(255,255,255,0);
// slave id = 1, rs485 control-pin = 8, baud = 9600
#define SLAVE_ID 1
// Modbus object declaration
ModbusTCP slave(SLAVE_ID);
#include <ModbusMaster232.h>
#include <SoftwareSerial.h> // Modbus RTU pins D7(13),D8(15) RX,TX
// MAX485 half duplex control lines
#define not_RE 14 // D5. Enable receiver, active low
#define DE 12 // D6 Enable Transmitter, active high
// Instantiate ModbusMaster object as slave ID 1
ModbusMaster232 node(1);
void setup() {
pinMode(not_RE, OUTPUT);
pinMode(DE, OUTPUT);
// default to transmit mode to reduce noise
digitalWrite(not_RE, HIGH); // disable receiver
digitalWrite(DE, HIGH); // enable transmitter
Serial.begin(9600);
delay(100);
node.begin(9600); // Modbus RTU
delay(100);
/* Connect WiFi to the network
*/
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.disconnect();
WiFi.hostname("ModbusTCPslaveRS485master");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, pass);
WiFi.config(staticIP, gateway, subnet); // Static IP. Not required for dhcp
int wifi_loop = 0;
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
/*
if (wifi_loop++ == 10)
{
Serial.println("Reconnecting ...");
WiFi.disconnect();
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, pass);
delay(1000);
wifi_loop = 0;
}
*/
}
Serial.println(WiFi.localIP());
/* register handler functions
* into the modbus slave callback vector.
*/
slave.cbVector[CB_WRITE_COIL] = writeDigitlOut;
slave.cbVector[CB_READ_DISCRETE_INPUT] = readDigitalIn; //
slave.cbVector[CB_READ_COILS] = readDigitalIn;
slave.cbVector[CB_READ_REGISTERS] = readAnalogIn;
slave.cbVector[CB_WRITE_MULTIPLE_REGISTERS] = writeAnalogOut; // cmheong
/* start slave and listen to TCP port 502
*/
slave.begin();
// log to serial port
Serial.println("");
Serial.print("Modbus ready, listen on ");
Serial.print(WiFi.localIP());
Serial.println(" : 502");
}
int loop_i = 0;
uint16_t readDiscreteInputs[10];
int Mdelay = 10; // from 5
void loop() {
/*
node.readDiscreteInputs(loop_i, 1);
readDiscreteInputs[loop_i] = node.getResponseBuffer(0);
node.clearResponseBuffer();
Serial.print("[");
Serial.print(loop_i);
Serial.print("] ");
Serial.print(readDiscreteInputs[loop_i]);
if (++loop_i >= 10)
{
Serial.println("");
loop_i = 0;
}
*/
// delay(Mdelay); // no need for delay(5) since we print 5 char at 9600
/* listen for modbus commands con serial port
*
* on a request, handle the request.
* if the request has a user handler function registered in cbVector
* call the user handler function.
*/
slave.poll();
}
/**
* Handel Force Single Coil (FC=05)
* set digital output pins (coils) on and off
*/
void writeDigitlOut(uint8_t fc, uint16_t address, uint16_t status) {
digitalWrite(address, status);
}
/**
* Handel Read Input Status (FC=02/01)
* write back the values from digital in pins (input status).
*
* handler functions must return void and take:
* uint8_t fc - function code
* uint16_t address - first register/coil address
* uint16_t length/status - length of data / coil status
*/
void readDigitalIn(uint8_t fc, uint16_t address, uint16_t length)
{
int data = 0;
// read digital input
Serial.printf("digital input bytes fc %02x at address %04x length %d data ", fc, address, length);
node.readDiscreteInputs(address, length);
for (int i = 0; i <= (length-1)/8; i++) // cmheong 2018-08-06
{
data = node.getResponseBuffer(i);
slave.writeCoilsToBuffer(i, (uint8_t) data); // digitalRead(address + i));
Serial.printf(" %x", node.getResponseBuffer(i));
}
delay(Mdelay);
Serial.println("");
node.clearResponseBuffer();
}
/**
* Handel Read Input Registers (FC=04/03)
* write back the values from analog in pins (input registers).
*/
void readAnalogIn(uint8_t fc, uint16_t address, uint16_t length) {
// read analog input
for (int i = 0; i < length; i++) {
slave.writeRegisterToBuffer(i, analogRead(address + i));
}
}
// cmheong 2018-07-31 write_registers()
void writeAnalogOut(uint8_t fc, uint16_t address, uint16_t length)
{
Serial.printf("analog output bytes at address %04x length %d data ", address, length);
for (int i = 0; i < length; i++)
{
Serial.printf("%x ", slave.readRegisterFromBuffer(i));
Serial.println("");
node.send(slave.readRegisterFromBuffer(i));
// node.writeSingleRegister(address, slave.readRegisterFromBuffer(i));
delay(Mdelay);
}
node.writeMultipleRegisters(address, length);
}
I only tested 'Read Discrete Registers' (function code 2) and 'Write Multiple Registers' (function code 16) on a real Modbus RTU device, but you get the idea. yaakov's code did not process function code 2 properly, so I modifiled ModbusSlaveTCP.cpp of his library:
case FC_READ_DISCRETE_INPUT: // read input state (digital in)
address = word(bufIn[MLEN + 2], bufIn[MLEN + 3]); // coil to set.
length = word(bufIn[MLEN + 4], bufIn[MLEN + 5]);
// sanity check.
if (length > MAX_BUFFER) return 0;
// check command length.
if (lengthIn != (MLEN + 6)) return 0;
// build valid empty answer.
lengthOut = MLEN + 3 + (length - 1) / 8 + 1; // cmheong 2018-08-06
bufOut[MLEN + 2] = length; // cmheong 2018-08-06
// clear data out.
memset(MLEN + bufOut + 2, 0, bufOut[2]); // cmheong 2018-08-06
if (cbVector[CB_READ_DISCRETE_INPUT]) // cmheong 2018-08-02
{
cbVector[CB_READ_DISCRETE_INPUT](fc, address, length);
}
break;
And added a new function:
void ModbusTCP::writeCoilsToBuffer(int offset, uint8_t state)
{
int address = MLEN + 3 + offset;
bufOut[address] = state;
}
The ESP-12E will connect to your WiFi and use a fixed IP address (change it to suit your own address assignments) 10.0.0.100. To test it, I used my laptop to connect to the same WiFi access point. I then modified pymodbus's excellent synchronous-client.py thus:
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
client = ModbusClient('10.0.0.100', method='rtu', port=502) # 2018-07-29
The test code is:
rr = client.read_discrete_inputs(1,1,unit=0x01)
if rr != None :
print "\nread discrete inputs from", coils, rr.bits, '\n'
rq = client.write_registers(0x1001, [0x001f]*1, unit=0x01)
if rq != None :
print "\write holding_registers from", 10, rq, '\n'
A sample working output is:
root@aspireF15:/home/heong/EMS/pymodbus/pymodbus-master/examples/current$python ./esp8266-tcpipclient.py 0x01
DEBUG:pymodbus.transaction:Current transaction state - IDLE
DEBUG:pymodbus.transaction:Running transaction 1
DEBUG:pymodbus.transaction:SEND: 0x0 0x1 0x0 0x0 0x0 0x6 0x1 0x2 0x0 0x1 0x0 0x1
DEBUG:pymodbus.client.sync:New Transaction state 'SENDING'
DEBUG:pymodbus.transaction:Changing transaction state from 'SENDING' to 'WAITING FOR REPLY'
DEBUG:pymodbus.transaction:Changing transaction state from 'WAITING FOR REPLY' to 'PROCESSING REPLY'
DEBUG:pymodbus.transaction:RECV: 0x0 0x1 0x0 0x0 0x0 0x4 0x1 0x2 0x1 0x7
DEBUG:pymodbus.framer.socket_framer:Processing: 0x0 0x1 0x0 0x0 0x0 0x4 0x1 0x2 0x1 0x7
DEBUG:pymodbus.factory:Factory Response[ReadDiscreteInputsResponse: 2]
DEBUG:pymodbus.transaction:Adding transaction 1
DEBUG:pymodbus.transaction:Getting transaction 1
DEBUG:pymodbus.transaction:Changing transaction state from 'PROCESSING REPLY' to 'TRANSACTION_COMPLETE'
read discrete inputs from 1 [True, True, True, False, False, False, False, False]
DEBUG:pymodbus.transaction:Current transaction state - TRANSCATION_COMPLETE
DEBUG:pymodbus.transaction:Running transaction 2
DEBUG:pymodbus.transaction:SEND: 0x0 0x2 0x0 0x0 0x0 0x9 0x1 0x10 0x10 0x1 0x0 0x1 0x2 0x0 0x1f
DEBUG:pymodbus.client.sync:New Transaction state 'SENDING'
DEBUG:pymodbus.transaction:Changing transaction state from 'SENDING' to 'WAITING FOR REPLY'
DEBUG:pymodbus.transaction:Changing transaction state from 'WAITING FOR REPLY' to 'PROCESSING REPLY'
DEBUG:pymodbus.transaction:RECV: 0x0 0x2 0x0 0x0 0x0 0x6 0x1 0x10 0x10 0x1 0x0 0x1
DEBUG:pymodbus.framer.socket_framer:Processing: 0x0 0x2 0x0 0x0 0x0 0x6 0x1 0x10 0x10 0x1 0x0 0x1
DEBUG:pymodbus.factory:Factory Response[WriteMultipleRegistersResponse: 16]
DEBUG:pymodbus.transaction:Adding transaction 2
DEBUG:pymodbus.transaction:Getting transaction 2
DEBUG:pymodbus.transaction:Changing transaction state from 'PROCESSING REPLY' to 'TRANSACTION_COMPLETE'
Modbus device replied!
There you have it, a Modbus TCP/IP to RS485 passthrough gateway for less than RM25. Slap on an AWS or Google Cloud server and you are ready for a free docker microservice demon Modbus host.
Happy Trails!