Many modbus devices support only one or very few clients. This proxy acts as a bridge between the client and the modbus device. It can be seen as a layer 7 reverse proxy. This allows multiple clients to communicate with the same modbus device.
When multiple clients are connected, cross messages are avoided by serializing communication on a first come first served REQ/REP basis.
From within your favorite python 3 environment type:
$ pip install modbus-proxy
Note: On some systems pip
points to a python 2 installation.
You might need to use pip3
command instead.
Additionally, if you want logging configuration:
- YAML:
pip install modbus-proxy[yaml]
(see below) - TOML:
pip install modbus-proxy[toml]
(see below)
First, you will need write a configuration file where you specify for each modbus device you which to control:
- modbus connection (the modbus device url)
- listen interface (to which url your clients should connect)
Configuration files can be written in YAML (.yml or .yaml) or TOML (.toml).
Suppose you have a PLC modbus device listening on plc1.acme.org:502 and you want your clients to connect to your machine on port 9000. A YAML configuration would look like this:
devices:
- modbus:
url: plc1.acme.org:502 # device url (mandatory)
timeout: 10 # communication timeout (s) (optional, default: 10)
connection_time: 0.1 # delay after connection (s) (optional, default: 0)
listen:
bind: 0:9000 # listening address (mandatory)
Assuming you saved this file as modbus-config.yml
, start the server with:
$ modbus-proxy -c ./modbus-config.yml
Now, instead of connecting your client(s) to plc1.acme.org:502
you just need to
tell them to connect to *machine*:9000
(where machine is the host where
modbus-proxy is running).
Note that the server is capable of handling multiple modbus devices. Here is a configuration example for 2 devices:
devices:
- modbus:
url: plc1.acme.org:502
listen:
bind: 0:9000
- modbus:
url: plc2.acme.org:502
listen:
bind: 0:9001
If you have a single modbus device, you can avoid writting a configuration file by providing all arguments in the command line:
modbus-proxy -b tcp://0:9000 --modbus tcp://plc1.acme.org:502
(hint: run modbus-proxy --help
to see all available options)
To run the examples you will need to have
umodbus installed (do it
with pip install umodbus
).
Start the simple_tcp_server.py
(this will simulate an actual modbus hardware):
$ python examples/simple_tcp_server.py -b :5020
You can run the example client just to be sure direct communication works:
$ python examples/simple_tcp_client.py -a 0:5020
holding registers: [1, 2, 3, 4]
Now for the real test:
Start a modbus-proxy bridge server with:
$ modbus-proxy -b tcp://:9000 --modbus tcp://:5020
Finally run a the example client but now address the proxy instead of the server (notice we are now using port 9000 and not 5020):
$ python examples/simple_tcp_client.py -a 0:9000
holding registers: [1, 2, 3, 4]
- move the config file to a location you can remember, for example: to
/usr/lib/mproxy-conf.yaml
- go to
/etc/systemd/system/
- use nano or any other text editor of your choice to create a service file
mproxy.service
- the file should contain the following information:
[Unit]
Description=Modbus-Proxy
After=network.target
[Service]
Type=simple
Restart=always
ExecStart = modbus-proxy -c ./usr/lib/mproxy-conf.yaml
[Install]
WantedBy=multi-user.target
run systemctl daemon-reload
systemctl enable mproxy.service
systemctl start mproxy.service
The file names given here are examples, you can choose other names, if you wish.
This project ships with a basic Dockerfile which you can use as a base to launch modbus-proxy inside a docker container.
First, build the docker image with:
$ docker build -t modbus-proxy .
To bridge a single modbus device without needing a configuration file is straight forward:
$ docker run -d -p 5020:502 modbus-proxy -b tcp://0:502 --modbus tcp://plc1.acme.org:502
Now you should be able to access your modbus device through the modbus-proxy by
connecting your client(s) to <your-hostname/ip>:5020
.
If, instead, you want to use a configuration file, you must mount the file so it is visible by the container.
Assuming you have prepared a conf.yml
in the current directory:
devices:
- modbus:
url: plc1.acme.org:502
listen:
bind: 0:502
Here is an example of how to run the container:
docker run -p 5020:502 -v $PWD/conf.yml:/src/conf.yml modbus-proxy -c /src/conf.yml
Note that for each modbus device you add in the configuration file you need
to publish the corresponding bind port on the host
(-p <host port>:<container port>
argument).
Logging configuration can be added to the configuration file by adding a new logging
keyword.
The logging configuration will be passed to logging.config.dictConfig() so the file contents must obey the Configuration dictionary schema.
Here is a YAML example:
devices:
- modbus:
url: plc1.acme.org:502
listen:
bind: 0:9000
logging:
version: 1
formatters:
standard:
format: "%(asctime)s %(levelname)8s %(name)s: %(message)s"
handlers:
console:
class: logging.StreamHandler
formatter: standard
root:
handlers: ['console']
level: DEBUG
Logging configuration file.
If a relative path is given, it is relative to the current working directory.
If a .conf
or .ini
file is given, it is passed directly to
logging.config.fileConfig() so the file contents must
obey the
Configuration file format.
A simple logging configuration (also available at log.conf) which mimics the default configuration looks like this:
[formatters]
keys=standard
[handlers]
keys=console
[loggers]
keys=root
[formatter_standard]
format=%(asctime)s %(levelname)8s %(name)s: %(message)s
[handler_console]
class=StreamHandler
formatter=standard
[logger_root]
level=INFO
handlers=console
A more verbose example logging with a rotating file handler: log-verbose.conf
The same example above (also available at log.yml) can be achieved in YAML with:
version: 1
formatters:
standard:
format: "%(asctime)s %(levelname)8s %(name)s: %(message)s"
handlers:
console:
class: logging.StreamHandler
formatter: standard
root:
handlers: ['console']
level: DEBUG
- Tiago Coutinho coutinhotiago@gmail.com
None yet. Why not be the first?