Skip to content

Embedded-Focus/modbus-multiplexer

Repository files navigation

Modbus TCP, RTU-over-TCP, and RTU to RTU Multiplexer

An async Python proxy that accepts Modbus TCP, Modbus RTU-over-TCP, and Modbus RTU clients on arbitrary upstream listeners, then forwards supported requests to one downstream Modbus RTU bus through one serialized worker. The current implementation is read-only and supports Modbus function codes 0x03 (Read Holding Registers) and 0x04 (Read Input Registers).

QuickStart

1. Install dependencies

uv sync

This project depends on pymodbus[serial], so serial support is included.

2. Create a config file

version: 1

logging:
  level: INFO
  frame_checks: false

queue:
  size: 100

timing:
  response_delay: null

downstream_options:
  connect_retries: 3
  remote_device_id: null

downstream:
  port: /dev/ttyUSB0
  baudrate: 9600
  bytesize: 8
  parity: N
  stopbits: 1
  timeout: 1.0

listeners:
  - name: evcc
    type: tcp
    host: 0.0.0.0
    port: 5020
    protocol: tcp

  - name: victron-rtu-over-tcp
    type: tcp
    host: 0.0.0.0
    port: 5021
    protocol: rtu-over-tcp

  - name: cerbo-gx
    type: serial
    port: /dev/ttyUSB1
    baudrate: 9600
    bytesize: 8
    parity: N
    stopbits: 1
    timeout: 1.0

3. Start the proxy

uv run modbus-multiplexer --config config.yaml

Example with a fixed RTU unit id:

downstream_options:
  remote_device_id: 1

Example for clients that send Modbus RTU frames over a TCP socket:

listeners:
  - name: rtu-over-tcp
    type: tcp
    host: 0.0.0.0
    port: 502
    protocol: rtu-over-tcp

If downstream_options.remote_device_id is not provided, the proxy forwards the incoming client unit id to the RTU side.

4. Point your clients at the proxy

Use the configured upstream listeners:

  • TCP host and port: configured per TCP listener
  • TCP protocol: tcp or rtu-over-tcp
  • serial RTU port and serial settings: configured per serial listener
  • function codes: 0x03 and 0x04

5. Run checks

uv run ruff check --fix
uv run ruff format
uv run ty check
uv run pytest

Learn More

Usage

The CLI entrypoint is implemented in src/modbus_multiplexer/__main__.py. The only runtime option is --config.

Configuration keys:

  • version: config format version, currently 1
  • logging.level: Python logging level, default INFO
  • logging.frame_checks: include PyModbus RTU frame-check debug logs, default false
  • queue.size: maximum pending RTU requests, default 100
  • timing.response_delay: optional delay in seconds before replying upstream after a downstream read, default null
  • downstream: required downstream serial RTU bus settings
  • downstream.port: serial device path, required
  • downstream.baudrate: serial baud rate, default 9600
  • downstream.bytesize: serial byte size, default 8
  • downstream.parity: serial parity, N, E, or O, default N
  • downstream.stopbits: serial stop bits, default 1
  • downstream.timeout: RTU request timeout in seconds, default 1.0
  • downstream_options.remote_device_id: optional fixed RTU unit id
  • downstream_options.connect_retries: PyModbus serial reconnect retries, default 3
  • listeners: non-empty list of upstream listeners
  • listeners[].name: unique listener name, required
  • listeners[].type: tcp or serial, required
  • TCP listener keys: host, port, protocol
  • serial listener keys: port, baudrate, bytesize, parity, stopbits, timeout

Current limitations:

  • only read functions 0x03 and 0x04 are forwarded
  • write requests are rejected
  • no end-to-end simulator or hardware integration test is included yet

Implementation

The main design goal is protecting the downstream RTU bus from concurrent access. Multiple upstream clients may talk to the proxy at once over any configured TCP or serial listener, but all downstream RTU requests are serialized through a bounded queue and one async worker.

Key modules:

Request flow:

  1. a Modbus TCP, RTU-over-TCP, or serial RTU client sends a read request to one configured upstream listener
  2. the corresponding PyModbus listener decodes the request and passes datastore access into the proxy context
  3. the proxy context enqueues the request for the RTU worker
  4. the worker performs exactly one RTU operation at a time
  5. registers or Modbus exception codes are returned to the upstream client

Error handling behavior:

  • full queue: DEVICE_BUSY
  • timeout: GATEWAY_NO_RESPONSE
  • RTU connection failure: GATEWAY_PATH_UNAVIABLE
  • unsupported function: ILLEGAL_FUNCTION
  • unexpected internal error: DEVICE_FAILURE

Development Notes

The implementation guidance for future changes is documented in AGENT.md.

AI Disclosure

All code in this project was generated with AI assistance and then reviewed by a human maintainer.

About

Async Modbus TCP-to-RTU proxy with serialized RTU access for multiple TCP clients.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors