#!/usr/bin/env python

# home : https://github.com/sourceperl/modbus-serial-monitor

# modbus/RTU python monitor (check serial frame between devices)

# format modbus RTU serial frame receive from a serial port to console stdout
# check with python2.7 and python3, and module serial 2.6

# synoptic :
#
#   RS-485 bus -> FTDI USB cable -> /dev/ttyUSBx -> this_script -> CSV modbus
#   RS-422                                                           decode
#   RS-232

# CSV out sample :
# DATE=2015-11-05T17:31:27.977;ERR=NO;FRAME=01-03-50-00-00-04-55-09;SLAVE=1;

# use it like this :
#   - help :
#       $ ./mb_rtu_mon.py -h
#   - see frame only for slave 17 :
#       $ ./mb_rtu_mon.py | grep SLAVE=0x11
#   - store all traffic on a file
#       $ ./mb_rtu_mon.py > modbus.log


import argparse
import sys
import serial
import struct
import time
from datetime import datetime

# some const 
SEP = ";"


def frame2crc(frame):
    """Compute modbus CRC16 (for RTU mode)

    :param label: modbus frame
    :type label: str (Python2) or class bytes (Python3)
    :returns: CRC16
    :rtype: int
    """
    crc = 0xFFFF
    for index, item in enumerate(bytearray(frame)):
        next_byte = item
        crc ^= next_byte
        for i in range(8):
            lsb = crc & 1
            crc >>= 1
            if lsb:
                crc ^= 0xA001
    return crc


if __name__ == '__main__':
    # parse args
    parser = argparse.ArgumentParser()
    parser.add_argument('-d', '--device', type=str, default='/dev/ttyUSB0',
                        help='serial device (default is /dev/ttyUSB0)')
    parser.add_argument('-b', '--baudrate', type=int, default=9600,
                        help='serial baudrate (default is 9600)')
    args = parser.parse_args()

    # init serial object
    ser = serial.Serial(args.device, args.baudrate)

    # serial timeout = modbus end of frame (3.5 * byte tx time + 10% margin)
    ser.timeout = (1.0 / args.baudrate) * 11 * 3.5 * 1.10

    # flush all
    ser.flushInput()

    # first read (used for sync to frame, avoid bad CRC)
    ser.read(256)

    # main loop
    while True:
        # maximum size of modbus RTU frame is 256 bytes
        frame = ser.read(256)
        # for null size (no data)
        if not frame:
            continue
        # init vars
        err_str = "NO"
        slave_id = 0
        f_code = 0
        e_code = 0
        # add date and time
        now = datetime.now()
        date = now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
        # format modbus frame as txt string
        txt_frame = ''
        for item in bytearray(frame):
            if txt_frame:
                txt_frame += "-"
            txt_frame += "%02x" % item
        # check short frame
        if len(frame) >= 5:
            # check CRC
            r_crc = struct.unpack("<H", frame[-2:])[0]
            c_crc = frame2crc(frame[:-2])
            crc_ok = (r_crc == c_crc)
            if not crc_ok:
                err_str = "BAD_CRC"
            else:
                # slave ID
                slave_id = struct.unpack("B", frame[0:1])[0]
                # function code
                f_code = struct.unpack("B", frame[1:2])[0]
                # except code
                if f_code > 0x80:
                    err_str = "EXCEPT_FRAME"
                    e_code = struct.unpack("B", frame[2:3])[0]
        else:
            err_str = "SHORT_FRAME"
        # send result to stdout
        items = ['DATE=%s' % date, 'ERR=%s' % err_str, 'FRAME=%s' % txt_frame, 'SLAVE=%s' % slave_id]
        csv_line = SEP.join(items)
        print(csv_line)
        sys.stdout.flush()

    ser.close()

@@ -279,16 +279,54 @@
 rial 2.6
+ (don't work with 3.x timeout issue ?)
 %0A%0A# syno
@@ -1804,124 +1804,47 @@
   # 
-init serial object%0A    ser = serial.Serial(args.device, args.baudrate)%0A%0A    # serial timeout = modbus end
+modbus end of frame is a tx silent
  of 
-frame (
+%5B
 3.5 
@@ -1865,17 +1865,17 @@
  + 1
-0
+2
 %25 margin
 )%0A  
@@ -1874,24 +1874,36 @@
 rgin
-)%0A    ser.timeou
+%5D seconds%0A    inter_char_wai
 t = 
@@ -1928,16 +1928,18 @@
 te) * 11
+.0
  * 3.5 *
@@ -1946,17 +1946,17 @@
  1.1
-0
+2
 %0A%0A    # 
 flus
@@ -1955,220 +1955,247 @@
   # 
-flush all%0A    ser.flushInput()%0A%0A    # first read (used for sync to frame, avoid bad CRC)%0A    ser.read(256)%0A%0A    # main loop%0A    while True:%0A        # maximum size of modbus RTU frame is 256 bytes%0A        frame = 
+init serial object%0A    ser = serial.Serial(args.device, args.baudrate, timeout=inter_char_wait)%0A%0A    # wait serial start and flush all%0A    time.sleep(.5)%0A    ser.read(ser.inWaiting())%0A%0A    # main loop%0A    while True:%0A        frame = bytes(
 ser.
@@ -2203,16 +2203,17 @@
 ead(256)
+)
 %0A       
@@ -2219,31 +2219,23 @@
   # 
-for
+skip
  null 
-size (no data)
+frame
 %0A   

improve flush