> ## 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 Server Route

> Expose MQTT topics as Modbus TCP registers and coils so SCADA, HMIs, and Modbus masters connect to your Coreflux broker

## Modbus TCP Server

<Info>
  This page documents the **server** route (`MODBUS_SERVER`)—the broker **hosts** a Modbus TCP server and exposes MQTT topics as register and coil addresses. External masters connect to the broker over TCP. To connect the broker **to** a remote Modbus device instead, see [Modbus TCP Client](./modbus-tcp-client).
</Info>

The `MODBUS_SERVER` route makes your broker act as a **Modbus TCP server (slave)**. External masters—SCADA, HMIs, historians, PLCs—connect **to the broker** and read or write a declared register/coil address space that bridges bidirectionally to MQTT topics.

Use `MODBUS_TCP` when you need to **poll a device**. Use `MODBUS_SERVER` when OT systems need to **read and write** values that live on your MQTT bus—no separate gateway required.

<Tip>
  **Like a shop counter with a price list.** MQTT updates what's on the shelf (register values), and customers (Modbus masters) read the tags—or, for writable addresses, change items that publish back to MQTT.
</Tip>

## When to Use This Route

* **SCADA / HMI integration** — Let plant-floor tools poll broker data over standard Modbus without custom drivers.
* **OT/IT bridge** — Expose IoT sensor streams to masters that only speak Modbus.
* **Bidirectional control** — Writable tags let masters adjust setpoints; the broker publishes the new value to MQTT on a cadence you choose.
* **Client-only field devices** — Integrate master-only equipment into MQTT with no middleware between the broker and the bus.

***

The sections below cover data flow, configuration, persistence, and production patterns.

## Quick Start

<Tabs>
  <Tab title="Expose sensor values">
    Map an MQTT topic to a holding register masters can poll:

    ```lot wrap  theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE TempServer WITH TYPE MODBUS_SERVER
        ADD MAPPING Sensors
            ADD TAG Temperature
                WITH ADDRESS "100"
                WITH ADDRESS_TYPE "HOLDING_REGISTER"
                WITH DATA_TYPE "FLOAT"
                WITH SOURCE_TOPIC "plant/line1/temperature"
    ```

    Publish `23.5` to `plant/line1/temperature` → registers 100–101 hold the FLOAT; a master reading them gets `23.5`.
  </Tab>

  <Tab title="Writable setpoint">
    Allow a master to write a register; the broker publishes the value to MQTT within one `WITH EVERY` interval:

    ```lot wrap  theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE SetpointServer WITH TYPE MODBUS_SERVER
        ADD MAPPING Control
            WITH EVERY 500 MILLISECONDS
            WITH SOURCE_TOPIC "plant/line1"
            ADD TAG Setpoint
                WITH ADDRESS "200"
                WITH ADDRESS_TYPE "HOLDING_REGISTER"
                WITH DATA_TYPE "FLOAT"
                WITH WRITABLE "true"
                WITH DESTINATION_TOPIC "plant/line1/setpoint/written"
    ```

    MQTT publishes to `plant/line1/Setpoint` update registers 200–201. A master write surfaces on `plant/line1/setpoint/written` within 500 ms.
  </Tab>

  <Tab title="Full plant server">
    Listen on port 502 as unit 1 with multiple tags and connection settings:

    ```lot wrap  theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE PlantModbusServer WITH TYPE MODBUS_SERVER
        ADD MODBUS_SERVER_CONFIG
            WITH HOST "0.0.0.0"
            WITH PORT 502
            WITH UNIT_ID 1
            WITH MAX_CONNECTIONS 64
        ADD MAPPING ServerTags
            WITH EVERY 500 MILLISECONDS
            WITH SOURCE_TOPIC "plant/modbus"
            ADD TAG Temperature
                WITH ADDRESS "100"
                WITH ADDRESS_TYPE "HOLDING_REGISTER"
                WITH DATA_TYPE "FLOAT"
            ADD TAG Setpoint
                WITH ADDRESS "200"
                WITH ADDRESS_TYPE "HOLDING_REGISTER"
                WITH DATA_TYPE "FLOAT"
                WITH WRITABLE "true"
                WITH DESTINATION_TOPIC "plant/modbus/setpoint/set"
            ADD TAG Running
                WITH ADDRESS "0"
                WITH ADDRESS_TYPE "COIL"
                WITH DATA_TYPE "BOOL"
                WITH WRITABLE "true"
                WITH DESTINATION_TOPIC "plant/modbus/running/set"
    ```

    Masters read `Temperature`; `Setpoint` and `Running` are bidirectional—MQTT drives them and master writes publish to their `DESTINATION_TOPIC` within 500 ms.
  </Tab>

  <Tab title="Crash-resilient setpoints">
    Persist master-written setpoints across broker restarts:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE ResilientServer WITH TYPE MODBUS_SERVER
        ADD MODBUS_SERVER_CONFIG
            WITH PORT 502
            WITH UNIT_ID 1
            WITH PERSISTENCE true
        ADD MAPPING Control
            WITH EVERY 500 MILLISECONDS
            WITH SOURCE_TOPIC "plant/line1"
            ADD TAG Setpoint
                WITH ADDRESS "200"
                WITH ADDRESS_TYPE "HOLDING_REGISTER"
                WITH DATA_TYPE "FLOAT"
                WITH WRITABLE "true"
                WITH DESTINATION_TOPIC "plant/line1/setpoint/written"
            ADD TAG Temperature
                WITH ADDRESS "100"
                WITH ADDRESS_TYPE "INPUT_REGISTER"
                WITH DATA_TYPE "FLOAT"
    ```

    After a restart, a master reading `Setpoint` (200–201) gets the last value it wrote. `Temperature` (100–101) refreshes from the live MQTT source, not the snapshot.
  </Tab>
</Tabs>

***

## How Data Flows

```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
graph LR
    subgraph mqtt [MQTT]
        Topics[Topics]
    end

    subgraph broker [Coreflux Broker]
        Route[MODBUS_SERVER Route]
        Registers[Register / Coil Space]
    end

    subgraph ot [OT Masters]
        Master[SCADA / HMI / PLC]
    end

    Topics -->|publish sets value| Route
    Route --> Registers
    Registers -->|read / write| Master
    Master -->|write writable tag| Route
    Route -->|publish on EVERY cadence| Topics
```

Sync is **hybrid**—`WITH EVERY` controls outbound latency:

| Direction                      | Behavior                                                                                                                                                                                                                                                                                                    |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **MQTT → register (inbound)**  | A publish on a tag's `SOURCE_TOPIC` updates the backing register or coil immediately. Works for every address type, including read-only `INPUT_REGISTER` and `DISCRETE_INPUT`. Retained MQTT values are seeded at startup.                                                                                  |
| **register → MQTT (outbound)** | When a master writes a `WRITABLE` `HOLDING_REGISTER` or `COIL`, the broker detects the change on the mapping's `WITH EVERY` cadence and publishes the decoded value to the tag's `DESTINATION_TOPIC` (rounded per `DECIMAL_PLACES`, retained). A master write surfaces on MQTT within one cadence interval. |

A master write never loops back into the register from MQTT, and an MQTT-driven change is never re-published to `DESTINATION_TOPIC` (direction guard).

On server routes, route status **Connected** means the endpoint is **listening**—not that a remote session is established.

***

## Server Configuration

All transport settings live in the `MODBUS_SERVER_CONFIG` block. A valid route needs only `WITH TYPE MODBUS_SERVER` plus a mapping with one tag—it listens on `0.0.0.0:502` as unit 1 by default.

```lot wrap focus={2-6} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantModbusServer WITH TYPE MODBUS_SERVER
    ADD MODBUS_SERVER_CONFIG
        WITH HOST "0.0.0.0"
        WITH PORT 502
        WITH UNIT_ID 1
        WITH MAX_CONNECTIONS 64
```

### Connection parameters

<ParamField path="HOST" type="string" default="0.0.0.0">
  Bind interface to listen on (`0.0.0.0` = all interfaces). `IP` is accepted as an alias.
</ParamField>

<ParamField path="PORT" type="integer" default="502">
  TCP listen port. Each server route claims its port exclusively—a second route on the same port is refused.
</ParamField>

<ParamField path="MAX_CONNECTIONS" type="integer" default="64">
  Best-effort soft cap on concurrent master connections. See [Connection-limit behavior](#connection-limit-behavior).
</ParamField>

<ParamField path="UNIT_ID" type="integer" default="1">
  Modbus unit ID the broker answers as (1–247). `SLAVE_ID` is accepted as an alias.
</ParamField>

<ParamField path="PERSISTENCE" type="boolean" default="false">
  Persist served values across broker restarts. See [Data persistence & recovery](#data-persistence--recovery).
</ParamField>

<ParamField path="ONE_BASED_ADDRESSES" type="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 [Address notation](#address-notation).
</ParamField>

<Note>
  Modbus has no native authentication—there is no credential configuration. Bind to a specific interface and restrict access at the network layer; MQTT-side access is governed by HUB topic permissions. Use `GET ENV` / `GET SECRET` for environment-derived values (for example `WITH PORT GET ENV "MODBUS_PORT"`).
</Note>

***

## Tag and Mapping Configuration

Each exposed register or coil is declared as an `ADD TAG`. Only declared addresses are served—reads or writes to undeclared addresses return a Modbus **Illegal Data Address** exception.

```lot wrap focus={7-20} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantModbusServer WITH TYPE MODBUS_SERVER
    ADD MODBUS_SERVER_CONFIG
        WITH HOST "0.0.0.0"
        WITH PORT 502
        WITH UNIT_ID 1
        WITH MAX_CONNECTIONS 64
    ADD MAPPING ServerTags
        WITH EVERY 500 MILLISECONDS
        WITH SOURCE_TOPIC "modbus/server"
        ADD TAG Setpoint
            WITH ADDRESS "200"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH SOURCE_TOPIC "modbus/server/setpoint"
            WITH WRITABLE "true"
            WITH DESTINATION_TOPIC "modbus/server/setpoint/set"
            WITH BYTE_ORDER "BIGENDIAN"
            WITH WORD_ORDER "BIGENDIAN"
            WITH DECIMAL_PLACES 2
            WITH DESCRIPTION "Operator setpoint"
```

### Server vs. client semantics

| Setting             | Client (`MODBUS_TCP`)        | Server (`MODBUS_SERVER`)                    |
| ------------------- | ---------------------------- | ------------------------------------------- |
| Role                | Master—dials out to a device | Slave—listens for masters                   |
| `HOST` / `IP`       | Device IP to dial            | **Bind** interface to listen on             |
| `SOURCE_TOPIC`      | Register value → MQTT (read) | MQTT → register (write into address space)  |
| `DESTINATION_TOPIC` | MQTT → device write          | Register → MQTT when a master writes        |
| `EVERY`             | Poll interval                | Outbound sync cadence (master write → MQTT) |
| Transport           | TCP / Serial (RTU)           | TCP                                         |

### Modbus address banks

| `ADDRESS_TYPE`     | Bank              | Element     | Master access | Function codes |
| ------------------ | ----------------- | ----------- | ------------- | -------------- |
| `HOLDING_REGISTER` | Holding registers | 16-bit word | read / write  | 3, 6, 16       |
| `INPUT_REGISTER`   | Input registers   | 16-bit word | read-only     | 4              |
| `COIL`             | Coils             | 1 bit       | read / write  | 1, 5, 15       |
| `DISCRETE_INPUT`   | Discrete inputs   | 1 bit       | read-only     | 2              |

### Address notation

Tag `ADDRESS` values are converted to the **PDU offset** the broker exposes on the wire. External Modbus masters always use zero-based PDU addresses in their read/write requests—the `ONE_BASED_ADDRESSES` flag only affects how you label tags in LoT, not how masters address the server.

Three styles are supported (same rules as the [Modbus TCP Client](./modbus-tcp-client#modbus-addressing) route):

| Style                      | Config                      | Tag address for PDU 0 | Tag address for PDU 99 |
| -------------------------- | --------------------------- | --------------------- | ---------------------- |
| Zero-based plain (default) | `ONE_BASED_ADDRESSES false` | `"0"`                 | `"99"`                 |
| One-based plain            | `ONE_BASED_ADDRESSES true`  | `"1"`                 | `"100"`                |
| Modicon holding register   | either                      | `"400001"`            | `"400100"`             |

```lot wrap focus={2-12} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantModbusServer WITH TYPE MODBUS_SERVER
    ADD MODBUS_SERVER_CONFIG
        WITH PORT 502
        WITH UNIT_ID 1
        WITH ONE_BASED_ADDRESSES true
    ADD MAPPING ServerTags
        WITH SOURCE_TOPIC "plant/modbus"
        ADD TAG Temperature
            WITH ADDRESS "1"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
```

<Note>
  Modicon-style addresses are decoded the same way regardless of `ONE_BASED_ADDRESSES`. The flag only affects plain numeric addresses.
</Note>

### Tag parameters

<ParamField path="ADDRESS" type="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+`). Multi-word types occupy consecutive PDU addresses from the resolved offset. See [Address notation](#address-notation).
</ParamField>

<ParamField path="ADDRESS_TYPE" type="string" required>
  Which bank the tag lives in: `HOLDING_REGISTER`, `INPUT_REGISTER`, `COIL`, or `DISCRETE_INPUT`.
</ParamField>

<ParamField path="DATA_TYPE" type="string" default="INT16">
  Wire type: `BOOL`, `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64`, `UINT64`, `FLOAT`, `DOUBLE`, or `STRING`. `INT32`/`UINT32`/`FLOAT` span 2 registers; `INT64`/`UINT64`/`DOUBLE` span 4.
</ParamField>

<ParamField path="SOURCE_TOPIC" type="string">
  MQTT topic whose publishes set this register or coil. Defaults to `{mapping SOURCE_TOPIC}/{TagName}`.
</ParamField>

<ParamField path="WRITABLE" type="boolean" default="false">
  Allow masters to write the address. Only `HOLDING_REGISTER` and `COIL` are master-writable.
</ParamField>

<ParamField path="DESTINATION_TOPIC" type="string">
  Topic published to when a master writes this (writable) address.
</ParamField>

<ParamField path="BYTE_ORDER" type="string" default="BIGENDIAN">
  Byte order within each register for multi-byte types: `BIGENDIAN` or `LITTLEENDIAN`.
</ParamField>

<ParamField path="WORD_ORDER" type="string" default="BIGENDIAN">
  Word order across registers for 32-/64-bit types (high word first by default): `BIGENDIAN` or `LITTLEENDIAN`.
</ParamField>

<ParamField path="DECIMAL_PLACES" type="integer" default="2">
  Rounding applied to `FLOAT`/`DOUBLE` values published to `DESTINATION_TOPIC`.
</ParamField>

<ParamField path="PERSISTENCE" type="boolean">
  Per-tag override of the route persistence default. Precedence: tag → mapping → route. See [Data persistence & recovery](#data-persistence--recovery).
</ParamField>

<ParamField path="DESCRIPTION" type="string">
  Free-text documentation for the tag.
</ParamField>

<ParamField path="EVERY" type="duration" default="1 second">
  On the mapping block: register → MQTT sync cadence for master writes.
</ParamField>

### All four address types

One server exposing every bank type:

```lot wrap focus={4-22} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MixedServer WITH TYPE MODBUS_SERVER
    ADD MAPPING Points
        WITH SOURCE_TOPIC "plant/mixed"
        ADD TAG Pressure
            WITH ADDRESS "100"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
        ADD TAG MotorSpeed
            WITH ADDRESS "50"
            WITH ADDRESS_TYPE "INPUT_REGISTER"
            WITH DATA_TYPE "UINT16"
        ADD TAG PumpEnable
            WITH ADDRESS "0"
            WITH ADDRESS_TYPE "COIL"
            WITH DATA_TYPE "BOOL"
            WITH WRITABLE "true"
            WITH DESTINATION_TOPIC "plant/mixed/pump/cmd"
        ADD TAG DoorClosed
            WITH ADDRESS "0"
            WITH ADDRESS_TYPE "DISCRETE_INPUT"
            WITH DATA_TYPE "BOOL"
```

All four points are fed from `plant/mixed/<TagName>`; only `PumpEnable` accepts master writes (→ `plant/mixed/pump/cmd`).

***

## Protocol Conformance

This server implements Modbus Application Protocol **Conformance Class 0 and Class 1** over Modbus/TCP—the interoperability bar for standard masters:

| Class   | Function codes                                                                                                                 | Supported |
| ------- | ------------------------------------------------------------------------------------------------------------------------------ | --------- |
| Class 0 | FC 03 Read Holding Registers, FC 16 Write Multiple Registers                                                                   | ✅         |
| Class 1 | FC 01 Read Coils, FC 02 Read Discrete Inputs, FC 04 Read Input Registers, FC 05 Write Single Coil, FC 06 Write Single Register | ✅         |
| Class 2 | FC 15 Write Multiple Coils                                                                                                     | ✅         |

Writes are accepted only on `WRITABLE` `HOLDING_REGISTER`/`COIL` addresses. Undeclared addresses and writes to read-only banks return exception **02 (Illegal Data Address)**.

<Note>
  Modbus/TCP is **plaintext**—the protocol has no native authentication or encryption. Secure deployments by binding to a specific interface and restricting access at the network layer (firewall, VLAN, VPN). Modbus/TCP Security (TLS on port 802) is not yet implemented.
</Note>

***

## Data Persistence & Recovery

By default a server route holds register and coil values **in memory only**—a broker restart resets every address to zero. Set `WITH PERSISTENCE true` on `MODBUS_SERVER_CONFIG` to snapshot served values and restore them on startup.

### Ownership-aware policy

What is safe to persist depends on who owns the value:

| Bank                                                        | Owner                                           | Default           | On restart                                                     |
| ----------------------------------------------------------- | ----------------------------------------------- | ----------------- | -------------------------------------------------------------- |
| `HOLDING_REGISTER` / `COIL`, `WRITABLE true`                | Master-written setpoint lives only in the slave | **Persisted**     | Restored **authoritatively**—wins over retained MQTT           |
| `INPUT_REGISTER` / `DISCRETE_INPUT`, and non-writable banks | Fed from live MQTT                              | **Not persisted** | Recovered from **live MQTT** (retained message / next publish) |

Master-written setpoints survive a restart; live telemetry always reflects the current MQTT source.

### Per-tag overrides

`WITH PERSISTENCE true|false` on an individual `ADD TAG` overrides the route default:

```lot wrap  theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OverrideServer WITH TYPE MODBUS_SERVER
    ADD MODBUS_SERVER_CONFIG
        WITH PORT 502
        WITH PERSISTENCE true
    ADD MAPPING Registers
        WITH SOURCE_TOPIC "plant/line1"
        ADD TAG Setpoint
            WITH ADDRESS "100"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH WRITABLE "true"
        ADD TAG LastGoodTemp
            WITH ADDRESS "200"
            WITH ADDRESS_TYPE "INPUT_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH PERSISTENCE "true"
        ADD TAG ScratchSetpoint
            WITH ADDRESS "300"
            WITH ADDRESS_TYPE "HOLDING_REGISTER"
            WITH DATA_TYPE "INT16"
            WITH WRITABLE "true"
            WITH PERSISTENCE "false"
```

* `Setpoint` — restored authoritatively to the last master-written value.
* `LastGoodTemp` — served from snapshot until `plant/line1/LastGoodTemp` next publishes.
* `ScratchSetpoint` — always comes back at zero.

### Caching slow or infrequently-updated sources

Broker-fed tags are not persisted by default. When a source updates infrequently—an hourly energy total, a shift-latched value—force-persist with `WITH PERSISTENCE "true"` so the last received value is re-served on cold start until the next live publish overrides it.

```lot wrap focus={6-12} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MeterServer WITH TYPE MODBUS_SERVER
    ADD MODBUS_SERVER_CONFIG
        WITH PORT 502
        WITH PERSISTENCE true
    ADD MAPPING Meters
        WITH SOURCE_TOPIC "plant/energy"
        ADD TAG TotalEnergy
            WITH ADDRESS "100"
            WITH ADDRESS_TYPE "INPUT_REGISTER"
            WITH DATA_TYPE "FLOAT"
            WITH PERSISTENCE "true"
```

If every tag in a mapping is a slow source, set `WITH PERSISTENCE "true"` once on the `ADD MAPPING` block instead of per tag.

### Storage & durability

* Values are written through a non-blocking background worker on every change (rapid updates coalesce to the latest value).
* The snapshot lives in an encrypted-at-rest database under the broker data directory.
* A snapshot is keyed to the route's tag definitions by a fingerprint. If addresses or types change between runs, the stale snapshot is discarded rather than replayed into reshaped addresses.

***

## Verify Your Server

<Steps>
  <Step title="Deploy the route">
    Add the route definition to your broker and confirm route status shows **Connected** (the endpoint is listening).
  </Step>

  <Step title="Connect with a Modbus master">
    Use any standards-compliant Modbus TCP master. Connect to `<HOST>:<PORT>` and address the route's `UNIT_ID` (default 1).
  </Step>

  <Step title="Confirm MQTT → register">
    Publish a test value to a bound `SOURCE_TOPIC`. Read the corresponding address from the master and confirm the decoded value matches.
  </Step>

  <Step title="Confirm register → MQTT">
    For a writable tag with a `DESTINATION_TOPIC`, write a value from the master. Subscribe to the destination topic with MQTT Explorer or any MQTT client and confirm the publish arrives within one `WITH EVERY` interval.
  </Step>
</Steps>

***

## Limitations

<Warning>
  Each `MODBUS_SERVER` route binds exclusively to its `PORT`. A second route on the same port fails to start. Give each route its own port.
</Warning>

***

## Troubleshooting

Publish `-checkRouteConnection <RouteName>` to `$SYS/Coreflux/Command`. For a server route this reports whether the hosted endpoint is **listening**, along with the endpoint, port, unit ID, and current connection count.

<AccordionGroup>
  <Accordion title="Route status never Connected">
    * **Port in use** — Another route or process owns `PORT`. Choose a free port or stop the conflicting service.
    * **Bind error** — Check broker logs. Ensure `HOST` is a valid interface on the machine.
  </Accordion>

  <Accordion title="Master cannot connect">
    Confirm `HOST`/`PORT`, network path, and firewall rules. The master must target the route's `UNIT_ID` (default 1).
  </Accordion>

  <Accordion title="Master reads zero or stale values">
    Registers default to zero until first set. Verify `SOURCE_TOPIC` has published; retained MQTT values are seeded at startup.
  </Accordion>

  <Accordion title="Master write not appearing on MQTT">
    * Confirm the tag is `WRITABLE` on a `HOLDING_REGISTER` or `COIL` with a `DESTINATION_TOPIC`.
    * The value surfaces within one `WITH EVERY` interval—verify the cadence.
  </Accordion>

  <Accordion title="Write rejected (Modbus exception)">
    Read-only banks (`INPUT_REGISTER`, `DISCRETE_INPUT`) and non-writable tags reject master writes by design. Undeclared addresses return Illegal Data Address.
  </Accordion>

  <Accordion title="Wrong numeric value">
    Align `BYTE_ORDER` and `WORD_ORDER` with the master's expectation. Default is big-endian, high word first.
  </Accordion>

  <Accordion title="Address mismatch (Illegal Data Address)">
    Confirm tag `ADDRESS` values match how the master addresses the server. Use `WITH ONE_BASED_ADDRESSES true` when your tag labels follow device-manual numbering (register 1 = address `"1"`). Modicon notation (`400100`) works regardless of the flag.
  </Accordion>

  <Accordion title="Connection-limit behavior">
    `MAX_CONNECTIONS` is a **best-effort** soft cap—the underlying library exposes no per-connection control, so exceeding the cap is logged rather than enforced at accept time. Restrict concurrency at the network layer if a hard cap is required.
  </Accordion>
</AccordionGroup>

***

## Best Practices

<AccordionGroup>
  <Accordion title="Bind to a specific interface in production">
    Prefer `WITH HOST "192.168.10.5"` over `0.0.0.0` when only one network should reach the server.
  </Accordion>

  <Accordion title="Keep environment values out of route text">
    Use `WITH PORT GET ENV "MODBUS_PORT"` instead of hard-coding environment-specific ports.
  </Accordion>

  <Accordion title="Declare each exposed address explicitly">
    Only declared addresses are served. One `ADD TAG` per register or coil a master needs.
  </Accordion>

  <Accordion title="Make tags WRITABLE only when needed">
    Leave `WRITABLE` unset for read-only points. Non-writable holding registers and read-only banks reject writes with a Modbus exception.
  </Accordion>

  <Accordion title="Match byte and word order to the master">
    Set `WORD_ORDER "LITTLEENDIAN"` when the master expects low word first. Default is big-endian, high word first.
  </Accordion>

  <Accordion title="Size WITH EVERY to outbound latency">
    Use a shorter interval for responsive control loops; a longer one to reduce churn for slowly-changing values.
  </Accordion>

  <Accordion title="Reserve ports per route">
    Document which route owns which port to avoid startup conflicts when adding routes.
  </Accordion>

  <Accordion title="Restrict access at the network layer">
    Modbus TCP has no authentication. Place the server behind a firewall or VLAN and rely on HUB topic permissions for MQTT-side security.
  </Accordion>

  <Accordion title="Monitor route status">
    Watch `$SYS/Coreflux/Routes/{RouteName}/status`. `Connected` means the server is listening—investigate `Error` immediately (usually a port conflict).
  </Accordion>
</AccordionGroup>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Modbus TCP Client" icon="network-wired" href="./modbus-tcp-client">
    Connect to remote Modbus devices and poll register data into MQTT.
  </Card>

  <Card title="Modbus Serial Client" icon="plug" href="./modbus-serial-client">
    Connect to remote Modbus RTU devices over serial.
  </Card>
</CardGroup>
