Heterogeneous fleet — 3 PLC brands under one VPN tunnel (Modbus TCP)
A bakery has a Schneider mixer, a Carel proofer and a WAGO oven. All speak Modbus TCP but with different quirks. How to wire them in a single Rela AI tenant without losing hours debugging byte order.
Heterogeneous fleet — 3 PLC brands, one VPN tunnel
Modbus TCP is the lingua franca of industrial automation: virtually every PLC brand speaks it. But "speaks Modbus" does not mean "speaks the same Modbus" — every manufacturer has its own quirks for byte order, addressing convention and concurrent connection limits. This case shows how Rela AI handles a mixed fleet without forcing the customer to normalize anything on the PLC side.
Executive summary
The game changer: a single Rela tenant treats PLCs from different brands as if they came from the same vendor. The AHI, the AI agent, the alarm rules and KPIs run on unified events — heterogeneity is captured in each source's configuration and never leaks into the predictive pipeline.
| Before | After |
|---|---|
| Each brand demands a different SCADA integration | 3 Modbus sources in Rela, one click each |
| Operators learn 3 different SCADAs (one per brand) | Single dashboard, same language for the 3 machines |
| Duplicated rules per brand: one for Schneider ovens, another for WAGO ovens | One rule "oven temperature > 280" — tag_enrichment discriminates |
| When a probe drifts on one brand, no cross-learning happens | The ML anomaly detector learns per-asset patterns regardless of brand |
What is it for?
- Watch machines from different brands already on the floor without replacing anything.
- Centralize the alarm inbox: a single screen, a single AI agent, common escalation.
- Apply deterministic rules and predictive maintenance to the assets that matter most, not just to the ones already in a homogeneous SCADA.
- Understand brand differences (byte order, addressing) once during configuration and forget about them afterward.
Before you start — why you configure the peer yourself
Even with a multi-brand fleet, the access model does not change: the customer creates their own WireGuard peer; Rela never receives corporate VPN credentials.
Three critical reasons (the full 9 are in Why a dedicated tunnel):
- Least privilege: your corporate VPN opens the entire network (ERP, mail, SharePoint, OT). Rela only needs the subnet of the 3 PLCs. A breach on the Rela side hits one OT subnet, not the whole company.
- Seconds to revoke: you delete the peer in your router whenever you want, no ticket nor coordination with us. The private key lives on your hardware and never leaves it.
- Compliance (IEC 62443, NIST SP 800-82, SOC 2, ISO 27001): all require IT/OT separation via dedicated tunnel. Sharing corporate credentials is a direct findable in any audit.
The dedicated-peer model works identically for 1, 3 or N PLCs behind the same LAN — the physical site decides the number of tunnels, not the machines. Sharing credentials falls outside the self-service model and escalates to enterprise custom deployment.
How it works
flowchart LR
M["Schneider M340<br/>Mixer<br/>192.168.10.55"] --> R[("Customer router<br/>WireGuard + DNAT")]
C["Carel pCO5 plus<br/>Proofing chamber<br/>192.168.10.50"] --> R
W["WAGO 750-881<br/>Rotary oven<br/>192.168.10.65"] --> R
R -- "1 single tunnel" --> CONC[("Rela VPN<br/>concentrator")]
CONC --> WORKER[("Cloud Run worker<br/>3 modbus_listener tasks")]
WORKER --> PIPE[("Unified pipeline<br/>_machine_events")]
PIPE --> AGENT[("AI agent<br/>Plant Watcher")]
PIPE --> MAST[Asset Mixer]
PIPE --> CAST[Asset Proofer]
PIPE --> WAST[Asset Oven]The trick: the 3 listener tasks run on the same worker. Each decodes per its own configuration (byte order specific per register), but all emit to the same _machine_events. The AI agent and rules don't know — and don't care — that they come from different brands.
Parameters / Configuration
Byte order per brand (the most common gotcha)
modbus_byte_order is configured per register, not per source — because some PLCs mix conventions depending on firmware or DB. Realistic defaults:
| Brand / firmware | Default float32 | Default 32-bit int | Notes |
|---|---|---|---|
| Schneider Modicon M340 / M580 | big_endian_swap (CDAB) | big_endian (ABCD) | Word-swap is a Modicon historical quirk. Verify with mbpoll |
| Carel pCO5+ / pCOWeb | big_endian (ABCD) | big_endian | Carel standard |
| WAGO 750-881 | big_endian (ABCD) | big_endian | Configurable; default ABCD |
| Siemens S7 with Modbus module | big_endian (ABCD) | big_endian | Native S7 big-endian |
| Allen-Bradley via Modbus gateway | little_endian (DCBA) | little_endian | Inverse of Schneider |
| Omron CJ/CP | big_endian (ABCD) | big_endian | Standard |
Operational tip: when in doubt, read a register with a known value (e.g., setpoint =
25.0) using all 4 byte orders and pick the one returning the right number. Takes 5 minutes and saves days of debugging.
Addressing convention
Rela uses the 0-based wire Modbus address (the one pymodbus sends). Quick conversion from vendor docs:
| Vendor doc says | Use in Rela |
|---|---|
Modicon 40101 (5-digit) | address 100, FC 3 |
Modicon 30015 | address 14, FC 4 |
| Carel "register 1" | address 0, FC 3 |
| WAGO "%MW100" | address 100, FC 3 |
| Siemens DB1.DBW0 over Modbus | depends on TIA mapping |
Concurrent connection limits
| PLC | Concurrent connections | Implication |
|---|---|---|
| Schneider M340 | 8 | Comfortable for Rela + SCADA + HMI |
| Schneider M580 | 16 | Plenty |
| Carel pCOWeb | 4 | Critical: SCADA + Rela + HMI + tablet already saturate |
| WAGO 750-881 | 4-8 (firmware) | Verify before adding clients |
| Siemens S7 with CM 1241 | 1-3 | Very tight — Rela may be the only client |
Modbus mapping for the fleet (executable summary)
Mixer — Schneider Modicon M340 (10.200.7.55:502, unit_id 1)
| Register | Address | FC | Type | Byte order | Unit |
|---|---|---|---|---|---|
| motor_torque | 100 | 3 | float32 | big_endian_swap | % |
| motor_speed | 102 | 3 | float32 | big_endian_swap | rpm |
| motor_temperature | 104 | 3 | float32 | big_endian_swap | °C |
| mixing_time_seconds | 106 | 3 | uint16 | big_endian | s |
| motor_running | 50 | 1 | bool | — | 0/1 |
| alarm_overpressure | 60 | 1 | bool | — | 0/1 |
Proofing chamber — Carel pCO5+ (10.200.7.50:502, unit_id 1)
| Register | Address | FC | Type | Byte order | Unit |
|---|---|---|---|---|---|
| measured_temperature | 2 | 3 | float32 | big_endian | °C |
| measured_humidity | 4 | 3 | float32 | big_endian | % |
| cycle_phase | 10 | 3 | uint16 | big_endian | 1-5 |
| door_open | 1 | 2 | bool | — | 0/1 |
| probe_t_alarm | 20 | 2 | bool | — | 0/1 |
Rotary oven — WAGO 750-881 (10.200.7.65:502, unit_id 1)
| Register | Address | FC | Type | Byte order | Unit |
|---|---|---|---|---|---|
| chamber_temperature | 200 | 3 | float32 | big_endian | °C |
| deck_temperature | 202 | 3 | float32 | big_endian | °C |
| fan_speed | 204 | 3 | uint16 | big_endian | % |
| batch_count | 206 | 3 | uint32 | big_endian | pieces |
| door_open | 10 | 2 | bool | — | 0/1 |
| alarm_overheat | 11 | 2 | bool | — | 0/1 |
How to use it
Step 1 — Create the VPN tunnel (just one, same as homogeneous fleets)
Sidebar -> Settings -> Connectivity -> label Bakery - Production -> download .conf -> import on router.
Step 2 — DNAT the 3 IPs on the router
/ip firewall nat
add chain=dstnat dst-address=10.200.7.55 dst-port=502 \
protocol=tcp action=dst-nat to-addresses=192.168.10.55 to-ports=502
add chain=dstnat dst-address=10.200.7.50 dst-port=502 \
protocol=tcp action=dst-nat to-addresses=192.168.10.50 to-ports=502
add chain=dstnat dst-address=10.200.7.65 dst-port=502 \
protocol=tcp action=dst-nat to-addresses=192.168.10.65 to-ports=502
/ip firewall filter
add action=accept chain=forward in-interface=rela-vpn src-address=10.200.0.0/16Step 3 — Create the 3 assets
| Field | Mixer | Proofer | Oven |
|---|---|---|---|
| Name | Spiral Mixer | Proofing Chamber 1 | Rotary Oven 1 |
| Asset code | MIX-01 | PRF-01 | OVN-01 |
| Asset type | mixer | fermentation_chamber | rotary_oven |
| Criticality | medium | high | high |
| Plant | Bakery Central | Bakery Central | Bakery Central |
| Area | Production | Production | Production |
Step 4 — Single AI agent
Sidebar -> Alarms -> Machine agents -> + New.
Name: Plant Watcher
Model: gemini-3.1-pro-preview
Auto-task: yes -> Department "Maintenance"
Auto-notify: yes -> Plant manager WhatsApp
Escalation: yes (5 min / 15 min / 30 min)A single agent covers the 3 brands. Rules filter by tag_enrichment.machine_type when specialization is needed.
Step 5 — Create the 3 Modbus sources with brand-specific config
Sidebar -> Alarms -> Sources -> + New source, repeat 3 times.
Source 1: Schneider mixer
Source ID: mixer-schneider
Agent: Plant Watcher
Protocol: Modbus TCP
Modbus host: 10.200.7.55
Modbus port: 502
Unit ID: 1
Registers:
- motor_torque addr=100 fc=3 type=float32 byte_order=big_endian_swap unit="%"
- motor_speed addr=102 fc=3 type=float32 byte_order=big_endian_swap unit="rpm"
- motor_temperature addr=104 fc=3 type=float32 byte_order=big_endian_swap unit="°C"
- alarm_overpressure addr=60 fc=1 type=bool emit=bit_flip
Field mapping:
tag_enrichment:
machine_type: mixer
brand: schneider
model: modicon-m340Source 2: Carel proofer
Source ID: proofer-carel
Agent: Plant Watcher
Protocol: Modbus TCP
Modbus host: 10.200.7.50
Modbus port: 502
Unit ID: 1
Registers:
- measured_temperature addr=2 fc=3 type=float32 byte_order=big_endian unit="°C"
- measured_humidity addr=4 fc=3 type=float32 byte_order=big_endian unit="%"
- door_open addr=1 fc=2 type=bool emit=bit_flip
Field mapping:
tag_enrichment:
machine_type: proofer
brand: carel
model: pco5plusSource 3: WAGO oven
Source ID: oven-wago
Agent: Plant Watcher
Protocol: Modbus TCP
Modbus host: 10.200.7.65
Modbus port: 502
Unit ID: 1
Registers:
- chamber_temperature addr=200 fc=3 type=float32 byte_order=big_endian unit="°C"
- deck_temperature addr=202 fc=3 type=float32 byte_order=big_endian unit="°C"
- fan_speed addr=204 fc=3 type=uint16 unit="%"
- alarm_overheat addr=11 fc=2 type=bool emit=bit_flip
Field mapping:
tag_enrichment:
machine_type: oven
brand: wago
model: 750-881Step 6 — Link each source to its asset
Sidebar -> Assets -> edit each one -> add the corresponding event_source_ids.
Step 7 — One rule that covers multiple brands
Sidebar -> Alarms -> Rules -> + New rule.
Name: Oven overheat
Source: (empty — applies globally)
Conditions:
- field: machine_type
operator: eq
value: oven
- field: chamber_temperature
operator: gt
value: 280
Recurrence:
count_threshold: 3
window_minutes: 2
Actions:
- change_severity: critical
- create_task:
title: "Oven overheat"
priority: urgent
department_id: <Maintenance>
- trigger_escalationWhy it matters: this rule applies to ALL ovens in the tenant (today WAGO 750-881; tomorrow if you add a Schneider or Siemens). Discrimination comes from tag_enrichment.machine_type, not source_id. Reusable and duplication-free.
Real use cases
One alarm logic for 3 different brands
Operator leaves the oven door open and door_open on the WAGO flips to 1. Same logic as the door_open event on the Carel proofer in proofer-fleet-modbus — but the oven has its own bit_flip and its own asset. The agent sends two WhatsApps (one per machine), not one mixed.
Cross-brand predictive maintenance
The mixer motor (Schneider) shows motor_temperature drifting +3°C consistently for 7 days. AHI drops from A to C. The ML detector learned the normal pattern of THAT mixer (not the average of Schneider mixers in general — the model is per-asset). Generates a PM task "Check motor cooling" 2 weeks before projected failure.
The same would happen if the brand changed to Siemens or ABB — the model is per-asset, not per-brand. No prior training, no migration.
Cross-brand benchmarking
/dashboard/operational shows the 3 assets side by side:
- Mixer MIX-01: AHI 92 (A), last failure 180 days ago, RUL 90 days.
- Proofer PRF-01: AHI 78 (B), 2 critical alarms in last 7 days, RUL 30 days.
- Oven OVN-01: AHI 85 (B), usage 1,200 hours/month, RUL 120 days.
The manager sees this and decides to prioritize the proofer even though it's a different brand. Without Rela they would be comparing 3 incompatible SCADA dashboards.
Limitations and assumptions
- Each brand demands its Modbus manual. No magic. Addresses, byte orders and FCs come from the manufacturer manual or are discovered with
mbpollfrom the LAN before configuring. - If a brand exposes the data via a non-Modbus protocol the PLC speaks natively (typical: Siemens speaks S7comm natively, Allen-Bradley speaks EtherNet/IP CIP), it is better to use THAT protocol in Rela instead of forcing Modbus for compatibility. Native listeners extract richer metadata (data type without ambiguity, status codes, source timestamps).
- Saturated concurrent connections: if the customer already has SCADA + HMI polling a PLC with
max_connections=4(typical Carel), adding Rela can drop the oldest connection. Coordinate the rollout with the automation team. - Mixing brands multiplies config failure vectors. Troubleshooting without methodology costs days. That is why this doc has the byte_order matrix — it is the first thing to verify when readings look odd.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Mixer shows motor_speed = 1.4e-39 (absurd value) | byte_order wrong: you tried big_endian and the M340 wanted big_endian_swap | Switch the register byte_order to big_endian_swap and verify with mbpoll -t 4 -1 -r 102 -c 2 -F |
| Carel disconnected but Schneider and WAGO are fine | Carel pCOWeb saturated: 4 connections taken | Close one client (duplicate HMI, duplicate SCADA) or upgrade to pCOWeb Pro |
| Oven emitting events every 30 min instead of 10 s | min_interval too high, or outdated WAGO firmware | Lower min_interval to 10 s; if it persists, update WAGO firmware |
3 sources green but machine_type does not appear in events | tag_enrichment not applied (source missing field_mapping in config) | Edit source and add the field_mapping.tag_enrichment block |
Rules using field: machine_type do not fire | tag_enrichment is on the source but the rule evaluates before the injection | Verify pipeline order in logs: field_mapping runs before event_rules_engine |
Key benefits
- 1 tenant, 1 VPN, N brands: any combination of Modbus TCP PLCs lands in the same dashboard with no organizational overhead.
- Rules that scale with the plant:
tag_enrichmentlets you write 1 rule per machine type instead of 1 per brand; adding a new brand does not require duplicating rules. - Brand-agnostic predictive maintenance: the ML model learns patterns per asset, not per brand. Replacing a PLC on a machine (tech upgrade) does not require retraining.
- Pedagogical onboarding: the byte_order matrix and addressing convention are documented in this case so the implementation team does not learn the gotchas at the worst time (customer in production).
- Cross-brand benchmarking: see AHI and RUL of heterogeneous machines side by side in the same dashboard — impossible with per-brand SCADAs.
See also
- Homogeneous fleet — 3 fermentation chambers same model — simpler, same single-tunnel principle.
- Multiple machines, single VPN tunnel — the "1 tunnel = 1 physical site" mental model.
- Why a dedicated tunnel — the 9 reasons we never share the customer's corporate VPN.
Bakery — Proofing Chamber over VPN (zero to production)
Real step-by-step: a bakery connects its proofing chamber to Rela AI over a WireGuard VPN, without knowing the PLC brand and without enterprise networking gear.
Many machines, one VPN tunnel
How to add oven, mixers, compressors, and sensors behind the same bakery VPN tunnel — without reconfiguring or touching the Mikrotik.