Skip to main content

Async-native Modbus RTU client for Python, built on AnyIO and anyserial.

Project description

anymodbus

Async-native Modbus RTU client for Python, built on AnyIO and anyserial.

CI PyPI version Python versions License: MIT

[!WARNING] Alpha. The v0.1 surface is implemented and tested but has not yet been exercised against a wide range of real hardware. Expect minor API tweaks before v1.0. See DESIGN.md for the full plan and CHANGELOG.md for the current state.

Overview

anymodbus is a small, opinionated Modbus client (RTU and ASCII serial framing) built on AnyIO and anyserial. It is intentionally protocol-only and narrow in scope. It does not ship servers, TCP transport (planned for v0.3), or device-specific drivers — pymodbus is the right choice if you need any of those, and the two libraries can coexist in one project.

The cases anymodbus is built for:

  • AnyIO-native. Same code runs under asyncio, uvloop, or trio. pymodbus is asyncio-only.
  • Tx-side 3.5-char inter-frame gap. Enforced before sending. pymodbus relies on its concurrency lock plus OS scheduling and does not enforce a pre-tx idle gap.
  • Idempotent-only retries by default. Reads (FC 1-4) retry on transient transport errors; writes (FC 5/6/15/16) do not, unless you opt in. Protects against silent double-writes when a successful write's response is lost in transit.
  • Strict typing. mypy strict = true plus pyright typeCheckingMode = "strict". pymodbus uses partial-strict mypy and standard pyright.
  • Required baudrate and parity. No defaults — mismatched parity silently drops every frame, so making it explicit at the call site is worth the small ergonomic cost.
  • Transport-agnostic. Takes any anyio.abc.ByteStream, defaults to an anyserial.SerialPort. TCP support is planned in v0.3 with the same Bus API.

See docs/migration-from-pymodbus.md for an honest comparison.

Requirements

  • Python 3.13 or 3.14
  • anyio >= 4.13
  • anyserial >= 0.1.1

Installation

uv add anymodbus
# or
pip install anymodbus

Optional extras:

uv add "anymodbus[trio]"    # trio runtime

Usage

Async, one slave

import anyio
from anymodbus import open_modbus_rtu


async def main() -> None:
    async with await open_modbus_rtu("/dev/ttyUSB0", baudrate=19_200, parity="even") as bus:
        slave = bus.slave(address=1)
        regs = await slave.read_holding_registers(0x0040, count=2)
        print(regs)


anyio.run(main)

Sync

from anymodbus.sync import open_modbus_rtu

with open_modbus_rtu("/dev/ttyUSB0", baudrate=19_200, parity="even") as bus:
    slave = bus.slave(1)
    regs = slave.read_holding_registers(0, count=4, timeout=1.0)

Reading a 32-bit float across two registers

Word order varies by device — the Modbus spec doesn't standardize multi-register layout. The default is HIGH_LOW × big-endian-within-word, equivalent to struct.pack(">f", ...); pass word_order="low_high" for devices that store the LSW first.

from anymodbus import WordOrder

async with await open_modbus_rtu("/dev/ttyUSB0", baudrate=19_200, parity="even") as bus:
    slave = bus.slave(address=1)
    high_low_value = await slave.read_float(0x0040)                              # default
    low_high_value = await slave.read_float(0x0044, word_order=WordOrder.LOW_HIGH)

Wrapping an existing serial port (with RS-485)

from anyserial import open_serial_port, SerialConfig, RS485Config, Parity
from anymodbus import Bus

port = await open_serial_port(
    "/dev/ttyUSB0",
    SerialConfig(
        baudrate=19_200,
        parity=Parity.EVEN,
        rs485=RS485Config(enabled=True, rts_on_send=True, rts_after_send=False),
    ),
)
async with Bus(port) as bus:
    regs = await bus.slave(1).read_holding_registers(0, count=4)

Modbus ASCII framing and a diagnostic loopback probe

from anymodbus import Framing, RegisterSource, open_modbus_ascii

# Classic 7E1 ASCII wire (data_bits=7); 8 also works.
async with await open_modbus_ascii(
    "/dev/ttyUSB0", baudrate=19_200, parity="even", data_bits=7
) as bus:
    slave = bus.slave(30)
    # Cheap, side-effect-free liveness probe (FC08 sub-0 loopback):
    assert await slave.diagnostic_loopback(b"\xAB\xCD") == b"\xAB\xCD"
    # Read a measurement published as input registers (FC04):
    o2 = await slave.read_float(0, source=RegisterSource.INPUT)

# Caller-owned stream (e.g. a port shared across modes): pick framing explicitly.
from anymodbus import Bus
bus = Bus(my_byte_stream, framing=Framing.ASCII)

Concurrent fan-out across multiple buses

import anyio
from anymodbus import open_modbus_rtu


async def poll_one(path: str, results: dict[str, tuple[int, ...]]) -> None:
    async with await open_modbus_rtu(path, baudrate=19_200, parity="even") as bus:
        results[path] = await bus.slave(1).read_holding_registers(0, count=4)


async def main() -> None:
    paths = ["/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2"]
    results: dict[str, tuple[int, ...]] = {}
    async with anyio.create_task_group() as tg:
        for p in paths:
            tg.start_soon(poll_one, p, results)
    for path, regs in results.items():
        print(path, regs)


anyio.run(main)

Testing without hardware

from anymodbus.testing import client_slave_pair

async with client_slave_pair(slave_address=1) as (bus, mock):
    mock.holding_registers[0:4] = [10, 20, 30, 40]
    regs = await bus.slave(1).read_holding_registers(0, count=4)
    assert regs == (10, 20, 30, 40)

Documentation

Full documentation will live at https://graysonbellamy.github.io/anymodbus/. Starting points:

Contributing

Issues and PRs are welcome. To get a local checkout running:

git clone https://github.com/GraysonBellamy/anymodbus
cd anymodbus
uv sync --all-extras
uv run pre-commit install

Before opening a PR:

uv run pytest
uv run ruff check
uv run ruff format --check
uv run mypy
uv run pyright

Hardware-dependent tests are opt-in via pytest -m hardware with ANYMODBUS_TEST_PORT and ANYMODBUS_TEST_SLAVE_ADDRESS set.

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

anymodbus-0.2.0.tar.gz (111.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

anymodbus-0.2.0-py3-none-any.whl (67.5 kB view details)

Uploaded Python 3

File details

Details for the file anymodbus-0.2.0.tar.gz.

File metadata

  • Download URL: anymodbus-0.2.0.tar.gz
  • Upload date:
  • Size: 111.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for anymodbus-0.2.0.tar.gz
Algorithm Hash digest
SHA256 32e7da29abc0f847ab8834791ab8d9d66aae72e90720a3ec4b18e35221eac873
MD5 a54143634274dd8782ec193608289126
BLAKE2b-256 1fd240d8f9009ccaacd16f0b4d46872d58ce30cb506f74df4c368c24f4c77f1a

See more details on using hashes here.

Provenance

The following attestation bundles were made for anymodbus-0.2.0.tar.gz:

Publisher: publish.yml on GraysonBellamy/anymodbus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file anymodbus-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: anymodbus-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 67.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for anymodbus-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 28c5ce49fcd9da9b77f4719cbe47f64c7cb7abdb34ae01d1b286acf641780f93
MD5 de26b19b53a154c8fd12740f0fdffeb0
BLAKE2b-256 13302728298110461425a82af42690e9f63b09339fd12f0db2b7a723a1a65cd6

See more details on using hashes here.

Provenance

The following attestation bundles were made for anymodbus-0.2.0-py3-none-any.whl:

Publisher: publish.yml on GraysonBellamy/anymodbus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page