From 44bcf73ba8190d55c94490785ce220bccb4ac458 Mon Sep 17 00:00:00 2001 From: Albert Brandl Date: Mon, 28 Feb 2011 11:43:17 +0000 Subject: [PATCH 001/243] Refactored and simplified calculation of RTU frames, added missing unit tests --- pymodbus/bit_read_message.py | 20 ++------ pymodbus/bit_write_message.py | 41 ++------------- pymodbus/diag_message.py | 20 +------- pymodbus/file_message.py | 14 ++--- pymodbus/other_message.py | 82 +++--------------------------- pymodbus/pdu.py | 25 +++++---- pymodbus/register_read_message.py | 53 +++++-------------- pymodbus/register_write_message.py | 40 ++------------- pymodbus/transaction.py | 8 ++- test/test_pdu.py | 24 +++++++++ test/test_transaction.py | 53 ++++++++++++++++--- 11 files changed, 126 insertions(+), 254 deletions(-) diff --git a/pymodbus/bit_read_message.py b/pymodbus/bit_read_message.py index 35b64dbae..ed37379bb 100644 --- a/pymodbus/bit_read_message.py +++ b/pymodbus/bit_read_message.py @@ -7,19 +7,12 @@ from pymodbus.pdu import ModbusRequest from pymodbus.pdu import ModbusResponse from pymodbus.pdu import ModbusExceptions as merror -from pymodbus.utilities import pack_bitstring, unpack_bitstring, rtuFrameSize +from pymodbus.utilities import pack_bitstring, unpack_bitstring class ReadBitsRequestBase(ModbusRequest): ''' Base class for Messages Requesting bit values ''' - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a request to read multiple bits. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 8) in the request. - ''' - return 8 + _rtu_frame_size = 8 def __init__(self, address, count, **kwargs): ''' Initializes the read request data @@ -55,14 +48,7 @@ def __str__(self): class ReadBitsResponseBase(ModbusResponse): ''' Base class for Messages responding to bit-reading values ''' - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a response containing multiple bits. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes in this response. - ''' - return rtuFrameSize(buffer, 2) + _rtu_byte_count_pos = 2 def __init__(self, values, **kwargs): ''' Initializes a new instance diff --git a/pymodbus/bit_write_message.py b/pymodbus/bit_write_message.py index d529f7eb5..78b7d907a 100644 --- a/pymodbus/bit_write_message.py +++ b/pymodbus/bit_write_message.py @@ -10,7 +10,7 @@ from pymodbus.pdu import ModbusResponse from pymodbus.pdu import ModbusExceptions as merror from pymodbus.exceptions import ParameterException -from pymodbus.utilities import pack_bitstring, unpack_bitstring, rtuFrameSize +from pymodbus.utilities import pack_bitstring, unpack_bitstring #---------------------------------------------------------------------------# # Local Constants @@ -38,15 +38,7 @@ class WriteSingleCoilRequest(ModbusRequest): will not affect the coil. ''' function_code = 5 - - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a request to write a single coil. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 8) in the request. - ''' - return 8 + _rtu_frame_size = 8 def __init__(self, address=None, value=None, **kwargs): ''' Initializes a new instance @@ -103,15 +95,7 @@ class WriteSingleCoilResponse(ModbusResponse): state has been written. ''' function_code = 5 - - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a response containing a single coil. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 8) in the response. - ''' - return 8 + _rtu_frame_size = 8 def __init__(self, address=None, value=None, **kwargs): ''' Initializes a new instance @@ -159,15 +143,7 @@ class WriteMultipleCoilsRequest(ModbusRequest): corresponding output to be ON. A logical '0' requests it to be OFF." ''' function_code = 15 - - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a request to write multiple coils. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes in the request. - ''' - return rtuFrameSize(buffer, 6) + _rtu_byte_count_pos = 6 def __init__(self, address=None, values=None, **kwargs): ''' Initializes a new instance @@ -233,14 +209,7 @@ class WriteMultipleCoilsResponse(ModbusResponse): quantity of coils forced. ''' function_code = 15 - - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a response containing multiple registers. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 8) in the response. - ''' - return 8 + _rtu_frame_size = 8 def __init__(self, address=None, count=None, **kwargs): ''' Initializes a new instance diff --git a/pymodbus/diag_message.py b/pymodbus/diag_message.py index 863babae4..ab9856f5f 100644 --- a/pymodbus/diag_message.py +++ b/pymodbus/diag_message.py @@ -29,15 +29,7 @@ class DiagnosticStatusRequest(ModbusRequest): This is a base class for all of the diagnostic request functions ''' function_code = 0x08 - - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a diagnostic status request. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 8) in the request. - ''' - return 8 + _rtu_frame_size = 8 def __init__(self): ''' @@ -80,15 +72,7 @@ class DiagnosticStatusResponse(ModbusResponse): and how to execute a request ''' function_code = 0x08 - - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a response containing the diagnostic status. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 8) in the response. - ''' - return 8 + _rtu_frame_size = 8 def __init__(self): ''' diff --git a/pymodbus/file_message.py b/pymodbus/file_message.py index d2a2e0ede..c28d97a6e 100644 --- a/pymodbus/file_message.py +++ b/pymodbus/file_message.py @@ -28,15 +28,7 @@ class ReadFifoQueueRequest(ModbusRequest): The function reads the queue contents, but does not clear them. ''' function_code = 0x18 - - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a request to read a FIFO queue. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 6) in the request. - ''' - return 6 + _rtu_frame_size = 6 def __init__(self, address): ''' Initializes a new instance @@ -83,8 +75,8 @@ class ReadFifoQueueResponse(ModbusResponse): ''' function_code = 0x18 - @staticmethod - def calculateRtuFrameSize(buffer): + @classmethod + def calculateRtuFrameSize(cls, buffer): ''' Calculates the size of a response containing a FIFO queue. :param buffer: A buffer containing the data that have been received. diff --git a/pymodbus/other_message.py b/pymodbus/other_message.py index 076631172..2ac49a238 100644 --- a/pymodbus/other_message.py +++ b/pymodbus/other_message.py @@ -8,7 +8,6 @@ from pymodbus.pdu import ModbusRequest from pymodbus.pdu import ModbusResponse from pymodbus.device import ModbusControlBlock -from utilities import rtuFrameSize from pymodbus.exceptions import * _MCB = ModbusControlBlock() @@ -24,15 +23,7 @@ class ReadExceptionStatusRequest(ModbusRequest): known (no output reference is needed in the function). ''' function_code = 0x07 - - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a request to read the exception status. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 4) in the request. - ''' - return 4 + _rtu_frame_size = 4 def __init__(self): ''' Initializes a new instance @@ -75,14 +66,7 @@ class ReadExceptionStatusResponse(ModbusResponse): Exception Status outputs are device specific. ''' function_code = 0x07 - - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a response containing the exception status. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 5) in the response. - ''' - return 5 + _rtu_frame_size = 5 def __init__(self, status): ''' Initializes a new instance @@ -137,16 +121,7 @@ class GetCommEventCounterRequest(ModbusRequest): Clear Counters and Diagnostic Register (code 00 0A). ''' function_code = 0x0b - - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a request to read the communication event - counter. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 4) in the request. - ''' - return 4 + _rtu_frame_size = 4 def __init__(self): ''' Initializes a new instance @@ -189,15 +164,7 @@ class GetCommEventCounterResponse(ModbusResponse): all zeros. ''' function_code = 0x0b - - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a response containing a communication event - counter. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 8) in the response. - ''' - return 8 + _rtu_frame_size = 8 def __init__(self, count): ''' Initializes a new instance @@ -255,16 +222,7 @@ class GetCommEventLogRequest(ModbusRequest): from the field. ''' function_code = 0x0c - - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a request to read the communication event - log. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 4) in the request. - ''' - return 4 + _rtu_frame_size = 4 def __init__(self): ''' Initializes a new instance @@ -311,15 +269,7 @@ class GetCommEventLogResponse(ModbusResponse): defines the total length of the data in these four field ''' function_code = 0x0c - - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a response to read the communication event - log. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes in the response. - ''' - return rtuFrameSize(buffer, 3) + _rtu_byte_count_pos = 3 def __init__(self, **kwargs): ''' Initializes a new instance @@ -379,15 +329,7 @@ class ReportSlaveIdRequest(ModbusRequest): status, and other information specific to a remote device. ''' function_code = 0x11 - - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a request to report the slave ID. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 4) in the request. - ''' - return 4 + _rtu_frame_size = 4 def __init__(self): ''' Initializes a new instance @@ -427,15 +369,7 @@ class ReportSlaveIdResponse(ModbusResponse): data contents are specific to each type of device. ''' function_code = 0x11 - - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a response containing a slave ID. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes in the response. - ''' - return rtuFrameSize(buffer, 2) + _rtu_byte_count_pos = 2 def __init__(self, identifier, status=True): ''' Initializes a new instance diff --git a/pymodbus/pdu.py b/pymodbus/pdu.py index d0e69be56..655a7a50f 100644 --- a/pymodbus/pdu.py +++ b/pymodbus/pdu.py @@ -4,7 +4,7 @@ from pymodbus.interfaces import Singleton from pymodbus.exceptions import NotImplementedException from pymodbus.constants import Defaults - +from utilities import rtuFrameSize #---------------------------------------------------------------------------# # Logging #---------------------------------------------------------------------------# @@ -62,14 +62,21 @@ def decode(self, data): ''' raise NotImplementedException() - @staticmethod - def calculateRtuFrameSize(buffer): + @classmethod + def calculateRtuFrameSize(cls, buffer): ''' Calculates the size of a PDU. :param buffer: A buffer containing the data that have been received. :returns: The number of bytes in the PDU. ''' - raise NotImplementedException() + try: + return cls._rtu_frame_size + except AttributeError: + try: + return rtuFrameSize(buffer, cls._rtu_byte_count_pos) + except AttributeError: + raise NotImplementedException( + "Cannot determine RTU frame size for %s" % cls.__name__) class ModbusRequest(ModbusPDU): ''' Base class for a modbus request PDU ''' @@ -115,15 +122,7 @@ class ModbusExceptions(Singleton): class ExceptionResponse(ModbusResponse): ''' Base class for a modbus exception PDU ''' ExceptionOffset = 0x80 - - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of an exception response. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 5) in the request. - ''' - return 5 + _rtu_frame_size = 5 def __init__(self, function_code, exception_code=None, **kwargs): ''' Initializes the modbus exception response diff --git a/pymodbus/register_read_message.py b/pymodbus/register_read_message.py index e2d5fee6d..baa246eaa 100644 --- a/pymodbus/register_read_message.py +++ b/pymodbus/register_read_message.py @@ -6,21 +6,12 @@ from pymodbus.pdu import ModbusRequest from pymodbus.pdu import ModbusResponse from pymodbus.pdu import ModbusExceptions as merror -from utilities import rtuFrameSize class ReadRegistersRequestBase(ModbusRequest): ''' Base class for reading a modbus register ''' - - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a request to read multiple registers. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 8) in the request. - ''' - return 8 + _rtu_frame_size = 8 def __init__(self, address, count, **kwargs): ''' Initializes a new instance @@ -58,14 +49,7 @@ class ReadRegistersResponseBase(ModbusResponse): Base class for responsing to a modbus register read ''' - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a response containing multiple registers. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes in this response. - ''' - return rtuFrameSize(buffer, 2) + _rtu_byte_count_pos = 2 def __init__(self, values, **kwargs): ''' Initializes a new instance @@ -223,14 +207,7 @@ class ReadWriteMultipleRegistersRequest(ModbusRequest): ''' function_code = 23 - @staticmethod - def calculateRtuFrameSize(buffer): - '''Calculates the size of a request to read and write multiple registers. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes in the request. - ''' - return rtuFrameSize(buffer, 10) + _rtu_byte_count_pos = 10 def __init__(self, read_address, read_count, write_address, write_registers, **kwargs): @@ -288,12 +265,16 @@ def execute(self, context): return self.doException(merror.IllegalValue) if (self.write_byte_count != self.write_count * 2): return self.doException(merror.IllegalValue) - if not context.validate(self.function_code, self.write_address, self.write_count): + if not context.validate(self.function_code, self.write_address, + self.write_count): return self.doException(merror.IllegalAddress) - if not context.validate(self.function_code, self.read_address, self.read_count): + if not context.validate(self.function_code, self.read_address, + self.read_count): return self.doException(merror.IllegalAddress) - context.setValues(self.function_code, self.write_address, self.write_registers) - registers = context.getValues(self.function_code, self.read_address, self.read_count) + context.setValues(self.function_code, self.write_address, + self.write_registers) + registers = context.getValues(self.function_code, self.read_address, + self.read_count) return ReadWriteMultipleRegistersResponse(registers) def __str__(self): @@ -301,7 +282,8 @@ def __str__(self): :returns: A string representation of the instance ''' - params = (self.read_address, self.read_count, self.write_address, self.write_count) + params = (self.read_address, self.read_count, self.write_address, + self.write_count) return "ReadWriteNRegisterRequest R(%d,%d) W(%d,%d)" % params class ReadWriteMultipleRegistersResponse(ModbusResponse): @@ -311,14 +293,7 @@ class ReadWriteMultipleRegistersResponse(ModbusResponse): follow in the read data field. ''' function_code = 23 - - def calculateRtuFrameSize(buffer): - '''Calculates the size of a response containing multiple registers. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes in this response. - ''' - return rtuFrameSize(buffer, 2) + _rtu_byte_count_pos = 2 def __init__(self, values=None, **kwargs): ''' Initializes a new instance diff --git a/pymodbus/register_write_message.py b/pymodbus/register_write_message.py index 3f6352fc2..21c19a82d 100644 --- a/pymodbus/register_write_message.py +++ b/pymodbus/register_write_message.py @@ -6,7 +6,6 @@ from pymodbus.pdu import ModbusRequest from pymodbus.pdu import ModbusResponse from pymodbus.pdu import ModbusExceptions as merror -from utilities import rtuFrameSize from pymodbus.exceptions import ParameterException class WriteSingleRegisterRequest(ModbusRequest): @@ -19,15 +18,7 @@ class WriteSingleRegisterRequest(ModbusRequest): numbered 1 is addressed as 0. ''' function_code = 6 - - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a request to write a single register. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 8) in the request. - ''' - return 8 + _rtu_frame_size = 8 def __init__(self, address=None, value=None, **kwargs): ''' Initializes a new instance @@ -81,15 +72,7 @@ class WriteSingleRegisterResponse(ModbusResponse): register contents have been written. ''' function_code = 6 - - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a response containing a single register. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 8) in the response. - ''' - return 8 + _rtu_frame_size = 8 def __init__(self, address=None, value=None, **kwargs): ''' Initializes a new instance @@ -136,15 +119,7 @@ class WriteMultipleRegistersRequest(ModbusRequest): Data is packed as two bytes per register. ''' function_code = 16 - - @staticmethod - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a request to write multiple registers. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes in the request. - ''' - return rtuFrameSize(buffer, 6) + _rtu_byte_count_pos = 6 def __init__(self, address=None, values=None, **kwargs): ''' Initializes a new instance @@ -211,14 +186,7 @@ class WriteMultipleRegistersResponse(ModbusResponse): quantity of registers written. ''' function_code = 16 - - def calculateRtuFrameSize(buffer): - ''' Calculates the size of a response containing multiple registers. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes (always 8) in the response. - ''' - return 8 + _rtu_frame_size = 8 def __init__(self, address=None, count=None, **kwargs): ''' Initializes a new instance diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index c3675f9ee..e74985134 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -308,8 +308,12 @@ def checkFrame(self): ''' try: self.populateHeader() - return len(self.__buffer) >= self.__header['len'] - except IndexError: + frame_size = self.__header['len'] + data = self.__buffer[:frame_size - 2] + crc = self.__buffer[frame_size - 2:frame_size] + crc_val = ord(crc[0]) * 2**8 + ord(crc[1]) + return checkCRC(data, crc_val) + except (IndexError, KeyError): return False def advanceFrame(self): diff --git a/test/test_pdu.py b/test/test_pdu.py index 9a80474d5..cff84c7f7 100644 --- a/test/test_pdu.py +++ b/test/test_pdu.py @@ -51,6 +51,30 @@ def testRequestExceptionFactory(self): result = request.doException(error) self.assertEqual(str(result), "Exception Response (129, %d)" % error) + def testCalculateRtuFrameSize(self): + ''' Test the calculation of Modbus/RTU frame sizes ''' + self.assertRaises(NotImplementedException, + ModbusRequest.calculateRtuFrameSize, "") + ModbusRequest._rtu_frame_size = 5 + self.assertEqual(ModbusRequest.calculateRtuFrameSize(""), 5) + del ModbusRequest._rtu_frame_size + + ModbusRequest._rtu_byte_count_pos = 2 + self.assertEqual(ModbusRequest.calculateRtuFrameSize( + "\x11\x01\x05\xcd\x6b\xb2\x0e\x1b\x45\xe6"), 10) + del ModbusRequest._rtu_byte_count_pos + + self.assertRaises(NotImplementedException, + ModbusResponse.calculateRtuFrameSize, "") + ModbusResponse._rtu_frame_size = 12 + self.assertEqual(ModbusResponse.calculateRtuFrameSize(""), 12) + del ModbusResponse._rtu_frame_size + ModbusResponse._rtu_byte_count_pos = 2 + self.assertEqual(ModbusResponse.calculateRtuFrameSize( + "\x11\x01\x05\xcd\x6b\xb2\x0e\x1b\x45\xe6"), 10) + del ModbusResponse._rtu_byte_count_pos + + #---------------------------------------------------------------------------# # Main #---------------------------------------------------------------------------# diff --git a/test/test_transaction.py b/test/test_transaction.py index 6d6a25f82..5067bc764 100644 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -157,30 +157,67 @@ def testTCPFramerPacket(self): # RTU tests #---------------------------------------------------------------------------# def testRTUFramerTransactionReady(self): - ''' Test a rtu frame transaction ''' - msg = ":\xab\xcd\x12\x34\x12\x34\xaa\xaa\r\n" - self._rtu.addToFrame(msg) - self.assertTrue(self._rtu.checkFrame()) + ''' Test if the checks for a complete frame work ''' + self.assertFalse(self._rtu.isFrameReady()) + + msg_parts = ["\x00\x01\x00", "\x00\x00\x01\xfc\x1b"] + self._rtu.addToFrame(msg_parts[0]) + self.assertTrue(self._rtu.isFrameReady()) + self.assertFalse(self._rtu.checkFrame()) + + self._rtu.addToFrame(msg_parts[1]) self.assertTrue(self._rtu.isFrameReady()) - # test a full transaction + self.assertTrue(self._rtu.checkFrame()) def testRTUFramerTransactionFull(self): ''' Test a full rtu frame transaction ''' - pass + msg = "\x00\x01\x00\x00\x00\x01\xfc\x1b" + stripped_msg = msg[1:-2] + self._rtu.addToFrame(msg) + self.assertTrue(self._rtu.checkFrame()) + result = self._rtu.getFrame() + self.assertEqual(stripped_msg, result) + self._rtu.advanceFrame() def testRTUFramerTransactionHalf(self): ''' Test a half completed rtu frame transaction ''' - pass + msg_parts = ["\x00\x01\x00", "\x00\x00\x01\xfc\x1b"] + stripped_msg = "".join(msg_parts)[1:-2] + self._rtu.addToFrame(msg_parts[0]) + self.assertFalse(self._rtu.checkFrame()) + self._rtu.addToFrame(msg_parts[1]) + self.assertTrue(self._rtu.isFrameReady()) + self.assertTrue(self._rtu.checkFrame()) + result = self._rtu.getFrame() + self.assertEqual(stripped_msg, result) + self._rtu.advanceFrame() def testRTUFramerPopulate(self): ''' Test a rtu frame packet build ''' request = ModbusRequest() + msg = "\x00\x01\x00\x00\x00\x01\xfc\x1b" + self._rtu.addToFrame(msg) + self._rtu.populateHeader() self._rtu.populateResult(request) + + header_dict = self._rtu._ModbusRtuFramer__header + self.assertEqual(len(msg), header_dict['len']) + self.assertEqual(ord(msg[0]), header_dict['uid']) + self.assertEqual(msg[-2:], header_dict['crc']) + self.assertEqual(0x00, request.unit_id) def testRTUFramerPacket(self): ''' Test a rtu frame packet build ''' - pass + old_encode = ModbusRequest.encode + ModbusRequest.encode = lambda self: '' + message = ModbusRequest() + message.unit_id = 0xff + message.function_code = 0x01 + expected = "\xff\x01\x81\x80" # only header + CRC - no data + actual = self._rtu.buildPacket(message) + self.assertEqual(expected, actual) + ModbusRequest.encode = old_encode #---------------------------------------------------------------------------# # ASCII tests From 71d0b00e64aa870f70362120d7c009b2c2ae7070 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 28 Feb 2011 18:01:09 +0000 Subject: [PATCH 002/243] - fixing a few small issues - bringing a few areas of coverage back up --- pymodbus/pdu.py | 12 +++++------- pymodbus/transaction.py | 2 +- test/test_factory.py | 12 ++++++++++++ test/test_interfaces.py | 1 + 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/pymodbus/pdu.py b/pymodbus/pdu.py index 655a7a50f..1bfb96cf0 100644 --- a/pymodbus/pdu.py +++ b/pymodbus/pdu.py @@ -69,14 +69,12 @@ def calculateRtuFrameSize(cls, buffer): :param buffer: A buffer containing the data that have been received. :returns: The number of bytes in the PDU. ''' - try: + if hasattr(cls, '_rtu_frame_size'): return cls._rtu_frame_size - except AttributeError: - try: - return rtuFrameSize(buffer, cls._rtu_byte_count_pos) - except AttributeError: - raise NotImplementedException( - "Cannot determine RTU frame size for %s" % cls.__name__) + elif hasattr(cls, '_rtu_byte_count_pos'): + return rtuFrameSize(buffer, cls._rtu_byte_count_pos) + else: raise NotImplementedException( + "Cannot determine RTU frame size for %s" % cls.__name__) class ModbusRequest(ModbusPDU): ''' Base class for a modbus request PDU ''' diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index e74985134..8e0a07a86 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -311,7 +311,7 @@ def checkFrame(self): frame_size = self.__header['len'] data = self.__buffer[:frame_size - 2] crc = self.__buffer[frame_size - 2:frame_size] - crc_val = ord(crc[0]) * 2**8 + ord(crc[1]) + crc_val = (ord(crc[0]) << 8) + ord(crc[1]) return checkCRC(data, crc_val) except (IndexError, KeyError): return False diff --git a/test/test_factory.py b/test/test_factory.py index d846e2a4a..47823236d 100644 --- a/test/test_factory.py +++ b/test/test_factory.py @@ -68,6 +68,18 @@ def tearDown(self): del self.request del self.response + def testResponseLookup(self): + ''' Test a working response factory lookup ''' + for func, _ in self.response: + response = self.client.lookupPduClass(func) + self.assertNotEqual(response, None) + + def testRequestLookup(self): + ''' Test a working request factory lookup ''' + for func, _ in self.request: + request = self.client.lookupPduClass(func) + self.assertNotEqual(request, None) + def testResponseWorking(self): ''' Test a working response factory decoders ''' for func, msg in self.response: diff --git a/test/test_interfaces.py b/test/test_interfaces.py index ce002ba0f..c9cb84a2b 100644 --- a/test/test_interfaces.py +++ b/test/test_interfaces.py @@ -30,6 +30,7 @@ def testModbusDecoderInterface(self): x = None instance = IModbusDecoder() self.assertRaises(NotImplementedException, lambda: instance.decode(x)) + self.assertRaises(NotImplementedException, lambda: instance.lookupPduClass(x)) def testModbusFramerInterface(self): ''' Test that the base class isn't implemented ''' From bced3d60a209f262f680be3634269ad402862e80 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Tue, 1 Mar 2011 15:58:58 +0000 Subject: [PATCH 003/243] adding installer test script --- examples/tools/test-install.sh | 58 ++++++++++++++++++++++++++++++++++ setup.py | 5 +-- 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100755 examples/tools/test-install.sh diff --git a/examples/tools/test-install.sh b/examples/tools/test-install.sh new file mode 100755 index 000000000..489f9f146 --- /dev/null +++ b/examples/tools/test-install.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# ------------------------------------------------------------------ # +# This script is used to test that we can create a virtual +# environment and install the latest version of the given package +# from pypi. +# ------------------------------------------------------------------ # +ENVIRONMENT="example" +PACKAGE="pymodbus" + +# ------------------------------------------------------------------ # +# Preflight Tests +# ------------------------------------------------------------------ # +if [[ "`which pip`" != "" ]]; then + INSTALL="pip install -qU" +elif [[ "`which easy_install`" != "" ]]; then + INSTALL="easy_install -qU" +else + echo -e "\E[31m" + echo "\E[31mPlease install distutils before continuting" + echo "wget http://peak.telecommunity.com/dist/ez_setup.py | sudo python" + echo -e "\E[0m" + exit -1 +fi + +if [[ "`which virtualenv`" == "" ]]; then + echo -e "\E[31m" + echo "Please install virtualenv before continuting" + echo "sudo easy_install virtualenv" + echo -e "\E[0m" + exit -1 +fi + +# ------------------------------------------------------------------ # +# Setup +# ------------------------------------------------------------------ # +echo -n "Setting up test..." +virtualenv -q --no-site-packages --distribute ${ENVIRONMENT} +source ${ENVIRONMENT}/bin/activate +echo -e "\E[32mPassed\E[0m" + +# ------------------------------------------------------------------ # +# Main Test +# ------------------------------------------------------------------ # +echo -n "Testing package installation..." +${INSTALL} ${PACKAGE} +if [[ "$?" == "0" ]]; then + echo -e "\E[32mPassed\E[0m" +else + echo -e "\E[31mPassed\E[0m" +fi + +# ------------------------------------------------------------------ # +# Cleanup +# ------------------------------------------------------------------ # +echo -n "Tearing down test..." +deactivate +rm -rf ${ENVIRONMENT} +echo -e "\E[32mPassed\E[0m" diff --git a/setup.py b/setup.py index b1b96fe1c..30ce33a0d 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,8 @@ class BuildApiDocs(Command): def initialize_options(self): ''' options setup ''' - pass + if not os.path.exists('./build'): + os.mkdir('./build') def finalize_options(self): ''' options teardown ''' @@ -42,7 +43,7 @@ def finalize_options(self): def run(self): ''' command runner ''' old_cwd = os.getcwd() - directories = (d for d in os.listdir('./api') if not d.startswith('.')) + directories = (d for d in os.listdir('./doc/api') if not d.startswith('.')) for entry in directories: os.chdir('./doc/api/%s' % entry) os.system('python build.py') From 3debbf0bcc65321a15b0e815f61bba0b7bd93c7c Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Tue, 1 Mar 2011 20:07:05 +0000 Subject: [PATCH 004/243] enabling the checksum tests in check frame, fixing tests --- examples/functional/database-slave-context.py | 4 ++- pymodbus/transaction.py | 13 +++---- test/test_file_message.py | 6 ++++ test/test_transaction.py | 34 ++++++++++++------- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/examples/functional/database-slave-context.py b/examples/functional/database-slave-context.py index 5778e51a0..c4269fcbd 100644 --- a/examples/functional/database-slave-context.py +++ b/examples/functional/database-slave-context.py @@ -12,12 +12,14 @@ class DatabaseSlaveContextTest(ContextRunner, unittest.TestCase): def setUp(self): ''' Initializes the test environment ''' - os.remove('./' + self.__database.split('///')[1]) + path = './' + self.__database.split('///')[1] + if os.path.exists(path): os.remove(path) self.context = DatabaseSlaveContext(database=self.__database) self.initialize() def tearDown(self): ''' Cleans up the test environment ''' + self.context._connection.close() self.shutdown() #---------------------------------------------------------------------------# diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 8e0a07a86..f8aec7637 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -473,9 +473,8 @@ def checkFrame(self): self.__header['len'] = end self.__header['uid'] = int(self.__buffer[1:3], 16) self.__header['lrc'] = int(self.__buffer[end-2:end], 16) - #data = self.__buffer[start:end-2] - #return checkLRC(data, self.__header['lrc']) - return True + data = self.__buffer[start:end-2] + return checkLRC(data, self.__header['lrc']) return False def advanceFrame(self): @@ -627,11 +626,9 @@ def checkFrame(self): if (end != -1): self.__header['len'] = end self.__header['uid'] = struct.unpack('>B', self.__buffer[1:2]) - self.__header['crc'] = self.__buffer[end-2:end] - #self.__header['crc'] = struct.unpack('>H', self.__buffer[end-2:end]) - #data = self.__buffer[start:end-1] - #return checkCRC(data, self.__header['crc']) - return True + self.__header['crc'] = struct.unpack('>H', self.__buffer[end-2:end])[0] + data = self.__buffer[start:end-2] + return checkCRC(data, self.__header['crc']) return False def advanceFrame(self): diff --git a/test/test_file_message.py b/test/test_file_message.py index 59d242118..34fe260d0 100644 --- a/test/test_file_message.py +++ b/test/test_file_message.py @@ -80,6 +80,12 @@ def testReadFifoQueueResponseDecode(self): handle.decode(message) self.assertEqual(handle.values, [1,2,3,4]) + def testRtuFrameSize(self): + ''' Test that the read fifo queue response can decode ''' + message = '\x00\n\x00\x08\x00\x01\x00\x02\x00\x03\x00\x04' + result = ReadFifoQueueResponse.calculateRtuFrameSize(message) + self.assertEqual(result, 14) + #---------------------------------------------------------------------------# # Main #---------------------------------------------------------------------------# diff --git a/test/test_transaction.py b/test/test_transaction.py index 5067bc764..2b2b5baeb 100644 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -5,9 +5,9 @@ from pymodbus.transaction import * from pymodbus.factory import ServerDecoder -class SimpleDataStoreTest(unittest.TestCase): +class ModbusTransactionTest(unittest.TestCase): ''' - This is the unittest for the pymod.transaction module + This is the unittest for the pymodbus.transaction module ''' #---------------------------------------------------------------------------# @@ -35,23 +35,31 @@ def tearDown(self): def testModbusTransactionManagerTID(self): ''' Test the tcp transaction manager TID ''' self.assertEqual(id(self._manager), id(ModbusTransactionManager())) - for i in range(10): - self.assertEqual(i+1, self._manager.getNextTID()) + for tid in range(1, self._manager.getNextTID() + 10): + self.assertEqual(tid+2, self._manager.getNextTID()) self._manager.resetTID() self.assertEqual(1, self._manager.getNextTID()) - def testModbusTransactionManagerTransaction(self): + def testGetTransactionManagerTransaction(self): ''' Test the tcp transaction manager ''' - class Request: - pass + class Request: pass self._manager.resetTID() handle = Request() handle.transaction_id = self._manager.getNextTID() handle.message = "testing" - self._manager.addTransaction(handle) result = self._manager.getTransaction(handle.transaction_id) self.assertEqual(handle.message, result.message) + + def testDeleteTransactionManagerTransaction(self): + ''' Test the tcp transaction manager ''' + class Request: pass + self._manager.resetTID() + handle = Request() + handle.transaction_id = self._manager.getNextTID() + handle.message = "testing" + + self._manager.addTransaction(handle) self._manager.delTransaction(handle.transaction_id) self.assertEqual(None, self._manager.getTransaction(handle.transaction_id)) @@ -237,7 +245,7 @@ def testASCIIFramerTransactionReady(self): def testASCIIFramerTransactionFull(self): ''' Test a full ascii frame transaction ''' - msg ='sss:01030000000AF2\r\n' + msg ='sss:01030000000A0C\r\n' pack = a2b_hex(msg[6:-4]) self._ascii.addToFrame(msg) self.assertTrue(self._ascii.checkFrame()) @@ -248,7 +256,7 @@ def testASCIIFramerTransactionFull(self): def testASCIIFramerTransactionHalf(self): ''' Test a half completed ascii frame transaction ''' msg1 = "sss:abcd1234" - msg2 = "1234aaaa\r\n" + msg2 = "1234aae6\r\n" pack = a2b_hex(msg1[6:] + msg2[:-4]) self._ascii.addToFrame(msg1) self.assertFalse(self._ascii.checkFrame()) @@ -283,7 +291,7 @@ def testASCIIFramerPacket(self): #---------------------------------------------------------------------------# def testBinaryFramerTransactionReady(self): ''' Test a binary frame transaction ''' - msg = '\x7b\x01\x03\x00\x00\x00\x05\x85\xc9\x7d' + msg = '\x7b\x01\x03\x00\x00\x00\x05\x55\xd5\x7d' self.assertFalse(self._binary.isFrameReady()) self.assertFalse(self._binary.checkFrame()) self._binary.addToFrame(msg) @@ -296,7 +304,7 @@ def testBinaryFramerTransactionReady(self): def testBinaryFramerTransactionFull(self): ''' Test a full binary frame transaction ''' - msg = '\x7b\x01\x03\x00\x00\x00\x05\x85\xc9\x7d' + msg = '\x7b\x01\x03\x00\x00\x00\x05\x55\xd5\x7d' pack = msg[3:-3] self._binary.addToFrame(msg) self.assertTrue(self._binary.checkFrame()) @@ -307,7 +315,7 @@ def testBinaryFramerTransactionFull(self): def testBinaryFramerTransactionHalf(self): ''' Test a half completed binary frame transaction ''' msg1 = '\x7b\x01\x03\x00' - msg2 = '\x00\x00\x05\x85\xc9\x7d' + msg2 = '\x00\x00\x05\x55\xd5\x7d' pack = msg1[3:] + msg2[:-3] self._binary.addToFrame(msg1) self.assertFalse(self._binary.checkFrame()) From 2c4d68e3d34aa51a8d60234c437ece77966d8c9e Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 2 Mar 2011 15:19:46 +0000 Subject: [PATCH 005/243] * Updating documentation * Adding code to handle messages that do not respond * Fixes issue 41 --- doc/current.coverage | 45 ++++++++++--------- doc/sphinx/library/datastore/context.rst | 20 +++++++++ doc/sphinx/library/datastore/database.rst | 16 +++++++ doc/sphinx/library/datastore/index.rst | 15 +++++++ doc/sphinx/library/datastore/modredis.rst | 16 +++++++ doc/sphinx/library/datastore/remote.rst | 16 +++++++ .../{datastore.rst => datastore/store.rst} | 9 ++-- doc/sphinx/library/index.rst | 2 +- doc/sphinx/library/utilities.rst | 6 +++ pymodbus/datastore/context.py | 1 + pymodbus/diag_message.py | 1 + pymodbus/pdu.py | 15 ++++++- pymodbus/server/async.py | 9 ++-- pymodbus/server/sync.py | 9 ++-- 14 files changed, 143 insertions(+), 37 deletions(-) create mode 100644 doc/sphinx/library/datastore/context.rst create mode 100644 doc/sphinx/library/datastore/database.rst create mode 100644 doc/sphinx/library/datastore/index.rst create mode 100644 doc/sphinx/library/datastore/modredis.rst create mode 100644 doc/sphinx/library/datastore/remote.rst rename doc/sphinx/library/{datastore.rst => datastore/store.rst} (71%) diff --git a/doc/current.coverage b/doc/current.coverage index b909184db..78a0a5b04 100644 --- a/doc/current.coverage +++ b/doc/current.coverage @@ -1,32 +1,35 @@ Name Stmts Exec Cover Missing --------------------------------------------------------------- -pymodbus 9 9 100% -pymodbus.bit_read_message 65 65 100% -pymodbus.bit_write_message 92 68 73% 74-82, 89, 130, 154, 184-193, 200-201, 232, 239 +pymodbus 12 12 100% +pymodbus.bit_read_message 68 68 100% +pymodbus.bit_write_message 94 94 100% pymodbus.client 1 1 100% -pymodbus.client.async 44 22 50% 49-51, 56-57, 62-63, 70, 76-80, 88-89, 97-102, 113-116 -pymodbus.client.common 35 35 100% -pymodbus.constants 18 18 100% -pymodbus.datastore 109 65 59% 82-83, 87, 96, 105, 113, 120, 127-129, 142, 153-155, 164-165, 173-174, 187-193, 202-203, 212, 220-221, 261, 265-266, 276-277, 287-288, 297-298, 311, 321, 330, 337, 375 +pymodbus.client.common 44 44 100% +pymodbus.client.sync 154 61 39% 38-57, 64, 71-73, 99, 104, 112, 120, 130-132, 142-144, 148, 152, 159, 184-194, 199-201, 209-211, 219, 226, 251-258, 263, 271-273, 281, 288, 308-317, 326-330, 337-345, 350-352, 360-362, 370, 377 +pymodbus.constants 21 21 100% +pymodbus.datastore 5 5 100% +pymodbus.datastore.context 47 47 100% +pymodbus.datastore.remote 31 31 100% +pymodbus.datastore.store 63 63 100% pymodbus.device 128 128 100% -pymodbus.diag_message 183 168 91% 49, 62, 104, 131, 165-168, 175, 190-193, 216, 246 -pymodbus.events 58 29 50% 20, 27, 70-73, 100-105, 112-116, 124-130, 146, 153-154, 180, 187-188 +pymodbus.diag_message 186 186 100% +pymodbus.events 60 60 100% pymodbus.exceptions 22 22 100% -pymodbus.factory 52 52 100% -pymodbus.file_message 36 33 91% 45, 52, 62 -pymodbus.interfaces 36 36 100% +pymodbus.factory 56 56 100% +pymodbus.file_message 41 41 100% +pymodbus.interfaces 43 43 100% pymodbus.internal 1 1 100% -pymodbus.other_message 129 60 46% 30, 35, 42, 49-50, 57, 74-75, 82, 89, 96-97, 127, 132, 139, 146-147, 154, 171-173, 180-181, 188-189, 196-197, 226, 231, 238, 245-251, 258, 277-281, 288-293, 300-308, 315-316, 331, 336, 343, 350-351, 358, 373-375, 382-383, 390-391, 398-399 -pymodbus.pdu 58 58 100% -pymodbus.register_read_message 120 94 78% 45, 86, 93, 124-125, 172-173, 244-249, 262-270, 277-278, 311-313, 320 -pymodbus.register_write_message 86 58 67% 52-59, 66, 104-105, 151-154, 162-171, 178-179, 210, 217-218 +pymodbus.other_message 143 143 100% +pymodbus.pdu 65 65 100% +pymodbus.register_read_message 124 124 100% +pymodbus.register_write_message 88 88 100% pymodbus.server 0 0 100% -pymodbus.transaction 231 140 60% 49-61, 226-236, 317-318, 336, 343-346, 376-385, 392-397, 518-527, 578-582, 592-605, 613-614, 623, 632, 639, 649, 669-678, 686-691, 702-704 -pymodbus.utilities 63 63 100% -pymodbus.version 4 4 100% +pymodbus.transaction 257 202 78% 49-61, 226-236, 404-413, 545-554, 623, 699-708, 733 +pymodbus.utilities 67 67 100% +pymodbus.version 13 13 100% --------------------------------------------------------------- -TOTAL 1580 1229 77% +TOTAL 1834 1686 91% ---------------------------------------------------------------------- -Ran 95 tests in 0.158s +Ran 154 tests in 0.292s OK diff --git a/doc/sphinx/library/datastore/context.rst b/doc/sphinx/library/datastore/context.rst new file mode 100644 index 000000000..16a62e2e1 --- /dev/null +++ b/doc/sphinx/library/datastore/context.rst @@ -0,0 +1,20 @@ +:mod:`context` --- Modbus Server Contexts +============================================================ + +.. module:: context + :synopsis: Modbus Server Contexts + +.. moduleauthor:: Galen Collins +.. sectionauthor:: Galen Collins + +API Documentation +------------------- + +.. automodule:: pymodbus.datastore.context + +.. autoclass:: ModbusSlaveContext + :members: + +.. autoclass:: ModbusServerContext + :members: + diff --git a/doc/sphinx/library/datastore/database.rst b/doc/sphinx/library/datastore/database.rst new file mode 100644 index 000000000..97daf8b31 --- /dev/null +++ b/doc/sphinx/library/datastore/database.rst @@ -0,0 +1,16 @@ +:mod:`database` --- Database Slave Context +============================================================ + +.. module:: database + :synopsis: Database Slave Context + +.. moduleauthor:: Galen Collins +.. sectionauthor:: Galen Collins + +API Documentation +------------------- + +.. automodule:: pymodbus.datastore.database + +.. autoclass:: DatabaseSlaveContext + :members: diff --git a/doc/sphinx/library/datastore/index.rst b/doc/sphinx/library/datastore/index.rst new file mode 100644 index 000000000..b180bc352 --- /dev/null +++ b/doc/sphinx/library/datastore/index.rst @@ -0,0 +1,15 @@ + +Server Datastores and Contexts +==================================== + +*The following are the API documentation strings taken +from the sourcecode* + +.. toctree:: + :maxdepth: 2 + + store.rst + context.rst + remote.rst + database.rst + modredis.rst diff --git a/doc/sphinx/library/datastore/modredis.rst b/doc/sphinx/library/datastore/modredis.rst new file mode 100644 index 000000000..0b5c96b14 --- /dev/null +++ b/doc/sphinx/library/datastore/modredis.rst @@ -0,0 +1,16 @@ +:mod:`modredis` --- Redis Slave Context +============================================================ + +.. module:: modredis + :synopsis: Redis Slave Context + +.. moduleauthor:: Galen Collins +.. sectionauthor:: Galen Collins + +API Documentation +------------------- + +.. automodule:: pymodbus.datastore.modredis + +.. autoclass:: RedisSlaveContext + :members: diff --git a/doc/sphinx/library/datastore/remote.rst b/doc/sphinx/library/datastore/remote.rst new file mode 100644 index 000000000..551f0b17b --- /dev/null +++ b/doc/sphinx/library/datastore/remote.rst @@ -0,0 +1,16 @@ +:mod:`remote` --- Remote Slave Context +============================================================ + +.. module:: remote + :synopsis: Remote Slave Context + +.. moduleauthor:: Galen Collins +.. sectionauthor:: Galen Collins + +API Documentation +------------------- + +.. automodule:: pymodbus.datastore.remote + +.. autoclass:: RemoteSlaveContext + :members: diff --git a/doc/sphinx/library/datastore.rst b/doc/sphinx/library/datastore/store.rst similarity index 71% rename from doc/sphinx/library/datastore.rst rename to doc/sphinx/library/datastore/store.rst index d8a3d2379..1a5ff2d67 100644 --- a/doc/sphinx/library/datastore.rst +++ b/doc/sphinx/library/datastore/store.rst @@ -1,7 +1,7 @@ -:mod:`datastore` --- Datastore for Modbus Server Context +:mod:`store` --- Datastore for Modbus Server Context ============================================================ -.. module:: datastore +.. module:: store :synopsis: Datastore for Modbus Server Context .. moduleauthor:: Galen Collins @@ -10,7 +10,7 @@ API Documentation ------------------- -.. automodule:: pymodbus.datastore +.. automodule:: pymodbus.datastore.store .. autoclass:: BaseModbusDataBlock :members: @@ -21,6 +21,3 @@ API Documentation .. autoclass:: ModbusSparseDataBlock :members: -.. autoclass:: ModbusServerContext - :members: - diff --git a/doc/sphinx/library/index.rst b/doc/sphinx/library/index.rst index 54eee1681..de915c255 100644 --- a/doc/sphinx/library/index.rst +++ b/doc/sphinx/library/index.rst @@ -14,7 +14,7 @@ from the sourcecode* sync-client.rst async-client.rst constants.rst - datastore.rst + datastore/index.rst diag-message.rst device.rst factory.rst diff --git a/doc/sphinx/library/utilities.rst b/doc/sphinx/library/utilities.rst index e8562d409..d8e78527e 100644 --- a/doc/sphinx/library/utilities.rst +++ b/doc/sphinx/library/utilities.rst @@ -12,6 +12,10 @@ API Documentation .. automodule:: pymodbus.utilities +.. autofunction:: default + +.. autofunction:: dict_property + .. autofunction:: pack_bitstring .. autofunction:: unpack_bitstring @@ -25,3 +29,5 @@ API Documentation .. autofunction:: computeLRC .. autofunction:: checkLRC + +.. autofunction:: rtuFrameSize diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index 2cc1812c0..7902c2f6a 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -127,3 +127,4 @@ def __getitem__(self, slave): if self.__slaves.has_key(slave): return self.__slaves.get(slave) else: raise ParameterException("slave does not exist, or is out of range") + diff --git a/pymodbus/diag_message.py b/pymodbus/diag_message.py index ab9856f5f..d807684b8 100644 --- a/pymodbus/diag_message.py +++ b/pymodbus/diag_message.py @@ -334,6 +334,7 @@ class ForceListenOnlyModeResponse(DiagnosticStatusResponse): This does not send a response ''' sub_function_code = 0x0004 + should_respond = False def __init__(self): ''' Initializer to block a return response diff --git a/pymodbus/pdu.py b/pymodbus/pdu.py index b41ef80b1..cb967e2be 100644 --- a/pymodbus/pdu.py +++ b/pymodbus/pdu.py @@ -95,7 +95,20 @@ def doException(self, exception): return ExceptionResponse(self.function_code, exception) class ModbusResponse(ModbusPDU): - ''' Base class for a modbus response PDU ''' + ''' Base class for a modbus response PDU + + .. attribute:: should_respond + + A flag that indicates if this response returns a result back + to the client issuing the request + + .. attribute:: _rtu_frame_size + + Indicates the size of the modbus rtu response used for + calculating how much to read. + ''' + + should_respond = True def __init__(self, **kwargs): ''' Proxy to the lower level initializer ''' diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index 59b176037..d1ca0fa33 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -78,10 +78,11 @@ def _send(self, message): :param message: The unencoded modbus response ''' - self.factory.control.Counter.BusMessage += 1 - pdu = self.framer.buildPacket(message) - _logger.debug('send: %s' % b2a_hex(pdu)) - return self.transport.write(pdu) + if message.should_respond: + self.factory.control.Counter.BusMessage += 1 + pdu = self.framer.buildPacket(message) + _logger.debug('send: %s' % b2a_hex(pdu)) + return self.transport.write(pdu) class ModbusServerFactory(ServerFactory): diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index 0646afb41..8d5e40f87 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -85,10 +85,11 @@ def send(self, message): :param message: The unencoded modbus response ''' - #self.server.control.Counter.BusMessage += 1 - pdu = self.framer.buildPacket(message) - _logger.debug('send: %s' % b2a_hex(pdu)) - return self.request.send(pdu) + if message.should_respond: + #self.server.control.Counter.BusMessage += 1 + pdu = self.framer.buildPacket(message) + _logger.debug('send: %s' % b2a_hex(pdu)) + return self.request.send(pdu) def decode(self, message): ''' Decodes a request packet From 05506c530407c08df92607b2d1cf2f3046ce02d3 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 2 Mar 2011 18:09:13 +0000 Subject: [PATCH 006/243] cleaning up the build tools a bit --- doc/api/doxygen/.doxygen | 8 ++++---- doc/api/doxygen/build.py | 6 ++++-- doc/api/epydoc/build.py | 8 ++++++-- doc/api/pydoc/build.py | 20 +++++++++++-------- doc/api/pydoctor/build.py | 8 ++++++-- setup.cfg | 9 ++++++++- setup.py | 42 ++++++++++++++++++++++++++++++++++++++- 7 files changed, 81 insertions(+), 20 deletions(-) diff --git a/doc/api/doxygen/.doxygen b/doc/api/doxygen/.doxygen index c829eac2d..500ed003a 100644 --- a/doc/api/doxygen/.doxygen +++ b/doc/api/doxygen/.doxygen @@ -517,20 +517,20 @@ QUIET = YES # generated by doxygen. Possible values are YES and NO. If left blank # NO is used. -WARNINGS = YES +WARNINGS = NO # If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings # for undocumented members. If EXTRACT_ALL is set to YES then this flag will # automatically be disabled. -WARN_IF_UNDOCUMENTED = YES +WARN_IF_UNDOCUMENTED = NO # If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for # potential errors in the documentation, such as not documenting some # parameters in a documented function, or documenting parameters that # don't exist or using markup commands wrongly. -WARN_IF_DOC_ERROR = YES +WARN_IF_DOC_ERROR = NO # This WARN_NO_PARAMDOC option can be abled to get warnings for # functions that are documented, but have no documentation for their parameters @@ -553,7 +553,7 @@ WARN_FORMAT = "$file:$line: $text" # and error messages should be written. If left blank the output is written # to stderr. -WARN_LOGFILE = doxygen.warnings +WARN_LOGFILE = #--------------------------------------------------------------------------- # configuration options related to the input files diff --git a/doc/api/doxygen/build.py b/doc/api/doxygen/build.py index 043dd9f2e..e34584fdf 100644 --- a/doc/api/doxygen/build.py +++ b/doc/api/doxygen/build.py @@ -3,7 +3,7 @@ Doxygen API Builder --------------------- ''' -import os +import os, shutil def is_exe(path): ''' Returns if the program is executable @@ -29,6 +29,8 @@ def which(program): return None if which('doxygen') is not None: + print "Building Doxygen API Documentation" os.system("doxygen .doxygen") - os.system("mv html ../../../build/doxygen") + if os.path.exists('../../../build'): + shutil.move("html", "../../../build/doxygen") else: print "Doxygen not available...not building" diff --git a/doc/api/epydoc/build.py b/doc/api/epydoc/build.py index bef17dba7..b6c890f62 100755 --- a/doc/api/epydoc/build.py +++ b/doc/api/epydoc/build.py @@ -7,7 +7,7 @@ if so, we use its cli program to compile the documents ''' try: - import sys, os + import sys, os, shutil import pkg_resources pkg_resources.require("epydoc") @@ -26,8 +26,12 @@ if not os.path.exists("./html"): os.mkdir("./html") + + print "Building Epydoc API Documentation" cli() - #os.system("mv html ../../../build/epydoc") + + if os.path.exists('../../../build'): + shutil.move("html", "../../../build/epydoc") except Exception, ex: import traceback,sys traceback.print_exc(file=sys.stdout) diff --git a/doc/api/pydoc/build.py b/doc/api/pydoc/build.py index c7be563a6..614985561 100644 --- a/doc/api/pydoc/build.py +++ b/doc/api/pydoc/build.py @@ -5,11 +5,14 @@ Taken from: http://pyopengl.sourceforge.net/pydoc/OpenGLContext.pydoc.pydoc2.html Author: Mike Fletcher """ -import pydoc, inspect, os, string +import logging +import pydoc, inspect, os, string, shutil import sys, imp, os, stat, re, types, inspect from repr import Repr from string import expandtabs, find, join, lower, split, strip, rfind, rstrip +_log = logging.getLogger(__name__) + def classify_class_attrs(cls): """Return list of attribute-descriptor tuples. @@ -179,9 +182,6 @@ def docmodule(self, object, name=None, mod=None, packageContext = None, *ignored if classes: -## print classes -## import pdb -## pdb.set_trace() classlist = map(lambda (key, value): value, classes) contents = [ self.formattree(inspect.getclasstree(classlist, 1), name)] @@ -337,7 +337,8 @@ def warn( self, message ): self.warnings.append (message) def info (self, message): """Information/status report""" - print message + _log.debug(message) + def addBase(self, specifier): """Set the base of the documentation set, only children of these modules will be documented""" try: @@ -348,7 +349,6 @@ def addBase(self, specifier): def addInteresting( self, specifier): """Add a module to the list of interesting modules""" if self.checkScope( specifier): -## print "addInteresting", specifier self.pending.append (specifier) else: self.completed[ specifier] = 1 @@ -418,7 +418,7 @@ def process( self ): del self.pending[0] finally: for item in self.warnings: - print item + log.info(item) def clean (self, objectList, object): """callback from the formatter object asking us to remove @@ -446,10 +446,14 @@ def recurseScan(self, objectList): if __name__ == "__main__": if not os.path.exists("./html"): os.mkdir("./html") + + print "Building Pydoc API Documentation" PackageDocumentationGenerator( baseModules = ['pymodbus', '__builtin__'], destinationDirectory = "./html/", exclusions = ['math', 'string', 'twisted'], recursionStops = [], ).process () - os.system("mv html ../../../build/pydoc") + + if os.path.exists('../../../build'): + shutil.move("html", "../../../build/pydoc") diff --git a/doc/api/pydoctor/build.py b/doc/api/pydoctor/build.py index cf1584228..ac89d54b4 100755 --- a/doc/api/pydoctor/build.py +++ b/doc/api/pydoctor/build.py @@ -7,7 +7,7 @@ if so, we use its cli program to compile the documents ''' try: - import sys, os + import sys, os, shutil import pkg_resources pkg_resources.require("pydoctor") @@ -18,6 +18,10 @@ --add-package=../../../pymodbus --html-output=html --html-write-function-pages --make-html'''.split() + + print "Building Pydoctor API Documentation" main(sys.argv[1:]) - os.system("mv html ../../../build/pydoctor") + + if os.path.exists('../../../build'): + shutil.move("html", "../../../build/pydoctor") except: print "Pydoctor unavailable...not building" diff --git a/setup.cfg b/setup.cfg index 0e7141405..608cda3fb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,7 @@ +[aliases] +upload_docs = build_sphinx upload_docs +package = build_apidocs build_sphinx sdist + [egg_info] #tag_build = dev tag_svn_revision = false @@ -15,5 +19,8 @@ cover-package=pymodbus [build-sphinx] source-dir = doc/sphinx/ -biuld-dir = doc/sphinx/build +build-dir = doc/sphinx/build all_files = 1 + +[upload_docs] +upload-dir = build/sphinx/html diff --git a/setup.py b/setup.py index 30ce33a0d..3e05317f6 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,7 @@ class BuildApiDocs(Command): build.py script underneath trying to build the api documentation for the given format. ''' + description = "build all the projects api documents" user_options = [] def initialize_options(self): @@ -51,6 +52,44 @@ def run(self): command_classes['build_apidocs'] = BuildApiDocs +class DeepClean(Command): + ''' Helper command to return the directory to a completely + clean state. + ''' + description = "clean everything that we don't want" + user_options = [] + + def initialize_options(self): + ''' options setup ''' + self.trash = ['build', 'dist', 'pymodbus.egg-info', + os.path.join(os.path.join('doc','sphinx'),'build'), + ] + + def finalize_options(self): + pass + + def run(self): + ''' command runner ''' + self.__delete_pyc_files() + self.__delete_trash_dirs() + + def __delete_trash_dirs(self): + ''' remove all directories created in building ''' + self.__delete_pyc_files() + import shutil + for directory in self.trash: + if os.path.exists(directory): + shutil.rmtree(directory) + + def __delete_pyc_files(self): + ''' remove all python cache files ''' + for root,dirs,files in os.walk('.'): + for file in files: + if file.endswith('.pyc'): + os.remove(os.path.join(root,file)) + +command_classes['deep_clean'] = DeepClean + #---------------------------------------------------------------------------# # Configuration #---------------------------------------------------------------------------# @@ -84,7 +123,8 @@ def run(self): maintainer_email = 'bashwork@gmail.com', url='http://code.google.com/p/pymodbus/', license = 'BSD', - packages = find_packages(exclude=['ez_setup', 'examples', 'tests', 'doc']), + packages = find_packages(exclude=['ez_setup', 'examples', 'test', 'doc']), + exclude_package_data = {'' : ['examples', 'test', 'tools', 'doc']}, platforms = ["Linux","Mac OS X","Win"], include_package_data = True, zip_safe = True, From 89467c90a4fe1adbeee0685e1bbcfe29591121a0 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Thu, 3 Mar 2011 18:06:09 +0000 Subject: [PATCH 007/243] Adding more commands to the setup.py - pep8, lint, 2to3 Fixed a few pep8/lint bugs to test --- doc/MANIFEST.IN | 6 - pymodbus/__init__.py | 1 - pymodbus/bit_read_message.py | 10 +- pymodbus/bit_write_message.py | 1 - pymodbus/client/async.py | 1 - pymodbus/client/sync.py | 5 +- pymodbus/constants.py | 8 +- pymodbus/datastore/context.py | 2 +- pymodbus/datastore/database.py | 2 +- pymodbus/datastore/modredis.py | 1 - pymodbus/datastore/remote.py | 2 +- pymodbus/device.py | 24 ++-- pymodbus/diag_message.py | 3 +- pymodbus/events.py | 5 +- pymodbus/factory.py | 2 +- pymodbus/file_message.py | 4 +- pymodbus/interfaces.py | 6 +- pymodbus/internal/ptwisted.py | 6 + pymodbus/register_write_message.py | 1 - pymodbus/server/async.py | 2 - pymodbus/server/sync.py | 1 - pymodbus/transaction.py | 4 +- pymodbus/version.py | 2 - setup.py | 85 ++----------- setup_commands.py | 187 +++++++++++++++++++++++++++++ tools/lint.sh | 102 ---------------- 26 files changed, 240 insertions(+), 233 deletions(-) delete mode 100644 doc/MANIFEST.IN create mode 100755 setup_commands.py delete mode 100644 tools/lint.sh diff --git a/doc/MANIFEST.IN b/doc/MANIFEST.IN deleted file mode 100644 index 90d48cfd2..000000000 --- a/doc/MANIFEST.IN +++ /dev/null @@ -1,6 +0,0 @@ -recursive-include pymodbus *.py -recursive-include test *.py -recursive-include examples *.py -graft doc -prune .svn -prune build diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index d3ab950ca..b098ce5bf 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -15,7 +15,6 @@ __version__ = _version.short() __author__ = 'Galen Collins' - #---------------------------------------------------------------------------# # Block unhandled logging #---------------------------------------------------------------------------# diff --git a/pymodbus/bit_read_message.py b/pymodbus/bit_read_message.py index ed37379bb..f0dd6e312 100644 --- a/pymodbus/bit_read_message.py +++ b/pymodbus/bit_read_message.py @@ -9,6 +9,7 @@ from pymodbus.pdu import ModbusExceptions as merror from pymodbus.utilities import pack_bitstring, unpack_bitstring + class ReadBitsRequestBase(ModbusRequest): ''' Base class for Messages Requesting bit values ''' @@ -45,6 +46,7 @@ def __str__(self): ''' return "ReadBitRequest(%d,%d)" % (self.address, self.count) + class ReadBitsResponseBase(ModbusResponse): ''' Base class for Messages responding to bit-reading values ''' @@ -105,6 +107,7 @@ def __str__(self): ''' return "ReadBitResponse(%d)" % len(self.bits) + class ReadCoilsRequest(ReadBitsRequestBase): ''' This function code is used to read from 1 to 2000(0x7d0) contiguous status @@ -140,6 +143,7 @@ def execute(self, context): values = context.getValues(self.function_code, self.address, self.count) return ReadCoilsResponse(values) + class ReadCoilsResponse(ReadBitsResponseBase): ''' The coils in the response message are packed as one coil per bit of @@ -162,6 +166,7 @@ def __init__(self, values=None, **kwargs): ''' ReadBitsResponseBase.__init__(self, values, **kwargs) + class ReadDiscreteInputsRequest(ReadBitsRequestBase): ''' This function code is used to read from 1 to 2000(0x7d0) contiguous status @@ -197,6 +202,7 @@ def execute(self, context): values = context.getValues(self.function_code, self.address, self.count) return ReadDiscreteInputsResponse(values) + class ReadDiscreteInputsResponse(ReadBitsResponseBase): ''' The discrete inputs in the response message are packed as one input per @@ -219,9 +225,9 @@ def __init__(self, values=None, **kwargs): ''' ReadBitsResponseBase.__init__(self, values, **kwargs) -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ "ReadCoilsRequest", "ReadCoilsResponse", "ReadDiscreteInputsRequest", "ReadDiscreteInputsResponse", diff --git a/pymodbus/bit_write_message.py b/pymodbus/bit_write_message.py index 78b7d907a..c118efa65 100644 --- a/pymodbus/bit_write_message.py +++ b/pymodbus/bit_write_message.py @@ -9,7 +9,6 @@ from pymodbus.pdu import ModbusRequest from pymodbus.pdu import ModbusResponse from pymodbus.pdu import ModbusExceptions as merror -from pymodbus.exceptions import ParameterException from pymodbus.utilities import pack_bitstring, unpack_bitstring #---------------------------------------------------------------------------# diff --git a/pymodbus/client/async.py b/pymodbus/client/async.py index a68697af4..b7bf3828b 100644 --- a/pymodbus/client/async.py +++ b/pymodbus/client/async.py @@ -15,7 +15,6 @@ def clientTest(): reactor.callLater(1, clientTest) reactor.run() """ -import struct from collections import deque from twisted.internet import reactor, defer, protocol diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index b13784760..2cc89b2a0 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -1,5 +1,4 @@ import socket -import struct import serial from pymodbus.constants import Defaults @@ -252,8 +251,8 @@ def connect(self): try: self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #self.socket.bind(('localhost', Defaults.Port)) - except socket.error, msg: - _logger.error('Unable to create udp socket') + except socket.error, ex: + _logger.error('Unable to create udp socket %s' % ex) self.close() return self.socket != None diff --git a/pymodbus/constants.py b/pymodbus/constants.py index a2872617e..b64ee1511 100644 --- a/pymodbus/constants.py +++ b/pymodbus/constants.py @@ -57,7 +57,7 @@ class Defaults(Singleton): - (E)ven - 1 0 1 0 | P(0) - (O)dd - 1 0 1 0 | P(1) - (N)one - 1 0 1 0 | no parity - + This defaults to (N)one. .. attribute:: Bytesize @@ -120,7 +120,7 @@ class ModbusStatus(Singleton): SlaveOn = 0xff SlaveOff = 0x00 -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported Identifiers -#---------------------------------------------------------------------------# -__all__ = [ "Defaults", "ModbusStatus" ] +#---------------------------------------------------------------------------# +__all__ = ["Defaults", "ModbusStatus"] diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index 7902c2f6a..dce4f1472 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -1,4 +1,4 @@ -from pymodbus.exceptions import NotImplementedException, ParameterException +from pymodbus.exceptions import ParameterException from pymodbus.interfaces import IModbusSlaveContext from pymodbus.datastore.store import ModbusSequentialDataBlock diff --git a/pymodbus/datastore/database.py b/pymodbus/datastore/database.py index e612ef4ef..abd365be0 100644 --- a/pymodbus/datastore/database.py +++ b/pymodbus/datastore/database.py @@ -4,7 +4,7 @@ from sqlalchemy.schema import UniqueConstraint from sqlalchemy.sql.expression import bindparam -from pymodbus.exceptions import NotImplementedException, ParameterException +from pymodbus.exceptions import NotImplementedException from pymodbus.interfaces import IModbusSlaveContext #---------------------------------------------------------------------------# diff --git a/pymodbus/datastore/modredis.py b/pymodbus/datastore/modredis.py index e526ace88..70ffd8be5 100644 --- a/pymodbus/datastore/modredis.py +++ b/pymodbus/datastore/modredis.py @@ -1,5 +1,4 @@ import redis -from pymodbus.exceptions import NotImplementedException, ParameterException from pymodbus.interfaces import IModbusSlaveContext from pymodbus.utilities import pack_bitstring, unpack_bitstring diff --git a/pymodbus/datastore/remote.py b/pymodbus/datastore/remote.py index 47dd5f56c..c7bf61218 100644 --- a/pymodbus/datastore/remote.py +++ b/pymodbus/datastore/remote.py @@ -1,4 +1,4 @@ -from pymodbus.exceptions import NotImplementedException, ParameterException +from pymodbus.exceptions import NotImplementedException from pymodbus.interfaces import IModbusSlaveContext #---------------------------------------------------------------------------# diff --git a/pymodbus/device.py b/pymodbus/device.py index 15f38a864..e0402aa75 100644 --- a/pymodbus/device.py +++ b/pymodbus/device.py @@ -76,15 +76,15 @@ class ModbusDeviceIdentification(object): application protocol. ''' __data = { - 0x00: '', # VendorName - 0x01: '', # ProductCode - 0x02: '', # MajorMinorRevision - 0x03: '', # VendorUrl - 0x04: '', # ProductName - 0x05: '', # ModelName - 0x06: '', # UserApplicationName - 0x07: '', # reserved - 0x08: '', # reserved + 0x00: '', # VendorName + 0x01: '', # ProductCode + 0x02: '', # MajorMinorRevision + 0x03: '', # VendorUrl + 0x04: '', # ProductName + 0x05: '', # ModelName + 0x06: '', # UserApplicationName + 0x07: '', # reserved + 0x08: '', # reserved # 0x80 -> 0xFF are private } @@ -261,9 +261,9 @@ def update(self, input): :param input: The value to copy values from ''' - for k,v in input.iteritems(): + for k, v in input.iteritems(): v += self.__getattribute__(k) - self.__setattr__(k,v) + self.__setattr__(k, v) def reset(self): ''' This clears all of the system counters @@ -340,7 +340,7 @@ def addEvent(self, event): :param event: A new event to add to the log ''' self.__events.insert(0, event) - self.__events = self.__events[0:64] # chomp to 64 entries + self.__events = self.__events[0:64] # chomp to 64 entries self.Counter.Event += 1 def getEvents(self): diff --git a/pymodbus/diag_message.py b/pymodbus/diag_message.py index d807684b8..5a2d0c24f 100644 --- a/pymodbus/diag_message.py +++ b/pymodbus/diag_message.py @@ -10,7 +10,6 @@ from pymodbus.constants import ModbusStatus from pymodbus.pdu import ModbusRequest from pymodbus.pdu import ModbusResponse -from pymodbus.pdu import ModbusExceptions as merror from pymodbus.device import ModbusControlBlock from pymodbus.exceptions import NotImplementedException from pymodbus.utilities import pack_bitstring @@ -595,7 +594,7 @@ class ReturnSlaveBusCharacterOverrunCountResponse(DiagnosticStatusSimpleResponse class ClearOverrunCountRequest(DiagnosticStatusSimpleRequest): ''' Clears the overrun error counter and reset the error flag - + An error flag should be cleared, but nothing else in the specification mentions is, so it is ignored. ''' diff --git a/pymodbus/events.py b/pymodbus/events.py index afa33261e..6f9a97b47 100644 --- a/pymodbus/events.py +++ b/pymodbus/events.py @@ -77,7 +77,7 @@ class RemoteSendEvent(ModbusEvent): The remote device stores this type of event byte when it finishes processing a request message. It is stored if the remote device returned a normal or exception response, or no response. - + This event is defined by bit 7 set to a logic '0', with bit 6 set to a '1'. The other bits will be set to a logic '1' if the corresponding condition is TRUE. The bit layout is:: @@ -167,7 +167,7 @@ class CommunicationRestartEvent(ModbusEvent): mode, the event byte is added to the existing event log. If the remote device is placed into 'Stop on Error' mode, the byte is added to the log and the rest of the log is cleared to zeros. - + The event is defined by a content of zero. ''' @@ -188,5 +188,4 @@ def decode(self, event): ''' if event != self.__encoded: raise ParameterException('Invalid decoded value') - diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 9ac68eb4b..5b19f30fa 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -137,7 +137,7 @@ def _helper(self, data): _logger.debug("Factory Response[%d]" % function_code) response = self.__lookup.get(function_code, lambda: None)() if function_code > 0x80: - code = function_code & 0x7f # strip error portion + code = function_code & 0x7f # strip error portion response = ExceptionResponse(code, ecode.IllegalFunction) if not response: raise ModbusException("Unknown response %d" % function_code) diff --git a/pymodbus/file_message.py b/pymodbus/file_message.py index c28d97a6e..5fbfa2c78 100644 --- a/pymodbus/file_message.py +++ b/pymodbus/file_message.py @@ -37,7 +37,7 @@ def __init__(self, address): ''' ModbusRequest.__init__(self) self.address = address - self.values = [] # dunno where this should come from + self.values = [] # dunno where this should come from def encode(self): ''' Encodes the request packet @@ -114,7 +114,7 @@ def decode(self, data): length, count = struct.unpack('>HH', data[0:4]) for index in xrange(0, count - 4): idx = 4 + index * 2 - self.values.append(struct.unpack('>H', data[idx:idx+2])[0]) + self.values.append(struct.unpack('>H', data[idx:idx + 2])[0]) #---------------------------------------------------------------------------# # Exported symbols diff --git a/pymodbus/interfaces.py b/pymodbus/interfaces.py index 04f5796bb..70b3ac288 100644 --- a/pymodbus/interfaces.py +++ b/pymodbus/interfaces.py @@ -147,17 +147,17 @@ class IModbusSlaveContext(object): getValues(self, fx, address, count=1) setValues(self, fx, address, values) ''' - __fx_mapper = {2:'d', 4:'i'} + __fx_mapper = {2: 'd', 4: 'i'} __fx_mapper.update([(i, 'h') for i in [3, 6, 16, 23]]) __fx_mapper.update([(i, 'c') for i in [1, 5, 15]]) def decode(self, fx): - ''' Converts the function code to the datastore to use + ''' Converts the function code to the datastore to :param fx: The function we are working with :returns: one of [d(iscretes),i(inputs),h(oliding),c(oils)] ''' - return self.__fx_mapper[fx] + return self.__fx_mapper[fx] def reset(self): ''' Resets all the datastores to their default values ''' diff --git a/pymodbus/internal/ptwisted.py b/pymodbus/internal/ptwisted.py index f3f8b6439..b44a8c5ca 100644 --- a/pymodbus/internal/ptwisted.py +++ b/pymodbus/internal/ptwisted.py @@ -5,6 +5,12 @@ from twisted.conch import manhole, manhole_ssh from twisted.conch.insults import insults +#---------------------------------------------------------------------------# +# Logging +#---------------------------------------------------------------------------# +import logging +_logger = logging.getLogger(__name__) + def InstallManagementConsole(namespace, users={'admin':'admin'}, port=503): ''' Helper method to start an ssh management console for the modbus server. diff --git a/pymodbus/register_write_message.py b/pymodbus/register_write_message.py index 21c19a82d..22d3883a8 100644 --- a/pymodbus/register_write_message.py +++ b/pymodbus/register_write_message.py @@ -6,7 +6,6 @@ from pymodbus.pdu import ModbusRequest from pymodbus.pdu import ModbusResponse from pymodbus.pdu import ModbusExceptions as merror -from pymodbus.exceptions import ParameterException class WriteSingleRegisterRequest(ModbusRequest): ''' diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index d1ca0fa33..541328197 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -14,8 +14,6 @@ from pymodbus.device import ModbusAccessControl from pymodbus.device import ModbusDeviceIdentification from pymodbus.transaction import ModbusSocketFramer, ModbusAsciiFramer -from pymodbus.interfaces import IModbusFramer -from pymodbus.exceptions import * from pymodbus.pdu import ModbusExceptions as merror from pymodbus.internal.ptwisted import InstallManagementConsole diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index 8d5e40f87..f4c99f283 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -13,7 +13,6 @@ from pymodbus.device import ModbusControlBlock from pymodbus.device import ModbusDeviceIdentification from pymodbus.transaction import * -from pymodbus.interfaces import IModbusFramer from pymodbus.exceptions import ModbusException from pymodbus.pdu import ModbusExceptions as merror diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index f8aec7637..155140369 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -48,7 +48,7 @@ def execute(self, request): ''' retries = Defaults.Retries request.transaction_id = self.__getNextTID() - _logging.debug("Running transaction %d" % request.transaction_id) + _logger.debug("Running transaction %d" % request.transaction_id) while retries > 0: try: @@ -57,7 +57,7 @@ def execute(self, request): self.socket.send(packet) except socket.error, msg: self.socket.close() - _logging.debug("Transaction failed. (%s) " % msg) + _logger.debug("Transaction failed. (%s) " % msg) retries -= 1 def addTransaction(self, request): diff --git a/pymodbus/version.py b/pymodbus/version.py index eb1f62adf..87fd9e6cb 100644 --- a/pymodbus/version.py +++ b/pymodbus/version.py @@ -26,7 +26,6 @@ def short(self): ''' return '%d.%d.%d' % (self.major, self.minor, self.micro) - def __str__(self): ''' Returns a string representation of the object @@ -34,7 +33,6 @@ def __str__(self): ''' return '[%s, version %s]' % (self.package, self.short()) - _version = Version('pymodbus', 0, 9, 0) _version.__name__ = 'pymodbus' # fix epydoc error diff --git a/setup.py b/setup.py index 3e05317f6..2a4c440e6 100644 --- a/setup.py +++ b/setup.py @@ -15,89 +15,15 @@ from ez_setup import use_setuptools use_setuptools() -from distutils.core import Command -import sys, os - -#---------------------------------------------------------------------------# -# Extra Commands -#---------------------------------------------------------------------------# -command_classes = {} - -class BuildApiDocs(Command): - ''' Helper command to build the available api documents - This scans all the subdirectories under api and runs the - build.py script underneath trying to build the api - documentation for the given format. - ''' - description = "build all the projects api documents" - user_options = [] - - def initialize_options(self): - ''' options setup ''' - if not os.path.exists('./build'): - os.mkdir('./build') - - def finalize_options(self): - ''' options teardown ''' - pass - - def run(self): - ''' command runner ''' - old_cwd = os.getcwd() - directories = (d for d in os.listdir('./doc/api') if not d.startswith('.')) - for entry in directories: - os.chdir('./doc/api/%s' % entry) - os.system('python build.py') - os.chdir(old_cwd) - -command_classes['build_apidocs'] = BuildApiDocs - -class DeepClean(Command): - ''' Helper command to return the directory to a completely - clean state. - ''' - description = "clean everything that we don't want" - user_options = [] - - def initialize_options(self): - ''' options setup ''' - self.trash = ['build', 'dist', 'pymodbus.egg-info', - os.path.join(os.path.join('doc','sphinx'),'build'), - ] - - def finalize_options(self): - pass - - def run(self): - ''' command runner ''' - self.__delete_pyc_files() - self.__delete_trash_dirs() - - def __delete_trash_dirs(self): - ''' remove all directories created in building ''' - self.__delete_pyc_files() - import shutil - for directory in self.trash: - if os.path.exists(directory): - shutil.rmtree(directory) - - def __delete_pyc_files(self): - ''' remove all python cache files ''' - for root,dirs,files in os.walk('.'): - for file in files: - if file.endswith('.pyc'): - os.remove(os.path.join(root,file)) - -command_classes['deep_clean'] = DeepClean - #---------------------------------------------------------------------------# # Configuration #---------------------------------------------------------------------------# +from setup_commands import command_classes from pymodbus import __version__, __author__ setup(name = 'pymodbus', version = __version__, - description = "A fully featured modbus protocol stack in python", + description = 'A fully featured modbus protocol stack in python', long_description=''' Pymodbus aims to be a fully implemented modbus protocol stack implemented using twisted. Its orignal goal was to allow simulation of thousands of @@ -116,7 +42,7 @@ def __delete_pyc_files(self): 'Topic :: System :: Networking', 'Topic :: Utilities' ], - keywords = 'modbus, twisted', + keywords = 'modbus, twisted, scada', author = __author__, author_email = 'bashwork@gmail.com', maintainer = __author__, @@ -125,7 +51,7 @@ def __delete_pyc_files(self): license = 'BSD', packages = find_packages(exclude=['ez_setup', 'examples', 'test', 'doc']), exclude_package_data = {'' : ['examples', 'test', 'tools', 'doc']}, - platforms = ["Linux","Mac OS X","Win"], + platforms = ['Linux', 'Mac OS X', 'Win'], include_package_data = True, zip_safe = True, install_requires = [ @@ -133,6 +59,9 @@ def __delete_pyc_files(self): 'nose >= 0.9.3', 'pyserial >= 2.4' ], + extras_require = { + 'quality' : [ 'epydoc >= 3.4.1', 'coverage >= 3.3.1', 'pyflakes >= 0.4.0' ], + }, test_suite = 'nose.collector', cmdclass = command_classes, ) diff --git a/setup_commands.py b/setup_commands.py new file mode 100755 index 000000000..ff68bc899 --- /dev/null +++ b/setup_commands.py @@ -0,0 +1,187 @@ +from distutils.core import Command +import sys, os, shutil + +#---------------------------------------------------------------------------# +# Extra Commands +#---------------------------------------------------------------------------# +class BuildApiDocsCommand(Command): + ''' Helper command to build the available api documents + This scans all the subdirectories under api and runs the + build.py script underneath trying to build the api + documentation for the given format. + ''' + description = "build all the projects api documents" + user_options = [] + + def initialize_options(self): + ''' options setup ''' + if not os.path.exists('./build'): + os.mkdir('./build') + + def finalize_options(self): + ''' options teardown ''' + pass + + def run(self): + ''' command runner ''' + old_cwd = os.getcwd() + directories = (d for d in os.listdir('./doc/api') if not d.startswith('.')) + for entry in directories: + os.chdir('./doc/api/%s' % entry) + os.system('python build.py') + os.chdir(old_cwd) + +class DeepCleanCommand(Command): + ''' Helper command to return the directory to a completely + clean state. + ''' + description = "clean everything that we don't want" + user_options = [] + + def initialize_options(self): + ''' options setup ''' + self.trash = ['build', 'dist', 'pymodbus.egg-info', + os.path.join(os.path.join('doc','sphinx'),'build'), + ] + + def finalize_options(self): + pass + + def run(self): + ''' command runner ''' + self.__delete_pyc_files() + self.__delete_trash_dirs() + + def __delete_trash_dirs(self): + ''' remove all directories created in building ''' + self.__delete_pyc_files() + for directory in self.trash: + if os.path.exists(directory): + shutil.rmtree(directory) + + def __delete_pyc_files(self): + ''' remove all python cache files ''' + for root,dirs,files in os.walk('.'): + for file in files: + if file.endswith('.pyc'): + os.remove(os.path.join(root,file)) + +class LintCommand(Command): + ''' Helper command to perform a lint scan of the + sourcecode and return the results. + ''' + description = "perform a lint scan of the code" + user_options = [] + + def initialize_options(self): + ''' options setup ''' + if not os.path.exists('./build'): + os.mkdir('./build') + + def finalize_options(self): + pass + + def run(self): + ''' command runner ''' + scanners = [s for s in dir(self) if s.find('__try') >= 0] + for scanner in scanners: + if getattr(self, scanner)(): + break + + def __try_pyflakes(self): + try: + from pyflakes.scripts.pyflakes import main + sys.argv = '''pyflakes pymodbus'''.split() + main() + return True + except: return False + + def __try_pychecker(self): + try: + import pychecker + sys.argv = '''pychecker pymodbus/*.py'''.split() + main() + return True + except: return False + + def __try_pylint(self): + try: + import pylint + sys.argv = '''pylint pymodbus/*.py'''.split() + main() + return True + except: return False + +class Python3Command(Command): + ''' Helper command to scan for potential python 3 + errors. + + ./setup.py scan_2to3 > build/diffs_2to3 build/report_2to3 + ''' + description = "perform 2to3 scan of the code" + user_options = [] + + def initialize_options(self): + ''' options setup ''' + if not os.path.exists('./build'): + os.mkdir('./build') + self.directories = ['pymodbus', 'test', 'examples'] + + def finalize_options(self): + pass + + def run(self): + ''' command runner ''' + self.__run_python3() + + def __run_python3(self): + try: + from lib2to3.main import main + sys.argv = ['2to3'] + self.directories + main("lib2to3.fixes") + return True + except: return False + +class Pep8Command(Command): + ''' Helper command to scan for potential pep8 violations + ''' + description = "perform pep8 scan of the code" + user_options = [] + + def initialize_options(self): + ''' options setup ''' + if not os.path.exists('./build'): + os.mkdir('./build') + self.directories = ['pymodbus'] + + def finalize_options(self): + pass + + def run(self): + ''' command runner ''' + self.__run_pep8() + + def __run_pep8(self): + try: + from pep8 import _main as main + sys.argv = '''pep8 --repeat --count --statistics + '''.split() + self.directories + main() + return True + except: return False + +#---------------------------------------------------------------------------# +# Command Configuration +#---------------------------------------------------------------------------# +command_classes = { + 'deep_clean' : DeepCleanCommand, + 'build_apidocs' : BuildApiDocsCommand, + 'lint' : LintCommand, + 'scan_2to3' : Python3Command, + 'pep8' : Pep8Command, +} + +#---------------------------------------------------------------------------# +# Export command list +#---------------------------------------------------------------------------# +__all__ = ['command_classes'] diff --git a/tools/lint.sh b/tools/lint.sh deleted file mode 100644 index b88438301..000000000 --- a/tools/lint.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/bin/bash -#---------------------------------------------------------------------------# -# Global Variables -#---------------------------------------------------------------------------# -root="../../" -files=`find $root | grep "py$"` - -#---------------------------------------------------------------------------# -# @brief Build a section header -# @param $1 The file to output to -# @param $2 The header to append to the section -#---------------------------------------------------------------------------# -header() -{ - echo "#---------------------------------------------------------------------------#" >>$1 - echo "# $2" >>$1 - echo "#---------------------------------------------------------------------------#" >>$1 -} - -#---------------------------------------------------------------------------# -# Run all the available lint commands on the source tree -#---------------------------------------------------------------------------# -do_lint() -{ - # Remove all old reports as we append - rm *.report - - # Check for and run pyflakes for each python file - if [ "`which pyflakes`" != "" ]; then - OPTS="" - RUN="`which pyflakes` $OPTS" - for file in $files; do - header ${file##*/}.report "PyFlakes Report" - $RUN $file >> ${file##*/}.report 2>&1 - done - fi - - # Check for and run pychecker for each python file - if [ "`which pychecker`" != "" ]; then - OPTS="--config=.pychecker" - RUN="`which pychecker` $OPTS" - for file in $files; do - header ${file##*/}.report "PyChecker Report" - $RUN $file >> ${file##*/}.report 2>/dev/null - done - fi - - # Check for and run pyflakes for each python file - if [ "`which pylint`" != "" ]; then - OPTS="--rcfile=.pylint" - RUN="`which pylint` $OPTS" - for file in $files; do - header ${file##*/}.report "PyLint Report" - $RUN $file >> ${file##*/}.report 2>/dev/null - done - fi -} - -#---------------------------------------------------------------------------# -# Print an excerpt from the reports with the final pylint scores -#---------------------------------------------------------------------------# -do_tally() -{ - msg="Your code has been rated" - ls *.report > /dev/null 2>&1 - if [ "$?" == "0" ]; then - grep "$msg" *.report | awk {' print $1" score is " $7 '} - else - echo "There are no available reports, please run lint first" - fi -} - -#---------------------------------------------------------------------------# -# Print the script help message -#---------------------------------------------------------------------------# -do_help() -{ - cat < Date: Thu, 3 Mar 2011 20:21:03 +0000 Subject: [PATCH 008/243] moving files around --- {tools => examples/tools}/jamod/build.xml | 0 {tools => examples/tools}/jamod/lib/jamod-1.2.jar | Bin .../tools}/jamod/src/ClientExample.java | 0 .../tools}/jamod/src/SerialSlave.java | 0 {tools => examples/tools}/reference/LICENSE-FREE | 0 {tools => examples/tools}/reference/README | 0 {tools => examples/tools}/reference/crc.c | 0 {tools => examples/tools}/reference/diagslave | Bin {tools => examples/tools}/reference/lrc.c | 0 {tools => examples/tools}/reference/modpoll | Bin .../tools}/reference/virtual-serial.c | 0 {tools => examples/tools}/reindent.py | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename {tools => examples/tools}/jamod/build.xml (100%) rename {tools => examples/tools}/jamod/lib/jamod-1.2.jar (100%) rename {tools => examples/tools}/jamod/src/ClientExample.java (100%) rename {tools => examples/tools}/jamod/src/SerialSlave.java (100%) rename {tools => examples/tools}/reference/LICENSE-FREE (100%) rename {tools => examples/tools}/reference/README (100%) rename {tools => examples/tools}/reference/crc.c (100%) rename {tools => examples/tools}/reference/diagslave (100%) rename {tools => examples/tools}/reference/lrc.c (100%) rename {tools => examples/tools}/reference/modpoll (100%) rename {tools => examples/tools}/reference/virtual-serial.c (100%) rename {tools => examples/tools}/reindent.py (100%) diff --git a/tools/jamod/build.xml b/examples/tools/jamod/build.xml similarity index 100% rename from tools/jamod/build.xml rename to examples/tools/jamod/build.xml diff --git a/tools/jamod/lib/jamod-1.2.jar b/examples/tools/jamod/lib/jamod-1.2.jar similarity index 100% rename from tools/jamod/lib/jamod-1.2.jar rename to examples/tools/jamod/lib/jamod-1.2.jar diff --git a/tools/jamod/src/ClientExample.java b/examples/tools/jamod/src/ClientExample.java similarity index 100% rename from tools/jamod/src/ClientExample.java rename to examples/tools/jamod/src/ClientExample.java diff --git a/tools/jamod/src/SerialSlave.java b/examples/tools/jamod/src/SerialSlave.java similarity index 100% rename from tools/jamod/src/SerialSlave.java rename to examples/tools/jamod/src/SerialSlave.java diff --git a/tools/reference/LICENSE-FREE b/examples/tools/reference/LICENSE-FREE similarity index 100% rename from tools/reference/LICENSE-FREE rename to examples/tools/reference/LICENSE-FREE diff --git a/tools/reference/README b/examples/tools/reference/README similarity index 100% rename from tools/reference/README rename to examples/tools/reference/README diff --git a/tools/reference/crc.c b/examples/tools/reference/crc.c similarity index 100% rename from tools/reference/crc.c rename to examples/tools/reference/crc.c diff --git a/tools/reference/diagslave b/examples/tools/reference/diagslave similarity index 100% rename from tools/reference/diagslave rename to examples/tools/reference/diagslave diff --git a/tools/reference/lrc.c b/examples/tools/reference/lrc.c similarity index 100% rename from tools/reference/lrc.c rename to examples/tools/reference/lrc.c diff --git a/tools/reference/modpoll b/examples/tools/reference/modpoll similarity index 100% rename from tools/reference/modpoll rename to examples/tools/reference/modpoll diff --git a/tools/reference/virtual-serial.c b/examples/tools/reference/virtual-serial.c similarity index 100% rename from tools/reference/virtual-serial.c rename to examples/tools/reference/virtual-serial.c diff --git a/tools/reindent.py b/examples/tools/reindent.py similarity index 100% rename from tools/reindent.py rename to examples/tools/reindent.py From 1f77110832e1fdb72affdc92a882bf74055329db Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Thu, 3 Mar 2011 21:04:07 +0000 Subject: [PATCH 009/243] pep8 and cleanup --- doc/INSTALL | 30 +- doc/LICENSE | 2 +- doc/TODO | 7 - doc/{ => quality}/current.coverage | 0 doc/quality/current.lint | 25 ++ doc/quality/current.pep8 | 428 +++++++++++++++++++++++++++++ pymodbus/__init__.py | 1 - pymodbus/bit_write_message.py | 8 +- pymodbus/constants.py | 2 + pymodbus/datastore/remote.py | 20 +- pymodbus/datastore/store.py | 17 +- pymodbus/device.py | 119 ++++---- pymodbus/diag_message.py | 34 +++ pymodbus/events.py | 54 ++-- pymodbus/exceptions.py | 13 +- pymodbus/factory.py | 12 +- pymodbus/file_message.py | 8 +- pymodbus/interfaces.py | 46 ++-- pymodbus/internal/ptwisted.py | 5 +- pymodbus/pdu.py | 14 +- pymodbus/server/async.py | 22 +- pymodbus/server/sync.py | 16 +- 22 files changed, 720 insertions(+), 163 deletions(-) rename doc/{ => quality}/current.coverage (100%) create mode 100644 doc/quality/current.lint create mode 100644 doc/quality/current.pep8 diff --git a/doc/INSTALL b/doc/INSTALL index a90331dd5..49a55b87f 100644 --- a/doc/INSTALL +++ b/doc/INSTALL @@ -1,11 +1,9 @@ Requirements ------------- -Python 2.3 or later. - -Zope Interfaces 3.0.1 (http://zope.org/Products/ZopeInterface) - if -you have ZopeX3 (at least version 3.0.0c1) installed that should -work too. +* Python 2.3 or later. +* Python Twisted +* Pyserial On Windows pywin32 is recommended (this is built in to ActivePython, so no need to reinstall if you use it instead of standard Python): @@ -14,14 +12,21 @@ so no need to reinstall if you use it instead of standard Python): The Windows IOCP reactor requires pywin32 build 205 or later. + Installation ------------- +To install the package from pypi, use either easy_install or pip:: + + pip install -U pymodbus + easy_install -U pymodbus + As with other Python packages, the standard way of installing from source is (as root or administrator):: python setup.py install + Running Tests -------------- @@ -32,6 +37,7 @@ use either of the following:: python setup.py test nosetests + Building Documentation ---------------------- @@ -50,4 +56,16 @@ The API documents can be generated using one of four programs: To bulid these, simply run the following command and the available packages will sipmly be built:: - python setup build_api + python setup.py build_apidocs + + +Quality Tests +---------------------- + +There are a number of quality tests that can be run against the code base +aside from unit tests:: + + python setup.py scan_2to3 # run a python3 compatability test + python setup.py pep8 # run a pop8 standards test + python setup.py lint # run a lint test + diff --git a/doc/LICENSE b/doc/LICENSE index ebde6fb3a..1a938446d 100644 --- a/doc/LICENSE +++ b/doc/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2010 Galen Collins +Copyright (c) 2011 Galen Collins All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/doc/TODO b/doc/TODO index 8517b2194..1082cdcdd 100644 --- a/doc/TODO +++ b/doc/TODO @@ -3,7 +3,6 @@ General --------------------------------------------------------------------------- - reorganize code into folder namespaces - put protocol code in protocol namespace -- add crc/lrc check on incoming data - make framer read header->read header.length - maybe just for sync - finish clients (and interface) @@ -28,16 +27,10 @@ Utilities - (tcp/serial) forwarder - (udp/serial) forwarder ---------------------------------------------------------------------------- -Slave Context ---------------------------------------------------------------------------- -- link to other clients (other machines, other instances) - --------------------------------------------------------------------------- Client --------------------------------------------------------------------------- - Rework transaction flow and response data -- Finish rtu framers --------------------------------------------------------------------------- Tools diff --git a/doc/current.coverage b/doc/quality/current.coverage similarity index 100% rename from doc/current.coverage rename to doc/quality/current.coverage diff --git a/doc/quality/current.lint b/doc/quality/current.lint new file mode 100644 index 000000000..5440d269b --- /dev/null +++ b/doc/quality/current.lint @@ -0,0 +1,25 @@ +running lint +pymodbus/__init__.py:25: redefinition of unused 'NullHandler' from line 23 +pymodbus/factory.py:12: 'from pymodbus.bit_read_message import *' used; unable to detect undefined names +pymodbus/factory.py:13: 'from pymodbus.bit_write_message import *' used; unable to detect undefined names +pymodbus/factory.py:14: 'from pymodbus.diag_message import *' used; unable to detect undefined names +pymodbus/factory.py:15: 'from pymodbus.file_message import *' used; unable to detect undefined names +pymodbus/factory.py:16: 'from pymodbus.other_message import *' used; unable to detect undefined names +pymodbus/factory.py:17: 'from pymodbus.register_read_message import *' used; unable to detect undefined names +pymodbus/factory.py:18: 'from pymodbus.register_write_message import *' used; unable to detect undefined names +pymodbus/transaction.py:58: undefined name 'socket' +pymodbus/other_message.py:11: 'from pymodbus.exceptions import *' used; unable to detect undefined names +pymodbus/other_message.py:389: local variable 'status' is assigned to but never used +pymodbus/server/async.py:229: local variable 'handle' is assigned to but never used +pymodbus/server/sync.py:16: 'from pymodbus.transaction import *' used; unable to detect undefined names +pymodbus/datastore/remote.py:66: local variable 'result' is assigned to but never used +pymodbus/client/async.py:20: 'reactor' imported but unused +pymodbus/client/common.py:3: 'from pymodbus.bit_read_message import *' used; unable to detect undefined names +pymodbus/client/common.py:4: 'from pymodbus.bit_write_message import *' used; unable to detect undefined names +pymodbus/client/common.py:5: 'from pymodbus.register_read_message import *' used; unable to detect undefined names +pymodbus/client/common.py:6: 'from pymodbus.register_write_message import *' used; unable to detect undefined names +pymodbus/client/common.py:7: 'from pymodbus.diag_message import *' used; unable to detect undefined names +pymodbus/client/common.py:8: 'from pymodbus.file_message import *' used; unable to detect undefined names +pymodbus/client/common.py:9: 'from pymodbus.other_message import *' used; unable to detect undefined names +pymodbus/client/sync.py:6: 'from pymodbus.exceptions import *' used; unable to detect undefined names +pymodbus/client/sync.py:7: 'from pymodbus.transaction import *' used; unable to detect undefined names diff --git a/doc/quality/current.pep8 b/doc/quality/current.pep8 new file mode 100644 index 000000000..8f6748467 --- /dev/null +++ b/doc/quality/current.pep8 @@ -0,0 +1,428 @@ +running pep8 +pymodbus/__init__.py:16:11: E221 multiple spaces before operator +pymodbus/bit_read_message.py:26:20: E221 multiple spaces before operator +pymodbus/bit_read_message.py:143:80: E501 line too long (80 characters) +pymodbus/bit_read_message.py:202:80: E501 line too long (80 characters) +pymodbus/bit_write_message.py:19:14: E221 multiple spaces before operator +pymodbus/bit_write_message.py:58:15: E221 multiple spaces before operator +pymodbus/bit_write_message.py:116:15: E221 multiple spaces before operator +pymodbus/bit_write_message.py:158:22: E701 multiple statements on one line (colon) +pymodbus/bit_write_message.py:159:45: E701 multiple statements on one line (colon) +pymodbus/bit_write_message.py:160:20: E221 multiple spaces before operator +pymodbus/bit_write_message.py:168:15: E221 multiple spaces before operator +pymodbus/bit_write_message.py:170:15: E221 multiple spaces before operator +pymodbus/constants.py:74:17: E221 multiple spaces before operator +pymodbus/constants.py:75:17: E221 multiple spaces before operator +pymodbus/constants.py:76:17: E221 multiple spaces before operator +pymodbus/constants.py:77:17: E221 multiple spaces before operator +pymodbus/constants.py:79:17: E221 multiple spaces before operator +pymodbus/constants.py:80:17: E221 multiple spaces before operator +pymodbus/constants.py:81:17: E221 multiple spaces before operator +pymodbus/constants.py:82:17: E221 multiple spaces before operator +pymodbus/constants.py:83:17: E221 multiple spaces before operator +pymodbus/constants.py:84:17: E221 multiple spaces before operator +pymodbus/constants.py:118:12: E221 multiple spaces before operator +pymodbus/constants.py:119:12: E221 multiple spaces before operator +pymodbus/constants.py:120:12: E221 multiple spaces before operator +pymodbus/constants.py:121:12: E221 multiple spaces before operator +pymodbus/constants.py:122:12: E221 multiple spaces before operator +pymodbus/device.py:163:23: E221 multiple spaces before operator +pymodbus/device.py:164:23: E221 multiple spaces before operator +pymodbus/device.py:165:23: E221 multiple spaces before operator +pymodbus/device.py:166:23: E221 multiple spaces before operator +pymodbus/device.py:167:23: E221 multiple spaces before operator +pymodbus/device.py:168:23: E221 multiple spaces before operator +pymodbus/device.py:245:13: E221 multiple spaces before operator +pymodbus/device.py:286:25: E701 multiple statements on one line (colon) +pymodbus/device.py:293:25: E221 multiple spaces before operator +pymodbus/device.py:295:25: E221 multiple spaces before operator +pymodbus/device.py:296:25: E221 multiple spaces before operator +pymodbus/device.py:297:25: E221 multiple spaces before operator +pymodbus/device.py:298:25: E221 multiple spaces before operator +pymodbus/device.py:299:25: E221 multiple spaces before operator +pymodbus/device.py:300:25: E221 multiple spaces before operator +pymodbus/device.py:301:25: E221 multiple spaces before operator +pymodbus/device.py:322:14: E221 multiple spaces before operator +pymodbus/device.py:370:12: E221 multiple spaces before operator +pymodbus/device.py:371:12: E221 multiple spaces before operator +pymodbus/diag_message.py:136:80: E501 line too long (81 characters) +pymodbus/diag_message.py:175:13: E701 multiple statements on one line (colon) +pymodbus/diag_message.py:201:13: E701 multiple statements on one line (colon) +pymodbus/diag_message.py:225:26: E221 multiple spaces before operator +pymodbus/diag_message.py:226:13: E701 multiple statements on one line (colon) +pymodbus/diag_message.py:255:26: E221 multiple spaces before operator +pymodbus/diag_message.py:256:13: E701 multiple statements on one line (colon) +pymodbus/diag_message.py:350:21: E221 multiple spaces before operator +pymodbus/diag_message.py:594:80: E501 line too long (80 characters) +pymodbus/diag_message.py:597:80: E501 line too long (80 characters) +pymodbus/diag_message.py:613:80: E501 line too long (82 characters) +pymodbus/diag_message.py:616:80: E501 line too long (80 characters) +pymodbus/diag_message.py:651:78: W291 trailing whitespace +pymodbus/diag_message.py:653:78: W291 trailing whitespace +pymodbus/diag_message.py:656:80: E501 line too long (80 characters) +pymodbus/diag_message.py:662:80: E501 line too long (90 characters) +pymodbus/diag_message.py:663:80: E501 line too long (82 characters) +pymodbus/diag_message.py:668:80: E501 line too long (96 characters) +pymodbus/events.py:54:22: E221 multiple spaces before operator +pymodbus/events.py:55:22: E221 multiple spaces before operator +pymodbus/events.py:63:13: E221 multiple spaces before operator +pymodbus/events.py:74:22: E221 multiple spaces before operator +pymodbus/events.py:75:22: E221 multiple spaces before operator +pymodbus/events.py:105:26: E221 multiple spaces before operator +pymodbus/events.py:106:26: E221 multiple spaces before operator +pymodbus/events.py:107:26: E221 multiple spaces before operator +pymodbus/events.py:108:26: E221 multiple spaces before operator +pymodbus/events.py:110:26: E221 multiple spaces before operator +pymodbus/events.py:119:13: E221 multiple spaces before operator +pymodbus/events.py:130:26: E221 multiple spaces before operator +pymodbus/events.py:131:26: E221 multiple spaces before operator +pymodbus/events.py:132:26: E221 multiple spaces before operator +pymodbus/events.py:133:26: E221 multiple spaces before operator +pymodbus/events.py:135:26: E221 multiple spaces before operator +pymodbus/events.py:165:54: W291 trailing whitespace +pymodbus/file_message.py:24:80: E501 line too long (83 characters) +pymodbus/file_message.py:24:84: W291 trailing whitespace +pymodbus/file_message.py:26:80: E501 line too long (86 characters) +pymodbus/file_message.py:26:87: W291 trailing whitespace +pymodbus/file_message.py:71:80: W291 trailing whitespace +pymodbus/interfaces.py:13:1: E302 expected 2 blank lines, found 1 +pymodbus/interfaces.py:61:80: E501 line too long (80 characters) +pymodbus/other_message.py:18:1: E302 expected 2 blank lines, found 1 +pymodbus/other_message.py:60:1: E302 expected 2 blank lines, found 1 +pymodbus/other_message.py:107:1: E302 expected 2 blank lines, found 1 +pymodbus/other_message.py:110:49: W291 trailing whitespace +pymodbus/other_message.py:112:80: E501 line too long (81 characters) +pymodbus/other_message.py:113:80: E501 line too long (82 characters) +pymodbus/other_message.py:113:83: W291 trailing whitespace +pymodbus/other_message.py:117:37: W291 trailing whitespace +pymodbus/other_message.py:119:80: E501 line too long (82 characters) +pymodbus/other_message.py:158:1: E302 expected 2 blank lines, found 1 +pymodbus/other_message.py:163:73: W291 trailing whitespace +pymodbus/other_message.py:176:27: E261 at least two spaces before inline comment +pymodbus/other_message.py:205:1: E302 expected 2 blank lines, found 1 +pymodbus/other_message.py:207:80: E501 line too long (80 characters) +pymodbus/other_message.py:208:55: W291 trailing whitespace +pymodbus/other_message.py:211:60: W291 trailing whitespace +pymodbus/other_message.py:214:80: E501 line too long (80 characters) +pymodbus/other_message.py:216:72: W291 trailing whitespace +pymodbus/other_message.py:250:28: E203 whitespace before ':' +pymodbus/other_message.py:264:1: E302 expected 2 blank lines, found 1 +pymodbus/other_message.py:268:68: W291 trailing whitespace +pymodbus/other_message.py:294:15: E221 multiple spaces before operator +pymodbus/other_message.py:312:34: E225 missing whitespace around operator +pymodbus/other_message.py:320:80: E501 line too long (91 characters) +pymodbus/other_message.py:326:1: E302 expected 2 blank lines, found 1 +pymodbus/other_message.py:329:63: W291 trailing whitespace +pymodbus/other_message.py:366:1: E302 expected 2 blank lines, found 1 +pymodbus/other_message.py:392:34: E261 at least two spaces before inline comment +pymodbus/other_message.py:405:40: E225 missing whitespace around operator +pymodbus/other_message.py:422:78: W291 trailing whitespace +pymodbus/other_message.py:424:78: W291 trailing whitespace +pymodbus/pdu.py:34:1: W293 blank line contains whitespace +pymodbus/pdu.py:36:80: E501 line too long (81 characters) +pymodbus/pdu.py:38:80: E501 line too long (86 characters) +pymodbus/pdu.py:41:1: W293 blank line contains whitespace +pymodbus/pdu.py:78:13: E701 multiple statements on one line (colon) +pymodbus/pdu.py:104:1: W293 blank line contains whitespace +pymodbus/pdu.py:109:1: W293 blank line contains whitespace +pymodbus/pdu.py:128:27: E221 multiple spaces before operator +pymodbus/pdu.py:129:27: E221 multiple spaces before operator +pymodbus/pdu.py:130:27: E221 multiple spaces before operator +pymodbus/pdu.py:131:27: E221 multiple spaces before operator +pymodbus/pdu.py:132:27: E221 multiple spaces before operator +pymodbus/pdu.py:133:27: E221 multiple spaces before operator +pymodbus/pdu.py:134:27: E221 multiple spaces before operator +pymodbus/pdu.py:135:27: E221 multiple spaces before operator +pymodbus/pdu.py:136:27: E221 multiple spaces before operator +pymodbus/register_read_message.py:10:1: E302 expected 2 blank lines, found 1 +pymodbus/register_read_message.py:47:1: E302 expected 2 blank lines, found 1 +pymodbus/register_read_message.py:80:63: E225 missing whitespace around operator +pymodbus/register_read_message.py:126:80: E501 line too long (80 characters) +pymodbus/register_read_message.py:129:1: E302 expected 2 blank lines, found 1 +pymodbus/register_read_message.py:146:1: E302 expected 2 blank lines, found 1 +pymodbus/register_read_message.py:174:80: E501 line too long (80 characters) +pymodbus/register_read_message.py:177:1: E302 expected 2 blank lines, found 1 +pymodbus/register_read_message.py:194:1: E302 expected 2 blank lines, found 1 +pymodbus/register_read_message.py:222:26: E221 multiple spaces before operator +pymodbus/register_read_message.py:223:26: E221 multiple spaces before operator +pymodbus/register_read_message.py:253:52: E225 missing whitespace around operator +pymodbus/register_read_message.py:289:1: E302 expected 2 blank lines, found 1 +pymodbus/register_read_message.py:297:1: W293 blank line contains whitespace +pymodbus/register_read_message.py:311:41: E225 missing whitespace around operator +pymodbus/register_read_message.py:323:63: E225 missing whitespace around operator +pymodbus/register_read_message.py:332:78: W291 trailing whitespace +pymodbus/register_read_message.py:334:78: W291 trailing whitespace +pymodbus/register_write_message.py:10:1: E302 expected 2 blank lines, found 1 +pymodbus/register_write_message.py:68:1: E302 expected 2 blank lines, found 1 +pymodbus/register_write_message.py:112:1: E302 expected 2 blank lines, found 1 +pymodbus/register_write_message.py:131:22: E701 multiple statements on one line (colon) +pymodbus/register_write_message.py:132:45: E701 multiple statements on one line (colon) +pymodbus/register_write_message.py:154:25: E261 at least two spaces before inline comment +pymodbus/register_write_message.py:156:64: E225 missing whitespace around operator +pymodbus/register_write_message.py:182:1: E302 expected 2 blank lines, found 1 +pymodbus/register_write_message.py:222:78: W291 trailing whitespace +pymodbus/register_write_message.py:224:78: W291 trailing whitespace +pymodbus/transaction.py:22:1: E302 expected 2 blank lines, found 1 +pymodbus/transaction.py:34:1: W293 blank line contains whitespace +pymodbus/transaction.py:65:1: W293 blank line contains whitespace +pymodbus/transaction.py:80:14: E231 missing whitespace after ',' +pymodbus/transaction.py:90:14: E231 missing whitespace after ',' +pymodbus/transaction.py:96:1: W293 blank line contains whitespace +pymodbus/transaction.py:113:1: E302 expected 2 blank lines, found 1 +pymodbus/transaction.py:118:1: W293 blank line contains whitespace +pymodbus/transaction.py:122:1: W293 blank line contains whitespace +pymodbus/transaction.py:127:1: W293 blank line contains whitespace +pymodbus/transaction.py:138:31: E231 missing whitespace after ':' +pymodbus/transaction.py:139:21: E221 multiple spaces before operator +pymodbus/transaction.py:140:21: E221 multiple spaces before operator +pymodbus/transaction.py:171:31: E231 missing whitespace after ':' +pymodbus/transaction.py:235:33: E261 at least two spaces before inline comment +pymodbus/transaction.py:236:17: E701 multiple statements on one line (colon) +pymodbus/transaction.py:255:1: E302 expected 2 blank lines, found 1 +pymodbus/transaction.py:261:1: W293 blank line contains whitespace +pymodbus/transaction.py:267:1: W293 blank line contains whitespace +pymodbus/transaction.py:275:1: W293 blank line contains whitespace +pymodbus/transaction.py:296:21: E221 multiple spaces before operator +pymodbus/transaction.py:297:21: E221 multiple spaces before operator +pymodbus/transaction.py:299:21: E221 multiple spaces before operator +pymodbus/transaction.py:339:1: W293 blank line contains whitespace +pymodbus/transaction.py:355:54: E225 missing whitespace around operator +pymodbus/transaction.py:371:14: E221 multiple spaces before operator +pymodbus/transaction.py:372:14: E221 multiple spaces before operator +pymodbus/transaction.py:378:1: W293 blank line contains whitespace +pymodbus/transaction.py:412:33: E261 at least two spaces before inline comment +pymodbus/transaction.py:413:17: E701 multiple statements on one line (colon) +pymodbus/transaction.py:430:1: E302 expected 2 blank lines, found 1 +pymodbus/transaction.py:433:1: W293 blank line contains whitespace +pymodbus/transaction.py:436:1: W293 blank line contains whitespace +pymodbus/transaction.py:441:1: W293 blank line contains whitespace +pymodbus/transaction.py:452:31: E231 missing whitespace after ':' +pymodbus/transaction.py:453:21: E221 multiple spaces before operator +pymodbus/transaction.py:454:21: E221 multiple spaces before operator +pymodbus/transaction.py:455:21: E221 multiple spaces before operator +pymodbus/transaction.py:456:21: E221 multiple spaces before operator +pymodbus/transaction.py:467:23: E701 multiple statements on one line (colon) +pymodbus/transaction.py:468:21: E203 whitespace before ':' +pymodbus/transaction.py:468:23: E261 at least two spaces before inline comment +pymodbus/transaction.py:475:57: E225 missing whitespace around operator +pymodbus/transaction.py:476:43: E225 missing whitespace around operator +pymodbus/transaction.py:487:31: E231 missing whitespace after ':' +pymodbus/transaction.py:512:14: E221 multiple spaces before operator +pymodbus/transaction.py:513:14: E221 multiple spaces before operator +pymodbus/transaction.py:519:1: W293 blank line contains whitespace +pymodbus/transaction.py:553:33: E261 at least two spaces before inline comment +pymodbus/transaction.py:554:17: E701 multiple statements on one line (colon) +pymodbus/transaction.py:563:16: E221 multiple spaces before operator +pymodbus/transaction.py:564:16: E221 multiple spaces before operator +pymodbus/transaction.py:575:1: E302 expected 2 blank lines, found 1 +pymodbus/transaction.py:606:31: E231 missing whitespace after ':' +pymodbus/transaction.py:607:21: E221 multiple spaces before operator +pymodbus/transaction.py:608:21: E221 multiple spaces before operator +pymodbus/transaction.py:608:31: E261 at least two spaces before inline comment +pymodbus/transaction.py:609:21: E221 multiple spaces before operator +pymodbus/transaction.py:609:31: E261 at least two spaces before inline comment +pymodbus/transaction.py:610:21: E221 multiple spaces before operator +pymodbus/transaction.py:621:23: E701 multiple statements on one line (colon) +pymodbus/transaction.py:622:21: E203 whitespace before ':' +pymodbus/transaction.py:622:23: E261 at least two spaces before inline comment +pymodbus/transaction.py:629:80: E501 line too long (83 characters) +pymodbus/transaction.py:629:73: E225 missing whitespace around operator +pymodbus/transaction.py:630:43: E225 missing whitespace around operator +pymodbus/transaction.py:641:31: E231 missing whitespace after ':' +pymodbus/transaction.py:666:14: E221 multiple spaces before operator +pymodbus/transaction.py:667:14: E221 multiple spaces before operator +pymodbus/transaction.py:673:1: W293 blank line contains whitespace +pymodbus/transaction.py:707:33: E261 at least two spaces before inline comment +pymodbus/transaction.py:708:17: E701 multiple statements on one line (colon) +pymodbus/transaction.py:733:21: E225 missing whitespace around operator +pymodbus/transaction.py:736:78: W291 trailing whitespace +pymodbus/transaction.py:738:78: W291 trailing whitespace +pymodbus/utilities.py:13:1: E302 expected 2 blank lines, found 1 +pymodbus/utilities.py:23:1: E302 expected 2 blank lines, found 1 +pymodbus/utilities.py:34:26: E231 missing whitespace after ',' +pymodbus/utilities.py:37:80: E501 line too long (87 characters) +pymodbus/utilities.py:37:26: E231 missing whitespace after ',' +pymodbus/utilities.py:40:26: E231 missing whitespace after ',' +pymodbus/utilities.py:47:1: E302 expected 2 blank lines, found 1 +pymodbus/utilities.py:60:15: E701 multiple statements on one line (colon) +pymodbus/utilities.py:65:13: E701 multiple statements on one line (colon) +pymodbus/utilities.py:67:21: E225 missing whitespace around operator +pymodbus/utilities.py:71:1: E302 expected 2 blank lines, found 1 +pymodbus/utilities.py:93:1: E302 expected 2 blank lines, found 1 +pymodbus/utilities.py:104:17: E701 multiple statements on one line (colon) +pymodbus/utilities.py:111:1: E302 expected 2 blank lines, found 1 +pymodbus/utilities.py:124:51: E702 multiple statements on one line (semicolon) +pymodbus/utilities.py:129:1: E302 expected 2 blank lines, found 1 +pymodbus/utilities.py:138:1: E302 expected 2 blank lines, found 1 +pymodbus/utilities.py:153:1: E302 expected 2 blank lines, found 1 +pymodbus/utilities.py:162:1: E302 expected 2 blank lines, found 1 +pymodbus/utilities.py:184:78: W291 trailing whitespace +pymodbus/utilities.py:186:78: W291 trailing whitespace +pymodbus/version.py:8:1: E302 expected 2 blank lines, found 0 +pymodbus/version.py:37:31: E261 at least two spaces before inline comment +pymodbus/version.py:39:78: W291 trailing whitespace +pymodbus/version.py:41:78: W291 trailing whitespace +pymodbus/client/async.py:13:1: W293 blank line contains whitespace +pymodbus/client/async.py:36:1: E302 expected 2 blank lines, found 1 +pymodbus/client/async.py:49:33: E261 at least two spaces before inline comment +pymodbus/client/async.py:129:1: E302 expected 2 blank lines, found 1 +pymodbus/client/async.py:134:78: W291 trailing whitespace +pymodbus/client/async.py:136:78: W291 trailing whitespace +pymodbus/client/common.py:11:1: E302 expected 2 blank lines, found 1 +pymodbus/client/common.py:133:1: W391 blank line at end of file +pymodbus/client/sync.py:19:1: E302 expected 2 blank lines, found 1 +pymodbus/client/sync.py:50:80: E501 line too long (83 characters) +pymodbus/client/sync.py:51:22: E702 multiple statements on one line (semicolon) +pymodbus/client/sync.py:74:1: E302 expected 2 blank lines, found 1 +pymodbus/client/sync.py:98:80: E501 line too long (80 characters) +pymodbus/client/sync.py:99:1: W293 blank line contains whitespace +pymodbus/client/sync.py:103:80: E501 line too long (80 characters) +pymodbus/client/sync.py:111:80: E501 line too long (80 characters) +pymodbus/client/sync.py:119:80: E501 line too long (80 characters) +pymodbus/client/sync.py:142:80: E501 line too long (81 characters) +pymodbus/client/sync.py:155:1: W293 blank line contains whitespace +pymodbus/client/sync.py:163:1: E302 expected 2 blank lines, found 1 +pymodbus/client/sync.py:177:1: W293 blank line contains whitespace +pymodbus/client/sync.py:180:1: W293 blank line contains whitespace +pymodbus/client/sync.py:183:23: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:194:1: W293 blank line contains whitespace +pymodbus/client/sync.py:222:1: W293 blank line contains whitespace +pymodbus/client/sync.py:230:1: E302 expected 2 blank lines, found 1 +pymodbus/client/sync.py:244:1: W293 blank line contains whitespace +pymodbus/client/sync.py:250:23: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:258:1: W293 blank line contains whitespace +pymodbus/client/sync.py:284:1: W293 blank line contains whitespace +pymodbus/client/sync.py:292:1: E302 expected 2 blank lines, found 1 +pymodbus/client/sync.py:307:21: E221 multiple spaces before operator +pymodbus/client/sync.py:308:21: E221 multiple spaces before operator +pymodbus/client/sync.py:311:21: E221 multiple spaces before operator +pymodbus/client/sync.py:314:21: E221 multiple spaces before operator +pymodbus/client/sync.py:316:21: E221 multiple spaces before operator +pymodbus/client/sync.py:326:31: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:327:29: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:328:32: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:330:1: W293 blank line contains whitespace +pymodbus/client/sync.py:336:23: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:338:78: W291 trailing whitespace +pymodbus/client/sync.py:345:1: W293 blank line contains whitespace +pymodbus/client/sync.py:373:1: W293 blank line contains whitespace +pymodbus/client/sync.py:378:78: W291 trailing whitespace +pymodbus/client/sync.py:380:78: W291 trailing whitespace +pymodbus/datastore/__init__.py:6:78: W291 trailing whitespace +pymodbus/datastore/__init__.py:8:78: W291 trailing whitespace +pymodbus/datastore/context.py:8:15: E702 multiple statements on one line (semicolon) +pymodbus/datastore/context.py:14:1: E302 expected 2 blank lines, found 1 +pymodbus/datastore/context.py:29:1: W191 indentation contains tabs +pymodbus/datastore/context.py:29:1: E101 indentation contains mixed spaces and tabs +pymodbus/datastore/context.py:55:30: E261 at least two spaces before inline comment +pymodbus/datastore/context.py:67:30: E261 at least two spaces before inline comment +pymodbus/datastore/context.py:78:30: E261 at least two spaces before inline comment +pymodbus/datastore/context.py:79:59: E231 missing whitespace after ',' +pymodbus/datastore/context.py:82:1: E302 expected 2 blank lines, found 1 +pymodbus/datastore/context.py:96:21: E221 multiple spaces before operator +pymodbus/datastore/context.py:115:23: E701 multiple statements on one line (colon) +pymodbus/datastore/context.py:118:13: E701 multiple statements on one line (colon) +pymodbus/datastore/context.py:126:23: E701 multiple statements on one line (colon) +pymodbus/datastore/context.py:127:25: W601 .has_key() is deprecated, use 'in' +pymodbus/datastore/context.py:129:80: E501 line too long (82 characters) +pymodbus/datastore/context.py:129:13: E701 multiple statements on one line (colon) +pymodbus/datastore/context.py:130:1: W391 blank line at end of file +pymodbus/datastore/database.py:13:15: E702 multiple statements on one line (semicolon) +pymodbus/datastore/database.py:19:1: E302 expected 2 blank lines, found 1 +pymodbus/datastore/database.py:45:40: E261 at least two spaces before inline comment +pymodbus/datastore/database.py:55:30: E261 at least two spaces before inline comment +pymodbus/datastore/database.py:67:30: E261 at least two spaces before inline comment +pymodbus/datastore/database.py:78:30: E261 at least two spaces before inline comment +pymodbus/datastore/database.py:79:60: E231 missing whitespace after ',' +pymodbus/datastore/database.py:82:80: E501 line too long (80 characters) +pymodbus/datastore/database.py:84:80: E501 line too long (80 characters) +pymodbus/datastore/database.py:100:1: W293 blank line contains whitespace +pymodbus/datastore/database.py:110:40: W291 trailing whitespace +pymodbus/datastore/database.py:109:14: E221 multiple spaces before operator +pymodbus/datastore/database.py:127:26: E203 whitespace before ':' +pymodbus/datastore/database.py:127:18: E225 missing whitespace around operator +pymodbus/datastore/database.py:141:15: E221 multiple spaces before operator +pymodbus/datastore/database.py:142:15: E221 multiple spaces before operator +pymodbus/datastore/database.py:144:1: W293 blank line contains whitespace +pymodbus/datastore/database.py:153:15: E221 multiple spaces before operator +pymodbus/datastore/database.py:154:15: E221 multiple spaces before operator +pymodbus/datastore/database.py:157:15: E221 multiple spaces before operator +pymodbus/datastore/database.py:169:40: W291 trailing whitespace +pymodbus/datastore/database.py:168:14: E221 multiple spaces before operator +pymodbus/datastore/database.py:174:1: W391 blank line at end of file +pymodbus/datastore/modredis.py:8:15: E702 multiple statements on one line (semicolon) +pymodbus/datastore/modredis.py:14:1: E302 expected 2 blank lines, found 1 +pymodbus/datastore/modredis.py:52:30: E261 at least two spaces before inline comment +pymodbus/datastore/modredis.py:64:30: E261 at least two spaces before inline comment +pymodbus/datastore/modredis.py:75:30: E261 at least two spaces before inline comment +pymodbus/datastore/modredis.py:76:59: E231 missing whitespace after ',' +pymodbus/datastore/modredis.py:79:80: E501 line too long (80 characters) +pymodbus/datastore/modredis.py:81:80: E501 line too long (80 characters) +pymodbus/datastore/modredis.py:96:16: E203 whitespace before ':' +pymodbus/datastore/modredis.py:96:27: E231 missing whitespace after ',' +pymodbus/datastore/modredis.py:102:16: E203 whitespace before ':' +pymodbus/datastore/modredis.py:102:27: E231 missing whitespace after ',' +pymodbus/datastore/modredis.py:108:16: E203 whitespace before ':' +pymodbus/datastore/modredis.py:108:27: E231 missing whitespace after ',' +pymodbus/datastore/modredis.py:114:80: E501 line too long (80 characters) +pymodbus/datastore/modredis.py:116:80: E501 line too long (80 characters) +pymodbus/datastore/modredis.py:117:17: E221 multiple spaces before operator +pymodbus/datastore/modredis.py:129:26: E225 missing whitespace around operator +pymodbus/datastore/modredis.py:131:59: E225 missing whitespace around operator +pymodbus/datastore/modredis.py:131:16: E221 multiple spaces before operator +pymodbus/datastore/modredis.py:157:36: E225 missing whitespace around operator +pymodbus/datastore/modredis.py:168:26: E225 missing whitespace around operator +pymodbus/datastore/modredis.py:174:61: E225 missing whitespace around operator +pymodbus/datastore/modredis.py:175:80: E501 line too long (90 characters) +pymodbus/datastore/modredis.py:175:31: E225 missing whitespace around operator +pymodbus/datastore/modredis.py:175:15: E221 multiple spaces before operator +pymodbus/datastore/modredis.py:178:58: E225 missing whitespace around operator +pymodbus/datastore/modredis.py:182:80: E501 line too long (80 characters) +pymodbus/datastore/modredis.py:184:80: E501 line too long (80 characters) +pymodbus/datastore/modredis.py:185:17: E221 multiple spaces before operator +pymodbus/datastore/modredis.py:200:68: E225 missing whitespace around operator +pymodbus/datastore/modredis.py:200:16: E221 multiple spaces before operator +pymodbus/datastore/modredis.py:224:38: E225 missing whitespace around operator +pymodbus/datastore/modredis.py:240:67: E225 missing whitespace around operator +pymodbus/datastore/modredis.py:243:1: W391 blank line at end of file +pymodbus/datastore/remote.py:98:32: E701 multiple statements on one line (colon) +pymodbus/datastore/remote.py:99:32: E701 multiple statements on one line (colon) +pymodbus/datastore/remote.py:100:13: E701 multiple statements on one line (colon) +pymodbus/datastore/store.py:144:13: E701 multiple statements on one line (colon) +pymodbus/datastore/store.py:154:15: E221 multiple spaces before operator +pymodbus/datastore/store.py:195:13: E701 multiple statements on one line (colon) +pymodbus/datastore/store.py:207:22: E701 multiple statements on one line (colon) +pymodbus/internal/ptwisted.py:50:15: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:60:34: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:64:19: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:130:20: E221 multiple spaces before operator +pymodbus/server/sync.py:154:35: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:179:20: E221 multiple spaces before operator +pymodbus/server/sync.py:203:35: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:228:20: E221 multiple spaces before operator +pymodbus/server/sync.py:235:21: E221 multiple spaces before operator +pymodbus/server/sync.py:238:21: E221 multiple spaces before operator +pymodbus/server/sync.py:240:21: E221 multiple spaces before operator +pymodbus/server/sync.py:241:21: E221 multiple spaces before operator +pymodbus/server/sync.py:249:23: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:280:19: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:291:1: E302 expected 2 blank lines, found 1 +pymodbus/server/sync.py:301:1: E302 expected 2 blank lines, found 1 +pymodbus/server/sync.py:311:1: E302 expected 2 blank lines, found 1 +pymodbus/server/sync.py:321:78: W291 trailing whitespace +pymodbus/server/sync.py:323:78: W291 trailing whitespace +1 E101 indentation contains mixed spaces and tabs +7 E203 whitespace before ':' +119 E221 multiple spaces before operator +25 E225 missing whitespace around operator +17 E231 missing whitespace after ',' +23 E261 at least two spaces before inline comment +51 E302 expected 2 blank lines, found 1 +42 E501 line too long (80 characters) +42 E701 multiple statements on one line (colon) +5 E702 multiple statements on one line (semicolon) +1 W191 indentation contains tabs +38 W291 trailing whitespace +36 W293 blank line contains whitespace +4 W391 blank line at end of file +1 W601 .has_key() is deprecated, use 'in' diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index b098ce5bf..3f0fe959d 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -28,4 +28,3 @@ def emit(self, record): h = NullHandler() logging.getLogger(__name__).addHandler(h) - diff --git a/pymodbus/bit_write_message.py b/pymodbus/bit_write_message.py index c118efa65..de951f5b5 100644 --- a/pymodbus/bit_write_message.py +++ b/pymodbus/bit_write_message.py @@ -19,6 +19,7 @@ _turn_coil_on = struct.pack(">H", ModbusStatus.On) _turn_coil_off = struct.pack(">H", ModbusStatus.Off) + class WriteSingleCoilRequest(ModbusRequest): ''' This function code is used to write a single output to either ON or OFF @@ -88,6 +89,7 @@ def __str__(self): ''' return "WriteCoilRequest(%d, %s) => " % (self.address, self.value) + class WriteSingleCoilResponse(ModbusResponse): ''' The normal response is an echo of the request, returned after the coil @@ -130,6 +132,7 @@ def __str__(self): ''' return "WriteCoilResponse(%d) => %d" % (self.address, self.value) + class WriteMultipleCoilsRequest(ModbusRequest): ''' "This function code is used to force each coil in a sequence of coils to @@ -202,6 +205,7 @@ def __str__(self): params = (self.address, len(self.values)) return "WriteNCoilRequest (%d) => %d " % params + class WriteMultipleCoilsResponse(ModbusResponse): ''' The normal response returns the function code, starting address, and @@ -241,9 +245,9 @@ def __str__(self): ''' return "WriteNCoilResponse(%d, %d)" % (self.address, self.count) -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ "WriteSingleCoilRequest", "WriteSingleCoilResponse", "WriteMultipleCoilsRequest", "WriteMultipleCoilsResponse", diff --git a/pymodbus/constants.py b/pymodbus/constants.py index b64ee1511..b0a37bd84 100644 --- a/pymodbus/constants.py +++ b/pymodbus/constants.py @@ -7,6 +7,7 @@ ''' from pymodbus.interfaces import Singleton + class Defaults(Singleton): ''' A collection of modbus default values @@ -82,6 +83,7 @@ class Defaults(Singleton): Bytesize = 8 Stopbits = 1 + class ModbusStatus(Singleton): ''' These represent various status codes in the modbus diff --git a/pymodbus/datastore/remote.py b/pymodbus/datastore/remote.py index c7bf61218..693e8eaae 100644 --- a/pymodbus/datastore/remote.py +++ b/pymodbus/datastore/remote.py @@ -4,9 +4,10 @@ #---------------------------------------------------------------------------# # Logging #---------------------------------------------------------------------------# -import logging; +import logging _logger = logging.getLogger(__name__) + #---------------------------------------------------------------------------# # Context #---------------------------------------------------------------------------# @@ -77,16 +78,16 @@ def __build_mapping(self): code mapper. ''' self.__get_callbacks = { - 'd' : lambda a,c: self._client.read_discrete_inputs(a, c), - 'c' : lambda a,c: self._client.read_coils(a, c), - 'h' : lambda a,c: self._client.read_holding_registers(a, c), - 'i' : lambda a,c: self._client.read_input_registers(a, c), + 'd': lambda a, c: self._client.read_discrete_inputs(a, c), + 'c': lambda a, c: self._client.read_coils(a, c), + 'h': lambda a, c: self._client.read_holding_registers(a, c), + 'i': lambda a, c: self._client.read_input_registers(a, c), } self.__set_callbacks = { - 'd' : lambda a,v: self._client.write_coils(a, v), - 'c' : lambda a,v: self._client.write_coils(a, v), - 'h' : lambda a,v: self._client.write_registers(a, v), - 'i' : lambda a,v: self._client.write_registers(a, v), + 'd': lambda a, v: self._client.write_coils(a, v), + 'c': lambda a, v: self._client.write_coils(a, v), + 'h': lambda a, v: self._client.write_registers(a, v), + 'i': lambda a, v: self._client.write_registers(a, v), } def __extract_result(self, fx, result): @@ -97,4 +98,3 @@ def __extract_result(self, fx, result): if fx in ['d', 'c']: return result.bits if fx in ['h', 'i']: return result.registers else: return result - diff --git a/pymodbus/datastore/store.py b/pymodbus/datastore/store.py index db8f246d2..e760bb6bc 100644 --- a/pymodbus/datastore/store.py +++ b/pymodbus/datastore/store.py @@ -51,9 +51,10 @@ #---------------------------------------------------------------------------# # Logging #---------------------------------------------------------------------------# -import logging; +import logging _logger = logging.getLogger(__name__) + #---------------------------------------------------------------------------# # Datablock Storage #---------------------------------------------------------------------------# @@ -127,6 +128,7 @@ def __iter__(self): return self.values.iteritems() return enumerate(self.values) + class ModbusSequentialDataBlock(BaseModbusDataBlock): ''' Creates a sequential modbus datastore ''' @@ -161,7 +163,7 @@ def getValues(self, address, count=1): :returns: The requested values from a:a+c ''' start = address - self.address - return self.values[start:start+count] + return self.values[start:start + count] def setValues(self, address, values): ''' Sets the requested values of the datastore @@ -172,7 +174,8 @@ def setValues(self, address, values): if not isinstance(values, list): values = [values] start = address - self.address - self.values[start:start+len(values)] = values + self.values[start:start + len(values)] = values + class ModbusSparseDataBlock(BaseModbusDataBlock): ''' Creates a sparse modbus datastore ''' @@ -189,7 +192,8 @@ def __init__(self, values): self.values = values if hasattr(values, '__iter__'): self.values = dict(enumerate(values)) - else: raise ParameterException("Values for datastore must be a list or dictionary") + else: raise ParameterException( + "Values for datastore must be a list or dictionary") self.default_value = self.values.values()[0].__class__() self.address = self.values.iterkeys().next() @@ -220,11 +224,10 @@ def setValues(self, address, values): :param values: The new values to be set ''' if isinstance(values, dict): - for idx,val in values.iteritems(): + for idx, val in values.iteritems(): self.values[idx] = val else: if not isinstance(values, list): values = [values] - for idx,val in enumerate(values): + for idx, val in enumerate(values): self.values[address + idx] = val - diff --git a/pymodbus/device.py b/pymodbus/device.py index e0402aa75..a202014ff 100644 --- a/pymodbus/device.py +++ b/pymodbus/device.py @@ -10,6 +10,7 @@ from pymodbus.interfaces import Singleton from pymodbus.utilities import dict_property + #---------------------------------------------------------------------------# # Network Access Control #---------------------------------------------------------------------------# @@ -64,6 +65,7 @@ def check(self, host): ''' return host in self.__nmstable + #---------------------------------------------------------------------------# # Device Information Control #---------------------------------------------------------------------------# @@ -155,9 +157,9 @@ def __str__(self): ''' return "DeviceIdentity" - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# # Properties - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# VendorName = dict_property(lambda s: s.__data, 0) ProductCode = dict_property(lambda s: s.__data, 1) MajorMinorRevision = dict_property(lambda s: s.__data, 2) @@ -166,6 +168,7 @@ def __str__(self): ModelName = dict_property(lambda s: s.__data, 5) UserApplicationName = dict_property(lambda s: s.__data, 6) + #---------------------------------------------------------------------------# # Counters Handler #---------------------------------------------------------------------------# @@ -178,60 +181,63 @@ class ModbusCountersHandler(object): Quantity of messages that the remote device has detected on the communications system since its last restart, clear counters operation, or power-up. Messages - with bad CRC are not taken into account. + with bad CRC are not taken into account. - 0x0C 2 Return Bus Communication Error Count + 0x0C 2 Return Bus Communication Error Count - Quantity of CRC errors encountered by the remote device since its last - restart, clear counters operation, or power-up. In case of an error - detected on the character level, (overrun, parity error), or in case of a - message length < 3 bytes, the receiving device is not able to calculate - the CRC. In such cases, this counter is also incremented. + Quantity of CRC errors encountered by the remote device since its + last restart, clear counters operation, or power-up. In case of + an error detected on the character level, (overrun, parity error), + or in case of a message length < 3 bytes, the receiving device is + not able to calculate the CRC. In such cases, this counter is + also incremented. 0x0D 3 Return Slave Exception Error Count - Quantity of MODBUS exception error detected by the remote device - since its last restart, clear counters operation, or power-up. It - comprises also the error detected in broadcast messages even if an - exception message is not returned in this case. - Exception errors are described and listed in "MODBUS Application - Protocol Specification" document. + Quantity of MODBUS exception error detected by the remote device + since its last restart, clear counters operation, or power-up. It + comprises also the error detected in broadcast messages even if an + exception message is not returned in this case. + Exception errors are described and listed in "MODBUS Application + Protocol Specification" document. 0xOE 4 Return Slave Message Count - Quantity of messages addressed to the remote device, including - broadcast messages, that the remote device has processed since its - last restart, clear counters operation, or power-up. + Quantity of messages addressed to the remote device, including + broadcast messages, that the remote device has processed since its + last restart, clear counters operation, or power-up. + + 0x0F 5 Return Slave No Response Count - 0x0F 5 Return Slave No Response Count - Quantity of messages received by the remote device for which it - returned no response (neither a normal response nor an exception - response), since its last restart, clear counters operation, or power-up. - Then, this counter counts the number of broadcast messages it has - received. + Quantity of messages received by the remote device for which it + returned no response (neither a normal response nor an exception + response), since its last restart, clear counters operation, or + power-up. Then, this counter counts the number of broadcast + messages it has received. 0x10 6 Return Slave NAK Count - Quantity of messages addressed to the remote device for which it - returned a Negative Acknowledge (NAK) exception response, since its - last restart, clear counters operation, or power-up. Exception - responses are described and listed in "MODBUS Application Protocol - Specification" document. + Quantity of messages addressed to the remote device for which it + returned a Negative Acknowledge (NAK) exception response, since + its last restart, clear counters operation, or power-up. Exception + responses are described and listed in "MODBUS Application Protocol + Specification" document. 0x11 7 Return Slave Busy Count - Quantity of messages addressed to the remote device for which it - returned a Slave Device Busy exception response, since its last restart, - clear counters operation, or power-up. Exception responses are - described and listed in "MODBUS Application Protocol Specification" - document + Quantity of messages addressed to the remote device for which it + returned a Slave Device Busy exception response, since its last + restart, clear counters operation, or power-up. Exception + responses are described and listed in "MODBUS Application + Protocol Specification" document. 0x12 8 Return Bus Character Overrun Count - Quantity of messages addressed to the remote device that it could not - handle due to a character overrun condition, since its last restart, clear - counters operation, or power-up. A character overrun is caused by data - characters arriving at the port faster than they can + Quantity of messages addressed to the remote device that it could + not handle due to a character overrun condition, since its last + restart, clear counters operation, or power-up. A character + overrun is caused by data characters arriving at the port faster + than they can. .. note:: I threw the event counter in here for convinience ''' @@ -281,9 +287,9 @@ def summary(self): count <<= 1 return result - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# # Properties - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# BusMessage = dict_property(lambda s: s.__data, 0) BusCommunicationError = dict_property(lambda s: s.__data, 1) BusExceptionError = dict_property(lambda s: s.__data, 2) @@ -294,6 +300,7 @@ def summary(self): BusCharacterOverrun = dict_property(lambda s: s.__data, 7) Event = dict_property(lambda s: s.__data, 8) + #---------------------------------------------------------------------------# # Main server controll block #---------------------------------------------------------------------------# @@ -314,9 +321,9 @@ class ModbusControlBlock(Singleton): __identity = ModbusDeviceIdentification() __events = [] - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# # Magic - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# def __str__(self): ''' Build a representation of the control block @@ -331,9 +338,9 @@ def __iter__(self): ''' return self.__counters.__iter__() - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# # Events - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# def addEvent(self, event): ''' Adds a new event to the event log @@ -356,9 +363,9 @@ def clearEvents(self): ''' self.__events = [] - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# # Other Properties - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# Identity = property(lambda s: s.__identity) Counter = property(lambda s: s.__counters) Events = property(lambda s: s.__events) @@ -371,9 +378,9 @@ def reset(self): self.__counters.reset() self.__diagnostic = [False] * 16 - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# # Listen Properties - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# def _setListenOnly(self, value): ''' This toggles the listen only status @@ -383,9 +390,9 @@ def _setListenOnly(self, value): ListenOnly = property(lambda s: s.__listen_only, _setListenOnly) - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# # Mode Properties - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# def _setMode(self, mode): ''' This toggles the current serial mode @@ -396,9 +403,9 @@ def _setMode(self, mode): Mode = property(lambda s: s.__mode, _setMode) - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# # Delimiter Properties - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# def _setDelimiter(self, char): ''' This changes the serial delimiter character @@ -411,9 +418,9 @@ def _setDelimiter(self, char): Delimiter = property(lambda s: s.__delimiter, _setDelimiter) - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# # Diagnostic Properties - #---------------------------------------------------------------------------# + #-------------------------------------------------------------------------# def setDiagnostic(self, mapping): ''' This sets the value in the diagnostic register @@ -440,9 +447,9 @@ def getDiagnosticRegister(self): ''' return self.__diagnostic -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported Identifiers -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ "ModbusAccessControl", "ModbusDeviceIdentification", diff --git a/pymodbus/diag_message.py b/pymodbus/diag_message.py index 5a2d0c24f..b61ff70e2 100644 --- a/pymodbus/diag_message.py +++ b/pymodbus/diag_message.py @@ -16,6 +16,7 @@ _MCB = ModbusControlBlock() + #---------------------------------------------------------------------------# # Diagnostic Function Codes Base Classes # diagnostic 08, 00-18,20 @@ -62,6 +63,7 @@ def decode(self, data): ''' self.sub_function_code, self.message = struct.unpack('>HH', data) + class DiagnosticStatusResponse(ModbusResponse): ''' This is a base class for all of the diagnostic response functions @@ -105,6 +107,7 @@ def decode(self, data): ''' self.sub_function_code, self.message = struct.unpack('>HH', data) + class DiagnosticStatusSimpleRequest(DiagnosticStatusRequest): ''' A large majority of the diagnostic functions are simple @@ -132,6 +135,7 @@ def execute(self, *args): ''' Base function to raise if not implemented ''' raise NotImplementedException("Diagnostic Message Has No Execute Method") + class DiagnosticStatusSimpleResponse(DiagnosticStatusResponse): ''' A large majority of the diagnostic functions are simple @@ -148,6 +152,7 @@ def __init__(self, data): DiagnosticStatusResponse.__init__(self) self.message = data + #---------------------------------------------------------------------------# # Diagnostic Sub Code 00 #---------------------------------------------------------------------------# @@ -176,6 +181,7 @@ def execute(self, *args): ''' return ReturnQueryDataResponse(self.message) + class ReturnQueryDataResponse(DiagnosticStatusResponse): ''' The data passed in the request data field is to be returned (looped back) @@ -194,6 +200,7 @@ def __init__(self, message=0x0000): self.message = message else: self.message = [message] + #---------------------------------------------------------------------------# # Diagnostic Sub Code 01 #---------------------------------------------------------------------------# @@ -226,6 +233,7 @@ def execute(self, *args): #if _MCB.ListenOnly: return RestartCommunicationsOptionResponse(self.message) + class RestartCommunicationsOptionResponse(DiagnosticStatusResponse): ''' The remote device serial line port must be initialized and restarted, and @@ -247,6 +255,7 @@ def __init__(self, toggle=False): self.message = [ModbusStatus.On] else: self.message = [ModbusStatus.Off] + #---------------------------------------------------------------------------# # Diagnostic Sub Code 02 #---------------------------------------------------------------------------# @@ -266,6 +275,7 @@ def execute(self, *args): register = pack_bitstring(_MCB.getDiagnosticRegister()) return ReturnDiagnosticRegisterResponse(register) + class ReturnDiagnosticRegisterResponse(DiagnosticStatusSimpleResponse): ''' The contents of the remote device's 16-bit diagnostic register are @@ -273,6 +283,7 @@ class ReturnDiagnosticRegisterResponse(DiagnosticStatusSimpleResponse): ''' sub_function_code = 0x0002 + #---------------------------------------------------------------------------# # Diagnostic Sub Code 03 #---------------------------------------------------------------------------# @@ -294,6 +305,7 @@ def execute(self, *args): _MCB.Delimiter = char return ChangeAsciiInputDelimiterResponse(self.message) + class ChangeAsciiInputDelimiterResponse(DiagnosticStatusSimpleResponse): ''' The character 'CHAR' passed in the request data field becomes the end of @@ -303,6 +315,7 @@ class ChangeAsciiInputDelimiterResponse(DiagnosticStatusSimpleResponse): ''' sub_function_code = 0x0003 + #---------------------------------------------------------------------------# # Diagnostic Sub Code 04 #---------------------------------------------------------------------------# @@ -323,6 +336,7 @@ def execute(self, *args): _MCB.ListenOnly = True return ForceListenOnlyModeResponse() + class ForceListenOnlyModeResponse(DiagnosticStatusResponse): ''' Forces the addressed remote device to its Listen Only Mode for MODBUS @@ -341,6 +355,7 @@ def __init__(self): DiagnosticStatusResponse.__init__(self) self.message = [] + #---------------------------------------------------------------------------# # Diagnostic Sub Code 10 #---------------------------------------------------------------------------# @@ -359,6 +374,7 @@ def execute(self, *args): _MCB.reset() return ClearCountersResponse(self.message) + class ClearCountersResponse(DiagnosticStatusSimpleResponse): ''' The goal is to clear ll counters and the diagnostic register. @@ -366,6 +382,7 @@ class ClearCountersResponse(DiagnosticStatusSimpleResponse): ''' sub_function_code = 0x000A + #---------------------------------------------------------------------------# # Diagnostic Sub Code 11 #---------------------------------------------------------------------------# @@ -385,6 +402,7 @@ def execute(self, *args): count = _MCB.Counter.BusMessage return ReturnBusMessageCountResponse(count) + class ReturnBusMessageCountResponse(DiagnosticStatusSimpleResponse): ''' The response data field returns the quantity of messages that the @@ -393,6 +411,7 @@ class ReturnBusMessageCountResponse(DiagnosticStatusSimpleResponse): ''' sub_function_code = 0x000B + #---------------------------------------------------------------------------# # Diagnostic Sub Code 12 #---------------------------------------------------------------------------# @@ -412,6 +431,7 @@ def execute(self, *args): count = _MCB.Counter.BusCommunicationError return ReturnBusCommunicationErrorCountResponse(count) + class ReturnBusCommunicationErrorCountResponse(DiagnosticStatusSimpleResponse): ''' The response data field returns the quantity of CRC errors encountered @@ -420,6 +440,7 @@ class ReturnBusCommunicationErrorCountResponse(DiagnosticStatusSimpleResponse): ''' sub_function_code = 0x000C + #---------------------------------------------------------------------------# # Diagnostic Sub Code 13 #---------------------------------------------------------------------------# @@ -439,6 +460,7 @@ def execute(self, *args): count = _MCB.Counter.BusExceptionError return ReturnBusExceptionErrorCountResponse(count) + class ReturnBusExceptionErrorCountResponse(DiagnosticStatusSimpleResponse): ''' The response data field returns the quantity of modbus exception @@ -447,6 +469,7 @@ class ReturnBusExceptionErrorCountResponse(DiagnosticStatusSimpleResponse): ''' sub_function_code = 0x000D + #---------------------------------------------------------------------------# # Diagnostic Sub Code 14 #---------------------------------------------------------------------------# @@ -466,6 +489,7 @@ def execute(self, *args): count = _MCB.Counter.SlaveMessage return ReturnSlaveMessageCountResponse(count) + class ReturnSlaveMessageCountResponse(DiagnosticStatusSimpleResponse): ''' The response data field returns the quantity of messages addressed to the @@ -474,6 +498,7 @@ class ReturnSlaveMessageCountResponse(DiagnosticStatusSimpleResponse): ''' sub_function_code = 0x000E + #---------------------------------------------------------------------------# # Diagnostic Sub Code 15 #---------------------------------------------------------------------------# @@ -493,6 +518,7 @@ def execute(self, *args): count = _MCB.Counter.SlaveNoResponse return ReturnSlaveNoReponseCountResponse(count) + class ReturnSlaveNoReponseCountResponse(DiagnosticStatusSimpleResponse): ''' The response data field returns the quantity of messages addressed to the @@ -501,6 +527,7 @@ class ReturnSlaveNoReponseCountResponse(DiagnosticStatusSimpleResponse): ''' sub_function_code = 0x000F + #---------------------------------------------------------------------------# # Diagnostic Sub Code 16 #---------------------------------------------------------------------------# @@ -521,6 +548,7 @@ def execute(self, *args): count = _MCB.Counter.SlaveNAK return ReturnSlaveNAKCountResponse(count) + class ReturnSlaveNAKCountResponse(DiagnosticStatusSimpleResponse): ''' The response data field returns the quantity of messages addressed to the @@ -530,6 +558,7 @@ class ReturnSlaveNAKCountResponse(DiagnosticStatusSimpleResponse): ''' sub_function_code = 0x0010 + #---------------------------------------------------------------------------# # Diagnostic Sub Code 17 #---------------------------------------------------------------------------# @@ -549,6 +578,7 @@ def execute(self, *args): count = _MCB.Counter.SlaveBusy return ReturnSlaveBusyCountResponse(count) + class ReturnSlaveBusyCountResponse(DiagnosticStatusSimpleResponse): ''' The response data field returns the quantity of messages addressed to the @@ -557,6 +587,7 @@ class ReturnSlaveBusyCountResponse(DiagnosticStatusSimpleResponse): ''' sub_function_code = 0x0011 + #---------------------------------------------------------------------------# # Diagnostic Sub Code 18 #---------------------------------------------------------------------------# @@ -578,6 +609,7 @@ def execute(self, *args): count = _MCB.Counter.BusCharacterOverrun return ReturnSlaveBusCharacterOverrunCountResponse(count) + class ReturnSlaveBusCharacterOverrunCountResponse(DiagnosticStatusSimpleResponse): ''' The response data field returns the quantity of messages addressed to the @@ -588,6 +620,7 @@ class ReturnSlaveBusCharacterOverrunCountResponse(DiagnosticStatusSimpleResponse ''' sub_function_code = 0x0012 + #---------------------------------------------------------------------------# # Diagnostic Sub Code 20 #---------------------------------------------------------------------------# @@ -608,6 +641,7 @@ def execute(self, *args): _MCB.Counter.BusCharacterOverrun = 0x0000 return ClearOverrunCountResponse(self.message) + class ClearOverrunCountResponse(DiagnosticStatusSimpleResponse): ''' Clears the overrun error counter and reset the error flag diff --git a/pymodbus/events.py b/pymodbus/events.py index 6f9a97b47..9ff5c9fba 100644 --- a/pymodbus/events.py +++ b/pymodbus/events.py @@ -10,6 +10,7 @@ from pymodbus.exceptions import ParameterException from pymodbus.utilities import pack_bitstring, unpack_bitstring + class ModbusEvent(object): def encode(self): @@ -26,21 +27,23 @@ def decode(self, event): ''' raise NotImplementedException() + class RemoteReceiveEvent(ModbusEvent): - ''' Remote device MODBUS Receive Event + ''' Remote device MODBUS Receive Event The remote device stores this type of event byte when a query message is received. It is stored before the remote device processes the message. This event is defined by bit 7 set to logic '1'. The other bits will be - set to a logic '1' if the corresponding condition is TRUE. The bit layout is:: + set to a logic '1' if the corresponding condition is TRUE. The bit layout + is:: - Bit Contents + Bit Contents ---------------------------------- - 0 Not Used - 2 Not Used - 3 Not Used - 4 Character Overrun - 5 Currently in Listen Only Mode + 0 Not Used + 2 Not Used + 3 Not Used + 4 Character Overrun + 5 Currently in Listen Only Mode 6 Broadcast Receive 7 1 ''' @@ -72,8 +75,10 @@ def decode(self, event): self.listen = bits[5] self.broadcast = bits[6] + class RemoteSendEvent(ModbusEvent): - ''' Remote device MODBUS Send Event + ''' Remote device MODBUS Send Event + The remote device stores this type of event byte when it finishes processing a request message. It is stored if the remote device returned a normal or exception response, or no response. @@ -82,15 +87,15 @@ class RemoteSendEvent(ModbusEvent): The other bits will be set to a logic '1' if the corresponding condition is TRUE. The bit layout is:: - Bit Contents + Bit Contents ----------------------------------------------------------- - 0 Read Exception Sent (Exception Codes 1-3) - 1 Slave Abort Exception Sent (Exception Code 4) - 2 Slave Busy Exception Sent (Exception Codes 5-6) - 3 Slave Program NAK Exception Sent (Exception Code 7) - 4 Write Timeout Error Occurred - 5 Currently in Listen Only Mode - 6 1 + 0 Read Exception Sent (Exception Codes 1-3) + 1 Slave Abort Exception Sent (Exception Code 4) + 2 Slave Busy Exception Sent (Exception Codes 5-6) + 3 Slave Program NAK Exception Sent (Exception Code 7) + 4 Write Timeout Error Occurred + 5 Currently in Listen Only Mode + 6 1 7 0 ''' @@ -129,11 +134,12 @@ def decode(self, event): self.write_timeout = bits[4] self.listen = bits[5] + class EnteredListenModeEvent(ModbusEvent): ''' Remote device Entered Listen Only Mode The remote device stores this type of event byte when it enters - the Listen Only Mode. The event is defined by a content of 04 hex. + the Listen Only Mode. The event is defined by a content of 04 hex. ''' value = 0x04 @@ -154,19 +160,20 @@ def decode(self, event): if event != self.__encoded: raise ParameterException('Invalid decoded value') + class CommunicationRestartEvent(ModbusEvent): ''' Remote device Initiated Communication Restart The remote device stores this type of event byte when its communications port is restarted. The remote device can be restarted by the Diagnostics function (code 08), with sub-function Restart Communications Option - (code 00 01). + (code 00 01). That function also places the remote device into a 'Continue on Error' - or 'Stop on Error' mode. If the remote device is placed into 'Continue on Error' - mode, the event byte is added to the existing event log. If the remote device - is placed into 'Stop on Error' mode, the byte is added to the log and the - rest of the log is cleared to zeros. + or 'Stop on Error' mode. If the remote device is placed into 'Continue on + Error' mode, the event byte is added to the existing event log. If the + remote device is placed into 'Stop on Error' mode, the byte is added to + the log and the rest of the log is cleared to zeros. The event is defined by a content of zero. ''' @@ -188,4 +195,3 @@ def decode(self, event): ''' if event != self.__encoded: raise ParameterException('Invalid decoded value') - diff --git a/pymodbus/exceptions.py b/pymodbus/exceptions.py index 2e9350d6f..eea0b4ce6 100644 --- a/pymodbus/exceptions.py +++ b/pymodbus/exceptions.py @@ -1,9 +1,10 @@ -""" +''' Pymodbus Exceptions -------------------- Custom exceptions to be used in the Modbus code. -""" +''' + class ModbusException(Exception): ''' Base modbus exception ''' @@ -17,6 +18,7 @@ def __init__(self, string): def __str__(self): return 'Modbus Error: %s' % self.string + class ModbusIOException(ModbusException): ''' Error resulting from data i/o ''' @@ -27,6 +29,7 @@ def __init__(self, string=""): message = "[Input/Output] %s" % string ModbusException.__init__(self, message) + class ParameterException(ModbusException): ''' Error resulting from invalid paramater ''' @@ -37,6 +40,7 @@ def __init__(self, string=""): message = "[Invalid Paramter] %s" % string ModbusException.__init__(self, message) + class NotImplementedException(ModbusException): ''' Error resulting from not implemented function ''' @@ -47,6 +51,7 @@ def __init__(self, string=""): message = "[Not Implemented] %s" % string ModbusException.__init__(self, message) + class ConnectionException(ModbusException): ''' Error resulting from a bad connection ''' @@ -57,9 +62,9 @@ def __init__(self, string=""): message = "[Connection] %s" % string ModbusException.__init__(self, message) -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ "ModbusException", "ModbusIOException", "ParameterException", "NotImplementedException", diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 5b19f30fa..647d72f64 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -23,10 +23,10 @@ import logging _logger = logging.getLogger(__name__) + #---------------------------------------------------------------------------# # Server Decoder #---------------------------------------------------------------------------# - class ServerDecoder(IModbusDecoder): ''' Request Message Factory (Server) @@ -82,10 +82,10 @@ def _helper(self, data): request.decode(data[1:]) return request -#---------------------------------------------------------------------------# -# Client Decoder -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# +# Client Decoder +#---------------------------------------------------------------------------# class ClientDecoder(IModbusDecoder): ''' Response Message Factory (Client) @@ -144,7 +144,7 @@ def _helper(self, data): response.decode(data[1:]) return response -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = ['ServerDecoder', 'ClientDecoder'] diff --git a/pymodbus/file_message.py b/pymodbus/file_message.py index 5fbfa2c78..b64961e12 100644 --- a/pymodbus/file_message.py +++ b/pymodbus/file_message.py @@ -4,19 +4,18 @@ Currently none of these messages are implemented ''' - import struct from pymodbus.pdu import ModbusRequest from pymodbus.pdu import ModbusResponse from pymodbus.pdu import ModbusExceptions as merror + #---------------------------------------------------------------------------# # TODO finish these requests #---------------------------------------------------------------------------# # Read File Record 20 # Write File Record 21 # mask write register 22 - class ReadFifoQueueRequest(ModbusRequest): ''' This function code allows to read the contents of a First-In-First-Out @@ -63,6 +62,7 @@ def execute(self, context): return self.doException(merror.IllegalValue) return ReadFifoQueueResponse(self.values) + class ReadFifoQueueResponse(ModbusResponse): ''' In a normal response, the byte count shows the quantity of bytes to @@ -116,9 +116,9 @@ def decode(self, data): idx = 4 + index * 2 self.values.append(struct.unpack('>H', data[idx:idx + 2])[0]) -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ "ReadFifoQueueRequest", "ReadFifoQueueResponse", ] diff --git a/pymodbus/interfaces.py b/pymodbus/interfaces.py index 70b3ac288..aa65f41de 100644 --- a/pymodbus/interfaces.py +++ b/pymodbus/interfaces.py @@ -22,6 +22,7 @@ def __new__(cls, *args, **kwargs): cls._inst = object.__new__(cls, *args, **kwargs) return cls._inst + #---------------------------------------------------------------------------# # Project Specific #---------------------------------------------------------------------------# @@ -40,7 +41,8 @@ def decode(self, message): :param message: The raw modbus request packet :return: The decoded modbus message or None if error ''' - raise NotImplementedException("Method not implemented by derived class") + raise NotImplementedException( + "Method not implemented by derived class") def lookupPduClass(self, function_code): ''' Use `function_code` to determine the class of the PDU. @@ -48,7 +50,9 @@ def lookupPduClass(self, function_code): :param function_code: The function code specified in a frame. :returns: The class of the PDU that has a matching `function_code`. ''' - raise NotImplementedException("Method not implemented by derived class") + raise NotImplementedException( + "Method not implemented by derived class") + class IModbusFramer(object): ''' @@ -62,7 +66,8 @@ def checkFrame(self): :returns: True if we successful, False otherwise ''' - raise NotImplementedException("Method not implemented by derived class") + raise NotImplementedException( + "Method not implemented by derived class") def advanceFrame(self): ''' Skip over the current framed message @@ -70,7 +75,8 @@ def advanceFrame(self): it or determined that it contains an error. It also has to reset the current frame header handle ''' - raise NotImplementedException("Method not implemented by derived class") + raise NotImplementedException( + "Method not implemented by derived class") def addToFrame(self, message): ''' Add the next message to the frame buffer @@ -80,7 +86,8 @@ def addToFrame(self, message): :param message: The most recent packet ''' - raise NotImplementedException("Method not implemented by derived class") + raise NotImplementedException( + "Method not implemented by derived class") def isFrameReady(self): ''' Check if we should continue decode logic @@ -90,14 +97,16 @@ def isFrameReady(self): :returns: True if ready, False otherwise ''' - raise NotImplementedException("Method not implemented by derived class") + raise NotImplementedException( + "Method not implemented by derived class") def getFrame(self): ''' Get the next frame from the buffer :returns: The frame data or '' ''' - raise NotImplementedException("Method not implemented by derived class") + raise NotImplementedException( + "Method not implemented by derived class") def populateResult(self, result): ''' Populates the modbus result with current frame header @@ -107,7 +116,8 @@ def populateResult(self, result): :param result: The response packet ''' - raise NotImplementedException("Method not implemented by derived class") + raise NotImplementedException( + "Method not implemented by derived class") def processIncomingPacket(self, data, callback): ''' The new packet processing pattern @@ -124,7 +134,8 @@ def processIncomingPacket(self, data, callback): :param data: The new packet data :param callback: The function to send results to ''' - raise NotImplementedException("Method not implemented by derived class") + raise NotImplementedException( + "Method not implemented by derived class") def buildPacket(self, message): ''' Creates a ready to send modbus packet @@ -135,7 +146,9 @@ def buildPacket(self, message): :param message: The request/response to send :returns: The built packet ''' - raise NotImplementedException("Method not implemented by derived class") + raise NotImplementedException( + "Method not implemented by derived class") + class IModbusSlaveContext(object): ''' @@ -152,15 +165,16 @@ class IModbusSlaveContext(object): __fx_mapper.update([(i, 'c') for i in [1, 5, 15]]) def decode(self, fx): - ''' Converts the function code to the datastore to + ''' Converts the function code to the datastore to :param fx: The function we are working with - :returns: one of [d(iscretes),i(inputs),h(oliding),c(oils)] + :returns: one of [d(iscretes),i(inputs),h(oliding),c(oils) ''' - return self.__fx_mapper[fx] + return self.__fx_mapper[fx] def reset(self): - ''' Resets all the datastores to their default values ''' + ''' Resets all the datastores to their default values + ''' raise NotImplementedException("Context Reset") def validate(self, fx, address, count=1): @@ -192,9 +206,9 @@ def setValues(self, fx, address, values): ''' raise NotImplementedException("set context values") -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ 'Singleton', 'IModbusDecoder', 'IModbusFramer', 'IModbusSlaveContext', diff --git a/pymodbus/internal/ptwisted.py b/pymodbus/internal/ptwisted.py index b44a8c5ca..493a3c2b9 100644 --- a/pymodbus/internal/ptwisted.py +++ b/pymodbus/internal/ptwisted.py @@ -11,7 +11,8 @@ import logging _logger = logging.getLogger(__name__) -def InstallManagementConsole(namespace, users={'admin':'admin'}, port=503): + +def InstallManagementConsole(namespace, users={'admin': 'admin'}, port=503): ''' Helper method to start an ssh management console for the modbus server. @@ -32,6 +33,7 @@ def build_protocol(): factory = manhole_ssh.ConchFactory(p) reactor.listenTCP(port, factory) + def InstallSpecializedReactor(): ''' This attempts to install a reactor specialized for the given @@ -48,4 +50,3 @@ def InstallSpecializedReactor(): except: pass _logger.debug("No specialized reactor was installed") return False - diff --git a/pymodbus/pdu.py b/pymodbus/pdu.py index cb967e2be..67d01cea0 100644 --- a/pymodbus/pdu.py +++ b/pymodbus/pdu.py @@ -12,6 +12,7 @@ import logging _logger = logging.getLogger(__name__) + #---------------------------------------------------------------------------# # Base PDU's #---------------------------------------------------------------------------# @@ -77,6 +78,7 @@ def calculateRtuFrameSize(cls, buffer): else: raise NotImplementedException( "Cannot determine RTU frame size for %s" % cls.__name__) + class ModbusRequest(ModbusPDU): ''' Base class for a modbus request PDU ''' @@ -94,6 +96,7 @@ def doException(self, exception): (self.function_code, exception)) return ExceptionResponse(self.function_code, exception) + class ModbusResponse(ModbusPDU): ''' Base class for a modbus response PDU @@ -114,6 +117,7 @@ def __init__(self, **kwargs): ''' Proxy to the lower level initializer ''' ModbusPDU.__init__(self, **kwargs) + #---------------------------------------------------------------------------# # Exception PDU's #---------------------------------------------------------------------------# @@ -131,6 +135,7 @@ class ModbusExceptions(Singleton): GatewayPathUnavailable = 0x0A GatewayNoResponse = 0x0B + class ExceptionResponse(ModbusResponse): ''' Base class for a modbus exception PDU ''' ExceptionOffset = 0x80 @@ -165,8 +170,9 @@ def __str__(self): :returns: The string representation of an exception response ''' - return "Exception Response (%d, %d)" % (self.function_code, - self.exception_code) + parameters = (self.function_code, self.exception_code) + return "Exception Response (%d, %d)" % parameters + class IllegalFunctionRequest(ModbusRequest): ''' @@ -201,9 +207,9 @@ def execute(self, context): ''' return ExceptionResponse(self.function_code, self.ErrorCode) -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ 'ModbusRequest', 'ModbusResponse', 'ModbusExceptions', 'ExceptionResponse', 'IllegalFunctionRequest', diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index 541328197..18a054de1 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -23,6 +23,7 @@ import logging _logger = logging.getLogger(__name__) + #---------------------------------------------------------------------------# # Modbus TCP Server #---------------------------------------------------------------------------# @@ -31,10 +32,10 @@ class ModbusTcpProtocol(protocol.Protocol): def connectionMade(self): ''' Callback for when a client connects - + Note, since the protocol factory cannot be accessed from the protocol __init__, the client connection made is essentially our - __init__ method. + __init__ method. ''' _logger.debug("Client Connected [%s]" % self.transport.getHost()) self.framer = self.factory.framer(decoder=self.factory.decoder) @@ -113,6 +114,7 @@ def __init__(self, store, framer=None, identity=None): if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) + #---------------------------------------------------------------------------# # Modbus UDP Server #---------------------------------------------------------------------------# @@ -177,9 +179,10 @@ def _send(self, message, addr): _logger.debug('send: %s' % b2a_hex(pdu)) return self.transport.write(pdu, addr) -#---------------------------------------------------------------------------# + +#---------------------------------------------------------------------------# # Starting Factories -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# def StartTcpServer(context, identity=None): ''' Helper method to start the Modbus Async TCP server @@ -191,10 +194,11 @@ def StartTcpServer(context, identity=None): _logger.info("Starting Modbus TCP Server on %s" % Defaults.Port) framer = ModbusSocketFramer factory = ModbusServerFactory(context, framer, identity) - InstallManagementConsole({ 'factory' : factory }) + InstallManagementConsole({'factory': factory}) reactor.listenTCP(Defaults.Port, factory) reactor.run() + def StartUdpServer(context, identity=None): ''' Helper method to start the Modbus Async Udp server @@ -209,7 +213,9 @@ def StartUdpServer(context, identity=None): reactor.listenUDP(Defaults.Port, server) reactor.run() -def StartSerialServer(context, identity=None, framer=ModbusAsciiFramer, **kwargs): + +def StartSerialServer(context, identity=None, + framer=ModbusAsciiFramer, **kwargs): ''' Helper method to start the Modbus Async Serial server :param context: The server data context :param identify: The server identity to use (default empty) @@ -224,9 +230,9 @@ def StartSerialServer(context, identity=None, framer=ModbusAsciiFramer, **kwargs handle = SerialPort(protocol, kwargs['device'], reactor, Defaults.Baudrate) reactor.run() -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ "StartTcpServer", "StartUdpServer", "StartSerialServer", ] diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index f4c99f283..bc8e42379 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -5,7 +5,8 @@ ''' from binascii import b2a_hex import SocketServer -import serial, socket +import serial +import socket from pymodbus.constants import Defaults from pymodbus.factory import ServerDecoder @@ -22,6 +23,7 @@ import logging _logger = logging.getLogger(__name__) + #---------------------------------------------------------------------------# # Server #---------------------------------------------------------------------------# @@ -102,6 +104,7 @@ def decode(self, message): _logger.warn("Unable to decode request %s" % er) return None + class ModbusTcpServer(SocketServer.ThreadingTCPServer): ''' A modbus threaded tcp socket server @@ -150,6 +153,7 @@ def server_close(self): self.socket.close() for thread in self.threads: thread.running = False + class ModbusUdpServer(SocketServer.ThreadingUDPServer): ''' A modbus threaded udp socket server @@ -198,6 +202,7 @@ def server_close(self): self.socket.close() for thread in self.threads: thread.running = False + class ModbusSerialServer(object): ''' A modbus threaded udp socket server @@ -243,7 +248,7 @@ def _connect(self): ''' if self.socket: return True try: - self.socket = serial.Serial(port=self.device, timeout=self.timeout, + self.socket = serial.Serial(port=self.device, timeout=self.timeout, bytesize=self.bytesize, stopbits=self.stopbits, baudrate=self.baudrate, parity=self.parity) except serial.SerialException, msg: @@ -260,7 +265,8 @@ def _build_handler(self): request = self.socket request.send = request.write request.recv = request.read - handler = ModbusRequestHandler(request, ('127.0.0.1', self.device), self) + handler = ModbusRequestHandler(request, + ('127.0.0.1', self.device), self) return handler def serve_forever(self): @@ -279,9 +285,9 @@ def server_close(self): _logger.debug("Modbus server stopped") self.socket.close() -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Creation Factories -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# def StartTcpServer(context=None, identity=None): ''' A factory to start and run a tcp modbus server From a2e716fd65bbe2b02bb0af53c06267c1c0384129 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Thu, 3 Mar 2011 21:26:56 +0000 Subject: [PATCH 010/243] more cleanup --- doc/quality/current.lint | 10 ++-------- pymodbus/__init__.py | 17 ++++++++--------- pymodbus/client/async.py | 4 +--- pymodbus/client/sync.py | 3 ++- pymodbus/datastore/modredis.py | 2 +- pymodbus/datastore/remote.py | 2 +- pymodbus/internal/ptwisted.py | 3 +++ pymodbus/other_message.py | 3 +-- pymodbus/transaction.py | 1 + pymodbus/version.py | 6 +++--- setup.cfg | 2 +- test/test_other_messages.py | 2 +- 12 files changed, 25 insertions(+), 30 deletions(-) diff --git a/doc/quality/current.lint b/doc/quality/current.lint index 5440d269b..848d63e86 100644 --- a/doc/quality/current.lint +++ b/doc/quality/current.lint @@ -7,13 +7,8 @@ pymodbus/factory.py:15: 'from pymodbus.file_message import *' used; unable to de pymodbus/factory.py:16: 'from pymodbus.other_message import *' used; unable to detect undefined names pymodbus/factory.py:17: 'from pymodbus.register_read_message import *' used; unable to detect undefined names pymodbus/factory.py:18: 'from pymodbus.register_write_message import *' used; unable to detect undefined names -pymodbus/transaction.py:58: undefined name 'socket' -pymodbus/other_message.py:11: 'from pymodbus.exceptions import *' used; unable to detect undefined names -pymodbus/other_message.py:389: local variable 'status' is assigned to but never used -pymodbus/server/async.py:229: local variable 'handle' is assigned to but never used +pymodbus/server/async.py:230: local variable 'handle' is assigned to but never used pymodbus/server/sync.py:16: 'from pymodbus.transaction import *' used; unable to detect undefined names -pymodbus/datastore/remote.py:66: local variable 'result' is assigned to but never used -pymodbus/client/async.py:20: 'reactor' imported but unused pymodbus/client/common.py:3: 'from pymodbus.bit_read_message import *' used; unable to detect undefined names pymodbus/client/common.py:4: 'from pymodbus.bit_write_message import *' used; unable to detect undefined names pymodbus/client/common.py:5: 'from pymodbus.register_read_message import *' used; unable to detect undefined names @@ -21,5 +16,4 @@ pymodbus/client/common.py:6: 'from pymodbus.register_write_message import *' use pymodbus/client/common.py:7: 'from pymodbus.diag_message import *' used; unable to detect undefined names pymodbus/client/common.py:8: 'from pymodbus.file_message import *' used; unable to detect undefined names pymodbus/client/common.py:9: 'from pymodbus.other_message import *' used; unable to detect undefined names -pymodbus/client/sync.py:6: 'from pymodbus.exceptions import *' used; unable to detect undefined names -pymodbus/client/sync.py:7: 'from pymodbus.transaction import *' used; unable to detect undefined names +pymodbus/client/sync.py:8: 'from pymodbus.transaction import *' used; unable to detect undefined names diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index 3f0fe959d..2b89b4d22 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -1,4 +1,4 @@ -""" +''' Pymodbus: Modbus Protocol Implementation ----------------------------------------- @@ -9,22 +9,21 @@ Hynek Petrak Released under the the BSD license -""" +''' -from pymodbus.version import _version -__version__ = _version.short() +import pymodbus.version as __version +__version__ = __version.version.short() __author__ = 'Galen Collins' #---------------------------------------------------------------------------# # Block unhandled logging #---------------------------------------------------------------------------# -import logging +import logging as __logging try: - from logging import NullHandler + from logging import NullHandler as __null except ImportError: - class NullHandler(logging.Handler): + class __null(__logging.Handler): def emit(self, record): pass -h = NullHandler() -logging.getLogger(__name__).addHandler(h) +__logging.getLogger(__name__).addHandler(__null()) diff --git a/pymodbus/client/async.py b/pymodbus/client/async.py index b7bf3828b..c9183aeec 100644 --- a/pymodbus/client/async.py +++ b/pymodbus/client/async.py @@ -16,9 +16,7 @@ def clientTest(): reactor.run() """ from collections import deque - -from twisted.internet import reactor, defer, protocol - +from twisted.internet import defer, protocol from pymodbus.factory import ClientDecoder from pymodbus.exceptions import ConnectionException from pymodbus.transaction import ModbusSocketFramer diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 2cc89b2a0..abc593ac8 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -3,7 +3,8 @@ from pymodbus.constants import Defaults from pymodbus.factory import ClientDecoder -from pymodbus.exceptions import * +from pymodbus.exceptions import NotImplementedException, ParameterException +from pymodbus.exceptions import ConnectionException from pymodbus.transaction import * from pymodbus.client.common import ModbusClientMixin diff --git a/pymodbus/datastore/modredis.py b/pymodbus/datastore/modredis.py index 70ffd8be5..67010db4f 100644 --- a/pymodbus/datastore/modredis.py +++ b/pymodbus/datastore/modredis.py @@ -27,7 +27,7 @@ def __init__(self, **kwargs): host = kwargs.get('host', 'localhost') port = kwargs.get('port', 6379) self.prefix = kwargs.get('prefix', 'pymodbus') - self.client = redis.Redis(host=host, port=port) + self.client = kwargs.get('client', redis.Redis(host=host, port=port)) self.__build_mapping() def __str__(self): diff --git a/pymodbus/datastore/remote.py b/pymodbus/datastore/remote.py index 693e8eaae..32d4ecf56 100644 --- a/pymodbus/datastore/remote.py +++ b/pymodbus/datastore/remote.py @@ -63,7 +63,7 @@ def setValues(self, fx, address, values): ''' # TODO deal with deferreds _logger.debug("set values[%d] %d:%d" % (fx, address, len(values))) - result = self.__set_callbacks[self.decode(fx)](address, values) + self.__set_callbacks[self.decode(fx)](address, values) def __str__(self): ''' Returns a string representation of the context diff --git a/pymodbus/internal/ptwisted.py b/pymodbus/internal/ptwisted.py index 493a3c2b9..ff6f22821 100644 --- a/pymodbus/internal/ptwisted.py +++ b/pymodbus/internal/ptwisted.py @@ -12,6 +12,9 @@ _logger = logging.getLogger(__name__) +#---------------------------------------------------------------------------# +# Twisted Helper Methods +#---------------------------------------------------------------------------# def InstallManagementConsole(namespace, users={'admin': 'admin'}, port=503): ''' Helper method to start an ssh management console for the modbus server. diff --git a/pymodbus/other_message.py b/pymodbus/other_message.py index 2ac49a238..9a7036953 100644 --- a/pymodbus/other_message.py +++ b/pymodbus/other_message.py @@ -8,7 +8,6 @@ from pymodbus.pdu import ModbusRequest from pymodbus.pdu import ModbusResponse from pymodbus.device import ModbusControlBlock -from pymodbus.exceptions import * _MCB = ModbusControlBlock() @@ -390,7 +389,7 @@ def encode(self): length = len(self.identifier) + 2 packet = struct.pack('>B', length) packet += self.identifier # we assume it is already encoded - packet += struct.pack('>B', self.status) + packet += struct.pack('>B', status) return packet def decode(self, data): diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 155140369..5c750f2ac 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -46,6 +46,7 @@ def execute(self, request): ''' Starts the producer to send the next request to consumer.write(Frame(request)) ''' + import socket retries = Defaults.Retries request.transaction_id = self.__getNextTID() _logger.debug("Running transaction %d" % request.transaction_id) diff --git a/pymodbus/version.py b/pymodbus/version.py index 87fd9e6cb..265a2c730 100644 --- a/pymodbus/version.py +++ b/pymodbus/version.py @@ -33,10 +33,10 @@ def __str__(self): ''' return '[%s, version %s]' % (self.package, self.short()) -_version = Version('pymodbus', 0, 9, 0) -_version.__name__ = 'pymodbus' # fix epydoc error +version = Version('pymodbus', 0, 9, 0) +version.__name__ = 'pymodbus' # fix epydoc error #---------------------------------------------------------------------------# # Exported symbols #---------------------------------------------------------------------------# -__all__ = ["_version"] +__all__ = ["version"] diff --git a/setup.cfg b/setup.cfg index 608cda3fb..e80c71af3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,7 @@ verbosity=0 detailed-errors=1 with-coverage=1 cover-html=1 -cover-html-dir=doc/coverage/ +cover-html-dir=build/coverage/ cover-package=pymodbus #debug=nose.loader #pdb=1 diff --git a/test/test_other_messages.py b/test/test_other_messages.py index 80e6bfc97..0ced6e7db 100644 --- a/test/test_other_messages.py +++ b/test/test_other_messages.py @@ -86,7 +86,7 @@ def testReportSlaveId(self): self.assertEqual(request.execute().function_code, 0x11) response = ReportSlaveIdResponse(request.execute().identifier, True) - self.assertEqual(response.encode(), '\x0apymodbus\x01') + self.assertEqual(response.encode(), '\x0apymodbus\xff') response.decode('\x03\x12\x00') self.assertEqual(response.status, False) self.assertEqual(response.identifier, '\x12') From 01510ca0aae6829f2b84b250b8d78d8cf64c0402 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Tue, 22 Mar 2011 13:35:10 +0000 Subject: [PATCH 011/243] Fixes issue 47 --- pymodbus/bit_write_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymodbus/bit_write_message.py b/pymodbus/bit_write_message.py index de951f5b5..74c87148d 100644 --- a/pymodbus/bit_write_message.py +++ b/pymodbus/bit_write_message.py @@ -78,7 +78,7 @@ def execute(self, context): if not context.validate(self.function_code, self.address, 1): return self.doException(merror.IllegalAddress) - context.setValues(self.function_code, self.address, self.value) + context.setValues(self.function_code, self.address, [self.value]) values = context.getValues(self.function_code, self.address, 1) return WriteSingleCoilResponse(self.address, values[0]) From b09de19c8c243958e015244845fa6059d6cc404b Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 11 Apr 2011 18:59:41 +0000 Subject: [PATCH 012/243] adding another quick example --- doc/sphinx/examples/index.rst | 1 + doc/sphinx/examples/performance.rst | 11 ++++++++ examples/common/asynchronous-client.py | 2 +- examples/common/asynchronous-server.py | 2 +- examples/common/performance.py | 35 ++++++++++++++++++++++++++ examples/common/synchronous-client.py | 2 +- examples/common/synchronous-server.py | 3 +-- 7 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 doc/sphinx/examples/performance.rst create mode 100755 examples/common/performance.py diff --git a/doc/sphinx/examples/index.rst b/doc/sphinx/examples/index.rst index 2c4222ba3..514284e5c 100644 --- a/doc/sphinx/examples/index.rst +++ b/doc/sphinx/examples/index.rst @@ -20,6 +20,7 @@ Example Library Code modbus-simulator synchronous-client synchronous-server + performance Example Frontend Code -------------------------------------------------- diff --git a/doc/sphinx/examples/performance.rst b/doc/sphinx/examples/performance.rst new file mode 100644 index 000000000..93185bdea --- /dev/null +++ b/doc/sphinx/examples/performance.rst @@ -0,0 +1,11 @@ +================================================== +Synchronous Client Performance Check +================================================== + +Below is a quick example of how to test the performance of a tcp modbus +device using the synchronous tcp client. If you do not have a device +to test with, feel free to run a pymodbus server instance or start +the reference tester in the tools directory. + +.. literalinclude:: ../../../examples/common/performance.py + diff --git a/examples/common/asynchronous-client.py b/examples/common/asynchronous-client.py index 06a201ec2..c7dfb22ce 100755 --- a/examples/common/asynchronous-client.py +++ b/examples/common/asynchronous-client.py @@ -1,6 +1,6 @@ #!/usr/bin/env python ''' -Pymodbus Asynchrnonous Client Examples +Pymodbus Asynchronous Client Examples -------------------------------------------------------------------------- The following is an example of how to use the asynchronous modbus diff --git a/examples/common/asynchronous-server.py b/examples/common/asynchronous-server.py index 96cff450c..7112f8fb3 100755 --- a/examples/common/asynchronous-server.py +++ b/examples/common/asynchronous-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python #---------------------------------------------------------------------------# -# the various server implementations +# import the various server implementations #---------------------------------------------------------------------------# from pymodbus.server.async import StartTcpServer from pymodbus.server.async import StartUdpServer diff --git a/examples/common/performance.py b/examples/common/performance.py new file mode 100755 index 000000000..cbe04c5f3 --- /dev/null +++ b/examples/common/performance.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +''' +Pymodbus Performance Example +-------------------------------------------------------------------------- + +The following is an quick performance check of the synchronous +modbus client. +''' +#---------------------------------------------------------------------------# +# import the necessary modules +#---------------------------------------------------------------------------# +from pymodbus.client.sync import ModbusTcpClient +from time import time + +#---------------------------------------------------------------------------# +# initialize the test +#---------------------------------------------------------------------------# +client = ModbusTcpClient('127.0.0.1') +count = 0 +start = time() +iterations = 10000 + +#---------------------------------------------------------------------------# +# perform the test +#---------------------------------------------------------------------------# +while count < iterations: + result = client.read_holding_registers(10, 1, 0).getRegister(0) + count += 1 + +#---------------------------------------------------------------------------# +# check our results +#---------------------------------------------------------------------------# +stop = time() +print "%d requests/second" % ((1.0 * count) / (stop - start)) + diff --git a/examples/common/synchronous-client.py b/examples/common/synchronous-client.py index 815904f2a..746b2094f 100755 --- a/examples/common/synchronous-client.py +++ b/examples/common/synchronous-client.py @@ -1,6 +1,6 @@ #!/usr/bin/env python ''' -Pymodbus Synchrnonous Client Examples +Pymodbus Synchronous Client Examples -------------------------------------------------------------------------- The following is an example of how to use the synchronous modbus client diff --git a/examples/common/synchronous-server.py b/examples/common/synchronous-server.py index 8b4c67e68..d9dea0d6c 100755 --- a/examples/common/synchronous-server.py +++ b/examples/common/synchronous-server.py @@ -1,7 +1,6 @@ #!/usr/bin/env python - #---------------------------------------------------------------------------# -# the various server implementations +# import the various server implementations #---------------------------------------------------------------------------# from pymodbus.server.sync import StartTcpServer from pymodbus.server.sync import StartUdpServer From 56b981c84c9fc0552f24d82ed88717bd1374ce06 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Tue, 12 Apr 2011 14:50:29 +0000 Subject: [PATCH 013/243] cutting 200 pep8 errors --- doc/quality/current.pep8 | 410 +++++++---------------------- pymodbus/client/async.py | 10 +- pymodbus/client/common.py | 3 +- pymodbus/client/sync.py | 35 +-- pymodbus/datastore/__init__.py | 4 +- pymodbus/datastore/context.py | 15 +- pymodbus/datastore/database.py | 27 +- pymodbus/datastore/modredis.py | 56 ++-- pymodbus/diag_message.py | 4 +- pymodbus/events.py | 2 +- pymodbus/file_message.py | 10 +- pymodbus/interfaces.py | 6 +- pymodbus/other_message.py | 86 +++--- pymodbus/pdu.py | 27 +- pymodbus/register_read_message.py | 21 +- pymodbus/register_write_message.py | 16 +- pymodbus/server/sync.py | 7 +- pymodbus/transaction.py | 73 ++--- pymodbus/utilities.py | 23 +- pymodbus/version.py | 8 +- 20 files changed, 342 insertions(+), 501 deletions(-) diff --git a/doc/quality/current.pep8 b/doc/quality/current.pep8 index 8f6748467..de4467c0b 100644 --- a/doc/quality/current.pep8 +++ b/doc/quality/current.pep8 @@ -57,8 +57,6 @@ pymodbus/diag_message.py:594:80: E501 line too long (80 characters) pymodbus/diag_message.py:597:80: E501 line too long (80 characters) pymodbus/diag_message.py:613:80: E501 line too long (82 characters) pymodbus/diag_message.py:616:80: E501 line too long (80 characters) -pymodbus/diag_message.py:651:78: W291 trailing whitespace -pymodbus/diag_message.py:653:78: W291 trailing whitespace pymodbus/diag_message.py:656:80: E501 line too long (80 characters) pymodbus/diag_message.py:662:80: E501 line too long (90 characters) pymodbus/diag_message.py:663:80: E501 line too long (82 characters) @@ -79,53 +77,10 @@ pymodbus/events.py:131:26: E221 multiple spaces before operator pymodbus/events.py:132:26: E221 multiple spaces before operator pymodbus/events.py:133:26: E221 multiple spaces before operator pymodbus/events.py:135:26: E221 multiple spaces before operator -pymodbus/events.py:165:54: W291 trailing whitespace -pymodbus/file_message.py:24:80: E501 line too long (83 characters) -pymodbus/file_message.py:24:84: W291 trailing whitespace -pymodbus/file_message.py:26:80: E501 line too long (86 characters) -pymodbus/file_message.py:26:87: W291 trailing whitespace -pymodbus/file_message.py:71:80: W291 trailing whitespace -pymodbus/interfaces.py:13:1: E302 expected 2 blank lines, found 1 -pymodbus/interfaces.py:61:80: E501 line too long (80 characters) -pymodbus/other_message.py:18:1: E302 expected 2 blank lines, found 1 -pymodbus/other_message.py:60:1: E302 expected 2 blank lines, found 1 -pymodbus/other_message.py:107:1: E302 expected 2 blank lines, found 1 -pymodbus/other_message.py:110:49: W291 trailing whitespace -pymodbus/other_message.py:112:80: E501 line too long (81 characters) -pymodbus/other_message.py:113:80: E501 line too long (82 characters) -pymodbus/other_message.py:113:83: W291 trailing whitespace -pymodbus/other_message.py:117:37: W291 trailing whitespace -pymodbus/other_message.py:119:80: E501 line too long (82 characters) -pymodbus/other_message.py:158:1: E302 expected 2 blank lines, found 1 -pymodbus/other_message.py:163:73: W291 trailing whitespace -pymodbus/other_message.py:176:27: E261 at least two spaces before inline comment -pymodbus/other_message.py:205:1: E302 expected 2 blank lines, found 1 -pymodbus/other_message.py:207:80: E501 line too long (80 characters) -pymodbus/other_message.py:208:55: W291 trailing whitespace -pymodbus/other_message.py:211:60: W291 trailing whitespace -pymodbus/other_message.py:214:80: E501 line too long (80 characters) -pymodbus/other_message.py:216:72: W291 trailing whitespace -pymodbus/other_message.py:250:28: E203 whitespace before ':' -pymodbus/other_message.py:264:1: E302 expected 2 blank lines, found 1 -pymodbus/other_message.py:268:68: W291 trailing whitespace -pymodbus/other_message.py:294:15: E221 multiple spaces before operator -pymodbus/other_message.py:312:34: E225 missing whitespace around operator -pymodbus/other_message.py:320:80: E501 line too long (91 characters) -pymodbus/other_message.py:326:1: E302 expected 2 blank lines, found 1 -pymodbus/other_message.py:329:63: W291 trailing whitespace -pymodbus/other_message.py:366:1: E302 expected 2 blank lines, found 1 -pymodbus/other_message.py:392:34: E261 at least two spaces before inline comment -pymodbus/other_message.py:405:40: E225 missing whitespace around operator -pymodbus/other_message.py:422:78: W291 trailing whitespace -pymodbus/other_message.py:424:78: W291 trailing whitespace -pymodbus/pdu.py:34:1: W293 blank line contains whitespace -pymodbus/pdu.py:36:80: E501 line too long (81 characters) -pymodbus/pdu.py:38:80: E501 line too long (86 characters) -pymodbus/pdu.py:41:1: W293 blank line contains whitespace -pymodbus/pdu.py:78:13: E701 multiple statements on one line (colon) -pymodbus/pdu.py:104:1: W293 blank line contains whitespace -pymodbus/pdu.py:109:1: W293 blank line contains whitespace -pymodbus/pdu.py:128:27: E221 multiple spaces before operator +pymodbus/other_message.py:256:28: E203 whitespace before ':' +pymodbus/other_message.py:301:15: E221 multiple spaces before operator +pymodbus/other_message.py:327:80: E501 line too long (91 characters) +pymodbus/pdu.py:79:13: E701 multiple statements on one line (colon) pymodbus/pdu.py:129:27: E221 multiple spaces before operator pymodbus/pdu.py:130:27: E221 multiple spaces before operator pymodbus/pdu.py:131:27: E221 multiple spaces before operator @@ -134,257 +89,106 @@ pymodbus/pdu.py:133:27: E221 multiple spaces before operator pymodbus/pdu.py:134:27: E221 multiple spaces before operator pymodbus/pdu.py:135:27: E221 multiple spaces before operator pymodbus/pdu.py:136:27: E221 multiple spaces before operator -pymodbus/register_read_message.py:10:1: E302 expected 2 blank lines, found 1 -pymodbus/register_read_message.py:47:1: E302 expected 2 blank lines, found 1 -pymodbus/register_read_message.py:80:63: E225 missing whitespace around operator -pymodbus/register_read_message.py:126:80: E501 line too long (80 characters) -pymodbus/register_read_message.py:129:1: E302 expected 2 blank lines, found 1 -pymodbus/register_read_message.py:146:1: E302 expected 2 blank lines, found 1 -pymodbus/register_read_message.py:174:80: E501 line too long (80 characters) -pymodbus/register_read_message.py:177:1: E302 expected 2 blank lines, found 1 -pymodbus/register_read_message.py:194:1: E302 expected 2 blank lines, found 1 -pymodbus/register_read_message.py:222:26: E221 multiple spaces before operator -pymodbus/register_read_message.py:223:26: E221 multiple spaces before operator -pymodbus/register_read_message.py:253:52: E225 missing whitespace around operator -pymodbus/register_read_message.py:289:1: E302 expected 2 blank lines, found 1 -pymodbus/register_read_message.py:297:1: W293 blank line contains whitespace -pymodbus/register_read_message.py:311:41: E225 missing whitespace around operator -pymodbus/register_read_message.py:323:63: E225 missing whitespace around operator -pymodbus/register_read_message.py:332:78: W291 trailing whitespace -pymodbus/register_read_message.py:334:78: W291 trailing whitespace -pymodbus/register_write_message.py:10:1: E302 expected 2 blank lines, found 1 -pymodbus/register_write_message.py:68:1: E302 expected 2 blank lines, found 1 -pymodbus/register_write_message.py:112:1: E302 expected 2 blank lines, found 1 -pymodbus/register_write_message.py:131:22: E701 multiple statements on one line (colon) -pymodbus/register_write_message.py:132:45: E701 multiple statements on one line (colon) -pymodbus/register_write_message.py:154:25: E261 at least two spaces before inline comment -pymodbus/register_write_message.py:156:64: E225 missing whitespace around operator -pymodbus/register_write_message.py:182:1: E302 expected 2 blank lines, found 1 -pymodbus/register_write_message.py:222:78: W291 trailing whitespace -pymodbus/register_write_message.py:224:78: W291 trailing whitespace -pymodbus/transaction.py:22:1: E302 expected 2 blank lines, found 1 -pymodbus/transaction.py:34:1: W293 blank line contains whitespace -pymodbus/transaction.py:65:1: W293 blank line contains whitespace -pymodbus/transaction.py:80:14: E231 missing whitespace after ',' -pymodbus/transaction.py:90:14: E231 missing whitespace after ',' -pymodbus/transaction.py:96:1: W293 blank line contains whitespace -pymodbus/transaction.py:113:1: E302 expected 2 blank lines, found 1 -pymodbus/transaction.py:118:1: W293 blank line contains whitespace -pymodbus/transaction.py:122:1: W293 blank line contains whitespace -pymodbus/transaction.py:127:1: W293 blank line contains whitespace -pymodbus/transaction.py:138:31: E231 missing whitespace after ':' -pymodbus/transaction.py:139:21: E221 multiple spaces before operator -pymodbus/transaction.py:140:21: E221 multiple spaces before operator -pymodbus/transaction.py:171:31: E231 missing whitespace after ':' -pymodbus/transaction.py:235:33: E261 at least two spaces before inline comment -pymodbus/transaction.py:236:17: E701 multiple statements on one line (colon) -pymodbus/transaction.py:255:1: E302 expected 2 blank lines, found 1 -pymodbus/transaction.py:261:1: W293 blank line contains whitespace -pymodbus/transaction.py:267:1: W293 blank line contains whitespace -pymodbus/transaction.py:275:1: W293 blank line contains whitespace -pymodbus/transaction.py:296:21: E221 multiple spaces before operator -pymodbus/transaction.py:297:21: E221 multiple spaces before operator -pymodbus/transaction.py:299:21: E221 multiple spaces before operator -pymodbus/transaction.py:339:1: W293 blank line contains whitespace -pymodbus/transaction.py:355:54: E225 missing whitespace around operator -pymodbus/transaction.py:371:14: E221 multiple spaces before operator -pymodbus/transaction.py:372:14: E221 multiple spaces before operator -pymodbus/transaction.py:378:1: W293 blank line contains whitespace -pymodbus/transaction.py:412:33: E261 at least two spaces before inline comment -pymodbus/transaction.py:413:17: E701 multiple statements on one line (colon) -pymodbus/transaction.py:430:1: E302 expected 2 blank lines, found 1 -pymodbus/transaction.py:433:1: W293 blank line contains whitespace -pymodbus/transaction.py:436:1: W293 blank line contains whitespace -pymodbus/transaction.py:441:1: W293 blank line contains whitespace -pymodbus/transaction.py:452:31: E231 missing whitespace after ':' -pymodbus/transaction.py:453:21: E221 multiple spaces before operator -pymodbus/transaction.py:454:21: E221 multiple spaces before operator -pymodbus/transaction.py:455:21: E221 multiple spaces before operator -pymodbus/transaction.py:456:21: E221 multiple spaces before operator -pymodbus/transaction.py:467:23: E701 multiple statements on one line (colon) -pymodbus/transaction.py:468:21: E203 whitespace before ':' -pymodbus/transaction.py:468:23: E261 at least two spaces before inline comment -pymodbus/transaction.py:475:57: E225 missing whitespace around operator -pymodbus/transaction.py:476:43: E225 missing whitespace around operator -pymodbus/transaction.py:487:31: E231 missing whitespace after ':' -pymodbus/transaction.py:512:14: E221 multiple spaces before operator -pymodbus/transaction.py:513:14: E221 multiple spaces before operator -pymodbus/transaction.py:519:1: W293 blank line contains whitespace -pymodbus/transaction.py:553:33: E261 at least two spaces before inline comment -pymodbus/transaction.py:554:17: E701 multiple statements on one line (colon) -pymodbus/transaction.py:563:16: E221 multiple spaces before operator -pymodbus/transaction.py:564:16: E221 multiple spaces before operator -pymodbus/transaction.py:575:1: E302 expected 2 blank lines, found 1 -pymodbus/transaction.py:606:31: E231 missing whitespace after ':' -pymodbus/transaction.py:607:21: E221 multiple spaces before operator -pymodbus/transaction.py:608:21: E221 multiple spaces before operator -pymodbus/transaction.py:608:31: E261 at least two spaces before inline comment -pymodbus/transaction.py:609:21: E221 multiple spaces before operator -pymodbus/transaction.py:609:31: E261 at least two spaces before inline comment -pymodbus/transaction.py:610:21: E221 multiple spaces before operator -pymodbus/transaction.py:621:23: E701 multiple statements on one line (colon) -pymodbus/transaction.py:622:21: E203 whitespace before ':' -pymodbus/transaction.py:622:23: E261 at least two spaces before inline comment -pymodbus/transaction.py:629:80: E501 line too long (83 characters) -pymodbus/transaction.py:629:73: E225 missing whitespace around operator -pymodbus/transaction.py:630:43: E225 missing whitespace around operator -pymodbus/transaction.py:641:31: E231 missing whitespace after ':' -pymodbus/transaction.py:666:14: E221 multiple spaces before operator -pymodbus/transaction.py:667:14: E221 multiple spaces before operator -pymodbus/transaction.py:673:1: W293 blank line contains whitespace -pymodbus/transaction.py:707:33: E261 at least two spaces before inline comment -pymodbus/transaction.py:708:17: E701 multiple statements on one line (colon) -pymodbus/transaction.py:733:21: E225 missing whitespace around operator -pymodbus/transaction.py:736:78: W291 trailing whitespace -pymodbus/transaction.py:738:78: W291 trailing whitespace -pymodbus/utilities.py:13:1: E302 expected 2 blank lines, found 1 -pymodbus/utilities.py:23:1: E302 expected 2 blank lines, found 1 -pymodbus/utilities.py:34:26: E231 missing whitespace after ',' -pymodbus/utilities.py:37:80: E501 line too long (87 characters) -pymodbus/utilities.py:37:26: E231 missing whitespace after ',' -pymodbus/utilities.py:40:26: E231 missing whitespace after ',' -pymodbus/utilities.py:47:1: E302 expected 2 blank lines, found 1 -pymodbus/utilities.py:60:15: E701 multiple statements on one line (colon) -pymodbus/utilities.py:65:13: E701 multiple statements on one line (colon) -pymodbus/utilities.py:67:21: E225 missing whitespace around operator -pymodbus/utilities.py:71:1: E302 expected 2 blank lines, found 1 -pymodbus/utilities.py:93:1: E302 expected 2 blank lines, found 1 -pymodbus/utilities.py:104:17: E701 multiple statements on one line (colon) -pymodbus/utilities.py:111:1: E302 expected 2 blank lines, found 1 -pymodbus/utilities.py:124:51: E702 multiple statements on one line (semicolon) -pymodbus/utilities.py:129:1: E302 expected 2 blank lines, found 1 -pymodbus/utilities.py:138:1: E302 expected 2 blank lines, found 1 -pymodbus/utilities.py:153:1: E302 expected 2 blank lines, found 1 -pymodbus/utilities.py:162:1: E302 expected 2 blank lines, found 1 -pymodbus/utilities.py:184:78: W291 trailing whitespace -pymodbus/utilities.py:186:78: W291 trailing whitespace -pymodbus/version.py:8:1: E302 expected 2 blank lines, found 0 -pymodbus/version.py:37:31: E261 at least two spaces before inline comment -pymodbus/version.py:39:78: W291 trailing whitespace -pymodbus/version.py:41:78: W291 trailing whitespace -pymodbus/client/async.py:13:1: W293 blank line contains whitespace -pymodbus/client/async.py:36:1: E302 expected 2 blank lines, found 1 -pymodbus/client/async.py:49:33: E261 at least two spaces before inline comment -pymodbus/client/async.py:129:1: E302 expected 2 blank lines, found 1 -pymodbus/client/async.py:134:78: W291 trailing whitespace -pymodbus/client/async.py:136:78: W291 trailing whitespace -pymodbus/client/common.py:11:1: E302 expected 2 blank lines, found 1 -pymodbus/client/common.py:133:1: W391 blank line at end of file -pymodbus/client/sync.py:19:1: E302 expected 2 blank lines, found 1 -pymodbus/client/sync.py:50:80: E501 line too long (83 characters) -pymodbus/client/sync.py:51:22: E702 multiple statements on one line (semicolon) -pymodbus/client/sync.py:74:1: E302 expected 2 blank lines, found 1 -pymodbus/client/sync.py:98:80: E501 line too long (80 characters) -pymodbus/client/sync.py:99:1: W293 blank line contains whitespace -pymodbus/client/sync.py:103:80: E501 line too long (80 characters) -pymodbus/client/sync.py:111:80: E501 line too long (80 characters) -pymodbus/client/sync.py:119:80: E501 line too long (80 characters) -pymodbus/client/sync.py:142:80: E501 line too long (81 characters) -pymodbus/client/sync.py:155:1: W293 blank line contains whitespace -pymodbus/client/sync.py:163:1: E302 expected 2 blank lines, found 1 -pymodbus/client/sync.py:177:1: W293 blank line contains whitespace -pymodbus/client/sync.py:180:1: W293 blank line contains whitespace -pymodbus/client/sync.py:183:23: E701 multiple statements on one line (colon) -pymodbus/client/sync.py:194:1: W293 blank line contains whitespace -pymodbus/client/sync.py:222:1: W293 blank line contains whitespace -pymodbus/client/sync.py:230:1: E302 expected 2 blank lines, found 1 -pymodbus/client/sync.py:244:1: W293 blank line contains whitespace -pymodbus/client/sync.py:250:23: E701 multiple statements on one line (colon) -pymodbus/client/sync.py:258:1: W293 blank line contains whitespace -pymodbus/client/sync.py:284:1: W293 blank line contains whitespace -pymodbus/client/sync.py:292:1: E302 expected 2 blank lines, found 1 -pymodbus/client/sync.py:307:21: E221 multiple spaces before operator -pymodbus/client/sync.py:308:21: E221 multiple spaces before operator -pymodbus/client/sync.py:311:21: E221 multiple spaces before operator +pymodbus/pdu.py:137:27: E221 multiple spaces before operator +pymodbus/register_read_message.py:128:80: E501 line too long (80 characters) +pymodbus/register_read_message.py:178:80: E501 line too long (80 characters) +pymodbus/register_read_message.py:228:26: E221 multiple spaces before operator +pymodbus/register_read_message.py:229:26: E221 multiple spaces before operator +pymodbus/register_write_message.py:133:22: E701 multiple statements on one line (colon) +pymodbus/register_write_message.py:134:45: E701 multiple statements on one line (colon) +pymodbus/register_write_message.py:154:80: E501 line too long (83 characters) +pymodbus/transaction.py:141:31: E231 missing whitespace after ':' +pymodbus/transaction.py:142:21: E221 multiple spaces before operator +pymodbus/transaction.py:143:21: E221 multiple spaces before operator +pymodbus/transaction.py:174:31: E231 missing whitespace after ':' +pymodbus/transaction.py:239:17: E701 multiple statements on one line (colon) +pymodbus/transaction.py:300:21: E221 multiple spaces before operator +pymodbus/transaction.py:301:21: E221 multiple spaces before operator +pymodbus/transaction.py:303:21: E221 multiple spaces before operator +pymodbus/transaction.py:375:14: E221 multiple spaces before operator +pymodbus/transaction.py:376:14: E221 multiple spaces before operator +pymodbus/transaction.py:417:17: E701 multiple statements on one line (colon) +pymodbus/transaction.py:457:31: E231 missing whitespace after ':' +pymodbus/transaction.py:458:21: E221 multiple spaces before operator +pymodbus/transaction.py:459:21: E221 multiple spaces before operator +pymodbus/transaction.py:460:21: E221 multiple spaces before operator +pymodbus/transaction.py:461:21: E221 multiple spaces before operator +pymodbus/transaction.py:472:23: E701 multiple statements on one line (colon) +pymodbus/transaction.py:473:21: E203 whitespace before ':' +pymodbus/transaction.py:492:31: E231 missing whitespace after ':' +pymodbus/transaction.py:517:14: E221 multiple spaces before operator +pymodbus/transaction.py:518:14: E221 multiple spaces before operator +pymodbus/transaction.py:559:17: E701 multiple statements on one line (colon) +pymodbus/transaction.py:568:16: E221 multiple spaces before operator +pymodbus/transaction.py:569:16: E221 multiple spaces before operator +pymodbus/transaction.py:612:31: E231 missing whitespace after ':' +pymodbus/transaction.py:613:21: E221 multiple spaces before operator +pymodbus/transaction.py:614:21: E221 multiple spaces before operator +pymodbus/transaction.py:615:21: E221 multiple spaces before operator +pymodbus/transaction.py:616:21: E221 multiple spaces before operator +pymodbus/transaction.py:627:23: E701 multiple statements on one line (colon) +pymodbus/transaction.py:628:21: E203 whitespace before ':' +pymodbus/transaction.py:635:80: E501 line too long (85 characters) +pymodbus/transaction.py:647:31: E231 missing whitespace after ':' +pymodbus/transaction.py:672:14: E221 multiple spaces before operator +pymodbus/transaction.py:673:14: E221 multiple spaces before operator +pymodbus/transaction.py:714:17: E701 multiple statements on one line (colon) +pymodbus/utilities.py:64:15: E701 multiple statements on one line (colon) +pymodbus/utilities.py:69:13: E701 multiple statements on one line (colon) +pymodbus/utilities.py:110:17: E701 multiple statements on one line (colon) +pymodbus/utilities.py:131:51: E702 multiple statements on one line (semicolon) +pymodbus/client/sync.py:52:80: E501 line too long (83 characters) +pymodbus/client/sync.py:53:22: E702 multiple statements on one line (semicolon) +pymodbus/client/sync.py:101:80: E501 line too long (80 characters) +pymodbus/client/sync.py:106:80: E501 line too long (80 characters) +pymodbus/client/sync.py:114:80: E501 line too long (80 characters) +pymodbus/client/sync.py:122:80: E501 line too long (80 characters) +pymodbus/client/sync.py:145:80: E501 line too long (81 characters) +pymodbus/client/sync.py:187:23: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:255:23: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:313:21: E221 multiple spaces before operator pymodbus/client/sync.py:314:21: E221 multiple spaces before operator -pymodbus/client/sync.py:316:21: E221 multiple spaces before operator -pymodbus/client/sync.py:326:31: E701 multiple statements on one line (colon) -pymodbus/client/sync.py:327:29: E701 multiple statements on one line (colon) -pymodbus/client/sync.py:328:32: E701 multiple statements on one line (colon) -pymodbus/client/sync.py:330:1: W293 blank line contains whitespace -pymodbus/client/sync.py:336:23: E701 multiple statements on one line (colon) -pymodbus/client/sync.py:338:78: W291 trailing whitespace -pymodbus/client/sync.py:345:1: W293 blank line contains whitespace -pymodbus/client/sync.py:373:1: W293 blank line contains whitespace -pymodbus/client/sync.py:378:78: W291 trailing whitespace -pymodbus/client/sync.py:380:78: W291 trailing whitespace -pymodbus/datastore/__init__.py:6:78: W291 trailing whitespace -pymodbus/datastore/__init__.py:8:78: W291 trailing whitespace +pymodbus/client/sync.py:317:21: E221 multiple spaces before operator +pymodbus/client/sync.py:320:21: E221 multiple spaces before operator +pymodbus/client/sync.py:322:21: E221 multiple spaces before operator +pymodbus/client/sync.py:332:31: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:333:29: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:334:32: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:342:23: E701 multiple statements on one line (colon) pymodbus/datastore/context.py:8:15: E702 multiple statements on one line (semicolon) -pymodbus/datastore/context.py:14:1: E302 expected 2 blank lines, found 1 -pymodbus/datastore/context.py:29:1: W191 indentation contains tabs -pymodbus/datastore/context.py:29:1: E101 indentation contains mixed spaces and tabs -pymodbus/datastore/context.py:55:30: E261 at least two spaces before inline comment -pymodbus/datastore/context.py:67:30: E261 at least two spaces before inline comment -pymodbus/datastore/context.py:78:30: E261 at least two spaces before inline comment -pymodbus/datastore/context.py:79:59: E231 missing whitespace after ',' -pymodbus/datastore/context.py:82:1: E302 expected 2 blank lines, found 1 -pymodbus/datastore/context.py:96:21: E221 multiple spaces before operator -pymodbus/datastore/context.py:115:23: E701 multiple statements on one line (colon) -pymodbus/datastore/context.py:118:13: E701 multiple statements on one line (colon) -pymodbus/datastore/context.py:126:23: E701 multiple statements on one line (colon) -pymodbus/datastore/context.py:127:25: W601 .has_key() is deprecated, use 'in' -pymodbus/datastore/context.py:129:80: E501 line too long (82 characters) -pymodbus/datastore/context.py:129:13: E701 multiple statements on one line (colon) -pymodbus/datastore/context.py:130:1: W391 blank line at end of file +pymodbus/datastore/context.py:98:21: E221 multiple spaces before operator +pymodbus/datastore/context.py:117:23: E701 multiple statements on one line (colon) +pymodbus/datastore/context.py:120:13: E701 multiple statements on one line (colon) +pymodbus/datastore/context.py:128:23: E701 multiple statements on one line (colon) +pymodbus/datastore/context.py:131:80: E501 line too long (82 characters) +pymodbus/datastore/context.py:131:13: E701 multiple statements on one line (colon) pymodbus/datastore/database.py:13:15: E702 multiple statements on one line (semicolon) -pymodbus/datastore/database.py:19:1: E302 expected 2 blank lines, found 1 -pymodbus/datastore/database.py:45:40: E261 at least two spaces before inline comment -pymodbus/datastore/database.py:55:30: E261 at least two spaces before inline comment -pymodbus/datastore/database.py:67:30: E261 at least two spaces before inline comment -pymodbus/datastore/database.py:78:30: E261 at least two spaces before inline comment -pymodbus/datastore/database.py:79:60: E231 missing whitespace after ',' -pymodbus/datastore/database.py:82:80: E501 line too long (80 characters) -pymodbus/datastore/database.py:84:80: E501 line too long (80 characters) -pymodbus/datastore/database.py:100:1: W293 blank line contains whitespace -pymodbus/datastore/database.py:110:40: W291 trailing whitespace -pymodbus/datastore/database.py:109:14: E221 multiple spaces before operator -pymodbus/datastore/database.py:127:26: E203 whitespace before ':' -pymodbus/datastore/database.py:127:18: E225 missing whitespace around operator -pymodbus/datastore/database.py:141:15: E221 multiple spaces before operator +pymodbus/datastore/database.py:83:80: E501 line too long (80 characters) +pymodbus/datastore/database.py:85:80: E501 line too long (80 characters) +pymodbus/datastore/database.py:110:14: E221 multiple spaces before operator +pymodbus/datastore/database.py:128:28: E203 whitespace before ':' pymodbus/datastore/database.py:142:15: E221 multiple spaces before operator -pymodbus/datastore/database.py:144:1: W293 blank line contains whitespace -pymodbus/datastore/database.py:153:15: E221 multiple spaces before operator +pymodbus/datastore/database.py:143:15: E221 multiple spaces before operator pymodbus/datastore/database.py:154:15: E221 multiple spaces before operator -pymodbus/datastore/database.py:157:15: E221 multiple spaces before operator -pymodbus/datastore/database.py:169:40: W291 trailing whitespace +pymodbus/datastore/database.py:155:15: E221 multiple spaces before operator +pymodbus/datastore/database.py:158:15: E221 multiple spaces before operator pymodbus/datastore/database.py:168:14: E221 multiple spaces before operator -pymodbus/datastore/database.py:174:1: W391 blank line at end of file pymodbus/datastore/modredis.py:8:15: E702 multiple statements on one line (semicolon) -pymodbus/datastore/modredis.py:14:1: E302 expected 2 blank lines, found 1 -pymodbus/datastore/modredis.py:52:30: E261 at least two spaces before inline comment -pymodbus/datastore/modredis.py:64:30: E261 at least two spaces before inline comment -pymodbus/datastore/modredis.py:75:30: E261 at least two spaces before inline comment -pymodbus/datastore/modredis.py:76:59: E231 missing whitespace after ',' -pymodbus/datastore/modredis.py:79:80: E501 line too long (80 characters) -pymodbus/datastore/modredis.py:81:80: E501 line too long (80 characters) -pymodbus/datastore/modredis.py:96:16: E203 whitespace before ':' -pymodbus/datastore/modredis.py:96:27: E231 missing whitespace after ',' -pymodbus/datastore/modredis.py:102:16: E203 whitespace before ':' -pymodbus/datastore/modredis.py:102:27: E231 missing whitespace after ',' -pymodbus/datastore/modredis.py:108:16: E203 whitespace before ':' -pymodbus/datastore/modredis.py:108:27: E231 missing whitespace after ',' -pymodbus/datastore/modredis.py:114:80: E501 line too long (80 characters) -pymodbus/datastore/modredis.py:116:80: E501 line too long (80 characters) -pymodbus/datastore/modredis.py:117:17: E221 multiple spaces before operator -pymodbus/datastore/modredis.py:129:26: E225 missing whitespace around operator -pymodbus/datastore/modredis.py:131:59: E225 missing whitespace around operator -pymodbus/datastore/modredis.py:131:16: E221 multiple spaces before operator -pymodbus/datastore/modredis.py:157:36: E225 missing whitespace around operator -pymodbus/datastore/modredis.py:168:26: E225 missing whitespace around operator -pymodbus/datastore/modredis.py:174:61: E225 missing whitespace around operator -pymodbus/datastore/modredis.py:175:80: E501 line too long (90 characters) -pymodbus/datastore/modredis.py:175:31: E225 missing whitespace around operator -pymodbus/datastore/modredis.py:175:15: E221 multiple spaces before operator -pymodbus/datastore/modredis.py:178:58: E225 missing whitespace around operator -pymodbus/datastore/modredis.py:182:80: E501 line too long (80 characters) -pymodbus/datastore/modredis.py:184:80: E501 line too long (80 characters) -pymodbus/datastore/modredis.py:185:17: E221 multiple spaces before operator -pymodbus/datastore/modredis.py:200:68: E225 missing whitespace around operator -pymodbus/datastore/modredis.py:200:16: E221 multiple spaces before operator -pymodbus/datastore/modredis.py:224:38: E225 missing whitespace around operator -pymodbus/datastore/modredis.py:240:67: E225 missing whitespace around operator -pymodbus/datastore/modredis.py:243:1: W391 blank line at end of file +pymodbus/datastore/modredis.py:80:80: E501 line too long (80 characters) +pymodbus/datastore/modredis.py:82:80: E501 line too long (80 characters) +pymodbus/datastore/modredis.py:97:16: E203 whitespace before ':' +pymodbus/datastore/modredis.py:103:16: E203 whitespace before ':' +pymodbus/datastore/modredis.py:109:16: E203 whitespace before ':' +pymodbus/datastore/modredis.py:115:80: E501 line too long (80 characters) +pymodbus/datastore/modredis.py:117:80: E501 line too long (80 characters) +pymodbus/datastore/modredis.py:118:17: E221 multiple spaces before operator +pymodbus/datastore/modredis.py:132:16: E221 multiple spaces before operator +pymodbus/datastore/modredis.py:176:80: E501 line too long (92 characters) +pymodbus/datastore/modredis.py:176:15: E221 multiple spaces before operator +pymodbus/datastore/modredis.py:183:80: E501 line too long (80 characters) +pymodbus/datastore/modredis.py:185:80: E501 line too long (80 characters) +pymodbus/datastore/modredis.py:186:17: E221 multiple spaces before operator +pymodbus/datastore/modredis.py:201:16: E221 multiple spaces before operator pymodbus/datastore/remote.py:98:32: E701 multiple statements on one line (colon) pymodbus/datastore/remote.py:99:32: E701 multiple statements on one line (colon) pymodbus/datastore/remote.py:100:13: E701 multiple statements on one line (colon) @@ -392,7 +196,7 @@ pymodbus/datastore/store.py:144:13: E701 multiple statements on one line (colon) pymodbus/datastore/store.py:154:15: E221 multiple spaces before operator pymodbus/datastore/store.py:195:13: E701 multiple statements on one line (colon) pymodbus/datastore/store.py:207:22: E701 multiple statements on one line (colon) -pymodbus/internal/ptwisted.py:50:15: E701 multiple statements on one line (colon) +pymodbus/internal/ptwisted.py:53:15: E701 multiple statements on one line (colon) pymodbus/server/sync.py:60:34: E701 multiple statements on one line (colon) pymodbus/server/sync.py:64:19: E701 multiple statements on one line (colon) pymodbus/server/sync.py:130:20: E221 multiple spaces before operator @@ -406,23 +210,9 @@ pymodbus/server/sync.py:240:21: E221 multiple spaces before operator pymodbus/server/sync.py:241:21: E221 multiple spaces before operator pymodbus/server/sync.py:249:23: E701 multiple statements on one line (colon) pymodbus/server/sync.py:280:19: E701 multiple statements on one line (colon) -pymodbus/server/sync.py:291:1: E302 expected 2 blank lines, found 1 -pymodbus/server/sync.py:301:1: E302 expected 2 blank lines, found 1 -pymodbus/server/sync.py:311:1: E302 expected 2 blank lines, found 1 -pymodbus/server/sync.py:321:78: W291 trailing whitespace -pymodbus/server/sync.py:323:78: W291 trailing whitespace -1 E101 indentation contains mixed spaces and tabs 7 E203 whitespace before ':' 119 E221 multiple spaces before operator -25 E225 missing whitespace around operator -17 E231 missing whitespace after ',' -23 E261 at least two spaces before inline comment -51 E302 expected 2 blank lines, found 1 -42 E501 line too long (80 characters) +6 E231 missing whitespace after ':' +32 E501 line too long (80 characters) 42 E701 multiple statements on one line (colon) 5 E702 multiple statements on one line (semicolon) -1 W191 indentation contains tabs -38 W291 trailing whitespace -36 W293 blank line contains whitespace -4 W391 blank line at end of file -1 W601 .has_key() is deprecated, use 'in' diff --git a/pymodbus/client/async.py b/pymodbus/client/async.py index c9183aeec..5322776d3 100644 --- a/pymodbus/client/async.py +++ b/pymodbus/client/async.py @@ -10,7 +10,7 @@ def clientTest(): requests = [ ReadCoilsRequest(0,99) ] p = reactor.connectTCP("localhost", 502, ModbusClientFactory(requests)) - + if __name__ == "__main__": reactor.callLater(1, clientTest) reactor.run() @@ -28,6 +28,7 @@ def clientTest(): import logging _logger = logging.getLogger(__name__) + #---------------------------------------------------------------------------# # Client Protocols #---------------------------------------------------------------------------# @@ -44,7 +45,7 @@ def __init__(self, framer=None): :param framer: The framer to use for the protocol ''' self.framer = framer or ModbusSocketFramer(ClientDecoder()) - self._requests = deque() # link queue to tid + self._requests = deque() # link queue to tid self._connected = False def connectionMade(self): @@ -121,6 +122,7 @@ def __getNextTID(self): # deferLater(clock, self.delay, send, message) # self.retry -= 1 + #---------------------------------------------------------------------------# # Client Factories #---------------------------------------------------------------------------# @@ -129,9 +131,9 @@ class ModbusClientFactory(protocol.ReconnectingClientFactory): protocol = ModbusClientProtocol -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ "ModbusClientProtocol", "ModbusClientFactory", ] diff --git a/pymodbus/client/common.py b/pymodbus/client/common.py index df5fbb52f..c08625c4f 100644 --- a/pymodbus/client/common.py +++ b/pymodbus/client/common.py @@ -8,6 +8,7 @@ from pymodbus.file_message import * from pymodbus.other_message import * + class ModbusClientMixin(object): ''' This is a modbus client mixin that provides additional factory @@ -129,5 +130,3 @@ def readwrite_registers(self, *args, **kwargs): request = ReadWriteMultipleRegistersRequest(*args, **kwargs) request.unit_id = kwargs.get('unit', 0x00) return self.execute(request) - - diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index abc593ac8..212e0bacf 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -14,6 +14,7 @@ import logging _logger = logging.getLogger(__name__) + #---------------------------------------------------------------------------# # Client Producer/Consumer #---------------------------------------------------------------------------# @@ -72,6 +73,7 @@ def __getNextTID(self): ModbusTransactionManager.__tid = tid return tid + class BaseModbusClient(ModbusClientMixin): ''' Inteface for a modbus synchronous client. Defined here are all the @@ -97,7 +99,7 @@ def connect(self): :returns: True if connection succeeded, False otherwise ''' raise NotImplementedException("Method not implemented by derived class") - + def close(self): ''' Closes the underlying socket connection ''' @@ -153,11 +155,12 @@ def __del__(self): def __str__(self): ''' Builds a string representation of the connection - + :returns: The string representation ''' return "Null Transport" + #---------------------------------------------------------------------------# # Modbus TCP Client Transport Implementation #---------------------------------------------------------------------------# @@ -175,10 +178,10 @@ def __init__(self, host='127.0.0.1', port=Defaults.Port): self.port = port self.socket = None BaseModbusClient.__init__(self, ModbusSocketFramer(ClientDecoder())) - + def connect(self): ''' Connect to the modbus tcp server - + :returns: True if connection succeeded, False otherwise ''' if self.socket: return True @@ -192,7 +195,7 @@ def connect(self): (self.host, self.port, msg)) self.close() return self.socket != None - + def close(self): ''' Closes the underlying socket connection ''' @@ -220,11 +223,12 @@ def _recv(self, size): def __str__(self): ''' Builds a string representation of the connection - + :returns: The string representation ''' return "%s:%s" % (self.host, self.port) + #---------------------------------------------------------------------------# # Modbus UDP Client Transport Implementation #---------------------------------------------------------------------------# @@ -242,7 +246,7 @@ def __init__(self, host='127.0.0.1', port=Defaults.Port): self.port = port self.socket = None BaseModbusClient.__init__(self, ModbusSocketFramer(ClientDecoder())) - + def connect(self): ''' Connect to the modbus tcp server @@ -256,7 +260,7 @@ def connect(self): _logger.error('Unable to create udp socket %s' % ex) self.close() return self.socket != None - + def close(self): ''' Closes the underlying socket connection ''' @@ -282,11 +286,12 @@ def _recv(self, size): def __str__(self): ''' Builds a string representation of the connection - + :returns: The string representation ''' return "%s:%s" % (self.host, self.port) + #---------------------------------------------------------------------------# # Modbus Serial Client Transport Implementation #---------------------------------------------------------------------------# @@ -328,7 +333,7 @@ def __implementation(method): elif method == 'rtu': return ModbusRtuFramer(ClientDecoder()) elif method == 'binary': return ModbusBinaryFramer(ClientDecoder()) raise ParameterException("Invalid framer method requested") - + def connect(self): ''' Connect to the modbus tcp server @@ -336,14 +341,14 @@ def connect(self): ''' if self.socket: return True try: - self.socket = serial.Serial(port=self.port, timeout=self.timeout, + self.socket = serial.Serial(port=self.port, timeout=self.timeout, bytesize=self.bytesize, stopbits=self.stopbits, baudrate=self.baudrate, parity=self.parity) except serial.SerialException, msg: _logger.error(msg) self.close() return self.socket != None - + def close(self): ''' Closes the underlying socket connection ''' @@ -371,14 +376,14 @@ def _recv(self, size): def __str__(self): ''' Builds a string representation of the connection - + :returns: The string representation ''' return "%s baud[%s]" % (self.method, self.baudrate) -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ "ModbusTcpClient", "ModbusUdpClient", "ModbusSerialClient" ] diff --git a/pymodbus/datastore/__init__.py b/pymodbus/datastore/__init__.py index 631aabd00..a981d4be2 100644 --- a/pymodbus/datastore/__init__.py +++ b/pymodbus/datastore/__init__.py @@ -3,9 +3,9 @@ from pymodbus.datastore.context import ModbusSlaveContext from pymodbus.datastore.context import ModbusServerContext -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ "ModbusSequentialDataBlock", "ModbusSparseDataBlock", "ModbusSlaveContext", "ModbusServerContext", diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index dce4f1472..b1e182dcf 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -8,6 +8,7 @@ import logging; _logger = logging.getLogger(__name__) + #---------------------------------------------------------------------------# # Slave Contexts #---------------------------------------------------------------------------# @@ -26,7 +27,7 @@ def __init__(self, *args, **kwargs): 'hr' - Holding Register initializer 'ir' - Input Registers iniatializer ''' - self.store = {} + self.store = {} self.store['d'] = kwargs.get('di', ModbusSequentialDataBlock(0, 0)) self.store['c'] = kwargs.get('co', ModbusSequentialDataBlock(0, 0)) self.store['i'] = kwargs.get('ir', ModbusSequentialDataBlock(0, 0)) @@ -52,7 +53,7 @@ def validate(self, fx, address, count=1): :param count: The number of values to test :returns: True if the request in within range, False otherwise ''' - address = address + 1 # section 4.4 of specification + address = address + 1 # section 4.4 of specification _logger.debug("validate[%d] %d:%d" % (fx, address, count)) return self.store[self.decode(fx)].validate(address, count) @@ -64,7 +65,7 @@ def getValues(self, fx, address, count=1): :param count: The number of values to retrieve :returns: The requested values from a:a+c ''' - address = address + 1 # section 4.4 of specification + address = address + 1 # section 4.4 of specification _logger.debug("getValues[%d] %d:%d" % (fx, address, count)) return self.store[self.decode(fx)].getValues(address, count) @@ -75,10 +76,11 @@ def setValues(self, fx, address, values): :param address: The starting address :param values: The new values to be set ''' - address = address + 1 # section 4.4 of specification - _logger.debug("setValues[%d] %d:%d" % (fx, address,len(values))) + address = address + 1 # section 4.4 of specification + _logger.debug("setValues[%d] %d:%d" % (fx, address, len(values))) self.store[self.decode(fx)].setValues(address, values) + class ModbusServerContext(object): ''' This represents a master collection of slave contexts. If single is set to true, it will be treated as a single @@ -124,7 +126,6 @@ def __getitem__(self, slave): :returns: The requested slave context ''' if self.single: slave = 0x00 - if self.__slaves.has_key(slave): + if slave in self.__slaves: return self.__slaves.get(slave) else: raise ParameterException("slave does not exist, or is out of range") - diff --git a/pymodbus/datastore/database.py b/pymodbus/datastore/database.py index abd365be0..c1c48b161 100644 --- a/pymodbus/datastore/database.py +++ b/pymodbus/datastore/database.py @@ -13,6 +13,7 @@ import logging; _logger = logging.getLogger(__name__) + #---------------------------------------------------------------------------# # Context #---------------------------------------------------------------------------# @@ -42,7 +43,7 @@ def reset(self): ''' Resets all the datastores to their default values ''' self._metadata.drop_all() self.__db_create(self.table, self.database) - raise NotImplementedException() # TODO drop table? + raise NotImplementedException() # TODO drop table? def validate(self, fx, address, count=1): ''' Validates the request to make sure it is in range @@ -52,7 +53,7 @@ def validate(self, fx, address, count=1): :param count: The number of values to test :returns: True if the request in within range, False otherwise ''' - address = address + 1 # section 4.4 of specification + address = address + 1 # section 4.4 of specification _logger.debug("validate[%d] %d:%d" % (fx, address, count)) return self.__validate(self.decode(fx), address, count) @@ -64,7 +65,7 @@ def getValues(self, fx, address, count=1): :param count: The number of values to retrieve :returns: The requested values from a:a+c ''' - address = address + 1 # section 4.4 of specification + address = address + 1 # section 4.4 of specification _logger.debug("get-values[%d] %d:%d" % (fx, address, count)) return self.__get(self.decode(fx), address, count) @@ -75,8 +76,8 @@ def setValues(self, fx, address, values): :param address: The starting address :param values: The new values to be set ''' - address = address + 1 # section 4.4 of specification - _logger.debug("set-values[%d] %d:%d" % (fx, address,len(values))) + address = address + 1 # section 4.4 of specification + _logger.debug("set-values[%d] %d:%d" % (fx, address, len(values))) self.__set(self.decode(fx), address, values) #--------------------------------------------------------------------------# @@ -97,7 +98,7 @@ def __db_create(self, table, database): UniqueConstraint('type', 'index', name='key')) self._table.create(checkfirst=True) self._connection = self._engine.connect() - + def __get(self, type, offset, count): ''' @@ -107,7 +108,7 @@ def __get(self, type, offset, count): :returns: The resulting values ''' query = self._table.select(and_( - self._table.c.type == type, + self._table.c.type == type, self._table.c.index >= offset, self._table.c.index <= offset + count)) query = query.order_by(self._table.c.index.asc()) @@ -124,9 +125,9 @@ def __build_set(self, type, offset, values, p=''): result = [] for index, value in enumerate(values): result.append({ - p+'type' : type, - p+'index' : offset + index, - 'value' : value + p + 'type' : type, + p + 'index' : offset + index, + 'value' : value }) return result @@ -141,7 +142,7 @@ def __set(self, type, offset, values): query = self._table.insert() result = self._connection.execute(query, context) return result.rowcount == len(values) - + def __update(self, type, offset, values): ''' @@ -159,16 +160,14 @@ def __update(self, type, offset, values): def __validate(self, key, offset, count): ''' - :param key: The key prefix to use :param offset: The address offset to start at :param count: The number of bits to read :returns: The result of the validation ''' query = self._table.select(and_( - self._table.c.type == type, + self._table.c.type == type, self._table.c.index >= offset, self._table.c.index <= offset + count)) result = self._connection.execute(query) return result.rowcount == count - diff --git a/pymodbus/datastore/modredis.py b/pymodbus/datastore/modredis.py index 67010db4f..ef44c6544 100644 --- a/pymodbus/datastore/modredis.py +++ b/pymodbus/datastore/modredis.py @@ -8,6 +8,7 @@ import logging; _logger = logging.getLogger(__name__) + #---------------------------------------------------------------------------# # Context #---------------------------------------------------------------------------# @@ -49,7 +50,7 @@ def validate(self, fx, address, count=1): :param count: The number of values to test :returns: True if the request in within range, False otherwise ''' - address = address + 1 # section 4.4 of specification + address = address + 1 # section 4.4 of specification _logger.debug("validate[%d] %d:%d" % (fx, address, count)) return self.__val_callbacks[self.decode(fx)](address, count) @@ -61,7 +62,7 @@ def getValues(self, fx, address, count=1): :param count: The number of values to retrieve :returns: The requested values from a:a+c ''' - address = address + 1 # section 4.4 of specification + address = address + 1 # section 4.4 of specification _logger.debug("getValues[%d] %d:%d" % (fx, address, count)) return self.__get_callbacks[self.decode(fx)](address, count) @@ -72,8 +73,8 @@ def setValues(self, fx, address, values): :param address: The starting address :param values: The new values to be set ''' - address = address + 1 # section 4.4 of specification - _logger.debug("setValues[%d] %d:%d" % (fx, address,len(values))) + address = address + 1 # section 4.4 of specification + _logger.debug("setValues[%d] %d:%d" % (fx, address, len(values))) self.__set_callbacks[self.decode(fx)](address, values) #--------------------------------------------------------------------------# @@ -93,22 +94,22 @@ def __build_mapping(self): code mapper. ''' self.__val_callbacks = { - 'd' : lambda o,c: self.__val_bit('d', o, c), - 'c' : lambda o,c: self.__val_bit('c', o, c), - 'h' : lambda o,c: self.__val_reg('h', o, c), - 'i' : lambda o,c: self.__val_reg('i', o, c), + 'd' : lambda o, c: self.__val_bit('d', o, c), + 'c' : lambda o, c: self.__val_bit('c', o, c), + 'h' : lambda o, c: self.__val_reg('h', o, c), + 'i' : lambda o, c: self.__val_reg('i', o, c), } self.__get_callbacks = { - 'd' : lambda o,c: self.__get_bit('d', o, c), - 'c' : lambda o,c: self.__get_bit('c', o, c), - 'h' : lambda o,c: self.__get_reg('h', o, c), - 'i' : lambda o,c: self.__get_reg('i', o, c), + 'd' : lambda o, c: self.__get_bit('d', o, c), + 'c' : lambda o, c: self.__get_bit('c', o, c), + 'h' : lambda o, c: self.__get_reg('h', o, c), + 'i' : lambda o, c: self.__get_reg('i', o, c), } self.__set_callbacks = { - 'd' : lambda o,v: self.__set_bit('d', o, v), - 'c' : lambda o,v: self.__set_bit('c', o, v), - 'h' : lambda o,v: self.__set_reg('h', o, v), - 'i' : lambda o,v: self.__set_reg('i', o, v), + 'd' : lambda o, v: self.__set_bit('d', o, v), + 'c' : lambda o, v: self.__set_bit('c', o, v), + 'h' : lambda o, v: self.__set_reg('h', o, v), + 'i' : lambda o, v: self.__set_reg('i', o, v), } #--------------------------------------------------------------------------# @@ -126,9 +127,9 @@ def __get_bit_values(self, key, offset, count): ''' key = self.__get_prefix(key) s = divmod(offset, self.__bit_size)[0] - e = divmod(offset+count, self.__bit_size)[0] + e = divmod(offset + count, self.__bit_size)[0] - request = ('%s:%s' % (key, v) for v in range(s, e+1)) + request = ('%s:%s' % (key, v) for v in range(s, e + 1)) response = self.client.mget(request) return response @@ -154,7 +155,7 @@ def __get_bit(self, key, offset, count): response = (r or self.__bit_default for r in response) result = ''.join(response) result = unpack_bitstring(result) - return result[offset:offset+count] + return result[offset:offset + count] def __set_bit(self, key, offset, values): ''' @@ -165,17 +166,17 @@ def __set_bit(self, key, offset, values): ''' count = len(values) s = divmod(offset, self.__bit_size)[0] - e = divmod(offset+count, self.__bit_size)[0] + e = divmod(offset + count, self.__bit_size)[0] value = pack_bitstring(values) current = self.__get_bit_values(key, offset, count) current = (r or self.__bit_default for r in current) current = ''.join(current) - current = current[0:offset] + value + current[offset+count:] - final = (current[s:s+self.__bit_size] for s in range(0, count, self.__bit_size)) + current = current[0:offset] + value + current[offset + count:] + final = (current[s:s + self.__bit_size] for s in range(0, count, self.__bit_size)) key = self.__get_prefix(key) - request = ('%s:%s' % (key, v) for v in range(s, e+1)) + request = ('%s:%s' % (key, v) for v in range(s, e + 1)) request = dict(zip(request, final)) self.client.mset(request) @@ -196,8 +197,8 @@ def __get_reg_values(self, key, offset, count): #s = divmod(offset, self.__reg_size)[0] #e = divmod(offset+count, self.__reg_size)[0] - #request = ('%s:%s' % (key, v) for v in range(s, e+1)) - request = ('%s:%s' % (key, v) for v in range(offset, count+1)) + #request = ('%s:%s' % (key, v) for v in range(s, e + 1)) + request = ('%s:%s' % (key, v) for v in range(offset, count + 1)) response = self.client.mget(request) return response @@ -221,7 +222,7 @@ def __get_reg(self, key, offset, count): ''' response = self.__get_reg_values(key, offset, count) response = [r or self.__reg_default for r in response] - return response[offset:offset+count] + return response[offset:offset + count] def __set_reg(self, key, offset, values): ''' @@ -237,7 +238,6 @@ def __set_reg(self, key, offset, values): #current = self.__get_reg_values(key, offset, count) key = self.__get_prefix(key) - request = ('%s:%s' % (key, v) for v in range(offset, count+1)) + request = ('%s:%s' % (key, v) for v in range(offset, count + 1)) request = dict(zip(request, values)) self.client.mset(request) - diff --git a/pymodbus/diag_message.py b/pymodbus/diag_message.py index b61ff70e2..fc7be4f0a 100644 --- a/pymodbus/diag_message.py +++ b/pymodbus/diag_message.py @@ -648,9 +648,9 @@ class ClearOverrunCountResponse(DiagnosticStatusSimpleResponse): ''' sub_function_code = 0x0014 -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ "ReturnQueryDataRequest", "ReturnQueryDataResponse", "RestartCommunicationsOptionRequest", "RestartCommunicationsOptionResponse", diff --git a/pymodbus/events.py b/pymodbus/events.py index 9ff5c9fba..b0937a25d 100644 --- a/pymodbus/events.py +++ b/pymodbus/events.py @@ -162,7 +162,7 @@ def decode(self, event): class CommunicationRestartEvent(ModbusEvent): - ''' Remote device Initiated Communication Restart + ''' Remote device Initiated Communication Restart The remote device stores this type of event byte when its communications port is restarted. The remote device can be restarted by the Diagnostics diff --git a/pymodbus/file_message.py b/pymodbus/file_message.py index b64961e12..de642ae31 100644 --- a/pymodbus/file_message.py +++ b/pymodbus/file_message.py @@ -21,10 +21,12 @@ class ReadFifoQueueRequest(ModbusRequest): This function code allows to read the contents of a First-In-First-Out (FIFO) queue of register in a remote device. The function returns a count of the registers in the queue, followed by the queued data. - Up to 32 registers can be read: the count, plus up to 31 queued data registers. + Up to 32 registers can be read: the count, plus up to 31 queued data + registers. - The queue count register is returned first, followed by the queued data registers. - The function reads the queue contents, but does not clear them. + The queue count register is returned first, followed by the queued data + registers. The function reads the queue contents, but does not clear + them. ''' function_code = 0x18 _rtu_frame_size = 6 @@ -68,7 +70,7 @@ class ReadFifoQueueResponse(ModbusResponse): In a normal response, the byte count shows the quantity of bytes to follow, including the queue count bytes and value register bytes (but not including the error check field). The queue count is the - quantity of data registers in the queue (not including the count register). + quantity of data registers in the queue (not including the count register). If the queue count exceeds 31, an exception response is returned with an error code of 03 (Illegal Data Value). diff --git a/pymodbus/interfaces.py b/pymodbus/interfaces.py index aa65f41de..1b40634c4 100644 --- a/pymodbus/interfaces.py +++ b/pymodbus/interfaces.py @@ -7,6 +7,7 @@ ''' from pymodbus.exceptions import NotImplementedException + #---------------------------------------------------------------------------# # Generic #---------------------------------------------------------------------------# @@ -57,8 +58,9 @@ def lookupPduClass(self, function_code): class IModbusFramer(object): ''' A framer strategy interface. The idea is that we abstract away all the - detail about how to detect if a current message frame exists, decoding it, - sending it, etc so that we can plug in a new Framer object (tcp, rtu, ascii) + detail about how to detect if a current message frame exists, decoding + it, sending it, etc so that we can plug in a new Framer object (tcp, + rtu, ascii). ''' def checkFrame(self): diff --git a/pymodbus/other_message.py b/pymodbus/other_message.py index 9a7036953..f6cb90037 100644 --- a/pymodbus/other_message.py +++ b/pymodbus/other_message.py @@ -11,6 +11,7 @@ _MCB = ModbusControlBlock() + #---------------------------------------------------------------------------# # TODO Make these only work on serial #---------------------------------------------------------------------------# @@ -56,6 +57,7 @@ def __str__(self): ''' return "ReadExceptionStatusRequest(%d)" % (self.function_code) + class ReadExceptionStatusResponse(ModbusResponse): ''' The normal response contains the status of the eight Exception Status @@ -100,24 +102,26 @@ def __str__(self): # Encapsulate interface transport 43, 14 # CANopen general reference 43, 13 + #---------------------------------------------------------------------------# # TODO Make these only work on serial #---------------------------------------------------------------------------# class GetCommEventCounterRequest(ModbusRequest): ''' - This function code is used to get a status word and an event count from the - remote device's communication event counter. + This function code is used to get a status word and an event count from + the remote device's communication event counter. - By fetching the current count before and after a series of messages, a client - can determine whether the messages were handled normally by the remote device. + By fetching the current count before and after a series of messages, a + client can determine whether the messages were handled normally by the + remote device. - The device's event counter is incremented once for each successful message - completion. It is not incremented for exception responses, poll commands, - or fetch event counter commands. + The device's event counter is incremented once for each successful + message completion. It is not incremented for exception responses, + poll commands, or fetch event counter commands. - The event counter can be reset by means of the Diagnostics function (code 08), - with a subfunction of Restart Communications Option (code 00 01) or - Clear Counters and Diagnostic Register (code 00 0A). + The event counter can be reset by means of the Diagnostics function + (code 08), with a subfunction of Restart Communications Option + (code 00 01) or Clear Counters and Diagnostic Register (code 00 0A). ''' function_code = 0x0b _rtu_frame_size = 4 @@ -154,13 +158,14 @@ def __str__(self): ''' return "GetCommEventCounterRequest(%d)" % (self.function_code) + class GetCommEventCounterResponse(ModbusResponse): ''' The normal response contains a two-byte status word, and a two-byte event count. The status word will be all ones (FF FF hex) if a - previously-issued program command is still being processed by the remote - device (a busy condition exists). Otherwise, the status word will be - all zeros. + previously-issued program command is still being processed by the + remote device (a busy condition exists). Otherwise, the status word + will be all zeros. ''' function_code = 0x0b _rtu_frame_size = 8 @@ -172,7 +177,7 @@ def __init__(self, count): ''' ModbusResponse.__init__(self) self.count = count - self.status = True # this means we are ready, not waiting + self.status = True # this means we are ready, not waiting def encode(self): ''' Encodes the response @@ -198,27 +203,29 @@ def __str__(self): arguments = (self.function_code, self.count, self.status) return "GetCommEventCounterResponse(%d, %d, %d)" % arguments + #---------------------------------------------------------------------------# # TODO Make these only work on serial #---------------------------------------------------------------------------# class GetCommEventLogRequest(ModbusRequest): ''' - This function code is used to get a status word, event count, message count, - and a field of event bytes from the remote device. + This function code is used to get a status word, event count, message + count, and a field of event bytes from the remote device. - The status word and event counts are identical to that returned by the - Get Communications Event Counter function (11, 0B hex). + The status word and event counts are identical to that returned by + the Get Communications Event Counter function (11, 0B hex). The message counter contains the quantity of messages processed by the - remote device since its last restart, clear counters operation, or power-up. - This count is identical to that returned by the Diagnostic function - (code 08), sub-function Return Bus Message Count (code 11, 0B hex). - - The event bytes field contains 0-64 bytes, with each byte corresponding to - the status of one MODBUS send or receive operation for the remote device. - The remote device enters the events into the field in chronological order. - Byte 0 is the most recent event. Each new byte flushes the oldest byte - from the field. + remote device since its last restart, clear counters operation, or + power-up. This count is identical to that returned by the Diagnostic + function (code 08), sub-function Return Bus Message Count (code 11, + 0B hex). + + The event bytes field contains 0-64 bytes, with each byte corresponding + to the status of one MODBUS send or receive operation for the remote + device. The remote device enters the events into the field in + chronological order. Byte 0 is the most recent event. Each new byte + flushes the oldest byte from the field. ''' function_code = 0x0c _rtu_frame_size = 4 @@ -260,12 +267,13 @@ def __str__(self): ''' return "GetCommEventLogRequest(%d)" % self.function_code + class GetCommEventLogResponse(ModbusResponse): ''' The normal response contains a two-byte status word field, a two-byte event count field, a two-byte message count field, - and a field containing 0-64 bytes of events. A byte count field - defines the total length of the data in these four field + and a field containing 0-64 bytes of events. A byte count + field defines the total length of the data in these four field ''' function_code = 0x0c _rtu_byte_count_pos = 3 @@ -308,7 +316,7 @@ def decode(self, data): self.message_count = struct.unpack('>H', data[5:7])[0] self.events = [] - for e in xrange(7, length+1): + for e in xrange(7, length + 1): self.events.append(struct.unpack('>B', data[e])[0]) def __str__(self): @@ -319,13 +327,14 @@ def __str__(self): arguments = (self.function_code, self.status, self.message_count, self.event_count) return "GetCommEventLogResponse(%d, %d, %d, %d)" % arguments + #---------------------------------------------------------------------------# # TODO Make these only work on serial #---------------------------------------------------------------------------# class ReportSlaveIdRequest(ModbusRequest): ''' - This function code is used to read the description of the type, the current - status, and other information specific to a remote device. + This function code is used to read the description of the type, the + current status, and other information specific to a remote device. ''' function_code = 0x11 _rtu_frame_size = 4 @@ -362,10 +371,11 @@ def __str__(self): ''' return "ResportSlaveIdRequest(%d)" % self.function_code + class ReportSlaveIdResponse(ModbusResponse): ''' - The format of a normal response is shown in the following example. The - data contents are specific to each type of device. + The format of a normal response is shown in the following example. + The data contents are specific to each type of device. ''' function_code = 0x11 _rtu_byte_count_pos = 2 @@ -388,7 +398,7 @@ def encode(self): status = ModbusStatus.SlaveOn if self.status else ModbusStatus.SlaveOff length = len(self.identifier) + 2 packet = struct.pack('>B', length) - packet += self.identifier # we assume it is already encoded + packet += self.identifier # we assume it is already encoded packet += struct.pack('>B', status) return packet @@ -401,7 +411,7 @@ def decode(self, data): :param data: The packet data to decode ''' length = struct.unpack('>B', data[0])[0] - self.identifier = data[1:length-1] + self.identifier = data[1:length - 1] status = struct.unpack('>B', data[-1])[0] self.status = status == ModbusStatus.SlaveOn @@ -418,9 +428,9 @@ def __str__(self): #---------------------------------------------------------------------------# # report device identification 43, 14 -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ "ReadExceptionStatusRequest", "ReadExceptionStatusResponse", "GetCommEventCounterRequest", "GetCommEventCounterResponse", diff --git a/pymodbus/pdu.py b/pymodbus/pdu.py index 67d01cea0..6c21b8f00 100644 --- a/pymodbus/pdu.py +++ b/pymodbus/pdu.py @@ -22,24 +22,25 @@ class ModbusPDU(object): .. attribute:: transaction_id - This value is used to uniquely identify a request - response pair. It can be implemented as a simple counter + This value is used to uniquely identify a request + response pair. It can be implemented as a simple counter .. attribute:: protocol_id - This is a constant set at 0 to indicate Modbus. It is - put here for ease of expansion. + This is a constant set at 0 to indicate Modbus. It is + put here for ease of expansion. .. attribute:: unit_id - - This is used to route the request to the correct child. In - the TCP modbus, it is used for routing (or not used at all. However, for - the serial versions, it is used to specify which child to perform the - requests against. The value 0x00 represents the broadcast address (also 0xff). + + This is used to route the request to the correct child. In + the TCP modbus, it is used for routing (or not used at all. However, + for the serial versions, it is used to specify which child to perform + the requests against. The value 0x00 represents the broadcast address + (also 0xff). .. attribute:: check - - This is used for LRC/CRC in the serial modbus protocols + + This is used for LRC/CRC in the serial modbus protocols ''' def __init__(self, **kwargs): @@ -101,12 +102,12 @@ class ModbusResponse(ModbusPDU): ''' Base class for a modbus response PDU .. attribute:: should_respond - + A flag that indicates if this response returns a result back to the client issuing the request .. attribute:: _rtu_frame_size - + Indicates the size of the modbus rtu response used for calculating how much to read. ''' diff --git a/pymodbus/register_read_message.py b/pymodbus/register_read_message.py index baa246eaa..b1190d2fb 100644 --- a/pymodbus/register_read_message.py +++ b/pymodbus/register_read_message.py @@ -7,6 +7,7 @@ from pymodbus.pdu import ModbusResponse from pymodbus.pdu import ModbusExceptions as merror + class ReadRegistersRequestBase(ModbusRequest): ''' Base class for reading a modbus register @@ -44,6 +45,7 @@ def __str__(self): ''' return "ReadRegisterRequest (%d,%d)" % (self.address, self.count) + class ReadRegistersResponseBase(ModbusResponse): ''' Base class for responsing to a modbus register read @@ -77,7 +79,7 @@ def decode(self, data): byte_count = ord(data[0]) self.registers = [] for i in range(1, byte_count + 1, 2): - self.registers.append(struct.unpack('>H', data[i:i+2])[0]) + self.registers.append(struct.unpack('>H', data[i:i + 2])[0]) def getRegister(self, index): ''' Get the requested register @@ -126,6 +128,7 @@ def execute(self, context): values = context.getValues(self.function_code, self.address, self.count) return ReadHoldingRegistersResponse(values) + class ReadHoldingRegistersResponse(ReadRegistersResponseBase): ''' This function code is used to read the contents of a contiguous block @@ -143,6 +146,7 @@ def __init__(self, values=None, **kwargs): ''' ReadRegistersResponseBase.__init__(self, values, **kwargs) + class ReadInputRegistersRequest(ReadRegistersRequestBase): ''' This function code is used to read from 1 to approx. 125 contiguous @@ -174,6 +178,7 @@ def execute(self, context): values = context.getValues(self.function_code, self.address, self.count) return ReadInputRegistersResponse(values) + class ReadInputRegistersResponse(ReadRegistersResponseBase): ''' This function code is used to read from 1 to approx. 125 contiguous @@ -191,6 +196,7 @@ def __init__(self, values=None, **kwargs): ''' ReadRegistersResponseBase.__init__(self, values, **kwargs) + class ReadWriteMultipleRegistersRequest(ModbusRequest): ''' This function code performs a combination of one read operation and one @@ -250,7 +256,7 @@ def decode(self, data): self.write_byte_count = struct.unpack('>HHHHB', data[:9]) self.write_registers = [] for i in range(9, self.write_byte_count + 9, 2): - register = struct.unpack('>H', data[i:i+2])[0] + register = struct.unpack('>H', data[i:i + 2])[0] self.write_registers.append(register) def execute(self, context): @@ -286,6 +292,7 @@ def __str__(self): self.write_count) return "ReadWriteNRegisterRequest R(%d,%d) W(%d,%d)" % params + class ReadWriteMultipleRegistersResponse(ModbusResponse): ''' The normal response contains the data from the group of registers that @@ -294,7 +301,7 @@ class ReadWriteMultipleRegistersResponse(ModbusResponse): ''' function_code = 23 _rtu_byte_count_pos = 2 - + def __init__(self, values=None, **kwargs): ''' Initializes a new instance @@ -308,7 +315,7 @@ def encode(self): :returns: The encoded packet ''' - result = chr(len(self.registers)*2) + result = chr(len(self.registers) * 2) for register in self.registers: result += struct.pack('>H', register) return result @@ -320,7 +327,7 @@ def decode(self, data): ''' bytes = ord(data[0]) for i in range(1, bytes, 2): - self.registers.append(struct.unpack('>H', data[i:i+2])[0]) + self.registers.append(struct.unpack('>H', data[i:i + 2])[0]) def __str__(self): ''' Returns a string representation of the instance @@ -329,9 +336,9 @@ def __str__(self): ''' return "ReadWriteNRegisterResponse (%d)" % len(self.registers) -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ "ReadHoldingRegistersRequest", "ReadHoldingRegistersResponse", "ReadInputRegistersRequest", "ReadInputRegistersResponse", diff --git a/pymodbus/register_write_message.py b/pymodbus/register_write_message.py index 22d3883a8..74d56753a 100644 --- a/pymodbus/register_write_message.py +++ b/pymodbus/register_write_message.py @@ -7,6 +7,7 @@ from pymodbus.pdu import ModbusResponse from pymodbus.pdu import ModbusExceptions as merror + class WriteSingleRegisterRequest(ModbusRequest): ''' This function code is used to write a single holding register in a @@ -65,6 +66,7 @@ def __str__(self): ''' return "WriteRegisterRequest %d => %d" % (self.address, self.value) + class WriteSingleRegisterResponse(ModbusResponse): ''' The normal response is an echo of the request, returned after the @@ -105,10 +107,10 @@ def __str__(self): params = (self.address, self.value) return "WriteRegisterResponse %d => %d" % params + #---------------------------------------------------------------------------# # Write Multiple Registers #---------------------------------------------------------------------------# - class WriteMultipleRegistersRequest(ModbusRequest): ''' This function code is used to write a block of contiguous registers (1 @@ -149,11 +151,10 @@ def decode(self, data): :param data: The request to decode ''' - self.address, self.count, self.byte_count = struct.unpack('>HHB', - data[:5]) - self.values = [] # reset + self.address, self.count, self.byte_count = struct.unpack('>HHB', data[:5]) + self.values = [] # reset for idx in range(5, (self.count * 2) + 5, 2): - self.values.append(struct.unpack('>H', data[idx:idx+2])[0]) + self.values.append(struct.unpack('>H', data[idx:idx + 2])[0]) def execute(self, context): ''' Run a write single register request against a datastore @@ -179,6 +180,7 @@ def __str__(self): params = (self.address, self.count) return "WriteMultipleRegisterRequest %d => %d" % params + class WriteMultipleRegistersResponse(ModbusResponse): ''' "The normal response returns the function code, starting address, and @@ -219,9 +221,9 @@ def __str__(self): params = (self.address, self.count) return "WriteMultipleRegisterResponse (%d,%d)" % params -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ "WriteSingleRegisterRequest", "WriteSingleRegisterResponse", "WriteMultipleRegistersRequest", "WriteMultipleRegistersResponse", diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index bc8e42379..d00124b1d 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -285,6 +285,7 @@ def server_close(self): _logger.debug("Modbus server stopped") self.socket.close() + #---------------------------------------------------------------------------# # Creation Factories #---------------------------------------------------------------------------# @@ -298,6 +299,7 @@ def StartTcpServer(context=None, identity=None): server = ModbusTcpServer(context, framer, identity) server.serve_forever() + def StartUdpServer(context=None, identity=None): ''' A factory to start and run a udp modbus server @@ -308,6 +310,7 @@ def StartUdpServer(context=None, identity=None): server = ModbusUdpServer(context, framer, identity) server.serve_forever() + def StartSerialServer(context=None, identity=None, **kwargs): ''' A factory to start and run a udp modbus server @@ -318,9 +321,9 @@ def StartSerialServer(context=None, identity=None, **kwargs): server = ModbusSerialServer(context, framer, identity, **kwargs) server.serve_forever() -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ "StartTcpServer", "StartUdpServer", "StartSerialServer" ] diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 5c750f2ac..9929aa85c 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -16,6 +16,7 @@ import logging _logger = logging.getLogger(__name__) + #---------------------------------------------------------------------------# # The Global Transaction Manager #---------------------------------------------------------------------------# @@ -31,7 +32,7 @@ class ModbusTransactionManager(Singleton): count++ else break while (count < 3) - + This module helps to abstract this away from the framer and protocol. ''' @@ -63,7 +64,7 @@ def execute(self, request): def addTransaction(self, request): ''' Adds a transaction to the handler - + This holds the requets in case it needs to be resent. After being sent, the request is removed. @@ -78,7 +79,7 @@ def getTransaction(self, tid): :param tid: The transaction to retrieve ''' - for k,v in enumerate(ModbusTransactionManager.__transactions): + for k, v in enumerate(ModbusTransactionManager.__transactions): if v.transaction_id == tid: return ModbusTransactionManager.__transactions.pop(k) return None @@ -88,13 +89,13 @@ def delTransaction(self, tid): :param tid: The transaction to remove ''' - for k,v in enumerate(ModbusTransactionManager.__transactions): + for k, v in enumerate(ModbusTransactionManager.__transactions): if v.transaction_id == tid: del ModbusTransactionManager.__transactions[k] def getNextTID(self): ''' Retrieve the next unique transaction identifier - + This handles incrementing the identifier after retrieval @@ -108,6 +109,7 @@ def resetTID(self): ''' Resets the transaction identifier ''' ModbusTransactionManager.__tid = Defaults.TransactionId + #---------------------------------------------------------------------------# # Modbus TCP Message #---------------------------------------------------------------------------# @@ -116,16 +118,16 @@ class ModbusSocketFramer(IModbusFramer): Before each modbus TCP message is an MBAP header which is used as a message frame. It allows us to easily separate messages as follows:: - + [ MBAP Header ] [ Function Code] [ Data ] [ tid ][ pid ][ length ][ uid ] 2b 2b 2b 1b 1b Nb - + while len(message) > 0: tid, pid, length`, uid = struct.unpack(">HHHB", message) request = message[0:7 + length - 1`] message = [7 + length - 1:] - + * length = uid + function code + data * The -1 is to account for the uid byte ''' @@ -233,7 +235,7 @@ def processIncomingPacket(self, data, callback): raise ModbusIOException("Unable to decode request") self.populateResult(result) self.advanceFrame() - callback(result) # defer or push to a thread? + callback(result) # defer or push to a thread? else: break def buildPacket(self, message): @@ -250,6 +252,7 @@ def buildPacket(self, message): message.function_code) + data return packet + #---------------------------------------------------------------------------# # Modbus RTU Message #---------------------------------------------------------------------------# @@ -259,13 +262,13 @@ class ModbusRtuFramer(IModbusFramer): [ Start Wait ] [Address ][ Function Code] [ Data ][ CRC ][ End Wait ] 3.5 chars 1b 1b Nb 2b 3.5 chars - + Wait refers to the amount of time required to transmist at least x many characters. In this case it is 3.5 characters. Also, if we recieve a wait of 1.5 characters at any point, we must trigger an error message. Also, it appears as though this message is little endian. The logic is simplified as the following:: - + block-on-read: read until 3.5 delay check for errors @@ -273,7 +276,7 @@ class ModbusRtuFramer(IModbusFramer): The following table is a listing of the baud wait times for the specified baud rates:: - + ------------------------------------------------------------------ Baud 1.5c (18 bits) 3.5c (38 bits) ------------------------------------------------------------------ @@ -337,7 +340,7 @@ def isFrameReady(self): def populateHeader(self): ''' Try to set the headers `uid`, `len` and `crc`. - + This method examines `self.__buffer` and writes meta information into `self.__header`. It calculates only the values for headers that are not already in the dictionary. @@ -353,7 +356,7 @@ def populateHeader(self): size = pdu_class.calculateRtuFrameSize(self.__buffer) self.__header['len'] = size if 'crc' not in self.__header: - self.__header['crc'] = self.__buffer[size-2:size] + self.__header['crc'] = self.__buffer[size - 2:size] def addToFrame(self, message): ''' @@ -376,7 +379,7 @@ def getFrame(self): def populateResult(self, result): ''' Populates the modbus result header - + The serial packets do not have any header information that is copied. @@ -410,7 +413,7 @@ def processIncomingPacket(self, data, callback): raise ModbusIOException("Unable to decode response") self.populateResult(result) self.advanceFrame() - callback(result) # defer or push to a thread? + callback(result) # defer or push to a thread? else: break def buildPacket(self, message): @@ -425,21 +428,22 @@ def buildPacket(self, message): packet += struct.pack(">H", computeCRC(packet)) return packet + #---------------------------------------------------------------------------# # Modbus ASCII Message #---------------------------------------------------------------------------# class ModbusAsciiFramer(IModbusFramer): ''' Modbus ASCII Frame Controller:: - + [ Start ][Address ][ Function ][ Data ][ LRC ][ End ] 1c 2c 2c Nc 2c 2c - + * data can be 0 - 2x252 chars * end is '\\r\\n' (Carriage return line feed), however the line feed character can be changed via a special command * start is ':' - + This framer is used for serial transmission. Unlike the RTU protocol, the data in this framer is transferred in plain text ascii. ''' @@ -466,15 +470,15 @@ def checkFrame(self): ''' start = self.__buffer.find(self.__start) if start == -1: return False - if start > 0 : # go ahead and skip old bad data + if start > 0 : # go ahead and skip old bad data self.__buffer = self.__buffer[start:] end = self.__buffer.find(self.__end) if (end != -1): self.__header['len'] = end self.__header['uid'] = int(self.__buffer[1:3], 16) - self.__header['lrc'] = int(self.__buffer[end-2:end], 16) - data = self.__buffer[start:end-2] + self.__header['lrc'] = int(self.__buffer[end - 2:end], 16) + data = self.__buffer[start:end - 2] return checkLRC(data, self.__header['lrc']) return False @@ -517,7 +521,7 @@ def getFrame(self): def populateResult(self, result): ''' Populates the modbus result header - + The serial packets do not have any header information that is copied. @@ -551,7 +555,7 @@ def processIncomingPacket(self, data, callback): raise ModbusIOException("Unable to decode response") self.populateResult(result) self.advanceFrame() - callback(result) # defer this + callback(result) # defer this else: break def buildPacket(self, message): @@ -570,6 +574,7 @@ def buildPacket(self, message): packet = '%c%s%02x%s' % (self.__start, packet, checksum, self.__end) return packet.upper() + #---------------------------------------------------------------------------# # Modbus Binary Message #---------------------------------------------------------------------------# @@ -606,8 +611,8 @@ def __init__(self, decoder): self.__buffer = '' self.__header = {'crc':0x0000, 'len':0, 'uid':0x00} self.__hsize = 0x02 - self.__start = '\x7b' # { - self.__end = '\x7d' # } + self.__start = '\x7b' # { + self.__end = '\x7d' # } self.decoder = decoder #-----------------------------------------------------------------------# @@ -620,15 +625,15 @@ def checkFrame(self): ''' start = self.__buffer.find(self.__start) if start == -1: return False - if start > 0 : # go ahead and skip old bad data + if start > 0 : # go ahead and skip old bad data self.__buffer = self.__buffer[start:] end = self.__buffer.find(self.__end) if (end != -1): self.__header['len'] = end self.__header['uid'] = struct.unpack('>B', self.__buffer[1:2]) - self.__header['crc'] = struct.unpack('>H', self.__buffer[end-2:end])[0] - data = self.__buffer[start:end-2] + self.__header['crc'] = struct.unpack('>H', self.__buffer[end - 2:end])[0] + data = self.__buffer[start:end - 2] return checkCRC(data, self.__header['crc']) return False @@ -671,7 +676,7 @@ def getFrame(self): def populateResult(self, result): ''' Populates the modbus result header - + The serial packets do not have any header information that is copied. @@ -705,7 +710,7 @@ def processIncomingPacket(self, data, callback): raise ModbusIOException("Unable to decode response") self.populateResult(result) self.advanceFrame() - callback(result) # defer or push to a thread? + callback(result) # defer or push to a thread? else: break def buildPacket(self, message): @@ -731,12 +736,12 @@ def _preflight(self, data): :returns: the escaped packet ''' def _filter(a): - return a*2 if a in ['}', '{'] else a, data + return a * 2 if a in ['}', '{'] else a, data return ''.join(map(_filter, data)) -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ "ModbusTransactionManager", "ModbusSocketFramer", "ModbusRtuFramer", diff --git a/pymodbus/utilities.py b/pymodbus/utilities.py index b315dbfad..00eb73c49 100644 --- a/pymodbus/utilities.py +++ b/pymodbus/utilities.py @@ -7,6 +7,7 @@ ''' import struct + #---------------------------------------------------------------------------# # Helpers #---------------------------------------------------------------------------# @@ -20,6 +21,7 @@ def default(value): ''' return type(value)() + def dict_property(store, index): ''' Helper to create class properties from a dictionary. Basically this allows you to remove a lot of possible @@ -31,16 +33,18 @@ def dict_property(store, index): ''' if hasattr(store, '__call__'): get = lambda self: store(self)[index] - set = lambda self,value: store(self).__setitem__(index, value) + set = lambda self, value: store(self).__setitem__(index, value) elif isinstance(store, str): get = lambda self: self.__getattribute__(store)[index] - set = lambda self,value: self.__getattribute__(store).__setitem__(index, value) + set = lambda self, value: self.__getattribute__(store).__setitem__( + index, value) else: get = lambda self: store[index] - set = lambda self,value: store.__setitem__(index, value) + set = lambda self, value: store.__setitem__(index, value) return property(get, set) + #---------------------------------------------------------------------------# # Bit packing functions #---------------------------------------------------------------------------# @@ -64,10 +68,11 @@ def pack_bitstring(bits): i = packed = 0 else: packed >>= 1 if i > 0 and i < 8: - packed >>= 7-i + packed >>= (7 - i) ret += chr(packed) return ret + def unpack_bitstring(string): ''' Creates bit array out of a string @@ -87,6 +92,7 @@ def unpack_bitstring(string): value >>= 1 return bits + #---------------------------------------------------------------------------# # Error Detection Functions #---------------------------------------------------------------------------# @@ -108,6 +114,7 @@ def __generate_crc16_table(): __crc16_table = __generate_crc16_table() + def computeCRC(data): ''' Computes a crc16 on the passed in string. For modbus, this is only used on the binary serial protocols (in this @@ -126,6 +133,7 @@ def computeCRC(data): swapped = ((crc << 8) & 0xff00) | ((crc >> 8) & 0x00ff) return swapped + def checkCRC(data, check): ''' Checks if the data matches the passed in CRC @@ -135,6 +143,7 @@ def checkCRC(data, check): ''' return computeCRC(data) == check + def computeLRC(data): ''' Used to compute the longitudinal redundancy check against a string. This is only used on the serial ASCII @@ -150,6 +159,7 @@ def computeLRC(data): lrc = (lrc ^ 0xff) + 1 return lrc & 0xff + def checkLRC(data, check): ''' Checks if the passed in data matches the LRC @@ -159,6 +169,7 @@ def checkLRC(data, check): ''' return computeLRC(data) == check + def rtuFrameSize(buffer, byte_count_pos): ''' Calculates the size of the frame based on the byte count. @@ -181,9 +192,9 @@ def rtuFrameSize(buffer, byte_count_pos): ''' return struct.unpack('>B', buffer[byte_count_pos])[0] + byte_count_pos + 3 -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = [ 'pack_bitstring', 'unpack_bitstring', 'default', 'computeCRC', 'checkCRC', 'computeLRC', 'checkLRC', 'rtuFrameSize' diff --git a/pymodbus/version.py b/pymodbus/version.py index 265a2c730..86af08be4 100644 --- a/pymodbus/version.py +++ b/pymodbus/version.py @@ -5,6 +5,8 @@ Since we are using twisted's version class, we can also query the svn version as well using the local .entries file. ''' + + class Version(object): def __init__(self, package, major, minor, micro): @@ -34,9 +36,9 @@ def __str__(self): return '[%s, version %s]' % (self.package, self.short()) version = Version('pymodbus', 0, 9, 0) -version.__name__ = 'pymodbus' # fix epydoc error +version.__name__ = 'pymodbus' # fix epydoc error -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# # Exported symbols -#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# __all__ = ["version"] From a2a7b8a86151eae3154e10a49a757327068ac708 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 11 May 2011 13:55:36 +0000 Subject: [PATCH 014/243] working on py3 merge --- doc/current.coverage | 29 +++ doc/sphinx/client.rst | 51 ++++ doc/sphinx/library/datastore.rst | 26 ++ doc/sphinx/logging.rst | 28 +++ examples/build-data.py | 153 ++++++++++++ examples/convert.py | 145 ++++++++++++ examples/modbus-scraper.py | 156 ++++++++++++ examples/modbus-simulator.py | 128 ++++++++++ examples/server.py | 38 +++ examples/tools/reindent.py | 4 +- pymodbus/datastore.py | 395 +++++++++++++++++++++++++++++++ pymodbus/file_message.py | 2 +- pymodbus/other_message.py | 2 +- test/test_bit_messages.py | 161 +++++++++++++ test/test_bit_read_messages.py | 6 +- test/test_register_messages.py | 111 +++++++++ test/test_server_context.py | 8 +- 17 files changed, 1432 insertions(+), 11 deletions(-) create mode 100644 doc/current.coverage create mode 100644 doc/sphinx/client.rst create mode 100644 doc/sphinx/library/datastore.rst create mode 100644 doc/sphinx/logging.rst create mode 100644 examples/build-data.py create mode 100644 examples/convert.py create mode 100644 examples/modbus-scraper.py create mode 100644 examples/modbus-simulator.py create mode 100644 examples/server.py create mode 100644 pymodbus/datastore.py create mode 100644 test/test_bit_messages.py create mode 100644 test/test_register_messages.py diff --git a/doc/current.coverage b/doc/current.coverage new file mode 100644 index 000000000..ff911a104 --- /dev/null +++ b/doc/current.coverage @@ -0,0 +1,29 @@ +Name Stmts Exec Cover Missing +--------------------------------------------------------------- +pymodbus 9 9 100% +pymodbus.bit_read_message 64 64 100% +pymodbus.bit_write_message 88 62 70% 73-81, 88, 129, 157, 174-176, 184-192, 199, 230, 237 +pymodbus.client 1 1 100% +pymodbus.client.async 86 32 37% 55-61, 67-72, 77-78, 87-90, 104-105, 112-113, 121-124, 130, 160-172, 183, 190-194, 201, 214-226 +pymodbus.constants 18 18 100% +pymodbus.datastore 99 60 60% 81-82, 86, 95, 104, 112, 119, 126-128, 141, 152-154, 163-164, 172-173, 186-192, 201-202, 211, 219-220, 260, 264-265, 275-276, 286-287, 296-297 +pymodbus.device 112 112 100% +pymodbus.diag_message 183 168 91% 49, 62, 104, 131, 165-168, 175, 190-193, 212, 242 +pymodbus.exceptions 22 22 100% +pymodbus.factory 52 46 88% 56-58, 107-109 +pymodbus.file_message 36 33 91% 45, 52, 62 +pymodbus.interfaces 31 21 67% 36, 56, 70, 78, 88, 98, 105, 115, 132, 143 +pymodbus.other_message 73 40 54% 29, 34, 41, 48-49, 66-67, 74, 81, 111, 116, 123, 130-131, 148-150, 157-158, 165-166, 186, 191, 198, 205-206, 221-223, 230-231, 238-239 +pymodbus.pdu 58 58 100% +pymodbus.register_read_message 119 93 78% 45, 86, 93, 124-125, 172-173, 223, 244-249, 262-270, 277, 311-313, 320 +pymodbus.register_write_message 79 54 68% 51-57, 64, 102, 128, 146-148, 156-164, 171, 202, 209 +pymodbus.server 0 0 100% +pymodbus.transaction 218 130 59% 48-60, 225-235, 315-316, 334, 341, 371-380, 387-392, 510-519, 566-570, 580-593, 601-602, 611, 620, 627, 637, 657-666, 674-679, 690-692 +pymodbus.utilities 53 53 100% +pymodbus.version 4 4 100% +--------------------------------------------------------------- +TOTAL 1405 1080 76% +---------------------------------------------------------------------- +Ran 85 tests in 0.136s + +OK diff --git a/doc/sphinx/client.rst b/doc/sphinx/client.rst new file mode 100644 index 000000000..3f2a67118 --- /dev/null +++ b/doc/sphinx/client.rst @@ -0,0 +1,51 @@ +================================= +Implementation of a Modbus Client +================================= + +This attempts to fire off requets in succession so as to work as fast as +possible, but still refrain from overloading the remote device (usually +very mediocre in hardware) + +Example Run:: + + def clientTest(): + requests = [ ReadCoilsRequest(0,99) ] + p = reactor.connectTCP("localhost", 502, ModbusClientFactory(requests)) + + if __name__ == "__main__": + reactor.callLater(1, clientTest) + reactor.run() + +What follows is a quick layout of the client logic: + 1. Build request array and instantiate a client factory + 2. Defer it until the reactor is running + 3. Upon connection, instantiate the producer and pass it: + + * A handle to the transport + * A handle to the request array + * A handle to a sent request handler + * A handle to the current framing object + + 4. It then sends a request and waits + 5. The protocol recieves data and processes its frame: + + * If we have a valid frame, we decode it and add the result(7) + * Otherwise we continue(6) + + 6. Afterwards, we instruct the producer to send the next request + 7. Upon adding a result: + + * The factory uses the handler object to translate the TID to a request + * Using the request paramaters, we corretly store the resulting data + * Each result is put into the appropriate store + + 7. When all the requests have been processed: + + * we stop the producer + * disconnect the protocol + * return the factory results + +Todo: + * Build a repeated request producer? + * Simplify request <-> response linking + diff --git a/doc/sphinx/library/datastore.rst b/doc/sphinx/library/datastore.rst new file mode 100644 index 000000000..557d86085 --- /dev/null +++ b/doc/sphinx/library/datastore.rst @@ -0,0 +1,26 @@ +:mod:`datastore` --- Datastore for Modbus Server Context +============================================================ + +.. module:: datastore + :synopsis: Datastore for Modbus Server Context + +.. moduleauthor:: Galen Collins +.. sectionauthor:: Galen Collins + +API Documentation +------------------- + +.. automodule:: pymodbus.datastore + +.. autoclass:: ModbusDataBlock + :members: + +.. autoclass:: ModbusSequentialDataBlock + :members: + +.. autoclass:: ModbusSparseDataBlock + :members: + +.. autoclass:: ModbusServerContext + :members: + diff --git a/doc/sphinx/logging.rst b/doc/sphinx/logging.rst new file mode 100644 index 000000000..992e232e4 --- /dev/null +++ b/doc/sphinx/logging.rst @@ -0,0 +1,28 @@ +=================== +Logging in PyModbus +=================== + +Use the following example as start to enable logging in pymodbus:: + + import logging + + # Get handles to the various logs + server_log = logging.getLogger("pysnmp.server") + client_log = logging.getLogger("pysnmp.client") + protocol_log = logging.getLogger("pysnmp.protocol") + store_log = logging.getLogger("pysnmp.store") + + # Enable logging levels + server_log.setLevel(logging.DEBUG) + protocol_log.setLevel(logging.DEBUG) + client_log.setLevel(logging.DEBUG) + store_log.setLevel(logging.DEBUG) + + # Initialize the logging + try: + logging.basicConfig() + except Exception, e: + print "Logging is not supported on this system" + +This can be included in a working project as a separate module +and then used by the rest of the project. diff --git a/examples/build-data.py b/examples/build-data.py new file mode 100644 index 000000000..f5d67420b --- /dev/null +++ b/examples/build-data.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python +''' +This creates a dummy datastore for use with the modbus simulator. + +It is also used to convert datastores to and from a register list +dump. This allows users to build their own data from scratch or +modifiy an exisiting dump. +''' +import pickle +from sys import exit +from optparse import OptionParser + +from pymodbus.datastore import ModbusSequentialDataBlock as seqblock +from pymodbus.datastore import ModbusSparseDataBlock as sparblock + +#--------------------------------------------------------------------------# +# Helper Classes +#--------------------------------------------------------------------------# +class ConfigurationException(Exception): + ''' Exception for configuration error ''' + + def __init__(self, string): + ''' A base string to make pylint happy + :param string: Additional information to append to exception + ''' + Exception.__init__(self, string) + self.string = string + + def __str__(self): + return 'Configuration Error: %s' % self.string + +#--------------------------------------------------------------------------# +# Datablock Builders +#--------------------------------------------------------------------------# +def build_translation(option, opt, value, parser): + ''' Converts a register dump list to a pickeld datastore + + :param option: The option instance + :param opt: The option string specified + :param value: The file to translate + :param parser: The parser object + ''' + raise ConfigurationException("This function is not implemented yet") + try: + with open(value, "r") as input: + data = pickle.load(input) + except: + raise ConfigurationException("File Not Found %s" % value) + + with open(value + ".trans", "w") as output: + pass # TODO + exit() # So we don't start a dummy build + +def build_conversion(option, opt, value, parser): + ''' This converts a pickled datastore to a register dump list + + :param option: The option instance + :param opt: The option string specified + :param value: The file to convert + :param parser: The parser object + ''' + try: + with open(value, "r") as input: + data = pickle.load(input) + except: + raise ConfigurationException("File Not Found %s" % value) + + with open(value + ".dump", "w") as output: + for dk,dv in data.items(): + output.write("[ %s ]\n\n" % dk) + + # handle sequential + if isinstance(dv.values, list): + output.write("\n".join(["[%d] = %d" % (vk,vv) + for vk,vv in enumerate(dv.values)])) + + # handle sparse + elif isinstance(data[k].values, dict): + output.write("\n".join(["[%d] = %d" % (vk,vv) + for vk,vv in dv.values.items()])) + else: raise ConfigurationException("Datastore is corrupted %s" % value) + output.write("\n\n") + exit() # So we don't start a dummy build + +#--------------------------------------------------------------------------# +# Datablock Builders +#--------------------------------------------------------------------------# +def build_sequential(): + ''' + This builds a quick mock sequential datastore with 100 values for each + discrete, coils, holding, and input bits/registers. + ''' + data = { + 'di' : seqblock(0, [bool(x) for x in range(1, 100)]), + 'ci' : seqblock(0, [bool(not x) for x in range(1, 100)]), + 'hr' : seqblock(0, [int(x) for x in range(1, 100)]), + 'ir' : seqblock(0, [int(2*x) for x in range(1, 100)]), + } + return data + +def build_sparse(): + ''' + This builds a quick mock sparse datastore with 100 values for each + discrete, coils, holding, and input bits/registers. + ''' + data = { + 'di' : sparblock([bool(x) for x in range(1, 100)]), + 'ci' : sparblock([bool(not x) for x in range(1, 100)]), + 'hr' : sparblock([int(x) for x in range(1, 100)]), + 'ir' : sparblock([int(2*x) for x in range(1, 100)]), + } + return data + +def main(): + ''' The main function for this script ''' + parser = OptionParser() + parser.add_option("-o", "--output", + help="The output file to write to", + dest="file", default="example.store") + parser.add_option("-t", "--type", + help="The type of block to create (sequential,sparse)", + dest="type", default="sparse") + parser.add_option("-c", "--convert", + help="Convert a file datastore to a register dump", + type="string", + action="callback", callback=build_conversion) + parser.add_option("-r", "--restore", + help="Convert a register dump to a file datastore", + type="string", + action="callback", callback=build_translation) + try: + (opt, arg) = parser.parse_args() # so we can catch the csv callback + + if opt.type == "sparse": + result = build_sparse() + elif opt.type == "sequential": + result = build_sequential() + else: + raise ConfigurationException("Unknown block type %s" % opt.type) + + with open(opt.file, "w") as output: + pickle.dump(result, output) + print("Created datastore: %s\n" % opt.file) + + except ConfigurationException as ex: + print(ex) + parser.print_help() + +#---------------------------------------------------------------------------# +# Main jumper +#---------------------------------------------------------------------------# +if __name__ == "__main__": + main() diff --git a/examples/convert.py b/examples/convert.py new file mode 100644 index 000000000..e25964714 --- /dev/null +++ b/examples/convert.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python +''' +This script is used to convert an XML dump to a +serialized ModbusDataStore for use with the simulator. + +For more information on the windows scraper, +google for modsrape +''' + +from pymodbus.datastore import ModbusSparseDataBlock as sblock +from optparse import OptionParser +from lxml import etree +import pickle + +#--------------------------------------------------------------------------# +# Helper Classes +#--------------------------------------------------------------------------# +class ConversionException(Exception): + ''' Exception for configuration error ''' + + def __init__(self, string): + ''' Initialize a ConversionException instance + + :param string: Additional information to append to exception + ''' + Exception.__init__(self, string) + self.string = string + + def __str__(self): + ''' Builds a string representation of the object + + :returns: The string representation of the object + ''' + return 'Conversion Error: %s' % self.string + +#--------------------------------------------------------------------------# +# Lxml Parser Tree +#--------------------------------------------------------------------------# +class ModbusXML: + convert = { + 'true':True, + 'false':False, + } + lookup = { + 'InputRegisters':'ir', + 'HoldingRegisters':'hr', + 'CoilDiscretes':'ci', + 'InputDiscretes':'di' + } + + def __init__(self): + ''' + Initializer for the parser object + ''' + self.next = 0 + self.result = {'di':{}, 'ci':{}, 'ir':{}, 'hr':{}} + + def start(self, tag, attrib): + ''' + Callback for start node + @param tag The starting tag found + @param attrib Attributes dict found in the tag + ''' + if tag == "value": + try: + self.next = attrib['index'] + except KeyError: raise ConversionException("Invalid XML: index") + elif tag in self.lookup.keys(): + self.h = self.result[self.lookup[tag]] + + def end(self, tag): + ''' + Callback for end node + @param tag The end tag found + ''' + pass + + def data(self, data): + ''' + Callback for node data + @param data The data for the current node + ''' + if data in self.convert.keys(): + result = self.convert[data] + else: result = data + self.h[self.next] = data + + def comment(self, text): + ''' + Callback for node data + @param data The data for the current node + ''' + pass + + def close(self): + ''' + Callback for node data + @param data The data for the current node + ''' + return self.result + +#--------------------------------------------------------------------------# +# Helper Functions +#--------------------------------------------------------------------------# +def store_dump(result, file): + ''' + Quick function to dump a result to a pickle + @param result The resulting parsed data + ''' + result['di'] = sblock(result['di']) + result['ci'] = sblock(result['ci']) + result['hr'] = sblock(result['hr']) + result['ir'] = sblock(result['ir']) + + with open(file, "w") as input: + pickle.dump(result, input) + +def main(): + ''' + The main function for this script + ''' + parser = OptionParser() + parser.add_option("-o", "--output", + help="The output file to write to", + dest="output", default="example.store") + parser.add_option("-i", "--input", + help="File to convert to a datastore", + dest="input", default="scrape.xml") + try: + (opt, arg) = parser.parse_args() + + parser = etree.XMLParser(target = ModbusXML()) + result = etree.parse(opt.input, parser) + store_dump(result, opt.output) + print("Created datastore: %s\n" % opt.output) + + except ConversionException as ex: + print(ex) + parser.print_help() + +#---------------------------------------------------------------------------# +# Main jumper +#---------------------------------------------------------------------------# +if __name__ == "__main__": + main() diff --git a/examples/modbus-scraper.py b/examples/modbus-scraper.py new file mode 100644 index 000000000..635bc5b3c --- /dev/null +++ b/examples/modbus-scraper.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python +''' +This utility can be used to fully scrape a modbus device +and store its data as a Mobus Context for use with the +simulator. +''' + +from twisted.internet import reactor + +from pymodbus.client.async import ModbusClientFactory +from pymodbus.bit_read_message import ReadCoilsRequest +from pymodbus.bit_read_message import ReadDiscreteInputsRequest +from pymodbus.register_read_message import ReadHoldingRegistersRequest +from pymodbus.register_read_message import ReadInputRegistersRequest + +from optparse import OptionParser +import pickle + +#--------------------------------------------------------------------------# +# Logging +#--------------------------------------------------------------------------# +import logging +client_log = logging.getLogger("pymodbus.client") + +#--------------------------------------------------------------------------# +# Helper Classes +#--------------------------------------------------------------------------# +class ClientException(Exception): + ''' Exception for configuration error ''' + + def __init__(self, string): + Exception.__init__(self, string) + self.string = string + + def __str__(self): + return 'Client Error: %s' % self.string + +class ClientScraper: + ''' Exception for configuration error ''' + + def __init__(self, host, port, address): + ''' + Initializes the connection paramaters and requests + @param host The host to connect to + @param port The port the server resides on + @param address The range to read to:from + ''' + self.host = host + + if isinstance(port, int): + self.port = port + elif isinstance(port, str): + self.port = int(port) + + self.requests = [] + for rqst in [ + ReadCoilsRequest, + ReadDiscreteInputsRequest, + ReadInputRegistersRequest, + ReadHoldingRegistersRequest]: + for i in range(*[int(j) for j in address.split(':')]): + self.requests.append(rqst(i,1)) + + def start(self): + ''' + Starts the device scrape + ''' + f = ModbusClientFactory(self.requests) + self.p = reactor.connectTCP(self.host, self.port, f) + + def process(self, data): + ''' + Starts the device scrape + ''' + f = ModbusClientFactory(self.requests) + self.p = reactor.connectTCP(self.host, self.port, f) + +class ContextBuilder: + ''' + This class is used to build our server datastore + for use with the modbus simulator. + ''' + + def __init__(self, output): + ''' + Initializes the ContextBuilder and checks data values + @param file The output file for the server context + ''' + try: + self.file = open(output, "w") + except Exception: + raise ClientException("Unable to open file [%s]" % output) + + def build(self): + ''' Builds the final output store file ''' + try: + pass + result = self.makeContext() + pickle.dump(result, self.file) + print("Device successfully scraped!") + except Exception: + raise ClientException("Invalid data") + self.file.close() + reactor.stop() + + def makeContext(self): + ''' Builds the server context based on the passed in data ''' + # ModbusServerContext(d=sd, c=sc, h=sh, i=si) + return "string" + +#--------------------------------------------------------------------------# +# Main start point +#--------------------------------------------------------------------------# +def main(): + ''' Server launcher ''' + parser = OptionParser() + parser.add_option("-o", "--output", + help="The resulting output file for the scrape", + dest="file", default="output.store") + parser.add_option("-p", "--port", + help="The port to connect to", + dest="port", default="502") + parser.add_option("-s", "--server", + help="The server to scrape", + dest="host", default="localhost") + parser.add_option("-r", "--range", + help="The address range to scan", + dest="range", default="0:500") + parser.add_option("-D", "--debug", + help="Enable debug tracing", + action="store_true", dest="debug", default=False) + (opt, arg) = parser.parse_args() + + # enable debugging information + if opt.debug: + try: + client_log.setLevel(logging.DEBUG) + logging.basicConfig() + except Exception as e: + print("Logging is not supported on this system") + + # Begin scrape + try: + #ctx = ContextBuilder(opt.file) + s = ClientScraper(opt.host, opt.port, opt.range) + reactor.callWhenRunning(s.start) + reactor.run() + except ClientException as err: + print(err) + parser.print_help() + +#---------------------------------------------------------------------------# +# Main jumper +#---------------------------------------------------------------------------# +if __name__ == "__main__": + main() diff --git a/examples/modbus-simulator.py b/examples/modbus-simulator.py new file mode 100644 index 000000000..8fdd420c6 --- /dev/null +++ b/examples/modbus-simulator.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +''' +An example of creating a fully implemented modbus server +with read/write data as well as user configurable base data +''' + +import pickle +from optparse import OptionParser +from twisted.internet import reactor + +from pymodbus.server.async import StartTcpServer +from pymodbus.datastore import ModbusServerContext,ModbusSlaveContext + +#--------------------------------------------------------------------------# +# Logging +#--------------------------------------------------------------------------# +import logging +logging.basicConfig() + +server_log = logging.getLogger("pymodbus.server") +protocol_log = logging.getLogger("pymodbus.protocol") + +#---------------------------------------------------------------------------# +# Extra Global Functions +#---------------------------------------------------------------------------# +# These are extra helper functions that don't belong in a class +#---------------------------------------------------------------------------# +import getpass +def root_test(): + ''' Simple test to see if we are running as root ''' + return True # removed for the time being as it isn't portable + #return getpass.getuser() == "root" + +#--------------------------------------------------------------------------# +# Helper Classes +#--------------------------------------------------------------------------# +class ConfigurationException(Exception): + ''' Exception for configuration error ''' + + def __init__(self, string): + ''' Initializes the ConfigurationException instance + + :param string: The message to append to the exception + ''' + Exception.__init__(self, string) + self.string = string + + def __str__(self): + ''' Builds a representation of the object + + :returns: A string representation of the object + ''' + return 'Configuration Error: %s' % self.string + +class Configuration: + ''' + Class used to parse configuration file and create and modbus + datastore. + + The format of the configuration file is actually just a + python pickle, which is a compressed memory dump from + the scraper. + ''' + + def __init__(self, config): + ''' + Trys to load a configuration file, lets the file not + found exception fall through + + :param config: The pickled datastore + ''' + try: + self.file = open(config, "r") + except Exception: + raise ConfigurationException("File not found %s" % config) + + def parse(self): + ''' Parses the config file and creates a server context + ''' + handle = pickle.load(self.file) + try: # test for existance, or bomb + dsd = handle['di'] + csd = handle['ci'] + hsd = handle['hr'] + isd = handle['ir'] + except Exception: + raise ConfigurationException("Invalid Configuration") + slave = ModbusSlaveContext(d=dsd, c=csd, h=hsd, i=isd) + return ModbusServerContext(slaves=slave) + +#--------------------------------------------------------------------------# +# Main start point +#--------------------------------------------------------------------------# +def main(): + ''' Server launcher ''' + parser = OptionParser() + parser.add_option("-c", "--conf", + help="The configuration file to load", + dest="file") + parser.add_option("-D", "--debug", + help="Turn on to enable tracing", + action="store_true", dest="debug", default=False) + (opt, arg) = parser.parse_args() + + # enable debugging information + if opt.debug: + try: + server_log.setLevel(logging.DEBUG) + protocol_log.setLevel(logging.DEBUG) + except Exception as e: + print("Logging is not supported on this system") + + # parse configuration file and run + try: + conf = Configuration(opt.file) + StartTcpServer(context=conf.parse()) + except ConfigurationException as err: + print(err) + parser.print_help() + +#---------------------------------------------------------------------------# +# Main jumper +#---------------------------------------------------------------------------# +if __name__ == "__main__": + if root_test(): + main() + else: print("This script must be run as root!") + diff --git a/examples/server.py b/examples/server.py new file mode 100644 index 000000000..3ed64d629 --- /dev/null +++ b/examples/server.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python + +#---------------------------------------------------------------------------# +# the various server implementations +#---------------------------------------------------------------------------# +from pymodbus.server.sync import StartTcpServer, StartUdpServer +from pymodbus.server.sync import StartSerialServer +from pymodbus.server.async import StartTcpServer as StartATcpServer +from pymodbus.server.async import StartUdpServer as StartAUdpServer +from pymodbus.server.async import StartSerialServer as StartASerialServer + +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext + +#---------------------------------------------------------------------------# +# configure the service logging +#---------------------------------------------------------------------------# +import logging +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.DEBUG) + +#---------------------------------------------------------------------------# +# initialize your data store +#---------------------------------------------------------------------------# +store = ModbusSlaveContext( + di = ModbusSequentialDataBlock(0, [17]*100), + co = ModbusSequentialDataBlock(0, [17]*100), + hr = ModbusSequentialDataBlock(0, [17]*100), + ir = ModbusSequentialDataBlock(0, [17]*100)) +context = ModbusServerContext(slaves=store, single=True) + +#---------------------------------------------------------------------------# +# run the server you want +#---------------------------------------------------------------------------# +#StartATcpServer(context) +#StartSerialServer(context, port='/dev/ptmx') +StartSerialServer(context, port='/tmp/tty1') diff --git a/examples/tools/reindent.py b/examples/tools/reindent.py index 658c652fd..5ab88f561 100755 --- a/examples/tools/reindent.py +++ b/examples/tools/reindent.py @@ -156,7 +156,7 @@ def run(self): want = have2want.get(have, -1) if want < 0: # Then it probably belongs to the next real stmt. - for j in xrange(i+1, len(stats)-1): + for j in range(i+1, len(stats)-1): jline, jlevel = stats[j] if jlevel >= 0: if have == getlspace(lines[jline]): @@ -166,7 +166,7 @@ def run(self): # comment like this one, # in which case we should shift it like its base # line got shifted. - for j in xrange(i-1, -1, -1): + for j in range(i-1, -1, -1): jline, jlevel = stats[j] if jlevel >= 0: want = have + getlspace(after[jline-1]) - \ diff --git a/pymodbus/datastore.py b/pymodbus/datastore.py new file mode 100644 index 000000000..e9dad41d0 --- /dev/null +++ b/pymodbus/datastore.py @@ -0,0 +1,395 @@ +""" +Modbus Server Datastore +------------------------- + +For each server, you will create a ModbusServerContext and pass +in the default address space for each data access. The class +will create and manage the data. + +Further modification of said data accesses should be performed +with [get,set][access]Values(address, count) + +Datastore Implementation +------------------------- + +There are two ways that the server datastore can be implemented. +The first is a complete range from 'address' start to 'count' +number of indecies. This can be thought of as a straight array:: + + data = range(1, 1 + count) + [1,2,3,...,count] + +The other way that the datastore can be implemented (and how +many devices implement it) is a associate-array:: + + data = {1:'1', 3:'3', ..., count:'count'} + [1,3,...,count] + +The difference between the two is that the latter will allow +arbitrary gaps in its datastore while the former will not. +This is seen quite commonly in some modbus implementations. +What follows is a clear example from the field: + +Say a company makes two devices to monitor power usage on a rack. +One works with three-phase and the other with a single phase. The +company will dictate a modbus data mapping such that registers:: + + n: phase 1 power + n+1: phase 2 power + n+2: phase 3 power + +Using this, layout, the first device will implement n, n+1, and n+2, +however, the second device may set the latter two values to 0 or +will simply not implmented the registers thus causing a single read +or a range read to fail. + +I have both methods implemented, and leave it up to the user to change +based on their preference. +""" +from pymodbus.utilities import default +from pymodbus.exceptions import * +from pymodbus.interfaces import IModbusSlaveContext + +#---------------------------------------------------------------------------# +# Logging +#---------------------------------------------------------------------------# +import logging; +_logger = logging.getLogger("pymodbus.protocol") + +#---------------------------------------------------------------------------# +# Datablock Storage +#---------------------------------------------------------------------------# +class BaseModbusDataBlock(object): + ''' + Base class for a modbus datastore + + Derived classes must create the following fields: + @address The starting address point + @defult_value The default value of the datastore + @values The actual datastore values + + Derived classes must implemented the following methods: + validate(self, address, count=1) + getValues(self, address, count=1) + setValues(self, address, values) + ''' + + def default(self, count, value=False): + ''' Used to initialize a store to one value + + :param count: The number of fields to set + :param value: The default value to set to the fields + ''' + self.default_value = value + self.values = [self.default_value] * count + + def reset(self): + ''' Resets the datastore to the initialized default value ''' + self.values = [self.default_value] * len(self.values) + + def validate(self, address, count=1): + ''' Checks to see if the request is in range + + :param address: The starting address + :param count: The number of values to test for + :returns: True if the request in within range, False otherwise + ''' + raise NotImplementedException("Datastore Address Check") + + def getValues(self, address, count=1): + ''' Returns the requested values from the datastore + + :param address: The starting address + :param count: The number of values to retrieve + :returns: The requested values from a:a+c + ''' + raise NotImplementedException("Datastore Value Retrieve") + + def setValues(self, address, values): + ''' Returns the requested values from the datastore + + :param address: The starting address + :param values: The values to store + ''' + raise NotImplementedException("Datastore Value Retrieve") + + def __str__(self): + ''' Build a representation of the datastore + + :returns: A string representation of the datastore + ''' + return "DataStore(%d, %d)" % (self.address, self.default_value) + + def __iter__(self): + ''' Iterater over the data block data + + :returns: An iterator of the data block data + ''' + if isinstance(dict, self.values): + return self.values.items() + return enumerate(self.values) + +class ModbusSequentialDataBlock(BaseModbusDataBlock): + ''' Creates a sequential modbus datastore ''' + + def __init__(self, address, values): + ''' Initializes the datastore + + :param address: The starting address of the datastore + :param values: Either a list or a dictionary of values + ''' + self.address = address + if isinstance(values, list): + self.values = values + else: self.values = [values] + self.default_value = self.values[0].__class__() + + def validate(self, address, count=1): + ''' Checks to see if the request is in range + + :param address: The starting address + :param count: The number of values to test for + :returns: True if the request in within range, False otherwise + ''' + result = (self.address <= address) + result &= ((self.address + len(self.values)) >= (address + count)) + return result + + def getValues(self, address, count=1): + ''' Returns the requested values of the datastore + + :param address: The starting address + :param count: The number of values to retrieve + :returns: The requested values from a:a+c + ''' + start = address - self.address + return self.values[start:start+count] + + def setValues(self, address, values): + ''' Sets the requested values of the datastore + + :param address: The starting address + :param values: The new values to be set + ''' + start = address - self.address + self.values[start:start+len(values)] = values + +class ModbusSparseDataBlock(BaseModbusDataBlock): + ''' Creates a sparse modbus datastore ''' + + def __init__(self, values): + ''' Initializes the datastore + + Using the input values we create the default + datastore value and the starting address + + :param values: Either a list or a dictionary of values + ''' + if isinstance(values, dict): + self.values = values + elif isinstance(values, list): + self.values = dict([(i,v) for i,v in enumerate(values)]) + else: raise ParameterException("Values for datastore must be a list or dictionary") + self.default_value = default(self.values) + self.address = next(self.values.keys()) + + def validate(self, address, count=1): + ''' Checks to see if the request is in range + + :param address: The starting address + :param count: The number of values to test for + :returns: True if the request in within range, False otherwise + ''' + handle = list(range(address, address + count)) + return set(handle).issubset(set(self.values.keys())) + + def getValues(self, address, count=1): + ''' Returns the requested values of the datastore + + :param address: The starting address + :param count: The number of values to retrieve + :returns: The requested values from a:a+c + ''' + return [self.values[i] for i in range(address, address + count)] + + def setValues(self, address, values): + ''' Sets the requested values of the datastore + + :param address: The starting address + :param values: The new values to be set + ''' + for idx,val in enumerate(values): + self.values[address + idx] = val + +#---------------------------------------------------------------------------# +# Device Data Control +#---------------------------------------------------------------------------# +class ModbusSlaveContext(IModbusSlaveContext): + ''' + This creates a modbus data model with each data access + stored in its own personal block + ''' + + def __init__(self, *args, **kwargs): + ''' Initializes the datastores + :param kwargs: Each element is a ModbusDataBlock + + 'di' - Discrete Inputs initializer + 'co' - Coils initializer + 'hr' - Holding Register initializer + 'ir' - Input Registers iniatializer + ''' + self.di = kwargs.get('di', ModbusSequentialDataBlock(0, 0)) + self.co = kwargs.get('co', ModbusSequentialDataBlock(0, 0)) + self.ir = kwargs.get('ir', ModbusSequentialDataBlock(0, 0)) + self.hr = kwargs.get('hr', ModbusSequentialDataBlock(0, 0)) + self.__build_mapping() + + def __build_mapping(self): + ''' + A quick helper method to build the function + code mapper. + ''' + self.__mapping = {2:self.di, 4:self.ir} + self.__mapping.update([(i, self.hr) for i in [3, 6, 16, 23]]) + self.__mapping.update([(i, self.co) for i in [1, 5, 15]]) + + def __str__(self): + ''' Returns a string representation of the context + + :returns: A string representation of the context + ''' + return "[Slave Context]\n", [self.co, self.di, self.ir, self.hr] + + def reset(self): + ''' Resets all the datastores to their default values ''' + for datastore in [self.di, self.co, self.ir, self.hr]: + datastore.reset() + + def validate(self, fx, address, count=1): + ''' Validates the request to make sure it is in range + + :param fx: The function we are working with + :param address: The starting address + :param count: The number of values to test + :returns: True if the request in within range, False otherwise + ''' + _logger.debug("validate[%d] %d:%d" % (fx, address, count)) + return self.__mapping[fx].validate(address, count) + + def getValues(self, fx, address, count=1): + ''' Validates the request to make sure it is in range + + :param fx: The function we are working with + :param address: The starting address + :param count: The number of values to retrieve + :returns: The requested values from a:a+c + ''' + _logger.debug("getValues[%d] %d:%d" % (fx, address, count)) + return self.__mapping[fx].getValues(address, count) + + def setValues(self, fx, address, values): + ''' Sets the datastore with the supplied values + + :param fx: The function we are working with + :param address: The starting address + :param values: The new values to be set + ''' + _logger.debug("setValues[%d] %d:%d" % (fx, address,len(values))) + self.__mapping[fx].setValues(address, values) + +class ModbusRemoteSlaveContext(IModbusSlaveContext): + ''' TODO + This creates a modbus data model that connects to + a remote device (depending on the client used) + ''' + + def __init__(self, client): + ''' Initializes the datastores + + :param client: The client to retrieve values with + ''' + self.client = client + + def getValues(self, fx, address, count=1): + ''' Validates the request to make sure it is in range + + :param fx: The function we are working with + :param address: The starting address + :param count: The number of values to retrieve + :returns: The requested values from a:a+c + ''' + raise NotImplementedException("Context Reset") + + def setValues(self, fx, address, values): + ''' Sets the datastore with the supplied values + + :param fx: The function we are working with + :param address: The starting address + :param values: The new values to be set + ''' + raise NotImplementedException("Context Reset") + + def __str__(self): + ''' Returns a string representation of the context + + :returns: A string representation of the context + ''' + return "Remote Slave Context(%s)", self.client + +class ModbusServerContext(object): + ''' This represents a master collection of slave contexts. + If single is set to true, it will be treated as a single + context so every unit-id returns the same context. If single + is set to false, it will be interpreted as a collection of + slave contexts. + ''' + + def __init__(self, slaves=None, single=True): + ''' Initializes a new instance of a modbus server context. + + :param slaves: A dictionary of client contexts + :param single: Set to true to treat this as a single context + ''' + self.single = single + self.__slaves = slaves or {} + if self.single: + self.__slaves = {0x00: self.__slaves} + + def __iter__(self): + ''' Iterater over the current collection of slave + contexts. + + :returns: An iterator over the slave contexts + ''' + return self.__slaves.items() + + def __setitem__(self, slave, context): + ''' Wrapper used to access the slave context + + :param slave: slave The context to set + :param context: The new context to set for this slave + ''' + if self.single: slave = 0x00 + if 0xf7 >= slave >= 0x00: + self.__slaves[slave] = context + else: raise ParameterException('slave index out of range') + + def __getitem__(self, slave): + ''' Wrapper used to access the slave context + + :param slave: The slave context to get + :returns: The requested slave context + ''' + if self.single: slave = 0x00 + if slave in self.__slaves: + return self.__slaves.get(slave) + else: raise ParameterException("slave does not exist, or is out of range") + +#---------------------------------------------------------------------------# +# Exported symbols +#---------------------------------------------------------------------------# +__all__ = [ + "ModbusSequentialDataBlock", "ModbusSparseDataBlock", + "ModbusSlaveContext", "ModbusServerContext", +] diff --git a/pymodbus/file_message.py b/pymodbus/file_message.py index de642ae31..b90c93935 100644 --- a/pymodbus/file_message.py +++ b/pymodbus/file_message.py @@ -114,7 +114,7 @@ def decode(self, data): ''' self.values = [] length, count = struct.unpack('>HH', data[0:4]) - for index in xrange(0, count - 4): + for index in range(0, count - 4): idx = 4 + index * 2 self.values.append(struct.unpack('>H', data[idx:idx + 2])[0]) diff --git a/pymodbus/other_message.py b/pymodbus/other_message.py index f6cb90037..d082dc4c9 100644 --- a/pymodbus/other_message.py +++ b/pymodbus/other_message.py @@ -316,7 +316,7 @@ def decode(self, data): self.message_count = struct.unpack('>H', data[5:7])[0] self.events = [] - for e in xrange(7, length + 1): + for e in range(7, length + 1): self.events.append(struct.unpack('>B', data[e])[0]) def __str__(self): diff --git a/test/test_bit_messages.py b/test/test_bit_messages.py new file mode 100644 index 000000000..568de6372 --- /dev/null +++ b/test/test_bit_messages.py @@ -0,0 +1,161 @@ +''' +Bit Message Test Fixture +-------------------------------- +This fixture tests the functionality of all the +bit based request/response messages: + +* Read/Write Discretes +* Read Coils +''' +import unittest, struct +from pymodbus.utilities import packBitsToString +from pymodbus.bit_read_message import * +from pymodbus.bit_read_message import ReadBitsRequestBase +from pymodbus.bit_read_message import ReadBitsResponseBase +from pymodbus.bit_write_message import * +from pymodbus.exceptions import * +from pymodbus.pdu import ModbusExceptions + +#---------------------------------------------------------------------------# +# Mocks +#---------------------------------------------------------------------------# +class Context: + def validate(self, a,b,c): + return False + + def getValues(self, a, b, count): + return [True] * count + +#---------------------------------------------------------------------------# +# Fixture +#---------------------------------------------------------------------------# +class ModbusBitMessageTests(unittest.TestCase): + + #-----------------------------------------------------------------------# + # Setup/TearDown + #-----------------------------------------------------------------------# + + def setUp(self): + ''' + Initializes the test environment and builds request/result + encoding pairs + ''' + pass + + def tearDown(self): + ''' Cleans up the test environment ''' + pass + + #-----------------------------------------------------------------------# + # Read Tests + #-----------------------------------------------------------------------# + + def testReadBitBaseClassMethods(self): + ''' Test basic bit message encoding/decoding ''' + handle = ReadBitsRequestBase(1, 1) + msg = "ReadBitRequest(1,1)" + self.assertEqual(msg, str(handle)) + handle = ReadBitsResponseBase([1,1]) + msg = "ReadBitResponse(2)" + self.assertEqual(msg, str(handle)) + + def testBitReadBaseRequestEncoding(self): + ''' Test basic bit message encoding/decoding ''' + for i in range(20): + handle = ReadBitsRequestBase(i, i) + result = struct.pack('>HH',i, i) + self.assertEqual(handle.encode(), result) + handle.decode(result) + self.assertEqual((handle.address, handle.count), (i,i)) + + def testBitReadBaseResponseEncoding(self): + ''' Test basic bit message encoding/decoding ''' + for i in range(20): + input = [True] * i + handle = ReadBitsResponseBase(input) + result = handle.encode() + handle.decode(result) + self.assertEqual(handle.bits[:i], input) + + def testBitReadBaseResponseHelperMethods(self): + ''' Test the extra methods on a ReadBitsResponseBase ''' + input = [False] * 8 + handle = ReadBitsResponseBase(input) + for i in [1,3,5]: handle.setBit(i, True) + for i in [1,3,5]: handle.resetBit(i) + for i in range(8): + self.assertEqual(handle.getBit(i), False) + + def testBitReadBaseRequests(self): + ''' Test bit read request encoding ''' + messages = { + ReadBitsRequestBase(12, 14) : '\x00\x0c\x00\x0e', + ReadBitsResponseBase([1,0,1,1,0]) : '\x01\x0d', + } + for request, expected in messages.items(): + self.assertEqual(request.encode(), expected) + + def testBitReadMessageExecuteValueErrors(self): + ''' Test bit read request encoding ''' + context = Context() + requests = [ + ReadCoilsRequest(1,0x800), + ReadDiscreteInputsRequest(1,0x800), + ] + for request in requests: + result = request.execute(context) + self.assertEqual(ModbusExceptions.IllegalValue, + result.exception_code) + + def testBitReadMessageExecuteAddressErrors(self): + ''' Test bit read request encoding ''' + context = Context() + requests = [ + ReadCoilsRequest(1,5), + ReadDiscreteInputsRequest(1,5), + ] + for request in requests: + result = request.execute(context) + self.assertEqual(ModbusExceptions.IllegalAddress, + result.exception_code) + + def testBitReadMessageExecuteSuccess(self): + ''' Test bit read request encoding ''' + context = Context() + context.validate = lambda a,b,c: True + requests = [ + ReadCoilsRequest(1,5), + ReadDiscreteInputsRequest(1,5), + ] + for request in requests: + result = request.execute(context) + self.assertEqual(result.bits, [True] * 5) + + #-----------------------------------------------------------------------# + # Write Tests + #-----------------------------------------------------------------------# + + def testBitWriteBaseRequests(self): + ''' Test bit write request encoding ''' + messages = { + WriteSingleCoilRequest(1, 0xabcd) : '\x00\x01\xff\x00', + WriteSingleCoilResponse(1, 0xabcd) : '\x00\x01\xff\x00', + WriteMultipleCoilsRequest(1, [True]*5) : '\x00\x01\x00\x05\x01\x1f', + WriteMultipleCoilsResponse(1, 5) : '\x00\x01\x00\x05', + } + for request, expected in messages.items(): + self.assertEqual(request.encode(), expected) + + def testWriteMultipleCoilsRequest(self): + ''' Test bit write request encoding ''' + request = WriteMultipleCoilsRequest(1, [True]*5) + request.decode('\x00\x01\x00\x05\x01\x1f') + self.assertEqual(request.byte_count, 1) + self.assertEqual(request.address, 1) + self.assertEqual(request.values, [True]*5) + +#---------------------------------------------------------------------------# +# Main +#---------------------------------------------------------------------------# +if __name__ == "__main__": + unittest.main() diff --git a/test/test_bit_read_messages.py b/test/test_bit_read_messages.py index 63b43c5e0..7f794751a 100644 --- a/test/test_bit_read_messages.py +++ b/test/test_bit_read_messages.py @@ -48,7 +48,7 @@ def testReadBitBaseClassMethods(self): def testBitReadBaseRequestEncoding(self): ''' Test basic bit message encoding/decoding ''' - for i in xrange(20): + for i in range(20): handle = ReadBitsRequestBase(i, i) result = struct.pack('>HH',i, i) self.assertEqual(handle.encode(), result) @@ -57,7 +57,7 @@ def testBitReadBaseRequestEncoding(self): def testBitReadBaseResponseEncoding(self): ''' Test basic bit message encoding/decoding ''' - for i in xrange(20): + for i in range(20): input = [True] * i handle = ReadBitsResponseBase(input) result = handle.encode() @@ -70,7 +70,7 @@ def testBitReadBaseResponseHelperMethods(self): handle = ReadBitsResponseBase(input) for i in [1,3,5]: handle.setBit(i, True) for i in [1,3,5]: handle.resetBit(i) - for i in xrange(8): + for i in range(8): self.assertEqual(handle.getBit(i), False) def testBitReadBaseRequests(self): diff --git a/test/test_register_messages.py b/test/test_register_messages.py new file mode 100644 index 000000000..a271a1695 --- /dev/null +++ b/test/test_register_messages.py @@ -0,0 +1,111 @@ +''' +Register Message Test Fixture +-------------------------------- +This fixture tests the functionality of all the +register based request/response messages: + +* Read/Write Input Registers +* Read Holding Registers +''' +import unittest +from pymodbus.register_read_message import * +from pymodbus.register_read_message import ReadRegistersRequestBase +from pymodbus.register_read_message import ReadRegistersResponseBase +from pymodbus.register_write_message import * +from pymodbus.exceptions import * +from pymodbus.pdu import ModbusExceptions + +#---------------------------------------------------------------------------# +# Mocks +#---------------------------------------------------------------------------# +class Context(object): + def validate(self, a,b,c): + return False + + def getValues(self, a, b, count): + return [1] * count +#---------------------------------------------------------------------------# +# Fixture +#---------------------------------------------------------------------------# +class RegisterMessagesTest(unittest.TestCase): + + def setUp(self): + ''' + Initializes the test environment and builds request/result + encoding pairs + ''' + self.value = 0xabcd + self.values = [0xa, 0xb, 0xc] + self.rread = { + ReadRegistersRequestBase(1, 5) :'\x00\x01\x00\x05', + ReadRegistersResponseBase(self.values) :'\x06\x00\x0a\x00\x0b\x00\x0c', + ReadHoldingRegistersRequest(1, 5) :'\x00\x01\x00\x05', + ReadHoldingRegistersResponse(self.values) :'\x06\x00\x0a\x00\x0b\x00\x0c', + ReadInputRegistersRequest(1,5) :'\x00\x01\x00\x05', + ReadInputRegistersResponse(self.values) :'\x06\x00\x0a\x00\x0b\x00\x0c', + ReadWriteMultipleRegistersRequest(1,5,1,5) :'\x00\x01\x00\x05\x00\x01\x00' + '\x05\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ReadWriteMultipleRegistersResponse(self.values) :'\x06\x00\x0a\x00\x0b\x00\x0c', + } + + self.rwrite = { + WriteSingleRegisterRequest(1, self.value) : '\x00\x01\xab\xcd', + WriteSingleRegisterResponse(1, self.value) : '\x00\x01\xab\xcd', + WriteMultipleRegistersRequest(1, 5) : '\x00\x01\x00\x05\x0a\x00\x00\x00\x00\x00\x00' + '\x00\x00\x00\x00', + WriteMultipleRegistersResponse(1, 5) : '\x00\x01\x00\x05', + } + + def tearDown(self): + ''' Cleans up the test environment ''' + del self.rread + del self.rwrite + + def testRegisterReadRequests(self): + ''' Test register read request encoding ''' + for rqst, rsp in self.rread.items(): + self.assertEqual(rqst.encode(), rsp) + + def testRegisterReadRequestsCountErrors(self): + ''' + This tests that the register request messages + will break on counts that are out of range + ''' + requests = [ + ReadHoldingRegistersRequest(1, 0x800), + ReadInputRegistersRequest(1,0x800), + ReadWriteMultipleRegistersRequest(1,0x800,1,5), + ReadWriteMultipleRegistersRequest(1,5,1,0x800), + ] + for request in requests: + result = request.execute(None) + self.assertEqual(ModbusExceptions.IllegalValue, + result.exception_code) + + def testRegisterReadRequestsValidateErrors(self): + ''' + This tests that the register request messages + will break on counts that are out of range + ''' + context = Context() + requests = [ + ReadHoldingRegistersRequest(-1, 5), + ReadInputRegistersRequest(-1,5), + #ReadWriteMultipleRegistersRequest(-1,5,1,5), + #ReadWriteMultipleRegistersRequest(1,5,-1,5), + ] + for request in requests: + result = request.execute(context) + self.assertEqual(ModbusExceptions.IllegalAddress, + result.exception_code) + + def testRegisterWriteRequests(self): + ''' Test register write request encoding ''' + for rqst, rsp in self.rwrite.items(): + self.assertEqual(rqst.encode(), rsp) + +#---------------------------------------------------------------------------# +# Main +#---------------------------------------------------------------------------# +if __name__ == "__main__": + unittest.main() diff --git a/test/test_server_context.py b/test/test_server_context.py index 829bc4279..2d928b441 100644 --- a/test/test_server_context.py +++ b/test/test_server_context.py @@ -19,7 +19,7 @@ def tearDown(self): def testSingleContextGets(self): ''' Test getting on a single context ''' - for id in xrange(0, 0xff): + for id in range(0, 0xff): self.assertEqual(self.slave, self.context[id]) def testSingleContextIter(self): @@ -48,7 +48,7 @@ class ModbusServerMultipleContextTest(unittest.TestCase): def setUp(self): ''' Sets up the test environment ''' - self.slaves = dict((id, ModbusSlaveContext()) for id in xrange(10)) + self.slaves = dict((id, ModbusSlaveContext()) for id in range(10)) self.context = ModbusServerContext(slaves=self.slaves, single=False) def tearDown(self): @@ -57,7 +57,7 @@ def tearDown(self): def testMultipleContextGets(self): ''' Test getting on multiple context ''' - for id in xrange(0, 10): + for id in range(0, 10): self.assertEqual(self.slaves[id], self.context[id]) def testMultipleContextIter(self): @@ -72,7 +72,7 @@ def testMultipleContextDefault(self): def testMultipleContextSet(self): ''' Test a setting multiple slave contexts ''' - slaves = dict((id, ModbusSlaveContext()) for id in xrange(10)) + slaves = dict((id, ModbusSlaveContext()) for id in range(10)) for id, slave in slaves.iteritems(): self.context[id] = slave for id, slave in slaves.iteritems(): From b7a3b4ec1113917720c46ddba97f7272d186388f Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 11 May 2011 14:40:37 +0000 Subject: [PATCH 015/243] updating to python3 --- doc/api/epydoc/build.py | 2 +- doc/api/pydoc/build.py | 14 +- examples/build-data.py | 153 ----------------- examples/common/modbus-scraper.py | 4 +- examples/common/modbus-simulator.py | 10 +- examples/common/performance.py | 2 +- examples/convert.py | 145 ---------------- .../functional/asynchronous-ascii-client.py | 2 +- .../functional/asynchronous-rtu-client.py | 2 +- .../functional/asynchronous-tcp-client.py | 2 +- .../functional/asynchronous-udp-client.py | 2 +- examples/functional/database-slave-context.py | 2 +- examples/functional/redis-slave-context.py | 2 +- .../functional/synchronous-ascii-client.py | 2 +- examples/functional/synchronous-rtu-client.py | 2 +- examples/functional/synchronous-tcp-client.py | 2 +- examples/functional/synchronous-udp-client.py | 2 +- examples/gui/gtk/simulator.py | 10 +- examples/gui/gui-common.py | 2 +- examples/gui/tk/simulator.py | 12 +- examples/gui/web/frontend.py | 2 +- examples/gui/wx/simulator.py | 10 +- examples/modbus-scraper.py | 156 ----------------- examples/modbus-simulator.py | 128 -------------- examples/server.py | 38 ----- examples/tools/build-datastore.py | 11 +- examples/tools/convert.py | 6 +- examples/tools/reindent.py | 4 +- pymodbus/client/sync.py | 8 +- pymodbus/datastore/context.py | 2 +- pymodbus/datastore/store.py | 10 +- pymodbus/device.py | 11 +- pymodbus/factory.py | 4 +- pymodbus/pdu.py | 2 +- pymodbus/server/async.py | 4 +- pymodbus/server/sync.py | 24 +-- pymodbus/transaction.py | 2 +- test/test_bit_messages.py | 161 ------------------ test/test_bit_read_messages.py | 4 +- test/test_bit_write_messages.py | 4 +- test/test_device.py | 2 +- test/test_exceptions.py | 6 +- test/test_file_message.py | 2 +- test/test_register_messages.py | 111 ------------ test/test_register_read_messages.py | 2 +- test/test_register_write_messages.py | 2 +- test/test_remote_datastore.py | 2 +- test/test_server_context.py | 4 +- 48 files changed, 101 insertions(+), 995 deletions(-) delete mode 100644 examples/build-data.py delete mode 100644 examples/convert.py delete mode 100644 examples/modbus-scraper.py delete mode 100644 examples/modbus-simulator.py delete mode 100644 examples/server.py delete mode 100644 test/test_bit_messages.py delete mode 100644 test/test_register_messages.py diff --git a/doc/api/epydoc/build.py b/doc/api/epydoc/build.py index b6c890f62..f8094ac98 100755 --- a/doc/api/epydoc/build.py +++ b/doc/api/epydoc/build.py @@ -32,7 +32,7 @@ if os.path.exists('../../../build'): shutil.move("html", "../../../build/epydoc") -except Exception, ex: +except Exception as ex: import traceback,sys traceback.print_exc(file=sys.stdout) print "Epydoc not avaliable...not building" diff --git a/doc/api/pydoc/build.py b/doc/api/pydoc/build.py index 614985561..a41c801b7 100644 --- a/doc/api/pydoc/build.py +++ b/doc/api/pydoc/build.py @@ -51,7 +51,7 @@ def classify_class_attrs(cls): else: try: obj = getattr(cls, name) - except AttributeError, err: + except AttributeError as err: continue # Figure out where it was defined. @@ -200,7 +200,7 @@ def docmodule(self, object, name=None, mod=None, packageContext = None, *ignored for key, value in data: try: contents.append(self.document(value, key)) - except Exception, err: + except Exception as err: pass result = result + self.bigsection( 'Data', '#ffffff', '#55aa55', join(contents, '
\n')) @@ -327,7 +327,7 @@ def __init__ ( for exclusion in exclusions: try: self.exclusions[ exclusion ]= pydoc.locate ( exclusion) - except pydoc.ErrorDuringImport, value: + except pydoc.ErrorDuringImport as value: self.warn( """Unable to import the module %s which was specified as an exclusion module"""% (repr(exclusion))) self.formatter = formatter or DefaultFormatter() for base in baseModules: @@ -344,7 +344,7 @@ def addBase(self, specifier): try: self.baseSpecifiers [specifier] = pydoc.locate ( specifier) self.pending.append (specifier) - except pydoc.ErrorDuringImport, value: + except pydoc.ErrorDuringImport as value: self.warn( """Unable to import the module %s which was specified as a base module"""% (repr(specifier))) def addInteresting( self, specifier): """Add a module to the list of interesting modules""" @@ -387,13 +387,13 @@ def process( self ): self.info( """ ... found %s"""% (repr(object.__name__))) except AlreadyDone: pass - except pydoc.ErrorDuringImport, value: + except pydoc.ErrorDuringImport as value: self.info( """ ... FAILED %s"""% (repr( value))) self.warn( """Unable to import the module %s"""% (repr(self.pending[0]))) - except (SystemError, SystemExit), value: + except (SystemError, SystemExit) as value: self.info( """ ... FAILED %s"""% (repr( value))) self.warn( """Unable to import the module %s"""% (repr(self.pending[0]))) - except Exception, value: + except Exception as value: self.info( """ ... FAILED %s"""% (repr( value))) self.warn( """Unable to import the module %s"""% (repr(self.pending[0]))) else: diff --git a/examples/build-data.py b/examples/build-data.py deleted file mode 100644 index f5d67420b..000000000 --- a/examples/build-data.py +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env python -''' -This creates a dummy datastore for use with the modbus simulator. - -It is also used to convert datastores to and from a register list -dump. This allows users to build their own data from scratch or -modifiy an exisiting dump. -''' -import pickle -from sys import exit -from optparse import OptionParser - -from pymodbus.datastore import ModbusSequentialDataBlock as seqblock -from pymodbus.datastore import ModbusSparseDataBlock as sparblock - -#--------------------------------------------------------------------------# -# Helper Classes -#--------------------------------------------------------------------------# -class ConfigurationException(Exception): - ''' Exception for configuration error ''' - - def __init__(self, string): - ''' A base string to make pylint happy - :param string: Additional information to append to exception - ''' - Exception.__init__(self, string) - self.string = string - - def __str__(self): - return 'Configuration Error: %s' % self.string - -#--------------------------------------------------------------------------# -# Datablock Builders -#--------------------------------------------------------------------------# -def build_translation(option, opt, value, parser): - ''' Converts a register dump list to a pickeld datastore - - :param option: The option instance - :param opt: The option string specified - :param value: The file to translate - :param parser: The parser object - ''' - raise ConfigurationException("This function is not implemented yet") - try: - with open(value, "r") as input: - data = pickle.load(input) - except: - raise ConfigurationException("File Not Found %s" % value) - - with open(value + ".trans", "w") as output: - pass # TODO - exit() # So we don't start a dummy build - -def build_conversion(option, opt, value, parser): - ''' This converts a pickled datastore to a register dump list - - :param option: The option instance - :param opt: The option string specified - :param value: The file to convert - :param parser: The parser object - ''' - try: - with open(value, "r") as input: - data = pickle.load(input) - except: - raise ConfigurationException("File Not Found %s" % value) - - with open(value + ".dump", "w") as output: - for dk,dv in data.items(): - output.write("[ %s ]\n\n" % dk) - - # handle sequential - if isinstance(dv.values, list): - output.write("\n".join(["[%d] = %d" % (vk,vv) - for vk,vv in enumerate(dv.values)])) - - # handle sparse - elif isinstance(data[k].values, dict): - output.write("\n".join(["[%d] = %d" % (vk,vv) - for vk,vv in dv.values.items()])) - else: raise ConfigurationException("Datastore is corrupted %s" % value) - output.write("\n\n") - exit() # So we don't start a dummy build - -#--------------------------------------------------------------------------# -# Datablock Builders -#--------------------------------------------------------------------------# -def build_sequential(): - ''' - This builds a quick mock sequential datastore with 100 values for each - discrete, coils, holding, and input bits/registers. - ''' - data = { - 'di' : seqblock(0, [bool(x) for x in range(1, 100)]), - 'ci' : seqblock(0, [bool(not x) for x in range(1, 100)]), - 'hr' : seqblock(0, [int(x) for x in range(1, 100)]), - 'ir' : seqblock(0, [int(2*x) for x in range(1, 100)]), - } - return data - -def build_sparse(): - ''' - This builds a quick mock sparse datastore with 100 values for each - discrete, coils, holding, and input bits/registers. - ''' - data = { - 'di' : sparblock([bool(x) for x in range(1, 100)]), - 'ci' : sparblock([bool(not x) for x in range(1, 100)]), - 'hr' : sparblock([int(x) for x in range(1, 100)]), - 'ir' : sparblock([int(2*x) for x in range(1, 100)]), - } - return data - -def main(): - ''' The main function for this script ''' - parser = OptionParser() - parser.add_option("-o", "--output", - help="The output file to write to", - dest="file", default="example.store") - parser.add_option("-t", "--type", - help="The type of block to create (sequential,sparse)", - dest="type", default="sparse") - parser.add_option("-c", "--convert", - help="Convert a file datastore to a register dump", - type="string", - action="callback", callback=build_conversion) - parser.add_option("-r", "--restore", - help="Convert a register dump to a file datastore", - type="string", - action="callback", callback=build_translation) - try: - (opt, arg) = parser.parse_args() # so we can catch the csv callback - - if opt.type == "sparse": - result = build_sparse() - elif opt.type == "sequential": - result = build_sequential() - else: - raise ConfigurationException("Unknown block type %s" % opt.type) - - with open(opt.file, "w") as output: - pickle.dump(result, output) - print("Created datastore: %s\n" % opt.file) - - except ConfigurationException as ex: - print(ex) - parser.print_help() - -#---------------------------------------------------------------------------# -# Main jumper -#---------------------------------------------------------------------------# -if __name__ == "__main__": - main() diff --git a/examples/common/modbus-scraper.py b/examples/common/modbus-scraper.py index 301d565c2..753492319 100644 --- a/examples/common/modbus-scraper.py +++ b/examples/common/modbus-scraper.py @@ -109,7 +109,7 @@ def main(): try: client_log.setLevel(logging.DEBUG) logging.basicConfig() - except Exception, e: + except Exception as e: print "Logging is not supported on this system" # Begin scraping @@ -118,7 +118,7 @@ def main(): s = ClientScraper(opt.host, opt.port, opt.range) reactor.callWhenRunning(s.start) reactor.run() - except ClientException, err: + except ClientException as err: print err parser.print_help() diff --git a/examples/common/modbus-simulator.py b/examples/common/modbus-simulator.py index 029199546..8fdd420c6 100644 --- a/examples/common/modbus-simulator.py +++ b/examples/common/modbus-simulator.py @@ -107,15 +107,15 @@ def main(): try: server_log.setLevel(logging.DEBUG) protocol_log.setLevel(logging.DEBUG) - except Exception, e: - print "Logging is not supported on this system" + except Exception as e: + print("Logging is not supported on this system") # parse configuration file and run try: conf = Configuration(opt.file) StartTcpServer(context=conf.parse()) - except ConfigurationException, err: - print err + except ConfigurationException as err: + print(err) parser.print_help() #---------------------------------------------------------------------------# @@ -124,5 +124,5 @@ def main(): if __name__ == "__main__": if root_test(): main() - else: print "This script must be run as root!" + else: print("This script must be run as root!") diff --git a/examples/common/performance.py b/examples/common/performance.py index cbe04c5f3..9d114b1c5 100755 --- a/examples/common/performance.py +++ b/examples/common/performance.py @@ -31,5 +31,5 @@ # check our results #---------------------------------------------------------------------------# stop = time() -print "%d requests/second" % ((1.0 * count) / (stop - start)) +print("%d requests/second" % ((1.0 * count) / (stop - start))) diff --git a/examples/convert.py b/examples/convert.py deleted file mode 100644 index e25964714..000000000 --- a/examples/convert.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env python -''' -This script is used to convert an XML dump to a -serialized ModbusDataStore for use with the simulator. - -For more information on the windows scraper, -google for modsrape -''' - -from pymodbus.datastore import ModbusSparseDataBlock as sblock -from optparse import OptionParser -from lxml import etree -import pickle - -#--------------------------------------------------------------------------# -# Helper Classes -#--------------------------------------------------------------------------# -class ConversionException(Exception): - ''' Exception for configuration error ''' - - def __init__(self, string): - ''' Initialize a ConversionException instance - - :param string: Additional information to append to exception - ''' - Exception.__init__(self, string) - self.string = string - - def __str__(self): - ''' Builds a string representation of the object - - :returns: The string representation of the object - ''' - return 'Conversion Error: %s' % self.string - -#--------------------------------------------------------------------------# -# Lxml Parser Tree -#--------------------------------------------------------------------------# -class ModbusXML: - convert = { - 'true':True, - 'false':False, - } - lookup = { - 'InputRegisters':'ir', - 'HoldingRegisters':'hr', - 'CoilDiscretes':'ci', - 'InputDiscretes':'di' - } - - def __init__(self): - ''' - Initializer for the parser object - ''' - self.next = 0 - self.result = {'di':{}, 'ci':{}, 'ir':{}, 'hr':{}} - - def start(self, tag, attrib): - ''' - Callback for start node - @param tag The starting tag found - @param attrib Attributes dict found in the tag - ''' - if tag == "value": - try: - self.next = attrib['index'] - except KeyError: raise ConversionException("Invalid XML: index") - elif tag in self.lookup.keys(): - self.h = self.result[self.lookup[tag]] - - def end(self, tag): - ''' - Callback for end node - @param tag The end tag found - ''' - pass - - def data(self, data): - ''' - Callback for node data - @param data The data for the current node - ''' - if data in self.convert.keys(): - result = self.convert[data] - else: result = data - self.h[self.next] = data - - def comment(self, text): - ''' - Callback for node data - @param data The data for the current node - ''' - pass - - def close(self): - ''' - Callback for node data - @param data The data for the current node - ''' - return self.result - -#--------------------------------------------------------------------------# -# Helper Functions -#--------------------------------------------------------------------------# -def store_dump(result, file): - ''' - Quick function to dump a result to a pickle - @param result The resulting parsed data - ''' - result['di'] = sblock(result['di']) - result['ci'] = sblock(result['ci']) - result['hr'] = sblock(result['hr']) - result['ir'] = sblock(result['ir']) - - with open(file, "w") as input: - pickle.dump(result, input) - -def main(): - ''' - The main function for this script - ''' - parser = OptionParser() - parser.add_option("-o", "--output", - help="The output file to write to", - dest="output", default="example.store") - parser.add_option("-i", "--input", - help="File to convert to a datastore", - dest="input", default="scrape.xml") - try: - (opt, arg) = parser.parse_args() - - parser = etree.XMLParser(target = ModbusXML()) - result = etree.parse(opt.input, parser) - store_dump(result, opt.output) - print("Created datastore: %s\n" % opt.output) - - except ConversionException as ex: - print(ex) - parser.print_help() - -#---------------------------------------------------------------------------# -# Main jumper -#---------------------------------------------------------------------------# -if __name__ == "__main__": - main() diff --git a/examples/functional/asynchronous-ascii-client.py b/examples/functional/asynchronous-ascii-client.py index afb19cc37..06a9b06b7 100644 --- a/examples/functional/asynchronous-ascii-client.py +++ b/examples/functional/asynchronous-ascii-client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.client.async import ModbusSerialClient as ModbusClient -from base_runner import Runner +from .base_runner import Runner class AsynchronousAsciiClient(Runner, unittest.TestCase): ''' diff --git a/examples/functional/asynchronous-rtu-client.py b/examples/functional/asynchronous-rtu-client.py index f4c84c78d..30c4299c0 100644 --- a/examples/functional/asynchronous-rtu-client.py +++ b/examples/functional/asynchronous-rtu-client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.client.async import ModbusSerialClient as ModbusClient -from base_runner import Runner +from .base_runner import Runner class AsynchronousRtuClient(Runner, unittest.TestCase): ''' diff --git a/examples/functional/asynchronous-tcp-client.py b/examples/functional/asynchronous-tcp-client.py index 4603b56b4..7bc29c7d8 100644 --- a/examples/functional/asynchronous-tcp-client.py +++ b/examples/functional/asynchronous-tcp-client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.client.async import ModbusTcpClient as ModbusClient -from base_runner import Runner +from .base_runner import Runner class AsynchronousTcpClient(Runner, unittest.TestCase): ''' diff --git a/examples/functional/asynchronous-udp-client.py b/examples/functional/asynchronous-udp-client.py index e5f993560..cfcf5382b 100644 --- a/examples/functional/asynchronous-udp-client.py +++ b/examples/functional/asynchronous-udp-client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.client.sync import ModbusUdpClient as ModbusClient -from base_runner import Runner +from .base_runner import Runner class AsynchronousUdpClient(Runner, unittest.TestCase): ''' diff --git a/examples/functional/database-slave-context.py b/examples/functional/database-slave-context.py index c4269fcbd..a331eee63 100644 --- a/examples/functional/database-slave-context.py +++ b/examples/functional/database-slave-context.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest, os from pymodbus.datastore.database import DatabaseSlaveContext -from base_context import ContextRunner +from .base_context import ContextRunner class DatabaseSlaveContextTest(ContextRunner, unittest.TestCase): ''' diff --git a/examples/functional/redis-slave-context.py b/examples/functional/redis-slave-context.py index 364aa50dd..678ed59e2 100644 --- a/examples/functional/redis-slave-context.py +++ b/examples/functional/redis-slave-context.py @@ -3,7 +3,7 @@ import os from subprocess import Popen as execute from pymodbus.datastore.modredis import RedisSlaveContext -from base_context import ContextRunner +from .base_context import ContextRunner class RedisSlaveContextTest(ContextRunner, unittest.TestCase): ''' diff --git a/examples/functional/synchronous-ascii-client.py b/examples/functional/synchronous-ascii-client.py index ad96c6c5c..8ff421980 100644 --- a/examples/functional/synchronous-ascii-client.py +++ b/examples/functional/synchronous-ascii-client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.client.sync import ModbusSerialClient as ModbusClient -from base_runner import Runner +from .base_runner import Runner class SynchronousAsciiClient(Runner, unittest.TestCase): ''' diff --git a/examples/functional/synchronous-rtu-client.py b/examples/functional/synchronous-rtu-client.py index 8f1b5f5b1..aa4033b54 100644 --- a/examples/functional/synchronous-rtu-client.py +++ b/examples/functional/synchronous-rtu-client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.client.sync import ModbusSerialClient as ModbusClient -from base_runner import Runner +from .base_runner import Runner class SynchronousRtuClient(Runner, unittest.TestCase): ''' diff --git a/examples/functional/synchronous-tcp-client.py b/examples/functional/synchronous-tcp-client.py index 4e6e904d5..429bf1e7b 100644 --- a/examples/functional/synchronous-tcp-client.py +++ b/examples/functional/synchronous-tcp-client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.client.sync import ModbusTcpClient as ModbusClient -from base_runner import Runner +from .base_runner import Runner class SynchronousTcpClient(Runner, unittest.TestCase): ''' diff --git a/examples/functional/synchronous-udp-client.py b/examples/functional/synchronous-udp-client.py index 9507ab78b..c2b39a93b 100644 --- a/examples/functional/synchronous-udp-client.py +++ b/examples/functional/synchronous-udp-client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.client.sync import ModbusUdpClient as ModbusClient -from base_runner import Runner +from .base_runner import Runner class SynchronousUdpClient(Runner, unittest.TestCase): ''' diff --git a/examples/gui/gtk/simulator.py b/examples/gui/gtk/simulator.py index ea0fb51e2..d658e5573 100755 --- a/examples/gui/gtk/simulator.py +++ b/examples/gui/gtk/simulator.py @@ -91,11 +91,11 @@ def _parse(self): def _simulator(self): ''' Starts the snmp simulator ''' - ports = [502]+range(20000,25000) + ports = [502]+(range(20000,25000)) for port in ports: try: reactor.listenTCP(port, ModbusServerFactory(self._parse())) - print 'listening on port', port + print('listening on port %d' % port) return port except twisted_error.CannotListenError: pass @@ -265,7 +265,7 @@ def start_clicked(self, widget): try: handle = Simulator(config=self.file) handle.run() - except ConfigurationException, ex: + except ConfigurationException as ex: self.error_dialog("Error %s" % ex) self.show_buttons(state=True) else: @@ -310,8 +310,8 @@ def main(): try: log.setLevel(logging.DEBUG) logging.basicConfig() - except Exception, e: - print "Logging is not supported on this system" + except Exception as e: + print("Logging is not supported on this system") simulator = SimulatorApp('./simulator.glade') reactor.run() diff --git a/examples/gui/gui-common.py b/examples/gui/gui-common.py index c78e595d1..bea69d72d 100755 --- a/examples/gui/gui-common.py +++ b/examples/gui/gui-common.py @@ -77,7 +77,7 @@ def _parse(self): def _simulator(self): ''' Starts the snmp simulator ''' - ports = [502]+range(20000,25000) + ports = [502]+list(range(20000,25000)) for port in ports: try: reactor.listenTCP(port, ModbusServerFactory(self._parse())) diff --git a/examples/gui/tk/simulator.py b/examples/gui/tk/simulator.py index 13cc767c9..14721b03d 100755 --- a/examples/gui/tk/simulator.py +++ b/examples/gui/tk/simulator.py @@ -13,8 +13,8 @@ #---------------------------------------------------------------------------# # For Gui #---------------------------------------------------------------------------# -from Tkinter import * -from tkFileDialog import askopenfilename as OpenFilename +from tkinter import * +from tkinter.filedialog import askopenfilename as OpenFilename from twisted.internet import tksupport root = Tk() tksupport.install(root) @@ -89,7 +89,7 @@ def _parse(self): def _simulator(self): ''' Starts the snmp simulator ''' - ports = [502]+range(20000,25000) + ports = [502]+list(range(20000,25000)) for port in ports: try: reactor.listenTCP(port, ModbusServerFactory(self._parse())) @@ -264,7 +264,7 @@ def start_clicked(self): try: handle = Simulator(config=filename) handle.run() - except ConfigurationException, ex: + except ConfigurationException as ex: self.error_dialog("Error %s" % ex) self.show_buttons(state=True) else: @@ -324,8 +324,8 @@ def main(): try: log.setLevel(logging.DEBUG) logging.basicConfig() - except Exception, e: - print "Logging is not supported on this system" + except Exception as e: + print("Logging is not supported on this system") simulator = SimulatorApp(root) root.title("Modbus Simulator") reactor.run() diff --git a/examples/gui/web/frontend.py b/examples/gui/web/frontend.py index b15a2e0b6..0d7421913 100644 --- a/examples/gui/web/frontend.py +++ b/examples/gui/web/frontend.py @@ -112,7 +112,7 @@ def register_routes(application, register): from bottle import route methods = dir(application) - methods = filter(lambda n: not n.startswith('_'), methods) + methods = (n for n in methods if not n.startswith('_')) for method in methods: pieces = method.split('_') verb, path = pieces[0], pieces[1:] diff --git a/examples/gui/wx/simulator.py b/examples/gui/wx/simulator.py index 789ec5623..4f2421295 100755 --- a/examples/gui/wx/simulator.py +++ b/examples/gui/wx/simulator.py @@ -87,11 +87,11 @@ def _parse(self): def _simulator(self): ''' Starts the snmp simulator ''' - ports = [502]+range(20000,25000) + ports = [502]+list(range(20000,25000)) for port in ports: try: reactor.listenTCP(port, ModbusServerFactory(self._parse())) - print 'listening on port', port + print('listening on port %d' % port) return port except twisted_error.CannotListenError: pass @@ -235,7 +235,7 @@ def start_clicked(self, widget): try: handle = Simulator(config=self.file) handle.run() - except ConfigurationException, ex: + except ConfigurationException as ex: self.error_dialog("Error %s" % ex) self.show_buttons(state=True) else: @@ -297,8 +297,8 @@ def main(): try: log.setLevel(logging.DEBUG) logging.basicConfig() - except Exception, e: - print "Logging is not supported on this system" + except Exception as e: + print("Logging is not supported on this system") simulator = SimulatorApp(0) reactor.run() diff --git a/examples/modbus-scraper.py b/examples/modbus-scraper.py deleted file mode 100644 index 635bc5b3c..000000000 --- a/examples/modbus-scraper.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env python -''' -This utility can be used to fully scrape a modbus device -and store its data as a Mobus Context for use with the -simulator. -''' - -from twisted.internet import reactor - -from pymodbus.client.async import ModbusClientFactory -from pymodbus.bit_read_message import ReadCoilsRequest -from pymodbus.bit_read_message import ReadDiscreteInputsRequest -from pymodbus.register_read_message import ReadHoldingRegistersRequest -from pymodbus.register_read_message import ReadInputRegistersRequest - -from optparse import OptionParser -import pickle - -#--------------------------------------------------------------------------# -# Logging -#--------------------------------------------------------------------------# -import logging -client_log = logging.getLogger("pymodbus.client") - -#--------------------------------------------------------------------------# -# Helper Classes -#--------------------------------------------------------------------------# -class ClientException(Exception): - ''' Exception for configuration error ''' - - def __init__(self, string): - Exception.__init__(self, string) - self.string = string - - def __str__(self): - return 'Client Error: %s' % self.string - -class ClientScraper: - ''' Exception for configuration error ''' - - def __init__(self, host, port, address): - ''' - Initializes the connection paramaters and requests - @param host The host to connect to - @param port The port the server resides on - @param address The range to read to:from - ''' - self.host = host - - if isinstance(port, int): - self.port = port - elif isinstance(port, str): - self.port = int(port) - - self.requests = [] - for rqst in [ - ReadCoilsRequest, - ReadDiscreteInputsRequest, - ReadInputRegistersRequest, - ReadHoldingRegistersRequest]: - for i in range(*[int(j) for j in address.split(':')]): - self.requests.append(rqst(i,1)) - - def start(self): - ''' - Starts the device scrape - ''' - f = ModbusClientFactory(self.requests) - self.p = reactor.connectTCP(self.host, self.port, f) - - def process(self, data): - ''' - Starts the device scrape - ''' - f = ModbusClientFactory(self.requests) - self.p = reactor.connectTCP(self.host, self.port, f) - -class ContextBuilder: - ''' - This class is used to build our server datastore - for use with the modbus simulator. - ''' - - def __init__(self, output): - ''' - Initializes the ContextBuilder and checks data values - @param file The output file for the server context - ''' - try: - self.file = open(output, "w") - except Exception: - raise ClientException("Unable to open file [%s]" % output) - - def build(self): - ''' Builds the final output store file ''' - try: - pass - result = self.makeContext() - pickle.dump(result, self.file) - print("Device successfully scraped!") - except Exception: - raise ClientException("Invalid data") - self.file.close() - reactor.stop() - - def makeContext(self): - ''' Builds the server context based on the passed in data ''' - # ModbusServerContext(d=sd, c=sc, h=sh, i=si) - return "string" - -#--------------------------------------------------------------------------# -# Main start point -#--------------------------------------------------------------------------# -def main(): - ''' Server launcher ''' - parser = OptionParser() - parser.add_option("-o", "--output", - help="The resulting output file for the scrape", - dest="file", default="output.store") - parser.add_option("-p", "--port", - help="The port to connect to", - dest="port", default="502") - parser.add_option("-s", "--server", - help="The server to scrape", - dest="host", default="localhost") - parser.add_option("-r", "--range", - help="The address range to scan", - dest="range", default="0:500") - parser.add_option("-D", "--debug", - help="Enable debug tracing", - action="store_true", dest="debug", default=False) - (opt, arg) = parser.parse_args() - - # enable debugging information - if opt.debug: - try: - client_log.setLevel(logging.DEBUG) - logging.basicConfig() - except Exception as e: - print("Logging is not supported on this system") - - # Begin scrape - try: - #ctx = ContextBuilder(opt.file) - s = ClientScraper(opt.host, opt.port, opt.range) - reactor.callWhenRunning(s.start) - reactor.run() - except ClientException as err: - print(err) - parser.print_help() - -#---------------------------------------------------------------------------# -# Main jumper -#---------------------------------------------------------------------------# -if __name__ == "__main__": - main() diff --git a/examples/modbus-simulator.py b/examples/modbus-simulator.py deleted file mode 100644 index 8fdd420c6..000000000 --- a/examples/modbus-simulator.py +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env python -''' -An example of creating a fully implemented modbus server -with read/write data as well as user configurable base data -''' - -import pickle -from optparse import OptionParser -from twisted.internet import reactor - -from pymodbus.server.async import StartTcpServer -from pymodbus.datastore import ModbusServerContext,ModbusSlaveContext - -#--------------------------------------------------------------------------# -# Logging -#--------------------------------------------------------------------------# -import logging -logging.basicConfig() - -server_log = logging.getLogger("pymodbus.server") -protocol_log = logging.getLogger("pymodbus.protocol") - -#---------------------------------------------------------------------------# -# Extra Global Functions -#---------------------------------------------------------------------------# -# These are extra helper functions that don't belong in a class -#---------------------------------------------------------------------------# -import getpass -def root_test(): - ''' Simple test to see if we are running as root ''' - return True # removed for the time being as it isn't portable - #return getpass.getuser() == "root" - -#--------------------------------------------------------------------------# -# Helper Classes -#--------------------------------------------------------------------------# -class ConfigurationException(Exception): - ''' Exception for configuration error ''' - - def __init__(self, string): - ''' Initializes the ConfigurationException instance - - :param string: The message to append to the exception - ''' - Exception.__init__(self, string) - self.string = string - - def __str__(self): - ''' Builds a representation of the object - - :returns: A string representation of the object - ''' - return 'Configuration Error: %s' % self.string - -class Configuration: - ''' - Class used to parse configuration file and create and modbus - datastore. - - The format of the configuration file is actually just a - python pickle, which is a compressed memory dump from - the scraper. - ''' - - def __init__(self, config): - ''' - Trys to load a configuration file, lets the file not - found exception fall through - - :param config: The pickled datastore - ''' - try: - self.file = open(config, "r") - except Exception: - raise ConfigurationException("File not found %s" % config) - - def parse(self): - ''' Parses the config file and creates a server context - ''' - handle = pickle.load(self.file) - try: # test for existance, or bomb - dsd = handle['di'] - csd = handle['ci'] - hsd = handle['hr'] - isd = handle['ir'] - except Exception: - raise ConfigurationException("Invalid Configuration") - slave = ModbusSlaveContext(d=dsd, c=csd, h=hsd, i=isd) - return ModbusServerContext(slaves=slave) - -#--------------------------------------------------------------------------# -# Main start point -#--------------------------------------------------------------------------# -def main(): - ''' Server launcher ''' - parser = OptionParser() - parser.add_option("-c", "--conf", - help="The configuration file to load", - dest="file") - parser.add_option("-D", "--debug", - help="Turn on to enable tracing", - action="store_true", dest="debug", default=False) - (opt, arg) = parser.parse_args() - - # enable debugging information - if opt.debug: - try: - server_log.setLevel(logging.DEBUG) - protocol_log.setLevel(logging.DEBUG) - except Exception as e: - print("Logging is not supported on this system") - - # parse configuration file and run - try: - conf = Configuration(opt.file) - StartTcpServer(context=conf.parse()) - except ConfigurationException as err: - print(err) - parser.print_help() - -#---------------------------------------------------------------------------# -# Main jumper -#---------------------------------------------------------------------------# -if __name__ == "__main__": - if root_test(): - main() - else: print("This script must be run as root!") - diff --git a/examples/server.py b/examples/server.py deleted file mode 100644 index 3ed64d629..000000000 --- a/examples/server.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python - -#---------------------------------------------------------------------------# -# the various server implementations -#---------------------------------------------------------------------------# -from pymodbus.server.sync import StartTcpServer, StartUdpServer -from pymodbus.server.sync import StartSerialServer -from pymodbus.server.async import StartTcpServer as StartATcpServer -from pymodbus.server.async import StartUdpServer as StartAUdpServer -from pymodbus.server.async import StartSerialServer as StartASerialServer - -from pymodbus.datastore import ModbusSequentialDataBlock -from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext - -#---------------------------------------------------------------------------# -# configure the service logging -#---------------------------------------------------------------------------# -import logging -logging.basicConfig() -log = logging.getLogger() -log.setLevel(logging.DEBUG) - -#---------------------------------------------------------------------------# -# initialize your data store -#---------------------------------------------------------------------------# -store = ModbusSlaveContext( - di = ModbusSequentialDataBlock(0, [17]*100), - co = ModbusSequentialDataBlock(0, [17]*100), - hr = ModbusSequentialDataBlock(0, [17]*100), - ir = ModbusSequentialDataBlock(0, [17]*100)) -context = ModbusServerContext(slaves=store, single=True) - -#---------------------------------------------------------------------------# -# run the server you want -#---------------------------------------------------------------------------# -#StartATcpServer(context) -#StartSerialServer(context, port='/dev/ptmx') -StartSerialServer(context, port='/tmp/tty1') diff --git a/examples/tools/build-datastore.py b/examples/tools/build-datastore.py index ff1267cb0..f5d67420b 100644 --- a/examples/tools/build-datastore.py +++ b/examples/tools/build-datastore.py @@ -6,7 +6,6 @@ dump. This allows users to build their own data from scratch or modifiy an exisiting dump. ''' -from __future__ import with_statement import pickle from sys import exit from optparse import OptionParser @@ -67,7 +66,7 @@ def build_conversion(option, opt, value, parser): raise ConfigurationException("File Not Found %s" % value) with open(value + ".dump", "w") as output: - for dk,dv in data.iteritems(): + for dk,dv in data.items(): output.write("[ %s ]\n\n" % dk) # handle sequential @@ -78,7 +77,7 @@ def build_conversion(option, opt, value, parser): # handle sparse elif isinstance(data[k].values, dict): output.write("\n".join(["[%d] = %d" % (vk,vv) - for vk,vv in dv.values.iteritems()])) + for vk,vv in dv.values.items()])) else: raise ConfigurationException("Datastore is corrupted %s" % value) output.write("\n\n") exit() # So we don't start a dummy build @@ -141,10 +140,10 @@ def main(): with open(opt.file, "w") as output: pickle.dump(result, output) - print "Created datastore: %s\n" % opt.file + print("Created datastore: %s\n" % opt.file) - except ConfigurationException, ex: - print ex + except ConfigurationException as ex: + print(ex) parser.print_help() #---------------------------------------------------------------------------# diff --git a/examples/tools/convert.py b/examples/tools/convert.py index a21d8df04..02e98f193 100755 --- a/examples/tools/convert.py +++ b/examples/tools/convert.py @@ -131,10 +131,10 @@ def main(): parser = etree.XMLParser(target = ModbusXML()) result = etree.parse(opt.input, parser) store_dump(result, opt.output) - print "Created datastore: %s\n" % opt.output + print("Created datastore: %s\n" % opt.output) - except ConversionException, ex: - print ex + except ConversionException as ex: + print(ex) parser.print_help() #---------------------------------------------------------------------------# diff --git a/examples/tools/reindent.py b/examples/tools/reindent.py index 5ab88f561..3801bdb35 100755 --- a/examples/tools/reindent.py +++ b/examples/tools/reindent.py @@ -45,7 +45,7 @@ def main(): global verbose, recurse, dryrun try: opts, args = getopt.getopt(sys.argv[1:], "drv") - except getopt.error, msg: + except getopt.error as msg: errprint(msg) return for o, a in opts: @@ -78,7 +78,7 @@ def check(file): print "checking", file, "...", try: f = open(file) - except IOError, msg: + except IOError as msg: errprint("%s: I/O Error: %s" % (file, str(msg))) return diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 212e0bacf..455a497fc 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -51,7 +51,7 @@ def execute(self, request): result = self.client._recv(1024) self.client.framer.processIncomingPacket(result, self.__set_result) break; - except socket.error, msg: + except socket.error as msg: self.client.close() _logger.debug("Transaction failed. (%s) " % msg) retries -= 1 @@ -190,7 +190,7 @@ def connect(self): self.socket.settimeout(Defaults.Timeout) self.socket.connect((self.host, self.port)) self.transaction = ModbusTransactionManager(self) - except socket.error, msg: + except socket.error as msg: _logger.error('Connection to (%s, %s) failed: %s' % \ (self.host, self.port, msg)) self.close() @@ -256,7 +256,7 @@ def connect(self): try: self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #self.socket.bind(('localhost', Defaults.Port)) - except socket.error, ex: + except socket.error as ex: _logger.error('Unable to create udp socket %s' % ex) self.close() return self.socket != None @@ -344,7 +344,7 @@ def connect(self): self.socket = serial.Serial(port=self.port, timeout=self.timeout, bytesize=self.bytesize, stopbits=self.stopbits, baudrate=self.baudrate, parity=self.parity) - except serial.SerialException, msg: + except serial.SerialException as msg: _logger.error(msg) self.close() return self.socket != None diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index b1e182dcf..fe8a1650d 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -106,7 +106,7 @@ def __iter__(self): :returns: An iterator over the slave contexts ''' - return self.__slaves.iteritems() + return self.__slaves.items() def __setitem__(self, slave, context): ''' Wrapper used to access the slave context diff --git a/pymodbus/datastore/store.py b/pymodbus/datastore/store.py index e760bb6bc..f1fc5b17b 100644 --- a/pymodbus/datastore/store.py +++ b/pymodbus/datastore/store.py @@ -125,7 +125,7 @@ def __iter__(self): :returns: An iterator of the data block data ''' if isinstance(self.values, dict): - return self.values.iteritems() + return self.values.items() return enumerate(self.values) @@ -194,8 +194,8 @@ def __init__(self, values): self.values = dict(enumerate(values)) else: raise ParameterException( "Values for datastore must be a list or dictionary") - self.default_value = self.values.values()[0].__class__() - self.address = self.values.iterkeys().next() + self.default_value = list(self.values.values())[0].__class__() + self.address = self.values.keys().next() def validate(self, address, count=1): ''' Checks to see if the request is in range @@ -206,7 +206,7 @@ def validate(self, address, count=1): ''' if count == 0: return False handle = set(range(address, address + count)) - return handle.issubset(set(self.values.iterkeys())) + return handle.issubset(set(self.values.keys())) def getValues(self, address, count=1): ''' Returns the requested values of the datastore @@ -224,7 +224,7 @@ def setValues(self, address, values): :param values: The new values to be set ''' if isinstance(values, dict): - for idx, val in values.iteritems(): + for idx, val in values.items(): self.values[idx] = val else: if not isinstance(values, list): diff --git a/pymodbus/device.py b/pymodbus/device.py index a202014ff..b79ecc0b4 100644 --- a/pymodbus/device.py +++ b/pymodbus/device.py @@ -6,7 +6,6 @@ maintained in the server context and the various methods should be inserted in the correct locations. """ -from itertools import izip from pymodbus.interfaces import Singleton from pymodbus.utilities import dict_property @@ -117,14 +116,14 @@ def __iter__(self): :returns: An iterator of the device information ''' - return self.__data.iteritems() + return self.__data.items() def summary(self): ''' Return a summary of the main items :returns: An dictionary of the main items ''' - return dict(zip(self.__names, self.__data.itervalues())) + return dict(zip(self.__names, self.__data.values())) def update(self, input): ''' Update the values of this identity @@ -259,7 +258,7 @@ def __iter__(self): :returns: An iterator of the device counters ''' - return izip(self.__names, self.__data.itervalues()) + return zip(self.__names, self.__data.values()) def update(self, input): ''' Update the values of this identity @@ -267,7 +266,7 @@ def update(self, input): :param input: The value to copy values from ''' - for k, v in input.iteritems(): + for k, v in input.items(): v += self.__getattribute__(k) self.__setattr__(k, v) @@ -426,7 +425,7 @@ def setDiagnostic(self, mapping): :param mapping: Dictionary of key:value pairs to set ''' - for entry in mapping.iteritems(): + for entry in mapping.items(): if entry[0] >= 0 and entry[0] < len(self.__diagnostic): self.__diagnostic[entry[0]] = (entry[1] != 0) diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 647d72f64..9ae4f1324 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -53,7 +53,7 @@ def decode(self, message): ''' try: return self._helper(message) - except ModbusException, er: + except ModbusException as er: _logger.warn("Unable to decode request %s" % er) return None @@ -120,7 +120,7 @@ def decode(self, message): ''' try: return self._helper(message) - except ModbusException, er: + except ModbusException as er: _logger.error("Unable to decode response %s" % er) return None diff --git a/pymodbus/pdu.py b/pymodbus/pdu.py index 6c21b8f00..2dec07bd2 100644 --- a/pymodbus/pdu.py +++ b/pymodbus/pdu.py @@ -4,7 +4,7 @@ from pymodbus.interfaces import Singleton from pymodbus.exceptions import NotImplementedException from pymodbus.constants import Defaults -from utilities import rtuFrameSize +from pymodbus.utilities import rtuFrameSize #---------------------------------------------------------------------------# # Logging diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index 18a054de1..51504e114 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -64,7 +64,7 @@ def _execute(self, request): try: context = self.factory.store[request.unit_id] response = request.execute(context) - except Exception, ex: + except Exception as ex: _logger.debug("Datastore unable to fulfill request %s" % ex) response = request.doException(merror.SlaveFailure) #self.framer.populateResult(response) @@ -160,7 +160,7 @@ def _execute(self, request, addr): try: context = self.store[request.unit_id] response = request.execute(context) - except Exception, ex: + except Exception as ex: _logger.debug("Datastore unable to fulfill request %s" % ex) response = request.doException(merror.SlaveFailure) #self.framer.populateResult(response) diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index d00124b1d..831a5b7a1 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -4,7 +4,7 @@ ''' from binascii import b2a_hex -import SocketServer +import socketserver import serial import socket @@ -27,7 +27,7 @@ #---------------------------------------------------------------------------# # Server #---------------------------------------------------------------------------# -class ModbusRequestHandler(SocketServer.BaseRequestHandler): +class ModbusRequestHandler(socketserver.BaseRequestHandler): ''' Implements the modbus server protocol This uses the socketserver.BaseRequestHandler to implement @@ -58,7 +58,7 @@ def handle(self): # if not self.server.control.ListenOnly: self.framer.processIncomingPacket(data, self.execute) except socket.timeout: pass - except socket.error, msg: + except socket.error as msg: _logger.error("Socket error occurred %s" % msg) self.running = False except: self.running = False @@ -74,7 +74,7 @@ def execute(self, request): try: context = self.server.context[request.unit_id] response = request.execute(context) - except Exception, ex: + except Exception as ex: _logger.debug("Datastore unable to fulfill request %s" % ex) response = request.doException(merror.SlaveFailure) response.transaction_id = request.transaction_id @@ -100,12 +100,12 @@ def decode(self, message): ''' try: return decodeModbusRequestPDU(message) - except ModbusException, er: + except ModbusException as er: _logger.warn("Unable to decode request %s" % er) return None -class ModbusTcpServer(SocketServer.ThreadingTCPServer): +class ModbusTcpServer(socketserver.ThreadingTCPServer): ''' A modbus threaded tcp socket server @@ -134,7 +134,7 @@ def __init__(self, context, framer=None, identity=None): if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) - SocketServer.ThreadingTCPServer.__init__(self, + socketserver.ThreadingTCPServer.__init__(self, ("", Defaults.Port), ModbusRequestHandler) def process_request(self, request, client): @@ -144,7 +144,7 @@ def process_request(self, request, client): :param client: The address of the client ''' _logger.debug("Started thread to serve client at " + str(client)) - SocketServer.ThreadingTCPServer.process_request(self, request, client) + socketserver.ThreadingTCPServer.process_request(self, request, client) def server_close(self): ''' Callback for stopping the running server @@ -154,7 +154,7 @@ def server_close(self): for thread in self.threads: thread.running = False -class ModbusUdpServer(SocketServer.ThreadingUDPServer): +class ModbusUdpServer(socketserver.ThreadingUDPServer): ''' A modbus threaded udp socket server @@ -183,7 +183,7 @@ def __init__(self, context, framer=None, identity=None): if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) - SocketServer.ThreadingUDPServer.__init__(self, + socketserver.ThreadingUDPServer.__init__(self, ("", Defaults.Port), ModbusRequestHandler) def process_request(self, request, client): @@ -193,7 +193,7 @@ def process_request(self, request, client): :param client: The address of the client ''' _logger.debug("Started thread to serve client at " + str(client)) - SocketServer.ThreadingUDPServer.process_request(self, request, client) + socketserver.ThreadingUDPServer.process_request(self, request, client) def server_close(self): ''' Callback for stopping the running server @@ -251,7 +251,7 @@ def _connect(self): self.socket = serial.Serial(port=self.device, timeout=self.timeout, bytesize=self.bytesize, stopbits=self.stopbits, baudrate=self.baudrate, parity=self.parity) - except serial.SerialException, msg: + except serial.SerialException as msg: _logger.error(msg) self.close() return self.socket != None diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 9929aa85c..adcf5f03f 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -57,7 +57,7 @@ def execute(self, request): self.socket.connect() packet = self.framer.buildPacket(request) self.socket.send(packet) - except socket.error, msg: + except socket.error as msg: self.socket.close() _logger.debug("Transaction failed. (%s) " % msg) retries -= 1 diff --git a/test/test_bit_messages.py b/test/test_bit_messages.py deleted file mode 100644 index 568de6372..000000000 --- a/test/test_bit_messages.py +++ /dev/null @@ -1,161 +0,0 @@ -''' -Bit Message Test Fixture --------------------------------- -This fixture tests the functionality of all the -bit based request/response messages: - -* Read/Write Discretes -* Read Coils -''' -import unittest, struct -from pymodbus.utilities import packBitsToString -from pymodbus.bit_read_message import * -from pymodbus.bit_read_message import ReadBitsRequestBase -from pymodbus.bit_read_message import ReadBitsResponseBase -from pymodbus.bit_write_message import * -from pymodbus.exceptions import * -from pymodbus.pdu import ModbusExceptions - -#---------------------------------------------------------------------------# -# Mocks -#---------------------------------------------------------------------------# -class Context: - def validate(self, a,b,c): - return False - - def getValues(self, a, b, count): - return [True] * count - -#---------------------------------------------------------------------------# -# Fixture -#---------------------------------------------------------------------------# -class ModbusBitMessageTests(unittest.TestCase): - - #-----------------------------------------------------------------------# - # Setup/TearDown - #-----------------------------------------------------------------------# - - def setUp(self): - ''' - Initializes the test environment and builds request/result - encoding pairs - ''' - pass - - def tearDown(self): - ''' Cleans up the test environment ''' - pass - - #-----------------------------------------------------------------------# - # Read Tests - #-----------------------------------------------------------------------# - - def testReadBitBaseClassMethods(self): - ''' Test basic bit message encoding/decoding ''' - handle = ReadBitsRequestBase(1, 1) - msg = "ReadBitRequest(1,1)" - self.assertEqual(msg, str(handle)) - handle = ReadBitsResponseBase([1,1]) - msg = "ReadBitResponse(2)" - self.assertEqual(msg, str(handle)) - - def testBitReadBaseRequestEncoding(self): - ''' Test basic bit message encoding/decoding ''' - for i in range(20): - handle = ReadBitsRequestBase(i, i) - result = struct.pack('>HH',i, i) - self.assertEqual(handle.encode(), result) - handle.decode(result) - self.assertEqual((handle.address, handle.count), (i,i)) - - def testBitReadBaseResponseEncoding(self): - ''' Test basic bit message encoding/decoding ''' - for i in range(20): - input = [True] * i - handle = ReadBitsResponseBase(input) - result = handle.encode() - handle.decode(result) - self.assertEqual(handle.bits[:i], input) - - def testBitReadBaseResponseHelperMethods(self): - ''' Test the extra methods on a ReadBitsResponseBase ''' - input = [False] * 8 - handle = ReadBitsResponseBase(input) - for i in [1,3,5]: handle.setBit(i, True) - for i in [1,3,5]: handle.resetBit(i) - for i in range(8): - self.assertEqual(handle.getBit(i), False) - - def testBitReadBaseRequests(self): - ''' Test bit read request encoding ''' - messages = { - ReadBitsRequestBase(12, 14) : '\x00\x0c\x00\x0e', - ReadBitsResponseBase([1,0,1,1,0]) : '\x01\x0d', - } - for request, expected in messages.items(): - self.assertEqual(request.encode(), expected) - - def testBitReadMessageExecuteValueErrors(self): - ''' Test bit read request encoding ''' - context = Context() - requests = [ - ReadCoilsRequest(1,0x800), - ReadDiscreteInputsRequest(1,0x800), - ] - for request in requests: - result = request.execute(context) - self.assertEqual(ModbusExceptions.IllegalValue, - result.exception_code) - - def testBitReadMessageExecuteAddressErrors(self): - ''' Test bit read request encoding ''' - context = Context() - requests = [ - ReadCoilsRequest(1,5), - ReadDiscreteInputsRequest(1,5), - ] - for request in requests: - result = request.execute(context) - self.assertEqual(ModbusExceptions.IllegalAddress, - result.exception_code) - - def testBitReadMessageExecuteSuccess(self): - ''' Test bit read request encoding ''' - context = Context() - context.validate = lambda a,b,c: True - requests = [ - ReadCoilsRequest(1,5), - ReadDiscreteInputsRequest(1,5), - ] - for request in requests: - result = request.execute(context) - self.assertEqual(result.bits, [True] * 5) - - #-----------------------------------------------------------------------# - # Write Tests - #-----------------------------------------------------------------------# - - def testBitWriteBaseRequests(self): - ''' Test bit write request encoding ''' - messages = { - WriteSingleCoilRequest(1, 0xabcd) : '\x00\x01\xff\x00', - WriteSingleCoilResponse(1, 0xabcd) : '\x00\x01\xff\x00', - WriteMultipleCoilsRequest(1, [True]*5) : '\x00\x01\x00\x05\x01\x1f', - WriteMultipleCoilsResponse(1, 5) : '\x00\x01\x00\x05', - } - for request, expected in messages.items(): - self.assertEqual(request.encode(), expected) - - def testWriteMultipleCoilsRequest(self): - ''' Test bit write request encoding ''' - request = WriteMultipleCoilsRequest(1, [True]*5) - request.decode('\x00\x01\x00\x05\x01\x1f') - self.assertEqual(request.byte_count, 1) - self.assertEqual(request.address, 1) - self.assertEqual(request.values, [True]*5) - -#---------------------------------------------------------------------------# -# Main -#---------------------------------------------------------------------------# -if __name__ == "__main__": - unittest.main() diff --git a/test/test_bit_read_messages.py b/test/test_bit_read_messages.py index 7f794751a..e65abe284 100644 --- a/test/test_bit_read_messages.py +++ b/test/test_bit_read_messages.py @@ -15,7 +15,7 @@ from pymodbus.exceptions import * from pymodbus.pdu import ModbusExceptions -from modbus_mocks import MockContext +from .modbus_mocks import MockContext #---------------------------------------------------------------------------# # Fixture @@ -79,7 +79,7 @@ def testBitReadBaseRequests(self): ReadBitsRequestBase(12, 14) : '\x00\x0c\x00\x0e', ReadBitsResponseBase([1,0,1,1,0]) : '\x01\x0d', } - for request, expected in messages.iteritems(): + for request, expected in messages.items(): self.assertEqual(request.encode(), expected) def testBitReadMessageExecuteValueErrors(self): diff --git a/test/test_bit_write_messages.py b/test/test_bit_write_messages.py index e2f48a48a..cc1e14f59 100644 --- a/test/test_bit_write_messages.py +++ b/test/test_bit_write_messages.py @@ -13,7 +13,7 @@ from pymodbus.exceptions import * from pymodbus.pdu import ModbusExceptions -from modbus_mocks import MockContext, FakeList +from .modbus_mocks import MockContext, FakeList #---------------------------------------------------------------------------# # Fixture @@ -42,7 +42,7 @@ def testBitWriteBaseRequests(self): WriteMultipleCoilsRequest(1, [True]*5) : '\x00\x01\x00\x05\x01\x1f', WriteMultipleCoilsResponse(1, 5) : '\x00\x01\x00\x05', } - for request, expected in messages.iteritems(): + for request, expected in messages.items(): self.assertEqual(request.encode(), expected) def testWriteMultipleCoilsRequest(self): diff --git a/test/test_device.py b/test/test_device.py index 994da0a74..e3b20af40 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -66,7 +66,7 @@ def testModbusDeviceIdentificationGet(self): def testModbusDeviceIdentificationSummary(self): ''' Test device identification summary creation ''' summary = sorted(self.ident.summary().values()) - expected = sorted(self.info.values()[:-3]) # remove private + expected = sorted(list(self.info.values())[:-3]) # remove private self.assertEqual(summary, expected) def testModbusDeviceIdentificationSet(self): diff --git a/test/test_exceptions.py b/test/test_exceptions.py index c92494218..99f69dc01 100644 --- a/test/test_exceptions.py +++ b/test/test_exceptions.py @@ -23,10 +23,10 @@ def tearDown(self): def testExceptions(self): ''' Test all module exceptions ''' - for ex in self.exceptions: + for exception in self.exceptions: try: - raise ex - except ModbusException, ex: + raise exception + except ModbusException as ex: self.assertTrue("Modbus Error:" in str(ex)) pass else: self.fail("Excepted a ModbusExceptions") diff --git a/test/test_file_message.py b/test/test_file_message.py index 34fe260d0..3dce7b138 100644 --- a/test/test_file_message.py +++ b/test/test_file_message.py @@ -13,7 +13,7 @@ from pymodbus.exceptions import * from pymodbus.pdu import ModbusExceptions -from modbus_mocks import MockContext +from .modbus_mocks import MockContext #---------------------------------------------------------------------------# # Fixture diff --git a/test/test_register_messages.py b/test/test_register_messages.py deleted file mode 100644 index a271a1695..000000000 --- a/test/test_register_messages.py +++ /dev/null @@ -1,111 +0,0 @@ -''' -Register Message Test Fixture --------------------------------- -This fixture tests the functionality of all the -register based request/response messages: - -* Read/Write Input Registers -* Read Holding Registers -''' -import unittest -from pymodbus.register_read_message import * -from pymodbus.register_read_message import ReadRegistersRequestBase -from pymodbus.register_read_message import ReadRegistersResponseBase -from pymodbus.register_write_message import * -from pymodbus.exceptions import * -from pymodbus.pdu import ModbusExceptions - -#---------------------------------------------------------------------------# -# Mocks -#---------------------------------------------------------------------------# -class Context(object): - def validate(self, a,b,c): - return False - - def getValues(self, a, b, count): - return [1] * count -#---------------------------------------------------------------------------# -# Fixture -#---------------------------------------------------------------------------# -class RegisterMessagesTest(unittest.TestCase): - - def setUp(self): - ''' - Initializes the test environment and builds request/result - encoding pairs - ''' - self.value = 0xabcd - self.values = [0xa, 0xb, 0xc] - self.rread = { - ReadRegistersRequestBase(1, 5) :'\x00\x01\x00\x05', - ReadRegistersResponseBase(self.values) :'\x06\x00\x0a\x00\x0b\x00\x0c', - ReadHoldingRegistersRequest(1, 5) :'\x00\x01\x00\x05', - ReadHoldingRegistersResponse(self.values) :'\x06\x00\x0a\x00\x0b\x00\x0c', - ReadInputRegistersRequest(1,5) :'\x00\x01\x00\x05', - ReadInputRegistersResponse(self.values) :'\x06\x00\x0a\x00\x0b\x00\x0c', - ReadWriteMultipleRegistersRequest(1,5,1,5) :'\x00\x01\x00\x05\x00\x01\x00' - '\x05\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', - ReadWriteMultipleRegistersResponse(self.values) :'\x06\x00\x0a\x00\x0b\x00\x0c', - } - - self.rwrite = { - WriteSingleRegisterRequest(1, self.value) : '\x00\x01\xab\xcd', - WriteSingleRegisterResponse(1, self.value) : '\x00\x01\xab\xcd', - WriteMultipleRegistersRequest(1, 5) : '\x00\x01\x00\x05\x0a\x00\x00\x00\x00\x00\x00' - '\x00\x00\x00\x00', - WriteMultipleRegistersResponse(1, 5) : '\x00\x01\x00\x05', - } - - def tearDown(self): - ''' Cleans up the test environment ''' - del self.rread - del self.rwrite - - def testRegisterReadRequests(self): - ''' Test register read request encoding ''' - for rqst, rsp in self.rread.items(): - self.assertEqual(rqst.encode(), rsp) - - def testRegisterReadRequestsCountErrors(self): - ''' - This tests that the register request messages - will break on counts that are out of range - ''' - requests = [ - ReadHoldingRegistersRequest(1, 0x800), - ReadInputRegistersRequest(1,0x800), - ReadWriteMultipleRegistersRequest(1,0x800,1,5), - ReadWriteMultipleRegistersRequest(1,5,1,0x800), - ] - for request in requests: - result = request.execute(None) - self.assertEqual(ModbusExceptions.IllegalValue, - result.exception_code) - - def testRegisterReadRequestsValidateErrors(self): - ''' - This tests that the register request messages - will break on counts that are out of range - ''' - context = Context() - requests = [ - ReadHoldingRegistersRequest(-1, 5), - ReadInputRegistersRequest(-1,5), - #ReadWriteMultipleRegistersRequest(-1,5,1,5), - #ReadWriteMultipleRegistersRequest(1,5,-1,5), - ] - for request in requests: - result = request.execute(context) - self.assertEqual(ModbusExceptions.IllegalAddress, - result.exception_code) - - def testRegisterWriteRequests(self): - ''' Test register write request encoding ''' - for rqst, rsp in self.rwrite.items(): - self.assertEqual(rqst.encode(), rsp) - -#---------------------------------------------------------------------------# -# Main -#---------------------------------------------------------------------------# -if __name__ == "__main__": - unittest.main() diff --git a/test/test_register_read_messages.py b/test/test_register_read_messages.py index 6ffc9e2c5..0445f2a7b 100644 --- a/test/test_register_read_messages.py +++ b/test/test_register_read_messages.py @@ -6,7 +6,7 @@ from pymodbus.exceptions import * from pymodbus.pdu import ModbusExceptions -from modbus_mocks import MockContext, FakeList +from .modbus_mocks import MockContext, FakeList #---------------------------------------------------------------------------# # Fixture diff --git a/test/test_register_write_messages.py b/test/test_register_write_messages.py index 107fbdfec..3ff2819cf 100644 --- a/test/test_register_write_messages.py +++ b/test/test_register_write_messages.py @@ -4,7 +4,7 @@ from pymodbus.exceptions import ParameterException from pymodbus.pdu import ModbusExceptions -from modbus_mocks import MockContext +from .modbus_mocks import MockContext #---------------------------------------------------------------------------# # Fixture diff --git a/test/test_remote_datastore.py b/test/test_remote_datastore.py index 8a88998db..a37ef4f49 100644 --- a/test/test_remote_datastore.py +++ b/test/test_remote_datastore.py @@ -6,7 +6,7 @@ from pymodbus.bit_write_message import * from pymodbus.register_read_message import * from pymodbus.pdu import ExceptionResponse -from modbus_mocks import mock +from .modbus_mocks import mock class RemoteModbusDataStoreTest(unittest.TestCase): ''' diff --git a/test/test_server_context.py b/test/test_server_context.py index 2d928b441..1dba934c1 100644 --- a/test/test_server_context.py +++ b/test/test_server_context.py @@ -73,9 +73,9 @@ def testMultipleContextDefault(self): def testMultipleContextSet(self): ''' Test a setting multiple slave contexts ''' slaves = dict((id, ModbusSlaveContext()) for id in range(10)) - for id, slave in slaves.iteritems(): + for id, slave in slaves.items(): self.context[id] = slave - for id, slave in slaves.iteritems(): + for id, slave in slaves.items(): actual = self.context[id] self.assertEqual(slave, actual) From 2b687c9bdfa1c12143acb7ce3b9c3aecd8a5307a Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 11 May 2011 15:00:51 +0000 Subject: [PATCH 016/243] reverting back changes, remember Switch flag next time --- doc/api/epydoc/build.py | 2 +- doc/api/pydoc/build.py | 14 +- doc/current.coverage | 29 -- doc/sphinx/client.rst | 51 --- doc/sphinx/library/datastore.rst | 26 -- doc/sphinx/logging.rst | 28 -- examples/common/modbus-scraper.py | 4 +- examples/common/modbus-simulator.py | 10 +- examples/common/performance.py | 2 +- .../functional/asynchronous-ascii-client.py | 2 +- .../functional/asynchronous-rtu-client.py | 2 +- .../functional/asynchronous-tcp-client.py | 2 +- .../functional/asynchronous-udp-client.py | 2 +- examples/functional/database-slave-context.py | 2 +- examples/functional/redis-slave-context.py | 2 +- .../functional/synchronous-ascii-client.py | 2 +- examples/functional/synchronous-rtu-client.py | 2 +- examples/functional/synchronous-tcp-client.py | 2 +- examples/functional/synchronous-udp-client.py | 2 +- examples/gui/gtk/simulator.py | 10 +- examples/gui/gui-common.py | 2 +- examples/gui/tk/simulator.py | 12 +- examples/gui/web/frontend.py | 2 +- examples/gui/wx/simulator.py | 10 +- examples/tools/build-datastore.py | 11 +- examples/tools/convert.py | 6 +- examples/tools/reindent.py | 8 +- pymodbus/client/sync.py | 8 +- pymodbus/datastore.py | 395 ------------------ pymodbus/datastore/context.py | 2 +- pymodbus/datastore/store.py | 10 +- pymodbus/device.py | 11 +- pymodbus/factory.py | 4 +- pymodbus/file_message.py | 2 +- pymodbus/other_message.py | 2 +- pymodbus/pdu.py | 2 +- pymodbus/server/async.py | 4 +- pymodbus/server/sync.py | 24 +- pymodbus/transaction.py | 2 +- test/test_bit_read_messages.py | 10 +- test/test_bit_write_messages.py | 4 +- test/test_device.py | 2 +- test/test_exceptions.py | 6 +- test/test_file_message.py | 2 +- test/test_register_read_messages.py | 2 +- test/test_register_write_messages.py | 2 +- test/test_remote_datastore.py | 2 +- test/test_server_context.py | 12 +- 48 files changed, 114 insertions(+), 641 deletions(-) delete mode 100644 doc/current.coverage delete mode 100644 doc/sphinx/client.rst delete mode 100644 doc/sphinx/library/datastore.rst delete mode 100644 doc/sphinx/logging.rst delete mode 100644 pymodbus/datastore.py diff --git a/doc/api/epydoc/build.py b/doc/api/epydoc/build.py index f8094ac98..b6c890f62 100755 --- a/doc/api/epydoc/build.py +++ b/doc/api/epydoc/build.py @@ -32,7 +32,7 @@ if os.path.exists('../../../build'): shutil.move("html", "../../../build/epydoc") -except Exception as ex: +except Exception, ex: import traceback,sys traceback.print_exc(file=sys.stdout) print "Epydoc not avaliable...not building" diff --git a/doc/api/pydoc/build.py b/doc/api/pydoc/build.py index a41c801b7..614985561 100644 --- a/doc/api/pydoc/build.py +++ b/doc/api/pydoc/build.py @@ -51,7 +51,7 @@ def classify_class_attrs(cls): else: try: obj = getattr(cls, name) - except AttributeError as err: + except AttributeError, err: continue # Figure out where it was defined. @@ -200,7 +200,7 @@ def docmodule(self, object, name=None, mod=None, packageContext = None, *ignored for key, value in data: try: contents.append(self.document(value, key)) - except Exception as err: + except Exception, err: pass result = result + self.bigsection( 'Data', '#ffffff', '#55aa55', join(contents, '
\n')) @@ -327,7 +327,7 @@ def __init__ ( for exclusion in exclusions: try: self.exclusions[ exclusion ]= pydoc.locate ( exclusion) - except pydoc.ErrorDuringImport as value: + except pydoc.ErrorDuringImport, value: self.warn( """Unable to import the module %s which was specified as an exclusion module"""% (repr(exclusion))) self.formatter = formatter or DefaultFormatter() for base in baseModules: @@ -344,7 +344,7 @@ def addBase(self, specifier): try: self.baseSpecifiers [specifier] = pydoc.locate ( specifier) self.pending.append (specifier) - except pydoc.ErrorDuringImport as value: + except pydoc.ErrorDuringImport, value: self.warn( """Unable to import the module %s which was specified as a base module"""% (repr(specifier))) def addInteresting( self, specifier): """Add a module to the list of interesting modules""" @@ -387,13 +387,13 @@ def process( self ): self.info( """ ... found %s"""% (repr(object.__name__))) except AlreadyDone: pass - except pydoc.ErrorDuringImport as value: + except pydoc.ErrorDuringImport, value: self.info( """ ... FAILED %s"""% (repr( value))) self.warn( """Unable to import the module %s"""% (repr(self.pending[0]))) - except (SystemError, SystemExit) as value: + except (SystemError, SystemExit), value: self.info( """ ... FAILED %s"""% (repr( value))) self.warn( """Unable to import the module %s"""% (repr(self.pending[0]))) - except Exception as value: + except Exception, value: self.info( """ ... FAILED %s"""% (repr( value))) self.warn( """Unable to import the module %s"""% (repr(self.pending[0]))) else: diff --git a/doc/current.coverage b/doc/current.coverage deleted file mode 100644 index ff911a104..000000000 --- a/doc/current.coverage +++ /dev/null @@ -1,29 +0,0 @@ -Name Stmts Exec Cover Missing ---------------------------------------------------------------- -pymodbus 9 9 100% -pymodbus.bit_read_message 64 64 100% -pymodbus.bit_write_message 88 62 70% 73-81, 88, 129, 157, 174-176, 184-192, 199, 230, 237 -pymodbus.client 1 1 100% -pymodbus.client.async 86 32 37% 55-61, 67-72, 77-78, 87-90, 104-105, 112-113, 121-124, 130, 160-172, 183, 190-194, 201, 214-226 -pymodbus.constants 18 18 100% -pymodbus.datastore 99 60 60% 81-82, 86, 95, 104, 112, 119, 126-128, 141, 152-154, 163-164, 172-173, 186-192, 201-202, 211, 219-220, 260, 264-265, 275-276, 286-287, 296-297 -pymodbus.device 112 112 100% -pymodbus.diag_message 183 168 91% 49, 62, 104, 131, 165-168, 175, 190-193, 212, 242 -pymodbus.exceptions 22 22 100% -pymodbus.factory 52 46 88% 56-58, 107-109 -pymodbus.file_message 36 33 91% 45, 52, 62 -pymodbus.interfaces 31 21 67% 36, 56, 70, 78, 88, 98, 105, 115, 132, 143 -pymodbus.other_message 73 40 54% 29, 34, 41, 48-49, 66-67, 74, 81, 111, 116, 123, 130-131, 148-150, 157-158, 165-166, 186, 191, 198, 205-206, 221-223, 230-231, 238-239 -pymodbus.pdu 58 58 100% -pymodbus.register_read_message 119 93 78% 45, 86, 93, 124-125, 172-173, 223, 244-249, 262-270, 277, 311-313, 320 -pymodbus.register_write_message 79 54 68% 51-57, 64, 102, 128, 146-148, 156-164, 171, 202, 209 -pymodbus.server 0 0 100% -pymodbus.transaction 218 130 59% 48-60, 225-235, 315-316, 334, 341, 371-380, 387-392, 510-519, 566-570, 580-593, 601-602, 611, 620, 627, 637, 657-666, 674-679, 690-692 -pymodbus.utilities 53 53 100% -pymodbus.version 4 4 100% ---------------------------------------------------------------- -TOTAL 1405 1080 76% ----------------------------------------------------------------------- -Ran 85 tests in 0.136s - -OK diff --git a/doc/sphinx/client.rst b/doc/sphinx/client.rst deleted file mode 100644 index 3f2a67118..000000000 --- a/doc/sphinx/client.rst +++ /dev/null @@ -1,51 +0,0 @@ -================================= -Implementation of a Modbus Client -================================= - -This attempts to fire off requets in succession so as to work as fast as -possible, but still refrain from overloading the remote device (usually -very mediocre in hardware) - -Example Run:: - - def clientTest(): - requests = [ ReadCoilsRequest(0,99) ] - p = reactor.connectTCP("localhost", 502, ModbusClientFactory(requests)) - - if __name__ == "__main__": - reactor.callLater(1, clientTest) - reactor.run() - -What follows is a quick layout of the client logic: - 1. Build request array and instantiate a client factory - 2. Defer it until the reactor is running - 3. Upon connection, instantiate the producer and pass it: - - * A handle to the transport - * A handle to the request array - * A handle to a sent request handler - * A handle to the current framing object - - 4. It then sends a request and waits - 5. The protocol recieves data and processes its frame: - - * If we have a valid frame, we decode it and add the result(7) - * Otherwise we continue(6) - - 6. Afterwards, we instruct the producer to send the next request - 7. Upon adding a result: - - * The factory uses the handler object to translate the TID to a request - * Using the request paramaters, we corretly store the resulting data - * Each result is put into the appropriate store - - 7. When all the requests have been processed: - - * we stop the producer - * disconnect the protocol - * return the factory results - -Todo: - * Build a repeated request producer? - * Simplify request <-> response linking - diff --git a/doc/sphinx/library/datastore.rst b/doc/sphinx/library/datastore.rst deleted file mode 100644 index 557d86085..000000000 --- a/doc/sphinx/library/datastore.rst +++ /dev/null @@ -1,26 +0,0 @@ -:mod:`datastore` --- Datastore for Modbus Server Context -============================================================ - -.. module:: datastore - :synopsis: Datastore for Modbus Server Context - -.. moduleauthor:: Galen Collins -.. sectionauthor:: Galen Collins - -API Documentation -------------------- - -.. automodule:: pymodbus.datastore - -.. autoclass:: ModbusDataBlock - :members: - -.. autoclass:: ModbusSequentialDataBlock - :members: - -.. autoclass:: ModbusSparseDataBlock - :members: - -.. autoclass:: ModbusServerContext - :members: - diff --git a/doc/sphinx/logging.rst b/doc/sphinx/logging.rst deleted file mode 100644 index 992e232e4..000000000 --- a/doc/sphinx/logging.rst +++ /dev/null @@ -1,28 +0,0 @@ -=================== -Logging in PyModbus -=================== - -Use the following example as start to enable logging in pymodbus:: - - import logging - - # Get handles to the various logs - server_log = logging.getLogger("pysnmp.server") - client_log = logging.getLogger("pysnmp.client") - protocol_log = logging.getLogger("pysnmp.protocol") - store_log = logging.getLogger("pysnmp.store") - - # Enable logging levels - server_log.setLevel(logging.DEBUG) - protocol_log.setLevel(logging.DEBUG) - client_log.setLevel(logging.DEBUG) - store_log.setLevel(logging.DEBUG) - - # Initialize the logging - try: - logging.basicConfig() - except Exception, e: - print "Logging is not supported on this system" - -This can be included in a working project as a separate module -and then used by the rest of the project. diff --git a/examples/common/modbus-scraper.py b/examples/common/modbus-scraper.py index 753492319..301d565c2 100644 --- a/examples/common/modbus-scraper.py +++ b/examples/common/modbus-scraper.py @@ -109,7 +109,7 @@ def main(): try: client_log.setLevel(logging.DEBUG) logging.basicConfig() - except Exception as e: + except Exception, e: print "Logging is not supported on this system" # Begin scraping @@ -118,7 +118,7 @@ def main(): s = ClientScraper(opt.host, opt.port, opt.range) reactor.callWhenRunning(s.start) reactor.run() - except ClientException as err: + except ClientException, err: print err parser.print_help() diff --git a/examples/common/modbus-simulator.py b/examples/common/modbus-simulator.py index 8fdd420c6..029199546 100644 --- a/examples/common/modbus-simulator.py +++ b/examples/common/modbus-simulator.py @@ -107,15 +107,15 @@ def main(): try: server_log.setLevel(logging.DEBUG) protocol_log.setLevel(logging.DEBUG) - except Exception as e: - print("Logging is not supported on this system") + except Exception, e: + print "Logging is not supported on this system" # parse configuration file and run try: conf = Configuration(opt.file) StartTcpServer(context=conf.parse()) - except ConfigurationException as err: - print(err) + except ConfigurationException, err: + print err parser.print_help() #---------------------------------------------------------------------------# @@ -124,5 +124,5 @@ def main(): if __name__ == "__main__": if root_test(): main() - else: print("This script must be run as root!") + else: print "This script must be run as root!" diff --git a/examples/common/performance.py b/examples/common/performance.py index 9d114b1c5..cbe04c5f3 100755 --- a/examples/common/performance.py +++ b/examples/common/performance.py @@ -31,5 +31,5 @@ # check our results #---------------------------------------------------------------------------# stop = time() -print("%d requests/second" % ((1.0 * count) / (stop - start))) +print "%d requests/second" % ((1.0 * count) / (stop - start)) diff --git a/examples/functional/asynchronous-ascii-client.py b/examples/functional/asynchronous-ascii-client.py index 06a9b06b7..afb19cc37 100644 --- a/examples/functional/asynchronous-ascii-client.py +++ b/examples/functional/asynchronous-ascii-client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.client.async import ModbusSerialClient as ModbusClient -from .base_runner import Runner +from base_runner import Runner class AsynchronousAsciiClient(Runner, unittest.TestCase): ''' diff --git a/examples/functional/asynchronous-rtu-client.py b/examples/functional/asynchronous-rtu-client.py index 30c4299c0..f4c84c78d 100644 --- a/examples/functional/asynchronous-rtu-client.py +++ b/examples/functional/asynchronous-rtu-client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.client.async import ModbusSerialClient as ModbusClient -from .base_runner import Runner +from base_runner import Runner class AsynchronousRtuClient(Runner, unittest.TestCase): ''' diff --git a/examples/functional/asynchronous-tcp-client.py b/examples/functional/asynchronous-tcp-client.py index 7bc29c7d8..4603b56b4 100644 --- a/examples/functional/asynchronous-tcp-client.py +++ b/examples/functional/asynchronous-tcp-client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.client.async import ModbusTcpClient as ModbusClient -from .base_runner import Runner +from base_runner import Runner class AsynchronousTcpClient(Runner, unittest.TestCase): ''' diff --git a/examples/functional/asynchronous-udp-client.py b/examples/functional/asynchronous-udp-client.py index cfcf5382b..e5f993560 100644 --- a/examples/functional/asynchronous-udp-client.py +++ b/examples/functional/asynchronous-udp-client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.client.sync import ModbusUdpClient as ModbusClient -from .base_runner import Runner +from base_runner import Runner class AsynchronousUdpClient(Runner, unittest.TestCase): ''' diff --git a/examples/functional/database-slave-context.py b/examples/functional/database-slave-context.py index a331eee63..c4269fcbd 100644 --- a/examples/functional/database-slave-context.py +++ b/examples/functional/database-slave-context.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest, os from pymodbus.datastore.database import DatabaseSlaveContext -from .base_context import ContextRunner +from base_context import ContextRunner class DatabaseSlaveContextTest(ContextRunner, unittest.TestCase): ''' diff --git a/examples/functional/redis-slave-context.py b/examples/functional/redis-slave-context.py index 678ed59e2..364aa50dd 100644 --- a/examples/functional/redis-slave-context.py +++ b/examples/functional/redis-slave-context.py @@ -3,7 +3,7 @@ import os from subprocess import Popen as execute from pymodbus.datastore.modredis import RedisSlaveContext -from .base_context import ContextRunner +from base_context import ContextRunner class RedisSlaveContextTest(ContextRunner, unittest.TestCase): ''' diff --git a/examples/functional/synchronous-ascii-client.py b/examples/functional/synchronous-ascii-client.py index 8ff421980..ad96c6c5c 100644 --- a/examples/functional/synchronous-ascii-client.py +++ b/examples/functional/synchronous-ascii-client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.client.sync import ModbusSerialClient as ModbusClient -from .base_runner import Runner +from base_runner import Runner class SynchronousAsciiClient(Runner, unittest.TestCase): ''' diff --git a/examples/functional/synchronous-rtu-client.py b/examples/functional/synchronous-rtu-client.py index aa4033b54..8f1b5f5b1 100644 --- a/examples/functional/synchronous-rtu-client.py +++ b/examples/functional/synchronous-rtu-client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.client.sync import ModbusSerialClient as ModbusClient -from .base_runner import Runner +from base_runner import Runner class SynchronousRtuClient(Runner, unittest.TestCase): ''' diff --git a/examples/functional/synchronous-tcp-client.py b/examples/functional/synchronous-tcp-client.py index 429bf1e7b..4e6e904d5 100644 --- a/examples/functional/synchronous-tcp-client.py +++ b/examples/functional/synchronous-tcp-client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.client.sync import ModbusTcpClient as ModbusClient -from .base_runner import Runner +from base_runner import Runner class SynchronousTcpClient(Runner, unittest.TestCase): ''' diff --git a/examples/functional/synchronous-udp-client.py b/examples/functional/synchronous-udp-client.py index c2b39a93b..9507ab78b 100644 --- a/examples/functional/synchronous-udp-client.py +++ b/examples/functional/synchronous-udp-client.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.client.sync import ModbusUdpClient as ModbusClient -from .base_runner import Runner +from base_runner import Runner class SynchronousUdpClient(Runner, unittest.TestCase): ''' diff --git a/examples/gui/gtk/simulator.py b/examples/gui/gtk/simulator.py index d658e5573..ea0fb51e2 100755 --- a/examples/gui/gtk/simulator.py +++ b/examples/gui/gtk/simulator.py @@ -91,11 +91,11 @@ def _parse(self): def _simulator(self): ''' Starts the snmp simulator ''' - ports = [502]+(range(20000,25000)) + ports = [502]+range(20000,25000) for port in ports: try: reactor.listenTCP(port, ModbusServerFactory(self._parse())) - print('listening on port %d' % port) + print 'listening on port', port return port except twisted_error.CannotListenError: pass @@ -265,7 +265,7 @@ def start_clicked(self, widget): try: handle = Simulator(config=self.file) handle.run() - except ConfigurationException as ex: + except ConfigurationException, ex: self.error_dialog("Error %s" % ex) self.show_buttons(state=True) else: @@ -310,8 +310,8 @@ def main(): try: log.setLevel(logging.DEBUG) logging.basicConfig() - except Exception as e: - print("Logging is not supported on this system") + except Exception, e: + print "Logging is not supported on this system" simulator = SimulatorApp('./simulator.glade') reactor.run() diff --git a/examples/gui/gui-common.py b/examples/gui/gui-common.py index bea69d72d..c78e595d1 100755 --- a/examples/gui/gui-common.py +++ b/examples/gui/gui-common.py @@ -77,7 +77,7 @@ def _parse(self): def _simulator(self): ''' Starts the snmp simulator ''' - ports = [502]+list(range(20000,25000)) + ports = [502]+range(20000,25000) for port in ports: try: reactor.listenTCP(port, ModbusServerFactory(self._parse())) diff --git a/examples/gui/tk/simulator.py b/examples/gui/tk/simulator.py index 14721b03d..13cc767c9 100755 --- a/examples/gui/tk/simulator.py +++ b/examples/gui/tk/simulator.py @@ -13,8 +13,8 @@ #---------------------------------------------------------------------------# # For Gui #---------------------------------------------------------------------------# -from tkinter import * -from tkinter.filedialog import askopenfilename as OpenFilename +from Tkinter import * +from tkFileDialog import askopenfilename as OpenFilename from twisted.internet import tksupport root = Tk() tksupport.install(root) @@ -89,7 +89,7 @@ def _parse(self): def _simulator(self): ''' Starts the snmp simulator ''' - ports = [502]+list(range(20000,25000)) + ports = [502]+range(20000,25000) for port in ports: try: reactor.listenTCP(port, ModbusServerFactory(self._parse())) @@ -264,7 +264,7 @@ def start_clicked(self): try: handle = Simulator(config=filename) handle.run() - except ConfigurationException as ex: + except ConfigurationException, ex: self.error_dialog("Error %s" % ex) self.show_buttons(state=True) else: @@ -324,8 +324,8 @@ def main(): try: log.setLevel(logging.DEBUG) logging.basicConfig() - except Exception as e: - print("Logging is not supported on this system") + except Exception, e: + print "Logging is not supported on this system" simulator = SimulatorApp(root) root.title("Modbus Simulator") reactor.run() diff --git a/examples/gui/web/frontend.py b/examples/gui/web/frontend.py index 0d7421913..b15a2e0b6 100644 --- a/examples/gui/web/frontend.py +++ b/examples/gui/web/frontend.py @@ -112,7 +112,7 @@ def register_routes(application, register): from bottle import route methods = dir(application) - methods = (n for n in methods if not n.startswith('_')) + methods = filter(lambda n: not n.startswith('_'), methods) for method in methods: pieces = method.split('_') verb, path = pieces[0], pieces[1:] diff --git a/examples/gui/wx/simulator.py b/examples/gui/wx/simulator.py index 4f2421295..789ec5623 100755 --- a/examples/gui/wx/simulator.py +++ b/examples/gui/wx/simulator.py @@ -87,11 +87,11 @@ def _parse(self): def _simulator(self): ''' Starts the snmp simulator ''' - ports = [502]+list(range(20000,25000)) + ports = [502]+range(20000,25000) for port in ports: try: reactor.listenTCP(port, ModbusServerFactory(self._parse())) - print('listening on port %d' % port) + print 'listening on port', port return port except twisted_error.CannotListenError: pass @@ -235,7 +235,7 @@ def start_clicked(self, widget): try: handle = Simulator(config=self.file) handle.run() - except ConfigurationException as ex: + except ConfigurationException, ex: self.error_dialog("Error %s" % ex) self.show_buttons(state=True) else: @@ -297,8 +297,8 @@ def main(): try: log.setLevel(logging.DEBUG) logging.basicConfig() - except Exception as e: - print("Logging is not supported on this system") + except Exception, e: + print "Logging is not supported on this system" simulator = SimulatorApp(0) reactor.run() diff --git a/examples/tools/build-datastore.py b/examples/tools/build-datastore.py index f5d67420b..ff1267cb0 100644 --- a/examples/tools/build-datastore.py +++ b/examples/tools/build-datastore.py @@ -6,6 +6,7 @@ dump. This allows users to build their own data from scratch or modifiy an exisiting dump. ''' +from __future__ import with_statement import pickle from sys import exit from optparse import OptionParser @@ -66,7 +67,7 @@ def build_conversion(option, opt, value, parser): raise ConfigurationException("File Not Found %s" % value) with open(value + ".dump", "w") as output: - for dk,dv in data.items(): + for dk,dv in data.iteritems(): output.write("[ %s ]\n\n" % dk) # handle sequential @@ -77,7 +78,7 @@ def build_conversion(option, opt, value, parser): # handle sparse elif isinstance(data[k].values, dict): output.write("\n".join(["[%d] = %d" % (vk,vv) - for vk,vv in dv.values.items()])) + for vk,vv in dv.values.iteritems()])) else: raise ConfigurationException("Datastore is corrupted %s" % value) output.write("\n\n") exit() # So we don't start a dummy build @@ -140,10 +141,10 @@ def main(): with open(opt.file, "w") as output: pickle.dump(result, output) - print("Created datastore: %s\n" % opt.file) + print "Created datastore: %s\n" % opt.file - except ConfigurationException as ex: - print(ex) + except ConfigurationException, ex: + print ex parser.print_help() #---------------------------------------------------------------------------# diff --git a/examples/tools/convert.py b/examples/tools/convert.py index 02e98f193..a21d8df04 100755 --- a/examples/tools/convert.py +++ b/examples/tools/convert.py @@ -131,10 +131,10 @@ def main(): parser = etree.XMLParser(target = ModbusXML()) result = etree.parse(opt.input, parser) store_dump(result, opt.output) - print("Created datastore: %s\n" % opt.output) + print "Created datastore: %s\n" % opt.output - except ConversionException as ex: - print(ex) + except ConversionException, ex: + print ex parser.print_help() #---------------------------------------------------------------------------# diff --git a/examples/tools/reindent.py b/examples/tools/reindent.py index 3801bdb35..658c652fd 100755 --- a/examples/tools/reindent.py +++ b/examples/tools/reindent.py @@ -45,7 +45,7 @@ def main(): global verbose, recurse, dryrun try: opts, args = getopt.getopt(sys.argv[1:], "drv") - except getopt.error as msg: + except getopt.error, msg: errprint(msg) return for o, a in opts: @@ -78,7 +78,7 @@ def check(file): print "checking", file, "...", try: f = open(file) - except IOError as msg: + except IOError, msg: errprint("%s: I/O Error: %s" % (file, str(msg))) return @@ -156,7 +156,7 @@ def run(self): want = have2want.get(have, -1) if want < 0: # Then it probably belongs to the next real stmt. - for j in range(i+1, len(stats)-1): + for j in xrange(i+1, len(stats)-1): jline, jlevel = stats[j] if jlevel >= 0: if have == getlspace(lines[jline]): @@ -166,7 +166,7 @@ def run(self): # comment like this one, # in which case we should shift it like its base # line got shifted. - for j in range(i-1, -1, -1): + for j in xrange(i-1, -1, -1): jline, jlevel = stats[j] if jlevel >= 0: want = have + getlspace(after[jline-1]) - \ diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 455a497fc..212e0bacf 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -51,7 +51,7 @@ def execute(self, request): result = self.client._recv(1024) self.client.framer.processIncomingPacket(result, self.__set_result) break; - except socket.error as msg: + except socket.error, msg: self.client.close() _logger.debug("Transaction failed. (%s) " % msg) retries -= 1 @@ -190,7 +190,7 @@ def connect(self): self.socket.settimeout(Defaults.Timeout) self.socket.connect((self.host, self.port)) self.transaction = ModbusTransactionManager(self) - except socket.error as msg: + except socket.error, msg: _logger.error('Connection to (%s, %s) failed: %s' % \ (self.host, self.port, msg)) self.close() @@ -256,7 +256,7 @@ def connect(self): try: self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #self.socket.bind(('localhost', Defaults.Port)) - except socket.error as ex: + except socket.error, ex: _logger.error('Unable to create udp socket %s' % ex) self.close() return self.socket != None @@ -344,7 +344,7 @@ def connect(self): self.socket = serial.Serial(port=self.port, timeout=self.timeout, bytesize=self.bytesize, stopbits=self.stopbits, baudrate=self.baudrate, parity=self.parity) - except serial.SerialException as msg: + except serial.SerialException, msg: _logger.error(msg) self.close() return self.socket != None diff --git a/pymodbus/datastore.py b/pymodbus/datastore.py deleted file mode 100644 index e9dad41d0..000000000 --- a/pymodbus/datastore.py +++ /dev/null @@ -1,395 +0,0 @@ -""" -Modbus Server Datastore -------------------------- - -For each server, you will create a ModbusServerContext and pass -in the default address space for each data access. The class -will create and manage the data. - -Further modification of said data accesses should be performed -with [get,set][access]Values(address, count) - -Datastore Implementation -------------------------- - -There are two ways that the server datastore can be implemented. -The first is a complete range from 'address' start to 'count' -number of indecies. This can be thought of as a straight array:: - - data = range(1, 1 + count) - [1,2,3,...,count] - -The other way that the datastore can be implemented (and how -many devices implement it) is a associate-array:: - - data = {1:'1', 3:'3', ..., count:'count'} - [1,3,...,count] - -The difference between the two is that the latter will allow -arbitrary gaps in its datastore while the former will not. -This is seen quite commonly in some modbus implementations. -What follows is a clear example from the field: - -Say a company makes two devices to monitor power usage on a rack. -One works with three-phase and the other with a single phase. The -company will dictate a modbus data mapping such that registers:: - - n: phase 1 power - n+1: phase 2 power - n+2: phase 3 power - -Using this, layout, the first device will implement n, n+1, and n+2, -however, the second device may set the latter two values to 0 or -will simply not implmented the registers thus causing a single read -or a range read to fail. - -I have both methods implemented, and leave it up to the user to change -based on their preference. -""" -from pymodbus.utilities import default -from pymodbus.exceptions import * -from pymodbus.interfaces import IModbusSlaveContext - -#---------------------------------------------------------------------------# -# Logging -#---------------------------------------------------------------------------# -import logging; -_logger = logging.getLogger("pymodbus.protocol") - -#---------------------------------------------------------------------------# -# Datablock Storage -#---------------------------------------------------------------------------# -class BaseModbusDataBlock(object): - ''' - Base class for a modbus datastore - - Derived classes must create the following fields: - @address The starting address point - @defult_value The default value of the datastore - @values The actual datastore values - - Derived classes must implemented the following methods: - validate(self, address, count=1) - getValues(self, address, count=1) - setValues(self, address, values) - ''' - - def default(self, count, value=False): - ''' Used to initialize a store to one value - - :param count: The number of fields to set - :param value: The default value to set to the fields - ''' - self.default_value = value - self.values = [self.default_value] * count - - def reset(self): - ''' Resets the datastore to the initialized default value ''' - self.values = [self.default_value] * len(self.values) - - def validate(self, address, count=1): - ''' Checks to see if the request is in range - - :param address: The starting address - :param count: The number of values to test for - :returns: True if the request in within range, False otherwise - ''' - raise NotImplementedException("Datastore Address Check") - - def getValues(self, address, count=1): - ''' Returns the requested values from the datastore - - :param address: The starting address - :param count: The number of values to retrieve - :returns: The requested values from a:a+c - ''' - raise NotImplementedException("Datastore Value Retrieve") - - def setValues(self, address, values): - ''' Returns the requested values from the datastore - - :param address: The starting address - :param values: The values to store - ''' - raise NotImplementedException("Datastore Value Retrieve") - - def __str__(self): - ''' Build a representation of the datastore - - :returns: A string representation of the datastore - ''' - return "DataStore(%d, %d)" % (self.address, self.default_value) - - def __iter__(self): - ''' Iterater over the data block data - - :returns: An iterator of the data block data - ''' - if isinstance(dict, self.values): - return self.values.items() - return enumerate(self.values) - -class ModbusSequentialDataBlock(BaseModbusDataBlock): - ''' Creates a sequential modbus datastore ''' - - def __init__(self, address, values): - ''' Initializes the datastore - - :param address: The starting address of the datastore - :param values: Either a list or a dictionary of values - ''' - self.address = address - if isinstance(values, list): - self.values = values - else: self.values = [values] - self.default_value = self.values[0].__class__() - - def validate(self, address, count=1): - ''' Checks to see if the request is in range - - :param address: The starting address - :param count: The number of values to test for - :returns: True if the request in within range, False otherwise - ''' - result = (self.address <= address) - result &= ((self.address + len(self.values)) >= (address + count)) - return result - - def getValues(self, address, count=1): - ''' Returns the requested values of the datastore - - :param address: The starting address - :param count: The number of values to retrieve - :returns: The requested values from a:a+c - ''' - start = address - self.address - return self.values[start:start+count] - - def setValues(self, address, values): - ''' Sets the requested values of the datastore - - :param address: The starting address - :param values: The new values to be set - ''' - start = address - self.address - self.values[start:start+len(values)] = values - -class ModbusSparseDataBlock(BaseModbusDataBlock): - ''' Creates a sparse modbus datastore ''' - - def __init__(self, values): - ''' Initializes the datastore - - Using the input values we create the default - datastore value and the starting address - - :param values: Either a list or a dictionary of values - ''' - if isinstance(values, dict): - self.values = values - elif isinstance(values, list): - self.values = dict([(i,v) for i,v in enumerate(values)]) - else: raise ParameterException("Values for datastore must be a list or dictionary") - self.default_value = default(self.values) - self.address = next(self.values.keys()) - - def validate(self, address, count=1): - ''' Checks to see if the request is in range - - :param address: The starting address - :param count: The number of values to test for - :returns: True if the request in within range, False otherwise - ''' - handle = list(range(address, address + count)) - return set(handle).issubset(set(self.values.keys())) - - def getValues(self, address, count=1): - ''' Returns the requested values of the datastore - - :param address: The starting address - :param count: The number of values to retrieve - :returns: The requested values from a:a+c - ''' - return [self.values[i] for i in range(address, address + count)] - - def setValues(self, address, values): - ''' Sets the requested values of the datastore - - :param address: The starting address - :param values: The new values to be set - ''' - for idx,val in enumerate(values): - self.values[address + idx] = val - -#---------------------------------------------------------------------------# -# Device Data Control -#---------------------------------------------------------------------------# -class ModbusSlaveContext(IModbusSlaveContext): - ''' - This creates a modbus data model with each data access - stored in its own personal block - ''' - - def __init__(self, *args, **kwargs): - ''' Initializes the datastores - :param kwargs: Each element is a ModbusDataBlock - - 'di' - Discrete Inputs initializer - 'co' - Coils initializer - 'hr' - Holding Register initializer - 'ir' - Input Registers iniatializer - ''' - self.di = kwargs.get('di', ModbusSequentialDataBlock(0, 0)) - self.co = kwargs.get('co', ModbusSequentialDataBlock(0, 0)) - self.ir = kwargs.get('ir', ModbusSequentialDataBlock(0, 0)) - self.hr = kwargs.get('hr', ModbusSequentialDataBlock(0, 0)) - self.__build_mapping() - - def __build_mapping(self): - ''' - A quick helper method to build the function - code mapper. - ''' - self.__mapping = {2:self.di, 4:self.ir} - self.__mapping.update([(i, self.hr) for i in [3, 6, 16, 23]]) - self.__mapping.update([(i, self.co) for i in [1, 5, 15]]) - - def __str__(self): - ''' Returns a string representation of the context - - :returns: A string representation of the context - ''' - return "[Slave Context]\n", [self.co, self.di, self.ir, self.hr] - - def reset(self): - ''' Resets all the datastores to their default values ''' - for datastore in [self.di, self.co, self.ir, self.hr]: - datastore.reset() - - def validate(self, fx, address, count=1): - ''' Validates the request to make sure it is in range - - :param fx: The function we are working with - :param address: The starting address - :param count: The number of values to test - :returns: True if the request in within range, False otherwise - ''' - _logger.debug("validate[%d] %d:%d" % (fx, address, count)) - return self.__mapping[fx].validate(address, count) - - def getValues(self, fx, address, count=1): - ''' Validates the request to make sure it is in range - - :param fx: The function we are working with - :param address: The starting address - :param count: The number of values to retrieve - :returns: The requested values from a:a+c - ''' - _logger.debug("getValues[%d] %d:%d" % (fx, address, count)) - return self.__mapping[fx].getValues(address, count) - - def setValues(self, fx, address, values): - ''' Sets the datastore with the supplied values - - :param fx: The function we are working with - :param address: The starting address - :param values: The new values to be set - ''' - _logger.debug("setValues[%d] %d:%d" % (fx, address,len(values))) - self.__mapping[fx].setValues(address, values) - -class ModbusRemoteSlaveContext(IModbusSlaveContext): - ''' TODO - This creates a modbus data model that connects to - a remote device (depending on the client used) - ''' - - def __init__(self, client): - ''' Initializes the datastores - - :param client: The client to retrieve values with - ''' - self.client = client - - def getValues(self, fx, address, count=1): - ''' Validates the request to make sure it is in range - - :param fx: The function we are working with - :param address: The starting address - :param count: The number of values to retrieve - :returns: The requested values from a:a+c - ''' - raise NotImplementedException("Context Reset") - - def setValues(self, fx, address, values): - ''' Sets the datastore with the supplied values - - :param fx: The function we are working with - :param address: The starting address - :param values: The new values to be set - ''' - raise NotImplementedException("Context Reset") - - def __str__(self): - ''' Returns a string representation of the context - - :returns: A string representation of the context - ''' - return "Remote Slave Context(%s)", self.client - -class ModbusServerContext(object): - ''' This represents a master collection of slave contexts. - If single is set to true, it will be treated as a single - context so every unit-id returns the same context. If single - is set to false, it will be interpreted as a collection of - slave contexts. - ''' - - def __init__(self, slaves=None, single=True): - ''' Initializes a new instance of a modbus server context. - - :param slaves: A dictionary of client contexts - :param single: Set to true to treat this as a single context - ''' - self.single = single - self.__slaves = slaves or {} - if self.single: - self.__slaves = {0x00: self.__slaves} - - def __iter__(self): - ''' Iterater over the current collection of slave - contexts. - - :returns: An iterator over the slave contexts - ''' - return self.__slaves.items() - - def __setitem__(self, slave, context): - ''' Wrapper used to access the slave context - - :param slave: slave The context to set - :param context: The new context to set for this slave - ''' - if self.single: slave = 0x00 - if 0xf7 >= slave >= 0x00: - self.__slaves[slave] = context - else: raise ParameterException('slave index out of range') - - def __getitem__(self, slave): - ''' Wrapper used to access the slave context - - :param slave: The slave context to get - :returns: The requested slave context - ''' - if self.single: slave = 0x00 - if slave in self.__slaves: - return self.__slaves.get(slave) - else: raise ParameterException("slave does not exist, or is out of range") - -#---------------------------------------------------------------------------# -# Exported symbols -#---------------------------------------------------------------------------# -__all__ = [ - "ModbusSequentialDataBlock", "ModbusSparseDataBlock", - "ModbusSlaveContext", "ModbusServerContext", -] diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index fe8a1650d..b1e182dcf 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -106,7 +106,7 @@ def __iter__(self): :returns: An iterator over the slave contexts ''' - return self.__slaves.items() + return self.__slaves.iteritems() def __setitem__(self, slave, context): ''' Wrapper used to access the slave context diff --git a/pymodbus/datastore/store.py b/pymodbus/datastore/store.py index f1fc5b17b..e760bb6bc 100644 --- a/pymodbus/datastore/store.py +++ b/pymodbus/datastore/store.py @@ -125,7 +125,7 @@ def __iter__(self): :returns: An iterator of the data block data ''' if isinstance(self.values, dict): - return self.values.items() + return self.values.iteritems() return enumerate(self.values) @@ -194,8 +194,8 @@ def __init__(self, values): self.values = dict(enumerate(values)) else: raise ParameterException( "Values for datastore must be a list or dictionary") - self.default_value = list(self.values.values())[0].__class__() - self.address = self.values.keys().next() + self.default_value = self.values.values()[0].__class__() + self.address = self.values.iterkeys().next() def validate(self, address, count=1): ''' Checks to see if the request is in range @@ -206,7 +206,7 @@ def validate(self, address, count=1): ''' if count == 0: return False handle = set(range(address, address + count)) - return handle.issubset(set(self.values.keys())) + return handle.issubset(set(self.values.iterkeys())) def getValues(self, address, count=1): ''' Returns the requested values of the datastore @@ -224,7 +224,7 @@ def setValues(self, address, values): :param values: The new values to be set ''' if isinstance(values, dict): - for idx, val in values.items(): + for idx, val in values.iteritems(): self.values[idx] = val else: if not isinstance(values, list): diff --git a/pymodbus/device.py b/pymodbus/device.py index b79ecc0b4..a202014ff 100644 --- a/pymodbus/device.py +++ b/pymodbus/device.py @@ -6,6 +6,7 @@ maintained in the server context and the various methods should be inserted in the correct locations. """ +from itertools import izip from pymodbus.interfaces import Singleton from pymodbus.utilities import dict_property @@ -116,14 +117,14 @@ def __iter__(self): :returns: An iterator of the device information ''' - return self.__data.items() + return self.__data.iteritems() def summary(self): ''' Return a summary of the main items :returns: An dictionary of the main items ''' - return dict(zip(self.__names, self.__data.values())) + return dict(zip(self.__names, self.__data.itervalues())) def update(self, input): ''' Update the values of this identity @@ -258,7 +259,7 @@ def __iter__(self): :returns: An iterator of the device counters ''' - return zip(self.__names, self.__data.values()) + return izip(self.__names, self.__data.itervalues()) def update(self, input): ''' Update the values of this identity @@ -266,7 +267,7 @@ def update(self, input): :param input: The value to copy values from ''' - for k, v in input.items(): + for k, v in input.iteritems(): v += self.__getattribute__(k) self.__setattr__(k, v) @@ -425,7 +426,7 @@ def setDiagnostic(self, mapping): :param mapping: Dictionary of key:value pairs to set ''' - for entry in mapping.items(): + for entry in mapping.iteritems(): if entry[0] >= 0 and entry[0] < len(self.__diagnostic): self.__diagnostic[entry[0]] = (entry[1] != 0) diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 9ae4f1324..647d72f64 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -53,7 +53,7 @@ def decode(self, message): ''' try: return self._helper(message) - except ModbusException as er: + except ModbusException, er: _logger.warn("Unable to decode request %s" % er) return None @@ -120,7 +120,7 @@ def decode(self, message): ''' try: return self._helper(message) - except ModbusException as er: + except ModbusException, er: _logger.error("Unable to decode response %s" % er) return None diff --git a/pymodbus/file_message.py b/pymodbus/file_message.py index b90c93935..de642ae31 100644 --- a/pymodbus/file_message.py +++ b/pymodbus/file_message.py @@ -114,7 +114,7 @@ def decode(self, data): ''' self.values = [] length, count = struct.unpack('>HH', data[0:4]) - for index in range(0, count - 4): + for index in xrange(0, count - 4): idx = 4 + index * 2 self.values.append(struct.unpack('>H', data[idx:idx + 2])[0]) diff --git a/pymodbus/other_message.py b/pymodbus/other_message.py index d082dc4c9..f6cb90037 100644 --- a/pymodbus/other_message.py +++ b/pymodbus/other_message.py @@ -316,7 +316,7 @@ def decode(self, data): self.message_count = struct.unpack('>H', data[5:7])[0] self.events = [] - for e in range(7, length + 1): + for e in xrange(7, length + 1): self.events.append(struct.unpack('>B', data[e])[0]) def __str__(self): diff --git a/pymodbus/pdu.py b/pymodbus/pdu.py index 2dec07bd2..6c21b8f00 100644 --- a/pymodbus/pdu.py +++ b/pymodbus/pdu.py @@ -4,7 +4,7 @@ from pymodbus.interfaces import Singleton from pymodbus.exceptions import NotImplementedException from pymodbus.constants import Defaults -from pymodbus.utilities import rtuFrameSize +from utilities import rtuFrameSize #---------------------------------------------------------------------------# # Logging diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index 51504e114..18a054de1 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -64,7 +64,7 @@ def _execute(self, request): try: context = self.factory.store[request.unit_id] response = request.execute(context) - except Exception as ex: + except Exception, ex: _logger.debug("Datastore unable to fulfill request %s" % ex) response = request.doException(merror.SlaveFailure) #self.framer.populateResult(response) @@ -160,7 +160,7 @@ def _execute(self, request, addr): try: context = self.store[request.unit_id] response = request.execute(context) - except Exception as ex: + except Exception, ex: _logger.debug("Datastore unable to fulfill request %s" % ex) response = request.doException(merror.SlaveFailure) #self.framer.populateResult(response) diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index 831a5b7a1..d00124b1d 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -4,7 +4,7 @@ ''' from binascii import b2a_hex -import socketserver +import SocketServer import serial import socket @@ -27,7 +27,7 @@ #---------------------------------------------------------------------------# # Server #---------------------------------------------------------------------------# -class ModbusRequestHandler(socketserver.BaseRequestHandler): +class ModbusRequestHandler(SocketServer.BaseRequestHandler): ''' Implements the modbus server protocol This uses the socketserver.BaseRequestHandler to implement @@ -58,7 +58,7 @@ def handle(self): # if not self.server.control.ListenOnly: self.framer.processIncomingPacket(data, self.execute) except socket.timeout: pass - except socket.error as msg: + except socket.error, msg: _logger.error("Socket error occurred %s" % msg) self.running = False except: self.running = False @@ -74,7 +74,7 @@ def execute(self, request): try: context = self.server.context[request.unit_id] response = request.execute(context) - except Exception as ex: + except Exception, ex: _logger.debug("Datastore unable to fulfill request %s" % ex) response = request.doException(merror.SlaveFailure) response.transaction_id = request.transaction_id @@ -100,12 +100,12 @@ def decode(self, message): ''' try: return decodeModbusRequestPDU(message) - except ModbusException as er: + except ModbusException, er: _logger.warn("Unable to decode request %s" % er) return None -class ModbusTcpServer(socketserver.ThreadingTCPServer): +class ModbusTcpServer(SocketServer.ThreadingTCPServer): ''' A modbus threaded tcp socket server @@ -134,7 +134,7 @@ def __init__(self, context, framer=None, identity=None): if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) - socketserver.ThreadingTCPServer.__init__(self, + SocketServer.ThreadingTCPServer.__init__(self, ("", Defaults.Port), ModbusRequestHandler) def process_request(self, request, client): @@ -144,7 +144,7 @@ def process_request(self, request, client): :param client: The address of the client ''' _logger.debug("Started thread to serve client at " + str(client)) - socketserver.ThreadingTCPServer.process_request(self, request, client) + SocketServer.ThreadingTCPServer.process_request(self, request, client) def server_close(self): ''' Callback for stopping the running server @@ -154,7 +154,7 @@ def server_close(self): for thread in self.threads: thread.running = False -class ModbusUdpServer(socketserver.ThreadingUDPServer): +class ModbusUdpServer(SocketServer.ThreadingUDPServer): ''' A modbus threaded udp socket server @@ -183,7 +183,7 @@ def __init__(self, context, framer=None, identity=None): if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) - socketserver.ThreadingUDPServer.__init__(self, + SocketServer.ThreadingUDPServer.__init__(self, ("", Defaults.Port), ModbusRequestHandler) def process_request(self, request, client): @@ -193,7 +193,7 @@ def process_request(self, request, client): :param client: The address of the client ''' _logger.debug("Started thread to serve client at " + str(client)) - socketserver.ThreadingUDPServer.process_request(self, request, client) + SocketServer.ThreadingUDPServer.process_request(self, request, client) def server_close(self): ''' Callback for stopping the running server @@ -251,7 +251,7 @@ def _connect(self): self.socket = serial.Serial(port=self.device, timeout=self.timeout, bytesize=self.bytesize, stopbits=self.stopbits, baudrate=self.baudrate, parity=self.parity) - except serial.SerialException as msg: + except serial.SerialException, msg: _logger.error(msg) self.close() return self.socket != None diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index adcf5f03f..9929aa85c 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -57,7 +57,7 @@ def execute(self, request): self.socket.connect() packet = self.framer.buildPacket(request) self.socket.send(packet) - except socket.error as msg: + except socket.error, msg: self.socket.close() _logger.debug("Transaction failed. (%s) " % msg) retries -= 1 diff --git a/test/test_bit_read_messages.py b/test/test_bit_read_messages.py index e65abe284..63b43c5e0 100644 --- a/test/test_bit_read_messages.py +++ b/test/test_bit_read_messages.py @@ -15,7 +15,7 @@ from pymodbus.exceptions import * from pymodbus.pdu import ModbusExceptions -from .modbus_mocks import MockContext +from modbus_mocks import MockContext #---------------------------------------------------------------------------# # Fixture @@ -48,7 +48,7 @@ def testReadBitBaseClassMethods(self): def testBitReadBaseRequestEncoding(self): ''' Test basic bit message encoding/decoding ''' - for i in range(20): + for i in xrange(20): handle = ReadBitsRequestBase(i, i) result = struct.pack('>HH',i, i) self.assertEqual(handle.encode(), result) @@ -57,7 +57,7 @@ def testBitReadBaseRequestEncoding(self): def testBitReadBaseResponseEncoding(self): ''' Test basic bit message encoding/decoding ''' - for i in range(20): + for i in xrange(20): input = [True] * i handle = ReadBitsResponseBase(input) result = handle.encode() @@ -70,7 +70,7 @@ def testBitReadBaseResponseHelperMethods(self): handle = ReadBitsResponseBase(input) for i in [1,3,5]: handle.setBit(i, True) for i in [1,3,5]: handle.resetBit(i) - for i in range(8): + for i in xrange(8): self.assertEqual(handle.getBit(i), False) def testBitReadBaseRequests(self): @@ -79,7 +79,7 @@ def testBitReadBaseRequests(self): ReadBitsRequestBase(12, 14) : '\x00\x0c\x00\x0e', ReadBitsResponseBase([1,0,1,1,0]) : '\x01\x0d', } - for request, expected in messages.items(): + for request, expected in messages.iteritems(): self.assertEqual(request.encode(), expected) def testBitReadMessageExecuteValueErrors(self): diff --git a/test/test_bit_write_messages.py b/test/test_bit_write_messages.py index cc1e14f59..e2f48a48a 100644 --- a/test/test_bit_write_messages.py +++ b/test/test_bit_write_messages.py @@ -13,7 +13,7 @@ from pymodbus.exceptions import * from pymodbus.pdu import ModbusExceptions -from .modbus_mocks import MockContext, FakeList +from modbus_mocks import MockContext, FakeList #---------------------------------------------------------------------------# # Fixture @@ -42,7 +42,7 @@ def testBitWriteBaseRequests(self): WriteMultipleCoilsRequest(1, [True]*5) : '\x00\x01\x00\x05\x01\x1f', WriteMultipleCoilsResponse(1, 5) : '\x00\x01\x00\x05', } - for request, expected in messages.items(): + for request, expected in messages.iteritems(): self.assertEqual(request.encode(), expected) def testWriteMultipleCoilsRequest(self): diff --git a/test/test_device.py b/test/test_device.py index e3b20af40..994da0a74 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -66,7 +66,7 @@ def testModbusDeviceIdentificationGet(self): def testModbusDeviceIdentificationSummary(self): ''' Test device identification summary creation ''' summary = sorted(self.ident.summary().values()) - expected = sorted(list(self.info.values())[:-3]) # remove private + expected = sorted(self.info.values()[:-3]) # remove private self.assertEqual(summary, expected) def testModbusDeviceIdentificationSet(self): diff --git a/test/test_exceptions.py b/test/test_exceptions.py index 99f69dc01..c92494218 100644 --- a/test/test_exceptions.py +++ b/test/test_exceptions.py @@ -23,10 +23,10 @@ def tearDown(self): def testExceptions(self): ''' Test all module exceptions ''' - for exception in self.exceptions: + for ex in self.exceptions: try: - raise exception - except ModbusException as ex: + raise ex + except ModbusException, ex: self.assertTrue("Modbus Error:" in str(ex)) pass else: self.fail("Excepted a ModbusExceptions") diff --git a/test/test_file_message.py b/test/test_file_message.py index 3dce7b138..34fe260d0 100644 --- a/test/test_file_message.py +++ b/test/test_file_message.py @@ -13,7 +13,7 @@ from pymodbus.exceptions import * from pymodbus.pdu import ModbusExceptions -from .modbus_mocks import MockContext +from modbus_mocks import MockContext #---------------------------------------------------------------------------# # Fixture diff --git a/test/test_register_read_messages.py b/test/test_register_read_messages.py index 0445f2a7b..6ffc9e2c5 100644 --- a/test/test_register_read_messages.py +++ b/test/test_register_read_messages.py @@ -6,7 +6,7 @@ from pymodbus.exceptions import * from pymodbus.pdu import ModbusExceptions -from .modbus_mocks import MockContext, FakeList +from modbus_mocks import MockContext, FakeList #---------------------------------------------------------------------------# # Fixture diff --git a/test/test_register_write_messages.py b/test/test_register_write_messages.py index 3ff2819cf..107fbdfec 100644 --- a/test/test_register_write_messages.py +++ b/test/test_register_write_messages.py @@ -4,7 +4,7 @@ from pymodbus.exceptions import ParameterException from pymodbus.pdu import ModbusExceptions -from .modbus_mocks import MockContext +from modbus_mocks import MockContext #---------------------------------------------------------------------------# # Fixture diff --git a/test/test_remote_datastore.py b/test/test_remote_datastore.py index a37ef4f49..8a88998db 100644 --- a/test/test_remote_datastore.py +++ b/test/test_remote_datastore.py @@ -6,7 +6,7 @@ from pymodbus.bit_write_message import * from pymodbus.register_read_message import * from pymodbus.pdu import ExceptionResponse -from .modbus_mocks import mock +from modbus_mocks import mock class RemoteModbusDataStoreTest(unittest.TestCase): ''' diff --git a/test/test_server_context.py b/test/test_server_context.py index 1dba934c1..829bc4279 100644 --- a/test/test_server_context.py +++ b/test/test_server_context.py @@ -19,7 +19,7 @@ def tearDown(self): def testSingleContextGets(self): ''' Test getting on a single context ''' - for id in range(0, 0xff): + for id in xrange(0, 0xff): self.assertEqual(self.slave, self.context[id]) def testSingleContextIter(self): @@ -48,7 +48,7 @@ class ModbusServerMultipleContextTest(unittest.TestCase): def setUp(self): ''' Sets up the test environment ''' - self.slaves = dict((id, ModbusSlaveContext()) for id in range(10)) + self.slaves = dict((id, ModbusSlaveContext()) for id in xrange(10)) self.context = ModbusServerContext(slaves=self.slaves, single=False) def tearDown(self): @@ -57,7 +57,7 @@ def tearDown(self): def testMultipleContextGets(self): ''' Test getting on multiple context ''' - for id in range(0, 10): + for id in xrange(0, 10): self.assertEqual(self.slaves[id], self.context[id]) def testMultipleContextIter(self): @@ -72,10 +72,10 @@ def testMultipleContextDefault(self): def testMultipleContextSet(self): ''' Test a setting multiple slave contexts ''' - slaves = dict((id, ModbusSlaveContext()) for id in range(10)) - for id, slave in slaves.items(): + slaves = dict((id, ModbusSlaveContext()) for id in xrange(10)) + for id, slave in slaves.iteritems(): self.context[id] = slave - for id, slave in slaves.items(): + for id, slave in slaves.iteritems(): actual = self.context[id] self.assertEqual(slave, actual) From d81d68a669188f65790315ffeeb60ec3a6e0caf6 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 11 May 2011 18:17:18 +0000 Subject: [PATCH 017/243] adding some magic methods, just cause --- pymodbus/datastore/context.py | 8 ++++++++ pymodbus/device.py | 7 +++++++ test/test_device.py | 2 ++ test/test_server_context.py | 1 + 4 files changed, 18 insertions(+) diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index b1e182dcf..6d3c722cc 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -108,6 +108,14 @@ def __iter__(self): ''' return self.__slaves.iteritems() + def __contains__(self, slave): + ''' Check if the given slave is in this list + + :param slave: slave The slave to check for existance + :returns: True if the slave exists, False otherwise + ''' + return slave in self.__slaves + def __setitem__(self, slave, context): ''' Wrapper used to access the slave context diff --git a/pymodbus/device.py b/pymodbus/device.py index a202014ff..8823dd0f8 100644 --- a/pymodbus/device.py +++ b/pymodbus/device.py @@ -36,6 +36,13 @@ def __iter__(self): ''' return self.__nmstable.__iter__() + def __contains__(self, host): + ''' Check if a host is allowed to access resources + + :param host: The host to check + ''' + return host in self.__nmstable + def add(self, host): ''' Add allowed host(s) from the NMS table diff --git a/test/test_device.py b/test/test_device.py index 994da0a74..f1f9313e4 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -191,6 +191,8 @@ def testNetworkAccessListIterator(self): self.access.add(list) for host in self.access: self.assertTrue(host in list) + for host in list: + self.assertTrue(host in self.access) def testClearingControlEvents(self): ''' Test adding and clearing modbus events ''' diff --git a/test/test_server_context.py b/test/test_server_context.py index 829bc4279..3641f299f 100644 --- a/test/test_server_context.py +++ b/test/test_server_context.py @@ -64,6 +64,7 @@ def testMultipleContextIter(self): ''' Test iterating over multiple context ''' for id, slave in self.context: self.assertEqual(slave, self.slaves[id]) + self.assertTrue(id in self.context) def testMultipleContextDefault(self): ''' Test that the multiple context default values work ''' From 92c2474c7e5bfb540d1b811e584da2cd237ebb83 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Thu, 19 May 2011 15:42:55 +0000 Subject: [PATCH 018/243] adding documentation pdf --- doc/pymodbus.pdf | Bin 0 -> 509208 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/pymodbus.pdf diff --git a/doc/pymodbus.pdf b/doc/pymodbus.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d3dc1fff81c84175dbe7b84cd2c3d8acc61595b2 GIT binary patch literal 509208 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%0D+X^EsOduo}1s2 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
`;aR6u%&Ky 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⪼;grX<>l5#YpsM^O=G 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 H^%MRND!GX0 literal 0 HcmV?d00001 From bb1ab650120a4e4be5f89305709cb0d6a5ffdffc Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Tue, 24 May 2011 14:30:27 +0000 Subject: [PATCH 019/243] Fixes issue 49 --- pymodbus/transaction.py | 3 ++- test/test_transaction.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 9929aa85c..f5cb6e425 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -472,13 +472,14 @@ def checkFrame(self): if start == -1: return False if start > 0 : # go ahead and skip old bad data self.__buffer = self.__buffer[start:] + start = 0 end = self.__buffer.find(self.__end) if (end != -1): self.__header['len'] = end self.__header['uid'] = int(self.__buffer[1:3], 16) self.__header['lrc'] = int(self.__buffer[end - 2:end], 16) - data = self.__buffer[start:end - 2] + data = a2b_hex(self.__buffer[start + 1:end - 2]) return checkLRC(data, self.__header['lrc']) return False diff --git a/test/test_transaction.py b/test/test_transaction.py index 2b2b5baeb..a0ac8c7ad 100644 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -232,7 +232,7 @@ def testRTUFramerPacket(self): #---------------------------------------------------------------------------# def testASCIIFramerTransactionReady(self): ''' Test a ascii frame transaction ''' - msg = ":abcd12341234aae6\r\n" + msg = ':F7031389000A60\r\n' self.assertFalse(self._ascii.isFrameReady()) self.assertFalse(self._ascii.checkFrame()) self._ascii.addToFrame(msg) @@ -245,7 +245,7 @@ def testASCIIFramerTransactionReady(self): def testASCIIFramerTransactionFull(self): ''' Test a full ascii frame transaction ''' - msg ='sss:01030000000A0C\r\n' + msg = 'sss:F7031389000A60\r\n' pack = a2b_hex(msg[6:-4]) self._ascii.addToFrame(msg) self.assertTrue(self._ascii.checkFrame()) @@ -255,8 +255,8 @@ def testASCIIFramerTransactionFull(self): def testASCIIFramerTransactionHalf(self): ''' Test a half completed ascii frame transaction ''' - msg1 = "sss:abcd1234" - msg2 = "1234aae6\r\n" + msg1 = 'sss:F7031389' + msg2 = '000A60\r\n' pack = a2b_hex(msg1[6:] + msg2[:-4]) self._ascii.addToFrame(msg1) self.assertFalse(self._ascii.checkFrame()) From 5e4f7a88af9877a5c9fc5e49dde34873d19ef421 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 15 Jun 2011 16:43:37 +0000 Subject: [PATCH 020/243] Adds True and False constants for older python versions. Update issue 50 --- pymodbus/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index 2b89b4d22..72a447381 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -27,3 +27,11 @@ def emit(self, record): pass __logging.getLogger(__name__).addHandler(__null()) + +#---------------------------------------------------------------------------# +# Define True and False if we don't have them (2.3.2) +#---------------------------------------------------------------------------# +try: + True, False +except NameError: + True, False = 1, 0 From e276a93ae477e19b9e52bc9427da4a7824095bc4 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 15 Jun 2011 18:09:33 +0000 Subject: [PATCH 021/243] adding tests for a few fixes --- test/test_fixes.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 test/test_fixes.py diff --git a/test/test_fixes.py b/test/test_fixes.py new file mode 100644 index 000000000..5a7a0d83a --- /dev/null +++ b/test/test_fixes.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +import unittest + +class ModbusFixesTest(unittest.TestCase): + ''' + This is the unittest for the pymodbus._version code + ''' + + def setUp(self): + ''' Initializes the test environment ''' + pass + + def tearDown(self): + ''' Cleans up the test environment ''' + pass + + def testTrueFalseDefined(self): + ''' Test that True and False are defined on all versions''' + try: + True,False + except NameError: + import pymodbus + self.assertEqual(True, 1) + self.assertEqual(False, 1) + + def testNullLoggerAttached(self): + ''' Test that the null logger is attached''' + import logging + if len(logging._handlers) == 0: + import pymodbus + self.assertEqual(logging._handlers, 1) + +#---------------------------------------------------------------------------# +# Main +#---------------------------------------------------------------------------# +if __name__ == "__main__": + unittest.main() From d1b4de239e54d4b6020eb2587d0a9ca7207b5db5 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Tue, 30 Aug 2011 16:09:11 +0000 Subject: [PATCH 022/243] Fixes issue 51 --- examples/common/synchronous-client.py | 23 ++++++++++++++++++----- pymodbus/client/common.py | 4 ++++ pymodbus/register_read_message.py | 19 +++++++++---------- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/examples/common/synchronous-client.py b/examples/common/synchronous-client.py index 746b2094f..56061cda8 100755 --- a/examples/common/synchronous-client.py +++ b/examples/common/synchronous-client.py @@ -41,7 +41,13 @@ # example requests #---------------------------------------------------------------------------# # simply call the methods that you would like to use. An example session -# is displayed below along with some assert checks. +# is displayed below along with some assert checks. Note that some modbus +# implementations differentiate holding/input discrete/coils and as such +# you will not be able to write to these, therefore the starting values +# are not known to these tests. Furthermore, some use the same memory +# blocks for the two sets, so a change to one is a change to the other. +# Keep both of these cases in mind when testing as the following will +# _only_ pass with the supplied async modbus server (script supplied). #---------------------------------------------------------------------------# rq = client.write_coil(1, True) rr = client.read_coils(1,1) @@ -56,7 +62,7 @@ rq = client.write_coils(1, [False]*8) rr = client.read_discrete_inputs(1,8) assert(rq.function_code < 0x80) # test that we are not an error -assert(rr.bits == [False]*8) # test the expected value +assert(rr.bits == [True]*8) # test the expected value rq = client.write_register(1, 10) rr = client.read_holding_registers(1,1) @@ -66,12 +72,19 @@ rq = client.write_registers(1, [10]*8) rr = client.read_input_registers(1,8) assert(rq.function_code < 0x80) # test that we are not an error -assert(rr.registers == [10]*8) # test the expected value +assert(rr.registers == [17]*8) # test the expected value -rq = client.readwrite_registers(1, [20]*8) +arguments = { + 'read_address': 1, + 'read_count': 8, + 'write_address': 1, + 'write_registers': [20]*8, +} +rq = client.readwrite_registers(**arguments) rr = client.read_input_registers(1,8) assert(rq.function_code < 0x80) # test that we are not an error -assert(rr.registers == [20]*8) # test the expected value +assert(rq.registers == [20]*8) # test the expected value +assert(rr.registers == [17]*8) # test the expected value #---------------------------------------------------------------------------# # close the client diff --git a/pymodbus/client/common.py b/pymodbus/client/common.py index c08625c4f..91ee380e5 100644 --- a/pymodbus/client/common.py +++ b/pymodbus/client/common.py @@ -125,6 +125,10 @@ def readwrite_registers(self, *args, **kwargs): ''' :param unit: The slave unit this request is targeting + :param read_address: The address to start reading from + :param read_count: The number of registers to read from address + :param write_address: The address to start writing to + :param write_registers: The registers to write to the specified address :returns: A deferred response handle ''' request = ReadWriteMultipleRegistersRequest(*args, **kwargs) diff --git a/pymodbus/register_read_message.py b/pymodbus/register_read_message.py index b1190d2fb..ccab840aa 100644 --- a/pymodbus/register_read_message.py +++ b/pymodbus/register_read_message.py @@ -215,8 +215,7 @@ class ReadWriteMultipleRegistersRequest(ModbusRequest): _rtu_byte_count_pos = 10 - def __init__(self, read_address, read_count, - write_address, write_registers, **kwargs): + def __init__(self, **kwargs): ''' Initializes a new request message :param read_address: The address to start reading from @@ -225,12 +224,12 @@ def __init__(self, read_address, read_count, :param write_registers: The registers to write to the specified address ''' ModbusRequest.__init__(self, **kwargs) - self.read_address = read_address - self.read_count = read_count - self.write_address = write_address - self.write_registers = write_registers - if not hasattr(write_registers, '__iter__'): - self.write_registers = [write_registers] + self.read_address = kwargs.get('read_address', 0x00) + self.read_count = kwargs.get('read_count', 0) + self.write_address = kwargs.get('write_address', 0x00) + self.write_registers = kwargs.get('write_registers', None) + if not hasattr(self.write_registers, '__iter__'): + self.write_registers = [self.write_registers] self.write_count = len(self.write_registers) self.write_byte_count = self.write_count * 2 @@ -251,10 +250,10 @@ def decode(self, data): :param data: The request to decode ''' - self.read_address, self.read_count, \ + self.read_address, self.read_count, \ self.write_address, self.write_count, \ self.write_byte_count = struct.unpack('>HHHHB', data[:9]) - self.write_registers = [] + self.write_registers = [] for i in range(9, self.write_byte_count + 9, 2): register = struct.unpack('>H', data[i:i + 2])[0] self.write_registers.append(register) From ecdfc1969365c113c6cc17e30d5adad5e9cd4076 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Tue, 30 Aug 2011 17:35:55 +0000 Subject: [PATCH 023/243] fixing tests broken by interface change --- test/test_all_messages.py | 6 +++++- test/test_client_common.py | 6 +++++- test/test_register_read_messages.py | 18 +++++++++++++----- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/test/test_all_messages.py b/test/test_all_messages.py index cdcca9035..7003ceebc 100644 --- a/test/test_all_messages.py +++ b/test/test_all_messages.py @@ -20,6 +20,10 @@ def setUp(self): Initializes the test environment and builds request/result encoding pairs ''' + arguments = { + 'read_address': 1, 'read_count': 1, + 'write_address': 1, 'write_registers': 1 + } self.requests = [ lambda unit: ReadCoilsRequest(1, 5, unit=unit), lambda unit: ReadDiscreteInputsRequest(1, 5, unit=unit), @@ -27,7 +31,7 @@ def setUp(self): lambda unit: WriteMultipleCoilsRequest(1, [1], unit=unit), lambda unit: ReadHoldingRegistersRequest(1, 5, unit=unit), lambda unit: ReadInputRegistersRequest(1, 5, unit=unit), - lambda unit: ReadWriteMultipleRegistersRequest(1, 5, 1, [1], unit=unit), + lambda unit: ReadWriteMultipleRegistersRequest(unit=unit, **arguments), lambda unit: WriteSingleRegisterRequest(1, 1, unit=unit), lambda unit: WriteMultipleRegistersRequest(1, [1], unit=unit), ] diff --git a/test/test_client_common.py b/test/test_client_common.py index 97bf253a2..f8cf91b94 100644 --- a/test/test_client_common.py +++ b/test/test_client_common.py @@ -38,6 +38,10 @@ def tearDown(self): #-----------------------------------------------------------------------# def testModbusClientMixinMethods(self): ''' This tests that the mixing returns the correct request object ''' + arguments = { + 'read_address': 1, 'read_count': 1, + 'write_address': 1, 'write_registers': 1 + } self.assertTrue(isinstance(self.client.read_coils(1,1), ReadCoilsRequest)) self.assertTrue(isinstance(self.client.read_discrete_inputs(1,1), ReadDiscreteInputsRequest)) self.assertTrue(isinstance(self.client.write_coil(1,True), WriteSingleCoilRequest)) @@ -46,4 +50,4 @@ def testModbusClientMixinMethods(self): self.assertTrue(isinstance(self.client.write_registers(1,[0x00]), WriteMultipleRegistersRequest)) self.assertTrue(isinstance(self.client.read_holding_registers(1,1), ReadHoldingRegistersRequest)) self.assertTrue(isinstance(self.client.read_input_registers(1,1), ReadInputRegistersRequest)) - self.assertTrue(isinstance(self.client.readwrite_registers(1,1,1,1), ReadWriteMultipleRegistersRequest)) + self.assertTrue(isinstance(self.client.readwrite_registers(**arguments), ReadWriteMultipleRegistersRequest)) diff --git a/test/test_register_read_messages.py b/test/test_register_read_messages.py index 6ffc9e2c5..601d97be6 100644 --- a/test/test_register_read_messages.py +++ b/test/test_register_read_messages.py @@ -27,13 +27,17 @@ def setUp(self): Initializes the test environment and builds request/result encoding pairs ''' + arguments = { + 'read_address': 1, 'read_count': 5, + 'write_address': 1, 'write_registers': [0x00]*5, + } self.value = 0xabcd self.values = [0xa, 0xb, 0xc] self.request_read = { ReadRegistersRequestBase(1, 5) :'\x00\x01\x00\x05', ReadHoldingRegistersRequest(1, 5) :'\x00\x01\x00\x05', ReadInputRegistersRequest(1,5) :'\x00\x01\x00\x05', - ReadWriteMultipleRegistersRequest(1,5,1,[0x00]*5) :'\x00\x01\x00\x05\x00\x01\x00' + ReadWriteMultipleRegistersRequest(**arguments) :'\x00\x01\x00\x05\x00\x01\x00' '\x05\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', } self.response_read = { @@ -83,8 +87,10 @@ def testRegisterReadRequestsCountErrors(self): requests = [ ReadHoldingRegistersRequest(1, 0x800), ReadInputRegistersRequest(1,0x800), - ReadWriteMultipleRegistersRequest(1,0x800,1,5), - ReadWriteMultipleRegistersRequest(1,5,1,mock), + ReadWriteMultipleRegistersRequest(read_address=1, + read_count=0x800, write_address=1, write_registers=5), + ReadWriteMultipleRegistersRequest(read_address=1, + read_count=5, write_address=1, write_registers=mock), ] for request in requests: result = request.execute(None) @@ -124,14 +130,16 @@ def testRegisterReadRequestsExecute(self): def testReadWriteMultipleRegistersRequest(self): context = MockContext(True) - request = ReadWriteMultipleRegistersRequest(1, 10, 1, [0x00]) + request = ReadWriteMultipleRegistersRequest(read_address=1, + read_count=10, write_address=1, write_registers=[0x00]) response = request.execute(context) self.assertEqual(request.function_code, response.function_code) def testReadWriteMultipleRegistersValidate(self): context = MockContext() context.validate = lambda f,a,c: a == 1 - request = ReadWriteMultipleRegistersRequest(1, 10, 2, [0x00]) + request = ReadWriteMultipleRegistersRequest(read_address=1, + read_count=10, write_address=2, write_registers=[0x00]) response = request.execute(context) self.assertEqual(response.exception_code, ModbusExceptions.IllegalAddress) From 40fb36640c9fd372c5d3663a07c8c9b904d933bb Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Tue, 30 Aug 2011 19:26:37 +0000 Subject: [PATCH 024/243] Updates issue 52 --- doc/sphinx/library/constants.rst | 3 + doc/sphinx/library/payload.rst | 19 +++ pymodbus/constants.py | 19 ++- pymodbus/payload.py | 208 +++++++++++++++++++++++++++++++ test/test_payload.py | 108 ++++++++++++++++ 5 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 doc/sphinx/library/payload.rst create mode 100644 pymodbus/payload.py create mode 100644 test/test_payload.py diff --git a/doc/sphinx/library/constants.rst b/doc/sphinx/library/constants.rst index 4a02ac225..f7774c0f0 100644 --- a/doc/sphinx/library/constants.rst +++ b/doc/sphinx/library/constants.rst @@ -17,3 +17,6 @@ API Documentation .. autoclass:: ModbusStatus :members: + +.. autoclass:: Endian + :members: diff --git a/doc/sphinx/library/payload.rst b/doc/sphinx/library/payload.rst new file mode 100644 index 000000000..d81f80312 --- /dev/null +++ b/doc/sphinx/library/payload.rst @@ -0,0 +1,19 @@ +:mod:`payload` --- Modbus Payload Utilities +============================================================ + +.. module:: payload + :synopsis: Modbus Payload Utilities + +.. moduleauthor:: Galen Collins +.. sectionauthor:: Galen Collins + +API Documentation +------------------- + +.. automodule:: pymodbus.payload + +.. autoclass:: PayloadBuilder + :members: + +.. autoclass:: PayloadDecoder + :members: diff --git a/pymodbus/constants.py b/pymodbus/constants.py index b0a37bd84..efbeb20a3 100644 --- a/pymodbus/constants.py +++ b/pymodbus/constants.py @@ -122,7 +122,24 @@ class ModbusStatus(Singleton): SlaveOn = 0xff SlaveOff = 0x00 + +class Endian(Singleton): + ''' An enumeration representing the various byte endianess. + + .. attribute:: Big + + This indicates that the bytes are in little endian format + + .. attribute:: Little + + This indicates that the bytes are in big endian format + + ''' + Big = 0x00 + Little = 0x01 + + #---------------------------------------------------------------------------# # Exported Identifiers #---------------------------------------------------------------------------# -__all__ = ["Defaults", "ModbusStatus"] +__all__ = ["Defaults", "ModbusStatus", "Endian"] diff --git a/pymodbus/payload.py b/pymodbus/payload.py new file mode 100644 index 000000000..cb5e386bd --- /dev/null +++ b/pymodbus/payload.py @@ -0,0 +1,208 @@ +''' +Modbus Payload Builders +------------------------ + +A collection of utilities for building and decoding +modbus messages payloads. +''' +from struct import pack, unpack +from pymodbus.interfaces import Singleton +from pymodbus.constants import Endian + + +class PayloadBuilder(object): + + def __init__(self, payload=None, endian=Endian.Big): + ''' Initialize a new instance of the payload builder + + :param payload: Raw payload data to initialize with + :param endian: The endianess of the payload + ''' + self.payload = payload or [] + + def reset(self): + ''' Reset the payload buffer + ''' + self.payload = [] + + def tostring(self): + ''' Return the payload buffer as a string + + :returns: The payload buffer as a string + ''' + return ''.join(self.payload) + + def tolist(self): + ''' Return the payload buffer as a list + + :returns: The payload buffer as a list + ''' + return self.payload + + def add_8bit_uint(self, value): + ''' Adds a 8 bit unsigned int to the buffer + + :param value: The value to add to the buffer + ''' + self.payload.append(pack('B', value)) + + def add_16bit_uint(self, value): + ''' Adds a 16 bit unsigned int to the buffer + + :param value: The value to add to the buffer + ''' + self.payload.append(pack('H', value)) + + def add_32bit_uint(self, value): + ''' Adds a 32 bit unsigned int to the buffer + + :param value: The value to add to the buffer + ''' + self.payload.append(pack('I', value)) + + def add_64bit_uint(self, value): + ''' Adds a 64 bit unsigned int to the buffer + + :param value: The value to add to the buffer + ''' + self.payload.append(pack('Q', value)) + + def add_8bit_int(self, value): + ''' Adds a 8 bit signed int to the buffer + + :param value: The value to add to the buffer + ''' + self.payload.append(pack('b', value)) + + def add_16bit_int(self, value): + ''' Adds a 16 bit signed int to the buffer + + :param value: The value to add to the buffer + ''' + self.payload.append(pack('h', value)) + + def add_32bit_int(self, value): + ''' Adds a 32 bit signed int to the buffer + + :param value: The value to add to the buffer + ''' + self.payload.append(pack('i', value)) + + def add_64bit_int(self, value): + ''' Adds a 64 bit signed int to the buffer + + :param value: The value to add to the buffer + ''' + self.payload.append(pack('q', value)) + + def add_32bit_float(self, value): + ''' Adds a 32 bit float to the buffer + + :param value: The value to add to the buffer + ''' + self.payload.append(pack('f', value)) + + def add_64bit_float(self, value): + ''' Adds a 64 bit float(double) to the buffer + + :param value: The value to add to the buffer + ''' + self.payload.append(pack('d', value)) + + def add_string(self, value): + ''' Adds a string to the buffer + + :param value: The value to add to the buffer + ''' + for c in value: + self.payload.append(pack('s', c)) + + +class PayloadDecoder(object): + + def __init__(self, payload, endian=Endian.Big): + ''' Initialize a new payload decoder + + :param payload: The payload to decode with + :param endian: The endianess of the payload + ''' + self.payload = payload + self.pointer = 0x00 + + def reset(self): + ''' Reset the decoder pointer back to the start + ''' + self.pointer = 0x00 + + def decode_8bit_uint(self): + ''' Decodes a 8 bit unsigned int from the buffer + ''' + self.pointer += 1 + return unpack('B', self.payload[self.pointer - 1:self.pointer])[0] + + def decode_16bit_uint(self): + ''' Decodes a 16 bit unsigned int from the buffer + ''' + self.pointer += 2 + return unpack('H', self.payload[self.pointer - 2:self.pointer])[0] + + def decode_32bit_uint(self): + ''' Decodes a 32 bit unsigned int from the buffer + ''' + self.pointer += 4 + return unpack('I', self.payload[self.pointer - 4:self.pointer])[0] + + def decode_64bit_uint(self): + ''' Decodes a 64 bit unsigned int from the buffer + ''' + self.pointer += 8 + return unpack('Q', self.payload[self.pointer - 8:self.pointer])[0] + + def decode_8bit_int(self): + ''' Decodes a 8 bit signed int from the buffer + ''' + self.pointer += 1 + return unpack('b', self.payload[self.pointer - 1:self.pointer])[0] + + def decode_16bit_int(self): + ''' Decodes a 16 bit signed int from the buffer + ''' + self.pointer += 2 + return unpack('h', self.payload[self.pointer - 2:self.pointer])[0] + + def decode_32bit_int(self): + ''' Decodes a 32 bit signed int from the buffer + ''' + self.pointer += 4 + return unpack('i', self.payload[self.pointer - 4:self.pointer])[0] + + def decode_64bit_int(self): + ''' Decodes a 64 bit signed int from the buffer + ''' + self.pointer += 8 + return unpack('q', self.payload[self.pointer - 8:self.pointer])[0] + + def decode_32bit_float(self): + ''' Decodes a 32 bit float from the buffer + ''' + self.pointer += 4 + return unpack('f', self.payload[self.pointer - 4:self.pointer])[0] + + def decode_64bit_float(self): + ''' Decodes a 64 bit float(double) from the buffer + ''' + self.pointer += 8 + return unpack('d', self.payload[self.pointer - 8:self.pointer])[0] + + def decode_string(self, size=1): + ''' Decodes a string from the buffer + + :param size: The size of the string to decode + ''' + self.pointer += size + return self.payload[self.pointer - size:self.pointer] + +#---------------------------------------------------------------------------# +# Exported Identifiers +#---------------------------------------------------------------------------# +__all__ = ["PayloadBuilder", "PayloadDecoder"] diff --git a/test/test_payload.py b/test/test_payload.py new file mode 100644 index 000000000..e43670399 --- /dev/null +++ b/test/test_payload.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +''' +Payload Utilities Test Fixture +-------------------------------- +This fixture tests the functionality of the payload +utilities. + +* PayloadBuilder +* PayloadDecoder +''' +import unittest +from pymodbus.constants import Endian +from pymodbus.payload import PayloadBuilder, PayloadDecoder + +#---------------------------------------------------------------------------# +# Fixture +#---------------------------------------------------------------------------# +class ModbusPayloadUtilityTests(unittest.TestCase): + + #-----------------------------------------------------------------------# + # Setup/TearDown + #-----------------------------------------------------------------------# + + def setUp(self): + ''' + Initializes the test environment and builds request/result + encoding pairs + ''' + self.little_endian_payload = \ + '\x01\x02\x00\x03\x00\x00\x00\x04\x00\x00\x00\x00' \ + '\x00\x00\x00\xff\xfe\xff\xfd\xff\xff\xff\xfc\xff' \ + '\xff\xff\xff\xff\xff\xff\x00\x00\xa0\x3f\x00\x00' \ + '\x00\x00\x00\x00\x19\x40\x74\x65\x73\x74' + + self.big_endian_payload = \ + '\x01\x00\x02\x00\x00\x00\x03\x00\x00\x00\x00\x00' \ + '\x00\x00\x04\xff\xff\xfe\xff\xff\xff\xfd\xff\xff' \ + '\xff\xff\xff\xff\xff\xfc\x00\x00\xa0\x3f\x00\x00' \ + '\x00\x00\x00\x00\x19\x40\x74\x65\x73\x74' + + def tearDown(self): + ''' Cleans up the test environment ''' + pass + + #-----------------------------------------------------------------------# + # Payload Builder Tests + #-----------------------------------------------------------------------# + + def testLittleEndianPayloadBuilder(self): + ''' Test basic bit message encoding/decoding ''' + builder = PayloadBuilder(endian=Endian.Little) + builder.add_8bit_uint(1) + builder.add_16bit_uint(2) + builder.add_32bit_uint(3) + builder.add_64bit_uint(4) + builder.add_8bit_int(-1) + builder.add_16bit_int(-2) + builder.add_32bit_int(-3) + builder.add_64bit_int(-4) + builder.add_32bit_float(1.25) + builder.add_64bit_float(6.25) + builder.add_string('test') + self.assertEqual(self.little_endian_payload, builder.tostring()) + + def testPayloadBuilderReset(self): + ''' Test basic bit message encoding/decoding ''' + builder = PayloadBuilder() + builder.add_8bit_uint(0x12) + builder.add_8bit_uint(0x34) + self.assertEqual('\x12\x34', builder.tostring()) + self.assertEqual(['\x12', '\x34'], builder.tolist()) + builder.reset() + self.assertEqual('', builder.tostring()) + self.assertEqual([], builder.tolist()) + + #-----------------------------------------------------------------------# + # Payload Decoder Tests + #-----------------------------------------------------------------------# + + def testLittleEndianPayloadDecoder(self): + ''' Test basic bit message encoding/decoding ''' + decoder = PayloadDecoder(self.little_endian_payload) + self.assertEqual(1, decoder.decode_8bit_uint()) + self.assertEqual(2, decoder.decode_16bit_uint()) + self.assertEqual(3, decoder.decode_32bit_uint()) + self.assertEqual(4, decoder.decode_64bit_uint()) + self.assertEqual(-1, decoder.decode_8bit_int()) + self.assertEqual(-2, decoder.decode_16bit_int()) + self.assertEqual(-3, decoder.decode_32bit_int()) + self.assertEqual(-4, decoder.decode_64bit_int()) + self.assertEqual(1.25, decoder.decode_32bit_float()) + self.assertEqual(6.25, decoder.decode_64bit_float()) + self.assertEqual('test', decoder.decode_string(4)) + + def testPayloadDecoderReset(self): + ''' Test the payload decoder reset functionality ''' + decoder = PayloadDecoder('\x12\x34') + self.assertEqual(0x12, decoder.decode_8bit_uint()) + self.assertEqual(0x34, decoder.decode_8bit_uint()) + decoder.reset() + self.assertEqual(0x3412, decoder.decode_16bit_uint()) + + +#---------------------------------------------------------------------------# +# Main +#---------------------------------------------------------------------------# +if __name__ == "__main__": + unittest.main() From 7c9c7b129dacaac1a52b9e14172fd792b66b56d9 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 31 Aug 2011 14:12:48 +0000 Subject: [PATCH 025/243] Fixes issue 53 --- pymodbus/server/sync.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index d00124b1d..7448c08f3 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -54,6 +54,7 @@ def handle(self): while self.running: try: data = self.request.recv(1024) + if not data: self.running = False _logger.debug(" ".join([hex(ord(x)) for x in data])) # if not self.server.control.ListenOnly: self.framer.processIncomingPacket(data, self.execute) From ec8d571f444522691cb5ca2ed03a9925f3c9f544 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 31 Aug 2011 14:32:43 +0000 Subject: [PATCH 026/243] Fix to add Python < 2.5 compatibility This fix simply removes all the ternaries that were added in Python 2.5. Fixes issue 50 --- pymodbus/bit_write_message.py | 10 ++++++---- pymodbus/other_message.py | 9 ++++++--- pymodbus/register_read_message.py | 4 ++-- pymodbus/transaction.py | 12 ++++++++---- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/pymodbus/bit_write_message.py b/pymodbus/bit_write_message.py index 74c87148d..64f291daf 100644 --- a/pymodbus/bit_write_message.py +++ b/pymodbus/bit_write_message.py @@ -48,7 +48,7 @@ def __init__(self, address=None, value=None, **kwargs): ''' ModbusRequest.__init__(self, **kwargs) self.address = address - self.value = True if value else False + self.value = bool(value) def encode(self): ''' Encodes write coil request @@ -56,7 +56,8 @@ def encode(self): :returns: The byte encoded message ''' result = struct.pack('>H', self.address) - result += _turn_coil_on if self.value else _turn_coil_off + if self.value: result += _turn_coil_on + else: result += _turn_coil_off return result def decode(self, data): @@ -65,7 +66,7 @@ def decode(self, data): :param data: The packet data to decode ''' self.address, value = struct.unpack('>HH', data) - self.value = True if value == ModbusStatus.On else False + self.value = (value == ModbusStatus.On) def execute(self, context): ''' Run a write coil request against a datastore @@ -114,7 +115,8 @@ def encode(self): :return: The byte encoded message ''' result = struct.pack('>H', self.address) - result += _turn_coil_on if self.value else _turn_coil_off + if self.value: result += _turn_coil_on + else: result += _turn_coil_off return result def decode(self, data): diff --git a/pymodbus/other_message.py b/pymodbus/other_message.py index f6cb90037..ec11151f5 100644 --- a/pymodbus/other_message.py +++ b/pymodbus/other_message.py @@ -184,7 +184,8 @@ def encode(self): :returns: The byte encoded message ''' - ready = ModbusStatus.Ready if self.status else ModbusStatus.Waiting + if self.status: ready = ModbusStatus.Ready + else: ready = ModbusStatus.Waiting return struct.pack('>HH', ready, self.count) def decode(self, data): @@ -297,7 +298,8 @@ def encode(self): :returns: The byte encoded message ''' - ready = ModbusStatus.Ready if self.status else ModbusStatus.Waiting + if self.status: ready = ModbusStatus.Ready + else: ready = ModbusStatus.Waiting packet = struct.pack('>B', 6 + len(self.events)) packet += struct.pack('>H', ready) packet += struct.pack('>HH', self.event_count, self.message_count) @@ -395,7 +397,8 @@ def encode(self): :returns: The byte encoded message ''' - status = ModbusStatus.SlaveOn if self.status else ModbusStatus.SlaveOff + if self.status: status = ModbusStatus.SlaveOn + else: status = ModbusStatus.SlaveOff length = len(self.identifier) + 2 packet = struct.pack('>B', length) packet += self.identifier # we assume it is already encoded diff --git a/pymodbus/register_read_message.py b/pymodbus/register_read_message.py index ccab840aa..2bca52397 100644 --- a/pymodbus/register_read_message.py +++ b/pymodbus/register_read_message.py @@ -59,7 +59,7 @@ def __init__(self, values, **kwargs): :param values: The values to write to ''' ModbusResponse.__init__(self, **kwargs) - self.registers = values if values != None else [] + self.registers = values or [] def encode(self): ''' Encodes the response packet @@ -307,7 +307,7 @@ def __init__(self, values=None, **kwargs): :param values: The register values to write ''' ModbusResponse.__init__(self, **kwargs) - self.registers = values if values != None else [] + self.registers = values or [] def encode(self): ''' Encodes the response packet diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index f5cb6e425..79ce2c013 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -375,7 +375,8 @@ def getFrame(self): start = self.__hsize end = self.__header['len'] - 2 buffer = self.__buffer[start:end] - return buffer if end > 0 else '' + if end > 0: return buffer + return '' def populateResult(self, result): ''' Populates the modbus result header @@ -518,7 +519,8 @@ def getFrame(self): start = self.__hsize + 1 end = self.__header['len'] - 2 buffer = self.__buffer[start:end] - return a2b_hex(buffer) if end > 0 else '' + if end > 0: return a2b_hex(buffer) + return '' def populateResult(self, result): ''' Populates the modbus result header @@ -673,7 +675,8 @@ def getFrame(self): start = self.__hsize + 1 end = self.__header['len'] - 2 buffer = self.__buffer[start:end] - return buffer if end > 0 else '' + if end > 0: return buffer + return '' def populateResult(self, result): ''' Populates the modbus result header @@ -737,7 +740,8 @@ def _preflight(self, data): :returns: the escaped packet ''' def _filter(a): - return a * 2 if a in ['}', '{'] else a, data + if a in ['}', '{']: return a * 2 + else: return a, data return ''.join(map(_filter, data)) #---------------------------------------------------------------------------# From 305769f348c07ffbb09db59a9e9271b465dd28ff Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 31 Aug 2011 17:38:14 +0000 Subject: [PATCH 027/243] adding a mostly complete diagnostic register implementation --- pymodbus/constants.py | 19 +++++++- pymodbus/device.py | 41 ++++++++++++++++++ pymodbus/diag_message.py | 77 +++++++++++++++++++++++++++++++-- test/test_bit_write_messages.py | 7 ++- test/test_device.py | 9 ++++ test/test_diag_messages.py | 4 ++ 6 files changed, 152 insertions(+), 5 deletions(-) diff --git a/pymodbus/constants.py b/pymodbus/constants.py index efbeb20a3..c0543a1d4 100644 --- a/pymodbus/constants.py +++ b/pymodbus/constants.py @@ -139,7 +139,24 @@ class Endian(Singleton): Little = 0x01 +class ModbusPlusOperation(Singleton): + ''' Represents the type of modbus plus request + + .. attribute:: GetStatistics + + Operation requesting that the current modbus plus statistics + be returned in the response. + + .. attribute:: ClearStatistics + + Operation requesting that the current modbus plus statistics + be cleared and not returned in the response. + ''' + GetStatistics = 0x0003 + ClearStatistics = 0x0004 + + #---------------------------------------------------------------------------# # Exported Identifiers #---------------------------------------------------------------------------# -__all__ = ["Defaults", "ModbusStatus", "Endian"] +__all__ = ["Defaults", "ModbusStatus", "Endian", "ModbusPlusOperation"] diff --git a/pymodbus/device.py b/pymodbus/device.py index 8823dd0f8..6f38ad8de 100644 --- a/pymodbus/device.py +++ b/pymodbus/device.py @@ -73,6 +73,44 @@ def check(self, host): return host in self.__nmstable +#---------------------------------------------------------------------------# +# Modbus Plus Statistics +#---------------------------------------------------------------------------# +class ModbusPlusStatistics(object): + ''' + This is used to maintain the current modbus plus statistics count. As of + right now this is simply a stub to complete the modbus implementation. + For more information, see the modbus implementation guide page 87. + ''' + + def __init__(self): + ''' + Initialize the modbus plus statistics with the default + information. + ''' + self.reset() + + def reset(self): + ''' This clears all of the modbus plus statistics + ''' + self.statistics = [0x0000] * 56 # temporary + #self.token_station_bit_map = [0x0000] * 4 + #self.active_station_bit_map = [0x0000] * 4 + #self.global_data_bit_map = [0x0000] * 4 + #self.receive_buffer_use_bit_map = [0x0000] * 4 + #self.data_master_output_path = [0x0000] * 4 + #self.data_slave_input_path = [0x0000] * 4 + #self.program_master_outptu_path = [0x0000] * 4 + #self.program_slave_input_path = [0x0000] * 4 + + def summary(self): + ''' Returns a summary of the modbus plus statistics + + :returns: 54 16-bit words representing the status + ''' + return self.statistics + + #---------------------------------------------------------------------------# # Device Information Control #---------------------------------------------------------------------------# @@ -326,6 +364,7 @@ class ModbusControlBlock(Singleton): __delimiter = '\r' __counters = ModbusCountersHandler() __identity = ModbusDeviceIdentification() + __plus = ModbusPlusStatistics() __events = [] #-------------------------------------------------------------------------# @@ -376,6 +415,7 @@ def clearEvents(self): Identity = property(lambda s: s.__identity) Counter = property(lambda s: s.__counters) Events = property(lambda s: s.__events) + Plus = property(lambda s: s.__plus) def reset(self): ''' This clears all of the system counters and the @@ -459,6 +499,7 @@ def getDiagnosticRegister(self): #---------------------------------------------------------------------------# __all__ = [ "ModbusAccessControl", + "ModbusPlusStatistics", "ModbusDeviceIdentification", "ModbusControlBlock" ] diff --git a/pymodbus/diag_message.py b/pymodbus/diag_message.py index fc7be4f0a..30b49d8c8 100644 --- a/pymodbus/diag_message.py +++ b/pymodbus/diag_message.py @@ -7,7 +7,8 @@ ''' import struct -from pymodbus.constants import ModbusStatus +from pymodbus.interfaces import Singleton +from pymodbus.constants import ModbusStatus, ModbusPlusOperation from pymodbus.pdu import ModbusRequest from pymodbus.pdu import ModbusResponse from pymodbus.device import ModbusControlBlock @@ -21,8 +22,7 @@ # Diagnostic Function Codes Base Classes # diagnostic 08, 00-18,20 #---------------------------------------------------------------------------# -# TODO Implement subfunction 19 (Return IOP Overrun Count) -# TODO Implement subfunction 21 (Get/Clear Modbus Plus statistics) +# TODO Make sure all the data is decoded from the response #---------------------------------------------------------------------------# class DiagnosticStatusRequest(ModbusRequest): ''' @@ -621,6 +621,36 @@ class ReturnSlaveBusCharacterOverrunCountResponse(DiagnosticStatusSimpleResponse sub_function_code = 0x0012 +#---------------------------------------------------------------------------# +# Diagnostic Sub Code 19 +#---------------------------------------------------------------------------# +class ReturnIopOverrunCountRequest(DiagnosticStatusSimpleRequest): + ''' + An IOP overrun is caused by data characters arriving at the port + faster than they can be stored, or by the loss of a character due + to a hardware malfunction. This function is specific to the 884. + ''' + sub_function_code = 0x0013 + + def execute(self, *args): + ''' Execute the diagnostic request on the given device + + :returns: The initialized response message + ''' + count = _MCB.Counter.BusCharacterOverrun + return ReturnIopOverrunCountResponse(count) + + +class ReturnIopOverrunCountResponse(DiagnosticStatusSimpleResponse): + ''' + The response data field returns the quantity of messages + addressed to the slave that it could not handle due to an 884 + IOP overrun condition, since its last restart, clear counters + operation, or power-up. + ''' + sub_function_code = 0x0013 + + #---------------------------------------------------------------------------# # Diagnostic Sub Code 20 #---------------------------------------------------------------------------# @@ -648,6 +678,45 @@ class ClearOverrunCountResponse(DiagnosticStatusSimpleResponse): ''' sub_function_code = 0x0014 + +#---------------------------------------------------------------------------# +# Diagnostic Sub Code 21 +#---------------------------------------------------------------------------# +class GetClearModbusPlusRequest(DiagnosticStatusSimpleRequest): + ''' + In addition to the Function code (08) and Subfunction code + (00 15 hex) in the query, a two-byte Operation field is used + to specify either a 'Get Statistics' or a 'Clear Statistics' + operation. The two operations are exclusive - the 'Get' + operation cannot clear the statistics, and the 'Clear' + operation does not return statistics prior to clearing + them. Statistics are also cleared on power-up of the slave + device. + ''' + sub_function_code = 0x0015 + + def execute(self, *args): + ''' Execute the diagnostic request on the given device + + :returns: The initialized response message + ''' + message = None # the clear operation does not return info + if self.message == ModbusPlusOperation.ClearStatistics: + _MCB.Plus.reset() + else: message = _MCB.Plus.summary() + return GetClearModbusPlusResponse(message) + + +class GetClearModbusPlusResponse(DiagnosticStatusSimpleResponse): + ''' + Returns a series of 54 16-bit words (108 bytes) in the data field + of the response (this function differs from the usual two-byte + length of the data field). The data contains the statistics for + the Modbus Plus peer processor in the slave device. + ''' + sub_function_code = 0x0015 + + #---------------------------------------------------------------------------# # Exported symbols #---------------------------------------------------------------------------# @@ -666,5 +735,7 @@ class ClearOverrunCountResponse(DiagnosticStatusSimpleResponse): "ReturnSlaveNAKCountRequest", "ReturnSlaveNAKCountResponse", "ReturnSlaveBusyCountRequest", "ReturnSlaveBusyCountResponse", "ReturnSlaveBusCharacterOverrunCountRequest", "ReturnSlaveBusCharacterOverrunCountResponse", + "ReturnIopOverrunCountRequest", "ReturnIopOverrunCountResponse", "ClearOverrunCountRequest", "ClearOverrunCountResponse", + "GetClearModbusPlusRequest", "GetClearModbusPlusResponse", ] diff --git a/test/test_bit_write_messages.py b/test/test_bit_write_messages.py index e2f48a48a..315e78d6d 100644 --- a/test/test_bit_write_messages.py +++ b/test/test_bit_write_messages.py @@ -57,7 +57,7 @@ def testInvalidWriteMultipleCoilsRequest(self): self.assertEquals(request.values, []) def testWriteSingleCoilExecute(self): - context = MockContext(False) + context = MockContext(False, default=True) request = WriteSingleCoilRequest(2, True) result = request.execute(context) self.assertEqual(result.exception_code, ModbusExceptions.IllegalAddress) @@ -66,6 +66,11 @@ def testWriteSingleCoilExecute(self): result = request.execute(context) self.assertEqual(result.encode(), '\x00\x02\xff\x00') + context = MockContext(True, default=False) + request = WriteSingleCoilRequest(2, False) + result = request.execute(context) + self.assertEqual(result.encode(), '\x00\x02\x00\x00') + def testWriteMultipleCoilsExecute(self): context = MockContext(False) # too many values diff --git a/test/test_device.py b/test/test_device.py index f1f9313e4..27f70e96e 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -214,6 +214,15 @@ def testRetrievingControlEvents(self): packet = self.control.getEvents() self.assertEqual(packet, '\x40') + def testModbusPlusStatistics(self): + ''' Test device identification reading ''' + default = [0x0000] * 56 + statistics = ModbusPlusStatistics() + self.assertEqual(default, statistics.summary()) + statistics.reset() + self.assertEqual(default, statistics.summary()) + self.assertEqual(default, self.control.Plus.summary()) + #---------------------------------------------------------------------------# # Main #---------------------------------------------------------------------------# diff --git a/test/test_diag_messages.py b/test/test_diag_messages.py index 6da66d3ce..01892df35 100644 --- a/test/test_diag_messages.py +++ b/test/test_diag_messages.py @@ -30,7 +30,9 @@ def setUp(self): (ReturnSlaveNAKCountRequest, '\x00\x10\x00\x00', '\x00\x10\x00\x00'), (ReturnSlaveBusyCountRequest, '\x00\x11\x00\x00', '\x00\x11\x00\x00'), (ReturnSlaveBusCharacterOverrunCountRequest, '\x00\x12\x00\x00', '\x00\x12\x00\x00'), + (ReturnIopOverrunCountRequest, '\x00\x13\x00\x00', '\x00\x13\x00\x00'), (ClearOverrunCountRequest, '\x00\x14\x00\x00', '\x00\x14\x00\x00'), + (GetClearModbusPlusRequest, '\x00\x15\x00\x00', '\x00\x15' + '\x00\x00' * 56), ] self.responses = [ @@ -51,7 +53,9 @@ def setUp(self): (ReturnSlaveNAKCountResponse, '\x00\x10\x00\x00'), (ReturnSlaveBusyCountResponse, '\x00\x11\x00\x00'), (ReturnSlaveBusCharacterOverrunCountResponse, '\x00\x12\x00\x00'), + (ReturnIopOverrunCountResponse, '\x00\x13\x00\x00'), (ClearOverrunCountResponse, '\x00\x14\x00\x00'), + (GetClearModbusPlusResponse, '\x00\x15' + '\x00\x00' * 56), ] def tearDown(self): From 297b16c7ffeff29109e24b75a0b53a08fca48962 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 31 Aug 2011 19:22:36 +0000 Subject: [PATCH 028/243] Adding more documentation and helpful factory methods. The documentation added was to address some questions with the synchronous and asynchrounous server implementations as well as the functionality of the data contexts. The factory methods were added by request to simplify the creation of a fully populated DataBlock address space. --- doc/sphinx/library/constants.rst | 3 +++ doc/sphinx/library/device.rst | 3 +++ doc/sphinx/library/diag-message.rst | 12 ++++++++++ examples/common/asynchronous-server.py | 30 +++++++++++++++++++++++++ examples/common/synchronous-server.py | 31 ++++++++++++++++++++++++++ pymodbus/datastore/context.py | 12 +++++----- pymodbus/datastore/store.py | 18 +++++++++++++++ pymodbus/diag_message.py | 1 - pymodbus/payload.py | 1 - test/test_datastore.py | 10 +++++++++ 10 files changed, 114 insertions(+), 7 deletions(-) diff --git a/doc/sphinx/library/constants.rst b/doc/sphinx/library/constants.rst index f7774c0f0..47edd5bae 100644 --- a/doc/sphinx/library/constants.rst +++ b/doc/sphinx/library/constants.rst @@ -20,3 +20,6 @@ API Documentation .. autoclass:: Endian :members: + +.. autoclass:: ModbusPlusOperation + :members: diff --git a/doc/sphinx/library/device.rst b/doc/sphinx/library/device.rst index ccee0c700..12b100b97 100644 --- a/doc/sphinx/library/device.rst +++ b/doc/sphinx/library/device.rst @@ -15,6 +15,9 @@ API Documentation .. autoclass:: ModbusAccessControl :members: +.. autoclass:: ModbusPlusStatistics + :members: + .. autoclass:: ModbusDeviceIdentification :members: diff --git a/doc/sphinx/library/diag-message.rst b/doc/sphinx/library/diag-message.rst index 0ed413987..5500a1cbc 100644 --- a/doc/sphinx/library/diag-message.rst +++ b/doc/sphinx/library/diag-message.rst @@ -108,9 +108,21 @@ API Documentation .. autoclass:: ReturnSlaveBusCharacterOverrunCountResponse :members: +.. autoclass:: ReturnIopOverrunCountRequest + :members: + +.. autoclass:: ReturnIopOverrunCountResponse + :members: + .. autoclass:: ClearOverrunCountRequest :members: .. autoclass:: ClearOverrunCountResponse :members: +.. autoclass:: GetClearModbusPlusRequest + :members: + +.. autoclass:: GetClearModbusPlusResponse + :members: + diff --git a/examples/common/asynchronous-server.py b/examples/common/asynchronous-server.py index 7112f8fb3..a8ed86c53 100755 --- a/examples/common/asynchronous-server.py +++ b/examples/common/asynchronous-server.py @@ -1,4 +1,12 @@ #!/usr/bin/env python +''' +Pymodbus Asynchronous Server Example +-------------------------------------------------------------------------- + +The asynchronous server is a high performance implementation using the +twisted library as its backend. This allows it to scale to many thousands +of nodes which can be helpful for testing monitoring software. +''' #---------------------------------------------------------------------------# # import the various server implementations #---------------------------------------------------------------------------# @@ -20,6 +28,28 @@ #---------------------------------------------------------------------------# # initialize your data store #---------------------------------------------------------------------------# +# The datastores only respond to the addresses that they are initialized to. +# Therefore, if you initialize a DataBlock to addresses of 0x00 to 0xFF, a +# request to 0x100 will respond with an invalid address exception. This is +# because many devices exhibit this kind of behavior (but not all): +# +# block = ModbusSequentialDataBlock(0x00, [0]*0xff) +# +# Continuting, you can choose to use a sequential or a sparse DataBlock in +# your data context. The difference is that the sequential has no gaps in +# the data while the sparse can. Once again, there are devices that exhibit +# both forms of behavior: +# +# block = ModbusSparseDataBlock({0x00: 0, 0x05: 1}) +# block = ModbusSequentialDataBlock(0x00, [0]*5) +# +# Alternately, you can use the factory methods to initialize the DataBlocks +# or simply do not pass them to have them initialized to 0x00 on the full +# address range: +# +# store = ModbusSlaveContext(di = ModbusSequentialDataBlock.create()) +# store = ModbusSlaveContext() +#---------------------------------------------------------------------------# store = ModbusSlaveContext( di = ModbusSequentialDataBlock(0, [17]*100), co = ModbusSequentialDataBlock(0, [17]*100), diff --git a/examples/common/synchronous-server.py b/examples/common/synchronous-server.py index d9dea0d6c..59a038f9f 100755 --- a/examples/common/synchronous-server.py +++ b/examples/common/synchronous-server.py @@ -1,4 +1,13 @@ #!/usr/bin/env python +''' +Pymodbus Synchronous Server Example +-------------------------------------------------------------------------- + +The synchronous server is implemented in pure python without any third +party libraries (unless you need to use the serial protocols which require +pyserial). This is helpful in constrained or old environments where using +twisted just is not feasable. What follows is an examle of its use: +''' #---------------------------------------------------------------------------# # import the various server implementations #---------------------------------------------------------------------------# @@ -20,6 +29,28 @@ #---------------------------------------------------------------------------# # initialize your data store #---------------------------------------------------------------------------# +# The datastores only respond to the addresses that they are initialized to. +# Therefore, if you initialize a DataBlock to addresses of 0x00 to 0xFF, a +# request to 0x100 will respond with an invalid address exception. This is +# because many devices exhibit this kind of behavior (but not all): +# +# block = ModbusSequentialDataBlock(0x00, [0]*0xff) +# +# Continuting, you can choose to use a sequential or a sparse DataBlock in +# your data context. The difference is that the sequential has no gaps in +# the data while the sparse can. Once again, there are devices that exhibit +# both forms of behavior: +# +# block = ModbusSparseDataBlock({0x00: 0, 0x05: 1}) +# block = ModbusSequentialDataBlock(0x00, [0]*5) +# +# Alternately, you can use the factory methods to initialize the DataBlocks +# or simply do not pass them to have them initialized to 0x00 on the full +# address range: +# +# store = ModbusSlaveContext(di = ModbusSequentialDataBlock.create()) +# store = ModbusSlaveContext() +#---------------------------------------------------------------------------# store = ModbusSlaveContext( di = ModbusSequentialDataBlock(0, [17]*100), co = ModbusSequentialDataBlock(0, [17]*100), diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index 6d3c722cc..ef9ab36c1 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -19,7 +19,9 @@ class ModbusSlaveContext(IModbusSlaveContext): ''' def __init__(self, *args, **kwargs): - ''' Initializes the datastores + ''' Initializes the datastores, defaults to fully populated + sequential data blocks if none are passed in. + :param kwargs: Each element is a ModbusDataBlock 'di' - Discrete Inputs initializer @@ -28,10 +30,10 @@ def __init__(self, *args, **kwargs): 'ir' - Input Registers iniatializer ''' self.store = {} - self.store['d'] = kwargs.get('di', ModbusSequentialDataBlock(0, 0)) - self.store['c'] = kwargs.get('co', ModbusSequentialDataBlock(0, 0)) - self.store['i'] = kwargs.get('ir', ModbusSequentialDataBlock(0, 0)) - self.store['h'] = kwargs.get('hr', ModbusSequentialDataBlock(0, 0)) + self.store['d'] = kwargs.get('di', ModbusSequentialDataBlock.create()) + self.store['c'] = kwargs.get('co', ModbusSequentialDataBlock.create()) + self.store['i'] = kwargs.get('ir', ModbusSequentialDataBlock.create()) + self.store['h'] = kwargs.get('hr', ModbusSequentialDataBlock.create()) def __str__(self): ''' Returns a string representation of the context diff --git a/pymodbus/datastore/store.py b/pymodbus/datastore/store.py index e760bb6bc..3306d62a0 100644 --- a/pymodbus/datastore/store.py +++ b/pymodbus/datastore/store.py @@ -144,6 +144,15 @@ def __init__(self, address, values): else: self.values = [values] self.default_value = self.values[0].__class__() + @staticmethod + def create(): + ''' Factory method to create a datastore with the + full address space initialized to 0x00 + + :returns: An initialized datastore + ''' + return ModbusSequentialDataBlock(0x00, [0x00]*65536) + def validate(self, address, count=1): ''' Checks to see if the request is in range @@ -197,6 +206,15 @@ def __init__(self, values): self.default_value = self.values.values()[0].__class__() self.address = self.values.iterkeys().next() + @staticmethod + def create(): + ''' Factory method to create a datastore with the + full address space initialized to 0x00 + + :returns: An initialized datastore + ''' + return ModbusSparseDataBlock([0x00]*65536) + def validate(self, address, count=1): ''' Checks to see if the request is in range diff --git a/pymodbus/diag_message.py b/pymodbus/diag_message.py index 30b49d8c8..b6b310530 100644 --- a/pymodbus/diag_message.py +++ b/pymodbus/diag_message.py @@ -7,7 +7,6 @@ ''' import struct -from pymodbus.interfaces import Singleton from pymodbus.constants import ModbusStatus, ModbusPlusOperation from pymodbus.pdu import ModbusRequest from pymodbus.pdu import ModbusResponse diff --git a/pymodbus/payload.py b/pymodbus/payload.py index cb5e386bd..abcc3b958 100644 --- a/pymodbus/payload.py +++ b/pymodbus/payload.py @@ -6,7 +6,6 @@ modbus messages payloads. ''' from struct import pack, unpack -from pymodbus.interfaces import Singleton from pymodbus.constants import Endian diff --git a/test/test_datastore.py b/test/test_datastore.py index f0c1eea63..acb0f1d4f 100644 --- a/test/test_datastore.py +++ b/test/test_datastore.py @@ -63,6 +63,11 @@ def testModbusSequentialDataBlock(self): block.setValues(0x00, [True]*10) self.assertEqual(block.getValues(0x00, 10), [True]*10) + def testModbusSequentialDataBlockFactory(self): + ''' Test the sequential data block store factory ''' + block = ModbusSequentialDataBlock.create() + self.assertEqual(block.getValues(0x00, 65536), [False]*65536) + def testModbusSparseDataBlock(self): ''' Test a sparse data block store ''' values = dict(enumerate([True]*10)) @@ -84,6 +89,11 @@ def testModbusSparseDataBlock(self): block.setValues(0x00, dict(enumerate([False]*10))) self.assertEqual(block.getValues(0x00, 10), [False]*10) + def testModbusSparseDataBlockFactory(self): + ''' Test the sparse data block store factory ''' + block = ModbusSparseDataBlock.create() + self.assertEqual(block.getValues(0x00, 65536), [False]*65536) + def testModbusSparseDataBlockOther(self): block = ModbusSparseDataBlock([True]*10) self.assertEqual(block.getValues(0x00, 10), [True]*10) From f45eb916174bf6fd6c8280fc4503b4ade4227d08 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 31 Aug 2011 21:04:20 +0000 Subject: [PATCH 029/243] adding modbus plus registers --- pymodbus/device.py | 84 +++++++++++++++++++++++++++++++++----- pymodbus/diag_message.py | 2 +- test/test_device.py | 8 ++-- test/test_diag_messages.py | 4 +- 4 files changed, 81 insertions(+), 17 deletions(-) diff --git a/pymodbus/device.py b/pymodbus/device.py index 6f38ad8de..88e738496 100644 --- a/pymodbus/device.py +++ b/pymodbus/device.py @@ -83,6 +83,60 @@ class ModbusPlusStatistics(object): For more information, see the modbus implementation guide page 87. ''' + __data = { + 'node_type_id' : [0x00] * 2, # 00 + 'software_version_number' : [0x00] * 2, # 01 + 'network_address' : [0x00] * 2, # 02 + 'mac_state_variable' : [0x00] * 2, # 03 + 'peer_status_code' : [0x00] * 2, # 04 + 'token_pass_counter' : [0x00] * 2, # 05 + 'token_rotation_time' : [0x00] * 2, # 06 + + 'program_master_token_failed' : [0x00], # 07 hi + 'data_master_token_failed' : [0x00], # 07 lo + 'program_master_token_owner' : [0x00], # 08 hi + 'data_master_token_owner' : [0x00], # 08 lo + 'program_slave_token_owner' : [0x00], # 09 hi + 'data_slave_token_owner' : [0x00], # 09 lo + 'data_slave_command_transfer' : [0x00], # 10 hi + '__unused_10_lowbit' : [0x00], # 10 lo + + 'program_slave_command_transfer' : [0x00], # 11 hi + 'program_master_rsp_transfer' : [0x00], # 11 lo + 'program_slave_auto_logout' : [0x00], # 12 hi + 'program_master_connect_status' : [0x00], # 12 lo + 'receive_buffer_dma_overrun' : [0x00], # 13 hi + 'pretransmit_deferral_error' : [0x00], # 13 lo + 'frame_size_error' : [0x00], # 14 hi + 'repeated_command_received' : [0x00], # 14 lo + 'receiver_alignment_error' : [0x00], # 15 hi + 'receiver_collision_abort_error' : [0x00], # 15 lo + 'bad_packet_length_error' : [0x00], # 16 hi + 'receiver_crc_error' : [0x00], # 16 lo + 'transmit_buffer_dma_underrun' : [0x00], # 17 hi + 'bad_link_address_error' : [0x00], # 17 lo + + 'bad_mac_function_code_error' : [0x00], # 18 hi + 'internal_packet_length_error' : [0x00], # 18 lo + 'communication_failed_error' : [0x00], # 19 hi + 'communication_retries' : [0x00], # 19 lo + 'no_response_error' : [0x00], # 20 hi + 'good_receive_packet' : [0x00], # 20 lo + 'unexpected_path_error' : [0x00], # 21 hi + 'exception_response_error' : [0x00], # 21 lo + 'forgotten_transaction_error' : [0x00], # 22 hi + 'unexpected_response_error' : [0x00], # 22 lo + + 'active_station_bit_map' : [0x00] * 8, # 23-26 + 'token_station_bit_map' : [0x00] * 8, # 27-30 + 'global_data_bit_map' : [0x00] * 8, # 31-34 + 'receive_buffer_use_bit_map' : [0x00] * 8, # 35-37 + 'data_master_output_path' : [0x00] * 8, # 38-41 + 'data_slave_input_path' : [0x00] * 8, # 42-45 + 'program_master_outptu_path' : [0x00] * 8, # 46-49 + 'program_slave_input_path' : [0x00] * 8, # 50-53 + } + def __init__(self): ''' Initialize the modbus plus statistics with the default @@ -90,25 +144,35 @@ def __init__(self): ''' self.reset() + def __iter__(self): + ''' Iterater over the statistics + + :returns: An iterator of the modbus plus statistics + ''' + return self.__data.iteritems() + def reset(self): ''' This clears all of the modbus plus statistics ''' - self.statistics = [0x0000] * 56 # temporary - #self.token_station_bit_map = [0x0000] * 4 - #self.active_station_bit_map = [0x0000] * 4 - #self.global_data_bit_map = [0x0000] * 4 - #self.receive_buffer_use_bit_map = [0x0000] * 4 - #self.data_master_output_path = [0x0000] * 4 - #self.data_slave_input_path = [0x0000] * 4 - #self.program_master_outptu_path = [0x0000] * 4 - #self.program_slave_input_path = [0x0000] * 4 + for key in self.__data: + self.__data[key] = [0x00] * len(self.__data[key]) def summary(self): ''' Returns a summary of the modbus plus statistics :returns: 54 16-bit words representing the status ''' - return self.statistics + return self.__data.values() + + def encode(self): + ''' Returns a summary of the modbus plus statistics + + :returns: 54 16-bit words representing the status + ''' + total, values = [], sum(self.__data.values(), []) + for c in xrange(0, len(values), 2): + total.append((values[c] << 8) | values[c+1]) + return total #---------------------------------------------------------------------------# diff --git a/pymodbus/diag_message.py b/pymodbus/diag_message.py index b6b310530..439332490 100644 --- a/pymodbus/diag_message.py +++ b/pymodbus/diag_message.py @@ -702,7 +702,7 @@ def execute(self, *args): message = None # the clear operation does not return info if self.message == ModbusPlusOperation.ClearStatistics: _MCB.Plus.reset() - else: message = _MCB.Plus.summary() + else: message = _MCB.Plus.encode() return GetClearModbusPlusResponse(message) diff --git a/test/test_device.py b/test/test_device.py index 27f70e96e..64346e1f1 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -216,12 +216,12 @@ def testRetrievingControlEvents(self): def testModbusPlusStatistics(self): ''' Test device identification reading ''' - default = [0x0000] * 56 + default = [0x0000] * 55 statistics = ModbusPlusStatistics() - self.assertEqual(default, statistics.summary()) + self.assertEqual(default, statistics.encode()) statistics.reset() - self.assertEqual(default, statistics.summary()) - self.assertEqual(default, self.control.Plus.summary()) + self.assertEqual(default, statistics.encode()) + self.assertEqual(default, self.control.Plus.encode()) #---------------------------------------------------------------------------# # Main diff --git a/test/test_diag_messages.py b/test/test_diag_messages.py index 01892df35..bff959837 100644 --- a/test/test_diag_messages.py +++ b/test/test_diag_messages.py @@ -32,7 +32,7 @@ def setUp(self): (ReturnSlaveBusCharacterOverrunCountRequest, '\x00\x12\x00\x00', '\x00\x12\x00\x00'), (ReturnIopOverrunCountRequest, '\x00\x13\x00\x00', '\x00\x13\x00\x00'), (ClearOverrunCountRequest, '\x00\x14\x00\x00', '\x00\x14\x00\x00'), - (GetClearModbusPlusRequest, '\x00\x15\x00\x00', '\x00\x15' + '\x00\x00' * 56), + (GetClearModbusPlusRequest, '\x00\x15\x00\x00', '\x00\x15' + '\x00\x00' * 55), ] self.responses = [ @@ -55,7 +55,7 @@ def setUp(self): (ReturnSlaveBusCharacterOverrunCountResponse, '\x00\x12\x00\x00'), (ReturnIopOverrunCountResponse, '\x00\x13\x00\x00'), (ClearOverrunCountResponse, '\x00\x14\x00\x00'), - (GetClearModbusPlusResponse, '\x00\x15' + '\x00\x00' * 56), + (GetClearModbusPlusResponse, '\x00\x15' + '\x00\x00' * 55), ] def tearDown(self): From a4d681114c0f42aa5d918db98a2e551c4c5e816c Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Tue, 6 Sep 2011 15:50:41 +0000 Subject: [PATCH 030/243] adding endian ability to payload builder --- pymodbus/constants.py | 6 +- pymodbus/payload.py | 133 ++++++++++++++++++++++++++++-------------- test/test_payload.py | 37 +++++++++++- 3 files changed, 127 insertions(+), 49 deletions(-) diff --git a/pymodbus/constants.py b/pymodbus/constants.py index c0543a1d4..5d76ba16d 100644 --- a/pymodbus/constants.py +++ b/pymodbus/constants.py @@ -134,9 +134,11 @@ class Endian(Singleton): This indicates that the bytes are in big endian format + .. note:: I am simply borrowing the format strings from the + python struct module for my convenience. ''' - Big = 0x00 - Little = 0x01 + Big = '>' + Little = '<' class ModbusPlusOperation(Singleton): diff --git a/pymodbus/payload.py b/pymodbus/payload.py index abcc3b958..1494c4f25 100644 --- a/pymodbus/payload.py +++ b/pymodbus/payload.py @@ -10,196 +10,241 @@ class PayloadBuilder(object): - - def __init__(self, payload=None, endian=Endian.Big): + ''' + A utility that helps build payload messages to be + written with the various modbus messages. It really is just + a simple wrapper around the struct module, however it saves + time looking up the format strings. What follows is a simple + example:: + + builder = PayloadBuilder(endian=Endian.Little) + builder.add_8bit_uint(1) + builder.add_16bit_uint(2) + payload = builder.tostring() + ''' + + def __init__(self, payload=None, endian=Endian.Little): ''' Initialize a new instance of the payload builder :param payload: Raw payload data to initialize with :param endian: The endianess of the payload ''' - self.payload = payload or [] + self._payload = payload or [] + self._endian = endian def reset(self): ''' Reset the payload buffer ''' - self.payload = [] + self._payload = [] def tostring(self): ''' Return the payload buffer as a string :returns: The payload buffer as a string ''' - return ''.join(self.payload) + return ''.join(self._payload) def tolist(self): ''' Return the payload buffer as a list :returns: The payload buffer as a list ''' - return self.payload + return self._payload def add_8bit_uint(self, value): ''' Adds a 8 bit unsigned int to the buffer :param value: The value to add to the buffer ''' - self.payload.append(pack('B', value)) + fstring = self._endian + 'B' + self._payload.append(pack(fstring, value)) def add_16bit_uint(self, value): ''' Adds a 16 bit unsigned int to the buffer :param value: The value to add to the buffer ''' - self.payload.append(pack('H', value)) + fstring = self._endian + 'H' + self._payload.append(pack(fstring, value)) def add_32bit_uint(self, value): ''' Adds a 32 bit unsigned int to the buffer :param value: The value to add to the buffer ''' - self.payload.append(pack('I', value)) + fstring = self._endian + 'I' + self._payload.append(pack(fstring, value)) def add_64bit_uint(self, value): ''' Adds a 64 bit unsigned int to the buffer :param value: The value to add to the buffer ''' - self.payload.append(pack('Q', value)) + fstring = self._endian + 'Q' + self._payload.append(pack(fstring, value)) def add_8bit_int(self, value): ''' Adds a 8 bit signed int to the buffer :param value: The value to add to the buffer ''' - self.payload.append(pack('b', value)) + fstring = self._endian + 'b' + self._payload.append(pack(fstring, value)) def add_16bit_int(self, value): ''' Adds a 16 bit signed int to the buffer :param value: The value to add to the buffer ''' - self.payload.append(pack('h', value)) + fstring = self._endian + 'h' + self._payload.append(pack(fstring, value)) def add_32bit_int(self, value): ''' Adds a 32 bit signed int to the buffer :param value: The value to add to the buffer ''' - self.payload.append(pack('i', value)) + fstring = self._endian + 'i' + self._payload.append(pack(fstring, value)) def add_64bit_int(self, value): ''' Adds a 64 bit signed int to the buffer :param value: The value to add to the buffer ''' - self.payload.append(pack('q', value)) + fstring = self._endian + 'q' + self._payload.append(pack(fstring, value)) def add_32bit_float(self, value): ''' Adds a 32 bit float to the buffer :param value: The value to add to the buffer ''' - self.payload.append(pack('f', value)) + fstring = self._endian + 'f' + self._payload.append(pack(fstring, value)) def add_64bit_float(self, value): ''' Adds a 64 bit float(double) to the buffer :param value: The value to add to the buffer ''' - self.payload.append(pack('d', value)) + fstring = self._endian + 'd' + self._payload.append(pack(fstring, value)) def add_string(self, value): ''' Adds a string to the buffer :param value: The value to add to the buffer ''' + fstring = self._endian + 's' for c in value: - self.payload.append(pack('s', c)) + self._payload.append(pack(fstring, c)) class PayloadDecoder(object): - - def __init__(self, payload, endian=Endian.Big): + ''' + A utility that helps decode payload messages from a modbus + reponse message. It really is just a simple wrapper around + the struct module, however it saves time looking up the format + strings. What follows is a simple example:: + + decoder = PayloadDecoder(self.little_endian_payload) + first = decoder.decode_8bit_uint() + second = decoder.decode_16bit_uint() + ''' + + def __init__(self, payload, endian=Endian.Little): ''' Initialize a new payload decoder :param payload: The payload to decode with :param endian: The endianess of the payload ''' - self.payload = payload - self.pointer = 0x00 + self._payload = payload + self._pointer = 0x00 + self._endian = endian def reset(self): ''' Reset the decoder pointer back to the start ''' - self.pointer = 0x00 + self._pointer = 0x00 def decode_8bit_uint(self): ''' Decodes a 8 bit unsigned int from the buffer ''' - self.pointer += 1 - return unpack('B', self.payload[self.pointer - 1:self.pointer])[0] + self._pointer += 1 + fstring = self._endian + 'B' + return unpack('B', self._payload[self._pointer - 1:self._pointer])[0] def decode_16bit_uint(self): ''' Decodes a 16 bit unsigned int from the buffer ''' - self.pointer += 2 - return unpack('H', self.payload[self.pointer - 2:self.pointer])[0] + self._pointer += 2 + fstring = self._endian + 'H' + return unpack(fstring, self._payload[self._pointer - 2:self._pointer])[0] def decode_32bit_uint(self): ''' Decodes a 32 bit unsigned int from the buffer ''' - self.pointer += 4 - return unpack('I', self.payload[self.pointer - 4:self.pointer])[0] + self._pointer += 4 + fstring = self._endian + 'I' + return unpack(fstring, self._payload[self._pointer - 4:self._pointer])[0] def decode_64bit_uint(self): ''' Decodes a 64 bit unsigned int from the buffer ''' - self.pointer += 8 - return unpack('Q', self.payload[self.pointer - 8:self.pointer])[0] + self._pointer += 8 + fstring = self._endian + 'Q' + return unpack(fstring, self._payload[self._pointer - 8:self._pointer])[0] def decode_8bit_int(self): ''' Decodes a 8 bit signed int from the buffer ''' - self.pointer += 1 - return unpack('b', self.payload[self.pointer - 1:self.pointer])[0] + self._pointer += 1 + fstring = self._endian + 'b' + return unpack(fstring, self._payload[self._pointer - 1:self._pointer])[0] def decode_16bit_int(self): ''' Decodes a 16 bit signed int from the buffer ''' - self.pointer += 2 - return unpack('h', self.payload[self.pointer - 2:self.pointer])[0] + self._pointer += 2 + fstring = self._endian + 'h' + return unpack(fstring, self._payload[self._pointer - 2:self._pointer])[0] def decode_32bit_int(self): ''' Decodes a 32 bit signed int from the buffer ''' - self.pointer += 4 - return unpack('i', self.payload[self.pointer - 4:self.pointer])[0] + self._pointer += 4 + fstring = self._endian + 'i' + return unpack(fstring, self._payload[self._pointer - 4:self._pointer])[0] def decode_64bit_int(self): ''' Decodes a 64 bit signed int from the buffer ''' - self.pointer += 8 - return unpack('q', self.payload[self.pointer - 8:self.pointer])[0] + self._pointer += 8 + fstring = self._endian + 'q' + return unpack(fstring, self._payload[self._pointer - 8:self._pointer])[0] def decode_32bit_float(self): ''' Decodes a 32 bit float from the buffer ''' - self.pointer += 4 - return unpack('f', self.payload[self.pointer - 4:self.pointer])[0] + self._pointer += 4 + fstring = self._endian + 'f' + return unpack(fstring, self._payload[self._pointer - 4:self._pointer])[0] def decode_64bit_float(self): ''' Decodes a 64 bit float(double) from the buffer ''' - self.pointer += 8 - return unpack('d', self.payload[self.pointer - 8:self.pointer])[0] + self._pointer += 8 + fstring = self._endian + 'd' + return unpack(fstring, self._payload[self._pointer - 8:self._pointer])[0] def decode_string(self, size=1): ''' Decodes a string from the buffer :param size: The size of the string to decode ''' - self.pointer += size - return self.payload[self.pointer - size:self.pointer] + self._pointer += size + return self._payload[self._pointer - size:self._pointer] #---------------------------------------------------------------------------# # Exported Identifiers diff --git a/test/test_payload.py b/test/test_payload.py index e43670399..a2227de55 100644 --- a/test/test_payload.py +++ b/test/test_payload.py @@ -35,8 +35,8 @@ def setUp(self): self.big_endian_payload = \ '\x01\x00\x02\x00\x00\x00\x03\x00\x00\x00\x00\x00' \ '\x00\x00\x04\xff\xff\xfe\xff\xff\xff\xfd\xff\xff' \ - '\xff\xff\xff\xff\xff\xfc\x00\x00\xa0\x3f\x00\x00' \ - '\x00\x00\x00\x00\x19\x40\x74\x65\x73\x74' + '\xff\xff\xff\xff\xff\xfc\x3f\xa0\x00\x00\x40\x19' \ + '\x00\x00\x00\x00\x00\x00\x74\x65\x73\x74' def tearDown(self): ''' Cleans up the test environment ''' @@ -62,6 +62,22 @@ def testLittleEndianPayloadBuilder(self): builder.add_string('test') self.assertEqual(self.little_endian_payload, builder.tostring()) + def testBigEndianPayloadBuilder(self): + ''' Test basic bit message encoding/decoding ''' + builder = PayloadBuilder(endian=Endian.Big) + builder.add_8bit_uint(1) + builder.add_16bit_uint(2) + builder.add_32bit_uint(3) + builder.add_64bit_uint(4) + builder.add_8bit_int(-1) + builder.add_16bit_int(-2) + builder.add_32bit_int(-3) + builder.add_64bit_int(-4) + builder.add_32bit_float(1.25) + builder.add_64bit_float(6.25) + builder.add_string('test') + self.assertEqual(self.big_endian_payload, builder.tostring()) + def testPayloadBuilderReset(self): ''' Test basic bit message encoding/decoding ''' builder = PayloadBuilder() @@ -79,7 +95,22 @@ def testPayloadBuilderReset(self): def testLittleEndianPayloadDecoder(self): ''' Test basic bit message encoding/decoding ''' - decoder = PayloadDecoder(self.little_endian_payload) + decoder = PayloadDecoder(self.little_endian_payload, endian=Endian.Little) + self.assertEqual(1, decoder.decode_8bit_uint()) + self.assertEqual(2, decoder.decode_16bit_uint()) + self.assertEqual(3, decoder.decode_32bit_uint()) + self.assertEqual(4, decoder.decode_64bit_uint()) + self.assertEqual(-1, decoder.decode_8bit_int()) + self.assertEqual(-2, decoder.decode_16bit_int()) + self.assertEqual(-3, decoder.decode_32bit_int()) + self.assertEqual(-4, decoder.decode_64bit_int()) + self.assertEqual(1.25, decoder.decode_32bit_float()) + self.assertEqual(6.25, decoder.decode_64bit_float()) + self.assertEqual('test', decoder.decode_string(4)) + + def testBigEndianPayloadDecoder(self): + ''' Test basic bit message encoding/decoding ''' + decoder = PayloadDecoder(self.big_endian_payload, endian=Endian.Big) self.assertEqual(1, decoder.decode_8bit_uint()) self.assertEqual(2, decoder.decode_16bit_uint()) self.assertEqual(3, decoder.decode_32bit_uint()) From d5a440203c0242f497fd6119be85c6f4c5cf03da Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Tue, 6 Sep 2011 20:08:16 +0000 Subject: [PATCH 031/243] Finishing the remaining modbus protocol * Adding the remaining portions of the protocol (request/response) * Tieing these into the factory decoder * Adding tests to cover the new code (need more) * Fixing a few bugs found along the way --- pymodbus/diag_message.py | 2 +- pymodbus/factory.py | 60 ++++++- pymodbus/file_message.py | 309 +++++++++++++++++++++++++++++++++++- pymodbus/other_message.py | 8 +- test/test_factory.py | 38 ++--- test/test_other_messages.py | 2 +- 6 files changed, 385 insertions(+), 34 deletions(-) diff --git a/pymodbus/diag_message.py b/pymodbus/diag_message.py index 439332490..8468f3d41 100644 --- a/pymodbus/diag_message.py +++ b/pymodbus/diag_message.py @@ -143,7 +143,7 @@ class DiagnosticStatusSimpleResponse(DiagnosticStatusResponse): 2 bytes of data. ''' - def __init__(self, data): + def __init__(self, data=0x0000): ''' General initializer for a simple diagnostic response :param data: The resulting data to return to the client diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 647d72f64..3e30bc56b 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -41,7 +41,35 @@ class ServerDecoder(IModbusDecoder): WriteMultipleRegistersRequest, WriteSingleRegisterRequest, WriteSingleCoilRequest, - ReadWriteMultipleRegistersRequest + ReadWriteMultipleRegistersRequest, + + ReturnQueryDataRequest, + RestartCommunicationsOptionRequest, + ReturnDiagnosticRegisterRequest, + ChangeAsciiInputDelimiterRequest, + ForceListenOnlyModeRequest, + ClearCountersRequest, + ReturnBusMessageCountRequest, + ReturnBusCommunicationErrorCountRequest, + ReturnBusExceptionErrorCountRequest, + ReturnSlaveMessageCountRequest, + ReturnSlaveNoResponseCountRequest, + ReturnSlaveNAKCountRequest, + ReturnSlaveBusyCountRequest, + ReturnSlaveBusCharacterOverrunCountRequest, + ReturnIopOverrunCountRequest, + ClearOverrunCountRequest, + GetClearModbusPlusRequest, + + ReadExceptionStatusRequest, + GetCommEventCounterRequest, + GetCommEventLogRequest, + ReportSlaveIdRequest, + + ReadFileRecordRequest, + WriteFileRecordRequest, + MaskWriteRegisterRequest, + ReadFifoQueueRequest, ] __lookup = dict([(f.function_code, f) for f in __function_table]) @@ -100,7 +128,35 @@ class ClientDecoder(IModbusDecoder): WriteMultipleRegistersResponse, WriteSingleRegisterResponse, WriteSingleCoilResponse, - ReadWriteMultipleRegistersResponse + ReadWriteMultipleRegistersResponse, + + ReturnQueryDataResponse, + RestartCommunicationsOptionResponse, + ReturnDiagnosticRegisterResponse, + ChangeAsciiInputDelimiterResponse, + ForceListenOnlyModeResponse, + ClearCountersResponse, + ReturnBusMessageCountResponse, + ReturnBusCommunicationErrorCountResponse, + ReturnBusExceptionErrorCountResponse, + ReturnSlaveMessageCountResponse, + ReturnSlaveNoReponseCountResponse, + ReturnSlaveNAKCountResponse, + ReturnSlaveBusyCountResponse, + ReturnSlaveBusCharacterOverrunCountResponse, + ReturnIopOverrunCountResponse, + ClearOverrunCountResponse, + GetClearModbusPlusResponse, + + ReadExceptionStatusResponse, + GetCommEventCounterResponse, + GetCommEventLogResponse, + ReportSlaveIdResponse, + + ReadFileRecordResponse, + WriteFileRecordResponse, + MaskWriteRegisterResponse, + ReadFifoQueueResponse, ] __lookup = dict([(f.function_code, f) for f in __function_table]) diff --git a/pymodbus/file_message.py b/pymodbus/file_message.py index de642ae31..aa34546bb 100644 --- a/pymodbus/file_message.py +++ b/pymodbus/file_message.py @@ -11,11 +11,301 @@ #---------------------------------------------------------------------------# -# TODO finish these requests +# File Record Types #---------------------------------------------------------------------------# -# Read File Record 20 -# Write File Record 21 -# mask write register 22 +class FileRecordRequest(object): + ''' Represents a file record read request + ''' + + def __init__(self, **kwargs): + ''' Initializes a new instance + ''' + self.reference_type = kwargs.get('reference_type', 0x06) + self.file_number = kwargs.get('file_number', 0x00) + self.record_number = kwargs.get('record_number', 0x00) + self.record_length = kwargs.get('record_length', 0x00) + + +class FileRecordResponse(object): + ''' Represents a file record read response + ''' + + def __init__(self, **kwargs): + ''' Initializes a new instance + ''' + self.reference_type = kwargs.get('reference_type', 0x06) + self.record_data = kwargs.get('record_number', 0x00) + self.record_length = kwargs.get('record_length', 0x00) + + +#---------------------------------------------------------------------------# +# File Requests/Responses +#---------------------------------------------------------------------------# +class ReadFileRecordRequest(ModbusRequest): + ''' + This function code is used to perform a file record read. All request + data lengths are provided in terms of number of bytes and all record + lengths are provided in terms of registers. + + A file is an organization of records. Each file contains 10000 records, + addressed 0000 to 9999 decimal or 0x0000 to 0x270f. For example, record + 12 is addressed as 12. The function can read multiple groups of + references. The groups can be separating (non-contiguous), but the + references within each group must be sequential. Each group is defined + in a seperate 'sub-request' field that contains seven bytes:: + + The reference type: 1 byte (must be 0x06) + The file number: 2 bytes + The starting record number within the file: 2 bytes + The length of the record to be read: 2 bytes + + The quantity of registers to be read, combined with all other fields + in the expected response, must not exceed the allowable length of the + MODBUS PDU: 235 bytes. + ''' + function_code = 0x14 + + def __init__(self, records=None): + ''' Initializes a new instance + + :param records: The file record requests to be read + ''' + ModbusRequest.__init__(self) + self.records = records or [] + + def encode(self): + ''' Encodes the request packet + + :returns: The byte encoded packet + ''' + packet = struct.pack('B', len(self.records) * 7) + for record in self.records: + packet += struct.pack('>BHHH', 0x06, record.file_number, + record.record_number, record.record_length) + return packet + + def decode(self, data): + ''' Decodes the incoming request + + :param data: The data to decode into the address + ''' + self.records = [] + byte_count = struct.unpack('B', data[0])[0] + for count in xrange(1, byte_count, 7): + decoded = struct.unpack('>BHHH', data[count:count+7]) + record = FileRecordRequest(file_number=decoded[1], + record_number=decoded[2], record_length=decoded[3]) + if decoded[0] == 0x06: self.records.append(record) + + def execute(self, context): + ''' Run a read exeception status request against the store + + :param context: The datastore to request from + :returns: The populated response + ''' + # do some new context operation here + files = [] + return ReadFileRecordResponse(files) + + +class ReadFileRecordResponse(ModbusResponse): + ''' + The normal response is a series of 'sub-responses,' one for each + 'sub-request.' The byte count field is the total combined count of + bytes in all 'sub-responses.' In addition, each 'sub-response' + contains a field that shows its own byte count. + ''' + function_code = 0x14 + + def __init__(self, records=None): + ''' Initializes a new instance + + :param records: The requested file records + ''' + ModbusResponse.__init__(self) + self.records = records or [] + + def encode(self): + ''' Encodes the response + + :returns: The byte encoded message + ''' + total = sum(record.record_length for record in self.records) + packet = struct.pack('B', total) + for record in self.records: + packet += struct.pack('>BB', 0x06, record.record_length) + packet += record.record_data + return packet + + def decode(self, data): + ''' Decodes a the response + + :param data: The packet data to decode + ''' + count, self.records = 1, [] + byte_count = struct.unpack('B', data[0])[0] + while count < byte_count: + record_length, reference_type = struct.unpack('>BB', data[count:count+2]) + count += record_length + record = FileRecordResponse(record_length=record_length, + record_data=data[count - record_length:count]) + if reference_type == 0x06: self.records.append(record) + + +class WriteFileRecordRequest(ModbusRequest): + ''' + ''' + function_code = 0x15 + + def __init__(self, records=None): + ''' Initializes a new instance + + :param records: The file record requests to be read + ''' + ModbusRequest.__init__(self) + self.records = records or [] + + def encode(self): + ''' Encodes the request packet + + :returns: The byte encoded packet + ''' + packet = struct.pack('B', len(self.records) * 7) + for record in self.records: + packet += struct.pack('>BHHH', 0x06, record.file_number, + record.record_number, record.record_length) + return packet + + def decode(self, data): + ''' Decodes the incoming request + + :param data: The data to decode into the address + ''' + self.records = [] + byte_count = struct.unpack('B', data[0])[0] + # todo, decode records + + def execute(self, context): + ''' Run the write file record request against the context + + :param context: The datastore to request from + :returns: The populated response + ''' + # do some new context operation here + files = [] + return WriteFileRecordResponse(files) + + +class WriteFileRecordResponse(ModbusResponse): + ''' + ''' + function_code = 0x15 + _rtu_frame_size = 10 + + def __init__(self, address=0x0000): + ''' Initializes a new instance + + :param address: The mask pointer address (0x0000 to 0xffff) + ''' + ModbusResponse.__init__(self) + pass + + def encode(self): + ''' Encodes the response + + :returns: The byte encoded message + ''' + pass + + def decode(self, data): + ''' Decodes a the response + + :param data: The packet data to decode + ''' + pass + + +class MaskWriteRegisterRequest(ModbusRequest): + ''' + This function code is used to modify the contents of a specified holding + register using a combination of an AND mask, an OR mask, and the + register's current contents. The function can be used to set or clear + individual bits in the register. + ''' + function_code = 0x16 + _rtu_frame_size = 10 + + def __init__(self, address=0x0000, and_mask=0xffff, or_mask=0x0000): + ''' Initializes a new instance + + :param address: The mask pointer address (0x0000 to 0xffff) + :param and_mask: The and bitmask to apply to the register address + :param or_mask: The or bitmask to apply to the register address + ''' + ModbusRequest.__init__(self) + self.address = address + self.and_mask = and_mask + self.or_mask = or_mask + + def encode(self): + ''' Encodes the request packet + + :returns: The byte encoded packet + ''' + return struct.pack('>HHH', self.address, self.and_mask, self.or_mask) + + def decode(self, data): + ''' Decodes the incoming request + + :param data: The data to decode into the address + ''' + self.address, self.and_mask, self.or_mask = struct.unpack('>HHH', data) + + def execute(self, context): + ''' Run a mask write register request against the store + + :param context: The datastore to request from + :returns: The populated response + ''' + # do some new context operation here + return MaskWriteRegisterResponse(self.address, self.and_mask, self.or_mask) + + +class MaskWriteRegisterResponse(ModbusResponse): + ''' + The normal response is an echo of the request. The response is returned + after the register has been written. + ''' + function_code = 0x16 + _rtu_frame_size = 10 + + def __init__(self, address=0x0000, and_mask=0xffff, or_mask=0x0000): + ''' Initializes a new instance + + :param address: The mask pointer address (0x0000 to 0xffff) + :param and_mask: The and bitmask applied to the register address + :param or_mask: The or bitmask applied to the register address + ''' + ModbusResponse.__init__(self) + self.address = address + self.and_mask = and_mask + self.or_mask = or_mask + + def encode(self): + ''' Encodes the response + + :returns: The byte encoded message + ''' + return struct.pack('>HHH', self.address, self.and_mask, self.or_mask) + + def decode(self, data): + ''' Decodes a the response + + :param data: The packet data to decode + ''' + self.address, self.and_mask, self.or_mask = struct.unpack('>HHH', data) + + class ReadFifoQueueRequest(ModbusRequest): ''' This function code allows to read the contents of a First-In-First-Out @@ -31,14 +321,14 @@ class ReadFifoQueueRequest(ModbusRequest): function_code = 0x18 _rtu_frame_size = 6 - def __init__(self, address): + def __init__(self, address=0x0000): ''' Initializes a new instance :param address: The fifo pointer address (0x0000 to 0xffff) ''' ModbusRequest.__init__(self) self.address = address - self.values = [] # dunno where this should come from + self.values = [] # this should be added to the context def encode(self): ''' Encodes the request packet @@ -88,13 +378,13 @@ def calculateRtuFrameSize(cls, buffer): lo_byte = struct.unpack(">B", buffer[3])[0] return (hi_byte << 16) + lo_byte + 6 - def __init__(self, values): + def __init__(self, values=None): ''' Initializes a new instance :param values: The list of values of the fifo to return ''' ModbusResponse.__init__(self) - self.values = values + self.values = values or [] def encode(self): ''' Encodes the response @@ -122,5 +412,8 @@ def decode(self, data): # Exported symbols #---------------------------------------------------------------------------# __all__ = [ + "ReadFileRecordRequest", "ReadFileRecordResponse", + "WriteFileRecordRequest", "WriteFileRecordResponse", + "MaskWriteRegisterRequest", "MaskWriteRegisterResponse", "ReadFifoQueueRequest", "ReadFifoQueueResponse", ] diff --git a/pymodbus/other_message.py b/pymodbus/other_message.py index ec11151f5..4675f7f16 100644 --- a/pymodbus/other_message.py +++ b/pymodbus/other_message.py @@ -69,7 +69,7 @@ class ReadExceptionStatusResponse(ModbusResponse): function_code = 0x07 _rtu_frame_size = 5 - def __init__(self, status): + def __init__(self, status=0x00): ''' Initializes a new instance :param status: The status response to report @@ -170,7 +170,7 @@ class GetCommEventCounterResponse(ModbusResponse): function_code = 0x0b _rtu_frame_size = 8 - def __init__(self, count): + def __init__(self, count=0x0000): ''' Initializes a new instance :param count: The current event counter value @@ -382,7 +382,7 @@ class ReportSlaveIdResponse(ModbusResponse): function_code = 0x11 _rtu_byte_count_pos = 2 - def __init__(self, identifier, status=True): + def __init__(self, identifier=0x00, status=True): ''' Initializes a new instance :param identifier: The identifier of the slave @@ -414,7 +414,7 @@ def decode(self, data): :param data: The packet data to decode ''' length = struct.unpack('>B', data[0])[0] - self.identifier = data[1:length - 1] + self.identifier = data[1:length + 1] status = struct.unpack('>B', data[-1])[0] self.status = status == ModbusStatus.SlaveOn diff --git a/test/test_factory.py b/test/test_factory.py index 47823236d..e6a4acd10 100644 --- a/test/test_factory.py +++ b/test/test_factory.py @@ -22,17 +22,18 @@ def setUp(self): (0x04, '\x04\x00\x01\x00\x01'), # read input registers (0x05, '\x05\x00\x01\x00\x01'), # write single coil (0x06, '\x06\x00\x01\x00\x01'), # write single register - #(0x07, '\x07'), # read exception status - #(0x08, '\x08\x00\x00'), # read diagnostic - #(0x0b, '\x0b'), # get comm event counters - #(0x0c, '\x0c'), # get comm event log - #(0x0f, '\x0f\x00\x01\x00\x08\x010\xff'), # write multiple coils - #(0x10, '\x10\x00\x01\x00\x02\x04\0xff\xff'), # write multiple registers - #(0x11, '\x11'), # report slave id + (0x07, '\x07'), # read exception status + (0x08, '\x08\x00\x00\x00\x00'), # read diagnostic + (0x0b, '\x0b'), # get comm event counters + (0x0c, '\x0c'), # get comm event log + (0x0f, '\x0f\x00\x01\x00\x08\x01\x00\xff'), # write multiple coils + (0x10, '\x10\x00\x01\x00\x02\x04\0xff\xff'), # write multiple registers + (0x11, '\x11'), # report slave id #(0x14, '\x14'), # read file record #(0x15, '\x15'), # write file record - #(0x16, '\x16\x00\x01\x00\xff\xff\x00'), # mask write register - #(0x17, '\x17\x00\x01\x00\x01\x00\x01\x00\x01\x02\x12\x34'),# read/write multiple registers + (0x16, '\x16\x00\x01\x00\xff\xff\x00'), # mask write register + (0x17, '\x17\x00\x01\x00\x01\x00\x01\x00\x01\x02\x12\x34'),# read/write multiple registers + (0x18, '\x18\x00\x01'), # read fifo queue #(0x2b, '\x2b\x0e\x01\x00'), # read device identification ) @@ -43,17 +44,18 @@ def setUp(self): (0x04, '\x04\x02\x01\x01'), # read input registers (0x05, '\x05\x00\x01\x00\x01'), # write single coil (0x06, '\x06\x00\x01\x00\x01'), # write single register - #(0x07, '\x07\x00'), # read exception status - #(0x08, '\x08\x00\x00'), # read diagnostic - #(0x0b, '\x0b\x00\x00\x00\x00'), # get comm event counters - #(0x0c, '\x0c\x08\x00\x00\x01\x08\x01\x21\x20\x00'), # get comm event log - #(0x0f, '\x0f\x00\x01\x00\x08'), # write multiple coils - #(0x10, '\x10\x00\x01\x00\x02'), # write multiple registers - #(0x11, '\x11\x03\x05\x01\x54'), # report slave id (device specific) + (0x07, '\x07\x00'), # read exception status + (0x08, '\x08\x00\x00\x00\x00'), # read diagnostic + (0x0b, '\x0b\x00\x00\x00\x00'), # get comm event counters + (0x0c, '\x0c\x08\x00\x00\x01\x08\x01\x21\x20\x00'), # get comm event log + (0x0f, '\x0f\x00\x01\x00\x08'), # write multiple coils + (0x10, '\x10\x00\x01\x00\x02'), # write multiple registers + (0x11, '\x11\x03\x05\x01\x54'), # report slave id (device specific) #(0x14, '\x14'), # read file record #(0x15, '\x15'), # write file record - #(0x16, '\x16\x00\x01\x00\xff\xff\x00'), # mask write register - #(0x17, '\x17\x02\x12\x34'), # read/write multiple registers + (0x16, '\x16\x00\x01\x00\xff\xff\x00'), # mask write register + (0x17, '\x17\x02\x12\x34'), # read/write multiple registers + (0x18, '\x18\x00\x01\x00\x01\x00\x00'), # read fifo queue #(0x2b, '\x2b\x0e\x01\x01\0x00\0x00\x01\x00\x01\x77'),# read device identification ) diff --git a/test/test_other_messages.py b/test/test_other_messages.py index 0ced6e7db..93161c255 100644 --- a/test/test_other_messages.py +++ b/test/test_other_messages.py @@ -89,7 +89,7 @@ def testReportSlaveId(self): self.assertEqual(response.encode(), '\x0apymodbus\xff') response.decode('\x03\x12\x00') self.assertEqual(response.status, False) - self.assertEqual(response.identifier, '\x12') + self.assertEqual(response.identifier, '\x12\x00') #---------------------------------------------------------------------------# # Main From f0f594bc73372bed3a7a59cb1144552a7b27a678 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 7 Sep 2011 16:20:33 +0000 Subject: [PATCH 032/243] finished file read/write record requests --- doc/quality/current.coverage | 31 +++--- doc/sphinx/library/file-message.rst | 15 +++ pymodbus/factory.py | 7 ++ pymodbus/file_message.py | 157 +++++++++++++++++++++------- pymodbus/payload.py | 34 +++--- pymodbus/register_write_message.py | 3 +- test/test_factory.py | 16 +-- test/test_file_message.py | 141 +++++++++++++++++++++++++ 8 files changed, 334 insertions(+), 70 deletions(-) diff --git a/doc/quality/current.coverage b/doc/quality/current.coverage index 78a0a5b04..1f1b238d3 100644 --- a/doc/quality/current.coverage +++ b/doc/quality/current.coverage @@ -1,35 +1,36 @@ Name Stmts Exec Cover Missing --------------------------------------------------------------- -pymodbus 12 12 100% +pymodbus 15 13 86% 36-37 pymodbus.bit_read_message 68 68 100% -pymodbus.bit_write_message 94 94 100% +pymodbus.bit_write_message 95 95 100% pymodbus.client 1 1 100% pymodbus.client.common 44 44 100% -pymodbus.client.sync 154 61 39% 38-57, 64, 71-73, 99, 104, 112, 120, 130-132, 142-144, 148, 152, 159, 184-194, 199-201, 209-211, 219, 226, 251-258, 263, 271-273, 281, 288, 308-317, 326-330, 337-345, 350-352, 360-362, 370, 377 -pymodbus.constants 21 21 100% +pymodbus.client.sync 154 89 57% 54-57, 101, 106, 114, 122, 134, 144-146, 150, 154, 161, 193-196, 202-204, 214, 229, 255-262, 267, 275-277, 285, 292, 313-322, 331-335, 342-350, 355-357, 365-367, 375, 382 +pymodbus.constants 27 27 100% pymodbus.datastore 5 5 100% -pymodbus.datastore.context 47 47 100% +pymodbus.datastore.context 49 49 100% pymodbus.datastore.remote 31 31 100% -pymodbus.datastore.store 63 63 100% -pymodbus.device 128 128 100% -pymodbus.diag_message 186 186 100% +pymodbus.datastore.store 67 67 100% +pymodbus.device 148 148 100% +pymodbus.diag_message 202 202 100% pymodbus.events 60 60 100% pymodbus.exceptions 22 22 100% pymodbus.factory 56 56 100% -pymodbus.file_message 41 41 100% +pymodbus.file_message 178 178 100% pymodbus.interfaces 43 43 100% pymodbus.internal 1 1 100% -pymodbus.other_message 143 143 100% -pymodbus.pdu 65 65 100% +pymodbus.other_message 145 145 100% +pymodbus.payload 97 97 100% +pymodbus.pdu 66 66 100% pymodbus.register_read_message 124 124 100% -pymodbus.register_write_message 88 88 100% +pymodbus.register_write_message 87 87 100% pymodbus.server 0 0 100% -pymodbus.transaction 257 202 78% 49-61, 226-236, 404-413, 545-554, 623, 699-708, 733 +pymodbus.transaction 263 217 82% 50-63, 235, 239, 379, 409-418, 553-561, 709-716, 718, 743-744 pymodbus.utilities 67 67 100% pymodbus.version 13 13 100% --------------------------------------------------------------- -TOTAL 1834 1686 91% +TOTAL 2128 2015 94% ---------------------------------------------------------------------- -Ran 154 tests in 0.292s +Ran 181 tests in 0.391s OK diff --git a/doc/sphinx/library/file-message.rst b/doc/sphinx/library/file-message.rst index b55375072..e5d0b666d 100644 --- a/doc/sphinx/library/file-message.rst +++ b/doc/sphinx/library/file-message.rst @@ -12,6 +12,21 @@ API Documentation .. automodule:: pymodbus.file_message +.. autoclass:: FileRecord + :members: + +.. autoclass:: ReadFileRecordRequest + :members: + +.. autoclass:: ReadFileRecordResponse + :members: + +.. autoclass:: WriteFileRecordRequest + :members: + +.. autoclass:: WriteFileRecordResponse + :members: + .. autoclass:: ReadFifoQueueRequest :members: diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 3e30bc56b..3e7c4ea86 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -2,6 +2,13 @@ Modbus Request/Response Decoder Factories ------------------------------------------- +The following factories make it easy to decode request/response messages. +To add a new request/response pair to be decodeable by the library, simply +add them to the respective function lookup table (order doesn't matter, but +it does help keep things organized). + +Regardless of how many functions are added to the lookup, O(1) behavior is +kept as a result of a pre-computed lookup dictionary. """ from pymodbus.pdu import IllegalFunctionRequest diff --git a/pymodbus/file_message.py b/pymodbus/file_message.py index aa34546bb..f0491b7ad 100644 --- a/pymodbus/file_message.py +++ b/pymodbus/file_message.py @@ -13,29 +13,46 @@ #---------------------------------------------------------------------------# # File Record Types #---------------------------------------------------------------------------# -class FileRecordRequest(object): - ''' Represents a file record read request +class FileRecord(object): + ''' Represents a file record and its relevant data. ''' def __init__(self, **kwargs): ''' Initializes a new instance - ''' - self.reference_type = kwargs.get('reference_type', 0x06) - self.file_number = kwargs.get('file_number', 0x00) - self.record_number = kwargs.get('record_number', 0x00) - self.record_length = kwargs.get('record_length', 0x00) - -class FileRecordResponse(object): - ''' Represents a file record read response - ''' + :params reference_type: Defaults to 0x06 (must be) + :params file_number: Indicates which file number we are reading + :params record_number: Indicates which record in the file + :params record_data: The actual data of the record + :params record_length: The length in registers of the record + :params response_length: The length in bytes of the record + ''' + self.reference_type = kwargs.get('reference_type', 0x06) + self.file_number = kwargs.get('file_number', 0x00) + self.record_number = kwargs.get('record_number', 0x00) + self.record_data = kwargs.get('record_data', '') + self.record_length = kwargs.get('record_length', len(self.record_data) / 2) + self.response_length = kwargs.get('response_length', len(self.record_data) + 1) + + def __eq__(self, relf): + ''' Compares the left object to the right + ''' + return self.reference_type == relf.reference_type \ + and self.file_number == relf.file_number \ + and self.record_number == relf.record_number \ + and self.record_length == relf.record_length \ + and self.record_data == relf.record_data + + def __ne__(self, relf): + ''' Compares the left object to the right + ''' + return not self.__eq__(relf) - def __init__(self, **kwargs): - ''' Initializes a new instance + def __repr__(self): + ''' Gives a representation of the file record ''' - self.reference_type = kwargs.get('reference_type', 0x06) - self.record_data = kwargs.get('record_number', 0x00) - self.record_length = kwargs.get('record_length', 0x00) + params = (self.file_number, self.record_number, self.record_length) + return 'FileRecord(file=%d, record=%d, length=%d)' % params #---------------------------------------------------------------------------# @@ -73,6 +90,16 @@ def __init__(self, records=None): ModbusRequest.__init__(self) self.records = records or [] + @classmethod + def calculateRtuFrameSize(cls, buffer): + ''' Calculates the size of the message + + :param buffer: A buffer containing the data that have been received. + :returns: The number of bytes in the response. + ''' + byte_count = struct.unpack('B', buffer[0])[0] + return byte_count + def encode(self): ''' Encodes the request packet @@ -93,7 +120,7 @@ def decode(self, data): byte_count = struct.unpack('B', data[0])[0] for count in xrange(1, byte_count, 7): decoded = struct.unpack('>BHHH', data[count:count+7]) - record = FileRecordRequest(file_number=decoded[1], + record = FileRecord(file_number=decoded[1], record_number=decoded[2], record_length=decoded[3]) if decoded[0] == 0x06: self.records.append(record) @@ -125,12 +152,22 @@ def __init__(self, records=None): ModbusResponse.__init__(self) self.records = records or [] + @classmethod + def calculateRtuFrameSize(cls, buffer): + ''' Calculates the size of the message + + :param buffer: A buffer containing the data that have been received. + :returns: The number of bytes in the response. + ''' + byte_count = struct.unpack('B', buffer[0])[0] + return byte_count + def encode(self): ''' Encodes the response :returns: The byte encoded message ''' - total = sum(record.record_length for record in self.records) + total = sum(record.response_length + 1 for record in self.records) packet = struct.pack('B', total) for record in self.records: packet += struct.pack('>BB', 0x06, record.record_length) @@ -145,15 +182,19 @@ def decode(self, data): count, self.records = 1, [] byte_count = struct.unpack('B', data[0])[0] while count < byte_count: - record_length, reference_type = struct.unpack('>BB', data[count:count+2]) - count += record_length - record = FileRecordResponse(record_length=record_length, - record_data=data[count - record_length:count]) + response_length, reference_type = struct.unpack('>BB', data[count:count+2]) + count += response_length + 1 # the count is not included + record = FileRecord(response_length=response_length, + record_data=data[count - response_length + 1:count]) if reference_type == 0x06: self.records.append(record) class WriteFileRecordRequest(ModbusRequest): ''' + This function code is used to perform a file record write. All + request data lengths are provided in terms of number of bytes + and all record lengths are provided in terms of the number of 16 + bit words. ''' function_code = 0x15 @@ -165,15 +206,27 @@ def __init__(self, records=None): ModbusRequest.__init__(self) self.records = records or [] + @classmethod + def calculateRtuFrameSize(cls, buffer): + ''' Calculates the size of the message + + :param buffer: A buffer containing the data that have been received. + :returns: The number of bytes in the response. + ''' + total_length = struct.unpack('B', buffer[0])[0] + return total_length + def encode(self): ''' Encodes the request packet :returns: The byte encoded packet ''' - packet = struct.pack('B', len(self.records) * 7) + total_length = sum((record.record_length * 2) + 7 for record in self.records) + packet = struct.pack('B', total_length) for record in self.records: packet += struct.pack('>BHHH', 0x06, record.file_number, record.record_number, record.record_length) + packet += record.record_data return packet def decode(self, data): @@ -181,9 +234,16 @@ def decode(self, data): :param data: The data to decode into the address ''' - self.records = [] + count, self.records = 1, [] byte_count = struct.unpack('B', data[0])[0] - # todo, decode records + while count < byte_count: + decoded = struct.unpack('>BHHH', data[count:count+7]) + response_length = decoded[3] * 2 + count += response_length + 7 + record = FileRecord(record_length=decoded[3], + file_number=decoded[1], record_number=decoded[2], + record_data=data[count - response_length:count]) + if decoded[0] == 0x06: self.records.append(record) def execute(self, context): ''' Run the write file record request against the context @@ -192,37 +252,61 @@ def execute(self, context): :returns: The populated response ''' # do some new context operation here - files = [] - return WriteFileRecordResponse(files) + return WriteFileRecordResponse(self.records) class WriteFileRecordResponse(ModbusResponse): ''' + The normal response is an echo of the request. ''' function_code = 0x15 - _rtu_frame_size = 10 - def __init__(self, address=0x0000): + def __init__(self, records=None): ''' Initializes a new instance - :param address: The mask pointer address (0x0000 to 0xffff) + :param records: The file record requests to be read ''' ModbusResponse.__init__(self) - pass + self.records = records or [] + + @classmethod + def calculateRtuFrameSize(cls, buffer): + ''' Calculates the size of the message + + :param buffer: A buffer containing the data that have been received. + :returns: The number of bytes in the response. + ''' + total_length = struct.unpack('B', buffer[0])[0] + return total_length def encode(self): ''' Encodes the response :returns: The byte encoded message ''' - pass + total_length = sum((record.record_length * 2) + 7 for record in self.records) + packet = struct.pack('B', total_length) + for record in self.records: + packet += struct.pack('>BHHH', 0x06, record.file_number, + record.record_number, record.record_length) + packet += record.record_data + return packet def decode(self, data): - ''' Decodes a the response + ''' Decodes the incoming request - :param data: The packet data to decode + :param data: The data to decode into the address ''' - pass + count, self.records = 1, [] + byte_count = struct.unpack('B', data[0])[0] + while count < byte_count: + decoded = struct.unpack('>BHHH', data[count:count+7]) + response_length = decoded[3] * 2 + count += response_length + 7 + record = FileRecord(record_length=decoded[3], + file_number=decoded[1], record_number=decoded[2], + record_data=data[count - response_length:count]) + if decoded[0] == 0x06: self.records.append(record) class MaskWriteRegisterRequest(ModbusRequest): @@ -369,7 +453,7 @@ class ReadFifoQueueResponse(ModbusResponse): @classmethod def calculateRtuFrameSize(cls, buffer): - ''' Calculates the size of a response containing a FIFO queue. + ''' Calculates the size of the message :param buffer: A buffer containing the data that have been received. :returns: The number of bytes in the response. @@ -412,6 +496,7 @@ def decode(self, data): # Exported symbols #---------------------------------------------------------------------------# __all__ = [ + "FileRecord", "ReadFileRecordRequest", "ReadFileRecordResponse", "WriteFileRecordRequest", "WriteFileRecordResponse", "MaskWriteRegisterRequest", "MaskWriteRegisterResponse", diff --git a/pymodbus/payload.py b/pymodbus/payload.py index 1494c4f25..31d02bfc0 100644 --- a/pymodbus/payload.py +++ b/pymodbus/payload.py @@ -130,7 +130,7 @@ def add_64bit_float(self, value): ''' fstring = self._endian + 'd' self._payload.append(pack(fstring, value)) - + def add_string(self, value): ''' Adds a string to the buffer @@ -173,71 +173,81 @@ def decode_8bit_uint(self): ''' self._pointer += 1 fstring = self._endian + 'B' - return unpack('B', self._payload[self._pointer - 1:self._pointer])[0] + handle = self._payload[self._pointer - 1:self._pointer] + return unpack('B', handle)[0] def decode_16bit_uint(self): ''' Decodes a 16 bit unsigned int from the buffer ''' self._pointer += 2 fstring = self._endian + 'H' - return unpack(fstring, self._payload[self._pointer - 2:self._pointer])[0] + handle = self._payload[self._pointer - 2:self._pointer] + return unpack(fstring, handle)[0] def decode_32bit_uint(self): ''' Decodes a 32 bit unsigned int from the buffer ''' self._pointer += 4 fstring = self._endian + 'I' - return unpack(fstring, self._payload[self._pointer - 4:self._pointer])[0] + handle = self._payload[self._pointer - 4:self._pointer] + return unpack(fstring, handle)[0] def decode_64bit_uint(self): ''' Decodes a 64 bit unsigned int from the buffer ''' self._pointer += 8 fstring = self._endian + 'Q' - return unpack(fstring, self._payload[self._pointer - 8:self._pointer])[0] + handle = self._payload[self._pointer - 8:self._pointer] + return unpack(fstring, handle)[0] def decode_8bit_int(self): ''' Decodes a 8 bit signed int from the buffer ''' self._pointer += 1 fstring = self._endian + 'b' - return unpack(fstring, self._payload[self._pointer - 1:self._pointer])[0] + handle = self._payload[self._pointer - 1:self._pointer] + return unpack(fstring, handle)[0] def decode_16bit_int(self): ''' Decodes a 16 bit signed int from the buffer ''' self._pointer += 2 fstring = self._endian + 'h' - return unpack(fstring, self._payload[self._pointer - 2:self._pointer])[0] + handle = self._payload[self._pointer - 2:self._pointer] + return unpack(fstring, handle)[0] def decode_32bit_int(self): ''' Decodes a 32 bit signed int from the buffer ''' self._pointer += 4 fstring = self._endian + 'i' - return unpack(fstring, self._payload[self._pointer - 4:self._pointer])[0] + handle = self._payload[self._pointer - 4:self._pointer] + return unpack(fstring, handle)[0] def decode_64bit_int(self): ''' Decodes a 64 bit signed int from the buffer ''' self._pointer += 8 fstring = self._endian + 'q' - return unpack(fstring, self._payload[self._pointer - 8:self._pointer])[0] + handle = self._payload[self._pointer - 8:self._pointer] + return unpack(fstring, handle)[0] def decode_32bit_float(self): ''' Decodes a 32 bit float from the buffer ''' self._pointer += 4 fstring = self._endian + 'f' - return unpack(fstring, self._payload[self._pointer - 4:self._pointer])[0] + handle = self._payload[self._pointer - 4:self._pointer] + return unpack(fstring, handle)[0] def decode_64bit_float(self): ''' Decodes a 64 bit float(double) from the buffer ''' self._pointer += 8 fstring = self._endian + 'd' - return unpack(fstring, self._payload[self._pointer - 8:self._pointer])[0] - + handle = self._payload[self._pointer - 8:self._pointer] + return unpack(fstring, handle)[0] + def decode_string(self, size=1): ''' Decodes a string from the buffer diff --git a/pymodbus/register_write_message.py b/pymodbus/register_write_message.py index 74d56753a..d3bd7b0a8 100644 --- a/pymodbus/register_write_message.py +++ b/pymodbus/register_write_message.py @@ -151,7 +151,8 @@ def decode(self, data): :param data: The request to decode ''' - self.address, self.count, self.byte_count = struct.unpack('>HHB', data[:5]) + self.address, self.count, \ + self.byte_count = struct.unpack('>HHB', data[:5]) self.values = [] # reset for idx in range(5, (self.count * 2) + 5, 2): self.values.append(struct.unpack('>H', data[idx:idx + 2])[0]) diff --git a/test/test_factory.py b/test/test_factory.py index e6a4acd10..1c1918ede 100644 --- a/test/test_factory.py +++ b/test/test_factory.py @@ -29,9 +29,11 @@ def setUp(self): (0x0f, '\x0f\x00\x01\x00\x08\x01\x00\xff'), # write multiple coils (0x10, '\x10\x00\x01\x00\x02\x04\0xff\xff'), # write multiple registers (0x11, '\x11'), # report slave id - #(0x14, '\x14'), # read file record - #(0x15, '\x15'), # write file record - (0x16, '\x16\x00\x01\x00\xff\xff\x00'), # mask write register + (0x14, '\x14\x0e\x06\x00\x04\x00\x01\x00\x02' \ + '\x06\x00\x03\x00\x09\x00\x02'), # read file record + (0x15, '\x15\x0d\x06\x00\x04\x00\x07\x00\x03' \ + '\x06\xaf\x04\xbe\x10\x0d'), # write file record + (0x16, '\x16\x00\x01\x00\xff\xff\x00'), # mask write register (0x17, '\x17\x00\x01\x00\x01\x00\x01\x00\x01\x02\x12\x34'),# read/write multiple registers (0x18, '\x18\x00\x01'), # read fifo queue #(0x2b, '\x2b\x0e\x01\x00'), # read device identification @@ -51,9 +53,11 @@ def setUp(self): (0x0f, '\x0f\x00\x01\x00\x08'), # write multiple coils (0x10, '\x10\x00\x01\x00\x02'), # write multiple registers (0x11, '\x11\x03\x05\x01\x54'), # report slave id (device specific) - #(0x14, '\x14'), # read file record - #(0x15, '\x15'), # write file record - (0x16, '\x16\x00\x01\x00\xff\xff\x00'), # mask write register + (0x14, '\x14\x0c\x05\x06\x0d\xfe\x00\x20\x05' \ + '\x06\x33\xcd\x00\x40'), # read file record + (0x15, '\x15\x0d\x06\x00\x04\x00\x07\x00\x03' \ + '\x06\xaf\x04\xbe\x10\x0d'), # write file record + (0x16, '\x16\x00\x01\x00\xff\xff\x00'), # mask write register (0x17, '\x17\x02\x12\x34'), # read/write multiple registers (0x18, '\x18\x00\x01\x00\x01\x00\x00'), # read fifo queue #(0x2b, '\x2b\x0e\x01\x01\0x00\0x00\x01\x00\x01\x77'),# read device identification diff --git a/test/test_file_message.py b/test/test_file_message.py index 34fe260d0..b164b9e39 100644 --- a/test/test_file_message.py +++ b/test/test_file_message.py @@ -86,6 +86,147 @@ def testRtuFrameSize(self): result = ReadFifoQueueResponse.calculateRtuFrameSize(message) self.assertEqual(result, 14) + #-----------------------------------------------------------------------# + # File Record + #-----------------------------------------------------------------------# + + def testFileRecordLength(self): + ''' Test file record length generation ''' + record = FileRecord(file_number=0x01, record_number=0x02, + record_data='\x00\x01\x02\x04') + self.assertEqual(record.record_length, 0x02) + self.assertEqual(record.response_length, 0x05) + + def testFileRecordComapre(self): + ''' Test file record comparison operations ''' + record1 = FileRecord(file_number=0x01, record_number=0x02, record_data='\x00\x01\x02\x04') + record2 = FileRecord(file_number=0x01, record_number=0x02, record_data='\x00\x0a\x0e\x04') + record3 = FileRecord(file_number=0x02, record_number=0x03, record_data='\x00\x01\x02\x04') + record4 = FileRecord(file_number=0x01, record_number=0x02, record_data='\x00\x01\x02\x04') + self.assertNotEqual(record1, record2) + self.assertNotEqual(record1, record3) + self.assertNotEqual(record2, record3) + self.assertEqual(record1, record4) + + #-----------------------------------------------------------------------# + # Read File Record Request + #-----------------------------------------------------------------------# + + def testReadFileRecordRequestEncode(self): + ''' Test basic bit message encoding/decoding ''' + records = [FileRecord(file_number=0x01, record_number=0x02)] + handle = ReadFileRecordRequest(records) + result = handle.encode() + self.assertEqual(result, '\x07\x06\x00\x01\x00\x02\x00\x00') + + def testReadFileRecordRequestDecode(self): + ''' Test basic bit message encoding/decoding ''' + record = FileRecord(file_number=0x04, record_number=0x01, record_length=0x02) + request = '\x0e\x06\x00\x04\x00\x01\x00\x02\x06\x00\x03\x00\x09\x00\x02' + handle = ReadFileRecordRequest() + handle.decode(request) + self.assertEqual(handle.records[0], record) + + def testReadFileRecordRequestRtuFrameSize(self): + ''' Test basic bit message encoding/decoding ''' + request = '\x0e\x06\x00\x04\x00\x01\x00\x02\x06\x00\x03\x00\x09\x00\x02' + handle = ReadFileRecordRequest() + size = handle.calculateRtuFrameSize(request) + self.assertEqual(size, 0x0e) + + def testReadFileRecordRequestExecute(self): + ''' Test basic bit message encoding/decoding ''' + handle = ReadFileRecordRequest() + result = handle.execute(None) + self.assertTrue(isinstance(result, ReadFileRecordResponse)) + + #-----------------------------------------------------------------------# + # Read File Record Response + #-----------------------------------------------------------------------# + + def testReadFileRecordResponseEncode(self): + ''' Test basic bit message encoding/decoding ''' + records = [FileRecord(record_data='\x00\x01\x02\x03')] + handle = ReadFileRecordResponse(records) + result = handle.encode() + self.assertEqual(result, '\x06\x06\x02\x00\x01\x02\x03') + + def testReadFileRecordResponseDecode(self): + ''' Test basic bit message encoding/decoding ''' + record = FileRecord(file_number=0x00, record_number=0x00, + record_data='\x0d\xfe\x00\x20') + request = '\x0c\x05\x06\x0d\xfe\x00\x20\x05\x05\x06\x33\xcd\x00\x40' + handle = ReadFileRecordResponse() + handle.decode(request) + self.assertEqual(handle.records[0], record) + + def testReadFileRecordResponseRtuFrameSize(self): + ''' Test basic bit message encoding/decoding ''' + request = '\x0c\x05\x06\x0d\xfe\x00\x20\x05\x05\x06\x33\xcd\x00\x40' + handle = ReadFileRecordRequest() + size = handle.calculateRtuFrameSize(request) + self.assertEqual(size, 0x0c) + + #-----------------------------------------------------------------------# + # Write File Record Request + #-----------------------------------------------------------------------# + + def testWriteFileRecordRequestEncode(self): + ''' Test basic bit message encoding/decoding ''' + records = [FileRecord(file_number=0x01, record_number=0x02, record_data='\x00\x01\x02\x03')] + handle = WriteFileRecordRequest(records) + result = handle.encode() + self.assertEqual(result, '\x0b\x06\x00\x01\x00\x02\x00\x02\x00\x01\x02\x03') + + def testWriteFileRecordRequestDecode(self): + ''' Test basic bit message encoding/decoding ''' + record = FileRecord(file_number=0x04, record_number=0x07, + record_data='\x06\xaf\x04\xbe\x10\x0d') + request = '\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d' + handle = WriteFileRecordRequest() + handle.decode(request) + self.assertEqual(handle.records[0], record) + + def testWriteFileRecordRequestRtuFrameSize(self): + ''' Test write file record request rtu frame size calculation ''' + request = '\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d' + handle = WriteFileRecordRequest() + size = handle.calculateRtuFrameSize(request) + self.assertEqual(size, 0x0d) + + def testWriteFileRecordRequestExecute(self): + ''' Test basic bit message encoding/decoding ''' + handle = WriteFileRecordRequest() + result = handle.execute(None) + self.assertTrue(isinstance(result, WriteFileRecordResponse)) + + #-----------------------------------------------------------------------# + # Write File Record Response + #-----------------------------------------------------------------------# + + def testWriteFileRecordResponseEncode(self): + ''' Test basic bit message encoding/decoding ''' + records = [FileRecord(file_number=0x01, record_number=0x02, record_data='\x00\x01\x02\x03')] + handle = WriteFileRecordResponse(records) + result = handle.encode() + self.assertEqual(result, '\x0b\x06\x00\x01\x00\x02\x00\x02\x00\x01\x02\x03') + + def testWriteFileRecordResponseDecode(self): + ''' Test basic bit message encoding/decoding ''' + record = FileRecord(file_number=0x04, record_number=0x07, + record_data='\x06\xaf\x04\xbe\x10\x0d') + request = '\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d' + handle = WriteFileRecordResponse() + handle.decode(request) + self.assertEqual(handle.records[0], record) + + def testWriteFileRecordResponseRtuFrameSize(self): + ''' Test write file record response rtu frame size calculation ''' + request = '\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d' + handle = WriteFileRecordRequest() + size = handle.calculateRtuFrameSize(request) + self.assertEqual(size, 0x0d) + #---------------------------------------------------------------------------# # Main #---------------------------------------------------------------------------# From fcd572e36ab8cf6399c858f321b128f8676a13ad Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 7 Sep 2011 17:36:45 +0000 Subject: [PATCH 033/243] documentation, more tests, fixing extra commands --- doc/quality/current.coverage | 14 +++++++------- examples/common/asynchronous-server.py | 14 +++++++++++--- examples/common/synchronous-server.py | 14 +++++++++++--- pymodbus/file_message.py | 21 ++++++++++++++++++--- pymodbus/interfaces.py | 2 +- test/test_bit_write_messages.py | 4 ++++ test/test_datastore.py | 2 ++ test/test_device.py | 15 +++++++++++++++ test/test_file_message.py | 19 +++++++++++++++++-- test/test_other_messages.py | 9 +++++++++ 10 files changed, 95 insertions(+), 19 deletions(-) diff --git a/doc/quality/current.coverage b/doc/quality/current.coverage index 1f1b238d3..17e9c1c0b 100644 --- a/doc/quality/current.coverage +++ b/doc/quality/current.coverage @@ -5,32 +5,32 @@ pymodbus.bit_read_message 68 68 100% pymodbus.bit_write_message 95 95 100% pymodbus.client 1 1 100% pymodbus.client.common 44 44 100% -pymodbus.client.sync 154 89 57% 54-57, 101, 106, 114, 122, 134, 144-146, 150, 154, 161, 193-196, 202-204, 214, 229, 255-262, 267, 275-277, 285, 292, 313-322, 331-335, 342-350, 355-357, 365-367, 375, 382 +pymodbus.client.sync 154 61 39% 39-58, 65, 72-74, 101, 106, 114, 122, 132-134, 144-146, 150, 154, 161, 187-197, 202-204, 212-214, 222, 229, 255-262, 267, 275-277, 285, 292, 313-322, 331-335, 342-350, 355-357, 365-367, 375, 382 pymodbus.constants 27 27 100% pymodbus.datastore 5 5 100% pymodbus.datastore.context 49 49 100% pymodbus.datastore.remote 31 31 100% pymodbus.datastore.store 67 67 100% pymodbus.device 148 148 100% -pymodbus.diag_message 202 202 100% +pymodbus.diag_message 202 201 99% 704 pymodbus.events 60 60 100% pymodbus.exceptions 22 22 100% pymodbus.factory 56 56 100% -pymodbus.file_message 178 178 100% +pymodbus.file_message 189 177 93% 343, 358-367, 395 pymodbus.interfaces 43 43 100% pymodbus.internal 1 1 100% pymodbus.other_message 145 145 100% -pymodbus.payload 97 97 100% +pymodbus.payload 107 107 100% pymodbus.pdu 66 66 100% pymodbus.register_read_message 124 124 100% pymodbus.register_write_message 87 87 100% pymodbus.server 0 0 100% -pymodbus.transaction 263 217 82% 50-63, 235, 239, 379, 409-418, 553-561, 709-716, 718, 743-744 +pymodbus.transaction 263 205 77% 50-63, 229-239, 379, 409-418, 553-562, 632, 709-718, 743-744 pymodbus.utilities 67 67 100% pymodbus.version 13 13 100% --------------------------------------------------------------- -TOTAL 2128 2015 94% +TOTAL 2149 1983 92% ---------------------------------------------------------------------- -Ran 181 tests in 0.391s +Ran 183 tests in 0.446s OK diff --git a/examples/common/asynchronous-server.py b/examples/common/asynchronous-server.py index a8ed86c53..4fcc61cf4 100755 --- a/examples/common/asynchronous-server.py +++ b/examples/common/asynchronous-server.py @@ -31,24 +31,32 @@ # The datastores only respond to the addresses that they are initialized to. # Therefore, if you initialize a DataBlock to addresses of 0x00 to 0xFF, a # request to 0x100 will respond with an invalid address exception. This is -# because many devices exhibit this kind of behavior (but not all): +# because many devices exhibit this kind of behavior (but not all):: # # block = ModbusSequentialDataBlock(0x00, [0]*0xff) # # Continuting, you can choose to use a sequential or a sparse DataBlock in # your data context. The difference is that the sequential has no gaps in # the data while the sparse can. Once again, there are devices that exhibit -# both forms of behavior: +# both forms of behavior:: # # block = ModbusSparseDataBlock({0x00: 0, 0x05: 1}) # block = ModbusSequentialDataBlock(0x00, [0]*5) # # Alternately, you can use the factory methods to initialize the DataBlocks # or simply do not pass them to have them initialized to 0x00 on the full -# address range: +# address range:: # # store = ModbusSlaveContext(di = ModbusSequentialDataBlock.create()) # store = ModbusSlaveContext() +# +# Finally, you are allowed to use the same DataBlock reference for every +# table or you you may use a seperate DataBlock for each table. This depends +# if you would like functions to be able to access and modify the same data +# or not:: +# +# block = ModbusSequentialDataBlock(0x00, [0]*0xff) +# store = ModbusSlaveContext(di=block, co=block, hr=block, ir=block) #---------------------------------------------------------------------------# store = ModbusSlaveContext( di = ModbusSequentialDataBlock(0, [17]*100), diff --git a/examples/common/synchronous-server.py b/examples/common/synchronous-server.py index 59a038f9f..ce00c33a6 100755 --- a/examples/common/synchronous-server.py +++ b/examples/common/synchronous-server.py @@ -32,24 +32,32 @@ # The datastores only respond to the addresses that they are initialized to. # Therefore, if you initialize a DataBlock to addresses of 0x00 to 0xFF, a # request to 0x100 will respond with an invalid address exception. This is -# because many devices exhibit this kind of behavior (but not all): +# because many devices exhibit this kind of behavior (but not all):: # # block = ModbusSequentialDataBlock(0x00, [0]*0xff) # # Continuting, you can choose to use a sequential or a sparse DataBlock in # your data context. The difference is that the sequential has no gaps in # the data while the sparse can. Once again, there are devices that exhibit -# both forms of behavior: +# both forms of behavior:: # # block = ModbusSparseDataBlock({0x00: 0, 0x05: 1}) # block = ModbusSequentialDataBlock(0x00, [0]*5) # # Alternately, you can use the factory methods to initialize the DataBlocks # or simply do not pass them to have them initialized to 0x00 on the full -# address range: +# address range:: # # store = ModbusSlaveContext(di = ModbusSequentialDataBlock.create()) # store = ModbusSlaveContext() +# +# Finally, you are allowed to use the same DataBlock reference for every +# table or you you may use a seperate DataBlock for each table. This depends +# if you would like functions to be able to access and modify the same data +# or not:: +# +# block = ModbusSequentialDataBlock(0x00, [0]*0xff) +# store = ModbusSlaveContext(di=block, co=block, hr=block, ir=block) #---------------------------------------------------------------------------# store = ModbusSlaveContext( di = ModbusSequentialDataBlock(0, [17]*100), diff --git a/pymodbus/file_message.py b/pymodbus/file_message.py index f0491b7ad..424277495 100644 --- a/pymodbus/file_message.py +++ b/pymodbus/file_message.py @@ -130,7 +130,9 @@ def execute(self, context): :param context: The datastore to request from :returns: The populated response ''' - # do some new context operation here + # TODO do some new context operation here + # if file number, record number, or address + length + # is too big, return an error. files = [] return ReadFileRecordResponse(files) @@ -251,7 +253,9 @@ def execute(self, context): :param context: The datastore to request from :returns: The populated response ''' - # do some new context operation here + # TODO do some new context operation here + # if file number, record number, or address + length + # is too big, return an error. return WriteFileRecordResponse(self.records) @@ -351,7 +355,15 @@ def execute(self, context): :param context: The datastore to request from :returns: The populated response ''' - # do some new context operation here + if not (0x0000 <= self.and_mask <= 0xffff): + return self.doException(merror.IllegalValue) + if not (0x0000 <= self.or_mask <= 0xffff): + return self.doException(merror.IllegalValue) + if not context.validate(self.function_code, self.address, 1): + return self.doException(merror.IllegalAddress) + values = context.getValues(self.function_code, self.address, 1) + values = ((values & self.and_mask) | self.or_mask) + context.setValues(self.function_code, self.address, [values]) return MaskWriteRegisterResponse(self.address, self.and_mask, self.or_mask) @@ -434,8 +446,11 @@ def execute(self, context): :param context: The datastore to request from :returns: The populated response ''' + if not (0x0000 <= self.address <= 0xffff): + return self.doException(merror.IllegalValue) if len(self.values) > 31: return self.doException(merror.IllegalValue) + # TODO pull the values from some context return ReadFifoQueueResponse(self.values) diff --git a/pymodbus/interfaces.py b/pymodbus/interfaces.py index 1b40634c4..974a69239 100644 --- a/pymodbus/interfaces.py +++ b/pymodbus/interfaces.py @@ -163,7 +163,7 @@ class IModbusSlaveContext(object): setValues(self, fx, address, values) ''' __fx_mapper = {2: 'd', 4: 'i'} - __fx_mapper.update([(i, 'h') for i in [3, 6, 16, 23]]) + __fx_mapper.update([(i, 'h') for i in [3, 6, 16, 22, 23]]) __fx_mapper.update([(i, 'c') for i in [1, 5, 15]]) def decode(self, fx): diff --git a/test/test_bit_write_messages.py b/test/test_bit_write_messages.py index 315e78d6d..c0499bfea 100644 --- a/test/test_bit_write_messages.py +++ b/test/test_bit_write_messages.py @@ -56,6 +56,10 @@ def testInvalidWriteMultipleCoilsRequest(self): request = WriteMultipleCoilsRequest(1, None) self.assertEquals(request.values, []) + def testWriteSingleCoilRequestEncode(self): + request = WriteSingleCoilRequest(1, False) + self.assertEquals(request.encode(), '\x00\x01\x00\x00') + def testWriteSingleCoilExecute(self): context = MockContext(False, default=True) request = WriteSingleCoilRequest(2, True) diff --git a/test/test_datastore.py b/test/test_datastore.py index acb0f1d4f..40c1de5cd 100644 --- a/test/test_datastore.py +++ b/test/test_datastore.py @@ -67,6 +67,8 @@ def testModbusSequentialDataBlockFactory(self): ''' Test the sequential data block store factory ''' block = ModbusSequentialDataBlock.create() self.assertEqual(block.getValues(0x00, 65536), [False]*65536) + block = ModbusSequentialDataBlock(0x00, 0x01) + self.assertEqual(block.values, [0x01]) def testModbusSparseDataBlock(self): ''' Test a sparse data block store ''' diff --git a/test/test_device.py b/test/test_device.py index 64346e1f1..ca2c3118d 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -223,6 +223,21 @@ def testModbusPlusStatistics(self): self.assertEqual(default, statistics.encode()) self.assertEqual(default, self.control.Plus.encode()) + + + def testModbusPlusStatisticsHelpers(self): + ''' Test modbus plus statistics helper methods ''' + statistics = ModbusPlusStatistics() + summary = [ + [0],[0],[0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0],[0,0],[0], + [0,0,0,0,0,0,0,0],[0],[0],[0],[0],[0,0],[0],[0],[0],[0], + [0],[0],[0],[0,0],[0],[0],[0],[0],[0,0,0,0,0,0,0,0],[0], + [0,0,0,0,0,0,0,0],[0,0],[0],[0,0,0,0,0,0,0,0], + [0,0,0,0,0,0,0,0],[0],[0],[0,0],[0],[0],[0],[0],[0,0], + [0],[0],[0],[0],[0],[0,0],[0],[0,0,0,0,0,0,0,0]] + self.assertEqual(summary, statistics.summary()) + self.assertEqual(0x00, sum(sum(value[1]) for value in statistics)) + #---------------------------------------------------------------------------# # Main #---------------------------------------------------------------------------# diff --git a/test/test_file_message.py b/test/test_file_message.py index b164b9e39..b32fc6dc9 100644 --- a/test/test_file_message.py +++ b/test/test_file_message.py @@ -58,6 +58,16 @@ def testReadFifoQueueRequest(self): result = handle.execute(context) self.assertTrue(isinstance(result, ReadFifoQueueResponse)) + handle.address = -1 + result = handle.execute(context) + self.assertEqual(ModbusExceptions.IllegalValue, + result.exception_code) + + handle.values = [0x00]*33 + result = handle.execute(context) + self.assertEqual(ModbusExceptions.IllegalValue, + result.exception_code) + def testReadFifoQueueRequestError(self): ''' Test basic bit message encoding/decoding ''' context = MockContext() @@ -103,10 +113,15 @@ def testFileRecordComapre(self): record2 = FileRecord(file_number=0x01, record_number=0x02, record_data='\x00\x0a\x0e\x04') record3 = FileRecord(file_number=0x02, record_number=0x03, record_data='\x00\x01\x02\x04') record4 = FileRecord(file_number=0x01, record_number=0x02, record_data='\x00\x01\x02\x04') + self.assertTrue(record1 == record4) + self.assertTrue(record1 != record2) self.assertNotEqual(record1, record2) self.assertNotEqual(record1, record3) self.assertNotEqual(record2, record3) self.assertEqual(record1, record4) + self.assertEqual(str(record1), "FileRecord(file=1, record=2, length=2)") + self.assertEqual(str(record2), "FileRecord(file=1, record=2, length=2)") + self.assertEqual(str(record3), "FileRecord(file=2, record=3, length=2)") #-----------------------------------------------------------------------# # Read File Record Request @@ -163,7 +178,7 @@ def testReadFileRecordResponseDecode(self): def testReadFileRecordResponseRtuFrameSize(self): ''' Test basic bit message encoding/decoding ''' request = '\x0c\x05\x06\x0d\xfe\x00\x20\x05\x05\x06\x33\xcd\x00\x40' - handle = ReadFileRecordRequest() + handle = ReadFileRecordResponse() size = handle.calculateRtuFrameSize(request) self.assertEqual(size, 0x0c) @@ -223,7 +238,7 @@ def testWriteFileRecordResponseDecode(self): def testWriteFileRecordResponseRtuFrameSize(self): ''' Test write file record response rtu frame size calculation ''' request = '\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d' - handle = WriteFileRecordRequest() + handle = WriteFileRecordResponse() size = handle.calculateRtuFrameSize(request) self.assertEqual(size, 0x0d) diff --git a/test/test_other_messages.py b/test/test_other_messages.py index 93161c255..67e07a2ed 100644 --- a/test/test_other_messages.py +++ b/test/test_other_messages.py @@ -56,6 +56,9 @@ def testGetCommEventCounter(self): self.assertEqual(response.status, True) self.assertEqual(response.count, 0x12) + response.status = False + self.assertEqual(response.encode(), '\xFF\xFF\x00\x12') + def testGetCommEventLog(self): request = GetCommEventLogRequest() request.decode('\x12') @@ -70,6 +73,9 @@ def testGetCommEventLog(self): self.assertEqual(response.event_count, 0x12) self.assertEqual(response.events, []) + response.status = False + self.assertEqual(response.encode(), '\x06\xff\xff\x00\x12\x00\x12') + def testGetCommEventLogWithEvents(self): response = GetCommEventLogResponse(events=[0x12,0x34,0x56]) self.assertEqual(response.encode(), '\x09\x00\x00\x00\x00\x00\x00\x12\x34\x56') @@ -91,6 +97,9 @@ def testReportSlaveId(self): self.assertEqual(response.status, False) self.assertEqual(response.identifier, '\x12\x00') + response.status = False + self.assertEqual(response.encode(), '\x04\x12\x00\x00') + #---------------------------------------------------------------------------# # Main #---------------------------------------------------------------------------# From 87eb6a4d34400b821264430a0051e11a5ecd798d Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 7 Sep 2011 21:27:11 +0000 Subject: [PATCH 034/243] adding more tests --- pymodbus/__init__.py | 2 +- pymodbus/file_message.py | 2 +- test/test_diag_messages.py | 13 +++++++- test/test_file_message.py | 63 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index 72a447381..a204cebf1 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -34,4 +34,4 @@ def emit(self, record): try: True, False except NameError: - True, False = 1, 0 + True, False = (1 == 1), (0 == 1) diff --git a/pymodbus/file_message.py b/pymodbus/file_message.py index 424277495..cc399357e 100644 --- a/pymodbus/file_message.py +++ b/pymodbus/file_message.py @@ -361,7 +361,7 @@ def execute(self, context): return self.doException(merror.IllegalValue) if not context.validate(self.function_code, self.address, 1): return self.doException(merror.IllegalAddress) - values = context.getValues(self.function_code, self.address, 1) + values = context.getValues(self.function_code, self.address, 1)[0] values = ((values & self.and_mask) | self.or_mask) context.setValues(self.function_code, self.address, [values]) return MaskWriteRegisterResponse(self.address, self.and_mask, self.or_mask) diff --git a/test/test_diag_messages.py b/test/test_diag_messages.py index bff959837..ef53e0d54 100644 --- a/test/test_diag_messages.py +++ b/test/test_diag_messages.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.exceptions import * +from pymodbus.constants import ModbusPlusOperation from pymodbus.diag_message import * from pymodbus.diag_message import DiagnosticStatusRequest from pymodbus.diag_message import DiagnosticStatusResponse @@ -115,7 +116,7 @@ def testReturnQueryDataResponse(self): message = ReturnQueryDataResponse(0x0000) self.assertEqual(message.encode(), '\x00\x00\x00\x00'); - def testtRestartCommunicationsOption(self): + def testRestartCommunicationsOption(self): ''' Testing diagnostic message execution ''' request = RestartCommunicationsOptionRequest(True); self.assertEqual(request.encode(), '\x00\x01\xff\x00') @@ -127,6 +128,16 @@ def testtRestartCommunicationsOption(self): response = RestartCommunicationsOptionResponse(False); self.assertEqual(response.encode(), '\x00\x01\x00\x00') + def testGetClearModbusPlusRequestExecute(self): + ''' Testing diagnostic message execution ''' + request = GetClearModbusPlusRequest(ModbusPlusOperation.ClearStatistics); + response = request.execute() + self.assertEqual(response.message, None) + + request = GetClearModbusPlusRequest(ModbusPlusOperation.GetStatistics); + response = request.execute() + self.assertEqual(response.message, [0x00] * 55) + #---------------------------------------------------------------------------# # Main #---------------------------------------------------------------------------# diff --git a/test/test_file_message.py b/test/test_file_message.py index b32fc6dc9..5dd1bef2e 100644 --- a/test/test_file_message.py +++ b/test/test_file_message.py @@ -242,6 +242,69 @@ def testWriteFileRecordResponseRtuFrameSize(self): size = handle.calculateRtuFrameSize(request) self.assertEqual(size, 0x0d) + #-----------------------------------------------------------------------# + # Mask Write Register Request + #-----------------------------------------------------------------------# + + def testMaskWriteRegisterRequestEncode(self): + ''' Test basic bit message encoding/decoding ''' + handle = MaskWriteRegisterRequest(0x0000, 0x0101, 0x1010) + result = handle.encode() + self.assertEqual(result, '\x00\x00\x01\x01\x10\x10') + + def testMaskWriteRegisterRequestDecode(self): + ''' Test basic bit message encoding/decoding ''' + request = '\x00\x04\x00\xf2\x00\x25' + handle = MaskWriteRegisterRequest() + handle.decode(request) + self.assertEqual(handle.address, 0x0004) + self.assertEqual(handle.and_mask, 0x00f2) + self.assertEqual(handle.or_mask, 0x0025) + + def testMaskWriteRegisterRequestExecute(self): + ''' Test write register request valid execution ''' + context = MockContext(valid=True, default=0x0000) + handle = MaskWriteRegisterRequest(0x0000, 0x0101, 0x1010) + result = handle.execute(context) + self.assertTrue(isinstance(result, MaskWriteRegisterResponse)) + + def testMaskWriteRegisterRequestInvalidExecute(self): + ''' Test write register request execute with invalid data ''' + context = MockContext(valid=False, default=0x0000) + handle = MaskWriteRegisterRequest(0x0000, -1, 0x1010) + result = handle.execute(context) + self.assertEqual(ModbusExceptions.IllegalValue, + result.exception_code) + + handle = MaskWriteRegisterRequest(0x0000, 0x0101, -1) + result = handle.execute(context) + self.assertEqual(ModbusExceptions.IllegalValue, + result.exception_code) + + handle = MaskWriteRegisterRequest(0x0000, 0x0101, 0x1010) + result = handle.execute(context) + self.assertEqual(ModbusExceptions.IllegalAddress, + result.exception_code) + + #-----------------------------------------------------------------------# + # Mask Write Register Response + #-----------------------------------------------------------------------# + + def testMaskWriteRegisterResponseEncode(self): + ''' Test basic bit message encoding/decoding ''' + handle = MaskWriteRegisterResponse(0x0000, 0x0101, 0x1010) + result = handle.encode() + self.assertEqual(result, '\x00\x00\x01\x01\x10\x10') + + def testMaskWriteRegisterResponseDecode(self): + ''' Test basic bit message encoding/decoding ''' + request = '\x00\x04\x00\xf2\x00\x25' + handle = MaskWriteRegisterResponse() + handle.decode(request) + self.assertEqual(handle.address, 0x0004) + self.assertEqual(handle.and_mask, 0x00f2) + self.assertEqual(handle.or_mask, 0x0025) + #---------------------------------------------------------------------------# # Main #---------------------------------------------------------------------------# From 2885f4f89362574388622f76dbbccb4e5c41e67b Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Fri, 9 Sep 2011 15:33:42 +0000 Subject: [PATCH 035/243] Fixing the client/server async/sync implementations - asynchronous client/server for tcp working and tested - synchronous client/server for tcp/udp working and tested - adding tests to exercise the synchronous client/server - more documentation --- doc/sphinx/library/index.rst | 1 + examples/common/asynchronous-client.py | 115 ++++++++++------- pymodbus/client/async.py | 122 +++++++++++++----- pymodbus/client/sync.py | 66 +--------- pymodbus/interfaces.py | 2 +- pymodbus/server/sync.py | 126 +++++++++++++----- pymodbus/transaction.py | 32 +++-- test/test_client.py | 25 ---- test/test_client_sync.py | 170 +++++++++++++++++++++++++ 9 files changed, 449 insertions(+), 210 deletions(-) delete mode 100644 test/test_client.py create mode 100644 test/test_client_sync.py diff --git a/doc/sphinx/library/index.rst b/doc/sphinx/library/index.rst index de915c255..5adf68eba 100644 --- a/doc/sphinx/library/index.rst +++ b/doc/sphinx/library/index.rst @@ -23,6 +23,7 @@ from the sourcecode* other-message.rst file-message.rst events.rst + payload.rst pdu.rst pymodbus.rst register-read-message.rst diff --git a/examples/common/asynchronous-client.py b/examples/common/asynchronous-client.py index c7dfb22ce..8f5345cb4 100755 --- a/examples/common/asynchronous-client.py +++ b/examples/common/asynchronous-client.py @@ -7,11 +7,16 @@ client implementation from pymodbus. ''' #---------------------------------------------------------------------------# -# import the various server implementations +# import needed libraries #---------------------------------------------------------------------------# -from pymodbus.client.async import ModbusTcpClient as ModbusClient -#from pymodbus.client.async import ModbusUdpClient as ModbusClient -#from pymodbus.client.async import ModbusSerialClient as ModbusClient +from twisted.internet import reactor, protocol +from pymodbus.constants import Defaults + +#---------------------------------------------------------------------------# +# choose the requested modbus protocol +#---------------------------------------------------------------------------# +from pymodbus.client.async import ModbusClientProtocol +#from pymodbus.client.async import ModbusUdpClientProtocol #---------------------------------------------------------------------------# # configure the client logging @@ -21,61 +26,75 @@ log = logging.getLogger() log.setLevel(logging.DEBUG) -#---------------------------------------------------------------------------# -# choose the client you want -#---------------------------------------------------------------------------# -# make sure to start an implementation to hit against. For this -# you can use an existing device, the reference implementation in the tools -# directory, or start a pymodbus server. -#---------------------------------------------------------------------------# -client = ModbusClient('127.0.0.1') - #---------------------------------------------------------------------------# # helper method to test deferred callbacks #---------------------------------------------------------------------------# def dassert(deferred, callback): - def _tester(): - assert(callback()) - deferred.callback(_tester) - deferred.errback(lambda _: assert(False)) + def _assertor(value): assert(value) + deferred.addCallback(lambda r: _assertor(callback(r))) + deferred.addErrback(lambda _: _assertor(False)) #---------------------------------------------------------------------------# # example requests #---------------------------------------------------------------------------# # simply call the methods that you would like to use. An example session -# is displayed below along with some assert checks. +# is displayed below along with some assert checks. Note that unlike the +# synchronous version of the client, the asynchronous version returns +# deferreds which can be thought of as a handle to the callback to send +# the result of the operation. We are handling the result using the +# deferred assert helper(dassert). #---------------------------------------------------------------------------# -rq = client.write_coil(1, True) -rr = client.read_coils(1,1) -dassert(rq, lambda r: r.function_code < 0x80) # test that we are not an error -dassert(rr, lambda r: r.bits[0] == True) # test the expected value - -rq = client.write_coils(1, [True]*8) -rr = client.read_coils(1,8) -dassert(rq, lambda r: r.function_code < 0x80) # test that we are not an error -dassert(rr, lambda r: r.bits == [True]*8) # test the expected value - -rq = client.write_coils(1, [False]*8) -rr = client.read_discrete_inputs(1,8) -dassert(rq, lambda r: r.function_code < 0x80) # test that we are not an error -dassert(rr, lambda r: r.bits == [False]*8) # test the expected value - -rq = client.write_register(1, 10) -rr = client.read_holding_registers(1,1) -dassert(rq, lambda r: r.function_code < 0x80) # test that we are not an error -dassert(rr, lambda r: r.registers[0] == 10) # test the expected value - -rq = client.write_registers(1, [10]*8) -rr = client.read_input_registers(1,8) -dassert(rq, lambda r: r.function_code < 0x80) # test that we are not an error -dassert(rr, lambda r: r.registers == [10]*8) # test the expected value +def beginAsynchronousTest(client): + rq = client.write_coil(1, True) + rr = client.read_coils(1,1) + dassert(rq, lambda r: r.function_code < 0x80) # test that we are not an error + dassert(rr, lambda r: r.bits[0] == True) # test the expected value + + rq = client.write_coils(1, [True]*8) + rr = client.read_coils(1,8) + dassert(rq, lambda r: r.function_code < 0x80) # test that we are not an error + dassert(rr, lambda r: r.bits == [True]*8) # test the expected value + + rq = client.write_coils(1, [False]*8) + rr = client.read_discrete_inputs(1,8) + dassert(rq, lambda r: r.function_code < 0x80) # test that we are not an error + dassert(rr, lambda r: r.bits == [True]*8) # test the expected value + + rq = client.write_register(1, 10) + rr = client.read_holding_registers(1,1) + dassert(rq, lambda r: r.function_code < 0x80) # test that we are not an error + dassert(rr, lambda r: r.registers[0] == 10) # test the expected value + + rq = client.write_registers(1, [10]*8) + rr = client.read_input_registers(1,8) + dassert(rq, lambda r: r.function_code < 0x80) # test that we are not an error + dassert(rr, lambda r: r.registers == [17]*8) # test the expected value + + arguments = { + 'read_address': 1, + 'read_count': 8, + 'write_address': 1, + 'write_registers': [20]*8, + } + rq = client.readwrite_registers(**arguments) + rr = client.read_input_registers(1,8) + dassert(rq, lambda r: r.registers == [20]*8) # test the expected value + dassert(rr, lambda r: r.registers == [17]*8) # test the expected value -rq = client.readwrite_registers(1, [20]*8) -rr = client.read_input_registers(1,8) -dassert(rq, lambda r: r.function_code < 0x80) # test that we are not an error -dassert(rr, lambda r: r.registers == [20]*8) # test the expected value + #-----------------------------------------------------------------------# + # close the client at some time later + #-----------------------------------------------------------------------# + reactor.callLater(1, client.transport.loseConnection) + reactor.callLater(2, reactor.stop) #---------------------------------------------------------------------------# -# close the client +# choose the client you want +#---------------------------------------------------------------------------# +# make sure to start an implementation to hit against. For this +# you can use an existing device, the reference implementation in the tools +# directory, or start a pymodbus server. #---------------------------------------------------------------------------# -client.close() +defer = protocol.ClientCreator(reactor, ModbusClientProtocol + ).connectTCP("localhost", Defaults.Port) +defer.addCallback(beginAsynchronousTest) +reactor.run() diff --git a/pymodbus/client/async.py b/pymodbus/client/async.py index 5322776d3..25b64ffef 100644 --- a/pymodbus/client/async.py +++ b/pymodbus/client/async.py @@ -2,24 +2,41 @@ Implementation of a Modbus Client Using Twisted -------------------------------------------------- -Example Run:: +Example run:: + from twisted.internet import reactor, protocol + from pymodbus.client.async import ModbusClientProtocol + + def printResult(result): + print "Result: %d" % result.bits[0] + + def process(client): + result = client.write_coil(1, True) + result.addCallback(printResult) + reactor.callLater(1, reactor.stop) + + defer = protocol.ClientCreator(reactor, ModbusClientProtocol + ).connectTCP("localhost", 502) + defer.addCallback(process) + +Another example:: + + from twisted.internet import reactor from pymodbus.client.async import ModbusClientFactory - from pymodbus.bit_read_message import ReadCoilsRequest - def clientTest(): - requests = [ ReadCoilsRequest(0,99) ] - p = reactor.connectTCP("localhost", 502, ModbusClientFactory(requests)) + def process(): + factory = reactor.connectTCP("localhost", 502, ModbusClientFactory()) + reactor.stop() if __name__ == "__main__": - reactor.callLater(1, clientTest) + reactor.callLater(1, process) reactor.run() """ from collections import deque from twisted.internet import defer, protocol from pymodbus.factory import ClientDecoder from pymodbus.exceptions import ConnectionException -from pymodbus.transaction import ModbusSocketFramer +from pymodbus.transaction import ModbusSocketFramer, ModbusTransactionManager from pymodbus.client.common import ModbusClientMixin #---------------------------------------------------------------------------# @@ -28,16 +45,20 @@ def clientTest(): import logging _logger = logging.getLogger(__name__) +#---------------------------------------------------------------------------# +# A manager for the transaction identifiers +#---------------------------------------------------------------------------# +_manager = ModbusTransactionManager() + #---------------------------------------------------------------------------# -# Client Protocols +# Connected Client Protocols #---------------------------------------------------------------------------# class ModbusClientProtocol(protocol.Protocol, ModbusClientMixin): ''' This represents the base modbus client protocol. All the application layer code is deferred to a higher level wrapper. ''' - __tid = 0 def __init__(self, framer=None): ''' Initializes the framer module @@ -67,27 +88,22 @@ def dataReceived(self, data): :param data: The data returned from the server ''' - self.framer.processIncomingPacket(data, self._callback) + def _callback(reply): # todo errback/callback + if self._requests: + self._requests.popleft().callback(reply) + + self.framer.processIncomingPacket(data, _callback) def execute(self, request): ''' Starts the producer to send the next request to consumer.write(Frame(request)) ''' - request.transaction_id = self.__getNextTID() + request.transaction_id = _manager.getNextTID() #self.handler[request.transaction_id] = request packet = self.framer.buildPacket(request) self.transport.write(packet) return self._buildResponse() - def _callback(self, reply): - ''' The callback to call with the response message - - :param reply: The decoded response message - ''' - # todo errback/callback - if self._requests: - self._requests.popleft().callback(reply) - def _buildResponse(self): ''' Helper method to return a deferred response for the current request. @@ -101,19 +117,6 @@ def _buildResponse(self): self._requests.append(d) return d - def __getNextTID(self): - ''' Used to retrieve the next transaction id - :return: The next unique transaction id - - As the transaction identifier is represented with two - bytes, the highest TID is 0xffff - - ..todo:: Remove this and use the transaction manager - ''' - tid = (ModbusClientProtocol.__tid + 1) & 0xffff - ModbusClientProtocol.__tid = tid - return tid - #----------------------------------------------------------------------# # Extra Functions #----------------------------------------------------------------------# @@ -122,6 +125,56 @@ def __getNextTID(self): # deferLater(clock, self.delay, send, message) # self.retry -= 1 +#---------------------------------------------------------------------------# +# Not Connected Client Protocol +#---------------------------------------------------------------------------# +class ModbusUdpClientProtocol(protocol.DatagramProtocol, ModbusClientMixin): + ''' + This represents the base modbus client protocol. All the application + layer code is deferred to a higher level wrapper. + ''' + __tid = 0 + + def __init__(self, framer=None): + ''' Initializes the framer module + + :param framer: The framer to use for the protocol + ''' + self.framer = framer or ModbusSocketFramer(ClientDecoder()) + self._requests = deque() # link queue to tid + + def datagramReceived(self, data, (host, port)): + ''' Get response, check for valid message, decode result + + :param data: The data returned from the server + ''' + def _callback(reply): # todo errback/callback + if self._requests: + self._requests.popleft().callback(reply) + + _logger.debug("Datagram from: %s:%d" % (host, port)) + self.framer.processIncomingPacket(data, _callback) + + def execute(self, request): + ''' Starts the producer to send the next request to + consumer.write(Frame(request)) + ''' + request.transaction_id = _manager.getNextTID() + #self.handler[request.transaction_id] = request + packet = self.framer.buildPacket(request) + self.transport.write(packet) + return self._buildResponse() + + def _buildResponse(self): + ''' Helper method to return a deferred response + for the current request. + + :returns: A defer linked to the latest request + ''' + d = defer.Deferred() + self._requests.append(d) + return d + #---------------------------------------------------------------------------# # Client Factories @@ -135,5 +188,6 @@ class ModbusClientFactory(protocol.ReconnectingClientFactory): # Exported symbols #---------------------------------------------------------------------------# __all__ = [ - "ModbusClientProtocol", "ModbusClientFactory", + "ModbusClientProtocol", "ModbusUdpClientProtocol", + "ModbusClientFactory", ] diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 212e0bacf..88c774ab9 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -5,7 +5,9 @@ from pymodbus.factory import ClientDecoder from pymodbus.exceptions import NotImplementedException, ParameterException from pymodbus.exceptions import ConnectionException -from pymodbus.transaction import * +from pymodbus.transaction import ModbusTransactionManager +from pymodbus.transaction import ModbusSocketFramer, ModbusBinaryFramer +from pymodbus.transaction import ModbusAsciiFramer, ModbusRtuFramer from pymodbus.client.common import ModbusClientMixin #---------------------------------------------------------------------------# @@ -16,64 +18,8 @@ #---------------------------------------------------------------------------# -# Client Producer/Consumer +# The Synchronous Clients #---------------------------------------------------------------------------# -class ModbusTransactionManager: - ''' - This is a simply pull producer that feeds requests to the modbus client - ''' - - __tid = Defaults.TransactionId - - def __init__(self, client): - ''' Sets up the producer to begin sending requests - - :param client: The client socket wrapper - ''' - self.client = client - - def execute(self, request): - ''' Starts the producer to send the next request to - consumer.write(Frame(request)) - ''' - self.response = None - retries = Defaults.Retries - request.transaction_id = self.__getNextTID() - _logger.debug("Running transaction %d" % request.transaction_id) - - while retries > 0: - try: - self.client.connect() - self.client._send(self.client.framer.buildPacket(request)) - # I need to fix this to read the header and the result size, - # as this may not read the full result set, but right now - # it should be fine... - result = self.client._recv(1024) - self.client.framer.processIncomingPacket(result, self.__set_result) - break; - except socket.error, msg: - self.client.close() - _logger.debug("Transaction failed. (%s) " % msg) - retries -= 1 - return self.response - - def __set_result(self, message): - ''' Quick helper that lets me reuse the async framer - - :param message: The decoded message - ''' - self.response = message - - def __getNextTID(self): - ''' Used internally to handle the transaction identifiers. - As the transaction identifier is represented with two - bytes, the highest TID is 0xffff - ''' - tid = (ModbusTransactionManager.__tid + 1) & 0xffff - ModbusTransactionManager.__tid = tid - return tid - - class BaseModbusClient(ModbusClientMixin): ''' Inteface for a modbus synchronous client. Defined here are all the @@ -103,7 +49,7 @@ def connect(self): def close(self): ''' Closes the underlying socket connection ''' - raise NotImplementedException("Method not implemented by derived class") + pass def _send(self, request): ''' Sends data on the underlying socket @@ -255,7 +201,7 @@ def connect(self): if self.socket: return True try: self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - #self.socket.bind(('localhost', Defaults.Port)) + #self.socket.bind((self.host, self.port)) except socket.error, ex: _logger.error('Unable to create udp socket %s' % ex) self.close() diff --git a/pymodbus/interfaces.py b/pymodbus/interfaces.py index 974a69239..4509a95d0 100644 --- a/pymodbus/interfaces.py +++ b/pymodbus/interfaces.py @@ -20,7 +20,7 @@ def __new__(cls, *args, **kwargs): ''' Create a new instance ''' if '_inst' not in vars(cls): - cls._inst = object.__new__(cls, *args, **kwargs) + cls._inst = object.__new__(cls) return cls._inst diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index 7448c08f3..92654d0e1 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -14,7 +14,7 @@ from pymodbus.device import ModbusControlBlock from pymodbus.device import ModbusDeviceIdentification from pymodbus.transaction import * -from pymodbus.exceptions import ModbusException +from pymodbus.exceptions import ModbusException, NotImplementedException from pymodbus.pdu import ModbusExceptions as merror #---------------------------------------------------------------------------# @@ -25,9 +25,9 @@ #---------------------------------------------------------------------------# -# Server +# Protocol Handlers #---------------------------------------------------------------------------# -class ModbusRequestHandler(SocketServer.BaseRequestHandler): +class ModbusBaseRequestHandler(SocketServer.BaseRequestHandler): ''' Implements the modbus server protocol This uses the socketserver.BaseRequestHandler to implement @@ -48,6 +48,56 @@ def finish(self): _logger.debug("Client Disconnected [%s:%s]" % self.client_address) self.server.threads.remove(self) + def execute(self, request): + ''' The callback to call with the resulting message + + :param request: The decoded request message + ''' + try: + context = self.server.context[request.unit_id] + response = request.execute(context) + except Exception, ex: + _logger.debug("Datastore unable to fulfill request %s" % ex) + response = request.doException(merror.SlaveFailure) + response.transaction_id = request.transaction_id + response.unit_id = request.unit_id + self.send(response) + + def decode(self, message): + ''' Decodes a request packet + + :param message: The raw modbus request packet + :returns: The decoded modbus message or None if error + ''' + try: + return decodeModbusRequestPDU(message) + except ModbusException, er: + _logger.warn("Unable to decode request %s" % er) + return None + + #---------------------------------------------------------------------------# + # Base class implementations + #---------------------------------------------------------------------------# + def handle(self): + ''' Callback when we receive any data + ''' + raise NotImplementedException("Method not implemented by derived class") + + def send(self, message): + ''' Send a request (string) to the network + + :param message: The unencoded modbus response + ''' + raise NotImplementedException("Method not implemented by derived class") + + +class ModbusConnectedRequestHandler(ModbusBaseRequestHandler): + ''' Implements the modbus server protocol + + This uses the socketserver.BaseRequestHandler to implement + the client handler for a connected protocol (TCP). + ''' + def handle(self): ''' Callback when we receive any data ''' @@ -64,24 +114,6 @@ def handle(self): self.running = False except: self.running = False -#---------------------------------------------------------------------------# -# Extra Helper Functions -#---------------------------------------------------------------------------# - def execute(self, request): - ''' The callback to call with the resulting message - - :param request: The decoded request message - ''' - try: - context = self.server.context[request.unit_id] - response = request.execute(context) - except Exception, ex: - _logger.debug("Datastore unable to fulfill request %s" % ex) - response = request.doException(merror.SlaveFailure) - response.transaction_id = request.transaction_id - response.unit_id = request.unit_id - self.send(response) - def send(self, message): ''' Send a request (string) to the network @@ -93,19 +125,46 @@ def send(self, message): _logger.debug('send: %s' % b2a_hex(pdu)) return self.request.send(pdu) - def decode(self, message): - ''' Decodes a request packet - :param message: The raw modbus request packet - :returns: The decoded modbus message or None if error +class ModbusDisconnectedRequestHandler(ModbusBaseRequestHandler): + ''' Implements the modbus server protocol + + This uses the socketserver.BaseRequestHandler to implement + the client handler for a disconnected protocol (UDP). The + only difference is that we have to specify who to send the + resulting packet data to. + ''' + + def handle(self): + ''' Callback when we receive any data ''' - try: - return decodeModbusRequestPDU(message) - except ModbusException, er: - _logger.warn("Unable to decode request %s" % er) - return None + while self.running: + try: + data, self.request = self.request + if not data: self.running = False + _logger.debug(" ".join([hex(ord(x)) for x in data])) + # if not self.server.control.ListenOnly: + self.framer.processIncomingPacket(data, self.execute) + except socket.timeout: pass + except socket.error, msg: + _logger.error("Socket error occurred %s" % msg) + self.running = False + except: self.running = False + + def send(self, message): + ''' Send a request (string) to the network + :param message: The unencoded modbus response + ''' + if message.should_respond: + #self.server.control.Counter.BusMessage += 1 + pdu = self.framer.buildPacket(message) + _logger.debug('send: %s' % b2a_hex(pdu)) + return self.request.sendto(pdu, self.client_address) +#---------------------------------------------------------------------------# +# Server Implementations +#---------------------------------------------------------------------------# class ModbusTcpServer(SocketServer.ThreadingTCPServer): ''' A modbus threaded tcp socket server @@ -136,7 +195,7 @@ def __init__(self, context, framer=None, identity=None): self.control.Identity.update(identity) SocketServer.ThreadingTCPServer.__init__(self, - ("", Defaults.Port), ModbusRequestHandler) + ("", Defaults.Port), ModbusConnectedRequestHandler) def process_request(self, request, client): ''' Callback for connecting a new client thread @@ -185,7 +244,7 @@ def __init__(self, context, framer=None, identity=None): self.control.Identity.update(identity) SocketServer.ThreadingUDPServer.__init__(self, - ("", Defaults.Port), ModbusRequestHandler) + ("", Defaults.Port), ModbusDisconnectedRequestHandler) def process_request(self, request, client): ''' Callback for connecting a new client thread @@ -193,6 +252,7 @@ def process_request(self, request, client): :param request: The request to handle :param client: The address of the client ''' + packet, socket = request # TODO I might have to rewrite _logger.debug("Started thread to serve client at " + str(client)) SocketServer.ThreadingUDPServer.process_request(self, request, client) @@ -266,7 +326,7 @@ def _build_handler(self): request = self.socket request.send = request.write request.recv = request.read - handler = ModbusRequestHandler(request, + handler = ModbusConnectedRequestHandler(request, ('127.0.0.1', self.device), self) return handler diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 79ce2c013..4118ba826 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -2,6 +2,7 @@ Collection of transaction based abstractions ''' import struct +import socket from binascii import b2a_hex, a2b_hex from pymodbus.exceptions import ModbusIOException @@ -39,28 +40,41 @@ class ModbusTransactionManager(Singleton): __tid = Defaults.TransactionId __transactions = [] - def __init__(self): - ''' Initializes an instance of the ModbusTransactionManager ''' - pass + def __init__(self, client=None): + ''' Initializes an instance of the ModbusTransactionManager + + :param client: The client socket wrapper + ''' + self.client = client def execute(self, request): ''' Starts the producer to send the next request to consumer.write(Frame(request)) ''' - import socket + def _set_result(message): + ''' a helper method so I can reuse the async framers''' + self.response = message + + self.response = None retries = Defaults.Retries - request.transaction_id = self.__getNextTID() + request.transaction_id = self.getNextTID() _logger.debug("Running transaction %d" % request.transaction_id) while retries > 0: try: - self.socket.connect() - packet = self.framer.buildPacket(request) - self.socket.send(packet) + self.client.connect() + self.client._send(self.client.framer.buildPacket(request)) + # I need to fix this to read the header and the result size, + # as this may not read the full result set, but right now + # it should be fine... + result = self.client._recv(1024) + self.client.framer.processIncomingPacket(result, _set_result) + break; except socket.error, msg: - self.socket.close() + self.client.close() _logger.debug("Transaction failed. (%s) " % msg) retries -= 1 + return self.response def addTransaction(self, request): ''' Adds a transaction to the handler diff --git a/test/test_client.py b/test/test_client.py deleted file mode 100644 index 3d694af7b..000000000 --- a/test/test_client.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python -import unittest -from twisted.test import test_protocols -from pymodbus.client.sync import ModbusTcpClient, ModbusUdpClient -from pymodbus.client.sync import ModbusSerialClient - -class SynchronousClientTest(unittest.TestCase): - ''' - This is the unittest for the pymodbus.client.sync module - ''' - - def setUp(self): - pass - - def testSyncUdpClientInstantiation(self): - client = ModbusUdpClient() - self.assertNotEqual(client, None) - - def testSyncTcpClientInstantiation(self): - client = ModbusTcpClient() - self.assertNotEqual(client, None) - - def testSyncSerialClientInstantiation(self): - client = ModbusSerialClient - self.assertNotEqual(client, None) diff --git a/test/test_client_sync.py b/test/test_client_sync.py new file mode 100644 index 000000000..672bd9f5e --- /dev/null +++ b/test/test_client_sync.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +import unittest +from twisted.test import test_protocols +from pymodbus.client.sync import ModbusTcpClient, ModbusUdpClient +from pymodbus.client.sync import ModbusSerialClient, BaseModbusClient +from pymodbus.exceptions import ConnectionException, NotImplementedException +from pymodbus.exceptions import ParameterException +from pymodbus.transaction import ModbusAsciiFramer, ModbusRtuFramer +from pymodbus.transaction import ModbusBinaryFramer + +#---------------------------------------------------------------------------# +# Mock Classes +#---------------------------------------------------------------------------# +class mockTransaction(object): + def execute(self, request): return True + +class mockSocket(object): + def close(self): return True + def recv(self, size): return '\x00'*size + def read(self, size): return '\x00'*size + def send(self, msg): return len(msg) + def write(self, msg): return len(msg) + def recvfrom(self, size): return '\x00'*size + def sendto(self, msg, *args): return len(msg) + +#---------------------------------------------------------------------------# +# Fixture +#---------------------------------------------------------------------------# +class SynchronousClientTest(unittest.TestCase): + ''' + This is the unittest for the pymodbus.client.sync module + ''' + + #-----------------------------------------------------------------------# + # Setup/TearDown + #-----------------------------------------------------------------------# + + def setUp(self): + ''' + Initializes the test environment + ''' + pass + + def tearDown(self): + ''' Cleans up the test environment ''' + pass + + #-----------------------------------------------------------------------# + # Test Base Client + #-----------------------------------------------------------------------# + + def testBaseModbusClient(self): + ''' Test the base class for all the clients ''' + + client = BaseModbusClient(None) + client.transaction = None + self.assertRaises(NotImplementedException, lambda: client.connect()) + self.assertRaises(NotImplementedException, lambda: client._send(None)) + self.assertRaises(NotImplementedException, lambda: client._recv(None)) + self.assertRaises(NotImplementedException, lambda: client.__enter__()) + self.assertRaises(ConnectionException, lambda: client.execute()) + self.assertEquals("Null Transport", str(client)) + client.close() + client.__exit__(0,0,0) + + # a successful execute + client.transaction = mockTransaction() + self.assertTrue(client.execute()) + + # a successful connect + client.connect = lambda: True + self.assertEqual(client.__enter__(), client) + + # a unsuccessful connect + client.connect = lambda: False + self.assertRaises(ConnectionException, lambda: client.__enter__()) + + #-----------------------------------------------------------------------# + # Test UDP Client + #-----------------------------------------------------------------------# + + def testSyncUdpClientInstantiation(self): + client = ModbusUdpClient() + self.assertNotEqual(client, None) + + def testBasicSyncUdpClient(self): + ''' Test the basic methods for the udp sync client''' + + # receive/send + client = ModbusUdpClient() + client.socket = mockSocket() + self.assertEqual(0, client._send(None)) + self.assertEqual(1, client._send('\x00')) + self.assertEqual('\x00', client._recv(1)) + + # connect/disconnect + self.assertTrue(client.connect()) + client.close() + + # already closed socket + client.socket = False + client.close() + + self.assertEqual("127.0.0.1:502", str(client)) + + #-----------------------------------------------------------------------# + # Test TCP Client + #-----------------------------------------------------------------------# + + def testSyncTcpClientInstantiation(self): + client = ModbusTcpClient() + self.assertNotEqual(client, None) + + def testBasicSyncTcpClient(self): + ''' Test the basic methods for the tcp sync client''' + + # receive/send + client = ModbusTcpClient() + client.socket = mockSocket() + self.assertEqual(0, client._send(None)) + self.assertEqual(1, client._send('\x00')) + self.assertEqual('\x00', client._recv(1)) + + # connect/disconnect + self.assertTrue(client.connect()) + client.close() + + # already closed socket + client.socket = False + client.close() + + self.assertEqual("127.0.0.1:502", str(client)) + + #-----------------------------------------------------------------------# + # Test Serial Client + #-----------------------------------------------------------------------# + + def testSyncSerialClientInstantiation(self): + client = ModbusSerialClient() + self.assertNotEqual(client, None) + self.assertTrue(isinstance(ModbusSerialClient(method='ascii').framer, ModbusAsciiFramer)) + self.assertTrue(isinstance(ModbusSerialClient(method='rtu').framer, ModbusRtuFramer)) + self.assertTrue(isinstance(ModbusSerialClient(method='binary').framer, ModbusBinaryFramer)) + self.assertRaises(ParameterException, lambda: ModbusSerialClient(method='something')) + + def testBasicSyncSerialClient(self): + ''' Test the basic methods for the serial sync client''' + + # receive/send + client = ModbusSerialClient() + client.socket = mockSocket() + self.assertEqual(0, client._send(None)) + self.assertEqual(1, client._send('\x00')) + self.assertEqual('\x00', client._recv(1)) + + # connect/disconnect + self.assertTrue(client.connect()) + client.close() + + # already closed socket + client.socket = False + client.close() + + self.assertEqual('ascii baud[19200]', str(client)) + +#---------------------------------------------------------------------------# +# Main +#---------------------------------------------------------------------------# +if __name__ == "__main__": + unittest.main() From 9a82d03310b13579308fab4f3f4e49025fecd80f Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Fri, 9 Sep 2011 15:54:32 +0000 Subject: [PATCH 036/243] adding test stubs to get full coverage count --- doc/quality/current.coverage | 16 ++++++++----- test/test_client_async.py | 43 +++++++++++++++++++++++++++++++++ test/test_server_async.py | 44 ++++++++++++++++++++++++++++++++++ test/test_server_sync.py | 46 ++++++++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 test/test_client_async.py create mode 100644 test/test_server_async.py create mode 100644 test/test_server_sync.py diff --git a/doc/quality/current.coverage b/doc/quality/current.coverage index 17e9c1c0b..58d4a2f4e 100644 --- a/doc/quality/current.coverage +++ b/doc/quality/current.coverage @@ -4,33 +4,37 @@ pymodbus 15 13 86% 36-37 pymodbus.bit_read_message 68 68 100% pymodbus.bit_write_message 95 95 100% pymodbus.client 1 1 100% +pymodbus.client.async 59 28 47% 75-76, 83-84, 91-95, 101-105, 113-118, 143-144, 151-156, 162-166, 174-176 pymodbus.client.common 44 44 100% -pymodbus.client.sync 154 61 39% 39-58, 65, 72-74, 101, 106, 114, 122, 132-134, 144-146, 150, 154, 161, 187-197, 202-204, 212-214, 222, 229, 255-262, 267, 275-277, 285, 292, 313-322, 331-335, 342-350, 355-357, 365-367, 375, 382 +pymodbus.client.sync 129 114 88% 135-138, 140-143, 205, 207-208, 289, 293-296 pymodbus.constants 27 27 100% pymodbus.datastore 5 5 100% pymodbus.datastore.context 49 49 100% pymodbus.datastore.remote 31 31 100% pymodbus.datastore.store 67 67 100% pymodbus.device 148 148 100% -pymodbus.diag_message 202 201 99% 704 +pymodbus.diag_message 202 202 100% pymodbus.events 60 60 100% pymodbus.exceptions 22 22 100% pymodbus.factory 56 56 100% -pymodbus.file_message 189 177 93% 343, 358-367, 395 +pymodbus.file_message 189 189 100% pymodbus.interfaces 43 43 100% pymodbus.internal 1 1 100% +pymodbus.internal.ptwisted 26 7 26% 26-37, 47-55 pymodbus.other_message 145 145 100% pymodbus.payload 107 107 100% pymodbus.pdu 66 66 100% pymodbus.register_read_message 124 124 100% pymodbus.register_write_message 87 87 100% pymodbus.server 0 0 100% -pymodbus.transaction 263 205 77% 50-63, 229-239, 379, 409-418, 553-562, 632, 709-718, 743-744 +pymodbus.server.async 104 32 30% 40-41, 48, 55-57, 64-73, 80-84, 108-115, 135-142, 149-153, 160-169, 177-180, 192-199, 208-214, 224-231 +pymodbus.server.sync 165 45 27% 40-43, 48-49, 56-64, 72-76, 84, 91, 104-115, 122-126, 141-152, 159-163, 188-197, 206-207, 212-214, 237-246, 255-257, 262-264, 287-303, 310-318, 326-331, 339-341, 346-347, 359-361, 370-372, 381-383 +pymodbus.transaction 269 238 88% 54-64, 66-72, 76-77, 243-245, 423-424, 431-432, 567-570, 574, 723-725, 732, 757 pymodbus.utilities 67 67 100% pymodbus.version 13 13 100% --------------------------------------------------------------- -TOTAL 2149 1983 92% +TOTAL 2484 2194 88% ---------------------------------------------------------------------- -Ran 183 tests in 0.446s +Ran 197 tests in 0.527s OK diff --git a/test/test_client_async.py b/test/test_client_async.py new file mode 100644 index 000000000..f30040c49 --- /dev/null +++ b/test/test_client_async.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +import unittest +from twisted.test import test_protocols +from pymodbus.client.async import ModbusClientProtocol, ModbusUdpClientProtocol +from pymodbus.client.async import ModbusClientFactory +from pymodbus.exceptions import ConnectionException, NotImplementedException +from pymodbus.exceptions import ParameterException + +#---------------------------------------------------------------------------# +# Fixture +#---------------------------------------------------------------------------# +class AsynchronousClientTest(unittest.TestCase): + ''' + This is the unittest for the pymodbus.client.async module + ''' + + #-----------------------------------------------------------------------# + # Setup/TearDown + #-----------------------------------------------------------------------# + + def setUp(self): + ''' + Initializes the test environment + ''' + pass + + def tearDown(self): + ''' Cleans up the test environment ''' + pass + + #-----------------------------------------------------------------------# + # Test Base Client + #-----------------------------------------------------------------------# + + def testExampleTest(self): + ''' Test the base class for all the clients ''' + self.assertTrue(True) + +#---------------------------------------------------------------------------# +# Main +#---------------------------------------------------------------------------# +if __name__ == "__main__": + unittest.main() diff --git a/test/test_server_async.py b/test/test_server_async.py new file mode 100644 index 000000000..a560da5f7 --- /dev/null +++ b/test/test_server_async.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +import unittest +from twisted.test import test_protocols +from pymodbus.server.async import ModbusTcpProtocol, ModbusUdpProtocol +from pymodbus.server.async import ModbusServerFactory +from pymodbus.server.async import StartTcpServer, StartUdpServer, StartSerialServer +from pymodbus.exceptions import ConnectionException, NotImplementedException +from pymodbus.exceptions import ParameterException + +#---------------------------------------------------------------------------# +# Fixture +#---------------------------------------------------------------------------# +class AsynchronousServerTest(unittest.TestCase): + ''' + This is the unittest for the pymodbus.server.async module + ''' + + #-----------------------------------------------------------------------# + # Setup/TearDown + #-----------------------------------------------------------------------# + + def setUp(self): + ''' + Initializes the test environment + ''' + pass + + def tearDown(self): + ''' Cleans up the test environment ''' + pass + + #-----------------------------------------------------------------------# + # Test Base Client + #-----------------------------------------------------------------------# + + def testExampleTest(self): + ''' Test the base class for all the clients ''' + self.assertTrue(True) + +#---------------------------------------------------------------------------# +# Main +#---------------------------------------------------------------------------# +if __name__ == "__main__": + unittest.main() diff --git a/test/test_server_sync.py b/test/test_server_sync.py new file mode 100644 index 000000000..ebb6543e8 --- /dev/null +++ b/test/test_server_sync.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +import unittest +from twisted.test import test_protocols +from pymodbus.server.sync import ModbusBaseRequestHandler +from pymodbus.server.sync import ModbusConnectedRequestHandler +from pymodbus.server.sync import ModbusDisconnectedRequestHandler +from pymodbus.server.sync import ModbusTcpServer, ModbusUdpServer, ModbusSerialServer +from pymodbus.server.sync import StartTcpServer, StartUdpServer, StartSerialServer +from pymodbus.exceptions import ConnectionException, NotImplementedException +from pymodbus.exceptions import ParameterException + +#---------------------------------------------------------------------------# +# Fixture +#---------------------------------------------------------------------------# +class AsynchronousClientTest(unittest.TestCase): + ''' + This is the unittest for the pymodbus.server.sync module + ''' + + #-----------------------------------------------------------------------# + # Setup/TearDown + #-----------------------------------------------------------------------# + + def setUp(self): + ''' + Initializes the test environment + ''' + pass + + def tearDown(self): + ''' Cleans up the test environment ''' + pass + + #-----------------------------------------------------------------------# + # Test Base Client + #-----------------------------------------------------------------------# + + def testExampleTest(self): + ''' Test the base class for all the clients ''' + self.assertTrue(True) + +#---------------------------------------------------------------------------# +# Main +#---------------------------------------------------------------------------# +if __name__ == "__main__": + unittest.main() From 5df177992ae83113c2f2e28287b79a5b3fd8a263 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Fri, 9 Sep 2011 20:58:45 +0000 Subject: [PATCH 037/243] working on jamod, need to set up more complete project --- .../functional/asynchronous-tcp-client.py | 15 +++- examples/functional/base_runner.py | 8 +- examples/tools/jamod/bin/jamod | 44 +++++++++++ examples/tools/jamod/build.xml | 10 ++- examples/tools/jamod/src/ClientExample.java | 2 +- ...erialSlave.java => ModbusSerialSlave.java} | 10 +-- examples/tools/jamod/src/ModbusTcpSlave.java | 76 +++++++++++++++++++ examples/tools/jamod/src/ModbusUdpSlave.java | 76 +++++++++++++++++++ 8 files changed, 227 insertions(+), 14 deletions(-) create mode 100755 examples/tools/jamod/bin/jamod rename examples/tools/jamod/src/{SerialSlave.java => ModbusSerialSlave.java} (92%) create mode 100644 examples/tools/jamod/src/ModbusTcpSlave.java create mode 100644 examples/tools/jamod/src/ModbusUdpSlave.java diff --git a/examples/functional/asynchronous-tcp-client.py b/examples/functional/asynchronous-tcp-client.py index 4603b56b4..1dd9d6d2a 100644 --- a/examples/functional/asynchronous-tcp-client.py +++ b/examples/functional/asynchronous-tcp-client.py @@ -1,6 +1,8 @@ #!/usr/bin/env python import unittest -from pymodbus.client.async import ModbusTcpClient as ModbusClient +from twisted.internet import reactor, protocol +from pymodbus.constants import Defaults +from pymodbus.client.async import ModbusClientProtocol from base_runner import Runner class AsynchronousTcpClient(Runner, unittest.TestCase): @@ -11,13 +13,18 @@ class AsynchronousTcpClient(Runner, unittest.TestCase): def setUp(self): ''' Initializes the test environment ''' + def _callback(client): self.client = client self.initialize(["../tools/reference/diagslave", "-m", "tcp", "-p", "12345"]) - self.client = ModbusClient(port=12345) + defer = protocol.ClientCreator(reactor, ModbusClientProtocol + ).connectTCP("localhost", Defaults.Port) + defer.addCallback(_callback) + reactor.run() def tearDown(self): ''' Cleans up the test environment ''' - self.client.close() - self.shutdown() + reactor.callLater(1, client.transport.loseConnection) + reactor.callLater(2, reactor.stop) + reactor.shutdown() #---------------------------------------------------------------------------# # Main diff --git a/examples/functional/base_runner.py b/examples/functional/base_runner.py index bc340e8db..c432f798d 100644 --- a/examples/functional/base_runner.py +++ b/examples/functional/base_runner.py @@ -58,7 +58,13 @@ def testReadWriteInputRegisters(self): self.__validate(rr, lambda r: r.registers == [10]*8) def testReadWriteRegistersTogether(self): - rq = self.client.readwrite_registers(1, 8, 1, [20]*8) + arguments = { + 'read_address': 1, + 'read_count': 8, + 'write_address': 1, + 'write_registers': [20]*8, + } + rq = self.client.readwrite_registers(**arguments) rr = self.client.read_input_registers(1,8) self.__validate(rq, lambda r: r.function_code < 0x80) self.__validate(rr, lambda r: r.registers == [20]*8) diff --git a/examples/tools/jamod/bin/jamod b/examples/tools/jamod/bin/jamod new file mode 100755 index 000000000..6dca15e19 --- /dev/null +++ b/examples/tools/jamod/bin/jamod @@ -0,0 +1,44 @@ +#!/bin/bash +#------------------------------------------------------------ # +# setup +#------------------------------------------------------------ # +export JAVA_HOME="/usr" +export ROOT="org.pymodbus" +export ARGUMENTS="-Dnet.wimpi.modbus.debug=true" +export CLASSPATH="-cp ../jar/jamod-pymodbus.jar" +export JAVA="java ${ARGUMENTS} ${CLASSPATH}" + +#------------------------------------------------------------ # +# script runner +#------------------------------------------------------------ # +case "$1" in + + #---------------------------------------------------------- # + # run the tcp slave implementation + #---------------------------------------------------------- # + tcp) ${JAVA} ${ROOT}.ModbusTcpSlave + ;; + + #---------------------------------------------------------- # + # run the udp slave implementation + #---------------------------------------------------------- # + udp) ${JAVA} ${ROOT}.ModbusUdpSlave + ;; + + #---------------------------------------------------------- # + # run the serial slave implementation + #---------------------------------------------------------- # + serial) ${JAVA} ${ROOT}.ModbusSerialSlave /dev/tty0 + ;; + + #---------------------------------------------------------- # + # script help + #---------------------------------------------------------- # + *) + echo "Usage: serial|udp|tcp" + exit 1 + ;; + +esac + +exit 0 diff --git a/examples/tools/jamod/build.xml b/examples/tools/jamod/build.xml index a4df4f87b..6ceab473e 100644 --- a/examples/tools/jamod/build.xml +++ b/examples/tools/jamod/build.xml @@ -16,6 +16,12 @@ + + + + + + @@ -30,9 +36,7 @@ - - - + diff --git a/examples/tools/jamod/src/ClientExample.java b/examples/tools/jamod/src/ClientExample.java index 999369e3f..521592ff8 100644 --- a/examples/tools/jamod/src/ClientExample.java +++ b/examples/tools/jamod/src/ClientExample.java @@ -14,7 +14,7 @@ * limitations under the License. ***/ -//package net.wimpi.modbus.cmd; +package org.pymodbus; import java.net.InetAddress; diff --git a/examples/tools/jamod/src/SerialSlave.java b/examples/tools/jamod/src/ModbusSerialSlave.java similarity index 92% rename from examples/tools/jamod/src/SerialSlave.java rename to examples/tools/jamod/src/ModbusSerialSlave.java index 14e434cb7..581491134 100644 --- a/examples/tools/jamod/src/SerialSlave.java +++ b/examples/tools/jamod/src/ModbusSerialSlave.java @@ -14,7 +14,7 @@ * limitations under the License. **/ -//package net.wimpi.modbus.cmd; +package org.pymodbus; import net.wimpi.modbus.Modbus; import net.wimpi.modbus.ModbusCoupler; @@ -24,14 +24,14 @@ /** - * Class implementing a simple Modbus slave. + * Class implementing a simple Modbus Serial slave. * A simple process image is available to test * functionality and behaviour of the implementation. * - * @author Dieter Wimberger + * @author Galen Collins * @version @version@ (@date@) */ -public class SerialSlave { +public class ModbusSerialSlave { public static void main(String[] args) { @@ -39,7 +39,7 @@ public static void main(String[] args) { SimpleProcessImage spi = new SimpleProcessImage(); String portname = args[0]; - if (Modbus.debug) System.out.println("jModbus ModbusSerial Slave"); + if (Modbus.debug) System.out.println("jModbus Modbus Serial Slave"); try { diff --git a/examples/tools/jamod/src/ModbusTcpSlave.java b/examples/tools/jamod/src/ModbusTcpSlave.java new file mode 100644 index 000000000..80a0d638d --- /dev/null +++ b/examples/tools/jamod/src/ModbusTcpSlave.java @@ -0,0 +1,76 @@ +/** + * Copyright 2002-2010 jamod development team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +package org.pymodbus; + +import net.wimpi.modbus.Modbus; +import net.wimpi.modbus.ModbusCoupler; +import net.wimpi.modbus.net.ModbusTCPListener; +import net.wimpi.modbus.procimg.*; +import net.wimpi.modbus.util.SerialParameters; + + +/** + * Class implementing a simple Modbus TCP slave. + * A simple process image is available to test + * functionality and behaviour of the implementation. + * + * @author Galen Collins + * @version @version@ (@date@) + */ +public class ModbusTcpSlave { + + public static void main(String[] args) { + + ModbusTCPListener listener = null; + SimpleProcessImage spi = new SimpleProcessImage(); + int port = Modbus.DEFAULT_PORT; + + if (Modbus.debug) System.out.println("jModbus Modbus TCP Slave"); + if (args != null && args.length >= 1) { + port = Integer.parseInt(args[0]); + } + + try { + + //1. Prepare a process image + spi = new SimpleProcessImage(); + spi.addDigitalOut(new SimpleDigitalOut(true)); + spi.addDigitalOut(new SimpleDigitalOut(false)); + spi.addDigitalIn(new SimpleDigitalIn(false)); + spi.addDigitalIn(new SimpleDigitalIn(true)); + spi.addDigitalIn(new SimpleDigitalIn(false)); + spi.addDigitalIn(new SimpleDigitalIn(true)); + spi.addRegister(new SimpleRegister(251)); + spi.addInputRegister(new SimpleInputRegister(45)); + + //2. Create the coupler and set the slave identity + ModbusCoupler.getReference().setProcessImage(spi); + ModbusCoupler.getReference().setMaster(false); + ModbusCoupler.getReference().setUnitID(2); + + //3. Create a listener with 3 threads + listener = new ModbusTCPListener(3); + listener.setPort(port); + listener.start(); + + } catch (Exception ex) { + ex.printStackTrace(); + } + }//main + +} + diff --git a/examples/tools/jamod/src/ModbusUdpSlave.java b/examples/tools/jamod/src/ModbusUdpSlave.java new file mode 100644 index 000000000..e8ce9b2cd --- /dev/null +++ b/examples/tools/jamod/src/ModbusUdpSlave.java @@ -0,0 +1,76 @@ +/** + * Copyright 2002-2010 jamod development team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +package org.pymodbus; + +import net.wimpi.modbus.Modbus; +import net.wimpi.modbus.ModbusCoupler; +import net.wimpi.modbus.net.ModbusUDPListener; +import net.wimpi.modbus.procimg.*; +import net.wimpi.modbus.util.SerialParameters; + + +/** + * Class implementing a simple Modbus slave. + * A simple process image is available to test + * functionality and behaviour of the implementation. + * + * @author Galen Collins + * @version @version@ (@date@) + */ +public class ModbusUdpSlave { + + public static void main(String[] args) { + + ModbusUDPListener listener = null; + SimpleProcessImage spi = new SimpleProcessImage(); + int port = Modbus.DEFAULT_PORT; + + if (Modbus.debug) System.out.println("jModbus Modbus UDP Slave"); + if (args != null && args.length >= 1) { + port = Integer.parseInt(args[0]); + } + + try { + + //1. Prepare a process image + spi = new SimpleProcessImage(); + spi.addDigitalOut(new SimpleDigitalOut(true)); + spi.addDigitalOut(new SimpleDigitalOut(false)); + spi.addDigitalIn(new SimpleDigitalIn(false)); + spi.addDigitalIn(new SimpleDigitalIn(true)); + spi.addDigitalIn(new SimpleDigitalIn(false)); + spi.addDigitalIn(new SimpleDigitalIn(true)); + spi.addRegister(new SimpleRegister(251)); + spi.addInputRegister(new SimpleInputRegister(45)); + + //2. Create the coupler and set the slave identity + ModbusCoupler.getReference().setProcessImage(spi); + ModbusCoupler.getReference().setMaster(false); + ModbusCoupler.getReference().setUnitID(2); + + //3. Create a listener with 3 threads + listener = new ModbusUDPListener(); + listener.setPort(port); + listener.start(); + + } catch (Exception ex) { + ex.printStackTrace(); + } + }//main + +} + From ce574cbfe0e191186acee89b26dd41fc7cdcf77f Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Fri, 16 Sep 2011 20:14:46 +0000 Subject: [PATCH 038/243] adding GetDeviceInformationRequest --- doc/sphinx/library/constants.rst | 6 ++ doc/sphinx/library/device.rst | 3 + doc/sphinx/library/mei-message.rst | 19 ++++ pymodbus/constants.py | 52 +++++++++- pymodbus/device.py | 47 +++++++++ pymodbus/mei_message.py | 157 +++++++++++++++++++++++++++++ test/test_device.py | 28 +++++ test/test_file_message.py | 1 + test/test_mei_messages.py | 123 ++++++++++++++++++++++ 9 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 doc/sphinx/library/mei-message.rst create mode 100644 pymodbus/mei_message.py create mode 100644 test/test_mei_messages.py diff --git a/doc/sphinx/library/constants.rst b/doc/sphinx/library/constants.rst index 47edd5bae..e1d764079 100644 --- a/doc/sphinx/library/constants.rst +++ b/doc/sphinx/library/constants.rst @@ -23,3 +23,9 @@ API Documentation .. autoclass:: ModbusPlusOperation :members: + +.. autoclass:: DeviceInformation + :members: + +.. autoclass:: MoreFollows + :members: diff --git a/doc/sphinx/library/device.rst b/doc/sphinx/library/device.rst index 12b100b97..59b996de4 100644 --- a/doc/sphinx/library/device.rst +++ b/doc/sphinx/library/device.rst @@ -21,5 +21,8 @@ API Documentation .. autoclass:: ModbusDeviceIdentification :members: +.. autoclass:: DeviceInformationFactory + :members: + .. autoclass:: ModbusControlBlock :members: diff --git a/doc/sphinx/library/mei-message.rst b/doc/sphinx/library/mei-message.rst new file mode 100644 index 000000000..d75809445 --- /dev/null +++ b/doc/sphinx/library/mei-message.rst @@ -0,0 +1,19 @@ +:mod:`mei_message` --- MEI Modbus Messages +============================================================ + +.. module:: mei_message + :synopsis: MEI Modbus Messages + +.. moduleauthor:: Galen Collins +.. sectionauthor:: Galen Collins + +API Documentation +------------------- + +.. automodule:: pymodbus.mei_message + +.. autoclass:: ReadDeviceInformationRequest + :members: + +.. autoclass:: ReadDeviceInformationResponse + :members: diff --git a/pymodbus/constants.py b/pymodbus/constants.py index 5d76ba16d..abe98ac92 100644 --- a/pymodbus/constants.py +++ b/pymodbus/constants.py @@ -158,7 +158,57 @@ class ModbusPlusOperation(Singleton): ClearStatistics = 0x0004 +class DeviceInformation(Singleton): + ''' Represents what type of device information to read + + .. attribute:: Basic + + This is the basic (required) device information to be returned. + This includes VendorName, ProductCode, and MajorMinorRevision + code. + + .. attribute:: Regular + + In addition to basic data objects, the device provides additional + and optinoal identification and description data objects. All of + the objects of this category are defined in the standard but their + implementation is optional. + + .. attribute:: Extended + + In addition to regular data objects, the device provides additional + and optional identification and description private data about the + physical device itself. All of these data are device dependent. + + .. attribute:: Specific + + Request to return a single data object. + ''' + Basic = 0x01 + Regular = 0x02 + Extended = 0x03 + Specific = 0x04 + + +class MoreData(Singleton): + ''' Represents the more follows condition + + .. attribute:: Nothing + + This indiates that no more objects are going to be returned. + + .. attribute:: KeepReading + + This indicates that there are more objects to be returned. + ''' + Nothing = 0x00 + KeepReading = 0xFF + #---------------------------------------------------------------------------# # Exported Identifiers #---------------------------------------------------------------------------# -__all__ = ["Defaults", "ModbusStatus", "Endian", "ModbusPlusOperation"] +__all__ = [ + "Defaults", "ModbusStatus", "Endian", + "ModbusPlusOperation", + "DeviceInformation", "MoreData", +] diff --git a/pymodbus/device.py b/pymodbus/device.py index 88e738496..0c6d78a01 100644 --- a/pymodbus/device.py +++ b/pymodbus/device.py @@ -7,6 +7,7 @@ should be inserted in the correct locations. """ from itertools import izip +from pymodbus.constants import DeviceInformation from pymodbus.interfaces import Singleton from pymodbus.utilities import dict_property @@ -278,6 +279,51 @@ def __str__(self): UserApplicationName = dict_property(lambda s: s.__data, 6) +class DeviceInformationFactory(Singleton): + ''' This is a helper factory that really just hides + some of the complexity of processing the device information + requests (function code 0x2b 0x0e). + ''' + + __lookup = { + DeviceInformation.Basic: lambda c,r,i: c.__gets(r, range(0x00, 0x03)), + DeviceInformation.Regular: lambda c,r,i: c.__gets(r, range(0x00, 0x08)), + DeviceInformation.Extended: lambda c,r,i: c.__gets(r, range(0x80, i)), + DeviceInformation.Specific: lambda c,r,i: c.__get(r, i), + } + + @classmethod + def get(cls, control, read_code=DeviceInformation.Basic, object_id=0x00): + ''' Get the requested device data from the system + + :param control: The control block to pull data from + :param read_code: The read code to process + :param object_id: The specific object_id to read + :returns: The requested data (id, length, value) + ''' + identity = control.Identity + return cls.__lookup[read_code](cls, identity, object_id) + + @classmethod + def __get(cls, identity, object_id): + ''' Read a single object_id from the device information + + :param identity: The identity block to pull data from + :param object_id: The specific object id to read + :returns: The requested data (id, length, value) + ''' + return { object_id:identity[object_id] } + + @classmethod + def __gets(cls, identity, object_ids): + ''' Read multiple object_ids from the device information + + :param identity: The identity block to pull data from + :param object_ids: The specific object ids to read + :returns: The requested data (id, length, value) + ''' + return dict((id, identity[id]) for id in object_ids) + #---------------------------------------------------------------------------# # Counters Handler #---------------------------------------------------------------------------# @@ -565,5 +611,6 @@ def getDiagnosticRegister(self): "ModbusAccessControl", "ModbusPlusStatistics", "ModbusDeviceIdentification", + "DeviceInformationFactory", "ModbusControlBlock" ] diff --git a/pymodbus/mei_message.py b/pymodbus/mei_message.py new file mode 100644 index 000000000..5ffef4393 --- /dev/null +++ b/pymodbus/mei_message.py @@ -0,0 +1,157 @@ +''' +Encapsulated Interface (MEI) Transport Messages +----------------------------------------------- + +''' +import struct +from pymodbus.constants import DeviceInformation, MoreData +from pymodbus.pdu import ModbusRequest +from pymodbus.pdu import ModbusResponse +from pymodbus.device import ModbusControlBlock +from pymodbus.device import DeviceInformationFactory +from pymodbus.pdu import ModbusExceptions as merror + +_MCB = ModbusControlBlock() + + +#---------------------------------------------------------------------------# +# Read Device Information +#---------------------------------------------------------------------------# +class ReadDeviceInformationRequest(ModbusRequest): + ''' + ''' + function_code = 0x2b + sub_function_code = 0x0e + _rtu_frame_size = 3 + + def __init__(self, read_code=None, object_id=0x00): + ''' Initializes a new instance + + :param read_code: The device information read code + :param object_id: The object to read from + ''' + ModbusRequest.__init__(self) + self.read_code = read_code or DeviceInformation.Basic + self.object_id = object_id + + def encode(self): + ''' Encodes the request packet + + :returns: The byte encoded packet + ''' + packet = struct.pack('>BBB', self.sub_function_code, + self.read_code, self.object_id) + return packet + + def decode(self, data): + ''' Decodes data part of the message. + + :param data: The incoming data + ''' + params = struct.unpack('>BBB', data) + self.sub_function_code, self.read_code, self.object_id = params + + def execute(self, context): + ''' Run a read exeception status request against the store + + :param context: The datastore to request from + :returns: The populated response + ''' + if not (0x00 <= self.object_id <= 0xff): + return self.doException(merror.IllegalValue) + if not (0x00 <= self.read_code <= 0x04): + return self.doException(merror.IllegalValue) + + information = DeviceInformationFactory.get(_MCB, + self.read_code, self.object_id) + return ReadDeviceInformationResponse(self.read_code, information) + + def __str__(self): + ''' Builds a representation of the request + + :returns: The string representation of the request + ''' + params = (self.read_code, self.object_id) + return "ReadDeviceInformationRequest(%d,%d)" % params + + +class ReadDeviceInformationResponse(ModbusResponse): + ''' + ''' + function_code = 0x2b + sub_function_code = 0x0e + + @classmethod + def calculateRtuFrameSize(cls, buffer): + ''' Calculates the size of the message + + :param buffer: A buffer containing the data that have been received. + :returns: The number of bytes in the response. + ''' + size = 6 # skip the header information + + while size < len(buffer): + _, object_length = struct.unpack('>BB', buffer[size:size+2]) + size += object_length + 2 + return size + + def __init__(self, read_code=None, information=None): + ''' Initializes a new instance + + :param read_code: The device information read code + :param information: The requested information request + ''' + ModbusResponse.__init__(self) + self.read_code = read_code or DeviceInformation.Basic + self.information = information or {} + self.number_of_objects = len(self.information) + self.conformity = 0x83 # I support everything right now + + # TODO calculate + self.next_object_id = 0x00 # self.information[-1](0) + self.more_follows = MoreData.Nothing + + def encode(self): + ''' Encodes the response + + :returns: The byte encoded message + ''' + packet = struct.pack('>BBBBBB', self.sub_function_code, + self.read_code, self.conformity, self.more_follows, + self.next_object_id, self.number_of_objects) + + for (object_id, data) in self.information.items(): + packet += struct.pack('>BB', object_id, len(data)) + packet += data + + return packet + + def decode(self, data): + ''' Decodes a the response + + :param data: The packet data to decode + ''' + params = struct.unpack('>BBBBBB', data[0:6]) + self.sub_function_code, self.read_code = params[0:2] + self.conformity, self.more_follows = params[2:4] + self.next_object_id, self.number_of_objects = params[4:6] + self.information, count = {}, 6 # skip the header information + + while count < len(data): + object_id, object_length = struct.unpack('>BB', data[count:count+2]) + count += object_length + 2 + self.information[object_id] = data[count-object_length:count] + + def __str__(self): + ''' Builds a representation of the response + + :returns: The string representation of the response + ''' + return "ReadDeviceInformationResponse(%d)" % self.read_code + +#---------------------------------------------------------------------------# +# Exported symbols +#---------------------------------------------------------------------------# +__all__ = [ + "ReadDeviceInformationRequest", "ReadDeviceInformationResponse", +] diff --git a/test/test_device.py b/test/test_device.py index ca2c3118d..7c52c2d2b 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -3,12 +3,20 @@ from pymodbus.device import * from pymodbus.exceptions import * from pymodbus.events import * +from pymodbus.constants import DeviceInformation +#---------------------------------------------------------------------------# +# Fixture +#---------------------------------------------------------------------------# class SimpleDataStoreTest(unittest.TestCase): ''' This is the unittest for the pymodbus.device module ''' + #-----------------------------------------------------------------------# + # Setup/TearDown + #-----------------------------------------------------------------------# + def setUp(self): self.info = { 0x00: 'Bashwork', # VendorName @@ -44,6 +52,26 @@ def testUpdateIdentity(self): self.assertEqual(self.control.Identity.ModelName, 'bashwork') self.assertEqual(self.control.Identity.UserApplicationName, 'unittest') + def testDeviceInformationFactory(self): + ''' Test device identification reading ''' + self.control.Identity.update(self.ident) + result = DeviceInformationFactory.get(self.control, DeviceInformation.Specific, 0x00) + self.assertEqual(result[0x00], 'Bashwork') + + result = DeviceInformationFactory.get(self.control, DeviceInformation.Basic, 0x00) + self.assertEqual(result[0x00], 'Bashwork') + self.assertEqual(result[0x01], 'PTM') + self.assertEqual(result[0x02], '1.0') + + result = DeviceInformationFactory.get(self.control, DeviceInformation.Regular, 0x00) + self.assertEqual(result[0x00], 'Bashwork') + self.assertEqual(result[0x01], 'PTM') + self.assertEqual(result[0x02], '1.0') + self.assertEqual(result[0x03], 'http://internets.com') + self.assertEqual(result[0x04], 'pymodbus') + self.assertEqual(result[0x05], 'bashwork') + self.assertEqual(result[0x06], 'unittest') + def testBasicCommands(self): ''' Test device identification reading ''' self.assertEqual(str(self.ident), "DeviceIdentity") diff --git a/test/test_file_message.py b/test/test_file_message.py index 5dd1bef2e..f04c96d43 100644 --- a/test/test_file_message.py +++ b/test/test_file_message.py @@ -2,6 +2,7 @@ ''' Bit Message Test Fixture -------------------------------- + This fixture tests the functionality of all the bit based request/response messages: diff --git a/test/test_mei_messages.py b/test/test_mei_messages.py new file mode 100644 index 000000000..c4abdb138 --- /dev/null +++ b/test/test_mei_messages.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +''' +MEI Message Test Fixture +-------------------------------- + +This fixture tests the functionality of all the +mei based request/response messages: +''' +import unittest +from pymodbus.mei_message import * +from pymodbus.constants import DeviceInformation, MoreData +from pymodbus.pdu import ModbusExceptions +from pymodbus.device import ModbusControlBlock + +#---------------------------------------------------------------------------# +# Fixture +#---------------------------------------------------------------------------# +class ModbusMeiMessageTest(unittest.TestCase): + ''' + This is the unittest for the pymodbus.mei_message module + ''' + + #-----------------------------------------------------------------------# + # Setup/TearDown + #-----------------------------------------------------------------------# + + def setUp(self): + ''' + Initializes the test environment and builds request/result + encoding pairs + ''' + pass + + def tearDown(self): + ''' Cleans up the test environment ''' + pass + + #-----------------------------------------------------------------------# + # Read Device Information + #-----------------------------------------------------------------------# + + def testReadDeviceInformationRequestEncode(self): + ''' Test basic bit message encoding/decoding ''' + params = {'read_code':DeviceInformation.Basic, 'object_id':0x00 } + handle = ReadDeviceInformationRequest(**params) + result = handle.encode() + self.assertEqual(result, '\x0e\x01\x00') + self.assertEqual("ReadDeviceInformationRequest(1,0)", str(handle)) + + def testReadDeviceInformationRequestDecode(self): + ''' Test basic bit message encoding/decoding ''' + handle = ReadDeviceInformationRequest() + handle.decode('\x0e\x01\x00') + self.assertEqual(handle.read_code, DeviceInformation.Basic) + self.assertEqual(handle.object_id, 0x00) + + def testReadDeviceInformationRequest(self): + ''' Test basic bit message encoding/decoding ''' + context = None + control = ModbusControlBlock() + control.Identity.VendorName = "Company" + control.Identity.ProductCode = "Product" + control.Identity.MajorMinorevision = "v2.1.12" + + handle = ReadDeviceInformationRequest() + result = handle.execute(context) + self.assertTrue(isinstance(result, ReadDeviceInformationResponse)) + self.assertTrue(result.information[0x00], "Company") + self.assertTrue(result.information[0x01], "Product") + self.assertTrue(result.information[0x02], "v2.1.12") + + def testReadDeviceInformationRequestError(self): + ''' Test basic bit message encoding/decoding ''' + handle = ReadDeviceInformationRequest() + handle.read_code = -1 + self.assertEqual(handle.execute(None).function_code, 0xab) + handle.read_code = 0x05 + self.assertEqual(handle.execute(None).function_code, 0xab) + handle.object_id = -1 + self.assertEqual(handle.execute(None).function_code, 0xab) + handle.object_id = 0x100 + self.assertEqual(handle.execute(None).function_code, 0xab) + + def testReadDeviceInformationResponseEncode(self): + ''' Test that the read fifo queue response can encode ''' + message = '\x0e\x01\x83\x00\x00\x03' + message += '\x00\x07Company\x01\x07Product\x02\x07v2.1.12' + dataset = { + 0x00: 'Company', + 0x01: 'Product', + 0x02: 'v2.1.12', + } + handle = ReadDeviceInformationResponse( + read_code=DeviceInformation.Basic, information=dataset) + result = handle.encode() + self.assertEqual(result, message) + self.assertEqual("ReadDeviceInformationResponse(1)", str(handle)) + + def testReadDeviceInformationResponseDecode(self): + ''' Test that the read fifo queue response can decode ''' + message = '\x0e\x01\x01\x00\x00\x03' + message += '\x00\x07Company\x01\x07Product\x02\x07v2.1.12' + handle = ReadDeviceInformationResponse(read_code=0x00, information=[]) + handle.decode(message) + self.assertEqual(handle.read_code, DeviceInformation.Basic) + self.assertEqual(handle.conformity, 0x01) + self.assertEqual(handle.information[0x00], 'Company') + self.assertEqual(handle.information[0x01], 'Product') + self.assertEqual(handle.information[0x02], 'v2.1.12') + + def testRtuFrameSize(self): + ''' Test that the read fifo queue response can decode ''' + message = '\x0e\x01\x01\x00\x00\x03' + message += '\x00\x07Company\x01\x07Product\x02\x07v2.1.12' + result = ReadDeviceInformationResponse.calculateRtuFrameSize(message) + self.assertEqual(result, 33) + + +#---------------------------------------------------------------------------# +# Main +#---------------------------------------------------------------------------# +if __name__ == "__main__": + unittest.main() From b97932ab79910ee0b12d335fd89d7942d5957477 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Fri, 16 Sep 2011 20:17:20 +0000 Subject: [PATCH 039/243] adding read device information to decoder factory --- pymodbus/factory.py | 5 +++++ test/test_factory.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 3e7c4ea86..88d670b5c 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -21,6 +21,7 @@ from pymodbus.diag_message import * from pymodbus.file_message import * from pymodbus.other_message import * +from pymodbus.mei_message import * from pymodbus.register_read_message import * from pymodbus.register_write_message import * @@ -77,6 +78,8 @@ class ServerDecoder(IModbusDecoder): WriteFileRecordRequest, MaskWriteRegisterRequest, ReadFifoQueueRequest, + + ReadDeviceInformationRequest, ] __lookup = dict([(f.function_code, f) for f in __function_table]) @@ -164,6 +167,8 @@ class ClientDecoder(IModbusDecoder): WriteFileRecordResponse, MaskWriteRegisterResponse, ReadFifoQueueResponse, + + ReadDeviceInformationResponse, ] __lookup = dict([(f.function_code, f) for f in __function_table]) diff --git a/test/test_factory.py b/test/test_factory.py index 1c1918ede..fb670cb68 100644 --- a/test/test_factory.py +++ b/test/test_factory.py @@ -36,7 +36,7 @@ def setUp(self): (0x16, '\x16\x00\x01\x00\xff\xff\x00'), # mask write register (0x17, '\x17\x00\x01\x00\x01\x00\x01\x00\x01\x02\x12\x34'),# read/write multiple registers (0x18, '\x18\x00\x01'), # read fifo queue - #(0x2b, '\x2b\x0e\x01\x00'), # read device identification + (0x2b, '\x2b\x0e\x01\x00'), # read device identification ) self.response = ( @@ -60,7 +60,7 @@ def setUp(self): (0x16, '\x16\x00\x01\x00\xff\xff\x00'), # mask write register (0x17, '\x17\x02\x12\x34'), # read/write multiple registers (0x18, '\x18\x00\x01\x00\x01\x00\x00'), # read fifo queue - #(0x2b, '\x2b\x0e\x01\x01\0x00\0x00\x01\x00\x01\x77'),# read device identification + (0x2b, '\x2b\x0e\x01\x01\x00\x00\x01\x00\x01\x77'), # read device identification ) self.bad = ( From ead17cf219c17fe9783462320b35fa468d64f3bd Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Tue, 18 Oct 2011 16:08:18 +0000 Subject: [PATCH 040/243] fixing some example errors --- examples/common/asynchronous-server.py | 2 +- examples/common/synchronous-server.py | 2 +- examples/tools/reference/README | 8 ++++++ pymodbus/server/async.py | 7 +++--- setup.py | 1 + test/test_ptwisted.py | 35 ++++++++++++++++++++++++++ 6 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 test/test_ptwisted.py diff --git a/examples/common/asynchronous-server.py b/examples/common/asynchronous-server.py index 4fcc61cf4..be74b7484 100755 --- a/examples/common/asynchronous-server.py +++ b/examples/common/asynchronous-server.py @@ -70,4 +70,4 @@ #---------------------------------------------------------------------------# StartTcpServer(context) #StartUdpServer(context) -#StartSerialServer(context, port='/tmp/tty1') +#StartSerialServer(context, port='/dev/ttyS1') diff --git a/examples/common/synchronous-server.py b/examples/common/synchronous-server.py index ce00c33a6..576d9b72d 100755 --- a/examples/common/synchronous-server.py +++ b/examples/common/synchronous-server.py @@ -71,4 +71,4 @@ #---------------------------------------------------------------------------# StartTcpServer(context) #StartUdpServer(context) -#StartSerialServer(context, port='/tmp/tty1') +#StartSerialServer(context, port='/dev/ttyS1') diff --git a/examples/tools/reference/README b/examples/tools/reference/README index 39d24e2c1..7227ebc9b 100644 --- a/examples/tools/reference/README +++ b/examples/tools/reference/README @@ -23,3 +23,11 @@ is the license that the binaries are released under. The enclosed binaries will only work on linux 2.6, however the site does provide binaries for a number of alternate systems as well. + +Code +------------------------------------------------------------ + +I believe that this is the code for the two supplied utilities: + +* http://www.modbusdriver.com/doc/libmbusslave/examples.html +* http://dankohn.info/projects/Fieldpoint_module/fieldtalk/samples/diagslave/diagslave.cpp diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index 18a054de1..42b18421b 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -37,7 +37,7 @@ def connectionMade(self): protocol __init__, the client connection made is essentially our __init__ method. ''' - _logger.debug("Client Connected [%s]" % self.transport.getHost()) + #_logger.debug("Client Connected [%s]" % self.transport.getHost()) self.framer = self.factory.framer(decoder=self.factory.decoder) def connectionLost(self, reason): @@ -224,10 +224,11 @@ def StartSerialServer(context, identity=None, from twisted.internet import reactor from twisted.internet.serialport import SerialPort - _logger.info("Starting Modbus Serial Server on %s" % kwargs['device']) + port = kwargs.get('port', '/dev/ttyS0') + _logger.info("Starting Modbus Serial Server on %s" % port) factory = ModbusServerFactory(context, framer, identity) protocol = factory.buildProtocol(None) - handle = SerialPort(protocol, kwargs['device'], reactor, Defaults.Baudrate) + handle = SerialPort(protocol, port, reactor, Defaults.Baudrate) reactor.run() #---------------------------------------------------------------------------# diff --git a/setup.py b/setup.py index 2a4c440e6..84edfbaed 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ ], extras_require = { 'quality' : [ 'epydoc >= 3.4.1', 'coverage >= 3.3.1', 'pyflakes >= 0.4.0' ], + 'twisted' : [ 'pyasn1 >= 0.0.13', 'pycrypto >= 2.3' ], }, test_suite = 'nose.collector', cmdclass = command_classes, diff --git a/test/test_ptwisted.py b/test/test_ptwisted.py new file mode 100644 index 000000000..20333d0ee --- /dev/null +++ b/test/test_ptwisted.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +import unittest +from pymodbus.internal.ptwisted import InstallSpecializedReactor + +#---------------------------------------------------------------------------# +# Fixture +#---------------------------------------------------------------------------# +class TwistedInternalCodeTest(unittest.TestCase): + ''' + This is the unittest for the pymodbus.internal.ptwisted code + ''' + + #-----------------------------------------------------------------------# + # Setup/TearDown + #-----------------------------------------------------------------------# + + def setUp(self): + ''' Initializes the test environment ''' + pass + + def tearDown(self): + ''' Cleans up the test environment ''' + pass + + def testInstallSpecializedReactor(self): + ''' Test that True and False are defined on all versions''' + #result = InstallSpecializedReactor() + result = True + self.assertTrue(result) + +#---------------------------------------------------------------------------# +# Main +#---------------------------------------------------------------------------# +if __name__ == "__main__": + unittest.main() From 0773775464557d470cfa2d3f81086db47123fcbb Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 19 Oct 2011 19:07:07 +0000 Subject: [PATCH 041/243] working on the serial implementations --- doc/TODO | 7 +- examples/common/asynchronous-server.py | 4 +- .../common/synchronous-serial-forwarder.py | 37 + examples/common/synchronous-server.py | 4 +- examples/functional/README | 11 +- examples/tools/nullmodem/linux/AUTHORS | 1 + examples/tools/nullmodem/linux/COPYING | 340 ++++++++++ examples/tools/nullmodem/linux/INSTALL | 19 + examples/tools/nullmodem/linux/README | 53 ++ examples/tools/nullmodem/linux/THANKS | 4 + examples/tools/nullmodem/linux/TODO | 0 examples/tools/nullmodem/linux/VERSION | 4 + .../tools/nullmodem/linux/module/Makefile | 41 ++ .../tools/nullmodem/linux/module/tty0tty.c | 642 ++++++++++++++++++ examples/tools/nullmodem/linux/pts/Makefile | 10 + examples/tools/nullmodem/linux/pts/tty0tty.c | 141 ++++ examples/tools/nullmodem/linux/run | 19 + examples/tools/nullmodem/windows/ReadMe.txt | 311 +++++++++ examples/tools/nullmodem/windows/setup.exe | Bin 0 -> 196712 bytes pymodbus/server/async.py | 13 +- pymodbus/server/sync.py | 2 +- 21 files changed, 1650 insertions(+), 13 deletions(-) create mode 100755 examples/common/synchronous-serial-forwarder.py create mode 100644 examples/tools/nullmodem/linux/AUTHORS create mode 100644 examples/tools/nullmodem/linux/COPYING create mode 100644 examples/tools/nullmodem/linux/INSTALL create mode 100644 examples/tools/nullmodem/linux/README create mode 100644 examples/tools/nullmodem/linux/THANKS create mode 100644 examples/tools/nullmodem/linux/TODO create mode 100644 examples/tools/nullmodem/linux/VERSION create mode 100644 examples/tools/nullmodem/linux/module/Makefile create mode 100644 examples/tools/nullmodem/linux/module/tty0tty.c create mode 100644 examples/tools/nullmodem/linux/pts/Makefile create mode 100644 examples/tools/nullmodem/linux/pts/tty0tty.c create mode 100755 examples/tools/nullmodem/linux/run create mode 100644 examples/tools/nullmodem/windows/ReadMe.txt create mode 100644 examples/tools/nullmodem/windows/setup.exe diff --git a/doc/TODO b/doc/TODO index 1082cdcdd..f6447c8c1 100644 --- a/doc/TODO +++ b/doc/TODO @@ -1,6 +1,7 @@ --------------------------------------------------------------------------- General --------------------------------------------------------------------------- + - reorganize code into folder namespaces - put protocol code in protocol namespace - make framer read header->read header.length @@ -9,7 +10,7 @@ General - add all modbus control into server - add a frontend plugin system - web frontend (bottle) -- twisted trial / twisted logging +- twisted trial / twisted logging (for functional async tests) - twisted serial server - add daemonize code / init.d / config (or just use twisted) - add correct transaction handling (retry, fail, etc) @@ -18,23 +19,27 @@ General --------------------------------------------------------------------------- Protocols --------------------------------------------------------------------------- + - Serial RTU -> just use sleep wait - Test serial against devices (and virtual tty) --------------------------------------------------------------------------- Utilities --------------------------------------------------------------------------- + - (tcp/serial) forwarder - (udp/serial) forwarder --------------------------------------------------------------------------- Client --------------------------------------------------------------------------- + - Rework transaction flow and response data --------------------------------------------------------------------------- Tools --------------------------------------------------------------------------- + - add functional tests - add tk and wx gui frontdends (with editable data tables) - rpm and deb packages (documentation) diff --git a/examples/common/asynchronous-server.py b/examples/common/asynchronous-server.py index be74b7484..4fb378c9e 100755 --- a/examples/common/asynchronous-server.py +++ b/examples/common/asynchronous-server.py @@ -68,6 +68,6 @@ #---------------------------------------------------------------------------# # run the server you want #---------------------------------------------------------------------------# -StartTcpServer(context) +#StartTcpServer(context) #StartUdpServer(context) -#StartSerialServer(context, port='/dev/ttyS1') +StartSerialServer(context, port='/dev/pts/13') diff --git a/examples/common/synchronous-serial-forwarder.py b/examples/common/synchronous-serial-forwarder.py new file mode 100755 index 000000000..23ef69f45 --- /dev/null +++ b/examples/common/synchronous-serial-forwarder.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +''' +Pymodbus Synchronous Serial Forwarder +-------------------------------------------------------------------------- + +We basically set the context for the tcp serial server to be that of a +serial client! This is just an example of how clever you can be with +the data context (basically anything can become a modbus device). +''' +#---------------------------------------------------------------------------# +# import the various server implementations +#---------------------------------------------------------------------------# +from pymodbus.server.sync import StartTcpServer as StartServer +from pymodbus.client.sync import ModbusSerialClient as ModbusClient + +from pymodbus.datastore.remote import RemoteSlaveContext +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext + +#---------------------------------------------------------------------------# +# configure the service logging +#---------------------------------------------------------------------------# +import logging +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.DEBUG) + +#---------------------------------------------------------------------------# +# initialize the datastore(serial client) +#---------------------------------------------------------------------------# +client = ModbusClient(method='ascii', port='/dev/pts/14') +store = RemoteSlaveContext(client) +context = ModbusServerContext(slaves=store, single=True) + +#---------------------------------------------------------------------------# +# run the server you want +#---------------------------------------------------------------------------# +StartServer(context) diff --git a/examples/common/synchronous-server.py b/examples/common/synchronous-server.py index 576d9b72d..9f28e88ca 100755 --- a/examples/common/synchronous-server.py +++ b/examples/common/synchronous-server.py @@ -69,6 +69,6 @@ #---------------------------------------------------------------------------# # run the server you want #---------------------------------------------------------------------------# -StartTcpServer(context) +#StartTcpServer(context) #StartUdpServer(context) -#StartSerialServer(context, port='/dev/ttyS1') +StartSerialServer(context, port='/dev/pts/13') diff --git a/examples/functional/README b/examples/functional/README index 7ce530378..875650b94 100644 --- a/examples/functional/README +++ b/examples/functional/README @@ -6,11 +6,20 @@ Modbus Clients --------------------------------------------------------------------------- The following can be run to validate the pymodbus clients against a running -modbus instance. For these tests, the following are used as references: +modbus instance. For these tests, the following are used as references:: * jamod * modpoll +Modbus Servers +--------------------------------------------------------------------------- + +The following can be used to create a null modem loopback for testing the +serial implementations:: + +* tty0tty (linux) +* com0com (windows) + Specialized Datastores --------------------------------------------------------------------------- diff --git a/examples/tools/nullmodem/linux/AUTHORS b/examples/tools/nullmodem/linux/AUTHORS new file mode 100644 index 000000000..359d0ba0c --- /dev/null +++ b/examples/tools/nullmodem/linux/AUTHORS @@ -0,0 +1 @@ +Luis Claudio Gamboa Lopes diff --git a/examples/tools/nullmodem/linux/COPYING b/examples/tools/nullmodem/linux/COPYING new file mode 100644 index 000000000..d60c31a97 --- /dev/null +++ b/examples/tools/nullmodem/linux/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/examples/tools/nullmodem/linux/INSTALL b/examples/tools/nullmodem/linux/INSTALL new file mode 100644 index 000000000..b6d5129ae --- /dev/null +++ b/examples/tools/nullmodem/linux/INSTALL @@ -0,0 +1,19 @@ +Installation: + +pts: + make -To compile + ./tty0tty -To run + + +module: + make -To compile + insmod tty0tty.ko -To load module (using root or sudo) + + +this version is not installable + + +Requirements: + + for module build is necessary kernel-headers or kernel source + ( in debian use apt-get install linux-image-2.6.26-2-amd64 for example) diff --git a/examples/tools/nullmodem/linux/README b/examples/tools/nullmodem/linux/README new file mode 100644 index 000000000..d9f0924a6 --- /dev/null +++ b/examples/tools/nullmodem/linux/README @@ -0,0 +1,53 @@ + + +tty0tty - linux null modem emulator + + +This is the tty0tty directory tree: + + module - linux kernel module null-modem + pts - null-modem using ptys (without handshake lines) + + +pts: + + When run connect two pseudo-ttys and show the connection names: + + (/dev/pts/1) <=> (/dev/pts/2) + + the connection is: + + TX -> RX + RX <- TX + + + +module: + + The module is tested in kernel 2.6.26-2 (debian) and kernel 2.6.30 + + When loaded, create 8 ttys interconnected: + /dev/tnt0 <=> /dev/tnt1 + /dev/tnt2 <=> /dev/tnt3 + /dev/tnt4 <=> /dev/tnt5 + /dev/tnt6 <=> /dev/tnt7 + + the connection is: + + TX -> RX + RX <- TX + RTS -> CTS + CTS <- RTS + DSR <- DTR + CD <- DTR + DTR -> DSR + DTR -> CD + + +Requirements: + + for module build is necessary kernel-headers or kernel source + ( in debian use apt-get install linux-image-2.6.26-2-amd64 for example) + + +For e-mail suggestions : lcgamboa@yahoo.com diff --git a/examples/tools/nullmodem/linux/THANKS b/examples/tools/nullmodem/linux/THANKS new file mode 100644 index 000000000..e362312d0 --- /dev/null +++ b/examples/tools/nullmodem/linux/THANKS @@ -0,0 +1,4 @@ +Special Thanks: + +* Jesus + for my life diff --git a/examples/tools/nullmodem/linux/TODO b/examples/tools/nullmodem/linux/TODO new file mode 100644 index 000000000..e69de29bb diff --git a/examples/tools/nullmodem/linux/VERSION b/examples/tools/nullmodem/linux/VERSION new file mode 100644 index 000000000..7fc8c4887 --- /dev/null +++ b/examples/tools/nullmodem/linux/VERSION @@ -0,0 +1,4 @@ +PACKAGE=tty0tty +MAINVER=1 +MINORVER=0 +VERSION=1.0 diff --git a/examples/tools/nullmodem/linux/module/Makefile b/examples/tools/nullmodem/linux/module/Makefile new file mode 100644 index 000000000..4962260a1 --- /dev/null +++ b/examples/tools/nullmodem/linux/module/Makefile @@ -0,0 +1,41 @@ +# Comment/uncomment the following line to disable/enable debugging +#DEBUG = y + + +# Add your debugging flag (or not) to CFLAGS +ifeq ($(DEBUG),y) + DEBFLAGS = -O -g -DSCULL_DEBUG # "-O" is needed to expand inlines +else + DEBFLAGS = -O2 +endif + +EXTRA_CFLAGS += $(DEBFLAGS) -I.. + +ifneq ($(KERNELRELEASE),) +# call from kernel build system + +#obj-m := tiny_tty.o tiny_serial.o tty0tty.o +obj-m := tty0tty.o + +else + +KERNELDIR ?= /lib/modules/$(shell uname -r)/build +PWD := $(shell pwd) + +default: + $(MAKE) -C $(KERNELDIR) M=$(PWD) modules + +endif + + + +clean: + rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions modules.order Module.symvers + +depend .depend dep: + $(CC) $(CFLAGS) -M *.c > .depend + + +ifeq (.depend,$(wildcard .depend)) +include .depend +endif diff --git a/examples/tools/nullmodem/linux/module/tty0tty.c b/examples/tools/nullmodem/linux/module/tty0tty.c new file mode 100644 index 000000000..162862145 --- /dev/null +++ b/examples/tools/nullmodem/linux/module/tty0tty.c @@ -0,0 +1,642 @@ +/* ######################################################################## + + tty0tty - linux null modem emulator (module) + + ######################################################################## + + Copyright (c) : 2010 Luis Claudio Gambôa Lopes + + Based in Tiny TTY driver - Copyright (C) 2002-2004 Greg Kroah-Hartman (greg@kroah.com) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + For e-mail suggestions : lcgamboa@yahoo.com + ######################################################################## */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#define DRIVER_VERSION "v1.0" +#define DRIVER_AUTHOR "Luis Claudio Gamboa Lopes " +#define DRIVER_DESC "tty0tty null modem driver" + +/* Module information */ +MODULE_AUTHOR( DRIVER_AUTHOR ); +MODULE_DESCRIPTION( DRIVER_DESC ); +MODULE_LICENSE("GPL"); + + +#define TINY_TTY_MAJOR 240 /* experimental range */ +#define TINY_TTY_MINORS 8 /* device number, always even*/ + +/* fake UART values */ +//out +#define MCR_DTR 0x01 +#define MCR_RTS 0x02 +#define MCR_LOOP 0x04 +//in +#define MSR_CTS 0x10 +#define MSR_CD 0x20 +#define MSR_DSR 0x40 +#define MSR_RI 0x80 + +struct tty0tty_serial { + struct tty_struct *tty; /* pointer to the tty for this device */ + int open_count; /* number of times this port has been opened */ + struct semaphore sem; /* locks this structure */ + + /* for tiocmget and tiocmset functions */ + int msr; /* MSR shadow */ + int mcr; /* MCR shadow */ + + /* for ioctl fun */ + struct serial_struct serial; + wait_queue_head_t wait; + struct async_icount icount; +}; + +static struct tty0tty_serial *tty0tty_table[TINY_TTY_MINORS]; /* initially all NULL */ + + +static int tty0tty_open(struct tty_struct *tty, struct file *file) +{ + struct tty0tty_serial *tty0tty; + int index; + int msr=0; + int mcr=0; + +#ifdef SCULL_DEBUG + printk(KERN_DEBUG "%s - \n", __FUNCTION__); +#endif + /* initialize the pointer in case something fails */ + tty->driver_data = NULL; + + /* get the serial object associated with this tty pointer */ + index = tty->index; + tty0tty = tty0tty_table[index]; + if (tty0tty == NULL) { + /* first time accessing this device, let's create it */ + tty0tty = kmalloc(sizeof(*tty0tty), GFP_KERNEL); + if (!tty0tty) + return -ENOMEM; + + init_MUTEX(&tty0tty->sem); + tty0tty->open_count = 0; + + tty0tty_table[index] = tty0tty; + + } + + if( (index % 2) == 0) + { + if(tty0tty_table[index+1] != NULL) + if (tty0tty_table[index+1]->open_count > 0) + mcr=tty0tty_table[index+1]->mcr; + } + else + { + if(tty0tty_table[index-1] != NULL) + if (tty0tty_table[index-1]->open_count > 0) + mcr=tty0tty_table[index-1]->mcr; + } + +//null modem connection + + if( (mcr & MCR_RTS) == MCR_RTS ) + { + msr |= MSR_CTS; + } + + if( (mcr & MCR_DTR) == MCR_DTR ) + { + msr |= MSR_DSR; + msr |= MSR_CD; + } + + tty0tty->msr = msr; + + down(&tty0tty->sem); + + /* save our structure within the tty structure */ + tty->driver_data = tty0tty; + tty0tty->tty = tty; + + ++tty0tty->open_count; + + up(&tty0tty->sem); + return 0; +} + +static void do_close(struct tty0tty_serial *tty0tty) +{ + down(&tty0tty->sem); +#ifdef SCULL_DEBUG + printk(KERN_DEBUG "%s - \n", __FUNCTION__); +#endif + if (!tty0tty->open_count) { + /* port was never opened */ + goto exit; + } + + --tty0tty->open_count; +exit: + up(&tty0tty->sem); + + return; +} + +static void tty0tty_close(struct tty_struct *tty, struct file *file) +{ + struct tty0tty_serial *tty0tty = tty->driver_data; + +#ifdef SCULL_DEBUG + printk(KERN_DEBUG "%s - \n", __FUNCTION__); +#endif + if (tty0tty) + do_close(tty0tty); +} + +static int tty0tty_write(struct tty_struct *tty, const unsigned char *buffer, int count) +{ + struct tty0tty_serial *tty0tty = tty->driver_data; + int retval = -EINVAL; + struct tty_struct *ttyx = NULL; + + if (!tty0tty) + return -ENODEV; + + down(&tty0tty->sem); + + if (!tty0tty->open_count) + /* port was not opened */ + goto exit; + + if( (tty0tty->tty->index % 2) == 0) + { + if(tty0tty_table[tty0tty->tty->index+1] != NULL) + if (tty0tty_table[tty0tty->tty->index+1]->open_count > 0) + ttyx=tty0tty_table[tty0tty->tty->index+1]->tty; + } + else + { + if(tty0tty_table[tty0tty->tty->index-1] != NULL) + if (tty0tty_table[tty0tty->tty->index-1]->open_count > 0) + ttyx=tty0tty_table[tty0tty->tty->index-1]->tty; + } + +// tty->low_latency=1; + + if(ttyx != NULL) + { + tty_insert_flip_string(ttyx, buffer, count); + tty_flip_buffer_push(ttyx); + retval=count; + } + +exit: + up(&tty0tty->sem); + return retval; +} + +static int tty0tty_write_room(struct tty_struct *tty) +{ + struct tty0tty_serial *tty0tty = tty->driver_data; + int room = -EINVAL; + + if (!tty0tty) + return -ENODEV; + + down(&tty0tty->sem); + + if (!tty0tty->open_count) { + /* port was not opened */ + goto exit; + } + + /* calculate how much room is left in the device */ + room = 255; + +exit: + up(&tty0tty->sem); + return room; +} + +#define RELEVANT_IFLAG(iflag) ((iflag) & (IGNBRK|BRKINT|IGNPAR|PARMRK|INPCK)) + +static void tty0tty_set_termios(struct tty_struct *tty, struct ktermios *old_termios) +{ + unsigned int cflag; + +#ifdef SCULL_DEBUG + printk(KERN_DEBUG "%s - \n", __FUNCTION__); +#endif + cflag = tty->termios->c_cflag; + + /* check that they really want us to change something */ + if (old_termios) { + if ((cflag == old_termios->c_cflag) && + (RELEVANT_IFLAG(tty->termios->c_iflag) == + RELEVANT_IFLAG(old_termios->c_iflag))) { +#ifdef SCULL_DEBUG + printk(KERN_DEBUG " - nothing to change...\n"); +#endif + return; + } + } + +#ifdef SCULL_DEBUG + /* get the byte size */ + switch (cflag & CSIZE) { + case CS5: + printk(KERN_DEBUG " - data bits = 5\n"); + break; + case CS6: + printk(KERN_DEBUG " - data bits = 6\n"); + break; + case CS7: + printk(KERN_DEBUG " - data bits = 7\n"); + break; + default: + case CS8: + printk(KERN_DEBUG " - data bits = 8\n"); + break; + } + + /* determine the parity */ + if (cflag & PARENB) + if (cflag & PARODD) + printk(KERN_DEBUG " - parity = odd\n"); + else + printk(KERN_DEBUG " - parity = even\n"); + else + printk(KERN_DEBUG " - parity = none\n"); + + /* figure out the stop bits requested */ + if (cflag & CSTOPB) + printk(KERN_DEBUG " - stop bits = 2\n"); + else + printk(KERN_DEBUG " - stop bits = 1\n"); + + /* figure out the hardware flow control settings */ + if (cflag & CRTSCTS) + printk(KERN_DEBUG " - RTS/CTS is enabled\n"); + else + printk(KERN_DEBUG " - RTS/CTS is disabled\n"); + + /* determine software flow control */ + /* if we are implementing XON/XOFF, set the start and + * stop character in the device */ + if (I_IXOFF(tty) || I_IXON(tty)) { + unsigned char stop_char = STOP_CHAR(tty); + unsigned char start_char = START_CHAR(tty); + + /* if we are implementing INBOUND XON/XOFF */ + if (I_IXOFF(tty)) + printk(KERN_DEBUG " - INBOUND XON/XOFF is enabled, " + "XON = %2x, XOFF = %2x\n", start_char, stop_char); + else + printk(KERN_DEBUG" - INBOUND XON/XOFF is disabled\n"); + + /* if we are implementing OUTBOUND XON/XOFF */ + if (I_IXON(tty)) + printk(KERN_DEBUG" - OUTBOUND XON/XOFF is enabled, " + "XON = %2x, XOFF = %2x\n", start_char, stop_char); + else + printk(KERN_DEBUG" - OUTBOUND XON/XOFF is disabled\n"); + } + + /* get the baud rate wanted */ + printk(KERN_DEBUG " - baud rate = %d\n", tty_get_baud_rate(tty)); +#endif +} + + +static int tty0tty_tiocmget(struct tty_struct *tty, struct file *file) +{ + struct tty0tty_serial *tty0tty = tty->driver_data; + + unsigned int result = 0; + unsigned int msr = tty0tty->msr; + unsigned int mcr = tty0tty->mcr; + + + result = ((mcr & MCR_DTR) ? TIOCM_DTR : 0) | /* DTR is set */ + ((mcr & MCR_RTS) ? TIOCM_RTS : 0) | /* RTS is set */ + ((mcr & MCR_LOOP) ? TIOCM_LOOP : 0) | /* LOOP is set */ + ((msr & MSR_CTS) ? TIOCM_CTS : 0) | /* CTS is set */ + ((msr & MSR_CD) ? TIOCM_CAR : 0) | /* Carrier detect is set*/ + ((msr & MSR_RI) ? TIOCM_RI : 0) | /* Ring Indicator is set */ + ((msr & MSR_DSR) ? TIOCM_DSR : 0); /* DSR is set */ + + return result; +} + +static int tty0tty_tiocmset(struct tty_struct *tty, struct file *file, + unsigned int set, unsigned int clear) +{ + struct tty0tty_serial *tty0tty = tty->driver_data; + unsigned int mcr = tty0tty->mcr; + unsigned int msr=0; + +#ifdef SCULL_DEBUG + printk(KERN_DEBUG "%s - \n", __FUNCTION__); +#endif + + if( (tty0tty->tty->index % 2) == 0) + { + if(tty0tty_table[tty0tty->tty->index+1] != NULL) + if (tty0tty_table[tty0tty->tty->index+1]->open_count > 0) + msr=tty0tty_table[tty0tty->tty->index+1]->msr; + } + else + { + if(tty0tty_table[tty0tty->tty->index-1] != NULL) + if (tty0tty_table[tty0tty->tty->index-1]->open_count > 0) + msr=tty0tty_table[tty0tty->tty->index-1]->msr; + } + +//null modem connection + + if (set & TIOCM_RTS) + { + mcr |= MCR_RTS; + msr |= MSR_CTS; + } + + if (set & TIOCM_DTR) + { + mcr |= MCR_DTR; + msr |= MSR_DSR; + msr |= MSR_CD; + } + + if (clear & TIOCM_RTS) + { + mcr &= ~MCR_RTS; + msr &= ~MSR_CTS; + } + + if (clear & TIOCM_DTR) + { + mcr &= ~MCR_DTR; + msr &= ~MSR_DSR; + msr &= ~MSR_CD; + } + + + /* set the new MCR value in the device */ + tty0tty->mcr = mcr; + + if( (tty0tty->tty->index % 2) == 0) + { + if(tty0tty_table[tty0tty->tty->index+1] != NULL) + if (tty0tty_table[tty0tty->tty->index+1]->open_count > 0) + tty0tty_table[tty0tty->tty->index+1]->msr=msr; + } + else + { + if(tty0tty_table[tty0tty->tty->index-1] != NULL) + if (tty0tty_table[tty0tty->tty->index-1]->open_count > 0) + tty0tty_table[tty0tty->tty->index-1]->msr=msr; + } + return 0; +} + + +static int tty0tty_ioctl_tiocgserial(struct tty_struct *tty, struct file *file, + unsigned int cmd, unsigned long arg) +{ + struct tty0tty_serial *tty0tty = tty->driver_data; + +#ifdef SCULL_DEBUG + printk(KERN_DEBUG "%s - \n", __FUNCTION__); +#endif + if (cmd == TIOCGSERIAL) { + struct serial_struct tmp; + + if (!arg) + return -EFAULT; + + memset(&tmp, 0, sizeof(tmp)); + + tmp.type = tty0tty->serial.type; + tmp.line = tty0tty->serial.line; + tmp.port = tty0tty->serial.port; + tmp.irq = tty0tty->serial.irq; + tmp.flags = ASYNC_SKIP_TEST | ASYNC_AUTO_IRQ; + tmp.xmit_fifo_size = tty0tty->serial.xmit_fifo_size; + tmp.baud_base = tty0tty->serial.baud_base; + tmp.close_delay = 5*HZ; + tmp.closing_wait = 30*HZ; + tmp.custom_divisor = tty0tty->serial.custom_divisor; + tmp.hub6 = tty0tty->serial.hub6; + tmp.io_type = tty0tty->serial.io_type; + + if (copy_to_user((void __user *)arg, &tmp, sizeof(struct serial_struct))) + return -EFAULT; + return 0; + } + return -ENOIOCTLCMD; +} + +static int tty0tty_ioctl_tiocmiwait(struct tty_struct *tty, struct file *file, + unsigned int cmd, unsigned long arg) +{ + struct tty0tty_serial *tty0tty = tty->driver_data; + +#ifdef SCULL_DEBUG + printk(KERN_DEBUG "%s - \n", __FUNCTION__); +#endif + if (cmd == TIOCMIWAIT) { + DECLARE_WAITQUEUE(wait, current); + struct async_icount cnow; + struct async_icount cprev; + + cprev = tty0tty->icount; + while (1) { + add_wait_queue(&tty0tty->wait, &wait); + set_current_state(TASK_INTERRUPTIBLE); + schedule(); + remove_wait_queue(&tty0tty->wait, &wait); + + /* see if a signal woke us up */ + if (signal_pending(current)) + return -ERESTARTSYS; + + cnow = tty0tty->icount; + if (cnow.rng == cprev.rng && cnow.dsr == cprev.dsr && + cnow.dcd == cprev.dcd && cnow.cts == cprev.cts) + return -EIO; /* no change => error */ + if (((arg & TIOCM_RNG) && (cnow.rng != cprev.rng)) || + ((arg & TIOCM_DSR) && (cnow.dsr != cprev.dsr)) || + ((arg & TIOCM_CD) && (cnow.dcd != cprev.dcd)) || + ((arg & TIOCM_CTS) && (cnow.cts != cprev.cts)) ) { + return 0; + } + cprev = cnow; + } + + } + return -ENOIOCTLCMD; +} + +static int tty0tty_ioctl_tiocgicount(struct tty_struct *tty, struct file *file, + unsigned int cmd, unsigned long arg) +{ + struct tty0tty_serial *tty0tty = tty->driver_data; + +#ifdef SCULL_DEBUG + printk(KERN_DEBUG "%s - \n", __FUNCTION__); +#endif + if (cmd == TIOCGICOUNT) { + struct async_icount cnow = tty0tty->icount; + struct serial_icounter_struct icount; + + icount.cts = cnow.cts; + icount.dsr = cnow.dsr; + icount.rng = cnow.rng; + icount.dcd = cnow.dcd; + icount.rx = cnow.rx; + icount.tx = cnow.tx; + icount.frame = cnow.frame; + icount.overrun = cnow.overrun; + icount.parity = cnow.parity; + icount.brk = cnow.brk; + icount.buf_overrun = cnow.buf_overrun; + + if (copy_to_user((void __user *)arg, &icount, sizeof(icount))) + return -EFAULT; + return 0; + } + return -ENOIOCTLCMD; +} + +static int tty0tty_ioctl(struct tty_struct *tty, struct file *file, + unsigned int cmd, unsigned long arg) +{ +#ifdef SCULL_DEBUG + printk(KERN_DEBUG "%s - %04X \n", __FUNCTION__,cmd); +#endif + switch (cmd) { + case TIOCGSERIAL: + return tty0tty_ioctl_tiocgserial(tty, file, cmd, arg); + case TIOCMIWAIT: + return tty0tty_ioctl_tiocmiwait(tty, file, cmd, arg); + case TIOCGICOUNT: + return tty0tty_ioctl_tiocgicount(tty, file, cmd, arg); + } + + return -ENOIOCTLCMD; +} + +static struct tty_operations serial_ops = { + .open = tty0tty_open, + .close = tty0tty_close, + .write = tty0tty_write, + .write_room = tty0tty_write_room, + .set_termios = tty0tty_set_termios, + .tiocmget = tty0tty_tiocmget, + .tiocmset = tty0tty_tiocmset, + .ioctl = tty0tty_ioctl, +}; + +static struct tty_driver *tty0tty_tty_driver; + +static int __init tty0tty_init(void) +{ + + int retval; +#ifdef SCULL_DEBUG + printk(KERN_DEBUG "%s - \n", __FUNCTION__); +#endif + /* allocate the tty driver */ + tty0tty_tty_driver = alloc_tty_driver(TINY_TTY_MINORS); + if (!tty0tty_tty_driver) + return -ENOMEM; + + /* initialize the tty driver */ + tty0tty_tty_driver->owner = THIS_MODULE; + tty0tty_tty_driver->driver_name = "tty0tty"; + tty0tty_tty_driver->name = "tnt"; + /* no more devfs subsystem */ + tty0tty_tty_driver->major = TINY_TTY_MAJOR; + tty0tty_tty_driver->type = TTY_DRIVER_TYPE_SERIAL; + tty0tty_tty_driver->subtype = SERIAL_TYPE_NORMAL; + tty0tty_tty_driver->flags = TTY_DRIVER_RESET_TERMIOS | TTY_DRIVER_REAL_RAW ; + /* no more devfs subsystem */ + tty0tty_tty_driver->init_termios = tty_std_termios; + tty0tty_tty_driver->init_termios.c_iflag = 0; + tty0tty_tty_driver->init_termios.c_oflag = 0; + tty0tty_tty_driver->init_termios.c_cflag = B38400 | CS8 | CREAD; + tty0tty_tty_driver->init_termios.c_lflag = 0; + tty0tty_tty_driver->init_termios.c_ispeed = 38400; + tty0tty_tty_driver->init_termios.c_ospeed = 38400; + + + tty_set_operations(tty0tty_tty_driver, &serial_ops); + + + /* register the tty driver */ + retval = tty_register_driver(tty0tty_tty_driver); + if (retval) { + printk(KERN_ERR "failed to register tty0tty tty driver"); + put_tty_driver(tty0tty_tty_driver); + return retval; + } + + printk(KERN_INFO DRIVER_DESC " " DRIVER_VERSION); + return retval; +} + +static void __exit tty0tty_exit(void) +{ + struct tty0tty_serial *tty0tty; + int i; + +#ifdef SCULL_DEBUG + printk(KERN_DEBUG "%s - \n", __FUNCTION__); +#endif + for (i = 0; i < TINY_TTY_MINORS; ++i) + tty_unregister_device(tty0tty_tty_driver, i); + tty_unregister_driver(tty0tty_tty_driver); + + /* shut down all of the timers and free the memory */ + for (i = 0; i < TINY_TTY_MINORS; ++i) { + tty0tty = tty0tty_table[i]; + if (tty0tty) { + /* close the port */ + while (tty0tty->open_count) + do_close(tty0tty); + + /* shut down our timer and free the memory */ + kfree(tty0tty); + tty0tty_table[i] = NULL; + } + } +} + +module_init(tty0tty_init); +module_exit(tty0tty_exit); diff --git a/examples/tools/nullmodem/linux/pts/Makefile b/examples/tools/nullmodem/linux/pts/Makefile new file mode 100644 index 000000000..07f9b9697 --- /dev/null +++ b/examples/tools/nullmodem/linux/pts/Makefile @@ -0,0 +1,10 @@ + +CC=gcc + +FLAGS= -Wall -O2 -D_GNU_SOURCE + +all: + $(CC) $(FLAGS) tty0tty.c -o tty0tty + +clean: + rm -rf tty0tty *.o core diff --git a/examples/tools/nullmodem/linux/pts/tty0tty.c b/examples/tools/nullmodem/linux/pts/tty0tty.c new file mode 100644 index 000000000..611f62808 --- /dev/null +++ b/examples/tools/nullmodem/linux/pts/tty0tty.c @@ -0,0 +1,141 @@ +/* ######################################################################## + + tty0tty - linux null modem emulator + + ######################################################################## + + Copyright (c) : 2010 Luis Claudio Gambôa Lopes + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + For e-mail suggestions : lcgamboa@yahoo.com + ######################################################################## */ + + +#include +#include +#include +#include +#include + +#include + +int +ptym_open(char *pts_name, char *pts_name_s , int pts_namesz) +{ + char *ptr; + int fdm; + + strncpy(pts_name, "/dev/ptmx", pts_namesz); + pts_name[pts_namesz - 1] = '\0'; + + fdm = posix_openpt(O_RDWR | O_NONBLOCK); + if (fdm < 0) + return(-1); + if (grantpt(fdm) < 0) + { + close(fdm); + return(-2); + } + if (unlockpt(fdm) < 0) + { + close(fdm); + return(-3); + } + if ((ptr = ptsname(fdm)) == NULL) + { + close(fdm); + return(-4); + } + + strncpy(pts_name_s, ptr, pts_namesz); + pts_name[pts_namesz - 1] = '\0'; + + return(fdm); +} + + +int +conf_ser(int serialDev) +{ + +int rc=0; +struct termios params; + +// Get terminal atributes +rc = tcgetattr(serialDev, ¶ms); + +// Modify terminal attributes +cfmakeraw(¶ms); + +rc = cfsetispeed(¶ms, B9600); + +rc = cfsetospeed(¶ms, B9600); + +// CREAD - Enable port to read data +// CLOCAL - Ignore modem control lines +params.c_cflag |= (B9600 |CS8 | CLOCAL | CREAD); + +// Make Read Blocking +//fcntl(serialDev, F_SETFL, 0); + +// Set serial attributes +rc = tcsetattr(serialDev, TCSANOW, ¶ms); + +// Flush serial device of both non-transmitted +// output data and non-read input data.... +tcflush(serialDev, TCIOFLUSH); + + + return EXIT_SUCCESS; +} + + +int main(void) +{ + char master1[1024]; + char slave1[1024]; + char master2[1024]; + char slave2[1024]; + + int fd1; + int fd2; + + char c1,c2; + + fd1=ptym_open(master1,slave1,1024); + + fd2=ptym_open(master2,slave2,1024); + + printf("(%s) <=> (%s)\n",slave1,slave2); + + + conf_ser(fd1); + conf_ser(fd2); + + + while(1) + { + if(read (fd1,&c1,1) == 1) write(fd2,&c1,1); + usleep(20); + if(read (fd2,&c2,1) == 1) write(fd1,&c2,1); + usleep(20); + }; + + close(fd1); + close(fd2); + + return EXIT_SUCCESS; +} diff --git a/examples/tools/nullmodem/linux/run b/examples/tools/nullmodem/linux/run new file mode 100755 index 000000000..7c88a7f60 --- /dev/null +++ b/examples/tools/nullmodem/linux/run @@ -0,0 +1,19 @@ +#!/bin/bash +# ------------------------------------------------------------ +# Install the module if we haven't already +# ------------------------------------------------------------ +if [[ ! "`lsmod | grep tty0tty`" ]]; then + pushd . + cd module + make > /dev/null + insmod tty0tty.ko > /dev/null + popd +fi + +# ------------------------------------------------------------ +# Start the pts program +# ------------------------------------------------------------ +if [[ "`ls pts/tty0tty`" == "" ]]; then + make -C pts > /dev/null +fi +./pts/tty0tty diff --git a/examples/tools/nullmodem/windows/ReadMe.txt b/examples/tools/nullmodem/windows/ReadMe.txt new file mode 100644 index 000000000..2ac136d25 --- /dev/null +++ b/examples/tools/nullmodem/windows/ReadMe.txt @@ -0,0 +1,311 @@ + ============================= + Null-modem emulator (com0com) + ============================= + +INTRODUCTION +============ + +The Null-modem emulator is an open source kernel-mode virtual serial +port driver for Windows, available freely under GPL license. +You can create an unlimited number of virtual COM port +pairs and use any pair to connect one application to another. +Each COM port pair provides two COM ports with default names starting +at CNCA0 and CNCB0. The output to one port is the input from other +port and vice versa. + +Usually one port of the pair is used by Windows application that +requires a COM port to communicate with a device and other port is +used by device emulation program. + +For example, to send/receive faxes over IP you can connect Windows Fax +application to CNCA0 port and t38modem (http://t38modem.sourceforge.net/) +to CNCB0 port. In this case the t38modem is a fax modem emulation program. + +In conjunction with the hub4com the com0com allows you to + - handle data and signals from a single real serial device by a number of + different applications. For example, several applications can share data + from one GPS device; + - use real serial ports of remote computer like if they exist on the local + computer (supports RFC 2217). + +The homepage for com0com project is http://com0com.sourceforge.net/. + + +INSTALLING +========== + +NOTE (Windows Vista/Windows Server 2008/Windows 7): + Before installing/uninstalling the com0com driver or adding/removing/changing + ports the User Account Control (UAC) should be turned off (require reboot). + +NOTE (x64-based Windows Vista/Windows Server 2008/Windows 7): + The com0com.sys is a test-signed kernel-mode driver that will not load by + default. To enable test signing, enter command: + + bcdedit.exe -set TESTSIGNING ON + + and reboot the computer. + +NOTE: + Turning off UAC or enabling test signing will impair computer security. + +Simply run the installer (setup.exe). An installation wizard will guide +you through the required steps. +If the Found New Hardware Wizard will pop up then + - select "No, not this time" and click Next; + - select "Install the software automatically (Recommended)" and click Next. +The one COM port pair with names CNCA0 and CNCB0 will be available on your +system after the installation. + +You can add more pairs with the Setup Command Prompt: + + 1. Launch the Setup Command Prompt shortcut. + 2. Enter the install command, for example: + + command> install - - + +The system will create 3 new virtual devices. One of the devices has +name "com0com - bus for serial port pair emulator" and other two of +them have name "com0com - serial port emulator" and located on CNCAn +and CNCBn ports. + +To get more info enter the help command, for example: + + command> help + +Alternatively to setup ports you can invoke GUI-based setup utility by +launching Setup shortcut (Microsoft .NET Framework 2.0 is required). + +TESTING +======= + + 1. Start the HyperTerminal on CNCA0 port. + 2. Start the HyperTerminal on CNCB0 port. + 3. The output to CNCA0 port should be the input from CNCB0 port and + vice versa. + + +UNINSTALLING +============ + +Simply launch the com0com's Uninstall shortcut in the Start Menu or remove +the "Null-modem emulator (com0com)" entry from the "Add/Remove Programs" +section in the Control Panel. An uninstallation wizard will guide +you through the required steps. + +HINT: To uninstall the old version of com0com (distributed w/o installer) +install the new one and then uninstall it. + + +FAQs & HOWTOs +============= + +Q. Is it possible to run com0com on Windows 9x platform? +A. No, it is not possible. You need Windows 2000 platform or newer. + +Q. Is it possible to install or uninstall com0com silently (with no user + intervention and no user interface)? +A. Yes, it's possible with /S option, for example: + + setup.exe /S + "%ProgramFiles%\com0com\uninstall.exe" /S + + You can specify the installation directory with /D option, for example: + + setup.exe /S /D=C:\Program Files\com0com + + NOTE: Silent installation of com0com will not install any port pairs. + +Q. Is it possible to change the names CNCA0 and CNCB0 to COM2 and COM3? +A. Yes, it's possible. To change the names: + + 1. Launch the Setup Command Prompt shortcut. + 2. Enter the change commands, for example: + + command> change CNCA0 PortName=COM2 + command> change CNCB0 PortName=COM3 + +Q. The baud rate setting does not seem to make a difference: data is always + transferred at the same speed. How to enable the baud rate emulation? +A. To enable baud rate emulation for transferring data from CNCA0 to CNCB0: + + 1. Launch the Setup Command Prompt shortcut. + 2. Enter the change command, for example: + + command> change CNCA0 EmuBR=yes + +Q. The HyperTerminal test succeeds, but I get a failure when trying to open the + port with CreateFile("CNCA0", ...). GetLastError() returns ERROR_FILE_NOT_FOUND. +A. You must prefix the port name with the special characters "\\.\". Try to open + the port with CreateFile("\\\\.\\CNCA0", ...). + +Q. My application hangs during its startup when it sends anything to one paired + COM port. The only way to unhang it is to start HyperTerminal, which is connected + to the other paired COM port. I didn't have this problem with physical serial + ports. +A. Your application can hang because receive buffer overrun is disabled by + default. You can fix the problem by enabling receive buffer overrun for the + receiving port. Also, to prevent some flow control issues you need to enable + baud rate emulation for the sending port. So, if your application use port CNCA0 + and other paired port is CNCB0, then: + + 1. Launch the Setup Command Prompt shortcut. + 2. Enter the change commands, for example: + + command> change CNCB0 EmuOverrun=yes + command> change CNCA0 EmuBR=yes + +Q. I have to write an application connected to one side of the com0com port pair, + and I don't want users to 'see' all the virtual ports created by com0com, but + only the really available ones. +A. if your application use port CNCB0 and other (used by users) paired port is CNCA0, + then CNCB0 can be 'hidden' and CNCA0 can be 'shown' on opening CNCB0 by your + application. To enable it: + + 1. Launch the Setup Command Prompt shortcut. + 2. Enter the change commands: + + command> change CNCB0 ExclusiveMode=yes + command> change CNCA0 PlugInMode=yes + +Q. When I add a port pair, why does Windows XP always pops up a Found New Hardware + Wizard? The drivers are already there and it can install them silently in the + background and report when the device is ready. +A. It's because there is not signed com0com.cat catalog file. It can be created on + your test computer by this way: + + 1. Create a catalog file, for example: + + cd "C:\Program Files\com0com" + inf2cat /driver:. /os:XP_X86 + + 2. Create a test certificate, for example: + + makecert -r -n "CN=com0com (test)" -sv com0com.pvk com0com.cer + pvk2pfx -pvk com0com.pvk -spc com0com.cer -pfx com0com.pfx + + 3. Sign the catalog file by test certificate, for example: + + signtool sign /v /f com0com.pfx com0com.cat + + 4. Install a test certificate to the Trusted Root Certification Authorities + certificate store and the Trusted Publishers certificate store, for example: + + certmgr -add com0com.cer -s -r localMachine root + certmgr -add com0com.cer -s -r localMachine trustedpublisher + + The inf2cat tool can be installed with the Winqual Submission Tool. + The makecert, pvk2pfx, signtool and certmgr tools can be installed with the + Platform Software Development Kit (SDK). + +Q. How to monitor and get the paired port settings (baud rate, byte size, parity + and stop bits)? +A. It can be done with extended IOCTL_SERIAL_LSRMST_INSERT. See example in + + http://com0com.sourceforge.net/examples/LSRMST_INSERT/tstser.cpp + +Q. To transfer state to CTS and DSR they wired to RTS and DTR. How to transfer + state to DCD and RING? +A. The OUT1 can be wired to DCD and OUT2 to RING. Use extended + IOCTL_SERIAL_SET_MODEM_CONTROL and IOCTL_SERIAL_GET_MODEM_CONTROL to change + state of OUT1 and OUT2. See example in + + http://com0com.sourceforge.net/examples/MODEM_CONTROL/tstser.cpp + +Q. What version am I running? +A. In the device manager, the driver properties page shows the version and date + of the com0com.inf file, while the driver details page shows a version of + com0com.sys file. The version of com0com.sys file is the version that you + are running. + +Q. I'm able to use some application to talk to some hardware using com2tcp when + both the com2tcp 'server' and 'client' are running on the same computer. + When I try to move the client to a remote computer the application gives me + a timeout message and has no settings to increase the timeout. How to fix + the problem? +A. Try to ajust AddRTTO and AddRITO params for application's COM port: + + 1. Launch the Setup Command Prompt shortcut. + 2. Enter the change command, for example: + + command> change CNCA0 AddRTTO=100,AddRITO=100 + +Q. I would like to be able to add, remove and rename virtual comm ports from my + own custom application. Is there an API that I can use or some command line + utility that will do the job? +A. The setupc.exe is a command line utility that will do the job. To get more + info enter: + + setupc help + + BTW: The setupg.exe is a GUI wrapper for setupc.exe. + +Q. I need to use com0com ports with an application that doesn't recognize + com0com ports as "real" com ports. It does not see a com0com port even + though I have changed it's name to COMx. Is there a com0com settings that + will make the port appear to be a "real" com port? +A. No, there is not, but you can "deceive" the application this way: + + 1. With the "Add/Remove Hardware" wizard install new standard serial port. + You don't need a real serial hardware to do it. Select non conflicted + IO/IRQ resources. + 2. With the "Device Manager" disable the newly created port (let it be + COM4). + 3. Launch the Setup Command Prompt shortcut. + 4. Install the pair of ports, were one of them has name COM4, for example: + + command> install PortName=COM4 - + + Ignore a warning about the COM4 is "in use" (press Continue). + +Q. Is it possible to configure the com0com to randomly corrupt the data? It + would be nice to have this feature so that we can test our application + robustness. +A. Yes, it's possible by setting EmuNoise parameter: + + 1. Launch the Setup Command Prompt shortcut. + 2. Enter the change command, for example: + + command> change CNCA0 EmuNoise=0.00001,EmuBR=yes,EmuOverrun=yes + command> change CNCB0 EmuNoise=0.00001,EmuBR=yes,EmuOverrun=yes + + Now each character frame (including idle frames) will be corrupted with + probability 0.00001. + +Q. What is the maximum number of port pairs that can be defined? +A. It depends from your system. The com0com itself has internal limit + 1000000 port pairs. + +Q. In my application, users could be installing up to 250 com port pairs. + Initially, the installation is fairly quick, but each additional com port + generally takes longer to install than the previous one. It quickly + becomes unacceptable for a user to be expected to wait for the installation. +A. It's because the installing of each next port pair requires to update driver + for all installed pairs. You can speed up installing of multiple com port + pairs by using install commands with --no-update option and finish them by + update command, for example: + + command> --no-update install - - + command> --no-update install - - + ... + command> --no-update install - - + command> update + + Another example: + + > cd /D "%ProgramFiles%\com0com" + > FOR /L %i IN (0,1,249) DO setupc --no-update install - - + > setupc update + +Q. I am using the 64-bit version of com0com and I am having trouble. I'd like + to debug this, but I can not find any free serial port monitor software, + like portmon that works with a 64-bit OS. Does anyone know of any? +A. You can try to use internal com0com's tracing for debuging: + + - get trace.reg file from com0com's source; + - import trace.reg to the Registry; + - reload driver (or reboot system); + - do your tests and watch results in C:\com0com.log file. + + To disable tracing reinstall com0com or import trace_disable.reg to the + Registry and reload driver. diff --git a/examples/tools/nullmodem/windows/setup.exe b/examples/tools/nullmodem/windows/setup.exe new file mode 100644 index 0000000000000000000000000000000000000000..f459cf66145e59c1720e0ecb1a64ad61d8510e5d GIT binary patch literal 196712 zcmeFa4R}=5wLg63Bgv3Vm;sU+mGdT>!R;_Kl6&id0w6<5Q6tP-JfRccyK@hQ^Mx?sOi5dk%fXI1&Yo8esTe z?{nYh`M=L|hiB*P&$ZWHd+oK>UVEQI;XRuKogfH$1Vs^qop{pE#{d53f4WgTa^hb` z3fo7#K53_B(d(0zdMoNIHMP|b)RwNYtSqgnsunHxdn~n5m8GJ}l6R-mvZ}hwGb1@U zF+B?UAMfc6TVD0u8w)pX&$_pbrysm`4xW>@|KQ$jcz*rNJ@-C`XZc?$@7;{&l8TjH zYV*$+I_!e5NE0Vayl3;mSXqx?(j;l(1z|aInzVed7iJ=EMyR1$HZ`smQ3j_6kF&B3j2{|NyUWjLlCtpICJMrKiqxRd9TfQ=`T@@kDgxgLaE9nvgRX^w68s_jXsl7Z z5#v9LAgq{CTUIKT3c}N)kp-AS3c~#e!+tgpWQJNzxT^%6ZbBi!T0N{1qBcQjpHUU9 z=+_8B-*+SEr+T$@wJWJP(S&HT2+xNpW9Vn&AcxZb8~gvB0>yG~ptmH@U^Iyt_?s`= zQ>5fTL7G`k_D1Fl!2-QQk<5x@R3w9vT2N~f!t*iHPM4fjgYuC5SvM-6Xc3A6p0h&G zZVcH^t9cIO$qju0PoE%<&#OUOKSpHQk-x;W!{stPU(Dn;GYXmM`3~KlV4?XJ=akI1 zTKiC5ge=pJ#l@!Q3z@D%K8O;uSV}EgyHM^hJ#RPb_8`0CLxX8Yds0`(e%28z8~}>w zCSdQ=2kj{i6%nJ7x)g{gQi^gUU}yU524>pukfJDob$2PEM_D}!*rQc*JKBiFa)Z&Q zad+BJn}KVmof+^@QXQzEtiH<)SQL{|-1`-UEBgg1jhQzoi9aln*;A-b%x&aJl8BT0{RHt$1hXhnh^qnzaGN zb@SynDh^P2C{JUM4gDQjQFmKpLVt%|93j7XXY&Ek5;6C8n5887-ORrSKGI~q8%)cO zqZTTJ+JpBkzju#bz;qWC$;AfD-&Ym=R4$Jv1Tqik&bWhxgCL=tZ`i68ld=nqVto3p z`N4dHHI*ZgC-13f=SbA28A9_lvWf6W^l6r`--4Qy)JxO)J8Ys(u@CllXv8=?BS;Kb z;|PM>UHfWgI}kar#~=h61_kj5|2CL&PWVmyDT>PrlIcaUvgUaM@tT zCyBZ*b>~BcCxgjreg(l$cs$T?DR4g5^kXfQkdhwyw`n%P>0o2fOSm=}S#i^WefF|L zrlyrhI+fG`j3ewu+`+b8Qj`MJ-iK%S&=?du*t@gQ^3N1!-vDG_K(k(l8FMJ9Em*7J zU!g>i4k@XF1SW)3M~^mp*=cFA+|onC)&yHDz4Hauj>a!b>Q_ zemXR(naVTU<&oCcOpmuuDP(e9^U2`I@LW_-W<5cruJEDJsGU3$Ly@yqfbiDW#Dpb9 z$}yr+avrkLxJf}J2QtI`i1_;0Jwy_z2dFmz_2za{^Lr==|IW-y=MsVgZgzMco_*Fc z;(T9;i`~OhPIqg3%Y+ttl9KA@O{K9)YCU2(FM*GwBnYQ9NNqJsv`ky=NiFgDbM{KR zvX}LU6MP@DWDwNt6$%yzEV2L#MM)J=J-mE0dj2{*Gl{7CD#qtvd-H)&7VIitH~T$u zd>_btYzsxk$OG&tMBFYVwFa%Rh7x=qh0i1kn9kFtrc)+}Iu<~CjI8M>Qf?P}&4!kz z!*8IHlih>Xa$m@9Z~zzZEdW)jfTmzgTKBJ>Iy+6A*Md5}o7gMJ0)N|226_@)L1`dpH?Z-y0?l21ur`KkaIrtm2P#)_O1ju) zBpfW1Lw~m+?qa=pg0S1)25r?~(AD4MVj=`V<^c@G94cgi_LL$_{tBQYcVWbV_HV%U zf`3ZKx#MHoZRjt1nL&zoF$KM5wgZO7^aNPli%K>ZGXU4XfP(cXt)_}jFG(CXyPCS@ zaW)cBMe5_Ln@vFighn0KpNpZ{Pj(FG9BSFW&jBo^H#IpS4+_MwF5iW)dk}GHz;}vy zvQXM!(1!2%23c{87K{~KN~NE&+fb^+rC?DxowW3%)9fFq$V#THKm$|c0VzJv(5r}} zLv{v*&77D~6glMZ$YGR3;r)Kmc?TJ7M#+cgyFH8np9xU#)39HhDKW2ZRkJr0(F zQHLuiQ^Q_Irh}aUe}Iww zdyJUlBjQwQcpkzmbekUY{0u6|3GOy(1`Py4hm+8=SC85C*?`UukuRNu%*)q1*w11h zgYm&EY7%}QP<=W!m1qm&_7EG3SiikbgSmtemdU0ejeKO0QI^Njc-jzXI6ldzk@8ps zz47Tdg@|0(*KuAeP6!nbEGfduG+?bcARYA}N>soMM&=FWlfe*~&h`%TAg$TnoIhRKsTn6vfHP(GFs58Y+ku>Dro}w+SqS%(ll7Yqj@6Z?|^&Kh*4x`xP=YEy+(&K613_ zdhce$Tatf)zrXn<{;=Q81rrbmbrTYvl0eJtRw9T7%Pk9$>}3qg8~U8Ahw$5ADJ69- zMGtaSc_Y;&Q9z{jO_q88~{96nOB5Gpz=b)GGb9O!IhYGtPeTy8dq7brj0yP|HnHcTC z)bunUJ7P;MI*b;|15Q{1N7-~VgX*3YvcC#VbjaZ~Z^E>|s7O`|#>)0XN)0soO+bRj zjczs-aagyg7M=>Fm_0rXnl0Anh8+U1>th>0Q$C#mM6#Fl(wa>M>lr~7c>t2l?8BB! zNi|2);=_Q4<(NWgvhQ+8vn(4FuObcl;8|$eW5D#Ln6#Vb6@aRj1O->(T+9>i9t-SC zi6jD;?;LyO2F^;R=AU8i-0U1|NLqu9P+>xDTikK^Cf0d;|=R=7k_$IN(kw-S1*Du;Y0H=2oo-k?E z6}REOJ>{5efWQD#7Udy{NvAH0rt|o# zJU-5PvHr$gYko$QB)iiGJEwbhoK5hxjyuKt_ooQ?O1e5drNCtj5Y|m+bVNXGzns5O)FxfJfoe zo%YRIjF~-%U5>k@WTu9dBGyteTZ3-P<-*erXHa@J*swKd-%%bc`~y3fLDYX5vT6lK z+tl<6=&?v!PIBKLlf?1dTJkX`yB1&^Z1+rff5BMm!|PB>i91V{oEOo|e)u8wib+oP zHw=Mq?4aI8_(rimQyOBv7pckY|4{0x}iu@+*gd1puiciIh;7OxXnBMP|+ zSwoTch^?xqzL$&*@QMq|u^arDj;3d<;OQ(N?u z2HD|~;r|(jNMs_=Nka*kA@HFVd?59(Z>T1lPr1ZlO`$lD_ijLebx4^~FHK~hPfIAB zz_@**h?`0EV~^l0l9SD3vmVeoi=0lU10Ev2$G{-bBW=*;0$|{hhTHVgNZ%CpB$~R} ztGB=+4_wkCOUKHQOAI4sXXn+cqx{-PnD;CO_1D14A@$v>H z-+Q&771(XS*Ov*tnFVmxi!BpOO`E_Pp~8Vq`vB3g2pGGHfF$e+0LeU%iv>9gX1ZRy zI=j#ShXyQm2|FoBqCu>XmT@y9KU8kXYzLiPu(puP7GW7jN?InsD-UF6z}lvv4)W1qj8QUu45^};mZ(Rw71n{rXJZT(0mH>U zgOYKCKR*wq`HVTy5kwyCgxRc{LEizz)btxbaD?0Xh%HWwqhK(YQ0+J5roe7Lk_^rq ziwp%)E$kB1*s31A;g}W0SA-5>?k_`Jm52c5oOv?4P5x3tR3rg!NNlX zW+NuR)v_#!-HzsPOF-(aYmlX*A$zwI3getlL#jkcoz7RA=m|7*Yoz<2y`@S1hNNUM z1Ai06sjc>NE&5xr=f)mgv{|J?^}8b3rXBWkFeTr~1XB)zrl>R<#Utk?i~6~VQarVY zjDZLxd4xV@p^u0Towtm;)p{oU-WQ5ODDFg4Fq0EYg{TR;^Vfigp;CP)VAF;tQ6u~V z{GsYWJYDQZYL(fj5`KtECW>jH<`V35fzfica4rhLL-V3s-!dUwh??l;1#}Y%QDxe% z+E5L1B$-<#woHiD4NpN$K)Q(e3pKY8SXVd;Q{w9byOzTQWR(B{LnA1?%HEm5C13(3 z--TQkn*h1w(|SRBn~Tjvg70f#g8Un@wuq2YauylSQ0-7iaI0W-i$8L*!_Ykt@NV`b zkDP|)KSrV>D0Pdoz*8=)vmWNgQi`O>BjtQOynaQxP8ZzBT`UNDM<;d7o3FFCdADy!j^3tca?NmQ%@JNYFPt@;G2c z=BOS-WqTtf-RvOMohm2yJP#MZl&&HlEI{lK$mYiO07?b4L)wNk{G=N0KZyE>^}>_W zY{I4#oA5NkO9%%Mjv@3RJdkP=o>C zgygGj!eoS52!BTS7~$G$AdeCLfe=5|Cj2oPR*i$*j__NA4urAe(I-L&!q^FjBm51) zG0`SiEHkaVY#MIA-E=#Jn}Dq0D0pZnO2LiTv7 zB7GjiBL%&qhxa2NsNQ!eam#72EaBq^CE&{@1CR$aD-^1 z0nyg@T=x{aVVT-A&}(aKFq#2Ny4ojrFM#EJ8ovJf_*yrPK@n=uLs}#!`|rpVwrrB& z^6jeE%3Tg$Sy9wEkFBdWZ4*d-*)c(zSq|zfKvBXQ5B)?d_sq5A@}mPJoQ-6|F)yZV zX-NXm{bB;zGt>`eQO5GU+5Ab~$kkYg%l9g&8!!xOTqQ`bdWy?y&9K45tyZ#DAVW#* zMN?m#qm3G?+s);B`3d1Prz`V>JGg9sY!MD)mr$}mHId^O$!TXB!BkQo2YjE7#I@hW z8m|Y-3j5y9j>)5cE#^p#COO_lnWXf~z_>jWVVMv(x26r4?NC;NAxLmQjP=pv9q3PO+` z_KgeKkK>47+jYRme*AY(`ifNB*l_ZTU4MLEs%UIr3#;lxX+>kh>ALE2QF1pn^emPt zE2YIF>}1xfa~J5*j))V$UoaG{`-8~@-1L(_0k3|$0q$7*)l0c>tA}a^t(R7hmM=v{ zB2NqTAh0{ZRd-vnXfzGUWbc)eq0mer@C@{+ZyyM#w&64c=`;X{JgsZ%%GXM5MEsk$?UVL zNfFGS8`!V+*<4;fDzYV*!^_f}GtOOfJe1;=8_e*MH!Bi0`~!j?6N95TFol|Pwnfeb zNpaLxKT=NK2}n8|jG6g3Q)rw=FqJf=tMTl&U*$J76(c5(CuYW>?$qAxHh`SU=+Wp& z0SVU!?R`6IUW3Ao>&C9wfKj)t0rlEM{J|l2XIu>;6iD%I^a9*20j;y8lm?hpDZ5)x z1Z{hFwC6zpxK2t5=ClJ^Fl!(F$_-~hcQ4P#=|V=^A^c(g!3~(89f!?DHCJI!p^X#_{y+=`DtCqzSV2NN*OK?wn99yv*W=t z4Sn#7_KEYd?PkdW+Y|m@niI%gZkT{4GP9*#X@*7`jEvku7$9~Xol~5J*Ii?3o(CF5 zq-=YN=_h)M`h0_t@xDuutD6;YTa*YieqaglS>XHTuE;`J8i*Ce=#$OM#h#Ge?1Q-p z+h_=dZC1%FJ;ysvp>eL?9nl3P>^Ic&P%{Z%a0l)S7^y7@yDX9smo4bTY{@9akxj3~ zN#!yeRYE+|QKT_ek<41sXS;-@BuSpaj@QK=Pvw;8S3SKc)^kdl&c@g)ip10A6WGOm2iq48I1pU&WRE8iBNjT{?3a_VT>2s-8gI4SH!nwwTWmd7 zcgA{-CWdrIZCT3%2pboh21LQ(*JC;fF+2J}lnyNCAQ~)5HwTi9WGApXh!Av>#x`Mk zzyly+Hi}J6{{Y)^{|617?VAWD(JHB%33(ew-UvvldcvUkkdF>ub|OxEgolX7?D5pU zloZ9|uV5>H8>b$lJ&%;{n)1##;p}`NSGaH2Q!*46S&8_&VxtE zOIlS)eOm4C2B(Yt8H0^BOXtnjP&2k|0>n6+GGL8fRHi{+;$7V-{nA;?)}vK89t%5r z{8=LJkKlW-LiPFdGGjMWBo6+k>kzR1>hdK4VO)f*xaWGsPIJG2m*HoJ@d%hBkVjsV>l^(1nUX zZwlm!dPB=l>J zoh+GlYmo3wcC)WACoO4ib_)H6@@5ZOJ6B)peHHwHc~qB*^gYmh4d$3nJFXgF`*0ks z+)Tz_M=^U0O^exm935zk9cdVNG5a^tdSnS^EQ@3_Vr}X8qolqHb|@z+VjMa{gA{PFZziNL zz+-2@1u%F!BQMX3r<(&bfVBYP*v4p%olHp|X_qgCp z%N|b`YfR0Hv1G8l(xF~>KE*Zc+zFVUxTCe~-oU<;z2IW^Zep-k=x(r^6-^?8=2=i3 zE6uKM_2q?S@?q2SZ=CzPE_pLbbO#rg%h#1>vMG`B=Fh}GTq2O*jcH8ZVAQ$EId-XPI6S)2G9TO6BL71J-ZE z@x{g7GblkG`AqO=0wTo4uM@;e8cFAh%phN~G5dyRAF~PT=J^y@a@zZ>=cHNLH}qmg z#8fw%_;+l=a<={qD|ik2HPLz|RKf`4g2$$I#!(5-5F$O7d7aRT4Tk!T6*yTh+K2DN z9~vK#%=&KIJ`z%eau#8Z`;);Ox*Mh(^J(2}$BFNc`4rzcciU!2N@hn>C5u+H?<KLA=^ufG*8TIr@@Wwn`g9p|eq-KQz` znn23o*us~61LyL`+r!Ulu^4BVntq9=muSvRz+elZtcjyOI(p)DeOoAIjcYvgl zy@sa>kn-uGbF5Af^x|~UP~P~_HE+h~9~#J+N?axHol5iK;Kb*rzVcq|LLH2p zs)3L65L`r3^+4GAo~SF1Xnh**iay9w^idDe0@4~ha08c5O|kbZ<{9v2D@UUIF{T&} zRt@OlR*tCfBJoNVO&fa?oxef~h_68>JC5ClvO3Ml{(*>YuY-m0q`f*VNKA@>WkL~? zj`=s*zk=vb;2Bt_kCVo$y_uTH8*(sOf%XM)iM%<)FV-kcjQ4r))!6JCsJS@Cmxuy_(HTbtAOP526}x6Qc5tOrzed9Tn^>4{c?@2X zQc4zIR$LaqTmiz^Ix){Ua0u3-k-K7uSB&h96nOfKR70;$rOF~`1P>L=C>VL)hP8zd zgZwnb_E;bkaSU4UoxBm7R9D>VG@Ao)rXSN`h%Z8S!&5h5d~zQ&LPLLkidK%NyS2y? zbq?hn$2Z$Jts(2!)zb{sOE~+5IEKb6eWNcjJd?U~! z4;Nk0pbOvXZSP>d5k-89cmN{?vfN+uEG-Mk3{L{@a3^&x8QBeE!BK@NK@C;*{n2-l9Nd#h4g>L6bKnzayB=al)72E0pDcPt^`~h zmlvd90~_!RfE2f#JJ#`eT;O9(php8I2Ymi--D(q(`q&~+PpO;jn?i29c}Qfo`!w+S zJl-Cc2+*p(?`hD^#TKB{OP)2_7OVQ;xRcqCD`)YAjCu6li$<^v`2Ik)d2zRR#JS@j z3y~kb0`CH~9A=9~kEC5q2By-;q0T2=gFca;rRH;zN-5rjv1;vD_n=U?6vO~=NFm7M zxoYAlNcYHga|bh%QPn&Hg^;;%)W+0op{mKWibHEt`V%asrjf{TIJ|uzpqIuOagkBP(>kH5mnT#1?u_^bWQ73njctiPL@WYt;VbBN>$|{dn^un0n%rVuCZfK zX^{yaAt@hP)!#f7PgR7m*TdN5C(o4F0_gDu_DPO8|77U zu1VJ_t7rPg6HE2NmXh=5lK-CWaynwPW9??zYpR{iKxc5KM7h3T3(b-f7Yusjnz^!V zHm+y<|7zpdn2zGTRQeW#<--j$2Sc=H2DLjvK3Lh2eC zEZa2-6U7g}{IsV)*|f$>+$UfmPu&|Zw_!em6G5J+4bVlpD6UQEprK$E+aWD911Zi%{BItWS|hv<3Y?yX=Kxa+b3I`f2+-FY>+ z2Ou=XEI0}~#T)2V7;+QB3y$;o;9p1L#Eq!t9uc%3P!jh~5bu=zaA!G;%-t4>0Vur(ak>3$v zJ;>+v+`d5G;AhKi0Xk{P7v4I|&3@H&DH04d$zI4x_r@1CYr0W!P(P0o$ z*TGKWS|Z|7a`t$d5GjuR2+JeL6ZGbex@0b*R&;%ms%S83u)C+FD2+oU?)zf&iUJZ? zs^y>rNs(iysFiMrf=P9#>@yM=J@y8-yWv}aW0NGXC)R}fIipL{M!s{i+m`~S!aIHU z`A*(j7w=2F&-dQF$U%#`IN$r33KE%_P%9?9i14P(mvW@%Zrr2Q^@Z#`z7HI%0EPhO z3mZh1Ls5DERID#Se$$tj`Bq5p*R>yEcc1%Ge)GuZOI+SVkKuTd>^+Dj7c-&(Zq=}X zF);Q9FyR8m9N3qp`f6!4vvOQui5|Vl{5>H#xdj3&v%UE(G0oMVYtV=Z^lFPv|3YI2 zy(4qG^y%f-a%M-#i1PuQkoZP;6TxnHX9|Q9#JO-2fn=uLUC=S6-RS)oc#TwyHH@yY3&E9_`)gOT7k})FjnVG&+T65CZ z;3)B#S-$U4>H(xOafX%6->@=m+y}zr3Ig8S3KsW<=9~SqvDw~)%%HtX*7_`R_uau| z+ol}uv~Snq9ptUrPWw)*C0r-k7i>5gEN&NPhwR&sS%^%UQ2`_A#96B8qNw*8azCt*~q3R?e!#Xt%U=esRmgP33=IR&NL*{@$TAz zV1qd~D)WPdDUgJPI3zJH-Z=*4cz*!ep!pMw2a#amS*U}xdhVuZCV|%-EdZ1EZfgEt}#@>AeBZc zH6?EE)j(V&C@=9*$+W;4h``aZtt9&DU|yObV8F|YLpdeLnestYrAxKtjSc4W(BQO9 zj+&P_8fd}UNQ19$i*X`6i>4;p1iKwxM=Vo59=NE8IIr!?p8J%f^I7h7vKKKJ_)aYx zI|$`YJ2`=HgqAX-aq0Y6G6FnB_!xi>x7BXmLTd{vg!UcNvDi3sMbmWoCzwMOf;q%< zI2{Dh(3CW|shMzfyIISAsF(&b-heW&r@APfBFbO#sW0QP)|}ewx2Z3Q0G!kA`>FwDD<-Gjtco zf%`vaWqk|K7UiU%y}kc$xR3cNjAcO_6^KrWNz$r`MLrv12T-ZuQUTtid(G6<1H#wG6zz};$ z_Je2C+fJqp4`5xv_9J5R+do^lqF-YcQ(y}G2?s=%^?e_dFK9}`ma90;%#XHsf5%&KxV_{DdHDBSQapqj|U|q7VxqZI|s}y_c=^nOpLpbe%BJ ztb55YH#KbomRsm;s$Dx_$ICe39SD4)kwySl7h43#E;a{$vzHlh&@GOo-NyAu>+FUc z+AY=9aYavW3_yYEVdE=V)6j)#q9aQyRy&3F5ci{$AUD#>wrbVP2%jgU)!)u>( z2hy=K0ktwZ`OO}v4P2G;jzJDzPdHo}0rlAO6k*Bj1p}Skkf{)T6&E`&e!w~+j?PzM zK0k;%jW~_Y2<$iB*`GhC5l!USP;Oy|aU%!zt5J3D;tD|z+*NV3ZQ^g!u$$3PJ?z^; znXwy7xU7S%y_`c7%shdaB{_gMolMO#B7wqgB~W-06CY~m!6L$}3qjoRTy_)#eC|2b z%+H}sj+gGM$^|8|Z3T!NECgZA$~)fA!E|l(mM3w^JKh0Ga!OKC$)b`r>Ke1g)_xA= z*R$8~5ohf2D<#7PMr)SzTqHg?p4Z?k!`jf;OTJ;10)jA8k_UhZ$OW5D1%THGKL@or1|!r0 z6DUP|B4)~!ey>TTKt7jnMwZ1i~^PA?IHTQyZE?fO^s2{cZ$?vD`?z6`ePe~)y zZ8J8oY$Y}fa2GE`TPW}!;Z52Gv-EOw(ZFV=eFE*pjHZoe&wZ)7lK?Km zT3FWAX+KIPJZvNdTT86_-8ax=7aHgq8QQB-m+-=CiXkyxD}4llT98&!cLJcxH(xbZ z)fWZ#fyDgfs-Scf_xkztQ)69Ud1ribVt)PtE7n)TFn)I|isF?Bm2;pzk-*dE>v zYk-^FW?1c=acuW|ArJUEGTXfrckmw?Vnz=E1OzND_rYkU>4iE!K+evo-GHTDJ|ICr zA?l1hiUQd5aDf`+uEq})Wug$sp~E*pjO$>j(rtkpI|rp7M?Ax(X8-Fy5oo6vR= zf3~c^l(zLCMiAOgBLWohcLlB2=LymXe-2eFk&^g(Np4nw3U0Oxf92z$=A9t0!=?M+@*Qlt*VOb9XdG%j zjWXXnN9fNKVVMMQHf&5k%vwP28PcfMd`(lk=$$)5iubS63gUA38a|B!q8zkY zOa*TRdUSJjQXW>Uo1FoyNVc1yF8McT+z`qZj7u+$cKg0$cR)4zfZ-Wbz@=n3&NOp*TT}BAtUjJG8yWlq5qkeh1^j()wlpXfW=jKLF%^!@&GN8#z$dr3 z*)15Qn|+OO0g-ObKGIbjk;8<@9vBdCXgo-Gp#Oic7x;Fbc7f5e{xpnJx1Zi4Iffl%TtDvSx^ii|eI<^?Y&j@LlbT?|dFyB7R)FLJFdQago$O*S z!ac;s0zVgf8dYVwi${GfakG`+AXPZ~#l+SGFu^BtOT>>dE=5vU56VE}xPHMb9(FOh zS?6M(Ld07;UF$z&e+3VKXmrw_d4MePCpzQ~u5Y1f(Qyjfg}QjJMoc$HCR0Z1xF7h_ ze?XE;b%^1WW^pW1G+?{|z&3K&4M$b{v84A$`sq_3G*RZ}{&glg=E&%1O)<2L%3Um- zTOK>(xP09pIsFoyfIt>C0WFdnbOgeSlV*(Jft3_>iqlwZWr)L z*?o6*+6`58rC9tMQ0BWP^DQ_^aN`-i3W!luO@vrL3HUx4^z8Ou3uA+>sS(z6b`G`h zH!x6hJFri~n<{ZG-c>g_%(KC+@^~+r<1S*yd|`3%BAlx47BEMDr8HC%{L(I#1DZ$m zoV(bi93cUsLtAU7=_eP#_`BFEBtAG`UidLGsqlR~UB%v6kS1RC5}9=5y^)V|Qzo#_ ztZbVGFfR6etP7vsi`wlj_9kXjW`S>%U}buWsp)a#`fv=!uj0xBp}dKSaA&B>fgkx) z;~Xsw~eTf~o z--;WG*m%*@z>}8Zq6)CnagRw*XWxjuh(^S_)+t(1+k$;Un)-&5dV`(p_Y`77gEtK% zK?HXsX)f^kInW0_MRANViaCF!=Ab4|NOAhr#~%bP<lP?aI}dc8!qx0??CzkJroZc6^)=_=v9cCY_Pr z^}p`-AM*PzTna8@!9u*ncGlFKie?apxOu@RWo=q{j{Y8TbfBSciiip7n}OHf;6^7; zJAZR8kaHU7LIe8r%bQND{Q(ZvKbof-Ph$x5*c`Z$H4d}o#p>|0E-)5Eu&Mdy7%RS- zfp?g}vEe490vD%BBRT1yd`_e5$PWV-(se%F_DAm8QU<`82Nqj9#I)?fL2-<` z2tw%3ENWOnCXk;Cuci*=rIzspC0z}h3$7({AR~e|q51hk^9d=z|9!{|=~>baqcFs4 zU>pWCiv|0+$l%#zg++ zzX16LUuwpFhnvM?sBU%^734yrZh+5iBv~@Fj%vB;w%mvge_Ka2y=7{?9uirmu3b*pMJb)azuQk4@ zU3zmFu)$4xuGQs%)`YnhyiHfa$!7ha_8(Cdxoc)Sw4=H#ZJ;q-TBD1VWQ9PI)7@jN z4$}WFxzFA4u}&^L9qbEd4$90OFa~ipkME+s2 z)E>lf=MxI$G%1vK0O@0LPU~U3ihl!Zx}+O25BLmn$8x#be@fH;_pxs^!r`*>v%@TN z&tmCQe4wZX^>T6M1>UcJTV9%OCJVOtt#jS0r_en`w`oVV+}HRqZf^;h=itJVXZ@L) zWbV(sUHa(0bKRsr`KqS(p=o<;2-Uzjr^ich^2V*d!YchW8Aosp$&ZfXk#_mfXOVBk zduQEsaTy&25rh2bX5?gazz-1K3r`Y0@!|A>jtz(4>D_}2e1kzLJS4wn+94e28DVc% z>|KGqhQLK#EnLv;frzeSRl5$K(vj)23G`Koz=sKkS)juZU_x+X3i>rQ(E)RT=qeBk z3QWz*05tHX(iuMr0|N-i&mhMS?@9+%jC1uEkzCm22;y&dkPn7!CS+prt}`BP7$H0g zdH$S%zk}eW=FP~VgSMS(4$K#Fw!b!Cc)_qPC1=g+cxgj_sUtEX=Qu^RFC>_D9SGvH zJo=mgWYog}GO(N4IJ|4#f+M05OzIVp_I6=CdRyp^1|#rV@W;*k=+ighZeQG8XP%=) zuQsZoMm5wZPejasKQK`ycnK~mM3YN#gyv=f-jZ+LO!ZsxM{S{gg83<1sA(`iEpV+6 zo=Zaawa$1n5ZII2#aEaVGoO^q6F4R!7f3tyuKC!uhXKi#cvqO5 z(kc*!<8XY&hK!k~kST8@E|816t%s%SR3-`S+B}ww5%FsI9cy>6m>6bHK}4T%Q0}15 z?H!fwhlmJ1NsPwygcFa-8##v^rUnUe2Q`WD*dBQo^@66>cck&5CprAX;uu~v2x?p_xEDT_Ev=L`n)9O+T z??YqxQ34nn$R5fmr1fvwF-m^z$f*&<(e-aVUVGB?lHOhbHlBhtypehi=^ITu92(P0 zHZ6{>a2!_mJ^5h9-@(8bG7S%?hyQ?H0|TQaqYuh_fL8Hck?DcI70~yD4hw-_he7)MIJkaq)T;P+5fu4z>rUddA zhvp_)2TVV%$NdC+y2^|fbI)r`kF26g4D}lg&=*Pwcm`+ZVoqbPzTS zEp0@ZV@SjlVY1w~u+WET68KB-w2Ovz?Se|hbsBsNiJ#zeIB464aN03k+R(ehdAp1b z_CBmSYV#!8zyX6vH#RDSkm)A@WVIF!Ld-z;;ImE;Jvd@$S!{;v!N)oAz=f<7d^oZ* z9`(WB#uzW`!0i0@cww=DGeUueOD@w6L-PS?G%*DSOT**eZlJS?G`zP88moo+&UhGX zkbme22O0Qu6H@&h7TlMGeb5Oi= zFnpMA0)_vV!~B;489V8TYN_MgK+b!-ivw z4yQAocu%%>%PwSPywMoua`j-{7*(ie9L#tlv_Z4tG|WbjAmbn|bixVUgL7P9BR3pv zR!q&05eP1wt)2L`1ef7a0eukP@7ZJRs7uJ`T8OWGX*0woZ<0QX!&`!SzDukaNuIX>Jfp6LBZFF3cE0gLv($CrbIR4p5!X^AQ^25@}6&* zZPtUNiL+Im7Z@-E3i9a zQp3=$ZDZl>e>u2|Qjr&e9H?sd!|ZMB?FEIb z#>!XO(k5w)Y#$e}V3k<#7cT8SEzprHnI{(bO!BSrYrzJ~J-Aqu7utklrMcDFT>~Y zXRjg6W|P~*D6_7qo##Er2AGzVefo=Jh!+HJC%mhwAZvB z)pc9ns7r|?V_3SkV%B$TX~#)X5j)N}o7ErqypVGJ2VL?G*zwekDy zhW5w;dA(!XONgOeN#B+vIacJ|+|)EeNkox?|SjleK5?)m*%VE5CE(R79Wo)!|Q@ zk0JGD=2y5BCBG)WgO{w2oVp#KxjKmPTQ~4Ai`Qfq>L9iiAKp2VeI6@ObEkYT>5(N6~$Ma7Jqp(*#8O`(hVRs`GAl!^F4#A8d zAoStxH2pTN6@<+Q|ADXv;TXaxgbN7fhXuicFbm;!ge3^~Bh(-~ittN>e@EDk;79$F zcz%v>31Q@q1mPNl83?x`EJ7$js6u!IVH3i$2-^@|MR*IL7BI>Y?m@T}VJ5<41S3K} zUbj4j(2cMQVH?6P5$X|EAS^{FK*b|5^9@Hm1W z;WPsMPBy?ON4S8HxDNC}n2s+BSL>Ya6;IRumj;w$lHvt31J;V zEkZil-jC<+QT{YSD?&Yjh){;G1YthHOoYh@qY>f}E`Z*jBQV72xB3y#5n(X`{bu2r zjxZYGQk0+Q|MakzRftp`i{CsmHyY2pc_{zpnG~OqXV1?qUbM7iL3Nc_TU}{!lva5v zOBPm%p4zHXv7)-Fw36z5yExBN_n=r^Q?kTUS1r}9^en1gSz76#diO5zRF$ssEK5!l zgsEk)3-BS3he~TbC506$YpdzAASLdKsPi+!wY8oq(dDVFLpNwUtd82hN3_`2 z)Kpg2dTL9Gq>7afS{9W`RV%%yYaTUPFeHqKPmD{_8+FOrks6br6ZAry5RW$u48jN@ zQAiS!1rt`=7~v{mtT0iSB4h}ef>l@`IE9r$oe&V(guTK?0_`Hd>!%Uae_EVq&_7-D zpC0|tzqsf>`cFV=06@d*P<{gXPe#vI3zLMKgj}IiXcRgGjiynf(Q0%Wy(V5`&=@sl zO`67{nW?d9R>b@7e<%o>ht*r{2k)ca_F?t3;5i5Cl?|)+Eb$@gZ5Wo< zhrDUXJBc+O`{jXW*VJOI56jzxJTLOT>-WDh_J3vU|1C1sr!AB!^D3|s>V-T{rAPGS zS5)FF>IZ~;h_?KS+B#9q5RUMi#rSW9)QrFJ^n%LjI*(x1IXyhvQC&gORuG=kF7cG2 zJxS7^>)f>!Dq!Jp&21jhQCqRPRP;D%tIJWrDb`k0J&+51qSm`Y>4Su+STTRC=vi9r zt|;>?@RrsJUkLfN9?znR`)f;~&Yx1DSFg-1E35U?)d|8v?V{?^vS_(|4SIQq>J(O& zNtK?0(yB5VtgwXg>}x8-1^Dl%fYwUhwpr&ctq}98Yn{Na(sSqiKF>-~_*`>aW%d1~ zmARFb)hmUsqbb4y8dHFb2T*&}>WbRxs#Q=4>geing%@>|bz<$xRW;~uM=V~EE6fl$ z4OLu(lo%zKR;=BUf1kk6Et zk55$%WeH{4!s^x0nNtF4Q?IJ)RojZGx~A3U5KRR0UmrZ>%3@#ls#!qLextG)8p@ z!W_zt;uxEb9MGa>Emg`Do(G*Pz((psg{gK_6A#DhHOgP=SyiJFOE7S#)vH!vh89&+ z0Zt-e&PN6mFnF^HV^!$^Qo=SB!%`9P zm#MCEZJp>@H4NbUlzZ7Qxx>C>v3*hI%`?g>VGDH$_Np>RDb_5;D&*CcKC~3;J6AY` zwOv`cgxDRgm!SQE$_nr@&qx;LdmgB$iniJasZp-do`wZ1=77Jd$_lZ9N*^E+{Y#n6xaK ztqHo9Vstb@^AJudp@g-RLp@+^6klE&rIpnWI7;!K-eLT|0EMvdtJg-Me#m)O zP1M=f2-gcF$Vjf{3cnWeDyZ%J>NU~k{VMfK>*^L(l~?Cp<{`B`jrA$5I_10p7ig}AB|B6h5>ptQ159gfPFFI>jXRZ&*~ z_7mo-k{1N1xxc!!7Ath2I-+PgkN?kj9fEyTjktDLt*UF@_kiAjKw=(Q5-JgVG;xr7CE zuTGKhJxb4ikc-DTG0BAO)ll!(MRVuZN_E~?w(ygvH4%gRARmiob;U~0g3_8gAytJN z?d7(-g;9ilDL4yA)Pcgae9H#DO~8lZJ(ZRAHJ+8wO1Z)xPzRKuYU-j8)*@$qEjD5v zEaLp?$}$gF1`>_eBk7-CTfJ&w-XbgqR9I=lnvsC>Nnd zE5x;yl_aq(-qJctc`0VRY=#Ba?5gT2{7{pS5``8oBYqwZ)bee@V6F`x zdc&uy@tomVgY_y1)esf*oGQ(jDow4M3gse9qm1wPJ@8L?<9>#Zqax6A@Td5+HbPJI zc_{ka9(_KIC)M-gxdy?DP=Zi^ph{?tder(R$(n!XqCEQ&>e5+|D~Z*#yP_t$IKL7f_qrGM`i@F0FphGemG2+oI~~2c??Ja}brHfI0Ub0YZRT5pqP}2$~aO9>abbff&%N`RDuuPzAh*eDd722X@T--`U5b$X$km?ZkMJgmomVHrzdXWNQ2 zF=_Ny^9!U6u@rdZH@M$EZ1MU357L;E*pEG@7lzM6_kZY#Z!iA_p4Y4YuXi+O+cv#m z8CKr?f}Z&L@^9LUdO|Dc~1^qGara1`*~9}o1o2)|ztCjEAj@Ygrq5Z?df6XDqV?+ag?J12bd z#TTUOg>#61f%qpVdmm*@O-%&u+dp#F{}2EE#{vH5_$A{IW7rRsho$Ido1h#1`>)gg zg1;s)aRhz_`Vl-$Vtg#g)4JrLAMv`5*Hr&}R~l7@{eXYsPzNgfp$<6yL*SFY3*JBV z_pkUT40RBNpBS%I2Xq;JbP!jBAMJ$7zx@OLNkbiQ_(*ECY6mg+m;yeEE5S!OCm72A z6@UFu2ONG<0>G;sMB(!prVg$EpO;ey--*9*r~?&#l2#iX!DaZ#>Ikj~-=L1*JMlLT zb->~4le94s#o!a_M4~IfC)9~V--&!s)-+ud^J$vRJKYsk$Pe1)M z<-ZO5KdJcj*I$o-UbD2fx8J&X^XBD`KKkg|`X8-*vc7)x?;6&veqp_T?Th}#wJ$#M z@R}C_53l)MU|s#sTU%SD-~RTuckkP`Z_cSxr^XTP|4A2DEDDFisa;)NxBTi?zbb2} z|IvnrAFAE)*v9&kzuOw>`^#T8U)a0vxo-}<_Tr_3hyHx2>(76>)bab@U);WV^H*&@ zY3*rU_wXytkN7tL=lfoN{q>uAdwWM;v7_%O=;`U1@ZyUv-tovIkJO7lta|D3(ArO5 z_z(F~N5>ZB%{O-{$B*w*-h2O$@C>mL`HrssANJllpvt9N7$3V(%wrvkV;u{@KorCT6&0IM zKoq2;ySux)ySuwK-Q6XPqzF>;t!IPhczn;f_kHjE{r>vCf!XuSo;9;3*P2;t_Im#1 ztH#F0W8U80Z{%fV9W)gs8iU-`7n5QwaAAHBuB*+&9f0%Gr+VDi*Md`1<1jxz9}e>K z#a%5exU01lH$YuV@<=!(E*=N_`CwO5tpPoCWw(HU0QT0_)+7Id@P{vm@$&Wc{&g(Dz6~>6y^JG5Lvc?> z2kwM=)K^#IqI?ogN=(GQe%_0&=33(>S}MWBT>YnhAjT*$F_Bq8PTp2mLA1xqT5vHT zSOphmxZ>)vXx!FZfP1>@@IZenE-XmFKE^k3ztbw*=dcP(JX?>MZ!?nX(An08TbrA3 zb#(R3;5Hl)Z_u_`CT~R#tZ2(9lppO;-WMhXiATV>|qP&xa7y+S%Gscrn`Iz$3#Wp)|MvR*j$Ips!MTd zekKl#4Z$8B=Gfg>X2Q(aNQ1!jL${;u?(Y3QK0a?GMLxz@s_@MRI&k6iV0m1YWQ`k& z!f|VL67Fcs#66v5I4hbL4@5q|I2x{HdY!}A0^U|&4zFymM%Y|oGj_1E!HtdexW1_# zm(^C{%(8qOmYs;bBRsI1g9f%%|5)SW?Zrpn{9cFr{QN6g8kz?3g713F44&g~k9U|9 zp@yqdZEXm4SNdXH?0|8cD8_EISc_{1wlI=@A>!~~E<>k>6_=kpucEG$~mlpjPBP;Q0 z-q7kX4)OhfbKp3(Vw@6gi*ai*yvsEl?%Tq1tT2}0 zIDrlI^>IsU6RvG)z$Nw7IIF4*$Ci?CNKrcW%M8cfA=cQ%>La#O7px8N_kT^$$>!$f z-^|U;W#!(zZxxhh#oG3du&4ifoEWc(3$tv&?3EX8D-Fh7m9h9!eHQL&%E8H@Rv5RX zVq6~y`0bz%2>d6puC5NQZm7jYb(J^^#y-Bh5Qmgx!b z!yCrm3HwBuU=J?^>}tq~t>t*C;^N}&6TC%DTFy&S`@^hIlsHz;Qoy!Fdf2Dj8b{T7 z;pE0ZoY@qL^P3`Zd20f$Ye~W_HLxD45^!Th9ImK|!WFL-gFnhZI68ut*}ol;P(UkFvlGv9)AiA4SklLo_<4JQpQKzSbR<>MHQ>$8DYy3 zN9+mnI~3+uLQ?`xYe~g9?U}fsBNvx+=j}R-T-@tLjQ$<57oim!RjzCl(Dy;JofUI!X6HM*jeKqwtLS=#!u`S)TQM-BsGL) z1!APJa+Vl2&6CHj#dcr+!5Mpo+Q1q%$6kI$*vm&7d%7uLPdicUVf7xn>A%3vQjFMA z| z72@XlJX}+oj!UZ(aS_bF^72%iUz~!oOHy!hX$p=lP6C=63w;ud-6DOlTZjvG@iM{A zPHNb}L=0Oga$zGS@xq*(oNELfc6M|URpb|GmJ88Y)Fe4z8(8l_wJEspQv>e&+>iUl zKjXpi5j;3Lg1d(La6?}^uIX*XRltiYdzx_XU_Txn8p6QW{`8+e3I~QkSuQ)zFzBwTw;l9#GnHXg=%~{PnH|$&*1#_+#w+#2< z{x4&AWcmvpo1Vg>UncPY)S+##2R9CM;aZ{|{q4Aaco2UEe1snv8TrAF;{JhAT-!2+ zNmU~_lGKN76RWX?UGT84f1v31eQIrO?eCT*=4!HnGCj&k&Wjes5jdfx40nzV;j*S4 z92i}K^ULe;3oa!b9GQw;{SvW+k{vd*3&f)G<~S-Y6Z5=N$E9U8SWU|VTUZBSB{c_Z zVdaO%#>Q~pz!)y79)|VUj`e*q7q!fsGl2hG`_BKM-rM5i;~&Ti$;9Y6*v^~hN5Z-* z#l7RBxTLxtn>m!=kMiO80)q~AbWg`}8nIYXEdpD4cMvz_#&e=&dIC6%ugI~ zKu96x{osrl?ur8!YoSND5dTmqYowTp(5x>baPs&(;44Z8+0eb-hg6Yq9ybH7$?W6uV4 zL+2s`b0;rvzhJTaf`Y66X`BDj0Y611CMGC|xkH@637$ppO@en5`v=&=FcJGGVlUUy z(n9r5^ZTbF$x*Di3NIq0F^>=v$hi-Vf%v%tu5x686oc@ZAU+^G`VZare3_6J{HhKp zlPH_OLEr+l8pfHcFd?TP4kK^Df0u`A!ae>!9D#Wm`{i?+5d+O;bp6UfbmsJFbm`(n zbo26N^!)xlbm!(}wExH}M0JuA9jBH+S8pjHLHT<~SojqZkYh(O($A3MM}DNJAcRyD z6p$IuPUNn-75OS{g5OW9K=wQ|$W-toaumFRoTaZLlFABX%YO$sE8RtUO0SWVB0JI% zlR##QBFGNjil-&6fDGl8k+1Z4P!^<=@DK@xb0GQT*GNuJ z7=7>%MCwM0NYD>L&-X|)Mi40_3!!&GGDtXC4k=}*BgJe5lxRqSVhw*mk=_?ktrK(- z7}gqgT#07wRv>J(17VNjXwmHgDi6Ab7X7XwU9*SCMe8xr*L{Lq4PGD@2M*+C_Zmfc z@FEK@Ze*1561f5Ag5*(Pj0#GKQAODa+-NHN4q6Q7MWr#Ks3A!hWrrxD zqC_RM82cXSW`9IBSt7_dR}neosUTmH5~|KrMrY}*5bFzb^oHLUNy?a_2OJrQ<6Q{) zC?Ac)m5Pvof)SE8bwu*<=19&W7%2n=BC#w@q)0MCs<{qGJv$Dm=#{~58}*U3kukK@ z80qUfAV*U>3UHP{y3 z|K)%Z5*(3{eF*Y#3qUrZ;V3XL0L6y{0jv*7jt@Y-LGdUsJ|2M_0p-UVqx=**RGsFC z7Lv44c4!F7O%F$;3@z9BM3!L4^%*r~zz@c9e#p zjyi9&SRH~o%A?S9VJhmZO++0D50$qrM6e0w2o?&kl%+$`6N_TT#QIHMX0`xgz}1NQE^>0 zYG|xSUCnuDy0H{>wl<+@$Wz!+fNDAmP+@l?s_kh(pE`7c-C2vu>-te+?H1gKBKX*QPe*;hK44`(bzapjsNdl$hm*CnA^d?r(b(y~%hu>*sApnr=bBeo z=;!NfLuC0c5H&jK>Y3Z+6_%9r^^f;AH8uJBIvX4PV-6=nU0#dK!lM5Ep)XTkhK723 zdm)Fhv608$;b^qhePwP1g^zy$`Dvk!ZZELE-d=xyKw zb7g*Wm* zjRyPD+^zMV|JVi(o!=hty!FZ}>L32H3{GSqsx&h+)Yp^dVy*Ym`A-anRQFlm3VD-? zs)mQZWZvPwd+X*6aE$AAG=dyxuqQ3m$>jOVp6?l)jvjjOP{`A}sHm!H_{)1i!R&02 zyEm`@x{JXHz@b8|sV>&f+5V`J?zVfZJOE!>N+6gjv?(vo$riY~bG_5Z$P5AAn(A(5 z!uE2xQ=N`&Wq!aTcWK&&Tp5r637vKSWaG<@VCDp~``Tb=C2HQ3>Kj41n z<(&mWWvGxJ89rM8;Gr^ueeJC+sRTURQVSZ7ZQXq05%)W*tSnM#1t9SAB$p5H)=(L! z4HTc~V#>>Q-_g@@&3-{ahK~vww>lln$FQC_d55^3{{W zi-7B+573_B-vR z*WzIj5m6xUD=Mj|sH!T0CC~&_AzBL1i7vLbmRwKnAND8Ux1XmI6$=WBOi4+}&o3_N z2NXq+A*~SFnLt3>g0m#U6Adufewpq{U|?u+3hawAV4u`q0`P@}sV#|#1a!DdH~|ju z2lu%GyfO7JtPc+EA?BPh)Q4UyFeoxInP>n3U65N)kYr|N zYH0)_KX6>UTrYT^+&{R7s1MQJOQK@pVUbZO`872KRk;Lsev&JZ!33ar&3RwFc=6=P z{r!80m2bT5_p|3XKSGBR;KRevJ%FJg-_FSla+rX7#rx{XizoM4_8lPl(_!23(`O!k zln4WG*df9O`%6Iyz`MG-xFou`01it%-WN|;?;qSx?tkYa)EA*XVafU9lT%Z(nm<;b$4@gcQiHBd;V-2`KDOUF&cG^=$Q2OiJ4iln}Rdl#Kg?EWw$U7 z_Z6l;eQIuYbcgD|fbCETj!tju2JnT2g~i#KsTs%t@NvQ7@-GKV4s|tvEomp`-SD?TK6tN^0?)GxH6#wVxX5rfX|E zKGipT(yslct^KK^wiY^R#C^GQmT?&9JGFF9d}?l(pKh3GXr8Wb{xn_RFyBx=-Jo3$ z;Pv)Du^9t>z|b*G^d0<pg{+J&b__=f< zkD;u+7CFc4xB0nm^Gl-(P5la$`Lk<&K;xk#tL7M=&^<9ZJ2Sg98=xvf4Q}>-Q05Ql`{D_FF`^E0+UiD&KzD~g4}kP+9wE@VAy@rZ2y{OP6RSy2svt`LE) z5P_}`fj$p`o(zGm2!VVZfs7wH=%zfj)*{>iG2M$QLwKs0zDa=HGNTdKp+Ya2}Lm>!N@-zAhJBhUq)dm5$NI&=;TmgaV!Ge7|Jb9K_%cpkB*9(Vo^hF0_rXeMW1T@5v~p*pGk3Z0s@&a z0$Dc#xi&Jjt42lvnFwUY$i=rFf!rSX1(%?Z_*|5joP~Tsnvs883j$d%0@*MEIUout z%|c0qBn0wO6jxu2K-YkhNS{!CelDslEJC@pMX0eU59Jrvz&W)F)i>0kt`-u4I|Hbw zBNtV6&lA@EH0`KBJD4 zNx*;lVQUPxGYk!F|2CiPMJ+PpO`je5O^n_oYOx%F#*RxFAovnzm{Rj zM=zV!KQTGkxY^+I=b;`uLqW42APg@-a^b|}H`3A7E6fN0M}r>#Y%g8B>6$-0IY~Nt z;>5}|0MOfGqj2Sq5-wd}a?URtnjjrJaq{HJRV!>i4-Mv;i(gsBAh3t_rdxVp>BQ|j zcTXKZeqx&daaM{ke)oY4;I#KFEwfvCY3al;b4e1*?^`boLH>?*bMXsy1c1QCGfXZ- zdg+96Sy>Xx+P6gZj&^;yi{t|Ku0M12W-jXE) zNEa2k-`i<4*wxh@6Jstze@+>UL{RR(#uyNkkpVSKFD=T<&gvjiBh6K07|uZx3=bau z%_le{I-{VVu(TAWPJ1k*MyjX~>2QU{@X!%nKmRz;*NlvSpuHe6GBOYn17u`w(VgB+ zPXEo@KPoyJ_TrhD#hEHHGIF<7k&|f{@eT7H!N;!c6NRi_Uz;17 zm$%!~Q{CO_kPO%S--LF}jx|heveA92`lNxPmrL!L+T7e!ceO4#U4S3br>dSm(s=oz z@}b6~fyec1>lWswo0qZ^wuL%pr>oeTn!kW%?aQk#%}wR?3k#oXY{=PRh~DdWeQj=? z`qI+;rD>{my0*2cYVLD`)^alR!F#=~sc*A0-@eWe+xt194IOHKB!h*XXWE@(fQ{NunU_JV zR8_gv)wQ&MW&Yca*e&2O*gE{VJ^Mqh4tp2M4%kg)!q#Kiuh4{@!++%gZUq6`vE&IE z`p{))L)5OZMPxpdJ?%BK1XhlX6MR@+-%<>H)}!UVIhg!9OaQ0 zz?z4OAiZ>cmRvWTAr$ zEDfMMLu6;~j(nX>k)Ipz;sA5x9i#{R*bw=~8KR^Z3ltOpd^^Y%g@iaF&q!q#H6a4Mvvsp2*HE2^snYB7;O96zlJc62g5@R9G1DP4Glvap5Q=BM?PLhN9Sv zFys}U3gt$Dyf+?2#iXLhgbd)f!0Tf45h=w4<)nhWp4dQ?mk^2y5|coFYk*?1j8SNb zCGyX7Lp~L@$fVE*Ig`>+ASnQNY#7K&y-;Aa7fNafKw-sEz?V`{N>emy$#Ox3S-z+c zB6|pjuKOswxUaEyaPTrNIx?wnU(;@>tYd20XMS3Kg`apqhG!SH_^K z>U30FlZG1VQ&DA0I%;dmK%Es~sJku=VUQEsyHz9W$U+p9kck4qno&SRD+&y6K><-s zC^o$jMJKnR_>6Y&olv(-6kM8*;tR7-Vs-&a&n-d`jb$M3Ek?;zg($VC8>N;Fq0+oe zQ~1;+NJ*}v=uMKr{7Nee?2GrWtiR#eyLv&j%~*>~0e@ z1)<;e@;mcTY+7Mwb^QcH87S`wJMG)Qd5xKa4mDh5Shw}QfGnTfud5s^wHSfVt^S3L zSM0qE_j$wDuV}YhppB0?-iS&`3UR%Bdg09WhiJorXPhE}AJ}P6pW3r)3oBZ``-zl? z^v8EMnJ)gmbFB$F`rIWn+|S(yE*ED|;0(NP_~bmU-|(JiRJaRW_dHTYAJ z2h|hdE^GM)1_p(M`G2S8%vWCMyLss{lT(*ItgV;DD~oU@9D{Ri2b?7{;k>B{G?DB9 zPV+~<{5=6fz!G^DnU;R1`6rz{3_0Mg^=fkPUw#leT;iUov$LZNh$D6EY^-!VTwJ75 zQ&VsC!5!;=%1r3P;a&4>$ep?CjuKpaqMt9v2G|66ZqQTzc(IjRFaM@XvQE zh5@Eu%On2&*OV4y`9=sR{aQH>_@f++Wfl-QeY;5QiPF$Nlmf!1N5 zofzmH2EKxUf5CmvI=J%)_~9NN(0S}^EIg5!p2p(q=`A3t^7M1MS5?M={V` z40IC%O~$}WFwi>;bQ%Nw#=uiB@U_JN7i&#-M`uakgY!W5G0=1jbQ}Xc#?>tuaQ`e7 z1Dyt(Q5a}H27Z8n7h&L47g10RJuiqEl!ft*K>uRou-Dd#ufsTg<;u4qliKtD0ieGEJS1OLFl z$1w0TxZel&&jQThj;A~ZK8}4I%uEXl3+WWK6}x~(VW7hp=qQFW3kF&X_ov{lGTbQv zX)yWj)DYZb0eTE~W{0q4M8RxYM%EM9;!>LG8ao0nT?G1#frnt=-xzo-2HuT&l;8%;lD=_c}4159)z}=$Z&!ZT41qR-M<1$E- z0pZcIKhEFpWt5hdT{JLu@YQhl8U}v70K9Gy_|hWqp~bYE!WkFe@PhR0+(&JCp9DIDM-fuW&INL;z#=Oy_|mv2^y3Mz60nDk(Z#o zhr!@zPwTVGzPsaEgEs9>@YPA|QTUNW|cekbMy!fA7{Q zC`c})SP&9Mh<^V9;*#r$C|sr>9K;oVjvFqelS?4d4IuOP5|`s(=W7YWKg55ACrbRI zyyf!$z^}N34^S-Chd@XMSI{KaPhmY#gY|IDemPD#V`ykW?3*=_p`oUN0wj^0!E&5j zGVvj~9^;vH-x~<>Hv&vk0lpj%O~kQovH{3Q;+e~A+)e}pH>`2Rcp zLw$*6k()s@1>wbYICEwN&XfcZ>0zXi2~tz=OiTVPgoOVdC!b%4a|!y{Yc8EGR?I*M zBGnZjhb6?VEAY&J;pfq+zn)u`<~dRJcenNX13A?EU(a*$%z$|U@)vLhn(&JP!f6^V z<5`-@ifCzsAtXiy#`QlA$;HBaAmf43m!$UORYzWku=+INyMM>Sbtw$Dfc{vl_f}`# z05SpY%!=>(xb;MI#UrpbdKkPA5Vcyq2<4!`5(&vxqryx_&?~MRp`i# z4`2iK9TG*~$mQ!TSL=@e@SjAyFnZ+IZT~dxOZ~GK($}Mfe{7!&%!2Lczi$7|zit0+ z7{3pI{~dt=`u{oDgpxrokTlFx0rU#ymH<)!dy2GwRCnot=(j3#6mh~V+)f@h&7UC9 zlX|ex_!!C}UIzYs?){}-*B6oPVIai1qHQ_%zrS8qJ@E%?o3v;Pf^LEGf5yM!NyBpb zihoGowqhwi!M=aa4^{_mP=bXpVnxjUO}`WA2psE*EVT27_7UF^ZYx^(WDVH+y$L^v z-}DdZaE3!KAf4jx({CWiPo(eqn{=>J0OhBmTPxPX8=wB{Uovv?$4ZXx>8t-foxCm{ z!o1)359u4x8nkD{Mzr}K(t%Q;9q@kaEq{}~3hH+T>b48*M3l?v82?G}{xT0%e?O}I z=ky1f`eXlNHz0U3|xpIrb?E089%b0du7CPchaTMTCRd!UyeA>;)k#nMOu=rA$c zW#I2$=Q+@EazCAce)yS&5{TB^(7FLKUMOE2%|Q7}>*pb?8yUdzuj}XMc0(3$|Fd?3 z^>k?WdPKbB`}_JL7Y84j>$RTTgmpa#6qeu@@W$@%N#Dceb{+gnyMC-+!TKdTGYAC$ zrX~09u1{EVPfkL=C_>+_{hRthm4I(-B+m~rO9s04Sw_Y*i}9mv2*e?RENPztMCV~ zoVjc$ZVA_)1>NTdgQSF&u^;Pl?mST+$tAn6U^WsA{L zykrOSusrR#ix(NFe*}emgzZP@6(ypKrDxfj%N_7HE%*`bas~geFn?~zgr&^6CDUnV zmu=U}Q2p8N%S5aHyLL;;z56Kq9*ka*EnzWIUA{zr>D}dvG#4+vfB%7o{{t1+J6p1} z#YlCE>M*n(+Vn3~|KmHNX|iDO5$r&IkoY&p^a9m&1{pBnNZ5wE&LAxyB?qG`!2gHc zE}0*`T(b2D9sf{3Uf|>5kHjY<6_E#QUf$#v18am}QotW;e&0x#^>Ppl^gET*@ZBRVQ zV)tS2e1lo@I?~$xD)$r8#Dapt>w2!Yww|(k&|3S7HQHQUS;#)(g`Kr^cyJB>tNXv? zQ@!V)&VTEe)ov=7pSF2V-G||MEIVh78**+3^{@OLyYpNyE>qjxq9e(DQ z0{&aL%*42OSwc?nGUuA?9yQ)geJf#)zLgNIX+ZZhrSoQ=H5n@BE}Ak4T{4~I6&)*= zYp111eb+KB_S=_rwZEv0XFob=VK8qyKF>b0z;iJ@?PaRXO+T^HuF0~xCS}s-OrK7s z*SagCHKa*Xm(|0@n;2*&+K)v_>i8 z6&5|&dNyuKkb;IX;|}L7rM2mAJZtOcG!7Nrz9VB)tTLv;RbnS~eejLp#BlQLv`@>4 zAtu8KRFv{`abwT38Wp1>Gu`xolW%N8o|aw8o(s5+w`a8bRcZ^#n?Jo)zkh6jXMRh_ zp|7{Uj95-eUs@Y;a|j>gqz+DZE;kuhLUIzh)9rf%HOdxU7rv=2D!tE`d6Yb5;l)}; z&uSgRB&eZ2su9n>GHpz-ViUXTvBE}=7^aMArt;hLJ6G*D*0kc4O1$*z*E6C4H4Cfc;lP|v(a0z&2WD7R z_+3H{@Y)=wo=Q}GsD5x`&~&V06w{-LhcykOHjxImNG!^W`i4;<9xn=7+V*DYuvuhH z4p?jWjH%wpK4_j9Be9o(Qn%#k9(VS8&L=tIw|v#q6|<73rMUd8zSqt=edF}DuyF52 zK`J3;MfD@A<^6IeS8R5$y2_b!;TEq&UjK&Q__w^`8jV_?_T|f-*4fxS4fHgTb$d8i zDMIPje!Vk(yK5}<*5KN^qOBGw6n?vVP6sFri|)Pc+P<&P#hPvI_zR6~s8?pJhgv$w zpIyz{NVO?(v5F>g=t-)-q|)IARf=H&nn-q1rns3_>o2QR)Sf5K`@Qd9Xs+tvr{I1} zs`x1=tG?%87aAhYvMNHkZLsd8gqSf8`%{(Ck0QCFO559H*N)G>%2jvBo?P6@5OM5U zo$QB!y#c=+5;xXYJGu7j_@(R-)2tpFnkn@(3bxpb29vpUmzsRyLg_ZhOUcBl1xzja zl~xXwCd%b=&GhiR;AVZ(Egg0{jpxN1owrh#M;_c4b)K7c^P*wU*bHm_jrf~&Z|e5N zT#bELB0_u1+_|oUGJH29Z_UofY}$`JKD4EA#ov}zV~^?RyuI(-e)rYtv{}{}dB29` z^{U34!*@J#ppjI!Tz_)H_~r8q|S8!7#HcKVK$&Byrgo|VmK`S8w_&A;(6t=DQj z%*V7@>$6Ryhe767)`keGUc*zTof%IFGY^f`sc9{WZw#sruAQ?EWf{^;o0t>nm5-ZT z%yU%2*&F-P=svXlUZ*yaKBfe7e{%X#cGY%ysr8|}+2=NC2^<|}KeDj-UW3eZ*da!y zL$}Mz!z|s`z3W%9Y%}r{e3c!(*{k@-22o0_){}gNFDvxEs?ini<{JE}meW6>KqLDt zRQIW{C>MPg(~j!ifMcd(A1yK`ALhRcdL+78wT@YSjZBOT@8=Zih6LJ0RXu;+>&FJ7 zdDK6>b{NkZe6LhQy@w%Z$WG$!Ov=+J25l=Q#p3k1{158Q96V{%Q<5dgGq-6XEh4tb z3sRi(_;__e{afb=jx(c|=F~IZD^Uwy-CsP%-6dUMm@fbmsf;w?-V&ASwcfEPZSJj2 zLvy{F%TzRr$2_C?E+}31cTW1B-|p2Gd%MvUMOZ26zjHWzO4T7z{hSI0m@+`{{1`V z>W@pYkogGD`MJKd7f&d$+vrYxwp2 z5$M6Z zviHBPr5_S2^$xrswO>OTU&x*nHOunj?258qXnrcp?vQh#^2|mHC+Qo?-OckI8=G2= zkM>qg&wQP(>rxA(Q>UmeV0_qe0!W1`FW%Sx4W-Gz4&J|m4) zvC5oAI#!u&2eSf1m|SO+DUUU^_8!Dv^EC43IyZhjp?UUMcK!nO8?Mxm3zs7C#)hH7 zvb2`;ul=V*h8B*!_4xMi%CMRyx4hExlfyomjpeI`H5S^#HZq4d_KnRuDA4VVV5X|3 zi%aUaR~TJjjYw2sGmE>gv#_sRo~qH0Y1bB)+{t&Vhnf6y+tl|A$M0!4Kogm;LSEvI z24(lRcIM1<)4p~y%>@Ytth9QVU9qj{#249+ctK@x-b8$hL57caCaG?+hi6zojY{yg zftYP%$ehTiMe>6t-#fgDX`7Cn3FY~9n5w8cZ_?v(?MTll4$8OJ=0rZ*Y%vK@$k{hE zRaaZx=XEGDDt(MTIf|`@UB)+OLhYR^UnHlPX;@W#Vj5l3A=SIQu^PP>S=x;(h3QpJ zWbgR)cr}mA)ZLsBW3A7oTCH1G4Vh_8|2#$=%Ej(}jZ5 zmknCHDtC*sw{6!l5t-!U98K?KcIOp!IYeQ_Cg;Jz;leJYJe}1m*-*I0(V8hOqi5{J z<&H|ga7AJ-_jCnq>RkP{)Va=dW)9cTiV(_6`9q-ww-nXszlm0n#_k!0tK7RDamXV& z=+K6}IrL3x$DhV7a>#s9yTCVf@0WAd>2)Vu_UB8|9v>a^fntOx*q&%O6fIugU)3%% zeRp~~=+f6|PR`LyM;6q=sgpCPBcd`Q?iL)Mnl^J>OmE(;Hf=Vmbmk5v_PKrCI;^Ext~C~J^B3fH z*D#4tw|K@0->I2<+CDhQ?K$<_qS#%a%b1nsdIQ_BgYBEF1~&PP?(;8K-|JI;&N%74 z@O__gCOW!sH3<#@jcdNu={0f5$bBbU6Z?gbEcnMW-jPeF3>5{>5F%DVl?w| zWK92!MrHOYW6sxWg9AZ>Zo6-?MOQlb`Ap?1ovqYh>1>j{$Y?hzuf!TF-fClB?@|7d z(`E6EaN4F=^;gBz{LgD<(%XIwXQCF{kQ8!8`@>YcNF0O4)qF`klAw#b_e3O3Z&^*R zVT0L(pYojdbhrZZjQDG=u5RiR??1fBW?kr2=#R5bL`x)pAoU?q64>oFq?5 z?L7^oJq=1UkuvY!viYWLU<;#p+?-0f@>!YW<>G-9PLkY=l$a@o=l5{5@Z7wbHk+p1 zGoyS~k>^pL*6A&mvFY0;!kLyVen1IaVzq<`)9D34ic;qk`} z>HXB`S_G47#MCKUxC^3~x88-ZaPF*L+fAy63Ff^hAg<^%h~9iANu1 zd-hsfkR6$I7Z|yBedo3mp{jSg4F*Yz`u378$*grgeNzg$xwX&dL>)z5IH*@6Zki;mZLFIz` z=-T1a3!RapUrk!<&o@xd>ZBfI%rJQCz<(#bL}YAUOkgtnQbBK7i4E`NQC^>qZ5sRd zC%Hu}OXptw?lP*lvA9<`@6z!LY_2N5h;y$x_hMABWMOi&z>p*R#Gs>*Ox z-YoV*^&Mx6haag@^@?Y+#oy`U6caNums8_4U#LDC@|w>5e(Mee_G0C{Da>~K`gQE{ zP*Jt*O3t+C{P2dOEUKMNr=*ge&CMPCGRbzl&*wuLD zvVFZFrrs6eXsbvusC;Pdlu(?mq*fV?o>1K95XT&G?Mt$#TXRV4l7o(I?f!Mld#0xE{!Y2N{YAY;_H%=Qt3%;i)%>L|MQrqP3Z~n6Gv@iW z*jH28LSem~2Ysv}=v+H9#NzDZs$WW*-RMp^q01{|Hpas+YOC`+qjq0s&Z>826^+T! zm$Z#9e3TF~y)ay_7)rmIjVWR5hS*5Lxzo4h=(~d-X`ijlJ8A#=IZv#`dVn}?Ps4BE8@g&dwHz(d4 z_wBe(~+@vk~p`W?x3SC#(v~P1{?fqpai~919rE&|%5X`)#W1 zS_GZ_FT2hBUO!n>9iM5?W{-MMcq>ZCHZ`@It8S!qUFZX4#V>|E%JI*|UFdAYS+ZkG z%gn>wb(8YM^ZR_8g-0Ux4g5MNw@2o{jObR@2(F9C)(1#cVM5<(Pe$DQ#&$B{?7r96 z#sglCS-Q(@kF>cix3}AzbLz}w<{kMV{<4^jy^b$~%A9MBxnoan>eYUJsqgc<$}_Ju zU%GCVZw&Edos4u-I8g2s>{#ekl1PvCaZH-4WV_}hKFq(Xr&RoCVr=qp(5k#WIYX25 z^%IBq@4lx#es$aL4<2Vp7r*ZN?9khCOFH_Z=$W(~{TZuzb`~{18Dah=adLWnm)*_o zH&r~}mu6e$Q<4I1&qG)AmUX;mVH|)ZBXPc(l;#o}xgz zh}cG-i)P|1DN$>F^*rTtpKZQfoZ{`*g=C$_;g)B6{KGEquRS%ty?)deCyyQKtZzJ` zoZDA+fSY;xF@;?H)I}cKa|a{$JZ8@~&VRg$N11MYy}+=i1?3s()2(l##bXs2oq3fd zEBCla^_ux*a9#A@booe&#I7*oOAOz(A3Ux9e&3^Y`o~LFIeC=`iB$aR)W5&P|B-D; z1;>Z(wT11+^`!&3;!Dc7Q?8bp?Y}I3?^?{Ja7M=GG|g3?6;ox{;UyN={onm2lNn^c zDQTngJ;z4r>%!g>Qz<1D#ul0_~F|JHM z_2`!{v;8E&Gqt*ck8fH&xY6{b#xZE;nR54Yn`%xa?NGep@&2h?>F-+uJ8ai?m5$A2 zI?s^;Pu-7^STUomBy(^q*z>gep^%SBN#69&-0qb3dj5(kVvBc$yxGe5h+Rf}OM6(4 z3YDY9L|vGo-yI?4m#NHdvpzR>&!021i8baPzjq*sr|Q=eT+DCnUEK7ZDAmwCH@Gix zS2|zLojHS+@;E!s`2#glrJO;TeS*J=Byl-Oy`j8wif?wHH2B1av%>La^v781j0b~1 z2q~?5xU%cfxf$Psu_szxQ?}lhK6&Rf`@MLVd{4#6Bw9+jtet`9TaTqztlgD=KRWSO zx2U3ChJvf2J&(82i1d3L-pcoh>$&|b|?d*)E-g<`f2H}rX9ylMC@Mqj6+ry<)?C!%HEbi`n zj)Pyu@NM-gJ%NX>R_Awxys~Sv`pA`I9Qas&>y_R$_BHpMN*puUE7dIC9;t0HioP(K z@LDqPkpJT`!+KxbrCF z%){vZyRmm1cd)3OSY6;HEOq}Q_dzd3lhwvUHD6Y0eKkHW6#D5*f_0AJIO&Y$*P}`( z{>81D=%d2@wu8P8w=z!%aLB5NXyq9Q(I7)P8S^hPRPl@v&K@)}#{GxuO!K4-BzCzN zl$^?Ojjq_L@IvpPWBjpo+DyZov?&Ke>@+FjTAcZ5x`XBBP4{g&!?(es{^H_xpD9ji zVgJnS*MB)!LY4OHSqXZ6YoFo%T@*TO@k8Yl#gV&6$KK8tnQ}ViyS@tLlt^f-Yb>j~ z$C1w!yn{DhE>1_)Ql{{@Go!Od)XnEqR{5FUmp0Q2Uh9k6Yp;LKt9*S^3+b|9z|&DS zPq$ObhqsmL+4<~!Kea*abpEZ@HMJ~#Bc{QyKIKhh-|#IvNm{r2?yo8hMoW&~bK~kLWnkUk z?B(RScV2&Q^r=YmikH3@vbN=(apZU3e_Zi*_QNcslHFmhAy%BSd0lh^b8Ks}9Id0m zZBlP>x!0X|d`yu#zs<3x-gIYg`)=;bPqr|hj6HtofwuNGp=fUoZpqg?imRS)`KB1% zTGHQ1bKz2rguj8oY)p4!rx%UPTg9EcEan0RTaK124mZpQ#Y<@4nD6TBTpYe}+0A?1 zemdEE0Vi`eS%;EzSIcL!boMw}Do=bne{9c2GZ%`XGfMgCqT2&)_Xgb4&nZ58PcUwE z{r01qWpCZQcd~xRm#6L$iKChJ`7K9x7rAt7dF^@sR^o5hzlM&*9oaf@%de;85ySQ< z2D|OvTo+h>dFUwI$nb9SEwLvlnKkY?{F(}hyR=l!x4jcrPw4o~MJg+^GE9;E*(0go zwrKjHM24KGxtA!C+O8=!y?(0j9D7_>&_^>6d-MB zGPHDw?NE++mVN&Ad2s`MyKs|oesQ}u!Xm{!Jhu18Q%+s6e=*@!b+hnnDF24w`SXoi zB9vE7U+Jq=EX$3Rce?AldNZ${#hL+~jR_a~?1e6f_JusIJ#F0PCBwm{qcqNMzxn`a zmCxHrYO9*|symNF4$>Q{(g-@-_~mez>FmKtU4c^Thg&zD%zyM^()JQnyMA5TCFdt+ z2Kzs^_zK!pZfgA9^sJC`8y`N;F)PfRZZF%h`k?W%kLpZ!*HxcZjqVVmVPCvsWJi(W zU&X2w@5VWH_|VZULqlf3TlOJ+XSnIwScVh{* z^W(a|=J{)(15aNMc&?;L{lqYC?`TxGcBdnq@R)kYzOWm6e&>{K6|u3GG^XIoFA7_I z+|6AnyR1@jaLU8^NpOyP?1j6VT=voZ&SI|Be}rWf`@@IvMt&9Yn_5=VJhYvfE@d>k z(jLwr*&QB-5`qI}kWuJX`zt0%lYv@5SFnRqKyHPN<5+=%+N z`0`7UpPA~jGfdly3MV*k(9)OcsU|V5zbwgNs>eB55Vmi3lZVqjdduTgr+%>$^Y(wp z<9oH&)eNyDChP3CPNJ8HTHEwUK86(fiXr5R-#cC7mYoAfC-Yu4c?jKnALSe2Cw?o? zS14v^|2$J@Q}}v&!@9wJ+6p=D*I2I1v|4j4$jm%!TI;bZDMNEWsaD8FVBHmiseRdO zw70U4ddVHCw{(AG8+!lqx|-t~%v}3yEBCRyDJ++2Y~Pf+Ub!*m@T=TMHCiV)<5pLd zRcZ&_QVnM_>ergzZJ)OKuIm-sUny+79k0)xIVsnq_uCHoh;UI|{Sdpr>8zk*VJ}~N zkjoam>2*1mHs@=X*{Y#?txC2emRPS>5|8P*QjYSwUS9v&aLQ5E(@(-=O;+j7#8*4m zE({fk+val!_)&ZRqL>+Shvv*~t!)YLVv;l;YkIb?uhvG9o?h^}J07IA#fDw=%`vZr zKuZUoHG>VGzlgkNP<(Wz_Dz%IN!E{f+6P8I)6lS9NZqnNdArH3p%4EDKtR90%N6TX z72py<9NwJ@rM9*lVr+^Oc2J8WFLc_3LeG|IwP1_QmJ+G8-1Ib2O0}#OF+3C^OsG89 z`%bJ4`C$y>wA1&TIp-9sC1GEo{2do7C8@ZBAqrnM&_R=~QhFM8fd%7LN4N#+j`zLm z$54+xnJLka@?{X2gLpfB zY-Ayb#2YEW${_G{7825-md;R_Hk(9zxU4*BvG=6Bdop#FtK4KsE1f+PY-uH_8;Bd& z;QD!}A-nM1ekbJ4NAZtcrcn!Bpsh05vy?hmEMe-mgd#=>*Mmb{xgxMA3w_03dB;>N z+B&4RHyz;IBbR_n4L!NFJhcT_iuzx*23}MFa*L%#%vvPYOCKZ=_}c zJ{4#Q5wsGB@TQ!ge#y6o^b+s=ia2TQx(PHSQ#8Pyy^gSmnfVN+cP@HVOX>!+@Emx|Mn4I?33)U2(Vmd2QP^_*!D>icrx=kYFY#GX8 zS4L!j8bj)$ODJcWhb|`fd=kVE;M_X+!z2=cHO;DU$I6P(iQ>v zyw&NMx>afrg)Gft>7(UwXpPjxb2MTIAD}1j0YqCArV+6etll`vEu2!_4XO1F1gp|u ztea;k*vji6X}Xo-)_so@NgJ$J3ANZ~nP&&jiR?Y5bF@o)I)Vpzp9ULB4(VxOt;k&1 zG> znZs%SaGKWWBCJ}wjVi-!(Gx>4n~GPzIpJ^8=hQ;WWm7eSz4(fVq+`3n-#3>_zsAvU zGcYU;?Fvt-=pUh7<1W|5*4t#HDB2Adu^8Iw%lkYdy3fgwYt0N-*c+w&VUCQ*^q$ph zsR@h&^y^4iw`NgE2HTTjrvs63OPc;T$9d?DZ)>R@a$G^k`?_HDC^f4Tf97gXVurV% z>cvRrh6wivOI7@zr>P0#Pj2{Vy0|V}FJ@^6MO~y_rvM2A)DPT~3BW@9CDp<-$KZsE zqAfP%elOfkvNx7`X-dwg3hz?{#%vQiy(L>F^QpR?SGBzMgwBTS_%b1x5r}u%ROZR# zg2W1bA3G#i{rVmIin?8yY09yKoK&PY?|0D<&*0rKDa$Yx6tJXzeLjk=%V8^-dcL? zp4xG*&j`xZr42wft3~0(iIhy!br!HJSlG&&`I=~JK7#55b0%C~hx1r7eH364 z@{}6EadrP27KiLnU>JT3&Jv3WnP^F@0IU>sCqW}}oJC|wH*);iBapGyDsD1Y z43xN~H?QY_ia4X%?ocmj+XtcV(<+OZNbgo|ab{}*X|q+A)mTU*4f9i#ti|7iehoVp;>|s^ozLXtnuZ3bVW@WDq+z0QPrI^3 zl?E9ssB0!#`2C3>Y-*DLkTwx!epX~qC>faL_&U?cP^I)eFs@!DJeauCPfV+iQEZcWr*)dk z_Ov7inhBLF$BQGEl$TthPS)`M1XzYGG5NHY43nqX;76 zTD-JwF(L&^DK>?a<0?~8)0LXxAek!e6YvRkRie;YeEOtB*x2~xf&6!^Q{H1@xCt!nN@2eR2DmV@xgjpxQM)! znUQhx=)ug8IZUk|%>o@dYKQl;L*~&@q{$8);Pxm>b~Jn7;JA5kc(6Y+%Bi&5&^cE$ zM^dBX*~}QBaU|PcJEl#kF<`yPJenOpI6N^P;Eu7ShK`vxW{3I{W+uxGWnM5cni-?v zqwVZrAe;d(JCq)r=;th5%}bC)PRyYLsqEmyXakR-=V6eLnq*$nCPch3hUg0MfSDaY zSLuUt_RJcgJ!T%nRP<%=TB`p@mIY7t10=JOCd0A?Nyntj?ZfE!?U2;^jT^+t*R*j> zch`;`IK_Gw4(q;Wkq!yt9BW6%cKQ#G-;2lZr^j8L^j{bL>)e6=y6IC7|JO}`Ze!^Y zy!v63kMir(O6M+Wg)8#aEW-680-P^XwQ_o4U`2cf*^uV%J z*~TxK%e}O8FFO7_$`??+g!1U3(SyUdJ#-lN@44fZ9&YZeJ?HFyOzTf~$&Xz`Brx6h z@Q|_eH98Ya9e!P$2p;J4Pb=fR^#D%1dTln|YBvURNX?p&r^(YI$B$eHY-^lOEW33q zm%h9hhR&CP&eB&FjUzDFydgvc#=5731gL4t=-hGr8VT2D3}%OJG?u=)XbhKe%sYR= zSo)$Q46u1@(b(kC`147GS~nS^*yulOB`Y^q#Efg-!EHN?rMJ>7cJHMBz)WxFZ^U0+ zJBjMsjmH-kO?q}c@gy!0xMR^!?1FfR{~0S|gZt$(@c{hYoCk_ z4~)Z*Wg5@!`v?DzA4N#UnfH#T`M6<4p5{J2p&Ih*_KVcYA#Cv(L)fg@O6UBsHHMI6 z4tYGhX-Y4)8za(vb&VqnSY22+^)Ls2%_%x_#zCt%J7||r7!#v|6SI_Rn?;z5zR~a0 zBn|*DiA!QU8g*s=S$Lxi(&!*>AQ*H+G=stF;TVgcvGk{SG$@Z|7)yVKY3c6l*wG_? zbr}I?$fNcH_%W7V4-tI>%9~LB2<43^w=XJOB(dm~B*004lLVH}57WM~|HVe=#MpLt zZ!jbiKikIkcJ!Is4(tW1{bw}(T9h}V{4vU(EYcMd$G0T67>D;7V|$GQd-2z=CKs^g zHEx_V4if|Iyne@a{ za%FqZ@T@Urztq}|-c4LTVTwC0c4IjB$dLtnxm{GFRxk>UWot*F!N9@|FrTGOGaSZD z9s!#Uht)8>4$EK(TcZ6(8lB;@^jL))Zevq}z-%&Zs^ak43SLAn#*lNEH+}G76iSG0 zzqk`ubZqr(+B4Tg)}+fcwr-VYTQH>vtP$rMTf2-g%6~QwTji>Wix}JrcJ*<&Nb|FO zvC}>u57WcRB81782caSw>hMeYQ-6pJ5XqkOI6ah3b=W@b>_GCC@2mIZn5bKjirBBG$<<5-L)0Fre=XsbzKz~ zL_&oOX>IReu)KU_!v?z2!!B2?{l?OFAmPxqDFh1g@}dqC587+7lfs#~xs5LLu#;}L z|8CDtb-@R2_5h@l{_CRux)bzhbEUIuC;hg&#Q(B%^0&@*=n9~2vfcT-O*UK72lqhQOkvvl>bd!u}7-a-n0<{nE{VPVs zbshI=Go346N852&>m#iGQp)70PH&~t31U;lp+lV`lzNAyG16#q(-h0(I7-s!?&xN- zz_-ar-7C8{=em-((&cez7Uo7{Y?d-aa&rh&(eB1>Q$A7q`*%`ta)C!Zm!5C}RPfEA(D|g}P&FoznWxH} z@)I;z3-KQyv;gs>035v!anFIk$ds$jijc1Cd%uxED$f{pe`q`zPV2GwQ-!t*^iLMI zOC(XZKLA@(**QKBDVyINY$VN69t<>;IgIrX98oklEYH{}8jV^d$}04~kDah*XT>B? zrcFNw6g@i43-4%l(m`6a93weEd}za#JIN9`k*5mT;cH;Xr7~gJT@GU+U2#_Kv;;|G ziH#dZEn+DO?kLWpBou5dO_w=e6E%UCJS~p?lEzW1n0HEkKdY{=7PC2SpN+s2xsqwI2%M4VDm$3^AQMc$Gm9#zS# zCCn}4ktg8fq9Nmgett4d-)|u{%QoMKl^H(_Wy@s{lC(L@hk$)Dnoc=HqJ%CJ zgB1ru#7z3S1*D++v`o2#QFE`Bt1QAvBZZAOWkLM?-M|y{r-Jh+Y~DZs51gPxO~+OD ziKwGabfAnqHI5*&uLj=Zv~aSbd8=sLU~cY%dtq!I!igGlpCN|9UvxPKsW!9Ff<>+T z3I9xi%!&<>7VeTl1QD-eznH9eorsyFn8qx=D}y!Cf;he*;k%7tWafAQTJ*J? ziN*`HQlVr>VqmeRGWs@i%_P=n8a1R&s8t2{yxZ6;*+R~rViGp0@?dCSt z67-aGLaH~XiE)kJTzl>DdAb?_Nq6|qIM2UR@rAf>aRH?tf0X7{{JsO_o!TXg58?CY zQNDxnLzKr+F2#+)*Pv`i89~X(8;0M6-+zJfPLu~w9!2>XN>dgWEoZfhHSziR@;hDD zO@9cU{y01R!Sqa4T(tRF{QhT@pQHQ;H%;blGz<^rT`2#6@*@;;5SPcJ*eJK4+=uc3 zl>dqHZzz`^HjE_7ILdL9m!o_S1hVeR-J5c@xqqQ zeyRK;ocbT)Y?c>m{(l@80D$c}e|S?>mN!k(h9+$Sw55y{iqu~)V%kUAhCppCDPg1# z2u&KW#RAn*3uVfC2y0uSuR$Ky_+!S;Rmbrc$vKOqhLxL3jWvuaixMk3WMXz zK7?v3O|km+ocmHhop1KrKX$+UW_@4pyZ4@R-aY5sd+s^s-llrlJw#6kF+fpOLUt1E zDJTE+a{x-_+;vE z%b&Z*m%p}kb>obTjP!!!7*}$KPpw_@@~+WxYT-+}Zf5PByE?JI?8L5Ss5969d{+z9 z&Yw!V)IS_C31jjJC#>)>VX< zQZ(JzcvpOn?ppH0^MCT8a|=v6YRicY8nzwRLqq;FjgW9u(0i41Z*#et4Q68V$_ao5 zV>R?7bzA(}7w)V8`+`&D93u?XA3npc43%B&6UIlXbYA%w0E09k-w&TEX@2=2>-WlA zk%20?Ey5VDnr(vNX(D`77#A4jMtgnoHslvj1&dF92AbmQ7QdQp0Oax&^>wkoBDNi< zyc$VTOy8)OjDWgr{-#HAiA{BsLU+dv43?{UEsrBvBrnwc=M=db>^D?OKsefO?Ro%a zC}7>4S|ui0tatB+p0(>@cQJGN}&avjcl# z)Zmr-QkgDFNhmely1Ps~ZLgBwhrQ?x$XkH5lEdJsqHM47%5wqYm4^Tw3?@|zl@V*W zSyj~)Ff|&eetBK&abysV@_oR>=hc|XJn&)&U`R`o14wfaqP!%^>-qaV9&6V|7%V;q zUT?vyi6;%dfM1+b4m5->Rt1yXFaab^kXqr28tKxxINF@qh!w`k<$#jkfR}VBf|OLs zYW(UZ{OWZBeAs!dS6&T0z1!qvgGug@<-wt2`d;5+xn=@t=wt82_+GxOK3FTxPE8T? z>P+}cneHArep_sS3CFkSTS5wCndw7p4I zozqZnZj?`6eQl6trP7O$TO{k}QZ_c=tK)IS1~{gpZrL(~42W|)Y)aF6jBIYKX5*S}X75|FwUw<6E~eE9I!3ajfDi`JJ!oLl zU`s;vqfi(jc>Iu@r;SIe2Vo-tR}V^2^Q{FuuOBEUk7On7Ovm`DS*T&KR-LH>OTxK& zB1>CVFy7a)nCWacn4YLc<4xOZ#G3RNy-+RC>{#0~%W9LRui1B!sd_0Ast)uXqx(Ll z@&yK2hTR0)&4z%-(f9aaYhF$5;2*CH$Zr6aCL&b^Z$P7=8O%SW`X!ylkM^vnk8#oZ zm^kNpX2nkCqWogibk4(Qd}%*0wBT53b{0SxH%g>b){yM=;2#a>Ee+vld!Cq>*fD`- z;D^>{_vtgxODvY>gFL}loP!F@q{Bk^OT`WoX`c-5i5(LeZ}2wOXTj=?_)StbaItkU zUyYyg1vc&|_nB)z7{f)mcj8t^aJJ_JLTByP@D8vThz8qDq2W!uF*IB&_-KC>x<1w3 zo-V4+Ekbuhqo{We|19dh;FatcOVQNKKtPxip z{?V)*p_)kHKvDNeAL=y2E!F^HR$*>pF9x`IzgX&Jy!HOgE*}6GpJsa2|v-pr+Rp^ zN0{W3$0R_X@jkgb11vs69M%bAeDbqrLDb&h8Iftp1KGP~Fk1Pi-=HE*Uwuo<`M{9y zv;Bc)Q{?$v_#hbR=o@&_CszV>Yb6-_2gO8GQkn+t3;d_Hec9T@23Wng~`JkPQGCF6h2=r0t&C> z9+by0e6K1DN;MJPIXtVAzA;&yq?HYyw&2=Nzu6}65_rYuGz*P83 zh87nEitzi0Kxau~X7`AYDK_>Kv2l>lz4Wj$IXUvrG8q$y(^AbK0t&4d04*Ws)z&a3 zx>V?=9fNT#zn#tBC#HV{a!lt43h7K!jAX~d&HV+A9AeZ1&mYE04 z9V5yzZh(Wz2k0^(1DVq|<=+AqWwxthq zR%kmM{ z?3KS@^#RDIsQW~--W6LRx!>hYt^sS;XF8N~WV_9}`wiMXJ;nz_c`E-CQ8x0Id-!Zo z&asBwKqGaW=y4}lcyji*PvDuGaqL&7aZzVSAHZ}P%z9t69N=KCAdh0v1XK)S^$C@_ z-vtgAgDtKCVrSh4)IG?XrL9@8dE#CGd6X}xTZRAu3w2S`ufUm}JJ$3|kKi7Y9zkMS z-504cMRuFOOG=qQFFk_WG;}$c{-*SZ1?lPy{AKg`^!fbQ`PT3tOCEcZ*@G&aOy)oF zqimT@I^D9h>rc@3LoUL$70}4P7{Uk@#A$jS>9#%w0=Z(miGD--LBdn6ulQ8yQG`$8a>ScEf}bWK}US`s)tdSJA;X25RTHjao(+6O(^}Zy(UaCimP``WUGlCFr6??#BfKDlK8KH#n6E%Obe+x5x)jR1+$J+e8e3U6E)U5W&ptY z`_N-kLG*&~T&O5B9G82rQxDcAHYNzVA8xUAFXUl_iOBRmfc05(MPtuN&c~Zy7cI&J+1Y>DHC)m37^A_mn=54ob zGnFgHz}{f1%Dw>>#Nr9m|4o(E&DkJz4oE!#q|ODYCoYfo0X%ee8ovTEWblh;X9*3^ z5;DPEm4`~p1f$f^r(|orS%Mp>%6r^>%nhNZ!(F{`0x$)a0#**JlbikoCPZ!o#NQu5 z`{+Y=7Jr+8VYkTJZ1H{2(X15iWUO6x;vJ*B9VM5x;vP{tBAuqtHAEOYYRhX8>53_} z;9IT$Hm;bo6*q@4761ho0QfRGCm*xdNAN?R`g;Vbhr>~AWMOJk0p8%K*?D%XzKb@9 z3k@pW%RBT0c9+diBMJmY!Q^3pah^ShmsBOR;Z0o z|08_63fk8~9f5k~WH|ff${8Y=-v*XnTQ$JL-HC_ilTMKk^c0Xkr-=CW)ORs*p>o63K1W-IsDMGnY~; z#l871_EqOWI0nZSV`k1peKpS85a2|H}d7!O=pp%sp`Y}xv zZ_I;)(AxD!Oji;*Yu5{`kz(!oC2Q!dU0dE|V>Yn@>@O(i+g#H){F#s>%yU zdNtb%oj@`_o-VnR%WLElr@@g{k6zO%Je+&@b`QVG!?KE3FwAr8gjjR#{1tx^px>X@QAj{7 zRCGjMh`*+ zk7~q=!^iO@-o=hC&E-va``4V_3_%)Cv1)cHZae04_L80DFMm27uXW&-cQ1*TPv0lT z4U&m6bn@_+CpL}{AFVU_1Dr}JuIoJ4pV|u|A|?S8j^ZP(!yP8-wM4S+R}7J*IjR`V zpARP);W!m7%!q{Xa8Nk6=NVA3i5(+^84(`X(X}(U^A+R(;a2JxwBCt^5qZ8G>GhmC zk5-Lv`&~U9X4+}${xn4rOv*~kKw)jbzx05YC&;eHeG>om*yDy(Q3>yY?F@NWB8AWF z0udpsc_H=bsZQtX7yg+N?H*{cC{kYuw6Hz7WT`r8MOvx460Tz8d zUZuhQH0gHWAzW}Z&1ScM&4wm+W0*Y~?KHaAh`l{K5V->{4%u}qyIS6X%l*VcwDM^A z9t-}%RY%ycG=w%F_SwGG(|xO#AKQrn#dc!48{7FXzBPOn1EZsjK~5TLwm~sLvD7kx z&^8<(q{)ASLHnww+<`Gln*(4=EMUM$FQJ zEQB3Y4!(czKMwYqj|zi;?+2ms;4%+9?9gBI2ir1TKzEC&EzJeA8+8l0cj(?lLfX)g zp5oYcMm6W2B>lI6TeHW0~zLjrC*GCq&W1r%r~Tm&*Pd6=|w;p@-o9`2n* zn-?lRCU6$jRJMFa;C&!)?!9a(HEC@iZ6-)-*=U*$+Ft~-0ks8c+oH{*g0J}QY`^tZ zXt;bsc4#<%L#p-3J%{>Y^Z*wP9h-dUcnoiDV+^08;s5!-p|df%r~B+=BuK=dLHERd z_?PM5)st69$TTRWP^zH#p{#_m4oW+eZBQP9@-&p)P~L^o2jvWu)FN$+%aXMh>awEq zW9yPX8_-=)@Oj;So+h_Q`yGO*@I0&tV30Nt+Xb+d09#=H-__xiZeC04%7&JT7W|3B zm&smLTVo?3UnhIMHU9v;HM)6?{DOuJyt}QfwT+OM^(`CtwpHslR}k{*XnS1+IZg2E z1#4EeHMDIeztb=cJWxG=BY4|dS5>TD-PX8a1Ef?b3tAgiCuxD=7Nn?dT`jaUdK%WP zZfOMA%_-GF3%};t&3xn1*1($8jg?IeZR9mJZc8IDB$9pBPY<8v<=g_}s_0^j_(Rc)cjbX*Q9TEDu2c1to>E7ATXn3{yM&R=A!k)$G5gM{=fADm%9<~Y*=+Owi`7=CapIUQt^YE{QG@i zKjFQs(Vl6aYA>;u+i$nuYyTJftM)(I586Mn|HXdV{-yn_-B{==3>K~|ys7Y+!q*D- z6&@};R`^-rK;eX|!|$GN$c+|ArJ?q2R` zZYTFH_dfUH%vWX_9T|=+N3LUvW17S6nB%B)T<5snvCNiVKTNi}OlLORp)tz4V^a7fW9)-B)^~^jPT^rC*gAoo1)aInC*JwmKhj zKH+@Y`KU}&)PGqch+aKhG$i}VzZ}}Z7d6wJyh0yjrK#hXP_#vm)T#k&nOg% zJ}COE=ikr`K8Qtaq+*HaXWi+nl#Je=yG&2Hm15CC8;K)?Zd2+*hKGxS6h12~r=kI}16wNI6A_RTNR2iZsRVFCH3 zl9H@Q)XZ>(q=8K;WgW+I#C#t9h@`~*sssC4Jzf8=>bl{{fB9YR8vt0hLBCny2Gh29HN08*G>zMa zokqBQq`7!Cyy?@-V6M&XwNG>Qr?Qme(LPP9bppq9&n``)4d>DoDt($-Ch)=R)6_EA zU;?B#*^&v638vbKyC8UTa7x1|Q7)Wif~j-@SAihONrW#{E}S~0 zp_HRjlI>x|wj@*T44DQqhfY;67Y6|(IakVYAEJ~xVa@D!qRhb=EuM%H`3M>(s$9RR za)OR2adJ!m)>$Z+5~ntp5+@f-fF!408F#9K8Fi|&T;NOyX3Uuq%t$>-Q?kyqVDipX zCgV&DCfCjKAky^HxXx+a^Zv`!JM3C z-4RM*8WVFSDwj^IW2p}2rH?3*WM`P>;oxkf*tM4cvb zZep0j4~R2%q9>V;pB~OgACwqqA|sKRlZ8?PO>Dq4cXCj21dJE!zzK?hCQz+J=mhZy zj@UVOl2B5F+ZRp>x~?jWGH~Khtf~MVP z#7EDi<8=gK|Cp_1fs-wKTrH#f$<*H_ny2U)e{h0eT*Nm|h*L1+;9F6|866rSJq~u< z!RDrSk6x3v<%M`Ys0FjVR z?~9$WiW7D`MNXPEE+QPEq;ToX>ZD41LQvkrJqa66be%>L|@AI^*b^f}Wf3I;_qq zx?a3FH|OTuoSSoVel;g-&QI%v&*QY4^XG6rqg^te!)eMp7x#k|a+>1K;J&pY&fxyK z5>ERBrz!4S-v^lQT;H#l?VKf1$~jK~zmA}GE{-qcTpVA-85~~385~{08J3y?&efz- zzH>e2lC>g1-DQ=QTEn|fz_{ifa-U%#n!O4e^`os#vNN~dW3rp_r_zo~Kx*Kewv!u6X9 zr+EFQ!YN+ADR;`(Z;G9=3*aiJX8oqrsad}%bn4b`ik!Ojnn z>eSqsA#|$lAdxwB_m7C2%KKbIPUZbL0;l%2A&FC|lsMH)fm5rDJM~J&X%d4Occw5S z&J-o%w5dwU3CcCQQm0K+3Z3dS6|12-cKe}lfhR1rgynBNV6ih zSFsbyI{}J20m?gpa_)}Tn}2H6PN1B}?tVR4yYISI=>#C0&Hd1v-Sg#eJKk%J6M%3w zUETj?Q}1uiS}UADIA0I9L3?iePrK_`>O>o{-r2tOvVD7O_kB2Ar1RaL{jXi?gG0WLj=f3_NU1~1heWGn$y=FQad++N|ck0V$|M{Dl)2f}v>b0(& zwCUPib$dkhK64ti^W9TzSHHI|?{Dt)q5t@kGqMf^b?#0s(|1nlg!OHB-A8mx(-^(e zUhMwvYLRQ4I`4F9UkV;ho8qiqT33m#&CX|yulYW}Q+-FT$HkqtIoo~xzVAloyDJC} zCvn{{>U6TZV@=LoeVE!Mlj^i@_k;a#+S_?_lGir{PEYJ6XLUY}PU`w53!R@gKb*~N zaNcOAi973q+a~90+-c+StjAv0;Cwq3aK3z`*|TruoqIt%jnY{OoL1_*+kqx=8lkh^ z$(>f_JlSh2jwcCSI9uPux2~WzwZv%!&eo3S;<0ZRb(*O2KGgbd?1~7SHtMYR-6Qj@ z&}kyhdUx$+-4Kxzmrl;JoszV2C*A+_&PQLVJI(v^Q70ySI8Vn#mdl;=_~_-l%yNa3 z9B)opUfTp`usy~{L6&Qsi|uhfYR39*3p%Ug<#IW%#+zAAe7u?I#K*;(Q|)BuZ4#Z_ z_O+eDEp@#iw{6bNxj8rI|HJtowGjY-?K*96Th*0UvTQ4{Y%9b9rw-s0he8q($&#&a zW5<$X2RnI0j+_Ab5S9{2$dZxX%SYEHGS6yTUiD zH;$zH55BU`MYR9*ub|wk`&x0-nXCDP0wDwc_ArWs)*3H@4csyI*c@oc1F z3ymXQe6i^yRP$@YN_zAG1z^TEOm7q+)aSXd4-0i9K zinqej$x91`NlrI3754WR@W=gLiPK2B3bb5T@)F0nG0yow7R)-C;?6TEZc?gyjaugr z#zMcAuw_G2eT9Pid}g80aXzDwdy|=)gpNrF(t;zI(xIvTV!S<`Q3}UVI1LF_gC`(y z!B32w1~=Oa-v!3ob)k_;u(nV-ikUj77D3sJ5;9wb8hH<^VbIEHxLaXyiJLe47LbKW zD>J+g#bQ*zN@X0$c)BF6LgKWj5T};vPEw)Q!CEj`$a4B2iCdEQbSI#tI$W?z(pmI0 z`sYKlz=`7voH!z!*tBtjY*sY29b5oQnfZQAH8(Gq+68$=B;96t@&mLOHD)TW65=2R znJmPHe4)~+Xrc0W`K+?5e6SIA9brQSpIux&xCrVKpIucxI0kNZ&Ve3V=d6XM{97x4 z%vle&+e(}k#0=E|MqZ4`dk8Dy_^p61f5{D45J0yBLweLB_CvujO7;`wgP+4}VXgMa z@3mX(?{TF z#Qb#scL#_%uUfxI}n+@!BjP6l*7^nl%b`76hFLGS?BzsyB5YfcFc_PBm+_Y!Pzh@K?po<@+lLR|=$*KzBM9b9$p>bz&-Q zt1(UuW9Z7sW-5|&exO+~a2ylNPHEyFe4$><+%&s{{>tE*O4wv_n~PNUG~CcFM(WZ) zVM#gB`Y9T4VhxAIibX>_2`9@RAT;nL~|jn=LWHvj3D{arCmtb=N5CP6H;$_i^IbPCMZ$!V?b}pAEmkLAopd4`g`s9sd}9f;+y73?IYB4wewhqZCz*$XonJ*$gftz&_OGJB=Y`CjL6=MeMjjM-_2)Ar|~m+;nE_Sos5u#9g)Zh*eMn`PV8o9P^6(xZ_Mh+B_*&pQ4T9 zVL*AsV3uxaW&DUbcMJ}JB{ss8l~+$;XM06ECFY9hkt(BtIgGfQk_)aH&CJ3KrNt|% zDF;!3j+kO^0v>aN!&M-*H@^q}Bc6AUiN7tFwyka-;Euuh^i%^gOM1=kf~RDYZuKzn1S; z(7x`OJf-gGEBv^ZHp&N;u!^pWUf=XO8dS9(0}rY@9G%2NVCMZ;<*3NC-oZBc6rKg$ zYA+ORIHN0Ae^|NEew=0atm+XrpDlW%h0hkVm3&ssw(;3noB=95uWlSDC)!4mc>s>3 zfPT5i%@!u#P^Q!{aWfvCG$bc2t3{pAXN%Q~+iTP*HH;18 zCgTNXIt4w%h#4RB2)h8o3A2!vUnbq6V&vyNJs5{h5z4$NFxC>f{bdTkNE?r;VWly; zl9kT$Vtj@hC7p@x1ayC!1U?$dZw4=tDxxrj)*nM|eP5+;CIhtQUG-_#k++tYABK*;{|}jO7^jk#?&1nsGG%R<68_?>UFB(2 zGOb$PRDO64|9kz;39ydxv(b(Bql92bkJmW0rxGO$rCSwy=3J4`A{=QfKdk;sWc;0~ z;FGIlJd&4@{p772Gb-adRL13*vR07s0?KH5F>QL7l8mE_?|_US;8WhLMn+r<305+R zhkyAPKQMuBtJ3ad07yW$zYtqSB!8x93eXkyuT=s}JR zBmn!OOwrRDY9e7}2N!XLwh{6w#YO^ApwK8=1HRstcpyN?50M8Gk97wU3B-am=x9>} zUrlT2{hEN>`;%ftB=(>$BF{XOmjqb;AvYq^-Q!-f%H3|PUFwfS2(f{NQGW;U_iYWp z>WfHo&=+3?ujZTJQ53a|gh0tPpdxu!(c$ZLg?y3NR@%Rd(lYVDwkEPNCuuM6_&#w3 zVDZ?_u29q;+fJ%Tvo8{nJE%+(Y0ud;5sQ`Y!MPK^ZU`ssK)8=XqCD4{T z0lz#R+nxxt#NsWnh(7@C0-U=&A51FJ9P8-;bGvu25L5xbJ03^gB^XB@-?ji|!zy17 zN;wO0t9t{{8My9Pk3X_CFQFf}b@~zybp(27NXH_9{Lqyv|IO?$e9OW|+VULS0ewHf zK7jQ%fnO^LxvdH;4iE?U0N`Hup4J3l1!x2KBYZbn0HC;;kh@`gJ>X8m{9gfl2rvPV z1yGsc+aZ7s;1+<}0hR$Y0(b!K1K46lsTAq?DAAXwGj5 zs*xNqxu!W5h2+U2Y3Rzga-@LW)+PdR7k=%Uj-S=h<-YERpn;T(LZHA+Bt#LbVr*y& zI4=-^NJVSQ3+EBkkylXcn$C7?uB?Eu^nY{z56HnqpsH1)T8X|zOBX>u0f9#MKTn&- zRvk^`%vs%wsT+vnN8a8*oZX2u?QN^>!Pe8+)Y;Zd$eOn1R=2CIb5nDBlg9&n_*U!o zhgg}`)46>0I?}wlWBF>lfeUG|UB0%nbM-1h7W%KB%l#f87ge^=@!( zrGA;-tiN0TwEj8$&-ADCXZ509VJI!! zY&dNAwc%q!#_)+jGL+VSt@h5^`dVjgTdlt~Tx&Kq8M}>-8h0B{7~eL&ZyYyjP4i4< zlhd@y)M>iU{Y!MZv1o9jdMd+Sfve^5Wqe7kv>*<^N_ zSDO9iUNdKY#LSz&Yu;=A7xPcd|872JK5c%-{2TL_xzIA(GS^aNSz!5w*`H!Y7@MlBPTtfkPZwccp0vM#VLv#zjutx;>gb+7el>mloZSjVh?w5n`# zZFku0w)M8HwuJ2w+aBAGYzJ*Wv%PBjrR`U?G24XgPqs_8x%RKwSK9mSkK3QKpR&JW zAGQC^K4DimY8?Sb!tr;G0mru;k2{`n{Mhl5eS}EkE*@i^T*O zKqi~j)%C7ZJxV?J_wGyoUcPv^TwSz9u~V8%mr9pyku;`O{nV6u_;34(^r|^7T~}JxsoT76+N4lc*LhOBe6cch zqp~``)wNpS>rZ)=nL9gB$!(Gs$-K~NQEOeSRa<3P^ZxQo6?v*FtA{UN{M)oD#cpY> zlSb3NZB^uD-sntKZ8^sWW)b#08lNlL54$FLO>AbWwe;Cq(G%4em71#3X`@Vq=c^UA zN}AkM^eU-LbFFJ^dy=Hrftq}}s?BYlX{(w$6JA+$mp9ic(~G1n8dW7F7T6}Ko4i`S ze33Nj=;G*Lj~T(Q!@Z#*shYM~wGC}aT<{(cD)v@+MK2b$DHZeLnOkXoPsIS>VA7SwR=OwHF`-kWE=P9URN|EJAsFr0KpWdW6CThUT2r%P}?-ki8# zt~|>xU-(_z3b7H|0^(ZMNy)69LhbrKDOZIa&<<8t*>0^<4Wm$t!w+~-QD#MhGL~KzNH>ky4i)V$mfUh`B05WCJ*=_P#PVG4%!%U+8uQb_TdIu z(jYWOIYsq zI;n*dp(d6H6MKI-b-Vr~Op{;5EQve72y0!MhR+LA24h8j1G{G9T!Vw$lJBud3X@WV zpnbyXzHL`RYI1s{_V-`?V|(a>(X}b{Dp_idBVL}+!|!A%P?XcVd-F)`OoxKrbarrd zHoABphdg?7F`k{MzBjr51*gfb|7)siGSU6Ly&uD~e{8=c?hk1Xa$T%13MgwA?i2pl zP`-Tra8`FsB^;~mW91X5((=H`&I5i>^ zGFfEUOgG ze*<#&%}<-_N5U`fY0aYHoszaOC617C-sw*?=?I}*XWI|S4aH$8aBOB#4vmr~A7n;3 z0nE9p3m_{9200K$*C6;Ni-s*rZ^)B1;p+sr6(V&lY0!3&cuM+!2eDD9OGLF*<815I zP@N?R-On#1U#pw6rDG|zKU62x14g{L4mdQqFdJ-F;v2pHIef6Ac6?ZKr{nXxI_I^j zpkrUwNZdsXA48rl&AVsF9mv8YP}TE6W=G9i%Zx;!S;X&axL%aYsh*+afj>%+LrQ)~ zE+(@{fg(7SV$c++6ISQ-SxFR=*lpAj9$QM3To~u#oFR3w`gY;bYI=T9;QI>Nzooj7 zD+$zYMZ=(|8g`cSnuTPqIR!m)j!YE+K}a|;l;^Jog&lg zJ5%34`+F4syo0*rgjf{klVDg=Dh2tVAJ!{f&vaepNbO!;uKaLT_2tEf(c;$+%gCq{ zit@r{J=xJ9)j8olC@N7_UOb&2aRl`y31|n^WI2UMT&_vGl@i%f`hgI}%s zAN67UX*RZ-T4A;PiV7BK8BT7j%{iqS&Ptve@aA*+KkwBFa)7Iqzrd4*igH8F^%G?X zPhqGiq*gkr7f>4Pg>OldVBICxCmHa^z{>7So+FzVP%h;WNePAu%9^b*r|58twHe63TEiII!lmW9j(YS;FFXM{hqnh7@z;8FC+RF{7p|!_9po6Aaat zmp!+^J{Q>M700!x#E+*_b;BL-zC0P9zkW^~T;x5Z<`4b7d3Q7MI;t);*ynObNF8py zoa7Q@FTl$?I>i^39&Rpu?IcFAcb>GUH7Qp^b=2*v%xG~Ts}ap0qB24HNkT)G>bKJ3 zQTbflC2Pw&H+523ETCYMNcgvue_>fV8>$cHj%fKxd1L3$jEelbg8GRiET6C!^Gm$= z%y6)$d`tY_(9id&=cH^8C-2tUM)KZFiBhMxX$l!Z_lUA;#5D%>>Uq(k(72;mitNE< zy%D$yjPcIw1niebTyK$!^?ST|Z=i<5A*%NGx;aa1b5hr)?r$R@P@84d$z(E~T%C^2 zPOjjDSEu8T7so>}e}sgx1z)RLFY38o_I_YO9fY zl)D!6`7rHLWU;zlTk1ms%IK78gDUC`DNv|pUgtVZ1i29OY2K1^GW6Ww@_l0(FRE0w=GkRNiS7>kT0 zTzpSd89C!J>JAM(W6K*Rs&U1JMLVN3a% z&o_3`ut?9xFsh@=5sfLbyGcRHU3c6ZYiI3WQ|aMeq|UxaY?-y~C)9heH}KKNHy&j6 zXehR>^DNWVmaha3x;Iep9bOoj3y{glm)J;Jc#=-XU!1J+=D%V{U$>q6v}i57^&IWT z*Ujg$XqTg^Tgx`|iDWcl)FFwC0YPsq^?p__>6>by&L^CZ1l*1Ve_WRg^_B_nz;nY&tSAVxK!I?hK+ z+0u0g4)gPXAQTENz<^mIG*ZnsYP?x^%QlkF*h)Ed2RLoMmgo-AL{@O*rac>kpN$Ne z>OEI3_xvKj4*SIEu1NIlQ0*G~_P5Kc-+uhNBefsxD@)<|WGvoqJPN6`F~~ObL+#$G-9@}ptZ(UmyjO$>4rEhDUQbw=LrEph_m4H z&wSSie=Ee!_R_W6yN-lcEjrykusM)gscRY@htzYtma)8l`IqSAFCrKxg4XlOU8w*9=(ClJ>P zS=A3k2~ydb^ppRApR0tR^N=c`w3V8pD0TgO7dSFXxXduXWQLg4XnZs~yPBR%Mh9nC zXVb~~ban-(>SQv5@oVkXjsrWY@F z@pK|{tveP1-c0X+i>K5v3ghBO=s z;$hMoBATAeuFl8DC+Am3V;b|HNp%u0dH-lhX#OKsQSvZTo`IqI|H*q}$H(&iM1vy_ zs3D$@2*{#}!r911&O)BXJg^?fGnqW0E?-e(K=$QfA(bXFO$$_U5_G?E$jypw2xoQK z291mBxEX~s3sBg$JF96nPkIRv~dT{gav-^@0al042e>lgeG}%`heDKVGiPO z8cNOFEj$>E!{KkZAES5Ieo)lX9GUjtZ&GRhq&P55KyvvFN>!Q!N|m>TnGPbtR?LbY z-!e=Q&Df}Rk$R|+MwW3}Ce1|nip8VxFujlYUqaSam7<)4hgHD2@xWS=J28cs2VsIC z+A!8gwT2(w%QwOQ(6M#>v~+FG;e^lRcWT>#El*~%G4DXXNBE6^1&Eo`O2i{7Bj2k2 z^iE!gvU{7zUHh4Np0Z3cvluOP~m$M+RZg63=)+Rw^8P+ z6ylt(k!=1FK6advl~sbej;Xz0@Esh*$l_}+-7EcjJr^e~!d4!s2sx+9G{D=*o&+Y(|Z4hP%H2}_Ylc_m1} z0-SneixctgX!0vaND{QcG#U+V25nz1bf6 z^8fpSPgJ>wD`Ut_Y*9#jq#qMT&85U!vJu+ztI^4eJVwsn$h6x@XIXJ&4~5E2jATv` zo=11I6sqp!95#BY4^+%r9_i&(tGB-}w15`!B_Y2tKtppdaAisor~y#4l8p%b{sq1x z#_T7i6{!TDbg1J3Iv|?XxbULcQn}lCfVl{|XE1wj0L^$L^6`iYJ((XF0Ft^eIYBj) zXLJ;k{!;Lrd3v`u#Jq3s)ga(zgKtLL4!?_xOtg@yHQl-lxH!@d*<2r~y=WQ5%rm|$ zN<*aWa9zY{)ipSR1VanST#|y@ei&DmuH-6{`@C$+=pJb|920H(ZUlHfAdk?bWO~J* zIp;+NHRKA%Fuo~8V7o3B>^wZUq`3uOT@?lI@xQ0$aTRiHY*+&aBc|R=qswJ<+sCN= zzwN0FIMPL&5!$t!aKpjl0VNqXR>ChZ=}vDX60-*AY8ow(BEVg!1+}HF=+Ynok_h}3 z{V&vA$CF+Nt#VZs!kfV@6oe5L8Sha@2{EUOL-@X}5)5!wjobmI5=+Lsl0o$SD!{LI z37p+y+7IsjJb_ik0)kfgP!&3*=pszG`$}1QMa_V#@|i_s=qgQHQ0ai&uw))_J!P6 zIQ!YJ>p0`O4eh4rh>VEtOXkf}cRK{;Px!VJuLict)9%rhRN8*RmFaoA?q zsX?z9&*SOt`^E(;kFoe`cLK(9{Z8*oUog^bBwtT@UdPj+I;LU}lqrGVREVlz9Dn{2 zW|W-2r12qt39+fW$pgG1rE`&G@e%Mxq^BLhey~UnYweImj)ffwDSUqFKEcHyzF!`` za`@mxFX^JpI$Bz5zAoyz?mk}`;yaxZWw)cHD8>S)EZ6<-k_O%m&xR{f;8lQzea{^r zY7fqtuIzG0uJyx>Im9^LlvCj6QUBd-c{qHxkHrP&`yDxR3a{|cYXJ$Ol zyzK7vZ$I^FHm>2Hcs%ODrxV1ROfrpSLc@?I0guTh2pQRML6Un$=KC;gN8D!-VbLBC zAooRZV4{=hj`;2r;p>7dkJP(oGNcDG5j^CF6#ug$YBs(snUL_} zUz-gl*v2&33r60N)~nA!?DM}RnJ-?onvFZb4}s4FJkpjEt=SlbR7$uT%|~}!Br=nH z_esd8Hw{cO57Sv2>&i=3I6V`{ANsz{wmAu|LV!rY z9}7X@<0o6!^{%cd*_=j9%Y;*p$GYA&d-ajz@V)RDp|MZKOeR5`7C-a^7ZI(MxzzPI zkbkM5B~W2tRib3-8yb+(YCT3`fg$o#Ve*KzYv{TH7D5;YqJVR;J<~!|c(a_0{9)O2 zeY~gco`vTbjrSYs;XDu!9(H|RojQz#hsy!}1a#2HUmE#?ep=f+PGDwDLb^p_tOqaL z`wYfdDmSy*!RRP*ijGc1zTZnoj92jL2GB4%5u7KhOjN+*7^+fm(TV<~%Z`fV zWgH`SFXLHd>lt0w(+=>`BuIy7?Gw8gWYxS*1+b*SR8ZrlILq@%@q?|7!^9!*)OZ3L z*KH0RD)j;3>G;SL?=cH~(0ayqiKGs|wKHUn)%B{7b^XzT_oa#*(1Z1N7t$lH)dVhBsZWZIIH(B% zJI_>Qaon9S8R_mP%br^|?%19#EgO*u70C%#LXNH3a6WdO#n4^=&=P`+Y$YlKE!)1e z-TAFO8{Xu;-@^t#92L+owbfI>m6BXlduKpJ5k7i$B2|fJOiH?CI5~99wm!JlA9pIA zKQ+CjY0gn5EV5x1YuYNk>6xo32WvCA?f7AM{S+uYaL8gvt;{nLDV_Ze8-w|&-|^>L zd#vdfHEtdg5yV?r-wrXg{}peldl~Z8edwl_+S8Q?C+91JnTSh4iXBqAGLV5E<+7@D z5oYAiip8o-^zvuL`Xrg2Y9rj<;?b;h-b6Y1#em~unqJbbI(5E=j4vms{KLVrm2X|M zJpjUTYin3z@jNS;2xv&KRLG77;%vKM;It_yTPSK)C}m@ytdrtk5yfM4!2quJ2@`O_ ziD3XTpBw^8D2elsJ-QBAD?5&=sq>#NBU`l;`#(8>0D$ei3w%`7^*?$hnIr=Y%%B0I zL>)CK2x5}SYcfelNG1=-W0J`Wc?2YhlNdspoC&A|0+V1Shq1P`iY=vRvBk=-rPeAc z#RM<`r4}qzRDKm(+MaaKVx=KyWbSwEeP%Lw(E97|^S}3g?&k*1?0xpx&$ZWHd+)W@ zUc2Dl$An-(5Y%|QUP0I|@V|87-~X))5`<|tzdB8Lanf&Y*{{m~%`K(&x)wuI^V<8H zSFJIuUe(yRRy5pKV`z3X8tNJiSw%L(nzhw63#Lw;93}k!fB$dz`{r$TJ^9w6%p<-> zQ;>h;P3B*8MG*2;p~C5; zI~M!G&I;31Q&eG-gHRCs(hy zeuw?-N4{3_{1N2_PXK_w2twQ>K`^|IJpTophTa}CCQ#2v)z#xK5NpP03R`jPkVZJ3B{U0lLaR^%*CI3u zBHRZ7n`|LN$P@BWtK%c(*-((iS)&mu>V?;-vK6j)Jsj^0E#bwcDbp zu*x}TinC24v|Vfe0=4lvH0AQs-=Z36JP+H-<*CoXpB>Sk9k&&q`_t8WgG$){YOzXi zWiNAO=k8A_BX7Pd+uEO3Ka;$cQwA7ZX^T~r`VSvqjLG} zJya>RcZLSv2#9Locm~>3Jq{vy@a3SzqL78(R={U9gHJQUn;5oBq+5k>9wM?jwjh+z zZzV-xK>V>`2E?vmxr+E=U~w$?;s9AF@%xLEvDOTKqmoYp{P&|3jY0uZRx9~ig!O;#dVJzEL1d zJvQH(ak%*J^8R-z#+OmTaHSepw-#-r(OQa>G_uwr7ZKP*t!`o88d+abtye2J*79%j z&10zurmYj?K5=ei^J?^uoUhxD|1QP98b2BEQEOL&k_C<*f>7YQO^NluDEwW9IBQXI zU`!u9o48CHoBcC@7fp<^R4XCTUvs<@r0-{~` zyFa#wwrf#EiWBOxsj@ zN*Jw=&J2`cB`Ou1mNMp;54$W!bseryL8#rFbf`zyxe;DvLZ0q*bl5wP#wyo7O$(}{ zSgh2VB3zaphi*Sj`1alSI$*_)0VO|%lCd^>n$~PqZ+rwC3S@P7#)*}uS`!fRvkuJi zjvoK}&JI>f-L{tPk-h0e*V1Mauy|`fCDyH=Q#wqJL!Yf>v+7@wfums zw%>B_2BE+63<7Dd+uwN>K7)pqjD~|Mf!cUxgR$1M{D63u*Kq*wLczS$$xeT3=XxF6 z>~WOJaa3;IB?zADc#EwBV2TYO%a@$2n)N}IuI0O3%U^UY-|kwz*R_0qzhxioQCyZ? zF3XO73)V2gW3J`LUCU3nmLJh=tH;OsrL0x@D_(EC7VYXZz;|!GdP#r!MViW88dt8y z*%Nt>!{fH>c6CPLv#T=-zi!KmuFfR(n$BLgyE;qR>oWGb*VWm?Ue~kN{jSb!?DcW> zy3f_QhrRA&ue)5GN7(Ce_PWE>d6vD(>~)W;bC|uVz4UtA)fwTXR|9)J;p&WIuPOLN zmmEQtSXn?Neea~UQOERmM!;7IXxjQaw<3|^X-Q3DD(Be zcY^uOz;~MYhTuEPeBmfcX1-|nhM6xPKJ6r5mlIaWe`)7>m~`dy=Ya`Q`W#pG972-AJxJp? zRW9Ft(d*^Orr9@4BCL0u9>@>oE9Yp_^4-8Utgg*iEp_&6&?$&E-@b#2xY>Sc5*v<4 zBwqmbd#?2_gu^G(LimjzPy=2>X5!4gc47gklo$qkK4N&T?b=SE{$%Ye*`qAkb|u-W zQOWiz$yNm>JI9h$uw;9cWcQ6qrk}!--4~cFeF`Nr{D8*s{&IQlb*$Y|!3kGamq2-) z5Jy@hvdEwBP~seMb)98#js(Ox&f@%b2j7z%Zb00PbBsNk`wyVMp09l#Df`-c;GfQX zz3^SD#MnzQ@JD+aZ;op}y~E$%PJ@z|2`t4%yo51d8+<`*qqD&mb3Hu!)V%EJ%`C8@ zLfQH}(8k`|yDlS=#_QPQb?opuc6lB9D0c61rB%mWU0DR7<;O)|XWw9t&;oW-{f*4k zQX~KNeS+F)V3BvmHjaGRQI2c(Pa~qbL93wB6{@)9RQt-vO>msKk!nu&v7xeo*n&LM z)`}6bs!eQrMZxw{NblYlSHV7HZ=#a6!H>PNX%in3H&@6<+XTKbdmSG81+5lTKcucp?pUNvIry`zfMLHUu#L!8+R!Y|-ZLfltw&3Tr!05#86FZtr z%Qht?nR{sy*YxM20Z+uo)Z z_P@)MeZiAm;E}pVk|`n2Y#>}122HyKqEAL7&JW%kv(ye zWk_;FRMby#WviviIYipO{QP;Z}N^w^<@i8wxs*nVaV-=;!p)6@sK*F`UtLOXlJP^-KU`ETxP!f;#c@JKmP z2=y^ke6^cmpBVxEnY`gipz}HfovS!YWn_;!!eeltCI;YBG@dHN$j84ThV`Y`rBoqi z5&q?0jdNE_46bywt-|PawGDf2k#gsB9uu#t-E8XZap)zN9_vK&Z<|9iTo|mErF3m) zkK;oy^qgOM9U)lDF4vkrY7UbQ4}e)Ml)Kl3*}<9=dOr;=uUC0z%t3!jCpzD8TyK9A zt+IcQNQpxu|B7gH|G+k)m2dYB1ff^Iz=Zv_^W)&pe@ZoKQhV+Ml>&SWSmUB9*GpYg z6{*+xao8_a@BFD(4h}w~i9k)HR&D2?;~ix27QUtb?yU4y??ABbpmus^uNYGtJ9~oBveeoe@P9J-ni7DWgK8p(_aYVy|IiJe+d2Lv+Bu7|(;koN?TqxnssK z{c(QR*ghjVP^9dr0%<_tZi#rVBPHbvWyp)%3}FfSwgf$#gYOphmIb#Qxn{zhd$s;{ zo)52}(Pzh8DxixTTIX-Em`XzXJF%1aG4-N-@OoN~owsuKZGB`V>r{+bm*t|;sTVp2 z#b~$XBBqs#h+1AB1mBQ%MjXp%!zAZMjoJ|`ZPb=?;5so@b@g8A{0oKude5%Qa;fvAFR2m5{*Yb;4@En}Qag0q7Wvw)6*!=Hn zTZx!h&e6)~Z5Hty|1iImkl;Mty$)r1`4VdblVi9|Z~x;gHoc(S0c@WIKFQPz^>}iN^_51+s zbM4fJCU#F(0q>YFEyX z)W5{Ja#(Pst6hS#N1NyPtUhRoI};;L`_!e&?ws>=@6fd2&W}yPhSObiuGrf7q3-dX zavFey`w>G*EjJW`C~oej*v>D5HtZakSl*4Kb<%C$Qs%tL7{XYa6 zgFb7iSAYBorw$Y&N`l{{N56h{eTB!Un;ll}Xvy%$RAz0@61v zZpgNuF{l_l;hoKVL`t&wEH$fRg+-hh`{Bq?yJ(<(-tSgO4xh5d#VWmpVtpI2+G{OW zR&*c6fKyK(GbFfFK%SE~1`;FuV$K5>8U8kmf%(Mw*~Vf2cup*zKg2P!nhnq-DG;qS zGtplK_&0GmCHQR^g)w7$as-166XqLO?!@f~%-x0@*MP^qmPzzhBUK%v6Qkkbo9X^F zjK=Ms7?yNrTZ|tjA9jT8Qw>~)PTZsKvrJK81rx-!o~7t#bmia8hudcIt#a4$Da1*+ zl{)%4iHlk99?Z6A#JM&uZME_=m&T?!!qPaHz$FylYBKc$u^c`5|SXcN}O3Pe++uk(IZYuT0VtZ=m}N^PYevc~7nN7jg>@_#d~u2%p54TYE}zyF+qO*CT1moWjyL#vG0j-Cf;#C}2}R#ia= zt?L9g57?ds-?!LeahS-zZ(B4%{2eRoX|svsU-S|GjhYJYjFtGf#GNGsSu1Fb$#+7- zzJ1Gv(Dp)$$`hfGdK5_HzI-+fksm!Gu-TYT3S*-Dm_=zv6rY)@Xa< z?VPYwj%2DHTfwa?*N?XH3Z{!DqGb*v^3^!*f`*Zwq$wF_#?>3L=bx&?$o94=!v3vyFgo<;9Ylw|$0F8m<#dSVQ%mEj z`xY7HTVriIIHf;~ko(Pmm2kHYAXq}fK&eJ$T`_i*!c!Ilfv$}J$I*y4q z`{wmKbuS$&w)a2=Xg@NS1abehl~|-B)BYJ2`u$GQAHpv67J>d%0Jlp(9kM_9VgZv$8eWr zihJv@iUbB1??YyxE>^7T;8FH_g8iPtud!!I-_{ib6ZgThcn7=cFqlxTt^1iULUYoC zChUBX;_uI=of?>k73K1G^SF0EHQ8z{m*sR8UFv#VMVz&z;xyBK5pnUnBFn&|cStZL|-z z1Xp4+c}X&?Deu~Oh9I-E5-Dq4JFRf!KP&`j6xi=s#rNyb{ri5-XdtjY4M#hrHm&`I z`5;ljfa@e^AW0WCU!h8iQ=!86Th}^8g|SNN<2%>mcinZDp{NOC9v-%zj9yKzWAyuI zVW$P3kgaSgr1f*eY4s{wx&0v23guayBxPTC7)zmki-r^_J(DWrCx>ZUrZFwoxK>2A zUGI~ zua@8nD3!byN{@1Dhp&=1?58TSGA&d$f61Q7R)oT|yTC9ytG`0$@24S?cgP6ma09&pK+jU~b zOBGSB0y!_wRd(Lx(CRv-0ePGQA-Ybm5(QI}TGu(9BoaZ!WA2>jdri@zDkFs09rrjs zH@~gxxJX=_ha=5bK#jyODG{50Ffkp&ZuK-q*4*k&QWLiNINwCvE;>^BEn4Dx8wV9e z0BfZdvbyh4D_`A0t<<8GZ@Du4t<);a7qDoiN^iT@som?cjhLU3h98V3or)4}a>IW#7~r*koBuC!@<42=4qa+`f8RXT(!U2y(N=55Z*l~K~~ zyfZ{(#p3}tbsfn}^FRyClI^?R0cQR5Zo;hVAdwlk$xA0G{xxUl>fc7oF7#-(@z}gx z8YYlXhppp~iU}iK zg^?hn+EJKI1=Kp%2dNy>YMn{x%^GK}Ivw;ggmGDxQ|yry=-AaKPY&dp*H-~N5pT;C zr=V}8^a$eVEWt1_t&4ONmyawiJ+qa-lz>2cR4iYBvdYd~#fAk}a#6x&tYZA84WVE% zx^f~J^KNuY7=qSEjG(3?Y(HV8=Ps#014-4;!F-hTEdKh_^*TcIyeKaHapi_%(NpEh zh&1-B@Qu9DbB&f!r(Biu^7KtfVs7WL4GW!@LpS0lOuW^3d6GEYd0Fe2XDz3>_#+S@ z&qD5hlKe*JdUdd8igVyv@us{yQHO{1-Yilm_e8LWNx&J;WS&UiRLr-Y@>AV#BQW!U zO%qaGKCdwv!D+?lp3;L>Xr#qaB(l>=IcQ9}$~cJmEEh;jjn>=}epar;{Y7^k_{jxgr$&H8>s7ZgOZG3BckjIk!1;Feu*}Jt#He zB2Mau_<(%dNCMEUkeztu1`;MBzwZ(xZ`pV?@_S5ycPgtRcX9q+Fo-lakegQ&sfhP9R z>yrT7%gd_}L-NF%An=7k)&M#n&oRT>gfXa5_H*sDcCC#NAd%WYgl}U6VTsbP{wj{! zMHQBktF?>I#JpYQ_6yXm;SJQTFy5}&v>X<1_O&#-C7>fu-^!ME z(z=yWwib$m&if-lkTuehIj$cp1JnkcKc_v380mbC*5%M!No^2j355G+!D;Oom`^i# z%~>I2*n7^KN{2jjkI-DMd#T+8p^i7IGX##b*gxL>Dmh8bXg2yyxQo{2x{lwov7N5# zIL`P1yIs($v+Um7))S4Q93fC2#QvnmU9h_t!cLofb3E#ti80PL1&&GWuQ89PV==G_ z!0>w6NW=KAtXw{fcMtrX#Mq-MLW)n$v%$H!Vwu~r&1HGaW!Z*B1WJxV$q@3Ov|q$s zwX|)%((=ZYwryZ@$mlQtY%0)GvYbt2OXq2z?XC=r1m6HZE5DA_B(M;>4(AoMZp(To z1xcH<&MVsH>-V=NAWo6StZ_{3&QV8g>gu!;Qit_1cN1wQGG)rZ1|rbv{S|#T_H>7# zEq^v%wJke*ZE1@g+m;;zzf#lIN$s*6cMhRqA594Wr0yk&^t@aJr@Bx06G;_X0C7Ea z-&AMKso?&cNLrwIN}X-THO-kMqi#;!{}8olGMW_o^hS7Hi`4YFH4?gE&fX#P_(K}$ zGbkyHC@26^p=6m8=?PPEMCPRJQuRSLS)0tF4#ZHY&@Qzca9Q?` z?w0*lDsWyhi-3OFl*E1h;)bHfcEbLF=wOPp*98q#%ihuX?X^NGW|KcKu^7m3L6W~M zpws>yM@QpxISN7S$H$eici=dQ7&ofX$6rnw-N!>jiz~FQ$pO9W>jNL?4POO<{@Vol z$vBo*USX3zpNaM7`$7Dr`8rblJ?l>=4+o}uhSI$=iAR-YvZ$mXaR{+YA(TYd@zSUi zFHnkIN{Z#-C~2)yQaioe#~P(Uqi&zXM%1OS3+OyuXWcL@364Oi+hsWt2+_f?AaN?b zSlyX=tNdOHcxGWLUr3}?euIT*#5A{)@1pf31o!p;nSgS#0$nBYNqgZqP|xYH)-)kEOG4;t*&=wX;yIcS}Ued_WArT1AF2S^~6jKgSTF{<)ttNZz=4#4$vrF=MYvCmvYnw_Ts?{h;K@; zuH!6uH)#ajws)CN3*YfCNJYAV4c8|2+c0L(@~>8O^K*#|OGXb+_E9c10|OqRTsNvw zqo}X|w08fJX}%U{r3baN03X*dP z^pBMEvrq=4P~Dbq!6&WL`fJrK5yu96&FRU7A!0~!q1q9qOdig*v!L4U*GR?p*;>?h z;5f@hjco1-9o4b}BQ2{P->}fYhSgmTtldATcD7K)D+7w?wtN;sWcIX@Teq&O;3MbI zB;A&+bc#7Bk)|aTX!t{YmOrSZoDi4ggmcL2nC+}NfjRHAMqm}x=(aqEe4Y+Pd>4qf zIyg)S_7p!iJmekLb)JO^#dCDLTD+t7Ih9&Odr#B(Uog+rPIw28sF){o@CXkHku0Z8 z+3FVkfGvbLCn!ZAezdoJB#3jmEgvO2d)a;^*-84);-o>g0#UO?41sl^fL6H$ zC#2rlsFvVzo>mbP`=c%=^+bEy9{{8~2_4HGca?2-l|3%CXfBc3ol4j7D4g0$y{L3N zR-RHc(VEMub(7i;89I+Sf?ilTXgP_J+Fx7Fh(K`roBFP>oW8K2oUZF~Q08E7kjO)V zbGpAnLlWVGZ>Yp;SfD;fdV+MVUDrAL)q`&YiMO!e={en7Nk1De-CNt3r+X_YXLseO z02--G>sl1asu(1id_?Mj0wPk6Ga~g`^azo{7Ro<6;Hywh^r`jEvGnP2s~naTfHtw0 z0?}qNI(C4j10Z#J&}u{e-Oi278-(p4P^b3U-Iysdm9-7^8LIanJ`A?M zPpa#>jt0EA3p!i{U9N(TVym2QxavZv!?zF;=Z=Ly$H3k%86iOfKMWc*+&1`!8!9ty zxCI9ew=ez~2)7#uZs+_nR7Vh}n1L{G5&}p%6E?%U0dp5NwL2G$ZtS*8zQzJ2H{r$Y z7KvuM*Rx;pqt!cZnz+^1@YueFKRjpbT8F+4?Bhg~)_(`}Ts=5Y5D2fXGC5;Eyukr)u6KweBP3t~k(ji2*mDvdc;G z4x9Y51q8zlY^jP-{pa};FFnG2aeS+f?YEEZV){Ptn<3W45iLX+Z)244^MGo(e@XI} z`p_kyFVaKW&&SUz5rKU^{qw*+zXyFKXG6to;PO8L^6Wl={IJx2O%d(*@*DUaVtuwq zCFEwQgjX^kl1WwxGtDX?K1C%wmxeGr6-g@LZRGn?e6!*?jqh*a2}YbEJWph*gzq7b zc?(s-Im9o<^LnaEcnEQZ@O=@UzaY-HOe$du;w(T}4+4ii!t*8aKAA*IB!fYPCkRh4 z9yOj2JfV2P@Mx5W{w68UWIR*wXz@(NGYyXpj~>r-JlEj47Ed^y8F*&mxeiYRp6l`4 zfM*t-NW;kAjd*TSzTHe#DE!tZNQP0AS(;xZTvdVr_3`l%jGJJdL4NUpuP4Dc`uD3# zFw#$gt+gPB_{=}IkCI?;St;p7RDhdYr|2M%JeBkmg{$+iAgt}nApzN^_fr@cp^^Op zos}d!V+>z9Lqx21#v6?g#+H=DG#pkzla}l4lg7tF_K*L4o%p1|Pp9xnm%jvlE7RvF zMg1*IulawP2c^#bAEz79NBN(nD}-FT0A&heC0jVn0sc5=j!O^E(ExKmjK6jKRmE7W zhl$fFF3uGV3R`lDJvx_omGzL_x*Yob-wgsqm=)w#tej$B$-duHOA8ACQ6P#oidj?tq3q8XM{V(Vd)TjFkoja;fM-)idBW(5x)xGox zttxQbr7v5n(!KOeT5jZl`6S+%C*GPT&dd`hJ8Lxpz6ze3`m;4Ot9xd^!&n*=$ee}1Hw6%{T$vY&Y(5Y)qbk2)ns!} z;T*5CaK5)cJBqYq`m&?(#HjkRWAPaA#Nmm@lYl1?PZAyzo@6{`Rd;4?S9XdKaxl*l zKXAhH2sn?y6ova?pH9ljv+l|u({jTSK&oZ33+Iz)4)DyE%A%yB;KN9lYF$C@hk_sv zJ4*bR-Nqh7G7g&iU?-&@QYzECrbAFbh`>~1zXzRIMhPGv0Wavc(yGT{nN zBH5du3?VORqO}om)k*9oKAbBS=8CuE;_T#9ej?kx747jvDz7)#pU7uNDW@tO{djZpm2!uCp)kJG;&_JZVVFn}RxA>O{9 z;Xen`mt2P)^U|9nvY3bOuQBH8X!(<8#zbw^kO3s|CZ8B<2Eo~8Z-TCg{Zs)p=x?h= zO=1BxZquh@GU{iCWLv`c@TkXXsKVG&uU|q(snU(dz{hkg!l5-yZGTX@P}{#KC`fSK zqjlY*?_Wf`6F5;`Zy20N636y0GHjwvWdEW$I{Ljc>}Je7JL&g&XxN-(Z_ zqN|wIBY$mxsw86(&{Op)t9&Gqt2HV7{*szi)nh0>gEEw5HbxG%bGH6;)BdAuOAJvt zvGYB*ONA><530p$>^H%NQ$@K|zWy$TcXVh7JY6m0UC&jtd=!Ctxjh+?<+{~AfqI|> za0D+{js8PzixX6UX7#^ctKitvx;gS>uWz-|HnG0NX=&sV0_a)Bpt!z)4Bu+N1S35$`eVz!VjPhc65lUc!2!ipr z{tSh|FZroe|GGbUt^RWjnq5>#+^VnJr$PU&Pxls6pWd}%;yxwG{eIFa6C4V}=eoO2 zF*axCJ7NUJ8`hm##__~PhNg6^EWxZG7>&&deW_ND^nHE_fEuLq{lMI@eNVbv)cgJv z4*-f-g=sGZ3unDvQK?QTGdEUvFP|xxtnRkRn!N|O(Yx&?k>9) zDQ*dKZ9U=Nu`;!-<=Eg=p#e+%$_NwIJzop6F{=2RDGKBiEG~A`G(VWvSi81)%_^~O?NzkF(DNhIY5Qo5 zrg@2k%$Gwd#Cguk)7TdVy!d*}5>u|ag#uX1qHqGz*z^4JP%ZJysE@eUWK}o+31Muv z`T6s%@<^PZt@5issW{#rw*e)eUNIrwbA(dbVyh9a-V&w$)E@B~=YB3YB@Harh#T2{ z47}BAH%xI3g>MRF+7@7WY8Z>xuUw(x$`#v7QKe}dsV6WR$Q3Lm9)aMT&|ZpZfJqrE zD|9a6J$@SowSE=mq$v;h?)#_ zlJ=x&j@ZrXwM`@und{sXDTueZA3Z{&US&Sr7CP^+OXZnfBG=zas^NNQP00UstFma- zjyb(c&9VahA}gea)blPwKgAJ-EyrwP#6aPVs7FQcXPnuJdD9dpFg-LaNqQqyDwSn; z-jKK?_yV2ugOo6u@#3U*Qdz}m>k@atP_er}TZ}oUf}Pg%v27Pn%z`1vcqSpi+%8H0 zbVQn)Ml!~Uaq913S1y!=25`zv2)=-o3L>c`!4vE}>ZSax$bcK^7+u6Cke^6pLs-eb zOBKFZDhM~fEe6e#_Im)Q6nc)+XcgjBnsT-VkDL!71D!{IP2#PkqVAY{*YK* z77bIoLL%=z28V2Iu!Xvfn?X>TGrWKxrWpk>Qop0xh4b9LNoQj%!+_5p>$vVXl*`Ix57u2tJIP{=Xr#?GKj@(6rP-|VBe7)M zK`#x7^Vpun#*AI*RaP4kyPLQ6Qx z3!0lGSt8(>tXdBwtXw(0av6+5sFIYH=y?@i2V=fUF%mKQOf>Y%YMo|Pd7s5AKus%C zE#;D+RI*08%7z@*KwQd*!z3)n-1*Zro)CC~-?X|4H1dKkiNsy-7@Rdjf;$t~2(p!w zsnLD6r`Gv!Xb`?Z%xdvMtSVPJER8Y zwhL8`TUlA5&efRgW)k>ReVH1;F$FcOb?zd)bbTE7K}PS=gC>~^w2s@U-dK=VXgss1KviE30HG%?XS$}=^%%i|qP{7XqEz!C036e- z0iMaKTmamyU7sr>cwfqXkBnG%Ag$OvXqN zbzVn=uYsF~2+N|Nu&mM?I}|lm&#!zX?*KmWRRs4EVWG9|uc5?1loKSx5sFCzYiCAH zmyf2N#LVy*+=WR-C_6pIlwe7ygZfv<7jB@M>HF}T2x}rA(3uLFd!;_l;Ao9wm^YB3 zp_14@DVA$qr&-1P5uIYuIorYov8G!y*sB(EjUSSY9EZ;Qkz*OSQ4#Wc;F?LN@mc`X zM=f|A+BZfC;u5bTQeJZdjU>bCv^qondnx~ZPdeoy>IZu@beJVV&bXBw;2sq8;tyX7 zVuLSIj-|K-2G@!RsaWmKu7pv#@@1|dSB6?TTor)Md~JhTmshx3BBiu)dFXm(A}7?v z7E9QsIZ`3*xt`4y^)#^MDCF%-6JYcu2a3z;3YU}CE%F+~sc@yWYoKa0&!DVTG?dH^ zS$lege4cV1AYO9`;{_*!D_jkZ6rG7&t(*xgRL&-r;%yx}*JM|$D{`BdP0A_!`@nIo z9jjffhn)$wBO!xCj_`wQ7wH)8vka-^DMaL#>jy}huX`OU)|!h#ZZ4R|Vnwc)mg}h2 zz2s!tD^Aj05oh?8oe&~afg5L(uM`Q2#dP{syU|&ZRxG9!yc!bj;B{jWV4y z#ec|#loy>fdcXqnZ@7)wXMifg1z@ZE60*R$<)(m&qr!rGFA>3IwW6qCODLL@`s>W%&Gt-ytZCbW~LS?$Ql4!Wg z@=s7Tbr42ic$a0c+oG@NvRtB-3Da_~2bsGVER)eh#tU%V1^k~3@v*O4ZloOdA6?0g z`JGtFjvM3Gn;>r~WSaX`E99o7WI{~;FI&Q`B#~95K-{CVzT6V?1Z^4H4w}h8^-rLL0NVFtTR)tt`se%+d*)*-0% z^zJSaF;F|Y3NCebG0lyFq3(7P7NBP?2e#$W;lQ?t{|szP!+oqRC-Z%6@hnl=@~_5y ziEuZ1=|33pmA-A6Uiu-Op2KS3eRsJYwnTe^z_PUL0b8f+9Ap8mf-_={>6@el6^>wWoE7En>4RErkcJ<%W+J91t&b=&{@2ha6B%z0%hDEtwG>t26^ z0F*1vg8;7m^<`|Os0DEQAn~yr5j68f&OC65Rp2lrtZ~s)C4oug6^d*2I{V{bUA4?P%T=n zy@z_>D0XSTAmOWH7zd;#VS3VaJWb4NXIr1jp}q77X&HikfT=}6L5{mNx3Ru*Yz*iI z7^f}g5J5+#2Ogu*yrZ@KrtHJMzhv zGY2S7%I_)IJ(NOKnUzl4GQoK}TDTWlB^_O;%4<{~scB_<+9a3bxMMO{>y~{38n!|j zW>~X)pQs^in|n!`_)Th(BMbI$b)9((>yEn%joqr__cx;R&%AP~&PrwyVq2$na4(dASRvbT2?f@z9 z##rUdyRQzH%@^3dSJJ2oE1l&4jRAdmT(a!L4y%#$ZhlJlo1{(CbyBx(-pH2Gx=zyT zC$ti$w9`I11a>Gr*HO;ErV4h{7xU~{n`|ZhEz4f_x=3@Vu9Mgh&bIT6MH5jWU*aFl zp^gWgHGA23)^&V~=%d*z~ljFIUhaS|J7)@%~_*WRzF%k zPf7%}_N;Z}wYoC1N1ggv|})K7^v{hcTg$AravkQjV}BRi7RywU{3brAZeK=`X*>0ZZux);rZwBuf zg~5)AFc|iLKj#=+6=@E3tPH?j(?yQG!J_ENWjW1ZIo%5BHyHnWE9A8igsb})qTAXh(MgQF`Rq@QxhF|QSeg4`iuk9>;q;jGn_=Q z3@1b}ehBC@Y>99!4=-21+qIm{2=awbiDQct#t%o$s3yy))-YPNsys0*@vo4@fKJ9< z1!wVza6~X4Y7IA)omxkGMb2$4m&0T>K5$-q9h8IzA34F;VYiu4$7x5LWB@A>E0>pxR0L*vu5|XpS2#IP?Hm1h$S?svAGV zgrV))N$PABaxG~=TJ+t_DgbN(U6b@f@`*RWA1d`rS0s$u2p-#3ZmW>vk-&2T2*Nf? zf#q};Xl>oSw_5arCvPp#GovL>kP`0XLkQ^YJ{|i(SJ@O@2MnAGU0dmcZ(@Y~wqIls zJX~~<(Ltt%Dc(A`l~jj=#OtZfJ>7?UWc6g04C;C1vK1&acvvs$BqpwLp4Cj+O7Iyx zY#0gC`@+)2={#&US!?B?9UngEk7(dQD5+d-{)^X3P1BFIuZ7l)Z5*A|89X`p5E^$h z_DwXdtKeE)2kF7bz5`w4uB{ZIt8BQdOxsm9)K#VtGrAvrij;7l*iLXciErIce2=_O ze4nhk1RCNb~#S(s+fa;?fHW_CaN1D4MY`rWyc<))$Ben{RA zG|Pwfs3<4;uA$^RSx(IRLl#CksVSqUSw_29M$e37^do;pYRYJrFQaEhGWwA}BMoKr zEX!y&%jm}=8U4hck%ltb?aS!LBN_d~pHVwy^c>4*56kGMBN;vK%Ls;Wrgv|p?;4A? zd+SgA8Et)@g@Iu+EG(s41i~31)p}bs@{+G;+5nN!vEU(gT~ihUR~#Fq2>{G~g&23{ zUfm-pO!x98{JuhXLqXkyn-AfTTdtUavu^X9vRa(!l-E1%0~@B>60X@y^%~eAFdb$V zKrJ5F6%atM9M~NYK)p2Zd_Vvpz~JV~%vN|ogsy|Mm^6$P1hx~4!7$@Ysz77ao=W6K z%u5x(wGi)0rAyVs>8Ye1;dl$|#qNS|jmFdKOZroP=Ix%ReStsr2WmXqe1XsS1Jwf} zQy=ExJN@CRfn{8+n1w$zQtg2ufuI(|0lA&YByc28NTuBeYE0LqiPlsNIZ3JQ!cIoNoBC2K=Ud_AlEVlU*$1 zK8q$u+D_E5{ZGN|Uj{iQJ6U)TjeKSB=9VvE5hlr&a*rc5Jk*~c{Tnl({9 zoBODBDedlV>KI4JV5r71UD`r{QNbxej%x=)qaj7j(9YXJ-8fH`j@1m_RMuiyKTA+- z&W~;dB?j+SdW@=HF8xECHrP(cr+0)n-&R3{^_MGyy?S4$fkJ~Q^eZ&g7urstY6|`G zQXjR?aqFc+terR1I`>=oAqp_9wWg*>F|4nh;H+}(qY_t)k!yn&s)epzt|OYn_#8Q) zqfn-CZy~>=AwF}s^W#e{9Po8M1UYqB-xikNXxTc1g-Oe`;yjE^_qSNepgjg%=MM-4 zj~ZM-eVb{0sOsCy(lTqjOn|bL;_fkstVpmjiAA(485U_FQf&8tNU+SFW6+sYYhnh59FrjO(3(~dP}mwx zHBO!m+n9j->yHdmM`u##Nn44NO7hG_-p(J9 z_X>*e?DSQj;}EaFROi{RT&nhhAw0t>UWMuY)^{A@`|yr(wYb7xh;q)9m&H$;nvb?N z@J4C|v%`qYNu12NzC-=q=VYDj$>mk~ipukZ`Co=ro-RLRG_X(dBSxm2XD_)_ZYO0> zYtmyY+>a^M^UGRnNKwnFBtd2Y@`_T z#Cdt*9M~^nKaLrYDsCQN`YqF(1JPiM4a{LmF1W5z?;(3JAR#?tHC@=Sq}vI!R{1G9 z6?D7MZurVwsD@@rFQngAwWM)Ph8Dcrx~D{5N7^xZamVPtoicivIeH;?-? z{MoUjwxP{tYL6H}#a@TwV4J#B- zUADI@lJP6)ZjK08T7salb!B^Du*$O(!u2{YzfvheOlNswC-~3@3>Nc_0%KK77`(YH zf&hSOiZe+cY6i5iQW*%sQb!&o54m4S}>MNz^G!s~x zoI#{x5jgv`iyarn8N&3+!lM0HL>-qw|g zqa;0UE%e4UeZhiT?9zi=DCg;ll3v~RFw^i`7vrL)>X}Ghu(shrT>=ahQ20*(;#daia-1uTuyNfE(2NlaegwRVU9vUrlq<6uR&$nVYdJ_sOI&Hx95u2<>opTaOGRrhpK>isLN2;^~_?30^V|~ z#xt4u;e(8B(I+(awB5f8K$%ltj7CPEHX&R7xH0KtKV-#N@y^&ms2Jk@Wjh+W#NBrO z^JhtC4H`3;=SUk!EAM<4<`Zg$jK_j4!}VVK1$4xIArZ$C&^ z*D#Vab{ufMu7j*#UMMq+1~7mW-K=DbIa1z_{p998%C)=%EmZOxN87aSTs_VffBOop z^7Z4sMdDlQRlddH+r8tzaam*P9s*%@I&PwHvx}mcNA`K5nQZ<&DRno^0`_M%QDi$p z6#v%!JSMD3vRA!IdHn#doBJxMS>E@mCYnc4_KvSt_TGcOQvK!mOS*a)Ok~i-$mY4HQZ^QwJ1vGbGZFAZG}P#LzcK-BLu_|uf=}lZzz&4T`|FV2DQJGWTw&1asnk> zuN}$lm*a9{M+g)%BIC5lN#lz~h7`}bATyl?$3^eEZYS@CcWE5dLy#0OQqCg#g0r|y z$NXDpTDF~IV+L~E1$1_sRQJrkr9HqwLTtx;Q`R{9QNLsXd88;6L*d#XMu$oa{su`9fy8n;TU;7@7?4f#ZIUChU z$jEuv&W7|w-qmbCkE14Z8ha6~O`QY5;@mv20R|=q=v-?_=Q@lpPr^%xqOJ#_^h{#j zN3H~H&iM%N&e+F}Mr!6|)A^Uy{8{CtsLpdHGT@tAcPxF84u|E65%dg#%;n40O=%7> zXIHk+k~er>cIAqRbh@!`U2&gdrD;XJxRjTGE99l@+`1mh&sAP(eCJYYzH3FLYsIo! z*NT;@*nYP5saFpgSp z_DY&}Y!x=|I*p7Y`w-G>Tp7G^sp*Hzu5f{rqg?SPE`sH465^m?R`1L771NOWha}gM zABVn>3hTyURd!{)P&qw&nJKvSI;I_4VWpfse<2Ho zQ(=Y@@3eEeiFQspQ+YlHVRkOo@Eh*~-sHN<^F^pU-#`V++le1Owp=YnDCxk?(RAy@ zaNSGql(_RXs$x*|83zatj%Y_?JqaI~3U(5BGB_xaTpw0Bub+j?He2+q=pQ{>#7jq| z_i+@~z2Q0)$j)2r7_1Nk?e@`hjH2pZ3dO>KALx3L%rBJ7E2s-Y2T!k1wBpCkrm!G_ z;?X;oVe!e$IAE^ngF1tTSflmdWhWUcVX_iM4dudT5?nA!V z4c=i#Pk9A2U&TeHf^dMdK<`Mx4;H6ll-UwaEeyv7MFq>LN8QagN+-$i+OyJ|XyIH- z_~*Nc3Sk(?-)9FZ>9bHGFidC?ktAzFZP2K)SRYwo$Eq9e&fk_jhGmoBT982tQ zD8=j4iZ|4{jvvyugT#t!FPO5~=~dwdJ%XygL z@3MT_ZP9gE{*KOt>ApV8Cn{2GZVraQ6fF1V@(xGmpo^6bb34u}efIfo$#D+F%{wYy z&&EfHs$j@7t=OFvLU+Yol*uBS)F!vLU4iI^AB@n8S>0O?(p01Kos=;0a}uPZ+hw`p zm`PcQv*>u!6_(2eTHoW0jNWU}dkez5R9@WI1?^>B=UZq^cV_4wn1?3DTz7%4+6VIpLgAK;%dPduWE$ z`3nHTq69DO&RQ;*Es?q{*RW0Y1*i_=*i98o9u!2aaH&AW11)J6Q?9g#ungnUaX~pL zT_IWZIIw8JO^v1|&AJfDs-5?)xlG?2jPv(W+pzQ|BT#Qd>d+8+dXjSq+1nEt8(~VLu&*^9hva)@?Ti(fcGnlOA zS+t`$WW}teI5!(nVLXN%70wB`G&GbuzNavuZBlu-v_fzGU27E%7l7Sr?u8~jxS754 zeg{p1AnB01OjF`68!q7-W@|B%ZPSjB&5n8%toLVY%-PhAP?%c)Jmy;TSTqfp3bZ0< zjFza{Q=U6PIB-}2Ift6E_06;RDbWa?YcbFDG+GkerdhfI=U@D)2P3>bY|{1YM3;1B z-WTS}>t+GuaZsfw(sWz*a&6ZQI1V!1Y}GDx z;HZpCHEzdLOj85tbt_A>8FTd7p8l@84dHD~KvO8_DO3Q!82cTzg!37XeS zz#$-2xNRyrWQo~VN4Cqr>lqIoutjb1d2o|-FPT9sp1p;vXKJgKt~fS!)?vF*-Alpd zUeZqo<3-a9^-B%$!a&Gv4_6(5yXHx6ZGqo%EQFiR< zG*oV2Z^_tWf+;astTbin#l^@21KE_R0sM|iz2K>3>ZKO7)OL3C+=MZLQ$aR0;?K?0 zn@<9K*Sc(K6?98s!s%fUzWrF=bDQm%k(f=TOhf%77|9rNJ5J+L+-IxysaAk@wN$QI zKw7wC`NkUON^WMkeVaEzaMD$f-&-@f))#7|euWF?Y8Cm_77GcpfZ*sZ&8_-#W zhVd}W#X0nhHi97vOCFWB3iNHku<0Is%RLfo#@?$oe}q2omri5Md}LlubPXrvzyf;9 zkIcN^@6|~6Xi$LaUcLD!YCG9=4^$R3b8#N;XH`q-3>Db9W9S^)Cq!U}@FlKs29diN z)$r7y%>=731}jtt?N^qK(5%eIdA105Sp*1MI8in5GnvJ~2$jw=&E^k$k5s`^>y}aywx4frl$&J`jLkG6Hu+QI_Qc((A18VJhbx(4sv9F zA%5R^&*Xrj!NfQ@bn-0Q*GL!2$6M3C30A+cI;UY>!3tMaFa;Vi5mhy|WCn>Gb-*y9 z!{TGd5E%+-(-O;}9|Lip$`cgZGwz<20vrl7sdU>^B&>vqp$^g_sWn-`bse7y{A@6t zI-F9jd)X0=Ax6vlFQ0#2_i}G=AJ*f#myZJVB3R3uy+OtGDy%euc=NF2kRDd!a0)Yj zuX}lMP(}u8BJ~z;;ZND+E#4VNm?85{?}jRVBgV#{ion}2!o@`}_udxHQZwZ=e+Cr# zHg$Et&;XhD&geNvw^xQs%6%1{kk<*)P^UY4g^bt@GtJ0h-(?mWSJJqBD9yP1wyTq8 zxyn1E6o(koVa$osIRDuROpde@ryErl(SgU~{>vXv;eY^pcZ5U3@$ESRE8r*!0GlXO z&b>lc=Q*Iif86Uh0fy`nro}?%Bi@?B52=jN9uaJGs8zRZ3tOVZAE%t?)RE1`6t48% zccr1tUcz)o2^}~s;HCf_!~Q$APebQ6mL*}7(3hwaw%`n#(_L@bXW_81*rneUMTcK{)El-uLvLi zGvv}|=vmVfw2RVheUg~Zx|a?TJ3u~4k&Y!jN+s&P+iQHsoo@<4^f#s6JdE$#+DdymXj3vhCViL6a}SSsque%8O!;?Yu;6y{Z`-Z|HGV<$&L5-o+-NET zL6Bn#>ozUpg%P39kJ-n=%nH~S*U2{rw#HPZX;tMEItKiA2hiWsk6{BL&Def|F!@67 zr(wnmPd|>AA5r+zJIF`hp58^#-tPT4%y_i-<51(#AJHe{+wQDuRIVA6K=<=r*HntN z;_xU=ntwz|7f$}gy#hBCFOJ?%YGn}3YwBxP)Z3RzM~j&Z;FNL&y!e2zr-CkDhv}|4 z81--QQBP#6)@j1zti_JWbx%0HGCBYgL~4~#5~319(_M=o?}IU;uAr_(D%T=?t!q(` z)IaYK+|bcesdvT+CLiGZ++{c;)vVl9INI9Y*!vPk;sxG3I1-wTxD8|Pf^(kF0y8F_ zh=TD$cG4YFF@_!4#=@jgj9VxCH?)*tQnep^GTX#(z!145-0i7D?-FTI4#53vPS={cwY}@fhhvvSHkYUtX57K`fjch zl$)1J@lN-7(*1TKaupN{YU9IseBX`fzB?x!0pALgyZXj{6|VXvpdUA%zra@W5|jSd zhZzU44#82<&HV-~SpzMn11v6was%Dy?!Cb8oNL8q#QvA<{Gt{XvW0-{7;1~K2R)@? z_k{EAeHEMjfpRinnnVUif2{}oQ2N;2HthO!6y2P(KMDoAeoeRDu)uT{IEMw6`U01+ z!1*kYOq16e?q%WYS$H&sw{3HGQ5)*j^rh`__x?D%g(R_WdtB~FL|sCkEJwbccby!-x-JBd)T)W_AQHj%V*z+GM2M% zmFye4`QW>D_KmEfH?nWsKrk~vTh`HU>Do%61&_H3VBvaKzh(C`?0v7RZJ(>)MOWDY zVE7>xbQDMTeE7|bbI8vP66N%Z#U*AR4(nuRFZlk7O(7O$9CSaZZO6q~)3P`B(}Kx) zMYG}Wo*8w6^wO6#O{?M&-OHvyIIZ!?jId$2hJV=Ho315GM@@{{NQajm9wTYcOMmDN z&crg{GCMCk?+@k=u>1J{)AXIpsNQ0)pK#HX$QOy)dB1aKMR*tW5x=#Hv^X>?#@RX2 z2d+o;#H*cz1#vHO2kTQzD|cJH#=*{asFcxY*ifxlWLmBIkiW5gSiu{IKKL3o6dq{gCv|!0&1JV$R^f#`EK2#FB2p0vT5Ti=Vcp21D7j3_1*+$pPFSZuOPA=;g0;i zd)bl`o`>w+(3|F8WGt;>-|^fpr6F>sw{5!pQUAB&_;%?dTm9q;{{H9{7B|T`&>-IJ z90(I{%OiWA1Hq0ITfN>BjF?c9=u`6+$yHS7k60}{-zK4;0t;Q|-NfQ&zRx)r4I z;hM}AcNw79EugYCM#<@}U~Wrt4Wo|+(DO=m+Stmh%ogFqv5#KX>+MZ+Vb98&_{|U+ z7etLZ5zp~N zIefcG9|fQ~;R&A9raAdn$06Dju7KtrPjD+w(8dy=IQ#89;8p63BYNqCeE| zh|bq)L3xG!jM4<7SH6+*r!${oqUUbX<~ey=S-5$o!*J>3Ed}AEP-;I*v90pwSnog` z1-=3Ja-4ly$omDyTg*nrtt{p_9+P+w=oh=pUp2hO_6z*=`7aced%ZBb!I4Zex?y&7 zEY;O;r(t%B;m$GACLixkPu#h1x-)d!(`*R@?5e;VfdOY6L;)t+1)popz{$yZr=3^y zEs5^@c)43WCB@J@DP>bji0&l-yMDpF5KUR_k0TG{cdl!?eE(Ohg4t%4LGuwhdTHOG zeDBMO$I(kV8-pQ-Sg5-E?hSm?H`{hM>fR#Y|7h+8z=Zy*x4>3P|46Irxh88M2G;N-0Ym&SQ~G1Zmke!jQYFk=9^_ z?1r`L$gN!4m|wRh74U?&wpQ_v)y34>k37Q~rIByH%(~s1V7LCBHo5YDqZ0Px&oKX_ z%hc0q`#%2Z&{ynZ3ef zYm2z5QKXmU24>!I>A#9HdUo_S;lCF4GU?8@XnV0DWa73k%C4 zoVBO4ZcWWvhp6WKgs&?2_DMyBb{`ZK+7IiNMTwUBpO3$S}_Lp5-!1t`I& zL&d5(F?(%u-kLQv)pgY3(vmWmA2(#Iq2luz^BQ3R(!m=d1cal3fgcQQ!r1Zf#IwqH zm^sMC12{x5aQwYz8I}hj3)QscWsNX7J|C zF_bp1YHV3kCyJwjK@*x+t*PPo9}o;YtZA-W)lk%0)7#eeiwvryZAu_`@yD!jLzg%R3e`OLC=dSF7pws6EDwIu7p&#MLp=DGzF<8M zj`#@+-k}7yhI8;H@!(D#Y`5~)IT!ul9c6!rwt3zeXAh2S`n)sFFeW6<{c1?xm^5~V z`G?c^z;-6HqvITRV+P|tG?zJ&{XYw4M;phE@7Hs+#rT;_KN|W8r5`o@1nFL?W0@Cgj~E0GF|)iGR%|>mX}-EN$(5R z@^U&7?VWLsF@DWg+OB6?S#Ns^_DA*udz8LC@(DNbM?#E1UQ0FOC-`^=oKs-9bGCz~ z_Nlmr#js$(0&?%X^G-v{19eSJuwYK4q@k{{26O+NcjBWGH6Y(2#yMzWzr4kn%*K1m zn3n##G|h~E@wMQT!kQgIt%juS^5l)TIWITr+;l&WQ>eC=;`}8Rl6>9!@_Fn*dfBOP zDPB#?Uhlgd(BXKAX$p`zil>>?j5AjGF5`h5h4_^L?9i@;?d*5Zt81XrNy&Nz?ki7oyh@n;v1$q-m% zbhSYZ0rqhP&KBEFwd%KA6xey=Uz3JT*9f2_#^;irAaF=K8I(Q{YhNLI=jQjZV}9TL zVr&Fma`zUKNaO?vY;ZzqIHONJMQO6?DV7Jz&;2O#Pd7h7IXcKzGmqqangaZ>s2(G% z1KjGALt0pLd|KX#H4$rolYtdlCKkH9=RO!ZvMZuX32`9L%#eoG_~c=%MkJE=F>Q9(3cP6Fjm&@;&x}s5L;Aij#-*AupB3D8 zi+%5NBqFe%ea^==@opycW}Cc=10(w6hS7Qja>u92ZYHOKnUG_3->9na)m@G0${zJ! zzAf;vPw7%DeKD&b1#Ac6F{1G_)hZPHm17Ebg+UA(Hv?_&aAJvbLs_UIK6B1y2Y3!& zjW@~dXjL&=X89QNnal>7h24u%gn#*0r4MAEsxg34Ent?>YSkZm&Ik3Ea< z((UP36MRREiN1Y(h4jQz8fO zOO;RAe_{x`VY(u?=Y(1+9lc%<<{Pkk5gje5Z09YE z8a>$_W%R72b0-kn4bu?XsIvOU&k>9d@L@_==6Z}7+VxrGS$`*u$OsUcJM88Ae530D z4~u*s0_=0%CEJEP{F6_WL6(ArXca~o>Enct8Og8yO{q)LRjOk8j31MsI?(Zp;J+)m z{$HK->^u;~%$= zSNZSqAeDT&x4X)J0~H&Sh6ZKFgq0_~lRMvF&C5zw)}qR@$kD=V!n3tUfI0PK!>zMz zxAHhyHLZ24Yf5X@i&u%0Q-*vEvy&Ps@0uN_Jh7GduQ-AKm2yjJ8dx5*Z80c|WM92X zA8e|rQv3usdKv4|w~W=4LlxHR54HrxH3}_dN-wJ& zMGOn1c9K_k7Ah|!rSQZmFC_Nx%vN4V+~J8(UPwyfnWDUqoCAyQLD-v9M|%T zj;VCUb_o{619#!lU3uWI+{)H@UB``+nBiiet}VUT>O*KdhJ}6GFMOltE?60oe(M~V zCR&^Wn)=D&UAST1IRGtJ`(ud2?9*3xlI+{*W!F2HFK60!k)N({@l3YwA;0~1bX#tr zs@S8$NZdz3bX6IcB)pLOXv9Nvd5CzSl{k_SSs!|@`AsaGbdUUsS^szzzjOqzi*cZX zbXyH{x9w$h>s;JHpzFK_Z}$Dj7gu&OW2LeK?PPqT$FSQRc$G{iSi)7fUZPc9Za?q~ zzMiO%cl?d-e*m;_Sde%V>puHY|JP__0%!(tD8UpPeLjxQ0~c}IshmhthW&&u`oPBs zw4b5R4mwACd77@Hm>u1psq0upGM3A;pp+a&mM5}RPB~21@fAO2zK5M{SBvEo*lD3s z&iX6y{9mXg_HzWa)9(?~6n^{szftYNq2hxp{=zsnIz5t9q3QBetNjAi>43~W8W_p!NN8YYFNIQO;YcySjH|S&QGJ~iKhx-ED*b5bXA1pHrk_dl zqp|8<&JLv?wbh$|;_Y*g2kJ0qfMA|9uoM~FEB#eF{AX6RWmL62f$hu+sM_37Rhxx3 z@JQ@bw+FAVSh#jG1fkv8st(2OUy4e%^&jXN+;9BI-$IzFdT9kVr{Q+dUv>^Jdp(u? z7?9P*OMVl`Z*PCWHx_O|ET>1PpNv;AVzD^M-thuWpFsI-l=8yw7|K7+TJSs0i^N`< zGTGtJADWG65#BWVM^r@4;wYKo!b1Vnk)x0dv~Nm{+ZZ_r_%YUR$K^A#JtaRoxfAF z;ei2gj4qCC(}a7ba#+4X??bo|gFu@GGRdH+a8zouPanc5enfJIRcJR zj#ht~#Tt3p*GO(J2<@}snNMx}d$2FpKL@j1>F(HIe_eh~jXLI!(;G~u9#Gx=BT?R_ zC_nbMac3|)hp^=dxb_}@)rSAbsuppQuHjYj$OoySB93WXtlgY+xW_Tie#9UD_#cV( zU6k_Xcz;1k@967tboOgBP}S1V5?-GeUw!6KX-EC3A7!PL54_+9bXYKhqHfETAR<=O zFGu;%KsJA)!vKLme!m=Lnt_vKU#W@VkM5B~wsAbgaAg?-po*OESNX$_8GvYk!}0nAhSfk}kgoyRl=qO{GmU4H z$cw^_fp?7*wSWt zk%Bv$Rlcg)RZ90%QI==?rM_{Npn?MJ5x-}}yh_EiX@LT|twxG_)*tt&v&?E1GnLAl za1A@xp^4CKp)jS5-=qE*R1ibVL1wDv3@Ddd?dSYi?f4UM9JOeV^NLCuYQ4oiLH?%eAQdwEus_jPA2Ee#Pb(|*Haf~|7k=sc`q4+qHhkEI z3>W(|6iaECHy7H~{-`g0GzK8g{fPhqJ-ngT+wB^Eylo$`Q}E7S@NN#Pm7G62M>%uB z(j9iKKXTPaY%dV;OW$4~fj8}A8ibDX{E*m1`OZ(2wfl%7ps~k_Kl>$8Y@0EJy(3@6 zo2Qbn&7nFx0Mi#<9_F2`f8F=pJA2o`5&w$;{tE$qeSkmQ?>~Or9*YAz)lakKy_6ND zcQr)m-FW7mC80l#Le|K*>R=d4H!r&ch?GF8$xx1P7|W?@*HrQ@p=J~x7XHJ(#*YPI z3!ZyF=I=YSR^nNYrvuM+Jp1sxiRUkPg8qQ` zcw+IS;IZOq#IqI8PCP%ua|F)^c+TS)#`9B@v*07_MewY^(|~6Ko~?Mc0I31jVrxH&io~?Lx;du$q2|VZU`~y!Y+BFl;0z4^r9z}Vrc&hP~;7P`F z1M-VHGa5#xlkQkdE}i;Xh9~{E{5gB_>{eI%iRnAeX@s=sn_KQkDER9Z$B%QRDW9x- z`?2m~N<%%j8`mBGDoZ8!-cw;*ZZRJ0?Mf^;*wYo3T1v7rGU*Q4EK7M_rllamN_(9p z1^Ib}iv^pdv@oNp>Ido??+^Sy;TfXXTz8*CtSMbJ z?ptAvxNdFp14T_WjpOnwscBl(+)`s*Th}O#O;_w#E3O)qf~P51+gg*^;J~ev&ExV@ z?&rk&=!EpG=)U@z)nZ=t*m5XMb4^Rj*hCx-bqy^IYsX#?HtO5Bi_nzvtaVLj-|xBRuMS2bKE-Pk(XM95OtjL#>d0Z?efUbPV7rwQ`1G*%bYl4pW%84V4n!Njn_ zw{h*wXsEltaZSzWj`H_O^|(4`I~p1+t0rjs+LpSprBXQEEjqy`jzef(%LL`-*EK#c zHlLE3#&N*#W1y_u}NademQzJXrAs^)QcwP;mK-RcSIylAax&#G$% zF0P#rZwPMx>igBc6%|%3DzRiR8d6!5on2+K6w+N4*+nH)l|_Zj&C1N;uC0W*nWZ+t zno*KhiVFvd%PeJ>k0oc3n+#K!}BgIJ|+8z?T}GrqD{XuBy1KsFZH)K)XwpC~urDR%Pa96y{hMZM2nI z3aT=T3QJ3h@)iCVg(JPql3B(f=gVUxBoiG`YN;y7$ji6o7nKT``Id~rGOLhZl(~2$ zH{_FBR+?2*!Qjp4b74k)m9G*?v@x$jc3!@vYD~qlK;kn?i%^G=%&Cs$Mfqg~ma4o$ zbap{TDcI<~x=!>3dFPkea;p}VWoLuwuH<3K@TIY^OHL>(Fd6?gI-?4Jf#3%)us+Sq zFS60Q0vDQRvZ}0k){!vEwe0m{N}t3jkv)rEnYInU|S|;fPRMQCUeQwJkHF zFw>Hs&+_LGuPQAX%hoGVa}1-y(Dbm7iZ!WM#Cd ztTa|}V+7tdiZ5j2s})c7BEq#CLc%Hw_^y^p3`H7%meSm!EdMu*6I;fjd>Tvud?rU$ zOX0X6e>_V$I?d*f<9ii2fvBo|WN6eqXtKE7t~<%RW}F~A=i;3r^1X^NJ7LRn)=Q_bqS+By^o>MVd&uC#$uMWPEn z`pD%7HW~t~u@xD>(+b|tJWakOCnIwSjn=U=$W~TjEy=Ud2r4MTkP`gs)v7XEMh+X$ zc^FT=1U$d-87XyxghNRU$d~&2gU>ZoXQIjEw-Cwj(JsQo?7SR7p<%vP&|acLzER8u zIY@7Tf)+~NN_nN0%2I#W#7Rbf^Sx(gSTh#oKPacaF&z?q#;;l=V1T_&Tj}SD9Q?)LWP_O!f1Xh zVI5>+-?@*c&nzk^@byN{n0M}CaNTR6xx!LdR#270@UVnaf0hRiXJJ_uTV_C*(jw5l ztcr}1kvuGwR!mqU;rX_b0vkYGU){72wCl%@PQ23R{#>mQ_{4P+hQ=m9TXT8-G|J7G&5K z`|@OAN3^En)dU&`xCF0j@T*#GpMXtIsNhUKZuuQp$qf$w<*SOnzJ za)8f0^qtRByug6>LjPDD(_X%g!@8BD08bOp{*p?DSwgAemQ?b&t5k6rK?lKOZVbgw zipJ7_<&6$y{a3=j-kJVt9=*@i3nw@n=uaQuHw5^j1N>x`A|3DNb8z+%_dE?Q zhR`Tq<*(0--z*LLbJ_Q1{K|aI&BklgEx|H(TmGagH z#3Sj8orTX5D&cD3X0iIr65`;7!;OMVCgQ^3l7v{z>N!ie_#2fF1@}DMIdIRxoe%dE z+-SJR;l{x|0yhcnez+-c_x*<2H%r(Bmjok^!6h4|?Qrwqu7_)-dciG)TMCz?Nx5)I z0+0fCCEWRNtKmleh7?<730k-faED=Wl%xme;fiq2z$Hfb3Ah{J_K-!=phRI0Ts7Rs z;A-G*fUAXDeVpJ9c);zTa^ZHtO@h0P%7yzFTm#(4sa&|*;SRl}5_S+A-cpUi(Nk}6 z{ONg%_s2fC5Q7NY-{SqU4Q>ea3tR{Xg=)B@;g|mw@2?cN5QYnL;F2C?1l%cbhu@_B z0zSbdUg}Y}I=Bbk{i2j!s8yiBi&%KU_&ik6KAf z&&7U~U;pG>Kd%ctr}|lb?X0Q&qhY5~p8fqie|q-x^ZL=Vqkp8VZT%xse;gSa4aNmfFvU<+-vGLS%0xmA*>gj(j!$Ra;P`a>4USJeZ*W-ce}m)Go;Nss*!Bj;EAb6Z zA6CA>=|lb-tUW@?8=Ou=y}{ud@dl?8nm0I|5Z>T);=&P5C(a(>bmGJjPA85Y;dEmE z5l$!ez`dUO4(<&!@4%f!^*A!Rog0qucG{2dc2*wY?aVsD+c|$se1j4{T#2t%;tw6> z@y{I|9sl%U9{0Ug{;kNrQZ~u-l@gG0T<8M>qcPQ~UDDj&PbG)xU z%yDwtnDn_y`ZE-=6r&po(y}TbU z^zeQ>(Zl=k#U7gXf#+~^9@H$3)o$c1eb^qxzveejdiObWr8G(m{CG3VrSJ*R4yqHgm@Mr zu3ByPg|txG?B<%9F=-u$SyFR<9f=SX$pguDYF8mqIKDxe>CdHfts>Vz&;!C9_?ih( z+(@bWsSG9p)v=(oQ#Epei)QNToONBJ`z3Va?*0$6L z+u(zoktJVT^PsRE?<`|~h#jSq7}X~m;pNenKd2Vh5Rf2x!Qnz1Sa*=tJOCU~SFKwO zG1M9`4DUgH9Af?qg*ZPHZ4a)wZ*4=}YLch{*Qr(f!>DFKCk`oWKJknnY)oHP*o|)_ zZRRPM#04iz#b#>%my{zrggQFI1%8N*uE8JFs5mr_fipn`YZx5Ba1^2cj0Or-NR`(J zX;59Yzb{6FJV7c}F#!94nrfUDXs#i#B@A{1hUYcr z4G!kjKz7CD#xM&TWL=APHj9N&UuJ@*yyhn1FBDqC-#HXMLP$Vnf@y-DXR>YTeqooo zs%HJ_nkKQz4l!awO*2@r!GZO(tY3{U_tyw*3~L*3M6)K7WNHomZjKCE47r;~XGBrn zYrq8_fd_0?SD`j*91X&Iqp&2aX|?dts1P1sjog^5t%jZBq-et55l@kGi*!`6i4}&q z0C_`PSzXsE2sZ}Q!UFC5L+Uj(YbYDx7V7&996D>PRz_m!THjbSjQhsFjKugBrvPHj z{WZ;{Yex^|Lck=)hmF=|@Iso!2Zf&y9ulgtbCdaN@3#O6f$dXiY%4u6`qS^J461qQ zTKdhdt7)ii!EV&Do+BOq#3c5t;h*{7G4fe>9OH|XN7yjMD>|4x>~pekSwiR{Qy&D7hXkKiyZf7+G|!nKyYmo*S0*++}N<7 zW_=CW23lOJV1*waiV&8u`Wp@iiv3|(!s42dvhte0fianzk-wn2p@H~=YwE^P-XIF) z?emXfjPCrrMGGK({NGH(L2SK*wMaYL-|)S_sNDyxB&=5nw@(m0Nj)ymA5K>d?D``s zXJieEcXjaC{Z43l5FWPH#@lQm22|SsHx{Rqrn0r3o`sLRtAmMcRE`r&RrrMr4nA(_ zqY5r$=kU|tuhoCQLL~$#yD6W$(6?X1KdR2;fI3!_No>WoNFB~h-S0m=nTNMpoWsN> zg~f@+ekK}phTKK?#hi|nrAo2%)P!?Tjj%IS2zQNQ!K)nabXLla_ZH;2N@!&#wwjTG zPI$I5_=h78Uzu4f=4w`#Ce*18u^RbW4`)3a@b50&PJ<sl0(THl0rQjPCNOG$P% zKFk1~*lJ-e+*ssA5WGwAnGp7M;FOM4zY6`#^Q=?q>1$`>RqDF{`8~)iXyr0ETUnmf zfZ1x)DzLA;2Jr zok!wcjXM?hNnBjKIX)x)zWB}YUGeVt?eWjX?~Q*c{@3wG;@^t@ef&r91M%-C{4-%j z;;h756Jrt+(hAeIr0q?6DeY96oc8xL)xySw;==DO+`W)C9>|pFyu{RiRFyrK?%}w{QNN$Yy%_h~xc`p(YuvT* zH^xWB-x)ug7?Pw*x;`l;DI=*MX<1TrQd82dqpCwY=tZ@%8_HhibaV89 z=s%#Ju8En8UdoEei#Z$)}Pho1@H)W|w)F`M=DE%zrk2VU9?dpVFFgK4oU=w^E-@ zeJM3EEhTMb+QVs5+OD*|v=7q;(~=jKF05Yo;KH9Qd~YFVnw_Wpc=R^l)V>&R%%ic- z#U76RU921%Y<$G{tnoGDQR9CbuZf!-*MJs08Fx9ZKK?Oa_JR15sP}MuNW!FqsR`2) zW+YsnaAU$P39}RCCft<}lMtU^N=Qw}NU$X2B@`qSCzK^DNm!n+DxoH!E@4f=Hxe8P z4<@uFY)F(r|Y~(pQj1gTQpMD*sZXTtOno->ᙴEIu{)oI^I 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 E+E73nh5!Hn literal 0 HcmV?d00001 diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index 42b18421b..57c0c970a 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -33,11 +33,11 @@ class ModbusTcpProtocol(protocol.Protocol): def connectionMade(self): ''' Callback for when a client connects - Note, since the protocol factory cannot be accessed from the - protocol __init__, the client connection made is essentially our - __init__ method. + ..note:: since the protocol factory cannot be accessed from the + protocol __init__, the client connection made is essentially + our __init__ method. ''' - #_logger.debug("Client Connected [%s]" % self.transport.getHost()) + _logger.debug("Client Connected [%s]" % self.transport.getHost()) self.framer = self.factory.framer(decoder=self.factory.decoder) def connectionLost(self, reason): @@ -65,7 +65,7 @@ def _execute(self, request): context = self.factory.store[request.unit_id] response = request.execute(context) except Exception, ex: - _logger.debug("Datastore unable to fulfill request %s" % ex) + _logger.debug("Datastore unable to fulfill request: %s" % ex) response = request.doException(merror.SlaveFailure) #self.framer.populateResult(response) response.transaction_id = request.transaction_id @@ -161,7 +161,7 @@ def _execute(self, request, addr): context = self.store[request.unit_id] response = request.execute(context) except Exception, ex: - _logger.debug("Datastore unable to fulfill request %s" % ex) + _logger.debug("Datastore unable to fulfill request: %s" % ex) response = request.doException(merror.SlaveFailure) #self.framer.populateResult(response) response.transaction_id = request.transaction_id @@ -228,6 +228,7 @@ def StartSerialServer(context, identity=None, _logger.info("Starting Modbus Serial Server on %s" % port) factory = ModbusServerFactory(context, framer, identity) protocol = factory.buildProtocol(None) + SerialPort.getHost = lambda self: port # hack for logging handle = SerialPort(protocol, port, reactor, Defaults.Baudrate) reactor.run() diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index 92654d0e1..670488fb4 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -57,7 +57,7 @@ def execute(self, request): context = self.server.context[request.unit_id] response = request.execute(context) except Exception, ex: - _logger.debug("Datastore unable to fulfill request %s" % ex) + _logger.debug("Datastore unable to fulfill request: %s" % ex) response = request.doException(merror.SlaveFailure) response.transaction_id = request.transaction_id response.unit_id = request.unit_id From 6e480d1b1d4c651519228c5eddd21f16422113fc Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 19 Oct 2011 20:02:10 +0000 Subject: [PATCH 042/243] fixing the synchronous server implementation --- examples/common/asynchronous-server.py | 4 +-- examples/common/synchronous-server.py | 4 +-- pymodbus/server/sync.py | 38 ++++++++++++++++++++++++-- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/examples/common/asynchronous-server.py b/examples/common/asynchronous-server.py index 4fb378c9e..3b48a222b 100755 --- a/examples/common/asynchronous-server.py +++ b/examples/common/asynchronous-server.py @@ -68,6 +68,6 @@ #---------------------------------------------------------------------------# # run the server you want #---------------------------------------------------------------------------# -#StartTcpServer(context) +StartTcpServer(context) #StartUdpServer(context) -StartSerialServer(context, port='/dev/pts/13') +#StartSerialServer(context, port='/dev/pts/13') diff --git a/examples/common/synchronous-server.py b/examples/common/synchronous-server.py index 9f28e88ca..0dba6d209 100755 --- a/examples/common/synchronous-server.py +++ b/examples/common/synchronous-server.py @@ -69,6 +69,6 @@ #---------------------------------------------------------------------------# # run the server you want #---------------------------------------------------------------------------# -#StartTcpServer(context) +StartTcpServer(context) #StartUdpServer(context) -StartSerialServer(context, port='/dev/pts/13') +#StartSerialServer(context, port='/dev/pts/13', timeout=1) diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index 670488fb4..46dd8fc4d 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -90,6 +90,38 @@ def send(self, message): ''' raise NotImplementedException("Method not implemented by derived class") +class ModbusSingleRequestHandler(ModbusBaseRequestHandler): + ''' Implements the modbus server protocol + + This uses the socketserver.BaseRequestHandler to implement + the client handler for a single client(serial clients) + ''' + + def handle(self): + ''' Callback when we receive any data + ''' + while self.running: + try: + data = self.request.recv(1024) + if data: + _logger.debug(" ".join([hex(ord(x)) for x in data])) + self.framer.processIncomingPacket(data, self.execute) + except socket.timeout: pass + except socket.error, msg: + _logger.error("Socket error occurred %s" % msg) + except: pass + + def send(self, message): + ''' Send a request (string) to the network + + :param message: The unencoded modbus response + ''' + if message.should_respond: + #self.server.control.Counter.BusMessage += 1 + pdu = self.framer.buildPacket(message) + _logger.debug('send: %s' % b2a_hex(pdu)) + return self.request.send(pdu) + class ModbusConnectedRequestHandler(ModbusBaseRequestHandler): ''' Implements the modbus server protocol @@ -293,7 +325,7 @@ def __init__(self, context, framer=None, identity=None, **kwargs): if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) - self.device = kwargs.get('device', 0) + self.device = kwargs.get('port', 0) self.stopbits = kwargs.get('stopbits', Defaults.Stopbits) self.bytesize = kwargs.get('bytesize', Defaults.Bytesize) self.parity = kwargs.get('parity', Defaults.Parity) @@ -326,8 +358,8 @@ def _build_handler(self): request = self.socket request.send = request.write request.recv = request.read - handler = ModbusConnectedRequestHandler(request, - ('127.0.0.1', self.device), self) + handler = ModbusSingleRequestHandler(request, + (self.device, self.device), self) return handler def serve_forever(self): From d49a5d3ece733c611f3ce873879a98e704335568 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 19 Oct 2011 20:20:37 +0000 Subject: [PATCH 043/243] updating functional tests and documentation for sync-serial --- doc/sphinx/examples/serial-forwarder.rst | 6 ++++++ doc/sphinx/library/sync-server.rst | 11 ++++++++++- examples/functional/synchronous-ascii-client.py | 5 ++++- examples/functional/synchronous-rtu-client.py | 4 +++- 4 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 doc/sphinx/examples/serial-forwarder.rst diff --git a/doc/sphinx/examples/serial-forwarder.rst b/doc/sphinx/examples/serial-forwarder.rst new file mode 100644 index 000000000..be458025b --- /dev/null +++ b/doc/sphinx/examples/serial-forwarder.rst @@ -0,0 +1,6 @@ +================================================== +Synchronous Serial Forwarder +================================================== + +.. literalinclude:: ../../../examples/common/synchronous-serial-forwarder.py + diff --git a/doc/sphinx/library/sync-server.rst b/doc/sphinx/library/sync-server.rst index 4ef4be10e..3dcf9e04d 100644 --- a/doc/sphinx/library/sync-server.rst +++ b/doc/sphinx/library/sync-server.rst @@ -12,7 +12,16 @@ API Documentation .. automodule:: pymodbus.server.sync -.. autoclass:: ModbusRequestHandler +.. autoclass:: ModbusBaseRequestHandler + :members: + +.. autoclass:: ModbusSingleRequestHandler + :members: + +.. autoclass:: ModbusConnectedRequestHandler + :members: + +.. autoclass:: ModbusDisconnectedRequestHandler :members: .. autoclass:: ModbusTcpServer diff --git a/examples/functional/synchronous-ascii-client.py b/examples/functional/synchronous-ascii-client.py index ad96c6c5c..2099881cf 100644 --- a/examples/functional/synchronous-ascii-client.py +++ b/examples/functional/synchronous-ascii-client.py @@ -12,7 +12,10 @@ class SynchronousAsciiClient(Runner, unittest.TestCase): def setUp(self): ''' Initializes the test environment ''' super(Runner, self).setUp() - self.client = ModbusClient(method='ascii') + # "../tools/nullmodem/linux/run", + self.initialize(["../tools/reference/diagslave", "-m", "ascii", "/dev/pts/14"]) + self.client = ModbusClient(method='ascii', timeout=0.2, port='/dev/pts/13') + self.client.connect() def tearDown(self): ''' Cleans up the test environment ''' diff --git a/examples/functional/synchronous-rtu-client.py b/examples/functional/synchronous-rtu-client.py index 8f1b5f5b1..3885f9ff0 100644 --- a/examples/functional/synchronous-rtu-client.py +++ b/examples/functional/synchronous-rtu-client.py @@ -12,7 +12,9 @@ class SynchronousRtuClient(Runner, unittest.TestCase): def setUp(self): ''' Initializes the test environment ''' super(Runner, self).setUp() - self.client = ModbusClient(method='rtu') + self.initialize(["../tools/reference/diagslave", "-m", "rtu", "/dev/pts/14"]) + self.client = ModbusClient(method='rtu', timeout=0.2, port='/dev/pts/13') + self.client.connect() def tearDown(self): ''' Cleans up the test environment ''' From 8899ac9beffc0329b71c66a6e0a3ff718d886cc4 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 19 Oct 2011 21:15:24 +0000 Subject: [PATCH 044/243] updating tac files --- examples/twisted/modbus-tcp.tac | 2 ++ examples/twisted/modbus-udp.tac | 2 ++ 2 files changed, 4 insertions(+) diff --git a/examples/twisted/modbus-tcp.tac b/examples/twisted/modbus-tcp.tac index a4e33e9de..e1dcfc465 100644 --- a/examples/twisted/modbus-tcp.tac +++ b/examples/twisted/modbus-tcp.tac @@ -24,5 +24,7 @@ def BuildService(): return application application = service.Application("Modbus TCP Server") +logfile = DailyLogFile("pymodbus.log", "/tmp") +application.setComponent(ILogObserver, FileLogObserver(logfile).emit) service = BuildService() service.setServiceParent(application) diff --git a/examples/twisted/modbus-udp.tac b/examples/twisted/modbus-udp.tac index ed2719e03..d73fcae26 100644 --- a/examples/twisted/modbus-udp.tac +++ b/examples/twisted/modbus-udp.tac @@ -24,5 +24,7 @@ def BuildService(): return application application = service.Application("Modbus UDP Server") +logfile = DailyLogFile("pymodbus.log", "/tmp") +application.setComponent(ILogObserver, FileLogObserver(logfile).emit) service = BuildService() service.setServiceParent(application) From 6c5ebdd701dde547acdeceb123171b25e3729596 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Sun, 20 Nov 2011 18:43:09 +0000 Subject: [PATCH 045/243] Updating the documentation for the serial client/servers --- pymodbus/client/sync.py | 6 ++++++ pymodbus/server/async.py | 6 +++++- pymodbus/server/sync.py | 12 ++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 88c774ab9..b3f61d76d 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -255,6 +255,12 @@ def __init__(self, method='ascii', **kwargs): - binary :param method: The method to use for connection + :param port: The serial port to attach to + :param stopbits: The number of stop bits to use + :param bytesize: The bytesize of the serial messages + :param parity: Which kind of parity to use + :param baudrate: The baud rate to use for the serial device + :param timeout: The timeout to use for the serial device ''' self.method = method self.socket = None diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index 57c0c970a..e5fc9a28a 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -220,16 +220,20 @@ def StartSerialServer(context, identity=None, :param context: The server data context :param identify: The server identity to use (default empty) :param framer: The framer to use (default ModbusAsciiFramer) + :param port: The serial port to attach to + :param baudrate: The baud rate to use for the serial device ''' from twisted.internet import reactor from twisted.internet.serialport import SerialPort port = kwargs.get('port', '/dev/ttyS0') + baudrate = kwargs.get('baudrate', Defaults.Baudrate) + _logger.info("Starting Modbus Serial Server on %s" % port) factory = ModbusServerFactory(context, framer, identity) protocol = factory.buildProtocol(None) SerialPort.getHost = lambda self: port # hack for logging - handle = SerialPort(protocol, port, reactor, Defaults.Baudrate) + handle = SerialPort(protocol, port, reactor, baudrate) reactor.run() #---------------------------------------------------------------------------# diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index 46dd8fc4d..ca6db32ff 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -314,6 +314,12 @@ def __init__(self, context, framer=None, identity=None, **kwargs): :param context: The ModbusServerContext datastore :param framer: The framer strategy to use :param identity: An optional identify structure + :param port: The serial port to attach to + :param stopbits: The number of stop bits to use + :param bytesize: The bytesize of the serial messages + :param parity: Which kind of parity to use + :param baudrate: The baud rate to use for the serial device + :param timeout: The timeout to use for the serial device ''' self.threads = [] @@ -409,6 +415,12 @@ def StartSerialServer(context=None, identity=None, **kwargs): :param context: The ModbusServerContext datastore :param identity: An optional identify structure + :param port: The serial port to attach to + :param stopbits: The number of stop bits to use + :param bytesize: The bytesize of the serial messages + :param parity: Which kind of parity to use + :param baudrate: The baud rate to use for the serial device + :param timeout: The timeout to use for the serial device ''' framer = ModbusAsciiFramer server = ModbusSerialServer(context, framer, identity, **kwargs) From 0c28dfa0f7c3145e681b999c687048e9da9ff17c Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Sun, 4 Dec 2011 20:08:24 -0800 Subject: [PATCH 046/243] adding readme to root --- README.rst | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++++ doc/README | 44 ------------------- 2 files changed, 123 insertions(+), 44 deletions(-) create mode 100644 README.rst delete mode 100644 doc/README diff --git a/README.rst b/README.rst new file mode 100644 index 000000000..c720ce743 --- /dev/null +++ b/README.rst @@ -0,0 +1,123 @@ +============================================================ +Summary +============================================================ + +Pymodbus is a full Modbus protocol implementation using twisted for its +asynchronous communications core. It can also be used without any third +party dependencies (aside from pyserial) if a more lightweight project is +needed. Furthermore, it should work fine under any python version > 2.3 +with a python 3.0 branch currently being maintained as well. + +============================================================ +Features +============================================================ + +------------------------------------------------------------ +Client Features +------------------------------------------------------------ + + * Full read/write protocol on discrete and register + * Most of the extended protocol (diagnostic/file/pipe/setting/information) + * TCP, UDP, Serial ASCII, Serial RTU, and Serial Binary + * asynchronous(powered by twisted) and synchronous versions + * Payload builder/decoder utilities + +------------------------------------------------------------ +Server Features +------------------------------------------------------------ + + * Can function as a fully implemented modbus server + * TCP, UDP, Serial ASCII, Serial RTU, and Serial Binary + * asynchronous(powered by twisted) and synchronous versions + * Full server control context (device information, counters, etc) + * A number of backing contexts (database, redis, a slave device) + +============================================================ +Use Cases +============================================================ + +Although most system administrators will find little need for a Modbus +server on any modern hardware, they may find the need to query devices on +their network for status (PDU, PDR, UPS, etc). Since the library is written +in python, it allows for easy scripting and/or integration into their existing +solutions. + +Continuing, most monitoring software needs to be stress tested against +hundreds or even thousands of devices (why this was originally written), but +getting access to that many is unwieldy at best. The pymodbus server will allow +a user to test as many devices as their base operating system will allow (*allow* +in this case means how many Virtual IP addresses are allowed). + +For more information please browse the project documentation: +http://readthedocs.org/docs/pymodbus/en/latest/index.html + +------------------------------------------------------------ +Example Code +------------------------------------------------------------ + +For those of you that just want to get started fast, here you go:: + + from pymodbus.client.sync import ModbusTcpClient + + client = ModbusClient('127.0.0.1') + client.write_coil(1, True) + result = client.read_coils(1,1) + print result.bits[0] + client.close() + +For more advanced examples, check out the examples included in the +respository. If you have created any utilities that meet a specific +need, feel free to submit them so others can benefit. + +Also, if you have questions, please ask them on the mailing list +so that others can benefit from the results and so that I can +trace them. I get a lot of email and sometimes these requests +get lost in the noise: http://groups.google.com/group/pymodbus + +------------------------------------------------------------ +Installing +------------------------------------------------------------ + +You can install using pip or easy install by issuing the following +commands in a terminal window (make sure you have correct +permissions or a virtualenv currently running):: + + easy_install -U pymodbus + pip install -U pymodbus + +Otherwise you can pull the trunk source and install from there:: + + git clone git://github.com/bashwork/pymodbus.git + cd pymodbus + python setup.py install + +Either method will install all the required dependencies +(at their appropriate versions) for your current python distribution. + +------------------------------------------------------------ +Current Work In Progress +------------------------------------------------------------ + +Since I don't have access to any live modbus devices anymore +it is a bit hard to test on live hardware. However, if you would +like your device tested, I accept devices via mail or by IP address. + +That said, the current work mainly involves polishing the library as +I get time doing such tasks as: + + * Fixing bugs/feature requests + * Architecture documentation + * Functional testing against any reference I can find + * The remaining edges of the protocol (that I think no one uses) + +------------------------------------------------------------ +License Information +------------------------------------------------------------ + +Pymodbus is built on top of code developed from by: + * Copyright (c) 2001-2005 S.W.A.C. GmbH, Germany. + * Copyright (c) 2001-2005 S.W.A.C. Bohemia s.r.o., Czech Republic. + * Hynek Petrak + * Twisted Matrix + +Released under the BSD License diff --git a/doc/README b/doc/README deleted file mode 100644 index 7d8bf4ecd..000000000 --- a/doc/README +++ /dev/null @@ -1,44 +0,0 @@ -#---------------------------------------------------------------------------# -# About Pymodbus -#---------------------------------------------------------------------------# -Pymodbus is a full Modbus protocol implementation using twisted for its -comunications core. - -This package can supply modbus clients and servers: - client: - - Can perform single get/set on discretes and registers - - Can perform multiple get/set on discretes and registers - - Working on diagnostic/file/pipe/setting/info requets - - Can fully scrape a host to be cloned - - server: - - Can funtion as a fully implemented TCP modbus server - - Working on creating server control context - - Working on serial communication - - Working on funtioning as a RTU/ASCII - - Can mimic a server based on the supplied input data - -#---------------------------------------------------------------------------# -# Use Cases -#---------------------------------------------------------------------------# -Although most system administrators will find little need for a modbus -server on any modern hardware, they may find the need to query devices on -their network for status (PDU, PDR, UPS, etc). Since the library is written -in python, it allows for easy scripting and/or integration into their existing -solutions. - -Continuing, most monitoring software needs to be stress tested against -hundreds or even thousands of devices (why this was originally written), but -getting access to that many is unweildy at best. The server will allow -one to test as many devices as the base operating system will allow ip -address leases (linux allows just about as many virtual interfaces as you want) - -#---------------------------------------------------------------------------# -# License Information -#---------------------------------------------------------------------------# -PyModbus is built on top of the Pymodbus developed from code by: - Copyright (c) 2001-2005 S.W.A.C. GmbH, Germany. - Copyright (c) 2001-2005 S.W.A.C. Bohemia s.r.o., Czech Republic. - Hynek Petrak - -Under the terms of the BSD license which is included with this distribution. From a92c34192f9734a7e8c4659eb9ffd0a98065e1b2 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 16 Jan 2012 22:18:38 -0800 Subject: [PATCH 047/243] Fixes issue 56 --- pymodbus/client/sync.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index b3f61d76d..c3f6541d2 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -132,9 +132,7 @@ def connect(self): ''' if self.socket: return True try: - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.settimeout(Defaults.Timeout) - self.socket.connect((self.host, self.port)) + self.socket = socket.create_connection((self.host, self.port), Defaults.Timeout) self.transaction = ModbusTransactionManager(self) except socket.error, msg: _logger.error('Connection to (%s, %s) failed: %s' % \ From 59f8cb0326074a5fbf3142ef065cd56b12dd70d8 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 16 Jan 2012 22:43:24 -0800 Subject: [PATCH 048/243] Fixes issue 1 --- examples/common/synchronous-client.py | 4 ++++ pymodbus/client/sync.py | 22 ++++++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/examples/common/synchronous-client.py b/examples/common/synchronous-client.py index 56061cda8..32e6b3e93 100755 --- a/examples/common/synchronous-client.py +++ b/examples/common/synchronous-client.py @@ -34,8 +34,12 @@ # make sure to start an implementation to hit against. For this # you can use an existing device, the reference implementation in the tools # directory, or start a pymodbus server. +# +# It should be noted that you can supply an ipv4 or an ipv6 host address for +# both the UDP and TCP clients. #---------------------------------------------------------------------------# client = ModbusClient('127.0.0.1') +client.connect() #---------------------------------------------------------------------------# # example requests diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index c3f6541d2..f5419c387 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -75,6 +75,8 @@ def execute(self, request=None): :param request: The request to process :returns: The result of the request execution ''' + if not self.connect(): + raise ConnectionException("Failed to connect[%s]" % (self.__str__())) if self.transaction: return self.transaction.execute(request) raise ConnectionException("Client Not Connected") @@ -119,6 +121,8 @@ def __init__(self, host='127.0.0.1', port=Defaults.Port): :param host: The host to connect to (default 127.0.0.1) :param port: The modbus port to connect to (default 502) + + .. note:: The host argument will accept ipv4 and ipv6 hosts ''' self.host = host self.port = port @@ -191,6 +195,20 @@ def __init__(self, host='127.0.0.1', port=Defaults.Port): self.socket = None BaseModbusClient.__init__(self, ModbusSocketFramer(ClientDecoder())) + @classmethod + def _get_address_family(cls, address): + ''' A helper method to get the correct address family + for a given address. + + :param address: The address to get the af for + :returns: AF_INET for ipv4 and AF_INET6 for ipv6 + ''' + try: + addr = socket.inet_pton(socket.AF_INET6, address) + except socket.error: # not a valid ipv6 address + return socket.AF_INET + return socket.AF_INET6 + def connect(self): ''' Connect to the modbus tcp server @@ -198,8 +216,8 @@ def connect(self): ''' if self.socket: return True try: - self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - #self.socket.bind((self.host, self.port)) + family = ModbusUdpClient._get_address_family(self.host) + self.socket = socket.socket(family, socket.SOCK_DGRAM) except socket.error, ex: _logger.error('Unable to create udp socket %s' % ex) self.close() From 3e48cf34245257be0a4cba887740aae35412d284 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 16 Jan 2012 22:56:39 -0800 Subject: [PATCH 049/243] adding a requirements file for virtualenv folk --- requirements.txt | 14 ++++++++++++++ setup.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..76f54d308 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +distribute==0.6.24 +pyserial==2.5 +# ------------------------------------------------------------------- +# if you want to run the tests and code coverage, uncomment these +# these out +# ------------------------------------------------------------------- +#coverage==3.4 +#nose==1.0.0 +# ------------------------------------------------------------------- +# if you are just using the synchronous version, you can comment +# these out +# ------------------------------------------------------------------- +Twisted==11.0.0 +zope.interface==3.8.0 diff --git a/setup.py b/setup.py index 84edfbaed..59dcc95c7 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ zip_safe = True, install_requires = [ 'twisted >= 2.5.0', - 'nose >= 0.9.3', + 'nose >= 1.0.0', 'pyserial >= 2.4' ], extras_require = { From dd9fb0db7d6622517ef2af83610bcb36e45b9c7e Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Sat, 25 Feb 2012 17:31:09 -0800 Subject: [PATCH 050/243] adding more cohesive code to the payload utilities --- .gitignore | 6 +++ doc/quality/current.coverage | Bin 2683 -> 14775 bytes doc/sphinx/examples/modbus-payload.rst | 6 +++ examples/common/modbus-payload.py | 70 +++++++++++++++++++++++++ pymodbus/payload.py | 68 +++++++++++++++++++++++- test/test_payload.py | 33 ++++++++++-- 6 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 .gitignore create mode 100644 doc/sphinx/examples/modbus-payload.rst create mode 100755 examples/common/modbus-payload.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..5ba77b1e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.pyc +*.swp +build/ +dist/ +pymodbus.egg-info/ +.coverage diff --git a/doc/quality/current.coverage b/doc/quality/current.coverage index 58d4a2f4ee71da73714e6fc08d7cb68d34d0f751..28ed89062cafaab3aa4ce8dba1d1bb617fe148ff 100644 GIT binary patch literal 14775 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 diff --git a/doc/sphinx/examples/modbus-payload.rst b/doc/sphinx/examples/modbus-payload.rst new file mode 100644 index 000000000..79e46dfdb --- /dev/null +++ b/doc/sphinx/examples/modbus-payload.rst @@ -0,0 +1,6 @@ +================================================== +Modbus Payload Building/Decoding Example +================================================== + +.. literalinclude:: ../../../examples/common/modbus-payload.py + diff --git a/examples/common/modbus-payload.py b/examples/common/modbus-payload.py new file mode 100755 index 000000000..46072ff7a --- /dev/null +++ b/examples/common/modbus-payload.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +''' +Pymodbus Payload Building/Decoding Example +-------------------------------------------------------------------------- +''' +from pymodbus.constants import Endian +from pymodbus.payload import PayloadDecoder +from pymodbus.payload import PayloadBuilder +from pymodbus.client.sync import ModbusTcpClient as ModbusClient + +#---------------------------------------------------------------------------# +# We are going to use a simple client to send our requests +#---------------------------------------------------------------------------# +client = ModbusClient('127.0.0.1') +client.connect() + +#---------------------------------------------------------------------------# +# If you need to build a complex message to send, you can use the payload +# builder to simplify the packing logic. +# +# Here we demonstrate packing a random payload layout, unpacked it looks +# like the following: +# +# - a 8 byte string 'abcdefgh' +# - a 32 bit float 22.34 +# - a 16 bit unsigned int 0x1234 +# - an 8 bit int 0x12 +# - an 8 bit bitstring [0,1,0,1,1,0,1,0] +#---------------------------------------------------------------------------# +builder = PayloadBuilder(endian=Endian.Little) +builder.add_string('abcdefgh') +builder.add_32bit_float(22.34) +builder.add_16bit_uint(0x1234) +builder.add_8bit_int(0x12) +builder.add_bites([0,1,0,1,1,0,1,0]) +payload = builder.tolist() +address = 0x01 +result = client.write_registers(address, payload) + +#---------------------------------------------------------------------------# +# If you need to decode a collection of registers in a weird layout, the +# payload decoder can help you as well. +# +# Here we demonstrate decoding a random register layout, unpacked it looks +# like the following: +# +# - a 8 byte string 'abcdefgh' +# - a 32 bit float 22.34 +# - a 16 bit unsigned int 0x1234 +# - an 8 bit int 0x12 +# - an 8 bit bitstring [0,1,0,1,1,0,1,0] +#---------------------------------------------------------------------------# +address = 0x01 +count = 8 +result = client.read_input_registers(address, count) +decoder = PayloadDecoder.fromRegisters(result.registers, endian=Endian.Little) +decoded = [ + decoder.decode_string(8), + decoder.decode_32bit_float(), + decoder.decode_16bit_uint(), + decoder.decode_8bit_int(), + decoder.decode_bits(), +] +for decode in decoded: + print decode + +#---------------------------------------------------------------------------# +# close the client +#---------------------------------------------------------------------------# +client.close() diff --git a/pymodbus/payload.py b/pymodbus/payload.py index 31d02bfc0..fda90e6da 100644 --- a/pymodbus/payload.py +++ b/pymodbus/payload.py @@ -7,6 +7,9 @@ ''' from struct import pack, unpack from pymodbus.constants import Endian +from pymodbus.utilities import pack_bitstring +from pymodbus.utilities import unpack_bitstring +from pymodbus.exceptions import ParameterException class PayloadBuilder(object): @@ -47,9 +50,28 @@ def tostring(self): def tolist(self): ''' Return the payload buffer as a list + This list is two bytes per element and can + thus be treated as a list of registers. + :returns: The payload buffer as a list ''' - return self._payload + string = self.tostring() + length = len(string) + string = string + ('\x00' * (length % 2)) + return [string[i:i+2] for i in xrange(0, length, 2)] + + def add_bits(self, values): + ''' Adds a collection of bits to be encoded + + If these are less than a multiple of eight, + they will be left padded with 0 bits to make + it so. + + :param value: The value to add to the buffer + ''' +# TODO endianess issue here + value = pack_bitstring(values) + self._payload.append(value) def add_8bit_uint(self, value): ''' Adds a 8 bit unsigned int to the buffer @@ -163,6 +185,39 @@ def __init__(self, payload, endian=Endian.Little): self._pointer = 0x00 self._endian = endian + @staticmethod + def fromRegisters(registers, endian=Endian.Little): + ''' Initialize a payload decoder with the result of + reading a collection of registers from a modbus device. + + The registers are treated as a list of 2 byte values. + + :param registers: The register results to initialize with + :param endian: The endianess of the payload + :returns: An initialized PayloadDecoder + ''' + if isinstance(registers, list): + payload = ''.join(pack("H", x) for x in registers) + return PayloadDecoder(payload, endian) + raise ParameterException('Invalid collection of registers supplied') + + @staticmethod + def fromCoils(coils, endian=Endian.Little): + ''' Initialize a payload decoder with the result of + reading a collection of coils from a modbus device. + + The coils are treated as a list of bit(boolean) values. + + :param coils: The coil results to initialize with + :param endian: The endianess of the payload + :returns: An initialized PayloadDecoder + ''' + if isinstance(coils, list): +# TODO endianess issue here + payload = pack_bitstring(coils) + return PayloadDecoder(payload, endian) + raise ParameterException('Invalid collection of coils supplied') + def reset(self): ''' Reset the decoder pointer back to the start ''' @@ -176,6 +231,17 @@ def decode_8bit_uint(self): handle = self._payload[self._pointer - 1:self._pointer] return unpack('B', handle)[0] + def decode_bits(self): + ''' Decodes a byte worth of bits from the buffer + + :param bytes: The number of bytes to decode + ''' +# TODO endianess issue here + self._pointer += 1 + fstring = self._endian + 'B' + handle = self._payload[self._pointer - 1:self._pointer] + return unpack_bitstring(handle) + def decode_16bit_uint(self): ''' Decodes a 16 bit unsigned int from the buffer ''' diff --git a/test/test_payload.py b/test/test_payload.py index a2227de55..4c1ff6d7c 100644 --- a/test/test_payload.py +++ b/test/test_payload.py @@ -9,6 +9,7 @@ * PayloadDecoder ''' import unittest +from pymodbus.exceptions import ParameterException from pymodbus.constants import Endian from pymodbus.payload import PayloadBuilder, PayloadDecoder @@ -30,13 +31,15 @@ def setUp(self): '\x01\x02\x00\x03\x00\x00\x00\x04\x00\x00\x00\x00' \ '\x00\x00\x00\xff\xfe\xff\xfd\xff\xff\xff\xfc\xff' \ '\xff\xff\xff\xff\xff\xff\x00\x00\xa0\x3f\x00\x00' \ - '\x00\x00\x00\x00\x19\x40\x74\x65\x73\x74' + '\x00\x00\x00\x00\x19\x40\x74\x65\x73\x74\x11' self.big_endian_payload = \ '\x01\x00\x02\x00\x00\x00\x03\x00\x00\x00\x00\x00' \ '\x00\x00\x04\xff\xff\xfe\xff\xff\xff\xfd\xff\xff' \ '\xff\xff\xff\xff\xff\xfc\x3f\xa0\x00\x00\x40\x19' \ - '\x00\x00\x00\x00\x00\x00\x74\x65\x73\x74' + '\x00\x00\x00\x00\x00\x00\x74\x65\x73\x74\x11' + + self.bitstring = [True, False, False, False, True, False, False, False] def tearDown(self): ''' Cleans up the test environment ''' @@ -60,6 +63,7 @@ def testLittleEndianPayloadBuilder(self): builder.add_32bit_float(1.25) builder.add_64bit_float(6.25) builder.add_string('test') + builder.add_bits(self.bitstring) self.assertEqual(self.little_endian_payload, builder.tostring()) def testBigEndianPayloadBuilder(self): @@ -76,6 +80,7 @@ def testBigEndianPayloadBuilder(self): builder.add_32bit_float(1.25) builder.add_64bit_float(6.25) builder.add_string('test') + builder.add_bits(self.bitstring) self.assertEqual(self.big_endian_payload, builder.tostring()) def testPayloadBuilderReset(self): @@ -83,8 +88,10 @@ def testPayloadBuilderReset(self): builder = PayloadBuilder() builder.add_8bit_uint(0x12) builder.add_8bit_uint(0x34) - self.assertEqual('\x12\x34', builder.tostring()) - self.assertEqual(['\x12', '\x34'], builder.tolist()) + builder.add_8bit_uint(0x56) + builder.add_8bit_uint(0x78) + self.assertEqual('\x12\x34\x56\x78', builder.tostring()) + self.assertEqual(['\x12\x34', '\x56\x78'], builder.tolist()) builder.reset() self.assertEqual('', builder.tostring()) self.assertEqual([], builder.tolist()) @@ -107,6 +114,7 @@ def testLittleEndianPayloadDecoder(self): self.assertEqual(1.25, decoder.decode_32bit_float()) self.assertEqual(6.25, decoder.decode_64bit_float()) self.assertEqual('test', decoder.decode_string(4)) + self.assertEqual(self.bitstring, decoder.decode_bits()) def testBigEndianPayloadDecoder(self): ''' Test basic bit message encoding/decoding ''' @@ -122,6 +130,7 @@ def testBigEndianPayloadDecoder(self): self.assertEqual(1.25, decoder.decode_32bit_float()) self.assertEqual(6.25, decoder.decode_64bit_float()) self.assertEqual('test', decoder.decode_string(4)) + self.assertEqual(self.bitstring, decoder.decode_bits()) def testPayloadDecoderReset(self): ''' Test the payload decoder reset functionality ''' @@ -131,6 +140,22 @@ def testPayloadDecoderReset(self): decoder.reset() self.assertEqual(0x3412, decoder.decode_16bit_uint()) + def testPayloadDecoderRegisterFactory(self): + ''' Test the payload decoder reset functionality ''' + payload = [1,2,3,4] + decoder = PayloadDecoder.fromRegisters(payload, endian=Endian.Little) + encoded = '\x01\x00\x02\x00\x03\x00\x04\x00' + self.assertEqual(encoded, decoder.decode_string(8)) + self.assertRaises(ParameterException, lambda: PayloadDecoder.fromRegisters('abcd')) + + def testPayloadDecoderCoilFactory(self): + ''' Test the payload decoder reset functionality ''' + payload = [1,0,0,0,1,0,0,0] + decoder = PayloadDecoder.fromCoils(payload, endian=Endian.Little) + encoded = '\x11' + self.assertEqual(encoded, decoder.decode_string(1)) + self.assertRaises(ParameterException, lambda: PayloadDecoder.fromCoils('abcd')) + #---------------------------------------------------------------------------# # Main From 92a52f56fd98f0bf2aee1c7afbbc69b55753e020 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Sat, 25 Feb 2012 17:36:49 -0800 Subject: [PATCH 051/243] forgot to add the new example to the doc index --- doc/sphinx/examples/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/sphinx/examples/index.rst b/doc/sphinx/examples/index.rst index 514284e5c..640805959 100644 --- a/doc/sphinx/examples/index.rst +++ b/doc/sphinx/examples/index.rst @@ -16,6 +16,7 @@ Example Library Code asynchronous-server custom-message modbus-logging + modbus-payload modbus-scraper modbus-simulator synchronous-client From f91cef139a78f10f023aca0d47aa9c20a5b68056 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Sat, 25 Feb 2012 20:15:34 -0800 Subject: [PATCH 052/243] Fixing the rtu size calculation isse #55 on google code --- pymodbus/file_message.py | 44 +++---------------------------- pymodbus/register_read_message.py | 1 - test/test_client_sync.py | 3 ++- test/test_file_message.py | 16 +++++------ test/test_pdu.py | 4 +-- 5 files changed, 16 insertions(+), 52 deletions(-) diff --git a/pymodbus/file_message.py b/pymodbus/file_message.py index cc399357e..0dedbac4e 100644 --- a/pymodbus/file_message.py +++ b/pymodbus/file_message.py @@ -81,6 +81,7 @@ class ReadFileRecordRequest(ModbusRequest): MODBUS PDU: 235 bytes. ''' function_code = 0x14 + _rtu_byte_count_pos = 2 def __init__(self, records=None): ''' Initializes a new instance @@ -90,16 +91,6 @@ def __init__(self, records=None): ModbusRequest.__init__(self) self.records = records or [] - @classmethod - def calculateRtuFrameSize(cls, buffer): - ''' Calculates the size of the message - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes in the response. - ''' - byte_count = struct.unpack('B', buffer[0])[0] - return byte_count - def encode(self): ''' Encodes the request packet @@ -145,6 +136,7 @@ class ReadFileRecordResponse(ModbusResponse): contains a field that shows its own byte count. ''' function_code = 0x14 + _rtu_byte_count_pos = 2 def __init__(self, records=None): ''' Initializes a new instance @@ -154,16 +146,6 @@ def __init__(self, records=None): ModbusResponse.__init__(self) self.records = records or [] - @classmethod - def calculateRtuFrameSize(cls, buffer): - ''' Calculates the size of the message - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes in the response. - ''' - byte_count = struct.unpack('B', buffer[0])[0] - return byte_count - def encode(self): ''' Encodes the response @@ -199,6 +181,7 @@ class WriteFileRecordRequest(ModbusRequest): bit words. ''' function_code = 0x15 + _rtu_byte_count_pos = 2 def __init__(self, records=None): ''' Initializes a new instance @@ -208,16 +191,6 @@ def __init__(self, records=None): ModbusRequest.__init__(self) self.records = records or [] - @classmethod - def calculateRtuFrameSize(cls, buffer): - ''' Calculates the size of the message - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes in the response. - ''' - total_length = struct.unpack('B', buffer[0])[0] - return total_length - def encode(self): ''' Encodes the request packet @@ -264,6 +237,7 @@ class WriteFileRecordResponse(ModbusResponse): The normal response is an echo of the request. ''' function_code = 0x15 + _rtu_byte_count_pos = 2 def __init__(self, records=None): ''' Initializes a new instance @@ -273,16 +247,6 @@ def __init__(self, records=None): ModbusResponse.__init__(self) self.records = records or [] - @classmethod - def calculateRtuFrameSize(cls, buffer): - ''' Calculates the size of the message - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes in the response. - ''' - total_length = struct.unpack('B', buffer[0])[0] - return total_length - def encode(self): ''' Encodes the response diff --git a/pymodbus/register_read_message.py b/pymodbus/register_read_message.py index 2bca52397..e8a2bb648 100644 --- a/pymodbus/register_read_message.py +++ b/pymodbus/register_read_message.py @@ -212,7 +212,6 @@ class ReadWriteMultipleRegistersRequest(ModbusRequest): number of bytes to follow in the write data field." ''' function_code = 23 - _rtu_byte_count_pos = 10 def __init__(self, **kwargs): diff --git a/test/test_client_sync.py b/test/test_client_sync.py index 672bd9f5e..6875dbe6f 100644 --- a/test/test_client_sync.py +++ b/test/test_client_sync.py @@ -58,12 +58,13 @@ def testBaseModbusClient(self): self.assertRaises(NotImplementedException, lambda: client._send(None)) self.assertRaises(NotImplementedException, lambda: client._recv(None)) self.assertRaises(NotImplementedException, lambda: client.__enter__()) - self.assertRaises(ConnectionException, lambda: client.execute()) + self.assertRaises(NotImplementedException, lambda: client.execute()) self.assertEquals("Null Transport", str(client)) client.close() client.__exit__(0,0,0) # a successful execute + client.connect = lambda: True client.transaction = mockTransaction() self.assertTrue(client.execute()) diff --git a/test/test_file_message.py b/test/test_file_message.py index f04c96d43..3736ee338 100644 --- a/test/test_file_message.py +++ b/test/test_file_message.py @@ -145,10 +145,10 @@ def testReadFileRecordRequestDecode(self): def testReadFileRecordRequestRtuFrameSize(self): ''' Test basic bit message encoding/decoding ''' - request = '\x0e\x06\x00\x04\x00\x01\x00\x02\x06\x00\x03\x00\x09\x00\x02' + request = '\x00\x00\x0e\x06\x00\x04\x00\x01\x00\x02\x06\x00\x03\x00\x09\x00\x02' handle = ReadFileRecordRequest() size = handle.calculateRtuFrameSize(request) - self.assertEqual(size, 0x0e) + self.assertEqual(size, 0x0e + 5) def testReadFileRecordRequestExecute(self): ''' Test basic bit message encoding/decoding ''' @@ -178,10 +178,10 @@ def testReadFileRecordResponseDecode(self): def testReadFileRecordResponseRtuFrameSize(self): ''' Test basic bit message encoding/decoding ''' - request = '\x0c\x05\x06\x0d\xfe\x00\x20\x05\x05\x06\x33\xcd\x00\x40' + request = '\x00\x00\x0c\x05\x06\x0d\xfe\x00\x20\x05\x05\x06\x33\xcd\x00\x40' handle = ReadFileRecordResponse() size = handle.calculateRtuFrameSize(request) - self.assertEqual(size, 0x0c) + self.assertEqual(size, 0x0c + 5) #-----------------------------------------------------------------------# # Write File Record Request @@ -205,10 +205,10 @@ def testWriteFileRecordRequestDecode(self): def testWriteFileRecordRequestRtuFrameSize(self): ''' Test write file record request rtu frame size calculation ''' - request = '\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d' + request = '\x00\x00\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d' handle = WriteFileRecordRequest() size = handle.calculateRtuFrameSize(request) - self.assertEqual(size, 0x0d) + self.assertEqual(size, 0x0d + 5) def testWriteFileRecordRequestExecute(self): ''' Test basic bit message encoding/decoding ''' @@ -238,10 +238,10 @@ def testWriteFileRecordResponseDecode(self): def testWriteFileRecordResponseRtuFrameSize(self): ''' Test write file record response rtu frame size calculation ''' - request = '\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d' + request = '\x00\x00\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d' handle = WriteFileRecordResponse() size = handle.calculateRtuFrameSize(request) - self.assertEqual(size, 0x0d) + self.assertEqual(size, 0x0d + 5) #-----------------------------------------------------------------------# # Mask Write Register Request diff --git a/test/test_pdu.py b/test/test_pdu.py index cff84c7f7..bc0cc2189 100644 --- a/test/test_pdu.py +++ b/test/test_pdu.py @@ -61,7 +61,7 @@ def testCalculateRtuFrameSize(self): ModbusRequest._rtu_byte_count_pos = 2 self.assertEqual(ModbusRequest.calculateRtuFrameSize( - "\x11\x01\x05\xcd\x6b\xb2\x0e\x1b\x45\xe6"), 10) + "\x11\x01\x05\xcd\x6b\xb2\x0e\x1b\x45\xe6"), 0x05 + 5) del ModbusRequest._rtu_byte_count_pos self.assertRaises(NotImplementedException, @@ -71,7 +71,7 @@ def testCalculateRtuFrameSize(self): del ModbusResponse._rtu_frame_size ModbusResponse._rtu_byte_count_pos = 2 self.assertEqual(ModbusResponse.calculateRtuFrameSize( - "\x11\x01\x05\xcd\x6b\xb2\x0e\x1b\x45\xe6"), 10) + "\x11\x01\x05\xcd\x6b\xb2\x0e\x1b\x45\xe6"), 0x05 + 5) del ModbusResponse._rtu_byte_count_pos From b83e12ce0da77b61e258f4bd5b8d40ffabbead0d Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Sun, 26 Feb 2012 10:37:26 -0800 Subject: [PATCH 053/243] using the endian flag in register factory --- pymodbus/constants.py | 6 ++++++ pymodbus/payload.py | 3 ++- test/test_payload.py | 16 +++++++++++++--- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/pymodbus/constants.py b/pymodbus/constants.py index abe98ac92..e1d986998 100644 --- a/pymodbus/constants.py +++ b/pymodbus/constants.py @@ -126,6 +126,11 @@ class ModbusStatus(Singleton): class Endian(Singleton): ''' An enumeration representing the various byte endianess. + .. attribute:: Auto + + This indicates that the byte order is chosen by the + current native environment. + .. attribute:: Big This indicates that the bytes are in little endian format @@ -137,6 +142,7 @@ class Endian(Singleton): .. note:: I am simply borrowing the format strings from the python struct module for my convenience. ''' + Auto = '@' Big = '>' Little = '<' diff --git a/pymodbus/payload.py b/pymodbus/payload.py index fda90e6da..5630c4900 100644 --- a/pymodbus/payload.py +++ b/pymodbus/payload.py @@ -196,8 +196,9 @@ def fromRegisters(registers, endian=Endian.Little): :param endian: The endianess of the payload :returns: An initialized PayloadDecoder ''' + fstring = endian + 'H' if isinstance(registers, list): - payload = ''.join(pack("H", x) for x in registers) + payload = ''.join(pack(fstring, x) for x in registers) return PayloadDecoder(payload, endian) raise ParameterException('Invalid collection of registers supplied') diff --git a/test/test_payload.py b/test/test_payload.py index 4c1ff6d7c..8d3ee33e9 100644 --- a/test/test_payload.py +++ b/test/test_payload.py @@ -146,14 +146,24 @@ def testPayloadDecoderRegisterFactory(self): decoder = PayloadDecoder.fromRegisters(payload, endian=Endian.Little) encoded = '\x01\x00\x02\x00\x03\x00\x04\x00' self.assertEqual(encoded, decoder.decode_string(8)) + + decoder = PayloadDecoder.fromRegisters(payload, endian=Endian.Big) + encoded = '\x00\x01\x00\x02\x00\x03\x00\x04' + self.assertEqual(encoded, decoder.decode_string(8)) + self.assertRaises(ParameterException, lambda: PayloadDecoder.fromRegisters('abcd')) def testPayloadDecoderCoilFactory(self): ''' Test the payload decoder reset functionality ''' - payload = [1,0,0,0,1,0,0,0] + payload = [1,0,0,0, 1,0,0,0, 0,0,0,1, 0,0,0,1] decoder = PayloadDecoder.fromCoils(payload, endian=Endian.Little) - encoded = '\x11' - self.assertEqual(encoded, decoder.decode_string(1)) + encoded = '\x11\x88' + self.assertEqual(encoded, decoder.decode_string(2)) + + decoder = PayloadDecoder.fromCoils(payload, endian=Endian.Big) + encoded = '\x11\x88' + self.assertEqual(encoded, decoder.decode_string(2)) + self.assertRaises(ParameterException, lambda: PayloadDecoder.fromCoils('abcd')) From cb25defb44bcd0a6805201801f7cff30016d12b6 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 12 Mar 2012 20:17:02 -0700 Subject: [PATCH 054/243] fixing bad documentation --- pymodbus/payload.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pymodbus/payload.py b/pymodbus/payload.py index 5630c4900..6e0f9a848 100644 --- a/pymodbus/payload.py +++ b/pymodbus/payload.py @@ -234,8 +234,6 @@ def decode_8bit_uint(self): def decode_bits(self): ''' Decodes a byte worth of bits from the buffer - - :param bytes: The number of bytes to decode ''' # TODO endianess issue here self._pointer += 1 From 707c6df09e555840783352adbf72d165e0c9af58 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Thu, 22 Mar 2012 07:20:23 -0700 Subject: [PATCH 055/243] fixing issue 58 on google code --- README.rst | 2 +- pymodbus/client/async.py | 7 ++++++- pymodbus/client/common.py | 6 ++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index c720ce743..232f90906 100644 --- a/README.rst +++ b/README.rst @@ -114,7 +114,7 @@ I get time doing such tasks as: License Information ------------------------------------------------------------ -Pymodbus is built on top of code developed from by: +Pymodbus is built on top of code developed from/by: * Copyright (c) 2001-2005 S.W.A.C. GmbH, Germany. * Copyright (c) 2001-2005 S.W.A.C. Bohemia s.r.o., Czech Republic. * Hynek Petrak diff --git a/pymodbus/client/async.py b/pymodbus/client/async.py index 25b64ffef..3220c91b6 100644 --- a/pymodbus/client/async.py +++ b/pymodbus/client/async.py @@ -38,6 +38,7 @@ def process(): from pymodbus.exceptions import ConnectionException from pymodbus.transaction import ModbusSocketFramer, ModbusTransactionManager from pymodbus.client.common import ModbusClientMixin +from twisted.python.failure import Failure #---------------------------------------------------------------------------# # Logging @@ -82,6 +83,9 @@ def connectionLost(self, reason): ''' _logger.debug("Client disconnected from modbus server: %s" % reason) self._connected = False + while self._requests: + self._requests.popleft().errback(Failure( + ConnectionException('Connection lost during request'))) def dataReceived(self, data): ''' Get response, check for valid message, decode result @@ -111,7 +115,8 @@ def _buildResponse(self): :returns: A defer linked to the latest request ''' if not self._connected: - return defer.fail(ConnectionException('Client is not connected')) + return defer.fail(Failure( + ConnectionException('Client is not connected'))) d = defer.Deferred() self._requests.append(d) diff --git a/pymodbus/client/common.py b/pymodbus/client/common.py index 91ee380e5..aa2a5745d 100644 --- a/pymodbus/client/common.py +++ b/pymodbus/client/common.py @@ -1,4 +1,10 @@ ''' +Modbus Client Common +---------------------------------- + +This is a common client mixin that can be used by +both the synchronous and asynchronous clients to +simplify the interface. ''' from pymodbus.bit_read_message import * from pymodbus.bit_write_message import * From 6544edca44857c0ae230814b8d489b2ffd8d7d30 Mon Sep 17 00:00:00 2001 From: Philippe Gauthier Date: Thu, 29 Mar 2012 17:34:08 -0400 Subject: [PATCH 056/243] Fix binary framer and add start and end tokens Fixes a TypeError occuring while escaping token bytes in the message and adds the start and end tokens to the packet. --- pymodbus/transaction.py | 3 ++- test/test_transaction.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 4118ba826..9bfc4d779 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -742,6 +742,7 @@ def buildPacket(self, message): message.unit_id, message.function_code) + data packet += struct.pack(">H", computeCRC(packet)) + packet = '%s%s%s' % (self.__start, packet, self.__end) return packet def _preflight(self, data): @@ -755,7 +756,7 @@ def _preflight(self, data): ''' def _filter(a): if a in ['}', '{']: return a * 2 - else: return a, data + else: return a return ''.join(map(_filter, data)) #---------------------------------------------------------------------------# diff --git a/test/test_transaction.py b/test/test_transaction.py index a0ac8c7ad..2df2736f6 100644 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -340,7 +340,7 @@ def testBinaryFramerPacket(self): message = ModbusRequest() message.unit_id = 0xff message.function_code = 0x01 - expected = '\xff\x01\x81\x80' + expected = '\x7b\xff\x01\x81\x80\x7d' actual = self._binary.buildPacket(message) self.assertEqual(expected, actual) ModbusRequest.encode = old_encode From 9270f2bb19e80a9cef80b40499e0dd268ebfa026 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 2 May 2012 13:16:25 -0500 Subject: [PATCH 057/243] Adding checking in the client for an unconnected socket. --- pymodbus/client/sync.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index f5419c387..4e51dbd6e 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -157,6 +157,8 @@ def _send(self, request): :param request: The encoded request to send :return: The number of bytes written ''' + if not self.socket: + raise ConnectionException(self.__str__()) if request: return self.socket.send(request) return 0 @@ -167,6 +169,8 @@ def _recv(self, size): :param size: The number of bytes to read :return: The bytes read ''' + if not self.socket: + raise ConnectionException(self.__str__()) return self.socket.recv(size) def __str__(self): @@ -234,6 +238,8 @@ def _send(self, request): :param request: The encoded request to send :return: The number of bytes written ''' + if not self.socket: + raise ConnectionException(self.__str__()) if request: return self.socket.sendto(request, (self.host, self.port)) return 0 @@ -244,6 +250,8 @@ def _recv(self, size): :param size: The number of bytes to read :return: The bytes read ''' + if not self.socket: + raise ConnectionException(self.__str__()) return self.socket.recvfrom(size)[0] def __str__(self): From bc4898ae0ee7fc5d8f33d0c866fc36642b4eb544 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 2 May 2012 14:12:18 -0500 Subject: [PATCH 058/243] Fixing issue 60 on google code (including ez_setup) --- setup.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 59dcc95c7..8a07590f9 100644 --- a/setup.py +++ b/setup.py @@ -9,18 +9,25 @@ For information about setuptools http://peak.telecommunity.com/DevCenter/setuptools#new-and-changed-setup-keywords ''' +#---------------------------------------------------------------------------# +# initialization +#---------------------------------------------------------------------------# try: # if not installed, install and proceed from setuptools import setup, find_packages except ImportError: from ez_setup import use_setuptools use_setuptools() + from setuptools import setup, find_packages -#---------------------------------------------------------------------------# -# Configuration -#---------------------------------------------------------------------------# -from setup_commands import command_classes +try: + from setup_commands import command_classes +except ImportError: + command_classes = {} from pymodbus import __version__, __author__ +#---------------------------------------------------------------------------# +# configuration +#---------------------------------------------------------------------------# setup(name = 'pymodbus', version = __version__, description = 'A fully featured modbus protocol stack in python', @@ -49,8 +56,9 @@ maintainer_email = 'bashwork@gmail.com', url='http://code.google.com/p/pymodbus/', license = 'BSD', - packages = find_packages(exclude=['ez_setup', 'examples', 'test', 'doc']), + packages = find_packages(exclude=['examples', 'test']), exclude_package_data = {'' : ['examples', 'test', 'tools', 'doc']}, + py_modules = ['ez_setup'], platforms = ['Linux', 'Mac OS X', 'Win'], include_package_data = True, zip_safe = True, From 58f3de830fc7d235892dd90c9ae8eca199706629 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 21 May 2012 15:23:31 -0500 Subject: [PATCH 059/243] fixing github issue #7 --- pymodbus/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 88d670b5c..3fb0c3170 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -178,7 +178,7 @@ def lookupPduClass(self, function_code): :param function_code: The function code specified in a frame. :returns: The class of the PDU that has a matching `function_code`. ''' - return self.__lookup.get(function_code, None) + return self.__lookup.get(function_code, ExceptionResponse) def decode(self, message): ''' Wrapper to decode a response packet From fbf87170ab5ae7af227a88b2d03c151e9e0af524 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Tue, 22 May 2012 10:42:35 -0500 Subject: [PATCH 060/243] adding tests and two utilities --- examples/common/rtu-parser.py | 55 +++++++++++++++++++++++++++++++++++ examples/common/tcp-parser.py | 55 +++++++++++++++++++++++++++++++++++ pymodbus/factory.py | 2 +- test/test_factory.py | 22 ++++++++++++++ test/test_transaction.py | 7 +++++ 5 files changed, 140 insertions(+), 1 deletion(-) create mode 100755 examples/common/rtu-parser.py create mode 100755 examples/common/tcp-parser.py diff --git a/examples/common/rtu-parser.py b/examples/common/rtu-parser.py new file mode 100755 index 000000000..6e36c7715 --- /dev/null +++ b/examples/common/rtu-parser.py @@ -0,0 +1,55 @@ +import sys +from pymodbus.utilities import computeCRC +from pymodbus.transaction import ModbusRtuFramer +from pymodbus.factory import ClientDecoder, ServerDecoder + +class Decoder(object): + + def decode(self, message): + ''' Attempt to decode the supplied message + + :param message: The messge to decode + ''' + decoders = [ + ModbusRtuFramer(ServerDecoder()), + ModbusRtuFramer(ClientDecoder()), + ] + for decoder in decoders: + decoder.addToFrame(message) + if decoder.checkFrame(): + decoder.advanceFrame() + decoder.processIncomingPacket(message, self.report) + else: self.check_errors(decoder, message) + + def check_errors(self, decoder, message): + ''' Attempt to find message errors + + :param message: The message to find errors in + ''' + pass + + def report(self, message): + ''' The callback to print the message information + + :param message: The message to print + ''' + print "-"*80 + print "Decoded Message" + print "-"*80 + print "%-15s = %s" % ('name', message.__class__.__name__) + for k,v in message.__dict__.items(): + print "%-15s = %s" % (k, hex(v)) + print "%-15s = %s" % ('documentation', message.__doc__) + +def main(): + if len(sys.argv) < 2: + print "%s " % sys.argv[0] + sys.exit(-1) + + decoder = Decoder() + #decoder.decode(sys.argv[1]) + #decoder.decode("\x00\x89\x90\xd6\x56") + decoder.decode("\x00\x01\x00\x00\x00\x01\xfc\x1b") + +if __name__ == "__main__": + main() diff --git a/examples/common/tcp-parser.py b/examples/common/tcp-parser.py new file mode 100755 index 000000000..cfc42fa6f --- /dev/null +++ b/examples/common/tcp-parser.py @@ -0,0 +1,55 @@ +import sys +from pymodbus.utilities import computeCRC +from pymodbus.transaction import ModbusTcpFramer +from pymodbus.factory import ClientDecoder, ServerDecoder + +class Decoder(object): + + def decode(self, message): + ''' Attempt to decode the supplied message + + :param message: The messge to decode + ''' + decoders = [ + ModbusTcpFramer(ServerDecoder()), + ModbusTcpFramer(ClientDecoder()), + ] + for decoder in decoders: + decoder.addToFrame(message) + if decoder.checkFrame(): + decoder.advanceFrame() + decoder.processIncomingPacket(message, self.report) + else: self.check_errors(decoder, message) + + def check_errors(self, decoder, message): + ''' Attempt to find message errors + + :param message: The message to find errors in + ''' + pass + + def report(self, message): + ''' The callback to print the message information + + :param message: The message to print + ''' + print "-"*80 + print "Decoded Message" + print "-"*80 + print "%-15s = %s" % ('name', message.__class__.__name__) + for k,v in message.__dict__.items(): + print "%-15s = %s" % (k, hex(v)) + print "%-15s = %s" % ('documentation', message.__doc__) + +def main(): + if len(sys.argv) < 2: + print "%s " % sys.argv[0] + sys.exit(-1) + + decoder = Decoder() + #decoder.decode(sys.argv[1]) + #decoder.decode("\x00\x89\x90\xd6\x56") + decoder.decode("\x00\x01\x00\x00\x00\x01\xfc\x1b") + +if __name__ == "__main__": + main() diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 3fb0c3170..31e76463e 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -101,7 +101,7 @@ def lookupPduClass(self, function_code): :param function_code: The function code specified in a frame. :returns: The class of the PDU that has a matching `function_code`. ''' - return self.__lookup.get(function_code, None) + return self.__lookup.get(function_code, ExceptionResponse) def _helper(self, data): ''' diff --git a/test/test_factory.py b/test/test_factory.py index fb670cb68..37f51ce7f 100644 --- a/test/test_factory.py +++ b/test/test_factory.py @@ -63,6 +63,18 @@ def setUp(self): (0x2b, '\x2b\x0e\x01\x01\x00\x00\x01\x00\x01\x77'), # read device identification ) + self.exception = ( + (0x81, '\x81\x01\xd0\x50'), # illegal function exception + (0x82, '\x82\x02\x90\xa1'), # illegal data address exception + (0x83, '\x83\x03\x50\xf1'), # illegal data value exception + (0x84, '\x84\x04\x13\x03'), # skave device failure exception + (0x85, '\x85\x05\xd3\x53'), # acknowledge exception + (0x86, '\x86\x06\x93\xa2'), # slave device busy exception + (0x87, '\x87\x08\x53\xf2'), # memory parity exception + (0x88, '\x88\x0a\x16\x06'), # gateway path unavailable exception + (0x89, '\x89\x0b\xd6\x56'), # gateway target failed exception + ) + self.bad = ( (0x80, '\x80\x00\x00\x00'), # Unknown Function (0x81, '\x81\x00\x00\x00'), # error message @@ -74,6 +86,16 @@ def tearDown(self): del self.request del self.response + def testExceptionLookup(self): + ''' Test that we can look up exception messages ''' + for func, _ in self.exception: + response = self.client.lookupPduClass(func) + self.assertNotEqual(response, None) + + for func, _ in self.exception: + response = self.server.lookupPduClass(func) + self.assertNotEqual(response, None) + def testResponseLookup(self): ''' Test a working response factory lookup ''' for func, _ in self.response: diff --git a/test/test_transaction.py b/test/test_transaction.py index 2df2736f6..07905a572 100644 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -227,6 +227,13 @@ def testRTUFramerPacket(self): self.assertEqual(expected, actual) ModbusRequest.encode = old_encode + def testRTUDecodeException(self): + ''' Test that the RTU framer can decode errors ''' + message = "\x00\x90\x02\x9c\x01" + actual = self._rtu.addToFrame(message) + result = self._rtu.checkFrame() + self.assertTrue(result) + #---------------------------------------------------------------------------# # ASCII tests #---------------------------------------------------------------------------# From 4b2afccdde1c2b7dcf25e20457286b505e81cac8 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Tue, 22 May 2012 16:36:17 -0500 Subject: [PATCH 061/243] adding message parser utility --- doc/sphinx/examples/message-parser.rst | 6 ++ examples/common/message-parser.py | 124 +++++++++++++++++++++++++ examples/common/rtu-parser.py | 55 ----------- examples/common/tcp-parser.py | 55 ----------- 4 files changed, 130 insertions(+), 110 deletions(-) create mode 100644 doc/sphinx/examples/message-parser.rst create mode 100755 examples/common/message-parser.py delete mode 100755 examples/common/rtu-parser.py delete mode 100755 examples/common/tcp-parser.py diff --git a/doc/sphinx/examples/message-parser.rst b/doc/sphinx/examples/message-parser.rst new file mode 100644 index 000000000..42561d505 --- /dev/null +++ b/doc/sphinx/examples/message-parser.rst @@ -0,0 +1,6 @@ +================================================== +Modbus Message Parsing Example +================================================== + +.. literalinclude:: ../../../examples/common/message-parser.py + diff --git a/examples/common/message-parser.py b/examples/common/message-parser.py new file mode 100755 index 000000000..e6b0df5f7 --- /dev/null +++ b/examples/common/message-parser.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python +''' +Pymodbus TCP Message Parser +-------------------------------------------------------------------------- + +The following is an example of how to parse modbus tcp messages +using the supplied framers. +''' +#---------------------------------------------------------------------------# +# import needed libraries +#---------------------------------------------------------------------------# +import sys +import collections +import textwrap +from optparse import OptionParser +from pymodbus.utilities import computeCRC, computeLRC +from pymodbus.factory import ClientDecoder, ServerDecoder +from pymodbus.transaction import ModbusSocketFramer +from pymodbus.transaction import ModbusBinaryFramer +from pymodbus.transaction import ModbusAsciiFramer +from pymodbus.transaction import ModbusRtuFramer + +#--------------------------------------------------------------------------# +# Logging +#--------------------------------------------------------------------------# +import logging +modbus_log = logging.getLogger("pymodbus") + + +#---------------------------------------------------------------------------# +# build a quick wrapper around the framers +#---------------------------------------------------------------------------# +class Decoder(object): + + def __init__(self, framer): + ''' Initialize a new instance of the decoder + + :param framer: The framer to use + ''' + self.framer = framer + + def decode(self, message): + ''' Attempt to decode the supplied message + + :param message: The messge to decode + ''' + decoders = [ + self.framer(ServerDecoder()), + self.framer(ClientDecoder()), + ] + for decoder in decoders: + decoder.addToFrame(message) + if decoder.checkFrame(): + decoder.advanceFrame() + decoder.processIncomingPacket(message, self.report) + else: self.check_errors(decoder, message) + + def check_errors(self, decoder, message): + ''' Attempt to find message errors + + :param message: The message to find errors in + ''' + pass + + def report(self, message): + ''' The callback to print the message information + + :param message: The message to print + ''' + print "-"*80 + print "Decoded Message" + print "-"*80 + print "%-15s = %s" % ('name', message.__class__.__name__) + for k,v in message.__dict__.items(): + if isinstance(v, collections.Iterable): + print "%-15s =" % k + value = str([int(x) for x in v]) + for line in textwrap.wrap(value, 60): + print "%-15s . %s" % ("", line) + else: print "%-15s = %s" % (k, hex(v)) + print "%-15s = %s" % ('documentation', message.__doc__) + + +#---------------------------------------------------------------------------# +# and decode our message +#---------------------------------------------------------------------------# +def get_options(): + parser = OptionParser() + parser.add_option("-p", "--parser", + help="The type of parser to use (tcp, rtu, binary, ascii)", + dest="parser", default="tcp") + parser.add_option("-D", "--debug", + help="Enable debug tracing", + action="store_true", dest="debug", default=False) + parser.add_option("-m", "--message", + help="The message to parse", + dest="message", default="") + (opt, arg) = parser.parse_args() + + return opt + +def main(): + option = get_options() + + if option.debug: + try: + modbus_log.setLevel(logging.DEBUG) + logging.basicConfig() + except Exception, e: + print "Logging is not supported on this system" + + framer = lookup = { + 'tcp': ModbusSocketFramer, + 'rtc': ModbusRtuFramer, + 'binary': ModbusBinaryFramer, + 'ascii': ModbusAsciiFramer, + }[option.parser] + + decoder = Decoder(framer) + #decoder.decode(option.message) + decoder.decode("\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x04") + +if __name__ == "__main__": + main() diff --git a/examples/common/rtu-parser.py b/examples/common/rtu-parser.py deleted file mode 100755 index 6e36c7715..000000000 --- a/examples/common/rtu-parser.py +++ /dev/null @@ -1,55 +0,0 @@ -import sys -from pymodbus.utilities import computeCRC -from pymodbus.transaction import ModbusRtuFramer -from pymodbus.factory import ClientDecoder, ServerDecoder - -class Decoder(object): - - def decode(self, message): - ''' Attempt to decode the supplied message - - :param message: The messge to decode - ''' - decoders = [ - ModbusRtuFramer(ServerDecoder()), - ModbusRtuFramer(ClientDecoder()), - ] - for decoder in decoders: - decoder.addToFrame(message) - if decoder.checkFrame(): - decoder.advanceFrame() - decoder.processIncomingPacket(message, self.report) - else: self.check_errors(decoder, message) - - def check_errors(self, decoder, message): - ''' Attempt to find message errors - - :param message: The message to find errors in - ''' - pass - - def report(self, message): - ''' The callback to print the message information - - :param message: The message to print - ''' - print "-"*80 - print "Decoded Message" - print "-"*80 - print "%-15s = %s" % ('name', message.__class__.__name__) - for k,v in message.__dict__.items(): - print "%-15s = %s" % (k, hex(v)) - print "%-15s = %s" % ('documentation', message.__doc__) - -def main(): - if len(sys.argv) < 2: - print "%s " % sys.argv[0] - sys.exit(-1) - - decoder = Decoder() - #decoder.decode(sys.argv[1]) - #decoder.decode("\x00\x89\x90\xd6\x56") - decoder.decode("\x00\x01\x00\x00\x00\x01\xfc\x1b") - -if __name__ == "__main__": - main() diff --git a/examples/common/tcp-parser.py b/examples/common/tcp-parser.py deleted file mode 100755 index cfc42fa6f..000000000 --- a/examples/common/tcp-parser.py +++ /dev/null @@ -1,55 +0,0 @@ -import sys -from pymodbus.utilities import computeCRC -from pymodbus.transaction import ModbusTcpFramer -from pymodbus.factory import ClientDecoder, ServerDecoder - -class Decoder(object): - - def decode(self, message): - ''' Attempt to decode the supplied message - - :param message: The messge to decode - ''' - decoders = [ - ModbusTcpFramer(ServerDecoder()), - ModbusTcpFramer(ClientDecoder()), - ] - for decoder in decoders: - decoder.addToFrame(message) - if decoder.checkFrame(): - decoder.advanceFrame() - decoder.processIncomingPacket(message, self.report) - else: self.check_errors(decoder, message) - - def check_errors(self, decoder, message): - ''' Attempt to find message errors - - :param message: The message to find errors in - ''' - pass - - def report(self, message): - ''' The callback to print the message information - - :param message: The message to print - ''' - print "-"*80 - print "Decoded Message" - print "-"*80 - print "%-15s = %s" % ('name', message.__class__.__name__) - for k,v in message.__dict__.items(): - print "%-15s = %s" % (k, hex(v)) - print "%-15s = %s" % ('documentation', message.__doc__) - -def main(): - if len(sys.argv) < 2: - print "%s " % sys.argv[0] - sys.exit(-1) - - decoder = Decoder() - #decoder.decode(sys.argv[1]) - #decoder.decode("\x00\x89\x90\xd6\x56") - decoder.decode("\x00\x01\x00\x00\x00\x01\xfc\x1b") - -if __name__ == "__main__": - main() From c71fe2d4a79c93aa713c3d301970fceeff938380 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 23 May 2012 09:58:31 -0500 Subject: [PATCH 062/243] Fixing extended message decoding * Now correctly decoding sub function messages (diagnostic) * Message parser now finished and documented * Fixed broken logger unit test --- doc/sphinx/examples/index.rst | 1 + doc/sphinx/examples/message-parser.rst | 47 +++++++++++++++++ examples/common/message-parser.py | 71 ++++++++++++++++++++++---- examples/common/messages | 14 +++++ pymodbus/diag_message.py | 1 + pymodbus/factory.py | 66 +++++++++++++++--------- test/test_fixes.py | 6 +-- 7 files changed, 169 insertions(+), 37 deletions(-) create mode 100644 examples/common/messages diff --git a/doc/sphinx/examples/index.rst b/doc/sphinx/examples/index.rst index 640805959..fea09ae07 100644 --- a/doc/sphinx/examples/index.rst +++ b/doc/sphinx/examples/index.rst @@ -19,6 +19,7 @@ Example Library Code modbus-payload modbus-scraper modbus-simulator + message-parser synchronous-client synchronous-server performance diff --git a/doc/sphinx/examples/message-parser.rst b/doc/sphinx/examples/message-parser.rst index 42561d505..018150fb7 100644 --- a/doc/sphinx/examples/message-parser.rst +++ b/doc/sphinx/examples/message-parser.rst @@ -2,5 +2,52 @@ Modbus Message Parsing Example ================================================== +This is an example of a parser to decode raw messages +to a readable description. It will attempt to decode +a message to the request and response version of a +message if possible. Here is an example output:: + + $./message-parser.py -b -m 000112340006ff076d + ================================================================================ + Decoding Message 000112340006ff076d + ================================================================================ + ServerDecoder + -------------------------------------------------------------------------------- + name = ReadExceptionStatusRequest + check = 0x0 + unit_id = 0xff + transaction_id = 0x1 + protocol_id = 0x1234 + documentation = + This function code is used to read the contents of eight Exception Status + outputs in a remote device. The function provides a simple method for + accessing this information, because the Exception Output references are + known (no output reference is needed in the function). + + ClientDecoder + -------------------------------------------------------------------------------- + name = ReadExceptionStatusResponse + check = 0x0 + status = 0x6d + unit_id = 0xff + transaction_id = 0x1 + protocol_id = 0x1234 + documentation = + The normal response contains the status of the eight Exception Status + outputs. The outputs are packed into one data byte, with one bit + per output. The status of the lowest output reference is contained + in the least significant bit of the byte. The contents of the eight + Exception Status outputs are device specific. + +-------------------------------------------------- +Program Source +-------------------------------------------------- + .. literalinclude:: ../../../examples/common/message-parser.py +-------------------------------------------------- +Example Messages +-------------------------------------------------- + +.. literalinclude:: ../../../examples/common/messages + diff --git a/examples/common/message-parser.py b/examples/common/message-parser.py index e6b0df5f7..73441aeda 100755 --- a/examples/common/message-parser.py +++ b/examples/common/message-parser.py @@ -1,10 +1,15 @@ #!/usr/bin/env python ''' -Pymodbus TCP Message Parser +Modbus Message Parser -------------------------------------------------------------------------- -The following is an example of how to parse modbus tcp messages -using the supplied framers. +The following is an example of how to parse modbus messages +using the supplied framers for a number of protocols: + +* tcp +* ascii +* rtu +* binary ''' #---------------------------------------------------------------------------# # import needed libraries @@ -44,11 +49,16 @@ def decode(self, message): :param message: The messge to decode ''' + print "="*80 + print "Decoding Message %s" % message.encode('hex') + print "="*80 decoders = [ self.framer(ServerDecoder()), self.framer(ClientDecoder()), ] for decoder in decoders: + print "%s" % decoder.decoder.__class__.__name__ + print "-"*80 decoder.addToFrame(message) if decoder.checkFrame(): decoder.advanceFrame() @@ -67,9 +77,6 @@ def report(self, message): :param message: The message to print ''' - print "-"*80 - print "Decoded Message" - print "-"*80 print "%-15s = %s" % ('name', message.__class__.__name__) for k,v in message.__dict__.items(): if isinstance(v, collections.Iterable): @@ -85,21 +92,65 @@ def report(self, message): # and decode our message #---------------------------------------------------------------------------# def get_options(): + ''' A helper method to parse the command line options + + :returns: The options manager + ''' parser = OptionParser() + parser.add_option("-p", "--parser", help="The type of parser to use (tcp, rtu, binary, ascii)", dest="parser", default="tcp") + parser.add_option("-D", "--debug", help="Enable debug tracing", action="store_true", dest="debug", default=False) + parser.add_option("-m", "--message", help="The message to parse", - dest="message", default="") + dest="message", default=None) + + parser.add_option("-a", "--ascii", + help="The indicates that the message is ascii", + action="store_true", dest="ascii", default=True) + + parser.add_option("-b", "--binary", + help="The indicates that the message is binary", + action="store_false", dest="ascii") + + parser.add_option("-f", "--file", + help="The file containing messages to parse", + dest="file", default=None) + (opt, arg) = parser.parse_args() + if not opt.message and len(arg) > 0: + opt.message = arg[0] + return opt +def get_messages(option): + ''' A helper method to generate the messages to parse + + :param options: The option manager + :returns: The message iterator to parse + ''' + if option.message: + if not option.ascii: + option.message = option.message.decode('hex') + yield option.message + elif option.file: + with open(option.file, "r") as handle: + for line in handle: + if line.startswith('#'): continue + line = line.strip() + if not option.ascii: + line = line.decode('hex') + yield line + def main(): + ''' The main runner function + ''' option = get_options() if option.debug: @@ -114,11 +165,11 @@ def main(): 'rtc': ModbusRtuFramer, 'binary': ModbusBinaryFramer, 'ascii': ModbusAsciiFramer, - }[option.parser] + }.get(option.parser, ModbusSocketFramer) decoder = Decoder(framer) - #decoder.decode(option.message) - decoder.decode("\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x04") + for message in get_messages(option): + decoder.decode(message) if __name__ == "__main__": main() diff --git a/examples/common/messages b/examples/common/messages new file mode 100644 index 000000000..e7fea8b39 --- /dev/null +++ b/examples/common/messages @@ -0,0 +1,14 @@ +# ------------------------------------------------------------ +# Modbus TCP Messages +# ------------------------------------------------------------ +000112340006ff0101020004 +000112340006ff0201020004 +000112340006ff0302020002 +000112340006ff0402020002 +000112340006ff0500acff00 +000112340006ff0600010003 +000112340006ff076d +000112340006ff0800010000 +# ------------------------------------------------------------ +# Modbus RTU Messages +# ------------------------------------------------------------ diff --git a/pymodbus/diag_message.py b/pymodbus/diag_message.py index 8468f3d41..90a1279b1 100644 --- a/pymodbus/diag_message.py +++ b/pymodbus/diag_message.py @@ -720,6 +720,7 @@ class GetClearModbusPlusResponse(DiagnosticStatusSimpleResponse): # Exported symbols #---------------------------------------------------------------------------# __all__ = [ + "DiagnosticStatusRequest", "DiagnosticStatusResponse", "ReturnQueryDataRequest", "ReturnQueryDataResponse", "RestartCommunicationsOptionRequest", "RestartCommunicationsOptionResponse", "ReturnDiagnosticRegisterRequest", "ReturnDiagnosticRegisterResponse", diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 31e76463e..6ff91b232 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -51,6 +51,21 @@ class ServerDecoder(IModbusDecoder): WriteSingleCoilRequest, ReadWriteMultipleRegistersRequest, + DiagnosticStatusRequest, + + ReadExceptionStatusRequest, + GetCommEventCounterRequest, + GetCommEventLogRequest, + ReportSlaveIdRequest, + + ReadFileRecordRequest, + WriteFileRecordRequest, + MaskWriteRegisterRequest, + ReadFifoQueueRequest, + + ReadDeviceInformationRequest, + ] + __sub_function_table = [ ReturnQueryDataRequest, RestartCommunicationsOptionRequest, ReturnDiagnosticRegisterRequest, @@ -68,20 +83,9 @@ class ServerDecoder(IModbusDecoder): ReturnIopOverrunCountRequest, ClearOverrunCountRequest, GetClearModbusPlusRequest, - - ReadExceptionStatusRequest, - GetCommEventCounterRequest, - GetCommEventLogRequest, - ReportSlaveIdRequest, - - ReadFileRecordRequest, - WriteFileRecordRequest, - MaskWriteRegisterRequest, - ReadFifoQueueRequest, - - ReadDeviceInformationRequest, ] __lookup = dict([(f.function_code, f) for f in __function_table]) + __sub_lookup = dict([(f.sub_function_code, f) for f in __sub_function_table]) def decode(self, message): ''' Wrapper to decode a request packet @@ -118,6 +122,11 @@ def _helper(self, data): if not request: request = IllegalFunctionRequest(function_code) request.decode(data[1:]) + + if hasattr(request, 'sub_function_code'): + subtype = self.__sub_lookup.get(request.sub_function_code, None) + if subtype: request.__class__ = subtype + return request @@ -140,6 +149,21 @@ class ClientDecoder(IModbusDecoder): WriteSingleCoilResponse, ReadWriteMultipleRegistersResponse, + DiagnosticStatusResponse, + + ReadExceptionStatusResponse, + GetCommEventCounterResponse, + GetCommEventLogResponse, + ReportSlaveIdResponse, + + ReadFileRecordResponse, + WriteFileRecordResponse, + MaskWriteRegisterResponse, + ReadFifoQueueResponse, + + ReadDeviceInformationResponse, + ] + __sub_function_table = [ ReturnQueryDataResponse, RestartCommunicationsOptionResponse, ReturnDiagnosticRegisterResponse, @@ -157,20 +181,9 @@ class ClientDecoder(IModbusDecoder): ReturnIopOverrunCountResponse, ClearOverrunCountResponse, GetClearModbusPlusResponse, - - ReadExceptionStatusResponse, - GetCommEventCounterResponse, - GetCommEventLogResponse, - ReportSlaveIdResponse, - - ReadFileRecordResponse, - WriteFileRecordResponse, - MaskWriteRegisterResponse, - ReadFifoQueueResponse, - - ReadDeviceInformationResponse, ] __lookup = dict([(f.function_code, f) for f in __function_table]) + __sub_lookup = dict([(f.sub_function_code, f) for f in __sub_function_table]) def lookupPduClass(self, function_code): ''' Use `function_code` to determine the class of the PDU. @@ -210,6 +223,11 @@ def _helper(self, data): if not response: raise ModbusException("Unknown response %d" % function_code) response.decode(data[1:]) + + if hasattr(response, 'sub_function_code'): + subtype = self.__sub_lookup.get(response.sub_function_code, None) + if subtype: response.__class__ = subtype + return response #---------------------------------------------------------------------------# diff --git a/test/test_fixes.py b/test/test_fixes.py index 5a7a0d83a..49659e4dc 100644 --- a/test/test_fixes.py +++ b/test/test_fixes.py @@ -26,9 +26,9 @@ def testTrueFalseDefined(self): def testNullLoggerAttached(self): ''' Test that the null logger is attached''' import logging - if len(logging._handlers) == 0: - import pymodbus - self.assertEqual(logging._handlers, 1) + logger = logging.getLogger('pymodbus') + if len(logger.handlers) == 0: + self.assertEqual(len(logger.handlers), 1) #---------------------------------------------------------------------------# # Main From cbf6e845fe7e23689c4b1c514c4afb78c25dbc02 Mon Sep 17 00:00:00 2001 From: Gordon Broom Date: Wed, 6 Jun 2012 17:08:07 -0700 Subject: [PATCH 063/243] ModbusSparseDataBlock handled dictionaries incorrectly (they have an __iter__ attribute). Changed 'if' to 'elif' --- pymodbus/datastore/store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymodbus/datastore/store.py b/pymodbus/datastore/store.py index 3306d62a0..e228f7710 100644 --- a/pymodbus/datastore/store.py +++ b/pymodbus/datastore/store.py @@ -199,7 +199,7 @@ def __init__(self, values): ''' if isinstance(values, dict): self.values = values - if hasattr(values, '__iter__'): + elif hasattr(values, '__iter__'): self.values = dict(enumerate(values)) else: raise ParameterException( "Values for datastore must be a list or dictionary") From d1de763386854abecccbe9bfe9e4d8d954d2ddc0 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 18 Jun 2012 08:55:24 -0500 Subject: [PATCH 064/243] Updating the Readme.rst to explain how to install in zero dependency mode. --- README.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.rst b/README.rst index 232f90906..3ded8dfc0 100644 --- a/README.rst +++ b/README.rst @@ -51,6 +51,9 @@ in this case means how many Virtual IP addresses are allowed). For more information please browse the project documentation: http://readthedocs.org/docs/pymodbus/en/latest/index.html +If you have questions about pymodbus, please send them to the +mailing list http://groups.google.com/group/pymodbus + ------------------------------------------------------------ Example Code ------------------------------------------------------------ @@ -94,6 +97,12 @@ Otherwise you can pull the trunk source and install from there:: Either method will install all the required dependencies (at their appropriate versions) for your current python distribution. +If you would like to install pymodbus without the twisted dependency, +simply edit the setup.py file before running easy_install and comment +out all mentions of twisted. It should be noted that without twisted, +one will only be able to run the synchronized version as the +asynchronous versions uses twisted for its event loop. + ------------------------------------------------------------ Current Work In Progress ------------------------------------------------------------ From 1f380a4c898ce463875e4d0a2db06e4593b14abe Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 18 Jun 2012 09:53:20 -0500 Subject: [PATCH 065/243] Fixing issue #10 --- doc/quality/current.coverage | Bin 14775 -> 2763 bytes pymodbus/client/common.py | 19 ++++++++++--------- test/test_client_sync.py | 5 ++++- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/doc/quality/current.coverage b/doc/quality/current.coverage index 28ed89062cafaab3aa4ce8dba1d1bb617fe148ff..6a9a6266cf01bad65449146a0e4903d2bf969400 100644 GIT binary patch literal 2763 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 Date: Mon, 18 Jun 2012 10:18:27 -0500 Subject: [PATCH 066/243] updating the client documentation --- examples/common/asynchronous-client.py | 16 ++++++++++++++++ examples/common/synchronous-client.py | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/examples/common/asynchronous-client.py b/examples/common/asynchronous-client.py index 8f5345cb4..b067dd821 100755 --- a/examples/common/asynchronous-client.py +++ b/examples/common/asynchronous-client.py @@ -87,6 +87,22 @@ def beginAsynchronousTest(client): reactor.callLater(1, client.transport.loseConnection) reactor.callLater(2, reactor.stop) +#---------------------------------------------------------------------------# +# extra requests +#---------------------------------------------------------------------------# +# If you are performing a request that is not available in the client +# mixin, you have to perform the request like this instead:: +# +# from pymodbus.diag_message import ClearCountersRequest +# from pymodbus.diag_message import ClearCountersResponse +# +# request = ClearCountersRequest() +# response = client.execute(request) +# if isinstance(response, ClearCountersResponse): +# ... do something with the response +# +#---------------------------------------------------------------------------# + #---------------------------------------------------------------------------# # choose the client you want #---------------------------------------------------------------------------# diff --git a/examples/common/synchronous-client.py b/examples/common/synchronous-client.py index 32e6b3e93..4b79aeeb1 100755 --- a/examples/common/synchronous-client.py +++ b/examples/common/synchronous-client.py @@ -90,6 +90,22 @@ assert(rq.registers == [20]*8) # test the expected value assert(rr.registers == [17]*8) # test the expected value +#---------------------------------------------------------------------------# +# extra requests +#---------------------------------------------------------------------------# +# If you are performing a request that is not available in the client +# mixin, you have to perform the request like this instead:: +# +# from pymodbus.diag_message import ClearCountersRequest +# from pymodbus.diag_message import ClearCountersResponse +# +# request = ClearCountersRequest() +# response = client.execute(request) +# if isinstance(response, ClearCountersResponse): +# ... do something with the response +# +#---------------------------------------------------------------------------# + #---------------------------------------------------------------------------# # close the client #---------------------------------------------------------------------------# From 3b191bdb159901b76a9fea84a07b969a293442eb Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 18 Jun 2012 10:30:15 -0500 Subject: [PATCH 067/243] updating the server context documentation --- examples/common/asynchronous-server.py | 13 +++++++++++++ examples/common/synchronous-server.py | 13 +++++++++++++ pymodbus/datastore/context.py | 7 ++++--- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/examples/common/asynchronous-server.py b/examples/common/asynchronous-server.py index 3b48a222b..390106bd3 100755 --- a/examples/common/asynchronous-server.py +++ b/examples/common/asynchronous-server.py @@ -57,6 +57,19 @@ # # block = ModbusSequentialDataBlock(0x00, [0]*0xff) # store = ModbusSlaveContext(di=block, co=block, hr=block, ir=block) +# +# The server then makes use of a server context that allows the server to +# respond with different slave contexts for different unit ids. By default +# it will return the same context for every unit id supplied (broadcast +# mode). However, this can be overloaded by setting the single flag to False +# and then supplying a dictionary of unit id to context mapping:: +# +# slaves = { +# 0x01: ModbusSlaveContext(...), +# 0x02: ModbusSlaveContext(...), +# 0x03: ModbusSlaveContext(...), +# } +# context = ModbusServerContext(slaves=slaves, single=False) #---------------------------------------------------------------------------# store = ModbusSlaveContext( di = ModbusSequentialDataBlock(0, [17]*100), diff --git a/examples/common/synchronous-server.py b/examples/common/synchronous-server.py index 0dba6d209..0bc59b290 100755 --- a/examples/common/synchronous-server.py +++ b/examples/common/synchronous-server.py @@ -58,6 +58,19 @@ # # block = ModbusSequentialDataBlock(0x00, [0]*0xff) # store = ModbusSlaveContext(di=block, co=block, hr=block, ir=block) +# +# The server then makes use of a server context that allows the server to +# respond with different slave contexts for different unit ids. By default +# it will return the same context for every unit id supplied (broadcast +# mode). However, this can be overloaded by setting the single flag to False +# and then supplying a dictionary of unit id to context mapping:: +# +# slaves = { +# 0x01: ModbusSlaveContext(...), +# 0x02: ModbusSlaveContext(...), +# 0x03: ModbusSlaveContext(...), +# } +# context = ModbusServerContext(slaves=slaves, single=False) #---------------------------------------------------------------------------# store = ModbusSlaveContext( di = ModbusSequentialDataBlock(0, [17]*100), diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index ef9ab36c1..26e62b3c7 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -1,6 +1,7 @@ from pymodbus.exceptions import ParameterException from pymodbus.interfaces import IModbusSlaveContext from pymodbus.datastore.store import ModbusSequentialDataBlock +from pymodbus.constants import Defaults #---------------------------------------------------------------------------# # Logging @@ -100,7 +101,7 @@ def __init__(self, slaves=None, single=True): self.single = single self.__slaves = slaves or {} if self.single: - self.__slaves = {0x00: self.__slaves} + self.__slaves = {Defaults.UnitId: self.__slaves} def __iter__(self): ''' Iterater over the current collection of slave @@ -124,7 +125,7 @@ def __setitem__(self, slave, context): :param slave: slave The context to set :param context: The new context to set for this slave ''' - if self.single: slave = 0x00 + if self.single: slave = Defaults.UnitId if 0xf7 >= slave >= 0x00: self.__slaves[slave] = context else: raise ParameterException('slave index out of range') @@ -135,7 +136,7 @@ def __getitem__(self, slave): :param slave: The slave context to get :returns: The requested slave context ''' - if self.single: slave = 0x00 + if self.single: slave = Defaults.UnitId if slave in self.__slaves: return self.__slaves.get(slave) else: raise ParameterException("slave does not exist, or is out of range") From b4443839bf8502e0a6a3b9f57c215ad7771b14b3 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 18 Jun 2012 14:30:00 -0500 Subject: [PATCH 068/243] fixes #9 on github --- pymodbus/client/async.py | 61 +++++++++++++++++++++++++--------------- pymodbus/server/async.py | 1 + pymodbus/transaction.py | 25 ++++++---------- test/test_transaction.py | 1 - 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/pymodbus/client/async.py b/pymodbus/client/async.py index 3220c91b6..8e9f0c52d 100644 --- a/pymodbus/client/async.py +++ b/pymodbus/client/async.py @@ -32,7 +32,6 @@ def process(): reactor.callLater(1, process) reactor.run() """ -from collections import deque from twisted.internet import defer, protocol from pymodbus.factory import ClientDecoder from pymodbus.exceptions import ConnectionException @@ -67,7 +66,7 @@ def __init__(self, framer=None): :param framer: The framer to use for the protocol ''' self.framer = framer or ModbusSocketFramer(ClientDecoder()) - self._requests = deque() # link queue to tid + self._requests = {} self._connected = False def connectionMade(self): @@ -83,8 +82,8 @@ def connectionLost(self, reason): ''' _logger.debug("Client disconnected from modbus server: %s" % reason) self._connected = False - while self._requests: - self._requests.popleft().errback(Failure( + for key in self._requests: + self._requests.pop(key).errback(Failure( ConnectionException('Connection lost during request'))) def dataReceived(self, data): @@ -92,26 +91,35 @@ def dataReceived(self, data): :param data: The data returned from the server ''' - def _callback(reply): # todo errback/callback - if self._requests: - self._requests.popleft().callback(reply) - - self.framer.processIncomingPacket(data, _callback) + self.framer.processIncomingPacket(data, self._handleResponse) def execute(self, request): ''' Starts the producer to send the next request to consumer.write(Frame(request)) ''' request.transaction_id = _manager.getNextTID() - #self.handler[request.transaction_id] = request packet = self.framer.buildPacket(request) self.transport.write(packet) - return self._buildResponse() + return self._buildResponse(request.transaction_id) - def _buildResponse(self): + def _handleResponse(self, reply): + ''' Handle the processed response and link to correct deferred + + :param reply: The reply to process + ''' + if self._requests and reply: + tid = reply.transaction_id + handler = self.requests.pop(tid, None) + if handler: + handler.callback(reply) + else: _logger.debug("Unrequested message: " + str(reply)) + # TODO errback handled somewhere + + def _buildResponse(self, tid): ''' Helper method to return a deferred response for the current request. + :param tid: The transaction identifier for this response :returns: A defer linked to the latest request ''' if not self._connected: @@ -119,7 +127,7 @@ def _buildResponse(self): ConnectionException('Client is not connected'))) d = defer.Deferred() - self._requests.append(d) + self._requests[tid] = d # TODO add request here as well return d #----------------------------------------------------------------------# @@ -153,31 +161,40 @@ def datagramReceived(self, data, (host, port)): :param data: The data returned from the server ''' - def _callback(reply): # todo errback/callback - if self._requests: - self._requests.popleft().callback(reply) - _logger.debug("Datagram from: %s:%d" % (host, port)) - self.framer.processIncomingPacket(data, _callback) + self.framer.processIncomingPacket(data, self._handleResponse) def execute(self, request): ''' Starts the producer to send the next request to consumer.write(Frame(request)) ''' request.transaction_id = _manager.getNextTID() - #self.handler[request.transaction_id] = request packet = self.framer.buildPacket(request) self.transport.write(packet) - return self._buildResponse() + return self._buildResponse(request.transaction_id) + + def _handleResponse(self, reply): + ''' Handle the processed response and link to correct deferred - def _buildResponse(self): + :param reply: The reply to process + ''' + if self._requests and reply: + tid = reply.transaction_id + handler = self.requests.pop(tid, None) + if handler: + handler.callback(reply) + else: _logger.debug("Unrequested message: " + str(reply)) + # TODO errback handled somewhere + + def _buildResponse(self, tid): ''' Helper method to return a deferred response for the current request. + :param tid: The transaction identifier for this response :returns: A defer linked to the latest request ''' d = defer.Deferred() - self._requests.append(d) + self._requests[tid] = d # TODO add request here as well return d diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index e5fc9a28a..12a8b627d 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -217,6 +217,7 @@ def StartUdpServer(context, identity=None): def StartSerialServer(context, identity=None, framer=ModbusAsciiFramer, **kwargs): ''' Helper method to start the Modbus Async Serial server + :param context: The server data context :param identify: The server identity to use (default empty) :param framer: The framer to use (default ModbusAsciiFramer) diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 9bfc4d779..c3aaa3861 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -21,7 +21,7 @@ #---------------------------------------------------------------------------# # The Global Transaction Manager #---------------------------------------------------------------------------# -class ModbusTransactionManager(Singleton): +class ModbusTransactionManager(object): ''' Impelements a transaction for a manager The transaction protocol can be represented by the following pseudo code:: @@ -38,7 +38,7 @@ class ModbusTransactionManager(Singleton): ''' __tid = Defaults.TransactionId - __transactions = [] + __transactions = {} def __init__(self, client=None): ''' Initializes an instance of the ModbusTransactionManager @@ -51,11 +51,6 @@ def execute(self, request): ''' Starts the producer to send the next request to consumer.write(Frame(request)) ''' - def _set_result(message): - ''' a helper method so I can reuse the async framers''' - self.response = message - - self.response = None retries = Defaults.Retries request.transaction_id = self.getNextTID() _logger.debug("Running transaction %d" % request.transaction_id) @@ -68,13 +63,13 @@ def _set_result(message): # as this may not read the full result set, but right now # it should be fine... result = self.client._recv(1024) - self.client.framer.processIncomingPacket(result, _set_result) + self.client.framer.processIncomingPacket(result, self.addTransaction) break; except socket.error, msg: self.client.close() _logger.debug("Transaction failed. (%s) " % msg) retries -= 1 - return self.response + return self.getTransaction(request.transaction_id) def addTransaction(self, request): ''' Adds a transaction to the handler @@ -84,7 +79,8 @@ def addTransaction(self, request): :param request: The request to hold on to ''' - ModbusTransactionManager.__transactions.append(request) + tid = request.transaction_id + ModbusTransactionManager.__transactions[tid] = request def getTransaction(self, tid): ''' Returns a transaction matching the referenced tid @@ -93,19 +89,14 @@ def getTransaction(self, tid): :param tid: The transaction to retrieve ''' - for k, v in enumerate(ModbusTransactionManager.__transactions): - if v.transaction_id == tid: - return ModbusTransactionManager.__transactions.pop(k) - return None + return ModbusTransactionManager.__transactions.pop(tid, None) def delTransaction(self, tid): ''' Removes a transaction matching the referenced tid :param tid: The transaction to remove ''' - for k, v in enumerate(ModbusTransactionManager.__transactions): - if v.transaction_id == tid: - del ModbusTransactionManager.__transactions[k] + ModbusTransactionManager.__transactions.pop(tid, None) def getNextTID(self): ''' Retrieve the next unique transaction identifier diff --git a/test/test_transaction.py b/test/test_transaction.py index 07905a572..c89c1873e 100644 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -34,7 +34,6 @@ def tearDown(self): #---------------------------------------------------------------------------# def testModbusTransactionManagerTID(self): ''' Test the tcp transaction manager TID ''' - self.assertEqual(id(self._manager), id(ModbusTransactionManager())) for tid in range(1, self._manager.getNextTID() + 10): self.assertEqual(tid+2, self._manager.getNextTID()) self._manager.resetTID() From 5c47b8ecdc177b9392b0df8def5399c71da52b4f Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 18 Jun 2012 15:27:36 -0500 Subject: [PATCH 069/243] fixing the factory decoding and adding examples --- doc/sphinx/examples/index.rst | 2 + .../examples/synchronous-client-ext.rst | 6 + examples/common/synchronous-client-ext.py | 176 ++++++++++++++++++ examples/common/synchronous-client.py | 16 -- pymodbus/factory.py | 32 +++- 5 files changed, 210 insertions(+), 22 deletions(-) create mode 100644 doc/sphinx/examples/synchronous-client-ext.rst create mode 100755 examples/common/synchronous-client-ext.py diff --git a/doc/sphinx/examples/index.rst b/doc/sphinx/examples/index.rst index fea09ae07..4886d8d66 100644 --- a/doc/sphinx/examples/index.rst +++ b/doc/sphinx/examples/index.rst @@ -21,6 +21,7 @@ Example Library Code modbus-simulator message-parser synchronous-client + synchronous-client-ext synchronous-server performance @@ -34,3 +35,4 @@ Example Frontend Code tk-frontend wx-frontend web-frontend + diff --git a/doc/sphinx/examples/synchronous-client-ext.rst b/doc/sphinx/examples/synchronous-client-ext.rst new file mode 100644 index 000000000..5012ec8b0 --- /dev/null +++ b/doc/sphinx/examples/synchronous-client-ext.rst @@ -0,0 +1,6 @@ +================================================== +Synchronous Client Extended Example +================================================== + +.. literalinclude:: ../../../examples/common/synchronous-client-ext.py + diff --git a/examples/common/synchronous-client-ext.py b/examples/common/synchronous-client-ext.py new file mode 100755 index 000000000..19f1fe3bd --- /dev/null +++ b/examples/common/synchronous-client-ext.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python +''' +Pymodbus Synchronous Client Extended Examples +-------------------------------------------------------------------------- + +The following is an example of how to use the synchronous modbus client +implementation from pymodbus to perform the extended portions of the +modbus protocol. +''' +#---------------------------------------------------------------------------# +# import the various server implementations +#---------------------------------------------------------------------------# +from pymodbus.client.sync import ModbusTcpClient as ModbusClient +#from pymodbus.client.sync import ModbusUdpClient as ModbusClient +#from pymodbus.client.sync import ModbusSerialClient as ModbusClient + +#---------------------------------------------------------------------------# +# configure the client logging +#---------------------------------------------------------------------------# +import logging +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.DEBUG) + +#---------------------------------------------------------------------------# +# choose the client you want +#---------------------------------------------------------------------------# +# make sure to start an implementation to hit against. For this +# you can use an existing device, the reference implementation in the tools +# directory, or start a pymodbus server. +# +# It should be noted that you can supply an ipv4 or an ipv6 host address for +# both the UDP and TCP clients. +#---------------------------------------------------------------------------# +client = ModbusClient('127.0.0.1') +client.connect() + +#---------------------------------------------------------------------------# +# import the extended messages to perform +#---------------------------------------------------------------------------# +from pymodbus.diag_message import * +from pymodbus.file_message import * +from pymodbus.other_message import * +from pymodbus.mei_message import * + +#---------------------------------------------------------------------------# +# extra requests +#---------------------------------------------------------------------------# +# If you are performing a request that is not available in the client +# mixin, you have to perform the request like this instead:: +# +# from pymodbus.diag_message import ClearCountersRequest +# from pymodbus.diag_message import ClearCountersResponse +# +# request = ClearCountersRequest() +# response = client.execute(request) +# if isinstance(response, ClearCountersResponse): +# ... do something with the response +# +# +# What follows is a listing of all the supported methods. Feel free to +# comment, uncomment, or modify each result set to match with your reference. +#---------------------------------------------------------------------------# + +#---------------------------------------------------------------------------# +# information requests +#---------------------------------------------------------------------------# +rq = ReadDeviceInformationRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference +assert(rr.function_code < 0x80) # test that we are not an error +assert(rr.information[0] == 'proconX Pty Ltd') # test the vendor name +assert(rr.information[1] == 'FT-MBSV') # test the product code +assert(rr.information[2] == 'EXPERIMENTAL') # test the code revision + +rq = ReportSlaveIdRequest() +rr = client.execute(rq) +assert(rr == None) # not supported by reference +#assert(rr.function_code < 0x80) # test that we are not an error +#assert(rr.identifier == 0x00) # test the slave identifier +#assert(rr.status == 0x00) # test that the status is ok + +rq = ReadExceptionStatusRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference +assert(rr.function_code < 0x80) # test that we are not an error +assert(rr.status == 0x55) # test the status code + +rq = GetCommEventCounterRequest() +rr = client.execute(rq) +assert(rr == None) # not supported by reference +#assert(rr.function_code < 0x80) # test that we are not an error +#assert(rr.status == True) # test the status code +#assert(rr.count == 0x00) # test the status code + +rq = GetCommEventLogRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference +#assert(rr.function_code < 0x80) # test that we are not an error +#assert(rr.status == True) # test the status code +#assert(rr.event_count == 0x00) # test the number of events +#assert(rr.message_count == 0x00) # test the number of messages +#assert(len(rr.events) == 0x00) # test the number of events + +#---------------------------------------------------------------------------# +# diagnostic requests +#---------------------------------------------------------------------------# +rq = ReturnQueryDataRequest() +rr = client.execute(rq) +assert(rr == None) # not supported by reference +#assert(rr.message[0] == 0x0000) # test the resulting message + +rq = RestartCommunicationsOptionRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference +#assert(rr.message == 0x0000) # test the resulting message + +rq = ReturnDiagnosticRegisterRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference + +rq = ChangeAsciiInputDelimiterRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference + +rq = ForceListenOnlyModeRequest() +client.execute(rq) # does not send a response + +rq = ClearCountersRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference + +rq = ReturnBusCommunicationErrorCountRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference + +rq = ReturnBusExceptionErrorCountRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference + +rq = ReturnSlaveMessageCountRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference + +rq = ReturnSlaveNoResponseCountRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference + +rq = ReturnSlaveNAKCountRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference + +rq = ReturnSlaveBusyCountRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference + +rq = ReturnSlaveBusCharacterOverrunCountRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference + +rq = ReturnIopOverrunCountRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference + +rq = ClearOverrunCountRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference + +rq = GetClearModbusPlusRequest() +rr = client.execute(rq) +#assert(rr == None) # not supported by reference + +#---------------------------------------------------------------------------# +# close the client +#---------------------------------------------------------------------------# +client.close() diff --git a/examples/common/synchronous-client.py b/examples/common/synchronous-client.py index 4b79aeeb1..32e6b3e93 100755 --- a/examples/common/synchronous-client.py +++ b/examples/common/synchronous-client.py @@ -90,22 +90,6 @@ assert(rq.registers == [20]*8) # test the expected value assert(rr.registers == [17]*8) # test the expected value -#---------------------------------------------------------------------------# -# extra requests -#---------------------------------------------------------------------------# -# If you are performing a request that is not available in the client -# mixin, you have to perform the request like this instead:: -# -# from pymodbus.diag_message import ClearCountersRequest -# from pymodbus.diag_message import ClearCountersResponse -# -# request = ClearCountersRequest() -# response = client.execute(request) -# if isinstance(response, ClearCountersResponse): -# ... do something with the response -# -#---------------------------------------------------------------------------# - #---------------------------------------------------------------------------# # close the client #---------------------------------------------------------------------------# diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 6ff91b232..f55105c89 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -83,9 +83,18 @@ class ServerDecoder(IModbusDecoder): ReturnIopOverrunCountRequest, ClearOverrunCountRequest, GetClearModbusPlusRequest, + + ReadDeviceInformationRequest, ] - __lookup = dict([(f.function_code, f) for f in __function_table]) - __sub_lookup = dict([(f.sub_function_code, f) for f in __sub_function_table]) + + def __init__(self): + ''' Initializes the client lookup tables + ''' + functions = set(f.function_code for f in self.__function_table) + self.__lookup = dict([(f.function_code, f) for f in self.__function_table]) + self.__sub_lookup = dict((f, {}) for f in functions) + for f in self.__sub_function_table: + self.__sub_lookup[f.function_code][f.sub_function_code] = f def decode(self, message): ''' Wrapper to decode a request packet @@ -124,7 +133,8 @@ def _helper(self, data): request.decode(data[1:]) if hasattr(request, 'sub_function_code'): - subtype = self.__sub_lookup.get(request.sub_function_code, None) + lookup = self.__sub_lookup.get(request.function_code, {}) + subtype = lookup.get(request.sub_function_code, None) if subtype: request.__class__ = subtype return request @@ -181,9 +191,18 @@ class ClientDecoder(IModbusDecoder): ReturnIopOverrunCountResponse, ClearOverrunCountResponse, GetClearModbusPlusResponse, + + ReadDeviceInformationResponse, ] - __lookup = dict([(f.function_code, f) for f in __function_table]) - __sub_lookup = dict([(f.sub_function_code, f) for f in __sub_function_table]) + + def __init__(self): + ''' Initializes the client lookup tables + ''' + functions = set(f.function_code for f in self.__function_table) + self.__lookup = dict([(f.function_code, f) for f in self.__function_table]) + self.__sub_lookup = dict((f, {}) for f in functions) + for f in self.__sub_function_table: + self.__sub_lookup[f.function_code][f.sub_function_code] = f def lookupPduClass(self, function_code): ''' Use `function_code` to determine the class of the PDU. @@ -225,7 +244,8 @@ def _helper(self, data): response.decode(data[1:]) if hasattr(response, 'sub_function_code'): - subtype = self.__sub_lookup.get(response.sub_function_code, None) + lookup = self.__sub_lookup.get(response.function_code, {}) + subtype = lookup.get(response.sub_function_code, None) if subtype: response.__class__ = subtype return response From 46c58e0964baafd068ebf3fb3f62f9d7dc5ae219 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 18 Jun 2012 15:30:16 -0500 Subject: [PATCH 070/243] pushing to version 1.0 --- pymodbus/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymodbus/version.py b/pymodbus/version.py index 86af08be4..d7da61489 100644 --- a/pymodbus/version.py +++ b/pymodbus/version.py @@ -35,7 +35,7 @@ def __str__(self): ''' return '[%s, version %s]' % (self.package, self.short()) -version = Version('pymodbus', 0, 9, 0) +version = Version('pymodbus', 1, 0, 0) version.__name__ = 'pymodbus' # fix epydoc error #---------------------------------------------------------------------------# From 2c84fe4190b004f48f7e25ed8a0beb095b3e87f1 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 18 Jun 2012 20:17:04 -0500 Subject: [PATCH 071/243] Fixing a bug in the asynchronous client that slipped through. --- pymodbus/client/async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymodbus/client/async.py b/pymodbus/client/async.py index 8e9f0c52d..a65f48440 100644 --- a/pymodbus/client/async.py +++ b/pymodbus/client/async.py @@ -109,7 +109,7 @@ def _handleResponse(self, reply): ''' if self._requests and reply: tid = reply.transaction_id - handler = self.requests.pop(tid, None) + handler = self._requests.pop(tid, None) if handler: handler.callback(reply) else: _logger.debug("Unrequested message: " + str(reply)) From a57c87d4843ce82b71773c353a2299f9ee8e1588 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Tue, 19 Jun 2012 16:14:10 -0500 Subject: [PATCH 072/243] complete tests for the sync client --- README.rst | 3 -- doc/quality/current.coverage | 18 +++---- pymodbus/client/sync.py | 4 ++ setup.py | 1 + test/test_client_sync.py | 101 ++++++++++++++++++++++++++++++++++- test/test_fixes.py | 11 +--- 6 files changed, 115 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index 3ded8dfc0..445496728 100644 --- a/README.rst +++ b/README.rst @@ -51,9 +51,6 @@ in this case means how many Virtual IP addresses are allowed). For more information please browse the project documentation: http://readthedocs.org/docs/pymodbus/en/latest/index.html -If you have questions about pymodbus, please send them to the -mailing list http://groups.google.com/group/pymodbus - ------------------------------------------------------------ Example Code ------------------------------------------------------------ diff --git a/doc/quality/current.coverage b/doc/quality/current.coverage index 6a9a6266c..7a6fbe430 100644 --- a/doc/quality/current.coverage +++ b/doc/quality/current.coverage @@ -4,19 +4,19 @@ pymodbus 15 6 60% 24-27, 36-37 pymodbus.bit_read_message 68 0 100% pymodbus.bit_write_message 95 0 100% pymodbus.client 0 0 100% -pymodbus.client.async 62 36 42% 69-71, 76-77, 84-87, 95-99, 105-109, 117-123, 148-149, 156-161, 167-171, 179-181 -pymodbus.client.common 44 0 100% -pymodbus.client.sync 144 29 80% 138-145, 161, 173, 210-214, 222-228, 242, 254, 319-326 +pymodbus.client.async 69 42 39% 68-70, 75-76, 83-86, 94, 100-103, 110-115, 125-131, 156-157, 164-165, 171-174, 181-186, 196-198 +pymodbus.client.common 45 0 100% +pymodbus.client.sync 148 0 100% pymodbus.constants 36 0 100% pymodbus.datastore 5 0 100% -pymodbus.datastore.context 49 0 100% +pymodbus.datastore.context 50 0 100% pymodbus.datastore.remote 31 0 100% pymodbus.datastore.store 67 0 100% pymodbus.device 159 0 100% pymodbus.diag_message 202 0 100% pymodbus.events 60 0 100% pymodbus.exceptions 22 0 100% -pymodbus.factory 67 0 100% +pymodbus.factory 77 0 100% pymodbus.file_message 181 0 100% pymodbus.interfaces 43 0 100% pymodbus.internal 0 0 100% @@ -28,14 +28,14 @@ pymodbus.pdu 66 0 100% pymodbus.register_read_message 124 0 100% pymodbus.register_write_message 87 0 100% pymodbus.server 0 0 100% -pymodbus.server.async 107 75 30% 40-41, 48, 55-57, 64-73, 80-84, 108-115, 135-142, 149-153, 160-169, 177-180, 192-199, 208-214, 226-237 +pymodbus.server.async 107 75 30% 40-41, 48, 55-57, 64-73, 80-84, 108-115, 135-142, 149-153, 160-169, 177-180, 192-199, 208-214, 227-238 pymodbus.server.sync 182 134 26% 40-43, 48-49, 56-64, 72-76, 84, 91, 103-112, 119-123, 136-147, 154-158, 173-184, 191-195, 220-229, 238-239, 244-246, 269-278, 287-289, 294-296, 325-341, 348-356, 364-369, 377-379, 384-385, 397-399, 408-410, 425-427 -pymodbus.transaction 270 63 77% 54-77, 243-253, 393, 423-432, 567-576, 646, 723-732, 758-759 +pymodbus.transaction 263 60 77% 54-72, 234-244, 384, 414-423, 558-567, 637, 714-723, 749-750 pymodbus.utilities 67 0 100% pymodbus.version 13 0 100% --------------------------------------------------------------- -TOTAL 2639 362 86% +TOTAL 2655 336 87% ---------------------------------------------------------------------- -Ran 210 tests in 0.584s +Ran 220 tests in 0.521s OK diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 4e51dbd6e..f1a7cb653 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -338,6 +338,8 @@ def _send(self, request): :param request: The encoded request to send :return: The number of bytes written ''' + if not self.socket: + raise ConnectionException(self.__str__()) if request: return self.socket.write(request) return 0 @@ -348,6 +350,8 @@ def _recv(self, size): :param size: The number of bytes to read :return: The bytes read ''' + if not self.socket: + raise ConnectionException(self.__str__()) return self.socket.read(size) def __str__(self): diff --git a/setup.py b/setup.py index 8a07590f9..a29d41152 100644 --- a/setup.py +++ b/setup.py @@ -65,6 +65,7 @@ install_requires = [ 'twisted >= 2.5.0', 'nose >= 1.0.0', + 'mock >= 0.8.0', 'pyserial >= 2.4' ], extras_require = { diff --git a/test/test_client_sync.py b/test/test_client_sync.py index d25560209..8e910eece 100644 --- a/test/test_client_sync.py +++ b/test/test_client_sync.py @@ -1,5 +1,8 @@ #!/usr/bin/env python import unittest +import socket +import serial +from mock import patch from twisted.test import test_protocols from pymodbus.client.sync import ModbusTcpClient, ModbusUdpClient from pymodbus.client.sync import ModbusSerialClient, BaseModbusClient @@ -20,7 +23,7 @@ def recv(self, size): return '\x00'*size def read(self, size): return '\x00'*size def send(self, msg): return len(msg) def write(self, msg): return len(msg) - def recvfrom(self, size): return '\x00'*size + def recvfrom(self, size): return ['\x00'*size] def sendto(self, msg, *args): return len(msg) #---------------------------------------------------------------------------# @@ -107,6 +110,42 @@ def testBasicSyncUdpClient(self): self.assertEqual("127.0.0.1:502", str(client)) + def testUdpClientAddressFamily(self): + ''' Test the Udp client get address family method''' + client = ModbusUdpClient() + self.assertEqual(socket.AF_INET, client._get_address_family('127.0.0.1')) + self.assertEqual(socket.AF_INET6, client._get_address_family('::1')) + + def testUdpClientConnect(self): + ''' Test the Udp client connection method''' + with patch.object(socket, 'socket') as mock_method: + mock_method.return_value = object() + client = ModbusUdpClient() + self.assertTrue(client.connect()) + + with patch.object(socket, 'socket') as mock_method: + mock_method.side_effect = socket.error() + client = ModbusUdpClient() + self.assertFalse(client.connect()) + + def testUdpClientSend(self): + ''' Test the udp client send method''' + client = ModbusUdpClient() + self.assertRaises(ConnectionException, lambda: client._send(None)) + + client.socket = mockSocket() + self.assertEqual(0, client._send(None)) + self.assertEqual(4, client._send('1234')) + + def testUdpClientRecv(self): + ''' Test the udp client receive method''' + client = ModbusUdpClient() + self.assertRaises(ConnectionException, lambda: client._recv(1024)) + + client.socket = mockSocket() + self.assertEqual('', client._recv(0)) + self.assertEqual('\x00'*4, client._recv(4)) + #-----------------------------------------------------------------------# # Test TCP Client #-----------------------------------------------------------------------# @@ -134,6 +173,36 @@ def testBasicSyncTcpClient(self): client.close() self.assertEqual("127.0.0.1:502", str(client)) + + def testTcpClientConnect(self): + ''' Test the tcp client connection method''' + with patch.object(socket, 'create_connection') as mock_method: + mock_method.return_value = object() + client = ModbusTcpClient() + self.assertTrue(client.connect()) + + with patch.object(socket, 'create_connection') as mock_method: + mock_method.side_effect = socket.error() + client = ModbusTcpClient() + self.assertFalse(client.connect()) + + def testTcpClientSend(self): + ''' Test the tcp client send method''' + client = ModbusTcpClient() + self.assertRaises(ConnectionException, lambda: client._send(None)) + + client.socket = mockSocket() + self.assertEqual(0, client._send(None)) + self.assertEqual(4, client._send('1234')) + + def testTcpClientRecv(self): + ''' Test the tcp client receive method''' + client = ModbusTcpClient() + self.assertRaises(ConnectionException, lambda: client._recv(1024)) + + client.socket = mockSocket() + self.assertEqual('', client._recv(0)) + self.assertEqual('\x00'*4, client._recv(4)) #-----------------------------------------------------------------------# # Test Serial Client @@ -167,6 +236,36 @@ def testBasicSyncSerialClient(self): self.assertEqual('ascii baud[19200]', str(client)) + def testSerialClientConnect(self): + ''' Test the serial client connection method''' + with patch.object(serial, 'Serial') as mock_method: + mock_method.return_value = object() + client = ModbusSerialClient() + self.assertTrue(client.connect()) + + with patch.object(serial, 'Serial') as mock_method: + mock_method.side_effect = serial.SerialException() + client = ModbusSerialClient() + self.assertFalse(client.connect()) + + def testSerialClientSend(self): + ''' Test the serial client send method''' + client = ModbusSerialClient() + self.assertRaises(ConnectionException, lambda: client._send(None)) + + client.socket = mockSocket() + self.assertEqual(0, client._send(None)) + self.assertEqual(4, client._send('1234')) + + def testSerialClientRecv(self): + ''' Test the serial client receive method''' + client = ModbusSerialClient() + self.assertRaises(ConnectionException, lambda: client._recv(1024)) + + client.socket = mockSocket() + self.assertEqual('', client._recv(0)) + self.assertEqual('\x00'*4, client._recv(4)) + #---------------------------------------------------------------------------# # Main #---------------------------------------------------------------------------# diff --git a/test/test_fixes.py b/test/test_fixes.py index 49659e4dc..1b74fc486 100644 --- a/test/test_fixes.py +++ b/test/test_fixes.py @@ -6,14 +6,6 @@ class ModbusFixesTest(unittest.TestCase): This is the unittest for the pymodbus._version code ''' - def setUp(self): - ''' Initializes the test environment ''' - pass - - def tearDown(self): - ''' Cleans up the test environment ''' - pass - def testTrueFalseDefined(self): ''' Test that True and False are defined on all versions''' try: @@ -27,8 +19,7 @@ def testNullLoggerAttached(self): ''' Test that the null logger is attached''' import logging logger = logging.getLogger('pymodbus') - if len(logger.handlers) == 0: - self.assertEqual(len(logger.handlers), 1) + self.assertEqual(len(logger.handlers), 1) #---------------------------------------------------------------------------# # Main From 4f426981b4bbe9f31de11fbcc16e6c1377f514ae Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Tue, 19 Jun 2012 16:25:38 -0500 Subject: [PATCH 073/243] cleaning up some pep8 errors --- doc/quality/current.pep8 | 690 +++++++++++++++++++++++++++--------- pymodbus/client/async.py | 1 + pymodbus/datastore/store.py | 2 +- pymodbus/device.py | 5 +- pymodbus/diag_message.py | 2 +- pymodbus/factory.py | 2 +- pymodbus/file_message.py | 2 +- pymodbus/other_message.py | 4 +- pymodbus/server/sync.py | 2 + pymodbus/utilities.py | 2 +- 10 files changed, 533 insertions(+), 179 deletions(-) diff --git a/doc/quality/current.pep8 b/doc/quality/current.pep8 index de4467c0b..ba5cbbaf1 100644 --- a/doc/quality/current.pep8 +++ b/doc/quality/current.pep8 @@ -1,218 +1,568 @@ running pep8 pymodbus/__init__.py:16:11: E221 multiple spaces before operator -pymodbus/bit_read_message.py:26:20: E221 multiple spaces before operator +pymodbus/bit_read_message.py:26:19: E221 multiple spaces before operator pymodbus/bit_read_message.py:143:80: E501 line too long (80 characters) pymodbus/bit_read_message.py:202:80: E501 line too long (80 characters) pymodbus/bit_write_message.py:19:14: E221 multiple spaces before operator pymodbus/bit_write_message.py:58:15: E221 multiple spaces before operator -pymodbus/bit_write_message.py:116:15: E221 multiple spaces before operator -pymodbus/bit_write_message.py:158:22: E701 multiple statements on one line (colon) -pymodbus/bit_write_message.py:159:45: E701 multiple statements on one line (colon) -pymodbus/bit_write_message.py:160:20: E221 multiple spaces before operator -pymodbus/bit_write_message.py:168:15: E221 multiple spaces before operator -pymodbus/bit_write_message.py:170:15: E221 multiple spaces before operator -pymodbus/constants.py:74:17: E221 multiple spaces before operator -pymodbus/constants.py:75:17: E221 multiple spaces before operator -pymodbus/constants.py:76:17: E221 multiple spaces before operator -pymodbus/constants.py:77:17: E221 multiple spaces before operator -pymodbus/constants.py:79:17: E221 multiple spaces before operator -pymodbus/constants.py:80:17: E221 multiple spaces before operator -pymodbus/constants.py:81:17: E221 multiple spaces before operator -pymodbus/constants.py:82:17: E221 multiple spaces before operator -pymodbus/constants.py:83:17: E221 multiple spaces before operator -pymodbus/constants.py:84:17: E221 multiple spaces before operator +pymodbus/bit_write_message.py:59:22: E701 multiple statements on one line (colon) +pymodbus/bit_write_message.py:60:13: E701 multiple statements on one line (colon) +pymodbus/bit_write_message.py:117:15: E221 multiple spaces before operator +pymodbus/bit_write_message.py:118:22: E701 multiple statements on one line (colon) +pymodbus/bit_write_message.py:119:13: E701 multiple statements on one line (colon) +pymodbus/bit_write_message.py:160:22: E701 multiple statements on one line (colon) +pymodbus/bit_write_message.py:161:45: E701 multiple statements on one line (colon) +pymodbus/bit_write_message.py:162:20: E221 multiple spaces before operator +pymodbus/bit_write_message.py:170:14: E221 multiple spaces before operator +pymodbus/bit_write_message.py:172:15: E221 multiple spaces before operator +pymodbus/constants.py:74:9: E221 multiple spaces before operator +pymodbus/constants.py:75:12: E221 multiple spaces before operator +pymodbus/constants.py:76:12: E221 multiple spaces before operator +pymodbus/constants.py:77:15: E221 multiple spaces before operator +pymodbus/constants.py:79:15: E221 multiple spaces before operator +pymodbus/constants.py:80:11: E221 multiple spaces before operator +pymodbus/constants.py:81:13: E221 multiple spaces before operator +pymodbus/constants.py:82:11: E221 multiple spaces before operator +pymodbus/constants.py:83:13: E221 multiple spaces before operator +pymodbus/constants.py:84:13: E221 multiple spaces before operator pymodbus/constants.py:118:12: E221 multiple spaces before operator -pymodbus/constants.py:119:12: E221 multiple spaces before operator -pymodbus/constants.py:120:12: E221 multiple spaces before operator -pymodbus/constants.py:121:12: E221 multiple spaces before operator +pymodbus/constants.py:119:10: E221 multiple spaces before operator +pymodbus/constants.py:120:7: E221 multiple spaces before operator +pymodbus/constants.py:121:8: E221 multiple spaces before operator pymodbus/constants.py:122:12: E221 multiple spaces before operator -pymodbus/device.py:163:23: E221 multiple spaces before operator -pymodbus/device.py:164:23: E221 multiple spaces before operator -pymodbus/device.py:165:23: E221 multiple spaces before operator -pymodbus/device.py:166:23: E221 multiple spaces before operator -pymodbus/device.py:167:23: E221 multiple spaces before operator -pymodbus/device.py:168:23: E221 multiple spaces before operator -pymodbus/device.py:245:13: E221 multiple spaces before operator -pymodbus/device.py:286:25: E701 multiple statements on one line (colon) -pymodbus/device.py:293:25: E221 multiple spaces before operator -pymodbus/device.py:295:25: E221 multiple spaces before operator -pymodbus/device.py:296:25: E221 multiple spaces before operator -pymodbus/device.py:297:25: E221 multiple spaces before operator -pymodbus/device.py:298:25: E221 multiple spaces before operator -pymodbus/device.py:299:25: E221 multiple spaces before operator -pymodbus/device.py:300:25: E221 multiple spaces before operator -pymodbus/device.py:301:25: E221 multiple spaces before operator -pymodbus/device.py:322:14: E221 multiple spaces before operator -pymodbus/device.py:370:12: E221 multiple spaces before operator -pymodbus/device.py:371:12: E221 multiple spaces before operator -pymodbus/diag_message.py:136:80: E501 line too long (81 characters) -pymodbus/diag_message.py:175:13: E701 multiple statements on one line (colon) -pymodbus/diag_message.py:201:13: E701 multiple statements on one line (colon) -pymodbus/diag_message.py:225:26: E221 multiple spaces before operator -pymodbus/diag_message.py:226:13: E701 multiple statements on one line (colon) -pymodbus/diag_message.py:255:26: E221 multiple spaces before operator -pymodbus/diag_message.py:256:13: E701 multiple statements on one line (colon) -pymodbus/diag_message.py:350:21: E221 multiple spaces before operator -pymodbus/diag_message.py:594:80: E501 line too long (80 characters) -pymodbus/diag_message.py:597:80: E501 line too long (80 characters) -pymodbus/diag_message.py:613:80: E501 line too long (82 characters) -pymodbus/diag_message.py:616:80: E501 line too long (80 characters) -pymodbus/diag_message.py:656:80: E501 line too long (80 characters) -pymodbus/diag_message.py:662:80: E501 line too long (90 characters) -pymodbus/diag_message.py:663:80: E501 line too long (82 characters) -pymodbus/diag_message.py:668:80: E501 line too long (96 characters) -pymodbus/events.py:54:22: E221 multiple spaces before operator -pymodbus/events.py:55:22: E221 multiple spaces before operator +pymodbus/constants.py:145:9: E221 multiple spaces before operator +pymodbus/constants.py:146:8: E221 multiple spaces before operator +pymodbus/constants.py:163:18: E221 multiple spaces before operator +pymodbus/constants.py:193:10: E221 multiple spaces before operator +pymodbus/constants.py:194:12: E221 multiple spaces before operator +pymodbus/constants.py:210:12: E221 multiple spaces before operator +pymodbus/device.py:30:13: E126 continuation line over-indented for hanging indent +pymodbus/device.py:88:41: E203 whitespace before ':' +pymodbus/device.py:89:41: E203 whitespace before ':' +pymodbus/device.py:90:41: E203 whitespace before ':' +pymodbus/device.py:91:41: E203 whitespace before ':' +pymodbus/device.py:92:41: E203 whitespace before ':' +pymodbus/device.py:93:41: E203 whitespace before ':' +pymodbus/device.py:94:41: E203 whitespace before ':' +pymodbus/device.py:96:41: E203 whitespace before ':' +pymodbus/device.py:97:41: E203 whitespace before ':' +pymodbus/device.py:98:41: E203 whitespace before ':' +pymodbus/device.py:99:41: E203 whitespace before ':' +pymodbus/device.py:100:41: E203 whitespace before ':' +pymodbus/device.py:101:41: E203 whitespace before ':' +pymodbus/device.py:102:41: E203 whitespace before ':' +pymodbus/device.py:103:41: E203 whitespace before ':' +pymodbus/device.py:105:41: E203 whitespace before ':' +pymodbus/device.py:106:41: E203 whitespace before ':' +pymodbus/device.py:107:41: E203 whitespace before ':' +pymodbus/device.py:108:41: E203 whitespace before ':' +pymodbus/device.py:109:41: E203 whitespace before ':' +pymodbus/device.py:110:41: E203 whitespace before ':' +pymodbus/device.py:111:41: E203 whitespace before ':' +pymodbus/device.py:112:41: E203 whitespace before ':' +pymodbus/device.py:113:41: E203 whitespace before ':' +pymodbus/device.py:114:41: E203 whitespace before ':' +pymodbus/device.py:115:41: E203 whitespace before ':' +pymodbus/device.py:116:41: E203 whitespace before ':' +pymodbus/device.py:117:41: E203 whitespace before ':' +pymodbus/device.py:118:41: E203 whitespace before ':' +pymodbus/device.py:120:41: E203 whitespace before ':' +pymodbus/device.py:121:41: E203 whitespace before ':' +pymodbus/device.py:122:41: E203 whitespace before ':' +pymodbus/device.py:123:41: E203 whitespace before ':' +pymodbus/device.py:124:41: E203 whitespace before ':' +pymodbus/device.py:125:41: E203 whitespace before ':' +pymodbus/device.py:126:41: E203 whitespace before ':' +pymodbus/device.py:127:41: E203 whitespace before ':' +pymodbus/device.py:128:41: E203 whitespace before ':' +pymodbus/device.py:129:41: E203 whitespace before ':' +pymodbus/device.py:131:41: E203 whitespace before ':' +pymodbus/device.py:132:41: E203 whitespace before ':' +pymodbus/device.py:133:41: E203 whitespace before ':' +pymodbus/device.py:134:41: E203 whitespace before ':' +pymodbus/device.py:135:41: E203 whitespace before ':' +pymodbus/device.py:136:41: E203 whitespace before ':' +pymodbus/device.py:137:41: E203 whitespace before ':' +pymodbus/device.py:138:41: E203 whitespace before ':' +pymodbus/device.py:88:55: E261 at least two spaces before inline comment +pymodbus/device.py:89:55: E261 at least two spaces before inline comment +pymodbus/device.py:90:55: E261 at least two spaces before inline comment +pymodbus/device.py:91:55: E261 at least two spaces before inline comment +pymodbus/device.py:92:55: E261 at least two spaces before inline comment +pymodbus/device.py:93:55: E261 at least two spaces before inline comment +pymodbus/device.py:94:55: E261 at least two spaces before inline comment +pymodbus/device.py:131:55: E261 at least two spaces before inline comment +pymodbus/device.py:132:55: E261 at least two spaces before inline comment +pymodbus/device.py:133:55: E261 at least two spaces before inline comment +pymodbus/device.py:134:55: E261 at least two spaces before inline comment +pymodbus/device.py:135:55: E261 at least two spaces before inline comment +pymodbus/device.py:136:55: E261 at least two spaces before inline comment +pymodbus/device.py:137:55: E261 at least two spaces before inline comment +pymodbus/device.py:138:55: E261 at least two spaces before inline comment +pymodbus/device.py:175:53: E225 missing whitespace around operator +pymodbus/device.py:273:15: E221 multiple spaces before operator +pymodbus/device.py:274:16: E221 multiple spaces before operator +pymodbus/device.py:275:23: E221 multiple spaces before operator +pymodbus/device.py:276:14: E221 multiple spaces before operator +pymodbus/device.py:277:16: E221 multiple spaces before operator +pymodbus/device.py:278:14: E221 multiple spaces before operator +pymodbus/device.py:289:80: E501 line too long (81 characters) +pymodbus/device.py:290:80: E501 line too long (81 characters) +pymodbus/device.py:289:45: E231 missing whitespace after ',' +pymodbus/device.py:289:47: E231 missing whitespace after ',' +pymodbus/device.py:290:45: E231 missing whitespace after ',' +pymodbus/device.py:290:47: E231 missing whitespace after ',' +pymodbus/device.py:291:45: E231 missing whitespace after ',' +pymodbus/device.py:291:47: E231 missing whitespace after ',' +pymodbus/device.py:292:45: E231 missing whitespace after ',' +pymodbus/device.py:292:47: E231 missing whitespace after ',' +pymodbus/device.py:289:33: E272 multiple spaces before keyword +pymodbus/device.py:290:35: E272 multiple spaces before keyword +pymodbus/device.py:315:17: E201 whitespace after '{' +pymodbus/device.py:315:47: E202 whitespace before '}' +pymodbus/device.py:315:27: E231 missing whitespace after ':' +pymodbus/device.py:401:12: E221 multiple spaces before operator +pymodbus/device.py:442:25: E701 multiple statements on one line (colon) +pymodbus/device.py:449:15: E221 multiple spaces before operator +pymodbus/device.py:451:22: E221 multiple spaces before operator +pymodbus/device.py:452:17: E221 multiple spaces before operator +pymodbus/device.py:453:20: E221 multiple spaces before operator +pymodbus/device.py:454:13: E221 multiple spaces before operator +pymodbus/device.py:455:14: E221 multiple spaces before operator +pymodbus/device.py:456:24: E221 multiple spaces before operator +pymodbus/device.py:457:10: E221 multiple spaces before operator +pymodbus/device.py:478:11: E221 multiple spaces before operator +pymodbus/device.py:479:13: E221 multiple spaces before operator +pymodbus/device.py:527:12: E221 multiple spaces before operator +pymodbus/device.py:528:11: E221 multiple spaces before operator +pymodbus/device.py:529:9: E221 multiple spaces before operator +pymodbus/device.py:612:9: E126 continuation line over-indented for hanging indent +pymodbus/diag_message.py:135:80: E501 line too long (81 characters) +pymodbus/diag_message.py:174:13: E701 multiple statements on one line (colon) +pymodbus/diag_message.py:200:13: E701 multiple statements on one line (colon) +pymodbus/diag_message.py:224:25: E221 multiple spaces before operator +pymodbus/diag_message.py:225:13: E701 multiple statements on one line (colon) +pymodbus/diag_message.py:254:25: E221 multiple spaces before operator +pymodbus/diag_message.py:255:13: E701 multiple statements on one line (colon) +pymodbus/diag_message.py:349:19: E221 multiple spaces before operator +pymodbus/diag_message.py:593:80: E501 line too long (80 characters) +pymodbus/diag_message.py:596:80: E501 line too long (80 characters) +pymodbus/diag_message.py:612:80: E501 line too long (82 characters) +pymodbus/diag_message.py:615:80: E501 line too long (80 characters) +pymodbus/diag_message.py:702:23: E261 at least two spaces before inline comment +pymodbus/diag_message.py:705:13: E701 multiple statements on one line (colon) +pymodbus/diag_message.py:725:80: E501 line too long (80 characters) +pymodbus/diag_message.py:731:80: E501 line too long (90 characters) +pymodbus/diag_message.py:732:80: E501 line too long (82 characters) +pymodbus/diag_message.py:737:80: E501 line too long (96 characters) +pymodbus/events.py:54:21: E221 multiple spaces before operator +pymodbus/events.py:55:20: E221 multiple spaces before operator pymodbus/events.py:63:13: E221 multiple spaces before operator -pymodbus/events.py:74:22: E221 multiple spaces before operator -pymodbus/events.py:75:22: E221 multiple spaces before operator -pymodbus/events.py:105:26: E221 multiple spaces before operator -pymodbus/events.py:106:26: E221 multiple spaces before operator -pymodbus/events.py:107:26: E221 multiple spaces before operator -pymodbus/events.py:108:26: E221 multiple spaces before operator -pymodbus/events.py:110:26: E221 multiple spaces before operator +pymodbus/events.py:74:21: E221 multiple spaces before operator +pymodbus/events.py:75:20: E221 multiple spaces before operator +pymodbus/events.py:105:18: E221 multiple spaces before operator +pymodbus/events.py:106:25: E221 multiple spaces before operator +pymodbus/events.py:107:24: E221 multiple spaces before operator +pymodbus/events.py:108:23: E221 multiple spaces before operator +pymodbus/events.py:110:20: E221 multiple spaces before operator +pymodbus/events.py:118:13: E128 continuation line under-indented for visual indent pymodbus/events.py:119:13: E221 multiple spaces before operator -pymodbus/events.py:130:26: E221 multiple spaces before operator -pymodbus/events.py:131:26: E221 multiple spaces before operator -pymodbus/events.py:132:26: E221 multiple spaces before operator -pymodbus/events.py:133:26: E221 multiple spaces before operator -pymodbus/events.py:135:26: E221 multiple spaces before operator -pymodbus/other_message.py:256:28: E203 whitespace before ':' -pymodbus/other_message.py:301:15: E221 multiple spaces before operator -pymodbus/other_message.py:327:80: E501 line too long (91 characters) +pymodbus/events.py:130:18: E221 multiple spaces before operator +pymodbus/events.py:131:25: E221 multiple spaces before operator +pymodbus/events.py:132:24: E221 multiple spaces before operator +pymodbus/events.py:133:23: E221 multiple spaces before operator +pymodbus/events.py:135:20: E221 multiple spaces before operator +pymodbus/factory.py:44:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:45:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:46:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:47:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:48:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:49:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:50:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:51:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:52:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:54:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:56:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:57:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:58:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:59:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:61:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:62:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:63:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:64:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:66:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:69:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:70:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:71:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:72:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:73:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:74:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:75:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:76:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:77:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:78:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:79:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:80:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:81:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:82:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:83:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:84:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:85:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:87:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:94:80: E501 line too long (83 characters) +pymodbus/factory.py:138:23: E701 multiple statements on one line (colon) +pymodbus/factory.py:152:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:153:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:154:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:155:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:156:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:157:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:158:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:159:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:160:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:162:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:164:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:165:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:166:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:167:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:169:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:170:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:171:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:172:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:174:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:177:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:178:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:179:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:180:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:181:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:182:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:183:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:184:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:185:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:186:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:187:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:188:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:189:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:190:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:191:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:192:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:193:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:195:13: E126 continuation line over-indented for hanging indent +pymodbus/factory.py:202:80: E501 line too long (83 characters) +pymodbus/factory.py:249:23: E701 multiple statements on one line (colon) +pymodbus/file_message.py:30:28: E221 multiple spaces before operator +pymodbus/file_message.py:31:25: E221 multiple spaces before operator +pymodbus/file_message.py:32:27: E221 multiple spaces before operator +pymodbus/file_message.py:33:25: E221 multiple spaces before operator +pymodbus/file_message.py:34:80: E501 line too long (87 characters) +pymodbus/file_message.py:34:27: E221 multiple spaces before operator +pymodbus/file_message.py:35:80: E501 line too long (87 characters) +pymodbus/file_message.py:41:12: E127 continuation line over-indented for visual indent +pymodbus/file_message.py:42:12: E127 continuation line over-indented for visual indent +pymodbus/file_message.py:43:12: E127 continuation line over-indented for visual indent +pymodbus/file_message.py:44:12: E127 continuation line over-indented for visual indent +pymodbus/file_message.py:41:32: E221 multiple spaces before operator +pymodbus/file_message.py:42:34: E221 multiple spaces before operator +pymodbus/file_message.py:43:34: E221 multiple spaces before operator +pymodbus/file_message.py:44:32: E221 multiple spaces before operator +pymodbus/file_message.py:92:21: E221 multiple spaces before operator +pymodbus/file_message.py:102:17: E128 continuation line under-indented for visual indent +pymodbus/file_message.py:113:62: E225 missing whitespace around operator +pymodbus/file_message.py:115:17: E128 continuation line under-indented for visual indent +pymodbus/file_message.py:114:19: E221 multiple spaces before operator +pymodbus/file_message.py:116:34: E701 multiple statements on one line (colon) +pymodbus/file_message.py:154:14: E221 multiple spaces before operator +pymodbus/file_message.py:169:80: E501 line too long (87 characters) +pymodbus/file_message.py:169:84: E225 missing whitespace around operator +pymodbus/file_message.py:170:41: E261 at least two spaces before inline comment +pymodbus/file_message.py:172:17: E128 continuation line under-indented for visual indent +pymodbus/file_message.py:173:38: E701 multiple statements on one line (colon) +pymodbus/file_message.py:192:21: E221 multiple spaces before operator +pymodbus/file_message.py:199:80: E501 line too long (85 characters) +pymodbus/file_message.py:203:17: E128 continuation line under-indented for visual indent +pymodbus/file_message.py:215:62: E225 missing whitespace around operator +pymodbus/file_message.py:217:18: E221 multiple spaces before operator +pymodbus/file_message.py:219:17: E128 continuation line under-indented for visual indent +pymodbus/file_message.py:220:17: E128 continuation line under-indented for visual indent +pymodbus/file_message.py:218:19: E221 multiple spaces before operator +pymodbus/file_message.py:221:34: E701 multiple statements on one line (colon) +pymodbus/file_message.py:248:21: E221 multiple spaces before operator +pymodbus/file_message.py:255:80: E501 line too long (85 characters) +pymodbus/file_message.py:259:17: E128 continuation line under-indented for visual indent +pymodbus/file_message.py:271:62: E225 missing whitespace around operator +pymodbus/file_message.py:273:18: E221 multiple spaces before operator +pymodbus/file_message.py:275:17: E128 continuation line under-indented for visual indent +pymodbus/file_message.py:276:17: E128 continuation line under-indented for visual indent +pymodbus/file_message.py:274:19: E221 multiple spaces before operator +pymodbus/file_message.py:277:34: E701 multiple statements on one line (colon) +pymodbus/file_message.py:298:21: E221 multiple spaces before operator +pymodbus/file_message.py:300:21: E221 multiple spaces before operator +pymodbus/file_message.py:331:80: E501 line too long (83 characters) +pymodbus/file_message.py:350:21: E221 multiple spaces before operator +pymodbus/file_message.py:352:21: E221 multiple spaces before operator +pymodbus/mei_message.py:43:13: E128 continuation line under-indented for visual indent +pymodbus/mei_message.py:66:13: E128 continuation line under-indented for visual indent +pymodbus/mei_message.py:91:17: E261 at least two spaces before inline comment +pymodbus/mei_message.py:94:69: E225 missing whitespace around operator +pymodbus/mei_message.py:108:31: E261 at least two spaces before inline comment +pymodbus/mei_message.py:111:35: E261 at least two spaces before inline comment +pymodbus/mei_message.py:120:13: E128 continuation line under-indented for visual indent +pymodbus/mei_message.py:121:13: E128 continuation line under-indented for visual indent +pymodbus/mei_message.py:138:40: E261 at least two spaces before inline comment +pymodbus/mei_message.py:141:80: E501 line too long (80 characters) +pymodbus/mei_message.py:141:77: E225 missing whitespace around operator +pymodbus/mei_message.py:143:53: E225 missing whitespace around operator +pymodbus/other_message.py:187:23: E701 multiple statements on one line (colon) +pymodbus/other_message.py:188:13: E701 multiple statements on one line (colon) +pymodbus/other_message.py:257:28: E203 whitespace before ':' +pymodbus/other_message.py:258:28: E203 whitespace before ':' +pymodbus/other_message.py:259:28: E203 whitespace before ':' +pymodbus/other_message.py:260:28: E203 whitespace before ':' +pymodbus/other_message.py:301:23: E701 multiple statements on one line (colon) +pymodbus/other_message.py:302:13: E701 multiple statements on one line (colon) +pymodbus/other_message.py:303:15: E221 multiple spaces before operator +pymodbus/other_message.py:329:80: E501 line too long (91 characters) +pymodbus/other_message.py:400:23: E701 multiple statements on one line (colon) +pymodbus/other_message.py:401:13: E701 multiple statements on one line (colon) +pymodbus/payload.py:36:21: E221 multiple spaces before operator +pymodbus/payload.py:61:27: E225 missing whitespace around operator +pymodbus/payload.py:186:21: E221 multiple spaces before operator pymodbus/pdu.py:79:13: E701 multiple statements on one line (colon) -pymodbus/pdu.py:129:27: E221 multiple spaces before operator -pymodbus/pdu.py:130:27: E221 multiple spaces before operator -pymodbus/pdu.py:131:27: E221 multiple spaces before operator -pymodbus/pdu.py:132:27: E221 multiple spaces before operator -pymodbus/pdu.py:133:27: E221 multiple spaces before operator -pymodbus/pdu.py:134:27: E221 multiple spaces before operator -pymodbus/pdu.py:135:27: E221 multiple spaces before operator +pymodbus/pdu.py:97:17: E128 continuation line under-indented for visual indent +pymodbus/pdu.py:129:20: E221 multiple spaces before operator +pymodbus/pdu.py:130:19: E221 multiple spaces before operator +pymodbus/pdu.py:131:17: E221 multiple spaces before operator +pymodbus/pdu.py:132:17: E221 multiple spaces before operator +pymodbus/pdu.py:133:16: E221 multiple spaces before operator +pymodbus/pdu.py:134:14: E221 multiple spaces before operator +pymodbus/pdu.py:135:22: E221 multiple spaces before operator pymodbus/pdu.py:136:27: E221 multiple spaces before operator -pymodbus/pdu.py:137:27: E221 multiple spaces before operator +pymodbus/pdu.py:137:22: E221 multiple spaces before operator pymodbus/register_read_message.py:128:80: E501 line too long (80 characters) pymodbus/register_read_message.py:178:80: E501 line too long (80 characters) -pymodbus/register_read_message.py:228:26: E221 multiple spaces before operator -pymodbus/register_read_message.py:229:26: E221 multiple spaces before operator +pymodbus/register_read_message.py:226:26: E221 multiple spaces before operator +pymodbus/register_read_message.py:227:24: E221 multiple spaces before operator +pymodbus/register_read_message.py:228:27: E221 multiple spaces before operator +pymodbus/register_read_message.py:241:17: E128 continuation line under-indented for visual indent +pymodbus/register_read_message.py:242:17: E128 continuation line under-indented for visual indent +pymodbus/register_read_message.py:241:54: E502 the backslash is redundant between brackets +pymodbus/register_read_message.py:253:9: E122 continuation line missing indentation or outdented +pymodbus/register_read_message.py:254:9: E122 continuation line missing indentation or outdented +pymodbus/register_read_message.py:255:29: E221 multiple spaces before operator pymodbus/register_write_message.py:133:22: E701 multiple statements on one line (colon) pymodbus/register_write_message.py:134:45: E701 multiple statements on one line (colon) -pymodbus/register_write_message.py:154:80: E501 line too long (83 characters) -pymodbus/transaction.py:141:31: E231 missing whitespace after ':' -pymodbus/transaction.py:142:21: E221 multiple spaces before operator -pymodbus/transaction.py:143:21: E221 multiple spaces before operator -pymodbus/transaction.py:174:31: E231 missing whitespace after ':' -pymodbus/transaction.py:239:17: E701 multiple statements on one line (colon) -pymodbus/transaction.py:300:21: E221 multiple spaces before operator -pymodbus/transaction.py:301:21: E221 multiple spaces before operator -pymodbus/transaction.py:303:21: E221 multiple spaces before operator -pymodbus/transaction.py:375:14: E221 multiple spaces before operator -pymodbus/transaction.py:376:14: E221 multiple spaces before operator -pymodbus/transaction.py:417:17: E701 multiple statements on one line (colon) -pymodbus/transaction.py:457:31: E231 missing whitespace after ':' -pymodbus/transaction.py:458:21: E221 multiple spaces before operator -pymodbus/transaction.py:459:21: E221 multiple spaces before operator -pymodbus/transaction.py:460:21: E221 multiple spaces before operator -pymodbus/transaction.py:461:21: E221 multiple spaces before operator -pymodbus/transaction.py:472:23: E701 multiple statements on one line (colon) -pymodbus/transaction.py:473:21: E203 whitespace before ':' -pymodbus/transaction.py:492:31: E231 missing whitespace after ':' -pymodbus/transaction.py:517:14: E221 multiple spaces before operator -pymodbus/transaction.py:518:14: E221 multiple spaces before operator -pymodbus/transaction.py:559:17: E701 multiple statements on one line (colon) -pymodbus/transaction.py:568:16: E221 multiple spaces before operator -pymodbus/transaction.py:569:16: E221 multiple spaces before operator -pymodbus/transaction.py:612:31: E231 missing whitespace after ':' -pymodbus/transaction.py:613:21: E221 multiple spaces before operator -pymodbus/transaction.py:614:21: E221 multiple spaces before operator -pymodbus/transaction.py:615:21: E221 multiple spaces before operator -pymodbus/transaction.py:616:21: E221 multiple spaces before operator -pymodbus/transaction.py:627:23: E701 multiple statements on one line (colon) -pymodbus/transaction.py:628:21: E203 whitespace before ':' -pymodbus/transaction.py:635:80: E501 line too long (85 characters) -pymodbus/transaction.py:647:31: E231 missing whitespace after ':' -pymodbus/transaction.py:672:14: E221 multiple spaces before operator -pymodbus/transaction.py:673:14: E221 multiple spaces before operator -pymodbus/transaction.py:714:17: E701 multiple statements on one line (colon) +pymodbus/register_write_message.py:155:9: E122 continuation line missing indentation or outdented +pymodbus/transaction.py:9:24: E272 multiple spaces before keyword +pymodbus/transaction.py:11:24: E272 multiple spaces before keyword +pymodbus/transaction.py:12:24: E272 multiple spaces before keyword +pymodbus/transaction.py:66:80: E501 line too long (85 characters) +pymodbus/transaction.py:67:22: E702 multiple statements on one line (semicolon) +pymodbus/transaction.py:146:31: E231 missing whitespace after ':' +pymodbus/transaction.py:146:40: E231 missing whitespace after ':' +pymodbus/transaction.py:146:49: E231 missing whitespace after ':' +pymodbus/transaction.py:146:58: E231 missing whitespace after ':' +pymodbus/transaction.py:147:21: E221 multiple spaces before operator +pymodbus/transaction.py:148:21: E221 multiple spaces before operator +pymodbus/transaction.py:159:13: E122 continuation line missing indentation or outdented +pymodbus/transaction.py:160:21: E126 continuation line over-indented for hanging indent +pymodbus/transaction.py:179:31: E231 missing whitespace after ':' +pymodbus/transaction.py:179:40: E231 missing whitespace after ':' +pymodbus/transaction.py:179:49: E231 missing whitespace after ':' +pymodbus/transaction.py:179:58: E231 missing whitespace after ':' +pymodbus/transaction.py:244:17: E701 multiple statements on one line (colon) +pymodbus/transaction.py:253:13: E128 continuation line under-indented for visual indent +pymodbus/transaction.py:254:13: E128 continuation line under-indented for visual indent +pymodbus/transaction.py:255:13: E128 continuation line under-indented for visual indent +pymodbus/transaction.py:305:21: E221 multiple spaces before operator +pymodbus/transaction.py:306:19: E221 multiple spaces before operator +pymodbus/transaction.py:308:21: E221 multiple spaces before operator +pymodbus/transaction.py:380:14: E221 multiple spaces before operator +pymodbus/transaction.py:381:12: E221 multiple spaces before operator +pymodbus/transaction.py:383:19: E701 multiple statements on one line (colon) +pymodbus/transaction.py:423:17: E701 multiple statements on one line (colon) +pymodbus/transaction.py:432:13: E128 continuation line under-indented for visual indent +pymodbus/transaction.py:433:13: E128 continuation line under-indented for visual indent +pymodbus/transaction.py:463:31: E231 missing whitespace after ':' +pymodbus/transaction.py:463:45: E231 missing whitespace after ':' +pymodbus/transaction.py:463:54: E231 missing whitespace after ':' +pymodbus/transaction.py:464:21: E221 multiple spaces before operator +pymodbus/transaction.py:465:21: E221 multiple spaces before operator +pymodbus/transaction.py:466:19: E221 multiple spaces before operator +pymodbus/transaction.py:467:21: E221 multiple spaces before operator +pymodbus/transaction.py:478:23: E701 multiple statements on one line (colon) +pymodbus/transaction.py:479:21: E203 whitespace before ':' +pymodbus/transaction.py:499:31: E231 missing whitespace after ':' +pymodbus/transaction.py:499:45: E231 missing whitespace after ':' +pymodbus/transaction.py:499:54: E231 missing whitespace after ':' +pymodbus/transaction.py:524:14: E221 multiple spaces before operator +pymodbus/transaction.py:525:12: E221 multiple spaces before operator +pymodbus/transaction.py:527:19: E701 multiple statements on one line (colon) +pymodbus/transaction.py:567:17: E701 multiple statements on one line (colon) +pymodbus/transaction.py:576:16: E221 multiple spaces before operator +pymodbus/transaction.py:577:15: E221 multiple spaces before operator +pymodbus/transaction.py:620:31: E231 missing whitespace after ':' +pymodbus/transaction.py:620:45: E231 missing whitespace after ':' +pymodbus/transaction.py:620:54: E231 missing whitespace after ':' +pymodbus/transaction.py:621:21: E221 multiple spaces before operator +pymodbus/transaction.py:622:21: E221 multiple spaces before operator +pymodbus/transaction.py:623:19: E221 multiple spaces before operator +pymodbus/transaction.py:624:21: E221 multiple spaces before operator +pymodbus/transaction.py:635:23: E701 multiple statements on one line (colon) +pymodbus/transaction.py:636:21: E203 whitespace before ':' +pymodbus/transaction.py:643:80: E501 line too long (85 characters) +pymodbus/transaction.py:655:31: E231 missing whitespace after ':' +pymodbus/transaction.py:655:45: E231 missing whitespace after ':' +pymodbus/transaction.py:655:54: E231 missing whitespace after ':' +pymodbus/transaction.py:680:14: E221 multiple spaces before operator +pymodbus/transaction.py:681:12: E221 multiple spaces before operator +pymodbus/transaction.py:683:19: E701 multiple statements on one line (colon) +pymodbus/transaction.py:723:17: E701 multiple statements on one line (colon) +pymodbus/transaction.py:733:13: E128 continuation line under-indented for visual indent +pymodbus/transaction.py:734:13: E128 continuation line under-indented for visual indent +pymodbus/transaction.py:749:31: E701 multiple statements on one line (colon) +pymodbus/transaction.py:750:17: E701 multiple statements on one line (colon) pymodbus/utilities.py:64:15: E701 multiple statements on one line (colon) pymodbus/utilities.py:69:13: E701 multiple statements on one line (colon) pymodbus/utilities.py:110:17: E701 multiple statements on one line (colon) pymodbus/utilities.py:131:51: E702 multiple statements on one line (semicolon) -pymodbus/client/sync.py:52:80: E501 line too long (83 characters) -pymodbus/client/sync.py:53:22: E702 multiple statements on one line (semicolon) -pymodbus/client/sync.py:101:80: E501 line too long (80 characters) -pymodbus/client/sync.py:106:80: E501 line too long (80 characters) -pymodbus/client/sync.py:114:80: E501 line too long (80 characters) -pymodbus/client/sync.py:122:80: E501 line too long (80 characters) -pymodbus/client/sync.py:145:80: E501 line too long (81 characters) -pymodbus/client/sync.py:187:23: E701 multiple statements on one line (colon) -pymodbus/client/sync.py:255:23: E701 multiple statements on one line (colon) -pymodbus/client/sync.py:313:21: E221 multiple spaces before operator -pymodbus/client/sync.py:314:21: E221 multiple spaces before operator -pymodbus/client/sync.py:317:21: E221 multiple spaces before operator -pymodbus/client/sync.py:320:21: E221 multiple spaces before operator -pymodbus/client/sync.py:322:21: E221 multiple spaces before operator -pymodbus/client/sync.py:332:31: E701 multiple statements on one line (colon) -pymodbus/client/sync.py:333:29: E701 multiple statements on one line (colon) -pymodbus/client/sync.py:334:32: E701 multiple statements on one line (colon) -pymodbus/client/sync.py:342:23: E701 multiple statements on one line (colon) -pymodbus/datastore/context.py:8:15: E702 multiple statements on one line (semicolon) -pymodbus/datastore/context.py:98:21: E221 multiple spaces before operator -pymodbus/datastore/context.py:117:23: E701 multiple statements on one line (colon) -pymodbus/datastore/context.py:120:13: E701 multiple statements on one line (colon) +pymodbus/client/async.py:115:17: E701 multiple statements on one line (colon) +pymodbus/client/async.py:130:32: E261 at least two spaces before inline comment +pymodbus/client/async.py:187:17: E701 multiple statements on one line (colon) +pymodbus/client/async.py:198:32: E261 at least two spaces before inline comment +pymodbus/client/sync.py:47:80: E501 line too long (80 characters) +pymodbus/client/sync.py:60:80: E501 line too long (80 characters) +pymodbus/client/sync.py:68:80: E501 line too long (80 characters) +pymodbus/client/sync.py:79:80: E501 line too long (81 characters) +pymodbus/client/sync.py:93:80: E501 line too long (81 characters) +pymodbus/client/sync.py:137:23: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:139:80: E501 line too long (92 characters) +pymodbus/client/sync.py:143:17: E128 continuation line under-indented for visual indent +pymodbus/client/sync.py:142:65: E502 the backslash is redundant between brackets +pymodbus/client/sync.py:145:28: E711 comparison to None should be 'if cond is not None:' +pymodbus/client/sync.py:212:29: E261 at least two spaces before inline comment +pymodbus/client/sync.py:221:23: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:228:28: E711 comparison to None should be 'if cond is not None:' +pymodbus/client/sync.py:289:20: E221 multiple spaces before operator +pymodbus/client/sync.py:290:20: E221 multiple spaces before operator +pymodbus/client/sync.py:293:18: E221 multiple spaces before operator +pymodbus/client/sync.py:296:20: E221 multiple spaces before operator +pymodbus/client/sync.py:298:21: E221 multiple spaces before operator +pymodbus/client/sync.py:308:31: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:308:32: E272 multiple spaces before keyword +pymodbus/client/sync.py:309:29: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:309:30: E272 multiple spaces before keyword +pymodbus/client/sync.py:310:32: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:318:23: E701 multiple statements on one line (colon) +pymodbus/client/sync.py:321:17: E128 continuation line under-indented for visual indent +pymodbus/client/sync.py:322:17: E128 continuation line under-indented for visual indent +pymodbus/client/sync.py:326:28: E711 comparison to None should be 'if cond is not None:' +pymodbus/server/async.py:218:5: E128 continuation line under-indented for visual indent +pymodbus/server/async.py:218:5: E125 continuation line does not distinguish itself from next logical line +pymodbus/server/async.py:236:43: E261 at least two spaces before inline comment +pymodbus/server/sync.py:78:80: E501 line too long (81 characters) +pymodbus/server/sync.py:80:80: E501 line too long (81 characters) +pymodbus/server/sync.py:84:80: E501 line too long (80 characters) +pymodbus/server/sync.py:91:80: E501 line too long (80 characters) +pymodbus/server/sync.py:110:34: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:113:19: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:140:28: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:144:34: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:148:19: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:177:28: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:181:34: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:185:19: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:224:30: E272 multiple spaces before keyword +pymodbus/server/sync.py:224:20: E221 multiple spaces before operator +pymodbus/server/sync.py:232:13: E128 continuation line under-indented for visual indent +pymodbus/server/sync.py:248:35: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:273:30: E272 multiple spaces before keyword +pymodbus/server/sync.py:273:20: E221 multiple spaces before operator +pymodbus/server/sync.py:281:13: E128 continuation line under-indented for visual indent +pymodbus/server/sync.py:289:33: E261 at least two spaces before inline comment +pymodbus/server/sync.py:298:35: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:329:30: E272 multiple spaces before keyword +pymodbus/server/sync.py:329:20: E221 multiple spaces before operator +pymodbus/server/sync.py:336:20: E221 multiple spaces before operator +pymodbus/server/sync.py:339:20: E221 multiple spaces before operator +pymodbus/server/sync.py:341:21: E221 multiple spaces before operator +pymodbus/server/sync.py:342:20: E221 multiple spaces before operator +pymodbus/server/sync.py:350:23: E701 multiple statements on one line (colon) +pymodbus/server/sync.py:353:17: E128 continuation line under-indented for visual indent +pymodbus/server/sync.py:354:17: E128 continuation line under-indented for visual indent +pymodbus/server/sync.py:358:28: E711 comparison to None should be 'if cond is not None:' +pymodbus/server/sync.py:370:13: E128 continuation line under-indented for visual indent +pymodbus/server/sync.py:381:19: E701 multiple statements on one line (colon) +pymodbus/internal/ptwisted.py:53:15: E701 multiple statements on one line (colon) +pymodbus/datastore/context.py:9:15: E702 multiple statements on one line (semicolon) +pymodbus/datastore/context.py:101:20: E221 multiple spaces before operator pymodbus/datastore/context.py:128:23: E701 multiple statements on one line (colon) -pymodbus/datastore/context.py:131:80: E501 line too long (82 characters) pymodbus/datastore/context.py:131:13: E701 multiple statements on one line (colon) +pymodbus/datastore/context.py:139:23: E701 multiple statements on one line (colon) +pymodbus/datastore/context.py:142:80: E501 line too long (82 characters) +pymodbus/datastore/context.py:142:13: E701 multiple statements on one line (colon) pymodbus/datastore/database.py:13:15: E702 multiple statements on one line (semicolon) pymodbus/datastore/database.py:83:80: E501 line too long (80 characters) pymodbus/datastore/database.py:85:80: E501 line too long (80 characters) +pymodbus/datastore/database.py:95:13: E128 continuation line under-indented for visual indent pymodbus/datastore/database.py:110:14: E221 multiple spaces before operator pymodbus/datastore/database.py:128:28: E203 whitespace before ':' -pymodbus/datastore/database.py:142:15: E221 multiple spaces before operator +pymodbus/datastore/database.py:129:28: E203 whitespace before ':' +pymodbus/datastore/database.py:130:28: E203 whitespace before ':' +pymodbus/datastore/database.py:142:14: E221 multiple spaces before operator pymodbus/datastore/database.py:143:15: E221 multiple spaces before operator -pymodbus/datastore/database.py:154:15: E221 multiple spaces before operator -pymodbus/datastore/database.py:155:15: E221 multiple spaces before operator +pymodbus/datastore/database.py:154:14: E221 multiple spaces before operator +pymodbus/datastore/database.py:155:14: E221 multiple spaces before operator +pymodbus/datastore/database.py:156:31: E221 multiple spaces before operator pymodbus/datastore/database.py:158:15: E221 multiple spaces before operator pymodbus/datastore/database.py:168:14: E221 multiple spaces before operator pymodbus/datastore/modredis.py:8:15: E702 multiple statements on one line (semicolon) pymodbus/datastore/modredis.py:80:80: E501 line too long (80 characters) pymodbus/datastore/modredis.py:82:80: E501 line too long (80 characters) pymodbus/datastore/modredis.py:97:16: E203 whitespace before ':' +pymodbus/datastore/modredis.py:98:16: E203 whitespace before ':' +pymodbus/datastore/modredis.py:99:16: E203 whitespace before ':' +pymodbus/datastore/modredis.py:100:16: E203 whitespace before ':' pymodbus/datastore/modredis.py:103:16: E203 whitespace before ':' +pymodbus/datastore/modredis.py:104:16: E203 whitespace before ':' +pymodbus/datastore/modredis.py:105:16: E203 whitespace before ':' +pymodbus/datastore/modredis.py:106:16: E203 whitespace before ':' pymodbus/datastore/modredis.py:109:16: E203 whitespace before ':' +pymodbus/datastore/modredis.py:110:16: E203 whitespace before ':' +pymodbus/datastore/modredis.py:111:16: E203 whitespace before ':' +pymodbus/datastore/modredis.py:112:16: E203 whitespace before ':' pymodbus/datastore/modredis.py:115:80: E501 line too long (80 characters) pymodbus/datastore/modredis.py:117:80: E501 line too long (80 characters) -pymodbus/datastore/modredis.py:118:17: E221 multiple spaces before operator +pymodbus/datastore/modredis.py:118:15: E221 multiple spaces before operator pymodbus/datastore/modredis.py:132:16: E221 multiple spaces before operator pymodbus/datastore/modredis.py:176:80: E501 line too long (92 characters) -pymodbus/datastore/modredis.py:176:15: E221 multiple spaces before operator +pymodbus/datastore/modredis.py:176:14: E221 multiple spaces before operator pymodbus/datastore/modredis.py:183:80: E501 line too long (80 characters) pymodbus/datastore/modredis.py:185:80: E501 line too long (80 characters) -pymodbus/datastore/modredis.py:186:17: E221 multiple spaces before operator +pymodbus/datastore/modredis.py:186:15: E221 multiple spaces before operator pymodbus/datastore/modredis.py:201:16: E221 multiple spaces before operator pymodbus/datastore/remote.py:98:32: E701 multiple statements on one line (colon) pymodbus/datastore/remote.py:99:32: E701 multiple statements on one line (colon) pymodbus/datastore/remote.py:100:13: E701 multiple statements on one line (colon) pymodbus/datastore/store.py:144:13: E701 multiple statements on one line (colon) -pymodbus/datastore/store.py:154:15: E221 multiple spaces before operator -pymodbus/datastore/store.py:195:13: E701 multiple statements on one line (colon) -pymodbus/datastore/store.py:207:22: E701 multiple statements on one line (colon) -pymodbus/internal/ptwisted.py:53:15: E701 multiple statements on one line (colon) -pymodbus/server/sync.py:60:34: E701 multiple statements on one line (colon) -pymodbus/server/sync.py:64:19: E701 multiple statements on one line (colon) -pymodbus/server/sync.py:130:20: E221 multiple spaces before operator -pymodbus/server/sync.py:154:35: E701 multiple statements on one line (colon) -pymodbus/server/sync.py:179:20: E221 multiple spaces before operator -pymodbus/server/sync.py:203:35: E701 multiple statements on one line (colon) -pymodbus/server/sync.py:228:20: E221 multiple spaces before operator -pymodbus/server/sync.py:235:21: E221 multiple spaces before operator -pymodbus/server/sync.py:238:21: E221 multiple spaces before operator -pymodbus/server/sync.py:240:21: E221 multiple spaces before operator -pymodbus/server/sync.py:241:21: E221 multiple spaces before operator -pymodbus/server/sync.py:249:23: E701 multiple statements on one line (colon) -pymodbus/server/sync.py:280:19: E701 multiple statements on one line (colon) -7 E203 whitespace before ':' -119 E221 multiple spaces before operator -6 E231 missing whitespace after ':' -32 E501 line too long (80 characters) -42 E701 multiple statements on one line (colon) +pymodbus/datastore/store.py:163:15: E221 multiple spaces before operator +pymodbus/datastore/store.py:204:13: E701 multiple statements on one line (colon) +pymodbus/datastore/store.py:216:44: E225 missing whitespace around operator +pymodbus/datastore/store.py:225:22: E701 multiple statements on one line (colon) +4 E122 continuation line missing indentation or outdented +1 E125 continuation line does not distinguish itself from next logical line +77 E126 continuation line over-indented for hanging indent +4 E127 continuation line over-indented for visual indent +34 E128 continuation line under-indented for visual indent +1 E201 whitespace after '{' +1 E202 whitespace before '}' +68 E203 whitespace before ':' +154 E221 multiple spaces before operator +10 E225 missing whitespace around operator +29 E231 missing whitespace after ',' +26 E261 at least two spaces before inline comment +10 E272 multiple spaces before keyword +47 E501 line too long (80 characters) +2 E502 the backslash is redundant between brackets +72 E701 multiple statements on one line (colon) 5 E702 multiple statements on one line (semicolon) +4 E711 comparison to None should be 'if cond is not None:' diff --git a/pymodbus/client/async.py b/pymodbus/client/async.py index a65f48440..4aea64d38 100644 --- a/pymodbus/client/async.py +++ b/pymodbus/client/async.py @@ -138,6 +138,7 @@ def _buildResponse(self, tid): # deferLater(clock, self.delay, send, message) # self.retry -= 1 + #---------------------------------------------------------------------------# # Not Connected Client Protocol #---------------------------------------------------------------------------# diff --git a/pymodbus/datastore/store.py b/pymodbus/datastore/store.py index e228f7710..2c35da065 100644 --- a/pymodbus/datastore/store.py +++ b/pymodbus/datastore/store.py @@ -151,7 +151,7 @@ def create(): :returns: An initialized datastore ''' - return ModbusSequentialDataBlock(0x00, [0x00]*65536) + return ModbusSequentialDataBlock(0x00, [0x00] * 65536) def validate(self, address, count=1): ''' Checks to see if the request is in range diff --git a/pymodbus/device.py b/pymodbus/device.py index 0c6d78a01..4c90d8460 100644 --- a/pymodbus/device.py +++ b/pymodbus/device.py @@ -134,7 +134,7 @@ class ModbusPlusStatistics(object): 'receive_buffer_use_bit_map' : [0x00] * 8, # 35-37 'data_master_output_path' : [0x00] * 8, # 38-41 'data_slave_input_path' : [0x00] * 8, # 42-45 - 'program_master_outptu_path' : [0x00] * 8, # 46-49 + 'program_master_outptu_path' : [0x00] * 8, # 46-49 'program_slave_input_path' : [0x00] * 8, # 50-53 } @@ -286,7 +286,7 @@ class DeviceInformationFactory(Singleton): ''' __lookup = { - DeviceInformation.Basic: lambda c,r,i: c.__gets(r, range(0x00, 0x03)), + DeviceInformation.Basic: lambda c,r,i: c.__gets(r, range(0x00, 0x03)), DeviceInformation.Regular: lambda c,r,i: c.__gets(r, range(0x00, 0x08)), DeviceInformation.Extended: lambda c,r,i: c.__gets(r, range(0x80, i)), DeviceInformation.Specific: lambda c,r,i: c.__get(r, i), @@ -324,6 +324,7 @@ def __gets(cls, identity, object_ids): ''' return dict((id, identity[id]) for id in object_ids) + #---------------------------------------------------------------------------# # Counters Handler #---------------------------------------------------------------------------# diff --git a/pymodbus/diag_message.py b/pymodbus/diag_message.py index 90a1279b1..3e9e585b7 100644 --- a/pymodbus/diag_message.py +++ b/pymodbus/diag_message.py @@ -720,7 +720,7 @@ class GetClearModbusPlusResponse(DiagnosticStatusSimpleResponse): # Exported symbols #---------------------------------------------------------------------------# __all__ = [ - "DiagnosticStatusRequest", "DiagnosticStatusResponse", + "DiagnosticStatusRequest", "DiagnosticStatusResponse", "ReturnQueryDataRequest", "ReturnQueryDataResponse", "RestartCommunicationsOptionRequest", "RestartCommunicationsOptionResponse", "ReturnDiagnosticRegisterRequest", "ReturnDiagnosticRegisterResponse", diff --git a/pymodbus/factory.py b/pymodbus/factory.py index f55105c89..477423edc 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -159,7 +159,7 @@ class ClientDecoder(IModbusDecoder): WriteSingleCoilResponse, ReadWriteMultipleRegistersResponse, - DiagnosticStatusResponse, + DiagnosticStatusResponse, ReadExceptionStatusResponse, GetCommEventCounterResponse, diff --git a/pymodbus/file_message.py b/pymodbus/file_message.py index 0dedbac4e..ca57288e0 100644 --- a/pymodbus/file_message.py +++ b/pymodbus/file_message.py @@ -75,7 +75,7 @@ class ReadFileRecordRequest(ModbusRequest): The file number: 2 bytes The starting record number within the file: 2 bytes The length of the record to be read: 2 bytes - + The quantity of registers to be read, combined with all other fields in the expected response, must not exceed the allowable length of the MODBUS PDU: 235 bytes. diff --git a/pymodbus/other_message.py b/pymodbus/other_message.py index 4675f7f16..9ea59d0c6 100644 --- a/pymodbus/other_message.py +++ b/pymodbus/other_message.py @@ -184,7 +184,7 @@ def encode(self): :returns: The byte encoded message ''' - if self.status: ready = ModbusStatus.Ready + if self.status: ready = ModbusStatus.Ready else: ready = ModbusStatus.Waiting return struct.pack('>HH', ready, self.count) @@ -298,7 +298,7 @@ def encode(self): :returns: The byte encoded message ''' - if self.status: ready = ModbusStatus.Ready + if self.status: ready = ModbusStatus.Ready else: ready = ModbusStatus.Waiting packet = struct.pack('>B', 6 + len(self.events)) packet += struct.pack('>H', ready) diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index ca6db32ff..8ff77f4f4 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -90,6 +90,7 @@ def send(self, message): ''' raise NotImplementedException("Method not implemented by derived class") + class ModbusSingleRequestHandler(ModbusBaseRequestHandler): ''' Implements the modbus server protocol @@ -194,6 +195,7 @@ def send(self, message): _logger.debug('send: %s' % b2a_hex(pdu)) return self.request.sendto(pdu, self.client_address) + #---------------------------------------------------------------------------# # Server Implementations #---------------------------------------------------------------------------# diff --git a/pymodbus/utilities.py b/pymodbus/utilities.py index 00eb73c49..a9e308cfe 100644 --- a/pymodbus/utilities.py +++ b/pymodbus/utilities.py @@ -37,7 +37,7 @@ def dict_property(store, index): elif isinstance(store, str): get = lambda self: self.__getattribute__(store)[index] set = lambda self, value: self.__getattribute__(store).__setitem__( - index, value) + index, value) else: get = lambda self: store[index] set = lambda self, value: store.__setitem__(index, value) From 14092219ea5857dca1045a12aa971a935fc858a5 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 20 Jun 2012 10:19:12 -0500 Subject: [PATCH 074/243] finished unit tests for async client --- doc/quality/current.coverage | 10 +- pymodbus/client/async.py | 13 +-- test/test_client_async.py | 172 ++++++++++++++++++++++++++++++++++- 3 files changed, 177 insertions(+), 18 deletions(-) diff --git a/doc/quality/current.coverage b/doc/quality/current.coverage index 7a6fbe430..e17e376fa 100644 --- a/doc/quality/current.coverage +++ b/doc/quality/current.coverage @@ -4,7 +4,7 @@ pymodbus 15 6 60% 24-27, 36-37 pymodbus.bit_read_message 68 0 100% pymodbus.bit_write_message 95 0 100% pymodbus.client 0 0 100% -pymodbus.client.async 69 42 39% 68-70, 75-76, 83-86, 94, 100-103, 110-115, 125-131, 156-157, 164-165, 171-174, 181-186, 196-198 +pymodbus.client.async 68 0 100% pymodbus.client.common 45 0 100% pymodbus.client.sync 148 0 100% pymodbus.constants 36 0 100% @@ -29,13 +29,13 @@ pymodbus.register_read_message 124 0 100% pymodbus.register_write_message 87 0 100% pymodbus.server 0 0 100% pymodbus.server.async 107 75 30% 40-41, 48, 55-57, 64-73, 80-84, 108-115, 135-142, 149-153, 160-169, 177-180, 192-199, 208-214, 227-238 -pymodbus.server.sync 182 134 26% 40-43, 48-49, 56-64, 72-76, 84, 91, 103-112, 119-123, 136-147, 154-158, 173-184, 191-195, 220-229, 238-239, 244-246, 269-278, 287-289, 294-296, 325-341, 348-356, 364-369, 377-379, 384-385, 397-399, 408-410, 425-427 -pymodbus.transaction 263 60 77% 54-72, 234-244, 384, 414-423, 558-567, 637, 714-723, 749-750 +pymodbus.server.sync 182 134 26% 40-43, 48-49, 56-64, 72-76, 84, 91, 104-113, 120-124, 137-148, 155-159, 174-185, 192-196, 222-231, 240-241, 246-248, 271-280, 289-291, 296-298, 327-343, 350-358, 366-371, 379-381, 386-387, 399-401, 410-412, 427-429 +pymodbus.transaction 263 51 81% 54-72, 240, 244, 384, 414-423, 558-567, 637, 714-723, 749-750 pymodbus.utilities 67 0 100% pymodbus.version 13 0 100% --------------------------------------------------------------- -TOTAL 2655 336 87% +TOTAL 2654 285 89% ---------------------------------------------------------------------- -Ran 220 tests in 0.521s +Ran 232 tests in 0.507s OK diff --git a/pymodbus/client/async.py b/pymodbus/client/async.py index 4aea64d38..2ed5b3fca 100644 --- a/pymodbus/client/async.py +++ b/pymodbus/client/async.py @@ -82,7 +82,7 @@ def connectionLost(self, reason): ''' _logger.debug("Client disconnected from modbus server: %s" % reason) self._connected = False - for key in self._requests: + for key in self._requests.keys(): self._requests.pop(key).errback(Failure( ConnectionException('Connection lost during request'))) @@ -107,13 +107,12 @@ def _handleResponse(self, reply): :param reply: The reply to process ''' - if self._requests and reply: + if reply is not None: tid = reply.transaction_id handler = self._requests.pop(tid, None) if handler: handler.callback(reply) else: _logger.debug("Unrequested message: " + str(reply)) - # TODO errback handled somewhere def _buildResponse(self, tid): ''' Helper method to return a deferred response @@ -147,7 +146,6 @@ class ModbusUdpClientProtocol(protocol.DatagramProtocol, ModbusClientMixin): This represents the base modbus client protocol. All the application layer code is deferred to a higher level wrapper. ''' - __tid = 0 def __init__(self, framer=None): ''' Initializes the framer module @@ -155,7 +153,7 @@ def __init__(self, framer=None): :param framer: The framer to use for the protocol ''' self.framer = framer or ModbusSocketFramer(ClientDecoder()) - self._requests = deque() # link queue to tid + self._requests = {} def datagramReceived(self, data, (host, port)): ''' Get response, check for valid message, decode result @@ -179,13 +177,12 @@ def _handleResponse(self, reply): :param reply: The reply to process ''' - if self._requests and reply: + if reply is not None: tid = reply.transaction_id - handler = self.requests.pop(tid, None) + handler = self._requests.pop(tid, None) if handler: handler.callback(reply) else: _logger.debug("Unrequested message: " + str(reply)) - # TODO errback handled somewhere def _buildResponse(self, tid): ''' Helper method to return a deferred response diff --git a/test/test_client_async.py b/test/test_client_async.py index f30040c49..e2c22c3d8 100644 --- a/test/test_client_async.py +++ b/test/test_client_async.py @@ -1,10 +1,12 @@ #!/usr/bin/env python import unittest -from twisted.test import test_protocols +from mock import Mock from pymodbus.client.async import ModbusClientProtocol, ModbusUdpClientProtocol from pymodbus.client.async import ModbusClientFactory -from pymodbus.exceptions import ConnectionException, NotImplementedException +from pymodbus.exceptions import ConnectionException from pymodbus.exceptions import ParameterException +from pymodbus.transaction import ModbusSocketFramer +from pymodbus.bit_read_message import ReadCoilsRequest, ReadCoilsResponse #---------------------------------------------------------------------------# # Fixture @@ -29,12 +31,172 @@ def tearDown(self): pass #-----------------------------------------------------------------------# - # Test Base Client + # Test Client Protocol #-----------------------------------------------------------------------# - def testExampleTest(self): + def testClientProtocolInit(self): + ''' Test the client protocol initialize ''' + protocol = ModbusClientProtocol() + self.assertEqual(0, len(protocol._requests)) + self.assertFalse(protocol._connected) + self.assertTrue(isinstance(protocol.framer, ModbusSocketFramer)) + + framer = object() + protocol = ModbusClientProtocol(framer=framer) + self.assertEqual(0, len(protocol._requests)) + self.assertFalse(protocol._connected) + self.assertTrue(framer is protocol.framer) + + def testClientProtocolConnect(self): + ''' Test the client protocol connect ''' + protocol = ModbusClientProtocol() + self.assertFalse(protocol._connected) + protocol.connectionMade() + self.assertTrue(protocol._connected) + + def testClientProtocolDisconnect(self): + ''' Test the client protocol disconnect ''' + protocol = ModbusClientProtocol() + protocol.connectionMade() + def handle_failure(failure): + self.assertTrue(isinstance(failure.value, ConnectionException)) + d = protocol._buildResponse(0x00) + d.addErrback(handle_failure) + + self.assertTrue(protocol._connected) + protocol.connectionLost('because') + self.assertFalse(protocol._connected) + + def testClientProtocolDataReceived(self): + ''' Test the client protocol data received ''' + protocol = ModbusClientProtocol() + protocol.connectionMade() + out = [] + data = '\x00\x00\x12\x34\x00\x06\xff\x01\x01\x02\x00\x04' + + # setup existing request + d = protocol._buildResponse(0x00) + d.addCallback(lambda v: out.append(v)) + + protocol.dataReceived(data) + self.assertTrue(isinstance(out[0], ReadCoilsResponse)) + + def testClientProtocolExecute(self): + ''' Test the client protocol execute method ''' + protocol = ModbusClientProtocol() + protocol.connectionMade() + protocol.transport = Mock() + protocol.transport.write = Mock() + + request = ReadCoilsRequest(1, 1) + d = protocol.execute(request) + self.assertEqual(d, protocol._requests[request.transaction_id]) + + def testClientProtocolHandleResponse(self): + ''' Test the client protocol handles responses ''' + protocol = ModbusClientProtocol() + protocol.connectionMade() + out = [] + reply = ReadCoilsRequest(1, 1) + reply.transaction_id = 0x00 + + # handle skipped cases + protocol._handleResponse(None) + protocol._handleResponse(reply) + + # handle existing cases + d = protocol._buildResponse(0x00) + d.addCallback(lambda v: out.append(v)) + protocol._handleResponse(reply) + self.assertEqual(out[0], reply) + + def testClientProtocolBuildResponse(self): + ''' Test the udp client protocol builds responses ''' + protocol = ModbusClientProtocol() + self.assertEqual(0, len(protocol._requests)) + + def handle_failure(failure): + self.assertTrue(isinstance(failure.value, ConnectionException)) + d = protocol._buildResponse(0x00) + d.addErrback(handle_failure) + self.assertEqual(0, len(protocol._requests)) + + protocol._connected = True + d = protocol._buildResponse(0x00) + self.assertEqual(1, len(protocol._requests)) + + #-----------------------------------------------------------------------# + # Test Udp Client Protocol + #-----------------------------------------------------------------------# + + def testUdpClientProtocolInit(self): + ''' Test the udp client protocol initialize ''' + protocol = ModbusUdpClientProtocol() + self.assertEqual(0, len(protocol._requests)) + self.assertTrue(isinstance(protocol.framer, ModbusSocketFramer)) + + framer = object() + protocol = ModbusClientProtocol(framer=framer) + self.assertEqual(0, len(protocol._requests)) + self.assertTrue(framer is protocol.framer) + + def testUdpClientProtocolDataReceived(self): + ''' Test the udp client protocol data received ''' + protocol = ModbusUdpClientProtocol() + out = [] + data = '\x00\x00\x12\x34\x00\x06\xff\x01\x01\x02\x00\x04' + server = ('127.0.0.1', 12345) + + # setup existing request + d = protocol._buildResponse(0x00) + d.addCallback(lambda v: out.append(v)) + + protocol.datagramReceived(data, server) + self.assertTrue(isinstance(out[0], ReadCoilsResponse)) + + def testUdpClientProtocolExecute(self): + ''' Test the udp client protocol execute method ''' + protocol = ModbusUdpClientProtocol() + protocol.transport = Mock() + protocol.transport.write = Mock() + + request = ReadCoilsRequest(1, 1) + d = protocol.execute(request) + self.assertEqual(d, protocol._requests[request.transaction_id]) + + def testUdpClientProtocolHandleResponse(self): + ''' Test the udp client protocol handles responses ''' + protocol = ModbusUdpClientProtocol() + out = [] + reply = ReadCoilsRequest(1, 1) + reply.transaction_id = 0x00 + + # handle skipped cases + protocol._handleResponse(None) + protocol._handleResponse(reply) + + # handle existing cases + d = protocol._buildResponse(0x00) + d.addCallback(lambda v: out.append(v)) + protocol._handleResponse(reply) + self.assertEqual(out[0], reply) + + def testUdpClientProtocolBuildResponse(self): + ''' Test the udp client protocol builds responses ''' + protocol = ModbusUdpClientProtocol() + self.assertEqual(0, len(protocol._requests)) + + d = protocol._buildResponse(0x00) + self.assertEqual(1, len(protocol._requests)) + + #-----------------------------------------------------------------------# + # Test Client Factories + #-----------------------------------------------------------------------# + + def testModbusClientFactory(self): ''' Test the base class for all the clients ''' - self.assertTrue(True) + factory = ModbusClientFactory() + self.assertTrue(factory is not None) #---------------------------------------------------------------------------# # Main From c7f11218c76f542a1eb8e0b67cbef51e27aae11d Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 20 Jun 2012 10:55:06 -0500 Subject: [PATCH 075/243] adding another test to the test-install script --- examples/tools/build-datastore.py | 21 ++++++++++----------- examples/tools/test-install.sh | 19 +++++++++++++++---- 2 files changed, 25 insertions(+), 15 deletions(-) mode change 100644 => 100755 examples/tools/build-datastore.py diff --git a/examples/tools/build-datastore.py b/examples/tools/build-datastore.py old mode 100644 new mode 100755 index ff1267cb0..861cdd108 --- a/examples/tools/build-datastore.py +++ b/examples/tools/build-datastore.py @@ -6,7 +6,6 @@ dump. This allows users to build their own data from scratch or modifiy an exisiting dump. ''' -from __future__ import with_statement import pickle from sys import exit from optparse import OptionParser @@ -116,19 +115,19 @@ def main(): ''' The main function for this script ''' parser = OptionParser() parser.add_option("-o", "--output", - help="The output file to write to", - dest="file", default="example.store") + help="The output file to write to", + dest="file", default="example.store") parser.add_option("-t", "--type", - help="The type of block to create (sequential,sparse)", - dest="type", default="sparse") + help="The type of block to create (sequential,sparse)", + dest="type", default="sparse") parser.add_option("-c", "--convert", - help="Convert a file datastore to a register dump", - type="string", - action="callback", callback=build_conversion) + help="Convert a file datastore to a register dump", + type="string", + action="callback", callback=build_conversion) parser.add_option("-r", "--restore", - help="Convert a register dump to a file datastore", - type="string", - action="callback", callback=build_translation) + help="Convert a register dump to a file datastore", + type="string", + action="callback", callback=build_translation) try: (opt, arg) = parser.parse_args() # so we can catch the csv callback diff --git a/examples/tools/test-install.sh b/examples/tools/test-install.sh index 489f9f146..0b46ee07f 100755 --- a/examples/tools/test-install.sh +++ b/examples/tools/test-install.sh @@ -8,7 +8,7 @@ ENVIRONMENT="example" PACKAGE="pymodbus" # ------------------------------------------------------------------ # -# Preflight Tests +# preflight tests # ------------------------------------------------------------------ # if [[ "`which pip`" != "" ]]; then INSTALL="pip install -qU" @@ -31,7 +31,7 @@ if [[ "`which virtualenv`" == "" ]]; then fi # ------------------------------------------------------------------ # -# Setup +# setup test # ------------------------------------------------------------------ # echo -n "Setting up test..." virtualenv -q --no-site-packages --distribute ${ENVIRONMENT} @@ -39,7 +39,7 @@ source ${ENVIRONMENT}/bin/activate echo -e "\E[32mPassed\E[0m" # ------------------------------------------------------------------ # -# Main Test +# install test # ------------------------------------------------------------------ # echo -n "Testing package installation..." ${INSTALL} ${PACKAGE} @@ -50,7 +50,18 @@ else fi # ------------------------------------------------------------------ # -# Cleanup +# library test +# ------------------------------------------------------------------ # +echo -n "Testing python version..." +python -c "import pymodbus;print pymodbus.version.version" +if [[ "$?" == "0" ]]; then + echo -e "\E[32mPassed\E[0m" +else + echo -e "\E[31mPassed\E[0m" +fi + +# ------------------------------------------------------------------ # +# cleanup test # ------------------------------------------------------------------ # echo -n "Tearing down test..." deactivate From c6bf024fedca20b66245f7f7f75b507f9a0638a3 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Thu, 28 Jun 2012 12:29:51 -0700 Subject: [PATCH 076/243] finished covering sync client --- doc/quality/current.coverage | 10 +- pymodbus/internal/ptwisted.py | 17 -- pymodbus/server/sync.py | 47 +++--- test/test_client_sync.py | 7 +- test/test_ptwisted.py | 15 +- test/test_server_sync.py | 303 +++++++++++++++++++++++++++++++++- 6 files changed, 329 insertions(+), 70 deletions(-) diff --git a/doc/quality/current.coverage b/doc/quality/current.coverage index e17e376fa..552e95f10 100644 --- a/doc/quality/current.coverage +++ b/doc/quality/current.coverage @@ -1,6 +1,6 @@ Name Stmts Miss Cover Missing --------------------------------------------------------------- -pymodbus 15 6 60% 24-27, 36-37 +pymodbus 15 2 87% 36-37 pymodbus.bit_read_message 68 0 100% pymodbus.bit_write_message 95 0 100% pymodbus.client 0 0 100% @@ -20,7 +20,7 @@ pymodbus.factory 77 0 100% pymodbus.file_message 181 0 100% pymodbus.interfaces 43 0 100% pymodbus.internal 0 0 100% -pymodbus.internal.ptwisted 26 19 27% 26-37, 47-55 +pymodbus.internal.ptwisted 16 10 38% 26-37 pymodbus.mei_message 68 0 100% pymodbus.other_message 145 0 100% pymodbus.payload 134 0 100% @@ -29,13 +29,13 @@ pymodbus.register_read_message 124 0 100% pymodbus.register_write_message 87 0 100% pymodbus.server 0 0 100% pymodbus.server.async 107 75 30% 40-41, 48, 55-57, 64-73, 80-84, 108-115, 135-142, 149-153, 160-169, 177-180, 192-199, 208-214, 227-238 -pymodbus.server.sync 182 134 26% 40-43, 48-49, 56-64, 72-76, 84, 91, 104-113, 120-124, 137-148, 155-159, 174-185, 192-196, 222-231, 240-241, 246-248, 271-280, 289-291, 296-298, 327-343, 350-358, 366-371, 379-381, 386-387, 399-401, 410-412, 427-429 +pymodbus.server.sync 184 0 100% pymodbus.transaction 263 51 81% 54-72, 240, 244, 384, 414-423, 558-567, 637, 714-723, 749-750 pymodbus.utilities 67 0 100% pymodbus.version 13 0 100% --------------------------------------------------------------- -TOTAL 2654 285 89% +TOTAL 2646 138 95% ---------------------------------------------------------------------- -Ran 232 tests in 0.507s +Ran 249 tests in 1.448s OK diff --git a/pymodbus/internal/ptwisted.py b/pymodbus/internal/ptwisted.py index ff6f22821..54a68ec5f 100644 --- a/pymodbus/internal/ptwisted.py +++ b/pymodbus/internal/ptwisted.py @@ -36,20 +36,3 @@ def build_protocol(): factory = manhole_ssh.ConchFactory(p) reactor.listenTCP(port, factory) - -def InstallSpecializedReactor(): - ''' - This attempts to install a reactor specialized for the given - operating system. - - :returns: True if a specialized reactor was installed, False otherwise - ''' - from twisted.internet import epollreactor, kqreactor, iocpreactor - for reactor in [epollreactor, kqreactor, iocpreactor]: - try: - reactor.install() - _logger.debug("Installed %s" % reactor.__name__) - return True - except: pass - _logger.debug("No specialized reactor was installed") - return False diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index 8ff77f4f4..9e03df231 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -63,18 +63,6 @@ def execute(self, request): response.unit_id = request.unit_id self.send(response) - def decode(self, message): - ''' Decodes a request packet - - :param message: The raw modbus request packet - :returns: The decoded modbus message or None if error - ''' - try: - return decodeModbusRequestPDU(message) - except ModbusException, er: - _logger.warn("Unable to decode request %s" % er) - return None - #---------------------------------------------------------------------------# # Base class implementations #---------------------------------------------------------------------------# @@ -105,12 +93,12 @@ def handle(self): try: data = self.request.recv(1024) if data: - _logger.debug(" ".join([hex(ord(x)) for x in data])) + if _logger.isEnabledFor(logging.DEBUG): + _logger.debug(" ".join([hex(ord(x)) for x in data])) self.framer.processIncomingPacket(data, self.execute) - except socket.timeout: pass - except socket.error, msg: + except Exception, msg: + # since we only have a single socket, we cannot exit _logger.error("Socket error occurred %s" % msg) - except: pass def send(self, message): ''' Send a request (string) to the network @@ -120,7 +108,8 @@ def send(self, message): if message.should_respond: #self.server.control.Counter.BusMessage += 1 pdu = self.framer.buildPacket(message) - _logger.debug('send: %s' % b2a_hex(pdu)) + if _logger.isEnabledFor(logging.DEBUG): + _logger.debug('send: %s' % b2a_hex(pdu)) return self.request.send(pdu) @@ -138,7 +127,8 @@ def handle(self): try: data = self.request.recv(1024) if not data: self.running = False - _logger.debug(" ".join([hex(ord(x)) for x in data])) + if _logger.isEnabledFor(logging.DEBUG): + _logger.debug(" ".join([hex(ord(x)) for x in data])) # if not self.server.control.ListenOnly: self.framer.processIncomingPacket(data, self.execute) except socket.timeout: pass @@ -155,7 +145,8 @@ def send(self, message): if message.should_respond: #self.server.control.Counter.BusMessage += 1 pdu = self.framer.buildPacket(message) - _logger.debug('send: %s' % b2a_hex(pdu)) + if _logger.isEnabledFor(logging.DEBUG): + _logger.debug('send: %s' % b2a_hex(pdu)) return self.request.send(pdu) @@ -175,7 +166,8 @@ def handle(self): try: data, self.request = self.request if not data: self.running = False - _logger.debug(" ".join([hex(ord(x)) for x in data])) + if _logger.isEnabledFor(logging.DEBUG): + _logger.debug(" ".join([hex(ord(x)) for x in data])) # if not self.server.control.ListenOnly: self.framer.processIncomingPacket(data, self.execute) except socket.timeout: pass @@ -192,7 +184,8 @@ def send(self, message): if message.should_respond: #self.server.control.Counter.BusMessage += 1 pdu = self.framer.buildPacket(message) - _logger.debug('send: %s' % b2a_hex(pdu)) + if _logger.isEnabledFor(logging.DEBUG): + _logger.debug('send: %s' % b2a_hex(pdu)) return self.request.sendto(pdu, self.client_address) @@ -245,7 +238,8 @@ def server_close(self): ''' _logger.debug("Modbus server stopped") self.socket.close() - for thread in self.threads: thread.running = False + for thread in self.threads: + thread.running = False class ModbusUdpServer(SocketServer.ThreadingUDPServer): @@ -295,7 +289,8 @@ def server_close(self): ''' _logger.debug("Modbus server stopped") self.socket.close() - for thread in self.threads: thread.running = False + for thread in self.threads: + thread.running = False class ModbusSerialServer(object): @@ -341,6 +336,7 @@ def __init__(self, context, framer=None, identity=None, **kwargs): self.timeout = kwargs.get('timeout', Defaults.Timeout) self.socket = None self._connect() + self.is_running = True def _connect(self): ''' Connect to the serial server @@ -354,7 +350,6 @@ def _connect(self): baudrate=self.baudrate, parity=self.parity) except serial.SerialException, msg: _logger.error(msg) - self.close() return self.socket != None def _build_handler(self): @@ -378,12 +373,14 @@ def serve_forever(self): ''' _logger.debug("Started thread to serve client") handler = self._build_handler() - while True: handler.handle() + while self.is_running: + handler.handle() def server_close(self): ''' Callback for stopping the running server ''' _logger.debug("Modbus server stopped") + self.is_running = False self.socket.close() diff --git a/test/test_client_sync.py b/test/test_client_sync.py index 8e910eece..a9b96f213 100644 --- a/test/test_client_sync.py +++ b/test/test_client_sync.py @@ -2,7 +2,7 @@ import unittest import socket import serial -from mock import patch +from mock import patch, Mock from twisted.test import test_protocols from pymodbus.client.sync import ModbusTcpClient, ModbusUdpClient from pymodbus.client.sync import ModbusSerialClient, BaseModbusClient @@ -14,9 +14,6 @@ #---------------------------------------------------------------------------# # Mock Classes #---------------------------------------------------------------------------# -class mockTransaction(object): - def execute(self, request): return True - class mockSocket(object): def close(self): return True def recv(self, size): return '\x00'*size @@ -68,7 +65,7 @@ def testBaseModbusClient(self): # a successful execute client.connect = lambda: True - client.transaction = mockTransaction() + client.transaction = Mock(**{'execute.return_value': True}) self.assertTrue(client.execute()) # a successful connect, no transaction diff --git a/test/test_ptwisted.py b/test/test_ptwisted.py index 20333d0ee..e667f04fa 100644 --- a/test/test_ptwisted.py +++ b/test/test_ptwisted.py @@ -1,6 +1,5 @@ #!/usr/bin/env python import unittest -from pymodbus.internal.ptwisted import InstallSpecializedReactor #---------------------------------------------------------------------------# # Fixture @@ -14,20 +13,10 @@ class TwistedInternalCodeTest(unittest.TestCase): # Setup/TearDown #-----------------------------------------------------------------------# - def setUp(self): - ''' Initializes the test environment ''' + def testInstallConch(self): + ''' Test that we can install the conch backend ''' pass - def tearDown(self): - ''' Cleans up the test environment ''' - pass - - def testInstallSpecializedReactor(self): - ''' Test that True and False are defined on all versions''' - #result = InstallSpecializedReactor() - result = True - self.assertTrue(result) - #---------------------------------------------------------------------------# # Main #---------------------------------------------------------------------------# diff --git a/test/test_server_sync.py b/test/test_server_sync.py index ebb6543e8..0e2c34723 100644 --- a/test/test_server_sync.py +++ b/test/test_server_sync.py @@ -1,18 +1,34 @@ #!/usr/bin/env python import unittest -from twisted.test import test_protocols +from mock import patch, Mock +import SocketServer +import serial +import socket + from pymodbus.server.sync import ModbusBaseRequestHandler +from pymodbus.server.sync import ModbusSingleRequestHandler from pymodbus.server.sync import ModbusConnectedRequestHandler from pymodbus.server.sync import ModbusDisconnectedRequestHandler from pymodbus.server.sync import ModbusTcpServer, ModbusUdpServer, ModbusSerialServer from pymodbus.server.sync import StartTcpServer, StartUdpServer, StartSerialServer from pymodbus.exceptions import ConnectionException, NotImplementedException from pymodbus.exceptions import ParameterException +from pymodbus.bit_read_message import ReadCoilsRequest, ReadCoilsResponse + +#---------------------------------------------------------------------------# +# Mock Classes +#---------------------------------------------------------------------------# +class MockServer(object): + def __init__(self): + self.framer = lambda _: "framer" + self.decoder = "decoder" + self.threads = [] + self.context = {} #---------------------------------------------------------------------------# # Fixture #---------------------------------------------------------------------------# -class AsynchronousClientTest(unittest.TestCase): +class SynchronousServerTest(unittest.TestCase): ''' This is the unittest for the pymodbus.server.sync module ''' @@ -32,12 +48,289 @@ def tearDown(self): pass #-----------------------------------------------------------------------# - # Test Base Client + # Test Base Request Handler #-----------------------------------------------------------------------# - def testExampleTest(self): + def testBaseHandlerUndefinedMethods(self): + ''' Test the base handler undefined methods''' + handler = SocketServer.BaseRequestHandler(None, None, None) + handler.__class__ = ModbusBaseRequestHandler + self.assertRaises(NotImplementedException, lambda: handler.send(None)) + self.assertRaises(NotImplementedException, lambda: handler.handle()) + + def testBaseHandlerMethods(self): ''' Test the base class for all the clients ''' - self.assertTrue(True) + request = ReadCoilsRequest(1, 1) + address = ('server', 12345) + server = MockServer() + + with patch.object(ModbusBaseRequestHandler, 'handle') as mock_handle: + with patch.object(ModbusBaseRequestHandler, 'send') as mock_send: + mock_handle.return_value = True + mock_send.return_value = True + handler = ModbusBaseRequestHandler(request, address, server) + self.assertEqual(handler.running, True) + self.assertEqual(handler.framer, 'framer') + + handler.execute(request) + self.assertEqual(mock_send.call_count, 1) + + server.context[0x00] = object() + handler.execute(request) + self.assertEqual(mock_send.call_count, 2) + + #-----------------------------------------------------------------------# + # Test Single Request Handler + #-----------------------------------------------------------------------# + def testModbusSingleRequestHandlerSend(self): + handler = SocketServer.BaseRequestHandler(None, None, None) + handler.__class__ = ModbusSingleRequestHandler + handler.framer = Mock() + handler.framer.buildPacket.return_value = "message" + handler.request = Mock() + request = ReadCoilsResponse([1]) + handler.send(request) + self.assertEqual(handler.request.send.call_count, 1) + + request.should_respond = False + handler.send(request) + self.assertEqual(handler.request.send.call_count, 1) + + def testModbusSingleRequestHandlerHandle(self): + handler = SocketServer.BaseRequestHandler(None, None, None) + handler.__class__ = ModbusSingleRequestHandler + handler.framer = Mock() + handler.framer.buildPacket.return_value = "message" + handler.request = Mock() + handler.request.recv.return_value = "\x12\x34" + + # exit if we are not running + handler.running = False + handler.handle() + self.assertEqual(handler.framer.processIncomingPacket.call_count, 0) + + # run forever if we are running + def _callback(a, b): + handler.running = False # stop infinite loop + handler.framer.processIncomingPacket.side_effect = _callback + handler.running = True + handler.handle() + self.assertEqual(handler.framer.processIncomingPacket.call_count, 1) + + # exceptions are simply ignored + def _callback(a, b): + if handler.framer.processIncomingPacket.call_count == 2: + raise Exception("example exception") + else: handler.running = False # stop infinite loop + handler.framer.processIncomingPacket.side_effect = _callback + handler.running = True + handler.handle() + self.assertEqual(handler.framer.processIncomingPacket.call_count, 3) + + #-----------------------------------------------------------------------# + # Test Connected Request Handler + #-----------------------------------------------------------------------# + def testModbusConnectedRequestHandlerSend(self): + handler = SocketServer.BaseRequestHandler(None, None, None) + handler.__class__ = ModbusConnectedRequestHandler + handler.framer = Mock() + handler.framer.buildPacket.return_value = "message" + handler.request = Mock() + request = ReadCoilsResponse([1]) + handler.send(request) + self.assertEqual(handler.request.send.call_count, 1) + + request.should_respond = False + handler.send(request) + self.assertEqual(handler.request.send.call_count, 1) + + def testModbusConnectedRequestHandlerHandle(self): + handler = SocketServer.BaseRequestHandler(None, None, None) + handler.__class__ = ModbusConnectedRequestHandler + handler.framer = Mock() + handler.framer.buildPacket.return_value = "message" + handler.request = Mock() + handler.request.recv.return_value = "\x12\x34" + + # exit if we are not running + handler.running = False + handler.handle() + self.assertEqual(handler.framer.processIncomingPacket.call_count, 0) + + # run forever if we are running + def _callback(a, b): + handler.running = False # stop infinite loop + handler.framer.processIncomingPacket.side_effect = _callback + handler.running = True + handler.handle() + self.assertEqual(handler.framer.processIncomingPacket.call_count, 1) + + # socket errors cause the client to disconnect + handler.framer.processIncomingPacket.side_effect = socket.error() + handler.running = True + handler.handle() + self.assertEqual(handler.framer.processIncomingPacket.call_count, 2) + + # every other exception causes the client to disconnect + handler.framer.processIncomingPacket.side_effect = Exception() + handler.running = True + handler.handle() + self.assertEqual(handler.framer.processIncomingPacket.call_count, 3) + + # receiving no data causes the client to disconnect + handler.request.recv.return_value = None + handler.running = True + handler.handle() + self.assertEqual(handler.framer.processIncomingPacket.call_count, 3) + + #-----------------------------------------------------------------------# + # Test Disconnected Request Handler + #-----------------------------------------------------------------------# + def testModbusDisconnectedRequestHandlerSend(self): + handler = SocketServer.BaseRequestHandler(None, None, None) + handler.__class__ = ModbusDisconnectedRequestHandler + handler.framer = Mock() + handler.framer.buildPacket.return_value = "message" + handler.request = Mock() + request = ReadCoilsResponse([1]) + handler.send(request) + self.assertEqual(handler.request.sendto.call_count, 1) + + request.should_respond = False + handler.send(request) + self.assertEqual(handler.request.sendto.call_count, 1) + + def testModbusDisconnectedRequestHandlerHandle(self): + handler = SocketServer.BaseRequestHandler(None, None, None) + handler.__class__ = ModbusDisconnectedRequestHandler + handler.framer = Mock() + handler.framer.buildPacket.return_value = "message" + handler.request = ("\x12\x34", handler.request) + + # exit if we are not running + handler.running = False + handler.handle() + self.assertEqual(handler.framer.processIncomingPacket.call_count, 0) + + # run forever if we are running + def _callback(a, b): + handler.running = False # stop infinite loop + handler.framer.processIncomingPacket.side_effect = _callback + handler.running = True + handler.handle() + self.assertEqual(handler.framer.processIncomingPacket.call_count, 1) + + # socket errors cause the client to disconnect + handler.request = ("\x12\x34", handler.request) + handler.framer.processIncomingPacket.side_effect = socket.error() + handler.running = True + handler.handle() + self.assertEqual(handler.framer.processIncomingPacket.call_count, 2) + + # every other exception causes the client to disconnect + handler.request = ("\x12\x34", handler.request) + handler.framer.processIncomingPacket.side_effect = Exception() + handler.running = True + handler.handle() + self.assertEqual(handler.framer.processIncomingPacket.call_count, 3) + + # receiving no data causes the client to disconnect + handler.request = (None, handler.request) + handler.running = True + handler.handle() + self.assertEqual(handler.framer.processIncomingPacket.call_count, 3) + + #-----------------------------------------------------------------------# + # Test TCP Server + #-----------------------------------------------------------------------# + def testTcpServerClose(self): + ''' test that the synchronous TCP server closes correctly ''' + with patch.object(socket.socket, 'bind') as mock_socket: + server = ModbusTcpServer(None) + server.threads.append(Mock(**{'running': True})) + server.server_close() + self.assertFalse(server.threads[0].running) + + def testTcpServerProcess(self): + ''' test that the synchronous TCP server processes requests ''' + with patch('SocketServer.ThreadingTCPServer') as mock_server: + server = ModbusTcpServer(None) + server.process_request('request', 'client') + self.assertTrue(mock_server.process_request.called) + + #-----------------------------------------------------------------------# + # Test UDP Server + #-----------------------------------------------------------------------# + def testUdpServerClose(self): + ''' test that the synchronous UDP server closes correctly ''' + with patch.object(socket.socket, 'bind') as mock_socket: + server = ModbusUdpServer(None) + server.threads.append(Mock(**{'running': True})) + server.server_close() + self.assertFalse(server.threads[0].running) + + def testUdpServerProcess(self): + ''' test that the synchronous UDP server processes requests ''' + with patch('SocketServer.ThreadingUDPServer') as mock_server: + server = ModbusUdpServer(None) + request = ('data', 'socket') + server.process_request(request, 'client') + self.assertTrue(mock_server.process_request.called) + + #-----------------------------------------------------------------------# + # Test Serial Server + #-----------------------------------------------------------------------# + def testSerialServerConnect(self): + with patch.object(serial, 'Serial') as mock_serial: + mock_serial.return_value = "socket" + server = ModbusSerialServer(None) + self.assertEqual(server.socket, "socket") + + server._connect() + self.assertEqual(server.socket, "socket") + + with patch.object(serial, 'Serial') as mock_serial: + mock_serial.side_effect = serial.SerialException() + server = ModbusSerialServer(None) + self.assertEqual(server.socket, None) + + def testSerialServerServeForever(self): + ''' test that the synchronous serial server closes correctly ''' + with patch.object(serial, 'Serial') as mock_serial: + with patch('pymodbus.server.sync.ModbusSingleRequestHandler') as mock_handler: + server = ModbusSerialServer(None) + instance = mock_handler.return_value + instance.handle.side_effect = server.server_close + server.serve_forever() + instance.handle.assert_any_call() + + def testSerialServerClose(self): + ''' test that the synchronous serial server closes correctly ''' + with patch.object(serial, 'Serial') as mock_serial: + instance = mock_serial.return_value + server = ModbusSerialServer(None) + server.server_close() + instance.close.assert_any_call() + + #-----------------------------------------------------------------------# + # Test Synchronous Factories + #-----------------------------------------------------------------------# + def testStartTcpServer(self): + ''' Test the tcp server starting factory ''' + with patch.object(ModbusTcpServer, 'serve_forever') as mock_server: + with patch.object(SocketServer.TCPServer, 'server_bind') as mock_binder: + StartTcpServer() + + def testStartUdpServer(self): + ''' Test the udp server starting factory ''' + with patch.object(ModbusUdpServer, 'serve_forever') as mock_server: + with patch.object(SocketServer.UDPServer, 'server_bind') as mock_binder: + StartUdpServer() + + def testStartSerialServer(self): + ''' Test the serial server starting factory ''' + with patch.object(ModbusSerialServer, 'serve_forever') as mock_server: + StartSerialServer() #---------------------------------------------------------------------------# # Main From dd4913ae1efbd24f23b26ffefa9608fcfea4a576 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Fri, 29 Jun 2012 13:48:33 -0500 Subject: [PATCH 077/243] updating documentation --- doc/sphinx/conf.py | 6 +++++- doc/sphinx/examples/index.rst | 1 + doc/sphinx/library/constants.rst | 2 +- doc/sphinx/library/index.rst | 1 + pymodbus/client/sync.py | 4 ++-- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/doc/sphinx/conf.py b/doc/sphinx/conf.py index 9a7d06c26..e223ad565 100644 --- a/doc/sphinx/conf.py +++ b/doc/sphinx/conf.py @@ -87,6 +87,10 @@ #modindex_common_prefix = [] +# -- Options for extensions --------------------------------------------------- +autodoc_default_flags = ['members', 'inherited-members', 'show-inheritance'] +autoclass_content = 'both' + # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with @@ -120,7 +124,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ['static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. diff --git a/doc/sphinx/examples/index.rst b/doc/sphinx/examples/index.rst index 4886d8d66..80e2b7012 100644 --- a/doc/sphinx/examples/index.rst +++ b/doc/sphinx/examples/index.rst @@ -20,6 +20,7 @@ Example Library Code modbus-scraper modbus-simulator message-parser + serial-forwarder synchronous-client synchronous-client-ext synchronous-server diff --git a/doc/sphinx/library/constants.rst b/doc/sphinx/library/constants.rst index e1d764079..a99e207c6 100644 --- a/doc/sphinx/library/constants.rst +++ b/doc/sphinx/library/constants.rst @@ -27,5 +27,5 @@ API Documentation .. autoclass:: DeviceInformation :members: -.. autoclass:: MoreFollows +.. autoclass:: MoreData :members: diff --git a/doc/sphinx/library/index.rst b/doc/sphinx/library/index.rst index 5adf68eba..f21e8e4a7 100644 --- a/doc/sphinx/library/index.rst +++ b/doc/sphinx/library/index.rst @@ -21,6 +21,7 @@ from the sourcecode* interfaces.rst exceptions.rst other-message.rst + mei-message.rst file-message.rst events.rst payload.rst diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index f1a7cb653..158b681f8 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -266,7 +266,7 @@ def __str__(self): # Modbus Serial Client Transport Implementation #---------------------------------------------------------------------------# class ModbusSerialClient(BaseModbusClient): - ''' Implementation of a modbus udp client + ''' Implementation of a modbus serial client ''' def __init__(self, method='ascii', **kwargs): @@ -284,7 +284,7 @@ def __init__(self, method='ascii', **kwargs): :param bytesize: The bytesize of the serial messages :param parity: Which kind of parity to use :param baudrate: The baud rate to use for the serial device - :param timeout: The timeout to use for the serial device + :param timeout: The timeout between serial requests (default 3s) ''' self.method = method self.socket = None From f43765dc72995e301f8a76ad0b84e30425c8b6f8 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 2 Jul 2012 10:51:26 -0500 Subject: [PATCH 078/243] adding another example, adding debug checks --- .../examples/asynchronous-processor.rst | 15 ++ doc/sphinx/examples/index.rst | 1 + examples/common/asynchronous-processor.py | 184 ++++++++++++++++++ pymodbus/server/async.py | 12 +- test/test_server_async.py | 60 +++++- test/test_server_sync.py | 31 ++- 6 files changed, 275 insertions(+), 28 deletions(-) create mode 100644 doc/sphinx/examples/asynchronous-processor.rst create mode 100755 examples/common/asynchronous-processor.py diff --git a/doc/sphinx/examples/asynchronous-processor.rst b/doc/sphinx/examples/asynchronous-processor.rst new file mode 100644 index 000000000..4afd8504e --- /dev/null +++ b/doc/sphinx/examples/asynchronous-processor.rst @@ -0,0 +1,15 @@ +================================================== +Asynchronous Processor Example +================================================== + +Below is a simplified asynchronous client skeleton that was +submitted by a user of the library. It can be used as a guide +for implementing more complex pollers or state machines. + +Feel free to test it against whatever device you currently have +available. If you do not have a device to test with, feel free +to run a pymodbus server instance or start the reference tester in +the tools directory. + +.. literalinclude:: ../../../examples/common/asynchronous-processor.py + diff --git a/doc/sphinx/examples/index.rst b/doc/sphinx/examples/index.rst index 80e2b7012..50698bf5d 100644 --- a/doc/sphinx/examples/index.rst +++ b/doc/sphinx/examples/index.rst @@ -14,6 +14,7 @@ Example Library Code asynchronous-client asynchronous-server + asynchronous-processor custom-message modbus-logging modbus-payload diff --git a/examples/common/asynchronous-processor.py b/examples/common/asynchronous-processor.py new file mode 100755 index 000000000..b5d3f8f48 --- /dev/null +++ b/examples/common/asynchronous-processor.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python +''' +Pymodbus Asynchronous Processor Example +-------------------------------------------------------------------------- + +The following is a full example of a continuous client processor. Feel +free to use it as a skeleton guide in implementing your own. +''' +#---------------------------------------------------------------------------# +# import the neccessary modules +#---------------------------------------------------------------------------# +from twisted.internet import serialport, reactor +from twisted.internet.protocol import ClientFactory +from pymodbus.factory import ClientDecoder +from pymodbus.client.async import ModbusClientProtocol + +#---------------------------------------------------------------------------# +# Choose the framer you want to use +#---------------------------------------------------------------------------# +#from pymodbus.transaction import ModbusBinaryFramer as ModbusFramer +#from pymodbus.transaction import ModbusAsciiFramer as ModbusFramer +#from pymodbus.transaction import ModbusRtuFramer as ModbusFramer +from pymodbus.transaction import ModbusSocketFramer as ModbusFramer + +#---------------------------------------------------------------------------# +# configure the client logging +#---------------------------------------------------------------------------# +import logging +logging.basicConfig() +log = logging.getLogger("pymodbus") +log.setLevel(logging.DEBUG) + +#---------------------------------------------------------------------------# +# state a few constants +#---------------------------------------------------------------------------# +SERIAL_PORT = "/dev/ttyS0" +STATUS_REGS = (1, 2) +STATUS_COILS = (1, 3) +CLIENT_DELAY = 1 + + +#---------------------------------------------------------------------------# +# an example custom protocol +#---------------------------------------------------------------------------# +# Here you can perform your main procesing loop utilizing defereds and timed +# callbacks. +#---------------------------------------------------------------------------# +class ExampleProtocol(ModbusClientProtocol): + + def __init__(self, framer, endpoint): + ''' Initializes our custom protocol + + :param framer: The decoder to use to process messages + :param endpoint: The endpoint to send results to + ''' + ModbusClientProtocol.__init__(self, framer) + self.endpoint = endpoint + log.debug("Beginning the processing loop") + reactor.callLater(CLIENT_DELAY, self.fetch_holding_registers) + + def fetch_holding_registers(self): + ''' Defer fetching holding registers + ''' + log.debug("Starting the next cycle") + d = self.read_holding_registers(*STATUS_REGS) + d.addCallbacks(self.send_holding_registers, self.error_handler) + + def send_holding_registers(self, response): + ''' Write values of holding registers, defer fetching coils + + :param response: The response to process + ''' + self.endpoint.write(response.getRegister(0)) + self.endpoint.write(response.getRegister(1)) + d = self.read_coils(*STATUS_COILS) + d.addCallbacks(self.start_next_cycle, self.error_handler) + + def start_next_cycle(self, response): + ''' Write values of coils, trigger next cycle + + :param response: The response to process + ''' + self.endpoint.write(response.getBit(0)) + self.endpoint.write(response.getBit(1)) + self.endpoint.write(response.getBit(2)) + reactor.callLater(CLIENT_DELAY, self.fetch_holding_registers) + + def error_handler(self, failure): + ''' Handle any twisted errors + + :param failure: The error to handle + ''' + log.error(failure) + + +#---------------------------------------------------------------------------# +# a factory for the example protocol +#---------------------------------------------------------------------------# +# This is used to build client protocol's if you tie into twisted's method +# of processing. It basically produces client instances of the underlying +# protocol:: +# +# Factory(Protocol) -> ProtocolInstance +# +# It also persists data between client instances (think protocol singelton). +#---------------------------------------------------------------------------# +class ExampleFactory(ClientFactory): + + protocol = ExampleProtocol + + def __init__(self, framer, endpoint): + ''' Remember things necessary for building a protocols ''' + self.framer = framer + self.endpoint = endpoint + + def buildProtocol(self, _): + ''' Create a protocol and start the reading cycle ''' + proto = self.protocol(self.framer, self.endpoint) + proto.factory = self + return proto + + +#---------------------------------------------------------------------------# +# a custom client for our device +#---------------------------------------------------------------------------# +# Twisted provides a number of helper methods for creating and starting +# clients: +# - protocol.ClientCreator +# - reactor.connectTCP +# +# How you start your client is really up to you. +#---------------------------------------------------------------------------# +class SerialModbusClient(serialport.SerialPort): + + def __init__(self, factory, *args, **kwargs): + ''' Setup the client and start listening on the serial port + + :param factory: The factory to build clients with + ''' + protocol = factory.buildProtocol(None) + self.decoder = ClientDecoder() + serialport.SerialPort.__init__(self, protocol, *args, **kwargs) + + +#---------------------------------------------------------------------------# +# a custom endpoint for our results +#---------------------------------------------------------------------------# +# An example line reader, this can replace with: +# - the TCP protocol +# - a context recorder +# - a database or file recorder +#---------------------------------------------------------------------------# +class LoggingLineReader(object): + + def write(self, response): + ''' Handle the next modbus response + + :param response: The response to process + ''' + log.info("Read Data: %d" % response) + +#---------------------------------------------------------------------------# +# start running the processor +#---------------------------------------------------------------------------# +# This initializes the client, the framer, the factory, and starts the +# twisted event loop (the reactor). It should be noted that a number of +# things could be chanegd as one sees fit: +# - The ModbusRtuFramer could be replaced with a ModbusAsciiFramer +# - The SerialModbusClient could be replaced with reactor.connectTCP +# - The LineReader endpoint could be replaced with a database store +#---------------------------------------------------------------------------# +def main(): + log.debug("Initializing the client") + framer = ModbusFramer(ClientDecoder()) + reader = LoggingLineReader() + factory = ExampleFactory(framer, reader) + SerialModbusClient(factory, SERIAL_PORT, reactor) + #factory = reactor.connectTCP("localhost", 502, factory) + log.debug("Starting the client") + reactor.run() + +if __name__ == "__main__": + main() + diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index 12a8b627d..38f139b9a 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -52,7 +52,8 @@ def dataReceived(self, data): :param data: The data sent by the client ''' - _logger.debug(" ".join([hex(ord(x)) for x in data])) + if _logger.isEnabledFor(logging.DEBUG): + _logger.debug(" ".join([hex(ord(x)) for x in data])) if not self.factory.control.ListenOnly: self.framer.processIncomingPacket(data, self._execute) @@ -80,7 +81,8 @@ def _send(self, message): if message.should_respond: self.factory.control.Counter.BusMessage += 1 pdu = self.framer.buildPacket(message) - _logger.debug('send: %s' % b2a_hex(pdu)) + if _logger.isEnabledFor(logging.DEBUG): + _logger.debug('send: %s' % b2a_hex(pdu)) return self.transport.write(pdu) @@ -147,7 +149,8 @@ def datagramReceived(self, data, addr): :param data: The data sent by the client ''' _logger.debug("Client Connected [%s:%s]" % addr) - _logger.debug(" ".join([hex(ord(x)) for x in data])) + if _logger.isEnabledFor(logging.DEBUG): + _logger.debug(" ".join([hex(ord(x)) for x in data])) if not self.control.ListenOnly: continuation = lambda request: self._execute(request, addr) self.framer.processIncomingPacket(data, continuation) @@ -176,7 +179,8 @@ def _send(self, message, addr): ''' self.control.Counter.BusMessage += 1 pdu = self.framer.buildPacket(message) - _logger.debug('send: %s' % b2a_hex(pdu)) + if _logger.isEnabledFor(logging.DEBUG): + _logger.debug('send: %s' % b2a_hex(pdu)) return self.transport.write(pdu, addr) diff --git a/test/test_server_async.py b/test/test_server_async.py index a560da5f7..e7ec9c731 100644 --- a/test/test_server_async.py +++ b/test/test_server_async.py @@ -1,11 +1,13 @@ #!/usr/bin/env python import unittest -from twisted.test import test_protocols +from mock import patch, Mock +from pymodbus.device import ModbusDeviceIdentification from pymodbus.server.async import ModbusTcpProtocol, ModbusUdpProtocol from pymodbus.server.async import ModbusServerFactory from pymodbus.server.async import StartTcpServer, StartUdpServer, StartSerialServer from pymodbus.exceptions import ConnectionException, NotImplementedException from pymodbus.exceptions import ParameterException +from pymodbus.bit_read_message import ReadCoilsRequest, ReadCoilsResponse #---------------------------------------------------------------------------# # Fixture @@ -23,19 +25,67 @@ def setUp(self): ''' Initializes the test environment ''' - pass + values = dict((i, '') for i in range(10)) + identity = ModbusDeviceIdentification(info=values) def tearDown(self): ''' Cleans up the test environment ''' pass #-----------------------------------------------------------------------# - # Test Base Client + # Test Modbus Server Factory #-----------------------------------------------------------------------# - def testExampleTest(self): + def testModbusServerFactory(self): ''' Test the base class for all the clients ''' - self.assertTrue(True) + factory = ModbusServerFactory(store=None) + self.assertEqual(factory.control.Identity.VendorName, '') + + identity = ModbusDeviceIdentification(info={0x00: 'VendorName'}) + factory = ModbusServerFactory(store=None, identity=identity) + self.assertEqual(factory.control.Identity.VendorName, 'VendorName') + + #-----------------------------------------------------------------------# + # Test Modbus TCP Server + #-----------------------------------------------------------------------# + def testTCPServerDisconnect(self): + protocol = ModbusTcpProtocol() + protocol.connectionLost('because of an error') + + #-----------------------------------------------------------------------# + # Test Modbus UDP Server + #-----------------------------------------------------------------------# + def testUdpServerInitialize(self): + protocol = ModbusUdpProtocol(store=None) + self.assertEqual(protocol.control.Identity.VendorName, '') + + identity = ModbusDeviceIdentification(info={0x00: 'VendorName'}) + protocol = ModbusUdpProtocol(store=None, identity=identity) + self.assertEqual(protocol.control.Identity.VendorName, 'VendorName') + + #-----------------------------------------------------------------------# + # Test Modbus Server Startups + #-----------------------------------------------------------------------# + + def testTcpServerStartup(self): + ''' Test that the modbus tcp async server starts correctly ''' + with patch('twisted.internet.reactor') as mock_reactor: + StartTcpServer(context=None) + self.assertEqual(mock_reactor.listenTCP.call_count, 2) + self.assertEqual(mock_reactor.run.call_count, 1) + + def testUdpServerStartup(self): + ''' Test that the modbus udp async server starts correctly ''' + with patch('twisted.internet.reactor') as mock_reactor: + StartUdpServer(context=None) + self.assertEqual(mock_reactor.listenUDP.call_count, 1) + self.assertEqual(mock_reactor.run.call_count, 1) + + def testSerialServerStartup(self): + ''' Test that the modbus serial async server starts correctly ''' + with patch('twisted.internet.reactor') as mock_reactor: + StartSerialServer(context=None) + self.assertEqual(mock_reactor.run.call_count, 1) #---------------------------------------------------------------------------# # Main diff --git a/test/test_server_sync.py b/test/test_server_sync.py index 0e2c34723..87cfde499 100644 --- a/test/test_server_sync.py +++ b/test/test_server_sync.py @@ -5,6 +5,7 @@ import serial import socket +from pymodbus.device import ModbusDeviceIdentification from pymodbus.server.sync import ModbusBaseRequestHandler from pymodbus.server.sync import ModbusSingleRequestHandler from pymodbus.server.sync import ModbusConnectedRequestHandler @@ -33,20 +34,6 @@ class SynchronousServerTest(unittest.TestCase): This is the unittest for the pymodbus.server.sync module ''' - #-----------------------------------------------------------------------# - # Setup/TearDown - #-----------------------------------------------------------------------# - - def setUp(self): - ''' - Initializes the test environment - ''' - pass - - def tearDown(self): - ''' Cleans up the test environment ''' - pass - #-----------------------------------------------------------------------# # Test Base Request Handler #-----------------------------------------------------------------------# @@ -246,10 +233,12 @@ def _callback(a, b): def testTcpServerClose(self): ''' test that the synchronous TCP server closes correctly ''' with patch.object(socket.socket, 'bind') as mock_socket: - server = ModbusTcpServer(None) + identity = ModbusDeviceIdentification(info={0x00: 'VendorName'}) + server = ModbusTcpServer(context=None, identity=identity) server.threads.append(Mock(**{'running': True})) server.server_close() - self.assertFalse(server.threads[0].running) + self.assertEqual(server.control.Identity.VendorName, 'VendorName') + self.assertFalse(server.threads[0].running) def testTcpServerProcess(self): ''' test that the synchronous TCP server processes requests ''' @@ -264,10 +253,12 @@ def testTcpServerProcess(self): def testUdpServerClose(self): ''' test that the synchronous UDP server closes correctly ''' with patch.object(socket.socket, 'bind') as mock_socket: - server = ModbusUdpServer(None) + identity = ModbusDeviceIdentification(info={0x00: 'VendorName'}) + server = ModbusUdpServer(context=None, identity=identity) server.threads.append(Mock(**{'running': True})) server.server_close() - self.assertFalse(server.threads[0].running) + self.assertEqual(server.control.Identity.VendorName, 'VendorName') + self.assertFalse(server.threads[0].running) def testUdpServerProcess(self): ''' test that the synchronous UDP server processes requests ''' @@ -283,8 +274,10 @@ def testUdpServerProcess(self): def testSerialServerConnect(self): with patch.object(serial, 'Serial') as mock_serial: mock_serial.return_value = "socket" - server = ModbusSerialServer(None) + identity = ModbusDeviceIdentification(info={0x00: 'VendorName'}) + server = ModbusSerialServer(context=None, identity=identity) self.assertEqual(server.socket, "socket") + self.assertEqual(server.control.Identity.VendorName, 'VendorName') server._connect() self.assertEqual(server.socket, "socket") From e6e20a4158e2179df902eabb7a643aefc5a3a2e9 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 2 Jul 2012 14:23:43 -0500 Subject: [PATCH 079/243] Fixing the serial implementation of everything * updating lots of reference documentation * fixing the fifo semantics of serial clients * using transaction manager in async clients * fixing references --- examples/common/README.rst | 116 ++++++++++++++++++ examples/common/asynchronous-server.py | 4 +- examples/common/synchronous-client.py | 9 +- examples/common/synchronous-server.py | 4 +- examples/tools/nullmodem/generic/Makefile | 5 + .../tools/nullmodem/generic/virtual-serial.c | 62 ++++++++++ .../tools/nullmodem/linux/module/tty0tty.c | 2 +- .../windows/{ReadMe.txt => README.txt} | 0 examples/tools/reference/README | 2 +- examples/tools/reference/virtual-serial.c | 27 ---- pymodbus/client/async.py | 20 +-- pymodbus/client/sync.py | 7 +- pymodbus/transaction.py | 25 +++- test/test_client_async.py | 37 ++---- test/test_client_sync.py | 21 +--- 15 files changed, 245 insertions(+), 96 deletions(-) create mode 100644 examples/common/README.rst create mode 100644 examples/tools/nullmodem/generic/Makefile create mode 100644 examples/tools/nullmodem/generic/virtual-serial.c rename examples/tools/nullmodem/windows/{ReadMe.txt => README.txt} (100%) delete mode 100644 examples/tools/reference/virtual-serial.c diff --git a/examples/common/README.rst b/examples/common/README.rst new file mode 100644 index 000000000..e74667dd3 --- /dev/null +++ b/examples/common/README.rst @@ -0,0 +1,116 @@ +============================================================ +Modbus Implementations +============================================================ + +There are a few reference implementations that you can use +to test modbus serial:: + +------------------------------------------------------------ +pymodbus +------------------------------------------------------------ + +You can use pymodbus as a testing server by simply modifying +one of the run scripts supplied here. There is an +asynchronous version and a synchronous version (that really +differ in how mnay dependencies you are willing to have). +Regardless of which one you choose, they can be started +quite easily:: + + ./asynchronous-server.py + ./synchronous-server.py + +Currently, each version has implementations of the following: + +- modbus tcp +- modbus udp +- modbus udp binary +- modbus ascii serial +- modbus ascii rtu + +------------------------------------------------------------ +Modbus Driver +------------------------------------------------------------ + +Included are reference implementations of a modbus client +and server using the modbus driver library (as well as +the relevant source code). Both programs have a wealth of +options and can be used to test either side of your +application:: + + tools/reference/diagslave -h # (server) + tools/reference/modpoll -h # (client) + +------------------------------------------------------------ +jamod +------------------------------------------------------------ + +Jamod is a complete modbus implementation for the java jvm. +Included are a few simple reference servers using the +library, however, a great deal more can be produced using +it. I have not tested it, however, it may even be possible +to use this library in conjunction with jython to interop +between your python code and this library: + +* http://jamod.sourceforge.net/ + +------------------------------------------------------------ +nmodbus +------------------------------------------------------------ + +Although there is not any code included in this package, +nmodbus is a complete implementation of the modbus protocol +for the .net clr. The site has a number of examples that can +be tuned for your testing needs: + +* http://code.google.com/p/nmodbus/ + +============================================================ +Serial Loopback Testing +============================================================ + +In order to test the serial implementations, one needs to +create a loopback connection (virtual serial port). This can +be done in a number of ways. + +------------------------------------------------------------ +Linux +------------------------------------------------------------ + +For linux, there are three ways that are included with this +distribution. + +One is to use the socat utility. The following will get one +going quickly:: + + sudo apt-get install socat + sudo socat PTY,link=/dev/pts/13, PTY,link=/dev/pts/14 + # connect the master to /dev/pts/13 + # connect the client to /dev/pts/14 + +Next, you can include the loopback kernel driver included in +the tools/nullmodem/linux directory:: + + sudo ./run + +------------------------------------------------------------ +Windows +------------------------------------------------------------ + +For Windows, simply use the com2com application that is in +the directory tools/nullmodem/windows. Instructions are +included in the Readme.txt. + +------------------------------------------------------------ +Generic +------------------------------------------------------------ + +For most unix based systems, there is a simple virtual serial +forwarding application in the tools/nullmodem/ directory:: + + make run + # connect the master to the master output + # connect the client to the client output + +Or for a tried and true method, simply connect a null modem +cable between two of your serial ports and then simply reference +those. diff --git a/examples/common/asynchronous-server.py b/examples/common/asynchronous-server.py index 390106bd3..4163e7547 100755 --- a/examples/common/asynchronous-server.py +++ b/examples/common/asynchronous-server.py @@ -16,6 +16,7 @@ from pymodbus.datastore import ModbusSequentialDataBlock from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer #---------------------------------------------------------------------------# # configure the service logging @@ -83,4 +84,5 @@ #---------------------------------------------------------------------------# StartTcpServer(context) #StartUdpServer(context) -#StartSerialServer(context, port='/dev/pts/13') +#StartSerialServer(context, port='/dev/pts/3', framer=ModbusRtuFramer) +#StartSerialServer(context, port='/dev/pts/3', framer=ModbusAsciiFramer) diff --git a/examples/common/synchronous-client.py b/examples/common/synchronous-client.py index 32e6b3e93..fc0d479e8 100755 --- a/examples/common/synchronous-client.py +++ b/examples/common/synchronous-client.py @@ -16,9 +16,9 @@ #---------------------------------------------------------------------------# # import the various server implementations #---------------------------------------------------------------------------# -from pymodbus.client.sync import ModbusTcpClient as ModbusClient +#from pymodbus.client.sync import ModbusTcpClient as ModbusClient #from pymodbus.client.sync import ModbusUdpClient as ModbusClient -#from pymodbus.client.sync import ModbusSerialClient as ModbusClient +from pymodbus.client.sync import ModbusSerialClient as ModbusClient #---------------------------------------------------------------------------# # configure the client logging @@ -38,7 +38,9 @@ # It should be noted that you can supply an ipv4 or an ipv6 host address for # both the UDP and TCP clients. #---------------------------------------------------------------------------# -client = ModbusClient('127.0.0.1') +client = ModbusClient('localhost') +#client = ModbusClient(method='ascii', port='/dev/pts/2', timeout=1) +#client = ModbusClient(method='rtu', port='/dev/pts/2', timeout=1) client.connect() #---------------------------------------------------------------------------# @@ -53,6 +55,7 @@ # Keep both of these cases in mind when testing as the following will # _only_ pass with the supplied async modbus server (script supplied). #---------------------------------------------------------------------------# +#import pdb;pdb.set_trace() rq = client.write_coil(1, True) rr = client.read_coils(1,1) assert(rq.function_code < 0x80) # test that we are not an error diff --git a/examples/common/synchronous-server.py b/examples/common/synchronous-server.py index 0bc59b290..404afb189 100755 --- a/examples/common/synchronous-server.py +++ b/examples/common/synchronous-server.py @@ -82,6 +82,6 @@ #---------------------------------------------------------------------------# # run the server you want #---------------------------------------------------------------------------# -StartTcpServer(context) +#StartTcpServer(context) #StartUdpServer(context) -#StartSerialServer(context, port='/dev/pts/13', timeout=1) +StartSerialServer(context, port='/dev/pts/3', timeout=1) diff --git a/examples/tools/nullmodem/generic/Makefile b/examples/tools/nullmodem/generic/Makefile new file mode 100644 index 000000000..f98898c90 --- /dev/null +++ b/examples/tools/nullmodem/generic/Makefile @@ -0,0 +1,5 @@ +all:virtual-serial +clean: + rm virtual-serial > /dev/null +run: virtual-serial + ./virtual-serial diff --git a/examples/tools/nullmodem/generic/virtual-serial.c b/examples/tools/nullmodem/generic/virtual-serial.c new file mode 100644 index 000000000..660f84459 --- /dev/null +++ b/examples/tools/nullmodem/generic/virtual-serial.c @@ -0,0 +1,62 @@ +#include +#include +#include + +// +// constants +// +#define BUFFER_SIZE 256 + +int setnonblock(int sock) { + int flags = fcntl(sock, F_GETFL, 0); + if (flags == -1) { + return -1; + } + return fcntl(sock, F_SETFL, flags | O_NONBLOCK); +} + +// +// main virtual serial runner +// +int main(int argc, char *argv[]) +{ + char *buffer = calloc(BUFFER_SIZE, sizeof(char)); + + int size = 0; + int ptcl = open("/dev/ptmx", O_RDWR | O_NOCTTY); + if (ptcl < 0) { + perror("open /dev/ptmx"); + return -1; + } + grantpt(ptcl); + unlockpt(ptcl); + fprintf(stderr, "client device-> %s\n", (char *)ptsname(ptcl)); + + int ptma = open("/dev/ptmx", O_RDWR | O_NOCTTY); + if (ptma < 0) { + perror("open /dev/ptmx"); + return -1; + } + grantpt(ptma); + unlockpt(ptma); + fprintf(stderr, "master device-> %s\n", (char *)ptsname(ptma)); + + while (1) { + size = read(ptcl, buffer, BUFFER_SIZE); + if (size > 0) { + write(ptma, buffer, size); + fprintf(stderr, "client-> %s", buffer); + } + + size = read(ptma, buffer, BUFFER_SIZE); + if (size > 0) { + write(ptcl, buffer, size); + fprintf(stderr, "master-> %s", buffer); + } + } + free(buffer); + close(ptcl); + close(ptma); + + return 0; +} diff --git a/examples/tools/nullmodem/linux/module/tty0tty.c b/examples/tools/nullmodem/linux/module/tty0tty.c index 162862145..15bdf5e25 100644 --- a/examples/tools/nullmodem/linux/module/tty0tty.c +++ b/examples/tools/nullmodem/linux/module/tty0tty.c @@ -103,7 +103,7 @@ static int tty0tty_open(struct tty_struct *tty, struct file *file) if (!tty0tty) return -ENOMEM; - init_MUTEX(&tty0tty->sem); + sema_init(&tty0tty->sem, 1); tty0tty->open_count = 0; tty0tty_table[index] = tty0tty; diff --git a/examples/tools/nullmodem/windows/ReadMe.txt b/examples/tools/nullmodem/windows/README.txt similarity index 100% rename from examples/tools/nullmodem/windows/ReadMe.txt rename to examples/tools/nullmodem/windows/README.txt diff --git a/examples/tools/reference/README b/examples/tools/reference/README index 7227ebc9b..6b08070e7 100644 --- a/examples/tools/reference/README +++ b/examples/tools/reference/README @@ -15,7 +15,7 @@ Reference Server and Client ------------------------------------------------------------ The two reference implementations were provided by the modbus -drive team (http://www.modbusdriver.com/modpoll.html). Attached +driver team (http://www.modbusdriver.com/modpoll.html). Attached is the license that the binaries are released under. .. note:: diff --git a/examples/tools/reference/virtual-serial.c b/examples/tools/reference/virtual-serial.c deleted file mode 100644 index e8a8068fe..000000000 --- a/examples/tools/reference/virtual-serial.c +++ /dev/null @@ -1,27 +0,0 @@ -#include -#include -#include - -int main(int argc, char *argv[]) -{ - char *buffer = calloc(256, sizeof(char)); - - int pt = open("/dev/ptmx", O_RDWR | O_NOCTTY); - if (pt < 0) { - perror("open /dev/ptmx"); - return -1; - } - grantpt(pt); - unlockpt(pt); - fprintf(stderr, "Slave Device: %s\n", (char *)ptsname(pt)); - - while(1) { - int size = read(pt, buffer, 256); - if (size > 0) { - fprintf(stderr, "%s", buffer); - } - } - free(buffer); - - return 0; -} diff --git a/pymodbus/client/async.py b/pymodbus/client/async.py index 2ed5b3fca..10d1695d3 100644 --- a/pymodbus/client/async.py +++ b/pymodbus/client/async.py @@ -66,7 +66,8 @@ def __init__(self, framer=None): :param framer: The framer to use for the protocol ''' self.framer = framer or ModbusSocketFramer(ClientDecoder()) - self._requests = {} + serial = not isinstance(framer, ModbusSocketFramer) + self.transaction = ModbusTransactionManager(self, serial) self._connected = False def connectionMade(self): @@ -82,8 +83,8 @@ def connectionLost(self, reason): ''' _logger.debug("Client disconnected from modbus server: %s" % reason) self._connected = False - for key in self._requests.keys(): - self._requests.pop(key).errback(Failure( + for tid in self.transaction: + self.transaction.getTransaction(tid).errback(Failure( ConnectionException('Connection lost during request'))) def dataReceived(self, data): @@ -109,7 +110,7 @@ def _handleResponse(self, reply): ''' if reply is not None: tid = reply.transaction_id - handler = self._requests.pop(tid, None) + handler = self.transaction.getTransaction(tid) if handler: handler.callback(reply) else: _logger.debug("Unrequested message: " + str(reply)) @@ -126,7 +127,7 @@ def _buildResponse(self, tid): ConnectionException('Client is not connected'))) d = defer.Deferred() - self._requests[tid] = d # TODO add request here as well + self.transaction.addTransaction(d, tid) return d #----------------------------------------------------------------------# @@ -153,7 +154,8 @@ def __init__(self, framer=None): :param framer: The framer to use for the protocol ''' self.framer = framer or ModbusSocketFramer(ClientDecoder()) - self._requests = {} + serial = not isinstance(framer, ModbusSocketFramer) + self.transaction = ModbusTransactionManager(self, serial) def datagramReceived(self, data, (host, port)): ''' Get response, check for valid message, decode result @@ -167,7 +169,7 @@ def execute(self, request): ''' Starts the producer to send the next request to consumer.write(Frame(request)) ''' - request.transaction_id = _manager.getNextTID() + request.transaction_id = self.transaction.getNextTID() packet = self.framer.buildPacket(request) self.transport.write(packet) return self._buildResponse(request.transaction_id) @@ -179,7 +181,7 @@ def _handleResponse(self, reply): ''' if reply is not None: tid = reply.transaction_id - handler = self._requests.pop(tid, None) + handler = self.transaction.getTransaction(tid) if handler: handler.callback(reply) else: _logger.debug("Unrequested message: " + str(reply)) @@ -192,7 +194,7 @@ def _buildResponse(self, tid): :returns: A defer linked to the latest request ''' d = defer.Deferred() - self._requests[tid] = d # TODO add request here as well + self.transaction.addTransaction(d, tid) return d diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 158b681f8..90e17eee0 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -33,8 +33,9 @@ def __init__(self, framer): :param framer: The modbus framer implementation to use ''' + serial = not isinstance(framer, ModbusSocketFramer) self.framer = framer - self.transaction = ModbusTransactionManager(self) + self.transaction = ModbusTransactionManager(self, serial) #-----------------------------------------------------------------------# # Client interface @@ -77,9 +78,7 @@ def execute(self, request=None): ''' if not self.connect(): raise ConnectionException("Failed to connect[%s]" % (self.__str__())) - if self.transaction: - return self.transaction.execute(request) - raise ConnectionException("Client Not Connected") + return self.transaction.execute(request) #-----------------------------------------------------------------------# # The magic methods diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index c3aaa3861..d68424506 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -40,12 +40,21 @@ class ModbusTransactionManager(object): __tid = Defaults.TransactionId __transactions = {} - def __init__(self, client=None): + def __init__(self, client=None, fifo=False): ''' Initializes an instance of the ModbusTransactionManager :param client: The client socket wrapper + :param fifo: Should this just return results in FIFO order ''' self.client = client + self.fifo = fifo + + def __iter__(self): + ''' Iterater over the current managed transactions + + :returns: An iterator of the managed transactions + ''' + return iter(self.__transactions.keys()) def execute(self, request): ''' Starts the producer to send the next request to @@ -71,15 +80,18 @@ def execute(self, request): retries -= 1 return self.getTransaction(request.transaction_id) - def addTransaction(self, request): + def addTransaction(self, request, tid=None): ''' Adds a transaction to the handler This holds the requets in case it needs to be resent. After being sent, the request is removed. :param request: The request to hold on to + :param tid: The transaction id to attach this request with ''' - tid = request.transaction_id + if tid == None: + tid = request.transaction_id + _logger.debug("Adding transaction %d" % tid) ModbusTransactionManager.__transactions[tid] = request def getTransaction(self, tid): @@ -89,6 +101,10 @@ def getTransaction(self, tid): :param tid: The transaction to retrieve ''' + if self.fifo: + if len(ModbusTransactionManager.__transactions): + return ModbusTransactionManager.__transactions.popitem()[1] + else: return None return ModbusTransactionManager.__transactions.pop(tid, None) def delTransaction(self, tid): @@ -96,6 +112,9 @@ def delTransaction(self, tid): :param tid: The transaction to remove ''' + if self.fifo: + if len(ModbusTransactionManager.__transactions): + ModbusTransactionManager.__transactions.popitem() ModbusTransactionManager.__transactions.pop(tid, None) def getNextTID(self): diff --git a/test/test_client_async.py b/test/test_client_async.py index e2c22c3d8..710b9efeb 100644 --- a/test/test_client_async.py +++ b/test/test_client_async.py @@ -16,20 +16,6 @@ class AsynchronousClientTest(unittest.TestCase): This is the unittest for the pymodbus.client.async module ''' - #-----------------------------------------------------------------------# - # Setup/TearDown - #-----------------------------------------------------------------------# - - def setUp(self): - ''' - Initializes the test environment - ''' - pass - - def tearDown(self): - ''' Cleans up the test environment ''' - pass - #-----------------------------------------------------------------------# # Test Client Protocol #-----------------------------------------------------------------------# @@ -37,13 +23,13 @@ def tearDown(self): def testClientProtocolInit(self): ''' Test the client protocol initialize ''' protocol = ModbusClientProtocol() - self.assertEqual(0, len(protocol._requests)) + self.assertEqual(0, len(list(protocol.transaction))) self.assertFalse(protocol._connected) self.assertTrue(isinstance(protocol.framer, ModbusSocketFramer)) framer = object() protocol = ModbusClientProtocol(framer=framer) - self.assertEqual(0, len(protocol._requests)) + self.assertEqual(0, len(list(protocol.transaction))) self.assertFalse(protocol._connected) self.assertTrue(framer is protocol.framer) @@ -90,7 +76,8 @@ def testClientProtocolExecute(self): request = ReadCoilsRequest(1, 1) d = protocol.execute(request) - self.assertEqual(d, protocol._requests[request.transaction_id]) + tid = request.transaction_id + self.assertEqual(d, protocol.transaction.getTransaction(tid)) def testClientProtocolHandleResponse(self): ''' Test the client protocol handles responses ''' @@ -113,17 +100,17 @@ def testClientProtocolHandleResponse(self): def testClientProtocolBuildResponse(self): ''' Test the udp client protocol builds responses ''' protocol = ModbusClientProtocol() - self.assertEqual(0, len(protocol._requests)) + self.assertEqual(0, len(list(protocol.transaction))) def handle_failure(failure): self.assertTrue(isinstance(failure.value, ConnectionException)) d = protocol._buildResponse(0x00) d.addErrback(handle_failure) - self.assertEqual(0, len(protocol._requests)) + self.assertEqual(0, len(list(protocol.transaction))) protocol._connected = True d = protocol._buildResponse(0x00) - self.assertEqual(1, len(protocol._requests)) + self.assertEqual(1, len(list(protocol.transaction))) #-----------------------------------------------------------------------# # Test Udp Client Protocol @@ -132,12 +119,11 @@ def handle_failure(failure): def testUdpClientProtocolInit(self): ''' Test the udp client protocol initialize ''' protocol = ModbusUdpClientProtocol() - self.assertEqual(0, len(protocol._requests)) + self.assertEqual(0, len(list(protocol.transaction))) self.assertTrue(isinstance(protocol.framer, ModbusSocketFramer)) framer = object() protocol = ModbusClientProtocol(framer=framer) - self.assertEqual(0, len(protocol._requests)) self.assertTrue(framer is protocol.framer) def testUdpClientProtocolDataReceived(self): @@ -162,7 +148,8 @@ def testUdpClientProtocolExecute(self): request = ReadCoilsRequest(1, 1) d = protocol.execute(request) - self.assertEqual(d, protocol._requests[request.transaction_id]) + tid = request.transaction_id + self.assertEqual(d, protocol.transaction.getTransaction(tid)) def testUdpClientProtocolHandleResponse(self): ''' Test the udp client protocol handles responses ''' @@ -184,10 +171,10 @@ def testUdpClientProtocolHandleResponse(self): def testUdpClientProtocolBuildResponse(self): ''' Test the udp client protocol builds responses ''' protocol = ModbusUdpClientProtocol() - self.assertEqual(0, len(protocol._requests)) + self.assertEqual(0, len(list(protocol.transaction))) d = protocol._buildResponse(0x00) - self.assertEqual(1, len(protocol._requests)) + self.assertEqual(1, len(list(protocol.transaction))) #-----------------------------------------------------------------------# # Test Client Factories diff --git a/test/test_client_sync.py b/test/test_client_sync.py index a9b96f213..e03ecaf98 100644 --- a/test/test_client_sync.py +++ b/test/test_client_sync.py @@ -31,20 +31,6 @@ class SynchronousClientTest(unittest.TestCase): This is the unittest for the pymodbus.client.sync module ''' - #-----------------------------------------------------------------------# - # Setup/TearDown - #-----------------------------------------------------------------------# - - def setUp(self): - ''' - Initializes the test environment - ''' - pass - - def tearDown(self): - ''' Cleans up the test environment ''' - pass - #-----------------------------------------------------------------------# # Test Base Client #-----------------------------------------------------------------------# @@ -66,14 +52,9 @@ def testBaseModbusClient(self): # a successful execute client.connect = lambda: True client.transaction = Mock(**{'execute.return_value': True}) + self.assertEqual(client, client.__enter__()) self.assertTrue(client.execute()) - # a successful connect, no transaction - client.connect = lambda: True - client.transaction = None - self.assertEqual(client.__enter__(), client) - self.assertRaises(ConnectionException, lambda: client.execute()) - # a unsuccessful connect client.connect = lambda: False self.assertRaises(ConnectionException, lambda: client.__enter__()) From c584ebffe8ce87f429304642059aec3fe83c2f8d Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Thu, 12 Jul 2012 15:25:48 -0700 Subject: [PATCH 080/243] fixing incorrect rtu lenght calculation --- examples/common/message-parser.py | 9 +++++++-- examples/common/messages | 18 ++++++++++-------- pymodbus/mei_message.py | 15 ++++++++++++--- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/examples/common/message-parser.py b/examples/common/message-parser.py index 73441aeda..20e879306 100755 --- a/examples/common/message-parser.py +++ b/examples/common/message-parser.py @@ -79,7 +79,12 @@ def report(self, message): ''' print "%-15s = %s" % ('name', message.__class__.__name__) for k,v in message.__dict__.items(): - if isinstance(v, collections.Iterable): + if isinstance(v, dict): + print "%-15s =" % k + for kk,vv in v.items(): + print " %-12s => %s" % (kk, vv) + + elif isinstance(v, collections.Iterable): print "%-15s =" % k value = str([int(x) for x in v]) for line in textwrap.wrap(value, 60): @@ -162,7 +167,7 @@ def main(): framer = lookup = { 'tcp': ModbusSocketFramer, - 'rtc': ModbusRtuFramer, + 'rtu': ModbusRtuFramer, 'binary': ModbusBinaryFramer, 'ascii': ModbusAsciiFramer, }.get(option.parser, ModbusSocketFramer) diff --git a/examples/common/messages b/examples/common/messages index e7fea8b39..ba9c6f933 100644 --- a/examples/common/messages +++ b/examples/common/messages @@ -1,14 +1,16 @@ # ------------------------------------------------------------ # Modbus TCP Messages # ------------------------------------------------------------ -000112340006ff0101020004 -000112340006ff0201020004 -000112340006ff0302020002 -000112340006ff0402020002 -000112340006ff0500acff00 -000112340006ff0600010003 -000112340006ff076d -000112340006ff0800010000 +#000112340006ff0101020004 +#000112340006ff0201020004 +#000112340006ff0302020002 +#000112340006ff0402020002 +#000112340006ff0500acff00 +#000112340006ff0600010003 +#000112340006ff076d +#000112340006ff0800010000 +# # ------------------------------------------------------------ # Modbus RTU Messages # ------------------------------------------------------------ +042B0E01810001010006666F6F626172D73B diff --git a/pymodbus/mei_message.py b/pymodbus/mei_message.py index 5ffef4393..fd0e02cb7 100644 --- a/pymodbus/mei_message.py +++ b/pymodbus/mei_message.py @@ -19,6 +19,13 @@ #---------------------------------------------------------------------------# class ReadDeviceInformationRequest(ModbusRequest): ''' + This function code allows reading the identification and additional + information relative to the physical and functional description of a + remote device, only. + + The Read Device Identification interface is modeled as an address space + composed of a set of addressable data elements. The data elements are + called objects and an object Id identifies them. ''' function_code = 0x2b sub_function_code = 0x0e @@ -88,12 +95,14 @@ def calculateRtuFrameSize(cls, buffer): :param buffer: A buffer containing the data that have been received. :returns: The number of bytes in the response. ''' - size = 6 # skip the header information + size = 8 # skip the header information + count = struct.unpack('>B', buffer[7])[0] - while size < len(buffer): + while count > 0: _, object_length = struct.unpack('>BB', buffer[size:size+2]) size += object_length + 2 - return size + count -= 1 + return size + 2 def __init__(self, read_code=None, information=None): ''' Initializes a new instance From 8f2c834c055f0066a3d6749d277beb2d36c39af8 Mon Sep 17 00:00:00 2001 From: Perry Kundert Date: Fri, 13 Jul 2012 08:16:09 -0600 Subject: [PATCH 081/243] Add optional server_address to Sync and Async TCP/UDP Server API --- pymodbus/server/async.py | 14 ++++++++------ pymodbus/server/sync.py | 26 ++++++++++++++++---------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index 38f139b9a..c0ce26f61 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -187,7 +187,8 @@ def _send(self, message, addr): #---------------------------------------------------------------------------# # Starting Factories #---------------------------------------------------------------------------# -def StartTcpServer(context, identity=None): +def StartTcpServer(context, identity=None, + server_address=("", Defaults.Port)): ''' Helper method to start the Modbus Async TCP server :param context: The server data context @@ -195,15 +196,16 @@ def StartTcpServer(context, identity=None): ''' from twisted.internet import reactor - _logger.info("Starting Modbus TCP Server on %s" % Defaults.Port) + _logger.info("Starting Modbus TCP Server on %s:%s" % server_address) framer = ModbusSocketFramer factory = ModbusServerFactory(context, framer, identity) InstallManagementConsole({'factory': factory}) - reactor.listenTCP(Defaults.Port, factory) + reactor.listenTCP(server_address[1], factory, interface=server_address[0]) reactor.run() -def StartUdpServer(context, identity=None): +def StartUdpServer(context, identity=None, + server_address=("", Defaults.Port)): ''' Helper method to start the Modbus Async Udp server :param context: The server data context @@ -211,10 +213,10 @@ def StartUdpServer(context, identity=None): ''' from twisted.internet import reactor - _logger.info("Starting Modbus UDP Server on %s" % Defaults.Port) + _logger.info("Starting Modbus UDP Server on %s:%s" % server_address) framer = ModbusSocketFramer server = ModbusUdpProtocol(context, framer, identity) - reactor.listenUDP(Defaults.Port, server) + reactor.listenUDP(server_address[1], server, interface=server_address[0]) reactor.run() diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index 9e03df231..6a15dd6e7 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -201,7 +201,8 @@ class ModbusTcpServer(SocketServer.ThreadingTCPServer): server context instance. ''' - def __init__(self, context, framer=None, identity=None): + def __init__(self, context, framer=None, identity=None, + server_address=("", Defaults.Port)): ''' Overloaded initializer for the socket server If the identify structure is not passed in, the ModbusControlBlock @@ -210,7 +211,7 @@ def __init__(self, context, framer=None, identity=None): :param context: The ModbusServerContext datastore :param framer: The framer strategy to use :param identity: An optional identify structure - + :param server_address: An optional (interface,port) to bind to. ''' self.threads = [] self.decoder = ServerDecoder() @@ -222,7 +223,7 @@ def __init__(self, context, framer=None, identity=None): self.control.Identity.update(identity) SocketServer.ThreadingTCPServer.__init__(self, - ("", Defaults.Port), ModbusConnectedRequestHandler) + server_address, ModbusConnectedRequestHandler) def process_request(self, request, client): ''' Callback for connecting a new client thread @@ -251,7 +252,8 @@ class ModbusUdpServer(SocketServer.ThreadingUDPServer): server context instance. ''' - def __init__(self, context, framer=None, identity=None): + def __init__(self, context, framer=None, identity=None, + server_address=("", Defaults.Port)): ''' Overloaded initializer for the socket server If the identify structure is not passed in, the ModbusControlBlock @@ -260,7 +262,7 @@ def __init__(self, context, framer=None, identity=None): :param context: The ModbusServerContext datastore :param framer: The framer strategy to use :param identity: An optional identify structure - + :param server_address: An optional (interface,port) to bind to. ''' self.threads = [] self.decoder = ServerDecoder() @@ -272,7 +274,7 @@ def __init__(self, context, framer=None, identity=None): self.control.Identity.update(identity) SocketServer.ThreadingUDPServer.__init__(self, - ("", Defaults.Port), ModbusDisconnectedRequestHandler) + server_address, ModbusDisconnectedRequestHandler) def process_request(self, request, client): ''' Callback for connecting a new client thread @@ -387,25 +389,29 @@ def server_close(self): #---------------------------------------------------------------------------# # Creation Factories #---------------------------------------------------------------------------# -def StartTcpServer(context=None, identity=None): +def StartTcpServer(context=None, identity=None, + server_address=("", Defaults.Port)): ''' A factory to start and run a tcp modbus server :param context: The ModbusServerContext datastore :param identity: An optional identify structure + :param server_address: An optional (interface,port) to bind to. ''' framer = ModbusSocketFramer - server = ModbusTcpServer(context, framer, identity) + server = ModbusTcpServer(context, framer, identity, server_address) server.serve_forever() -def StartUdpServer(context=None, identity=None): +def StartUdpServer(context=None, identity=None, + server_address=("", Defaults.Port)): ''' A factory to start and run a udp modbus server :param context: The ModbusServerContext datastore :param identity: An optional identify structure + :param server_address: An optional (interface,port) to bind to. ''' framer = ModbusSocketFramer - server = ModbusUdpServer(context, framer, identity) + server = ModbusUdpServer(context, framer, identity, server_address) server.serve_forever() From c232a953f8af5614b736796ff15a451178dd394f Mon Sep 17 00:00:00 2001 From: Perry Kundert Date: Fri, 13 Jul 2012 09:04:51 -0600 Subject: [PATCH 082/243] Improve erver_address defaults to passing None --- pymodbus/server/async.py | 14 ++++++++------ pymodbus/server/sync.py | 22 +++++++++++----------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index c0ce26f61..a6e61177f 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -187,15 +187,16 @@ def _send(self, message, addr): #---------------------------------------------------------------------------# # Starting Factories #---------------------------------------------------------------------------# -def StartTcpServer(context, identity=None, - server_address=("", Defaults.Port)): +def StartTcpServer(context, identity=None, server_address=None): ''' Helper method to start the Modbus Async TCP server :param context: The server data context :param identify: The server identity to use (default empty) + :param server_address: An optional (interface,port) to bind to. ''' from twisted.internet import reactor - + if not server_address: + server_address = ("", Defaults.Port) _logger.info("Starting Modbus TCP Server on %s:%s" % server_address) framer = ModbusSocketFramer factory = ModbusServerFactory(context, framer, identity) @@ -204,15 +205,16 @@ def StartTcpServer(context, identity=None, reactor.run() -def StartUdpServer(context, identity=None, - server_address=("", Defaults.Port)): +def StartUdpServer(context, identity=None, server_address=None): ''' Helper method to start the Modbus Async Udp server :param context: The server data context :param identify: The server identity to use (default empty) + :param server_address: An optional (interface,port) to bind to. ''' from twisted.internet import reactor - + if not server_address: + server_address = ("", Defaults.Port) _logger.info("Starting Modbus UDP Server on %s:%s" % server_address) framer = ModbusSocketFramer server = ModbusUdpProtocol(context, framer, identity) diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index 6a15dd6e7..44eb1efb0 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -201,8 +201,8 @@ class ModbusTcpServer(SocketServer.ThreadingTCPServer): server context instance. ''' - def __init__(self, context, framer=None, identity=None, - server_address=("", Defaults.Port)): + def __init__(self, context, framer=None, identity=None, + server_address=None): ''' Overloaded initializer for the socket server If the identify structure is not passed in, the ModbusControlBlock @@ -223,7 +223,8 @@ def __init__(self, context, framer=None, identity=None, self.control.Identity.update(identity) SocketServer.ThreadingTCPServer.__init__(self, - server_address, ModbusConnectedRequestHandler) + server_address or ("", Defaults.Port), + ModbusConnectedRequestHandler) def process_request(self, request, client): ''' Callback for connecting a new client thread @@ -252,8 +253,8 @@ class ModbusUdpServer(SocketServer.ThreadingUDPServer): server context instance. ''' - def __init__(self, context, framer=None, identity=None, - server_address=("", Defaults.Port)): + def __init__(self, context, framer=None, identity=None, + server_address=None): ''' Overloaded initializer for the socket server If the identify structure is not passed in, the ModbusControlBlock @@ -273,8 +274,9 @@ def __init__(self, context, framer=None, identity=None, if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) - SocketServer.ThreadingUDPServer.__init__(self, - server_address, ModbusDisconnectedRequestHandler) + SocketServer.ThreadingUDPServer.__init__( + self, server_address or ("", Defaults.Port), + ModbusDisconnectedRequestHandler) def process_request(self, request, client): ''' Callback for connecting a new client thread @@ -389,8 +391,7 @@ def server_close(self): #---------------------------------------------------------------------------# # Creation Factories #---------------------------------------------------------------------------# -def StartTcpServer(context=None, identity=None, - server_address=("", Defaults.Port)): +def StartTcpServer(context=None, identity=None, server_address=None): ''' A factory to start and run a tcp modbus server :param context: The ModbusServerContext datastore @@ -402,8 +403,7 @@ def StartTcpServer(context=None, identity=None, server.serve_forever() -def StartUdpServer(context=None, identity=None, - server_address=("", Defaults.Port)): +def StartUdpServer(context=None, identity=None, server_address=None): ''' A factory to start and run a udp modbus server :param context: The ModbusServerContext datastore From eb2f67d37cb145d3d3a92827c1211a0ff7bb703d Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Sat, 14 Jul 2012 13:11:28 -0700 Subject: [PATCH 083/243] adding a few messages --- examples/common/messages | 47 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/examples/common/messages b/examples/common/messages index ba9c6f933..f569cf680 100644 --- a/examples/common/messages +++ b/examples/common/messages @@ -1,6 +1,19 @@ # ------------------------------------------------------------ +# What follows is a collection of encoded messages that can +# be used to test the message-parser. Simply uncomment the +# messages you want decoded and run the message parser with +# the given arguments. It should be noted that the messages +# below are in ascii format. +# +# ------------------------------------------------------------ # Modbus TCP Messages # ------------------------------------------------------------ +# [ MBAP Header ] [ Function Code] [ Data ] +# [ tid ][ pid ][ length ][ uid ] +# 2b 2b 2b 1b 1b Nb +# +# ./message-parser -b -p tcp -f messages +# ------------------------------------------------------------ #000112340006ff0101020004 #000112340006ff0201020004 #000112340006ff0302020002 @@ -13,4 +26,36 @@ # ------------------------------------------------------------ # Modbus RTU Messages # ------------------------------------------------------------ -042B0E01810001010006666F6F626172D73B +# [Address ][ Function Code] [ Data ][ CRC ] +# 1b 1b Nb 2b +# +# ./message-parser -b -p rtu -f messages +# ------------------------------------------------------------ +#042B0E01810001010006666F6F626172D73B +#00060001000399DA +#00050000FFFFCDAB +#00040000FFFFF06B +#000404010203044A4B +#00030000FFFF45AB +#000304010203044BFC +#00020000FFFF786B +#000204010203044A2D +# +# ------------------------------------------------------------ +# Modbus ASCII Messages +# ------------------------------------------------------------ +# [ Start ][Address ][ Function ][ Data ][ LRC ][ End ] +# 1c 2c 2c Nc 2c 2c +# +# ./message-parser -b -p ascii -f messages +# ------------------------------------------------------------ +# +# ------------------------------------------------------------ +# Modbus Binary Messages +# ------------------------------------------------------------ +# [ Start ][Address ][ Function ][ Data ][ CRC ][ End ] +# 1b 1b 1b Nb 2b 1b +# +# ./message-parser -b -p binary -f messages +# ------------------------------------------------------------ +# From 726a4235da9f45bcdd52b3af142985b1422d6aed Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Sun, 15 Jul 2012 18:13:24 -0700 Subject: [PATCH 084/243] Adding a message encoding generator for testing. - fixed messages not passing **kwargs to base - fixed binary framer off by 1 - fixed mei_message rtu size tests - added a message generator to use with message parser - fixed message parser with ascii - tested message parser with all formats (added to messages) --- examples/common/generate-messages.py | 230 +++++++++++++++++++++++++++ examples/common/message-parser.py | 11 +- examples/common/messages | 4 +- pymodbus/diag_message.py | 36 ++--- pymodbus/file_message.py | 32 ++-- pymodbus/mei_message.py | 8 +- pymodbus/other_message.py | 30 ++-- pymodbus/transaction.py | 2 +- test/test_mei_messages.py | 24 +-- test/test_transaction.py | 6 +- 10 files changed, 301 insertions(+), 82 deletions(-) create mode 100755 examples/common/generate-messages.py diff --git a/examples/common/generate-messages.py b/examples/common/generate-messages.py new file mode 100755 index 000000000..7912c08a5 --- /dev/null +++ b/examples/common/generate-messages.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python +''' +Modbus Message Generator +-------------------------------------------------------------------------- + +The following is an example of how to generate example encoded messages +for the supplied modbus format: + +* tcp - `./generate-messages.py -f tcp -m rx -b` +* ascii - `./generate-messages.py -f ascii -m tx -a` +* rtu - `./generate-messages.py -f rtu -m rx -b` +* binary - `./generate-messages.py -f binary -m tx -b` +''' +from optparse import OptionParser +#--------------------------------------------------------------------------# +# import all the available framers +#--------------------------------------------------------------------------# +from pymodbus.transaction import ModbusSocketFramer +from pymodbus.transaction import ModbusBinaryFramer +from pymodbus.transaction import ModbusAsciiFramer +from pymodbus.transaction import ModbusRtuFramer +#--------------------------------------------------------------------------# +# import all available messages +#--------------------------------------------------------------------------# +from pymodbus.bit_read_message import * +from pymodbus.bit_write_message import * +from pymodbus.diag_message import * +from pymodbus.file_message import * +from pymodbus.other_message import * +from pymodbus.mei_message import * +from pymodbus.register_read_message import * +from pymodbus.register_write_message import * + +#--------------------------------------------------------------------------# +# initialize logging +#--------------------------------------------------------------------------# +import logging +modbus_log = logging.getLogger("pymodbus") + + +#--------------------------------------------------------------------------# +# enumerate all request messages +#--------------------------------------------------------------------------# +_request_messages = [ + ReadHoldingRegistersRequest, + ReadDiscreteInputsRequest, + ReadInputRegistersRequest, + ReadCoilsRequest, + WriteMultipleCoilsRequest, + WriteMultipleRegistersRequest, + WriteSingleRegisterRequest, + WriteSingleCoilRequest, + ReadWriteMultipleRegistersRequest, + + ReadExceptionStatusRequest, + GetCommEventCounterRequest, + GetCommEventLogRequest, + ReportSlaveIdRequest, + + ReadFileRecordRequest, + WriteFileRecordRequest, + MaskWriteRegisterRequest, + ReadFifoQueueRequest, + + ReadDeviceInformationRequest, + + ReturnQueryDataRequest, + RestartCommunicationsOptionRequest, + ReturnDiagnosticRegisterRequest, + ChangeAsciiInputDelimiterRequest, + ForceListenOnlyModeRequest, + ClearCountersRequest, + ReturnBusMessageCountRequest, + ReturnBusCommunicationErrorCountRequest, + ReturnBusExceptionErrorCountRequest, + ReturnSlaveMessageCountRequest, + ReturnSlaveNoResponseCountRequest, + ReturnSlaveNAKCountRequest, + ReturnSlaveBusyCountRequest, + ReturnSlaveBusCharacterOverrunCountRequest, + ReturnIopOverrunCountRequest, + ClearOverrunCountRequest, + GetClearModbusPlusRequest, +] + + +#--------------------------------------------------------------------------# +# enumerate all response messages +#--------------------------------------------------------------------------# +_response_messages = [ + ReadHoldingRegistersResponse, + ReadDiscreteInputsResponse, + ReadInputRegistersResponse, + ReadCoilsResponse, + WriteMultipleCoilsResponse, + WriteMultipleRegistersResponse, + WriteSingleRegisterResponse, + WriteSingleCoilResponse, + ReadWriteMultipleRegistersResponse, + + ReadExceptionStatusResponse, + GetCommEventCounterResponse, + GetCommEventLogResponse, + ReportSlaveIdResponse, + + ReadFileRecordResponse, + WriteFileRecordResponse, + MaskWriteRegisterResponse, + ReadFifoQueueResponse, + + ReadDeviceInformationResponse, + + ReturnQueryDataResponse, + RestartCommunicationsOptionResponse, + ReturnDiagnosticRegisterResponse, + ChangeAsciiInputDelimiterResponse, + ForceListenOnlyModeResponse, + ClearCountersResponse, + ReturnBusMessageCountResponse, + ReturnBusCommunicationErrorCountResponse, + ReturnBusExceptionErrorCountResponse, + ReturnSlaveMessageCountResponse, + ReturnSlaveNoReponseCountResponse, + ReturnSlaveNAKCountResponse, + ReturnSlaveBusyCountResponse, + ReturnSlaveBusCharacterOverrunCountResponse, + ReturnIopOverrunCountResponse, + ClearOverrunCountResponse, + GetClearModbusPlusResponse, +] + + +#--------------------------------------------------------------------------# +# build an arguments singleton +#--------------------------------------------------------------------------# +# Feel free to override any values here to generate a specific message +# in question. It should be noted that many argument names are reused +# between different messages, and a number of messages are simply using +# their default values. +#--------------------------------------------------------------------------# +_arguments = { + 'address' : 0x12, + 'count' : 0x08, + 'value' : 0x01, + 'values' : [0x01] * 8, + 'read_address' : 0x12, + 'read_count' : 0x08, + 'write_address ' : 0x12, + 'write_registers' : [0x01] * 8, + 'transaction' : 0x01, + 'protocol' : 0x00, + 'unit' : 0x01, +} + + +#---------------------------------------------------------------------------# +# generate all the requested messages +#---------------------------------------------------------------------------# +def generate_messages(framer, options): + ''' A helper method to parse the command line options + + :param framer: The framer to encode the messages with + :param options: The message options to use + ''' + messages = _request_messages if options.messages == 'rx' else _response_messages + for message in messages: + message = message(**_arguments) + print "%-44s = " % message.__class__.__name__, + packet = framer.buildPacket(message) + if not options.ascii: + packet = packet.encode('hex') + '\n' + print packet, # because ascii ends with a \r\n + + +#---------------------------------------------------------------------------# +# initialize our program settings +#---------------------------------------------------------------------------# +def get_options(): + ''' A helper method to parse the command line options + + :returns: The options manager + ''' + parser = OptionParser() + + parser.add_option("-f", "--framer", + help="The type of framer to use (tcp, rtu, binary, ascii)", + dest="framer", default="tcp") + + parser.add_option("-D", "--debug", + help="Enable debug tracing", + action="store_true", dest="debug", default=False) + + parser.add_option("-a", "--ascii", + help="The indicates that the message is ascii", + action="store_true", dest="ascii", default=True) + + parser.add_option("-b", "--binary", + help="The indicates that the message is binary", + action="store_false", dest="ascii") + + parser.add_option("-m", "--messages", + help="The messages to encode (rx, tx)", + dest="messages", default='rx') + + (opt, arg) = parser.parse_args() + return opt + +def main(): + ''' The main runner function + ''' + option = get_options() + + if option.debug: + try: + modbus_log.setLevel(logging.DEBUG) + logging.basicConfig() + except Exception, e: + print "Logging is not supported on this system" + + framer = lookup = { + 'tcp': ModbusSocketFramer, + 'rtu': ModbusRtuFramer, + 'binary': ModbusBinaryFramer, + 'ascii': ModbusAsciiFramer, + }.get(option.framer, ModbusSocketFramer)(None) + + generate_messages(framer, option) + +if __name__ == "__main__": + main() diff --git a/examples/common/message-parser.py b/examples/common/message-parser.py index 20e879306..80b26b5e0 100755 --- a/examples/common/message-parser.py +++ b/examples/common/message-parser.py @@ -37,20 +37,23 @@ #---------------------------------------------------------------------------# class Decoder(object): - def __init__(self, framer): + def __init__(self, framer, encode=False): ''' Initialize a new instance of the decoder :param framer: The framer to use + :param encode: If the message needs to be encoded ''' self.framer = framer + self.encode = encode def decode(self, message): ''' Attempt to decode the supplied message :param message: The messge to decode ''' + value = message if self.encode else message.encode('hex') print "="*80 - print "Decoding Message %s" % message.encode('hex') + print "Decoding Message %s" % value print "="*80 decoders = [ self.framer(ServerDecoder()), @@ -148,8 +151,8 @@ def get_messages(option): with open(option.file, "r") as handle: for line in handle: if line.startswith('#'): continue - line = line.strip() if not option.ascii: + line = line.strip() line = line.decode('hex') yield line @@ -172,7 +175,7 @@ def main(): 'ascii': ModbusAsciiFramer, }.get(option.parser, ModbusSocketFramer) - decoder = Decoder(framer) + decoder = Decoder(framer, option.ascii) for message in get_messages(option): decoder.decode(message) diff --git a/examples/common/messages b/examples/common/messages index f569cf680..d7e8ada45 100644 --- a/examples/common/messages +++ b/examples/common/messages @@ -47,8 +47,9 @@ # [ Start ][Address ][ Function ][ Data ][ LRC ][ End ] # 1c 2c 2c Nc 2c 2c # -# ./message-parser -b -p ascii -f messages +# ./message-parser -a -p ascii -f messages # ------------------------------------------------------------ +#:000100340012B9 # # ------------------------------------------------------------ # Modbus Binary Messages @@ -59,3 +60,4 @@ # ./message-parser -b -p binary -f messages # ------------------------------------------------------------ # +7b010800150000f1cf7d diff --git a/pymodbus/diag_message.py b/pymodbus/diag_message.py index 3e9e585b7..b4de632d2 100644 --- a/pymodbus/diag_message.py +++ b/pymodbus/diag_message.py @@ -30,12 +30,12 @@ class DiagnosticStatusRequest(ModbusRequest): function_code = 0x08 _rtu_frame_size = 8 - def __init__(self): + def __init__(self, **kwargs): ''' Base initializer for a diagnostic request ''' + ModbusRequest.__init__(self, **kwargs) self.message = None - ModbusRequest.__init__(self) def encode(self): ''' @@ -74,12 +74,12 @@ class DiagnosticStatusResponse(ModbusResponse): function_code = 0x08 _rtu_frame_size = 8 - def __init__(self): + def __init__(self, **kwargs): ''' Base initializer for a diagnostic response ''' + ModbusResponse.__init__(self, **kwargs) self.message = None - ModbusResponse.__init__(self) def encode(self): ''' @@ -118,7 +118,7 @@ class DiagnosticStatusSimpleRequest(DiagnosticStatusRequest): the execute method ''' - def __init__(self, data=0x0000): + def __init__(self, data=0x0000, **kwargs): ''' General initializer for a simple diagnostic request @@ -127,7 +127,7 @@ def __init__(self, data=0x0000): :param data: The data to send along with the request ''' - DiagnosticStatusRequest.__init__(self) + DiagnosticStatusRequest.__init__(self, **kwargs) self.message = data def execute(self, *args): @@ -143,12 +143,12 @@ class DiagnosticStatusSimpleResponse(DiagnosticStatusResponse): 2 bytes of data. ''' - def __init__(self, data=0x0000): + def __init__(self, data=0x0000, **kwargs): ''' General initializer for a simple diagnostic response :param data: The resulting data to return to the client ''' - DiagnosticStatusResponse.__init__(self) + DiagnosticStatusResponse.__init__(self, **kwargs) self.message = data @@ -163,12 +163,12 @@ class ReturnQueryDataRequest(DiagnosticStatusRequest): ''' sub_function_code = 0x0000 - def __init__(self, message=0x0000): + def __init__(self, message=0x0000, **kwargs): ''' Initializes a new instance of the request :param message: The message to send to loopback ''' - DiagnosticStatusRequest.__init__(self) + DiagnosticStatusRequest.__init__(self, **kwargs) if isinstance(message, list): self.message = message else: self.message = [message] @@ -189,12 +189,12 @@ class ReturnQueryDataResponse(DiagnosticStatusResponse): ''' sub_function_code = 0x0000 - def __init__(self, message=0x0000): + def __init__(self, message=0x0000, **kwargs): ''' Initializes a new instance of the response :param message: The message to loopback ''' - DiagnosticStatusResponse.__init__(self) + DiagnosticStatusResponse.__init__(self, **kwargs) if isinstance(message, list): self.message = message else: self.message = [message] @@ -214,12 +214,12 @@ class RestartCommunicationsOptionRequest(DiagnosticStatusRequest): ''' sub_function_code = 0x0001 - def __init__(self, toggle=False): + def __init__(self, toggle=False, **kwargs): ''' Initializes a new request :param toggle: Set to True to toggle, False otherwise ''' - DiagnosticStatusRequest.__init__(self) + DiagnosticStatusRequest.__init__(self, **kwargs) if toggle: self.message = [ModbusStatus.On] else: self.message = [ModbusStatus.Off] @@ -244,12 +244,12 @@ class RestartCommunicationsOptionResponse(DiagnosticStatusResponse): ''' sub_function_code = 0x0001 - def __init__(self, toggle=False): + def __init__(self, toggle=False, **kwargs): ''' Initializes a new response :param toggle: Set to True if we toggled, False otherwise ''' - DiagnosticStatusResponse.__init__(self) + DiagnosticStatusResponse.__init__(self, **kwargs) if toggle: self.message = [ModbusStatus.On] else: self.message = [ModbusStatus.Off] @@ -348,10 +348,10 @@ class ForceListenOnlyModeResponse(DiagnosticStatusResponse): sub_function_code = 0x0004 should_respond = False - def __init__(self): + def __init__(self, **kwargs): ''' Initializer to block a return response ''' - DiagnosticStatusResponse.__init__(self) + DiagnosticStatusResponse.__init__(self, **kwargs) self.message = [] diff --git a/pymodbus/file_message.py b/pymodbus/file_message.py index ca57288e0..3f9c63bea 100644 --- a/pymodbus/file_message.py +++ b/pymodbus/file_message.py @@ -83,12 +83,12 @@ class ReadFileRecordRequest(ModbusRequest): function_code = 0x14 _rtu_byte_count_pos = 2 - def __init__(self, records=None): + def __init__(self, records=None, **kwargs): ''' Initializes a new instance :param records: The file record requests to be read ''' - ModbusRequest.__init__(self) + ModbusRequest.__init__(self, **kwargs) self.records = records or [] def encode(self): @@ -138,12 +138,12 @@ class ReadFileRecordResponse(ModbusResponse): function_code = 0x14 _rtu_byte_count_pos = 2 - def __init__(self, records=None): + def __init__(self, records=None, **kwargs): ''' Initializes a new instance :param records: The requested file records ''' - ModbusResponse.__init__(self) + ModbusResponse.__init__(self, **kwargs) self.records = records or [] def encode(self): @@ -183,12 +183,12 @@ class WriteFileRecordRequest(ModbusRequest): function_code = 0x15 _rtu_byte_count_pos = 2 - def __init__(self, records=None): + def __init__(self, records=None, **kwargs): ''' Initializes a new instance :param records: The file record requests to be read ''' - ModbusRequest.__init__(self) + ModbusRequest.__init__(self, **kwargs) self.records = records or [] def encode(self): @@ -239,12 +239,12 @@ class WriteFileRecordResponse(ModbusResponse): function_code = 0x15 _rtu_byte_count_pos = 2 - def __init__(self, records=None): + def __init__(self, records=None, **kwargs): ''' Initializes a new instance :param records: The file record requests to be read ''' - ModbusResponse.__init__(self) + ModbusResponse.__init__(self, **kwargs) self.records = records or [] def encode(self): @@ -287,14 +287,14 @@ class MaskWriteRegisterRequest(ModbusRequest): function_code = 0x16 _rtu_frame_size = 10 - def __init__(self, address=0x0000, and_mask=0xffff, or_mask=0x0000): + def __init__(self, address=0x0000, and_mask=0xffff, or_mask=0x0000, **kwargs): ''' Initializes a new instance :param address: The mask pointer address (0x0000 to 0xffff) :param and_mask: The and bitmask to apply to the register address :param or_mask: The or bitmask to apply to the register address ''' - ModbusRequest.__init__(self) + ModbusRequest.__init__(self, **kwargs) self.address = address self.and_mask = and_mask self.or_mask = or_mask @@ -339,14 +339,14 @@ class MaskWriteRegisterResponse(ModbusResponse): function_code = 0x16 _rtu_frame_size = 10 - def __init__(self, address=0x0000, and_mask=0xffff, or_mask=0x0000): + def __init__(self, address=0x0000, and_mask=0xffff, or_mask=0x0000, **kwargs): ''' Initializes a new instance :param address: The mask pointer address (0x0000 to 0xffff) :param and_mask: The and bitmask applied to the register address :param or_mask: The or bitmask applied to the register address ''' - ModbusResponse.__init__(self) + ModbusResponse.__init__(self, **kwargs) self.address = address self.and_mask = and_mask self.or_mask = or_mask @@ -381,12 +381,12 @@ class ReadFifoQueueRequest(ModbusRequest): function_code = 0x18 _rtu_frame_size = 6 - def __init__(self, address=0x0000): + def __init__(self, address=0x0000, **kwargs): ''' Initializes a new instance :param address: The fifo pointer address (0x0000 to 0xffff) ''' - ModbusRequest.__init__(self) + ModbusRequest.__init__(self, **kwargs) self.address = address self.values = [] # this should be added to the context @@ -441,12 +441,12 @@ def calculateRtuFrameSize(cls, buffer): lo_byte = struct.unpack(">B", buffer[3])[0] return (hi_byte << 16) + lo_byte + 6 - def __init__(self, values=None): + def __init__(self, values=None, **kwargs): ''' Initializes a new instance :param values: The list of values of the fifo to return ''' - ModbusResponse.__init__(self) + ModbusResponse.__init__(self, **kwargs) self.values = values or [] def encode(self): diff --git a/pymodbus/mei_message.py b/pymodbus/mei_message.py index fd0e02cb7..4653e6397 100644 --- a/pymodbus/mei_message.py +++ b/pymodbus/mei_message.py @@ -31,13 +31,13 @@ class ReadDeviceInformationRequest(ModbusRequest): sub_function_code = 0x0e _rtu_frame_size = 3 - def __init__(self, read_code=None, object_id=0x00): + def __init__(self, read_code=None, object_id=0x00, **kwargs): ''' Initializes a new instance :param read_code: The device information read code :param object_id: The object to read from ''' - ModbusRequest.__init__(self) + ModbusRequest.__init__(self, **kwargs) self.read_code = read_code or DeviceInformation.Basic self.object_id = object_id @@ -104,13 +104,13 @@ def calculateRtuFrameSize(cls, buffer): count -= 1 return size + 2 - def __init__(self, read_code=None, information=None): + def __init__(self, read_code=None, information=None, **kwargs): ''' Initializes a new instance :param read_code: The device information read code :param information: The requested information request ''' - ModbusResponse.__init__(self) + ModbusResponse.__init__(self, **kwargs) self.read_code = read_code or DeviceInformation.Basic self.information = information or {} self.number_of_objects = len(self.information) diff --git a/pymodbus/other_message.py b/pymodbus/other_message.py index 9ea59d0c6..88e1be2ec 100644 --- a/pymodbus/other_message.py +++ b/pymodbus/other_message.py @@ -25,10 +25,10 @@ class ReadExceptionStatusRequest(ModbusRequest): function_code = 0x07 _rtu_frame_size = 4 - def __init__(self): + def __init__(self, **kwargs): ''' Initializes a new instance ''' - ModbusRequest.__init__(self) + ModbusRequest.__init__(self, **kwargs) def encode(self): ''' Encodes the message @@ -69,12 +69,12 @@ class ReadExceptionStatusResponse(ModbusResponse): function_code = 0x07 _rtu_frame_size = 5 - def __init__(self, status=0x00): + def __init__(self, status=0x00, **kwargs): ''' Initializes a new instance :param status: The status response to report ''' - ModbusResponse.__init__(self) + ModbusResponse.__init__(self, **kwargs) self.status = status def encode(self): @@ -126,10 +126,10 @@ class GetCommEventCounterRequest(ModbusRequest): function_code = 0x0b _rtu_frame_size = 4 - def __init__(self): + def __init__(self, **kwargs): ''' Initializes a new instance ''' - ModbusRequest.__init__(self) + ModbusRequest.__init__(self, **kwargs) def encode(self): ''' Encodes the message @@ -170,12 +170,12 @@ class GetCommEventCounterResponse(ModbusResponse): function_code = 0x0b _rtu_frame_size = 8 - def __init__(self, count=0x0000): + def __init__(self, count=0x0000, **kwargs): ''' Initializes a new instance :param count: The current event counter value ''' - ModbusResponse.__init__(self) + ModbusResponse.__init__(self, **kwargs) self.count = count self.status = True # this means we are ready, not waiting @@ -231,10 +231,10 @@ class GetCommEventLogRequest(ModbusRequest): function_code = 0x0c _rtu_frame_size = 4 - def __init__(self): + def __init__(self, **kwargs): ''' Initializes a new instance ''' - ModbusRequest.__init__(self) + ModbusRequest.__init__(self, **kwargs) def encode(self): ''' Encodes the message @@ -287,7 +287,7 @@ def __init__(self, **kwargs): :param event_count: The current event count :param events: The collection of events to send ''' - ModbusResponse.__init__(self) + ModbusResponse.__init__(self, **kwargs) self.status = kwargs.get('status', True) self.message_count = kwargs.get('message_count', 0) self.event_count = kwargs.get('event_count', 0) @@ -341,10 +341,10 @@ class ReportSlaveIdRequest(ModbusRequest): function_code = 0x11 _rtu_frame_size = 4 - def __init__(self): + def __init__(self, **kwargs): ''' Initializes a new instance ''' - ModbusRequest.__init__(self) + ModbusRequest.__init__(self, **kwargs) def encode(self): ''' Encodes the message @@ -382,13 +382,13 @@ class ReportSlaveIdResponse(ModbusResponse): function_code = 0x11 _rtu_byte_count_pos = 2 - def __init__(self, identifier=0x00, status=True): + def __init__(self, identifier='\x00', status=True, **kwargs): ''' Initializes a new instance :param identifier: The identifier of the slave :param status: The status response to report ''' - ModbusResponse.__init__(self) + ModbusResponse.__init__(self, **kwargs) self.identifier = identifier self.status = status diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index d68424506..d1829a481 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -660,7 +660,7 @@ def checkFrame(self): self.__header['len'] = end self.__header['uid'] = struct.unpack('>B', self.__buffer[1:2]) self.__header['crc'] = struct.unpack('>H', self.__buffer[end - 2:end])[0] - data = self.__buffer[start:end - 2] + data = self.__buffer[start + 1:end - 2] return checkCRC(data, self.__header['crc']) return False diff --git a/test/test_mei_messages.py b/test/test_mei_messages.py index c4abdb138..c2f220285 100644 --- a/test/test_mei_messages.py +++ b/test/test_mei_messages.py @@ -20,21 +20,6 @@ class ModbusMeiMessageTest(unittest.TestCase): This is the unittest for the pymodbus.mei_message module ''' - #-----------------------------------------------------------------------# - # Setup/TearDown - #-----------------------------------------------------------------------# - - def setUp(self): - ''' - Initializes the test environment and builds request/result - encoding pairs - ''' - pass - - def tearDown(self): - ''' Cleans up the test environment ''' - pass - #-----------------------------------------------------------------------# # Read Device Information #-----------------------------------------------------------------------# @@ -97,7 +82,7 @@ def testReadDeviceInformationResponseEncode(self): self.assertEqual("ReadDeviceInformationResponse(1)", str(handle)) def testReadDeviceInformationResponseDecode(self): - ''' Test that the read fifo queue response can decode ''' + ''' Test that the read device information response can decode ''' message = '\x0e\x01\x01\x00\x00\x03' message += '\x00\x07Company\x01\x07Product\x02\x07v2.1.12' handle = ReadDeviceInformationResponse(read_code=0x00, information=[]) @@ -109,11 +94,10 @@ def testReadDeviceInformationResponseDecode(self): self.assertEqual(handle.information[0x02], 'v2.1.12') def testRtuFrameSize(self): - ''' Test that the read fifo queue response can decode ''' - message = '\x0e\x01\x01\x00\x00\x03' - message += '\x00\x07Company\x01\x07Product\x02\x07v2.1.12' + ''' Test that the read device information response can decode ''' + message = '\x04\x2B\x0E\x01\x81\x00\x01\x01\x00\x06\x66\x6F\x6F\x62\x61\x72\xD7\x3B' result = ReadDeviceInformationResponse.calculateRtuFrameSize(message) - self.assertEqual(result, 33) + self.assertEqual(result, 18) #---------------------------------------------------------------------------# diff --git a/test/test_transaction.py b/test/test_transaction.py index c89c1873e..a14cb3007 100644 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -297,7 +297,7 @@ def testASCIIFramerPacket(self): #---------------------------------------------------------------------------# def testBinaryFramerTransactionReady(self): ''' Test a binary frame transaction ''' - msg = '\x7b\x01\x03\x00\x00\x00\x05\x55\xd5\x7d' + msg = '\x7b\x01\x03\x00\x00\x00\x05\x85\xC9\x7d' self.assertFalse(self._binary.isFrameReady()) self.assertFalse(self._binary.checkFrame()) self._binary.addToFrame(msg) @@ -310,7 +310,7 @@ def testBinaryFramerTransactionReady(self): def testBinaryFramerTransactionFull(self): ''' Test a full binary frame transaction ''' - msg = '\x7b\x01\x03\x00\x00\x00\x05\x55\xd5\x7d' + msg = '\x7b\x01\x03\x00\x00\x00\x05\x85\xC9\x7d' pack = msg[3:-3] self._binary.addToFrame(msg) self.assertTrue(self._binary.checkFrame()) @@ -321,7 +321,7 @@ def testBinaryFramerTransactionFull(self): def testBinaryFramerTransactionHalf(self): ''' Test a half completed binary frame transaction ''' msg1 = '\x7b\x01\x03\x00' - msg2 = '\x00\x00\x05\x55\xd5\x7d' + msg2 = '\x00\x00\x05\x85\xC9\x7d' pack = msg1[3:] + msg2[:-3] self._binary.addToFrame(msg1) self.assertFalse(self._binary.checkFrame()) From 2f97b9b987c730bb47b32e51d7e0164af940caf3 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 16 Jul 2012 09:19:08 -0500 Subject: [PATCH 085/243] adding documentation for message formats --- doc/sphinx/examples/generate-messages.rst | 26 +++ doc/sphinx/examples/index.rst | 1 + doc/sphinx/examples/message-parser.rst | 4 +- examples/common/generate-messages.py | 2 +- examples/common/message-parser.py | 12 +- examples/common/messages | 63 ------- examples/common/rx-messages | 215 ++++++++++++++++++++++ examples/common/tx-messages | 215 ++++++++++++++++++++++ 8 files changed, 468 insertions(+), 70 deletions(-) create mode 100644 doc/sphinx/examples/generate-messages.rst delete mode 100644 examples/common/messages create mode 100644 examples/common/rx-messages create mode 100644 examples/common/tx-messages diff --git a/doc/sphinx/examples/generate-messages.rst b/doc/sphinx/examples/generate-messages.rst new file mode 100644 index 000000000..3f004559c --- /dev/null +++ b/doc/sphinx/examples/generate-messages.rst @@ -0,0 +1,26 @@ +================================================== +Modbus Message Generator Example +================================================== + +This is an example of a utility that will build +examples of modbus messages in all the available +formats in the pymodbus package. + +-------------------------------------------------- +Program Source +-------------------------------------------------- + +.. literalinclude:: ../../../examples/common/generate-messages.py + +-------------------------------------------------- +Example Request Messages +-------------------------------------------------- + +.. literalinclude:: ../../../examples/common/tx-messages + +-------------------------------------------------- +Example Response Messages +-------------------------------------------------- + +.. literalinclude:: ../../../examples/common/rx-messages + diff --git a/doc/sphinx/examples/index.rst b/doc/sphinx/examples/index.rst index 50698bf5d..607440eb0 100644 --- a/doc/sphinx/examples/index.rst +++ b/doc/sphinx/examples/index.rst @@ -16,6 +16,7 @@ Example Library Code asynchronous-server asynchronous-processor custom-message + generate-messages modbus-logging modbus-payload modbus-scraper diff --git a/doc/sphinx/examples/message-parser.rst b/doc/sphinx/examples/message-parser.rst index 018150fb7..971093140 100644 --- a/doc/sphinx/examples/message-parser.rst +++ b/doc/sphinx/examples/message-parser.rst @@ -49,5 +49,7 @@ Program Source Example Messages -------------------------------------------------- -.. literalinclude:: ../../../examples/common/messages +See the documentation for the message generator +for a collection of messages that can be parsed +by this utility. diff --git a/examples/common/generate-messages.py b/examples/common/generate-messages.py index 7912c08a5..114567084 100755 --- a/examples/common/generate-messages.py +++ b/examples/common/generate-messages.py @@ -162,7 +162,7 @@ def generate_messages(framer, options): :param framer: The framer to encode the messages with :param options: The message options to use ''' - messages = _request_messages if options.messages == 'rx' else _response_messages + messages = _request_messages if options.messages == 'tx' else _response_messages for message in messages: message = message(**_arguments) print "%-44s = " % message.__class__.__name__, diff --git a/examples/common/message-parser.py b/examples/common/message-parser.py index 80b26b5e0..fd36af026 100755 --- a/examples/common/message-parser.py +++ b/examples/common/message-parser.py @@ -62,11 +62,13 @@ def decode(self, message): for decoder in decoders: print "%s" % decoder.decoder.__class__.__name__ print "-"*80 - decoder.addToFrame(message) - if decoder.checkFrame(): - decoder.advanceFrame() - decoder.processIncomingPacket(message, self.report) - else: self.check_errors(decoder, message) + try: + decoder.addToFrame(message) + if decoder.checkFrame(): + decoder.advanceFrame() + decoder.processIncomingPacket(message, self.report) + else: self.check_errors(decoder, message) + except Exception, ex: self.check_errors(decoder, message) def check_errors(self, decoder, message): ''' Attempt to find message errors diff --git a/examples/common/messages b/examples/common/messages deleted file mode 100644 index d7e8ada45..000000000 --- a/examples/common/messages +++ /dev/null @@ -1,63 +0,0 @@ -# ------------------------------------------------------------ -# What follows is a collection of encoded messages that can -# be used to test the message-parser. Simply uncomment the -# messages you want decoded and run the message parser with -# the given arguments. It should be noted that the messages -# below are in ascii format. -# -# ------------------------------------------------------------ -# Modbus TCP Messages -# ------------------------------------------------------------ -# [ MBAP Header ] [ Function Code] [ Data ] -# [ tid ][ pid ][ length ][ uid ] -# 2b 2b 2b 1b 1b Nb -# -# ./message-parser -b -p tcp -f messages -# ------------------------------------------------------------ -#000112340006ff0101020004 -#000112340006ff0201020004 -#000112340006ff0302020002 -#000112340006ff0402020002 -#000112340006ff0500acff00 -#000112340006ff0600010003 -#000112340006ff076d -#000112340006ff0800010000 -# -# ------------------------------------------------------------ -# Modbus RTU Messages -# ------------------------------------------------------------ -# [Address ][ Function Code] [ Data ][ CRC ] -# 1b 1b Nb 2b -# -# ./message-parser -b -p rtu -f messages -# ------------------------------------------------------------ -#042B0E01810001010006666F6F626172D73B -#00060001000399DA -#00050000FFFFCDAB -#00040000FFFFF06B -#000404010203044A4B -#00030000FFFF45AB -#000304010203044BFC -#00020000FFFF786B -#000204010203044A2D -# -# ------------------------------------------------------------ -# Modbus ASCII Messages -# ------------------------------------------------------------ -# [ Start ][Address ][ Function ][ Data ][ LRC ][ End ] -# 1c 2c 2c Nc 2c 2c -# -# ./message-parser -a -p ascii -f messages -# ------------------------------------------------------------ -#:000100340012B9 -# -# ------------------------------------------------------------ -# Modbus Binary Messages -# ------------------------------------------------------------ -# [ Start ][Address ][ Function ][ Data ][ CRC ][ End ] -# 1b 1b 1b Nb 2b 1b -# -# ./message-parser -b -p binary -f messages -# ------------------------------------------------------------ -# -7b010800150000f1cf7d diff --git a/examples/common/rx-messages b/examples/common/rx-messages new file mode 100644 index 000000000..e8799493b --- /dev/null +++ b/examples/common/rx-messages @@ -0,0 +1,215 @@ +# ------------------------------------------------------------ +# What follows is a collection of encoded messages that can +# be used to test the message-parser. Simply uncomment the +# messages you want decoded and run the message parser with +# the given arguments. What follows is the listing of messages +# that are encoded in each format: +# +# - ReadHoldingRegistersResponse +# - ReadDiscreteInputsResponse +# - ReadInputRegistersResponse +# - ReadCoilsResponse +# - WriteMultipleCoilsResponse +# - WriteMultipleRegistersResponse +# - WriteSingleRegisterResponse +# - WriteSingleCoilResponse +# - ReadWriteMultipleRegistersResponse +# - ReadExceptionStatusResponse +# - GetCommEventCounterResponse +# - GetCommEventLogResponse +# - ReportSlaveIdResponse +# - ReadFileRecordResponse +# - WriteFileRecordResponse +# - MaskWriteRegisterResponse +# - ReadFifoQueueResponse +# - ReadDeviceInformationResponse +# - ReturnQueryDataResponse +# - RestartCommunicationsOptionResponse +# - ReturnDiagnosticRegisterResponse +# - ChangeAsciiInputDelimiterResponse +# - ForceListenOnlyModeResponse +# - ClearCountersResponse +# - ReturnBusMessageCountResponse +# - ReturnBusCommunicationErrorCountResponse +# - ReturnBusExceptionErrorCountResponse +# - ReturnSlaveMessageCountResponse +# - ReturnSlaveNoReponseCountResponse +# - ReturnSlaveNAKCountResponse +# - ReturnSlaveBusyCountResponse +# - ReturnSlaveBusCharacterOverrunCountResponse +# - ReturnIopOverrunCountResponse +# - ClearOverrunCountResponse +# - GetClearModbusPlusResponse +# ------------------------------------------------------------ +# Modbus TCP Messages +# ------------------------------------------------------------ +# [ MBAP Header ] [ Function Code] [ Data ] +# [ tid ][ pid ][ length ][ uid ] +# 2b 2b 2b 1b 1b Nb +# +# ./message-parser -b -p tcp -f messages +# ------------------------------------------------------------ +#00010000001301031000010001000100010001000100010001 +#000100000004010201ff +#00010000001301041000010001000100010001000100010001 +#000100000004010101ff +#000100000006010f00120008 +#000100000006011000120008 +#000100000006010600120001 +#00010000000601050012ff00 +#00010000001301171000010001000100010001000100010001 +#000100000003010700 +#000100000006010b00000008 +#000100000009010c06000000000000 +#00010000000501110300ff +#000100000003011400 +#000100000003011500 +#00010000000801160012ffff0000 +#00010000001601180012001000010001000100010001000100010001 +#000100000008012b0e0183000000 +#000100000006010800000000 +#000100000006010800010000 +#000100000006010800020000 +#000100000006010800030000 +#00010000000401080004 +#0001000000060108000a0000 +#0001000000060108000b0000 +#0001000000060108000c0000 +#0001000000060108000d0000 +#0001000000060108000e0000 +#0001000000060108000f0000 +#000100000006010800100000 +#000100000006010800110000 +#000100000006010800120000 +#000100000006010800130000 +#000100000006010800140000 +#000100000006010800150000 +# ------------------------------------------------------------ +# Modbus RTU Messages +# ------------------------------------------------------------ +# [Address ][ Function Code] [ Data ][ CRC ] +# 1b 1b Nb 2b +# +# ./message-parser -b -p rtu -f messages +# ------------------------------------------------------------ +#0103100001000100010001000100010001000193b4 +#010201ffe1c8 +#0104100001000100010001000100010001000122c1 +#010101ff11c8 +#010f00120008f408 +#01100012000861ca +#010600120001e80f +#01050012ff002c3f +#01171000010001000100010001000100010001d640 +#0107002230 +#010b00000008a5cd +#010c060000000000006135 +#01110300ffacbc +#0114002f00 +#0115002e90 +#01160012ffff00004e21 +#01180012001000010001000100010001000100010001d74d +#012b0e01830000000faf +#010800000000e00b +#010800010000b1cb +#01080002000041cb +#010800030000100b +#0108000481d9 +#0108000a0000c009 +#0108000b000091c9 +#0108000c00002008 +#0108000d000071c8 +#0108000e000081c8 +#0108000f0000d008 +#010800100000e1ce +#010800110000b00e +#010800120000400e +#01080013000011ce +#010800140000a00f +#010800150000f1cf +# ------------------------------------------------------------ +# Modbus ASCII Messages +# ------------------------------------------------------------ +# [ Start ][Address ][ Function ][ Data ][ LRC ][ End ] +# 1c 2c 2c Nc 2c 2c +# +# ./message-parser -a -p ascii -f messages +# ------------------------------------------------------------ +#:01031000010001000100010001000100010001E4 +#:010201FFFD +#:01041000010001000100010001000100010001E3 +#:010101FFFE +#:010F00120008D6 +#:011000120008D5 +#:010600120001E6 +#:01050012FF00E9 +#:01171000010001000100010001000100010001D0 +#:010700F8 +#:010B00000008EC +#:010C06000000000000ED +#:01110300FFEC +#:011400EB +#:011500EA +#:01160012FFFF0000D9 +#:01180012001000010001000100010001000100010001BD +#:012B0E018300000042 +#:010800000000F7 +#:010800010000F6 +#:010800020000F5 +#:010800030000F4 +#:01080004F3 +#:0108000A0000ED +#:0108000B0000EC +#:0108000C0000EB +#:0108000D0000EA +#:0108000E0000E9 +#:0108000F0000E8 +#:010800100000E7 +#:010800110000E6 +#:010800120000E5 +#:010800130000E4 +#:010800140000E3 +#:010800150000E2 +# ------------------------------------------------------------ +# Modbus Binary Messages +# ------------------------------------------------------------ +# [ Start ][Address ][ Function ][ Data ][ CRC ][ End ] +# 1b 1b 1b Nb 2b 1b +# +# ./message-parser -b -p binary -f messages +# ------------------------------------------------------------ +#7b0103100001000100010001000100010001000193b47d +#7b010201ffe1c87d +#7b0104100001000100010001000100010001000122c17d +#7b010101ff11c87d +#7b010f00120008f4087d +#7b01100012000861ca7d +#7b010600120001e80f7d +#7b01050012ff002c3f7d +#7b01171000010001000100010001000100010001d6407d +#7b01070022307d +#7b010b00000008a5cd7d +#7b010c0600000000000061357d +#7b01110300ffacbc7d +#7b0114002f007d +#7b0115002e907d +#7b01160012ffff00004e217d +#7b01180012001000010001000100010001000100010001d74d7d +#7b012b0e01830000000faf7d +#7b010800000000e00b7d +#7b010800010000b1cb7d +#7b01080002000041cb7d +#7b010800030000100b7d +#7b0108000481d97d +#7b0108000a0000c0097d +#7b0108000b000091c97d +#7b0108000c000020087d +#7b0108000d000071c87d +#7b0108000e000081c87d +#7b0108000f0000d0087d +#7b010800100000e1ce7d +#7b010800110000b00e7d +#7b010800120000400e7d +#7b01080013000011ce7d +#7b010800140000a00f7d +#7b010800150000f1cf7d diff --git a/examples/common/tx-messages b/examples/common/tx-messages new file mode 100644 index 000000000..2da177dde --- /dev/null +++ b/examples/common/tx-messages @@ -0,0 +1,215 @@ +# ------------------------------------------------------------ +# What follows is a collection of encoded messages that can +# be used to test the message-parser. Simply uncomment the +# messages you want decoded and run the message parser with +# the given arguments. What follows is the listing of messages +# that are encoded in each format: +# +# - ReadHoldingRegistersRequest +# - ReadDiscreteInputsRequest +# - ReadInputRegistersRequest +# - ReadCoilsRequest +# - WriteMultipleCoilsRequest +# - WriteMultipleRegistersRequest +# - WriteSingleRegisterRequest +# - WriteSingleCoilRequest +# - ReadWriteMultipleRegistersRequest +# - ReadExceptionStatusRequest +# - GetCommEventCounterRequest +# - GetCommEventLogRequest +# - ReportSlaveIdRequest +# - ReadFileRecordRequest +# - WriteFileRecordRequest +# - MaskWriteRegisterRequest +# - ReadFifoQueueRequest +# - ReadDeviceInformationRequest +# - ReturnQueryDataRequest +# - RestartCommunicationsOptionRequest +# - ReturnDiagnosticRegisterRequest +# - ChangeAsciiInputDelimiterRequest +# - ForceListenOnlyModeRequest +# - ClearCountersRequest +# - ReturnBusMessageCountRequest +# - ReturnBusCommunicationErrorCountRequest +# - ReturnBusExceptionErrorCountRequest +# - ReturnSlaveMessageCountRequest +# - ReturnSlaveNoReponseCountRequest +# - ReturnSlaveNAKCountRequest +# - ReturnSlaveBusyCountRequest +# - ReturnSlaveBusCharacterOverrunCountRequest +# - ReturnIopOverrunCountRequest +# - ClearOverrunCountRequest +# - GetClearModbusPlusRequest +# ------------------------------------------------------------ +# Modbus TCP Messages +# ------------------------------------------------------------ +# [ MBAP Header ] [ Function Code] [ Data ] +# [ tid ][ pid ][ length ][ uid ] +# 2b 2b 2b 1b 1b Nb +# +# ./message-parser -b -p tcp -f messages +# ------------------------------------------------------------ +#000100000006010300120008 +#000100000006010200120008 +#000100000006010400120008 +#000100000006010100120008 +#000100000008010f0012000801ff +#0001000000170110001200081000010001000100010001000100010001 +#000100000006010600120001 +#00010000000601050012ff00 +#00010000001b011700120008000000081000010001000100010001000100010001 +#0001000000020107 +#000100000002010b +#000100000002010c +#0001000000020111 +#000100000003011400 +#000100000003011500 +#00010000000801160012ffff0000 +#00010000000401180012 +#000100000005012b0e0100 +#000100000006010800000000 +#000100000006010800010000 +#000100000006010800020000 +#000100000006010800030000 +#000100000006010800040000 +#0001000000060108000a0000 +#0001000000060108000b0000 +#0001000000060108000c0000 +#0001000000060108000d0000 +#0001000000060108000e0000 +#0001000000060108000f0000 +#000100000006010800100000 +#000100000006010800110000 +#000100000006010800120000 +#000100000006010800130000 +#000100000006010800140000 +#000100000006010800150000 +# ------------------------------------------------------------ +# Modbus RTU Messages +# ------------------------------------------------------------ +# [Address ][ Function Code] [ Data ][ CRC ] +# 1b 1b Nb 2b +# +# ./message-parser -b -p rtu -f messages +# ------------------------------------------------------------ +#010300120008e409 +#010200120008d9c9 +#01040012000851c9 +#0101001200089dc9 +#010f0012000801ff06d6 +#0110001200081000010001000100010001000100010001d551 +#010600120001e80f +#01050012ff002c3f +#011700120008000000081000010001000100010001000100010001e6f8 +#010741e2 +#010b41e7 +#010c0025 +#0111c02c +#0114002f00 +#0115002e90 +#01160012ffff00004e21 +#0118001201d2 +#012b0e01007077 +#010800000000e00b +#010800010000b1cb +#01080002000041cb +#010800030000100b +#010800040000a1ca +#0108000a0000c009 +#0108000b000091c9 +#0108000c00002008 +#0108000d000071c8 +#0108000e000081c8 +#0108000f0000d008 +#010800100000e1ce +#010800110000b00e +#010800120000400e +#01080013000011ce +#010800140000a00f +#010800150000f1cf +# ------------------------------------------------------------ +# Modbus ASCII Messages +# ------------------------------------------------------------ +# [ Start ][Address ][ Function ][ Data ][ LRC ][ End ] +# 1c 2c 2c Nc 2c 2c +# +# ./message-parser -a -p ascii -f messages +# ------------------------------------------------------------ +#:010300120008E2 +#:010200120008E3 +#:010400120008E1 +#:010100120008E4 +#:010F0012000801FFD6 +#:0110001200081000010001000100010001000100010001BD +#:010600120001E6 +#:01050012FF00E9 +#:011700120008000000081000010001000100010001000100010001AE +#:0107F8 +#:010BF4 +#:010CF3 +#:0111EE +#:011400EB +#:011500EA +#:01160012FFFF0000D9 +#:01180012D5 +#:012B0E0100C5 +#:010800000000F7 +#:010800010000F6 +#:010800020000F5 +#:010800030000F4 +#:010800040000F3 +#:0108000A0000ED +#:0108000B0000EC +#:0108000C0000EB +#:0108000D0000EA +#:0108000E0000E9 +#:0108000F0000E8 +#:010800100000E7 +#:010800110000E6 +#:010800120000E5 +#:010800130000E4 +#:010800140000E3 +#:010800150000E2 +# ------------------------------------------------------------ +# Modbus Binary Messages +# ------------------------------------------------------------ +# [ Start ][Address ][ Function ][ Data ][ CRC ][ End ] +# 1b 1b 1b Nb 2b 1b +# +# ./message-parser -b -p binary -f messages +# ------------------------------------------------------------ +#7b010300120008e4097d +#7b010200120008d9c97d +#7b01040012000851c97d +#7b0101001200089dc97d +#7b010f0012000801ff06d67d +#7b0110001200081000010001000100010001000100010001d5517d +#7b010600120001e80f7d +#7b01050012ff002c3f7d +#7b011700120008000000081000010001000100010001000100010001e6f87d +#7b010741e27d +#7b010b41e77d +#7b010c00257d +#7b0111c02c7d +#7b0114002f007d +#7b0115002e907d +#7b01160012ffff00004e217d +#7b0118001201d27d +#7b012b0e010070777d +#7b010800000000e00b7d +#7b010800010000b1cb7d +#7b01080002000041cb7d +#7b010800030000100b7d +#7b010800040000a1ca7d +#7b0108000a0000c0097d +#7b0108000b000091c97d +#7b0108000c000020087d +#7b0108000d000071c87d +#7b0108000e000081c87d +#7b0108000f0000d0087d +#7b010800100000e1ce7d +#7b010800110000b00e7d +#7b010800120000400e7d +#7b01080013000011ce7d +#7b010800140000a00f7d +#7b010800150000f1cf7d From 26c7b04bd48e1f5be0fc9aeb51e2088d81f5cdcd Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 23 Jul 2012 21:24:30 -0700 Subject: [PATCH 086/243] updating documentation and being pedantic --- examples/common/asynchronous-server.py | 22 ++++++++++++++---- examples/common/synchronous-server.py | 20 +++++++++++++--- pymodbus/server/async.py | 32 ++++++++++++++------------ pymodbus/server/sync.py | 32 ++++++++++++-------------- 4 files changed, 67 insertions(+), 39 deletions(-) diff --git a/examples/common/asynchronous-server.py b/examples/common/asynchronous-server.py index 4163e7547..fb4fba3e7 100755 --- a/examples/common/asynchronous-server.py +++ b/examples/common/asynchronous-server.py @@ -14,6 +14,7 @@ from pymodbus.server.async import StartUdpServer from pymodbus.server.async import StartSerialServer +from pymodbus.device import ModbusDeviceIdentification from pymodbus.datastore import ModbusSequentialDataBlock from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer @@ -79,10 +80,23 @@ ir = ModbusSequentialDataBlock(0, [17]*100)) context = ModbusServerContext(slaves=store, single=True) +#---------------------------------------------------------------------------# +# initialize the server information +#---------------------------------------------------------------------------# +# If you don't set this or any fields, they are defaulted to empty strings. +#---------------------------------------------------------------------------# +identity = ModbusDeviceIdentification() +identity.VendorName = 'Pymodbus' +identity.ProductCode = 'PM' +identity.VendorUrl = 'http://github.com/bashwork/pymobdbus/' +identity.ProductName = 'Pymodbus Server' +identity.ModelName = 'Pymodbus Server' +identity.MajorMinorRevision = '1.0' + #---------------------------------------------------------------------------# # run the server you want #---------------------------------------------------------------------------# -StartTcpServer(context) -#StartUdpServer(context) -#StartSerialServer(context, port='/dev/pts/3', framer=ModbusRtuFramer) -#StartSerialServer(context, port='/dev/pts/3', framer=ModbusAsciiFramer) +StartTcpServer(context, identity=identity, address=("localhost", 5020)) +#StartUdpServer(context, identity=identity, address=("localhost", 502)) +#StartSerialServer(context, identity=identity, port='/dev/pts/3', framer=ModbusRtuFramer) +#StartSerialServer(context, identity=identity, port='/dev/pts/3', framer=ModbusAsciiFramer) diff --git a/examples/common/synchronous-server.py b/examples/common/synchronous-server.py index 404afb189..04dd590d5 100755 --- a/examples/common/synchronous-server.py +++ b/examples/common/synchronous-server.py @@ -15,6 +15,7 @@ from pymodbus.server.sync import StartUdpServer from pymodbus.server.sync import StartSerialServer +from pymodbus.device import ModbusDeviceIdentification from pymodbus.datastore import ModbusSequentialDataBlock from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext @@ -79,9 +80,22 @@ ir = ModbusSequentialDataBlock(0, [17]*100)) context = ModbusServerContext(slaves=store, single=True) +#---------------------------------------------------------------------------# +# initialize the server information +#---------------------------------------------------------------------------# +# If you don't set this or any fields, they are defaulted to empty strings. +#---------------------------------------------------------------------------# +identity = ModbusDeviceIdentification() +identity.VendorName = 'Pymodbus' +identity.ProductCode = 'PM' +identity.VendorUrl = 'http://github.com/bashwork/pymobdbus/' +identity.ProductName = 'Pymodbus Server' +identity.ModelName = 'Pymodbus Server' +identity.MajorMinorRevision = '1.0' + #---------------------------------------------------------------------------# # run the server you want #---------------------------------------------------------------------------# -#StartTcpServer(context) -#StartUdpServer(context) -StartSerialServer(context, port='/dev/pts/3', timeout=1) +StartTcpServer(context, identity=identity, address=("localhost", 502)) +#StartUdpServer(context, identity=identity, address=("localhost", 502)) +#StartSerialServer(context, identity=identity, port='/dev/pts/3', timeout=1) diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index a6e61177f..28a09e058 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -187,38 +187,40 @@ def _send(self, message, addr): #---------------------------------------------------------------------------# # Starting Factories #---------------------------------------------------------------------------# -def StartTcpServer(context, identity=None, server_address=None): +def StartTcpServer(context, identity=None, address=None): ''' Helper method to start the Modbus Async TCP server :param context: The server data context :param identify: The server identity to use (default empty) - :param server_address: An optional (interface,port) to bind to. + :param address: An optional (interface, port) to bind to. ''' from twisted.internet import reactor - if not server_address: - server_address = ("", Defaults.Port) - _logger.info("Starting Modbus TCP Server on %s:%s" % server_address) - framer = ModbusSocketFramer + + address = address or ("", Defaults.Port) + framer = ModbusSocketFramer factory = ModbusServerFactory(context, framer, identity) InstallManagementConsole({'factory': factory}) - reactor.listenTCP(server_address[1], factory, interface=server_address[0]) + + _logger.info("Starting Modbus TCP Server on %s:%s" % address) + reactor.listenTCP(address[1], factory, interface=address[0]) reactor.run() -def StartUdpServer(context, identity=None, server_address=None): +def StartUdpServer(context, identity=None, address=None): ''' Helper method to start the Modbus Async Udp server :param context: The server data context :param identify: The server identity to use (default empty) - :param server_address: An optional (interface,port) to bind to. + :param address: An optional (interface, port) to bind to. ''' from twisted.internet import reactor - if not server_address: - server_address = ("", Defaults.Port) - _logger.info("Starting Modbus UDP Server on %s:%s" % server_address) - framer = ModbusSocketFramer - server = ModbusUdpProtocol(context, framer, identity) - reactor.listenUDP(server_address[1], server, interface=server_address[0]) + + address = address or ("", Defaults.Port) + framer = ModbusSocketFramer + server = ModbusUdpProtocol(context, framer, identity) + + _logger.info("Starting Modbus UDP Server on %s:%s" % address) + reactor.listenUDP(address[1], server, interface=address[0]) reactor.run() diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index 44eb1efb0..d63ed22a4 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -201,8 +201,7 @@ class ModbusTcpServer(SocketServer.ThreadingTCPServer): server context instance. ''' - def __init__(self, context, framer=None, identity=None, - server_address=None): + def __init__(self, context, framer=None, identity=None, address=None): ''' Overloaded initializer for the socket server If the identify structure is not passed in, the ModbusControlBlock @@ -211,20 +210,20 @@ def __init__(self, context, framer=None, identity=None, :param context: The ModbusServerContext datastore :param framer: The framer strategy to use :param identity: An optional identify structure - :param server_address: An optional (interface,port) to bind to. + :param address: An optional (interface, port) to bind to. ''' self.threads = [] self.decoder = ServerDecoder() self.framer = framer or ModbusSocketFramer self.context = context or ModbusServerContext() self.control = ModbusControlBlock() + self.address = address or ("", Defaults.Port) if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) SocketServer.ThreadingTCPServer.__init__(self, - server_address or ("", Defaults.Port), - ModbusConnectedRequestHandler) + self.address, ModbusConnectedRequestHandler) def process_request(self, request, client): ''' Callback for connecting a new client thread @@ -253,8 +252,7 @@ class ModbusUdpServer(SocketServer.ThreadingUDPServer): server context instance. ''' - def __init__(self, context, framer=None, identity=None, - server_address=None): + def __init__(self, context, framer=None, identity=None, address=None): ''' Overloaded initializer for the socket server If the identify structure is not passed in, the ModbusControlBlock @@ -263,20 +261,20 @@ def __init__(self, context, framer=None, identity=None, :param context: The ModbusServerContext datastore :param framer: The framer strategy to use :param identity: An optional identify structure - :param server_address: An optional (interface,port) to bind to. + :param address: An optional (interface, port) to bind to. ''' self.threads = [] self.decoder = ServerDecoder() self.framer = framer or ModbusSocketFramer self.context = context or ModbusServerContext() self.control = ModbusControlBlock() + self.address = address or ("", Defaults.Port) if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) - SocketServer.ThreadingUDPServer.__init__( - self, server_address or ("", Defaults.Port), - ModbusDisconnectedRequestHandler) + SocketServer.ThreadingUDPServer.__init__(self, + self.address, ModbusDisconnectedRequestHandler) def process_request(self, request, client): ''' Callback for connecting a new client thread @@ -391,27 +389,27 @@ def server_close(self): #---------------------------------------------------------------------------# # Creation Factories #---------------------------------------------------------------------------# -def StartTcpServer(context=None, identity=None, server_address=None): +def StartTcpServer(context=None, identity=None, address=None): ''' A factory to start and run a tcp modbus server :param context: The ModbusServerContext datastore :param identity: An optional identify structure - :param server_address: An optional (interface,port) to bind to. + :param address: An optional (interface, port) to bind to. ''' framer = ModbusSocketFramer - server = ModbusTcpServer(context, framer, identity, server_address) + server = ModbusTcpServer(context, framer, identity, address) server.serve_forever() -def StartUdpServer(context=None, identity=None, server_address=None): +def StartUdpServer(context=None, identity=None, address=None): ''' A factory to start and run a udp modbus server :param context: The ModbusServerContext datastore :param identity: An optional identify structure - :param server_address: An optional (interface,port) to bind to. + :param address: An optional (interface, port) to bind to. ''' framer = ModbusSocketFramer - server = ModbusUdpServer(context, framer, identity, server_address) + server = ModbusUdpServer(context, framer, identity, address) server.serve_forever() From 0bf397490e0df65f320e2d7c45fe9c7b01afcd2d Mon Sep 17 00:00:00 2001 From: bashwork Date: Thu, 27 Sep 2012 12:47:05 -0500 Subject: [PATCH 087/243] Allowing overloading of message encoding * allow codes like payload builder to encode * added IPayloadBuilder interface (future) * renamed builder methods to reflect vision * added error code decoding to name * fixed affected tests --- doc/quality/current.coverage | 30 +++++++++--------- doc/sphinx/library/interfaces.rst | 3 ++ doc/sphinx/library/payload.rst | 4 +-- examples/common/modbus-payload.py | 44 +++++++++++++++++---------- pymodbus/client/common.py | 49 ++++++++++++++---------------- pymodbus/interfaces.py | 20 ++++++++++++ pymodbus/payload.py | 39 ++++++++++++------------ pymodbus/pdu.py | 26 ++++++++++++++-- pymodbus/register_write_message.py | 12 ++++++-- test/test_interfaces.py | 6 ++++ test/test_payload.py | 42 +++++++++++++------------ test/test_pdu.py | 8 ++--- test/test_server_async.py | 2 +- 13 files changed, 176 insertions(+), 109 deletions(-) diff --git a/doc/quality/current.coverage b/doc/quality/current.coverage index 552e95f10..376726edb 100644 --- a/doc/quality/current.coverage +++ b/doc/quality/current.coverage @@ -1,12 +1,12 @@ Name Stmts Miss Cover Missing --------------------------------------------------------------- -pymodbus 15 2 87% 36-37 +pymodbus 15 6 60% 24-27, 36-37 pymodbus.bit_read_message 68 0 100% pymodbus.bit_write_message 95 0 100% pymodbus.client 0 0 100% -pymodbus.client.async 68 0 100% -pymodbus.client.common 45 0 100% -pymodbus.client.sync 148 0 100% +pymodbus.client.async 70 0 100% +pymodbus.client.common 36 0 100% +pymodbus.client.sync 147 0 100% pymodbus.constants 36 0 100% pymodbus.datastore 5 0 100% pymodbus.datastore.context 50 0 100% @@ -18,24 +18,24 @@ pymodbus.events 60 0 100% pymodbus.exceptions 22 0 100% pymodbus.factory 77 0 100% pymodbus.file_message 181 0 100% -pymodbus.interfaces 43 0 100% +pymodbus.interfaces 46 0 100% pymodbus.internal 0 0 100% -pymodbus.internal.ptwisted 16 10 38% 26-37 -pymodbus.mei_message 68 0 100% +pymodbus.internal.ptwisted 16 2 88% 29-30 +pymodbus.mei_message 70 0 100% pymodbus.other_message 145 0 100% -pymodbus.payload 134 0 100% -pymodbus.pdu 66 0 100% +pymodbus.payload 140 2 99% 205, 224 +pymodbus.pdu 72 0 100% pymodbus.register_read_message 124 0 100% -pymodbus.register_write_message 87 0 100% +pymodbus.register_write_message 91 2 98% 39, 148 pymodbus.server 0 0 100% -pymodbus.server.async 107 75 30% 40-41, 48, 55-57, 64-73, 80-84, 108-115, 135-142, 149-153, 160-169, 177-180, 192-199, 208-214, 227-238 -pymodbus.server.sync 184 0 100% -pymodbus.transaction 263 51 81% 54-72, 240, 244, 384, 414-423, 558-567, 637, 714-723, 749-750 +pymodbus.server.async 113 39 65% 55-58, 65-74, 81-86, 151-156, 163-172, 180-184 +pymodbus.server.sync 186 0 100% +pymodbus.transaction 275 53 81% 63-81, 116-117, 259, 263, 403, 433-442, 577-586, 656, 733-742, 768-769 pymodbus.utilities 67 0 100% pymodbus.version 13 0 100% --------------------------------------------------------------- -TOTAL 2646 138 95% +TOTAL 2679 104 96% ---------------------------------------------------------------------- -Ran 249 tests in 1.448s +Ran 255 tests in 0.981s OK diff --git a/doc/sphinx/library/interfaces.rst b/doc/sphinx/library/interfaces.rst index 9c5a9c3ec..cb10d0c62 100644 --- a/doc/sphinx/library/interfaces.rst +++ b/doc/sphinx/library/interfaces.rst @@ -23,3 +23,6 @@ API Documentation .. autoclass:: IModbusSlaveContext :members: + +.. autoclass:: IPayloadBuilder + :members: diff --git a/doc/sphinx/library/payload.rst b/doc/sphinx/library/payload.rst index d81f80312..4083aa2ca 100644 --- a/doc/sphinx/library/payload.rst +++ b/doc/sphinx/library/payload.rst @@ -12,8 +12,8 @@ API Documentation .. automodule:: pymodbus.payload -.. autoclass:: PayloadBuilder +.. autoclass:: BinaryPayloadBuilder :members: -.. autoclass:: PayloadDecoder +.. autoclass:: BinaryPayloadDecoder :members: diff --git a/examples/common/modbus-payload.py b/examples/common/modbus-payload.py index 46072ff7a..b24b4a1ba 100755 --- a/examples/common/modbus-payload.py +++ b/examples/common/modbus-payload.py @@ -4,10 +4,18 @@ -------------------------------------------------------------------------- ''' from pymodbus.constants import Endian -from pymodbus.payload import PayloadDecoder -from pymodbus.payload import PayloadBuilder +from pymodbus.payload import BinaryPayloadDecoder +from pymodbus.payload import BinaryPayloadBuilder from pymodbus.client.sync import ModbusTcpClient as ModbusClient +#---------------------------------------------------------------------------# +# configure the client logging +#---------------------------------------------------------------------------# +import logging +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.INFO) + #---------------------------------------------------------------------------# # We are going to use a simple client to send our requests #---------------------------------------------------------------------------# @@ -27,15 +35,15 @@ # - an 8 bit int 0x12 # - an 8 bit bitstring [0,1,0,1,1,0,1,0] #---------------------------------------------------------------------------# -builder = PayloadBuilder(endian=Endian.Little) +builder = BinaryPayloadBuilder(endian=Endian.Little) builder.add_string('abcdefgh') builder.add_32bit_float(22.34) builder.add_16bit_uint(0x1234) builder.add_8bit_int(0x12) -builder.add_bites([0,1,0,1,1,0,1,0]) -payload = builder.tolist() +builder.add_bits([0,1,0,1,1,0,1,0]) +payload = builder.build() address = 0x01 -result = client.write_registers(address, payload) +result = client.write_registers(address, payload, skip_encode=True) #---------------------------------------------------------------------------# # If you need to decode a collection of registers in a weird layout, the @@ -53,16 +61,20 @@ address = 0x01 count = 8 result = client.read_input_registers(address, count) -decoder = PayloadDecoder.fromRegisters(result.registers, endian=Endian.Little) -decoded = [ - decoder.decode_string(8), - decoder.decode_32bit_float(), - decoder.decode_16bit_uint(), - decoder.decode_8bit_int(), - decoder.decode_bits(), -] -for decode in decoded: - print decode +decoder = BinaryPayloadDecoder.fromRegisters(result.registers, endian=Endian.Little) +decoded = { + 'string': decoder.decode_string(8), + 'float': decoder.decode_32bit_float(), + '16uint': decoder.decode_16bit_uint(), + '8int': decoder.decode_8bit_int(), + 'bits': decoder.decode_bits(), +} + +print "-" * 60 +print "Decoded Data" +print "-" * 60 +for name, value in decoded.items(): + print ("%s\t" % name), value #---------------------------------------------------------------------------# # close the client diff --git a/pymodbus/client/common.py b/pymodbus/client/common.py index 25e3b66fd..4b0caa143 100644 --- a/pymodbus/client/common.py +++ b/pymodbus/client/common.py @@ -13,7 +13,6 @@ from pymodbus.diag_message import * from pymodbus.file_message import * from pymodbus.other_message import * -from pymodbus.constants import Defaults class ModbusClientMixin(object): @@ -32,7 +31,7 @@ class ModbusClientMixin(object): response = client.read_coils(1, 10) ''' - def read_coils(self, address, count=1, unit=Defaults.UnitId): + def read_coils(self, address, count=1, **kwargs): ''' :param address: The starting address to read from @@ -40,11 +39,10 @@ def read_coils(self, address, count=1, unit=Defaults.UnitId): :param unit: The slave unit this request is targeting :returns: A deferred response handle ''' - request = ReadCoilsRequest(address, count) - request.unit_id = unit + request = ReadCoilsRequest(address, count, **kwargs) return self.execute(request) - def read_discrete_inputs(self, address, count=1, unit=Defaults.UnitId): + def read_discrete_inputs(self, address, count=1, **kwargs): ''' :param address: The starting address to read from @@ -52,11 +50,10 @@ def read_discrete_inputs(self, address, count=1, unit=Defaults.UnitId): :param unit: The slave unit this request is targeting :returns: A deferred response handle ''' - request = ReadDiscreteInputsRequest(address, count) - request.unit_id = unit + request = ReadDiscreteInputsRequest(address, count, **kwargs) return self.execute(request) - def write_coil(self, address, value, unit=Defaults.UnitId): + def write_coil(self, address, value, **kwargs): ''' :param address: The starting address to write to @@ -64,11 +61,10 @@ def write_coil(self, address, value, unit=Defaults.UnitId): :param unit: The slave unit this request is targeting :returns: A deferred response handle ''' - request = WriteSingleCoilRequest(address, value) - request.unit_id = unit + request = WriteSingleCoilRequest(address, value, **kwargs) return self.execute(request) - def write_coils(self, address, values, unit=Defaults.UnitId): + def write_coils(self, address, values, **kwargs): ''' :param address: The starting address to write to @@ -76,11 +72,10 @@ def write_coils(self, address, values, unit=Defaults.UnitId): :param unit: The slave unit this request is targeting :returns: A deferred response handle ''' - request = WriteMultipleCoilsRequest(address, values) - request.unit_id = unit + request = WriteMultipleCoilsRequest(address, values, **kwargs) return self.execute(request) - def write_register(self, address, value, unit=Defaults.UnitId): + def write_register(self, address, value, **kwargs): ''' :param address: The starting address to write to @@ -88,11 +83,10 @@ def write_register(self, address, value, unit=Defaults.UnitId): :param unit: The slave unit this request is targeting :returns: A deferred response handle ''' - request = WriteSingleRegisterRequest(address, value) - request.unit_id = unit + request = WriteSingleRegisterRequest(address, value, **kwargs) return self.execute(request) - def write_registers(self, address, values, unit=Defaults.UnitId): + def write_registers(self, address, values, **kwargs): ''' :param address: The starting address to write to @@ -100,11 +94,10 @@ def write_registers(self, address, values, unit=Defaults.UnitId): :param unit: The slave unit this request is targeting :returns: A deferred response handle ''' - request = WriteMultipleRegistersRequest(address, values) - request.unit_id = unit + request = WriteMultipleRegistersRequest(address, values, **kwargs) return self.execute(request) - def read_holding_registers(self, address, count=1, unit=Defaults.UnitId): + def read_holding_registers(self, address, count=1, **kwargs): ''' :param address: The starting address to read from @@ -112,11 +105,10 @@ def read_holding_registers(self, address, count=1, unit=Defaults.UnitId): :param unit: The slave unit this request is targeting :returns: A deferred response handle ''' - request = ReadHoldingRegistersRequest(address, count) - request.unit_id = unit + request = ReadHoldingRegistersRequest(address, count, **kwargs) return self.execute(request) - def read_input_registers(self, address, count=1, unit=Defaults.UnitId): + def read_input_registers(self, address, count=1, **kwargs): ''' :param address: The starting address to read from @@ -124,20 +116,23 @@ def read_input_registers(self, address, count=1, unit=Defaults.UnitId): :param unit: The slave unit this request is targeting :returns: A deferred response handle ''' - request = ReadInputRegistersRequest(address, count) - request.unit_id = unit + request = ReadInputRegistersRequest(address, count, **kwargs) return self.execute(request) def readwrite_registers(self, *args, **kwargs): ''' - :param unit: The slave unit this request is targeting :param read_address: The address to start reading from :param read_count: The number of registers to read from address :param write_address: The address to start writing to :param write_registers: The registers to write to the specified address + :param unit: The slave unit this request is targeting :returns: A deferred response handle ''' request = ReadWriteMultipleRegistersRequest(*args, **kwargs) - request.unit_id = kwargs.get('unit', Defaults.UnitId) return self.execute(request) + +#---------------------------------------------------------------------------# +# Exported symbols +#---------------------------------------------------------------------------# +__all__ = [ 'ModbusClientMixin' ] diff --git a/pymodbus/interfaces.py b/pymodbus/interfaces.py index 4509a95d0..8587e94e2 100644 --- a/pymodbus/interfaces.py +++ b/pymodbus/interfaces.py @@ -208,10 +208,30 @@ def setValues(self, fx, address, values): ''' raise NotImplementedException("set context values") + +class IPayloadBuilder(object): + ''' + This is an interface to a class that can build a payload + for a modbus register write command. It should abstract + the codec for encoding data to the required format + (bcd, binary, char, etc). + ''' + + def build(self): + ''' Return the payload buffer as a list + + This list is two bytes per element and can + thus be treated as a list of registers. + + :returns: The payload buffer as a list + ''' + raise NotImplementedException("set context values") + #---------------------------------------------------------------------------# # Exported symbols #---------------------------------------------------------------------------# __all__ = [ 'Singleton', 'IModbusDecoder', 'IModbusFramer', 'IModbusSlaveContext', + 'IPayloadBuilder', ] diff --git a/pymodbus/payload.py b/pymodbus/payload.py index 6e0f9a848..2a617976d 100644 --- a/pymodbus/payload.py +++ b/pymodbus/payload.py @@ -6,13 +6,14 @@ modbus messages payloads. ''' from struct import pack, unpack +from pymodbus.interfaces import IPayloadBuilder from pymodbus.constants import Endian from pymodbus.utilities import pack_bitstring from pymodbus.utilities import unpack_bitstring from pymodbus.exceptions import ParameterException -class PayloadBuilder(object): +class BinaryPayloadBuilder(IPayloadBuilder): ''' A utility that helps build payload messages to be written with the various modbus messages. It really is just @@ -20,10 +21,10 @@ class PayloadBuilder(object): time looking up the format strings. What follows is a simple example:: - builder = PayloadBuilder(endian=Endian.Little) + builder = BinaryPayloadBuilder(endian=Endian.Little) builder.add_8bit_uint(1) builder.add_16bit_uint(2) - payload = builder.tostring() + payload = builder.build() ''' def __init__(self, payload=None, endian=Endian.Little): @@ -35,19 +36,19 @@ def __init__(self, payload=None, endian=Endian.Little): self._payload = payload or [] self._endian = endian - def reset(self): - ''' Reset the payload buffer - ''' - self._payload = [] - - def tostring(self): + def __str__(self): ''' Return the payload buffer as a string :returns: The payload buffer as a string ''' return ''.join(self._payload) - def tolist(self): + def reset(self): + ''' Reset the payload buffer + ''' + self._payload = [] + + def build(self): ''' Return the payload buffer as a list This list is two bytes per element and can @@ -55,7 +56,7 @@ def tolist(self): :returns: The payload buffer as a list ''' - string = self.tostring() + string = str(self) length = len(string) string = string + ('\x00' * (length % 2)) return [string[i:i+2] for i in xrange(0, length, 2)] @@ -163,7 +164,7 @@ def add_string(self, value): self._payload.append(pack(fstring, c)) -class PayloadDecoder(object): +class BinaryPayloadDecoder(object): ''' A utility that helps decode payload messages from a modbus reponse message. It really is just a simple wrapper around @@ -191,15 +192,16 @@ def fromRegisters(registers, endian=Endian.Little): reading a collection of registers from a modbus device. The registers are treated as a list of 2 byte values. + We have to do this because of how the data has already + been decoded by the rest of the library. :param registers: The register results to initialize with :param endian: The endianess of the payload :returns: An initialized PayloadDecoder ''' - fstring = endian + 'H' - if isinstance(registers, list): - payload = ''.join(pack(fstring, x) for x in registers) - return PayloadDecoder(payload, endian) + if isinstance(registers, list): # repack into flat binary + payload = ''.join(pack('>H', x) for x in registers) + return BinaryPayloadDecoder(payload, endian) raise ParameterException('Invalid collection of registers supplied') @staticmethod @@ -214,9 +216,8 @@ def fromCoils(coils, endian=Endian.Little): :returns: An initialized PayloadDecoder ''' if isinstance(coils, list): -# TODO endianess issue here payload = pack_bitstring(coils) - return PayloadDecoder(payload, endian) + return BinaryPayloadDecoder(payload, endian) raise ParameterException('Invalid collection of coils supplied') def reset(self): @@ -324,4 +325,4 @@ def decode_string(self, size=1): #---------------------------------------------------------------------------# # Exported Identifiers #---------------------------------------------------------------------------# -__all__ = ["PayloadBuilder", "PayloadDecoder"] +__all__ = ["BinaryPayloadBuilder", "BinaryPayloadDecoder"] diff --git a/pymodbus/pdu.py b/pymodbus/pdu.py index 6c21b8f00..6090ac075 100644 --- a/pymodbus/pdu.py +++ b/pymodbus/pdu.py @@ -41,6 +41,14 @@ class ModbusPDU(object): .. attribute:: check This is used for LRC/CRC in the serial modbus protocols + + .. attribute:: skip_encode + + This is used when the message payload has already been encoded. + Generally this will occur when the PayloadBuilder is being used + to create a complicated message. By setting this to True, the + request will pass the currently encoded message through instead + of encoding it again. ''' def __init__(self, **kwargs): @@ -48,6 +56,7 @@ def __init__(self, **kwargs): self.transaction_id = kwargs.get('transaction', Defaults.TransactionId) self.protocol_id = kwargs.get('protocol', Defaults.ProtocolId) self.unit_id = kwargs.get('unit', Defaults.UnitId) + self.skip_encode = kwargs.get('skip_encode', False) self.check = 0x0000 def encode(self): @@ -136,6 +145,17 @@ class ModbusExceptions(Singleton): GatewayPathUnavailable = 0x0A GatewayNoResponse = 0x0B + @classmethod + def decode(cls, code): + ''' Given an error code, translate it to a + string error name. + + :param code: The code number to translate + ''' + values = dict((v, k) for k, v in cls.__dict__.items() + if not k.startswith('__') and not callable(v)) + return values.get(code, None) + class ExceptionResponse(ModbusResponse): ''' Base class for a modbus exception PDU ''' @@ -149,6 +169,7 @@ def __init__(self, function_code, exception_code=None, **kwargs): :param exception_code: The specific modbus exception to return ''' ModbusResponse.__init__(self, **kwargs) + self.original_code = function_code self.function_code = function_code | self.ExceptionOffset self.exception_code = exception_code @@ -171,8 +192,9 @@ def __str__(self): :returns: The string representation of an exception response ''' - parameters = (self.function_code, self.exception_code) - return "Exception Response (%d, %d)" % parameters + message = ModbusExceptions.decode(self.exception_code) + parameters = (self.function_code, self.original_code, message) + return "Exception Response(%d, %d, %s)" % parameters class IllegalFunctionRequest(ModbusRequest): diff --git a/pymodbus/register_write_message.py b/pymodbus/register_write_message.py index d3bd7b0a8..a1dcf91f7 100644 --- a/pymodbus/register_write_message.py +++ b/pymodbus/register_write_message.py @@ -35,6 +35,8 @@ def encode(self): :returns: The encoded packet ''' + if self.skip_encode: + return self.value return struct.pack('>HH', self.address, self.value) def decode(self, data): @@ -130,9 +132,9 @@ def __init__(self, address=None, values=None, **kwargs): ''' ModbusRequest.__init__(self, **kwargs) self.address = address - if not values: values = [] - elif not hasattr(values, '__iter__'): values = [values] - self.values = values + self.values = values or [] + if not hasattr(values, '__iter__'): + values = [values] self.count = len(self.values) self.byte_count = self.count * 2 @@ -142,8 +144,12 @@ def encode(self): :returns: The encoded packet ''' packet = struct.pack('>HHB', self.address, self.count, self.byte_count) + if self.skip_encode: + return packet + ''.join(self.values) + for value in self.values: packet += struct.pack('>H', value) + return packet def decode(self, data): diff --git a/test/test_interfaces.py b/test/test_interfaces.py index c9cb84a2b..093f9453d 100644 --- a/test/test_interfaces.py +++ b/test/test_interfaces.py @@ -54,6 +54,12 @@ def testModbusSlaveContextInterface(self): self.assertRaises(NotImplementedException, lambda: instance.getValues(x,x,x)) self.assertRaises(NotImplementedException, lambda: instance.setValues(x,x,x)) + def testModbusPayloadBuilderInterface(self): + ''' Test that the base class isn't implemented ''' + x = None + instance = IPayloadBuilder() + self.assertRaises(NotImplementedException, lambda: instance.build()) + #---------------------------------------------------------------------------# # Main #---------------------------------------------------------------------------# diff --git a/test/test_payload.py b/test/test_payload.py index 8d3ee33e9..453d82fce 100644 --- a/test/test_payload.py +++ b/test/test_payload.py @@ -11,7 +11,7 @@ import unittest from pymodbus.exceptions import ParameterException from pymodbus.constants import Endian -from pymodbus.payload import PayloadBuilder, PayloadDecoder +from pymodbus.payload import BinaryPayloadBuilder, BinaryPayloadDecoder #---------------------------------------------------------------------------# # Fixture @@ -51,7 +51,7 @@ def tearDown(self): def testLittleEndianPayloadBuilder(self): ''' Test basic bit message encoding/decoding ''' - builder = PayloadBuilder(endian=Endian.Little) + builder = BinaryPayloadBuilder(endian=Endian.Little) builder.add_8bit_uint(1) builder.add_16bit_uint(2) builder.add_32bit_uint(3) @@ -64,11 +64,11 @@ def testLittleEndianPayloadBuilder(self): builder.add_64bit_float(6.25) builder.add_string('test') builder.add_bits(self.bitstring) - self.assertEqual(self.little_endian_payload, builder.tostring()) + self.assertEqual(self.little_endian_payload, str(builder)) def testBigEndianPayloadBuilder(self): ''' Test basic bit message encoding/decoding ''' - builder = PayloadBuilder(endian=Endian.Big) + builder = BinaryPayloadBuilder(endian=Endian.Big) builder.add_8bit_uint(1) builder.add_16bit_uint(2) builder.add_32bit_uint(3) @@ -81,20 +81,20 @@ def testBigEndianPayloadBuilder(self): builder.add_64bit_float(6.25) builder.add_string('test') builder.add_bits(self.bitstring) - self.assertEqual(self.big_endian_payload, builder.tostring()) + self.assertEqual(self.big_endian_payload, str(builder)) def testPayloadBuilderReset(self): ''' Test basic bit message encoding/decoding ''' - builder = PayloadBuilder() + builder = BinaryPayloadBuilder() builder.add_8bit_uint(0x12) builder.add_8bit_uint(0x34) builder.add_8bit_uint(0x56) builder.add_8bit_uint(0x78) - self.assertEqual('\x12\x34\x56\x78', builder.tostring()) - self.assertEqual(['\x12\x34', '\x56\x78'], builder.tolist()) + self.assertEqual('\x12\x34\x56\x78', str(builder)) + self.assertEqual(['\x12\x34', '\x56\x78'], builder.build()) builder.reset() - self.assertEqual('', builder.tostring()) - self.assertEqual([], builder.tolist()) + self.assertEqual('', str(builder)) + self.assertEqual([], builder.build()) #-----------------------------------------------------------------------# # Payload Decoder Tests @@ -102,7 +102,7 @@ def testPayloadBuilderReset(self): def testLittleEndianPayloadDecoder(self): ''' Test basic bit message encoding/decoding ''' - decoder = PayloadDecoder(self.little_endian_payload, endian=Endian.Little) + decoder = BinaryPayloadDecoder(self.little_endian_payload, endian=Endian.Little) self.assertEqual(1, decoder.decode_8bit_uint()) self.assertEqual(2, decoder.decode_16bit_uint()) self.assertEqual(3, decoder.decode_32bit_uint()) @@ -118,7 +118,7 @@ def testLittleEndianPayloadDecoder(self): def testBigEndianPayloadDecoder(self): ''' Test basic bit message encoding/decoding ''' - decoder = PayloadDecoder(self.big_endian_payload, endian=Endian.Big) + decoder = BinaryPayloadDecoder(self.big_endian_payload, endian=Endian.Big) self.assertEqual(1, decoder.decode_8bit_uint()) self.assertEqual(2, decoder.decode_16bit_uint()) self.assertEqual(3, decoder.decode_32bit_uint()) @@ -134,7 +134,7 @@ def testBigEndianPayloadDecoder(self): def testPayloadDecoderReset(self): ''' Test the payload decoder reset functionality ''' - decoder = PayloadDecoder('\x12\x34') + decoder = BinaryPayloadDecoder('\x12\x34') self.assertEqual(0x12, decoder.decode_8bit_uint()) self.assertEqual(0x34, decoder.decode_8bit_uint()) decoder.reset() @@ -143,28 +143,30 @@ def testPayloadDecoderReset(self): def testPayloadDecoderRegisterFactory(self): ''' Test the payload decoder reset functionality ''' payload = [1,2,3,4] - decoder = PayloadDecoder.fromRegisters(payload, endian=Endian.Little) - encoded = '\x01\x00\x02\x00\x03\x00\x04\x00' + decoder = BinaryPayloadDecoder.fromRegisters(payload, endian=Endian.Little) + encoded = '\x00\x01\x00\x02\x00\x03\x00\x04' self.assertEqual(encoded, decoder.decode_string(8)) - decoder = PayloadDecoder.fromRegisters(payload, endian=Endian.Big) + decoder = BinaryPayloadDecoder.fromRegisters(payload, endian=Endian.Big) encoded = '\x00\x01\x00\x02\x00\x03\x00\x04' self.assertEqual(encoded, decoder.decode_string(8)) - self.assertRaises(ParameterException, lambda: PayloadDecoder.fromRegisters('abcd')) + self.assertRaises(ParameterException, + lambda: BinaryPayloadDecoder.fromRegisters('abcd')) def testPayloadDecoderCoilFactory(self): ''' Test the payload decoder reset functionality ''' payload = [1,0,0,0, 1,0,0,0, 0,0,0,1, 0,0,0,1] - decoder = PayloadDecoder.fromCoils(payload, endian=Endian.Little) + decoder = BinaryPayloadDecoder.fromCoils(payload, endian=Endian.Little) encoded = '\x11\x88' self.assertEqual(encoded, decoder.decode_string(2)) - decoder = PayloadDecoder.fromCoils(payload, endian=Endian.Big) + decoder = BinaryPayloadDecoder.fromCoils(payload, endian=Endian.Big) encoded = '\x11\x88' self.assertEqual(encoded, decoder.decode_string(2)) - self.assertRaises(ParameterException, lambda: PayloadDecoder.fromCoils('abcd')) + self.assertRaises(ParameterException, + lambda: BinaryPayloadDecoder.fromCoils('abcd')) #---------------------------------------------------------------------------# diff --git a/test/test_pdu.py b/test/test_pdu.py index bc0cc2189..a09468d77 100644 --- a/test/test_pdu.py +++ b/test/test_pdu.py @@ -46,10 +46,10 @@ def testRequestExceptionFactory(self): ''' Test all error methods ''' request = ModbusRequest() request.function_code = 1 - for error in [getattr(ModbusExceptions, i) - for i in dir(ModbusExceptions) if '__' not in i]: - result = request.doException(error) - self.assertEqual(str(result), "Exception Response (129, %d)" % error) + errors = dict((ModbusExceptions.decode(c), c) for c in range(1,20)) + for error, code in errors.items(): + result = request.doException(code) + self.assertEqual(str(result), "Exception Response(129, 1, %s)" % error) def testCalculateRtuFrameSize(self): ''' Test the calculation of Modbus/RTU frame sizes ''' diff --git a/test/test_server_async.py b/test/test_server_async.py index e7ec9c731..18438e3d1 100644 --- a/test/test_server_async.py +++ b/test/test_server_async.py @@ -84,7 +84,7 @@ def testUdpServerStartup(self): def testSerialServerStartup(self): ''' Test that the modbus serial async server starts correctly ''' with patch('twisted.internet.reactor') as mock_reactor: - StartSerialServer(context=None) + StartSerialServer(context=None, port='/dev/pts/0') self.assertEqual(mock_reactor.run.call_count, 1) #---------------------------------------------------------------------------# From d4791f934f5fdb7443fdc349183a41bb453ed4ce Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Tue, 2 Oct 2012 11:26:23 -0500 Subject: [PATCH 088/243] Cleaning up the build tools - moving custom datastores to examples - bumping required versions - making the debug server console optional - updating documentation --- doc/api/pydoc/build.py | 2 +- doc/sphinx/examples/database-datastore.rst | 6 ++++ doc/sphinx/examples/index.rst | 9 ++++++ doc/sphinx/examples/redis-datastore.rst | 6 ++++ doc/sphinx/library/datastore/database.rst | 16 ----------- doc/sphinx/library/datastore/index.rst | 2 -- doc/sphinx/library/datastore/modredis.rst | 16 ----------- examples/common/synchronous-client.py | 6 ++-- examples/datastore/README.rst | 25 ++++++++++++++++ {pymodbus => examples}/datastore/database.py | 0 {pymodbus => examples}/datastore/modredis.py | 0 examples/datastore/requirements.txt | 5 ++++ pymodbus/server/async.py | 9 ++++-- requirements.txt | 30 ++++++++++++++------ setup.py | 11 ++++--- test/test_server_async.py | 2 +- 16 files changed, 89 insertions(+), 56 deletions(-) create mode 100644 doc/sphinx/examples/database-datastore.rst create mode 100644 doc/sphinx/examples/redis-datastore.rst delete mode 100644 doc/sphinx/library/datastore/database.rst delete mode 100644 doc/sphinx/library/datastore/modredis.rst create mode 100644 examples/datastore/README.rst rename {pymodbus => examples}/datastore/database.py (100%) rename {pymodbus => examples}/datastore/modredis.py (100%) create mode 100644 examples/datastore/requirements.txt diff --git a/doc/api/pydoc/build.py b/doc/api/pydoc/build.py index 614985561..b61aa17b8 100644 --- a/doc/api/pydoc/build.py +++ b/doc/api/pydoc/build.py @@ -418,7 +418,7 @@ def process( self ): del self.pending[0] finally: for item in self.warnings: - log.info(item) + _log.info(item) def clean (self, objectList, object): """callback from the formatter object asking us to remove diff --git a/doc/sphinx/examples/database-datastore.rst b/doc/sphinx/examples/database-datastore.rst new file mode 100644 index 000000000..409c8cf7d --- /dev/null +++ b/doc/sphinx/examples/database-datastore.rst @@ -0,0 +1,6 @@ +================================================== +Database Datastore Example +================================================== + +.. literalinclude:: ../../../examples/datastore/database.py + diff --git a/doc/sphinx/examples/index.rst b/doc/sphinx/examples/index.rst index 607440eb0..63f06178f 100644 --- a/doc/sphinx/examples/index.rst +++ b/doc/sphinx/examples/index.rst @@ -28,6 +28,15 @@ Example Library Code synchronous-server performance +Custom Datastore Code +-------------------------------------------------- + +.. toctree:: + :maxdepth: 2 + + redis-datastore + database-datastore + Example Frontend Code -------------------------------------------------- diff --git a/doc/sphinx/examples/redis-datastore.rst b/doc/sphinx/examples/redis-datastore.rst new file mode 100644 index 000000000..3eb1657fb --- /dev/null +++ b/doc/sphinx/examples/redis-datastore.rst @@ -0,0 +1,6 @@ +================================================== +Redis Datastore Example +================================================== + +.. literalinclude:: ../../../examples/datastore/modredis.py + diff --git a/doc/sphinx/library/datastore/database.rst b/doc/sphinx/library/datastore/database.rst deleted file mode 100644 index 97daf8b31..000000000 --- a/doc/sphinx/library/datastore/database.rst +++ /dev/null @@ -1,16 +0,0 @@ -:mod:`database` --- Database Slave Context -============================================================ - -.. module:: database - :synopsis: Database Slave Context - -.. moduleauthor:: Galen Collins -.. sectionauthor:: Galen Collins - -API Documentation -------------------- - -.. automodule:: pymodbus.datastore.database - -.. autoclass:: DatabaseSlaveContext - :members: diff --git a/doc/sphinx/library/datastore/index.rst b/doc/sphinx/library/datastore/index.rst index b180bc352..6ea14b24e 100644 --- a/doc/sphinx/library/datastore/index.rst +++ b/doc/sphinx/library/datastore/index.rst @@ -11,5 +11,3 @@ from the sourcecode* store.rst context.rst remote.rst - database.rst - modredis.rst diff --git a/doc/sphinx/library/datastore/modredis.rst b/doc/sphinx/library/datastore/modredis.rst deleted file mode 100644 index 0b5c96b14..000000000 --- a/doc/sphinx/library/datastore/modredis.rst +++ /dev/null @@ -1,16 +0,0 @@ -:mod:`modredis` --- Redis Slave Context -============================================================ - -.. module:: modredis - :synopsis: Redis Slave Context - -.. moduleauthor:: Galen Collins -.. sectionauthor:: Galen Collins - -API Documentation -------------------- - -.. automodule:: pymodbus.datastore.modredis - -.. autoclass:: RedisSlaveContext - :members: diff --git a/examples/common/synchronous-client.py b/examples/common/synchronous-client.py index fc0d479e8..d334cb292 100755 --- a/examples/common/synchronous-client.py +++ b/examples/common/synchronous-client.py @@ -16,9 +16,9 @@ #---------------------------------------------------------------------------# # import the various server implementations #---------------------------------------------------------------------------# -#from pymodbus.client.sync import ModbusTcpClient as ModbusClient +from pymodbus.client.sync import ModbusTcpClient as ModbusClient #from pymodbus.client.sync import ModbusUdpClient as ModbusClient -from pymodbus.client.sync import ModbusSerialClient as ModbusClient +#from pymodbus.client.sync import ModbusSerialClient as ModbusClient #---------------------------------------------------------------------------# # configure the client logging @@ -38,7 +38,7 @@ # It should be noted that you can supply an ipv4 or an ipv6 host address for # both the UDP and TCP clients. #---------------------------------------------------------------------------# -client = ModbusClient('localhost') +client = ModbusClient('localhost', port=5020) #client = ModbusClient(method='ascii', port='/dev/pts/2', timeout=1) #client = ModbusClient(method='rtu', port='/dev/pts/2', timeout=1) client.connect() diff --git a/examples/datastore/README.rst b/examples/datastore/README.rst new file mode 100644 index 000000000..3710648da --- /dev/null +++ b/examples/datastore/README.rst @@ -0,0 +1,25 @@ +============================================================ +Custom Datastore Implementations +============================================================ + +There are a few example implementations of custom datastores +just to show what is possible. + +------------------------------------------------------------ +SqlAlchemy Backend +------------------------------------------------------------ + +This module allows one to use any database available through +the sqlalchemy package as a datastore for the modbus server. +This could be useful to have many servers who have data they +agree upon and is transactional. + +------------------------------------------------------------ +Redis Backend +------------------------------------------------------------ + +This module allows one to use redis as a modbus server +datastore backend. This achieves the same thing as the +sqlalchemy backend, however, it is much more lightweight and +easier to set up. + diff --git a/pymodbus/datastore/database.py b/examples/datastore/database.py similarity index 100% rename from pymodbus/datastore/database.py rename to examples/datastore/database.py diff --git a/pymodbus/datastore/modredis.py b/examples/datastore/modredis.py similarity index 100% rename from pymodbus/datastore/modredis.py rename to examples/datastore/modredis.py diff --git a/examples/datastore/requirements.txt b/examples/datastore/requirements.txt new file mode 100644 index 000000000..3c9b01bb0 --- /dev/null +++ b/examples/datastore/requirements.txt @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------- +# if you want to use the custom data stores, uncomment these +# ------------------------------------------------------------------- +#SQLAlchemy==0.7.9 +#redis==2.6.2 diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index 28a09e058..f0b1e395a 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -187,19 +187,20 @@ def _send(self, message, addr): #---------------------------------------------------------------------------# # Starting Factories #---------------------------------------------------------------------------# -def StartTcpServer(context, identity=None, address=None): +def StartTcpServer(context, identity=None, address=None, console=False): ''' Helper method to start the Modbus Async TCP server :param context: The server data context :param identify: The server identity to use (default empty) :param address: An optional (interface, port) to bind to. + :param console: A flag indicating if you want the debug console ''' from twisted.internet import reactor address = address or ("", Defaults.Port) framer = ModbusSocketFramer factory = ModbusServerFactory(context, framer, identity) - InstallManagementConsole({'factory': factory}) + if console: InstallManagementConsole({'factory': factory}) _logger.info("Starting Modbus TCP Server on %s:%s" % address) reactor.listenTCP(address[1], factory, interface=address[0]) @@ -233,15 +234,19 @@ def StartSerialServer(context, identity=None, :param framer: The framer to use (default ModbusAsciiFramer) :param port: The serial port to attach to :param baudrate: The baud rate to use for the serial device + :param console: A flag indicating if you want the debug console ''' from twisted.internet import reactor from twisted.internet.serialport import SerialPort port = kwargs.get('port', '/dev/ttyS0') baudrate = kwargs.get('baudrate', Defaults.Baudrate) + console = kwargs.get('console', False) _logger.info("Starting Modbus Serial Server on %s" % port) factory = ModbusServerFactory(context, framer, identity) + if console: InstallManagementConsole({'factory': factory}) + protocol = factory.buildProtocol(None) SerialPort.getHost = lambda self: port # hack for logging handle = SerialPort(protocol, port, reactor, baudrate) diff --git a/requirements.txt b/requirements.txt index 76f54d308..39d95377b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,26 @@ -distribute==0.6.24 -pyserial==2.5 +# ------------------------------------------------------------------- +# if want to use the pymodbus serial stack, uncomment these +# ------------------------------------------------------------------- +#pyserial==2.6 # ------------------------------------------------------------------- # if you want to run the tests and code coverage, uncomment these -# these out # ------------------------------------------------------------------- -#coverage==3.4 -#nose==1.0.0 +#coverage==3.5.3 +#mock==1.0b1 +#nose==1.2.1 +#pep8==1.3.3 +# ------------------------------------------------------------------- +# if you want to use the asynchronous version, uncomment these +# ------------------------------------------------------------------- +#Twisted==12.2.0 +#zope.interface==4.0.1 +#pyasn1==0.1.4 +#pycrypto==2.6 +#wsgiref==0.1.2 # ------------------------------------------------------------------- -# if you are just using the synchronous version, you can comment -# these out +# if you want to build the documentation, uncomment these # ------------------------------------------------------------------- -Twisted==11.0.0 -zope.interface==3.8.0 +#Jinja2==2.6 +#Pygments==1.5 +#Sphinx==1.1.3 +#docutils==0.9.1 diff --git a/setup.py b/setup.py index a29d41152..45232a6bc 100644 --- a/setup.py +++ b/setup.py @@ -63,14 +63,13 @@ include_package_data = True, zip_safe = True, install_requires = [ - 'twisted >= 2.5.0', - 'nose >= 1.0.0', - 'mock >= 0.8.0', - 'pyserial >= 2.4' + 'twisted >= 12.2.0', + 'pyserial >= 2.6' ], extras_require = { - 'quality' : [ 'epydoc >= 3.4.1', 'coverage >= 3.3.1', 'pyflakes >= 0.4.0' ], - 'twisted' : [ 'pyasn1 >= 0.0.13', 'pycrypto >= 2.3' ], + 'quality' : [ 'coverage >= 3.5.3', 'nose >= 1.2.1', 'mock >= 1.0.0', 'pep8 >= 1.3.3' ], + 'documents' : [ 'sphinx >= 1.1.3' ], + 'twisted' : [ 'pyasn1 >= 0.1.4', 'pycrypto >= 2.6' ], }, test_suite = 'nose.collector', cmdclass = command_classes, diff --git a/test/test_server_async.py b/test/test_server_async.py index 18438e3d1..6bfb5fa64 100644 --- a/test/test_server_async.py +++ b/test/test_server_async.py @@ -70,7 +70,7 @@ def testUdpServerInitialize(self): def testTcpServerStartup(self): ''' Test that the modbus tcp async server starts correctly ''' with patch('twisted.internet.reactor') as mock_reactor: - StartTcpServer(context=None) + StartTcpServer(context=None, console=True) self.assertEqual(mock_reactor.listenTCP.call_count, 2) self.assertEqual(mock_reactor.run.call_count, 1) From 8285cfb5114a8a05656801069f9dbf8023f50ce3 Mon Sep 17 00:00:00 2001 From: bashwork Date: Thu, 4 Oct 2012 10:10:46 -0500 Subject: [PATCH 089/243] adding support for pydev --- .project | 17 +++++++++++++++++ .pydevproject | 8 ++++++++ examples/common/synchronous-server.py | 2 +- test/test_bit_write_messages.py | 2 +- test/test_device.py | 25 +++++++++++++------------ test/test_server_sync.py | 13 ++++++------- 6 files changed, 46 insertions(+), 21 deletions(-) create mode 100644 .project create mode 100644 .pydevproject diff --git a/.project b/.project new file mode 100644 index 000000000..ad632cc2f --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + pymodbus + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/.pydevproject b/.pydevproject new file mode 100644 index 000000000..5d7021888 --- /dev/null +++ b/.pydevproject @@ -0,0 +1,8 @@ + + + +/pymodbus + +python 2.7 +Pymodbus Environment + diff --git a/examples/common/synchronous-server.py b/examples/common/synchronous-server.py index 04dd590d5..a2c6f55d0 100755 --- a/examples/common/synchronous-server.py +++ b/examples/common/synchronous-server.py @@ -96,6 +96,6 @@ #---------------------------------------------------------------------------# # run the server you want #---------------------------------------------------------------------------# -StartTcpServer(context, identity=identity, address=("localhost", 502)) +StartTcpServer(context, identity=identity, address=("localhost", 5020)) #StartUdpServer(context, identity=identity, address=("localhost", 502)) #StartSerialServer(context, identity=identity, port='/dev/pts/3', timeout=1) diff --git a/test/test_bit_write_messages.py b/test/test_bit_write_messages.py index c0499bfea..5648c3303 100644 --- a/test/test_bit_write_messages.py +++ b/test/test_bit_write_messages.py @@ -8,7 +8,7 @@ * Read/Write Discretes * Read Coils ''' -import unittest, struct +import unittest from pymodbus.bit_write_message import * from pymodbus.exceptions import * from pymodbus.pdu import ModbusExceptions diff --git a/test/test_device.py b/test/test_device.py index 7c52c2d2b..d53eb6e3f 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -120,7 +120,7 @@ def testModbusControlBlockAsciiModes(self): def testModbusControlBlockCounters(self): ''' Tests the MCB counters methods ''' self.assertEqual(0x0, self.control.Counter.BusMessage) - for i in range(10): + for _ in range(10): self.control.Counter.BusMessage += 1 self.control.Counter.SlaveMessage += 1 self.assertEqual(10, self.control.Counter.BusMessage) @@ -142,19 +142,19 @@ def testModbusControlBlockUpdate(self): def testModbusControlBlockIterator(self): ''' Tests the MCB counters iterator ''' self.control.Counter.reset() - for name,count in self.control: + for _,count in self.control: self.assertEqual(0, count) def testModbusCountersHandlerIterator(self): ''' Tests the MCB counters iterator ''' self.control.Counter.reset() - for name,count in self.control.Counter: + for _,count in self.control.Counter: self.assertEqual(0, count) def testModbusControlBlockCounterSummary(self): ''' Tests retrieving the current counter summary ''' self.assertEqual(0x00, self.control.Counter.summary()) - for i in range(10): + for _ in range(10): self.control.Counter.BusMessage += 1 self.control.Counter.SlaveMessage += 1 self.control.Counter.SlaveNAK += 1 @@ -171,6 +171,7 @@ def testModbusControlBlockListen(self): def testModbusControlBlockDelimiter(self): ''' Tests the MCB delimiter setting methods ''' + self.control.Delimiter = '\r' self.assertEqual(self.control.Delimiter, '\r') self.control.Delimiter = '=' self.assertEqual(self.control.Delimiter, '=') @@ -207,19 +208,19 @@ def testAddRemoveSingleClients(self): def testAddRemoveMultipleClients(self): ''' Test adding and removing a host ''' - list = ["192.168.1.1", "192.168.1.2", "192.168.1.3"] - self.access.add(list) - for host in list: + clients = ["192.168.1.1", "192.168.1.2", "192.168.1.3"] + self.access.add(clients) + for host in clients: self.assertTrue(self.access.check(host)) - self.access.remove(list) + self.access.remove(clients) def testNetworkAccessListIterator(self): ''' Test adding and removing a host ''' - list = ["127.0.0.1", "192.168.1.1", "192.168.1.2", "192.168.1.3"] - self.access.add(list) + clients = ["127.0.0.1", "192.168.1.1", "192.168.1.2", "192.168.1.3"] + self.access.add(clients) for host in self.access: - self.assertTrue(host in list) - for host in list: + self.assertTrue(host in clients) + for host in clients: self.assertTrue(host in self.access) def testClearingControlEvents(self): diff --git a/test/test_server_sync.py b/test/test_server_sync.py index 87cfde499..5af709f27 100644 --- a/test/test_server_sync.py +++ b/test/test_server_sync.py @@ -12,8 +12,7 @@ from pymodbus.server.sync import ModbusDisconnectedRequestHandler from pymodbus.server.sync import ModbusTcpServer, ModbusUdpServer, ModbusSerialServer from pymodbus.server.sync import StartTcpServer, StartUdpServer, StartSerialServer -from pymodbus.exceptions import ConnectionException, NotImplementedException -from pymodbus.exceptions import ParameterException +from pymodbus.exceptions import NotImplementedException from pymodbus.bit_read_message import ReadCoilsRequest, ReadCoilsResponse #---------------------------------------------------------------------------# @@ -50,7 +49,7 @@ def testBaseHandlerMethods(self): request = ReadCoilsRequest(1, 1) address = ('server', 12345) server = MockServer() - + with patch.object(ModbusBaseRequestHandler, 'handle') as mock_handle: with patch.object(ModbusBaseRequestHandler, 'send') as mock_send: mock_handle.return_value = True @@ -97,19 +96,19 @@ def testModbusSingleRequestHandlerHandle(self): self.assertEqual(handler.framer.processIncomingPacket.call_count, 0) # run forever if we are running - def _callback(a, b): + def _callback1(a, b): handler.running = False # stop infinite loop - handler.framer.processIncomingPacket.side_effect = _callback + handler.framer.processIncomingPacket.side_effect = _callback1 handler.running = True handler.handle() self.assertEqual(handler.framer.processIncomingPacket.call_count, 1) # exceptions are simply ignored - def _callback(a, b): + def _callback2(a, b): if handler.framer.processIncomingPacket.call_count == 2: raise Exception("example exception") else: handler.running = False # stop infinite loop - handler.framer.processIncomingPacket.side_effect = _callback + handler.framer.processIncomingPacket.side_effect = _callback2 handler.running = True handler.handle() self.assertEqual(handler.framer.processIncomingPacket.call_count, 3) From dbc902d0b2ec308a7eb20d653ed0246a825ec3b7 Mon Sep 17 00:00:00 2001 From: bashwork Date: Thu, 4 Oct 2012 11:01:45 -0500 Subject: [PATCH 090/243] fixing broken nosetest (/dev/pts) and pydev issues --- pymodbus/client/sync.py | 4 ++-- pymodbus/device.py | 16 ++++++++-------- pymodbus/file_message.py | 2 +- pymodbus/payload.py | 2 +- pymodbus/register_read_message.py | 4 ++-- pymodbus/server/async.py | 2 +- pymodbus/server/sync.py | 2 +- pymodbus/transaction.py | 2 +- pymodbus/utilities.py | 19 +++++++++---------- test/test_device.py | 5 +++-- test/test_server_async.py | 2 +- 11 files changed, 30 insertions(+), 30 deletions(-) diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index 90e17eee0..50eece25b 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -92,7 +92,7 @@ def __enter__(self): raise ConnectionException("Failed to connect[%s]" % (self.__str__())) return self - def __exit__(self, type, value, traceback): + def __exit__(self, klass, value, traceback): ''' Implement the client with exit block ''' self.close() @@ -207,7 +207,7 @@ def _get_address_family(cls, address): :returns: AF_INET for ipv4 and AF_INET6 for ipv6 ''' try: - addr = socket.inet_pton(socket.AF_INET6, address) + _ = socket.inet_pton(socket.AF_INET6, address) except socket.error: # not a valid ipv6 address return socket.AF_INET return socket.AF_INET6 diff --git a/pymodbus/device.py b/pymodbus/device.py index 4c90d8460..b35f5a192 100644 --- a/pymodbus/device.py +++ b/pymodbus/device.py @@ -236,13 +236,13 @@ def summary(self): ''' return dict(zip(self.__names, self.__data.itervalues())) - def update(self, input): + def update(self, value): ''' Update the values of this identity using another identify as the value - :param input: The value to copy values from + :param value: The value to copy values from ''' - self.__data.update(input) + self.__data.update(value) def __setitem__(self, key, value): ''' Wrapper used to access the device information @@ -322,7 +322,7 @@ def __gets(cls, identity, object_ids): :param object_ids: The specific object ids to read :returns: The requested data (id, length, value) ''' - return dict((id, identity[id]) for id in object_ids) + return dict((oid, identity[oid]) for oid in object_ids) #---------------------------------------------------------------------------# @@ -417,13 +417,13 @@ def __iter__(self): ''' return izip(self.__names, self.__data.itervalues()) - def update(self, input): + def update(self, values): ''' Update the values of this identity using another identify as the value - :param input: The value to copy values from + :param values: The value to copy values from ''' - for k, v in input.iteritems(): + for k, v in values.iteritems(): v += self.__getattribute__(k) self.__setattr__(k, v) @@ -544,7 +544,7 @@ def _setListenOnly(self, value): :param value: The value to set the listen status to ''' - self.__listen_only = value is not None + self.__listen_only = bool(value) ListenOnly = property(lambda s: s.__listen_only, _setListenOnly) diff --git a/pymodbus/file_message.py b/pymodbus/file_message.py index 3f9c63bea..366517cef 100644 --- a/pymodbus/file_message.py +++ b/pymodbus/file_message.py @@ -466,7 +466,7 @@ def decode(self, data): :param data: The packet data to decode ''' self.values = [] - length, count = struct.unpack('>HH', data[0:4]) + _, count = struct.unpack('>HH', data[0:4]) for index in xrange(0, count - 4): idx = 4 + index * 2 self.values.append(struct.unpack('>H', data[idx:idx + 2])[0]) diff --git a/pymodbus/payload.py b/pymodbus/payload.py index 2a617976d..3c4e337eb 100644 --- a/pymodbus/payload.py +++ b/pymodbus/payload.py @@ -231,7 +231,7 @@ def decode_8bit_uint(self): self._pointer += 1 fstring = self._endian + 'B' handle = self._payload[self._pointer - 1:self._pointer] - return unpack('B', handle)[0] + return unpack(fstring, handle)[0] def decode_bits(self): ''' Decodes a byte worth of bits from the buffer diff --git a/pymodbus/register_read_message.py b/pymodbus/register_read_message.py index e8a2bb648..06d82925f 100644 --- a/pymodbus/register_read_message.py +++ b/pymodbus/register_read_message.py @@ -323,8 +323,8 @@ def decode(self, data): :param data: The response to decode ''' - bytes = ord(data[0]) - for i in range(1, bytes, 2): + bytecount = ord(data[0]) + for i in range(1, bytecount, 2): self.registers.append(struct.unpack('>H', data[i:i + 2])[0]) def __str__(self): diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index f0b1e395a..968cf2630 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -249,7 +249,7 @@ def StartSerialServer(context, identity=None, protocol = factory.buildProtocol(None) SerialPort.getHost = lambda self: port # hack for logging - handle = SerialPort(protocol, port, reactor, baudrate) + SerialPort(protocol, port, reactor, baudrate) reactor.run() #---------------------------------------------------------------------------# diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index d63ed22a4..f9354ae49 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -14,7 +14,7 @@ from pymodbus.device import ModbusControlBlock from pymodbus.device import ModbusDeviceIdentification from pymodbus.transaction import * -from pymodbus.exceptions import ModbusException, NotImplementedException +from pymodbus.exceptions import NotImplementedException from pymodbus.pdu import ModbusExceptions as merror #---------------------------------------------------------------------------# diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index d1829a481..57d0e33fd 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -7,7 +7,7 @@ from pymodbus.exceptions import ModbusIOException from pymodbus.constants import Defaults -from pymodbus.interfaces import Singleton, IModbusFramer +from pymodbus.interfaces import IModbusFramer from pymodbus.utilities import checkCRC, computeCRC from pymodbus.utilities import checkLRC, computeLRC diff --git a/pymodbus/utilities.py b/pymodbus/utilities.py index a9e308cfe..440d2cd7e 100644 --- a/pymodbus/utilities.py +++ b/pymodbus/utilities.py @@ -32,17 +32,17 @@ def dict_property(store, index): :returns: An initialized property set ''' if hasattr(store, '__call__'): - get = lambda self: store(self)[index] - set = lambda self, value: store(self).__setitem__(index, value) + getter = lambda self: store(self)[index] + setter = lambda self, value: store(self).__setitem__(index, value) elif isinstance(store, str): - get = lambda self: self.__getattribute__(store)[index] - set = lambda self, value: self.__getattribute__(store).__setitem__( + getter = lambda self: self.__getattribute__(store)[index] + setter = lambda self, value: self.__getattribute__(store).__setitem__( index, value) else: - get = lambda self: store[index] - set = lambda self, value: store.__setitem__(index, value) + getter = lambda self: store[index] + setter = lambda self, value: store.__setitem__(index, value) - return property(get, set) + return property(getter, setter) #---------------------------------------------------------------------------# @@ -87,7 +87,7 @@ def unpack_bitstring(string): bits = [] for byte in range(byte_count): value = ord(string[byte]) - for bit in range(8): + for _ in range(8): bits.append((value & 1) == 1) value >>= 1 return bits @@ -104,7 +104,7 @@ def __generate_crc16_table(): result = [] for byte in range(256): crc = 0x0000 - for bit in range(8): + for _ in range(8): if (byte ^ crc) & 0x0001: crc = (crc >> 1) ^ 0xa001 else: crc >>= 1 @@ -154,7 +154,6 @@ def computeLRC(data): :returns: The calculated LRC ''' - lrc = 0 lrc = sum(ord(a) for a in data) & 0xff lrc = (lrc ^ 0xff) + 1 return lrc & 0xff diff --git a/test/test_device.py b/test/test_device.py index d53eb6e3f..e88e4ec00 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -1,8 +1,7 @@ #!/usr/bin/env python import unittest from pymodbus.device import * -from pymodbus.exceptions import * -from pymodbus.events import * +from pymodbus.events import ModbusEvent, RemoteReceiveEvent from pymodbus.constants import DeviceInformation #---------------------------------------------------------------------------# @@ -165,6 +164,8 @@ def testModbusControlBlockCounterSummary(self): def testModbusControlBlockListen(self): ''' Tests the MCB listen flag methods ''' + + self.control.ListenOnly = False self.assertEqual(self.control.ListenOnly, False) self.control.ListenOnly = not self.control.ListenOnly self.assertEqual(self.control.ListenOnly, True) diff --git a/test/test_server_async.py b/test/test_server_async.py index 6bfb5fa64..c8fbbaea7 100644 --- a/test/test_server_async.py +++ b/test/test_server_async.py @@ -84,7 +84,7 @@ def testUdpServerStartup(self): def testSerialServerStartup(self): ''' Test that the modbus serial async server starts correctly ''' with patch('twisted.internet.reactor') as mock_reactor: - StartSerialServer(context=None, port='/dev/pts/0') + StartSerialServer(context=None, port='/dev/ptmx') self.assertEqual(mock_reactor.run.call_count, 1) #---------------------------------------------------------------------------# From 5ba19152eb6105c33b12ce54cf81c25a791b3cc5 Mon Sep 17 00:00:00 2001 From: bashwork Date: Tue, 9 Oct 2012 21:46:12 -0500 Subject: [PATCH 091/243] adding bcd payload builder --- examples/common/bcd_payload.py | 178 +++++++++++++++++++++++++++++++++ pymodbus/payload.py | 4 +- 2 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 examples/common/bcd_payload.py diff --git a/examples/common/bcd_payload.py b/examples/common/bcd_payload.py new file mode 100644 index 000000000..099c7fcfb --- /dev/null +++ b/examples/common/bcd_payload.py @@ -0,0 +1,178 @@ +''' +Modbus BCD Payload Builder +----------------------------------------------------------- + +This is an example of building a custom payload builder +that can be used in the pymodbus library. Below is a +simple binary coded decimal builder and decoder. +''' +from pymodbus.constants import Endian +from pymodbus.interfaces import IPayloadBuilder +from pymodbus.utilities import pack_bitstring +from pymodbus.utilities import unpack_bitstring +from pymodbus.exceptions import ParameterException + + +class BcdPayloadBuilder(IPayloadBuilder): + ''' + A utility that helps build binary coded decimal payload + messages to be written with the various modbus messages. + example:: + + builder = BcdPayloadBuilder() + builder.add_8bit_uint(1) + builder.add_16bit_uint(2) + payload = builder.build() + ''' + + def __init__(self, payload=None): + ''' Initialize a new instance of the payload builder + + :param payload: Raw payload data to initialize with + ''' + self._payload = payload or [] + + def __str__(self): + ''' Return the payload buffer as a string + + :returns: The payload buffer as a string + ''' + return ''.join(self._payload) + + def reset(self): + ''' Reset the payload buffer + ''' + self._payload = [] + + def build(self): + ''' Return the payload buffer as a list + + This list is two bytes per element and can + thus be treated as a list of registers. + + :returns: The payload buffer as a list + ''' + return str(self) + + def add_bits(self, values): + ''' Adds a collection of bits to be encoded + + If these are less than a multiple of eight, + they will be left padded with 0 bits to make + it so. + + :param value: The value to add to the buffer + ''' + value = pack_bitstring(values) + self._payload.append(str(value)) + + def add_number(self, value, size=None): + ''' Adds any numeric type to the buffer + + :param value: The value to add to the buffer + ''' + value = str(value) + if size != None: + length = len(value) - size + value = (('0' * length) + value)[0:size] + self._payload.append(value) + + def add_string(self, value): + ''' Adds a string to the buffer + + :param value: The value to add to the buffer + ''' + self._payload.append(value) + + +class BcdPayloadDecoder(object): + ''' + A utility that helps decode binary coded decimal payload + messages from a modbus reponse message. What follows is + a simple example:: + + decoder = BcdPayloadDecoder(payload) + first = decoder.decode_8bit_uint() + second = decoder.decode_16bit_uint() + ''' + + def __init__(self, payload): + ''' Initialize a new payload decoder + + :param payload: The payload to decode with + ''' + self._payload = payload + self._pointer = 0x00 + + @staticmethod + def fromRegisters(registers, endian=Endian.Little): + ''' Initialize a payload decoder with the result of + reading a collection of registers from a modbus device. + + The registers are treated as a list of 2 byte values. + We have to do this because of how the data has already + been decoded by the rest of the library. + + :param registers: The register results to initialize with + :param endian: The endianess of the payload + :returns: An initialized PayloadDecoder + ''' + if isinstance(registers, list): # repack into flat binary + payload = ''.join(pack('>H', x) for x in registers) + return BinaryPayloadDecoder(payload, endian) + raise ParameterException('Invalid collection of registers supplied') + + @staticmethod + def fromCoils(coils, endian=Endian.Little): + ''' Initialize a payload decoder with the result of + reading a collection of coils from a modbus device. + + The coils are treated as a list of bit(boolean) values. + + :param coils: The coil results to initialize with + :param endian: The endianess of the payload + :returns: An initialized PayloadDecoder + ''' + if isinstance(coils, list): + payload = pack_bitstring(coils) + return BinaryPayloadDecoder(payload, endian) + raise ParameterException('Invalid collection of coils supplied') + + def reset(self): + ''' Reset the decoder pointer back to the start + ''' + self._pointer = 0x00 + + def decode_int(self, size=1): + ''' Decodes a int or long from the buffer + ''' + self._pointer += size + handle = self._payload[self._pointer - size:self._pointer] + return int(handle) + + def decode_float(self, size=1): + ''' Decodes a floating point number from the buffer + ''' + self._pointer += size + handle = self._payload[self._pointer - size:self._pointer] + return float(handle) + + def decode_bits(self): + ''' Decodes a byte worth of bits from the buffer + ''' + self._pointer += 1 + handle = self._payload[self._pointer - 1:self._pointer] + return unpack_bitstring(handle) + + def decode_string(self, size=1): + ''' Decodes a string from the buffer + + :param size: The size of the string to decode + ''' + self._pointer += size + return self._payload[self._pointer - size:self._pointer] + +#---------------------------------------------------------------------------# +# Exported Identifiers +#---------------------------------------------------------------------------# +__all__ = ["BcdPayloadBuilder", "BcdPayloadDecoder"] diff --git a/pymodbus/payload.py b/pymodbus/payload.py index 3c4e337eb..38fce8a56 100644 --- a/pymodbus/payload.py +++ b/pymodbus/payload.py @@ -70,7 +70,6 @@ def add_bits(self, values): :param value: The value to add to the buffer ''' -# TODO endianess issue here value = pack_bitstring(values) self._payload.append(value) @@ -171,7 +170,7 @@ class BinaryPayloadDecoder(object): the struct module, however it saves time looking up the format strings. What follows is a simple example:: - decoder = PayloadDecoder(self.little_endian_payload) + decoder = BinaryPayloadDecoder(payload) first = decoder.decode_8bit_uint() second = decoder.decode_16bit_uint() ''' @@ -236,7 +235,6 @@ def decode_8bit_uint(self): def decode_bits(self): ''' Decodes a byte worth of bits from the buffer ''' -# TODO endianess issue here self._pointer += 1 fstring = self._endian + 'B' handle = self._payload[self._pointer - 1:self._pointer] From 07a499ddd2d246f5aca8d8f0d0d78d4eef707d9b Mon Sep 17 00:00:00 2001 From: bashwork Date: Thu, 11 Oct 2012 10:15:22 -0500 Subject: [PATCH 092/243] reworking contrib packages --- doc/sphinx/examples/database-datastore.rst | 2 +- doc/sphinx/examples/index.rst | 3 +- doc/sphinx/examples/redis-datastore.rst | 2 +- examples/{datastore => contrib}/README.rst | 18 ++-- .../bcd_payload.py => contrib/bcd-payload.py} | 87 ++++++++++++++----- .../database-datastore.py} | 0 .../redis-datastore.py} | 0 .../{datastore => contrib}/requirements.txt | 0 8 files changed, 83 insertions(+), 29 deletions(-) rename examples/{datastore => contrib}/README.rst (61%) rename examples/{common/bcd_payload.py => contrib/bcd-payload.py} (72%) rename examples/{datastore/database.py => contrib/database-datastore.py} (100%) rename examples/{datastore/modredis.py => contrib/redis-datastore.py} (100%) rename examples/{datastore => contrib}/requirements.txt (100%) diff --git a/doc/sphinx/examples/database-datastore.rst b/doc/sphinx/examples/database-datastore.rst index 409c8cf7d..9186be812 100644 --- a/doc/sphinx/examples/database-datastore.rst +++ b/doc/sphinx/examples/database-datastore.rst @@ -2,5 +2,5 @@ Database Datastore Example ================================================== -.. literalinclude:: ../../../examples/datastore/database.py +.. literalinclude:: ../../../examples/contrib/database-datastore.py diff --git a/doc/sphinx/examples/index.rst b/doc/sphinx/examples/index.rst index 63f06178f..f084b7185 100644 --- a/doc/sphinx/examples/index.rst +++ b/doc/sphinx/examples/index.rst @@ -28,7 +28,7 @@ Example Library Code synchronous-server performance -Custom Datastore Code +Custom Pymodbus Code -------------------------------------------------- .. toctree:: @@ -36,6 +36,7 @@ Custom Datastore Code redis-datastore database-datastore + bcd-payload Example Frontend Code -------------------------------------------------- diff --git a/doc/sphinx/examples/redis-datastore.rst b/doc/sphinx/examples/redis-datastore.rst index 3eb1657fb..bb5554e04 100644 --- a/doc/sphinx/examples/redis-datastore.rst +++ b/doc/sphinx/examples/redis-datastore.rst @@ -2,5 +2,5 @@ Redis Datastore Example ================================================== -.. literalinclude:: ../../../examples/datastore/modredis.py +.. literalinclude:: ../../../examples/contrib/redis-datastore.py diff --git a/examples/datastore/README.rst b/examples/contrib/README.rst similarity index 61% rename from examples/datastore/README.rst rename to examples/contrib/README.rst index 3710648da..d6359d49c 100644 --- a/examples/datastore/README.rst +++ b/examples/contrib/README.rst @@ -1,12 +1,13 @@ ============================================================ -Custom Datastore Implementations +Contributed Implementations ============================================================ -There are a few example implementations of custom datastores -just to show what is possible. +There are a few example implementations of custom utilities +interacting with the pymodbus library just to show what is +possible. ------------------------------------------------------------ -SqlAlchemy Backend +SqlAlchemy Database Datastore Backend ------------------------------------------------------------ This module allows one to use any database available through @@ -15,7 +16,7 @@ This could be useful to have many servers who have data they agree upon and is transactional. ------------------------------------------------------------ -Redis Backend +Redis Datastore Backend ------------------------------------------------------------ This module allows one to use redis as a modbus server @@ -23,3 +24,10 @@ datastore backend. This achieves the same thing as the sqlalchemy backend, however, it is much more lightweight and easier to set up. +------------------------------------------------------------ +Binary Coded Decimal Payload +------------------------------------------------------------ + +This module allows one to write binary coded decimal data to +the modbus server using the payload encoder/decoder +interfaces. diff --git a/examples/common/bcd_payload.py b/examples/contrib/bcd-payload.py similarity index 72% rename from examples/common/bcd_payload.py rename to examples/contrib/bcd-payload.py index 099c7fcfb..81ca32e75 100644 --- a/examples/common/bcd_payload.py +++ b/examples/contrib/bcd-payload.py @@ -6,12 +6,54 @@ that can be used in the pymodbus library. Below is a simple binary coded decimal builder and decoder. ''' +from struct import pack, unpack from pymodbus.constants import Endian from pymodbus.interfaces import IPayloadBuilder from pymodbus.utilities import pack_bitstring from pymodbus.utilities import unpack_bitstring from pymodbus.exceptions import ParameterException +def convert_to_bcd(decimal): + ''' Converts a decimal value to a bcd value + + :param value: The decimal value to to pack into bcd + :returns: The number in bcd form + ''' + place, bcd = 0, 0 + while decimal > 0: + nibble = decimal % 10 + bcd += nibble << place + decimal /= 10 + place += 4 + return bcd + + +def convert_from_bcd(bcd): + ''' Converts a bcd value to a decimal value + + :param value: The value to unpack from bcd + :returns: The number in decimal form + ''' + place, decimal = 1, 0 + while bcd > 0: + nibble = bcd & 0xf + decimal += nibble * place + bcd >>= 4 + place *= 10 + return decimal + +def count_bcd_digits(bcd): + ''' Count the number of digits in a bcd value + + :param bcd: The bcd number to count the digits of + :returns: The number of digits in the bcd string + ''' + count = 0 + while bcd > 0: + count += 1 + bcd >>= 4 + return count + class BcdPayloadBuilder(IPayloadBuilder): ''' @@ -20,17 +62,19 @@ class BcdPayloadBuilder(IPayloadBuilder): example:: builder = BcdPayloadBuilder() - builder.add_8bit_uint(1) - builder.add_16bit_uint(2) + builder.add_number(1) + builder.add_number(int(2.234 * 1000)) payload = builder.build() ''' - def __init__(self, payload=None): + def __init__(self, payload=None, endian=Endian.Little): ''' Initialize a new instance of the payload builder :param payload: Raw payload data to initialize with + :param endian: The endianess of the payload ''' self._payload = payload or [] + self._endian = endian def __str__(self): ''' Return the payload buffer as a string @@ -52,7 +96,10 @@ def build(self): :returns: The payload buffer as a list ''' - return str(self) + string = str(self) + length = len(string) + string = string + ('\x00' * (length % 2)) + return [string[i:i+2] for i in xrange(0, length, 2)] def add_bits(self, values): ''' Adds a collection of bits to be encoded @@ -64,18 +111,22 @@ def add_bits(self, values): :param value: The value to add to the buffer ''' value = pack_bitstring(values) - self._payload.append(str(value)) + self._payload.append(value) def add_number(self, value, size=None): - ''' Adds any numeric type to the buffer + ''' Adds any 8bit numeric type to the buffer :param value: The value to add to the buffer ''' - value = str(value) - if size != None: - length = len(value) - size - value = (('0' * length) + value)[0:size] - self._payload.append(value) + encoded = [] + value = convert_to_bcd(value) + size = size or count_bcd_digits(value) + while size > 0: + nibble = value & 0xf + encoded.append(pack('B', nibble)) + value >>= 4 + size -= 1 + self._payload.extend(encoded) def add_string(self, value): ''' Adds a string to the buffer @@ -92,8 +143,8 @@ class BcdPayloadDecoder(object): a simple example:: decoder = BcdPayloadDecoder(payload) - first = decoder.decode_8bit_uint() - second = decoder.decode_16bit_uint() + first = decoder.decode_int(2) + second = decoder.decode_int(5) / 100 ''' def __init__(self, payload): @@ -148,14 +199,7 @@ def decode_int(self, size=1): ''' self._pointer += size handle = self._payload[self._pointer - size:self._pointer] - return int(handle) - - def decode_float(self, size=1): - ''' Decodes a floating point number from the buffer - ''' - self._pointer += size - handle = self._payload[self._pointer - size:self._pointer] - return float(handle) + return convert_from_bcd(handle) def decode_bits(self): ''' Decodes a byte worth of bits from the buffer @@ -172,6 +216,7 @@ def decode_string(self, size=1): self._pointer += size return self._payload[self._pointer - size:self._pointer] + #---------------------------------------------------------------------------# # Exported Identifiers #---------------------------------------------------------------------------# diff --git a/examples/datastore/database.py b/examples/contrib/database-datastore.py similarity index 100% rename from examples/datastore/database.py rename to examples/contrib/database-datastore.py diff --git a/examples/datastore/modredis.py b/examples/contrib/redis-datastore.py similarity index 100% rename from examples/datastore/modredis.py rename to examples/contrib/redis-datastore.py diff --git a/examples/datastore/requirements.txt b/examples/contrib/requirements.txt similarity index 100% rename from examples/datastore/requirements.txt rename to examples/contrib/requirements.txt From 7ab52cc73deae35d310e2cf4c0c09e42eb9f3fe3 Mon Sep 17 00:00:00 2001 From: bashwork Date: Thu, 11 Oct 2012 11:09:52 -0500 Subject: [PATCH 093/243] adding bcd payload contrib --- doc/sphinx/examples/bcd-payload.rst | 6 + examples/contrib/bcd.py | 250 ++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 doc/sphinx/examples/bcd-payload.rst create mode 100644 examples/contrib/bcd.py diff --git a/doc/sphinx/examples/bcd-payload.rst b/doc/sphinx/examples/bcd-payload.rst new file mode 100644 index 000000000..a95c5e7a6 --- /dev/null +++ b/doc/sphinx/examples/bcd-payload.rst @@ -0,0 +1,6 @@ +================================================== +Binary Coded Decimal Example +================================================== + +.. literalinclude:: ../../../examples/contrib/bcd-payload.py + diff --git a/examples/contrib/bcd.py b/examples/contrib/bcd.py new file mode 100644 index 000000000..43e57f6b9 --- /dev/null +++ b/examples/contrib/bcd.py @@ -0,0 +1,250 @@ +''' +Modbus BCD Payload Builder +----------------------------------------------------------- + +This is an example of building a custom payload builder +that can be used in the pymodbus library. Below is a +simple binary coded decimal builder and decoder. +''' +from struct import pack, unpack +from pymodbus.constants import Endian +from pymodbus.interfaces import IPayloadBuilder +from pymodbus.utilities import pack_bitstring +from pymodbus.utilities import unpack_bitstring +from pymodbus.exceptions import ParameterException + +def convert_to_bcd(decimal): + ''' Converts a decimal value to a bcd value + + :param value: The decimal value to to pack into bcd + :returns: The number in bcd form + ''' + place, bcd = 0, 0 + while decimal > 0: + nibble = decimal % 10 + bcd += nibble << place + decimal /= 10 + place += 4 + return bcd + + +def convert_from_bcd(bcd): + ''' Converts a bcd value to a decimal value + + :param value: The value to unpack from bcd + :returns: The number in decimal form + ''' + place, decimal = 1, 0 + while bcd > 0: + nibble = bcd & 0xf + decimal += nibble * place + bcd >>= 4 + place *= 10 + return decimal + +def count_bcd_digits(bcd): + ''' Count the number of digits in a bcd value + + :param bcd: The bcd number to count the digits of + :returns: The number of digits in the bcd string + ''' + count = 0 + while bcd > 0: + count += 1 + bcd >>= 4 + return count + + +class BcdPayloadBuilder(IPayloadBuilder): + ''' + A utility that helps build binary coded decimal payload + messages to be written with the various modbus messages. + example:: + + builder = BcdPayloadBuilder() + builder.add_number(1) + builder.add_number(int(2.234 * 1000)) + payload = builder.build() + ''' + + def __init__(self, payload=None, endian=Endian.Little): + ''' Initialize a new instance of the payload builder + + :param payload: Raw payload data to initialize with + :param endian: The endianess of the payload + ''' + self._payload = payload or [] + self._endian = endian + + def __str__(self): + ''' Return the payload buffer as a string + + :returns: The payload buffer as a string + ''' + return ''.join(self._payload) + + def reset(self): + ''' Reset the payload buffer + ''' + self._payload = [] + + def build(self): + ''' Return the payload buffer as a list + + This list is two bytes per element and can + thus be treated as a list of registers. + + :returns: The payload buffer as a list + ''' + string = str(self) + length = len(string) + string = string + ('\x00' * (length % 2)) + return [string[i:i+2] for i in xrange(0, length, 2)] + + def add_bits(self, values): + ''' Adds a collection of bits to be encoded + + If these are less than a multiple of eight, + they will be left padded with 0 bits to make + it so. + + :param value: The value to add to the buffer + ''' + value = pack_bitstring(values) + self._payload.append(value) + + def add_number(self, value, size=None): + ''' Adds any 8bit numeric type to the buffer + + :param value: The value to add to the buffer + ''' + encoded = [] + value = convert_to_bcd(value) + size = size or count_bcd_digits(value) + while size > 0: + nibble = value & 0xf + encoded.append(pack('B', nibble)) + value >>= 4 + size -= 1 + self._payload.extend(encoded) + + def add_string(self, value): + ''' Adds a string to the buffer + + :param value: The value to add to the buffer + ''' + self._payload.append(value) + + +class BcdPayloadDecoder(object): + ''' + A utility that helps decode binary coded decimal payload + messages from a modbus reponse message. What follows is + a simple example:: + + decoder = BcdPayloadDecoder(payload) + first = decoder.decode_int(2) + second = decoder.decode_int(5) / 100 + ''' + + def __init__(self, payload): + ''' Initialize a new payload decoder + + :param payload: The payload to decode with + ''' + self._payload = payload + self._pointer = 0x00 + + @staticmethod + def fromRegisters(registers, endian=Endian.Little): + ''' Initialize a payload decoder with the result of + reading a collection of registers from a modbus device. + + The registers are treated as a list of 2 byte values. + We have to do this because of how the data has already + been decoded by the rest of the library. + + :param registers: The register results to initialize with + :param endian: The endianess of the payload + :returns: An initialized PayloadDecoder + ''' + if isinstance(registers, list): # repack into flat binary + payload = ''.join(pack('>H', x) for x in registers) + return BinaryPayloadDecoder(payload, endian) + raise ParameterException('Invalid collection of registers supplied') + + @staticmethod + def fromCoils(coils, endian=Endian.Little): + ''' Initialize a payload decoder with the result of + reading a collection of coils from a modbus device. + + The coils are treated as a list of bit(boolean) values. + + :param coils: The coil results to initialize with + :param endian: The endianess of the payload + :returns: An initialized PayloadDecoder + ''' + if isinstance(coils, list): + payload = pack_bitstring(coils) + return BinaryPayloadDecoder(payload, endian) + raise ParameterException('Invalid collection of coils supplied') + + def reset(self): + ''' Reset the decoder pointer back to the start + ''' + self._pointer = 0x00 + + def decode_number(self, size=1): + ''' Decodes a number from the buffer + ''' + self._pointer += size + handle = self._payload[self._pointer - size:self._pointer] + size, value = size - 1, 0 + while size >= 0: + nibble = handle[size] + value += unpack('B', nibble)[0] << (size * 4) + size -= 1 + return convert_from_bcd(value) + + def decode_bits(self): + ''' Decodes a byte worth of bits from the buffer + ''' + self._pointer += 1 + handle = self._payload[self._pointer - 1:self._pointer] + return unpack_bitstring(handle) + + def decode_string(self, size=1): + ''' Decodes a string from the buffer + + :param size: The size of the string to decode + ''' + self._pointer += size + return self._payload[self._pointer - size:self._pointer] + + +#---------------------------------------------------------------------------# +# Exported Identifiers +#---------------------------------------------------------------------------# +__all__ = ["BcdPayloadBuilder", "BcdPayloadDecoder"] + +#---------------------------------------------------------------------------# +# Unit Tests +#---------------------------------------------------------------------------# +if __name__ == "__main__": + assert(0x1234 == convert_to_bcd(1234)) + assert(1234 == convert_from_bcd(0x1234)) + assert(4 == count_bcd_digits(0x1234)) + + encoder = BcdPayloadBuilder() + encoder.add_number(123) + encoder.add_number(int(123.23 * 100)) + encoder.add_bits([1, 0, 1, 0, 1, 0, 1, 0]) + encoder.add_string('1234') + payload = str(encoder) + assert('\x03\x02\x01\x03\x02\x03\x02\x01U1234' == payload) + + decoder = BcdPayloadDecoder(payload) + assert(123 == decoder.decode_number(3)) + assert(123.23 == decoder.decode_number(5) / 100.0) + assert([1,0,1,0,1,0,1,0] == decoder.decode_bits()) + assert('1234' == decoder.decode_string(4)) From 49d0e5e504faabf642f96ca0340e147a665d5fdb Mon Sep 17 00:00:00 2001 From: bashwork Date: Thu, 11 Oct 2012 11:29:44 -0500 Subject: [PATCH 094/243] moving complex examples to contrib --- doc/sphinx/examples/index.rst | 11 +++++------ .../{generate-messages.rst => message-generator.rst} | 6 +++--- doc/sphinx/examples/message-parser.rst | 2 +- doc/sphinx/examples/modbus-scraper.rst | 2 +- doc/sphinx/examples/modbus-simulator.rst | 2 +- doc/sphinx/examples/serial-forwarder.rst | 2 +- doc/sphinx/static/README | 1 + examples/contrib/README.rst | 12 ++++++++++++ .../message-generator.py} | 0 examples/{common => contrib}/message-parser.py | 0 examples/{common => contrib}/modbus-scraper.py | 0 examples/{common => contrib}/modbus-simulator.py | 0 examples/{common => contrib}/rx-messages | 0 .../serial-forwarder.py} | 0 examples/{common => contrib}/tx-messages | 0 15 files changed, 25 insertions(+), 13 deletions(-) rename doc/sphinx/examples/{generate-messages.rst => message-generator.rst} (77%) create mode 100644 doc/sphinx/static/README rename examples/{common/generate-messages.py => contrib/message-generator.py} (100%) rename examples/{common => contrib}/message-parser.py (100%) rename examples/{common => contrib}/modbus-scraper.py (100%) rename examples/{common => contrib}/modbus-simulator.py (100%) rename examples/{common => contrib}/rx-messages (100%) rename examples/{common/synchronous-serial-forwarder.py => contrib/serial-forwarder.py} (100%) rename examples/{common => contrib}/tx-messages (100%) diff --git a/doc/sphinx/examples/index.rst b/doc/sphinx/examples/index.rst index f084b7185..3114f63ba 100644 --- a/doc/sphinx/examples/index.rst +++ b/doc/sphinx/examples/index.rst @@ -11,18 +11,12 @@ Example Library Code .. toctree:: :maxdepth: 2 - asynchronous-client asynchronous-server asynchronous-processor custom-message - generate-messages modbus-logging modbus-payload - modbus-scraper - modbus-simulator - message-parser - serial-forwarder synchronous-client synchronous-client-ext synchronous-server @@ -37,6 +31,11 @@ Custom Pymodbus Code redis-datastore database-datastore bcd-payload + message-generator + message-parser + serial-forwarder + modbus-scraper + modbus-simulator Example Frontend Code -------------------------------------------------- diff --git a/doc/sphinx/examples/generate-messages.rst b/doc/sphinx/examples/message-generator.rst similarity index 77% rename from doc/sphinx/examples/generate-messages.rst rename to doc/sphinx/examples/message-generator.rst index 3f004559c..605554c45 100644 --- a/doc/sphinx/examples/generate-messages.rst +++ b/doc/sphinx/examples/message-generator.rst @@ -10,17 +10,17 @@ formats in the pymodbus package. Program Source -------------------------------------------------- -.. literalinclude:: ../../../examples/common/generate-messages.py +.. literalinclude:: ../../../examples/contrib/message-generator.py -------------------------------------------------- Example Request Messages -------------------------------------------------- -.. literalinclude:: ../../../examples/common/tx-messages +.. literalinclude:: ../../../examples/contrib/tx-messages -------------------------------------------------- Example Response Messages -------------------------------------------------- -.. literalinclude:: ../../../examples/common/rx-messages +.. literalinclude:: ../../../examples/contrib/rx-messages diff --git a/doc/sphinx/examples/message-parser.rst b/doc/sphinx/examples/message-parser.rst index 971093140..0bbaee9fc 100644 --- a/doc/sphinx/examples/message-parser.rst +++ b/doc/sphinx/examples/message-parser.rst @@ -43,7 +43,7 @@ message if possible. Here is an example output:: Program Source -------------------------------------------------- -.. literalinclude:: ../../../examples/common/message-parser.py +.. literalinclude:: ../../../examples/contrib/message-parser.py -------------------------------------------------- Example Messages diff --git a/doc/sphinx/examples/modbus-scraper.rst b/doc/sphinx/examples/modbus-scraper.rst index a98988b27..9931c4a62 100644 --- a/doc/sphinx/examples/modbus-scraper.rst +++ b/doc/sphinx/examples/modbus-scraper.rst @@ -2,5 +2,5 @@ Modbus Scraper Example ================================================== -.. literalinclude:: ../../../examples/common/modbus-scraper.py +.. literalinclude:: ../../../examples/contrib/modbus-scraper.py diff --git a/doc/sphinx/examples/modbus-simulator.rst b/doc/sphinx/examples/modbus-simulator.rst index 8769dee56..5adcee5ee 100644 --- a/doc/sphinx/examples/modbus-simulator.rst +++ b/doc/sphinx/examples/modbus-simulator.rst @@ -2,4 +2,4 @@ Modbus Simulator Example ================================================== -.. literalinclude:: ../../../examples/common/modbus-simulator.py +.. literalinclude:: ../../../examples/contrib/modbus-simulator.py diff --git a/doc/sphinx/examples/serial-forwarder.rst b/doc/sphinx/examples/serial-forwarder.rst index be458025b..87f6e0a0c 100644 --- a/doc/sphinx/examples/serial-forwarder.rst +++ b/doc/sphinx/examples/serial-forwarder.rst @@ -2,5 +2,5 @@ Synchronous Serial Forwarder ================================================== -.. literalinclude:: ../../../examples/common/synchronous-serial-forwarder.py +.. literalinclude:: ../../../examples/contrib/serial-forwarder.py diff --git a/doc/sphinx/static/README b/doc/sphinx/static/README new file mode 100644 index 000000000..06016c7ce --- /dev/null +++ b/doc/sphinx/static/README @@ -0,0 +1 @@ +include any html static content here diff --git a/examples/contrib/README.rst b/examples/contrib/README.rst index d6359d49c..535ce22c2 100644 --- a/examples/contrib/README.rst +++ b/examples/contrib/README.rst @@ -31,3 +31,15 @@ Binary Coded Decimal Payload This module allows one to write binary coded decimal data to the modbus server using the payload encoder/decoder interfaces. + +------------------------------------------------------------ +Message Generator and Parser +------------------------------------------------------------ + +These are two utilities that can be used to create a number +of modbus messages for any of the available protocols as well +as to decode the messages and print descriptive text about +them. + +Also included are a number of request and response messages +in tx-messages and rx-messages. diff --git a/examples/common/generate-messages.py b/examples/contrib/message-generator.py similarity index 100% rename from examples/common/generate-messages.py rename to examples/contrib/message-generator.py diff --git a/examples/common/message-parser.py b/examples/contrib/message-parser.py similarity index 100% rename from examples/common/message-parser.py rename to examples/contrib/message-parser.py diff --git a/examples/common/modbus-scraper.py b/examples/contrib/modbus-scraper.py similarity index 100% rename from examples/common/modbus-scraper.py rename to examples/contrib/modbus-scraper.py diff --git a/examples/common/modbus-simulator.py b/examples/contrib/modbus-simulator.py similarity index 100% rename from examples/common/modbus-simulator.py rename to examples/contrib/modbus-simulator.py diff --git a/examples/common/rx-messages b/examples/contrib/rx-messages similarity index 100% rename from examples/common/rx-messages rename to examples/contrib/rx-messages diff --git a/examples/common/synchronous-serial-forwarder.py b/examples/contrib/serial-forwarder.py similarity index 100% rename from examples/common/synchronous-serial-forwarder.py rename to examples/contrib/serial-forwarder.py diff --git a/examples/common/tx-messages b/examples/contrib/tx-messages similarity index 100% rename from examples/common/tx-messages rename to examples/contrib/tx-messages From a97322bbe83ed00825291cb9ede2c54fc647b201 Mon Sep 17 00:00:00 2001 From: bashwork Date: Thu, 11 Oct 2012 11:36:51 -0500 Subject: [PATCH 095/243] syncing version on pypi --- pymodbus/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymodbus/version.py b/pymodbus/version.py index d7da61489..3c17d959d 100644 --- a/pymodbus/version.py +++ b/pymodbus/version.py @@ -35,7 +35,7 @@ def __str__(self): ''' return '[%s, version %s]' % (self.package, self.short()) -version = Version('pymodbus', 1, 0, 0) +version = Version('pymodbus', 1, 1, 0) version.__name__ = 'pymodbus' # fix epydoc error #---------------------------------------------------------------------------# From bfcac1f132b520b6b426c68703922e6aca05db06 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Fri, 12 Oct 2012 12:05:39 -0500 Subject: [PATCH 096/243] adding server rest api --- examples/common/asynchronous-server.py | 2 +- examples/common/synchronous-server.py | 2 +- examples/contrib/requirements.txt | 4 +- examples/gui/web/frontend.py | 156 +++++++++++++++++++++---- 4 files changed, 138 insertions(+), 26 deletions(-) diff --git a/examples/common/asynchronous-server.py b/examples/common/asynchronous-server.py index fb4fba3e7..62a1c5f4c 100755 --- a/examples/common/asynchronous-server.py +++ b/examples/common/asynchronous-server.py @@ -88,7 +88,7 @@ identity = ModbusDeviceIdentification() identity.VendorName = 'Pymodbus' identity.ProductCode = 'PM' -identity.VendorUrl = 'http://github.com/bashwork/pymobdbus/' +identity.VendorUrl = 'http://github.com/bashwork/pymodbus/' identity.ProductName = 'Pymodbus Server' identity.ModelName = 'Pymodbus Server' identity.MajorMinorRevision = '1.0' diff --git a/examples/common/synchronous-server.py b/examples/common/synchronous-server.py index a2c6f55d0..47bbdc370 100755 --- a/examples/common/synchronous-server.py +++ b/examples/common/synchronous-server.py @@ -88,7 +88,7 @@ identity = ModbusDeviceIdentification() identity.VendorName = 'Pymodbus' identity.ProductCode = 'PM' -identity.VendorUrl = 'http://github.com/bashwork/pymobdbus/' +identity.VendorUrl = 'http://github.com/bashwork/pymodbus/' identity.ProductName = 'Pymodbus Server' identity.ModelName = 'Pymodbus Server' identity.MajorMinorRevision = '1.0' diff --git a/examples/contrib/requirements.txt b/examples/contrib/requirements.txt index 3c9b01bb0..a17bbdea5 100644 --- a/examples/contrib/requirements.txt +++ b/examples/contrib/requirements.txt @@ -1,5 +1,5 @@ # ------------------------------------------------------------------- # if you want to use the custom data stores, uncomment these # ------------------------------------------------------------------- -#SQLAlchemy==0.7.9 -#redis==2.6.2 +SQLAlchemy==0.7.9 +redis==2.6.2 diff --git a/examples/gui/web/frontend.py b/examples/gui/web/frontend.py index b15a2e0b6..8ca47a4bc 100644 --- a/examples/gui/web/frontend.py +++ b/examples/gui/web/frontend.py @@ -5,10 +5,17 @@ This is a simple web frontend using bottle as the web framework. This can be hosted using any wsgi adapter. ''' +import json, inspect from bottle import route, request, Bottle from bottle import jinja2_template as template -from pymodbus.device import ModbusAccessControl -from pymodbus.device import ModbusControlBlock + +#---------------------------------------------------------------------------# +# configure the client logging +#---------------------------------------------------------------------------# +import logging +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.DEBUG) #---------------------------------------------------------------------------# # REST API @@ -17,8 +24,8 @@ class Response(object): ''' A collection of common responses for the frontend api ''' - successful = { 'status' : 200 } - failure = { 'status' : 500 } + success = { 'status' : 200 } + failure = { 'status' : 500 } class ModbusApiWebApp(object): ''' @@ -57,10 +64,15 @@ def get_device_events(self): return { 'events' : self._server.control.Events } + + def get_device_plus(self): + return { + 'plus' : dict(self._server.control.Plus) + } def delete_device_events(self): self._server.control.clearEvents() - return Response.successful + return Response.success def get_device_host(self): return { @@ -71,54 +83,111 @@ def post_device_host(self): value = request.forms.get('host') if value: self._server.access.add(value) - return Response.successful + return Response.success def delete_device_host(self): value = request.forms.get('host') if value: self._server.access.remove(value) - return Response.successful + return Response.success def post_device_delimiter(self): value = request.forms.get('delimiter') if value: self._server.control.Delimiter = value - return Response.successful + return Response.success def post_device_mode(self): value = request.forms.get('mode') if value: self._server.control.Mode = value - return Response.successful + return Response.success def post_device_reset(self): self._server.control.reset() - return Response.successful + return Response.success #---------------------------------------------------------------------# - # Datastore API + # Datastore Get API #---------------------------------------------------------------------# + def __get_data(self, store, address, count, slave='00'): + try: + address, count = int(address), int(count) + context = self._server.store[int(store)] + values = context.getValues(store, address, count) + values = dict(zip(range(address, address + count), values)) + result = { 'data' : values } + result.update(Response.success) + return result + except Exception, ex: log.error(ex) + return Response.failure + + def get_coils(self, address='0', count='1'): + return self.__get_data(1, address, count) + + def get_discretes(self, address='0', count='1'): + return self.__get_data(2, address, count) + + def get_holding(self, address='0', count='1'): + return self.__get_data(3, address, count) + + def get_inputs(self, address='0', count='1'): + return self.__get_data(4, address, count) + + #---------------------------------------------------------------------# + # Datastore Update API + #---------------------------------------------------------------------# + def __set_data(self, store, address, values, slave='00'): + try: + address = int(address) + values = json.loads(values) + print values + context = self._server.store[int(store)] + context.setValues(store, address, values) + return Response.success + except Exception, ex: log.error(ex) + return Response.failure + + def post_coils(self, address='0'): + values = request.forms.get('data') + return self.__set_data(1, address, values) + + def post_discretes(self, address='0'): + values = request.forms.get('data') + return self.__set_data(2, address, values) + + def post_holding(self, address='0'): + values = request.forms.get('data') + return self.__set_data(3, address, values) + + def post_inputs(self, address='0'): + values = request.forms.get('data') + return self.__set_data(4, address, values) #---------------------------------------------------------------------------# # Configurations #---------------------------------------------------------------------------# def register_routes(application, register): ''' A helper method to register the routes of an application - based on convention. + based on convention. This is easier to manage than having to + decorate each method with a static route name. :param application: The application instance to register :param register: The bottle instance to register the application with ''' - from bottle import route - - methods = dir(application) - methods = filter(lambda n: not n.startswith('_'), methods) - for method in methods: + log.info("installing application routes:") + methods = inspect.getmembers(application) + methods = filter(lambda n: not n[0].startswith('_'), methods) + for method, func in dict(methods).items(): pieces = method.split('_') verb, path = pieces[0], pieces[1:] + args = inspect.getargspec(func).args[1:] + args = ['<%s>' % arg for arg in args] + args = '/'.join(args) + args = '' if len(args) == 0 else '/' + args path.insert(0, application._namespace) - path = '/'.join(path) - func = getattr(application, method) + path = '/'.join(path) + args + log.info("%6s: %s" % (verb, path)) register.route(path, method=verb, name=method)(func) def build_application(server): @@ -127,6 +196,7 @@ def build_application(server): :param server: The modbus server to pull instance data from :returns: An initialied bottle application ''' + log.info("building web application") api = ModbusApiWebApp(server) register = Bottle() register_routes(api, register) @@ -135,17 +205,18 @@ def build_application(server): #---------------------------------------------------------------------------# # Start Methods #---------------------------------------------------------------------------# -def RunModbusFrontend(server, port=503): +def RunModbusFrontend(server, port=8080): ''' Helper method to host bottle in twisted :param server: The modbus server to pull instance data from :param port: The port to host the service on ''' from bottle import TwistedServer, run + application = build_application(server) run(app=application, server=TwistedServer, port=port) -def RunDebugModbusFrontend(server, port=503): +def RunDebugModbusFrontend(server, port=8080): ''' Helper method to start the bottle server :param server: The modbus server to pull instance data from @@ -157,6 +228,47 @@ def RunDebugModbusFrontend(server, port=503): run(app=application, port=port) if __name__ == '__main__': + # ------------------------------------------------------------ + # an example server configuration + # ------------------------------------------------------------ from pymodbus.server.async import ModbusServerFactory + from pymodbus.constants import Defaults + from pymodbus.device import ModbusDeviceIdentification + from pymodbus.datastore import ModbusSequentialDataBlock + from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext + from twisted.internet import reactor + + # ------------------------------------------------------------ + # initialize the identity + # ------------------------------------------------------------ + + identity = ModbusDeviceIdentification() + identity.VendorName = 'Pymodbus' + identity.ProductCode = 'PM' + identity.VendorUrl = 'http://github.com/bashwork/pymodbus/' + identity.ProductName = 'Pymodbus Server' + identity.ModelName = 'Pymodbus Server' + identity.MajorMinorRevision = '1.0' + + # ------------------------------------------------------------ + # initialize the datastore + # ------------------------------------------------------------ + store = ModbusSlaveContext( + di = ModbusSequentialDataBlock(0, [17]*100), + co = ModbusSequentialDataBlock(0, [17]*100), + hr = ModbusSequentialDataBlock(0, [17]*100), + ir = ModbusSequentialDataBlock(0, [17]*100)) + context = ModbusServerContext(slaves=store, single=True) + + # ------------------------------------------------------------ + # initialize the factory + # ------------------------------------------------------------ + address = ("", Defaults.Port) + factory = ModbusServerFactory(context, None, identity) - RunDebugModbusFrontend(ModbusServerFactory) + # ------------------------------------------------------------ + # start the servers + # ------------------------------------------------------------ + log.info("Starting Modbus TCP Server on %s:%s" % address) + reactor.listenTCP(address[1], factory, interface=address[0]) + RunDebugModbusFrontend(factory) From ed99242fa9945aaa48e1a7e43488632e4027dde8 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Fri, 12 Oct 2012 12:09:38 -0500 Subject: [PATCH 097/243] moving web -> bottle --- examples/gui/{web => bottle}/frontend.py | 0 examples/gui/{web => bottle}/media/js/application.js | 0 examples/gui/{web => bottle}/media/js/ext/ext-all.js | 0 examples/gui/{web => bottle}/media/js/ext/ext-base.js | 0 examples/gui/bottle/requirements.txt | 4 ++++ examples/gui/{web => bottle}/views/base.html | 0 6 files changed, 4 insertions(+) rename examples/gui/{web => bottle}/frontend.py (100%) rename examples/gui/{web => bottle}/media/js/application.js (100%) rename examples/gui/{web => bottle}/media/js/ext/ext-all.js (100%) rename examples/gui/{web => bottle}/media/js/ext/ext-base.js (100%) create mode 100644 examples/gui/bottle/requirements.txt rename examples/gui/{web => bottle}/views/base.html (100%) diff --git a/examples/gui/web/frontend.py b/examples/gui/bottle/frontend.py similarity index 100% rename from examples/gui/web/frontend.py rename to examples/gui/bottle/frontend.py diff --git a/examples/gui/web/media/js/application.js b/examples/gui/bottle/media/js/application.js similarity index 100% rename from examples/gui/web/media/js/application.js rename to examples/gui/bottle/media/js/application.js diff --git a/examples/gui/web/media/js/ext/ext-all.js b/examples/gui/bottle/media/js/ext/ext-all.js similarity index 100% rename from examples/gui/web/media/js/ext/ext-all.js rename to examples/gui/bottle/media/js/ext/ext-all.js diff --git a/examples/gui/web/media/js/ext/ext-base.js b/examples/gui/bottle/media/js/ext/ext-base.js similarity index 100% rename from examples/gui/web/media/js/ext/ext-base.js rename to examples/gui/bottle/media/js/ext/ext-base.js diff --git a/examples/gui/bottle/requirements.txt b/examples/gui/bottle/requirements.txt new file mode 100644 index 000000000..9e1d5d56c --- /dev/null +++ b/examples/gui/bottle/requirements.txt @@ -0,0 +1,4 @@ +# ------------------------------------------------------------------- +# if you want to use this frontend uncomment these +# ------------------------------------------------------------------- +bottle==0.11.2 diff --git a/examples/gui/web/views/base.html b/examples/gui/bottle/views/base.html similarity index 100% rename from examples/gui/web/views/base.html rename to examples/gui/bottle/views/base.html From fca1500cc25a6b1654d58043d2ff1cf1a2b4815d Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Fri, 12 Oct 2012 12:14:05 -0500 Subject: [PATCH 098/243] fixing documentation --- doc/sphinx/examples/bottle-frontend.rst | 22 ++++++++++++++++++++++ doc/sphinx/examples/index.rst | 2 +- doc/sphinx/examples/web-frontend.rst | 6 ------ 3 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 doc/sphinx/examples/bottle-frontend.rst delete mode 100644 doc/sphinx/examples/web-frontend.rst diff --git a/doc/sphinx/examples/bottle-frontend.rst b/doc/sphinx/examples/bottle-frontend.rst new file mode 100644 index 000000000..662f2a7a0 --- /dev/null +++ b/doc/sphinx/examples/bottle-frontend.rst @@ -0,0 +1,22 @@ +================================================== +Bottle Web Frontend Example +================================================== + +-------------------------------------------------- +Summary +-------------------------------------------------- + +This is a simple example of adding a live REST api +on top of a running pymodbus server. This uses the +bottle microframework to achieve this. + +The example can be hosted under twisted as well as +the bottle internal server and can furthermore be +run behind gunicorn, cherrypi, etc wsgi containers. + +-------------------------------------------------- +Main Program +-------------------------------------------------- + +.. literalinclude:: ../../../examples/gui/web/frontend.py + diff --git a/doc/sphinx/examples/index.rst b/doc/sphinx/examples/index.rst index 3114f63ba..e5df40071 100644 --- a/doc/sphinx/examples/index.rst +++ b/doc/sphinx/examples/index.rst @@ -46,5 +46,5 @@ Example Frontend Code gtk-frontend tk-frontend wx-frontend - web-frontend + bottle-frontend diff --git a/doc/sphinx/examples/web-frontend.rst b/doc/sphinx/examples/web-frontend.rst deleted file mode 100644 index 2f37668a1..000000000 --- a/doc/sphinx/examples/web-frontend.rst +++ /dev/null @@ -1,6 +0,0 @@ -================================================== -Web Frontend Example -================================================== - -.. literalinclude:: ../../../examples/gui/web/frontend.py - From f697aed2a330d751c95f8e747fcabe6c4b469701 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Mon, 15 Oct 2012 11:41:50 -0500 Subject: [PATCH 099/243] adding gui base --- examples/gui/bottle/frontend.py | 19 +- examples/gui/bottle/media/css/application.css | 0 .../bottle/media/css/bootstrap-responsive.css | 1058 +++++++++++++++++ .../media/css/bootstrap-responsive.min.css | 9 + .../gui/bottle/media/css/bootstrap.min.css | 9 + .../media/img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes .../bottle/media/img/glyphicons-halflings.png | Bin 0 -> 12799 bytes examples/gui/bottle/media/js/application.js | 124 +- examples/gui/bottle/media/js/backbone.min.js | 38 + examples/gui/bottle/media/js/bootstrap.min.js | 6 + examples/gui/bottle/media/js/ext/ext-all.js | 11 - examples/gui/bottle/media/js/ext/ext-base.js | 7 - examples/gui/bottle/media/js/jquery.min.js | 154 +++ .../gui/bottle/media/js/underscore.min.js | 5 + examples/gui/bottle/views/base.html | 28 - examples/gui/bottle/views/index.html | 58 + 16 files changed, 1450 insertions(+), 76 deletions(-) create mode 100644 examples/gui/bottle/media/css/application.css create mode 100644 examples/gui/bottle/media/css/bootstrap-responsive.css create mode 100644 examples/gui/bottle/media/css/bootstrap-responsive.min.css create mode 100644 examples/gui/bottle/media/css/bootstrap.min.css create mode 100644 examples/gui/bottle/media/img/glyphicons-halflings-white.png create mode 100644 examples/gui/bottle/media/img/glyphicons-halflings.png create mode 100644 examples/gui/bottle/media/js/backbone.min.js create mode 100644 examples/gui/bottle/media/js/bootstrap.min.js delete mode 100644 examples/gui/bottle/media/js/ext/ext-all.js delete mode 100644 examples/gui/bottle/media/js/ext/ext-base.js create mode 100644 examples/gui/bottle/media/js/jquery.min.js create mode 100644 examples/gui/bottle/media/js/underscore.min.js delete mode 100644 examples/gui/bottle/views/base.html create mode 100644 examples/gui/bottle/views/index.html diff --git a/examples/gui/bottle/frontend.py b/examples/gui/bottle/frontend.py index 8ca47a4bc..b51df20ec 100644 --- a/examples/gui/bottle/frontend.py +++ b/examples/gui/bottle/frontend.py @@ -7,6 +7,7 @@ ''' import json, inspect from bottle import route, request, Bottle +from bottle import static_file from bottle import jinja2_template as template #---------------------------------------------------------------------------# @@ -49,7 +50,7 @@ def get_device(self): return { 'mode' : self._server.control.Mode, 'delimiter' : self._server.control.Delimiter, - 'listen-only' : self._server.control.ListenOnly, + 'readonly' : self._server.control.ListenOnly, 'identity' : self._server.control.Identity.summary(), 'counters' : dict(self._server.control.Counter), 'diagnostic' : self._server.control.getDiagnosticRegister(), @@ -59,6 +60,11 @@ def get_device_identity(self): return { 'identity' : dict(self._server.control.Identity) } + + def get_device_counters(self): + return { + 'counters' : dict(self._server.control.Counter) + } def get_device_events(self): return { @@ -164,6 +170,17 @@ def post_inputs(self, address='0'): values = request.forms.get('data') return self.__set_data(4, address, values) + #---------------------------------------------------------------------# + # webpage routes + #---------------------------------------------------------------------# + @route('/') + def _send_index_file(self): + return template('index.html') + + @route('/media/') + def _send_static_file(self, filename): + return static_file(filename, root='./media') + #---------------------------------------------------------------------------# # Configurations #---------------------------------------------------------------------------# diff --git a/examples/gui/bottle/media/css/application.css b/examples/gui/bottle/media/css/application.css new file mode 100644 index 000000000..e69de29bb diff --git a/examples/gui/bottle/media/css/bootstrap-responsive.css b/examples/gui/bottle/media/css/bootstrap-responsive.css new file mode 100644 index 000000000..9259d26dc --- /dev/null +++ b/examples/gui/bottle/media/css/bootstrap-responsive.css @@ -0,0 +1,1058 @@ +/*! + * Bootstrap Responsive v2.1.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.hidden { + display: none; + visibility: hidden; +} + +.visible-phone { + display: none !important; +} + +.visible-tablet { + display: none !important; +} + +.hidden-desktop { + display: none !important; +} + +.visible-desktop { + display: inherit !important; +} + +@media (min-width: 768px) and (max-width: 979px) { + .hidden-desktop { + display: inherit !important; + } + .visible-desktop { + display: none !important ; + } + .visible-tablet { + display: inherit !important; + } + .hidden-tablet { + display: none !important; + } +} + +@media (max-width: 767px) { + .hidden-desktop { + display: inherit !important; + } + .visible-desktop { + display: none !important; + } + .visible-phone { + display: inherit !important; + } + .hidden-phone { + display: none !important; + } +} + +@media (min-width: 1200px) { + .row { + margin-left: -30px; + *zoom: 1; + } + .row:before, + .row:after { + display: table; + line-height: 0; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + min-height: 1px; + margin-left: 30px; + } + .container, + .navbar-static-top .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { + width: 1170px; + } + .span12 { + width: 1170px; + } + .span11 { + width: 1070px; + } + .span10 { + width: 970px; + } + .span9 { + width: 870px; + } + .span8 { + width: 770px; + } + .span7 { + width: 670px; + } + .span6 { + width: 570px; + } + .span5 { + width: 470px; + } + .span4 { + width: 370px; + } + .span3 { + width: 270px; + } + .span2 { + width: 170px; + } + .span1 { + width: 70px; + } + .offset12 { + margin-left: 1230px; + } + .offset11 { + margin-left: 1130px; + } + .offset10 { + margin-left: 1030px; + } + .offset9 { + margin-left: 930px; + } + .offset8 { + margin-left: 830px; + } + .offset7 { + margin-left: 730px; + } + .offset6 { + margin-left: 630px; + } + .offset5 { + margin-left: 530px; + } + .offset4 { + margin-left: 430px; + } + .offset3 { + margin-left: 330px; + } + .offset2 { + margin-left: 230px; + } + .offset1 { + margin-left: 130px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, + .row-fluid:after { + display: table; + line-height: 0; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.564102564102564%; + *margin-left: 2.5109110747408616%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; + } + .row-fluid .span11 { + width: 91.45299145299145%; + *width: 91.39979996362975%; + } + .row-fluid .span10 { + width: 82.90598290598291%; + *width: 82.8527914166212%; + } + .row-fluid .span9 { + width: 74.35897435897436%; + *width: 74.30578286961266%; + } + .row-fluid .span8 { + width: 65.81196581196582%; + *width: 65.75877432260411%; + } + .row-fluid .span7 { + width: 57.26495726495726%; + *width: 57.21176577559556%; + } + .row-fluid .span6 { + width: 48.717948717948715%; + *width: 48.664757228587014%; + } + .row-fluid .span5 { + width: 40.17094017094017%; + *width: 40.11774868157847%; + } + .row-fluid .span4 { + width: 31.623931623931625%; + *width: 31.570740134569924%; + } + .row-fluid .span3 { + width: 23.076923076923077%; + *width: 23.023731587561375%; + } + .row-fluid .span2 { + width: 14.52991452991453%; + *width: 14.476723040552828%; + } + .row-fluid .span1 { + width: 5.982905982905983%; + *width: 5.929714493544281%; + } + .row-fluid .offset12 { + margin-left: 105.12820512820512%; + *margin-left: 105.02182214948171%; + } + .row-fluid .offset12:first-child { + margin-left: 102.56410256410257%; + *margin-left: 102.45771958537915%; + } + .row-fluid .offset11 { + margin-left: 96.58119658119658%; + *margin-left: 96.47481360247316%; + } + .row-fluid .offset11:first-child { + margin-left: 94.01709401709402%; + *margin-left: 93.91071103837061%; + } + .row-fluid .offset10 { + margin-left: 88.03418803418803%; + *margin-left: 87.92780505546462%; + } + .row-fluid .offset10:first-child { + margin-left: 85.47008547008548%; + *margin-left: 85.36370249136206%; + } + .row-fluid .offset9 { + margin-left: 79.48717948717949%; + *margin-left: 79.38079650845607%; + } + .row-fluid .offset9:first-child { + margin-left: 76.92307692307693%; + *margin-left: 76.81669394435352%; + } + .row-fluid .offset8 { + margin-left: 70.94017094017094%; + *margin-left: 70.83378796144753%; + } + .row-fluid .offset8:first-child { + margin-left: 68.37606837606839%; + *margin-left: 68.26968539734497%; + } + .row-fluid .offset7 { + margin-left: 62.393162393162385%; + *margin-left: 62.28677941443899%; + } + .row-fluid .offset7:first-child { + margin-left: 59.82905982905982%; + *margin-left: 59.72267685033642%; + } + .row-fluid .offset6 { + margin-left: 53.84615384615384%; + *margin-left: 53.739770867430444%; + } + .row-fluid .offset6:first-child { + margin-left: 51.28205128205128%; + *margin-left: 51.175668303327875%; + } + .row-fluid .offset5 { + margin-left: 45.299145299145295%; + *margin-left: 45.1927623204219%; + } + .row-fluid .offset5:first-child { + margin-left: 42.73504273504273%; + *margin-left: 42.62865975631933%; + } + .row-fluid .offset4 { + margin-left: 36.75213675213675%; + *margin-left: 36.645753773413354%; + } + .row-fluid .offset4:first-child { + margin-left: 34.18803418803419%; + *margin-left: 34.081651209310785%; + } + .row-fluid .offset3 { + margin-left: 28.205128205128204%; + *margin-left: 28.0987452264048%; + } + .row-fluid .offset3:first-child { + margin-left: 25.641025641025642%; + *margin-left: 25.53464266230224%; + } + .row-fluid .offset2 { + margin-left: 19.65811965811966%; + *margin-left: 19.551736679396257%; + } + .row-fluid .offset2:first-child { + margin-left: 17.094017094017094%; + *margin-left: 16.98763411529369%; + } + .row-fluid .offset1 { + margin-left: 11.11111111111111%; + *margin-left: 11.004728132387708%; + } + .row-fluid .offset1:first-child { + margin-left: 8.547008547008547%; + *margin-left: 8.440625568285142%; + } + input, + textarea, + .uneditable-input { + margin-left: 0; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 30px; + } + input.span12, + textarea.span12, + .uneditable-input.span12 { + width: 1156px; + } + input.span11, + textarea.span11, + .uneditable-input.span11 { + width: 1056px; + } + input.span10, + textarea.span10, + .uneditable-input.span10 { + width: 956px; + } + input.span9, + textarea.span9, + .uneditable-input.span9 { + width: 856px; + } + input.span8, + textarea.span8, + .uneditable-input.span8 { + width: 756px; + } + input.span7, + textarea.span7, + .uneditable-input.span7 { + width: 656px; + } + input.span6, + textarea.span6, + .uneditable-input.span6 { + width: 556px; + } + input.span5, + textarea.span5, + .uneditable-input.span5 { + width: 456px; + } + input.span4, + textarea.span4, + .uneditable-input.span4 { + width: 356px; + } + input.span3, + textarea.span3, + .uneditable-input.span3 { + width: 256px; + } + input.span2, + textarea.span2, + .uneditable-input.span2 { + width: 156px; + } + input.span1, + textarea.span1, + .uneditable-input.span1 { + width: 56px; + } + .thumbnails { + margin-left: -30px; + } + .thumbnails > li { + margin-left: 30px; + } + .row-fluid .thumbnails { + margin-left: 0; + } +} + +@media (min-width: 768px) and (max-width: 979px) { + .row { + margin-left: -20px; + *zoom: 1; + } + .row:before, + .row:after { + display: table; + line-height: 0; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + min-height: 1px; + margin-left: 20px; + } + .container, + .navbar-static-top .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { + width: 724px; + } + .span12 { + width: 724px; + } + .span11 { + width: 662px; + } + .span10 { + width: 600px; + } + .span9 { + width: 538px; + } + .span8 { + width: 476px; + } + .span7 { + width: 414px; + } + .span6 { + width: 352px; + } + .span5 { + width: 290px; + } + .span4 { + width: 228px; + } + .span3 { + width: 166px; + } + .span2 { + width: 104px; + } + .span1 { + width: 42px; + } + .offset12 { + margin-left: 764px; + } + .offset11 { + margin-left: 702px; + } + .offset10 { + margin-left: 640px; + } + .offset9 { + margin-left: 578px; + } + .offset8 { + margin-left: 516px; + } + .offset7 { + margin-left: 454px; + } + .offset6 { + margin-left: 392px; + } + .offset5 { + margin-left: 330px; + } + .offset4 { + margin-left: 268px; + } + .offset3 { + margin-left: 206px; + } + .offset2 { + margin-left: 144px; + } + .offset1 { + margin-left: 82px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, + .row-fluid:after { + display: table; + line-height: 0; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.7624309392265194%; + *margin-left: 2.709239449864817%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; + } + .row-fluid .span11 { + width: 91.43646408839778%; + *width: 91.38327259903608%; + } + .row-fluid .span10 { + width: 82.87292817679558%; + *width: 82.81973668743387%; + } + .row-fluid .span9 { + width: 74.30939226519337%; + *width: 74.25620077583166%; + } + .row-fluid .span8 { + width: 65.74585635359117%; + *width: 65.69266486422946%; + } + .row-fluid .span7 { + width: 57.18232044198895%; + *width: 57.12912895262725%; + } + .row-fluid .span6 { + width: 48.61878453038674%; + *width: 48.56559304102504%; + } + .row-fluid .span5 { + width: 40.05524861878453%; + *width: 40.00205712942283%; + } + .row-fluid .span4 { + width: 31.491712707182323%; + *width: 31.43852121782062%; + } + .row-fluid .span3 { + width: 22.92817679558011%; + *width: 22.87498530621841%; + } + .row-fluid .span2 { + width: 14.3646408839779%; + *width: 14.311449394616199%; + } + .row-fluid .span1 { + width: 5.801104972375691%; + *width: 5.747913483013988%; + } + .row-fluid .offset12 { + margin-left: 105.52486187845304%; + *margin-left: 105.41847889972962%; + } + .row-fluid .offset12:first-child { + margin-left: 102.76243093922652%; + *margin-left: 102.6560479605031%; + } + .row-fluid .offset11 { + margin-left: 96.96132596685082%; + *margin-left: 96.8549429881274%; + } + .row-fluid .offset11:first-child { + margin-left: 94.1988950276243%; + *margin-left: 94.09251204890089%; + } + .row-fluid .offset10 { + margin-left: 88.39779005524862%; + *margin-left: 88.2914070765252%; + } + .row-fluid .offset10:first-child { + margin-left: 85.6353591160221%; + *margin-left: 85.52897613729868%; + } + .row-fluid .offset9 { + margin-left: 79.8342541436464%; + *margin-left: 79.72787116492299%; + } + .row-fluid .offset9:first-child { + margin-left: 77.07182320441989%; + *margin-left: 76.96544022569647%; + } + .row-fluid .offset8 { + margin-left: 71.2707182320442%; + *margin-left: 71.16433525332079%; + } + .row-fluid .offset8:first-child { + margin-left: 68.50828729281768%; + *margin-left: 68.40190431409427%; + } + .row-fluid .offset7 { + margin-left: 62.70718232044199%; + *margin-left: 62.600799341718584%; + } + .row-fluid .offset7:first-child { + margin-left: 59.94475138121547%; + *margin-left: 59.838368402492065%; + } + .row-fluid .offset6 { + margin-left: 54.14364640883978%; + *margin-left: 54.037263430116376%; + } + .row-fluid .offset6:first-child { + margin-left: 51.38121546961326%; + *margin-left: 51.27483249088986%; + } + .row-fluid .offset5 { + margin-left: 45.58011049723757%; + *margin-left: 45.47372751851417%; + } + .row-fluid .offset5:first-child { + margin-left: 42.81767955801105%; + *margin-left: 42.71129657928765%; + } + .row-fluid .offset4 { + margin-left: 37.01657458563536%; + *margin-left: 36.91019160691196%; + } + .row-fluid .offset4:first-child { + margin-left: 34.25414364640884%; + *margin-left: 34.14776066768544%; + } + .row-fluid .offset3 { + margin-left: 28.45303867403315%; + *margin-left: 28.346655695309746%; + } + .row-fluid .offset3:first-child { + margin-left: 25.69060773480663%; + *margin-left: 25.584224756083227%; + } + .row-fluid .offset2 { + margin-left: 19.88950276243094%; + *margin-left: 19.783119783707537%; + } + .row-fluid .offset2:first-child { + margin-left: 17.12707182320442%; + *margin-left: 17.02068884448102%; + } + .row-fluid .offset1 { + margin-left: 11.32596685082873%; + *margin-left: 11.219583872105325%; + } + .row-fluid .offset1:first-child { + margin-left: 8.56353591160221%; + *margin-left: 8.457152932878806%; + } + input, + textarea, + .uneditable-input { + margin-left: 0; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 20px; + } + input.span12, + textarea.span12, + .uneditable-input.span12 { + width: 710px; + } + input.span11, + textarea.span11, + .uneditable-input.span11 { + width: 648px; + } + input.span10, + textarea.span10, + .uneditable-input.span10 { + width: 586px; + } + input.span9, + textarea.span9, + .uneditable-input.span9 { + width: 524px; + } + input.span8, + textarea.span8, + .uneditable-input.span8 { + width: 462px; + } + input.span7, + textarea.span7, + .uneditable-input.span7 { + width: 400px; + } + input.span6, + textarea.span6, + .uneditable-input.span6 { + width: 338px; + } + input.span5, + textarea.span5, + .uneditable-input.span5 { + width: 276px; + } + input.span4, + textarea.span4, + .uneditable-input.span4 { + width: 214px; + } + input.span3, + textarea.span3, + .uneditable-input.span3 { + width: 152px; + } + input.span2, + textarea.span2, + .uneditable-input.span2 { + width: 90px; + } + input.span1, + textarea.span1, + .uneditable-input.span1 { + width: 28px; + } +} + +@media (max-width: 767px) { + body { + padding-right: 20px; + padding-left: 20px; + } + .navbar-fixed-top, + .navbar-fixed-bottom, + .navbar-static-top { + margin-right: -20px; + margin-left: -20px; + } + .container-fluid { + padding: 0; + } + .dl-horizontal dt { + float: none; + width: auto; + clear: none; + text-align: left; + } + .dl-horizontal dd { + margin-left: 0; + } + .container { + width: auto; + } + .row-fluid { + width: 100%; + } + .row, + .thumbnails { + margin-left: 0; + } + .thumbnails > li { + float: none; + margin-left: 0; + } + [class*="span"], + .row-fluid [class*="span"] { + display: block; + float: none; + width: 100%; + margin-left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .span12, + .row-fluid .span12 { + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .input-large, + .input-xlarge, + .input-xxlarge, + input[class*="span"], + select[class*="span"], + textarea[class*="span"], + .uneditable-input { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .input-prepend input, + .input-append input, + .input-prepend input[class*="span"], + .input-append input[class*="span"] { + display: inline-block; + width: auto; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 0; + } + .modal { + position: fixed; + top: 20px; + right: 20px; + left: 20px; + width: auto; + margin: 0; + } + .modal.fade.in { + top: auto; + } +} + +@media (max-width: 480px) { + .nav-collapse { + -webkit-transform: translate3d(0, 0, 0); + } + .page-header h1 small { + display: block; + line-height: 20px; + } + input[type="checkbox"], + input[type="radio"] { + border: 1px solid #ccc; + } + .form-horizontal .control-label { + float: none; + width: auto; + padding-top: 0; + text-align: left; + } + .form-horizontal .controls { + margin-left: 0; + } + .form-horizontal .control-list { + padding-top: 0; + } + .form-horizontal .form-actions { + padding-right: 10px; + padding-left: 10px; + } + .modal { + top: 10px; + right: 10px; + left: 10px; + } + .modal-header .close { + padding: 10px; + margin: -10px; + } + .carousel-caption { + position: static; + } +} + +@media (max-width: 979px) { + body { + padding-top: 0; + } + .navbar-fixed-top, + .navbar-fixed-bottom { + position: static; + } + .navbar-fixed-top { + margin-bottom: 20px; + } + .navbar-fixed-bottom { + margin-top: 20px; + } + .navbar-fixed-top .navbar-inner, + .navbar-fixed-bottom .navbar-inner { + padding: 5px; + } + .navbar .container { + width: auto; + padding: 0; + } + .navbar .brand { + padding-right: 10px; + padding-left: 10px; + margin: 0 0 0 -5px; + } + .nav-collapse { + clear: both; + } + .nav-collapse .nav { + float: none; + margin: 0 0 10px; + } + .nav-collapse .nav > li { + float: none; + } + .nav-collapse .nav > li > a { + margin-bottom: 2px; + } + .nav-collapse .nav > .divider-vertical { + display: none; + } + .nav-collapse .nav .nav-header { + color: #777777; + text-shadow: none; + } + .nav-collapse .nav > li > a, + .nav-collapse .dropdown-menu a { + padding: 9px 15px; + font-weight: bold; + color: #777777; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + } + .nav-collapse .btn { + padding: 4px 10px 4px; + font-weight: normal; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + } + .nav-collapse .dropdown-menu li + li a { + margin-bottom: 2px; + } + .nav-collapse .nav > li > a:hover, + .nav-collapse .dropdown-menu a:hover { + background-color: #f2f2f2; + } + .navbar-inverse .nav-collapse .nav > li > a:hover, + .navbar-inverse .nav-collapse .dropdown-menu a:hover { + background-color: #111111; + } + .nav-collapse.in .btn-group { + padding: 0; + margin-top: 5px; + } + .nav-collapse .dropdown-menu { + position: static; + top: auto; + left: auto; + display: block; + float: none; + max-width: none; + padding: 0; + margin: 0 15px; + background-color: transparent; + border: none; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + } + .nav-collapse .dropdown-menu:before, + .nav-collapse .dropdown-menu:after { + display: none; + } + .nav-collapse .dropdown-menu .divider { + display: none; + } + .nav-collapse .nav > li > .dropdown-menu:before, + .nav-collapse .nav > li > .dropdown-menu:after { + display: none; + } + .nav-collapse .navbar-form, + .nav-collapse .navbar-search { + float: none; + padding: 10px 15px; + margin: 10px 0; + border-top: 1px solid #f2f2f2; + border-bottom: 1px solid #f2f2f2; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + } + .navbar-inverse .nav-collapse .navbar-form, + .navbar-inverse .nav-collapse .navbar-search { + border-top-color: #111111; + border-bottom-color: #111111; + } + .navbar .nav-collapse .nav.pull-right { + float: none; + margin-left: 0; + } + .nav-collapse, + .nav-collapse.collapse { + height: 0; + overflow: hidden; + } + .navbar .btn-navbar { + display: block; + } + .navbar-static .navbar-inner { + padding-right: 10px; + padding-left: 10px; + } +} + +@media (min-width: 980px) { + .nav-collapse.collapse { + height: auto !important; + overflow: visible !important; + } +} diff --git a/examples/gui/bottle/media/css/bootstrap-responsive.min.css b/examples/gui/bottle/media/css/bootstrap-responsive.min.css new file mode 100644 index 000000000..7b0158da0 --- /dev/null +++ b/examples/gui/bottle/media/css/bootstrap-responsive.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap Responsive v2.1.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade.in{top:auto}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .dropdown-menu a:hover{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:hover{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:block;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} diff --git a/examples/gui/bottle/media/css/bootstrap.min.css b/examples/gui/bottle/media/css/bootstrap.min.css new file mode 100644 index 000000000..31d8b960a --- /dev/null +++ b/examples/gui/bottle/media/css/bootstrap.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap v2.1.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{width:auto\9;height:auto;max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333;background-color:#fff}a{color:#08c;text-decoration:none}a:hover{color:#005580;text-decoration:underline}.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.1)}.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%}.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%}.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%}.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%}.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%}.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%}.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%}.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%}.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%}.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%}.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%}.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%}.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%}.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%}.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%}.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%}.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%}.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%}.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%}.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%}.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%}.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%}.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%}.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%}.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%}.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%}.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%}.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%}.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%}.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%}.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%}.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%}.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%}.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%}.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%}[class*="span"].hide,.row-fluid [class*="span"].hide{display:none}[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;line-height:0;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;line-height:0;content:""}.container-fluid:after{clear:both}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px}small{font-size:85%}strong{font-weight:bold}em{font-style:italic}cite{font-style:normal}.muted{color:#999}.text-warning{color:#c09853}.text-error{color:#b94a48}.text-info{color:#3a87ad}.text-success{color:#468847}h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:1;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999}h1{font-size:36px;line-height:40px}h2{font-size:30px;line-height:40px}h3{font-size:24px;line-height:40px}h4{font-size:18px;line-height:20px}h5{font-size:14px;line-height:20px}h6{font-size:12px;line-height:20px}h1 small{font-size:24px}h2 small{font-size:18px}h3 small{font-size:14px}h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eee}ul,ol{padding:0;margin:0 0 10px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}li{line-height:20px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}dl{margin-bottom:20px}dt,dd{line-height:20px}dt{font-weight:bold}dd{margin-left:10px}.dl-horizontal{*zoom:1}.dl-horizontal:before,.dl-horizontal:after{display:table;line-height:0;content:""}.dl-horizontal:after{clear:both}.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}hr{margin:20px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}abbr[title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:25px}blockquote small{display:block;line-height:20px;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:20px}code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}pre.prettyprint{margin-bottom:20px}pre code{padding:0;color:inherit;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 20px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:15px;color:#999}label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:9px;font-size:14px;line-height:20px;color:#555;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}input,textarea,.uneditable-input{width:206px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,box-shadow linear .2s;-o-transition:border linear .2s,box-shadow linear .2s;transition:border linear .2s,box-shadow linear .2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;*margin-top:0;line-height:normal;cursor:pointer}input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px}select{width:220px;background-color:#fff;border:1px solid #ccc}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.uneditable-input,.uneditable-textarea{color:#999;cursor:not-allowed;background-color:#fcfcfc;border-color:#ccc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025)}.uneditable-input{overflow:hidden;white-space:nowrap}.uneditable-textarea{width:auto;height:auto}input:-moz-placeholder,textarea:-moz-placeholder{color:#999}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999}.radio,.checkbox{min-height:18px;padding-left:18px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-18px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:926px}input.span11,textarea.span11,.uneditable-input.span11{width:846px}input.span10,textarea.span10,.uneditable-input.span10{width:766px}input.span9,textarea.span9,.uneditable-input.span9{width:686px}input.span8,textarea.span8,.uneditable-input.span8{width:606px}input.span7,textarea.span7,.uneditable-input.span7{width:526px}input.span6,textarea.span6,.uneditable-input.span6{width:446px}input.span5,textarea.span5,.uneditable-input.span5{width:366px}input.span4,textarea.span4,.uneditable-input.span4{width:286px}input.span3,textarea.span3,.uneditable-input.span3{width:206px}input.span2,textarea.span2,.uneditable-input.span2{width:126px}input.span1,textarea.span1,.uneditable-input.span1{width:46px}.controls-row{*zoom:1}.controls-row:before,.controls-row:after{display:table;line-height:0;content:""}.controls-row:after{clear:both}.controls-row [class*="span"]{float:left}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eee}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning>label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.control-group.error>label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48}.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.control-group.success>label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847}.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847}.control-group.info>label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad}.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad}.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3}.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad}input:focus:required:invalid,textarea:focus:required:invalid,select:focus:required:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:required:invalid:focus,textarea:focus:required:invalid:focus,select:focus:required:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;line-height:0;content:""}.form-actions:after{clear:both}.help-block,.help-inline{color:#595959}.help-block{display:block;margin-bottom:10px}.help-inline{display:inline-block;*display:inline;padding-left:5px;vertical-align:middle;*zoom:1}.input-append,.input-prepend{margin-bottom:5px;font-size:0;white-space:nowrap}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;font-size:14px;vertical-align:top;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2}.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #fff;background-color:#eee;border:1px solid #ccc}.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}.input-append .add-on,.input-append .btn{margin-left:-1px}.input-append .add-on:last-child,.input-append .btn:last-child{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0}input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;margin-bottom:0;vertical-align:middle;*zoom:1}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:10px}legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:20px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;line-height:0;content:""}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:180px}.form-horizontal .help-block{margin-bottom:0}.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block{margin-top:10px}.form-horizontal .form-actions{padding-left:180px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:20px}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child,.table-bordered tfoot:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px}.table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child,.table-bordered tfoot:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px}.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topleft:4px}.table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9}.table-hover tbody tr:hover td,.table-hover tbody tr:hover th{background-color:#f5f5f5}table [class*=span],.row-fluid table [class*=span]{display:table-cell;float:none;margin-left:0}.table .span1{float:none;width:44px;margin-left:0}.table .span2{float:none;width:124px;margin-left:0}.table .span3{float:none;width:204px;margin-left:0}.table .span4{float:none;width:284px;margin-left:0}.table .span5{float:none;width:364px;margin-left:0}.table .span6{float:none;width:444px;margin-left:0}.table .span7{float:none;width:524px;margin-left:0}.table .span8{float:none;width:604px;margin-left:0}.table .span9{float:none;width:684px;margin-left:0}.table .span10{float:none;width:764px;margin-left:0}.table .span11{float:none;width:844px;margin-left:0}.table .span12{float:none;width:924px;margin-left:0}.table .span13{float:none;width:1004px;margin-left:0}.table .span14{float:none;width:1084px;margin-left:0}.table .span15{float:none;width:1164px;margin-left:0}.table .span16{float:none;width:1244px;margin-left:0}.table .span17{float:none;width:1324px;margin-left:0}.table .span18{float:none;width:1404px;margin-left:0}.table .span19{float:none;width:1484px;margin-left:0}.table .span20{float:none;width:1564px;margin-left:0}.table .span21{float:none;width:1644px;margin-left:0}.table .span22{float:none;width:1724px;margin-left:0}.table .span23{float:none;width:1804px;margin-left:0}.table .span24{float:none;width:1884px;margin-left:0}.table tbody tr.success td{background-color:#dff0d8}.table tbody tr.error td{background-color:#f2dede}.table tbody tr.warning td{background-color:#fcf8e3}.table tbody tr.info td{background-color:#d9edf7}.table-hover tbody tr.success:hover td{background-color:#d0e9c6}.table-hover tbody tr.error:hover td{background-color:#ebcccc}.table-hover tbody tr.warning:hover td{background-color:#faf2cc}.table-hover tbody tr.info:hover td{background-color:#c4e3f3}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;margin-top:1px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat}.icon-white,.nav-tabs>.active>a>[class^="icon-"],.nav-tabs>.active>a>[class*=" icon-"],.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{width:16px;background-position:-216px -120px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{background-position:-384px -120px}.icon-folder-open{width:16px;background-position:-408px -120px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.dropdown .caret{margin-top:8px;margin-left:2px}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.dropdown-menu a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333;white-space:nowrap}.dropdown-menu li>a:hover,.dropdown-menu li>a:focus,.dropdown-submenu:hover>a{color:#fff;text-decoration:none;background-color:#08c;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu .active>a,.dropdown-menu .active>a:hover{color:#fff;text-decoration:none;background-color:#08c;background-color:#0081c2;background-image:linear-gradient(to bottom,#08c,#0077b3);background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-repeat:repeat-x;outline:0;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu .disabled>a,.dropdown-menu .disabled>a:hover{color:#999}.dropdown-menu .disabled>a:hover{text-decoration:none;cursor:default;background-color:transparent}.open{*z-index:1000}.open>.dropdown-menu{display:block}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropdown-submenu>a:after{display:block;float:right;width:0;height:0;margin-top:5px;margin-right:-10px;border-color:transparent;border-left-color:#ccc;border-style:solid;border-width:5px 0 5px 5px;content:" "}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown .dropdown-menu .nav-header{padding-right:20px;padding-left:20px}.typeahead{margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-moz-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;padding:4px 14px;margin-bottom:0;*margin-left:.3em;font-size:14px;line-height:20px;*line-height:20px;color:#333;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;*background-color:#e6e6e6;background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-repeat:repeat-x;border:1px solid #bbb;*border:0;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-bottom-color:#a2a2a2;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover{color:#333;text-decoration:none;background-color:#e6e6e6;*background-color:#d9d9d9;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-color:#e6e6e6;background-color:#d9d9d9 \9;background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-color:#e6e6e6;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:9px 14px;font-size:16px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.btn-large [class^="icon-"]{margin-top:2px}.btn-small{padding:3px 9px;font-size:12px;line-height:18px}.btn-small [class^="icon-"]{margin-top:0}.btn-mini{padding:2px 6px;font-size:11px;line-height:17px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn{border-color:#c5c5c5;border-color:rgba(0,0,0,0.15) rgba(0,0,0,0.15) rgba(0,0,0,0.25)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;*background-color:#04c;background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-image:-moz-linear-gradient(top,#08c,#04c);background-repeat:repeat-x;border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;*background-color:#f89406;background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-repeat:repeat-x;border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;*background-color:#bd362f;background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-repeat:repeat-x;border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;*background-color:#51a351;background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-image:-moz-linear-gradient(top,#62c462,#51a351);background-repeat:repeat-x;border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;*background-color:#2f96b4;background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-repeat:repeat-x;border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;*background-color:#222;background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-image:-moz-linear-gradient(top,#444,#222);background-repeat:repeat-x;border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{color:#08c;cursor:pointer;border-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover{color:#333;text-decoration:none}.btn-group{position:relative;*margin-left:.3em;font-size:0;white-space:nowrap;vertical-align:middle}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{margin-top:10px;margin-bottom:10px;font-size:0}.btn-toolbar .btn-group{display:inline-block;*display:inline;*zoom:1}.btn-toolbar .btn+.btn,.btn-toolbar .btn-group+.btn,.btn-toolbar .btn+.btn-group{margin-left:5px}.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.btn{margin-left:-1px}.btn-group>.btn,.btn-group>.dropdown-menu{font-size:14px}.btn-group>.btn-mini{font-size:11px}.btn-group>.btn-small{font-size:12px}.btn-group>.btn-large{font-size:16px}.btn-group>.btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{*padding-top:5px;padding-right:8px;*padding-bottom:5px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group>.btn-mini+.dropdown-toggle{*padding-top:2px;padding-right:5px;*padding-bottom:2px;padding-left:5px}.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px}.btn-group>.btn-large+.dropdown-toggle{*padding-top:7px;padding-right:12px;*padding-bottom:7px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#04c}.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222}.btn .caret{margin-top:8px;margin-left:0}.btn-mini .caret,.btn-small .caret,.btn-large .caret{margin-top:6px}.btn-large .caret{border-top-width:5px;border-right-width:5px;border-left-width:5px}.dropup .btn-large .caret{border-top:0;border-bottom:5px solid #000}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff}.btn-group-vertical{display:inline-block;*display:inline;*zoom:1}.btn-group-vertical .btn{display:block;float:none;width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical .btn+.btn{margin-top:-1px;margin-left:0}.btn-group-vertical .btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.btn-group-vertical .btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.btn-group-vertical .btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0}.btn-group-vertical .btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;color:#c09853;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.alert h4{margin:0}.alert .close{position:relative;top:-2px;right:-21px;line-height:20px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-danger,.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-bottom:20px;margin-left:0;list-style:none}.nav>li>a{display:block}.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>.pull-right{float:right}.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-right:15px;padding-left:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-right:-15px;margin-left:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#08c}.nav-list [class^="icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;line-height:0;content:""}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover{color:#fff;background-color:#08c}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-topleft:4px}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-moz-border-radius-bottomleft:4px}.nav-tabs.nav-stacked>li>a:hover{z-index:2;border-color:#ddd}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nav .dropdown-toggle .caret{margin-top:6px;border-top-color:#08c;border-bottom-color:#08c}.nav .dropdown-toggle:hover .caret{border-top-color:#005580;border-bottom-color:#005580}.nav-tabs .dropdown-toggle .caret{margin-top:8px}.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.nav>.dropdown.active>a:hover{cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover{color:#fff;background-color:#999;border-color:#999}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover{border-color:#999}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;line-height:0;content:""}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover{border-top-color:#ddd;border-bottom-color:transparent}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover{border-color:#eee #ddd #eee #eee}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#fff}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover{border-color:#eee #eee #eee #ddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#fff}.nav>.disabled>a{color:#999}.nav>.disabled>a:hover{text-decoration:none;cursor:default;background-color:transparent}.navbar{*position:relative;*z-index:2;margin-bottom:20px;overflow:visible;color:#777}.navbar-inner{min-height:40px;padding-right:20px;padding-left:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top,#fff,#f2f2f2);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#f2f2f2));background-image:-webkit-linear-gradient(top,#fff,#f2f2f2);background-image:-o-linear-gradient(top,#fff,#f2f2f2);background-image:linear-gradient(to bottom,#fff,#f2f2f2);background-repeat:repeat-x;border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff2f2f2',GradientType=0);*zoom:1;-webkit-box-shadow:0 1px 4px rgba(0,0,0,0.065);-moz-box-shadow:0 1px 4px rgba(0,0,0,0.065);box-shadow:0 1px 4px rgba(0,0,0,0.065)}.navbar-inner:before,.navbar-inner:after{display:table;line-height:0;content:""}.navbar-inner:after{clear:both}.navbar .container{width:auto}.nav-collapse.collapse{height:auto}.navbar .brand{display:block;float:left;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777;text-shadow:0 1px 0 #fff}.navbar .brand:hover{text-decoration:none}.navbar-text{margin-bottom:0;line-height:40px}.navbar-link{color:#777}.navbar-link:hover{color:#333}.navbar .divider-vertical{height:40px;margin:0 9px;border-right:1px solid #fff;border-left:1px solid #f2f2f2}.navbar .btn,.navbar .btn-group{margin-top:5px}.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn{margin-top:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;line-height:0;content:""}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px}.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:6px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0}.navbar-search .search-query{padding:4px 14px;margin-bottom:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.navbar-static-top{position:static;width:100%;margin-bottom:0}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px}.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-right:0;padding-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.1),0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.1),0 1px 10px rgba(0,0,0,0.1);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.1),0 1px 10px rgba(0,0,0,0.1)}.navbar-fixed-bottom{bottom:0}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:inset 0 1px 0 rgba(0,0,0,0.1),0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 0 rgba(0,0,0,0.1),0 -1px 10px rgba(0,0,0,0.1);box-shadow:inset 0 1px 0 rgba(0,0,0,0.1),0 -1px 10px rgba(0,0,0,0.1)}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right;margin-right:0}.navbar .nav>li{float:left}.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777;text-decoration:none;text-shadow:0 1px 0 #fff}.navbar .nav .dropdown-toggle .caret{margin-top:8px}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#333;text-decoration:none;background-color:transparent}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);box-shadow:inset 0 3px 8px rgba(0,0,0,0.125)}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-right:5px;margin-left:5px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#ededed;*background-color:#e5e5e5;background-image:-webkit-gradient(linear,0 0,0 100%,from(#f2f2f2),to(#e5e5e5));background-image:-webkit-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-o-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:linear-gradient(to bottom,#f2f2f2,#e5e5e5);background-image:-moz-linear-gradient(top,#f2f2f2,#e5e5e5);background-repeat:repeat-x;border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#fff2f2f2',endColorstr='#ffe5e5e5',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#fff;background-color:#e5e5e5;*background-color:#d9d9d9}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#ccc \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .nav>li>.dropdown-menu:before{position:absolute;top:-7px;left:9px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.navbar .nav>li>.dropdown-menu:after{position:absolute;top:-6px;left:10px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.navbar-fixed-bottom .nav>li>.dropdown-menu:before{top:auto;bottom:-7px;border-top:7px solid #ccc;border-bottom:0;border-top-color:rgba(0,0,0,0.2)}.navbar-fixed-bottom .nav>li>.dropdown-menu:after{top:auto;bottom:-6px;border-top:6px solid #fff;border-bottom:0}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{color:#555;background-color:#e5e5e5}.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777;border-bottom-color:#777}.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{right:12px;left:auto}.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{right:13px;left:auto}.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{right:100%;left:auto;margin-right:-1px;margin-left:0;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.navbar-inverse{color:#999}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top,#222,#111);background-image:-webkit-gradient(linear,0 0,0 100%,from(#222),to(#111));background-image:-webkit-linear-gradient(top,#222,#111);background-image:-o-linear-gradient(top,#222,#111);background-image:linear-gradient(to bottom,#222,#111);background-repeat:repeat-x;border-color:#252525;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff111111',GradientType=0)}.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover{color:#fff}.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#fff;background-color:#111}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .divider-vertical{border-right-color:#222;border-left-color:#111}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{color:#fff;background-color:#111}.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-search .search-query{color:#fff;background-color:#515151;border-color:#111;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333;text-shadow:0 1px 0 #fff;background-color:#fff;border:0;outline:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15)}.navbar-inverse .btn-navbar{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e0e0e;*background-color:#040404;background-image:-webkit-gradient(linear,0 0,0 100%,from(#151515),to(#040404));background-image:-webkit-linear-gradient(top,#151515,#040404);background-image:-o-linear-gradient(top,#151515,#040404);background-image:linear-gradient(to bottom,#151515,#040404);background-image:-moz-linear-gradient(top,#151515,#040404);background-repeat:repeat-x;border-color:#040404 #040404 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff151515',endColorstr='#ff040404',GradientType=0);filter:progid:dximagetransform.microsoft.gradient(enabled=false)}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#fff;background-color:#040404;*background-color:#000}.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000 \9}.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.breadcrumb li{display:inline-block;*display:inline;text-shadow:0 1px 0 #fff;*zoom:1}.breadcrumb .divider{padding:0 5px;color:#ccc}.breadcrumb .active{color:#999}.pagination{height:40px;margin:20px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination ul>li{display:inline}.pagination ul>li>a,.pagination ul>li>span{float:left;padding:0 14px;line-height:38px;text-decoration:none;background-color:#fff;border:1px solid #ddd;border-left-width:0}.pagination ul>li>a:hover,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5}.pagination ul>.active>a,.pagination ul>.active>span{color:#999;cursor:default}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover{color:#999;cursor:default;background-color:transparent}.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pager{margin:20px 0;text-align:center;list-style:none;*zoom:1}.pager:before,.pager:after{display:table;line-height:0;content:""}.pager:after{clear:both}.pager li{display:inline}.pager a,.pager span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.pager a:hover{text-decoration:none;background-color:#f5f5f5}.pager .next a,.pager .next span{float:right}.pager .previous a{float:left}.pager .disabled a,.pager .disabled a:hover,.pager .disabled span{color:#999;cursor:default;background-color:#fff}.modal-open .modal .dropdown-menu{z-index:2050}.modal-open .modal .dropdown.open{*z-index:2050}.modal-open .modal .popover{z-index:2060}.modal-open .modal .tooltip{z-index:2080}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:.8;filter:alpha(opacity=80)}.modal{position:fixed;top:50%;left:50%;z-index:1050;width:560px;margin:-250px 0 0 -280px;overflow:auto;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.modal.fade{top:-25%;-webkit-transition:opacity .3s linear,top .3s ease-out;-moz-transition:opacity .3s linear,top .3s ease-out;-o-transition:opacity .3s linear,top .3s ease-out;transition:opacity .3s linear,top .3s ease-out}.modal.fade.in{top:50%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-header h3{margin:0;line-height:30px}.modal-body{max-height:400px;padding:15px;overflow-y:auto}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;*zoom:1;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.modal-footer:before,.modal-footer:after{display:table;line-height:0;content:""}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.tooltip{position:absolute;z-index:1030;display:block;padding:5px;font-size:11px;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.8;filter:alpha(opacity=80)}.tooltip.top{margin-top:-3px}.tooltip.right{margin-left:3px}.tooltip.bottom{margin-top:3px}.tooltip.left{margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;width:236px;padding:1px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.popover.top{margin-bottom:10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-right:10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover-content p,.popover-content ul,.popover-content ol{margin-bottom:0}.popover .arrow,.popover .arrow:after{position:absolute;display:inline-block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow:after{z-index:-1;content:""}.popover.top .arrow{bottom:-10px;left:50%;margin-left:-10px;border-top-color:#fff;border-width:10px 10px 0}.popover.top .arrow:after{bottom:-1px;left:-11px;border-top-color:rgba(0,0,0,0.25);border-width:11px 11px 0}.popover.right .arrow{top:50%;left:-10px;margin-top:-10px;border-right-color:#fff;border-width:10px 10px 10px 0}.popover.right .arrow:after{bottom:-11px;left:-1px;border-right-color:rgba(0,0,0,0.25);border-width:11px 11px 11px 0}.popover.bottom .arrow{top:-10px;left:50%;margin-left:-10px;border-bottom-color:#fff;border-width:0 10px 10px}.popover.bottom .arrow:after{top:-1px;left:-11px;border-bottom-color:rgba(0,0,0,0.25);border-width:0 11px 11px}.popover.left .arrow{top:50%;right:-10px;margin-top:-10px;border-left-color:#fff;border-width:10px 0 10px 10px}.popover.left .arrow:after{right:-1px;bottom:-11px;border-left-color:rgba(0,0,0,0.25);border-width:11px 0 11px 11px}.thumbnails{margin-left:-20px;list-style:none;*zoom:1}.thumbnails:before,.thumbnails:after{display:table;line-height:0;content:""}.thumbnails:after{clear:both}.row-fluid .thumbnails{margin-left:0}.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px}.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.055);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.055);box-shadow:0 1px 3px rgba(0,0,0,0.055);-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}a.thumbnail:hover{border-color:#08c;-webkit-box-shadow:0 1px 4px rgba(0,105,214,0.25);-moz-box-shadow:0 1px 4px rgba(0,105,214,0.25);box-shadow:0 1px 4px rgba(0,105,214,0.25)}.thumbnail>img{display:block;max-width:100%;margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#555}.label,.badge{font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#999}.label{padding:1px 4px 2px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding:1px 9px 2px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}a.label:hover,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#b94a48}.label-important[href],.badge-important[href]{background-color:#953b39}.label-warning,.badge-warning{background-color:#f89406}.label-warning[href],.badge-warning[href]{background-color:#c67605}.label-success,.badge-success{background-color:#468847}.label-success[href],.badge-success[href]{background-color:#356635}.label-info,.badge-info{background-color:#3a87ad}.label-info[href],.badge-info[href]{background-color:#2d6987}.label-inverse,.badge-inverse{background-color:#333}.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a}.btn .label,.btn .badge{position:relative;top:-1px}.btn-mini .label,.btn-mini .badge{top:0}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f7f7f7;background-image:-moz-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f9f9f9));background-image:-webkit-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-o-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);background-repeat:repeat-x;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress .bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top,#149bdf,#0480be);background-image:-webkit-gradient(linear,0 0,0 100%,from(#149bdf),to(#0480be));background-image:-webkit-linear-gradient(top,#149bdf,#0480be);background-image:-o-linear-gradient(top,#149bdf,#0480be);background-image:linear-gradient(to bottom,#149bdf,#0480be);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15)}.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px}.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top,#ee5f5b,#c43c35);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#c43c35));background-image:-webkit-linear-gradient(top,#ee5f5b,#c43c35);background-image:-o-linear-gradient(top,#ee5f5b,#c43c35);background-image:linear-gradient(to bottom,#ee5f5b,#c43c35);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffc43c35',GradientType=0)}.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top,#62c462,#57a957);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#57a957));background-image:-webkit-linear-gradient(top,#62c462,#57a957);background-image:-o-linear-gradient(top,#62c462,#57a957);background-image:linear-gradient(to bottom,#62c462,#57a957);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff57a957',GradientType=0)}.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top,#5bc0de,#339bb9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#339bb9));background-image:-webkit-linear-gradient(top,#5bc0de,#339bb9);background-image:-o-linear-gradient(top,#5bc0de,#339bb9);background-image:linear-gradient(to bottom,#5bc0de,#339bb9);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff339bb9',GradientType=0)}.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0)}.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.accordion{margin-bottom:20px}.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.accordion-heading{border-bottom:0}.accordion-heading .accordion-toggle{display:block;padding:8px 15px}.accordion-toggle{cursor:pointer}.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5}.carousel{position:relative;margin-bottom:20px;line-height:1}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel .item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-moz-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel .item>img{display:block;line-height:1}.carousel .active,.carousel .next,.carousel .prev{display:block}.carousel .active{left:0}.carousel .next,.carousel .prev{position:absolute;top:0;width:100%}.carousel .next{left:100%}.carousel .prev{left:-100%}.carousel .next.left,.carousel .prev.right{left:0}.carousel .active.left{left:-100%}.carousel .active.right{left:100%}.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-align:center;background:#222;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;filter:alpha(opacity=50)}.carousel-control.right{right:15px;left:auto}.carousel-control:hover{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-caption{position:absolute;right:0;bottom:0;left:0;padding:15px;background:#333;background:rgba(0,0,0,0.75)}.carousel-caption h4,.carousel-caption p{line-height:20px;color:#fff}.carousel-caption h4{margin:0 0 5px}.carousel-caption p{margin-bottom:0}.hero-unit{padding:60px;margin-bottom:30px;background-color:#eee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;color:inherit}.hero-unit p{font-size:18px;font-weight:200;line-height:30px;color:inherit}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden}.affix{position:fixed} diff --git a/examples/gui/bottle/media/img/glyphicons-halflings-white.png b/examples/gui/bottle/media/img/glyphicons-halflings-white.png new file mode 100644 index 0000000000000000000000000000000000000000..3bf6484a29d8da269f9bc874b25493a45fae3bae GIT binary patch literal 8777 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 B7G?kd literal 0 HcmV?d00001 diff --git a/examples/gui/bottle/media/img/glyphicons-halflings.png b/examples/gui/bottle/media/img/glyphicons-halflings.png new file mode 100644 index 0000000000000000000000000000000000000000..a9969993201f9cee63cf9f49217646347297b643 GIT binary patch literal 12799 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 literal 0 HcmV?d00001 diff --git a/examples/gui/bottle/media/js/application.js b/examples/gui/bottle/media/js/application.js index 9632833da..987173395 100644 --- a/examples/gui/bottle/media/js/application.js +++ b/examples/gui/bottle/media/js/application.js @@ -1,31 +1,97 @@ -pymodbus = { - initialize = function() { - - var store = new Ext.data.Store({ - autoDestroy: true, - url: '/api/v1/device', - reader: new Ext.data.JsonReader({ - }), - sortInfo: { field:'common', direction: 'ASC' } - }); - - var grid = new Ext.grid.PropertyGrid({ - renderTo: 'modbus-control-grid', - width: 300, - autoHeight: true, - draggable: true, - #source - viewConfig : { - forceFit : true, - scrollOffset : 2, - } - }); - - store.load({ - callback: function() { - // remove spinner +//------------------------------------------------------------ +// models +//------------------------------------------------------------ +var Counters = Backbone.Model.extend({ + url: '/api/v1/device/counters', + sync: function(m, m, o) {}, + parse: function(data) { + return data.counters; + } +}); + +var Identity = Backbone.Model.extend({ + url: '/api/v1/device/identity', + sync: function(m, m, o) {}, + parse: function(data) { + return data.identity; + } +}); + +var Device = Backbone.Model.extend({ + url: '/api/v1/device', + defaults : { + 'delimiter' :'\r', + 'mode': 'ASCII', + 'readonly': false, + }, + sync: function(m, m, o) {}, + parse: function(data) { + return { + 'delimiter': data.delimiter, + 'mode': data.mode, + 'readonly': data.readonly } - }); + } +}); + +//------------------------------------------------------------ +// views +//------------------------------------------------------------ +var CounterView = Backbone.View.extend({ + id: 'py-counters', + template: _.template($('#py-table-template').html()), + initialize() { + this.model.bind('change', this.render, this); + }, + render: function() { + this.$el.html(this.template(this.model.toJSON())); + return this; + } +}); + +var IdentityView = Backbone.View.extend({ + id: 'py-identity', + template: _.template($('#py-table-template').html()), + initialize() { + this.model.bind('change', this.render, this); + }, + render: function() { + this.$el.html(this.template(this.model.toJSON())); + return this; + } +}); + +var DeviceView = Backbone.View.extend({ + id: 'py-device', + template: _.template($('#py-table-template').html()), + initialize() { + this.model.bind('change', this.render, this); + }, + render: function() { + this.$el.html(this.template(this.model.toJSON())); + return this; + } +}); + +//------------------------------------------------------------ +// application +//------------------------------------------------------------ +var Application = Backbone.View.extend({ + el: '#container', + initialize: function() { + this.device = new DeviceView({ model: new Device() }); + this.counters = new CountersView({ model: new Counters() }); + this.identity = new IdentityView({ model: new Identity() }); + }, + + render: function() { + this.$('#py-identity').html(this.identity.render().el); + this.$('#py-counters').html(this.counters.render().el); + this.$('#py-device').html(this.device.render().el); + }, +}); -}; -Ext.onReady(pymodbus.initialize) +jQuery(function initialize($) { + window.app = new Application(); + window.app.render(); +}); diff --git a/examples/gui/bottle/media/js/backbone.min.js b/examples/gui/bottle/media/js/backbone.min.js new file mode 100644 index 000000000..c1c0d4fff --- /dev/null +++ b/examples/gui/bottle/media/js/backbone.min.js @@ -0,0 +1,38 @@ +// Backbone.js 0.9.2 + +// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org +(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks= +{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g= +z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent= +{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null== +b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent: +b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)}; +a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error, +h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t(); +return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending= +{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length|| +!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator); +this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c=b))this.iframe=i('