A railway track machine on a Pilbara heavy haul line is not a fixed plant installation. It's mobile, self-powered, and moves through dead LTE zones between the yard and the remote track sections. The PLC talks Modbus TCP. The smart devices on board — the genset controller and the energy management hub — each have their own way of communicating. And the site operations team wants telemetry in MQTT, not a proprietary SCADA polling system.
This article walks through the full integration architecture for that problem: the M340 as the primary machine controller, Ignition Edge as the Modbus TCP polling engine and MQTT publisher, the DSE800E genset controller and Victron CerboGX as the two smart devices, and EMQX Edge running on the machine as the local broker. No OPC UA involved — this stack doesn't need it.
1. The Dual-Path Design — Telemetry Up, Interlocks In
Every smart device on the machine serves two purposes simultaneously:
- Telemetry path: Device → native MQTT → EMQX Edge broker → trackside WiFi or LTE → enterprise MQTT broker → ops dashboard
- Interlock path: Device → Modbus TCP → M340 (for hardwired safety interlocks that must survive broker unavailability)
The interlocks are hardwired through Modbus TCP registers — they work whether MQTT is up or down. The telemetry flows through MQTT and benefits from store-and-forward buffering at EMQX Edge. This separation is deliberate: the machine must stop safely if the genset trips, regardless of whether the ops team has visibility of it.
2. The Primary Controller — Modicon M340 Does Not Speak MQTT
The Schneider Modicon M340 is the onboard PLC. It runs brake sequencing, track positioning logic, motion interlocks, and (in most configurations) communicates via Modbus TCP. It has no built-in MQTT client. It doesn't need one.
The M340 speaks Modbus TCP natively — that's what the DSE800E and the Victron CerboGX both expose on their secondary interfaces. Ignition Edge handles the polling and MQTT translation:
- Ignition Edge polls M340 register ranges via Modbus TCP over the onboard Ethernet switch
- Ignition Edge converts the polled values to Sparkplug B payloads
- Ignition Edge publishes to the EMQX Edge broker running locally on the machine
The M340 program never changes. No ladder logic is added for MQTT. The PLC is a Modbus TCP server; Ignition Edge is the Modbus TCP client and MQTT publisher. If the M340 changes, the Ignition Edge tag browser updates to match the new register map.
M340 Modbus TCP Register Map — Brake Car Machine
| Register Range | Description | Data Type | Typical Poll Rate |
|---|---|---|---|
| 0–99 | Brake system status word, position sensors, speed feedback | Integer / Boolean coils | 250 ms |
| 100–199 | DSE800E genset status (via Modbus passthrough from DSE on secondary unit ID) | Integer | 500 ms |
| 200–299 | Victron CerboGX battery SOC, DC voltage, inverter state | Integer / Float | 1 s |
| 300–399 | Motion interlocks: rail clamp state, work order position, E-stop chain | Boolean coils | 100 ms |
| 400–499 | HMI/operator interface flags, shift state, maintenance mode | Integer | 1 s |
The M340's Modbus TCP server runs on port 502. Ignition Edge connects as a Modbus TCP client on the same onboard LAN segment (192.168.1.x/24, typically). The machine's EMQX Edge broker also lives on this segment.
3. DSE800E Genset Controller — Dual MQTT + Modbus Path
The Deep Sea Electronics DSE800E is the genset controller fitted to most diesel gensets on Pilbara mobile plant. It speaks Modbus RTU over RS-485 by default, but exposes a Modbus TCP bridge on its Ethernet port in most configurations used on rail equipment.
The DSE800E has two integration paths:
Path 1: Native MQTT → EMQX Edge (Telemetry)
The DSE800E can publish telemetry via its internal MQTT client (firmware permitting, depending on configuration variant). When it does, it publishes to the EMQX Edge broker running on the machine under the topic structure:
# DSE800E native MQTT topic structure N/DSE800E_{serial}/genset/0/status # Genset run/stop/fault status word N/DSE800E_{serial}/genset/0/fuel_level # Fuel level % N/DSE800E_{serial}/genset/0/run_hours # Accumulated run hours N/DSE800E_{serial}/genset/0/load_pct # Genset load percentage N/DSE800E_{serial}/genset/0/fault_code # Active fault code (0 = healthy)
Path 2: Modbus TCP → M340 (Hardwired Interlock)
The same genset data is also read via Modbus TCP by the M340 for safety-critical interlocks. If the genset trips, the M340 must stop the machine — not wait for an MQTT message to be published and delivered.
| Register | Description | Range / Units |
|---|---|---|
| 1000 | Genset status word (bit 0 = running, bit 1 = fault, bit 2 = auto mode) | Bitfield |
| 1002 | Fuel level percentage | 0–100 (%) |
| 1004 | Accumulated run hours (×10, e.g., 1234 = 12,340 hours) | Integer |
| 1006 | Active fault code (0 = healthy, non-zero = fault ID) | Integer |
| 1008 | Genset load % (0–100) | Integer |
4. Victron CerboGX — Battery SOC, Inverter State, PV Yield
The Victron CerboGX is the energy management hub for machines with battery-backed or hybrid power configs. It has a built-in Modbus TCP server and a native MQTT interface via the Victron VRM portal or local direct connection.
Path 1: Native MQTT → EMQX Edge (Telemetry)
The CerboGX publishes to EMQX Edge using the Victron topic structure via a local MQTT gateway (VRM local API or direct MQTT broker connection):
# Victron CerboGX MQTT topic structure (N/ = direct local publish) N/{VRM_ID}/system/0/soc # State of charge % (0–100) N/{VRM_ID}/system/0/dc/0/voltage # Battery DC bus voltage (V) N/{VRM_ID}/system/0/pv/0/power # PV array yield (W) N/{VRM_ID}/system/0/ac/0/load # AC load on inverter (W) N/{VRM_ID}/system/0/inverter/0/state # Inverter state (0=idle, 1=on, 2=fault) N/{VRM_ID}/system/0/relay/0/state # Transfer switch state (grid/gen)
Path 2: Modbus TCP → M340 (Battery SOC Interlock)
The M340 reads battery SOC via Modbus TCP from the CerboGX for the battery-low trip interlock and the generator transfer decision.
| Register | Description | Range / Units | Interlock Use |
|---|---|---|---|
| 259 | Battery state of charge (SOC %) | 0–100 (%) | SOC < 15% → M340 inhibits machine start, triggers genset auto-start |
| 261 | Battery DC bus voltage | Integer (×10, V) | DC voltage < 43V (48V nominal) → low voltage alarm |
| 263 | Inverter state (0=idle, 1=on, 2=fault) | Integer | Inverter fault (state=2) → M340 flags alarm, logs event |
| 265 | Grid/generator transfer relay state | Integer (0=grid, 1=generator) | Transfer to genset on SOC < 20% if genset not already running |
The CerboGX Modbus TCP server uses unit ID 100 by default (configurable in Victron settings). The M340 connects to it on the same onboard LAN segment.
5. EMQX Edge — Onboard Broker with Bridge-and-Forward
EMQX Edge runs on the machine as the local MQTT broker. All three data sources publish to it:
- Ignition Edge (M340 telemetry, polled via Modbus TCP)
- DSE800E native MQTT (genset telemetry)
- Victron CerboGX local MQTT (battery/inverter telemetry)
EMQX Edge is the right choice here because it's the only edge MQTT broker with built-in store-and-forward queueing and a clean bridge configuration to an upstream enterprise or cloud broker. When the machine moves out of trackside WiFi range or into an LTE dead zone, EMQX Edge queues messages locally and delivers them on reconnection — with QoS 1, not best-effort.
EMQX Edge Bridge Configuration — Cloud/Enterprise Broker Sync
# EMQX Edge — bridge-to-enterprise MQTT broker config # File: emqx_edge_bridge.conf (or EMQX dashboard → rules → bridge) bridge.mqtt.cloud: enable: true direction: both # Ingress (from local) + egress (from cloud) server: emqx-cloud.ratechos.polsia.app:8883 clientid: BrakeCar01_Edge username: brakecar_edge_01 password: {env:EMQX_CLOUD_BRIDGE_PASS} transport: tls: true server_name_indication: emqx-cloud.ratechos.polsia.app clean_start: false # Persistent session — survives bridge reconnect keepalive: 60 retry_interval: 5 # Bridge reconnection interval forwards: # Local → Cloud topic remapping PilbaraRail/+/+/+/DDATA: # Forward all device data messages qos: 1 retain: false PilbaraRail/+/+/+/DBIRTH: # Birth messages — critical for subscriber state qos: 2 retain: false bridge.queue: local: # Local queue config when bridge is offline max_length: 100000 # Max queued messages when LTE is down batch_size: 1000 # Messages per batch on reconnect store_qos0: false # Drop QoS 0 if buffer is full — QoS 1+ only drop_overly_new: true # Drop oldest when buffer is full (FIFO) bridge.reconnect: backoff: exponential initial_interval: 1s max_interval: 60s jitter: 2s
The queue-and-forward configuration is the critical part for mobile rail equipment. The machine can be offline for 20–45 minutes moving between coverage zones on a remote section of track. EMQX Edge buffers everything — including BIRTH messages — and replays them in order when connectivity resumes. Subscribers get the full state, not a snapshot of only the last value that changed.
6. Architecture: Pattern A (Ignition Edge Polling) vs Pattern B (Native MQTT)
There are two integration patterns depending on which data source you're connecting:
Pattern A: Ignition Edge Modbus TCP → MQTT Publisher
Pattern A — Ignition Edge as Modbus TCP Polling Engine
Stack: M340 (Modbus TCP server) → Ignition Edge (Modbus TCP client + MQTT Transmitter) → EMQX Edge → Sparkplug B namespace
Devices: M340 onboard data (brake state, position, motion interlocks), any RS-485 Modbus RTU device bridged to Modbus TCP
+ Ignition Edge has native Sparkplug B support in the MQTT Transmitter module — no custom scripting for namespace mapping
+ Ignition Designer gives a tag browser for the M340 — E&I engineers who know Ignition can configure it without ladder logic changes
+ COV (change-of-value) filtering built into the driver — only publishes on tag change, not on a fixed scan cycle
+ BIRTH/DEATH semantics handled by MQTT Transmitter module — subscriber alarm management works correctly
− Ignition Edge licensing (~USD $1,500/year for MQTT Transmitter module)
− Requires Java runtime on the edge node (manageable on Moxa DA-820C or Advantech UNO-2372G)
✓ Best fit: M340 onboard data, any Modbus RTU device, any site where the E&I team already runs Ignition as HMI/SCADA
Pattern B: Device Native MQTT → EMQX Edge
Pattern B — Native MQTT Direct to EMQX Edge
Stack: DSE800E or Victron CerboGX → native MQTT → EMQX Edge → Sparkplug B namespace
Devices: DSE800E genset controller (MQTT-capable firmware), Victron CerboGX (VRM local MQTT)
+ No polling overhead — device pushes on change, lower bus utilisation
+ No Ignition Edge license required for native-MQTT devices
+ EMQX Edge rule engine can do topic-to-Sparkplug-B mapping at the broker level — change mapping without touching device config
+ Devices with built-in MQTT are already configured for it — DSE800E and CerboGX both support this
− Not all devices have native MQTT — M340 doesn't, so Pattern A is still needed
− BIRTH/DEATH semantics depend on device firmware MQTT client implementation — test before commissioning
✓ Best fit: DSE800E genset telemetry, Victron CerboGX battery data, any device with built-in MQTT client and local broker connectivity
For a fully integrated brake car machine: use Pattern A for M340 data and any Modbus-only devices; use Pattern B for DSE800E and Victron CerboGX where firmware supports it. Both paths terminate at EMQX Edge. Both use Sparkplug B in the final namespace published to the upstream broker.
7. ISA-95 / Sparkplug B Namespace — Rail Equipment Structure
The Sparkplug B namespace for rail track machines reflects the equipment hierarchy: operator / site / equipment class / equipment ID / subsystem. The recommended structure for Pilbara heavy haul rail:
# Sparkplug B topic structure — Pilbara heavy haul rail # Format: spBv1.0/{namespace}/{group_id}/DBIRTH/{edge_node_id} # BIRTH message — defines the full edge node to subscribers spBv1.0/PilbaraRail/RioTinto/DBIRTH/BrakeCar01 # Namespace: PilbaraRail (site/operator level) # Group: RioTinto (operator/site identifier) # Edge Node: BrakeCar01 (equipment ID) # Device-level BIRTH messages — one per smart device spBv1.0/PilbaraRail/RioTinto/DBIRTH/BrakeCar01/M340_BrakeControl spBv1.0/PilbaraRail/RioTinto/DBIRTH/BrakeCar01/DSE800E_Genset spBv1.0/PilbaraRail/RioTinto/DBIRTH/BrakeCar01/CerboGX_EnergyMgmt # Data messages — metric values published on change spBv1.0/PilbaraRail/RioTinto/DDATA/BrakeCar01/M340_BrakeControl # Payload: { "brake_pressure": 420, "rail_clamp_state": 1, "position_km": 142.7 } spBv1.0/PilbaraRail/RioTinto/DDATA/BrakeCar01/DSE800E_Genset # Payload: { "status_word": 1, "fuel_level_pct": 78, "run_hours": 1243, "load_pct": 42 } spBv1.0/PilbaraRail/RioTinto/DDATA/BrakeCar01/CerboGX_EnergyMgmt # Payload: { "soc_pct": 67, "dc_voltage_x10": 494, "inverter_state": 1, "pv_power_w": 2300 } # DEATH message — sent when the edge node goes offline spBv1.0/PilbaraRail/RioTinto/DDEATH/BrakeCar01
Namespace consistency matters for subscriber tooling. If your ops dashboard uses a topic filter like PilbaraRail/+/+/DDATA/+/CerboGX_EnergyMgmt, it gets battery SOC from every machine running this stack on the network — without any custom adapter per machine. The Sparkplug B structure makes that possible.
8. Field Failure Modes — Trackside WiFi, LTE Dead Zones, 45°C Cab
These are the failure modes that don't show up in the architecture diagram. They're the ones that will wake you up at 2am on a night shift.
Trackside WiFi handoff — the machine crosses an AP boundary and MQTT drops. Track maintenance machines move through the yard and onto the open track. The trackside WiFi APs have overlapping coverage zones, but the handoff isn't instantaneous — your MQTT client may get a TCP reset during the roam. EMQX Edge queues the messages locally and replays them on reconnection. The ops dashboard shows a brief gap in telemetry (not a data loss), and the BIRTH message on reconnect restores subscriber state.
LTE dead zone on a remote track section — 45 minutes offline. Some track machines run on LTE for remote sections where trackside WiFi doesn't reach. Coverage on remote sections of Pilbara heavy haul lines is not guaranteed. EMQX Edge's store-and-forward queue holds up to 100,000 messages locally. On reconnection, it replays the queue in order — including all BIRTH/DBIRTH messages from all devices. Subscribers get the full state reconstruction.
Ignition Edge Modbus TCP reconnection after 6-hour LTE outage. When the LTE link restores after a long outage, Ignition Edge needs to re-establish its Modbus TCP session with the M340. Design for this: on reconnection, Ignition Edge reads the full critical tag set once (not just COV), publishes all values to MQTT, then resumes COV-only publishing. This eliminates the gap that COV-only recovery leaves when tags didn't change during the outage window.
45°C cab temperature and NTP clock drift. EMQX Edge uses X.509 certificates for MQTT TLS if bridging to the cloud broker. In a Pilbara cab at 45°C internal ambient, the edge node hardware Real Time Clock (RTC) battery may be weak, causing certificate validation timestamps to drift. Configure NTP fallback on the edge node — GPS time via the onboard GNSS receiver if available, otherwise the trackside WiFi AP's NTP server. Both EMQX Edge and Ignition Edge must have their system clocks synchronised before TLS will establish cleanly after a long offline period.
9. Ignition Edge + EMQX Edge Config — Copy-Paste Setup
Ignition Edge — M340 Modbus TCP + MQTT Transmitter Setup
# Ignition Edge Designer — Modbus TCP driver for M340 # Device Connections → Add → Modbus TCP → M340_Onboard [Modbus TCP — M340 Onboard] Host = 192.168.1.10 # M340 onboard LAN (machine internal) Port = 502 Device ID = 1 Connection Timeout = 5000 Request Timeout = 1000 Retry Count = 3 Zero-based Addressing = true # M340 uses 0-indexed registers [Tag Subscriptions — M340 Registers 0–499] Poll Rate = 250ms Read Mode = COV # Change-of-value only — not periodic COV Min Interval = 100ms # MQTT Transmitter module — Sparkplug B namespace [MQTT Transmitter — Sparkplug B] Primary Host ID = EMQX_Edge_Onboard Sparkplug Version = 3.0 Namespace Version = 2.0 Send DCMD Birth On Startup = true Send DCMD Death On Shutdown = true Publish Mode = On Change # COV — only publish on tag value change QOS = 1 [Edge Node — Brake Car 01] Group ID = PilbaraRail Edge Node ID = BrakeCar01 Birth Certificate = BrakeCar01_EdgeNode [EMQX Edge MQTT Connection] Broker Address = 192.168.1.1 # EMQX Edge on the machine Port = 1883 # Local non-TLS for onboard broker TLS Enabled = false # EMQX Edge is local — TLS not required onboard Keep Alive = 60 Auto-reconnect = true Clean Session = false # Persistent session for BIRTH/DEATH
EMQX Edge — Onboard Broker + Upstream Bridge
# EMQX Edge — local.conf (overlay or dashboard config) # EMQX Edge runs on 192.168.1.1:1883 on the machine mqtt.listener: tcp.1883: bind: 0.0.0.0:1883 max_connections: 1024 idle_timeout: 60s mqtt.listener.ssl.8883: bind: 0.0.0.0:8883 max_connections: 256 # TLS for bridge to upstream broker (trackside AP or LTE uplink) bridge.mqtt.upstream: enable: true server: emqx-enterprise.ratechos.polsia.app:8883 clientid: BrakeCar01_Edge username: brakecar_edge_01 password: {env:EMQX_UPSTREAM_PASS} bridge.forward_local_to_remote: # Local → Enterprise PilbaraRail/+/+/+/DDATA: qos: 1 PilbaraRail/+/+/+/DBIRTH: qos: 2 reconnect_interval: 5s keepalive: 60 # Rule engine — topic-to-Sparkplug-B transformation at broker level # Allows remapping device topics without changing device config rule.engine: my_victron_map: sql: SELECT payload FROM "N/+/system/0/soc" actions: [republish] republish_topic: PilbaraRail/+/CerboGX_EnergyMgmt/DDATA # Store-and-forward — queue depth and replay behaviour mqtt.queue: maxqueuedmessages: 100000 # ~50 MB at typical Sparkplug B payload size maxqueuetotalbytes: 52428800 # 50 MB cap — prevents unbounded disk use queuetype: fifo # Oldest messages sent first on reconnect
10. Pilot — Brake Car MQTT Integration, Two Weeks, Your Machine
You're running a Modicon M340 on a rail track machine. The genset is a DSE800E. The energy hub is a Victron CerboGX. You need the telemetry in MQTT, the interlocks handled by the M340, and a local EMQX broker that survives the dead zones between the yard and the remote track.
The pilot runs on your machine — not a simulation. We bring the Ignition Edge instance, the EMQX Edge broker, and the configuration for your actual register maps. You provide the M340 IP and a cabinet slot. Two weeks later you have a live Sparkplug B MQTT namespace with your real brake car data, and a Grafana dashboard showing genset load, battery SOC, and brake system state.
Get Your Brake Car Data into MQTT
Whether you're running a M340 with a DSE800E and CerboGX, or a different stack — RATECH scopes the integration, configures the Sparkplug B namespace against your actual register map, and gets your machine telemetry into MQTT without modifying the PLC program. Free pilot, real machine, two weeks.