ezvpn-fld-modbus is a service (provided as Docker container image) that acts as a broker between multiple Modbus RTU/TCP slave devices and one MQTT broker, which:

  • reads data from Modbus slave devices and publishes it to MQTT topics
  • subscribes to MQTT topics and writes incoming messages to Modbus slaves

Modbus Container Schema

By using ezvpn-fld-modbus, you can:

  • write to your Modbus devices by sending text messages to MQTT.
  • read from your devices by subscribing to MQTT topics.

This service is not a typical full-fledged Modbus driver. Modbus, despite being a very old protocol, is still widely adopted on PLCs and IoT devices. However, it is a "byte-oriented" protocol, whose primary purpose is to transfer bytes (or words) from/to a PLC/device while data interpretation is left to the programmer. No security feature is implemented in Modbus, and no "subscription" mechanism is available in the protocol.

The implementation in ezvpn-fld-modbus is high-level. We do not implement a low-level Modbus function.

We are abstracting from the Modbus layer through the implementations described below, making it a "modern" protocol implementation.

Additional features over Modbus protocol

Logical virtual namespace

Instead of using standard Modbus addressing (M1, MW10, I0.1, Q2.3, ...), all Modbus devices and all addresses used for communications are given a symbolic name (inlet temperature, extruder pressure, linear position, ...).

By using symbolic addressing, you can automatically access a network of Modbus devices just by address name. The mapping from the symbolic name and the physical location (plc and address) is managed automatically.

Subscription and polling

Modbus does not provide any mechanism for value change notifications or values polling while ezvpn-fld-modbus provides both. You can define different subscriptions (to get notification upon value change) or pollings (to get all values periodically) involving addresses, even if it belongs to different PLC's.

For example, you may create polling for all temperatures (even from different machines) every 30 seconds while defining a subscription for all pressure transducers and not only changes.

Semantic data interpretation

Modbus is a byte protocol. There is no interpretation attached to the bytes/words transferred to and from the device. You may want to store integers, long integers, floats, long floats, signed, unsigned, ... and on different PLC CPU architectures (Little Endian, Big Endian).

ezvpn-fld-modbus provides an automatic translation to your desired data type, managing the number of bytes to read, as well as the required transformation.

Basic security

Modbus was not designed with any security/ACL check. If you have a Modbus plc, you are exposing the full memory address set to any user. As a PLC designer, whenever you expose the full address map to your users or to system integrators, you trust them for not disrupting your application logic. However, this approach is risky; a simple software bug while reading/writing from/to the PLC may disrupt the full application and potentially endanger the machine users.

To avoid these issues, an additional security layer has been implemented. Only the addresses declared as writable will be enabled to receive new values. In this way, your device/machinery is always safe, and your program logic benefits from these additional safety checks.

How to use it

ezvpn-fld-modbus is a Docker container image pre-configured to communicate with ezvpn-mqtt. In case you need to use it with your MQTT broker, please see the section below related to customization.

You can use ezvpn-fld-modbus directly in IOhubTM for final production, or on your PC for testing purposes.

The Docker image is provided as follows, with the description of each environment variable used by the container.

If used with ezvpn-mqtt, only one environment variable must be provided: FLD_CFG.

FLD_CFG is a string representing a JSON object, describing the Modbus devices' configuration, addresses, polling, subscription, writable addresses.

Below is a working example representing an oversimplified case with:

  • two machines (extruder and pelletizer)
  • two thermocouples (one on the extruder tempextruder, one on the pelletizer tempdie), read-only
  • one pressure sensor (pressure), read-only
  • one temperature setpoint (tempSP), read-write
    "plcs": [
            "name": "extruder",
            "ip": "",
            "port": 502
            "name": "pelletizer",
            "device": "/dev/ttyUSB0",
            "baudRate": "9600",
            "dataBits": 8,
            "stopBits": 1,
            "parity": "none",
            "slave": 1
        }    ],
    "addresses": [
            "name": "tempextruder",
            "target": "extruder",
            "type": "32unsignedintLE",
            "address": 150
            "name": "tempdie",
            "target": "pelletizer",
            "type": "16signedintLE",
            "address": 10
            "name": "pressure",
            "target": "pelletizer",
            "type": "32signedintLE",
            "address": 80
            "name": "tempSP",
            "target": "extruder",
            "type": "32unsignedintLE",
            "address": 75
    "pollings": [
            "cron": "0 */10 * * * *",
            "addresses": ["tempextruder", "tempdie", "tempSP"]
            "every": "1s 500ms",
            "addresses": ["pressure"]
    "writables": ["tempSP"]

Mapping configuration (FLD_CFG)

the JSON object has 4 array fields:

    "plcs": [],
    "addresses": [],
    "pollings": [],
    "writables": []

plcs (required): contains the list of the available Modbus slaves.

addresses (required): contains the list of the available symbolic addresses.

pollings (required): contains the list of the polling cycles.

writables (optional): contains the list of the available addresses (each address must be defined in the addresses section).


plcs is an array of objects, representing the available Modbus slave devices. You can define Modbus TCP/RTU device. The plcs array cannot be empty.

Each TCP PLC is represented by the following object:

    "name": "<plc name>",
    "ip": "<plc address or hostname>",
    "port": <plc port>,
    "slave": <optional slave number>


name and ip are required.

port and slave are optional. If not defined, port defaults to 502.

Each RTU PLC is represented by the following object:

    "name": "<plc name>",
    "device": "<USB serial device>",
    "baudRate": "<baudrate>",
    "dataBits": <data bits>,
    "stopBits": <stop bits>,
    "parity": "<parity>",
    "slave": <slave number>


name is required.

device is required and it represents the serial device (usually mapping a USB/Serial RS485 adapter) as seen by the application inside the container. Valid values are /dev/ttyUSB0, /dev/ttyUSB1, ..., /dev/ttyUSB<n>.

The number of available devices depends on the hardware device in use (e.g. the X1 model has 4 usb ports and therefore it has available /dev/ttyUSB0 to /dev/ttyUSB3).

The device name, as available in the Modbus container, is managed by IOhubTM and it is not equal to the name of the real device in the Docker host.

The mapping between the real device name and the name available in the container is defined in the management web site. Please check the USB devices section.

baudRate valid values are 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200. Defaults to 9600.

dataBits valid values are 7 or 8. Defaults to 8.

stopBits valid values are 0 or 1. Defaults to 1.

parity valid values are none, even, mark, odd, space. Defaults to none.

slave valid values are 0 to 255.


addresses is an array of objects, representing the available Modbus slave devices. The addresses array cannot be empty.

Each address, if not in the writables array (see below), is treated as read-only. Every attempt to send values to read-only addresses will silently fail.

Each address is represented by the following object:

    "name": "<symbolic address name>",
    "target": "<a PLC defined in the plcs section>",
    "type": "<a data type>",
    "address": <Modbus address>,
    "len": <optional, number of consecutive words>

len is optional, while all the other properties are required. If absent, only one address is read. len can be used only with the basic data types (see below). The max allowed value of len is 125.

type can be one of the basic Modbus data types or an interpolated data type.

Basic data types
  • discreteinput: (digital input, as defined in Modbus, e.g. I0.2)
  • coil: (digital output, as defined in Modbus, e.g. Q0.3)
  • holdingregister: (internal word register, as defined in Modbus, e.g. MW12)
  • inputregister (analog input, as defined in Modbus, e.g. IA12)

Very few PLCs allow access to discrete inputs, coils, and input registers. The standard and suggested way to access PLC addresses is through holding registers, always available. If you are not able to access the other address types, it is probably caused by this common limitation in your plc.

Interpolated data types

This list of non-native Modbus data types was created for your convenience, giving an interpretation to the bytes read.

All the names ending in LE are interpreted as Little Endian. If they end in BE, they are interpreted as Big Endian. If you do not know your PLC architecture, it will most likely be Little Endian. Big Endian is not very common in PLCs or in computers.

  • 16signedintLE: 2 bytes, interpreted as signed integer
  • 16unsignedintLE: 2 bytes, interpreted as unsigned integer
  • 32signedintLE: 4 bytes, interpreted as signed integer
  • 32unsignedintLE: 4 bytes, interpreted as unsigned integer
  • 32floatLE: 4 bytes, interpreted as real number
  • 64floatLE: 8 bytes, interpreted as real number
  • 16signedintBE: 2 bytes, interpreted as signed integer
  • 16unsignedintBE: 2 bytes, interpreted as unsigned integer
  • 32signedintBE: 4 bytes, interpreted as signed integer
  • 32unsignedintBE: 4 bytes, interpreted as unsigned integer
  • 32floatBE: 4 bytes, interpreted as real number
  • 64floatBE: 8 bytes, interpreted as real number


pollings represents a list of polling cycles on the Modbus devices. The pollings array cannot be empty.

Each polling has its independent polling frequency and its set of polled addresses. The addresses can be used in more than one polling and each polling can involve addresses on different PLCs.

This is the structure of a polling object:

    "every": "<frequency>",
    "cron": "<frequency time>",
    "timezone": "<timezone>",
    "addresses": [<address list>],
    "sendAlways": <polling or subscription>,
    "sendBatch": <batch or single>,
    "disableSingle": <send or not single measurements>

The data reading frequency is specified using the alternative every or cron syntax.

every or cron cannot both be defined in the same polling definition, choose which type of polling frequency should be used.

  • every can be supplied in : human readable time format

  • cron. can be supplied in : cron time format

  • timezone: an optional timezone, see the list, to use for the cron expression. If not defined UTC is used.

  • addresses is an array of address names, in string format

  • sendAlways is a boolean optional property. If absent or false, only changed values (from previous polling cycles) are sent to MQTT. Otherwise, all the values are always sent.

At the first polling cycle, all the values are sent, even if sendAlways is false. As Modbus does not provide any subscription mechanism, ezvpn-fld-modbus is always polling the devices while keeping a cache of the last read values. On top of that, a simulated subscription mechanism is provided.

sendBatch is a boolean optional property. Defaults to false. If true, all the measurements are sent in aggregated form to the MQTT topic specified on FROM_DEVICE_AGGREGATED_TOPIC_PREFIX environment variable.

The payload is a JSON with 2 properties on default topic aggregate/<protocol>:

    "ts": 1612043704952,
    "data": [
        { "address": "tempextruder", "value": 115, "ts": 1612043702457 },
        { "address": "tempdie", "value": 120, "ts": 1612043702457 }
  • ts value is the time at which the payload has been sent.
  • data is an array of all the measures to be sent on each polling cycle. Each array of data is a measurement as sent in single form.

The sendAlways setting affects both the single data and the aggregated data.

disableSingle is a boolean optional property. Defaults to false. If true the measurements are not sent to the MQTT topic specified on TO_DEVICE_TOPIC_PREFIX environment variable.


An array of address names, in string format. Each address in the list can be written.

Environment variables

When you start the ezvpn-fld-modbus image, you can adjust the instance's configuration by passing one or more environment variables to the docker run command.

  • FLD_CFG: Modbus mapping description, as documented above. Required.
  • MQTT_HOST: ip address / host name of the MQTT broker. Defaults to (In IOhubTM the variable is set to ezpn-mqtt value)
  • MQTT_PORT: MQTT broker port. Defaults to 1883.
  • FROM_DEVICE_TOPIC_PREFIX: MQTT topic used to publish values read from devices, defaults to fld/modbus/r/.
  • TO_DEVICE_TOPIC_PREFIX: MQTT topic subscribed to get values to write on devices, defaults to fld/modbus/w/.
  • FROM_DEVICE_AGGREGATED_TOPIC_PREFIX: MQTT topic use to publish values read from devices, defaults to aggregate/modbus. Used if sendBatch is set to true.
  • NO_RETAIN: if true values are sent to MQTT_HOST without the retain flag; defaults to false.

With NO_RETAIN set to false, each client that subscribes to a topic pattern receives the retained message immediately after they subscribe.

The broker stores only one retained message per topic.

Examples in EXCH

Read a PLC address

Read an address from the PLC as a 16-bit value and write its value to the key-value DB as Celsius temperature

writeField "db-key-value" "temp" call(f_to_c, ${_v})

Write a value to the PLC

Write a 0-1000 Celsius temperature from the key-value DB to a PLC address, as a 16bit value.

writeField "modbus" "setpoint" call(scale, 0, 1000, 0, 65535, db-key-value/temp)

Docker Container details


Supported architecture: amd64


  • Parallel writes fix
  • Coil writable
  • Timezone added in cron expression
  • Smaller image
  • Cron expression added
  • FROM_DEVICE_AGGREGATED_TOPIC_PREFIX Environment variable added
  • NO_RETAIN Environment variable added
  • RTU version added
  • Internal refactoring
  • New types added:
    • 16signedintLE
    • 16unsignedintLE
    • 16signedintBE
    • 16unsignedintBE
  • First Release