Modbus TCP Connection — Technical Guide
How to connect a Modbus TCP PLC to Rela AI: form fields, register map, cloud_direct vs edge modes, emit policies, connection test, and troubleshooting by error type.
Modbus TCP Connection
Modbus TCP is the most common protocol in industrial plants for exposing registers from PLCs, RTUs, and I/O devices. Rela AI supports it natively: your PLC connects directly to the Cloud Run worker (or via an edge gateway) without additional middleware.
What it's for
Read equipment state in near real time: reactor temperature, motor RPM, production counters, safety alarms, process setpoints. Every register you configure becomes an event in the predictive pipeline — normalized, correlated, fed into the Asset Health Index, and evaluated against the rules you defined.
How it works
The Rela AI worker opens a TCP connection to the PLC (port 502 by default), performs read_holding_registers / read_coils / etc. at configurable intervals (default 5 seconds), decodes the value per the data type you declared (int16, uint32, float32, IEEE-754…), and emits an event only when the value changes significantly (deadband + emit policy). The circuit breaker pauses retries after 10 consecutive connect failures and emits a canonical PLC_UNREACHABLE alarm the operator sees.
Benefits
| No middleware | Direct PLC → Rela AI connection. No SCADA in the middle, no mandatory MQTT broker. |
| Declarative decoding | You pick data type + byte order in the UI; the worker handles IEEE-754, two's complement, word swap, etc. |
| Optimized polling | Contiguous registers are grouped into a single read_holding_registers (up to 125 words per call) to minimize round-trips. |
| Configurable emit policies | Per register: data_change (absolute/relative delta), threshold_crossed, bit_flip for booleans. |
| SRE-grade observability | Prometheus metrics per tenant+source: modbus_poll_latency_seconds, modbus_connection_state, modbus_tag_staleness_seconds. |
Creating a Modbus source — step by step
1. Navigate to sources
Dashboard → Alarms → Sources → New source. Pick protocol Modbus TCP.
2. Fill the main fields
| Field | Source | Example |
|---|---|---|
| Name | Free text (identifies the source in the inbox) | Reactor Line 3 |
| Host | PLC IP or hostname inside your network | 10.200.7.50 |
| Port | 502 by default (standard Modbus) | 502 |
| Unit ID | Device slave ID (1 for a direct PLC, >1 if there's an RS-485 gateway) | 1 |
| Deployment Mode | cloud_direct if the PLC is reachable from GCP via VPN/VPC; edge if behind strict firewall | cloud_direct |
| Edge Gateway ID | Only if deployment_mode=edge — reference to the registered gateway | gw_a1b2c3 |
If you configured a VPN in Settings → VPN Connectivity, the PLC host is the private IP inside your OT subnet (e.g. 192.168.10.20). Otherwise the PLC must be reachable by tunnel — never direct to the internet.
3. Add the register map
Each row in the map describes one register to read. Fields:
| Field | Meaning |
|---|---|
| Register type | holding (FC03), input (FC04), coil (FC01), discrete (FC02). holding is the most common. |
| Name | Human label — e.g. reactor.temperature. Used as tag in Prometheus metrics and in the inbox. |
| Address | 0-based. If your PLC documents 1-based (Modicon: 40001 = holding[0]), subtract 1. |
| Count | How many words to read. 1 for int16/uint16, 2 for int32/uint32/float32, 4 for float64. |
| Data type | int16, uint16, int32, uint32, float32, float64. For coil/discrete: always boolean. |
| Byte order | big_endian (ABCD, default), little_endian (DCBA), big_endian_swap (BADC), little_endian_swap (CDAB). |
| Poll interval | Seconds between reads (5 default). Lower = more traffic + PLC load. |
4. Configure emit policy per register
Three modes, tuned for different signal types:
- data_change — emits when the value changes beyond the deadband (absolute or relative %). For noisy analogs where every jitter doesn't matter.
- threshold_crossed — emits exactly once when the value crosses a threshold. Requires operator (
>,>=,<,<=) + value. Useful for hard alarms like "temperature > 90 °C". - bit_flip — emits on every
0→1or1→0change. Mandatory for coils/discretes (booleans).
5. Save and test the connection
On save, a Test connection button appears above the register list. Clicking it:
- Opens a TCP connection to the PLC.
- Reads each register in the map once.
- Shows RTT in ms + decoded values + warnings if any register failed.
If you see rtt_ms between 50 and 300 with the expected values, you're set. The listener starts automatically and begins feeding events to the pipeline.
Deployment modes — when to use each
cloud_direct
The Rela AI worker (Cloud Run in europe-west1) opens the TCP connection. Requires the PLC IP to be reachable from our VPC — typically via VPN Connectivity (dedicated WireGuard, see VPN / Setup).
Use this mode when:
- You have WireGuard VPN or site-to-site configured.
- The plant firewall allows inbound port 502 from the assigned VPN IP (10.200.X.X).
- The OT network is segmented but reachable from the DMZ or corporate network.
edge
You install a rela-ai-edge container on a mini-PC / Raspberry Pi inside the plant. The container opens an outbound HTTPS connection to the Cloud Run worker, and Modbus polling happens locally in the plant.
Use this mode when:
- The firewall only allows outbound HTTPS (443) to specific domains.
- You cannot configure VPN (IT won't approve, old equipment, etc.).
- You want local buffering in case of temporary internet loss.
See Edge Gateway for the full container install guide.
Data decoding — quick reference
Analogs (int16, uint16)
One word, 16 bits. int16 interprets MSB as sign (two's complement); uint16 ranges 0 to 65535.
Example: your PLC exposes reactor.temperature × 10 in holding[0]:
data_type: uint16
count: 1Then divide by 10 in the agent logic (via field mapping or a rule).
32-bit integers (int32, uint32)
Two consecutive words. Byte order matters: big_endian (ABCD) is the Modicon/Schneider default; big_endian_swap (BADC) is used by Siemens and some Allen-Bradley devices.
Example: runtime.hours as uint32 in holding[4-5]:
address: 4
count: 2
data_type: uint32
byte_order: big_endianIf you see nonsense values (huge number that should be small), try big_endian_swap — most common mistake.
Floating point (float32 IEEE-754)
Same as int32 but interpreted as float. 99% of PLCs use big_endian (ABCD) but Siemens often uses big_endian_swap (BADC).
If the connection test returns a number like 3.14e38, your byte order is wrong.
Coils / Discretes (booleans)
Bit-addressed, not byte-addressed. Each coil = 1 bit. Count controls how many bits you read in a single request (up to 2000).
For an E-stop in coil[1]:
register_type: coil
address: 1
count: 1
emit_as: bit_flipTroubleshooting by error type
Connection refused
The PLC rejects the TCP handshake. Causes:
- Modbus TCP service is off on the PLC (check PLC config).
- Plant firewall blocks port 502 between the source (VPN gateway or Cloud Run) and the PLC.
- Another Modbus client is occupying the only available slot (some PLCs only accept 1-2 concurrent clients).
Check: from the VPN gateway or edge container, run nc -zv <PLC_IP> 502. If it fails there, the problem is network-level.
Connection timeout
TCP starts but doesn't respond. Causes:
- PLC powered off or in STOP mode.
- Excessive network latency (VPN with many hops).
- MTU mismatch in the WireGuard tunnel (1420 vs 1500 — rare with Modbus because packets are small, but possible).
Check: ping <PLC_IP> from the same source as the worker. RTT >200 ms usually indicates a network issue.
Illegal data address (Modbus exception 02)
You're requesting an address the PLC doesn't expose. Causes:
- 0-based vs 1-based offset: Modicon documents
40001but the real Modbus address is0. - Miscount of words: float32 needs address + count=2 for 2 consecutive words. If you asked count=1, the next register reads "the second word of the previous float" — chaos.
Check: open the PLC manual in the "Mapping Table" section and confirm the exact address.
Illegal function (Modbus exception 01)
You're requesting a function the PLC doesn't support. Some PLCs (old ones, MicroLogix, etc.) only support FC03 (holding) and FC06 (write). Trying FC04 (input registers) fails.
Check: switch register_type to holding and retry.
Absurd values (very large, very small, 0xFFFF)
Decoding problems, almost always byte order.
| Symptom | Likely cause | Fix |
|---|---|---|
| Expected 25.0, got ~3.4e38 | float32 with swapped byte order | Try big_endian_swap |
| Expected 1800, got 0x0708 (1800) as uint16 but expected uint32 | Wrong data type | Change to uint16 or adjust count |
| Expected 1800, got 1799 or 1801 | Nothing broken — the sensor has jitter | Adjust the deadband |
ConnectionError: operation on closed connection
The PLC closed the connection. Usually:
- PLC idle timeout (some Schneider M340 close after 30s of no requests).
- TCP keepalive disabled on the worker —
poll_interval_seconds: 5avoids this but if any is >30s, it can happen.
Check: keepalive_seconds on the source (default: TCP_KEEPIDLE=30). If there's a known PLC timeout, lower it below that.
Connection test works but no events reach the inbox
Polling starts but registers don't emit. Causes:
- Emit policy = threshold_crossed: if the value hasn't crossed the threshold yet, nothing emits. Lower the threshold to validate.
- Agent disabled: verify the machine agent assigned to the source is
enabled: true. - Agent min severity: if the agent has
min_severity=critical,infoevents are filtered before reaching the inbox.
How to test the connection works end-to-end
Operational checklist (in order):
- Dashboard → Alarms → Sources → your-modbus-source: status banner is green (
connected). - Test connection:
rtt_ms < 300and sample_values show the expected value. - Dashboard → Alarms → Inbox: within minutes you should see events with
event_typematching the register name (reactor.temperature,motor.speed, etc.). - Metrics (if Grafana connected):
modbus_connection_state{source_id="<yours>"} == 1andmodbus_register_reads_totalrising. - Healthy circuit breaker: no
PLC_UNREACHABLEevents in the inbox. If you see them, your tunnel or firewall has an intermittent issue.
Device templates
For popular PLCs (Schneider M340, Siemens S7-1500, Allen-Bradley MicroLogix, etc.), we have device templates with a pre-configured register map. When creating a source, click Use template and pick your model — main fields auto-fill and you only adjust host, port, unit_id.
If your device isn't there, create a custom source and — optionally — turn your config into a per-tenant template to reuse on future plants.
Known limitations
- Writes: read-only today. For writes (setpoints, commands) use a Modbus write tool from the agent.
- Max registers per source: 64 simultaneous registers. For more, split into multiple sources.
- Temporal precision: polling every 5s (configurable to 1s min). Not a control loop — it's condition monitoring.