Exemple #1
0
class ModbusProtocol(ModbusBase):

    def __init__(self,
        updatecallback,
        address = 0x9d,
        name = "/dev/serial0",
        rate = 9600,
        Parity = None,
        OnePointFiveStopBits = None,
        config = None,
        host = None,
        port = None,
        modbustcp = False):     # True of Modbus TCP, else if TCP then assume serial over TCP (Modbus RTU over serial)

        super(ModbusProtocol, self).__init__(updatecallback = updatecallback, address = address, name = name, rate = rate, config = config)

        try:
            if config == None:
                self.ModbusTCP = modbustcp
                self.Host = host
                self.Port = port
            self.TransactionID = 0
            self.AlternateFileProtocol = False

            if host != None and port != None and self.config == None:
                # in this instance we do not use a config file, but config comes from command line
                self.UseTCP = True

            # ~3000 for 9600               bit time * 10 bits * 10 char * 2 packets + wait time(3000) (convert to ms * 1000)
            self.ModBusPacketTimoutMS = (((((1/float(self.Rate)) * 10.0) * 10.0 * 2.0) *1000.0)  + 3000.0)     # .00208

            self.ModBusPacketTimoutMS += self.AdditionalModbusTimeout * 1000.0

            if self.ModbusTCP:
                self.MIN_PACKET_RESPONSE_LENGTH -= 2
                self.MBUS_RES_PAYLOAD_SIZE_MINUS_LENGTH -= 2
                self.MBUS_FILE_READ_PAYLOAD_SIZE_MINUS_LENGTH -= 2
                self.MBUS_CRC_SIZE = 0
                self.MIN_PACKET_ERR_LENGTH -= 2

            if self.UseTCP:
                self.ModBusPacketTimoutMS = self.ModBusPacketTimoutMS + 2000
            #Starting serial connection
            if self.UseTCP:
                self.Slave = SerialTCPDevice(config = self.config, host = host, port = port)
            else:
                self.Slave = SerialDevice(name = name, rate = rate, Parity = Parity, OnePointFiveStopBits = OnePointFiveStopBits, config = self.config)
            self.Threads = self.MergeDicts(self.Threads, self.Slave.Threads)


        except Exception as e1:
            self.LogErrorLine("Error opening modbus device: " + str(e1))
            self.FatalError("Error opening modbus device.")

        try:
            # CRCMOD library, used for CRC calculations
            self.ModbusCrc = crcmod.predefined.mkCrcFun('modbus')
            self.InitComplete = True
        except Exception as e1:
            self.FatalError("Unable to find crcmod package: " + str(e1))

    #--------------------ModbusProtocol:GetExceptionString----------------------
    def GetExceptionString(self, Code):

        try:

            LookUp = {
                self.MBUS_EXCEP_FUNCTION : "Illegal Function",
                self.MBUS_EXCEP_ADDRESS : "Illegal Address",
                self.MBUS_EXCEP_DATA : "Illegal Data Value",
                self.MBUS_EXCEP_SLAVE_FAIL : "Slave Device Failure",
                self.MBUS_EXCEP_ACK : "Acknowledge",
                self.MBUS_EXCEP_BUSY : "Slave Device Busy",
                self.MBUS_EXCEP_NACK : "Negative Acknowledge",
                self.MBUS_EXCEP_MEM_PE : "Memory Parity Error",
                self.MBUS_EXCEP_GATEWAY : "Gateway Path Unavailable",
                self.MBUS_EXCEP_GATEWAY_TG : "Gateway Target Device Failed to Respond"
             }

            if Code == self.MBUS_EXCEP_FUNCTION:
                 self.ExcepFunction += 1
            elif Code == self.MBUS_EXCEP_ADDRESS:
                self.ExcepAddress += 1
            elif Code == self.MBUS_EXCEP_DATA:
                self.ExcepData += 1
            elif Code == self.MBUS_EXCEP_SLAVE_FAIL:
                self.ExcepSlave += 1
            elif Code == self.MBUS_EXCEP_ACK:
                self.ExcepAck += 1
            elif Code == self.MBUS_EXCEP_BUSY:
                self.ExcepBusy += 1
            elif Code == self.MBUS_EXCEP_NACK:
                self.ExcepNack += 1
            elif Code == self.MBUS_EXCEP_MEM_PE:
                self.ExcepMemPe += 1
            elif Code == self.MBUS_EXCEP_GATEWAY:
                self.ExcepGateway += 1
            elif Code == self.MBUS_EXCEP_GATEWAY_TG:
                self.ExcepGateWayTg += 1

            ReturnString = LookUp.get(Code, "Unknown")
            ReturnString = ReturnString + (": %02x" % Code)
            return ReturnString
        except Exception as e1:
            self.LogErrorLine("Error in GetExceptionString: " + str(e1))

        return ""
    # ---------- ModbusProtocol::CheckResponseAddress---------------------------
    def CheckResponseAddress(self, Address):

        if Address == self.Address:
            return True
        if self.ResponseAddress == None:
            return False
        if Address == self.ResponseAddress:
            return True
        return False

    # ---------- ModbusProtocol::GetPacketFromSlave-----------------------------
    #  This function returns two values, the first is boolean. The seconds is
    #  a packet (list). If the return value is True and an empty packet, then
    #  keep looking because the data has not arrived yet, if return is False there
    #  is and error. If True and a non empty packet then it is valid data
    def GetPacketFromSlave(self, min_response_override = None):

        LocalErrorCount = 0
        Packet = []
        EmptyPacket = []    # empty packet
        try:
            if not len(self.Slave.Buffer):
                return True, EmptyPacket

            if self.ModbusTCP:
                # byte 0:   transaction identifier - copied by server - usually 0
                # byte 1:   transaction identifier - copied by server - usually 0
                # byte 2:   protocol identifier = 0
                # byte 3:   protocol identifier = 0
                # byte 4:   length field (upper byte) = 0 (since all messages are smaller than 256)
                # byte 5:   length field (lower byte) = number of bytes following
                # byte 6:   unit identifier (previously 'slave address')
                # byte 7:   MODBUS function code
                # byte 8 on:    data as needed


                if len(self.Slave.Buffer) < (self.MIN_PACKET_ERR_LENGTH + self.MODBUS_TCP_HEADER_SIZE):
                    return True, EmptyPacket

                # transaction ID must match
                rxID = ((self.Slave.Buffer[0] <<8) | (self.Slave.Buffer[1] & 0xFF))
                if self.CurrentTransactionID != rxID:
                    self.LogError("ModbusTCP transaction ID mismatch: %x %x" % (self.CurrentTransactionID, rxID))
                    self.DiscardByte(reason = "Transaction ID")
                    self.Flush()
                    return False, EmptyPacket
                # protocol ID is zero
                if self.Slave.Buffer[2] != 0 or self.Slave.Buffer[3] != 0:
                    self.LogError("ModbusTCP protocool ID non zero: %x %x" % (self.Slave.Buffer[2], self.Slave.Buffer[3]))
                    self.DiscardByte(reason = "protocol error")
                    self.Flush()
                    return False, EmptyPacket
                # Modbus TCP payload length
                ModbusTCPLength = ((self.Slave.Buffer[4] <<8) | (self.Slave.Buffer[5] & 0xFF))
                if len(self.Slave.Buffer[6:]) != ModbusTCPLength:
                    # more data is needed
                    return True, EmptyPacket

                # remove modbud TCP header
                for i in range(0, self.MODBUS_TCP_HEADER_SIZE):
                    self.Slave.Buffer.pop(0)

            if not self.CheckResponseAddress(self.Slave.Buffer[self.MBUS_OFF_ADDRESS]):
                self.DiscardByte(reason = "Response Address")
                self.Flush()
                return False, EmptyPacket

            if len(self.Slave.Buffer) < self.MIN_PACKET_ERR_LENGTH:
                return True, EmptyPacket   # No full packet ready

            if self.Slave.Buffer[self.MBUS_OFF_COMMAND] & self.MBUS_ERROR_BIT:
                for i in range(0, self.MIN_PACKET_ERR_LENGTH):
                    Packet.append(self.Slave.Buffer.pop(0))  # pop Address, Function, Excetion code, and CRC
                if self.CheckCRC(Packet):
                    self.RxPacketCount += 1
                    self.ModbusException += 1
                    self.LogError("Modbus Exception: " + self.GetExceptionString(Packet[self.MBUS_OFF_EXCEPTION]) + " , Modbus Command: " + ("%02x" % Packet[self.MBUS_OFF_COMMAND]))
                else:
                    self.CrcError += 1
                return False, Packet

            if min_response_override != None:
                if len(self.Slave.Buffer) < min_response_override:
                    return True, EmptyPacket   # No full packet ready
            else:
                if len(self.Slave.Buffer) < self.MIN_PACKET_RESPONSE_LENGTH:
                    return True, EmptyPacket   # No full packet ready

            if  self.Slave.Buffer[self.MBUS_OFF_COMMAND] in [self.MBUS_CMD_READ_REGS]:
                # it must be a read command response
                length = self.Slave.Buffer[self.MBUS_OFF_RESPONSE_LEN]   # our packet tells us the length of the payload
                # if the full length of the packet has not arrived, return and try again
                if (length + self.MBUS_RES_PAYLOAD_SIZE_MINUS_LENGTH) > len(self.Slave.Buffer):
                    return True, EmptyPacket

                for i in range(0, length + self.MBUS_RES_PAYLOAD_SIZE_MINUS_LENGTH):
                    Packet.append(self.Slave.Buffer.pop(0))  # pop Address, Function, Length, message and CRC

                if self.CheckCRC(Packet):
                    self.RxPacketCount += 1
                    return True, Packet
                else:
                    self.CrcError += 1
                    return False, Packet
            elif self.Slave.Buffer[self.MBUS_OFF_COMMAND] in [self.MBUS_CMD_WRITE_REGS]:
                # it must be a write command response
                if len(self.Slave.Buffer) < self.MIN_PACKET_MIN_WRITE_RESPONSE_LENGTH:
                    return True, EmptyPacket
                for i in range(0, self.MIN_PACKET_MIN_WRITE_RESPONSE_LENGTH):
                    Packet.append(self.Slave.Buffer.pop(0))    # address, function, address hi, address low, quantity hi, quantity low, CRC high, crc low

                if self.CheckCRC(Packet):
                    self.RxPacketCount += 1
                    return True, Packet
                else:
                    self.CrcError += 1
                    return False, Packet
            elif self.Slave.Buffer[self.MBUS_OFF_COMMAND] in [self.MBUS_CMD_READ_FILE]:
                length = self.Slave.Buffer[self.MBUS_OFF_RESPONSE_LEN]   # our packet tells us the length of the payload
                if self.Slave.Buffer[self.MBUS_OFF_FILE_TYPE] != self.MBUS_FILE_TYPE_VALUE:
                    self.LogError("Invalid modbus file record type")
                    self.ComValidationError += 1
                    return False, EmptyPacket
                # if the full length of the packet has not arrived, return and try again
                if (length + self.MBUS_FILE_READ_PAYLOAD_SIZE_MINUS_LENGTH) > len(self.Slave.Buffer):
                    return True, EmptyPacket
                # we will copy the entire buffer, this will be validated at a later time
                for i in range(0, len(self.Slave.Buffer)):
                    Packet.append(self.Slave.Buffer.pop(0))  # pop Address, Function, Length, message and CRC

                if len(self.Slave.Buffer):
                    self.LogHexList(self.Slave.Buffer, prefix = "Left Over")

                if self.CheckCRC(Packet):
                    self.RxPacketCount += 1
                    return True, Packet
                else:
                    self.CrcError += 1
                    return False, Packet
            elif self.Slave.Buffer[self.MBUS_OFF_COMMAND] in [self.MBUS_CMD_WRITE_FILE]:
                length = self.Slave.Buffer[self.MBUS_OFF_RESPONSE_LEN]   # our packet tells us the length of the payload
                if self.Slave.Buffer[self.MBUS_OFF_WRITE_FILE_TYPE] != self.MBUS_FILE_TYPE_VALUE:
                    self.LogError("Invalid modbus write file record type")
                    self.ComValidationError += 1
                    return False, EmptyPacket
                # if the full length of the packet has not arrived, return and try again
                if (length + self.MBUS_FILE_READ_PAYLOAD_SIZE_MINUS_LENGTH) > len(self.Slave.Buffer):
                    return True, EmptyPacket
                # we will copy the entire buffer, this will be validated at a later time
                for i in range(0, len(self.Slave.Buffer)):
                    Packet.append(self.Slave.Buffer.pop(0))  # pop Address, Function, Length, message and CRC

                if len(self.Slave.Buffer):
                    self.LogHexList(self.Slave.Buffer, prefix = "Left Over")

                if self.CheckCRC(Packet):
                    self.RxPacketCount += 1
                    return True, Packet
                else:
                    self.CrcError += 1
                    return False, Packe
            else:
                # received a  response to a command we do not support
                self.DiscardByte(reason = "Invalid Modbus command")
                self.Flush()
                return False, EmptyPacket
        except Exception as e1:
            self.LogErrorLine("Error in GetPacketFromSlave: " + str(e1))
            self.ComValidationError += 1
            return False, EmptyPacket
    # ---------- GeneratorDevice::DiscardByte-----------------------------------
    def DiscardByte(self, reason = None):

        discard = self.Slave.DiscardByte()
        if reason == None:
            reason = "Unknown"
        self.LogError("Discarding byte slave: %02x : %s " % (discard, str(reason)))

    #-------------ModbusProtocol::PWT-------------------------------------------
    # called from derived calls to get to overridded function ProcessWriteTransaction
    def _PWT(self, Register, Length, Data, min_response_override = None):

        try:
            with self.CommAccessLock:
                MasterPacket = []

                MasterPacket = self.CreateMasterPacket(Register, length = int(Length), command = self.MBUS_CMD_WRITE_REGS, data = Data)

                if len(MasterPacket) == 0:
                    return False

                #skipupdate=True to skip writing results to cached reg values
                return self.ProcessOneTransaction(MasterPacket, skipupdate = True, min_response_override = min_response_override)
        except Exception as e1:
            self.LogErrorLine("Error in ProcessWriteTransaction: " + str(e1))
            return False

    #-------------ModbusProtocol::ProcessWriteTransaction-----------------------
    def ProcessWriteTransaction(self, Register, Length, Data):
        return self._PWT(Register, Length, Data)

    #-------------ModbusProtocol::PT--------------------------------------------
    # called from derived calls to get to overridded function ProcessTransaction
    def _PT(self, Register, Length, skipupdate = False, ReturnString = False):

        MasterPacket = []

        try:
            with self.CommAccessLock:
                MasterPacket = self.CreateMasterPacket(Register, length = int(Length))

                if len(MasterPacket) == 0:
                    return ""

                return self.ProcessOneTransaction(MasterPacket, skipupdate = skipupdate, ReturnString = ReturnString)     # don't log

        except Exception as e1:
            self.LogErrorLine("Error in ProcessTransaction: " + str(e1))
            return ""

    #-------------ModbusProtocol::ProcessTransaction----------------------------
    def ProcessTransaction(self, Register, Length, skipupdate = False, ReturnString = False):
        return self._PT(Register, Length, skipupdate, ReturnString)

    #-------------ModbusProtocol::ProcessFileReadTransaction--------------------
    def ProcessFileReadTransaction(self, Register, Length, skipupdate = False, file_num = 1, ReturnString = False):

        MasterPacket = []

        try:
            with self.CommAccessLock:
                MasterPacket = self.CreateMasterPacket(Register, length = int(Length), command = self.MBUS_CMD_READ_FILE, file_num = file_num)

                if len(MasterPacket) == 0:
                    return ""

                return self.ProcessOneTransaction(MasterPacket, skipupdate = skipupdate, ReturnString = ReturnString)     # don't log

        except Exception as e1:
            self.LogErrorLine("Error in ProcessFileReadTransaction: " + str(e1))
            return ""

    #-------------ModbusProtocol::ProcessFileWriteTransaction-------------------
    def ProcessFileWriteTransaction(self, Register, Length, Data, file_num = 1, min_response_override = None):

        MasterPacket = []

        try:
            with self.CommAccessLock:
                MasterPacket = self.CreateMasterPacket(Register, length = int(Length), command = self.MBUS_CMD_WRITE_FILE, file_num = file_num, data = Data)

                if len(MasterPacket) == 0:
                    return ""

                #skipupdate=True to skip writing results to cached reg values
                return self.ProcessOneTransaction(MasterPacket, skipupdate = True, min_response_override = min_response_override)

        except Exception as e1:
            self.LogErrorLine("Error in ProcessFileWriteTransaction: " + str(e1))
            return ""

    #------------ModbusProtocol::ProcessOneTransaction--------------------------
    def ProcessOneTransaction(self, MasterPacket, skipupdate = False, ReturnString = False, min_response_override = None):

        try:
            if self.ModbusTCP:
                PacketOffset = self.MODBUS_TCP_HEADER_SIZE
            else:
                PacketOffset = 0

            with self.CommAccessLock:       # this lock should allow calls from multiple threads

                if len(self.Slave.Buffer):
                    self.UnexpectedData += 1
                    self.LogError("Flushing, unexpected data. Likely timeout.")
                    self.Flush()
                self.SendPacketAsMaster(MasterPacket)

                SentTime = datetime.datetime.now()
                while True:
                    # be kind to other processes, we know we are going to have to wait for the packet to arrive
                    # so let's sleep for a bit before we start polling
                    if self.SlowCPUOptimization:
                        time.sleep(0.03)
                    else:
                        time.sleep(0.01)

                    if self.IsStopping:
                        return ""
                    RetVal, SlavePacket = self.GetPacketFromSlave(min_response_override = min_response_override)

                    if RetVal == True and len(SlavePacket) != 0:    # we receive a packet
                        self.TotalElapsedPacketeTime += (self.MillisecondsElapsed(SentTime) / 1000)
                        break
                    if RetVal == False:
                        self.LogError("Error Receiving slave packet for register %04x" % (self.GetRegisterFromPacket(MasterPacket, offset = PacketOffset) ))
                        # Errors returned here are logged in GetPacketFromSlave
                        time.sleep(1)
                        self.Flush()
                        return ""

                    msElapsed = self.MillisecondsElapsed(SentTime)
                    # This normally takes about 30 ms however in some instances it can take up to 950ms
                    # the theory is this is either a delay due to how python does threading, or
                    # delay caused by the generator controller.
                    # each char time is about 1 millisecond (at 9600 baud) so assuming a 10 byte packet
                    # transmitted and a 10 byte received with about 5 char times of silence
                    # in between should give us about 25ms
                    if msElapsed > self.ModBusPacketTimoutMS:
                        self.ComTimoutError += 1
                        self.LogError("Error: timeout receiving slave packet for register %04x Buffer: %d" % (self.GetRegisterFromPacket(MasterPacket, offset = PacketOffset), len(self.Slave.Buffer)) )
                        if len(self.Slave.Buffer):
                            self.LogHexList(self.Slave.Buffer, prefix = "Buffer")
                        self.Flush()
                        return ""

                # update our cached register dict
                ReturnRegValue = self.UpdateRegistersFromPacket(MasterPacket, SlavePacket, SkipUpdate = skipupdate, ReturnString = ReturnString)
                if ReturnRegValue == "Error":
                    self.LogHexList(MasterPacket, prefix = "Master")
                    self.LogHexList(SlavePacket, prefix = "Slave")
                    self.ComValidationError += 1
                    self.Flush()
                    ReturnRegValue = ""

            return ReturnRegValue


        except Exception as e1:
            self.LogErrorLine("Error in ProcessOneTransaction: " + str(e1))
            return ""

    # ---------- ModbusProtocol::MillisecondsElapsed----------------------------
    def MillisecondsElapsed(self, ReferenceTime):

        CurrentTime = datetime.datetime.now()
        Delta = CurrentTime - ReferenceTime
        return Delta.total_seconds() * 1000

    #------------GetRegisterFromPacket -----------------------------------------
    def GetRegisterFromPacket(self, Packet, offset = 0):
        try:
            Register = 0

            if Packet[self.MBUS_OFF_COMMAND + offset] in [self.MBUS_CMD_READ_FILE,self.MBUS_CMD_WRITE_FILE] :
                Register = Packet[self.MBUS_OFF_FILE_RECORD_HI + offset] << 8 |  Packet[self.MBUS_OFF_FILE_RECORD_LOW + offset] & 0x00FF
            else:
                Register = Packet[self.MBUS_OFF_REGISTER_HI + offset] << 8 |  Packet[self.MBUS_OFF_REGISTER_LOW + offset] & 0x00FF
            return Register
        except Exception as e1:
            self.LogErrorLine("Error in GetRegisterFromPacket: " + str(e1))
            return Register

    # ---------- ModbusProtocol::CreateMasterPacket-----------------------------
    # the length is the register length in words, as required by modbus
    # build Packet
    def CreateMasterPacket(self, register, length = 1, command = 0x03, data=[], file_num = 1):

        Packet = []
        try:
            RegisterInt = int(register,16)

            if RegisterInt < self.MIN_REGISTER or RegisterInt > self.MAX_REGISTER:
                self.ComValidationError += 1
                self.LogError("Validation Error: CreateMasterPacket maximum regiseter value exceeded: " + str(register))
                return []
            if file_num < self.MIN_FILE_NUMBER or file_num > self.MAX_FILE_NUMBER:
                self.ComValidationError += 1
                self.LogError("Validation Error: CreateMasterPacket maximum file number value exceeded: " + str(file_num))
                return []

            if command == self.MBUS_CMD_READ_REGS:
                Packet.append(self.Address)                 # address
                Packet.append(command)                      # command
                Packet.append(RegisterInt >> 8)             # reg high
                Packet.append(RegisterInt & 0x00FF)         # reg low
                Packet.append(length >> 8)                  # length high
                Packet.append(length & 0x00FF)              # length low
                CRCValue = self.GetCRC(Packet)
                if CRCValue != None:
                    Packet.append(CRCValue & 0x00FF)            # CRC low
                    Packet.append(CRCValue >> 8)                # CRC high

            elif command == self.MBUS_CMD_WRITE_REGS:
                if len(data) == 0:
                    self.LogError("Validation Error: CreateMasterPacket invalid length (1) %x %x" % (len(data), length))
                    self.ComValidationError += 1
                    return []
                if len(data)/2 != length:
                    self.LogError("Validation Error: CreateMasterPacket invalid length (2) %x %x" % (len(data), length))
                    self.ComValidationError += 1
                    return []
                Packet.append(self.Address)                 # address
                Packet.append(command)                      # command
                Packet.append(RegisterInt >> 8)             # reg higy
                Packet.append(RegisterInt & 0x00FF)         # reg low
                Packet.append(length >> 8)                  # Num of Reg higy
                Packet.append(length & 0x00FF)              # Num of Reg low
                Packet.append(len(data))                    # byte count
                for b in range(0, len(data)):
                    Packet.append(data[b])                  # data
                CRCValue = self.GetCRC(Packet)
                if CRCValue != None:
                    Packet.append(CRCValue & 0x00FF)            # CRC low
                    Packet.append(CRCValue >> 8)                # CRC high

            elif command == self.MBUS_CMD_READ_FILE:

                # Note, we only support one sub request at at time
                if RegisterInt < self.MIN_FILE_RECORD_NUM or RegisterInt > self.MAX_FILE_RECORD_NUM:
                    self.ComValidationError += 1
                    self.LogError("Validation Error: CreateMasterPacket maximum regiseter (record number) value exceeded: " + str(register))
                    return []
                Packet.append(self.Address)                 # address
                Packet.append(command)                      # command
                Packet.append(self.MBUS_READ_FILE_REQUEST_PAYLOAD_LENGTH)     # Byte count
                Packet.append(self.MBUS_FILE_TYPE_VALUE)    # always same value
                Packet.append(file_num >> 8)                # File Number hi
                Packet.append(file_num & 0x00FF)            # File Number low
                Packet.append(RegisterInt >> 8)             # register (file record number) high
                Packet.append(RegisterInt & 0x00FF)         # register (file record number) low
                Packet.append(length >> 8)                  # Length to return hi
                Packet.append(length & 0x00FF)              # Length to return lo
                CRCValue = self.GetCRC(Packet)
                if CRCValue != None:
                    Packet.append(CRCValue & 0x00FF)            # CRC low
                    Packet.append(CRCValue >> 8)                # CRC high
            elif command == self.MBUS_CMD_WRITE_FILE:
                # Note, we only support one sub request at at time
                if RegisterInt < self.MIN_FILE_RECORD_NUM or RegisterInt > self.MAX_FILE_RECORD_NUM:
                    self.ComValidationError += 1
                    self.LogError("Validation Error: CreateMasterPacket maximum regiseter (write record number) value exceeded: " + str(register))
                    return []
                if len(data)/2 != length:
                    self.LogError("Validation Error: CreateMasterPacket invalid length (3) %x %x" % (len(data), length))
                    self.ComValidationError += 1
                    return []
                Packet.append(self.Address)                 # address
                Packet.append(command)                      # command
                Packet.append(length * 2 + self.MBUS_FILE_WRITE_REQ_SIZE_MINUS_LENGTH)  # packet payload size from here
                Packet.append(self.MBUS_FILE_TYPE_VALUE)    # always same value
                Packet.append(file_num >> 8)                # File Number hi
                Packet.append(file_num & 0x00FF)            # File Number low
                Packet.append(RegisterInt >> 8)             # register (file record number) high
                Packet.append(RegisterInt & 0x00FF)         # register (file record number) low
                Packet.append(length >> 8)                  # Length to return hi
                Packet.append(length & 0x00FF)              # Length to return lo
                for b in range(0, len(data)):
                    Packet.append(data[b])                  # data
                CRCValue = self.GetCRC(Packet)
                if CRCValue != None:
                    Packet.append(CRCValue & 0x00FF)            # CRC low
                    Packet.append(CRCValue >> 8)                # CRC high

            else:
                self.LogError("Validation Error: Invalid command in CreateMasterPacket!")
                self.ComValidationError += 1
                return []
        except Exception as e1:
            self.LogErrorLine("Error in CreateMasterPacket: " + str(e1))

        if len(Packet) > self.MAX_MODBUS_PACKET_SIZE:
            self.LogError("Validation Error: CreateMasterPacket: Packet size exceeds max size")
            self.ComValidationError += 1
            return []

        if self.ModbusTCP:
            return self.ConvertToModbusModbusTCP(Packet)
        return Packet

    #-------------ModbusProtocol::GetTransactionID------------------------------
    def GetTransactionID(self):

        ID = self.TransactionID
        self.CurrentTransactionID = ID
        self.TransactionID += 1
        if self.TransactionID > 0xffff:
            self.TransactionID = 0

        return ID

    #-------------ModbusProtocol::ConvertToModbusModbusTCP----------------------
    def ConvertToModbusModbusTCP(self, Packet):

        # byte 0:   transaction identifier - copied by server - usually 0
        # byte 1:   transaction identifier - copied by server - usually 0
        # byte 2:   protocol identifier = 0
        # byte 3:   protocol identifier = 0
        # byte 4:   length field (upper byte) = 0 (since all messages are smaller than 256)
        # byte 5:   length field (lower byte) = number of bytes following
        # byte 6:   unit identifier (previously 'slave address')
        # byte 7:   MODBUS function code
        # byte 8 on:    data as needed
        try:
            if not self.ModbusTCP:
                return Packet
            # remove last two bytes of CRC
            Packet.pop()
            Packet.pop()
            length = len(Packet)
            # byte 6 (slave address) is already provided in the Packet argument
            Packet.insert(0,length & 0xff)            # byte 5:   length field (lower byte) = number of bytes following
            Packet.insert(0,length & 0xff00)          # byte 4:   length field (upper byte) = 0 (since all messages are smaller than 256)
            Packet.insert(0,0)                        # byte 3:   protocol identifier = 0
            Packet.insert(0,0)                        # byte 2:   protocol identifier = 0
            TransactionID = self.GetTransactionID()
            Packet.insert(0, TransactionID & 0xff)      # byte 1:   transaction identifier (low)- copied by server - usually 0
            Packet.insert(0, (TransactionID & 0xff00) >> 8)    # byte 0:   transaction identifier (high)- copied by server - usually 0
            return Packet
        except Exception as e1:
            self.LogErrorLine("Error in CreateModbusTCPHeader: " + str(e1))
            return []

    #-------------ModbusProtocol::SendPacketAsMaster----------------------------
    def SendPacketAsMaster(self, Packet):

        try:
            ByteArray = bytearray(Packet)
            self.Slave.Write(ByteArray)
            self.TxPacketCount += 1
        except Exception as e1:
            self.LogErrorLine("Error in SendPacketAsMaster: " + str(e1))
            self.LogHexList(Packet, prefix = "Packet")

    # ---------- ModbusProtocol::UpdateRegistersFromPacket----------------------
    #    Update our internal register list based on the request/response packet
    def UpdateRegistersFromPacket(self, MasterPacket, SlavePacket, SkipUpdate = False, ReturnString = False):
        return self._URFP(MasterPacket, SlavePacket, SkipUpdate, ReturnString)

    # ---------- ModbusProtocol::URFP-------------------------------------------
    #    Called from UpdateRegistersFromPacket
    def _URFP(self, MasterPacket, SlavePacket, SkipUpdate = False, ReturnString = False):

        try:

            if len(MasterPacket) < self.MIN_PACKET_RESPONSE_LENGTH or len(SlavePacket) < self.MIN_PACKET_RESPONSE_LENGTH:
                self.LogError("Validation Error, length: Master" + str(len(MasterPacket)) + " Slave: " + str(len(SlavePacket)))
                return "Error"

            if self.ModbusTCP:
                PacketOffset = self.MODBUS_TCP_HEADER_SIZE
            else:
                PacketOffset = 0
            if MasterPacket[self.MBUS_OFF_ADDRESS + PacketOffset] != self.Address:
                self.LogError("Validation Error: Invalid address in UpdateRegistersFromPacket (Master)")
                return "Error"
            if not self.CheckResponseAddress(SlavePacket[self.MBUS_OFF_ADDRESS]):
                self.LogError("Validation Error: Invalid address in UpdateRegistersFromPacket (Slave)")
                return "Error"
            if not SlavePacket[self.MBUS_OFF_COMMAND] in [self.MBUS_CMD_READ_REGS, self.MBUS_CMD_WRITE_REGS, self.MBUS_CMD_READ_FILE, self.MBUS_CMD_WRITE_FILE]:
                self.LogError("Validation Error: Unknown Function slave %02x %02x" %  (SlavePacket[self.MBUS_OFF_ADDRESS],SlavePacket[self.MBUS_OFF_COMMAND]))
                return "Error"
            if not MasterPacket[self.MBUS_OFF_COMMAND + PacketOffset] in [self.MBUS_CMD_READ_REGS, self.MBUS_CMD_WRITE_REGS, self.MBUS_CMD_READ_FILE, self.MBUS_CMD_WRITE_FILE]:
                self.LogError("Validation Error: Unknown Function master %02x %02x" %  (MasterPacket[self.MBUS_OFF_ADDRESS],MasterPacket[self.MBUS_OFF_COMMAND]))
                return "Error"

            if MasterPacket[self.MBUS_OFF_COMMAND + PacketOffset] != SlavePacket[self.MBUS_OFF_COMMAND]:
                self.LogError("Validation Error: Command Mismatch :" + str(MasterPacket[self.MBUS_OFF_COMMAND]) + ":" + str(SlavePacket[self.MBUS_OFF_COMMAND]))
                return "Error"

             # get register from master packet
            Register = "%04x" % (self.GetRegisterFromPacket(MasterPacket, offset = PacketOffset))

            if MasterPacket[self.MBUS_OFF_COMMAND + PacketOffset] in [self.MBUS_CMD_WRITE_REGS, self.MBUS_CMD_WRITE_FILE]:
                # get register from slave packet
                SlaveRegister = "%04x" % (self.GetRegisterFromPacket(SlavePacket))
                if SlaveRegister != Register:
                    self.LogError("Validation Error: Master Slave Register Mismatch : " + Register +  ":" + SlaveRegister)
                    return "Error"

            RegisterValue = ""
            RegisterStringValue = ""
            if MasterPacket[self.MBUS_OFF_COMMAND + PacketOffset] == self.MBUS_CMD_READ_REGS:
                # get value from slave packet
                length = SlavePacket[self.MBUS_OFF_RESPONSE_LEN]
                if (length + self.MBUS_RES_PAYLOAD_SIZE_MINUS_LENGTH) > len(SlavePacket):
                    self.LogError("Validation Error: Slave Length : " + length +  ":" + len(SlavePacket))
                    return "Error"

                for i in range(3, length+3):
                    RegisterValue += "%02x" % SlavePacket[i]
                    if ReturnString:
                        if SlavePacket[i]:
                            RegisterStringValue += chr(SlavePacket[i])
                # update register list
                if not SkipUpdate:
                    if not self.UpdateRegisterList == None:
                        if not ReturnString:
                            if not self.UpdateRegisterList(Register, RegisterValue):
                                self.ComSyncError += 1
                                return "Error"
                        else:
                            if not self.UpdateRegisterList(Register, RegisterStringValue, IsString = True):
                                self.ComSyncError += 1
                                return "Error"

            if MasterPacket[self.MBUS_OFF_COMMAND + PacketOffset] == self.MBUS_CMD_READ_FILE:
                payloadLen = SlavePacket[self.MBUS_OFF_FILE_PAYLOAD_LEN]
                if not self.AlternateFileProtocol:
                    # TODO This is emperical
                    payloadLen -= 1
                for i  in range (self.MBUS_OFF_FILE_PAYLOAD, (self.MBUS_OFF_FILE_PAYLOAD + payloadLen)):
                    RegisterValue += "%02x" % SlavePacket[i]
                    if ReturnString:
                        if SlavePacket[i]:
                            RegisterStringValue += chr(SlavePacket[i])

                if not SkipUpdate:
                    if not ReturnString:
                        if not self.UpdateRegisterList(Register, RegisterValue, IsFile = True):
                            self.ComSyncError += 1
                            return "Error"
                    else:
                        if not self.UpdateRegisterList(Register, RegisterStringValue, IsString = True, IsFile = True):
                            self.ComSyncError += 1
                            return "Error"

            if ReturnString:
                return str(RegisterStringValue)
            return str(RegisterValue)
        except Exception as e1:
            self.LogErrorLine("Error in UpdateRegistersFromPacket: " + str(e1))
            return "Error"

     #------------ModbusProtocol::CheckCrc--------------------------------------
    def CheckCRC(self, Packet):

        try:
            if self.ModbusTCP:
                return True

            if len(Packet) == 0:
                return False
            ByteArray = bytearray(Packet[:len(Packet)-2])

            if sys.version_info[0] < 3:
                results = self.ModbusCrc(str(ByteArray))
            else:   # PYTHON3
                results = self.ModbusCrc(ByteArray)

            CRCValue = ( ( Packet[-1] & 0xFF ) << 8 ) | ( Packet[ -2] & 0xFF )
            if results != CRCValue:
                self.LogError("Data Error: CRC check failed: %04x  %04x" % (results, CRCValue))
                return False
            return True
        except Exception as e1:
            self.LogErrorLine("Error in CheckCRC: " + str(e1))
            self.LogHexList(Packet, prefix = "Packet")
            return False

     #------------ModbusProtocol::GetCRC----------------------------------------
    def GetCRC(self, Packet):
        try:
            if len(Packet) == 0:
                return None
            ByteArray = bytearray(Packet)

            if sys.version_info[0] < 3:
                results = self.ModbusCrc(str(ByteArray))
            else:   # PYTHON3
                results = self.ModbusCrc(ByteArray)

            return results
        except Exception as e1:
            self.LogErrorLine("Error in GetCRC: " + str(e1))
            self.LogHexList(Packet, prefix = "Packet")
            return None
    # ---------- ModbusProtocol::GetCommStats-----------------------------------
    def GetCommStats(self):
        SerialStats = []

        try:
            SerialStats.append({"Packet Count" : "M: %d, S: %d" % (self.TxPacketCount, self.RxPacketCount)})

            if self.CrcError == 0 or self.TxPacketCount == 0:
                PercentErrors = 0.0
            else:
                PercentErrors = float(self.CrcError) / float(self.TxPacketCount)

            if self.ComTimoutError == 0 or self.TxPacketCount == 0:
                PercentTimeoutErrors = 0.0
            else:
                PercentTimeoutErrors = float(self.ComTimoutError) / float(self.TxPacketCount)

            SerialStats.append({"CRC Errors" : "%d " % self.CrcError})
            SerialStats.append({"CRC Percent Errors" : ("%.2f" % (PercentErrors * 100)) + "%"})
            SerialStats.append({"Timeout Errors" : "%d" %  self.ComTimoutError})
            SerialStats.append({"Timeout Percent Errors" : ("%.2f" % (PercentTimeoutErrors * 100)) + "%"})
            SerialStats.append({"Modbus Exceptions" : self.ModbusException})
            SerialStats.append({"Validation Errors" : self.ComValidationError})
            SerialStats.append({"Sync Errors" : self.ComSyncError})
            SerialStats.append({"Invalid Data" : self.UnexpectedData})
            # add serial stats
            SerialStats.append({"Discarded Bytes" : "%d" % self.Slave.DiscardedBytes})
            SerialStats.append({"Comm Restarts" : "%d" % self.Slave.Restarts})

            CurrentTime = datetime.datetime.now()
            #
            Delta = CurrentTime - self.ModbusStartTime        # yields a timedelta object
            PacketsPerSecond = float((self.TxPacketCount + self.RxPacketCount)) / float(Delta.total_seconds())
            SerialStats.append({"Packets Per Second" : "%.2f" % (PacketsPerSecond)})

            if self.RxPacketCount:
                AvgTransactionTime = float(self.TotalElapsedPacketeTime / self.RxPacketCount)
                SerialStats.append({"Average Transaction Time" : "%.4f sec" % (AvgTransactionTime)})
        except Exception as e1:
            self.LogErrorLine("Error in GetCommStats: " + str(e1))
        return SerialStats

    # ---------- ModbusProtocol::ResetCommStats---------------------------------
    def ResetCommStats(self):

        try:
            self.RxPacketCount = 0
            self.TxPacketCount = 0
            self.TotalElapsedPacketeTime = 0
            self.ModbusStartTime = datetime.datetime.now()     # used for com metrics
            self.Slave.ResetSerialStats()
        except Exception as e1:
            self.LogErrorLine("Error in ResetCommStats: " + str(e1))

    #------------ModbusProtocol::Flush------------------------------------------
    def Flush(self):

        self.Slave.Flush()

    #------------ModbusProtocol::Close------------------------------------------
    def Close(self):
        self.IsStopping = True
        self.Slave.Close()
Exemple #2
0
class ModbusProtocol(ModbusBase):
    def __init__(self,
        updatecallback,
        address = 0x9d,
        name = "/dev/serial0",
        rate=9600,
        Parity = None,
        OnePointFiveStopBits = None,
        config = None):

        super(ModbusProtocol, self).__init__(updatecallback = updatecallback, address = address, name = name, rate = rate, config = config)

        try:

            # ~3000 for 9600               bit time * 10 bits * 10 char * 2 packets + wait time(3000) (convert to ms * 1000)
            self.ModBusPacketTimoutMS = (((((1/float(rate)) * 10.0) * 10.0 * 2.0) *1000.0)  + 3000.0)     # .00208

            self.ModBusPacketTimoutMS += self.AdditionalModbusTimeout * 1000.0


            if self.UseTCP:
                self.ModBusPacketTimoutMS = self.ModBusPacketTimoutMS
            #Starting serial connection
            if self.UseTCP:
                self.Slave = SerialTCPDevice(config = self.config)
            else:
                self.Slave = SerialDevice(name = name, rate = rate, Parity = Parity, OnePointFiveStopBits = OnePointFiveStopBits, config = self.config)
            self.Threads = self.MergeDicts(self.Threads, self.Slave.Threads)


        except Exception as e1:
            self.LogErrorLine("Error opening serial device: " + str(e1))
            self.FatalError("Error opening serial device.")

        try:
            # CRCMOD library, used for CRC calculations
            self.ModbusCrc = crcmod.predefined.mkCrcFun('modbus')
            self.InitComplete = True
        except Exception as e1:
            self.FatalError("Unable to find crcmod package: " + str(e1))

    #--------------------ModbusProtocol:GetExceptionString----------------------
    def GetExceptionString(self, Code):

        try:

            LookUp = {
                MBUS_EXCEP_FUNCTION : "Illegal Function",
                MBUS_EXCEP_ADDRESS : "Illegal Address",
                MBUS_EXCEP_DATA : "Illegal Data Value",
                MBUS_EXCEP_SLAVE_FAIL : "Slave Device Failure",
                MBUS_EXCEP_ACK : "Acknowledge",
                MBUS_EXCEP_BUSY : "Slave Device Busy",
                MBUS_EXCEP_NACK : "Negative Acknowledge",
                MBUS_EXCEP_MEM_PE : "Memory Parity Error",
                MBUS_EXCEP_GATEWAY : "Gateway Path Unavailable",
                MBUS_EXCEP_GATEWAY_TG : "Gateway Target Device Failed to Respond"
             }

            ReturnString = LookUp.get(Code, "Unknown")
            ReturnString = ReturnString + (": %02x" % Code)
            return ReturnString
        except Exception as e1:
            self.LogErrorLine("Error in GetExceptionString: " + str(e1))

        return ""
    # ---------- ModbusProtocol::CheckResponseAddress---------------------------
    def CheckResponseAddress(self, Address):

        if Address == self.Address:
            return True
        if self.ResponseAddress == None:
            return False
        if Address == self.ResponseAddress:
            return True
        return False

    # ---------- ModbusProtocol::GetPacketFromSlave-----------------------------
    #  This function returns two values, the first is boolean. The seconds is
    #  a packet (list). If the return value is True and an empty packet, then
    #  keep looking because the data has not arrived yet, if return is False there
    #  is and error. If True and a non empty packet then it is valid data
    def GetPacketFromSlave(self):

        LocalErrorCount = 0
        Packet = []
        EmptyPacket = []    # empty packet
        try:
            if not len(self.Slave.Buffer):
                return True, EmptyPacket

            if not self.CheckResponseAddress(self.Slave.Buffer[MBUS_OFF_ADDRESS]):
                self.DiscardByte()
                self.Flush()
                return False, EmptyPacket

            if len(self.Slave.Buffer) < MIN_PACKET_ERR_LENGTH:
                return True, EmptyPacket   # No full packet ready

            if self.Slave.Buffer[MBUS_OFF_COMMAND] & MBUS_ERROR_BIT:
                for i in range(0, MIN_PACKET_ERR_LENGTH):
                    Packet.append(self.Slave.Buffer.pop(0))  # pop Address, Function, Excetion code, and CRC
                if self.CheckCRC(Packet):
                    self.RxPacketCount += 1
                    self.SlaveException += 1
                    self.LogError("Modbus Exception: " + self.GetExceptionString(Packet[MBUS_OFF_EXCEPTION]) + " , Modbus Command: " + ("%02x" % Packet[MBUS_OFF_COMMAND]))
                else:
                    self.CrcError += 1
                return False, Packet

            if len(self.Slave.Buffer) < MIN_PACKET_RESPONSE_LENGTH:
                return True, EmptyPacket   # No full packet ready

            if  self.Slave.Buffer[MBUS_OFF_COMMAND] in [MBUS_CMD_READ_REGS]:
                # it must be a read command response
                length = self.Slave.Buffer[MBUS_OFF_RESPONSE_LEN]   # our packet tells us the length of the payload
                # if the full length of the packet has not arrived, return and try again
                if (length + MBUS_RES_PAYLOAD_SIZE_MINUS_LENGTH) > len(self.Slave.Buffer):
                    return True, EmptyPacket

                for i in range(0, length + MBUS_RES_PAYLOAD_SIZE_MINUS_LENGTH):
                    Packet.append(self.Slave.Buffer.pop(0))  # pop Address, Function, Length, message and CRC

                if self.CheckCRC(Packet):
                    self.RxPacketCount += 1
                    return True, Packet
                else:
                    self.CrcError += 1
                    return False, Packet
            elif self.Slave.Buffer[MBUS_OFF_COMMAND] in [MBUS_CMD_WRITE_REGS]:
                # it must be a write command response
                if len(self.Slave.Buffer) < MIN_PACKET_MIN_WRITE_RESPONSE_LENGTH:
                    return True, EmptyPacket
                for i in range(0, MIN_PACKET_MIN_WRITE_RESPONSE_LENGTH):
                    Packet.append(self.Slave.Buffer.pop(0))    # address, function, address hi, address low, quantity hi, quantity low, CRC high, crc low

                if self.CheckCRC(Packet):
                    self.RxPacketCount += 1
                    return True, Packet
                else:
                    self.CrcError += 1
                    return False, Packet
            elif self.Slave.Buffer[MBUS_OFF_COMMAND] in [MBUS_CMD_READ_FILE]:
                length = self.Slave.Buffer[MBUS_OFF_RESPONSE_LEN]   # our packet tells us the length of the payload
                if self.Slave.Buffer[MBUS_OFF_FILE_TYPE] != MBUS_FILE_TYPE_VALUE:
                    self.LogError("Invalid modbus file record type")
                    self.ComValidationError += 1
                    return False, EmptyPacket
                # if the full length of the packet has not arrived, return and try again
                if (length + MBUS_FILE_READ_PAYLOAD_SIZE_MINUS_LENGTH) > len(self.Slave.Buffer):
                    return True, EmptyPacket
                for i in range(0, length + MBUS_FILE_READ_PAYLOAD_SIZE_MINUS_LENGTH):
                    Packet.append(self.Slave.Buffer.pop(0))  # pop Address, Function, Length, message and CRC

                if self.CheckCRC(Packet):
                    self.RxPacketCount += 1
                    return True, Packet
                else:
                    self.CrcError += 1
                    return False, Packet
            else:
                # received a  response to a command we do not support
                self.DiscardByte()
                self.Flush()
                return False, EmptyPacket
        except Exception as e1:
            self.LogErrorLine("Error in GetPacketFromSlave: " + str(e1))
            self.SlaveException += 1
            return False, EmptyPacket
    # ---------- GeneratorDevice::DiscardByte-----------------------------------
    def DiscardByte(self):

        discard = self.Slave.DiscardByte()
        self.LogError("Discarding byte slave: %02x" % (discard))

    #-------------ModbusProtocol::ProcessMasterSlaveWriteTransaction------------
    def ProcessMasterSlaveWriteTransaction(self, Register, Length, Data):

        try:
            MasterPacket = []

            MasterPacket = self.CreateMasterPacket(Register, length = int(Length), command = MBUS_CMD_WRITE_REGS, data = Data)

            if len(MasterPacket) == 0:
                return False

            return self.ProcessOneTransaction(MasterPacket, skipupdate = True)   # True to skip writing results to cached reg values
        except Exception as e1:
            self.LogErrorLine("Error in ProcessMasterSlaveWriteTransaction: " + str(e1))
            return False

    #-------------ModbusProtocol::ProcessMasterSlaveTransaction-----------------
    def ProcessMasterSlaveTransaction(self, Register, Length, skipupdate = False, ReturnString = False):

        MasterPacket = []

        try:
            MasterPacket = self.CreateMasterPacket(Register, length = int(Length))

            if len(MasterPacket) == 0:
                return ""

            return self.ProcessOneTransaction(MasterPacket, skipupdate = skipupdate, ReturnString = ReturnString)     # don't log

        except Exception as e1:
            self.LogErrorLine("Error in ProcessMasterSlaveTransaction: " + str(e1))
            return ""

    #-------------ModbusProtocol::ProcessMasterSlaveFileReadTransaction---------
    def ProcessMasterSlaveFileReadTransaction(self, Register, Length, skipupdate = False, file_num = 1, ReturnString = False):

        MasterPacket = []

        try:
            MasterPacket = self.CreateMasterPacket(Register, length = int(Length), command = MBUS_CMD_READ_FILE, file_num = file_num)

            if len(MasterPacket) == 0:
                return ""

            return self.ProcessOneTransaction(MasterPacket, skipupdate = skipupdate, ReturnString = ReturnString)     # don't log

        except Exception as e1:
            self.LogErrorLine("Error in ProcessMasterSlaveFileReadTransaction: " + str(e1))
            return ""

    #------------ModbusProtocol::ProcessOneTransaction--------------------------
    def ProcessOneTransaction(self, MasterPacket, skipupdate = False, ReturnString = False):

        try:
            with self.CommAccessLock:       # this lock should allow calls from multiple threads

                if len(self.Slave.Buffer):
                    self.UnexpectedData += 1
                    self.LogError("Flushing, unexpected data. Likely timeout.")
                    self.Flush()
                self.SendPacketAsMaster(MasterPacket)

                SentTime = datetime.datetime.now()
                while True:
                    # be kind to other processes, we know we are going to have to wait for the packet to arrive
                    # so let's sleep for a bit before we start polling
                    if self.SlowCPUOptimization:
                        time.sleep(0.03)
                    else:
                        time.sleep(0.01)

                    if self.IsStopping:
                        return ""
                    RetVal, SlavePacket = self.GetPacketFromSlave()

                    if RetVal == True and len(SlavePacket) != 0:    # we receive a packet
                        self.TotalElapsedPacketeTime += (self.MillisecondsElapsed(SentTime) / 1000)
                        break
                    if RetVal == False:
                        self.LogError("Error Receiving slave packet for register %04x" % (self.GetRegisterFromPacket(MasterPacket) ))
                        # Errors returned here are logged in GetPacketFromSlave
                        self.Flush()
                        return ""

                    msElapsed = self.MillisecondsElapsed(SentTime)
                    # This normally takes about 30 ms however in some instances it can take up to 950ms
                    # the theory is this is either a delay due to how python does threading, or
                    # delay caused by the generator controller.
                    # each char time is about 1 millisecond (at 9600 baud) so assuming a 10 byte packet
                    # transmitted and a 10 byte received with about 5 char times of silence
                    # in between should give us about 25ms
                    if msElapsed > self.ModBusPacketTimoutMS:
                        self.ComTimoutError += 1
                        self.LogError("Error: timeout receiving slave packet for register %04x Buffer:%d" % (self.GetRegisterFromPacket(MasterPacket), len(self.Slave.Buffer)) )
                        if len(self.Slave.Buffer):
                            self.LogError("Buffer: " + str(self.Slave.Buffer))
                        self.Flush()
                        return ""

                # update our cached register dict
                ReturnRegValue = self.UpdateRegistersFromPacket(MasterPacket, SlavePacket, SkipUpdate = skipupdate, ReturnString = ReturnString)
                if ReturnRegValue == "Error":
                    self.LogError("Master: " + str(MasterPacket))
                    self.LogError("Slave: " + str(SlavePacket))
                    self.ComValidationError += 1
                    self.Flush()
                    ReturnRegValue = ""

            return ReturnRegValue


        except Exception as e1:
            self.LogErrorLine("Error in ProcessOneTransaction: " + str(e1))
            return ""

    # ---------- ModbusProtocol::MillisecondsElapsed----------------------------
    def MillisecondsElapsed(self, ReferenceTime):

        CurrentTime = datetime.datetime.now()
        Delta = CurrentTime - ReferenceTime
        return Delta.total_seconds() * 1000

    #------------GetRegisterFromPacket -----------------------------------------
    def GetRegisterFromPacket(self, Packet):
        try:
            Register = 0
            if Packet[MBUS_OFF_COMMAND] == MBUS_CMD_READ_FILE:
                Register = Packet[MBUS_OFF_FILE_RECORD_HI] << 8 |  Packet[MBUS_OFF_FILE_RECORD_LOW] & 0x00FF
            else:
                Register = Packet[MBUS_OFF_REGISTER_HI] << 8 |  Packet[MBUS_OFF_REGISTER_LOW] & 0x00FF
            return Register
        except Exception as e1:
            self.LogErrorLine("Error in GetRegisterFromPacket: " + str(e1))
            return Register

    # ---------- ModbusProtocol::CreateMasterPacket-----------------------------
    # the length is the register length in words, as required by modbus
    # build Packet
    def CreateMasterPacket(self, register, length = 1, command = MBUS_CMD_READ_REGS, data=[], file_num = 1):

        Packet = []
        try:
            RegisterInt = int(register,16)

            if RegisterInt < MIN_REGISTER or RegisterInt > MAX_REGISTER:
                self.ComValidationError += 1
                self.LogError("Validation Error: CreateMasterPacket maximum regiseter value exceeded: " + str(register))
                return []
            if file_num < MIN_FILE_NUMBER or file_num > MAX_FILE_NUMBER:
                self.ComValidationError += 1
                self.LogError("Validation Error: CreateMasterPacket maximum file number value exceeded: " + str(file_num))
                return []

            if command == MBUS_CMD_READ_REGS:
                Packet.append(self.Address)                 # address
                Packet.append(command)                      # command
                Packet.append(RegisterInt >> 8)             # reg higy
                Packet.append(RegisterInt & 0x00FF)         # reg low
                Packet.append(length >> 8)                  # length high
                Packet.append(length & 0x00FF)              # length low
                CRCValue = self.GetCRC(Packet)
                Packet.append(CRCValue & 0x00FF)            # CRC low
                Packet.append(CRCValue >> 8)                # CRC high

            elif command == MBUS_CMD_WRITE_REGS:
                if len(data) == 0:
                    self.LogError("Validation Error: CreateMasterPacket invalid length (1) %x %x" % (len(data), length))
                    self.ComValidationError += 1
                    return []
                if len(data)/2 != length:
                    self.LogError("Validation Error: CreateMasterPacket invalid length (2) %x %x" % (len(data), length))
                    self.ComValidationError += 1
                    return []
                Packet.append(self.Address)                 # address
                Packet.append(command)                      # command
                Packet.append(RegisterInt >> 8)             # reg higy
                Packet.append(RegisterInt & 0x00FF)         # reg low
                Packet.append(length >> 8)                  # Num of Reg higy
                Packet.append(length & 0x00FF)              # Num of Reg low
                Packet.append(len(data))                    # byte count
                for b in range(0, len(data)):
                    Packet.append(data[b])                  # data
                CRCValue = self.GetCRC(Packet)
                Packet.append(CRCValue & 0x00FF)            # CRC low
                Packet.append(CRCValue >> 8)                # CRC high

            elif command == MBUS_CMD_READ_FILE:

                # Note, we only support one sub request at at time
                if RegisterInt < MIN_FILE_RECORD_NUM or RegisterInt > MAX_FILE_RECORD_NUM:
                    self.ComValidationError += 1
                    self.LogError("Validation Error: CreateMasterPacket maximum regiseter (record number) value exceeded: " + str(register))
                    return []
                Packet.append(self.Address)                 # address
                Packet.append(command)                      # command
                Packet.append(MBUS_READ_FILE_REQUEST_PAYLOAD_LENGTH)     # Byte count
                Packet.append(MBUS_FILE_TYPE_VALUE)         # always same value
                Packet.append(file_num >> 8)                # File Number hi
                Packet.append(file_num & 0x00FF)            # File Number low
                Packet.append(RegisterInt >> 8)             # register (file record number) high
                Packet.append(RegisterInt & 0x00FF)         # register (file record number) low
                Packet.append(length >> 8)                  # Length to return hi
                Packet.append(length & 0x00FF)              # Length to return lo
                CRCValue = self.GetCRC(Packet)
                Packet.append(CRCValue & 0x00FF)            # CRC low
                Packet.append(CRCValue >> 8)                # CRC high
            else:
                self.LogError("Validation Error: Invalid command in CreateMasterPacket!")
                self.ComValidationError += 1
                return []
        except Exception as e1:
            self.LogErrorLine("Error in CreateMasterPacket: " + str(e1))

        if len(Packet) > MAX_MODBUS_PACKET_SIZE:
            self.LogError("Validation Error: CreateMasterPacket: Packet size exceeds max size")
            self.ComValidationError += 1
            return []

        return Packet

    #-------------ModbusProtocol::SendPacketAsMaster----------------------------
    def SendPacketAsMaster(self, Packet):

        try:
            ByteArray = bytearray(Packet)
            self.Slave.Write(ByteArray)
            self.TxPacketCount += 1
        except Exception as e1:
            self.LogErrorLine("Error in SendPacketAsMaster: " + str(e1))
            self.LogError("Packet: " + str(Packet))

    # ---------- ModbusProtocol::UpdateRegistersFromPacket----------------------
    #    Update our internal register list based on the request/response packet
    def UpdateRegistersFromPacket(self, MasterPacket, SlavePacket, SkipUpdate = False, ReturnString = False):

        try:
            if len(MasterPacket) < MIN_PACKET_RESPONSE_LENGTH or len(SlavePacket) < MIN_PACKET_RESPONSE_LENGTH:
                self.LogError("Validation Error, length: Master" + str(len(MasterPacket)) + " Slave: " + str(len(SlavePacket)))
                return "Error"

            if MasterPacket[MBUS_OFF_ADDRESS] != self.Address:
                self.LogError("Validation Error: Invalid address in UpdateRegistersFromPacket (Master)")
                return "Error"
            if not self.CheckResponseAddress(SlavePacket[MBUS_OFF_ADDRESS]):
                self.LogError("Validation Error: Invalid address in UpdateRegistersFromPacket (Slave)")
                return "Error"
            if not SlavePacket[MBUS_OFF_COMMAND] in [MBUS_CMD_READ_REGS, MBUS_CMD_WRITE_REGS, MBUS_CMD_READ_FILE]:
                self.LogError("Validation Error: Unknown Function slave %02x %02x" %  (SlavePacket[MBUS_OFF_ADDRESS],SlavePacket[MBUS_OFF_COMMAND]))
                return "Error"
            if not MasterPacket[MBUS_OFF_COMMAND] in [MBUS_CMD_READ_REGS, MBUS_CMD_WRITE_REGS, MBUS_CMD_READ_FILE]:
                self.LogError("Validation Error: Unknown Function master %02x %02x" %  (MasterPacket[MBUS_OFF_ADDRESS],MasterPacket[MBUS_OFF_COMMAND]))
                return "Error"

            if MasterPacket[MBUS_OFF_COMMAND] != SlavePacket[MBUS_OFF_COMMAND]:
                self.LogError("Validation Error: Command Mismatch :" + str(MasterPacket[MBUS_OFF_COMMAND]) + ":" + str(SlavePacket[MBUS_OFF_COMMAND]))
                return "Error"

             # get register from master packet
            Register = "%04x" % (self.GetRegisterFromPacket(MasterPacket))
            if MasterPacket[MBUS_OFF_COMMAND] == MBUS_CMD_WRITE_REGS:
                # get register from slave packet
                SlaveRegister = "%04x" % (self.GetRegisterFromPacket(SlavePacket))
                if SlaveRegister != Register:
                    self.LogError("Validation Error: Master Slave Register Mismatch : " + Register +  ":" + SlaveRegister)
                    return "Error"

            RegisterValue = ""
            RegisterStringValue = ""
            if MasterPacket[MBUS_OFF_COMMAND] == MBUS_CMD_READ_REGS:
                # get value from slave packet
                length = SlavePacket[MBUS_OFF_RESPONSE_LEN]
                if (length + MBUS_RES_PAYLOAD_SIZE_MINUS_LENGTH) > len(SlavePacket):
                    self.LogError("Validation Error: Slave Lenght : " + length +  ":" + len(SlavePacket))
                    return "Error"

                for i in range(3, length+3):
                    RegisterValue += "%02x" % SlavePacket[i]
                    if ReturnString:
                        if SlavePacket[i]:
                            RegisterStringValue += chr(SlavePacket[i])
                # update register list
                if not SkipUpdate:
                    if not self.UpdateRegisterList == None:
                        if not ReturnString:
                            self.UpdateRegisterList(Register, RegisterValue)
                        else:
                            self.UpdateRegisterList(Register, RegisterStringValue, IsString = True)

            if MasterPacket[MBUS_OFF_COMMAND] == MBUS_CMD_READ_FILE:
                payloadLen = SlavePacket[MBUS_OFF_FILE_PAYLOAD_LEN]
                payloadLen -= 1
                for i  in range (MBUS_OFF_FILE_PAYLOAD, MBUS_OFF_FILE_PAYLOAD + payloadLen):
                    RegisterValue += "%02x" % SlavePacket[i]
                    if ReturnString:
                        if SlavePacket[i]:
                            RegisterStringValue += chr(SlavePacket[i])

                if not SkipUpdate:
                    if not ReturnString:
                        self.UpdateRegisterList(Register, RegisterValue, IsFile = True)
                    else:
                        self.UpdateRegisterList(Register, RegisterStringValue, IsString = True, IsFile = True)
                pass
            if ReturnString:
                return str(RegisterStringValue)
            return str(RegisterValue)
        except Exception as e1:
            self.LogErrorLine("Error in UpdateRegistersFromPacket: " + str(e1))
            return "Error"

     #------------ModbusProtocol::CheckCrc--------------------------------------
    def CheckCRC(self, Packet):

        try:
            if len(Packet) == 0:
                return False
            ByteArray = bytearray(Packet[:len(Packet)-2])

            if sys.version_info[0] < 3:
                results = self.ModbusCrc(str(ByteArray))
            else:   # PYTHON3
                results = self.ModbusCrc(ByteArray)

            CRCValue = ( ( Packet[-1] & 0xFF ) << 8 ) | ( Packet[ -2] & 0xFF )
            if results != CRCValue:
                self.LogError("Data Error: CRC check failed: %04x  %04x" % (results, CRCValue))
                return False
            return True
        except Exception as e1:
            self.LogErrorLine("Error in CheckCRC: " + str(e1))
            self.LogError("Packet: " + str(Packet))
            return False

     #------------ModbusProtocol::GetCRC----------------------------------------
    def GetCRC(self, Packet):
        try:
            if len(Packet) == 0:
                return False
            ByteArray = bytearray(Packet)

            if sys.version_info[0] < 3:
                results = self.ModbusCrc(str(ByteArray))
            else:   # PYTHON3
                results = self.ModbusCrc(ByteArray)

            return results
        except Exception as e1:
            self.LogErrorLine("Error in GetCRC: " + str(e1))
            self.LogError("Packet: " + str(Packet))
            return 0
    # ---------- ModbusProtocol::GetCommStats-----------------------------------
    def GetCommStats(self):
        SerialStats = []

        try:
            SerialStats.append({"Packet Count" : "M: %d, S: %d" % (self.TxPacketCount, self.RxPacketCount)})

            if self.CrcError == 0 or self.TxPacketCount == 0:
                PercentErrors = 0.0
            else:
                PercentErrors = float(self.CrcError) / float(self.TxPacketCount)

            if self.ComTimoutError == 0 or self.TxPacketCount == 0:
                PercentTimeoutErrors = 0.0
            else:
                PercentTimeoutErrors = float(self.ComTimoutError) / float(self.TxPacketCount)

            SerialStats.append({"CRC Errors" : "%d " % self.CrcError})
            SerialStats.append({"CRC Percent Errors" : ("%.2f" % (PercentErrors * 100)) + "%"})
            SerialStats.append({"Packet Timeouts" : "%d" %  self.ComTimoutError})
            SerialStats.append({"Packet Timeouts Percent Errors" : ("%.2f" % (PercentTimeoutErrors * 100)) + "%"})
            SerialStats.append({"Modbus Exceptions" : self.SlaveException})
            SerialStats.append({"Validation Errors" : self.ComValidationError})
            SerialStats.append({"Invalid Data" : self.UnexpectedData})
            # add serial stats
            SerialStats.append({"Discarded Bytes" : "%d" % self.Slave.DiscardedBytes})
            SerialStats.append({"Comm Restarts" : "%d" % self.Slave.Restarts})

            CurrentTime = datetime.datetime.now()
            #
            Delta = CurrentTime - self.ModbusStartTime        # yields a timedelta object
            PacketsPerSecond = float((self.TxPacketCount + self.RxPacketCount)) / float(Delta.total_seconds())
            SerialStats.append({"Packets Per Second" : "%.2f" % (PacketsPerSecond)})

            if self.RxPacketCount:
                AvgTransactionTime = float(self.TotalElapsedPacketeTime / self.RxPacketCount)
                SerialStats.append({"Average Transaction Time" : "%.4f sec" % (AvgTransactionTime)})
        except Exception as e1:
            self.LogErrorLine("Error in GetCommStats: " + str(e1))
        return SerialStats

    # ---------- ModbusProtocol::ResetCommStats---------------------------------
    def ResetCommStats(self):

        try:
            self.RxPacketCount = 0
            self.TxPacketCount = 0
            self.TotalElapsedPacketeTime = 0
            self.ModbusStartTime = datetime.datetime.now()     # used for com metrics
            self.Slave.ResetSerialStats()
        except Exception as e1:
            self.LogErrorLine("Error in ResetCommStats: " + str(e1))

    #------------ModbusProtocol::Flush------------------------------------------
    def Flush(self):

        self.Slave.Flush()

    #------------ModbusProtocol::Close------------------------------------------
    def Close(self):
        self.IsStopping = True
        self.Slave.Close()