# Modbus/TCP

Modbus/TCP is a communication protocol that is a variant of the Modbus family. It is based on a client/server architecture and intended for use in industrial automation, especially with PLCs or I/O modules. Modbus/TCP is defined in IEC 61158.

Modbus uses big-endian byte order for addresses and data items.

## Protocol Structure

Each Modbus/TCP frame consists of the following fields:

| Transaction identifier | Protocol identifier    | Length field | Unit identifier | Function code | Data   |
| ---------------------- | ---------------------- | ------------ | --------------- | ------------- | ------ |
| 2 byte                 | 2 byte (always 0x0000) | 2 byte (n+2) | 1 byte          | 1 byte        | n byte |

For configuration reference, see:

* [Connection Properties](https://docs.cybus.io/connectors/shop-floor-connectors/modbus-tcp/modbusconnection)
* [Endpoint Properties](https://docs.cybus.io/connectors/shop-floor-connectors/modbus-tcp/modbusendpoint)

## Supported Function Codes

For reading or writing data over Modbus/TCP, the protocol provides a set of functions. Which action should be performed on the other end of the connection is defined by the function code (FC).

The following function codes are supported:

| Function code | Action                           | Operation |
| ------------- | -------------------------------- | --------- |
| 1             | Read coils                       | subscribe |
| 2             | Read discrete inputs             | subscribe |
| 3             | Read holding registers           | subscribe |
| 4             | Read input registers             | subscribe |
| 5             | Write single coil                | write     |
| 6             | Write single holding register    | write     |
| 15            | Write multiple coils             | write     |
| 16            | Write multiple holding registers | write     |

## Supported Data Types

In Modbus/TCP there are no predefined data types. Connectware supports sending the raw payload over MQTT as binary. If the data type is known and supported by the respective function code, the payload is converted to JSON format.

The desired data type on read or on write is specified with the property `dataType`. The possible values are as follows:

| DataType/FC | 1 | 2 | 3 | 4 | Size     |
| ----------- | - | - | - | - | -------- |
| raw         | ✓ | ✓ | ✓ | ✓ | variable |
| boolean     | ✓ | ✓ | x | x | 1 bit    |
| base64      | x | x | ✓ | ✓ | variable |
| int16BE     | x | x | ✓ | ✓ | 16 bit   |
| int16LE     | x | x | ✓ | ✓ | 16 bit   |
| int32BE     | x | x | ✓ | ✓ | 32 bit   |
| int32LE     | x | x | ✓ | ✓ | 32 bit   |
| uint16BE    | x | x | ✓ | ✓ | 16 bit   |
| uint16LE    | x | x | ✓ | ✓ | 16 bit   |
| uint32BE    | x | x | ✓ | ✓ | 32 bit   |
| uint32LE    | x | x | ✓ | ✓ | 32 bit   |
| floatBE     | x | x | ✓ | ✓ | 32 bit   |
| floatLE     | x | x | ✓ | ✓ | 32 bit   |
| doubleBE    | x | x | ✓ | ✓ | 64 bit   |
| doubleLE    | x | x | ✓ | ✓ | 64 bit   |
| bigUInt64BE | x | x | ✓ | ✓ | 64 bit   |
| bigUInt64LE | x | x | ✓ | ✓ | 64 bit   |

If the payload is smaller than the required bits for the conversion (`payload.length < dataType.length`), an error is displayed in the logs and no message is sent over MQTT. If the payload has more bits (`payload.length > dataType.length`) than needed for the conversion, the extra bits are ignored and a message is sent over MQTT.

The following table maps Connectware data types to common names used in other documentation:

| Common type name | Size   | Minimum value | Maximum value |
| ---------------- | ------ | ------------- | ------------- |
| char             | 8 bit  | 0             | 255           |
| byte             | 8 bit  | -128          | 127           |
| short (int16)    | 16 bit | -2^15         | 2^15-1        |
| int (int32)      | 32 bit | -2^31         | 2^31-1        |
| uint (uint32)    | 32 bit | 0             | 2^32-1        |
| long64           | 64 bit | -2^63         | 2^63-1        |
| float            | 32 bit | IEEE 754      | IEEE 754      |
| double           | 64 bit | IEEE 754      | IEEE 754      |

## Input Format

This Modbus implementation supports writing using the standard function codes 5, 6, 15 and 16. In most cases you can directly send a number in the message payload when you are trying to write to a coil or a single register (function codes 5 or 6, respectively).

When trying to write several coils at once (function code 15) your data message's `value` must consist of an array of boolean values, for example: `[true, true, false, true]`.

Alternatively, if your goal is to write several registers at once, the data type of the endpoint needs to be considered. The property `dataType` (see [Endpoint properties](https://docs.cybus.io/documentation/services/service-commissioning-files/resources/cybus-endpoint#properties)) specifies how to properly serialize the data into an array of bytes suitable to write into the Modbus registers.

For the available integer and float data types (see [Supported Data Types](#supported-data-types) above) you can directly send the value in the message payload. A `BigInt` must be sent as a string value. You can also send base64 and utf8 encoded strings that will be parsed using the Node.js Buffer class.

Raw data in the form of an array of bytes is also supported.

### Input Format on Write

For writing data to Modbus, a message needs to be published to the `/set` topic of the endpoint with the following properties:

{% code lineNumbers="true" %}

```yaml
{ 'fc': '<function code value>', 'dataType': '<one of the Modbus data types>', 'data': '<the data to be written>' }
```

{% endcode %}

### Output Format on Write

Results are published to the `/res` topic of the endpoint. The format depends on the Modbus function being called and is returned as a JSON object.

## Output Format

If data is read from Modbus and `dataType` is set to any of the supported values

### Output Format on Read

When data is read from the endpoint, results are published to the `/res` topic of the endpoint. The output message is an object with two properties:

{% code lineNumbers="true" %}

```yaml
{ 'timestamp': '<unix timestamp in ms>', 'value': '<value>' }
```

{% endcode %}

The value of the `value` property will be the JSON representation of the configured `dataType`, i.e. a number, or a string, or for raw the JSON representation of a JavaScript buffer object.

Alternatively, the property `dataType` can be left undefined, in which case the output is a JavaScript buffer object directly. This is useful for further processing using suitable [mappings](https://docs.cybus.io/documentation/services/service-commissioning-files/resources/cybus-mapping).

## Batch Read Processing

By default, Connectware reads each Modbus endpoint individually, sending one request per endpoint to the device. In deployments with many endpoints, this can result in slow read cycles, especially when the device or network introduces even small delays with each request.

Batch read processing improves this by combining multiple read requests into a single, larger one wherever possible. Connectware automatically identifies which endpoints can be batched — those sharing the same unit ID and function code with addresses close enough to fit within a single read range. This reduces the total number of requests per cycle and can significantly shorten read times for large configurations.

{% hint style="info" %}
No changes to endpoint definitions are required when enabling batch read processing. Connectware automatically determines which endpoints can be grouped based on their address, unit ID, and function code.

Endpoints configured with a cron schedule are not included in the batch scan cycle. They continue to poll individually as usual.
{% endhint %}

### Enabling Batch Read Processing

Enable batch processing by setting `batchReadEnabled` and `maxBatchSize` on the Modbus connection resource. For more information, see [Connection Properties](https://docs.cybus.io/connectors/shop-floor-connectors/modbus-tcp/modbusconnection).

**Example**

The following example enables batch processing on a Modbus connection:

{% code title="modbus-batch-example.yml" lineNumbers="true" %}

```yaml
modbusConnection:
  type: Cybus::Connection
  properties:
    protocol: Modbus
    connection:
      host: !ref IP_Address
      port: !ref Port_Number
      batchReadEnabled: true
      maxBatchSize: 125
```

{% endcode %}

## Service Commissioning File Example

The following example demonstrates how to configure a simple Modbus connection and endpoint that reads holding registers (FC 3) from the device.

{% file src="<https://2355450750-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FGzXPesVecsUM1eHBfwea%2Fuploads%2Fgit-blob-7c5bdb78b5eee761d47188955accb8788ad79d21%2Fmodbus-example.yml?alt=media>" %}

{% code title="modbus-example.yml" lineNumbers="true" %}

```yaml
# ----------------------------------------------------------------------------#
# Commissioning File
# ----------------------------------------------------------------------------#
# Copyright: Cybus GmbH
# Contact: support@cybus.io
# ----------------------------------------------------------------------------#
# Source Interface Definition - Modbus TCP
# ----------------------------------------------------------------------------#

description: |
  Sample commissioning file for Modbus TCP protocol connectivity and data mapping

metadata:
  name: Modbus TCP Protocol Connectivity
  icon: https://www.cybus.io/wp-content/uploads/2019/03/Cybus-logo-Claim-lang.svg
  provider: cybus
  homepage: https://www.cybus.io
  version: 0.0.1

parameters:
  IP_Address:
    type: string
    default: 192.168.10.30

  Port_Number:
    type: number
    default: 502

  initialReconnectDelay:
    type: integer
    default: 1000

  maxReconnectDelay:
    type: integer
    default: 30000

  factorReconnectDelay:
    type: integer
    default: 2

resources:
  modbusConnection:
    type: Cybus::Connection
    properties:
      protocol: Modbus
      targetState: connected
      connection:
        host: !ref IP_Address
        port: !ref Port_Number
        connectionStrategy:
          initialDelay: !ref initialReconnectDelay
          maxDelay: !ref maxReconnectDelay
          incrementFactor: !ref factorReconnectDelay

  readCoil:
    type: Cybus::Endpoint
    properties:
      protocol: Modbus
      connection: !ref modbusConnection
      subscribe:
        fc: 1
        length: 2
        interval: 1000
        address: 3
        dataType: boolean

  writeCoil:
    type: Cybus::Endpoint
    properties:
      protocol: Modbus
      connection: !ref modbusConnection
      write:
        fc: 5
        length: 2
        address: 3
        dataType: boolean

  readRegister:
    type: Cybus::Endpoint
    properties:
      protocol: Modbus
      connection: !ref modbusConnection
      subscribe:
        fc: 3
        length: 2
        interval: 1000
        address: 1
        dataType: int16BE

  writeRegister:
    type: Cybus::Endpoint
    properties:
      protocol: Modbus
      connection: !ref modbusConnection
      write:
        fc: 6
        length: 2
        address: 1
        dataType: int16BE

  mapping:
    type: Cybus::Mapping
    properties:
      mappings:
        - subscribe:
            endpoint: !ref readCoil
          publish:
            topic: !sub '${Cybus::MqttRoot}/read/coil'
        - subscribe:
            topic: !sub '${Cybus::MqttRoot}/write/coil'
          publish:
            endpoint: !ref writeCoil
        - subscribe:
            endpoint: !ref readRegister
          publish:
            topic: !sub '${Cybus::MqttRoot}/read/register'
        - subscribe:
            topic: !sub '${Cybus::MqttRoot}/write/register'
          publish:
            endpoint: !ref writeRegister
```

{% endcode %}
