From 1e0bcde13153469365dd1debf6456e4b0e048566 Mon Sep 17 00:00:00 2001 From: Galen Collins Date: Wed, 23 Jul 2014 14:47:29 -0700 Subject: [PATCH] Fixes #42 --- pymodbus/constants.py | 34 +++++++++++++++++++++------------- pymodbus/datastore/context.py | 8 ++++---- pymodbus/exceptions.py | 20 +++++++++++++++++++- pymodbus/server/async.py | 33 ++++++++++++++++++++++++--------- pymodbus/server/sync.py | 30 ++++++++++++++++++++++-------- test/test_datastore.py | 5 +++-- test/test_server_context.py | 6 +++--- 7 files changed, 96 insertions(+), 40 deletions(-) diff --git a/pymodbus/constants.py b/pymodbus/constants.py index 8624a2f0a..595a306a7 100644 --- a/pymodbus/constants.py +++ b/pymodbus/constants.py @@ -81,20 +81,28 @@ class Defaults(Singleton): Indicates if the slave datastore should use indexing at 0 or 1. Mor 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 - RetryOnEmpty = False - Timeout = 3 - Reconnects = 0 - TransactionId = 0 - ProtocolId = 0 - UnitId = 0x00 - Baudrate = 19200 - Parity = 'N' - Bytesize = 8 - Stopbits = 1 - ZeroMode = False + 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 d59587fa1..5aadc220b 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 @@ -129,7 +129,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 +138,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 +149,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..59ec3597f 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 @@ -35,17 +37,32 @@ class ParameterException(ModbusException): def __init__(self, string=""): ''' Initialize the exception + :param string: The message to append to the error ''' message = "[Invalid Paramter] %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 = "[No Such Slave] %s" % string + ModbusException.__init__(self, message) + + class NotImplementedException(ModbusException): ''' Error resulting from not implemented function ''' 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/server/async.py b/pymodbus/server/async.py index 968cf2630..89fac6637 100644 --- a/pymodbus/server/async.py +++ b/pymodbus/server/async.py @@ -65,6 +65,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, ex: _logger.debug("Datastore unable to fulfill request: %s" % ex) response = request.doException(merror.SlaveFailure) @@ -96,7 +101,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 @@ -105,13 +110,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) @@ -123,7 +129,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 @@ -132,13 +138,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) @@ -163,6 +170,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, ex: _logger.debug("Datastore unable to fulfill request: %s" % ex) response = request.doException(merror.SlaveFailure) @@ -187,19 +199,20 @@ 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) + factory = ModbusServerFactory(context, framer, identity, **kwargs) if console: InstallManagementConsole({'factory': factory}) _logger.info("Starting Modbus TCP Server on %s:%s" % address) @@ -207,18 +220,19 @@ def StartTcpServer(context, identity=None, address=None, console=False): 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]) @@ -235,6 +249,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 @@ -244,7 +259,7 @@ def StartSerialServer(context, identity=None, console = kwargs.get('console', False) _logger.info("Starting Modbus Serial Server on %s" % port) - factory = ModbusServerFactory(context, framer, identity) + factory = ModbusServerFactory(context, framer, identity, **kwargs) if console: InstallManagementConsole({'factory': factory}) protocol = factory.buildProtocol(None) diff --git a/pymodbus/server/sync.py b/pymodbus/server/sync.py index 3063effc1..f367fac2c 100644 --- a/pymodbus/server/sync.py +++ b/pymodbus/server/sync.py @@ -15,7 +15,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 #---------------------------------------------------------------------------# @@ -57,6 +57,11 @@ def execute(self, request): try: context = self.server.context[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.server.ignore_missing_slaves: + return # the client will simply timeout waiting for a response + response = request.doException(merror.GatewayNoResponse) except Exception, ex: _logger.debug("Datastore unable to fulfill request: %s; %s", ex, traceback.format_exc() ) response = request.doException(merror.SlaveFailure) @@ -120,6 +125,7 @@ class ModbusConnectedRequestHandler(ModbusBaseRequestHandler): This uses the socketserver.BaseRequestHandler to implement the client handler for a connected protocol (TCP). ''' + def handle(self): '''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 @@ -216,7 +222,7 @@ class ModbusTcpServer(SocketServer.ThreadingTCPServer): server context instance. ''' - def __init__(self, context, framer=None, identity=None, address=None, handler=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 @@ -227,6 +233,7 @@ def __init__(self, context, framer=None, identity=None, address=None, handler=No :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() @@ -234,6 +241,7 @@ def __init__(self, context, framer=None, identity=None, address=None, handler=No 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) @@ -268,7 +276,7 @@ class ModbusUdpServer(SocketServer.ThreadingUDPServer): server context instance. ''' - def __init__(self, context, framer=None, identity=None, address=None, handler=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 @@ -279,6 +287,7 @@ def __init__(self, context, framer=None, identity=None, address=None, handler=No :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() @@ -286,6 +295,7 @@ def __init__(self, context, framer=None, identity=None, address=None, handler=No 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) @@ -336,7 +346,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() @@ -353,6 +363,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 @@ -406,27 +417,29 @@ 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 ignore_missing_slaves: True to not send errors on a request to a missing slave ''' framer = ModbusSocketFramer - server = ModbusUdpServer(context, framer, identity, address) + server = ModbusUdpServer(context, framer, identity, address, **kwargs) server.serve_forever() @@ -441,6 +454,7 @@ def StartSerialServer(context=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 ''' framer = ModbusAsciiFramer server = ModbusSerialServer(context, framer, identity, **kwargs) 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_server_context.py b/test/test_server_context.py index 46fb69871..aeb985b5c 100644 --- a/test/test_server_context.py +++ b/test/test_server_context.py @@ -26,7 +26,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 ''' @@ -69,7 +69,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 ''' @@ -80,7 +80,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 '''