Integrating the Digi IA Modbus Bridge to Python
From Digi Developer
Most Digi products support a Modbus Bridge, which allows multiple Modbus clients (or masters) to share a collection of Modbus servers (or slaves). The bridge freely 'bridges' the three Modbus encoding forms of: Modbus/TCP, Modbus/RTU, and Modbus/ASCII.
Contents |
Supported Digi Products with both Modbus and Python:
- Digi Connect WANIA (Ethernet, Cellular, Serial - needs firmware 82001661 rev 'A" or higher)
- Digi ConnectPort X4 (Ethernet, Cellular, Serial, Zigbee - needs firmware rev 'D" or higher)
- Digi ConnectPort X8 (Ethernet, Cellular, Serial, Zigbee - needs firmware rev 'E" or higher)
Why link the Digi IA Modbus Bridge with Python?
Of course you could use Python to create a server task, but to match the power already built into the IA Modbus Bridge you'd need to handle:
- Three encoding forms (Modbus/TCP, Modbus/RTU, and Modbus/ASCII.)
- Both TCP/IP and UDP/IP (SSL/TLS in the works)
- Defragmentation of TCP/IP and UDP/IP Modbus packets
- Dead-client detection and disconnect
So you can use the existing Digi IA Modbus Bridge to parse and validate Modbus requests, which are then forwarded in perfect form to Python waiting on a UDP socket on localhost. Your Python code can treat the Modbus/TCP requests as simple events.
What can you do?
- Poll local Modbus slave, then issue Master writes when situations
Digi Configuration Example
Using Telnet or SSH, log into a supported Digi product and enter these lines to create the basic configuration. Notice that any Unit Id (slave address) 33 to 255 are sent to your Python code.
(A python script to automate this process is on this Wiki page: Create_IA_Configuration_by_Python_Script)
# misc preparation configs set profile profile=ia set term state=off set serial baudrate=19200 databits=8 stopbits=1 parity=none flowcontrol=none # table defines how requests are solved into responses set ia table=1 state=on name=python family=modbus accessmode=multi set ia table=1 addroute=1 set ia table=1 addroute=2 # first route defines Modbus slaves 1 to 32 on the serial ports set ia table=1 route=1 active=on type=serial protaddr=1-32 port=1 # second route pushes Modbus/TCP requests to localhost UDP port 49502 - for Python set ia table=1 route=2 active=on type=ip protaddr=0-255 protocol=modbustcp set ia table=1 route=2 transport=udp connect=passive address=127.0.0.1 set ia table=1 route=2 ipport=49502 replaceip=off slavetimeout=1000 set ia table=1 route=2 chartimeout=50 idletimeout=0 fixedaddress=0 rbx=off # Serial port supports multi-drop of Modbus/RTU set ia serial=1 type=slave protocol=modbusrtu slavetimeout=1000 chartimeout=20 set ia serial=1 fixedaddress=0 rbx=off active=on # 1st and 2nd masters Modbus/TCP on TCP and UDP port 502 set ia master=1 active=on type=tcp ipport=502 protocol=modbustcp table=1 set ia master=1 priority=medium messagetimeout=2500 chartimeout=50 set ia master=1 idletimeout=0 errorresponse=on broadcast=replace set ia master=2 active=on type=udp ipport=502 protocol=modbustcp table=1 set ia master=2 priority=medium messagetimeout=2500 chartimeout=50 set ia master=2 idletimeout=0 errorresponse=on broadcast=replace # 3rd master serial Modbus/RTU encapsulated in UDP/IP on port 501 set ia master=3 active=on type=udp ipport=501 protocol=modbusrtu table=1 set ia master=3 priority=medium messagetimeout=2500 chartimeout=50 set ia master=3 idletimeout=0 errorresponse=on broadcast=replace
Python Code Example (Server - receive requests)
Here is a simple example of the server portion of your Python code. It waits upon UDP port 49502 (must match the number in the Digi Modbus bridge configuration above, plus CANNOT be 502 since the Digi Modbus bridge already binds on that port under both TCP and UDP.
You would customize the routine send_request_to_user( req_tuple) to process the requests and create the responses. Remember that as configured above, you must return a response within 1 second or the Digi Modbus bridge will give up and discard interest in any response with that sequence number.
import traceback import socket UDP_BREAK = 5.0 UDP_DEF_PORT = 49502 UDP_DEF_ADDRESS = "" UDP_RCV_SIZE = 1024 def udp_run( dct): """Run UDP message loop forever""" port = dct.get('ipport', UDP_DEF_PORT ) address = dct.get('address', UDP_DEF_ADDRESS ) try: # attempt to bind on port linked to the Digi IA Modbus bridge udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udpSock.bind((address, port)) udpSock.settimeout( UDP_BREAK) except: return while True: # because we don't block, your code can do periodic other things try: req, addr = udpSock.recvfrom( UDP_RCV_SIZE) except socket.timeout: # this is hit every 5 seconds (or as you configured) continue except: # all other errors, we just print out details and continue traceback.print_exc() break if(len(req)>0): # then we received something - should always be true if here # SEND IT TO USER rsp_tuple = send_request_to_user( (req, addr, dct) ) if(rsp_tuple and (len(rsp_tuple[0]) > 0)): # then user returned a response as (rsp,addr,dct) rsp = rsp_tuple[0] numBytes = udpSock.sendto( rsp, addr) # else no response desired req = "" rsp = "" rsp_tuple = () # endwhile(true) udpSock.close() return def send_request_to_user( req_tuple): """Given Modbus/TCP message, return exception response bad-data Keyword arguments: req_tuple = the request/address pair req_tuple[0] = binstr of the protocol request req_tuple[1] = address from socket (TCP or UDP) req_tuple[2] = dictionary of options Return form: rsp_tuple = the response/address pair if None, ia_server will hang-up rsp_tuple[0] = binstr of the protocol response ("" is okay - no rsp) rsp_tuple[1] = address from req_tuple[1] rsp_tuple[2] = dictionary of options """ msg = req_tuple[0] addr = req_tuple[1] dct = req_tuple[2] # print 'dummy mbtcp_responder saw addr ', addr # we'll throw excpetion if not types.StringTypes if( len(msg) < 8): # header is too short to know full length return None # Here we have full header, format = SS SS 00 00 LL LL # SS SS is sequence number - can be anything # 00 00 is protocol format, should be 0x0000 or protocolver # LL LL is the length, which we allow to be up to 0xFFFF rsp = msg[:4] rsp += chr(0) rsp += chr(3) rsp += msg[6] # old unit id rsp += chr(ord(msg[7])|0x80) # old func made to error rsp += chr(2) return (rsp,addr,dct)
Python Code Example (Client - send requests)
The code sample isn't included yet, but sending out your own Modbus requests is very straight forward. Since the configuration for the Digi IA Modbus bridge is waiting for Modbus/TCP form on either TCP or UDP port 502, your code should just connect to the localhost (IP=127.0.0.1) by either TCP or UDP. You should use Modbus/TCP format and vary the sequence number between polls. Blocking is okay since the Digi IA Modbus bridge always returns a response to Modbus/TCP clients - either the true response or a Modbus gateway exception such as 0x0A or 0x0B (no path or timeout) if there is no response.
Pending.
