# ModBus TCP proxy
[![ModBus proxy][pypi-version]](https://pypi.python.org/pypi/modbus-proxy)
[![Python Versions][pypi-python-versions]](https://pypi.python.org/pypi/modbus-proxy)
[![Pypi status][pypi-status]](https://pypi.python.org/pypi/modbus-proxy)
![License][license]
[![CI][CI]](https://github.com/tiagocoutinho/modbus-proxy/actions/workflows/ci.yml)
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.
## Installation
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)
## Running the server
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:
```yaml
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)
unit_id_remapping: # remap/forward unit IDs (optional, empty by default)
1: 0
```
Assuming you saved this file as `modbus-config.yml`, start the server with:
```bash
$ 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:
```yaml
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:
```bash
modbus-proxy -b tcp://0:9000 --modbus tcp://plc1.acme.org:502
```
(hint: run `modbus-proxy --help` to see all available options)
### Forwarding Unit Identifiers
You can also forward one unit ID to another whilst proxying. This is handy if the target
modbus server has a unit on an index that is not supported by one of your clients.
```yaml
devices:
- modbus: ... # see above.
listen: ... # see above.
unit_id_remapping:
1: 0
```
The above forwards requests to unit ID 1 to your modbus-proxy server to unit ID 0 on the
actual modbus server.
Note that **the reverse also applies**: if you forward unit ID 1 to unit ID 0, **all** responses coming from unit 0 will look as if they are coming from 1, so this may pose problems if you want to use unit ID 0 for some clients and unit ID 1 for others (use unit ID 1 for all in that case).
## Running the examples
To run the examples you will need to have
[umodbus](https://github.com/AdvancedClimateSystems/uModbus) installed (do it
with `pip install umodbus`).
Start the `simple_tcp_server.py` (this will simulate an actual modbus hardware):
```bash
$ python examples/simple_tcp_server.py -b :5020
```
You can run the example client just to be sure direct communication works:
```bash
$ 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:
```bash
$ 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*):
```bash
$ python examples/simple_tcp_client.py -a 0:9000
holding registers: [1, 2, 3, 4]
```
## Running as a Service
1. move the config file to a location you can remember, for example: to `/usr/lib/mproxy-conf.yaml`
2. go to `/etc/systemd/system/`
3. use nano or any other text editor of your choice to create a service file `mproxy.service`
4. 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
```
5. `run systemctl daemon-reload`
6. `systemctl enable mproxy.service`
7. `systemctl start mproxy.service`
The file names given here are examples, you can choose other names, if you wish.
## Docker
This project ships with a basic [Dockerfile](./Dockerfile) which you can use
as a base to launch modbus-proxy inside a docker container.
First, build the docker image with:
```bash
$ docker build -t modbus-proxy .
```
To bridge a single modbus device without needing a configuration file is
straight forward:
```bash
$ 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:
```yaml
devices:
- modbus:
url: plc1.acme.org:502
listen:
bind: 0:502
```
Here is an example of how to run the container:
```bash
docker run -p 5020:502 -v $PWD/conf.yml:/config/modbus-proxy.yml modbus-proxy
```
By default the Dockerfile will run `modbus-proxy -c /config/modbus-proxy.yml` so
if your mounting that volume you don't need to pass any arguments.
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
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()](https://docs.python.org/library/logging.config.html#logging.config.dictConfig)
so the file contents must obey the
[Configuration dictionary schema](https://docs.python.org/library/logging.config.html#configuration-dictionary-schema).
Here is a YAML example:
```yaml
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
```
### `--log-config-file` (deprecated)
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()](https://docs.python.org/library/logging.config.html#logging.config.fileConfig) so the file contents must
obey the
[Configuration file format](https://docs.python.org/library/logging.config.html#configuration-file-format).
A simple logging configuration (also available at [log.conf](examples/log.conf))
which mimics the default configuration looks like this:
```toml
[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](examples/log-verbose.conf)
The same example above (also available at [log.yml](examples/log.yml)) can be achieved in YAML with:
```yaml
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
```
## Credits
### Development Lead
* Tiago Coutinho <coutinhotiago@gmail.com>
### Contributors
None yet. Why not be the first?
[pypi-python-versions]: https://img.shields.io/pypi/pyversions/modbus-proxy.svg
[pypi-version]: https://img.shields.io/pypi/v/modbus-proxy.svg
[pypi-status]: https://img.shields.io/pypi/status/modbus-proxy.svg
[license]: https://img.shields.io/pypi/l/modbus-proxy.svg
[CI]: https://github.com/tiagocoutinho/modbus-proxy/actions/workflows/ci.yml/badge.svg
## History
### 0.6.1 (2021-09-29)
* Change default command line `--modbus-connection-time` from 0.1 to 0
* Add basic unit tests
* Github actions
* Repository cleanup
### 0.5.0 (2021-09-28)
* Add support for multiple devices
* Adapt docker to changes
* Deprecate `--log-config-file` command line parameter
### 0.4.2 (2021-09-23)
* Add connection time delay (fixes #4)
### 0.4.1 (2021-01-26)
* Logging improvements
### 0.4.0 (2021-01-26)
* Logging improvements
### 0.3.0 (2021-01-25)
* More robust server (fixes #2)
### 0.2.0 (2021-01-23)
* Document (README)
* Add docker intructions (fixes #1)
* Fix setup dependencies and meta data
### 0.1.1 (2020-12-02)
* Fix project package
### 0.1.0 (2020-11-11)
* First release on PyPI.
Raw data
{
"_id": null,
"home_page": "https://github.com/tiagocoutinho/modbus-proxy",
"name": "modbus-proxy",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.9",
"maintainer_email": null,
"keywords": "modbus, tcp, proxy",
"author": "Tiago Coutinho",
"author_email": "coutinhotiago@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/e8/1b/c265f2f53cfedaeea4b13df5517da1c0556c29aaca7b150323e9e5561de6/modbus_proxy-0.8.0.tar.gz",
"platform": null,
"description": "# ModBus TCP proxy\n\n[![ModBus proxy][pypi-version]](https://pypi.python.org/pypi/modbus-proxy)\n[![Python Versions][pypi-python-versions]](https://pypi.python.org/pypi/modbus-proxy)\n[![Pypi status][pypi-status]](https://pypi.python.org/pypi/modbus-proxy)\n![License][license]\n[![CI][CI]](https://github.com/tiagocoutinho/modbus-proxy/actions/workflows/ci.yml)\n\nMany 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\nlayer 7 reverse proxy.\nThis allows multiple clients to communicate with the same modbus device.\n\nWhen multiple clients are connected, cross messages are avoided by serializing communication on a first come first served REQ/REP basis.\n\n## Installation\n\nFrom within your favorite python 3 environment type:\n\n`$ pip install modbus-proxy`\n\nNote: On some systems `pip` points to a python 2 installation.\nYou might need to use `pip3` command instead.\n\nAdditionally, if you want logging configuration:\n* YAML: `pip install modbus-proxy[yaml]` (see below)\n* TOML: `pip install modbus-proxy[toml]` (see below)\n\n## Running the server\n\nFirst, you will need write a configuration file where you specify for each modbus device you which to control:\n\n* modbus connection (the modbus device url)\n* listen interface (to which url your clients should connect)\n\nConfiguration files can be written in YAML (*.yml* or *.yaml*) or TOML (*.toml*).\n\nSuppose you have a PLC modbus device listening on *plc1.acme.org:502* and you want your clients to\nconnect to your machine on port 9000. A YAML configuration would look like this:\n\n```yaml\ndevices:\n- modbus:\n url: plc1.acme.org:502 # device url (mandatory)\n timeout: 10 # communication timeout (s) (optional, default: 10)\n connection_time: 0.1 # delay after connection (s) (optional, default: 0)\n listen:\n bind: 0:9000 # listening address (mandatory)\n unit_id_remapping: # remap/forward unit IDs (optional, empty by default)\n 1: 0\n```\n\nAssuming you saved this file as `modbus-config.yml`, start the server with:\n\n```bash\n$ modbus-proxy -c ./modbus-config.yml\n```\n\nNow, instead of connecting your client(s) to `plc1.acme.org:502` you just need to\ntell them to connect to `*machine*:9000` (where *machine* is the host where\nmodbus-proxy is running).\n\nNote that the server is capable of handling multiple modbus devices. Here is a\nconfiguration example for 2 devices:\n\n```yaml\ndevices:\n- modbus:\n url: plc1.acme.org:502\n listen:\n bind: 0:9000\n- modbus:\n url: plc2.acme.org:502\n listen:\n bind: 0:9001\n```\n\nIf you have a *single* modbus device, you can avoid writting a configuration file by\nproviding all arguments in the command line:\n\n```bash\nmodbus-proxy -b tcp://0:9000 --modbus tcp://plc1.acme.org:502\n```\n\n(hint: run `modbus-proxy --help` to see all available options)\n\n### Forwarding Unit Identifiers\n\nYou can also forward one unit ID to another whilst proxying. This is handy if the target\nmodbus server has a unit on an index that is not supported by one of your clients.\n\n```yaml\ndevices:\n- modbus: ... # see above.\n listen: ... # see above.\n unit_id_remapping:\n 1: 0\n```\n\nThe above forwards requests to unit ID 1 to your modbus-proxy server to unit ID 0 on the\nactual modbus server.\n\nNote that **the reverse also applies**: if you forward unit ID 1 to unit ID 0, **all** responses coming from unit 0 will look as if they are coming from 1, so this may pose problems if you want to use unit ID 0 for some clients and unit ID 1 for others (use unit ID 1 for all in that case).\n\n## Running the examples\n\nTo run the examples you will need to have\n[umodbus](https://github.com/AdvancedClimateSystems/uModbus) installed (do it\nwith `pip install umodbus`).\n\nStart the `simple_tcp_server.py` (this will simulate an actual modbus hardware):\n\n```bash\n$ python examples/simple_tcp_server.py -b :5020\n```\n\nYou can run the example client just to be sure direct communication works:\n\n```bash\n$ python examples/simple_tcp_client.py -a 0:5020\nholding registers: [1, 2, 3, 4]\n```\n\nNow for the real test:\n\nStart a modbus-proxy bridge server with:\n\n```bash\n$ modbus-proxy -b tcp://:9000 --modbus tcp://:5020\n```\n\nFinally run a the example client but now address the proxy instead of the server\n(notice we are now using port *9000* and not *5020*):\n\n```bash\n$ python examples/simple_tcp_client.py -a 0:9000\nholding registers: [1, 2, 3, 4]\n```\n## Running as a Service\n1. move the config file to a location you can remember, for example: to `/usr/lib/mproxy-conf.yaml`\n2. go to `/etc/systemd/system/`\n3. use nano or any other text editor of your choice to create a service file `mproxy.service`\n4. the file should contain the following information:\n```\n[Unit]\nDescription=Modbus-Proxy\nAfter=network.target\n\n[Service]\nType=simple\nRestart=always\nExecStart = modbus-proxy -c ./usr/lib/mproxy-conf.yaml\n\n[Install]\nWantedBy=multi-user.target\n```\n5. `run systemctl daemon-reload`\n6. `systemctl enable mproxy.service`\n7. `systemctl start mproxy.service`\n\nThe file names given here are examples, you can choose other names, if you wish.\n\n## Docker\n\nThis project ships with a basic [Dockerfile](./Dockerfile) which you can use\nas a base to launch modbus-proxy inside a docker container.\n\nFirst, build the docker image with:\n\n```bash\n$ docker build -t modbus-proxy .\n```\n\n\nTo bridge a single modbus device without needing a configuration file is\nstraight forward:\n\n```bash\n$ docker run -d -p 5020:502 modbus-proxy -b tcp://0:502 --modbus tcp://plc1.acme.org:502\n```\n\nNow you should be able to access your modbus device through the modbus-proxy by\nconnecting your client(s) to `<your-hostname/ip>:5020`.\n\nIf, instead, you want to use a configuration file, you must mount the file so\nit is visible by the container.\n\nAssuming you have prepared a `conf.yml` in the current directory:\n\n```yaml\ndevices:\n- modbus:\n url: plc1.acme.org:502\n listen:\n bind: 0:502\n```\n\nHere is an example of how to run the container:\n\n```bash\ndocker run -p 5020:502 -v $PWD/conf.yml:/config/modbus-proxy.yml modbus-proxy\n```\n\nBy default the Dockerfile will run `modbus-proxy -c /config/modbus-proxy.yml` so\nif your mounting that volume you don't need to pass any arguments.\n\nNote that for each modbus device you add in the configuration file you need\nto publish the corresponding bind port on the host\n(`-p <host port>:<container port>` argument).\n\n## Logging configuration\n\nLogging configuration can be added to the configuration file by adding a new `logging` keyword.\n\nThe logging configuration will be passed to\n[logging.config.dictConfig()](https://docs.python.org/library/logging.config.html#logging.config.dictConfig)\nso the file contents must obey the\n[Configuration dictionary schema](https://docs.python.org/library/logging.config.html#configuration-dictionary-schema).\n\nHere is a YAML example:\n\n```yaml\ndevices:\n- modbus:\n url: plc1.acme.org:502\n listen:\n bind: 0:9000\nlogging:\n version: 1\n formatters:\n standard:\n format: \"%(asctime)s %(levelname)8s %(name)s: %(message)s\"\n handlers:\n console:\n class: logging.StreamHandler\n formatter: standard\n root:\n handlers: ['console']\n level: DEBUG\n```\n\n### `--log-config-file` (deprecated)\n\nLogging configuration file.\n\nIf a relative path is given, it is relative to the current working directory.\n\nIf a `.conf` or `.ini` file is given, it is passed directly to\n[logging.config.fileConfig()](https://docs.python.org/library/logging.config.html#logging.config.fileConfig) so the file contents must\nobey the\n[Configuration file format](https://docs.python.org/library/logging.config.html#configuration-file-format).\n\nA simple logging configuration (also available at [log.conf](examples/log.conf))\nwhich mimics the default configuration looks like this:\n\n```toml\n[formatters]\nkeys=standard\n\n[handlers]\nkeys=console\n\n[loggers]\nkeys=root\n\n[formatter_standard]\nformat=%(asctime)s %(levelname)8s %(name)s: %(message)s\n\n[handler_console]\nclass=StreamHandler\nformatter=standard\n\n[logger_root]\nlevel=INFO\nhandlers=console\n```\n\nA more verbose example logging with a rotating file handler:\n[log-verbose.conf](examples/log-verbose.conf)\n\nThe same example above (also available at [log.yml](examples/log.yml)) can be achieved in YAML with:\n\n```yaml\nversion: 1\nformatters:\n standard:\n format: \"%(asctime)s %(levelname)8s %(name)s: %(message)s\"\nhandlers:\n console:\n class: logging.StreamHandler\n formatter: standard\nroot:\n handlers: ['console']\n level: DEBUG\n```\n\n\n## Credits\n\n### Development Lead\n\n* Tiago Coutinho <coutinhotiago@gmail.com>\n\n### Contributors\n\nNone yet. Why not be the first?\n\n[pypi-python-versions]: https://img.shields.io/pypi/pyversions/modbus-proxy.svg\n[pypi-version]: https://img.shields.io/pypi/v/modbus-proxy.svg\n[pypi-status]: https://img.shields.io/pypi/status/modbus-proxy.svg\n[license]: https://img.shields.io/pypi/l/modbus-proxy.svg\n[CI]: https://github.com/tiagocoutinho/modbus-proxy/actions/workflows/ci.yml/badge.svg\n\n## History\n\n### 0.6.1 (2021-09-29)\n\n* Change default command line `--modbus-connection-time` from 0.1 to 0\n* Add basic unit tests\n* Github actions\n* Repository cleanup\n\n### 0.5.0 (2021-09-28)\n\n* Add support for multiple devices\n* Adapt docker to changes\n* Deprecate `--log-config-file` command line parameter\n\n### 0.4.2 (2021-09-23)\n\n* Add connection time delay (fixes #4)\n\n### 0.4.1 (2021-01-26)\n\n* Logging improvements\n\n### 0.4.0 (2021-01-26)\n\n* Logging improvements\n\n### 0.3.0 (2021-01-25)\n\n* More robust server (fixes #2)\n\n### 0.2.0 (2021-01-23)\n\n* Document (README)\n* Add docker intructions (fixes #1)\n* Fix setup dependencies and meta data\n\n### 0.1.1 (2020-12-02)\n\n* Fix project package\n\n### 0.1.0 (2020-11-11)\n\n* First release on PyPI.\n",
"bugtrack_url": null,
"license": "GNU General Public License v3",
"summary": "ModBus TCP proxy",
"version": "0.8.0",
"project_urls": {
"Homepage": "https://github.com/tiagocoutinho/modbus-proxy"
},
"split_keywords": [
"modbus",
" tcp",
" proxy"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "16f61a17c0c20043199514fe3a9aad4e0329d6ba51752af1d28a2d3ddbc9a75d",
"md5": "4f98517753af115434468a0fdb4b014b",
"sha256": "55f01533d7591021205c61419b9b9ae9b2c926ec6bebd4e436797484f375ed03"
},
"downloads": -1,
"filename": "modbus_proxy-0.8.0-py2.py3-none-any.whl",
"has_sig": false,
"md5_digest": "4f98517753af115434468a0fdb4b014b",
"packagetype": "bdist_wheel",
"python_version": "py2.py3",
"requires_python": ">=3.9",
"size": 9605,
"upload_time": "2024-09-02T06:20:30",
"upload_time_iso_8601": "2024-09-02T06:20:30.162818Z",
"url": "https://files.pythonhosted.org/packages/16/f6/1a17c0c20043199514fe3a9aad4e0329d6ba51752af1d28a2d3ddbc9a75d/modbus_proxy-0.8.0-py2.py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "e81bc265f2f53cfedaeea4b13df5517da1c0556c29aaca7b150323e9e5561de6",
"md5": "1c76bcfae0354cb8fa128a2f6bdc510c",
"sha256": "97a6b89732a0d6c727045df968cffd4e6e21e63200e7eae2ea1db15dad6689e1"
},
"downloads": -1,
"filename": "modbus_proxy-0.8.0.tar.gz",
"has_sig": false,
"md5_digest": "1c76bcfae0354cb8fa128a2f6bdc510c",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.9",
"size": 13833,
"upload_time": "2024-09-02T06:20:16",
"upload_time_iso_8601": "2024-09-02T06:20:16.751301Z",
"url": "https://files.pythonhosted.org/packages/e8/1b/c265f2f53cfedaeea4b13df5517da1c0556c29aaca7b150323e9e5561de6/modbus_proxy-0.8.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-09-02 06:20:16",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "tiagocoutinho",
"github_project": "modbus-proxy",
"travis_ci": true,
"coveralls": false,
"github_actions": true,
"tox": true,
"lcname": "modbus-proxy"
}