diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ba1e82e6f..b36bc7db7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,14 @@ Version 1.3.0 Version 1.2.0 ------------------------------------------------------------ +* Added ability to ignore missing slaves +* Added ability to revert to ZeroMode +* Passed a number of extra options through the stack +* Fixed documenation and added a number of examples + +Version 1.2.0 +------------------------------------------------------------ + * Reworking the transaction managers to be more explicit and to handle modbus RTU over TCP. * Adding examples for a number of unique requested use cases diff --git a/README.rst b/README.rst index 55297215e..9d002aea3 100644 --- a/README.rst +++ b/README.rst @@ -123,7 +123,7 @@ 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 + * Hynek Petrak, https://github.com/HynekPetrak * Twisted Matrix Released under the BSD License diff --git a/doc/sphinx/examples/concurrent-client.rst b/doc/sphinx/examples/concurrent-client.rst new file mode 100644 index 000000000..1a3799ac9 --- /dev/null +++ b/doc/sphinx/examples/concurrent-client.rst @@ -0,0 +1,6 @@ +================================================== +Modbus Concurrent Client Example +================================================== + +.. literalinclude:: ../../../examples/contrib/concurrent-client.py + diff --git a/doc/sphinx/examples/custom-datablock.rst b/doc/sphinx/examples/custom-datablock.rst new file mode 100644 index 000000000..7139c0f08 --- /dev/null +++ b/doc/sphinx/examples/custom-datablock.rst @@ -0,0 +1,6 @@ +================================================== +Custom Datablock Example +================================================== + +.. literalinclude:: ../../../examples/common/custom-datablock.py + diff --git a/doc/sphinx/examples/index.rst b/doc/sphinx/examples/index.rst index f5a76d566..84f5c941c 100644 --- a/doc/sphinx/examples/index.rst +++ b/doc/sphinx/examples/index.rst @@ -17,6 +17,7 @@ Example Library Code custom-message modbus-logging modbus-payload + modbus-payload-server synchronous-client synchronous-client-ext synchronous-server @@ -24,6 +25,7 @@ Example Library Code updating-server callback-server changing-framers + thread-safe-datastore Custom Pymodbus Code -------------------------------------------------- @@ -40,6 +42,9 @@ Custom Pymodbus Code serial-forwarder modbus-scraper modbus-simulator + concurrent-client + libmodbus-client + remote-server-context Example Frontend Code -------------------------------------------------- diff --git a/doc/sphinx/examples/libmodbus-client.rst b/doc/sphinx/examples/libmodbus-client.rst new file mode 100644 index 000000000..17ac8e2cf --- /dev/null +++ b/doc/sphinx/examples/libmodbus-client.rst @@ -0,0 +1,6 @@ +================================================== +Libmodbus Client Facade +================================================== + +.. literalinclude:: ../../../examples/contrib/libmodbus-client.py + diff --git a/doc/sphinx/examples/modbus-payload-server.rst b/doc/sphinx/examples/modbus-payload-server.rst new file mode 100644 index 000000000..9144f0f53 --- /dev/null +++ b/doc/sphinx/examples/modbus-payload-server.rst @@ -0,0 +1,6 @@ +================================================== +Modbus Payload Server Context Building Example +================================================== + +.. literalinclude:: ../../../examples/common/modbus-payload-server.py + diff --git a/doc/sphinx/examples/remote-server-context.rst b/doc/sphinx/examples/remote-server-context.rst new file mode 100644 index 000000000..2a2ac3c05 --- /dev/null +++ b/doc/sphinx/examples/remote-server-context.rst @@ -0,0 +1,6 @@ +================================================== +Remote Single Server Context +================================================== + +.. literalinclude:: ../../../examples/contrib/remote_server_context.py + diff --git a/doc/sphinx/examples/thread-safe-datastore.rst b/doc/sphinx/examples/thread-safe-datastore.rst new file mode 100644 index 000000000..7a965a3f4 --- /dev/null +++ b/doc/sphinx/examples/thread-safe-datastore.rst @@ -0,0 +1,6 @@ +================================================== +Thread Safe Datastore Example +================================================== + +.. literalinclude:: ../../../examples/contrib/thread_safe_datastore.py + diff --git a/examples/common/asynchronous-client.py b/examples/common/asynchronous-client.py index 3eff0618d..3090624ab 100755 --- a/examples/common/asynchronous-client.py +++ b/examples/common/asynchronous-client.py @@ -35,6 +35,16 @@ def _assertor(value, message=None): deferred.addCallback(lambda r: _assertor(callback(r))) deferred.addErrback(lambda e: _assertor(False, e)) +#---------------------------------------------------------------------------# +# specify slave to query +#---------------------------------------------------------------------------# +# The slave to query is specified in an optional parameter for each +# individual request. This can be done by specifying the `unit` parameter +# which defaults to `0x00` +#---------------------------------------------------------------------------# +def exampleRequests(client): + rr = client.read_coils(1, 1, unit=0x02) + #---------------------------------------------------------------------------# # example requests #---------------------------------------------------------------------------# diff --git a/examples/common/asynchronous-server.py b/examples/common/asynchronous-server.py index 62a1c5f4c..377e7e8ea 100755 --- a/examples/common/asynchronous-server.py +++ b/examples/common/asynchronous-server.py @@ -37,7 +37,7 @@ # # block = ModbusSequentialDataBlock(0x00, [0]*0xff) # -# Continuting, you can choose to use a sequential or a sparse DataBlock in +# Continuing, 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:: @@ -72,6 +72,13 @@ # 0x03: ModbusSlaveContext(...), # } # context = ModbusServerContext(slaves=slaves, single=False) +# +# The slave context can also be initialized in zero_mode which means that a +# request to address(0-7) will map to the address (0-7). The default is +# False which is based on section 4.4 of the specification, so address(0-7) +# will map to (1-8):: +# +# store = ModbusSlaveContext(..., zero_mode=True) #---------------------------------------------------------------------------# store = ModbusSlaveContext( di = ModbusSequentialDataBlock(0, [17]*100), @@ -96,7 +103,7 @@ #---------------------------------------------------------------------------# # run the server you want #---------------------------------------------------------------------------# -StartTcpServer(context, identity=identity, address=("localhost", 5020)) +StartTcpServer(context, identity=identity, address=("localhost", 502)) #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/custom-datablock.py b/examples/common/custom-datablock.py new file mode 100755 index 000000000..42376ddf6 --- /dev/null +++ b/examples/common/custom-datablock.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +''' +Pymodbus Server With Custom Datablock Side Effect +-------------------------------------------------------------------------- + +This is an example of performing custom logic after a value has been +written to the datastore. +''' +#---------------------------------------------------------------------------# +# import the modbus libraries we need +#---------------------------------------------------------------------------# +from pymodbus.server.async import StartTcpServer +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.datastore import ModbusSparseDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from pymodbus.transaction import ModbusRtuFramer, ModbusAsciiFramer + +#---------------------------------------------------------------------------# +# configure the service logging +#---------------------------------------------------------------------------# + +import logging +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.DEBUG) + +#---------------------------------------------------------------------------# +# create your custom data block here +#---------------------------------------------------------------------------# + +class CustomDataBlock(ModbusSparseDataBlock): + ''' A datablock that stores the new value in memory + and performs a custom action after it has been stored. + ''' + + def setValues(self, address, value): + ''' Sets the requested values of the datastore + + :param address: The starting address + :param values: The new values to be set + ''' + super(ModbusSparseDataBlock, self).setValues(address, value) + + # whatever you want to do with the written value is done here, + # however make sure not to do too much work here or it will + # block the server, espectially if the server is being written + # to very quickly + print "wrote {} to {}".format(value, address) + + +#---------------------------------------------------------------------------# +# initialize your data store +#---------------------------------------------------------------------------# + +block = CustomDataBlock() +store = ModbusSlaveContext(di=block, co=block, hr=block, ir=block) +context = ModbusServerContext(slaves=store, single=True) + +#---------------------------------------------------------------------------# +# initialize the server information +#---------------------------------------------------------------------------# + +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' + +#---------------------------------------------------------------------------# +# run the server you want +#---------------------------------------------------------------------------# + +p = Process(target=device_writer, args=(queue,)) +p.start() +StartTcpServer(context, identity=identity, address=("localhost", 5020)) diff --git a/examples/common/modbus-payload-server.py b/examples/common/modbus-payload-server.py new file mode 100755 index 000000000..3c6d1953a --- /dev/null +++ b/examples/common/modbus-payload-server.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +''' +Pymodbus Server Payload Example +-------------------------------------------------------------------------- + +If you want to initialize a server context with a complicated memory +layout, you can actually use the payload builder. +''' +#---------------------------------------------------------------------------# +# import the various server implementations +#---------------------------------------------------------------------------# +from pymodbus.server.sync import StartTcpServer + +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext + +#---------------------------------------------------------------------------# +# import the payload builder +#---------------------------------------------------------------------------# + +from pymodbus.constants import Endian +from pymodbus.payload import BinaryPayloadDecoder +from pymodbus.payload import BinaryPayloadBuilder + +#---------------------------------------------------------------------------# +# configure the service logging +#---------------------------------------------------------------------------# +import logging +logging.basicConfig() +log = logging.getLogger() +log.setLevel(logging.DEBUG) + +#---------------------------------------------------------------------------# +# build your payload +#---------------------------------------------------------------------------# +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_bits([0,1,0,1,1,0,1,0]) + +#---------------------------------------------------------------------------# +# use that payload in the data store +#---------------------------------------------------------------------------# +# Here we use the same reference block for each underlying store. +#---------------------------------------------------------------------------# + +block = ModbusSequentialDataBlock(1, builder.to_registers()) +store = ModbusSlaveContext(di = block, co = block, hr = block, ir = block) +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/pymodbus/' +identity.ProductName = 'Pymodbus Server' +identity.ModelName = 'Pymodbus Server' +identity.MajorMinorRevision = '1.0' + +#---------------------------------------------------------------------------# +# run the server you want +#---------------------------------------------------------------------------# +StartTcpServer(context, identity=identity, address=("localhost", 5020)) diff --git a/examples/common/synchronous-client.py b/examples/common/synchronous-client.py index af12a8fe6..373a5bb1d 100755 --- a/examples/common/synchronous-client.py +++ b/examples/common/synchronous-client.py @@ -43,12 +43,32 @@ # # It should be noted that you can supply an ipv4 or an ipv6 host address for # both the UDP and TCP clients. +# +# There are also other options that can be set on the client that controls +# how transactions are performed. The current ones are: +# +# * retries - Specify how many retries to allow per transaction (default = 3) +# * retry_on_empty - Is an empty response a retry (default = False) +# * source_address - Specifies the TCP source address to bind to +# +# Here is an example of using these options:: +# +# client = ModbusClient('localhost', retries=3, retry_on_empty=True) #---------------------------------------------------------------------------# -client = ModbusClient('localhost', port=5020) +client = ModbusClient('localhost', port=502) #client = ModbusClient(method='ascii', port='/dev/pts/2', timeout=1) #client = ModbusClient(method='rtu', port='/dev/pts/2', timeout=1) client.connect() +#---------------------------------------------------------------------------# +# specify slave to query +#---------------------------------------------------------------------------# +# The slave to query is specified in an optional parameter for each +# individual request. This can be done by specifying the `unit` parameter +# which defaults to `0x00` +#---------------------------------------------------------------------------# +rr = client.read_coils(1, 1, unit=0x02) + #---------------------------------------------------------------------------# # example requests #---------------------------------------------------------------------------# @@ -74,7 +94,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 == [True]*8) # test the expected value +assert(rr.bits == [False]*8) # test the expected value rq = client.write_register(1, 10) rr = client.read_holding_registers(1,1) @@ -84,7 +104,7 @@ 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 == [17]*8) # test the expected value +assert(rr.registers == [10]*8) # test the expected value arguments = { 'read_address': 1, @@ -96,7 +116,7 @@ rr = client.read_input_registers(1,8) assert(rq.function_code < 0x80) # test that we are not an error assert(rq.registers == [20]*8) # test the expected value -assert(rr.registers == [17]*8) # test the expected value +assert(rr.registers == [20]*8) # test the expected value #---------------------------------------------------------------------------# # close the client diff --git a/examples/common/synchronous-server.py b/examples/common/synchronous-server.py index 47bbdc370..f46386472 100755 --- a/examples/common/synchronous-server.py +++ b/examples/common/synchronous-server.py @@ -19,6 +19,7 @@ from pymodbus.datastore import ModbusSequentialDataBlock from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext +from pymodbus.transaction import ModbusRtuFramer #---------------------------------------------------------------------------# # configure the service logging #---------------------------------------------------------------------------# @@ -37,7 +38,7 @@ # # block = ModbusSequentialDataBlock(0x00, [0]*0xff) # -# Continuting, you can choose to use a sequential or a sparse DataBlock in +# Continuing, 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:: @@ -72,6 +73,13 @@ # 0x03: ModbusSlaveContext(...), # } # context = ModbusServerContext(slaves=slaves, single=False) +# +# The slave context can also be initialized in zero_mode which means that a +# request to address(0-7) will map to the address (0-7). The default is +# False which is based on section 4.4 of the specification, so address(0-7) +# will map to (1-8):: +# +# store = ModbusSlaveContext(..., zero_mode=True) #---------------------------------------------------------------------------# store = ModbusSlaveContext( di = ModbusSequentialDataBlock(0, [17]*100), @@ -96,6 +104,14 @@ #---------------------------------------------------------------------------# # run the server you want #---------------------------------------------------------------------------# +# Tcp: StartTcpServer(context, identity=identity, address=("localhost", 5020)) + +# Udp: #StartUdpServer(context, identity=identity, address=("localhost", 502)) + +# Ascii: #StartSerialServer(context, identity=identity, port='/dev/pts/3', timeout=1) + +# RTU: +#StartSerialServer(context, framer=ModbusRtuFramer, identity=identity, port='/dev/pts/3', timeout=.005) diff --git a/examples/contrib/concurrent-client.py b/examples/contrib/concurrent-client.py new file mode 100755 index 000000000..cd1b05ff6 --- /dev/null +++ b/examples/contrib/concurrent-client.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python +''' +Concurrent Modbus Client +--------------------------------------------------------------------------- + +This is an example of writing a high performance modbus client that allows +a high level of concurrency by using worker threads/processes to handle +writing/reading from one or more client handles at once. +''' +#--------------------------------------------------------------------------# +# import system libraries +#--------------------------------------------------------------------------# +import multiprocessing +import threading +import logging +import time +import itertools +from collections import namedtuple + +# we are using the future from the concurrent.futures released with +# python3. Alternatively we will try the backported library:: +# pip install futures +try: + from concurrent.futures import Future +except ImportError: + from futures import Future + +#--------------------------------------------------------------------------# +# import neccessary modbus libraries +#--------------------------------------------------------------------------# +from pymodbus.client.common import ModbusClientMixin + +#--------------------------------------------------------------------------# +# configure the client logging +#--------------------------------------------------------------------------# +import logging +log = logging.getLogger("pymodbus") +log.setLevel(logging.DEBUG) +logging.basicConfig() + + +#--------------------------------------------------------------------------# +# Initialize out concurrency primitives +#--------------------------------------------------------------------------# +class _Primitives(object): + ''' This is a helper class used to group the + threading primitives depending on the type of + worker situation we want to run (threads or processes). + ''' + + def __init__(self, **kwargs): + self.queue = kwargs.get('queue') + self.event = kwargs.get('event') + self.worker = kwargs.get('worker') + + @classmethod + def create(klass, in_process=False): + ''' Initialize a new instance of the concurrency + primitives. + + :param in_process: True for threaded, False for processes + :returns: An initialized instance of concurrency primitives + ''' + if in_process: + from Queue import Queue + from threading import Thread + from threading import Event + return klass(queue=Queue, event=Event, worker=Thread) + else: + from multiprocessing import Queue + from multiprocessing import Event + from multiprocessing import Process + return klass(queue=Queue, event=Event, worker=Process) + + +#--------------------------------------------------------------------------# +# Define our data transfer objects +#--------------------------------------------------------------------------# +# These will be used to serialize state between the various workers. +# We use named tuples here as they are very lightweight while giving us +# all the benefits of classes. +#--------------------------------------------------------------------------# +WorkRequest = namedtuple('WorkRequest', 'request, work_id') +WorkResponse = namedtuple('WorkResponse', 'is_exception, work_id, response') + +#--------------------------------------------------------------------------# +# Define our worker processes +#--------------------------------------------------------------------------# +def _client_worker_process(factory, input_queue, output_queue, is_shutdown): + ''' This worker process takes input requests, issues them on its + client handle, and then sends the client response (success or failure) + to the manager to deliver back to the application. + + It should be noted that there are N of these workers and they can + be run in process or out of process as all the state serializes. + + :param factory: A client factory used to create a new client + :param input_queue: The queue to pull new requests to issue + :param output_queue: The queue to place client responses + :param is_shutdown: Condition variable marking process shutdown + ''' + log.info("starting up worker : %s", threading.current_thread()) + client = factory() + while not is_shutdown.is_set(): + try: + workitem = input_queue.get(timeout=1) + log.debug("dequeue worker request: %s", workitem) + if not workitem: continue + try: + log.debug("executing request on thread: %s", workitem) + result = client.execute(workitem.request) + output_queue.put(WorkResponse(False, workitem.work_id, result)) + except Exception, exception: + log.exception("error in worker thread: %s", threading.current_thread()) + output_queue.put(WorkResponse(True, workitem.work_id, exception)) + except Exception, ex: pass + log.info("request worker shutting down: %s", threading.current_thread()) + + +def _manager_worker_process(output_queue, futures, is_shutdown): + ''' This worker process manages taking output responses and + tying them back to the future keyed on the initial transaction id. + Basically this can be thought of as the delivery worker. + + It should be noted that there are one of these threads and it must + be an in process thread as the futures will not serialize across + processes.. + + :param output_queue: The queue holding output results to return + :param futures: The mapping of tid -> future + :param is_shutdown: Condition variable marking process shutdown + ''' + log.info("starting up manager worker: %s", threading.current_thread()) + while not is_shutdown.is_set(): + try: + workitem = output_queue.get() + future = futures.get(workitem.work_id, None) + log.debug("dequeue manager response: %s", workitem) + if not future: continue + if workitem.is_exception: + future.set_exception(workitem.response) + else: future.set_result(workitem.response) + log.debug("updated future result: %s", future) + del futures[workitem.work_id] + except Exception, ex: log.exception("error in manager") + log.info("manager worker shutting down: %s", threading.current_thread()) + + +#--------------------------------------------------------------------------# +# Define our concurrent client +#--------------------------------------------------------------------------# +class ConcurrentClient(ModbusClientMixin): + ''' This is a high performance client that can be used + to read/write a large number of reqeusts at once asyncronously. + This operates with a backing worker pool of processes or threads + to achieve its performance. + ''' + + def __init__(self, **kwargs): + ''' Initialize a new instance of the client + ''' + worker_count = kwargs.get('count', multiprocessing.cpu_count()) + self.factory = kwargs.get('factory') + primitives = _Primitives.create(kwargs.get('in_process', False)) + self.is_shutdown = primitives.event() # condition marking process shutdown + self.input_queue = primitives.queue() # input requests to process + self.output_queue = primitives.queue() # output results to return + self.futures = {} # mapping of tid -> future + self.workers = [] # handle to our worker threads + self.counter = itertools.count() + + # creating the response manager + self.manager = threading.Thread(target=_manager_worker_process, + args=(self.output_queue, self.futures, self.is_shutdown)) + self.manager.start() + self.workers.append(self.manager) + + # creating the request workers + for i in range(worker_count): + worker = primitives.worker(target=_client_worker_process, + args=(self.factory, self.input_queue, self.output_queue, self.is_shutdown)) + worker.start() + self.workers.append(worker) + + def shutdown(self): + ''' Shutdown all the workers being used to + concurrently process the requests. + ''' + log.info("stating to shut down workers") + self.is_shutdown.set() + self.output_queue.put(WorkResponse(None, None, None)) # to wake up the manager + for worker in self.workers: + worker.join() + log.info("finished shutting down workers") + + def execute(self, request): + ''' Given a request, enqueue it to be processed + and then return a future linked to the response + of the call. + + :param request: The request to execute + :returns: A future linked to the call's response + ''' + future, work_id = Future(), self.counter.next() + self.input_queue.put(WorkRequest(request, work_id)) + self.futures[work_id] = future + return future + + def execute_silently(self, request): + ''' Given a write request, enqueue it to + be processed without worrying about calling the + application back (fire and forget) + + :param request: The request to execute + ''' + self.input_queue.put(WorkRequest(request, None)) + +if __name__ == "__main__": + from pymodbus.client.sync import ModbusTcpClient + + def client_factory(): + log.debug("creating client for: %s", threading.current_thread()) + client = ModbusTcpClient('127.0.0.1', port=5020) + client.connect() + return client + + client = ConcurrentClient(factory = client_factory) + try: + log.info("issuing concurrent requests") + futures = [client.read_coils(i * 8, 8) for i in range(10)] + log.info("waiting on futures to complete") + for future in futures: + log.info("future result: %s", future.result(timeout=1)) + finally: client.shutdown() diff --git a/examples/contrib/libmodbus-client.py b/examples/contrib/libmodbus-client.py new file mode 100755 index 000000000..3922433f3 --- /dev/null +++ b/examples/contrib/libmodbus-client.py @@ -0,0 +1,427 @@ +#!/usr/bin/env python +''' +Libmodbus Protocol Wrapper +------------------------------------------------------------ + +What follows is an example wrapper of the libmodbus library +(http://libmodbus.org/documentation/) for use with pymodbus. +There are two utilities involved here: + +* LibmodbusLevel1Client + + This is simply a python wrapper around the c library. It is + mostly a clone of the pylibmodbus implementation, but I plan + on extending it to implement all the available protocol using + the raw execute methods. + +* LibmodbusClient + + This is just another modbus client that can be used just like + any other client in pymodbus. + +For these to work, you must have `cffi` and `libmodbus-dev` installed: + + sudo apt-get install libmodbus-dev + pip install cffi +''' +#--------------------------------------------------------------------------# +# import system libraries +#--------------------------------------------------------------------------# + +from cffi import FFI + +#--------------------------------------------------------------------------# +# import pymodbus libraries +#--------------------------------------------------------------------------# + +from pymodbus.constants import Defaults +from pymodbus.exceptions import ModbusException +from pymodbus.client.common import ModbusClientMixin +from pymodbus.bit_read_message import ReadCoilsResponse, ReadDiscreteInputsResponse +from pymodbus.register_read_message import ReadHoldingRegistersResponse, ReadInputRegistersResponse +from pymodbus.register_read_message import ReadWriteMultipleRegistersResponse +from pymodbus.bit_write_message import WriteSingleCoilResponse, WriteMultipleCoilsResponse +from pymodbus.register_write_message import WriteSingleRegisterResponse, WriteMultipleRegistersResponse + +#-------------------------------------------------------------------------------- +# create the C interface +#-------------------------------------------------------------------------------- +# * TODO add the protocol needed for the servers +#-------------------------------------------------------------------------------- + +compiler = FFI() +compiler.cdef(""" + typedef struct _modbus modbus_t; + + int modbus_connect(modbus_t *ctx); + int modbus_flush(modbus_t *ctx); + void modbus_close(modbus_t *ctx); + + const char *modbus_strerror(int errnum); + int modbus_set_slave(modbus_t *ctx, int slave); + + void modbus_get_response_timeout(modbus_t *ctx, uint32_t *to_sec, uint32_t *to_usec); + void modbus_set_response_timeout(modbus_t *ctx, uint32_t to_sec, uint32_t to_usec); + + int modbus_read_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest); + int modbus_read_input_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest); + int modbus_read_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest); + int modbus_read_input_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest); + + int modbus_write_bit(modbus_t *ctx, int coil_addr, int status); + int modbus_write_bits(modbus_t *ctx, int addr, int nb, const uint8_t *data); + int modbus_write_register(modbus_t *ctx, int reg_addr, int value); + int modbus_write_registers(modbus_t *ctx, int addr, int nb, const uint16_t *data); + int modbus_write_and_read_registers(modbus_t *ctx, int write_addr, int write_nb, const uint16_t *src, int read_addr, int read_nb, uint16_t *dest); + + int modbus_mask_write_register(modbus_t *ctx, int addr, uint16_t and_mask, uint16_t or_mask); + int modbus_send_raw_request(modbus_t *ctx, uint8_t *raw_req, int raw_req_length); + + float modbus_get_float(const uint16_t *src); + void modbus_set_float(float f, uint16_t *dest); + + modbus_t* modbus_new_tcp(const char *ip_address, int port); + modbus_t* modbus_new_rtu(const char *device, int baud, char parity, int data_bit, int stop_bit); + void modbus_free(modbus_t *ctx); + + int modbus_receive(modbus_t *ctx, uint8_t *req); + int modbus_receive_from(modbus_t *ctx, int sockfd, uint8_t *req); + int modbus_receive_confirmation(modbus_t *ctx, uint8_t *rsp); +""") +LIB = compiler.dlopen('modbus') # create our bindings + +#-------------------------------------------------------------------------------- +# helper utilites +#-------------------------------------------------------------------------------- + +def get_float(data): + return LIB.modbus_get_float(data) + +def set_float(value, data): + LIB.modbus_set_float(value, data) + +def cast_to_int16(data): + return int(compiler.cast('int16_t', data)) + +def cast_to_int32(data): + return int(compiler.cast('int32_t', data)) + +#-------------------------------------------------------------------------------- +# level1 client +#-------------------------------------------------------------------------------- + +class LibmodbusLevel1Client(object): + ''' A raw wrapper around the libmodbus c library. Feel free + to use it if you want increased performance and don't mind the + entire protocol not being implemented. + ''' + + @classmethod + def create_tcp_client(klass, host='127.0.0.1', port=Defaults.Port): + ''' Create a TCP modbus client for the supplied parameters. + + :param host: The host to connect to + :param port: The port to connect to on that host + :returns: A new level1 client + ''' + client = LIB.modbus_new_tcp(host.encode(), port) + return klass(client) + + @classmethod + def create_rtu_client(klass, **kwargs): + ''' Create a TCP modbus client for the supplied parameters. + + :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 + :returns: A new level1 client + ''' + port = kwargs.get('port', '/dev/ttyS0') + baudrate = kwargs.get('baud', Defaults.Baudrate) + parity = kwargs.get('parity', Defaults.Parity) + bytesize = kwargs.get('bytesize', Defaults.Bytesize) + stopbits = kwargs.get('stopbits', Defaults.Stopbits) + client = LIB.modbus_new_rtu(port, baudrate, parity, bytesize, stopbits) + return klass(client) + + def __init__(self, client): + ''' Initalize a new instance of the LibmodbusLevel1Client. This + method should not be used, instead new instances should be created + using the two supplied factory methods: + + * LibmodbusLevel1Client.create_rtu_client(...) + * LibmodbusLevel1Client.create_tcp_client(...) + + :param client: The underlying client instance to operate with. + ''' + self.client = client + self.slave = Defaults.UnitId + + def set_slave(self, slave): + ''' Set the current slave to operate against. + + :param slave: The new slave to operate against + :returns: The resulting slave to operate against + ''' + self.slave = self._execute(LIB.modbus_set_slave, slave) + return self.slave + + def connect(self): + ''' Attempt to connect to the client target. + + :returns: True if successful, throws otherwise + ''' + return (self.__execute(LIB.modbus_connect) == 0) + + def flush(self): + ''' Discards the existing bytes on the wire. + + :returns: The number of flushed bytes, or throws + ''' + return self.__execute(LIB.modbus_flush) + + def close(self): + ''' Closes and frees the underlying connection + and context structure. + + :returns: Always True + ''' + LIB.modbus_close(self.client) + LIB.modbus_free(self.client) + return True + + def __execute(self, command, *args): + ''' Run the supplied command against the currently + instantiated client with the supplied arguments. This + will make sure to correctly handle resulting errors. + + :param command: The command to execute against the context + :param *args: The arguments for the given command + :returns: The result of the operation unless -1 which throws + ''' + result = command(self.client, *args) + if result == -1: + message = LIB.modbus_strerror(compiler.errno) + raise ModbusException(compiler.string(message)) + return result + + def read_bits(self, address, count=1): + ''' + + :param address: The starting address to read from + :param count: The number of coils to read + :returns: The resulting bits + ''' + result = compiler.new("uint8_t[]", count) + self.__execute(LIB.modbus_read_bits, address, count, result) + return result + + def read_input_bits(self, address, count=1): + ''' + + :param address: The starting address to read from + :param count: The number of discretes to read + :returns: The resulting bits + ''' + result = compiler.new("uint8_t[]", count) + self.__execute(LIB.modbus_read_input_bits, address, count, result) + return result + + def write_bit(self, address, value): + ''' + + :param address: The starting address to write to + :param value: The value to write to the specified address + :returns: The number of written bits + ''' + return self.__execute(LIB.modbus_write_bit, address, value) + + def write_bits(self, address, values): + ''' + + :param address: The starting address to write to + :param values: The values to write to the specified address + :returns: The number of written bits + ''' + count = len(values) + return self.__execute(LIB.modbus_write_bits, address, count, values) + + def write_register(self, address, value): + ''' + + :param address: The starting address to write to + :param value: The value to write to the specified address + :returns: The number of written registers + ''' + return self.__execute(LIB.modbus_write_register, address, value) + + def write_registers(self, address, values): + ''' + + :param address: The starting address to write to + :param values: The values to write to the specified address + :returns: The number of written registers + ''' + count = len(values) + return self.__execute(LIB.modbus_write_registers, address, count, values) + + def read_registers(self, address, count=1): + ''' + + :param address: The starting address to read from + :param count: The number of registers to read + :returns: The resulting read registers + ''' + result = compiler.new("uint16_t[]", count) + self.__execute(LIB.modbus_read_registers, address, count, result) + return result + + def read_input_registers(self, address, count=1): + ''' + + :param address: The starting address to read from + :param count: The number of registers to read + :returns: The resulting read registers + ''' + result = compiler.new("uint16_t[]", count) + self.__execute(LIB.modbus_read_input_registers, address, count, result) + return result + + def read_and_write_registers(self, read_address, read_count, write_address, write_registers): + ''' + + :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: The resulting read registers + ''' + write_count = len(write_registers) + read_result = compiler.new("uint16_t[]", read_count) + self.__execute(LIB.modbus_write_and_read_registers, + write_address, write_count, write_registers, + read_address, read_count, read_result) + return read_result + +#-------------------------------------------------------------------------------- +# level2 client +#-------------------------------------------------------------------------------- + +class LibmodbusClient(ModbusClientMixin): + ''' A facade around the raw level 1 libmodbus client + that implements the pymodbus protocol on top of the lower level + client. + ''' + + #-----------------------------------------------------------------------# + # these are used to convert from the pymodbus request types to the + # libmodbus operations (overloaded operator). + #-----------------------------------------------------------------------# + + __methods = { + 'ReadCoilsRequest' : lambda c, r: c.read_bits(r.address, r.count), + 'ReadDiscreteInputsRequest' : lambda c, r: c.read_input_bits(r.address, r.count), + 'WriteSingleCoilRequest' : lambda c, r: c.write_bit(r.address, r.value), + 'WriteMultipleCoilsRequest' : lambda c, r: c.write_bits(r.address, r.values), + 'WriteSingleRegisterRequest' : lambda c, r: c.write_register(r.address, r.value), + 'WriteMultipleRegistersRequest' : lambda c, r: c.write_registers(r.address, r.values), + 'ReadHoldingRegistersRequest' : lambda c, r: c.read_registers(r.address, r.count), + 'ReadInputRegistersRequest' : lambda c, r: c.read_input_registers(r.address, r.count), + 'ReadWriteMultipleRegistersRequest' : lambda c, r: c.read_and_write_registers(r.read_address, r.read_count, r.write_address, r.write_registers), + } + + #-----------------------------------------------------------------------# + # these are used to convert from the libmodbus result to the + # pymodbus response type + #-----------------------------------------------------------------------# + + __adapters = { + 'ReadCoilsRequest' : lambda tx, rx: ReadCoilsResponse(list(rx)), + 'ReadDiscreteInputsRequest' : lambda tx, rx: ReadDiscreteInputsResponse(list(rx)), + 'WriteSingleCoilRequest' : lambda tx, rx: WriteSingleCoilResponse(tx.address, rx), + 'WriteMultipleCoilsRequest' : lambda tx, rx: WriteMultipleCoilsResponse(tx.address, rx), + 'WriteSingleRegisterRequest' : lambda tx, rx: WriteSingleRegisterResponse(tx.address, rx), + 'WriteMultipleRegistersRequest' : lambda tx, rx: WriteMultipleRegistersResponse(tx.address, rx), + 'ReadHoldingRegistersRequest' : lambda tx, rx: ReadHoldingRegistersResponse(list(rx)), + 'ReadInputRegistersRequest' : lambda tx, rx: ReadInputRegistersResponse(list(rx)), + 'ReadWriteMultipleRegistersRequest' : lambda tx, rx: ReadWriteMultipleRegistersResponse(list(rx)), + } + + def __init__(self, client): + ''' Initalize a new instance of the LibmodbusClient. This should + be initialized with one of the LibmodbusLevel1Client instances: + + * LibmodbusLevel1Client.create_rtu_client(...) + * LibmodbusLevel1Client.create_tcp_client(...) + + :param client: The underlying client instance to operate with. + ''' + self.client = client + + #-----------------------------------------------------------------------# + # We use the client mixin to implement the api methods which are all + # forwarded to this method. It is implemented using the previously + # defined lookup tables. Any method not defined simply throws. + #-----------------------------------------------------------------------# + + def execute(self, request): + ''' Execute the supplied request against the server. + + :param request: The request to process + :returns: The result of the request execution + ''' + if self.client.slave != request.unit_id: + self.client.set_slave(request.unit_id) + + method = request.__class__.__name__ + operation = self.__methods.get(method, None) + adapter = self.__adapters.get(method, None) + + if not operation or not adapter: + raise NotImplementedException("Method not implemented: " + name) + + response = operation(self.client, request) + return adapter(request, response) + + #-----------------------------------------------------------------------# + # Other methods can simply be forwarded using the decorator pattern + #-----------------------------------------------------------------------# + + def connect(self): return self.client.connect() + def close(self): return self.client.close() + + #-----------------------------------------------------------------------# + # magic methods + #-----------------------------------------------------------------------# + + def __enter__(self): + ''' Implement the client with enter block + + :returns: The current instance of the client + ''' + self.client.connect() + return self + + def __exit__(self, klass, value, traceback): + ''' Implement the client with exit block ''' + self.client.close() + +#--------------------------------------------------------------------------# +# main example runner +#--------------------------------------------------------------------------# + +if __name__ == '__main__': + + # create our low level client + host = '127.0.0.1' + port = 502 + protocol = LibmodbusLevel1Client.create_tcp_client(host, port) + + # operate with our high level client + with LibmodbusClient(protocol) as client: + registers = client.write_registers(0, [13, 12, 11]) + print registers + registers = client.read_holding_registers(0, 10) + print registers.registers diff --git a/examples/contrib/modbus-scraper.py b/examples/contrib/modbus-scraper.py new file mode 100755 index 000000000..6d256c12e --- /dev/null +++ b/examples/contrib/modbus-scraper.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python +''' +This is a simple scraper that can be pointed at a +modbus device to pull down all its values and store +them as a collection of sequential data blocks. +''' +import pickle +from optparse import OptionParser +from twisted.internet import serialport, reactor +from twisted.internet.protocol import ClientFactory +from pymodbus.datastore import ModbusSequentialDataBlock +from pymodbus.datastore import ModbusSlaveContext +from pymodbus.factory import ClientDecoder +from pymodbus.client.async import ModbusClientProtocol + +#--------------------------------------------------------------------------# +# Configure the client logging +#--------------------------------------------------------------------------# +import logging +log = logging.getLogger("pymodbus") + +#---------------------------------------------------------------------------# +# Choose the framer you want to use +#---------------------------------------------------------------------------# +from pymodbus.transaction import ModbusBinaryFramer +from pymodbus.transaction import ModbusAsciiFramer +from pymodbus.transaction import ModbusRtuFramer +from pymodbus.transaction import ModbusSocketFramer + +#---------------------------------------------------------------------------# +# Define some constants +#---------------------------------------------------------------------------# +COUNT = 8 # The number of bits/registers to read at once +DELAY = 0 # The delay between subsequent reads +SLAVE = 0x01 # The slave unit id to read from + +#---------------------------------------------------------------------------# +# A simple scraper protocol +#---------------------------------------------------------------------------# +# I tried to spread the load across the device, but feel free to modify the +# logic to suit your own purpose. +#---------------------------------------------------------------------------# +class ScraperProtocol(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 + + def connectionMade(self): + ''' Callback for when the client has connected + to the remote server. + ''' + super(ScraperProtocol, self).connectionMade() + log.debug("Beginning the processing loop") + self.address = self.factory.starting + reactor.callLater(DELAY, self.scrape_holding_registers) + + def connectionLost(self, reason): + ''' Callback for when the client disconnects from the + server. + + :param reason: The reason for the disconnection + ''' + reactor.callLater(DELAY, reactor.stop) + + def scrape_holding_registers(self): + ''' Defer fetching holding registers + ''' + log.debug("reading holding registers: %d" % self.address) + d = self.read_holding_registers(self.address, count=COUNT, unit=SLAVE) + d.addCallbacks(self.scrape_discrete_inputs, self.error_handler) + + def scrape_discrete_inputs(self, response): + ''' Defer fetching holding registers + ''' + log.debug("reading discrete inputs: %d" % self.address) + self.endpoint.write((3, self.address, response.registers)) + d = self.read_discrete_inputs(self.address, count=COUNT, unit=SLAVE) + d.addCallbacks(self.scrape_input_registers, self.error_handler) + + def scrape_input_registers(self, response): + ''' Defer fetching holding registers + ''' + log.debug("reading discrete inputs: %d" % self.address) + self.endpoint.write((2, self.address, response.bits)) + d = self.read_input_registers(self.address, count=COUNT, unit=SLAVE) + d.addCallbacks(self.scrape_coils, self.error_handler) + + def scrape_coils(self, response): + ''' Write values of holding registers, defer fetching coils + + :param response: The response to process + ''' + log.debug("reading coils: %d" % self.address) + self.endpoint.write((4, self.address, response.registers)) + d = self.read_coils(self.address, count=COUNT, unit=SLAVE) + 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 + ''' + log.debug("starting next round: %d" % self.address) + self.endpoint.write((1, self.address, response.bits)) + self.address += COUNT + if self.address >= self.factory.ending: + self.endpoint.finalize() + self.transport.loseConnection() + else: reactor.callLater(DELAY, self.scrape_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 ScraperFactory(ClientFactory): + + protocol = ScraperProtocol + + def __init__(self, framer, endpoint, query): + ''' Remember things necessary for building a protocols ''' + self.framer = framer + self.endpoint = endpoint + self.starting, self.ending = query + + def buildProtocol(self, _): + ''' Create a protocol and start the reading cycle ''' + protocol = self.protocol(self.framer, self.endpoint) + protocol.factory = self + return protocol + + +#---------------------------------------------------------------------------# +# 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 LoggingContextReader(object): + + def __init__(self, output): + ''' Initialize a new instance of the logger + + :param output: The output file to save to + ''' + self.output = output + self.context = ModbusSlaveContext( + di = ModbusSequentialDataBlock.create(), + co = ModbusSequentialDataBlock.create(), + hr = ModbusSequentialDataBlock.create(), + ir = ModbusSequentialDataBlock.create()) + + def write(self, response): + ''' Handle the next modbus response + + :param response: The response to process + ''' + log.info("Read Data: %s" % str(response)) + fx, address, values = response + self.context.setValues(fx, address, values) + + def finalize(self): + with open(self.output, "w") as handle: + pickle.dump(self.context, handle) + + +#--------------------------------------------------------------------------# +# Main start point +#--------------------------------------------------------------------------# +def get_options(): + ''' A helper method to parse the command line options + + :returns: The options manager + ''' + parser = OptionParser() + + parser.add_option("-o", "--output", + help="The resulting output file for the scrape", + dest="output", default="datastore.pickle") + + parser.add_option("-p", "--port", + help="The port to connect to", type='int', + dest="port", default=502) + + parser.add_option("-s", "--server", + help="The server to scrape", + dest="host", default="127.0.0.1") + + parser.add_option("-r", "--range", + help="The address range to scan", + dest="query", default="0:1000") + + parser.add_option("-d", "--debug", + help="Enable debug tracing", + action="store_true", dest="debug", default=False) + + (opt, arg) = parser.parse_args() + return opt + +def main(): + ''' The main runner function ''' + options = get_options() + + if options.debug: + try: + log.setLevel(logging.DEBUG) + logging.basicConfig() + except Exception, ex: + print "Logging is not supported on this system" + + # split the query into a starting and ending range + query = [int(p) for p in options.query.split(':')] + + try: + log.debug("Initializing the client") + framer = ModbusSocketFramer(ClientDecoder()) + reader = LoggingContextReader(options.output) + factory = ScraperFactory(framer, reader, query) + + # how to connect based on TCP vs Serial clients + if isinstance(framer, ModbusSocketFramer): + reactor.connectTCP(options.host, options.port, factory) + else: SerialModbusClient(factory, options.port, reactor) + + log.debug("Starting the client") + reactor.run() + log.debug("Finished scraping the client") + except Exception, ex: + print ex + +#---------------------------------------------------------------------------# +# Main jumper +#---------------------------------------------------------------------------# +if __name__ == "__main__": + main() diff --git a/examples/contrib/modicon-payload.py b/examples/contrib/modicon-payload.py index 3fe08a74a..97d9ec7e4 100644 --- a/examples/contrib/modicon-payload.py +++ b/examples/contrib/modicon-payload.py @@ -161,13 +161,14 @@ class ModiconPayloadDecoder(object): second = decoder.decode_16bit_uint() ''' - def __init__(self, payload): + def __init__(self, payload, endian): ''' Initialize a new payload decoder :param payload: The payload to decode with ''' self._payload = payload self._pointer = 0x00 + self._endian = endian @staticmethod def fromRegisters(registers, endian=Endian.Little): diff --git a/examples/contrib/remote_server_context.py b/examples/contrib/remote_server_context.py new file mode 100644 index 000000000..e8e027007 --- /dev/null +++ b/examples/contrib/remote_server_context.py @@ -0,0 +1,196 @@ +''' +Although there is a remote server context already in the main library, +it works under the assumption that users would have a server context +of the following form:: + + server_context = { + 0x00: client('host1.something.com'), + 0x01: client('host2.something.com'), + 0x02: client('host3.something.com') + } + +This example is how to create a server context where the client is +pointing to the same host, but the requested slave id is used as the +slave for the client:: + + server_context = { + 0x00: client('host1.something.com', 0x00), + 0x01: client('host1.something.com', 0x01), + 0x02: client('host1.something.com', 0x02) + } +''' +from pymodbus.exceptions import NotImplementedException +from pymodbus.interfaces import IModbusSlaveContext + +#---------------------------------------------------------------------------# +# Logging +#---------------------------------------------------------------------------# + +import logging +_logger = logging.getLogger(__name__) + +#---------------------------------------------------------------------------# +# Slave Context +#---------------------------------------------------------------------------# +# Basically we create a new slave context for the given slave identifier so +# that this slave context will only make requests to that slave with the +# client that the server is maintaining. +#---------------------------------------------------------------------------# + +class RemoteSingleSlaveContext(IModbusSlaveContext): + ''' This is a remote server context that allows one + to create a server context backed by a single client that + may be attached to many slave units. This can be used to + effectively create a modbus forwarding server. + ''' + + def __init__(self, context, unit_id): + ''' Initializes the datastores + + :param context: The underlying context to operate with + :param unit_id: The slave that this context will contact + ''' + self.context = context + self.unit_id = unit_id + + def reset(self): + ''' Resets all the datastores to their default values ''' + raise NotImplementedException() + + 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)) + result = context.get_callbacks[self.decode(fx)](address, count, self.unit_id) + return result.function_code < 0x80 + + 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("get values[%d] %d:%d" % (fx, address, count)) + result = context.get_callbacks[self.decode(fx)](address, count, self.unit_id) + return self.__extract_result(self.decode(fx), result) + + 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("set values[%d] %d:%d" % (fx, address, len(values))) + context.set_callbacks[self.decode(fx)](address, values, self.unit_id) + + def __str__(self): + ''' Returns a string representation of the context + + :returns: A string representation of the context + ''' + return "Remote Single Slave Context(%s)" % self.unit_id + + def __extract_result(self, fx, result): + ''' A helper method to extract the values out of + a response. The future api should make the result + consistent so we can just call `result.getValues()`. + + :param fx: The function to call + :param result: The resulting data + ''' + if result.function_code < 0x80: + if fx in ['d', 'c']: return result.bits + if fx in ['h', 'i']: return result.registers + else: return result + +#---------------------------------------------------------------------------# +# Server Context +#---------------------------------------------------------------------------# +# Think of this as simply a dictionary of { unit_id: client(req, unit_id) } +#---------------------------------------------------------------------------# + +class RemoteServerContext(object): + ''' This is a remote server context that allows one + to create a server context backed by a single client that + may be attached to many slave units. This can be used to + effectively create a modbus forwarding server. + ''' + + def __init__(self, client): + ''' Initializes the datastores + + :param client: The client to retrieve values with + ''' + self.get_callbacks = { + 'd': lambda a, c, s: client.read_discrete_inputs(a, c, s), + 'c': lambda a, c, s: client.read_coils(a, c, s), + 'h': lambda a, c, s: client.read_holding_registers(a, c, s), + 'i': lambda a, c, s: client.read_input_registers(a, c, s), + } + self.set_callbacks = { + 'd': lambda a, v, s: client.write_coils(a, v, s), + 'c': lambda a, v, s: client.write_coils(a, v, s), + 'h': lambda a, v, s: client.write_registers(a, v, s), + 'i': lambda a, v, s: client.write_registers(a, v, s), + } + self.slaves = {} # simply a cache + + def __str__(self): + ''' Returns a string representation of the context + + :returns: A string representation of the context + ''' + return "Remote Server Context(%s)" % self._client + + def __iter__(self): + ''' Iterater over the current collection of slave + contexts. + + :returns: An iterator over the slave contexts + ''' + # note, this may not include all slaves + 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 + ''' + # we don't want to check the cache here as the + # slave may not exist yet or may not exist any + # more. The best thing to do is try and fail. + return True + + def __setitem__(self, slave, context): + ''' Used to set a new slave context + + :param slave: The slave context to set + :param context: The new context to set for this slave + ''' + raise NotImplementedException() # doesn't make sense here + + def __delitem__(self, slave): + ''' Wrapper used to access the slave context + + :param slave: The slave context to remove + ''' + raise NotImplementedException() # doesn't make sense here + + def __getitem__(self, slave): + ''' Used to get access to a slave context + + :param slave: The slave context to get + :returns: The requested slave context + ''' + if slave not in self.slaves: + self.slaves[slave] = RemoteSingleSlaveContext(self, slave) + return self.slaves[slave] diff --git a/examples/contrib/thread_safe_datastore.py b/examples/contrib/thread_safe_datastore.py new file mode 100644 index 000000000..6a13e947e --- /dev/null +++ b/examples/contrib/thread_safe_datastore.py @@ -0,0 +1,209 @@ +import threading +from contextlib import contextmanager +from pymodbus.datastore.store import BaseModbusDataBlock + + +class ContextWrapper(object): + ''' This is a simple wrapper around enter + and exit functions that conforms to the pyhton + context manager protocol: + + with ContextWrapper(enter, leave): + do_something() + ''' + + def __init__(self, enter=None, leave=None, factory=None): + self._enter = enter + self._leave = leave + self._factory = factory + + def __enter__(self): + if self.enter: self._enter() + return self if not self._factory else self._factory() + + def __exit__(self, args): + if self._leave: self._leave() + + +class ReadWriteLock(object): + ''' This reader writer lock gurantees write order, but not + read order and is generally biased towards allowing writes + if they are available to prevent starvation. + + TODO: + + * allow user to choose between read/write/random biasing + - currently write biased + - read biased allow N readers in queue + - random is 50/50 choice of next + ''' + + def __init__(self): + ''' Initializes a new instance of the ReadWriteLock + ''' + self.queue = [] # the current writer queue + self.lock = threading.Lock() # the underlying condition lock + self.read_condition = threading.Condition(self.lock) # the single reader condition + self.readers = 0 # the number of current readers + self.writer = False # is there a current writer + + def __is_pending_writer(self): + return (self.writer # if there is a current writer + or (self.queue # or if there is a waiting writer + and (self.queue[0] != self.read_condition))) # or if the queue head is not a reader + + def acquire_reader(self): + ''' Notifies the lock that a new reader is requesting + the underlying resource. + ''' + with self.lock: + if self.__is_pending_writer(): # if there are existing writers waiting + if self.read_condition not in self.queue: # do not pollute the queue with readers + self.queue.append(self.read_condition) # add the readers in line for the queue + while self.__is_pending_writer(): # until the current writer is finished + self.read_condition.wait(1) # wait on our condition + if self.queue and self.read_condition == self.queue[0]: # if the read condition is at the queue head + self.queue.pop(0) # then go ahead and remove it + self.readers += 1 # update the current number of readers + + def acquire_writer(self): + ''' Notifies the lock that a new writer is requesting + the underlying resource. + ''' + with self.lock: + if self.writer or self.readers: # if we need to wait on a writer or readers + condition = threading.Condition(self.lock) # create a condition just for this writer + self.queue.append(condition) # and put it on the waiting queue + while self.writer or self.readers: # until the write lock is free + condition.wait(1) # wait on our condition + self.queue.pop(0) # remove our condition after our condition is met + self.writer = True # stop other writers from operating + + def release_reader(self): + ''' Notifies the lock that an existing reader is + finished with the underlying resource. + ''' + with self.lock: + self.readers = max(0, self.readers - 1) # readers should never go below 0 + if not self.readers and self.queue: # if there are no active readers + self.queue[0].notify_all() # then notify any waiting writers + + def release_writer(self): + ''' Notifies the lock that an existing writer is + finished with the underlying resource. + ''' + with self.lock: + self.writer = False # give up current writing handle + if self.queue: # if someone is waiting in the queue + self.queue[0].notify_all() # wake them up first + else: self.read_condition.notify_all() # otherwise wake up all possible readers + + @contextmanager + def get_reader_lock(self): + ''' Wrap some code with a reader lock using the + python context manager protocol:: + + with rwlock.get_reader_lock(): + do_read_operation() + ''' + try: + self.acquire_reader() + yield self + finally: self.release_reader() + + @contextmanager + def get_writer_lock(self): + ''' Wrap some code with a writer lock using the + python context manager protocol:: + + with rwlock.get_writer_lock(): + do_read_operation() + ''' + try: + self.acquire_writer() + yield self + finally: self.release_writer() + + +class ThreadSafeDataBlock(BaseModbusDataBlock): + ''' This is a simple decorator for a data block. This allows + a user to inject an existing data block which can then be + safely operated on from multiple cocurrent threads. + + It should be noted that the choice was made to lock around the + datablock instead of the manager as there is less source of + contention (writes can occur to slave 0x01 while reads can + occur to slave 0x02). + ''' + + def __init__(self, block): + ''' Initialize a new thread safe decorator + + :param block: The block to decorate + ''' + self.rwlock = ReadWriteLock() + self.block = block + + 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 + ''' + with self.rwlock.get_reader_lock(): + return self.block.validate(address, count) + + 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 + ''' + with self.rwlock.get_reader_lock(): + return self.block.getValues(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 + ''' + with self.rwlock.get_writer_lock(): + return self.block.setValues(address, values) + + +if __name__ == "__main__": + + class AtomicCounter(object): + def __init__(self, **kwargs): + self.counter = kwargs.get('start', 0) + self.finish = kwargs.get('finish', 1000) + self.lock = threading.Lock() + + def increment(self, count=1): + with self.lock: + self.counter += count + + def is_running(self): + return self.counter <= self.finish + + locker = ReadWriteLock() + readers, writers = AtomicCounter(), AtomicCounter() + + def read(): + while writers.is_running() and readers.is_running(): + with locker.get_reader_lock(): + readers.increment() + + def write(): + while writers.is_running() and readers.is_running(): + with locker.get_writer_lock(): + writers.increment() + + rthreads = [threading.Thread(target=read) for i in range(50)] + wthreads = [threading.Thread(target=write) for i in range(2)] + for t in rthreads + wthreads: t.start() + for t in rthreads + wthreads: t.join() + print "readers[%d] writers[%d]" % (readers.counter, writers.counter) diff --git a/examples/tools/test-install.sh b/examples/tools/test-install.sh index 0b46ee07f..9d85326b8 100755 --- a/examples/tools/test-install.sh +++ b/examples/tools/test-install.sh @@ -16,7 +16,7 @@ elif [[ "`which easy_install`" != "" ]]; then INSTALL="easy_install -qU" else echo -e "\E[31m" - echo "\E[31mPlease install distutils before continuting" + echo "\E[31mPlease install distutils before continuing" echo "wget http://peak.telecommunity.com/dist/ez_setup.py | sudo python" echo -e "\E[0m" exit -1 @@ -24,7 +24,7 @@ fi if [[ "`which virtualenv`" == "" ]]; then echo -e "\E[31m" - echo "Please install virtualenv before continuting" + echo "Please install virtualenv before continuing" echo "sudo easy_install virtualenv" echo -e "\E[0m" exit -1 diff --git a/pymodbus/client/async.py b/pymodbus/client/async.py index 912641a0a..6dc332f37 100644 --- a/pymodbus/client/async.py +++ b/pymodbus/client/async.py @@ -57,7 +57,7 @@ class ModbusClientProtocol(protocol.Protocol, ModbusClientMixin): layer code is deferred to a higher level wrapper. ''' - def __init__(self, framer=None): + def __init__(self, framer=None, **kwargs): ''' Initializes the framer module :param framer: The framer to use for the protocol @@ -65,8 +65,8 @@ def __init__(self, framer=None): self._connected = False self.framer = framer or ModbusSocketFramer(ClientDecoder()) if isinstance(self.framer, ModbusSocketFramer): - self.transaction = DictTransactionManager(self) - else: self.transaction = FifoTransactionManager(self) + self.transaction = DictTransactionManager(self, **kwargs) + else: self.transaction = FifoTransactionManager(self, **kwargs) def connectionMade(self): ''' Called upon a successful client connection. @@ -146,15 +146,15 @@ class ModbusUdpClientProtocol(protocol.DatagramProtocol, ModbusClientMixin): layer code is deferred to a higher level wrapper. ''' - def __init__(self, framer=None): + def __init__(self, framer=None, **kwargs): ''' Initializes the framer module :param framer: The framer to use for the protocol ''' self.framer = framer or ModbusSocketFramer(ClientDecoder()) if isinstance(self.framer, ModbusSocketFramer): - self.transaction = DictTransactionManager(self) - else: self.transaction = FifoTransactionManager(self) + self.transaction = DictTransactionManager(self, **kwargs) + else: self.transaction = FifoTransactionManager(self, **kwargs) def datagramReceived(self, data, params): ''' Get response, check for valid message, decode result diff --git a/pymodbus/client/common.py b/pymodbus/client/common.py index 4b0caa143..4e2f4bde3 100644 --- a/pymodbus/client/common.py +++ b/pymodbus/client/common.py @@ -132,6 +132,18 @@ def readwrite_registers(self, *args, **kwargs): request = ReadWriteMultipleRegistersRequest(*args, **kwargs) return self.execute(request) + def mask_write_register(self, *args, **kwargs): + ''' + + :param address: The address of the register to write + :param and_mask: The and bitmask to apply to the register address + :param or_mask: The or bitmask to apply to the register address + :param unit: The slave unit this request is targeting + :returns: A deferred response handle + ''' + request = MaskWriteRegisterRequest(*args, **kwargs) + return self.execute(request) + #---------------------------------------------------------------------------# # Exported symbols #---------------------------------------------------------------------------# diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py index d01fbb2e9..1c1fc3ade 100644 --- a/pymodbus/client/sync.py +++ b/pymodbus/client/sync.py @@ -29,15 +29,15 @@ class BaseModbusClient(ModbusClientMixin): framer. ''' - def __init__(self, framer): + def __init__(self, framer, **kwargs): ''' Initialize a client instance :param framer: The modbus framer implementation to use ''' self.framer = framer if isinstance(self.framer, ModbusSocketFramer): - self.transaction = DictTransactionManager(self) - else: self.transaction = FifoTransactionManager(self) + self.transaction = DictTransactionManager(self, **kwargs) + else: self.transaction = FifoTransactionManager(self, **kwargs) #-----------------------------------------------------------------------# # Client interface @@ -113,19 +113,22 @@ class ModbusTcpClient(BaseModbusClient): ''' Implementation of a modbus tcp client ''' - def __init__(self, host='127.0.0.1', port=Defaults.Port, framer=ModbusSocketFramer): + def __init__(self, host='127.0.0.1', port=Defaults.Port, + framer=ModbusSocketFramer, **kwargs): ''' Initialize a client instance :param host: The host to connect to (default 127.0.0.1) :param port: The modbus port to connect to (default 502) + :param source_address: The source address tuple to bind to (default ('', 0)) :param framer: The modbus framer to use (default ModbusSocketFramer) .. note:: The host argument will accept ipv4 and ipv6 hosts ''' self.host = host self.port = port + self.source_address = kwargs.get('source_address', ('', 0)) self.socket = None - BaseModbusClient.__init__(self, framer(ClientDecoder())) + BaseModbusClient.__init__(self, framer(ClientDecoder()), **kwargs) def connect(self): ''' Connect to the modbus tcp server @@ -185,17 +188,20 @@ class ModbusUdpClient(BaseModbusClient): ''' Implementation of a modbus udp client ''' - def __init__(self, host='127.0.0.1', port=Defaults.Port, framer=ModbusSocketFramer): + def __init__(self, host='127.0.0.1', port=Defaults.Port, + framer=ModbusSocketFramer, **kwargs): ''' Initialize a client instance :param host: The host to connect to (default 127.0.0.1) :param port: The modbus port to connect to (default 502) :param framer: The modbus framer to use (default ModbusSocketFramer) + :param timeout: The timeout to use for this socket (default None) ''' - self.host = host - self.port = port - self.socket = None - BaseModbusClient.__init__(self, framer(ClientDecoder())) + self.host = host + self.port = port + self.socket = None + self.timeout = kwargs.get('timeout', None) + BaseModbusClient.__init__(self, framer(ClientDecoder()), **kwargs) @classmethod def _get_address_family(cls, address): @@ -286,7 +292,7 @@ def __init__(self, method='ascii', **kwargs): ''' self.method = method self.socket = None - BaseModbusClient.__init__(self, self.__implementation(method)) + BaseModbusClient.__init__(self, self.__implementation(method), **kwargs) self.port = kwargs.get('port', 0) self.stopbits = kwargs.get('stopbits', Defaults.Stopbits) @@ -310,7 +316,7 @@ def __implementation(method): raise ParameterException("Invalid framer method requested") def connect(self): - ''' Connect to the modbus tcp server + ''' Connect to the modbus serial server :returns: True if connection succeeded, False otherwise ''' diff --git a/pymodbus/constants.py b/pymodbus/constants.py index e1d986998..45b722e19 100644 --- a/pymodbus/constants.py +++ b/pymodbus/constants.py @@ -20,6 +20,12 @@ class Defaults(Singleton): The default number of times a client should retry the given request before failing (3) + .. attribute:: RetryOnEmpty + + A flag indicating if a transaction should be retried in the + case that an empty response is received. This is useful for + slow clients that may need more time to process a requst. + .. attribute:: Timeout The default amount of time a client should wait for a request @@ -70,18 +76,33 @@ class Defaults(Singleton): The number of bits sent after each character in a message to indicate the end of the byte. This defaults to 1. + + .. attribute:: ZeroMode + + Indicates if the slave datastore should use indexing at 0 or 1. + More about this can be read in section 4.4 of the modbus specification. + + .. attribute:: IgnoreMissingSlaves + + In case a request is made to a missing slave, this defines if an error + should be returned or simply ignored. This is useful for the case of a + serial server emulater where a request to a non-existant slave on a bus + will never respond. The client in this case will simply timeout. ''' - Port = 502 - Retries = 3 - Timeout = 3 - Reconnects = 0 - TransactionId = 0 - ProtocolId = 0 - UnitId = 0x00 - Baudrate = 19200 - Parity = 'N' - Bytesize = 8 - Stopbits = 1 + Port = 502 + Retries = 3 + RetryOnEmpty = False + Timeout = 3 + Reconnects = 0 + TransactionId = 0 + ProtocolId = 0 + UnitId = 0x00 + Baudrate = 19200 + Parity = 'N' + Bytesize = 8 + Stopbits = 1 + ZeroMode = False + IgnoreMissingSlaves = False class ModbusStatus(Singleton): diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index 17297ea7f..45d16574d 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -1,4 +1,4 @@ -from pymodbus.exceptions import ParameterException +from pymodbus.exceptions import NoSuchSlaveException from pymodbus.interfaces import IModbusSlaveContext from pymodbus.datastore.store import ModbusSequentialDataBlock from pymodbus.constants import Defaults @@ -36,6 +36,7 @@ def __init__(self, *args, **kwargs): self.store['c'] = kwargs.get('co', ModbusSequentialDataBlock.create()) self.store['i'] = kwargs.get('ir', ModbusSequentialDataBlock.create()) self.store['h'] = kwargs.get('hr', ModbusSequentialDataBlock.create()) + self.zero_mode = kwargs.get('zero_mode', Defaults.ZeroMode) def __str__(self): ''' Returns a string representation of the context @@ -57,7 +58,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 + if not self.zero_mode: address = address + 1 _logger.debug("validate[%d] %d:%d" % (fx, address, count)) return self.store[self.decode(fx)].validate(address, count) @@ -69,7 +70,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 + if not self.zero_mode: address = address + 1 _logger.debug("getValues[%d] %d:%d" % (fx, address, count)) return self.store[self.decode(fx)].getValues(address, count) @@ -80,7 +81,7 @@ 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 + if not self.zero_mode: address = address + 1 _logger.debug("setValues[%d] %d:%d" % (fx, address, len(values))) self.store[self.decode(fx)].setValues(address, values) @@ -129,7 +130,7 @@ def __setitem__(self, slave, context): if self.single: slave = Defaults.UnitId if 0xf7 >= slave >= 0x00: self.__slaves[slave] = context - else: raise ParameterException('slave index out of range') + else: raise NoSuchSlaveException('slave index[%d] out of range' % slave) def __delitem__(self, slave): ''' Wrapper used to access the slave context @@ -138,7 +139,7 @@ def __delitem__(self, slave): ''' if not self.single and (0xf7 >= slave >= 0x00): del self.__slaves[slave] - else: raise ParameterException('slave index out of range') + else: raise NoSuchSlaveException('slave index[%d] out of range' % slave) def __getitem__(self, slave): ''' Used to get access to a slave context @@ -149,4 +150,4 @@ def __getitem__(self, slave): 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") + else: raise NoSuchSlaveException('slave index[%d] out of range' % slave) diff --git a/pymodbus/exceptions.py b/pymodbus/exceptions.py index eea0b4ce6..e35db8da1 100644 --- a/pymodbus/exceptions.py +++ b/pymodbus/exceptions.py @@ -11,6 +11,7 @@ class ModbusException(Exception): def __init__(self, string): ''' Initialize the exception + :param string: The message to append to the error ''' self.string = string @@ -24,6 +25,7 @@ class ModbusIOException(ModbusException): def __init__(self, string=""): ''' Initialize the exception + :param string: The message to append to the error ''' message = "[Input/Output] %s" % string @@ -31,13 +33,27 @@ def __init__(self, string=""): class ParameterException(ModbusException): - ''' Error resulting from invalid paramater ''' + ''' Error resulting from invalid parameter ''' + + def __init__(self, string=""): + ''' Initialize the exception + + :param string: The message to append to the error + ''' + message = "[Invalid Parameter] %s" % string + ModbusException.__init__(self, message) + + +class NoSuchSlaveException(ModbusException): + ''' Error resulting from making a request to a slave + that does not exist ''' def __init__(self, string=""): ''' Initialize the exception + :param string: The message to append to the error ''' - message = "[Invalid Paramter] %s" % string + message = "[No Such Slave] %s" % string ModbusException.__init__(self, message) @@ -46,6 +62,7 @@ class NotImplementedException(ModbusException): def __init__(self, string=""): ''' Initialize the exception + :param string: The message to append to the error ''' message = "[Not Implemented] %s" % string @@ -57,6 +74,7 @@ class ConnectionException(ModbusException): def __init__(self, string=""): ''' Initialize the exception + :param string: The message to append to the error ''' message = "[Connection] %s" % string @@ -68,5 +86,5 @@ def __init__(self, string=""): __all__ = [ "ModbusException", "ModbusIOException", "ParameterException", "NotImplementedException", - "ConnectionException", + "ConnectionException", "NoSuchSlaveException", ] diff --git a/pymodbus/mei_message.py b/pymodbus/mei_message.py index fbc597f76..2e37c29f0 100644 --- a/pymodbus/mei_message.py +++ b/pymodbus/mei_message.py @@ -26,11 +26,11 @@ class ReadDeviceInformationRequest(ModbusRequest): 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. + called objects and an object Id identifies them. ''' function_code = 0x2b sub_function_code = 0x0e - _rtu_frame_size = 3 + _rtu_frame_size = 7 def __init__(self, read_code=None, object_id=0x00, **kwargs): ''' Initializes a new instance diff --git a/pymodbus/other_message.py b/pymodbus/other_message.py index fccae76d6..d63290fbb 100644 --- a/pymodbus/other_message.py +++ b/pymodbus/other_message.py @@ -43,7 +43,7 @@ def decode(self, data): ''' pass - def execute(self): + def execute(self, context): ''' Run a read exeception status request against the store :returns: The populated response @@ -144,7 +144,7 @@ def decode(self, data): ''' pass - def execute(self): + def execute(self, context): ''' Run a read exeception status request against the store :returns: The populated response @@ -249,7 +249,7 @@ def decode(self, data): ''' pass - def execute(self): + def execute(self, context): ''' Run a read exeception status request against the store :returns: The populated response @@ -359,7 +359,7 @@ def decode(self, data): ''' pass - def execute(self): + def execute(self, context): ''' Run a read exeception status request against the store :returns: The populated response diff --git a/pymodbus/payload.py b/pymodbus/payload.py index a39def7f1..58097b22a 100644 --- a/pymodbus/payload.py +++ b/pymodbus/payload.py @@ -55,6 +55,16 @@ def reset(self): ''' self._payload = [] + def to_registers(self): + ''' Convert the payload buffer into a register + layout that can be used as a context block. + + :returns: The register layout to use as a block + ''' + fstring = self._endian + 'H' + payload = self.build() + return [unpack(fstring, value)[0] for value in payload] + def build(self): ''' Return the payload buffer as a list diff --git a/pymodbus/register_write_message.py b/pymodbus/register_write_message.py index edd29f818..ad2fa2ef0 100644 --- a/pymodbus/register_write_message.py +++ b/pymodbus/register_write_message.py @@ -132,9 +132,11 @@ def __init__(self, address=None, values=None, **kwargs): ''' ModbusRequest.__init__(self, **kwargs) self.address = address - self.values = values or [] - if not hasattr(values, '__iter__'): + if values is None: + values = [] + elif not hasattr(values, '__iter__'): values = [values] + self.values = values self.count = len(self.values) self.byte_count = self.count * 2 diff --git a/pymodbus/server/async.py b/pymodbus/server/async.py index a69080ae6..4b5ca3b08 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -3,6 +3,7 @@ ------------------------------------------ ''' +import traceback from binascii import b2a_hex from twisted.internet import protocol from twisted.internet.protocol import ServerFactory @@ -13,11 +14,13 @@ from pymodbus.device import ModbusControlBlock from pymodbus.device import ModbusAccessControl from pymodbus.device import ModbusDeviceIdentification +from pymodbus.exceptions import NoSuchSlaveException from pymodbus.transaction import ModbusSocketFramer, ModbusAsciiFramer from pymodbus.pdu import ModbusExceptions as merror from pymodbus.internal.ptwisted import InstallManagementConsole from pymodbus.compat import byte2int + #---------------------------------------------------------------------------# # Logging #---------------------------------------------------------------------------# @@ -66,6 +69,11 @@ def _execute(self, request): try: context = self.factory.store[request.unit_id] response = request.execute(context) + except NoSuchSlaveException, ex: + _logger.debug("requested slave does not exist: %s; %s", ex, traceback.format_exc() ) + if self.factory.ignore_missing_slaves: + return # the client will simply timeout waiting for a response + response = request.doException(merror.GatewayNoResponse) except Exception as ex: _logger.debug("Datastore unable to fulfill request: %s" % ex) response = request.doException(merror.SlaveFailure) @@ -97,7 +105,7 @@ class ModbusServerFactory(ServerFactory): protocol = ModbusTcpProtocol - def __init__(self, store, framer=None, identity=None): + def __init__(self, store, framer=None, identity=None, **kwargs): ''' Overloaded initializer for the modbus factory If the identify structure is not passed in, the ModbusControlBlock @@ -106,13 +114,14 @@ def __init__(self, store, framer=None, identity=None): :param store: The ModbusServerContext datastore :param framer: The framer strategy to use :param identity: An optional identify structure - + :param ignore_missing_slaves: True to not send errors on a request to a missing slave ''' self.decoder = ServerDecoder() self.framer = framer or ModbusSocketFramer self.store = store or ModbusServerContext() self.control = ModbusControlBlock() self.access = ModbusAccessControl() + self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', Defaults.IgnoreMissingSlaves) if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) @@ -124,7 +133,7 @@ def __init__(self, store, framer=None, identity=None): class ModbusUdpProtocol(protocol.DatagramProtocol): ''' Implements a modbus udp server in twisted ''' - def __init__(self, store, framer=None, identity=None): + def __init__(self, store, framer=None, identity=None, **kwargs): ''' Overloaded initializer for the modbus factory If the identify structure is not passed in, the ModbusControlBlock @@ -133,13 +142,14 @@ def __init__(self, store, framer=None, identity=None): :param store: The ModbusServerContext datastore :param framer: The framer strategy to use :param identity: An optional identify structure - + :param ignore_missing_slaves: True to not send errors on a request to a missing slave ''' framer = framer or ModbusSocketFramer self.framer = framer(decoder=ServerDecoder()) self.store = store or ModbusServerContext() self.control = ModbusControlBlock() self.access = ModbusAccessControl() + self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', Defaults.IgnoreMissingSlaves) if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) @@ -164,6 +174,11 @@ def _execute(self, request, addr): try: context = self.store[request.unit_id] response = request.execute(context) + except NoSuchSlaveException, ex: + _logger.debug("requested slave does not exist: %s; %s", ex, traceback.format_exc() ) + if self.ignore_missing_slaves: + return # the client will simply timeout waiting for a response + response = request.doException(merror.GatewayNoResponse) except Exception as ex: _logger.debug("Datastore unable to fulfill request: %s" % ex) response = request.doException(merror.SlaveFailure) @@ -188,38 +203,42 @@ def _send(self, message, addr): #---------------------------------------------------------------------------# # Starting Factories #---------------------------------------------------------------------------# -def StartTcpServer(context, identity=None, address=None, console=False): +def StartTcpServer(context, identity=None, address=None, console=False, **kwargs): ''' 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 + :param ignore_missing_slaves: True to not send errors on a request to a missing slave ''' from twisted.internet import reactor address = address or ("", Defaults.Port) framer = ModbusSocketFramer - factory = ModbusServerFactory(context, framer, identity) - if console: InstallManagementConsole({'factory': factory}) + factory = ModbusServerFactory(context, framer, identity, **kwargs) + if console: + from pymodbus.internal.ptwisted import InstallManagementConsole + InstallManagementConsole({'factory': factory}) _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, address=None): +def StartUdpServer(context, identity=None, address=None, **kwargs): ''' 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 address: An optional (interface, port) to bind to. + :param ignore_missing_slaves: True to not send errors on a request to a missing slave ''' from twisted.internet import reactor address = address or ("", Defaults.Port) framer = ModbusSocketFramer - server = ModbusUdpProtocol(context, framer, identity) + server = ModbusUdpProtocol(context, framer, identity, **kwargs) _logger.info("Starting Modbus UDP Server on %s:%s" % address) reactor.listenUDP(address[1], server, interface=address[0]) @@ -236,6 +255,7 @@ def StartSerialServer(context, identity=None, :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 + :param ignore_missing_slaves: True to not send errors on a request to a missing slave ''' from twisted.internet import reactor from twisted.internet.serialport import SerialPort @@ -245,8 +265,10 @@ def StartSerialServer(context, identity=None, console = kwargs.get('console', False) _logger.info("Starting Modbus Serial Server on %s" % port) - factory = ModbusServerFactory(context, framer, identity) - if console: InstallManagementConsole({'factory': factory}) + factory = ModbusServerFactory(context, framer, identity, **kwargs) + if console: + from pymodbus.internal.ptwisted import InstallManagementConsole + InstallManagementConsole({'factory': factory}) protocol = factory.buildProtocol(None) SerialPort.getHost = lambda self: port # hack for logging diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index cb5ab976a..ce1f37dc5 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -6,6 +6,7 @@ from binascii import b2a_hex import serial import socket +import traceback from pymodbus.constants import Defaults from pymodbus.factory import ServerDecoder @@ -13,7 +14,7 @@ from pymodbus.device import ModbusControlBlock from pymodbus.device import ModbusDeviceIdentification from pymodbus.transaction import * -from pymodbus.exceptions import NotImplementedException +from pymodbus.exceptions import NotImplementedException, NoSuchSlaveException from pymodbus.pdu import ModbusExceptions as merror from pymodbus.compat import socketserver, byte2int @@ -56,8 +57,13 @@ def execute(self, request): try: context = self.server.context[request.unit_id] response = request.execute(context) + except NoSuchSlaveException as ex: + _logger.debug("requested slave does not exist: %s; %s", ex, traceback.format_exc() ) + if self.server.ignore_missing_slaves: + return # the client will simply timeout waiting for a response + response = request.doException(merror.GatewayNoResponse) except Exception as ex: - _logger.debug("Datastore unable to fulfill request: %s" % ex) + _logger.debug("Datastore unable to fulfill request: %s; %s", ex, traceback.format_exc() ) response = request.doException(merror.SlaveFailure) response.transaction_id = request.transaction_id response.unit_id = request.unit_id @@ -121,7 +127,17 @@ class ModbusConnectedRequestHandler(ModbusBaseRequestHandler): ''' def handle(self): - ''' Callback when we receive any data + '''Callback when we receive any data, until self.running becomes not True. Blocks indefinitely + awaiting data. If shutdown is required, then the global socket.settimeout() may be + used, to allow timely checking of self.running. However, since this also affects socket + connects, if there are outgoing socket connections used in the same program, then these will + be prevented, if the specfied timeout is too short. Hence, this is unreliable. + + To respond to Modbus...Server.server_close() (which clears each handler's self.running), + derive from this class to provide an alternative handler that awakens from time to time when + no input is available and checks self.running. Use Modbus...Server( handler=... ) keyword + to supply the alternative request handler class. + ''' while self.running: try: @@ -131,11 +147,14 @@ def handle(self): _logger.debug(' '.join([hex(byte2int(x)) for x in data])) # if not self.server.control.ListenOnly: self.framer.processIncomingPacket(data, self.execute) - except socket.timeout: pass + except socket.timeout as msg: + pass except socket.error as msg: - _logger.error("Socket error occurred %s" % msg) + _logger.error("Socket error occurred %s", msg) + self.running = False + except: + _logger.error("Socket exception occurred %s", traceback.format_exc() ) self.running = False - except: self.running = False def send(self, message): ''' Send a request (string) to the network @@ -201,7 +220,7 @@ class ModbusTcpServer(socketserver.ThreadingTCPServer): server context instance. ''' - def __init__(self, context, framer=None, identity=None, address=None): + def __init__(self, context, framer=None, identity=None, address=None, handler=None, **kwargs): ''' Overloaded initializer for the socket server If the identify structure is not passed in, the ModbusControlBlock @@ -211,6 +230,8 @@ def __init__(self, context, framer=None, identity=None, address=None): :param framer: The framer strategy to use :param identity: An optional identify structure :param address: An optional (interface, port) to bind to. + :param handler: A handler for each client session; default is ModbusConnectedRequestHandler + :param ignore_missing_slaves: True to not send errors on a request to a missing slave ''' self.threads = [] self.decoder = ServerDecoder() @@ -218,6 +239,7 @@ def __init__(self, context, framer=None, identity=None, address=None): self.context = context or ModbusServerContext() self.control = ModbusControlBlock() self.address = address or ("", Defaults.Port) + self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', Defaults.IgnoreMissingSlaves) if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) @@ -234,6 +256,15 @@ def process_request(self, request, client): _logger.debug("Started thread to serve client at " + str(client)) socketserver.ThreadingTCPServer.process_request(self, request, client) + def shutdown(self): + ''' Stops the serve_forever loop. + + Overridden to signal handlers to stop. + ''' + for thread in self.threads: + thread.running = False + SocketServer.ThreadingTCPServer.shutdown(self) + def server_close(self): ''' Callback for stopping the running server ''' @@ -252,7 +283,7 @@ class ModbusUdpServer(socketserver.ThreadingUDPServer): server context instance. ''' - def __init__(self, context, framer=None, identity=None, address=None): + def __init__(self, context, framer=None, identity=None, address=None, handler=None, **kwargs): ''' Overloaded initializer for the socket server If the identify structure is not passed in, the ModbusControlBlock @@ -262,6 +293,8 @@ def __init__(self, context, framer=None, identity=None, address=None): :param framer: The framer strategy to use :param identity: An optional identify structure :param address: An optional (interface, port) to bind to. + :param handler: A handler for each client session; default is ModbusDisonnectedRequestHandler + :param ignore_missing_slaves: True to not send errors on a request to a missing slave ''' self.threads = [] self.decoder = ServerDecoder() @@ -269,6 +302,7 @@ def __init__(self, context, framer=None, identity=None, address=None): self.context = context or ModbusServerContext() self.control = ModbusControlBlock() self.address = address or ("", Defaults.Port) + self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', Defaults.IgnoreMissingSlaves) if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) @@ -297,7 +331,7 @@ def server_close(self): class ModbusSerialServer(object): ''' - A modbus threaded udp socket server + A modbus threaded serial socket server We inherit and overload the socket server so that we can control the client threads as well as have a single @@ -319,7 +353,7 @@ def __init__(self, context, framer=None, identity=None, **kwargs): :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 ignore_missing_slaves: True to not send errors on a request to a missing slave ''' self.threads = [] self.decoder = ServerDecoder() @@ -336,6 +370,7 @@ def __init__(self, context, framer=None, identity=None, **kwargs): self.parity = kwargs.get('parity', Defaults.Parity) self.baudrate = kwargs.get('baudrate', Defaults.Baudrate) self.timeout = kwargs.get('timeout', Defaults.Timeout) + self.ignore_missing_slaves = kwargs.get('ignore_missing_slaves', Defaults.IgnoreMissingSlaves) self.socket = None self._connect() self.is_running = True @@ -389,43 +424,48 @@ def server_close(self): #---------------------------------------------------------------------------# # Creation Factories #---------------------------------------------------------------------------# -def StartTcpServer(context=None, identity=None, address=None): +def StartTcpServer(context=None, identity=None, address=None, **kwargs): ''' A factory to start and run a tcp modbus server :param context: The ModbusServerContext datastore :param identity: An optional identify structure :param address: An optional (interface, port) to bind to. + :param ignore_missing_slaves: True to not send errors on a request to a missing slave ''' framer = ModbusSocketFramer - server = ModbusTcpServer(context, framer, identity, address) + server = ModbusTcpServer(context, framer, identity, address, **kwargs) server.serve_forever() -def StartUdpServer(context=None, identity=None, address=None): +def StartUdpServer(context=None, identity=None, address=None, **kwargs): ''' A factory to start and run a udp modbus server :param context: The ModbusServerContext datastore :param identity: An optional identify structure :param address: An optional (interface, port) to bind to. + :param framer: The framer to operate with (default ModbusSocketFramer) + :param ignore_missing_slaves: True to not send errors on a request to a missing slave ''' - framer = ModbusSocketFramer - server = ModbusUdpServer(context, framer, identity, address) + framer = kwargs.pop('framer', ModbusSocketFramer) + server = ModbusUdpServer(context, framer, identity, address, **kwargs) server.serve_forever() def StartSerialServer(context=None, identity=None, **kwargs): - ''' A factory to start and run a udp modbus server + ''' A factory to start and run a serial modbus server :param context: The ModbusServerContext datastore :param identity: An optional identify structure + :param framer: The framer to operate with (default ModbusAsciiFramer) :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 + :param ignore_missing_slaves: True to not send errors on a request to a missing slave ''' - framer = ModbusAsciiFramer + framer = kwargs.pop('framer', ModbusAsciiFramer) server = ModbusSerialServer(context, framer, identity, **kwargs) server.serve_forever() diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index f9d03f228..c72b6a213 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -39,19 +39,23 @@ class ModbusTransactionManager(object): This module helps to abstract this away from the framer and protocol. ''' - def __init__(self, client): + def __init__(self, client, **kwargs): ''' Initializes an instance of the ModbusTransactionManager :param client: The client socket wrapper + :param retry_on_empty: Should the client retry on empty + :param retries: The number of retries to allow ''' self.tid = Defaults.TransactionId self.client = client + self.retry_on_empty = kwargs.get('retry_on_empty', Defaults.RetryOnEmpty) + self.retries = kwargs.get('retries', Defaults.Retries) def execute(self, request): ''' Starts the producer to send the next request to consumer.write(Frame(request)) ''' - retries = Defaults.Retries + retries = self.retries request.transaction_id = self.getNextTID() _logger.debug("Running transaction %d" % request.transaction_id) @@ -63,6 +67,11 @@ def execute(self, request): # as this may not read the full result set, but right now # it should be fine... result = self.client._recv(1024) + if not result and self.retry_on_empty: + retries -= 1 + continue + if _logger.isEnabledFor(logging.DEBUG): + _logger.debug("recv: " + " ".join([hex(ord(x)) for x in result])) self.client.framer.processIncomingPacket(result, self.addTransaction) break; except socket.error as msg: @@ -120,13 +129,13 @@ class DictTransactionManager(ModbusTransactionManager): results are keyed based on the supplied transaction id. ''' - def __init__(self, client): + def __init__(self, client, **kwargs): ''' Initializes an instance of the ModbusTransactionManager :param client: The client socket wrapper ''' self.transactions = {} - super(DictTransactionManager, self).__init__(client) + super(DictTransactionManager, self).__init__(client, **kwargs) def __iter__(self): ''' Iterater over the current managed transactions @@ -172,12 +181,12 @@ class FifoTransactionManager(ModbusTransactionManager): results are returned in a FIFO manner. ''' - def __init__(self, client): + def __init__(self, client, **kwargs): ''' Initializes an instance of the ModbusTransactionManager :param client: The client socket wrapper ''' - super(FifoTransactionManager, self).__init__(client) + super(FifoTransactionManager, self).__init__(client, **kwargs) self.transactions = [] def __iter__(self): @@ -335,7 +344,8 @@ def processIncomingPacket(self, data, callback): :param data: The new packet data :param callback: The function to send results to ''' - _logger.debug(' '.join([hex(byte2int(x)) for x in data])) + _logger.debug(' '.join([hex(byte2int(x)) for x in data]) + self.addToFrame(data) while self.isFrameReady(): if self.checkFrame(): diff --git a/pymodbus/version.py b/pymodbus/version.py index 5d9fbbac2..c002097b4 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, 2, 0) +version = Version('pymodbus', 1, 3, 0) version.__name__ = 'pymodbus' # fix epydoc error #---------------------------------------------------------------------------# diff --git a/test/test_client_common.py b/test/test_client_common.py index f8cf91b94..f182aafb1 100644 --- a/test/test_client_common.py +++ b/test/test_client_common.py @@ -3,6 +3,7 @@ from pymodbus.client.common import ModbusClientMixin from pymodbus.bit_read_message import * from pymodbus.bit_write_message import * +from pymodbus.file_message import * from pymodbus.register_read_message import * from pymodbus.register_write_message import * @@ -51,3 +52,4 @@ def testModbusClientMixinMethods(self): 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(**arguments), ReadWriteMultipleRegistersRequest)) + self.assertTrue(isinstance(self.client.mask_write_register(1,0,0), MaskWriteRegisterRequest)) diff --git a/test/test_client_sync.py b/test/test_client_sync.py index 5d2b267f5..de5b9c8a4 100644 --- a/test/test_client_sync.py +++ b/test/test_client_sync.py @@ -100,7 +100,10 @@ def testUdpClientAddressFamily(self): def testUdpClientConnect(self): ''' Test the Udp client connection method''' with patch.object(socket, 'socket') as mock_method: - mock_method.return_value = object() + class DummySocket(object): + def settimeout(self, *a, **kwa): + pass + mock_method.return_value = DummySocket() client = ModbusUdpClient() self.assertTrue(client.connect()) diff --git a/test/test_datastore.py b/test/test_datastore.py index 40c1de5cd..b6b401517 100644 --- a/test/test_datastore.py +++ b/test/test_datastore.py @@ -3,6 +3,7 @@ from pymodbus.datastore import * from pymodbus.datastore.store import BaseModbusDataBlock from pymodbus.exceptions import NotImplementedException +from pymodbus.exceptions import NoSuchSlaveException from pymodbus.exceptions import ParameterException from pymodbus.datastore.remote import RemoteSlaveContext @@ -128,8 +129,8 @@ def testModbusServerContext(self): def _set(ctx): ctx[0xffff] = None context = ModbusServerContext(single=False) - self.assertRaises(ParameterException, lambda: _set(context)) - self.assertRaises(ParameterException, lambda: context[0xffff]) + self.assertRaises(NoSuchSlaveException, lambda: _set(context)) + self.assertRaises(NoSuchSlaveException, lambda: context[0xffff]) #---------------------------------------------------------------------------# # Main diff --git a/test/test_mei_messages.py b/test/test_mei_messages.py index b4452d951..602a7f05e 100644 --- a/test/test_mei_messages.py +++ b/test/test_mei_messages.py @@ -3,7 +3,7 @@ MEI Message Test Fixture -------------------------------- -This fixture tests the functionality of all the +This fixture tests the functionality of all the mei based request/response messages: ''' import unittest @@ -70,6 +70,7 @@ def testReadDeviceInformationResponseEncode(self): ''' Test that the read fifo queue response can encode ''' message = b'\x0e\x01\x83\x00\x00\x03' message += b'\x00\x07Company\x01\x07Product\x02\x07v2.1.12' + dataset = { 0x00: b'Company', 0x01: b'Product', @@ -85,6 +86,7 @@ def testReadDeviceInformationResponseDecode(self): ''' Test that the read device information response can decode ''' message = b'\x0e\x01\x01\x00\x00\x03' message += b'\x00\x07Company\x01\x07Product\x02\x07v2.1.12' + handle = ReadDeviceInformationResponse(read_code=0x00, information=[]) handle.decode(message) self.assertEqual(handle.read_code, DeviceInformation.Basic) @@ -98,6 +100,9 @@ def testRtuFrameSize(self): message = b'\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, 18) + message = '\x00\x2B\x0E\x02\x00\x4D\x47' + result = ReadDeviceInformationRequest.calculateRtuFrameSize(message) + self.assertEqual(result, 7) #---------------------------------------------------------------------------# diff --git a/test/test_other_messages.py b/test/test_other_messages.py index b45cd3f46..3cfa07d1c 100644 --- a/test/test_other_messages.py +++ b/test/test_other_messages.py @@ -39,6 +39,7 @@ def testReadExceptionStatus(self): self.assertEqual(request.encode(), b'') self.assertEqual(request.execute().function_code, 0x07) + response = ReadExceptionStatusResponse(0x12) self.assertEqual(response.encode(), b'\x12') response.decode(b'\x12') @@ -94,6 +95,7 @@ def testReportSlaveId(self): response = ReportSlaveIdResponse(request.execute().identifier, True) self.assertEqual(response.encode(), b'\x0apymodbus\xff') response.decode(b'\x03\x12\x00') + self.assertEqual(response.status, False) self.assertEqual(response.identifier, b'\x12\x00') diff --git a/test/test_payload.py b/test/test_payload.py index 1b7b55779..fb0ffb4ae 100644 --- a/test/test_payload.py +++ b/test/test_payload.py @@ -145,6 +145,7 @@ def testPayloadDecoderRegisterFactory(self): payload = [1,2,3,4] decoder = BinaryPayloadDecoder.fromRegisters(payload, endian=Endian.Little) encoded = b'\x01\x00\x02\x00\x03\x00\x04\x00' + self.assertEqual(encoded, decoder.decode_string(8)) decoder = BinaryPayloadDecoder.fromRegisters(payload, endian=Endian.Big) diff --git a/test/test_server_context.py b/test/test_server_context.py index 8fe28e1ed..17defce45 100644 --- a/test/test_server_context.py +++ b/test/test_server_context.py @@ -27,7 +27,7 @@ def testSingleContextDeletes(self): ''' Test removing on multiple context ''' def _test(): del self.context[0x00] - self.assertRaises(ParameterException, _test) + self.assertRaises(NoSuchSlaveException, _test) def testSingleContextIter(self): ''' Test iterating over a single context ''' @@ -70,7 +70,7 @@ def testMultipleContextGets(self): def testMultipleContextDeletes(self): ''' Test removing on multiple context ''' del self.context[0x00] - self.assertRaises(ParameterException, lambda: self.context[0x00]) + self.assertRaises(NoSuchSlaveException, lambda: self.context[0x00]) def testMultipleContextIter(self): ''' Test iterating over multiple context ''' @@ -81,7 +81,7 @@ def testMultipleContextIter(self): def testMultipleContextDefault(self): ''' Test that the multiple context default values work ''' self.context = ModbusServerContext(single=False) - self.assertRaises(ParameterException, lambda: self.context[0x00]) + self.assertRaises(NoSuchSlaveException, lambda: self.context[0x00]) def testMultipleContextSet(self): ''' Test a setting multiple slave contexts '''