Skip to content

Make treatment of exceptions more pythonic #298

@chintal

Description

@chintal

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 :

  1. 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.
  2. 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 result

For 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=False
  • raise_server_exceptions=False
  • raise_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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions