Skip to content

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

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

ROXSIE integration: PLC alarms into ROS 2 faults#569
mfaferek93 wants to merge 8 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:

  1. 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.

  2. 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.

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. The bridge needs a mapping: which bit or alarm number means which fault code, and which fault classes set which bits going back. Kept by hand, that mapping and the PLC program drift apart, which is the spreadsheet problem all over again. The generator reads the config and the project export already, so it could emit the mapping file alongside the generated code and there would be one source of truth. Does the project export carry the alarm declarations and texts needed for that?

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.
Hand-kept bit maps drift from the PLC program, which is the spreadsheet
problem again. One source of truth means the generator emits the map.
@mfaferek93 mfaferek93 self-assigned this Jul 28, 2026
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