-
Notifications
You must be signed in to change notification settings - Fork 1k
Description
Versions
- Pymodbus: 1.4.0+ atleast, probably historic versions as well
Pymodbus Specific
- Server: Probably unaffected, though I can't quite say for sure
- Client: observed in sync, though probably applies to async as well
Pymodbus (mis-)handles exceptions, presenting a non-intuitive interface and in some cases potentially dangerous behavior in terms of exception handling. This Issue lists out some of the problems I have noticed over the last few weeks of my use of pymodbus, with the hope that it may result in a discussion leading to viable approaches to better handle exceptions.
Current Behavior
Pymodbus doesn't often raise exceptions except in cases of catastrophic failure. Instead, it returns an exception response in place of a regular response. This is functionally sufficient, but in practice this means that every command sent from a client to a server needs to be checked for an exception response. This makes pymodbus code look and feel very unpythonic, and more verbose than it ought to be.
Types of exceptions returned and their detection
As far as I can tell, a pymodbus client call might result in one of two kinds of exception responses :
- An ExceptionResponse, returned from and indirectly by the modbus server. This is, in all cases, a real exception. The server was asked to do something which it refused to do. Such a response should always be treated as an exception.
- A ModbusIOException, raised by the Client itself, implying that no ModbusServer responded to the command issued. This is not always a real exception - some (one, ForceListenOnly) Modbus commands return no response whatsoever. All modbus server return no response for any command issued in broadcast. However, the client issuing the command knows or can know at the time of its issual whether or not it should expect a response. If this information is made available to the client or its command executor, it can determine if a returned ModbusIOException is a real exception or something which can be silently discarded (as no response was expected anyway). On a side note, ModbusIOExceptions should in principle also result from a failure of the client to send the message, such as if, say, the tty interface disappears mid-message. I'm not sure what the current handling of exceptions does in such a case.
Exception Raising
A modbus client should, in my opinion, raise an exception that is determined to be a real exception and not just return it with the expectation that user code will detect and respond to the problem. There are two dimensions to solving such a problem, viz.,
a. How do you know an exception should be raised?
b. How do you actually raise an exception?
(a) is addressed previously. Additional work might be needed to embed information about whether a response should be expected in the case of (2), but exceptions of type (1) can be immediately raised.
(b) is a relatively more complex question. In the case of the sync clients, raising an exception is fairly trivial. In fact, for exceptions of type 1. in sync clients, all that is needed to change behavior is :
class ModbusServerException(Exception):
def __init_(self, response):
self.response = response
class RaisingModbusClient(ModbusSerialClient):
def execute(self, request=None):
result = super(RaisingModbusClient, self).execute(request)
if isinstance(result, ExceptionResponse):
raise ModbusServerException(result)
return resultFor the type 2. exceptions, more code would be needed here, such as, say :
...
class RaisingModbusClient(ModbusSerialClient):
def execute(self, request=None):
...
if isinstance(result, ModbusIOException) and request.expect_response:
raise result
...In the async clients, this problem can be relatively more intractable. It has been some time since I've used twisted, and I have, in the past, made an effort to avoid the complexity of errbacks. However, the correct way for pymodbus to handle exceptions in it's async clients should be to accept errbacks and return exceptions though them, once detected as discussed above.
Maintaining Backward Compatibility
Such a change would cause some fairly severe backward compatibility issues, given all pymodbus users don't expect it raise exceptions and instead to return them. As such, I would suggest instead implementing such behavior but leave it disabled by default. Allow each user to switch to the new format at their choice. This can be done by providing additional keyword arguments to the Client class' init() methods, tentatively raise_exceptions=False, which defaults to the old (current) behavior. A user may wish to use raise_exceptions=True and then not have to check every response for an exception.
In the interim, or even beyond, given the obvious complexity of the type 2. exception detection, the provided arguments may instead be :
raise_exceptions=Falseraise_server_exceptions=Falseraise_io_exceptions=False
This will preserve enough flexibility for a user to choose not to rely on pymodbus for determining the context of ModbusIOExceptions, but allow it to raise the certain exceptions of ExceptionResponse.