Skip to content

ROXSIE integration: PLC alarms into ROS 2 faults - #569

Draft
mfaferek93 wants to merge 6 commits into
mainfrom
design/plc-alarm-interface
Draft

ROXSIE integration: PLC alarms into ROS 2 faults#569
mfaferek93 wants to merge 6 commits into
mainfrom
design/plc-alarm-interface

Conversation

@mfaferek93

@mfaferek93 mfaferek93 commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Pull Request

Summary

Alarms in the PLC show up in ROS 2 as faults, with the values latched when the alarm fired.
Back the other way, the PLC gets one condition it can act on. Draft, because the message
definitions are what we want to agree on first. The bridge comes after that.

Context

ROXSIE already generates the PLC blocks and a ROS 2 package for the data you declare in the
YAML config, exchanged over shared memory on the host. That covers process values. It does
not cover alarms, because alarms live in the CPU message system and not in a data block.

So on a machine with a SIMATIC PLC and ROS 2 you still have two separate lists of what is
wrong. To join them today you either read alarm bits over a protocol and rebuild their
meaning from an engineering export, or someone copies the alarms by hand into a second list
on the panel. Both drift.

A picture, not a stream

AlarmList carries every alarm that is active or waiting for acknowledgement, published
cyclically. Two reasons.

The transport is cyclic anyway. The PLC-side node reads a data block at a fixed rate, so a
picture is what it naturally has.

And a picture survives a restart. Whoever restarts, the next cycle carries the full truth
again. An event stream loses whatever happened while it was away, and the consumer cannot
tell an empty stream from a healthy machine. That gap is not hypothetical: reading alarms
over OPC UA has the same hole, because no table of pending alarms is served on reconnect.

The bridge compares consecutive pictures. An entry that appears becomes a fault, an entry
that disappears heals it. Debounce, freeze-frame capture and the audit trail are the same
ones every other fault source goes through.

Two ways to fill the list

From bits. The customer declares alarm bits in a data block and ROXSIE carries them like
any other declared data. Nothing new is needed on the generator side, so this works today.
alarm_class, priority, producer, plc_timestamp and associated_values stay empty,
and the freeze-frame comes from the process topics the deployment already carries.
Acknowledgement is a bit, so either side can set it.

From the CPU alarm system. Generated code reads the alarm system with Get_Alarm and
mirrors it into the data block. Still no OPC UA and no license. This fills everything: the
event time from the CPU clock, the alarm class, the priority the plant already engineers
with, and the values latched when the alarm fired.

It also picks up alarms nobody declared. The CPU message system carries system diagnostics,
ProDiag, GRAPH, motion and security alarms, and producer says which one an entry came
from. A module fault or a drive alarm arrives without any extra engineering. That is the
strongest argument for this path.

The trade is acknowledgement. The CPU owns the acknowledged state and has no per-alarm
acknowledge instruction, so setting it for a single alarm needs the panel or OPC UA. We
mirror it rather than write it.

One cost worth naming: Get_Alarm reads from a queue, one entry at a time, while the data
block has to hold a picture. The generated code has to keep that picture itself. That is
real work, not a flag.

flowchart TB
    subgraph back["2. One condition back into the PLC"]
        direction TB
        B1["FaultManager"]
        B2["ros2_medkit_roxsie_bridge"]
        B3["ROXSIE generated node"]
        B4["Consumed status block"]
        B5["Machine logic<br>signal column"]
        B1 --> B2
        B2 ==>|DiagnosticsStatus| B3
        B3 --> B4
        B4 --> B5
    end

    subgraph out["1. Alarms out of the PLC"]
        direction TB
        A1["CPU alarm system<br>or alarm bits"]
        A2["ROXSIE generated node"]
        A3["ros2_medkit_roxsie_bridge"]
        A4["FaultManager<br>debounce, freeze-frame, audit"]
        A5["SOVD gateway, REST + SSE"]
        A6["Panel page, dashboards,<br>maintenance clients"]
        A1 --> A2
        A2 ==>|AlarmList| A3
        A3 --> A4
        A4 --> A5
        A5 -->|HTTPS| A6
    end

    linkStyle 1,5 stroke:#00966b,stroke-width:2.5px
Loading

The generated node talks to the PLC over shared memory on the same host, but publishes on
the ROS 2 graph over the network. So the diagnostics side does not have to run on the
controller.

What goes back into the PLC

One value and a heartbeat. DiagnosticsStatus says what the diagnostics side is asking of
the machine: nothing, something is abnormal, someone has to act, or stop.

It does not say what to light up. Machines already have a state machine, and where PackML is
used, a block that turns machine state into signal column bits according to ISA TR88.00.02.
Driving the lamp ourselves would put two writers on it. We hand over a condition and let the
machine's own logic decide what it means for the lamp, for an interlock, or for a state
transition.

The condition covers every active fault in scope, not only the ones that came from the PLC.
A navigation fault on the robot moves the same value, which is how a problem on the ROS 2
side reaches an operator who never looks at a screen.

Messages

Alarm

Field Type Meaning
alarm_id uint32 Numeric alarm identifier from the PLC project
source string Block or instance that declared the alarm
alarm_class string Alarm class from the project, empty in the bit path
priority uint8 0 to 16 as the PLC knows it, higher is more important
producer string user_program, system_diagnostics, prodiag, graph, motion, security
active bool Whether the condition is currently present
acknowledged bool Whether the alarm has been acknowledged
plc_timestamp builtin_interfaces/Time Last state change, from the PLC clock, UTC
associated_values float64[] Values latched when the alarm fired, 512 bytes total in the CPU

Two flags rather than one enum, because an alarm that is gone but unacknowledged is a real
state and stays in the list. Those are the same two bits the CPU alarm system tracks.

Alarm text does not travel. It belongs to the project, changes with the project, and is
resolved on the ROS 2 side from a table that ships with the deployment.

AlarmList

Field Type Meaning
stamp builtin_interfaces/Time When this picture was taken
alarms Alarm[] Alarms active or not yet acknowledged

DiagnosticsStatus

Field Type Meaning
heartbeat uint16 Liveness counter of the diagnostics layer, about 1 Hz
condition uint8 OK, ABNORMAL, ACTION_REQUIRED, STOP
class_bitmask uint16 Optional, coarse fault classes for interlocks
scope string Which entity in the SOVD tree this covers

When something breaks

Failure What happens
Our side or the link is down The heartbeat stops and the PLC raises its own alarm. No stale green light.
PLC or the generated node is down We raise a communications fault on the ROS 2 side.
Anything restarts The next picture carries the full set of active alarms, so nothing is silently lost.

The first row matters most. If our layer dies, the plant has to see it.

What comes after

This covers alarms from the PLC and one condition back. Two extensions follow, both shaped
by what a deployment needs rather than by what is interesting to build.

OpenTelemetry as another fault source. A cell is more than a PLC and a robot. The switch
that flaps, the controller that runs hot, the compute in another building that this machine
depends on - none of them speak the PLC alarm system, and most already speak OpenTelemetry.
Every one of those vendors ships its own API, and asking each of them for an integration does
not scale; asking all of them for OpenTelemetry does. An OTLP receiver next to this bridge
leaves three things to settle:

  • Which signal becomes a fault. Log records at error level are the obvious answer.
    Metrics are the more interesting one, because ros2_medkit_fault_detection already
    evaluates thresholds and status words for PLC tags and can take a metric the same way.
  • Which entity a stream belongs to. Resource attributes have to map onto the tree, or
    every host lands in one unattached pile.
  • What the operator sees. Nothing new to wire: an IT fault in the same list already moves
    the condition that reaches the signal column.

OpenTelemetry as an export. The same faults served over REST can be emitted as
OpenTelemetry, and the per-scope condition as a metric. Two renderings of one state, not two
sources of truth. Teams that already run an observability pipeline add a source rather than
integrate an API.

Neither exists today. There is a hook where an exporter plugs in and nothing behind it.
Building it against a concrete consumer beats guessing at a generic one.

Open questions

  1. Is alarm_id unique per CPU, or only per block? If only per block, the key has to be
    (source, alarm_id).
  2. Where does the alarm number to text table come from - the project export the generator
    already reads, or a separate export?
  3. How many entries can the generated data block hold? The array is fixed size once
    generated, and a plant can declare hundreds of alarms. If the ceiling is low, the fill has
    to prioritise and say that it truncated.
  4. Is there any per-alarm acknowledge path in the CPU that we have missed? If not,
    acknowledgement from the ROS 2 side stays a bit-path feature.
  5. Numeric associated values, or typed? Numeric keeps the entry fixed size. Typed carries
    more and brings strings back.

Issue


Type

  • Bug fix
  • New feature or tests
  • Breaking change
  • Documentation only

Testing

Message definitions only, no runtime code yet. colcon build --packages-select ros2_medkit_roxsie_msgs succeeds on jazzy and the types introspect through ros2 interface show. Bridge unit tests and an integration test that drives an alarm picture through to a
confirmed fault come with the implementation, once we agree on the contract.


Checklist

  • Breaking changes are clearly described (none, new package)
  • Tests were added or updated if needed (with the implementation)
  • Docs were updated if behavior or public API changed

Alarm events out of a PLC alarm system become structured faults;
aggregate state goes back as four fixed values, no fault list in the PLC.
Names the code generator this interface targets first and lists the
obligations of any producer: observe the alarm system, latch values at
trigger time, timestamp at the source, re-publish pending after restart.
Renames the package and the two types, and rewrites the docs around the
generated node as the producer instead of a hypothetical vendor bridge.
@mfaferek93 mfaferek93 changed the title Interface for PLC alarms and aggregate diagnostics state ROXSIE integration: PLC alarms into ROS 2 faults Jul 27, 2026
Shorter sentences, direct statements, no rhetorical framing.
…l faults

The audit log ships disabled by default, so an entry is written only when it
is on. The four status values aggregate every active fault in scope, not just
the ones the PLC reported.
The transport is cyclic and a picture survives a restart, so AlarmList
holds every alarm active or unacknowledged and the bridge diffs it.
Adds the PLC priority scale and the producer, drops the invented
severity, and states what each of the two fill paths can populate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ROXSIE integration: carry PLC alarms into ROS 2 faults

1 participant