2. Configuration

This section describes how to configure Routing Service Modbus Adapter.

All configuration is specified in Routing Service’s XML configuration file.

2.1. Load the Modbus Adapter Plugin

Routing Service Modbus Adapter must be registered as a Routing Service plugin by using the <adapter_plugin> tag.

The following snippet demonstrates how to register the plugin in the <plugin_library> section of Routing Service’s XML configuration:

<?xml version="1.0"?>
<dds>
    <plugin_library name="MyPlugins">
        <adapter_plugin name="ModbusAdapter">
            <dll>rtimodbusadapter</dll>
            <create_function>
                ModbusAdapter_create_adapter_plugin
            </create_function>
        </adapter_plugin>
    </plugin_library>
</dds>

Warning

Routing Service must be able to find the Routing Service Modbus Adapter dynamic library (librtimodbusadapter.so on Linux® systems, librtimodbusadapter.dylib on macOS® systems, or rtimodbusadapter.dll on Windows® systems). Make sure to include the library’s directory in the library search path environment variable appropriate for your system (LD_LIBRARY_PATH on Linux systems, RTI_LD_LIBRARY_PATH on macOS systems, or PATH on Windows systems, etc.).

Once the dynamic library and constructor function have been registered, Routing Service will create an instance of the plugin during start-up, and you can use the plugin to create one or more connections to Modbus Devices.

2.2. Modbus Connection

2.2.1. Configuration

Once the plugin has been registered with Routing Service, you can use it to create <connection> elements within a <domain_route>.

A Connection identifies a Modbus Device, therefore you have to specify the Modbus Device’s IP and port. In order to do that the <connection>’s configuration must include the properties modbus_server_ip and modbus_server_port to configure the associated Modbus Device.

Optionally, you can also set a response timeout for a specific Modbus connection by setting the property modbus_response_timeout_msec. If not set, it will use the default value from libmodbus.

The following snippet shows an example <connection> that connects the adapter to a local Modbus Device:

<?xml version="1.0"?>
<dds>
    <routing_service>
        <domain_route>
            <connection name="CO2_Device_1" plugin_name="AdapterLib::ModbusAdapter">
            <property>
                <value>
                    <element>
                        <name>modbus_server_ip</name>
                        <value>127.0.0.1</value>
                    </element>
                    <element>
                        <name>modbus_server_port</name>
                        <value>1502</value>
                    </element>
                    <element>
                        <name>modbus_response_timeout_msec</name>
                        <value>5000</value>
                    </element>
                </value>
            </property>
            <registered_type name="MBus_WTH_CO2_LCD_ETH_WRITE" type_name="MBus_WTH_CO2_LCD_ETH_WRITE" />
            <registered_type name="MBus_WTH_CO2_LCD_ETH_INPUT" type_name="MBus_WTH_CO2_LCD_ETH_INPUT" />
        </connection>
        </domain_route>
    </routing_service>
</dds>

The Modbus connection maps to a (TCP) connection to the Modbus service. It maps to what typical Modbus client libraries do when they connect to a Modbus server. For example, see modbus_connect and modbus_new_tcp.

2.3. Modbus Input/Output

This adapter allows you to write or read multiple Modbus registers or coils using the same input/output.

2.3.1. Modbus Output (Routing Data from DDS to Modbus)

2.3.1.1. Concept

A single Modbus output can write multiple Modbus holding registers and/or coils within the same Modbus Device. Therefore, the configuration must provide a way to associate each of the DynamicData fields with the Modbus register/coil where it should be written. This is basically an array, where each element is a tuple consisting of:

  • The modbus_datatype (COIL, INT16, etc.)

  • The modbus_register_address

    • Optionally, a modbus_register_count starting from that address

  • The DynamicData member that is copied into the Modbus register/coil

  • Optionally, in order to support linear transformations:

    • ouput_data_factor

    • output_data_offset

  • Optionally, in order to ensure written values are within range for the Modbus server:

    • modbus_min_value, modbus_max_value

    • modbus_valid_values

The Routing Service Modbus Adapter allows you to write/read multiple registers by using the JSON syntax, either in-line or in a separate file.

2.3.1.2. Example

Consider the following custom data-type, which will hold the information to be written to each Modbus Device.

This example shows the configuration for DataNab MBus_WTH_CO2_LCD_ETH Modbus Device. The data-type is defined as follows:

enum AlarmControlSetting {
    @value(0)   AUTO,
    @value(128) OFF,
    @value(129) FORCE_PRE_ALARM,
    @value(130) FORCE_CONTINUOUS_ALARM
};

enum LCDTemperatureUnit {
    @value(0)   CELSIUS,
    @value(1)   FAHRENHEIT
};

@mutable
struct MBus_WTH_CO2_LCD_ETH_WRITE {
    @optional LCDTemperatureUnit  lcd_units_to_display;
    @optional int16 co2_calibration_offset;
    @optional int16 setpoint_for_co2_pre_alarm;
    @optional int16 setpoint_for_co2_continuous_alarm;
    @optional AlarmControlSetting alarm_control;
    @optional uint16 alarm_sound_on_sec_for_prealarm;
    @optional uint16 alarm_sound_off_sec_for_prealarm;
    uint8  gateway_ip[4];
    uint8  subnet_mask[4];
    uint8  ip_address[4];
};

Then you can use this data-type to write one or more registers in the same Modbus server with a single sample.

To configure this output, use the following JSON configuration:

[
    {
        "field": "lcd_units_to_display",
        "modbus_register_address": 201,
        "modbus_datatype": "HOLDING_REGISTER_INT8",
        "modbus_valid_values": [
            0,
            1
        ]
    },
    {
        "field": "co2_calibration_offset",
        "modbus_register_address": 212,
        "modbus_datatype": "HOLDING_REGISTER_INT16",
        "modbus_min_value": -1000,
        "modbus_max_value": 1000
    },
    {
        "field": "setpoint_for_co2_pre_alarm",
        "modbus_register_address": 213,
        "modbus_datatype": "HOLDING_REGISTER_INT16",
        "modbus_min_value": 0,
        "modbus_max_value": 2000
    },
    {
        "field": "setpoint_for_co2_continuous_alarm",
        "modbus_register_address": 214,
        "modbus_datatype": "HOLDING_REGISTER_INT16",
        "modbus_min_value": 0,
        "modbus_max_value": 2000
    },
    {
        "field": "alarm_control",
        "modbus_register_address": 1246,
        "modbus_datatype": "HOLDING_REGISTER_INT8",
        "modbus_valid_values": [
            0,
            128,
            129,
            130
        ]
    },
    {
        "field": "alarm_sound_on_sec_for_prealarm",
        "modbus_register_address": 1247,
        "modbus_datatype": "HOLDING_REGISTER_INT16",
        "modbus_min_value": 0,
        "modbus_max_value": 20
    },
    {
        "field": "alarm_sound_off_sec_for_prealarm",
        "modbus_register_address": 1248,
        "modbus_datatype": "HOLDING_REGISTER_INT16",
        "modbus_min_value": 0,
        "modbus_max_value": 20
    },
    {
        "field": "ip_address",
        "modbus_register_address": 107,
        "modbus_register_count": 4,
        "modbus_datatype": "HOLDING_REGISTER_INT8",
        "modbus_min_value": 0,
        "modbus_max_value": 255
    },
    {
        "field": "subnet_mask",
        "modbus_register_address": 111,
        "modbus_register_count": 4,
        "modbus_datatype": "HOLDING_REGISTER_INT8",
        "modbus_min_value": 0,
        "modbus_max_value": 255
    },
    {
        "field": "gateway_ip",
        "modbus_register_address": 115,
        "modbus_register_count": 4,
        "modbus_datatype": "HOLDING_REGISTER_INT8",
        "modbus_min_value": 0,
        "modbus_max_value": 255
    }
]

The output will be configured as follows:

<session name="session">
    <route>
        <route_types>true</route_types>
        <dds_input participant="ModbusParticipant">
            <datareader_qos base_name="AdapterLibrary::ModbusProfile" />
            <topic_name>MBus_WTH_CO2_LCD_ETH_WRITE StreamWriter</topic_name>
            <registered_type_name>MBus_WTH_CO2_LCD_ETH_WRITE</registered_type_name>
        </dds_input>
        <output connection="CO2_Device_1">
            <registered_type_name>MBus_WTH_CO2_LCD_ETH_WRITE</registered_type_name>
            <stream_name>ModbusDevice1_configuration</stream_name>
            <property>
                <value>
                    <element>
                        <name>configuration_file_json</name>
                        <value>output_config.json</value>
                    </element>
                </value>
            </property>
        </output>
    </route>
</session>

Or you can provide the JSON configuration in-line, like this:

<session name="session">
    <route>
        <route_types>true</route_types>
        <dds_input participant="ModbusParticipant">
            <datareader_qos base_name="AdapterLibrary::ModbusProfile" />
            <topic_name>MBus_WTH_CO2_LCD_ETH_WRITE StreamWriter</topic_name>
            <registered_type_name>MBus_WTH_CO2_LCD_ETH_WRITE</registered_type_name>
        </dds_input>
        <output connection="CO2_Device_1">
            <registered_type_name>MBus_WTH_CO2_LCD_ETH_WRITE</registered_type_name>
            <stream_name>ModbusDevice1_configuration</stream_name>
            <property>
                <value>
                    <element>
                        <name>configuration_string_json</name>
                        <value>
                            [
                                {
                                "field": "lcd_units_to_display",
                                "modbus_register_address": 201,
                                "modbus_datatype": "HOLDING_REGISTER_8",
                                "modbus_valid_values": [0, 1]
                                }
                            ]
                        </value>
                    </element>
                </value>
            </property>
        </output>
    </route>
</session>

2.3.1.3. Output Configuration Attributes

The configuration is a JSON array. Each element contains the attributes described in the table below:

Table 2.1 Output Configuration Attributes

Attribute name

Required

JSON type

Meaning

field

YES

string

Member Name on the associated DynamicData.
The name can use the hierarchical naming format to indicate access to nested fields.

modbus_register_address

YES

integer

The register address in the Modbus Server. This is zero-based, one less than the register number.

modbus_datatype

YES

string

Can be

  • COIL_BOOLEAN

  • HOLDING_REGISTER_INT8

  • HOLDING_REGISTER_INT16

  • HOLDING_REGISTER_INT32

  • HOLDING_REGISTER_INT64

  • HOLDING_REGISTER_FLOAT_ABCD

  • HOLDING_REGISTER_FLOAT_BADC

  • HOLDING_REGISTER_FLOAT_CDAB

  • HOLDING_REGISTER_FLOAT_DCBA

The Modbus function used will be adjusted automatically based on the data-type.
See Access to the Modbus Server

modbus_register_count

NO

integer

The number of registers to write.

If unspecified: it defaults to one for COIL, INT8, INT16, to two for INT32 and FLOAT_XXXX, and to four for INT64.
If specified: the user should take into account that it takes two registers to read each INT32 and FLOAT_XXXX value, and it takes four registers to read an INT64.

modbus_slave_device_id

NO

number

Slave number of a Modbus device on a serial network. The value of 0 represents a broadcast address.

modbus_min_value

NO

number

The minimum value for the value written

modbus_max_value

NO

number

The maximum value for the value written

modbus_valid_values

YES for Enums, NO for other datatypes

array of numbers

The complete list of values that could be written. Values not in the list are considered invalid outputs.

ouput_data_factor

NO

number

The value in the DynamicData field is multiplied by this factor.
The data  finally written to Modbus is:

  • output_data_offset + output_data_factor * <value>

output_data_offset

NO

number

The offset is added to the value before writing it to Modbus.
The data finally written to Modbus is:

  • output_data_offset + output_data_factor * <value>

2.3.1.4. Behavior

The information provided with the above parameters will be used when creating the DynamicData and when writing to the Modbus Device. If any of the configuration parameters provided are wrong or the data that they define do not accomplish these restrictions (e.g., modbus_min_value), an error will be thrown.

For each element defined in the JSON configuration array, the Routing Service Modbus Adapter will check that:

  • The member exists.

  • The associated type is compatible with the Modbus data-type.

Each instruction results in a synchronous call to the Modbus client API to write the corresponding data into the Modbus Device. The following checks are performed:

  • The value is between modbus_min_value and modbus_max_value (if provided).

  • The value belongs to the values in modbus_valid_values (if provided).

If these checks pass, modbus_register_count registers will be written to the Modbus Device with the value or values (if it is an array or sequence) applying linear transformation as follows:

  • input_data_offset + input_data_factor * <value>

Note

If the DynamicData type is an array or a sequence, the linear transformation applies to all of them.

2.3.1.4.1. Access to the Modbus Server

There are four different functions that can be used to write Modbus coils and registers. They are distinguished by the Modbus function code. The function code used is controlled by the setting of modbus_datatype as seen in the table below:

Table 2.2 Access to the Modbus Server

modbus_datatype

modbus_register_count

Modbus function code

libmodbus function

  • COIL_BOOLEAN

1

0x05 (force single coil)

modbus_write_bit

  • COIL_BOOLEAN

>=2

0x0F (force multiple coils)

modbus_write_bits

  • HOLDING_REGISTER_INT8

  • HOLDING_REGISTER_INT16

1

0x06 (preset single register)

modbus_write_register

  • HOLDING_REGISTER_INT8

  • HOLDING_REGISTER_INT16

>=2

0x10 (preset multiple registers)

modbus_write_registers

  • HOLDING_REGISTER_INT32

  • HOLDING_REGISTER_INT64

  • HOLDING_REGISTER_FLOAT_ABCD

  • HOLDING_REGISTER_FLOAT_BADC

  • HOLDING_REGISTER_FLOAT_CDAB

  • HOLDING_REGISTER_FLOAT_DCBA

any

0x10 (preset multiple registers)

modbus_write_registers

2.3.1.4.2. Conversion from DynamicData Fields

Data is written to Modbus according to the type of the DynamicData field and the modbus_datatype.

Conversion from DynamicData fields to Modbus registers

Table 2.3 Conversion from DynamicData Fields

DynamicData field

modbus_datatype

Number of (16-bit) Modbus registers written

BOOL

COIL_BOOLEAN

1
(writes coils, not registers)

Array or Sequence of:
BOOL

COIL_BOOLEAN

modbus_register_count
(writes coils, not registers)

INT8
UINT8
ENUM literals fitting into its type

HOLDING_REGISTER_INT8

1

Array or Sequence of:
INT8
UINT8
ENUM with all literals fitting into its type

HOLDING_REGISTER_INT8

modbus_register_count

If an Array, the size must be exactly modbus_register_count

If a Sequence, then the length must be exactly modbus_register_count

INT16
UINT16
ENUM with all literals fitting into its type

If output_data_offset or ouput_data_factor are set, the value written to Modbus will be a linear transformation of the DynamicData field value:

  • <register value> = output_data_offset + ouput_data_factor * <field value>

HOLDING_REGISTER_INT16

The value will be written to Modbus in big endian order (this is the Modbus standard).

1

Array or Sequence of:
INT16
UINT16
ENUM with all literals fitting into their type
If output_data_offset or ouput_data_factor are set, then the value written to each Modbus register “i” will be a linear transformation of the DynamicData field value:

  • <register value>[i] = output_data_offset + ouput_data_factor * <field value>[i]

HOLDING_REGISTER_INT16

The value will be written to Modbus in big endian order (this is the Modbus standard).

modbus_register_count

If an Array, the size must be exactly modbus_register_count

If a Sequence, the length must be exactly modbus_register_count

INT32
UINT32
ENUM with all literals fitting into its type

If output_data_offset or output_data_factor are set, the value written to Modbus will be a linear transformation of the DynamicData field value:

  • <register value> = output_data_offset + ouput_data_factor * <field value>

HOLDING_REGISTER_INT32

The value will be written to Modbus in big endian order.

The 32-bit integer field is written to two consecutive Modbus registers

2

INT64
UINT64

If output_data_offset or output_data_factor are set, the value written to Modbus will be a linear transformation of the DynamicData field value:

  • <register value> = output_data_offset + ouput_data_factor * <field value>

HOLDING_REGISTER_INT64

The value will be written to Modbus in big endian order.

The 64-bit integer field is written to four consecutive Modbus registers

4

FLOAT32

If output_data_offset or output_data_factor are set, the value written to Modbus will be a linear transformation of the DynamicData field value:

  • <register value> = output_data_offset + ouput_data_factor * <field value>

HOLDING_REGISTER_FLOAT_ABCD
HOLDING_REGISTER_FLOAT_BADC
HOLDING_REGISTER_FLOAT_CDAB
HOLDING_REGISTER_FLOAT_DCBA

The float value is written to two consecutive Modbus registers according to the format specified in the suffix.

2

Array or Sequence of:
INT32
UINT32
ENUM with all literals fitting into its type

If output_data_offset or output_data_factor are set. Then the value written to each Modbus register “i” will be a linear transformation of the DynamicData field value:

  • <register value>[i] = output_data_offset + ouput_data_factor * <field value>[i]

HOLDING_REGISTER_INT32

The value will be written to Modbus in big endian order.

Each 32-bit integer Array/Sequence element is written to 2 consecutive Modbus registers.

modbus_register_count

Note that it must be a multiple of 2.

If an Array, the size must be exactly modbus_register_count / 2

If a Sequence, the length must be exactly modbus_register_count / 2

Array or Sequence of:
INT64
UINT64

If output_data_offset or output_data_factor are set. Then the value written to each Modbus register “i” will be a linear transformation of the DynamicData field value:

  • <register value>[i] = output_data_offset + ouput_data_factor * <field value>[i]

HOLDING_REGISTER_INT64

The value will be written to Modbus in big endian order.

Each 64-bit integer Array/Sequence element is written to 4 consecutive Modbus registers.

modbus_register_count

Note that it must be a multiple of 4.

If an Array, the size must be exactly modbus_register_count / 4

If a Sequence, the length must be exactly modbus_register_count / 4

Array or Sequence of:
FLOAT32

If output_data_offset or output_data_factor are set, the value written to each Modbus register “i” will be a linear transformation of the DynamicData field value:

  • <register value>[i] = output_data_offset + ouput_data_factor * <field value>[i]

HOLDING_REGISTER_FLOAT_ABCD
HOLDING_REGISTER_FLOAT_BADC
HOLDING_REGISTER_FLOAT_CDAB
HOLDING_REGISTER_FLOAT_DCBA

Each 32-bit integer Array/Sequence element is written to two consecutive Modbus registers according to the format specified in the suffix.

modbus_register_count / 2

Note that it must be a multiple of 2.

If an Array, the size must be exactly modbus_register_count / 2

If a Sequence, the length must be exactly modbus_register_count / 2

Floating point values can be stored to pairs of Modbus registers according to the following formats:

  • “ABCD”: IEEE big endian (usual Modbus format)

  • “DCBA”: IEEE little endian

  • “BADC”: Big endian, byte swapped

  • “CDAB”: Little endian, byte swapped

2.3.2. Modbus Input (Routing Data from Modbus to DDS)

2.3.2.1. Concept

A single Modbus input can be used to read multiple Modbus registers/coils within the same Modbus Device and place them into the same DynamicData object. Therefore the configuration must provide a way to associate each Modbus register/coil with the corresponding DyamicData member where the value will be copied into. This is an array of tuples, similar to the Modbus Output (Routing Data from DDS to Modbus), using JSON.

2.3.2.2. Example

Consider the following custom data-type, which will hold the information to be read from each Modbus device.

This example shows the configuration for a DataNab MBus_WTH_CO2_LCD_ETH Modbus Device. The data-type defined is the following:

@mutable
struct MBus_WTH_CO2_LCD_ETH_INPUT {
    @key      string<64>  device_name; // Configured in the input
    @optional string<64>  device_type;
    uint8  mac_address[6];
    uint8  ip_address[4];
    @optional @unit("celsius") float temperature;
    @optional float humidity;
    @optional float co2_value;
    @optional int8  analog_output_config;
};
[
    {
        "field": "device_name",
        "value": "device_37"
    },
    {
        "field": "device_type",
        "value": "MBus_WTH_CO2_LCD_ETH"
    },
    {
        "field": "mac_address",
        "modbus_register_address": 100,
        "modbus_register_count": 6,
        "modbus_datatype": "HOLDING_REGISTER_INT8"
    },
    {
        "field": "ip_address",
        "modbus_register_address": 107,
        "modbus_register_count": 4,
        "modbus_datatype": "HOLDING_REGISTER_INT8",
        "modbus_min_value": 0,
        "modbus_max_value": 255
    },
    {
        "field": "temperature",
        "modbus_register_address": 204,
        "modbus_datatype": "HOLDING_REGISTER_INT16",
        "input_data_factor" : 0.1
    },
    {
        "field": "humidity",
        "modbus_register_address": 207,
        "modbus_datatype": "HOLDING_REGISTER_INT16",
        "input_data_factor" : 0.1
    },
    {
        "field": "co2_value",
        "modbus_register_address": 211,
        "modbus_datatype": "HOLDING_REGISTER_INT16"
    },
    {
        "field": "analog_output_config",
        "modbus_register_address": 1254,
        "modbus_datatype": "HOLDING_REGISTER_INT8"
    }
]

The input is configured as follows:

<input connection="ModbusDevice1">
    <registered_type_name>
      MBus_WTH_CO2_LCD_ETH_INPUT
    </registered_type_name>
    <stream_name>ModbusDevice1_input</stream_name>
    <property>
        <value>
            <element>
                <name>polling_period_msec</name>
                <value>2000</value>
            </element>
            <element>
                <name>configuration_file_json</name>
                <value>input_config.json</value>
            </element>
        </value>
    </property>
</input>

Similar to the output, the JSON configuration can be provided in-line as in:

<input connection="ModbusDevice1">
    <registered_type_name>test</registered_type_name>
    <stream_name>ModbusDevice1_input</stream_name>
    <property>
        <value>
            <element>
                <name>polling_period_msec</name>
                <value>2000</value>
            </element>
            <element>
                <name>configuration_string_json</name>
                <value>
                [
                  {
                    "field": "device_type",
                    "value": "MBus_WTH_CO2_LCD_ETH",
                  },
                  {
                    "field": "mac_address",
                    "modbus_register_address": 100,
                    "modbus_register_count": 6,
                    "modbus_datatype": "INPUT_REGISTER_INT8"
                  },
                  …
                ]
                </value>
            </element>
        </value>
    </property>
</input>

2.3.2.3. Configuration Attributes

The configuration is a JSON array. Each element contains the attributes described in the table below:

Table 2.4 Input Configuration Attributes

Attribute name

Required

JSON type

Meaning

field

YES

string

Member Name on the associated DynamicData. The name can use the hierarchical naming format to indicate access to nested fields.

modbus_register_address

NO if value is set, YES otherwise

integer

The register address in the Modbus Server. This is zero-based, one less than the register number.

modbus_datatype

NO if value is set, YES otherwise

string

  • DISCRETE_INPUT_BOOLEAN

  • INPUT_REGISTER_INT8

  • INPUT_REGISTER_INT16

  • INPUT_REGISTER_INT32

  • INPUT_REGISTER_INT64

  • INPUT_REGISTER_FLOAT_ABCD

  • INPUT_REGISTER_FLOAT_BADC

  • INPUT_REGISTER_FLOAT_CDAB

  • INPUT_REGISTER_FLOAT_DCBA

  • COIL_BOOLEAN

  • HOLDING_REGISTER_INT8

  • HOLDING_REGISTER_INT16

  • HOLDING_REGISTER_INT32

  • HOLDING_REGISTER_INT64

  • HOLDING_REGISTER_FLOAT_ABCD

  • HOLDING_REGISTER_FLOAT_BADC

  • HOLDING_REGISTER_FLOAT_CDAB

  • HOLDING_REGISTER_FLOAT_DCBA

value

NO if modbus_register_address and modbus_datatype are set, YES otherwise

One of the following:

  • string

  • number

  • array of numbers

Value of a constant that will be set to the specified DynamicData field if it is compatible.

modbus_register_count

NO

number

The number of registers to read.

If unspecified: it defaults to one for COIL, INT8, INT16, to two for INT32 and FLOAT_XXXX, and to four for INT64.

If specified: the user should take into account that it takes two registers to read each INT32 and FLOAT_XXXX value and it takes four registers to read an INT64.

modbus_slave_device_id

NO

number

Slave number of a Modbus device on a serial network. The value of 0 represents a broadcast address.

modbus_min_value

NO

number

The minimum value for the value read

modbus_max_value

NO

number

The maximum value for the value read

modbus_valid_values

YES for Enums, NO for other datatypes

array of numbers

The complete list of values that can be written. Values not in the list are considered invalid outputs.

input_data_factor

NO

number

The value read from Modbus is multiplied by this factor.

The data stored in the DynamicData field is:

  • input_data_offset + input_data_factor * <value>

input_data_offset

NO

number

The offset is added to the value read from Modbus after it has been multiplied times the data_factor.

The data stored in the DynamicData field is:

  • input_data_offset + input_data_factor * <value>

2.3.2.4. Behavior

2.3.2.4.1. Accessing the Modbus Server

To read data from a Modbus Device, the adapter can either use an internal thread or the Route’s session thread. Which thread to use depends on whether polling_period_msec is set:

  • If it is set: each period, the adapter will actively read from the Modbus Server, save the value, and notify the Routing Service.

    • The new data read from the server replaces any previous value stored in the input. If polling_period_msec is set, the Input Adapter read/take operations are non-blocking. They just return data that has already been read from the Modbus server and is kept in the Adapter input, if any.

  • If it is not set: the adapter will only read in the context of a read/take operation.

    • The read/take operation is blocking and synchronously calls the Modbus Client API to read data from the Modbus Server and stores the value with the input prior to returning it as the output of the read/take call.

There are four different functions that can be used to access registers, discrete inputs, coils in a Modbus Server. They are distinguished by the Modbus function code. The function code used is controlled by the setting of modbus_datatype, as seen in the table below:

Table 2.5 Read Data From Modbus Server

modbus_datatype

Modbus function code

libmodbus function

  • DISCRETE_INPUT_BOOLEAN

0x02 (read input status)

modbus_read_input_bits

  • INPUT_REGISTER_INT8

  • INPUT_REGISTER_INT16

0x04 (read input registers)

modbus_read_input_registers

  • INPUT_REGISTER_INT32

  • INPUT_REGISTER_FLOAT_ABCD

  • INPUT_REGISTER_FLOAT_BADC

  • INPUT_REGISTER_FLOAT_CDAB

  • INPUT_REGISTER_FLOAT_DCBA

0x04 (read input registers)

modbus_read_input_registers
modbus_register_count must be a power of 2.

  • COIL_BOOLEAN

0x01 (read coil status)

modbus_read_bits

  • HOLDING_REGISTER_INT8

  • HOLDING_REGISTER_INT16

0x03 (read holding registers)

modbus_read_registers

  • HOLDING_REGISTER_INT32

  • HOLDING_REGISTER_FLOAT_ABCD

  • HOLDING_REGISTER_FLOAT_BADC

  • HOLDING_REGISTER_FLOAT_CDAB

  • HOLDING_REGISTER_FLOAT_DCBA

0x03 (read holding registers)

modbus_read_registers
modbus_register_count must be a power of 2.

2.3.2.4.2. Data caching

The Modbus input keeps at most one data value from the Server. In other words, it’s semantically as if it was storing data samples from a single instance and had History QoS set to KEEP_LAST with depth=1.

The reason is that, semantically, Modbus looks like memory registers and those have “KEEP_LAST 1” semantics. So the Modbus input simply caches the most current value accessed from the corresponding Modbus server registers.

2.3.2.4.3. Read and take behavior

The read/take operations return a sequence with one DynamicData sample.

The semantics of read vs. take are the usual ones:

  • With a “read”, the data is not removed from the Adapter input.

  • With a “take”, the data is removed from the Adapter input.

2.3.2.4.4. Conversion to DynamicData

The configuration of the input dictates where (in the DynamicData object) to store the registers read from Modbus.

modbus_register_count determines the number of registers that are read from a Modbus Device. Then, depending on this setting and the modbus_datatype, the DynamicData object interprets the data that the read registers contain. The following sections show examples about how this data is interpreted.

2.3.2.4.4.1. Reading Primitive Values

The simplest case is when a reading a primitive value is read from Modbus. Depending on the type of value, this requires reading a single bit coil, a 16-bit register, two or four consecutive 16-bit registers. The first register address is specified by the setting modbus_register_address. In this case, the modbus_register_count shall not be specified.

The following table shows how Modbus registers are accessed and converted to primitive values:

Table 2.6 Conversion from Modbus Registers to Primitive Value fields

modbus_datatype

Number of (16-bit) Modbus registers read

Conversion into a DynamicData field

  • DISCRETE_INPUT_BOOLEAN

  • COIL_BOOLEAN

N/A

The (single) bit returned from Modbus may be placed in a boolean or integer DynamicData field.

For a boolean field:

  • 0 -> FALSE

  • 1 -> TRUE

If it is stored into an integer field, 0 or 1 are the only possible values.

  • INPUT_REGISTER_INT8

  • HOLDING_REGISTER_INT8

1

Modbus registers are always 2 bytes. But this datatype ignores the high-order byte (treats it as if it was zero).

The low-order byte returned from Modbus can placed into:

  • Signed integer fields

  • Unsigned integer fields

  • Enumerated fields

  • Floating point fields


If input_data_offset or input_data_factor are set, the value stored is a linear transformation relative to what is read from Modbus:

  • <field value> = input_data_offset + input_data_factor * <register value>


An Unsigned integer field can only be selected if the configuration specifies a modbus_min_value such that:

  • input_data_offset + input_data_factor * modbus_min_value >= 0

It is recommended to use a floating point destination DynamicData field if either input_data_offset or input_data_factor are floating point numbers. If the destination field in DynamicData is an integer and input_data_offset or input_data_factor are floating point numbers, the result will be truncated.

An Enumerated field can only be selected if the configuration specifies a modbus_valid_values that is a subset of the values in the enumeration.

  • INPUT_REGISTER_INT16

  • HOLDING_REGISTER_INT16

1

The value returned from Modbus can be placed in:

  • Signed integer fields except int8

  • Unsigned integer fields except uint8

  • Enumerated fields

  • Floating point fields

The value read from Modbus is interpreted as a big-endian integer (this is the Modbus standard).

If input_data_offset or input_data_factor are set, the value stored is a linear transformation relative to what is read from Modbus:

  • <field value> = input_data_offset + input_data_factor * <register value>


An Unsigned integer field can only be selected if the configuration specifies a modbus_min_value such that:

  • input_data_offset + input_data_factor * modbus_min_value >= 0

It is recommended to use a floating point destination DynamicData field if either input_data_offset or input_data_factor are floating point numbers. If the destination field in DynamicData is an integer and input_data_offset or input_data_factor are floating point numbers, the result will be truncated.

An Enumerated field can only be selected if the configuration specifies a modbus_valid_values that is a subset of the values in the enumeration.

  • INPUT_REGISTER_INT32

  • HOLDING_REGISTER_INT32

2

(modbus_register_address and modbus_register_address +1)

The value returned from Modbus can placed into:

  • Signed integer fields int32 and int64

  • Unsigned integer fields uint32 uint64

  • Floating point fields

The value read from Modbus is interpreted as a big-endian integer (this is the Modbus standard).

If input_data_offset or input_data_factor are set, the value stored is a linear transformation relative to what is read from Modbus:

  • <field value> = input_data_offset + input_data_factor * <register value>


An Unsigned integer field can only be selected if the configuration specifies a modbus_min_value such that:

  • input_data_offset + input_data_factor * modbus_min_value >= 0

It is recommended to use a floating point destination DynamicData field if either input_data_offset or input_data_factor are floating point numbers. If the destination field in DynamicData is an integer and input_data_offset or input_data_factor are floating point numbers, the result will be truncated.

An Enumerated field can only be selected if the configuration specifies a modbus_valid_values that is a subset of the values in the enumeration.

  • INPUT_REGISTER_INT64

  • HOLDING_REGISTER_INT64

4

(modbus_register_address , modbus_register_address +1, modbus_register_address +2, and modbus_register_address +3)

The value returned from Modbus can placed into:

  • Signed integer fields int64

  • Unsigned integer fields uint64

  • Floating point fields

The value read from Modbus is interpreted as a big-endian integer (this is the Modbus standard).

If input_data_offset or input_data_factor are set, the value stored is a linear transformation relative to what is read from Modbus:

  • <field value> = input_data_offset + input_data_factor * <register value>


An Unsigned integer field can only be selected if the configuration specifies a modbus_min_value such that:

  • input_data_offset + input_data_factor * modbus_min_value >= 0

It is recommended to use a floating point destination DynamicData field if either input_data_offset or input_data_factor are floating point numbers. If the destination field in DynamicData is an integer and input_data_offset or input_data_factor are floating point numbers, the result will be truncated.

An Enumerated field can only be selected if the configuration specifies a modbus_valid_values that is a subset of the values in the enumeration.

  • INPUT_REGISTER_FLOAT_ABCD

  • INPUT_REGISTER_FLOAT_BADC

  • INPUT_REGISTER_FLOAT_CDAB

  • INPUT_REGISTER_FLOAT_DCBA

  • HOLDING_REGISTER_FLOAT_ABCD

  • HOLDING_REGISTER_FLOAT_BADC

  • HOLDING_REGISTER_FLOAT_CDAB

  • HOLDING_REGISTER_FLOAT_DCBA

2

(modbus_register_address and modbus_register_address +1)

The value returned from Modbus can only be placed into a floating point field.
The four bytes read from two Modbus registers are transformed to a 32-bit floating point according to the byte order indicated by the suffix:

  • ABCD: IEEE big endian (usual Modbus format)

  • DCBA: IEEE little endian

  • BADC: Big endian, byte swapped

  • CDAB: Little endian, byte swapped


If input_data_offset or input_data_factor are set, the value stored is a linear transformation relative to what is read from Modbus:

  • <field value> = input_data_offset + input_data_factor * <register value>

The resulting value may be stored in a 32-bit or 64-bit floating point field.

2.3.2.4.4.2. Reading Array/Sequence Values

It is also possible to read a list of consecutive registers as long as they are all the same type and are interpreted the same way. This is achieved by setting modbus_register_count.

If modbus_register_count is specified, then the corresponding DynamicData field must be an array or sequence of a type compatible with the modbus_datatype. The compatibility rules are the same as for accessing a primitive field.

For example, consider this mapping:

{
    "field": "mac_address",
    "modbus_register_address": 100,
    "modbus_register_count": 6,
    "modbus_datatype": "INPUT_REGISTER_INT8"
}

In the corresponding definition of MBus_WTH_CO2_LCD_ETH_INPUT, the field mac_address is defined as an int 8 array:

struct MBus_WTH_CO2_LCD_ETH_INPUT {
    ...
    @optional int8 mac_address[6];
    ...
};

In Table Conversion from Modbus Registers to Primitive Value fields we see that the modbus_datatype INPUT_REGISTER_INT8 is indeed compatible with all signed integer fields.

If the field being assigned is an array, then, the size must be exactly what is needed to read the specified number of primitive values. Note that the size of the array may not match the number of registers, because in some cases it is necessary to read two registers for each primitive value.

In the case of the mac_address we can see from Conversion from Modbus Registers to Primitive Value fields that the modbus_datatype INPUT_REGISTER_INT8 only uses one register to hold the value. Therefore, the size of the array must match modbus_register_count.

However if we had modbus_datatype INPUT_REGISTER_INT32, or INPUT_REGISTER_FLOAT_ABCD, then each primitive value read (INT32 or FLOAT_ABCD) requires two Modbus registers. So to read an array of six elements, we would need to specify 12 Modbus registers. For example, the following IDL type:

struct MBus_WTH_CO2_LCD_ETH_INPUT {
    ...
    @optional float float_array[6];
    ...
};

Should be configured by using:

{
    "field": "float_array",
    "modbus_register_address": 2000,
    "modbus_register_count": 12,
    "modbus_datatype": "INPUT_REGISTER_FLOAT_ABCD"
}

The DynamicData field may also be of a sequence type rather than an array type. This situation is handled the same way as the array except that the size of the sequence will be adjusted to match the number of primitive elements read. So the length will end up being the number of registers read, except in the case where modbus_datatype requires two or more registers per primitive value (i.e., the 32-bit integers and the floating-point types). For the configuration to be valid, the maximum length of the sequence must accommodate this.