> For the complete documentation index, see [llms.txt](https://wiki.inels.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wiki.inels.com/inels-bus/inels-design-manager-idm3/linking-two-inels-central-units-over-modbus-ip.md).

# Tutorial 11 — linking two central units over Modbus IP

*Part 2 — Tutorials. Assumes* [*Chapter 32*](/inels-bus/inels-design-manager-idm3/idm3-modbus-rtu-and-ip-integration.md)*.*

***

## What you will build

Two separate iNELS installations, each with its own central unit, talking to each other over the network. A button on **CU-A** switches a light wired to **CU-B**.

By the end you will be able to share any value — a switch state, a temperature, a mode — in either direction between central units.

**This tutorial assumes you have never configured Modbus before.** Every step says what to type and why it matters.

***

## 33.1 Why you would do this

A single central unit has limits — how many units on the bus, how much current, how many wires. Large sites therefore end up with more than one CU: one per floor, one per building, one for the plant room.

Left alone, those are separate installations that know nothing about each other. Modbus IP is what joins them, over the network cable that is already there.

Typical reasons:

| Need                                        | Example                                  |
| ------------------------------------------- | ---------------------------------------- |
| One button controls something on another CU | Reception switches the car-park lights   |
| Share a measured value                      | One outdoor sensor feeds every building  |
| A global mode                               | "Holiday" set once, obeyed everywhere    |
| Central override                            | A master "all off" across the whole site |
| Report a state upward                       | Plant-room alarm shown on the main CU    |

> **This is the same mechanism as talking to third-party equipment.** The other CU is simply another Modbus device. Nothing new to learn beyond [Chapter 32](/inels-bus/inels-design-manager-idm3/idm3-modbus-rtu-and-ip-integration.md) — the only twist is that you configure both ends yourself.

***

## 33.2 The idea in one picture

One CU **offers** values. The other **fetches** them. The link is a numbered pigeonhole — a **register** — that both sides agree on.

```
        CU-A  (MASTER — asks)                 CU-B  (SLAVE — answers)
        the one doing the controlling         the one being controlled

   ┌──────────────────────────────┐      ┌──────────────────────────────┐
   │ Modbus-IP-Master             │      │ Modbus-IP-Slave              │
   │  └ Modbus-IP-Master-Unit     │      │  Port: 502                   │
   │     IP:   192.168.1.11 ──────┼──────┼─▶ (listening)                │
   │     Port: 502                │      │                              │
   │     └ Modbus-Device1         │      │  └ Modbus-IP-Slave-Unit      │
   │        Register address: 0 ══╪══════╪═▶   └ Modbus-Device1         │
   │        Holding Reg 16-bit    │      │         Register address: 0  │
   │              │               │      │         Holding Reg 16-bit   │
   └──────────────┼───────────────┘      └──────────────┼───────────────┘
                  │                                     │
            wire from a button                    wire to a light
            on CU-A                               on CU-B
```

**Register 0 is the pigeonhole.** CU-A writes into it; CU-B reads out of it. They must both use the **same register number**, or nothing happens.

### The three rules

Everything that goes wrong with CU-to-CU Modbus comes from breaking one of these:

| Rule                                          | Why                                                                  |
| --------------------------------------------- | -------------------------------------------------------------------- |
| **1. Same register number on both sides**     | Different numbers = different pigeonholes. No error is shown.        |
| **2. Same data type on both sides**           | One side reading 16-bit while the other writes 32-bit gives nonsense |
| **3. Same Value specification on both sides** | Scale on one side only and the value is out by 10 or 100             |

Write these three down for every value you link. That table *is* your register map.

> **Use `Read-Write-Holding-Register-UInt16` for everything.** It is the iDM3 default, it carries both on/off (as `0` and `1`) and numbers (0–65535), and it is readable and writable. Mixing coils and registers works, but doubles the ways you can mismatch the two ends. One type everywhere is the easier system to hand over.

***

## 33.3 Before you start

|                               |                                                                                                                                   |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Two central units             | Any models — Modbus IP works on all of them ([§32.3](/inels-bus/inels-design-manager-idm3/idm3-modbus-rtu-and-ip-integration.md)) |
| Both on the same network      | Each must be able to reach the other's IP                                                                                         |
| **Static IP addresses**       | See the warning below                                                                                                             |
| A project for each            | They are separate `.elp` files, edited separately                                                                                 |
| Firmware supporting Modbus IP | [Chapter 25](/inels-bus/inels-design-manager-idm3/idm3-firmware-update-manager.md)                                                |

> **Give both central units static IP addresses before you start.** If CU-B gets its address from DHCP, that address can change — and the moment it does, CU-A is pointing at nothing. The link dies silently, with no error anywhere. Set a static address in **CU configuration** ([Chapter 22](/inels-bus/inels-design-manager-idm3/idm3-central-unit-configuration.md)), or a permanent DHCP reservation on the router. This is the single most common cause of a CU-to-CU link that "worked last month".

**Our example:**

|          | Role                    | IP address     |
| -------- | ----------------------- | -------------- |
| **CU-A** | Master — has the button | `192.168.1.10` |
| **CU-B** | Slave — has the light   | `192.168.1.11` |

***

## 33.4 Plan the register map first

Do this on paper before touching iDM3. Two minutes here saves an afternoon later.

| Register | Direction | Carries                              | Data type               | Scaling  |
| -------- | --------- | ------------------------------------ | ----------------------- | -------- |
| `0`      | A → B     | Hall light command (0 = off, 1 = on) | Holding Register UInt16 | Not-Used |

That is the whole map for this tutorial. On a real site it grows:

| Register    | Direction | Carries              | Data type               | Scaling            |
| ----------- | --------- | -------------------- | ----------------------- | ------------------ |
| `0`–`9`     | A → B     | commands from A to B | Holding Register UInt16 | Not-Used           |
| `10`–`19`   | B → A     | status back from B   | Holding Register UInt16 | Not-Used           |
| `100`–`109` | B → A     | temperatures ×10     | Holding Register UInt16 | Multiplication 10x |

> **Leave gaps and group by direction.** Reserving `0–9` for one direction and `10–19` for the other means adding a value later never renumbers anything. Renumbering means editing both projects and re-uploading both CUs.

> **One register carries one direction.** If you want A to control B *and* B to report back, use **two** registers — one for each way. Pointing both CUs at the same register to write is how you get two systems fighting over one value.

***

## 33.5 Part 1 — Configure CU-B, the slave

The slave is the CU that **owns the light** and waits to be told what to do.

Open **CU-B's project**.

### Step 1 — Add the Modbus IP slave module

1. **Project** ribbon tab → **Device manager**.
2. Select the **central unit** at the top of the tree.
3. Click **New slave**.
4. Choose **`Modbus-IP-Slave`** → **OK**.

> **"New slave" here means "add a module under the CU"** — it is the ordinary Device manager button from [Chapter 5](/inels-bus/inels-design-manager-idm3/tutorial-your-first-idm3-project.md), nothing to do with Modbus slave role. The coincidence of wording is unfortunate. You use **New slave** to add Modbus *master* modules too.

### Step 2 — Set the port

Select the `Modbus-IP-Slave` module. In **Parameters** there is one setting:

| Parameter | Set it to |
| --------- | --------- |
| **Port:** | `502`     |

**502 is the standard Modbus TCP port.** Use it unless something else on the CU already occupies it. Whatever you choose here must be typed identically on CU-A later.

Give it a **Description** — `Modbus link to CU-A`.

### Step 3 — Add the slave unit

1. **Select the `Modbus-IP-Slave` module.**
2. Click **New unit**.
3. Choose **`Modbus-IP-Slave-Unit`** → **OK**.
4. Describe it — `Link registers`.

This unit has **no parameters of its own**. It is a container for the registers. An empty Parameters panel here is correct, not a fault.

### Step 4 — Create the register

The unit arrives holding a single **`Modbus-Device1`**. **One `Modbus-Device` is one register** — not one device. That naming trips up everyone; see [§32.5](/inels-bus/inels-design-manager-idm3/idm3-modbus-rtu-and-ip-integration.md).

Select `Modbus-Device1` and set its three parameters:

| Parameter                | Set it to                            | Why                                  |
| ------------------------ | ------------------------------------ | ------------------------------------ |
| **Register address:**    | `0`                                  | The pigeonhole number, from your map |
| **Data type:**           | `Read-Write-Holding-Register-UInt16` | 16-bit, readable and writable        |
| **Value specification:** | `Not-Used`                           | An on/off command needs no scaling   |

Describe it — `Hall light command`.

Need more values? Add another `Modbus-Device` for each, with the next register number.

### Step 5 — Wire the register to the light

**This is the step that makes it real, and the one people miss.** So far you have a register sitting in memory connected to nothing.

The Modbus register is an ordinary object in your project. Wire it exactly as you would a button ([Chapter 2](/inels-bus/inels-design-manager-idm3/how-programming-works-in-idm3.md)).

1. **Design** workspace tab → **Controls** ribbon tab.
2. Drag an object onto the floor plan and **Connect** it to `Modbus-Device1`. Name it `Cmd from CU-A`.
3. Confirm the yellow dot has gone.
4. **Function** workspace tab → **Functions** ribbon tab → **Add connection**.
5. Drag from **`Cmd from CU-A`** → the **hall light output**. Switch **Add connection** off.
6. **Functions** ribbon tab → **Wire manager** → select the new wire → name it `Hall light from CU-A` → click **+** twice:

| Action                   | User function  | Result                               |
| ------------------------ | -------------- | ------------------------------------ |
| `Analog in value change` | `DIGITAL_COPY` | The light follows the register value |

Or, if you prefer explicit control over the two states, use two functions with conditions:

| Action                   | Condition                                    | User function |
| ------------------------ | -------------------------------------------- | ------------- |
| `Analog in value change` | **Restriction value** — the register `=` `1` | `DIGITAL_ON`  |
| `Analog in value change` | **Restriction value** — the register `=` `0` | `DIGITAL_OFF` |

The copy approach is shorter; the conditional approach is clearer to read six months later and extends naturally when you add more values. Either is correct.

7. **Save**, then **Save to central unit**.

> **Why an&#x20;*****analog*****&#x20;action for an on/off command?** A 16-bit holding register is an **analog object** in iDM3, whatever you use it to carry. So it raises `Analog in value change` and is written with analog functions — even when the only values you ever put in it are `0` and `1`. That surprises people; it is correct.

**CU-B is done.** It is now listening on port 502 and will act on whatever arrives in register 0.

***

## 33.6 Part 2 — Configure CU-A, the master

The master is the CU with **the button**, and the one that does the asking.

Open **CU-A's project** — a different `.elp` file.

### Step 1 — Add the Modbus IP master module

1. **Device manager** → select the **central unit**.
2. **New slave** → **`Modbus-IP-Master`** → **OK**.
3. Describe it — `Modbus link to CU-B`.

This module has **no parameters at all**. Nothing to configure at this level — the connection details live on the unit beneath it. An empty Parameters panel is expected.

### Step 2 — Add the unit and point it at CU-B

1. **Select the `Modbus-IP-Master` module.**
2. **New unit** → **`Modbus-IP-Master-Unit`** → **OK**.
3. Describe it — `CU-B`.
4. In **Parameters**, set the two connection settings:

| Parameter       | Set it to      | Must match                                               |
| --------------- | -------------- | -------------------------------------------------------- |
| **IP Address:** | `192.168.1.11` | CU-B's actual IP address                                 |
| **Port:**       | `502`          | The **Port:** you set on CU-B's `Modbus-IP-Slave` module |

> **One unit = one remote central unit.** The IP and port are entered **once**, here — not on every register. If you later link to a third CU, that is a second `Modbus-IP-Master-Unit` under the same module, with its own IP.

### Step 3 — Create the matching register

Select the unit's **`Modbus-Device1`** and set the same three parameters as on CU-B:

| Parameter                | Set it to                            | Must match CU-B   |
| ------------------------ | ------------------------------------ | ----------------- |
| **Register address:**    | `0`                                  | **Yes — exactly** |
| **Data type:**           | `Read-Write-Holding-Register-UInt16` | **Yes — exactly** |
| **Value specification:** | `Not-Used`                           | **Yes — exactly** |

Describe it — `Hall light cmd to CU-B`.

> **Check these three against CU-B now, character by character.** A mismatch produces no error message on either central unit. The link simply does nothing, and you will spend an hour looking at wiring that is perfectly fine.

### Step 4 — Wire the button to the register

Same idea as CU-B, in the opposite direction: the register is now a **consumer**.

1. **Design** tab → place an object, **Connect** it to `Modbus-Device1`, name it `Cmd to CU-B`.
2. **Function** tab → **Add connection** → drag from your **button** → **`Cmd to CU-B`**.
3. **Wire manager** → name it `Hall light to CU-B` → **+**:

| Action       | User function    | Effect               |
| ------------ | ---------------- | -------------------- |
| `Short down` | `MODBUS_WRITE_1` | Writes 1 → light on  |
| `Long down`  | `MODBUS_WRITE_0` | Writes 0 → light off |

Define those two in **Function manager** ([Tutorial 2 §3](/inels-bus/inels-design-manager-idm3/tutorial-toggles-impulses-and-delays.md)):

|                      | `MODBUS_WRITE_1`   | `MODBUS_WRITE_0`   |
| -------------------- | ------------------ | ------------------ |
| **Function type:**   | `Analog`           | `Analog`           |
| **Select function:** | `Analog set level` | `Analog set level` |
| **Analog level**     | `1`                | `0`                |

> **Use `Analog` functions, not `System` ones.** The register is an analog object, so `Analog set level` is what writes to it. `Sys int set` is for system integers — a different thing, despite both holding numbers.

4. **Save**, then **Save to central unit**.

> **Check what actually lands in the register.** For `0` and `1` this is straightforward. If you later write larger numbers — a setpoint, a mode — watch the value in **Monitor** on CU-B and confirm it matches what you set, rather than assuming. Analog levels are conventionally a percentage, and how that maps onto a 0–65535 register is worth seeing once with your own eyes before you rely on it.

***

## 33.7 Part 3 — Test it

Do this in order. Each step isolates a different failure.

### 1. Can they see each other?

From a PC on the same network, ping CU-B's address. If that fails, stop — it is a network problem, not an iNELS one. Check the IP, the subnet, VLANs and any firewall between them.

### 2. Watch the register on CU-A

Connect iDM3 to **CU-A** and open **Managers → Monitor**. Find your Modbus object.

Press the button. **The value should change to 1.**

* **It changes** → CU-A is writing correctly. Move on.
* **It does not** → the fault is on CU-A: the wire, the function, or a yellow dot on the object. Nothing to do with Modbus yet.

### 3. Watch the register on CU-B

Now connect iDM3 to **CU-B** and open **Monitor**. Press the button on CU-A again.

* **The value appears** → the link works. Any remaining problem is CU-B's wire to the light.
* **Nothing arrives** → the link itself is broken. Work through §33.8.

### 4. The light

Press the button. The light on CU-B switches.

> **Expect a short delay.** The master polls; it does not receive instant notifications. A fraction of a second to a couple of seconds is normal and cannot be tuned away. See §33.9.

***

## 33.8 When it does not work

Almost every failure is one of six things.

| Symptom                            | Cause                                             | Fix                                               |
| ---------------------------------- | ------------------------------------------------- | ------------------------------------------------- |
| Value moves on CU-A, never on CU-B | **Register address** differs                      | Compare both. They must be identical.             |
| Value arrives but is wildly wrong  | **Data type** differs                             | Both must be `Read-Write-Holding-Register-UInt16` |
| Value is out by 10 or 100          | **Value specification** differs                   | Both `Not-Used`, or both the same multiplication  |
| Nothing at all, ever               | Wrong **IP Address:** or **Port:** on CU-A's unit | Compare with CU-B's slave module port             |
| Worked, then stopped               | CU-B's IP changed — DHCP                          | Set a static address, then correct CU-A           |
| Value arrives, light does nothing  | The wire on CU-B, or a yellow dot                 | Check in **Wire manager** and on the plan         |
| Nothing on either side             | One CU was never uploaded                         | **Save to central unit** on *both*                |

> **The commonest mistake of all: changing one project and not the other.** Both central units hold their own copy. Editing CU-A's register number and re-uploading only CU-A breaks the link, and the only symptom is silence. **Every change to a linked register means editing and uploading both projects.**

### Reading the fault from where the value stops

| Value stops at                       | Look at                                      |
| ------------------------------------ | -------------------------------------------- |
| Never appears on CU-A's Monitor      | CU-A's wire, function, or object assignment  |
| Appears on CU-A, not on CU-B         | The link — register, type, scaling, IP, port |
| Appears on CU-B, light does not move | CU-B's wire to the output                    |

***

## 33.9 What to know before you rely on this

Honest limitations, worth understanding before you design around them.

**It is polled, not instant.** The master asks repeatedly. Expect a fraction of a second to a couple of seconds. Fine for lighting, scenes, modes and setpoints. **Not suitable for safety interlocks or anything needing guaranteed timing.**

**A broken link is silent.** Pull the network cable and the last value simply stays there — CU-B does not know it has gone stale. If that matters, build your own watchdog: have CU-A increment a counter into a spare register every minute, and have CU-B raise an alarm if it stops changing.

**Each CU is still independent.** They do not share projects, groups, programs or time. Each holds its own configuration and keeps running alone if the other fails — usually a strength.

**Only what you map crosses.** There is no automatic discovery. A device added on CU-B is invisible to CU-A until you add a register for it on both sides.

**Both projects must stay in step.** Treat the register map as a shared contract. Keep it with both projects, and update it whenever either side changes.

***

## 33.10 Growing it

**More values** — add another `Modbus-Device` under the same unit on each CU, with the next register number. The IP and port are already set; you never re-enter them.

**Both directions** — give each CU *both* a `Modbus-IP-Slave` module (so it can be asked) and a `Modbus-IP-Master` module (so it can ask). Use separate registers for each direction, exactly as in §33.4.

**Three or more CUs** — the usual pattern is a hub: one CU is master, every other is slave, each with its own `Modbus-IP-Master-Unit` on the hub carrying that CU's IP. Avoid every CU polling every other — the register map becomes unmanageable.

**Sharing a temperature** — the only differences are that the source is a sensor rather than a button, and you set **Value specification** to `Multiplication 10x` **on both sides** so 21.5 °C survives the trip as `215`.

**A site-wide mode** — link a system bit ([Tutorial 8](/inels-bus/inels-design-manager-idm3/tutorial-timers-counters-and-conditions.md)) rather than a physical device. Set `HOLIDAY_MODE` on one CU, write it to a register, and have every other CU read it into its own system bit. Each CU then uses its local bit in conditions, so the logic stays readable and works even if the link drops.

***

## What you learned

* Two central units link over Modbus IP: one **slave** (offers values), one **master** (fetches them)
* A **register** is a numbered pigeonhole both sides must agree on
* **Register address, Data type and Value specification must match exactly** — mismatches fail silently
* `Read-Write-Holding-Register-UInt16` handles both on/off and numbers; use it for everything
* A register is an **analog object**: `Analog in value change` reads it, `Analog set level` writes it
* **IP and port are set once on the unit**; each register is a `Modbus-Device` beneath it
* The register does nothing until you **wire it** on *both* central units
* **Static IPs are essential** — DHCP will break the link eventually
* One register carries **one direction**; use two registers for two-way
* Every change to a linked register means **editing and uploading both projects**

***

## Before handover

* [ ] Both central units have **static IP addresses**
* [ ] The **register map** is written down and stored with both projects
* [ ] Register address, data type and scaling **verified identical** on both sides
* [ ] Both projects **saved to their central units**
* [ ] Tested from the button through to the output
* [ ] Tested what happens when the link drops — and the client knows what to expect
* [ ] A watchdog exists if a stale value would be dangerous

***

**See also:** [Chapter 32 — Modbus RTU and IP](/inels-bus/inels-design-manager-idm3/idm3-modbus-rtu-and-ip-integration.md) · [Chapter 22 — Central unit configuration](/inels-bus/inels-design-manager-idm3/idm3-central-unit-configuration.md) · [Chapter 26 — Monitor](/inels-bus/inels-design-manager-idm3/idm3-simulation-and-monitor.md)
