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=None, message_spacing=None, connect_delay=None)
Parameter Type Meaning
params ModbusTcpParams | ModbusUdpParams | ModbusTlsParams | ModbusSerialParams The transport to connect over. See the parameter dataclasses.
timeout float | None, default None Per-request timeout in seconds.
message_spacing float | None, default None 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 | None, default None 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.

Each tuning value is optional. A value given here joins the resolution with every unit requirement, and the largest wins. With nothing asked at all the connection uses a 10 second timeout, no message spacing and no connect delay.

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, because every unit operation connects first.

Return this backend’s stateless ModbusUnit handle bound to unit_id. Handles are cheap. Consumers receive a handle and 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. Waits up to half a second for the request in flight. A request still running after that is cut and fails. 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. The connection is marked closed first, then the call waits up to half a second for the request in flight.

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" | None None Deprecated; omit it. Passing any value warns, and any value but these three raises ValueError. "rtu" and "ascii" frame a serial line; use ModbusSerialParams with a socket:// device instead. "socket" is the only framing a Modbus TCP link has. Omitted, it reads back as "socket".
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), or a URL: socket://host:port, rfc2217://host:port.
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 also works as a dictionary key for grouping shared connections:

Class Endpoint Excluded settings
ModbusTcpParams ("tcp", host, port) none
ModbusTcpParams, deprecated rtu or ascii framing ("serial", f"socket://{host}:{port}") none
ModbusUdpParams ("udp", host, port) framer
ModbusTlsParams ("tcp", host, port) all TLS options
ModbusSerialParams ("serial", device) baudrate, bytesize, parity, stopbits, framer

ModbusTlsParams shares the "tcp" transport tag on purpose. 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 deprecated serial framing over TCP is the one case where framer changes the endpoint. Such a link is a serial line, so it takes the serial endpoint, the same one ModbusSerialParams gives for it. A gateway answering native Modbus TCP at that address is a different service, and keeps the "tcp" endpoint.

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.

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[bool, int]: ready flag, 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]

diagnostics() sends any sub-function code with one data word, and returns the data word the device answers with. get_comm_event_counter() returns whether the device is ready and its event count. The device is not ready while it is still processing a program function.

bool. Whether the owning connection’s link is currently established.

Keep the line quiet for seconds before each request to this unit and after each of its requests. The interval is measured from the completion of the previous request on the connection, to any 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.

Ask the link for a per-request timeout of at least seconds. The connection runs with the largest value asked of it, so this never shortens another unit’s timeout. Raising it above what a live link carries drops that link. Pass None to withdraw the requirement. Raises ValueError if seconds is negative. See Device requirements.

Ask the link for a pause of at least seconds after it opens, resolved the same way. It applies to the next connect. Pass None to withdraw the requirement. Raises ValueError if seconds is negative.

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

async. Drop the owning connection’s link. The next request establishes a new one. Equivalent to disconnect() on the owning connection, for holders of a unit that do not hold the connection itself.

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, as in IllegalDataAddressError(). This 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 is 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 are the old backend-specific names, kept for compatibility. New code imports ModbusConnection and types against the abstract modbus_connection.ModbusConnection and the ModbusUnit Protocol.
  • The legacy factories connect_tcp, connect_udp, connect_tls, and connect_serial are 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 constructs ModbusConnection with a shared parameter object instead.