Rela AIRela AI Docs
Machine Agents

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 middlewareDirect PLC → Rela AI connection. No SCADA in the middle, no mandatory MQTT broker.
Declarative decodingYou pick data type + byte order in the UI; the worker handles IEEE-754, two's complement, word swap, etc.
Optimized pollingContiguous registers are grouped into a single read_holding_registers (up to 125 words per call) to minimize round-trips.
Configurable emit policiesPer register: data_change (absolute/relative delta), threshold_crossed, bit_flip for booleans.
SRE-grade observabilityPrometheus 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 → SourcesNew source. Pick protocol Modbus TCP.

2. Fill the main fields

FieldSourceExample
NameFree text (identifies the source in the inbox)Reactor Line 3
HostPLC IP or hostname inside your network10.200.7.50
Port502 by default (standard Modbus)502
Unit IDDevice slave ID (1 for a direct PLC, >1 if there's an RS-485 gateway)1
Deployment Modecloud_direct if the PLC is reachable from GCP via VPN/VPC; edge if behind strict firewallcloud_direct
Edge Gateway IDOnly if deployment_mode=edge — reference to the registered gatewaygw_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:

FieldMeaning
Register typeholding (FC03), input (FC04), coil (FC01), discrete (FC02). holding is the most common.
NameHuman label — e.g. reactor.temperature. Used as tag in Prometheus metrics and in the inbox.
Address0-based. If your PLC documents 1-based (Modicon: 40001 = holding[0]), subtract 1.
CountHow many words to read. 1 for int16/uint16, 2 for int32/uint32/float32, 4 for float64.
Data typeint16, uint16, int32, uint32, float32, float64. For coil/discrete: always boolean.
Byte orderbig_endian (ABCD, default), little_endian (DCBA), big_endian_swap (BADC), little_endian_swap (CDAB).
Poll intervalSeconds 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→1 or 1→0 change. Mandatory for coils/discretes (booleans).

5. Save and test the connection

On save, a Test connection button appears above the register list. Clicking it:

  1. Opens a TCP connection to the PLC.
  2. Reads each register in the map once.
  3. 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: 1

Then 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_endian

If 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_flip

Troubleshooting 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 40001 but the real Modbus address is 0.
  • 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.

SymptomLikely causeFix
Expected 25.0, got ~3.4e38float32 with swapped byte orderTry big_endian_swap
Expected 1800, got 0x0708 (1800) as uint16 but expected uint32Wrong data typeChange to uint16 or adjust count
Expected 1800, got 1799 or 1801Nothing broken — the sensor has jitterAdjust 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: 5 avoids 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, info events are filtered before reaching the inbox.

How to test the connection works end-to-end

Operational checklist (in order):

  1. Dashboard → Alarms → Sources → your-modbus-source: status banner is green (connected).
  2. Test connection: rtt_ms < 300 and sample_values show the expected value.
  3. Dashboard → Alarms → Inbox: within minutes you should see events with event_type matching the register name (reactor.temperature, motor.speed, etc.).
  4. Metrics (if Grafana connected): modbus_connection_state{source_id="<yours>"} == 1 and modbus_register_reads_total rising.
  5. Healthy circuit breaker: no PLC_UNREACHABLE events 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.

On this page