How to create Modbus/RTU request in Python
From Digi Developer
If you have serial Modbus/RTU slaves attached to Digi XBee serial modules, then you'll need to create the original Modbus/RTU requests to send. This is quite easily done with Python, and what follows is a suggested practice after six years of using Modbus under Python.
The CRC16 checksum
Use the crc16.py module documented here: Python CRC16 for Modbus or DF1
Handling BigEndian words
I normally include these two routines right in my Modbus code module.
def u16_to_bestr( u): """Given word, return as big-endian binary string""" u = (int(u) & 0xFFFF) return( chr(u>>8) + chr(u&0xFF) ) def bestr_to_u16( st): """Given big-endian binary string, return bytes[0-1] as int""" return( (ord(st[0])<<8) + ord(st[1]))
Feeding Input into the routine
While your first instinct might be to create routines such as make_func3(slv,offset,count) and make_func16(slv,offset,count,data), these don't make very good use of Python - plus they don't encourage creative negative testing long-term. For example, how would you create a func-3 call with a bad CRC or too much data requested - create a second routine called make_func3_badcrc() ?
Instead I suggest you fill in a dictionary holding the inputs, with some suitably defaulted if missing. While the key names are up to you, here is the list I've used with good results:
- ['dest'] is the Unit Id or slave address
- ['func'] is the Modbus code, such as 3 or 16
- ['protErr'] is the Modbus exception code
- ['rdOffset'] is the zero-based offset, so register 4x00001 is offset zero (0)
- ['rdCount'] is the word or bit count for the function
- ['rdData'] is a tuple or list holding words read
- ['wrOffset'] is like 'rdOffset', but used for writes - or for read/write functions
- ['wrCount'] is like 'rdCount', but used for writes - or for read/write functions
- ['wrData'] is a tuple or list holding words to write
- ['badCrc'] (for example) could be added to force a bad CRC to any request created
Now we can create a read of 10 registers from 4x00001 with the call mbrtu_make_request( {'dest':1, 'func':3, 'rdOffset':0, 'rcCount':10}) Below is a quick example - note that it does NOT survive missing keys to reduce the complexity.
def mbrtu_make_request( inDct): inDct.update({'error':"", 'request':None }) # indicate no error dest = int(inDct.get('dest',1)) # default unit id to 1 if((dest < 0) or (dest >255)): inDct.update({'error':"Bad Unit Id or Slave Address"}) return inDct func = int(inDct.get('func',-1)) # default is -1/error req = None if(func in [1,2,3,4]): req = chr(dest) + chr(func) + u16_to_bestr(inDct['rdOffset']) + u16_to_bestr(inDct['rdCount']) elif( fnc == 5): if(inDct['wrData'][0]): # if first item is True, so 0xFF00 (not 0xFFFF) dat = 0xFF00 else: # is False dat = 0x0000 req = chr(dest) + chr(func) + u16_to_bestr(inDct['wrOffset']) + u16_to_bestr(dat) elif( fnc == 6): req = chr(dest) + chr(func) + u16_to_bestr(inDct['wrOffset']) + u16_to_bestr(inDct['wrData'][0]) # elif( fnc == 15): not here yet # elif( fnc == 16): not here yet else: inDct.update({'error':"Unsupported Function"}) return inDct # at this point we have the basic Modbus message if( req): crc = crc16.calcString( req, 0xFFFF) # oddly, we need to add the CRC as LittleEndian req += chr(u&0xFF) + chr(u>>8) inDct.update({'request':req}) return inDct
