How to create Modbus/RTU request in Python

From Digi Developer

Jump to: navigation, search

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
Personal tools
Wiki Editing