# Modbus TCP

> Modbus registers and coils over TCP/IP, with typed addressing, byte-order control and a bounded network scan.

The Modbus TCP driver reads and writes Modbus registers and coils over TCP/IP —
PLCs, meters, gateways and any server speaking Modbus/TCP. Addressing is fully
typed: you pick a register class and a 0-based register number, and the app
assembles the wire request; there is no free-text address to get wrong.

## Device {#device}

| Field | Meaning | Default |
| --- | --- | --- |
| **Host** | DNS name, IPv4 or IPv6 address of the Modbus server or gateway. | `localhost` when left empty |
| **Port** | TCP port. | 502 (a configured 0 falls back to 502) |
| **Unit ID** | The Modbus unit (slave) id carried in every request — what a gateway uses to route to the device behind it. 0 to 255. | 1 |
| **Minimum transaction interval (ms)** | Compatibility pacing: the minimum silence after one transaction completes before the next starts, 0 to 1000. Zero adds no artificial delay. | 0 |
| **Poll interval (ms)** | Default polling rate for tags that do not override it. | 1000 |

The Base URI shown in the panel header is
`modbus+tcp://host:port/unitID`, plus `?minimumTransactionIntervalMs=n` when the
interval is set.

Devices that share a host and port also share one transaction gate: requests to
the same endpoint never overlap, and the **largest** configured minimum
transaction interval among the enabled devices on that endpoint paces all of
them. The panel says when another device raises the effective interval above the
one configured here. The interval governs only traffic this application emits —
it is a compatibility aid for a slow server or gateway, not a repair for network
faults.

Connecting waits at most 5 seconds; a response is waited on for at most 10
seconds; there are no protocol-level retries. The background retry loop owns
reconnection, so a device that is off costs nothing but a bad-quality reading.

## Tag addressing {#tag-addressing}

| Field | Meaning | Default |
| --- | --- | --- |
| **Register class** | Which Modbus entity the tag addresses: `HoldingRegister`, `InputRegister`, `Coil` or `DiscreteInput`. | Derived from type and access: Boolean tags get `DiscreteInput` (read-only) or `Coil` (writable); every other type gets `InputRegister` (read-only) or `HoldingRegister` (writable). |
| **Register number (0-based)** | The raw protocol address. This is the 0-based address on the wire, not the classic 4xxxx notation: holding register "40001" is register number 0. The register travels the wire as a 16-bit number, so the field takes 0 to 65535 and refuses anything wider where it is typed. | 0 |
| **Quantity (registers, blank = auto)** | How many 16-bit registers the value occupies. The box is drawn on String tags only: every other type has a fixed width, so there is nothing to override. | By type: Boolean 1, Int32 and Float 2, String 4 |
| **Most significant byte first** | Which byte of each 16-bit register comes first on the wire. Numeric tags only. | on (what the Modbus specification asks for) |
| **Most significant word first** | Which register of a two-register value carries the high half. Numeric tags only. | on |

The Source card shows the assembled wire address (for example `100?quantity=2`)
as a read-only preview, so what the panel shows is exactly what the request
carries.

### Value types and quantities {#value-types-and-quantities}

Four value types are offered, and the protocol width of each is fixed:

| Data type | Registers | Wire form |
| --- | --- | --- |
| Boolean | 1 | On a register class: any non-zero register reads true, and a write sends 1 or 0. On `Coil`/`DiscreteInput`: the bit itself. |
| Int32 | 2 | 32-bit signed integer across a register pair. |
| Float | 2 | IEEE 754 single precision across a register pair. |
| String | 1 or more (default 4) | UTF-8 bytes packed two per register. A read stops at the first NUL byte; a write must fit in the declared quantity (2 bytes per register) and is zero-padded. |

A String tag is read and written in the order the device holds the bytes, and a
write always sends UTF-8. Bytes that are not valid UTF-8, which is what a range
the device never initialised or text written in another encoding looks like,
read as the Unicode replacement character rather than failing: the tag shows
something visibly wrong, and every other tag on that device keeps reading.

**Quantity** is asked on String tags only. Boolean, Int32 and Float widths are
fixed, so the box is not drawn on those points, neither in the tag panel nor in the
tag grid; the override exists to size String tags.

### Byte and word order {#byte-and-word-order}

The two order boxes are drawn on numeric tags. They are independent questions:
which byte leads inside each register, and which register leads across a pair.
Modbus defines neither for text, so a String tag is not offered them: its bytes
travel in the order the device holds them. For the value `0x01020304`,
with `A` the most significant byte, the four layouts are:

| Most significant byte first | Most significant word first | Wire bytes |
| --- | --- | --- |
| on | on | `AB CD` (the specification default) |
| on | off | `CD AB` (the classic word swap) |
| off | on | `BA DC` |
| off | off | `DC BA` |

Reads and writes apply the same two answers in the same order, so a write lays
down exactly the bytes a read would have taken back.

### Function codes {#function-codes}

| Register class | Read | Write |
| --- | --- | --- |
| HoldingRegister | 3 (Read Holding Registers) | 6 (single register) or 16 (multiple registers) |
| InputRegister | 4 (Read Input Registers) | read-only; a write is refused |
| Coil | 1 (Read Coils) | 5 (Force Single Coil) |
| DiscreteInput | 2 (Read Discrete Inputs) | read-only; a write is refused |

A write returns success only when the server's protocol response confirms
acceptance; the cached value advances only then.

## Discovery {#discovery}

The scan is explicit and bounded — the app never infers or sweeps your local
subnet. The scope form takes:

| Field | Meaning | Default |
| --- | --- | --- |
| **IPv4 host or CIDR** | One address (`192.168.1.10`) or a subnet no broader than /24 (`192.168.1.0/24`). Network and broadcast addresses are skipped. | required |
| **Port** | The port to probe. | 502 |
| **Unit IDs** | Values, comma-separated lists or ascending ranges: `1`, `1,5,9`, `1, 3, 8-16` (0 to 255). | 1 |
| **Minimum transaction interval (ms)** | Pacing between completed probes on the same host, 0 to 1000. | 0 |

A single scan may send at most **256 probes** (hosts × unit ids); a larger plan is
refused before any socket opens. The only request sent is **Read Basic Device
Identification** (function 43/14) — discovery never reads registers or guesses a
register map. A fully valid Modbus exception response proves a unit is present
without claiming its identity; TCP reachability, timeouts and malformed frames do
not. Up to four hosts are scanned in parallel, sequentially within each host, with
750 ms connect and probe timeouts.

Results carry the complete endpoint and a suggested name built from the vendor
and product code when the device reports them. Candidates have zero tags —
registers are configured after **Add**. A unit that is already configured is
marked as such and offers **Open** instead of creating a duplicate.

## Limits worth knowing {#limits-worth-knowing}

- Wide integer types (Int64, unsigned widths) and Double are not offered: the
  wire codec round-trips exactly the four types above.
- One request is outstanding per endpoint at a time; many devices behind one
  gateway share that serialization by design.
- Writes address one coil or one register run at a time; there is no multi-coil
  write.
