Skip to main content

ModBus routing gateway with pluggable frontends/backends and MQTT mirroring

Project description

Modbus Gateway

A flexible and extensible ModBus gateway written in Python, supporting pluggable frontends and backends, advanced routing, and security filtering.

The gateway allows to bridge, transform, and secure ModBus communication across heterogeneous systems - from serial devices to TCP and Unix domain sockets - while maintaining full control over addressing and access boundaries. It exposes a variety of frontends (emulating serial ports, ModBus TCP over UDS or IP with and without (m)TLS support) as well as backends (serial ports, ModBus IP targets, etc.). It allows multiple applications to access the same backends, providing arbitration and synchronization.

Note: Arbitration does not work when multiple applications access the same virtual serial port frontend. In this case the operating system mimics the behaviour of a real serial port, leading to interleaving of reads and writes of the various applications. When using virtual serial ports each application instance should use their own virtual serial port. This does not apply to ModBus/TCP frontends.

This project has been developed to allow multiple services to access various services attached to the same hardware ModBus network on a machine exploiting multiple RS485 interfaces (identified via unique device names).

Features

  • Pluggable architecture: Modular frontends and backends, allowing further extensions in future versions.
  • Multiple frontends: Virtual serial ports (pty), ModBus TCP over TCP and Unix domain sockets
  • Secure TCP support: TLS and mTLS (with client certificate authentication) for ModBus TCP over TCP/IP
  • Multiple backends: Hardware serial ports and ModBus TCP IP backends
  • Flexible Routing Engine: Map device IDs and registers between frontends and backends; split or aggregate devices across multiple backends and passthrough mode for transparent forwarding
  • Security Filtering: Per-frontend filtering rules enforcing access boundaries on register/device level. Ideal for isolating subsystems or exposing limited views.
  • Future extensions: Planned MQTT interface for IoT integration

Work in Progress

The following features are still work in progress:

  • MQTT support
  • REST API support

Installation

The gateway can be installed via PyPi via

pip install modbus-gateway

or from the repository root via

pip install -e .

The associated client library is available via

pip install modbusgw-client

Again it can also be installed from the repositories modbusgw_client subdirectory via

pip install -e ./modbusgw_client/

Configuration

The default configuration file is located at ~/.config/modbusgateway.cfg. It is composed of a single large JSON dictionary consisting of the following keys:

  • service provides configuration of the main daemon
  • bus configures the internal message bus
  • frontends contains a list of frontend configurations over which clients are capable of accessing the daemon
  • backends is the counterparts and defines the interfaces that are accessed on behalf of the clients via the gateway.
  • routes provides a match-list based configuration on how to route messages between frontends and backends.

The service section configures PID file to prevent multiple running instances, the state directory that will be used for log- and tracefiles as well as the loglevel:

"service" : {
   "log_level" : "INFO",
   "pid_file" : "/var/run/modbusgw.pid",
   "state_dir" : "/var/modbusgw/",
   "reload_grace_seconds" : 5
}

The bus configuration configures the internal buffer for incoming requests that are routed to various backends:

"bus" : {
   "request_queue_size" : 64,
   "response_timeout_ms" : 1500
}

Note that this timeout should be shorter than the applications and frontends timeouts.

Frontend Configurations

Virtual Serial Ports (pty)

Virtual serial ports are directly accessible via pyserial and similar interfaces. This allows existing legacy software to access the gateway via unmodified code by pointing it at the virtual serial port file handles:

{
   "id" : "virtual_serial_rtu",
   "type" : "serial_rtu_socket",
   "socket_path" : "/var/modbusgw/ttyBus0",
   "pty_mode" : "rw",
   "idle_close_seconds" : 600,
   "frame_timeout_ms" : 5.0
}

The shown configuration instantiates a virtual serial port at the specified socket_path, allowing read-write transactions. The frame timeout handles incomplete messages on the application side. The name virtual_serial_rtu is an arbitrary chosen name that is used in the routing configuration.

ModBus IP TCP Socket

A ModBus IP socket speaks the ModBus IP protocol over an TCP socket (optionally supporting TLS or mTLS for authenticated sessions). The following configuration exposes unencrypted ModBus IP applying only IP subnet based filters:

{
   "id" : "frontend_tcp",
   "type" : "tcp_modbus_tcp",
   "host" : "192.0.2.1",
   "port" : 1234,
   "cidr_allow" : [
      "127.0.0.0/8",
      "192.0.2.0/24"
   ]
}

If TLS is desired the following configuration can be added to the frontend configuration object:

   "tls" : {
      "cert_file" : "/path/to/server.crt",
      "key_file" : "/path/to/server.key",
      "ca_file" : "/path/to/rootca.crt",
      "require_client_cert" : true,
      "client_dn_allow" : [
         "CN=ModbusGW Test Client"
      ]
   }

The cert_file and key_file establish the server identity. The ca_file is only used when require_client_cert is set to true to allow client authentication. The additional (optional) client_dn_allow filter allows to filter the DNs from valid certificates (after certificate validation) that are allowed to access the frontend.

Backend Configurations

Hardware Serial Ports

The pyserial backend uses the pyserial library to access an USB to RS485 based interface. This is the most simple hardware interface for DIY setups. The specified serial configuration is applied when accessing the backend. Again the arbitrary id is used in the routing configuration.

{
   "id" : "hardware_serial",
   "type" : "pyserial",
   "device" : "/dev/ttyU0",
   "baudrate" : 9600,
   "parity" : "N",
   "stop_bits" : 1,
   "request_timeout_ms" : 1200
}

ModBus IP via TCP

A TCP backend can be configured via the tcp_modbus backend:

{
   "id" : "tcp_backend",
   "type" : "tcp_modbus",
   "host" : "127.0.0.1",
   "port" : 1234,
   "connect_timeout" : 2.0,
   "pool_size" : 2,
   "use_tls" : true,
   "tls" : {
      "ca_file" : "/path/to/root.crt",
      "cert_file" : "/path/to/client.crt",
      "key_file" : "/path/to/client.key"
   }
}

The use_tls and tls blocks are optional and are only used when (m)TLS is desired. The root.crt is used for validation, the client keys for authentication via mTLS.

Routing Configuration

The routing configuration is provided as a list of routing commands that are matched against incoming requests from the frontends. The first match determines to which backend a message is routed. The backend key and the mirror_to_mqtt key is not used for matching, all other fields apply:

{
   "frontend" : "virtual_serial_rtu",
   "backend" : "hardware_serial",
   "match" : {
      "unit_ids" : [ "*" ],
      "function_codes" : [ "*" ]
   },
   "mirror_to_mqtt" : [ ]
}

The routing match block allows to filter given device IDs and function codes as well as operations. For example to allow only function code 1 (read coils) for the virtual device 5, redirecting the operation to the backend device id 1, one would use

{
   "frontend" : "virtual_serial_rtu",
   "backend" : "hardware_serial",
   "match" : {
      "unit_ids" : [ 5 ],
      "function_codes" : [ 1 ],
      "operations" : [ "read" ]
   },
   "unit_override" : 1,
   "mirror_to_mqtt" : [ ]
}

Here the match block specifies conditions that have to be fulfilled (all have to be fulfilled). The optional unit_override replaces the device ID on the virtual frontend bus to the given unit number before handing off the the backend device. All fields can be used in arbitrary combinations.

Example configuration file

The following configuration exposes a single serial to RS485 interface via a local virtual serial port as well as a ModBus IP socket available via unencrypted TCP:

{
   "service" : {
      "log_level" : "INFO",
      "pid_file" : "/var/run/modbusgw.pid",
      "state_dir" : "/var/modbusgw/",
      "reload_grace_seconds" : 5
   },
   "bus" : {
      "request_queue_size" : 64,
      "response_timeout_ms" : 1500
   },
   "frontends" : [
      {
         "id" : "virtual_serial_rtu",
         "type" : "serial_rtu_socket",
         "socket_path" : "/var/modbusgw/ttyBus0",
         "pty_mode" : "rw",
         "idle_close_seconds" : 600,
         "frame_timeout_ms" : 5.0
      },
      {
         "id" : "frontend_tcp",
         "type" : "tcp_modbus_tcp",
         "host" : "192.0.2.1",
         "port" : 1234,
         "cidr_allow" : [
            "127.0.0.0/8",
            "192.0.2.0/24"
         ]
      }
   ],
   "backends" : [
      {
         "id" : "hardware_serial",
         "type" : "pyserial",
         "device" : "/dev/ttyU0",
         "baudrate" : 9600,
         "parity" : "N",
         "stop_bits" : 1,
         "request_timeout_ms" : 1200
      }
   ],
   "routes" : [
      {
         "frontend" : "virtual_serial_rtu",
         "backend" : "hardware_serial",
         "match" : {
            "unit_ids" : [ "*" ],
            "function_codes" : [ "*" ]
         },
         "mirror_to_mqtt" : [ ]
      },
      {
         "frontend" : "frontend_tcp",
         "backend" : "hardware_serial",
         "match" : {
            "unit_ids" : [ "*" ],
            "function_codes" : [ "*" ]
         },
         "mirror_to_mqtt" : [ ]
      }
   ]
}

FreeBSD rc.init script

The repository contains an rc.init script in the rc.d subdirectory. This is suited for FreeBSDs rc.init infrastructure and allows setting startup parameters in rc.conf:

modbusgw_enable="YES"
modbusgw_user="modbusgw"
modbusgw_group="modbusgw"

The default configuration file location when using the rc.init script is /usr/local/etc/modbusgw/modbusgateway.cfg, which can be overriden via the modbusgw_config variable. In addition the script ensures the presence and writeability of the /var/modbusgw directory. The script supports:

  • start
  • stop
  • status
  • restart
  • reload (note that this does not reload the configuration for logging!)

Running

When not using the rc.init script mentioned above one can launch the application in foreground via

$ modbusgw

To execute the daemon simply add the appropriate lifecycle commands:

$ modbusgw start
$ modbusgw stop
$ modbusgw status
$ modbusgw restart
$ modbusgw reload

Client Library

This repository also contains an independent client library for interacting with ModBus systems via serial ports or ModBus TCP. The documentation is found in the modbusgw-client directory.

Common Pitfalls

  • Seeing collisions even when using modbus-gateway and accessing it via virtual serial ports. Arbitration only starts after the data has entered the application. Virtual serial ports do not provide arbitration between different applications by operating system design. When using this frontend each application should use their own virtual serial port. When accessing the same serial ports the reads and writes may get interleaved and interfer, as for a real serial port. This does not apply to the TCP frontends.

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

modbus_gateway-0.0.5.tar.gz (34.4 kB view details)

Uploaded Source

Built Distribution

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

modbus_gateway-0.0.5-py3-none-any.whl (36.4 kB view details)

Uploaded Python 3

File details

Details for the file modbus_gateway-0.0.5.tar.gz.

File metadata

  • Download URL: modbus_gateway-0.0.5.tar.gz
  • Upload date:
  • Size: 34.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.11.11

File hashes

Hashes for modbus_gateway-0.0.5.tar.gz
Algorithm Hash digest
SHA256 b00fe72d50ce52fad7d424713c45a0d6e1f9b1ccf0f61da0437750961249403d
MD5 77cc98ac08f900d1a3cf3469497d56e0
BLAKE2b-256 bd4193f7092d403221e115536b0c69e7f7827be93f894b5fd81433be71051ca1

See more details on using hashes here.

File details

Details for the file modbus_gateway-0.0.5-py3-none-any.whl.

File metadata

  • Download URL: modbus_gateway-0.0.5-py3-none-any.whl
  • Upload date:
  • Size: 36.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.11.11

File hashes

Hashes for modbus_gateway-0.0.5-py3-none-any.whl
Algorithm Hash digest
SHA256 ff92658ec4294f2b16f3d555dcd962b75896bf79287b502f711e6d17004af833
MD5 ea27361eab67801e2421811c63254856
BLAKE2b-256 7aca23caad1b7afb01acf082d11c28e7b84c0d5949e4c95f3d6fbbea26f166cf

See more details on using hashes here.

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