Skip to main content
Version: 5.6.0

Modbus Source

The Tributech Modbus Source allows to connect to Modbus TCP devices, or to serial slaves behind a TCP-to-serial gateway, and read coils and registers from them. The Source acts like a Modbus TCP client in a docker environment and enables the forwarding of the data to a Tributech Agent stream.

info

The Tributech Modbus Source is released as version 0.2.0 (beta) and requires Tributech platform 5.6 or later.

One Source serves up to 50 Modbus devices. Each device carries its own connection settings and owns the addresses read from it, so a single Source can cover several devices on a plant network as well as several serial slaves behind one gateway.

The Tributech Modbus Source repeatedly, in a fixed cycle:

  • reads the coils and registers configured as streams and forwards each value to the Tributech Agent (one value stream per address),
  • writes the values configured as parameters onto the device and keeps them asserted,
  • reports its health, so the reachability of every device and the state of every configured address is visible in the Tributech Node (see Health Check).

Single values can additionally be written on demand with the WriteOnce Command.

Setup

The Tributech Modbus Source image can be started without any dependencies but will not be functional without a valid Twin Configuration or MessageBroker connect to the Tributech Agent. The TwinConfiguration can be provided via the Tributech Node (recommended) or MessageBroker (see Source Integration). The Modbus Source will automatically connect to the Tributech Agent if the Tributech Agent is running and correct MessageBroker settings are set.

In the following part we will describe the setup of a Tributech Modbus Source.

  • Create a docker-compose.yml file with the following content (adjustments required):
docker-compose.yml
version: '3.6'

services:
# Prepares the bind-mounted folders for the Tributech Agent, which runs as the
# non-root user 1654 and ships without the tooling to change ownership itself.
# This replaces a manual `sudo chown -R 1654:1654 enrollment volumes` on the host.
init-permissions:
image: busybox:1.36
user: "0:0"
command: sh -c "chown -R 1654:1654 /mnt/enrollment /mnt/volumes"
volumes:
- ./enrollment:/mnt/enrollment
- ./volumes:/mnt/volumes

source-modbus:
restart: unless-stopped
image: ${DOCKER_REGISTRY-tributech.azurecr.io/}tributech-source-modbus:${SOURCE_TAG:-0.2.0}
depends_on:
- mosquitto-server
- tributech-agent
environment:
- MqttOptions__MQTTHost=mosquitto-server
- Logging__LogLevel__Default=Information
networks:
- modbus-net
logging:
driver: "json-file"
options:
max-size: "2m"
max-file: "5"

tributech-agent:
image: ${DOCKER_REGISTRY-tributech.azurecr.io/}tributech-agent:${AGENT_TAG:-5.6.0}
depends_on:
mosquitto-server:
condition: service_started
init-permissions:
condition: service_completed_successfully
environment:
- Logging__LogLevel__Default=Information
- MqttOptions__MQTTHost=mosquitto-server
- EdgeDeviceOptions__NodeUrl=${NODE_URL:?"The Tributech Node Url is required"}
networks:
- modbus-net
ports:
- "5001:8080" # enable access to agent REST-API
volumes:
- ./volumes:/app/data # volume mapping for permanent storage of keys and datatwin file
- ./enrollment:/app/enrollment # local enrollment folder mounted to /app/enrollment
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "2m"
max-file: "5"

mosquitto-server:
image: eclipse-mosquitto:${MQTT_TAG:-1.6}
restart: unless-stopped
networks:
- modbus-net

networks:
modbus-net:

The enrollment and volumes folders must be accessible to the unprivileged user the Agent runs as, see Container File Permissions.

Adjust the setting for the Tributech Agent to your environment, sample value:

env specific settings
tributech-agent:
...
environment:
...
- EdgeDeviceOptions__NodeUrl=https://my-environment.tributech-node.com

The Tributech Agent authenticates with the Tributech Node using enrollment certificates provided through a local enrollment folder mounted into the container (the ./enrollment:/app/enrollment volume in the docker-compose.yml above). See Authentication Certificates (Enrollment) and Docker volumes in the Setup guide for how to create and provide it. If no Agent ID is configured, the agent generates a random one on first start.

The Modbus Source needs to reach every configured Modbus TCP device on its Modbus port (502 by default) in the private network, see Tributech Agent Source Requirements.

Configuration

After setting up the Tributech Modbus Source we need to activate it in the Tributech Node (see Agent Management) and configure the TwinConfiguration.

Configure AgentConfigure Agent

The complete configuration of the Modbus Source is done with the Twin Configuration, there is nothing to edit by hand on the edge device. The Agent keeps the configuration it receives in its volumes folder, together with its keys and certificates (see Docker volumes), so it survives a restart of the edge device and does not have to be applied again. Changes are picked up by the running Source, it does not need to be restarted.

TwinModelPurpose
Modbus Sourcedtmi:io:tributech:source:modbus;3Groups the devices. Carries no Modbus settings itself. Attached to the edge device twin.
Modbus Devicedtmi:io:tributech:device:modbus;1One per device to be reached. Holds the connection settings and the timing. Attached to the source twin via the Devices relationship, up to 50 per source.
Modbus Streamdtmi:io:tributech:stream:modbus;2One per address to be read. Attached to a device twin via the Streams relationship.
Modbus Parameterdtmi:io:tributech:parameter:modbus;2One per address to be held at a fixed value. Attached to a device twin via the Parameter relationship.

Streams and parameters always belong to a device, not to the source. The same address can therefore be configured on several devices without the configurations interfering with each other.

Modbus twin hierarchy: one source, many devices, one connection per endpointModbus twin hierarchy: one source, many devices, one connection per endpoint

Modbus Source

We can now add a new Modbus Source with the three dots menu on the right side of the Device Edge entry. The Source carries no connection settings, those live on the devices below it:

SettingValueRequiredDefaultDescription
NamestringNoHuman-readable name of the source. Display name only.
Expected Health FrequencyISO 8601 durationNoHow often the Source reports its health state, i.e. PT30S for 30 seconds, PT5M for 5 minutes, PT1H for one hour. Left empty, the Source sends no health messages.

Modbus Device

We then add a Modbus Device with the three dots menu on the right side of the Modbus Source entry, selecting Add Devices. Every device we want to read from gets its own entry, with the connection settings of that device:

SettingValueRequiredDefaultDescription
HoststringYesIP address or hostname of the Modbus TCP device or gateway.
PortintegerNo502Modbus TCP port.
UnitIdentifier0…255No255Selects the device behind the address, see Unit Identifier.
DefaultReadCycleIntervalsecondsNo10How often this device is polled. Applies to every stream of the device that does not define its own interval. Values below 1s are treated as 1s.
WriteCycleIntervalsecondsNo10How often the parameter values of this device are re-asserted. Values below 1s are treated as 1s.
NamestringNoDisplay name only.

A sample device configuration:

{
"Name": "Compressor 1",
"Host": "192.168.1.50",
"Port": 502,
"UnitIdentifier": 255,
"DefaultReadCycleInterval": 5,
"WriteCycleInterval": 5
}

Connections

The Source opens one TCP connection per distinct Host and Port, not one per device. Devices that share an address share the connection, and the UnitIdentifier is sent with every single request to select the device behind it. This is what makes a TCP-to-serial gateway work: all slaves behind it are configured as separate devices with the same Host and Port and different unit identifiers, and the Source talks to them over one session.

Two consequences are worth knowing:

  • An address is identified by the Host string as written, plus the Port. The string is trimmed and compared case-insensitively, but it is not resolved, so plc-01 and 192.168.1.50 count as two different addresses and get two connections even when they point at the same device.
  • A configuration change only reconnects the addresses it adds or removes. Adding a stream, changing a parameter or changing a UnitIdentifier never drops a working connection, and a device becoming unreachable does not affect devices on other addresses.

Unit Identifier

ValueUse
255Device addressed directly over Modbus TCP. This is the default.
1247Serial slave behind a TCP-to-serial gateway, or a TCP device that insists on an identifier. Use the slave ID of the device.
248254Reserved by the specification. Accepted, but only useful if the device documentation asks for it.
0Broadcast address on the serial side. A gateway forwards the request and no slave answers it, so every read of this device times out.
warning

A UnitIdentifier of 0 is accepted and logged as a warning, but the device will not answer. Because the requests still travel over the connection shared with the other devices on that address, the timeouts also count towards that connection being declared lost. Only use 0 if the device documentation explicitly asks for it.

A value outside 0255 does not fit into the Modbus protocol header. The device is dropped from the configuration with an error, no requests are sent for it, and a WriteOnce Command addressed to it is rejected.

Modbus Stream

We can then add a new Modbus Stream with the three dots menu on the right side of a Modbus Device entry, selecting Add Streams. Each stream describes exactly one address on that device.

SettingValueRequiredDefaultDescription
IdentifierRegisterType:AddressYesWhich address to read, see Addressing.
DatatypeBOOL, INT16, UINT16, INT32, UINT32, FLOAT32, FLOAT64YesHow the device stores the value. This also decides how many registers are read.
EncodingBOOL, INT32, INT64, FLOAT, DOUBLE, BYTEARRAY, STRING_UTF8, STRING_BASE64YesHow the value is published to the platform. Must fit the Datatype, see Datatype and Encoding.
WordOrderHighWordFirst / LowWordFirstNoHighWordFirstRegister order for values spanning several registers. Must match what the device documentation states. Ignored for BOOL, INT16 and UINT16.
ReadCycleIntervalsecondsNodevice defaultOverrides DefaultReadCycleInterval for this stream only.
Name, UnitstringNoDescriptive only.

We can continue adding every address we want to read from that device, and repeat this for the other devices. After all devices and streams have been configured, we can apply the configuration to the Tributech Agent by clicking on the APPLY CONFIGURATION button in the top right corner.

A sample stream configuration:

{
"Name": "Motor temperature",
"Identifier": "HoldingRegister:100",
"Datatype": "INT32",
"WordOrder": "HighWordFirst",
"Encoding": "INT32",
"Unit": "°C"
}
warning

Without a reachable Modbus device we will not receive any data on the Tributech Node.

Addressing

The Identifier is written as RegisterType:Address, for example HoldingRegister:100. The following register types are supported:

Register typeAccessTypical content
Coilread / writeSwitchable digital output
DiscreteInputread onlyDigital input
HoldingRegisterread / writeSetpoints, configuration, measured values
InputRegisterread onlyMeasured values
  • The register type is case-sensitive, holdingregister:100 is not accepted.
  • The address is the 0-based PDU address. Vendor documentation often uses 1-based or 4xxxx-style numbering, i.e. the vendor register 40101 corresponds to HoldingRegister:100.
  • An address that cannot be parsed marks the stream as permanently faulty. It is then no longer polled and the Source reports Degraded, all other streams keep working.

Datatype and Encoding

Datatype describes the device side, Encoding the platform side. They are not matched up automatically and need to be selected together:

DatatypeRegisters readEncoding to use
BOOL (only for Coil / DiscreteInput)BOOL
INT16 / UINT161INT32
INT32 / UINT322INT32
FLOAT322FLOAT
FLOAT644DOUBLE
any of the aboveBYTEARRAY, if the raw bytes are wanted instead of a number

The platform has no 16-bit encoding. An INT16 or UINT16 stream reads its single register and publishes it widened to a 4-byte INT32 with the same numeric value: INT16 keeps its sign, so a register holding 0xFFFE arrives as -2, and UINT16 is filled up with zeroes, so 0xFFFF arrives as 65535.

tip

Use INT16 or UINT16 for a register that really holds a 16-bit value, and let the Source widen it. Configuring INT32 instead reads a second register that does not belong to the value, and whatever that register happens to contain silently ends up in the published number.

A multi-register datatype consumes consecutive addresses: INT32 at HoldingRegister:100 reads 100 and 101, FLOAT64 reads 100 to 103. The device needs to store the value across all of them, and no other stream should be configured on an address that is already part of a wider value.

Writing is not widened. A parameter or a WriteOnce Command with a 16-bit datatype is checked against the 16-bit range and written as exactly one register.

Word Order

WordOrder decides in which order the registers of a value spanning several registers are combined, and is set per stream and per parameter.

The following figure shows how the Datatype decides the width of a value behind one Identifier, and what the WordOrder does to a value spanning two registers:

Modbus register layout: datatype width and word orderModbus register layout: datatype width and word order
warning

A wrong WordOrder does not cause an error. The values arrive, but the halves of every multi-register number are swapped, the result looks plausible and is wrong. If numbers are far off, this is the first thing to check.

Modbus Parameter

A parameter tells the Source to write a fixed value onto a device and keep it asserted. It is used for setpoints that must survive a device restart and is added with the three dots menu on the right side of the Modbus Device entry, selecting Add Parameter.

SettingValueRequiredDefaultDescription
IdentifierRegisterType:AddressYesSame syntax as for streams, but only Coil and HoldingRegister are allowed.
Datatypesame list as for streamsYesBOOL only together with Coil. Decides how many registers the value spans.
WordOrderHighWordFirst / LowWordFirstNoHighWordFirstRegister order, for a value spanning several registers.
ValuestringYesConverted to the datatype when written.
Name, UnitstringNoDescriptive only.

A parameter is configuration, not an action: it describes the state the address is supposed to be in, and the Source is what carries out the writes. The handling behaves as follows:

  • Every WriteCycleInterval the Source reads the address back and compares it with the Value configured on the parameter. It only writes when the device holds something else, so an unchanged setpoint causes no write traffic.
  • If something else changes the address on the device, i.e. another system or a WriteOnce Command, the Source writes the configured Value back on the next cycle. A WriteOnce onto a parameter address is therefore undone again.
  • Each device runs its own write cycle, on its own WriteCycleInterval.
  • A parameter creates no data stream in the Tributech Node. The value the Source reads back is only used for the comparison and is not forwarded, so nothing about a parameter shows up in the Node on its own. To see the value there, configure a stream on the same address of the same device.
  • A read-only target (DiscreteInput, InputRegister), an unparseable address or a Value that does not fit the datatype is reported as permanently failed and puts the Source into Degraded.
  • Some devices refuse to be read on a write-only register. If a device answers the read-back with an illegal-address or illegal-function error, the Source writes the value unconditionally on every cycle instead of giving up on the parameter.

WriteOnce Command

The Modbus Source supports the command WriteOnce, which writes a single value on one device immediately and does not keep it asserted. The command is sent in the COMMANDS tab of the agent (see Commands), commandBody is the base64 encoded payload:

{
"DeviceId": "6f1d2c3b-4a5e-4f60-8b7c-9d0e1f2a3b4c",
"Identifier": "HoldingRegister:100",
"Datatype": "INT32",
"WordOrder": "HighWordFirst",
"Value": "777"
}
FieldRequiredDescription
DeviceIdYesThe twin ID of the Modbus Device to write to. It selects the address and the unit identifier the write is sent with.
IdentifierYesSame syntax as for streams, and must point at a Coil or a HoldingRegister.
DatatypeYesDecides how Value is read and how many registers are written. BOOL only with a Coil, the numeric types only with a HoldingRegister.
WordOrderNoRegister order of a write spanning several registers, HighWordFirst by default.
ValueYesAlways given as a string. "1" and "true" are both accepted for BOOL.

The command returns one of the following results:

ResultMeaning
SuccessThe device confirmed the write. The device, the address and the value are contained in the response.
RejectedThe request itself is not valid and nothing was sent to the device, i.e. an unknown DeviceId, a read-only address space, a missing or unknown Datatype (the tokens are case-sensitive, int32 is not accepted), a datatype that does not fit the register type, or a Value that cannot be read as the given datatype or does not fit into it.
CommandFailedThe request was valid, but it could not be carried out: the connection to the device is down, or the device answered with an error.

Health States

With an Expected Health Frequency configured on the source twin (see Modbus Source), the Modbus Source reports one state for the whole source to the Tributech Node in that interval (see Health Check). The state covers every device and every connection:

StateMeaning
OkEvery connection is up, all configured devices answer, all streams and parameters are working.
DegradedEverything is connected, but at least one device is not answering, or at least one stream or parameter is permanently faulty, e.g. a wrong address or a read-only write target. Everything else keeps working. Also reported for one interval after a connection was lost and came back within the same interval.
UnhealthyAt least one connection is up and at least one is down. The devices on the remaining connections keep working.
CriticalNo connection is up at all, or no device is configured yet.

The health message additionally reports the detail behind the state, so a single failing device can be told apart from a failing network:

ReportedMeaning
Connection state per addressOne entry per Host:Port, not per device.
Unreachable devicesDevices the gateway answers with an unknown-device error. Their streams are not marked faulty and keep being polled, and the entry disappears on the next successful read.
Faulty streams and parametersKeyed by the device they belong to, so the same address on two devices can be told apart.
Connection losses and failed requests since the last messageFailed requests are diagnostic and do not change the state on their own.

Modbus TCP has no keep-alive, so a lost connection is only detected once a number of requests in a row have gone unanswered on that address, with a slow read cycle this can take about a minute. The Source then reconnects that address on its own, without touching the connections that are still working, and returns to Ok as soon as the devices answer again. An error response from a device proves that the connection is alive, and results in Degraded for the affected address or device, not in a lost connection.

Troubleshooting

SymptomLikely cause
Values arrive but are far offWordOrder does not match the device, or the value does not actually span all the registers the datatype reads.
A 16-bit value is wrong, the neighbouring register appears in itDatatype is INT32 on a register that holds 16 bits. Use INT16 or UINT16, the Source widens the value on its own.
Values read from the wrong placeOff-by-one address, the Identifier uses the 0-based PDU address, vendor documentation often counts from 1.
A device receives no requests at allUnitIdentifier outside 0255, the device is dropped from the configuration.
One device gets no values, the others doHost, Port or UnitIdentifier of that device wrong, device switched off, or blocked by a firewall.
A device times out on every read behind a gatewayUnitIdentifier is 0 or does not match the slave ID of the device.
Source reports DegradedA stream or parameter has an unparseable address, a parameter targets a read-only register type or has a value that does not fit its datatype, or a device is not answering. Fix or remove it, the state returns to Ok.
Source reports UnhealthyOne of several devices, or one gateway, is unreachable. The devices on the other addresses keep working.
Source reports CriticalNo device can be reached at all, or no Modbus Device is configured on the source.
Values arrive less often than expectedReadCycleInterval, DefaultReadCycleInterval of the device, or Value Change Options attached to the stream.
A WriteOnce onto a parameter address does not stickExpected behaviour, the parameter is re-asserted on the next write cycle. Change the parameter value in the twin instead.
A WriteOnce is RejectedDeviceId unknown, Datatype missing or not fitting the register type, or Value not readable as that datatype. Nothing was sent to the device.