Modbus Server to Modbus Master Gateway

Single Ethernet, single COM

[22/02/2024]

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).