"""Exception hierarchy for pymod.
Three families with distinct semantics:
* `ModbusTransportError` — the request never got a valid reply (timeout,
socket dropped, CRC mismatch). Retrying is usually appropriate.
* `ModbusProtocolError` — a frame was received but malformed (bad MBAP,
mismatched transaction id, length lie). Retrying rarely helps; the device
or the wire is misbehaving.
* `ModbusExceptionResponse` — the slave returned a valid Modbus exception
response (FC | 0x80). Retrying will hit the same illegal address forever;
do not retry by default.
"""
from __future__ import annotations
[docs]
class ModbusError(Exception):
"""Base class for everything pymod raises."""
# ---------- Transport-level failures -----------------------------------------
[docs]
class ModbusTransportError(ModbusError):
"""The request did not produce a usable reply on the wire."""
[docs]
class ModbusTimeoutError(ModbusTransportError):
"""No response within the configured timeout."""
[docs]
class ModbusConnectionError(ModbusTransportError):
"""TCP connection refused/dropped, or serial port unavailable."""
class ModbusCRCError(ModbusTransportError):
"""RTU frame failed CRC16 check."""
# ---------- Protocol-level failures ------------------------------------------
[docs]
class ModbusProtocolError(ModbusError):
"""A response was received but is malformed or out-of-spec."""
# ---------- Slave-returned exception responses --------------------------------
[docs]
class ModbusExceptionResponse(ModbusError):
"""Slave returned a valid Modbus exception response. Code in `code`."""
code: int = 0
def __init__(self, message: str = "", code: int | None = None) -> None:
super().__init__(message or self.__class__.__name__)
if code is not None:
self.code = code
[docs]
class IllegalFunction(ModbusExceptionResponse):
code = 0x01
[docs]
class IllegalDataAddress(ModbusExceptionResponse):
code = 0x02
[docs]
class IllegalDataValue(ModbusExceptionResponse):
code = 0x03
[docs]
class SlaveDeviceFailure(ModbusExceptionResponse):
code = 0x04
class Acknowledge(ModbusExceptionResponse):
code = 0x05
[docs]
class SlaveDeviceBusy(ModbusExceptionResponse):
code = 0x06
[docs]
class MemoryParityError(ModbusExceptionResponse):
code = 0x08
[docs]
class GatewayPathUnavailable(ModbusExceptionResponse):
code = 0x0A
[docs]
class GatewayTargetFailedToRespond(ModbusExceptionResponse):
code = 0x0B
EXCEPTION_BY_CODE: dict[int, type[ModbusExceptionResponse]] = {
0x01: IllegalFunction,
0x02: IllegalDataAddress,
0x03: IllegalDataValue,
0x04: SlaveDeviceFailure,
0x05: Acknowledge,
0x06: SlaveDeviceBusy,
0x08: MemoryParityError,
0x0A: GatewayPathUnavailable,
0x0B: GatewayTargetFailedToRespond,
}