> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Modbus TCP Client Route

> Connect to PLCs and industrial devices using Modbus TCP protocol with TAG-based polling and automatic batch optimization

## Modbus TCP Client

> [!NOTE]
>
> This page documents the **client** route (`MODBUS_TCP`)—the broker connects **to** a remote Modbus device and polls registers over MQTT. To expose MQTT topics **to** Modbus masters instead (SCADA, HMIs connect to the broker), see [Modbus TCP Server](./modbus-tcp-server).

The `MODBUS_TCP` route enables communication with PLCs, RTUs, and industrial devices using the Modbus TCP/IP protocol. It supports reading and writing registers and coils with configurable polling intervals and automatic batch optimization.

> [!TIP]
>
> Modbus TCP is one of the most widely supported industrial protocols. If your device supports Modbus, this route provides a reliable way to integrate it with MQTT.

## Basic Syntax

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ModbusDevice WITH TYPE MODBUS_TCP
    ADD MODBUS_CONFIG
        WITH HOST "192.168.1.100"
        WITH PORT 502
        WITH SLAVE_ID 1
    ADD MAPPING SensorReadings
        WITH EVERY 500 MILLISECONDS
        ADD TAG Temperature
            WITH ADDRESS "100"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH SOURCE_TOPIC "modbus/temperature"
```

***

## Connection Configuration

### MODBUS\_CONFIG Parameters

**HOST** `string` *required*

Modbus TCP server hostname or IP address.

**PORT** `integer`

Modbus TCP port. Default: 502.

**SLAVE\_ID** `integer`

Modbus slave/unit ID (1-247). Default: 1.

**CONNECTION\_TIMEOUT** `integer`

Connection timeout in milliseconds. Default: 5000.

**READ\_TIMEOUT** `integer`

Read operation timeout in milliseconds. Default: 1000.

**WRITE\_TIMEOUT** `integer`

Write operation timeout in milliseconds. Default: 1000.

**RETRY\_COUNT** `integer`

Number of retry attempts on failure. Default: 3.

**RETRY\_DELAY** `integer`

Delay between retries in milliseconds. Default: 100.

**ONE\_BASED\_ADDRESSES** `boolean` (default: false)

Treat plain numeric tag addresses as one-based (address `1` = PDU register 0). Modicon-style addresses (`400001+`, `300001+`, `100001+`) are always decoded regardless of this flag. See [Modbus addressing](#modbus-addressing).

### Connection Example

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD MODBUS_CONFIG
    WITH HOST "192.168.1.100"
    WITH PORT 502
    WITH SLAVE_ID 1
    WITH CONNECTION_TIMEOUT 5000
    WITH READ_TIMEOUT 1000
    WITH WRITE_TIMEOUT 1000
    WITH RETRY_COUNT 3
    WITH RETRY_DELAY 100
```

***

## Address Types

Modbus defines four address spaces:

| Address Type | Description | Function Codes | Typical Use |
| - | - | - | - |
| `HOLDING_REGISTER` | Read/Write 16-bit registers | FC03, FC06, FC16 | Setpoints, configuration |
| `INPUT_REGISTER` | Read-only 16-bit registers | FC04 | Sensor values, measurements |
| `COIL` | Read/Write single bits | FC01, FC05, FC15 | Digital outputs, controls |
| `DISCRETE_INPUT` | Read-only single bits | FC02 | Digital inputs, status |

> [!NOTE]
>
> Most Modbus devices use Holding Registers for configuration and Input Registers for measured values. Check your device documentation for the correct address type.

***

## Modbus Addressing

Modbus addressing varies by vendor. Coreflux supports three styles—choose the one that matches your device documentation.

### Style 1 — Zero-based PDU offsets (default)

The Modbus protocol uses zero-based addresses in the PDU. This is the default (`ONE_BASED_ADDRESSES = false`).

| Device manual label | Tag `ADDRESS` |
| - | - |
| Register 1 | `"0"` |
| Register 100 | `"99"` |

Use this when documentation labels the first register as `0`, or when targeting integration tests that follow PDU numbering.

### Style 2 — One-based plain addresses

Most device manuals label the first holding register as **1**, not 0. Set `WITH ONE_BASED_ADDRESSES true` on `MODBUS_CONFIG` to use those labels directly in tag addresses.

| Device manual label | Tag `ADDRESS` |
| - | - |
| Register 1 | `"1"` |
| Register 100 | `"100"` |

```lot wrap focus={5-11} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ModbusDevice WITH TYPE MODBUS_TCP
    ADD MODBUS_CONFIG
        WITH HOST "192.168.1.100"
        WITH PORT 502
        WITH SLAVE_ID 1
        WITH ONE_BASED_ADDRESSES true
    ADD MAPPING Sensors
        WITH EVERY 1 SECOND
        ADD TAG Temperature
            WITH ADDRESS "1"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH SOURCE_TOPIC "sensors/temperature"
```

### Style 3 — Modicon notation

Traditional Modicon prefix notation is always accepted. The prefix determines the address type automatically:

| Modicon range | Address type | Example |
| - | - | - |
| 400001–465536 | Holding register | `400001` → PDU 0 |
| 300001–365536 | Input register | `300001` → PDU 0 |
| 100001–165536 | Discrete input | `100001` → PDU 0 |

```lot wrap focus={4-8} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ModbusDevice WITH TYPE MODBUS_TCP
    ADD MODBUS_CONFIG
        WITH HOST "192.168.1.100"
        WITH SLAVE_ID 1
    ADD MAPPING Setpoints
        WITH EVERY 1 SECOND
        ADD TAG Setpoint
            WITH ADDRESS "400100"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "INT16"
            WITH SOURCE_TOPIC "plc/setpoint"
```

> [!NOTE]
>
> Modicon-style addresses are decoded the same way regardless of `ONE_BASED_ADDRESSES`. The flag only affects plain numeric addresses (those without a Modicon prefix).

### Conversion reference

| Device manual label | `ONE_BASED_ADDRESSES false` | `ONE_BASED_ADDRESSES true` | Modicon notation |
| - | - | - | - |
| Register 1 | `"0"` | `"1"` | `"400001"` |
| Register 100 | `"99"` | `"100"` | `"400100"` |
| Register 1000 | `"999"` | `"1000"` | `"401000"` |

Multi-word types (`FLOAT`, `INT32`, and so on) occupy consecutive registers from the resolved starting address.

***

## Data Types

**DATA\_TYPE** `string` *required*

The data type determines how raw register values are interpreted:

| Data Type | Size | Description |
| - | - | - |
| `BOOL` | 1 bit | Boolean value (for coils/discrete inputs) |
| `BIT` | 1 bit | Alias for BOOL |
| `INT8` | 8 bits | Signed 8-bit integer |
| `UINT8` | 8 bits | Unsigned 8-bit integer |
| `INT16` | 16 bits | Signed 16-bit integer (1 register) |
| `UINT16` | 16 bits | Unsigned 16-bit integer (1 register) |
| `INT32` | 32 bits | Signed 32-bit integer (2 registers) |
| `UINT32` | 32 bits | Unsigned 32-bit integer (2 registers) |
| `INT64` | 64 bits | Signed 64-bit integer (4 registers) |
| `UINT64` | 64 bits | Unsigned 64-bit integer (4 registers) |
| `FLOAT` | 32 bits | 32-bit floating point (2 registers) |
| `DOUBLE` | 64 bits | 64-bit floating point (4 registers) |
| `STRING` | Variable | ASCII string across consecutive registers; sized with `LENGTH` (default 256 bytes) |

### String sizing

For `STRING` tags, `WITH LENGTH` sets the maximum string size in **bytes** (default **256**). `STRING_SIZE` is accepted as an alias for the same field. Each Modbus register holds two bytes, so the route reads enough **consecutive registers** starting at the tag's `ADDRESS` to cover the configured size.

```lot wrap focus={12} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ModbusStrings WITH TYPE MODBUS_TCP
    ADD MODBUS_CONFIG
        WITH HOST "192.168.1.100"
        WITH PORT 502
        WITH SLAVE_ID 1
    ADD MAPPING ProductData
        WITH EVERY 1 SECOND
        ADD TAG ProductName
            WITH ADDRESS "0"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "STRING"
            WITH LENGTH 32
            WITH SOURCE_TOPIC "factory/product/name"
```

For cross-protocol defaults and caps, see [String sizing by Route](./overview#string-sizing-by-route) on the industrial overview.

***

## TAG Configuration

### Complete TAG Parameters

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG TemperatureSensor
    WITH ADDRESS "100"
    WITH ADDRESS_TYPE "HOLDING_REGISTER"
    WITH DATA_TYPE "FLOAT"
    WITH SOURCE_TOPIC "modbus/sensors/temperature"
    WITH SCALING 0.1
    WITH OFFSET 0
    WITH UNIT "°C"
    WITH DECIMAL_PLACES 2
    WITH MIN_VALUE -40
    WITH MAX_VALUE 150
    WITH DEADBAND 0.5
    WITH PUBLISH_MODE "JSON"
    WITH WRITABLE "true"
    WITH DESTINATION_TOPIC "modbus/sensors/temperature/set"
    WITH BYTE_ORDER "BIGENDIAN"
    WITH WORD_ORDER "BIGENDIAN"
    WITH DESCRIPTION "Main process temperature sensor"
```

### TAG Parameters Reference

> **Address Configuration**
>
> **ADDRESS** `string` *required*
>
> Register or coil address. Plain numeric (0–65535 zero-based by default, or 1–65536 when `ONE_BASED_ADDRESSES true`), or Modicon notation (`400001+`, `300001+`, `100001+`). See [Modbus addressing](#modbus-addressing).
>
> **ADDRESS\_TYPE** `string` *required*
>
> Type of Modbus address: `HOLDING_REGISTER`, `INPUT_REGISTER`, `COIL`, or `DISCRETE_INPUT`.
>
> **LENGTH** `integer` (default: 256)
>
> **STRING only.** Maximum string size in bytes. The route allocates consecutive registers from the starting `ADDRESS` (2 bytes per register). Alias: `STRING_SIZE`.

> **Value Transformation**
>
> **SCALING** `double`
>
> Multiplier applied to raw value. Default: 1.0. Example: Raw value 245 with SCALING 0.1 becomes 24.5.
>
> **OFFSET** `double`
>
> Value added after scaling. Default: 0.0. Formula: `result = (raw * scaling) + offset`.
>
> **DECIMAL\_PLACES** `integer`
>
> Number of decimal places in published value. Default: 2.

> **Filtering**
>
> **MIN\_VALUE** `double`
>
> Minimum allowed value. Values below this threshold are not published.
>
> **MAX\_VALUE** `double`
>
> Maximum allowed value. Values above this threshold are not published.
>
> **DEADBAND** `double`
>
> Minimum change required to publish a new value. Default: 0.0.

> **Publishing**
>
> **SOURCE\_TOPIC** `string`
>
> Topic where PLC values are published. Subscribe here to receive sensor data.
>
> **PUBLISH\_MODE** `string`
>
> Output format: `VALUE_ONLY` or `JSON`. Default: VALUE\_ONLY.
>
> **UNIT** `string`
>
> Engineering unit for documentation (e.g., °C, bar, RPM).
>
> **DESCRIPTION** `string`
>
> Human-readable description of the TAG.

> **Write Configuration**
>
> **WRITABLE** `boolean`
>
> Allow writing to this register/coil. Default: false.
>
> **DESTINATION\_TOPIC** `string`
>
> Topic to send write commands. Publish a value here to write it to the PLC.

> **Byte Ordering**
>
> **BYTE\_ORDER** `string`
>
> Byte order for multi-byte values: `BIGENDIAN` or `LITTLEENDIAN`. Default: BIGENDIAN.
>
> **WORD\_ORDER** `string`
>
> Word order for 32/64-bit values: `BIGENDIAN` or `LITTLEENDIAN`. Default: BIGENDIAN.
>
> > [!WARNING]
> >
> > Different manufacturers use different byte/word ordering. If values appear incorrect, try changing BYTE\_ORDER or WORD\_ORDER.

***

## Event-Based Operations

For on-demand Modbus operations (not polling), use the EVENT syntax. Publish a message to SOURCE\_TOPIC to trigger the operation; the route executes it and publishes the result to DESTINATION\_TOPIC.

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT ReadOnDemand
    WITH SOURCE_TOPIC "modbus/commands/read"
    WITH DESTINATION_TOPIC "modbus/responses/read"
    WITH QUERY "{operation: READ_HOLDING_REGISTERS, start_address: 0, count: 10}"
```

### Query Syntax

The `WITH QUERY` value uses LoT object syntax: unquoted keys, unquoted identifiers, and numeric values. The entire query is wrapped in double quotes.

| Element | Format | Example |
| - | - | - |
| Keys | Unquoted | `operation`, `start_address`, `count` |
| Operation values | Unquoted identifiers | `READ_HOLDING_REGISTERS`, `READ_COILS` |
| Numeric values | Unquoted | `0`, `10`, `100` |
| String values | Unquoted (addresses) | `address: 100` |

### Supported Operations

| Operation | Description |
| - | - |
| `READ_HOLDING_REGISTERS` | Read holding registers (FC03) |
| `READ_INPUT_REGISTERS` | Read input registers (FC04) |
| `READ_COILS` | Read coils (FC01) |
| `READ_DISCRETE_INPUTS` | Read discrete inputs (FC02) |
| `WRITE_SINGLE_REGISTER` | Write single holding register (FC06) |
| `WRITE_MULTIPLE_REGISTERS` | Write multiple holding registers (FC16) |
| `WRITE_SINGLE_COIL` | Write single coil (FC05) |
| `WRITE_MULTIPLE_COILS` | Write multiple coils (FC15) |

### Query Parameters by Operation

| Operation | Parameters | Example |
| - | - | - |
| `READ_HOLDING_REGISTERS` | `start_address`, `count` | `{operation: READ_HOLDING_REGISTERS, start_address: 0, count: 10}` |
| `READ_INPUT_REGISTERS` | `start_address`, `count` | `{operation: READ_INPUT_REGISTERS, start_address: 0, count: 5}` |
| `READ_COILS` | `start_address`, `count` | `{operation: READ_COILS, start_address: 0, count: 8}` |
| `READ_DISCRETE_INPUTS` | `start_address`, `count` | `{operation: READ_DISCRETE_INPUTS, start_address: 0, count: 8}` |
| `WRITE_SINGLE_REGISTER` | `address`, `value` | `{operation: WRITE_SINGLE_REGISTER, address: 100, value: 42}` |
| `WRITE_MULTIPLE_REGISTERS` | `start_address`, `values` | `{operation: WRITE_MULTIPLE_REGISTERS, start_address: 0, values: [100, 200]}` |
| `WRITE_SINGLE_COIL` | `address`, `value` | `{operation: WRITE_SINGLE_COIL, address: 0, value: true}` |
| `WRITE_MULTIPLE_COILS` | `start_address`, `values` | `{operation: WRITE_MULTIPLE_COILS, start_address: 0, values: [true, false, true]}` |

To write a value on demand, publish a message to SOURCE\_TOPIC. The route executes the write and publishes the result to DESTINATION\_TOPIC:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT WriteSetpoint
    WITH SOURCE_TOPIC "modbus/commands/write"
    WITH DESTINATION_TOPIC "modbus/responses/write"
    WITH QUERY "{operation: WRITE_SINGLE_REGISTER, address: 100, value: 42}"
```

***

## Complete Examples

**Basic Sensor Reading**

Read temperature and pressure from a Modbus device:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ProcessSensors WITH TYPE MODBUS_TCP
    ADD MODBUS_CONFIG
        WITH HOST "192.168.1.100"
        WITH PORT 502
        WITH SLAVE_ID 1
    ADD MAPPING Sensors
        WITH EVERY 1 SECOND
        ADD TAG Temperature
            WITH ADDRESS "100"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH SOURCE_TOPIC "process/temperature"
            WITH SCALING 0.1
            WITH UNIT "°C"
        ADD TAG Pressure
            WITH ADDRESS "102"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH SOURCE_TOPIC "process/pressure"
            WITH UNIT "bar"
```

**Multiple Polling Rates**

Group TAGs by update frequency:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PLCConnection WITH TYPE MODBUS_TCP
    ADD MODBUS_CONFIG
        WITH HOST "192.168.1.50"
        WITH PORT 502
        WITH SLAVE_ID 1
    
    ADD MAPPING FastLoop
        WITH EVERY 100 MILLISECONDS
        ADD TAG MotorSpeed
            WITH ADDRESS "200"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "UINT16"
            WITH SOURCE_TOPIC "plc/motor/speed"
        ADD TAG MotorCurrent
            WITH ADDRESS "201"
            WITH ADDRESS_TYPE "INPUT_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH SOURCE_TOPIC "plc/motor/current"
    
    ADD MAPPING SlowLoop
        WITH EVERY 5 SECONDS
        ADD TAG TotalRuntime
            WITH ADDRESS "300"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "UINT32"
            WITH SOURCE_TOPIC "plc/stats/runtime"
        ADD TAG FaultCode
            WITH ADDRESS "400"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "UINT16"
            WITH SOURCE_TOPIC "plc/status/fault"
```

**Bidirectional Control**

Read values and write setpoints:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ControlSystem WITH TYPE MODBUS_TCP
    ADD MODBUS_CONFIG
        WITH HOST "192.168.1.100"
        WITH PORT 502
        WITH SLAVE_ID 1
    ADD MAPPING ControlLoop
        WITH EVERY 500 MILLISECONDS
        ADD TAG ProcessValue
            WITH ADDRESS "100"
            WITH ADDRESS_TYPE "INPUT_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH SOURCE_TOPIC "control/pv"
            WITH PUBLISH_MODE "JSON"
        ADD TAG Setpoint
            WITH ADDRESS "200"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH SOURCE_TOPIC "control/sp"
            WITH WRITABLE "true"
            WITH DESTINATION_TOPIC "control/sp/set"
            WITH MIN_VALUE 0
            WITH MAX_VALUE 100
        ADD TAG OutputValue
            WITH ADDRESS "300"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH SOURCE_TOPIC "control/output"
```

**Coils and Digital I/O**

Read digital inputs and control outputs:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DigitalIO WITH TYPE MODBUS_TCP
    ADD MODBUS_CONFIG
        WITH HOST "192.168.1.75"
        WITH PORT 502
        WITH SLAVE_ID 1
    ADD MAPPING DigitalInputs
        WITH EVERY 100 MILLISECONDS
        ADD TAG StartButton
            WITH ADDRESS "0"
            WITH ADDRESS_TYPE "DISCRETE_INPUT"
            WITH DATA_TYPE "BOOL"
            WITH SOURCE_TOPIC "io/inputs/start"
        ADD TAG StopButton
            WITH ADDRESS "1"
            WITH ADDRESS_TYPE "DISCRETE_INPUT"
            WITH DATA_TYPE "BOOL"
            WITH SOURCE_TOPIC "io/inputs/stop"
        ADD TAG RunningLight
            WITH ADDRESS "0"
            WITH ADDRESS_TYPE "COIL"
            WITH DATA_TYPE "BOOL"
            WITH SOURCE_TOPIC "io/outputs/running"
            WITH WRITABLE "true"
            WITH DESTINATION_TOPIC "io/outputs/running/set"
```

**Combined (Cyclic + On-Demand)**

Continuous monitoring with on-demand read and write events in the same route:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE FullModbusSetup WITH TYPE MODBUS_TCP
    ADD MODBUS_CONFIG
        WITH HOST "192.168.1.100"
        WITH PORT 502
        WITH SLAVE_ID 1
    
    ADD MAPPING ProcessSensors
        WITH EVERY 500 MILLISECONDS
        ADD TAG Temperature
            WITH ADDRESS "100"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH SOURCE_TOPIC "process/temperature"
            WITH UNIT "°C"
        ADD TAG Pressure
            WITH ADDRESS "102"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH SOURCE_TOPIC "process/pressure"
            WITH UNIT "bar"
    
    ADD EVENT ReadRegisters
        WITH SOURCE_TOPIC "modbus/commands/read"
        WITH DESTINATION_TOPIC "modbus/responses/read"
        WITH QUERY "{operation: READ_HOLDING_REGISTERS, start_address: 0, count: 10}"
    
    ADD EVENT WriteSetpoint
        WITH SOURCE_TOPIC "modbus/commands/write"
        WITH DESTINATION_TOPIC "modbus/responses/write"
        WITH QUERY "{operation: WRITE_SINGLE_REGISTER, address: 200, value: 0}"
```

To trigger the on-demand read, publish any message to `modbus/commands/read`. The route reads registers 0-9 and publishes the result to `modbus/responses/read`. To write, publish to `modbus/commands/write`.

***

## Troubleshooting

> **Connection Timeout**
>
> * Verify IP address and port are correct
> * Check firewall settings (port 502 must be open)
> * Ensure device is powered on and connected to network
> * Try increasing `CONNECTION_TIMEOUT`

> **Incorrect Values**
>
> * Verify `ADDRESS` matches device documentation—try `WITH ONE_BASED_ADDRESSES true` if the manual labels registers starting at 1
> * Use Modicon notation (`400100`) when the datasheet shows prefixed addresses
> * Check `BYTE_ORDER` and `WORD_ORDER` settings
> * Confirm `DATA_TYPE` matches register size
> * Verify `SCALING` and `OFFSET` calculations

> **Write Operations Fail**
>
> * Ensure `WRITABLE` is set to "true"
> * Verify `ADDRESS_TYPE` is `HOLDING_REGISTER` or `COIL`
> * Check device permissions for write operations
> * Verify value is within `MIN_VALUE` and `MAX_VALUE`

> **Slave ID Errors**
>
> * Confirm correct `SLAVE_ID` (usually 1 for TCP devices)
> * Some gateways use different slave IDs for different devices
> * Check device documentation for correct unit ID

***

## Next Steps

> **[Modbus Serial Client](./modbus-serial-client)**
>
> Connect to RS-232/RS-485 Modbus RTU devices.

> **[Modbus TCP Server](./modbus-tcp-server)**
>
> Expose MQTT topics as Modbus registers for SCADA and HMIs.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.