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:
Attribute name |
Required |
JSON type |
Meaning |
---|---|---|---|
field |
YES |
string |
Member Name on the associated DynamicData. |
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
The Modbus function used will be adjusted automatically based on the data-type. |
modbus_register_count |
NO |
integer |
The number of registers to write. |
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.
|
output_data_offset |
NO |
number |
The offset is added to the value before writing it to Modbus.
|
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:
modbus_datatype |
modbus_register_count |
Modbus function code |
libmodbus function |
---|---|---|---|
|
1 |
0x05 (force single coil) |
|
|
>=2 |
0x0F (force multiple coils) |
|
|
1 |
0x06 (preset single register) |
|
|
>=2 |
0x10 (preset multiple registers) |
|
|
any |
0x10 (preset multiple 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
DynamicData field |
modbus_datatype |
Number of (16-bit) Modbus registers written |
---|---|---|
BOOL |
COIL_BOOLEAN |
1 |
Array or Sequence of: |
COIL_BOOLEAN |
modbus_register_count |
INT8 |
HOLDING_REGISTER_INT8 |
1 |
Array or Sequence of: |
HOLDING_REGISTER_INT8 |
modbus_register_count |
INT16
|
HOLDING_REGISTER_INT16 |
1 |
Array or Sequence of:
|
HOLDING_REGISTER_INT16 |
modbus_register_count |
INT32
|
HOLDING_REGISTER_INT32 |
2 |
INT64
|
HOLDING_REGISTER_INT64 |
4 |
FLOAT32
|
HOLDING_REGISTER_FLOAT_ABCD |
2 |
Array or Sequence of:
|
HOLDING_REGISTER_INT32 |
modbus_register_count |
Array or Sequence of:
|
HOLDING_REGISTER_INT64 |
modbus_register_count |
Array or Sequence of:
|
HOLDING_REGISTER_FLOAT_ABCD |
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:
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 |
|
value |
NO if modbus_register_address and modbus_datatype are set, YES otherwise |
One of the following:
|
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. |
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.
|
input_data_offset |
NO |
number |
The offset is added to the value read from Modbus after it has been multiplied times the data_factor.
|
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:
modbus_datatype |
Modbus function code |
libmodbus function |
---|---|---|
|
0x02 (read input status) |
|
|
0x04 (read input registers) |
|
|
0x04 (read input registers) |
modbus_read_input_registers
|
|
0x01 (read coil status) |
|
|
0x03 (read holding registers) |
|
|
0x03 (read holding registers) |
modbus_read_registers
|
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:
modbus_datatype |
Number of (16-bit) Modbus registers read |
Conversion into a DynamicData field |
---|---|---|
|
N/A |
The (single) bit returned from Modbus may be placed in a boolean or integer DynamicData field.
If it is stored into an integer field, 0 or 1 are the only possible values. |
|
1 |
Modbus registers are always 2 bytes. But this datatype ignores the high-order byte (treats it as if it was zero).
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. |
|
1 |
The value returned from Modbus can be placed in:
The value read from Modbus is interpreted as a big-endian integer (this is the Modbus standard).
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. |
|
2 |
The value returned from Modbus can placed into:
The value read from Modbus is interpreted as a big-endian integer (this is the Modbus standard).
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. |
|
4 |
The value returned from Modbus can placed into:
The value read from Modbus is interpreted as a big-endian integer (this is the Modbus standard).
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. |
|
2 |
The value returned from Modbus can only be placed into a floating point field.
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.