Skip to content

Modbus Connection reference

The complete API of the connection layer. Everything here is importable from the top-level modbus_connection package unless stated otherwise.

The abstract connection base class (modbus_connection.ModbusConnection). Each backend module exports a concrete subclass under the same name: modbus_connection.tmodbus.ModbusConnection and modbus_connection.pymodbus.ModbusConnection. The constructor and API are identical, so selecting a backend changes only the import.

ModbusConnection(params, *, timeout=10, message_spacing=0.0, connect_delay=0.0)
Parameter Type Meaning
params ModbusTcpParams | ModbusUdpParams | ModbusTlsParams | ModbusSerialParams The transport to connect over — see the parameter dataclasses.
timeout float, default 10 Per-request timeout in seconds.
message_spacing float, default 0.0 Connection-wide minimum interval, in seconds, from the completion of one request to the start of the next. 0 disables spacing. Raises ValueError if negative.
connect_delay float, default 0.0 Pause, in seconds, after the link is established before it is used. For devices that need a moment after connecting before they answer reliably. Concurrent connectors share one pause.

Constructing a connection performs no I/O. The first unit operation connects on demand. See Connections and units for the ownership and lifecycle model.

bool — whether the link is currently established. False before the first request, after a drop, and after close().

async — establish the connection eagerly; a no-op if already connected. Concurrent callers share a single in-flight connect attempt. Raises ModbusConnectionError if the connection fails and ClientClosedError if the connection was closed. You rarely need it: every unit operation connects first.

Return this backend’s stateless ModbusUnit handle bound to unit_id. Handles are cheap. Consumers should receive a handle, never the connection.

Register a Callable[[], None] fired when the link drops; returns an unsubscribe callable. A connection is lost when the transport takes it away. close() and disconnect() are the owner tearing it down, so neither fires the callbacks.

async — drop the link; the next request establishes a new one. Use it to recycle a link that is up but unusable, such as a peer that keeps the socket open but stops answering. Unlike close(), the connection stays usable: existing unit handles and components reconnect on their next request. A no-op when there is no link. Raises ModbusConnectionError if tearing the old link down fails; the link is dropped regardless.

async — close the connection permanently. After close(), connect() and every unit operation raise ClientClosedError. Construct a new connection to reconnect.

All four are frozen, keyword-only dataclasses importable from modbus_connection. See Connection parameters for usage guidance.

Field Type Default Meaning
host str required Host name or IP address of the device.
port int 502 TCP port.
framer "socket" | "rtu" | "ascii" "socket" Wire framing: native Modbus TCP (MBAP), RTU-over-TCP, or ASCII-over-TCP. Any other value raises ValueError.
Field Type Default Meaning
host str required Host name or IP address of the device.
port int 502 UDP port.
framer "socket" | "rtu" | "ascii" "socket" Wire framing. Any other value raises ValueError.
Field Type Default Meaning
host str required Host name or IP address of the device.
port int 802 TLS port.
verify bool | str True True verifies the server certificate against the system trust store; a path verifies against a private CA file or directory; False disables verification.
check_hostname bool True Whether to verify the certificate hostname.
client_cert str | None None Path to the client certificate.
client_key str | None None Path to the private key belonging to client_cert.
client_key_password str | None None Password for client_key, if it is encrypted.
sslctx ssl.SSLContext | None None TLS context overriding the other TLS options.

async — return the supplied sslctx or build an ssl.SSLContext from the other parameters. The backends call this for you when connecting.

Field Type Default Meaning
device str required Serial port device path (e.g. /dev/ttyUSB0).
baudrate int 9600 Line speed in baud.
bytesize 7 | 8 8 Data bits per character.
parity "N" | "E" | "O" "N" Parity: none, even, or odd.
stopbits 1 | 2 1 Stop bits per character.
framer "rtu" | "ascii" "rtu" Serial framing. Any other value raises ValueError.

Every parameter dataclass has an endpoint property: a hashable tuple that identifies the physical target the params point at, excluding link settings. Two params objects with equal endpoints address the same device. The property answers “do these configurations target the same device?” and doubles as a dictionary key for grouping shared connections:

Class Endpoint Excluded settings
ModbusTcpParams ("tcp", host, port) framer
ModbusUdpParams ("udp", host, port) framer
ModbusTlsParams ("tcp", host, port) all TLS options
ModbusSerialParams ("serial", device) baudrate, bytesize, parity, stopbits, framer

ModbusTlsParams deliberately shares the "tcp" transport tag: a TLS link and a plain-TCP link to the same host and port target the same TCP endpoint, and therefore the same device. A host is folded to lower case on construction, since DNS names and IPv6 hex digits are case-insensitive. The serial device path is compared verbatim. Aliases of the same port (a /dev/serial/by-id symlink versus /dev/ttyUSB0) are not resolved.

Equal endpoints with unequal params signal conflicting configurations for one device — for example two serial configs for /dev/ttyUSB0 at different baud rates. A connection manager can detect and reject that:

if new_params.endpoint == existing_params.endpoint and new_params != existing_params:
raise ValueError("conflicting settings for the same device")

A runtime-checkable Protocol representing one unit on a shared connection. Obtain one from connection.for_unit(unit_id). The mock unit and any other object with these methods satisfies it too.

Every operation is async, connects on demand, and raises a subclass of ModbusError on failure. An operation the selected backend does not implement raises NotImplementedError — see Choosing a backend.

Method Function code Returns
read_holding_registers(address, count) 3 (0x03) list[int]
read_input_registers(address, count) 4 (0x04) list[int]
write_register(address, value) 6 (0x06) None
write_registers(address, values) 16 (0x10) None
Method Function code Returns
read_coils(address, count) 1 (0x01) list[bool]
read_discrete_inputs(address, count) 2 (0x02) list[bool]
write_coil(address, value) 5 (0x05) None
write_coils(address, values) 15 (0x0F) None

Diagnostic, file-record, and identification operations

Section titled “Diagnostic, file-record, and identification operations”
Method Function code Returns
read_exception_status() 7 (0x07) int
diagnostics(sub_function, data=0) 8 (0x08) int
get_comm_event_counter() 11 (0x0B) tuple[int, int] — status, event count
get_comm_event_log() 12 (0x0C) bytes
report_server_id() 17 (0x11) bytes
read_file_record(file, record, length) 20 (0x14) list[int]
write_file_record(file, record, values) 21 (0x15) None
mask_write_register(address, and_mask, or_mask) 22 (0x16) None
read_write_registers(read_address, read_count, write_address, write_values) 23 (0x17) list[int]
read_fifo_queue(address) 24 (0x18) list[int]
read_device_identification() 43 / 14 (0x2B / 0x0E) dict[int, bytes]

bool — whether the owning connection’s link is currently established.

Set the minimum interval between requests to this unit. The setting belongs to the unit ID and combines with connection-wide spacing by waiting for the longer interval. Pass 0 to clear it. Raises ValueError if seconds is negative. See Request spacing.

Register a callback fired when the connection’s link drops; returns an unsubscribe callable. Equivalent to registering on the owning connection.

Converters between register words and Python values. The modelling fields use them internally, and they are available for direct use — see Decoding what you read for examples.

Literal["big", "little"] (importable from modbus_connection) — the order of 16-bit registers within a multi-register value. "big" (the common Modbus convention) puts the most-significant word first; "little" puts the least-significant word first.

All decoders take words: list[int]; the multi-word numeric ones also take a word_order keyword (default "big").

Function Registers Returns
decode_uint16(words) 1 int (unsigned)
decode_int16(words) 1 int (signed)
decode_uint32(words, *, word_order="big") 2 int
decode_int32(words, *, word_order="big") 2 int
decode_uint64(words, *, word_order="big") 4 int
decode_int64(words, *, word_order="big") 4 int
decode_int(words, *, signed, word_order="big") any int of any width
decode_float32(words, *, word_order="big") 2 float (IEEE-754 single)
decode_float64(words, *, word_order="big") 4 float (IEEE-754 double)
decode_string(words) any str — null-padded ASCII, two characters per word
decode_ipaddr(words) 2 ipaddress.IPv4Address
decode_ipv6addr(words) 8 ipaddress.IPv6Address
decode_eui48(words) 3 str — colon-separated EUI-48 / MAC address
combine_words(words, *, word_order="big") any int — the raw unsigned value

All encoders return list[int] register words. The integer encoders raise OverflowError if the value does not fit the width.

Function Registers Encodes
encode_uint16(value) 1 an unsigned/signed 16-bit integer
encode_int16(value) 1 a signed 16-bit integer
encode_uint32(value, *, word_order="big") 2 a 32-bit integer
encode_int32(value, *, word_order="big") 2 a signed 32-bit integer
encode_uint64(value, *, word_order="big") 4 a 64-bit integer
encode_int64(value, *, word_order="big") 4 a signed 64-bit integer
encode_int(value, *, count, word_order="big") count an integer of any width
encode_float32(value, *, word_order="big") 2 an IEEE-754 single-precision float
encode_float64(value, *, word_order="big") 4 an IEEE-754 double-precision float
encode_string(value, *, length) length an ASCII string, null-padded (two characters per word)
split_words(raw, *, count, word_order="big") count an unsigned integer into raw words

Both backends map their errors onto the same neutral hierarchy. except ModbusError catches everything, whichever backend produced the error. Import the classes from the top-level package:

ModbusError
├── ModbusConnectionError
│ └── ClientClosedError (request on a close()d connection)
├── ModbusTimeoutError (also a builtin TimeoutError)
├── ModbusProtocolError
│ └── ModbusDesyncError (reply answers a different exchange)
└── ModbusExceptionError (.exception_code; .block set when a
│ component update aborted)
├── IllegalFunctionError (code 1)
├── IllegalDataAddressError (code 2)
├── IllegalDataValueError (code 3)
├── ServerDeviceFailureError (code 4)
├── AcknowledgeError (code 5)
├── ServerDeviceBusyError (code 6)
├── MemoryParityError (code 8)
├── GatewayPathUnavailableError (code 10)
└── GatewayTargetError (code 11)

The base class. Catch it to handle any Modbus failure uniformly:

try:
values = await unit.read_holding_registers(0, 10)
except ModbusError as err:
log.warning("read failed: %s", err)

The link is down, not connected, or the transport failed. The connection is not discarded: the next request attempts to establish it again.

A request or connect() was attempted on a connection after its owner called close(). A closed connection never reconnects; the owner must construct a new one.

An operation timed out: a request got no valid response in time, or a connect attempt did not complete in time. It also subclasses the builtin TimeoutError, so except TimeoutError catches it too:

try:
await unit.read_holding_registers(0, 1)
except TimeoutError: # catches ModbusTimeoutError
...

A reply arrived but could not be used. Either the frame was corrupt (bad CRC/LRC, framing), or a well-formed reply answered a different request than the one sent. The latter is the signature of a bridge shared by several simultaneous clients.

The reply answers a different exchange than the one in flight: a mismatched header, or the wrong function code. Retrying would read the same offset. The connection therefore disconnect()s before this raises, and the next request opens a fresh link.

The device returned a Modbus exception response: it understood the request but refused it. A code with a standard meaning raises the matching subclass, so callers can branch without magic numbers:

try:
await unit.write_register(40, 99)
except IllegalDataValueError:
... # the device rejected the value
except GatewayTargetError:
... # the bridge is fine; the device behind it is not answering
Subclass Code Meaning
IllegalFunctionError 1 The device does not support the function.
IllegalDataAddressError 2 The device does not serve the address.
IllegalDataValueError 3 The device rejected a value in the request.
ServerDeviceFailureError 4 The device failed performing the request.
AcknowledgeError 5 Accepted, but the device needs time to process.
ServerDeviceBusyError 6 The device is busy; retry later.
MemoryParityError 8 Parity error in the device’s memory.
GatewayPathUnavailableError 10 The gateway has no path to the target.
GatewayTargetError 11 The gateway’s target device did not respond.

.exception_code carries the code as an ExceptionCode IntEnum member when it is a standard one, and a plain int otherwise. Existing err.exception_code == 2 comparisons keep working. An unknown code raises the base ModbusExceptionError. Each subclass constructs with its code implied — IllegalDataAddressError() — which is useful for arming the mock.

.block says where the refusal happened. For an exception response that aborted a component update, it is the refused ReadBlock(space, address, count). For a raw unit request it is None.

A deprecated alias of ModbusExceptionError. It is kept so existing except BlockReadError handlers keep catching an aborted component update, and so .space / .address / .count still read (from .block; None on a raw request error). New code should catch the typed class and read .block.

modbus_connection.tmodbus and modbus_connection.pymodbus each export:

  • ModbusConnection — the concrete connection class described above. Constructing one with parameters the backend does not support raises ValueError (see Choosing a backend).
  • TmodbusConnection / PymodbusConnection and TmodbusUnit / PymodbusUnit — the old backend-specific names, kept for compatibility. New code should import ModbusConnection and type against the abstract modbus_connection.ModbusConnection and the ModbusUnit Protocol.
  • The legacy factories connect_tcp, connect_udp, connect_tls, and connect_serial — kept for compatibility. Each builds the matching parameter dataclass from keyword arguments, also accepts the constructor’s timeout, message_spacing, and connect_delay, constructs a ModbusConnection, eagerly connect()s it, and returns it. New code should construct ModbusConnection with a shared parameter object instead.