#!/usr/bin/env python #------------------------------------------------------------------------------- # FILE: modbus_file.py # PURPOSE: simulate modbus, registers backed by text file # # AUTHOR: Jason G Yates # DATE: 19-Apr-2018 # # MODIFICATIONS: #------------------------------------------------------------------------------- from __future__ import print_function # For python 3.x compatibility with print function import datetime, threading, crcmod, sys, time, os, collections, json from genmonlib.modbusbase import ModbusBase from genmonlib.mythread import MyThread from genmonlib.program_defaults import ProgramDefaults #------------ ModbusBase class ------------------------------------------------- class ModbusFile(ModbusBase): def __init__(self, updatecallback, address = 0x9d, name = "/dev/serial", rate=9600, config = None, inputfile = None): super(ModbusFile, self).__init__(updatecallback = updatecallback, address = address, name = name, rate = rate, config = config) self.Address = address self.Rate = rate self.PortName = name self.InputFile = inputfile self.InitComplete = False self.UpdateRegisterList = updatecallback self.RxPacketCount = 0 self.TxPacketCount = 0 self.ComTimoutError = 0 self.TotalElapsedPacketeTime = 0 self.ComTimoutError = 0 self.CrcError = 0 self.SimulateTime = True self.ModbusStartTime = datetime.datetime.now() # used for com metrics self.Registers = {} self.Strings = {} self.FileData = {} if self.InputFile == None: self.InputFile = os.path.dirname(os.path.realpath(__file__)) + "/modbusregs.txt" if not os.path.isfile(self.InputFile): self.LogError("Error: File not present: " + self.InputFile) self.CommAccessLock = threading.RLock() # lock to synchronize access to the serial port comms self.UpdateRegisterList = updatecallback if not self.ReadInputFile(self.InputFile): self.LogError("ModusFile Init(): Error loading input file: " + self.InputFile) else: if not self.AdjustInputData(): self.LogInfo("Error parsing input data") self.Threads["ReadInputFileThread"] = MyThread(self.ReadInputFileThread, Name = "ReadInputFileThread", start = False) self.Threads["ReadInputFileThread"].Start() self.InitComplete = False #-------------ModbusBase::ReadInputFileThread------------------------------- def ReadInputFileThread(self): while True: if self.IsStopSignaled("ReadInputFileThread"): break self.ReadInputFile(self.InputFile) if not self.AdjustInputData(): self.LogInfo("Error parsing input data") time.sleep(5) #-------------ModbusBase::ProcessMasterSlaveWriteTransaction---------------- def ProcessMasterSlaveWriteTransaction(self, Register, Length, Data): return #-------------ModbusBase::ProcessMasterSlaveTransaction-------------------- def ProcessMasterSlaveTransaction(self, Register, Length, skipupdate = False, ReturnString = False): # TODO need more validation if ReturnString: RegValue = self.Strings.get(Register, "") else: RegValue = self.Strings.get(Register, None) if RegValue == None : RegValue = self.Registers.get(Register, "") if len(RegValue): while (len(RegValue) != Length * 4): if len(RegValue) < Length * 4: RegValue = "0" + RegValue elif len(RegValue) > Length * 4: RegValue = RegValue[1:] self.TxPacketCount += 1 self.RxPacketCount += 1 if self.SimulateTime: time.sleep(.02) if not skipupdate: if not self.UpdateRegisterList == None: self.UpdateRegisterList(Register, RegValue, IsFile = False, IsString = ReturnString) return RegValue #-------------ModbusProtocol::ProcessMasterSlaveFileReadTransaction--------- def ProcessMasterSlaveFileReadTransaction(self, Register, Length, skipupdate = False, file_num = 1, ReturnString = False): RegValue = self.FileData.get(Register, "") self.TxPacketCount += 1 self.RxPacketCount += 1 if self.SimulateTime: time.sleep(.02) RegValue = self.FileData.get(Register, "") if not skipupdate: if not self.UpdateRegisterList == None: self.UpdateRegisterList(Register, RegValue, IsFile = True, IsString = ReturnString) return RegValue #---------- AdjustInputData ---------------------------------------------- def AdjustInputData(self): if not len(self.Registers): self.LogError("Error in AdjustInputData, no data.") return False # No need to adjust data for registers, move on to strings and File data for Reg, Value in self.Strings.items(): RegInt = int(Reg,16) if not len(Value): self.Registers["%04x" % (RegInt)] = "0000" continue if self.StringIsHex(Value): # Not a string, just hex data in a string format for i in range( 0, len(Value), 4): self.Registers["%04x" % (RegInt + int(i / 4))] = Value[i:i+4] else: for i in range(0, len(Value), 2): HiByte = ord(Value[i]) if i + 1 >= len(Value): LowByte = 0 else: LowByte = ord(Value[i+1]) self.Registers["%04x" % (RegInt + int(i / 2))] = "%02x%02x" % (HiByte, LowByte) return True #---------- ReadJSONFile ------------------------------------------------- def ReadJSONFile(self, FileName): if not len(FileName): self.LogError("Error in ReadJSONFile: No Input File") return False try: with open(FileName) as f: data = json.load(f,object_pairs_hook=collections.OrderedDict) self.Registers = data["Registers"] self.Strings = data["Strings"] self.FileData = data["FileData"] return True except Exception as e1: #self.LogErrorLine("Error in ReadJSONFile: " + str(e1)) return False #---------- GeneratorDevice:ReadInputFile -------------------------------- def ReadInputFile(self, FileName): REGISTERS = 0 STRINGS = 1 FILE_DATA = 2 Section = REGISTERS if not len(FileName): self.LogError("Error in ReadInputFile: No Input File") return False if self.ReadJSONFile(FileName): return True try: with open(FileName,"r") as InputFile: #opens file for line in InputFile: line = line.strip() # remove beginning and ending whitespace if not len(line): continue if line[0] == "#": # comment? continue if "Strings :"in line: Section = STRINGS elif "FileData :" in line: Section = FILE_DATA if Section == REGISTERS: line = line.replace('\t', ' ') line = line.replace(' : ', ':') Items = line.split(" ") for entry in Items: RegEntry = entry.split(":") if len(RegEntry) == 2: if len(RegEntry[0]) and len(RegEntry[1]): try: if Section == REGISTERS: HexVal = int(RegEntry[0], 16) HexVal = int(RegEntry[1], 16) #self.LogError("REGISTER: <" + RegEntry[0] + ": " + RegEntry[1] + ">") self.Registers[RegEntry[0]] = RegEntry[1] except: continue elif Section == STRINGS: Items = line.split(" : ") if len(Items) == 2: #self.LogError("STRINGS: <" + Items[0] + ": " + Items[1] + ">") self.Strings[Items[0]] = Items[1] else: pass #self.LogError("Error in STRINGS: " + str(Items)) elif Section == FILE_DATA: Items = line.split(" : ") if len(Items) == 2: #self.LogError("FILEDATA: <" + Items[0] + ": " + Items[1] + ">") self.FileData[Items[0]] = Items[1] else: pass #self.LogError("Error in FILEDATA: " + str(Items)) return True except Exception as e1: self.LogErrorLine("Error in ReadInputFile: " + str(e1)) return False # ---------- ModbusBase::GetCommStats--------------------------------------- def GetCommStats(self): SerialStats = [] SerialStats.append({"Packet Count" : "M: %d, S: %d" % (self.TxPacketCount, self.RxPacketCount)}) if self.CrcError == 0 or self.RxPacketCount == 0: PercentErrors = 0.0 else: PercentErrors = float(self.CrcError) / float(self.RxPacketCount) SerialStats.append({"CRC Errors" : "%d " % self.CrcError}) SerialStats.append({"CRC Percent Errors" : "%.2f" % PercentErrors}) SerialStats.append({"Timeouts Errors" : "%d" % self.ComTimoutError}) # Add serial stats here 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)}) return SerialStats # ---------- ModbusBase::ResetCommStats------------------------------------- def ResetCommStats(self): self.RxPacketCount = 0 self.TxPacketCount = 0 self.TotalElapsedPacketeTime = 0 self.ModbusStartTime = datetime.datetime.now() # used for com metrics pass #------------ModbusBase::Flush---------------------------------------------- def Flush(self): pass #------------ModbusBase::Close---------------------------------------------- def Close(self): pass