1. Introduction
Modbus Gateway allows to exchange data between Modbus Client (TCP) and Modbus Slave (RTU). Which means that Modbus Gateway itself, integrates functionality of Modbus Server (TCP) and Modbus Master (RTU). In fact, we don't need full implementation of Modbus Server as we did it in one of the previous artilces. We are simply going to transform Modbus message obtained by TCP protocol and send it over serial line. Response from Modbus Slave will be transformed back to match Modbus TCP. It doens't matter whatever it is Modbus TCP or Modbus RTU, the core of the message has always the same structure. It is called PDU (Protol Data Unit) and it contains:
- Function Code - 1byte
- Data of different size based on the function code used
Modbus TCP is built with:
- MBAP Header:
- Transaction ID - 2 bytes
- Protocol ID - 2 bytes
- Length - 2 byte
- Unit ID - 1 byte
- PDU
Modbus RTU is buld with:
- Slave ID - 1 byte
- PDU
- CRC - 2 bytes
Modbus Gateway general functionality is to:
- listen and detect incoming message over TCP protocol from Modbus Client
- strip incoming message of MBAP header and leave only PDU
- add Slave ID at the front of PDU (Slave ID will be Unit ID - although it is not always the case)
- calculate CRC and add it at the end of frame
- send such prepared frame over serial line and wait for resposne
- strip received response of CRC and Slave ID leaving only PDU
- put MBAP header at the beginning of PDU
- send response message over TCP protocol back to Modbus Client
For Modbus Gateway, we are going to use "AdvancedTCPServer" application as a base.
2. Implementation
2.1 TCP Server Task
In "AdvancedTCPServer" application, we run TCP Server as a program. We are going to change that and move all the code to the Function Block named "TcpServer". It will allows us to replicate the code easily in the future.
PROGRAM Server
VAR
ModbusGatewayServer : TcpServer;
END_VAR
ModbusGatewayServer(bridge := ADR(MODBUS.bridgeCom1));
Server IP address and Port number become input parameters. As we are going to use separate tasks for TCP and Serial communication, we need a way to exchange data between them. For that, we will utilize Global Variables, but we will pass data to TcpServer FB as an input, allowing ourselves to pass another data with another instance of TcpServer.
FUNCTION_BLOCK TcpServer
VAR_INPUT
serverIp : STRING(16) := '192.168.0.204';
serverPort : WORD := 502;
bridge : POINTER TO ModbusBridgeData;
END_VAR
VAR
// general
serverStep : UINT := 10;
socketList : TcpSocketList;
// 10 - socket initialization
serverAddr : SOCKADDRESS;
rCreate : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
// 20 - unblock mode
nonBlock : DWORD := 1;
rIoctl :SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
// 30 - socket binding
rBind : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
// 40 - listen
rListen : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
// 45 - select
masterSocketSet : SysSocket.SysSocket_Interfaces.SOCKET_FD_SET;
socketsReady : DINT;
selectTimout : SysSocket.SysSocket_Interfaces.SOCKET_TIMEVAL;
// 50 - accept
// 60 - exchange data - receive
// 70 - exchange data - send
// 90 - close sever
rServerClose : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
END_VAR
CASE serverStep OF
10:
SysSockInetAddr(serverIp, ADR(serverAddr.sin_addr));
serverAddr.sin_family := SOCKET_AF_INET;
serverAddr.sin_port := SysSockHtons(serverPort);
socketList.server.handle := SysSockCreate(SOCKET_AF_INET, SOCKET_STREAM, SOCKET_IPPROTO_TCP, ADR(rCreate));
IF socketList.server.handle <> SysSocket.SysSocket_Interfaces.RTS_INVALID_HANDLE THEN
serverStep := 20;
END_IF
20:
rIoctl := SysSockIoctl(socketList.server.handle, SOCKET_FIONBIO, ADR(nonBlock));
IF rIoctl = CmpErrors.Errors.ERR_OK THEN
serverStep := 30;
END_IF
30:
rBind := SysSockBind(socketList.server.handle, ADR(serverAddr), SIZEOF(SOCKADDRESS));
IF rBind = CmpErrors.Errors.ERR_OK THEN
serverStep := 40;
END_IF
40:
rListen := SysSockListen(socketList.server.handle, 1);
IF rListen = CmpErrors.Errors.ERR_OK THEN
serverStep := 45;
END_IF
45:
BuildSocketSet(ADR(masterSocketSet), ADR(socketList));
SysSockSelect(SysSocket.SysSocket_Interfaces.MAX_SOCKET_FD_SETSIZE, ADR(masterSocketSet), 0, 0, ADR(selectTimout), ADR(socketsReady));
serverStep := 50;
50:
AcceptOrDenyTcpClient(ADR(masterSocketSet), ADR(socketList));
serverStep := 60;
60:
ReceiveFromTcpClient(ADR(masterSocketSet), ADR(socketList), bridge);
serverStep := 70;
70:
SendToTcpClient(ADR(masterSocketSet), ADR(socketList), bridge);
serverStep := 45;
90:
rServerClose := SysSockClose(socketList.server.handle);
IF rServerClose = CmpErrors.Errors.ERR_OK THEN
serverStep := 91;
END_IF
END_CASE
As You can see, this is almost the same code as used in Advanced TCP Server application. There are minor differences though.
Modbus Master works sequentially. It sends a request, and waits for an resposne. Once completed, it sends another request and waits for another respone, and so on. In Modbus Gateway, we are using both ethernet and serial line, but we need to treat them as it would be one serial line. Modbus Client needs to send request one by one waiting for respone before sending another request. That also means, we can not allow more that one Modbus Client simultaneously as we did in Modbus Server application. For that, we are going to limit number of clients to one and close all other connection attempts. To do that, we are going to modify "AddSocketToTheList" within "AcceptOrDenyTcpClient" function (serverStep 50).
FUNCTION AcceptOrDenyTcpClient : bool
VAR_INPUT
socketSet : POINTER TO SysSocket.SysSocket_Interfaces.SOCKET_FD_SET;
list : POINTER TO TcpSocketList;
END_VAR
VAR
hResult : SysSocket.SysSocket_Interfaces.RTS_IEC_HANDLE;
rAccept : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
rClose : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
clientAddress : SOCKADDRESS;
clientAddrSize : DINT := SIZEOF(clientAddress);
END_VAR
IF SysSockFdIsset(list^.server.handle, socketSet^) THEN
hResult := SysSockAccept(list^.server.handle, ADR(clientAddress), ADR(clientAddrSize), ADR(rAccept));
IF rAccept = CmpErrors.Errors.ERR_OK THEN
IF AddSocketToTheList(hResult, ADR(list^)) <> 0 THEN
rClose := SysSockClose(hResult);
END_IF
END_IF
END_IF
In a way that it utilize only the first element of socket array.
FUNCTION AddSocketToTheList : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT
VAR_INPUT
handle : SysSocket.SysSocket_Interfaces.RTS_IEC_HANDLE;
list : POINTER TO TcpSocketList;
END_VAR
VAR
i : UINT;
END_VAR
i := 1;
WHILE (list^.client[i].handle <> SysSocket.SysSocket_Interfaces.RTS_INVALID_HANDLE) AND (i < SysSocket.SysSocket_Interfaces.MAX_SOCKET_FD_SETSIZE) DO
i := i + 1;
END_WHILE
IF i<=1 THEN //IF i < SysSocket.SysSocket_Interfaces.MAX_SOCKET_FD_SETSIZE THEN
list^.client[i].handle := handle;
AddSocketToTheList := CmpErrors.Errors.ERR_OK;
ELSE
AddSocketToTheList := CmpErrors.Errors.ERR_FAILED;
END_IF
Finally, we will separate Receive (serverStep 60) and Send (serverStep 70) functions for our convenience.
Index of socket array is always one as we know that the code restricts more that one Modbus Client simultaneously. When message is received from a client, it is being transfromed from TCP into RTU format and send over serial line with SysComWrite function (in another task).
FUNCTION ReceiveFromTcpClient : bool
VAR_INPUT
socketSet : POINTER TO SysSocket.SysSocket_Interfaces.SOCKET_FD_SET;
list : POINTER TO TcpSocketList;
bridge : POINTER TO ModbusBridgeData;
END_VAR
VAR
rRecv : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
rClose : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
END_VAR
IF list^.client[1].handle <> SysSocket.SysSocket_Interfaces.RTS_INVALID_HANDLE THEN
IF SysSockFdIsset(list^.client[1].handle, socketSet^) THEN
bridge^.tcpBytesReceived := SysSockRecv(list^.client[1].handle, ADR(bridge^.tcpDataReceived), SIZEOF(bridge^.tcpDataReceived), 0, ADR(rRecv));
IF rRecv = 0 THEN
IF bridge^.tcpBytesReceived <> 0 THEN
tcp2rtu(bridge);
bridge^.tcpBytesReceived := 0;
END_IF
ELSE
rClose := SysSockClose(list^.client[1].handle);
RemoveSocketFromTheList(list^.client[1].handle, list);
END_IF
END_IF
END_IF
And vice verse, when the message is received from Modbus Slave, it is being transformed from RTU into TCP format (in another task) and send over ethernet back to the client. Again, we use only first element of socket array.
FUNCTION SendToTcpClient : BOOL
VAR_INPUT
socketSet : POINTER TO SysSocket.SysSocket_Interfaces.SOCKET_FD_SET;
list : POINTER TO TcpSocketList;
bridge : POINTER TO ModbusBridgeData;
END_VAR
VAR
rSend : SysSocket.SysSocket_Interfaces.RTS_IEC_RESULT;
END_VAR
IF bridge^.tcpBytesToSend <> 0 THEN
SysSockSend(list^.client[1].handle, ADR(bridge^.tcpDataToSend), bridge^.tcpBytesToSend, 0, ADR(rSend));
bridge^.tcpBytesToSend := 0;
END_IF
All that message transformations results are stored in "bridge" global variable. Send and receive functions, both for TCP and RTU, are executed once the message array is not empty. When the message lenght is not zero to be more precise.
2.2 Global variables
Global variable will be a structure which holds, all Modbus incoming and outgoing messages (frames) for both TCP and Serial line.
VAR_GLOBAL
bridgeCom1 : ModbusBridgeData;
END_VAR
Modbus protocol is limited to 2000 bit or 125 words per message. That gives 250 bytes. 1 byte of Slave ID, 1 byte of Function Code, 1 byte of "Byte count" and 2 bytes of CRC give 255 bytes in total and this is the maximum what we can get on Modbus RTU. TCP message doesn't contain CRC (2 bytes less) but it contains MBAP header of 7 bytes from which we exclude Unit ID, so 6 bytes. It all gives additional 4 bytes. 259 bytes is the maximum we can get on Modbus TCP.
We will also hold a variable with expected response lenght on Serial line.
TYPE ModbusBridgeData :
STRUCT
expectedResponseLength : UINT := 256;
// Modbus TCP
tcpDataReceived : ARRAY[0..258] OF BYTE;
tcpBytesReceived : __XINT;
tcpDataToSend : ARRAY[0..258] OF BYTE;
tcpBytesToSend : __XINT;
// Modbus RTU
rtuDataReceived : ARRAY[0..254] OF BYTE;
rtuBytesReceived : UDINT;
rtuDataToSend : ARRAY[0..254] OF BYTE;
rtuBytesToSend : UDINT;
END_STRUCT
END_TYPE
2.3 Expected response length
Requests and responses of Modbus are precisely defined by protocol specification. By analyzing request message, we know exactly what is the response message structure and length. Response message length depends on Function Code and quantity of data requested. Information of expected response length will be necessary while listening for incoming messages on Serial line. Depending of Serial line baud rate, quantity of data request and controller speed, we may not get all byte at once and we will have to wait for them.
FUNCTION expectedResponseLength : UINT
VAR_INPUT
request : POINTER TO BYTE;
END_VAR
VAR
MSB : POINTER TO BYTE;
LSB : POINTER TO BYTE;
quantity : UINT;
END_VAR
CASE getFunctionCode(request) OF
1, 2:
MSB := request + 4*SIZEOF(BYTE);
LSB := request + 5*SIZEOF(BYTE);
quantity := SHL(BYTE_TO_UINT(MSB^), 8) OR BYTE_TO_UINT(LSB^);
expectedResponseLength := (quantity/8) + ((7 + (quantity MOD 8))/8) + 5;
3, 4:
MSB := request + 4*SIZEOF(BYTE);
LSB := request + 5*SIZEOF(BYTE);
quantity := SHL(BYTE_TO_UINT(MSB^), 8) OR BYTE_TO_UINT(LSB^);
expectedResponseLength := quantity*2 + 5;
5, 6, 15, 16:
expectedResponseLength := 8;
END_CASE
2.4 Utility functions
Function Code extraction.
FUNCTION getFunctionCode : BYTE
VAR_INPUT
frame : POINTER TO BYTE;
END_VAR
VAR
tmp : POINTER TO BYTE;
END_VAR
tmp := frame + SIZEOF(BYTE);
getFunctionCode := tmp^;
CRC calculation for Modbus RTU.
FUNCTION CRC16 : UINT
VAR_INPUT
buf : POINTER TO BYTE;
len : UDINT;
END_VAR
VAR
loc : POINTER TO BYTE;
pos : UDINT;
i : UINT;
END_VAR
CRC16 := 16#FFFF;
FOR pos := 0 TO len-1 DO
loc := buf + pos*SIZEOF(BYTE);
CRC16 := CRC16 XOR BYTE_TO_UINT(loc^);
FOR i := 1 TO 8 DO
IF ((CRC16 AND 16#0001) <> 0) THEN
CRC16 := SHR(CRC16, 1);
CRC16 := CRC16 XOR 16#A001;
ELSE
CRC16 := SHR(CRC16, 1);
END_IF
END_FOR
END_FOR
CRC16 := SHR(CRC16, 8) OR SHL(CRC16, 8);
Verification whatever CRC of specific message is correct.
FUNCTION isCrcValid : BOOL
VAR_INPUT
frame : POINTER TO BYTE;
len : UDINT;
END_VAR
VAR
CalcCRC : UINT;
MSB, LSB : BYTE;
tmp : POINTER TO BYTE;
END_VAR
CalcCRC := CRC16(frame, UDINT_TO_UINT(len-2));
tmp := frame + (len-2)*SIZEOF(BYTE);
MSB := tmp^;
tmp := frame + (len-1)*SIZEOF(BYTE);
LSB := tmp^;
IF (UINT_TO_BYTE(SHR(CalcCRC, 8)) = MSB) AND (UINT_TO_BYTE(CalcCRC) = LSB) THEN
isCrcValid := TRUE;
ELSE
isCrcValid := FALSE;
END_IF
Error message indentification.
FUNCTION isErrorResponse : BOOL
VAR_INPUT
frame : POINTER TO BYTE;
END_VAR
VAR
END_VAR
IF (getFunctionCode(frame) AND 16#80) = 16#80 THEN
isErrorResponse := TRUE;
ELSE
isErrorResponse := FALSE;
END_IF
2.5 Com Port Task
Operations related to Com Port will also be implemented within Function Block to allow functionality replication for other Com Ports. Separate task for Com Port operation contains instance of "ComPort" Function Block which is called with default parameters.
PROGRAM COM
VAR
ComPort : ComPort;
END_VAR
ComPort(bridge := ADR(MODBUS.bridgeCom1));
From six input parameters only one is not initialized. "bridge" parameter holds a pointer to global variable structure from paragraph 2.2.
Below Function Block opens specified Com Port, read and write serial data and implements timeout for message resposne. When message is received, it transorms RTU message fromat into TCP for TcpServer to respond to client.
FUNCTION_BLOCK ComPort
VAR_INPUT
com : SysCom.SYS_COM_PORTS := SysCom.SYS_COMPORT1;
baudRate : SysCom.SYS_COM_BAUDRATE := SysCom.SYS_BR_19200;
stopBits : SysCom.SYS_COM_STOPBITS := SysCom.SYS_ONESTOPBIT;
parity : SysCom.SYS_COM_PARITY := SysCom.SYS_NOPARITY;
bridge : POINTER TO ModbusBridgeData;
slaveTimeout : TIME := T#1S;
END_VAR
VAR_OUTPUT
END_VAR
VAR
ComPort : PortManagement;
purgeSerailPort : R_TRIG;
// SysComWrite variables
resultWrite : SysCom.RTS_IEC_RESULT;
resultWriteAddr : POINTER TO SysCom.RTS_IEC_RESULT := ADR(resultWrite);
// SysComRead variables
buffer : ARRAY[1..600] OF BYTE;
bytesReadFromSerialLine : UDINT;
resultRead : SysCom.RTS_IEC_RESULT;
resultReadAddr : POINTER TO SysCom.RTS_IEC_RESULT := ADR(resultRead);
waitingForResposne : BOOL;
resposneTimeout : TON;
END_VAR
ComPort(PortNo := com, BaudRate := baudRate, StopBits := stopBits, Parity := parity);
purgeSerailPort(CLK := ComPort.opened);
IF purgeSerailPort.Q THEN
sysComPurge(hCom := ComPort.hCom);
END_IF
IF ComPort.opened THEN
bytesReadFromSerialLine := SysComRead(hCom := ComPort.hCom, pbyBuffer := ADR(buffer), uLsize := 600, ultimeout := 0, pResult := resultReadAddr);
rtu2tcp(bridge, ADR(buffer), bytesReadFromSerialLine, resposneTimeout.Q);
IF resposneTimeout.Q OR (bridge^.tcpBytesToSend <> 0) THEN
waitingForResposne := FALSE;
END_IF
IF bridge^.rtuBytesToSend <> 0 THEN
SysComWrite(hCom := ComPort.hCom, pbyBuffer := ADR(bridge^.rtuDataToSend), ulSize := bridge^.rtuBytesToSend, ultimeout := 0, pResult := resultWriteAddr);
bridge^.rtuBytesToSend := 0;
waitingForResposne := TRUE;
END_IF
resposneTimeout(IN := waitingForResposne, PT := slaveTimeout);
END_IF
PortManagement Function Block holds all configuration of the Com Port and implements basic functions related to it (opening, closing and purging the port).
FUNCTION_BLOCK PortManagement
VAR_INPUT
PortNo : SysCom.SYS_COM_PORTS;
BaudRate : SysCom.COM_BAUDRATE;
StopBits : SysCom.COM_STOPBITS;
Parity : SysCom.COM_PARITY;
END_VAR
VAR_OUTPUT
hCom : SysCom.RTS_IEC_HANDLE;
opened : BOOL;
failedToOpen : BOOL;
END_VAR
VAR
initOpen : BOOL := TRUE;
initClose : BOOL := FALSE;
initPurge : BOOL := FALSE;
commSettings : SysCom.SysComSettings;
resultOpen : sysCom.RTS_IEC_RESULT;
END_VAR
IF initOpen AND NOT opened THEN
commSettings.byParity := Parity; //no parity
commSettings.byStopBits := StopBits; // one stop bit
commSettings.sPort := PortNo;
commSettings.ulBaudrate := BaudRate;
commSettings.ulBufferSize := 10000;
commSettings.ultimeout := 0;
hCom := sysComOpen(sPort := PortNo, pResult := ADR(resultOpen));
IF hCom = sysCom.HandleConstants.RTS_INVALID_HANDLE THEN
failedToOpen := TRUE;
ELSE
IF hCom <> 0 THEN
opened := TRUE;
SysComSetSettings2(hCom := hCom, pSettings := ADR(commSettings), pSettingsEx2 := 0);
END_IF
END_IF
initOpen := FALSE;
END_IF
IF initClose AND opened THEN // make sure that polling is disabled otherwise program will crush
IF sysComClose(hCom := hCom) = 0 THEN
opened := FALSE;
initClose := FALSE;
END_IF
END_IF
IF initPurge THEN
sysComPurge(hCom := hCom);
initPurge := FALSE;
END_IF
2.6 Modbus message transformation
Message transformation from TCP to RTU is pretty straightforward. When message is received from a client, it must be stripped of 6 bytes of MBAP header (excluding Unit ID which becomes Slave ID). With FOR loop, we copy PDU and Unit ID to RTU message. When copied, CRC is calculate and placed at the end of the message. Message lenght is first decreased by 6 bytes and then increased by 2 bytes of CRC. Eventually it is saved in "rtuBytesToSend" variable. Expected message lenght is calculated. Other variables representing lengths of other frames are reset by assigning zero.
FUNCTION tcp2rtu : BOOL
VAR_INPUT
data : POINTER TO ModbusBridgeData;
END_VAR
VAR
i : UDINT;
CRC : UINT;
len : UDINT;
END_VAR
len := __XINT_TO_UDINT(data^.tcpBytesReceived-6);
FOR i:=0 TO len DO
data^.rtuDataToSend[i] := data^.tcpDataReceived[i+6];
END_FOR
CRC := CRC16(ADR(data^.rtuDataToSend), len);
data^.rtuDataToSend[len+0] := UINT_TO_BYTE(SHR(CRC, 8));
data^.rtuDataToSend[len+1] := UINT_TO_BYTE(CRC);
len := len + 2;
data^.rtuBytesToSend := len;
data^.expectedResponseLength := expectedResponseLength(ADR(data^.rtuDataToSend));
data^.rtuBytesReceived := 0;
data^.tcpBytesToSend := 0;
Obtaining data from Serial line requires more attention. As mentioned earlier, in each controller scan, we can get only a part of a message and we need to wait for the remaining bytes. Expected message length variable is very handy here because we can check quantity of bytes received and decide whatever we should wait longer or not. Every controller scan, check is performed until:
- Expected amount of bytes is received. In this case we do not wait any longer and transform received message cutting CRC off and adding the MBAP header and the beginning (we also remember to update MBAP header length according to quantity of bytes received.
- Only 5 bytes are received. Modbus Slave may have responded with an error. We analyze received message content and if the message is an error message we will never get expected amount of byte. In this case, we transform message in the same way as if we received correct resposne. If we received 5 bytes but it is not an error message from Modbus Slave, we simply wait for next bytes.
- Timeout. We generate resposne to the client ourselves with exception code of 16#0B (indicating no response from the Modbus Slave).
FUNCTION rtu2tcp : BOOL
VAR_INPUT
data : POINTER TO ModbusBridgeData;
buffer : POINTER TO BYTE;
bytesReadFromSerialLine : UDINT;
timeout : BOOL;
END_VAR
VAR
i : UDINT;
b : POINTER TO BYTE;
END_VAR
IF bytesReadFromSerialLine <> 0 THEN
FOR i:=0 TO bytesReadFromSerialLine-1 DO
b := buffer+i*SIZEOF(BYTE);
data^.rtuDataReceived[i+data^.rtuBytesReceived] := b^;
END_FOR
data^.rtuBytesReceived := data^.rtuBytesReceived + bytesReadFromSerialLine;
IF data^.rtuBytesReceived = 5 THEN
FOR i:=0 TO 8 DO
data^.tcpDataToSend[i] := data^.tcpDataReceived[i];
END_FOR
IF isCrcValid(ADR(data^.rtuDataReceived), data^.rtuBytesReceived) AND isErrorResponse(ADR(data^.rtuDataReceived)) THEN
FOR i:=0 TO 2 DO
data^.tcpDataToSend[i+6] := data^.rtuDataReceived[i];
END_FOR
data^.tcpDataToSend[5] := 3;
data^.tcpBytesToSend := 9;
END_IF
END_IF
IF data^.rtuBytesReceived = data^.expectedResponseLength THEN
IF isCrcValid(ADR(data^.rtuDataReceived), data^.rtuBytesReceived) THEN
FOR i:=0 TO 8 DO
data^.tcpDataToSend[i] := data^.tcpDataReceived[i];
END_FOR
FOR i:=0 TO data^.rtuBytesReceived-1 DO
data^.tcpDataToSend[i+6] := data^.rtuDataReceived[i];
END_FOR
data^.tcpDataToSend[5] := UDINT_TO_BYTE(data^.rtuBytesReceived-2);
data^.tcpBytesToSend := 6+data^.rtuBytesReceived-2;
END_IF
END_IF
END_IF IF timeout THEN
FOR i:=0 TO 8 DO
data^.tcpDataToSend[i] := data^.tcpDataReceived[i];
END_FOR
data^.tcpDataToSend[7] := data^.tcpDataToSend[7] OR 16#80;
data^.tcpDataToSend[8] := 16#0B;
data^.tcpDataToSend[5] := 3;
data^.tcpBytesToSend := 9;
END_IF
IF timeout THEN
FOR i:=0 TO 8 DO
data^.tcpDataToSend[i] := data^.tcpDataReceived[i];
END_FOR
data^.tcpDataToSend[7] := data^.tcpDataToSend[7] OR 16#80;
data^.tcpDataToSend[8] := 16#0B;
data^.tcpDataToSend[5] := 3;
data^.tcpBytesToSend := 9;
END_IF
3. Summary
Modbus Gateways is not a new concept. There are already plenty of them available in the market from different manufacturers and there are differences in how they work and what could be configured. Some manufacturers created dedicated tools for configuration, ex. Lantronix (they use Device Installer). Some of them, like MOXA allow to change parameters thorugh web interface. In our example, there are several parameters to set through the Function Block input parameters. We can buy a Modbus Gateway from the market, but if we need flexibility we can have our own Modbus Gateway. In this case, we pay with time spent on developing the code.
This application was tested on Advantech industrial computer equipped with dedicated board of 8 Com Ports each. CodeSys run on Linux and on the level of operating system, we can do things that usually are not implemented in Gateways available from the market, for example ethernet port aggregation (for redundant ethernet connection).
Source code here (IP: 192.168.0.204, PORT: 502).