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:

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.

Design principle: MQTT carries telemetry. Modbus TCP carries safety interlocks. The two paths are independent — MQTT going down does not affect the M340's ability to read genset status or battery SOC via Modbus. The ops team loses visibility before they lose control.

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:

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
Interlock rule: The M340 reads genset register 1000 (status word) at 100ms intervals. Bit 1 (fault) set within 500ms of genset stop → M340 triggers rail clamp + engages park brake via hardwired output. This path does not involve MQTT or EMQX.

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:

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.

Design rule for long outages: On every network reconnection event, Ignition Edge reads the full critical tag set once, publishes all values with full timestamp, then resumes COV publishing. Do not rely on COV-only recovery — tags that didn't change during the outage will not be republished, leaving gaps in the historian record.

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.