From 586a40bc5594170b38295d2183d4bfbd571e30a4 Mon Sep 17 00:00:00 2001 From: mfaferek93 Date: Mon, 27 Jul 2026 18:20:37 +0200 Subject: [PATCH 1/8] feat(plc_msgs): interface for PLC alarms and diagnostics status 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. --- docs/design/index.rst | 1 + docs/design/ros2_medkit_plc_msgs | 1 + src/ros2_medkit_plc_msgs/CHANGELOG.rst | 7 + src/ros2_medkit_plc_msgs/CMakeLists.txt | 35 ++++ src/ros2_medkit_plc_msgs/README.md | 79 +++++++++ src/ros2_medkit_plc_msgs/design/index.rst | 159 ++++++++++++++++++ src/ros2_medkit_plc_msgs/msg/PlcAlarm.msg | 75 +++++++++ .../msg/PlcDiagnosticsStatus.msg | 51 ++++++ src/ros2_medkit_plc_msgs/package.xml | 26 +++ 9 files changed, 434 insertions(+) create mode 120000 docs/design/ros2_medkit_plc_msgs create mode 100644 src/ros2_medkit_plc_msgs/CHANGELOG.rst create mode 100644 src/ros2_medkit_plc_msgs/CMakeLists.txt create mode 100644 src/ros2_medkit_plc_msgs/README.md create mode 100644 src/ros2_medkit_plc_msgs/design/index.rst create mode 100644 src/ros2_medkit_plc_msgs/msg/PlcAlarm.msg create mode 100644 src/ros2_medkit_plc_msgs/msg/PlcDiagnosticsStatus.msg create mode 100644 src/ros2_medkit_plc_msgs/package.xml diff --git a/docs/design/index.rst b/docs/design/index.rst index 29ab9016f..8b6d27c2a 100644 --- a/docs/design/index.rst +++ b/docs/design/index.rst @@ -19,6 +19,7 @@ This section contains design documentation for the ros2_medkit project packages. ros2_medkit_msgs/index ros2_medkit_opcua/index ros2_medkit_param_beacon/index + ros2_medkit_plc_msgs/index ros2_medkit_serialization/index ros2_medkit_sovd_service_interface/index ros2_medkit_topic_beacon/index diff --git a/docs/design/ros2_medkit_plc_msgs b/docs/design/ros2_medkit_plc_msgs new file mode 120000 index 000000000..f3f3ea237 --- /dev/null +++ b/docs/design/ros2_medkit_plc_msgs @@ -0,0 +1 @@ +../../src/ros2_medkit_plc_msgs/design \ No newline at end of file diff --git a/src/ros2_medkit_plc_msgs/CHANGELOG.rst b/src/ros2_medkit_plc_msgs/CHANGELOG.rst new file mode 100644 index 000000000..28a5b401b --- /dev/null +++ b/src/ros2_medkit_plc_msgs/CHANGELOG.rst @@ -0,0 +1,7 @@ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Changelog for package ros2_medkit_plc_msgs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Forthcoming +----------- +* Initial interface definitions: ``PlcAlarm.msg`` for alarm events coming out of a PLC alarm system, ``PlcDiagnosticsStatus.msg`` for aggregate diagnostics state going back in. diff --git a/src/ros2_medkit_plc_msgs/CMakeLists.txt b/src/ros2_medkit_plc_msgs/CMakeLists.txt new file mode 100644 index 000000000..4ca63a274 --- /dev/null +++ b/src/ros2_medkit_plc_msgs/CMakeLists.txt @@ -0,0 +1,35 @@ +# Copyright 2026 mfaferek93 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.8) +project(ros2_medkit_plc_msgs) + +# Note: ROS2MedkitWarnings is intentionally NOT included here. +# rosidl-generated code triggers strict warnings we cannot fix. +find_package(ament_cmake REQUIRED) +find_package(rosidl_default_generators REQUIRED) +find_package(builtin_interfaces REQUIRED) + +rosidl_generate_interfaces(${PROJECT_NAME} + "msg/PlcAlarm.msg" + "msg/PlcDiagnosticsStatus.msg" + DEPENDENCIES builtin_interfaces +) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + ament_lint_auto_find_test_dependencies() +endif() + +ament_package() diff --git a/src/ros2_medkit_plc_msgs/README.md b/src/ros2_medkit_plc_msgs/README.md new file mode 100644 index 000000000..b4c433ae2 --- /dev/null +++ b/src/ros2_medkit_plc_msgs/README.md @@ -0,0 +1,79 @@ +# ros2_medkit_plc_msgs + +Message definitions for exchanging PLC alarms and aggregate diagnostics state with ROS 2. + +## Overview + +A machine that combines a PLC and ROS 2 has two fault stories that never meet. The PLC +has a mature alarm system: numbered alarms, coming and going states, acknowledgement, and +values latched at the moment the alarm fired. ROS 2 has faults, freeze-frames and a +diagnostics tree. + +This package defines the interface between them. Alarms declared in a PLC project arrive +in ROS 2 as structured events and become faults; the diagnostics side sends back one small +status word for the signal column and for machine logic. + +Two directions, deliberately asymmetric: + +- **Alarms flow out in full** - every alarm event becomes a `PlcAlarm` message. +- **Only a status word flows back in** - `PlcDiagnosticsStatus` carries four fixed values, + no fault list and no strings the PLC would have to build. + +Fault text is dynamic and unbounded, and string handling inside a PLC costs determinism. +A fault list inside the PLC would also be a second copy that drifts from the first. What +the machine needs from the diagnostics side is a condition it can act on; everything +richer is read over REST by whoever needs it. + +## Messages + +### PlcAlarm.msg + +One alarm event as reported by a PLC alarm system. + +| Field | Type | Description | +|-------|------|-------------| +| `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, e.g. whether acknowledgement is required | +| `state` | uint8 | `STATE_COMING` / `STATE_GOING` | +| `ack_state` | uint8 | `ACK_UNACKNOWLEDGED` / `ACK_ACKNOWLEDGED` | +| `plc_timestamp` | builtin_interfaces/Time | Event time from the PLC clock, UTC | +| `associated_values` | float64[] | Values latched when the alarm fired | + +Alarm texts are not carried at runtime. The mapping from alarm number to text is static, +exported once from the engineering project, and resolved on the ROS 2 side. + +A `STATE_GOING` event is not a delete. It moves the fault towards healing, the same way an +alarm system keeps a condition until it is acknowledged, so the history survives for the +audit trail. + +`ack_state` is the shared acknowledgement state. Whoever acknowledges first, every other +consumer sees the same value rather than keeping a private copy. + +### PlcDiagnosticsStatus.msg + +Aggregate diagnostics state sent towards a PLC. + +| Field | Type | Description | +|-------|------|-------------| +| `heartbeat` | uint16 | Liveness counter of the diagnostics layer, about 1 Hz | +| `aggregate_severity` | uint8 | Highest severity in scope, `Fault` SEVERITY_* semantics | +| `active_fault_count` | uint16 | Active faults in scope | +| `class_bitmask` | uint16 | Coarse fault classes for machine logic | +| `scope` | string | Which entity in the diagnostics tree this status covers | + +A stalled `heartbeat` lets a watchdog in the PLC raise its own alarm, so the plant sees +that diagnostics are offline instead of a stale healthy state. + +`scope` lets one diagnostics instance serve several cells without them seeing each other's +faults. + +## Related packages + +- `ros2_medkit_msgs` - the core fault model these events are mapped onto +- `ros2_medkit_fault_manager` - fault aggregation, debounce and freeze-frame capture +- `ros2_medkit_diagnostic_bridge` - the same bridging pattern for ROS 2 `/diagnostics` + +## License + +Apache-2.0 diff --git a/src/ros2_medkit_plc_msgs/design/index.rst b/src/ros2_medkit_plc_msgs/design/index.rst new file mode 100644 index 000000000..d0a1ff1ca --- /dev/null +++ b/src/ros2_medkit_plc_msgs/design/index.rst @@ -0,0 +1,159 @@ +ros2_medkit_plc_msgs +==================== + +This section contains design documentation for the ros2_medkit_plc_msgs package. + +Overview +-------- + +A machine that combines a PLC and ROS 2 has two fault stories that never meet. The +PLC has a mature alarm system: numbered alarms, coming and going states, +acknowledgement, and values latched at the moment the alarm fired. ROS 2 has faults, +freeze-frames and a diagnostics tree. Joining them today means reading alarm bits over +a protocol and rebuilding their meaning from an engineering spreadsheet, or copying +alarms by hand into a second list. Both lose semantics, and both end with two fault +lists that drift apart. + +This package defines the interface that removes the guesswork. It has no runtime code - +only ``.msg`` definitions that are compiled by ``rosidl`` into C++ and Python bindings. + +Design +------ + +Two directions, deliberately asymmetric. + +**Alarms flow out in full.** Every alarm event the PLC alarm system produces is +published as ``PlcAlarm``: which alarm, coming or going, acknowledged or not, the PLC's +own timestamp, and the values latched when it fired. A bridge turns each event into a +fault with a freeze-frame and an audit trail. + +**Only a status word flows back in.** The diagnostics side does not push faults into the +PLC. It publishes ``PlcDiagnosticsStatus``, four fixed values written into a small data +block: heartbeat, worst severity, active count, coarse class bits. + +The asymmetry is the point: + +* Fault text is dynamic and unbounded. String handling inside a PLC costs determinism, + which is the one property a PLC exists to provide. +* A fault list inside the PLC would be a second copy of the fault list, and two copies + drift apart. The value of one source of truth disappears the moment it is duplicated. +* What the machine needs from the diagnostics side is a condition it can act on: a lamp + for the operator, a bit for an interlock. Everything richer is read over REST by + whoever needs it, at the moment they need it. + +Alarm texts never travel at runtime. The mapping from alarm number to text is static, +exported once from the engineering project, and resolved on the ROS 2 side. + +Acknowledgement is one shared state. Whoever acknowledges first - an operator on the +panel or a client on the ROS 2 side - every other consumer sees the same value rather +than keeping a private copy. + +Architecture +------------ + +.. plantuml:: + :caption: PLC alarm interface - data flow + + @startuml plc_alarm_interface_architecture + + skinparam linetype ortho + + package "PLC" { + [Alarm system] as AS + [Vendor bridge or\ngenerated glue code] as GLUE + [Status data block] as DB + [Signal column\nMachine interlocks] as SIG + } + + package "ROS 2" { + [plc_alarm_bridge] as AB + [plc_status_publisher] as SP + [FaultManager] as FM + [SOVD gateway] as GW + } + + [Panel page, dashboards,\nmaintenance clients] as CONS + + AS --> GLUE : alarm events + GLUE --> AB : PlcAlarm + AB --> FM : ReportFault + FM --> SP : fault events + SP --> GLUE : PlcDiagnosticsStatus + GLUE --> DB + DB --> SIG + FM -- GW + GW --> CONS : HTTPS + + @enduml + +Message Definitions +------------------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Message + - Purpose + * - ``PlcAlarm.msg`` + - One alarm event from a PLC alarm system: identifier, source, class, coming or + going, acknowledgement state, PLC timestamp, latched values + * - ``PlcDiagnosticsStatus.msg`` + - Aggregate diagnostics state towards a PLC: heartbeat, worst severity, active + count, coarse class bits, scope + +Lifecycle +--------- + +A PLC alarm and a medkit fault already model the same thing, so the mapping is direct +rather than invented. + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Alarm event + - Fault behaviour + * - ``STATE_COMING`` + - Reported as a FAILED event; debounce confirms it and a freeze-frame is captured + from the latched values + * - ``STATE_GOING`` + - Reported as a PASSED event; the fault heals but is not deleted, so history stays + for the audit trail + * - ``ACK_ACKNOWLEDGED`` + - The fault is cleared; the acknowledgement is the same state on both sides + +Failure Behaviour +----------------- + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Failure + - Behaviour + * - Diagnostics layer or link down + - The heartbeat stops and a watchdog in the PLC raises its own alarm, so the plant + sees that diagnostics are offline rather than a stale healthy state + * - PLC down or unreachable + - The bridge reports a communications fault on the ROS 2 side + * - Bridge restarts + - Currently pending alarms are re-published, so a restart does not silently lose + active alarms + +The first row is the load-bearing one: the failure mode of the diagnostics layer must +never be indistinguishable from a healthy machine. + +Open Questions +-------------- + +#. Is ``alarm_id`` unique per CPU, or only per block? If only per block, the unique key + has to be ``(source, alarm_id)`` and consumers cannot key on the number alone. +#. Numeric or typed associated values? Numeric keeps the message fixed-size and cheap on + the PLC side; typed is richer but reintroduces strings. +#. Can generated code acknowledge an alarm in the PLC alarm system, or is acknowledgement + panel-only? It decides whether an acknowledgement made on the ROS 2 side can reach the + native alarm list. +#. Who owns the class catalogue behind ``class_bitmask``, and is 16 bits enough? +#. A separate interface package, or these messages in the core message package? Separate + lets a generator depend on the interface alone; core is one less package to release. diff --git a/src/ros2_medkit_plc_msgs/msg/PlcAlarm.msg b/src/ros2_medkit_plc_msgs/msg/PlcAlarm.msg new file mode 100644 index 000000000..f9510db7c --- /dev/null +++ b/src/ros2_medkit_plc_msgs/msg/PlcAlarm.msg @@ -0,0 +1,75 @@ +# Copyright 2026 mfaferek93 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# PlcAlarm.msg - one alarm event as reported by a PLC alarm system. +# +# Published by a vendor bridge or by generated PLC glue code, consumed by +# ros2_medkit_plc_bridge, which maps each event onto a SOVD fault. +# +# Design intent: the payload is fixed-size and free of runtime strings that the +# PLC would have to build. Alarm texts are not carried here. A generator or an +# engineering export provides a static alarm_id -> text table that the bridge +# resolves on the ROS 2 side. String handling in a PLC costs determinism, and +# alarm texts do not change at runtime. + +# Numeric alarm identifier as declared in the PLC project. +# +# OPEN QUESTION: is this identifier unique per CPU, or only per function block? +# If it is only unique per block, the unique key must be (source, alarm_id) and +# consumers cannot key on alarm_id alone. +uint32 alarm_id + +# Origin of the alarm inside the PLC program, e.g. the function block or +# instance name that declared it. Static knowledge of the generator or the +# engineering export, not read from the PLC at runtime. +# Required when alarm_id is not globally unique (see above). +string source + +# Alarm class as declared in the PLC project, e.g. whether the alarm requires +# acknowledgement. Carried as an opaque string because the class catalogue is +# project and vendor specific. +string alarm_class + +# Whether the alarm condition appeared or disappeared. Use STATE_* constants. +# +# The event is not a level: a GOING event does not delete the fault, it moves +# it towards healing. The fault stays visible for the audit trail until it is +# cleared, which mirrors how PLC alarm systems latch conditions. +uint8 state +uint8 STATE_COMING = 0 +uint8 STATE_GOING = 1 + +# Acknowledgement state as seen by the PLC alarm system. Use ACK_* constants. +# +# This is the shared acknowledgement truth. Whoever acknowledges first - an +# operator on the panel or a consumer on the ROS 2 side - every other consumer +# sees the same state instead of keeping a private copy. +uint8 ack_state +uint8 ACK_UNACKNOWLEDGED = 0 +uint8 ACK_ACKNOWLEDGED = 1 + +# Time of the event as taken from the PLC clock, in UTC. +# +# Deliberately the source time, not the time the message was received. A +# consumer can compare it against its own clock to detect a PLC that is not +# time-synchronised, which is common on plant networks. +builtin_interfaces/Time plc_timestamp + +# Process values latched by the alarm system at the moment the alarm fired. +# They become the freeze-frame of the resulting fault. +# +# OPEN QUESTION: numeric only, or a typed variant? Alarm systems can attach +# non-numeric values. Numeric keeps the message fixed-size and cheap on the PLC +# side; a typed variant is richer but reintroduces strings. +float64[] associated_values diff --git a/src/ros2_medkit_plc_msgs/msg/PlcDiagnosticsStatus.msg b/src/ros2_medkit_plc_msgs/msg/PlcDiagnosticsStatus.msg new file mode 100644 index 000000000..74d43ca0c --- /dev/null +++ b/src/ros2_medkit_plc_msgs/msg/PlcDiagnosticsStatus.msg @@ -0,0 +1,51 @@ +# Copyright 2026 mfaferek93 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# PlcDiagnosticsStatus.msg - aggregate diagnostics state sent towards a PLC. +# +# Published by ros2_medkit_plc_bridge, consumed by a vendor bridge or generated +# PLC glue code, which writes it into a small data block. +# +# Design intent: this is the whole of what the IT side puts inside a PLC. Rich +# fault information stays on the diagnostics side and is read over REST by +# whoever needs it - an HMI page, a dashboard, a maintenance client. What a PLC +# genuinely needs is a lamp and, optionally, a condition it can interlock on. +# Four fixed words, no strings, no lists. + +# Liveness counter of the diagnostics layer, incremented about once per second. +# +# A watchdog on the PLC side turns a stalled counter into its own alarm, so the +# plant sees "diagnostics offline" instead of a stale healthy state. The failure +# mode of the diagnostics layer must never look like a healthy machine. +uint16 heartbeat + +# Highest severity among the active faults in scope. +# Values follow ros2_medkit_msgs/Fault SEVERITY_* semantics: 0 INFO, 1 WARN, +# 2 ERROR, 3 CRITICAL. Typically drives a signal column. +uint8 aggregate_severity + +# Number of active faults in scope. Optional indicator on a panel. +uint16 active_fault_count + +# Coarse fault classes currently active, one bit per class. +# +# Intended for machine logic, for example an interlock that must not depend on +# reading a fault list. The mapping from fault codes to bits is deployment +# configuration on the diagnostics side, not a fixed catalogue in this message. +uint16 class_bitmask + +# Which part of the plant this status covers, expressed as an entity in the +# diagnostics tree, e.g. an area or a component id. Lets one diagnostics +# instance serve several cells without them seeing each other's faults. +string scope diff --git a/src/ros2_medkit_plc_msgs/package.xml b/src/ros2_medkit_plc_msgs/package.xml new file mode 100644 index 000000000..8c49d35c4 --- /dev/null +++ b/src/ros2_medkit_plc_msgs/package.xml @@ -0,0 +1,26 @@ + + + + ros2_medkit_plc_msgs + 0.6.0 + Message definitions for exchanging PLC alarms and aggregate diagnostics state with ROS 2 + + mfaferek93 + Apache-2.0 + + ament_cmake + rosidl_default_generators + + builtin_interfaces + + rosidl_default_runtime + + ament_lint_auto + ament_lint_common + + rosidl_interface_packages + + + ament_cmake + + From 8f0dc3cac40d23b333f7286773590657d46caed6 Mon Sep 17 00:00:00 2001 From: mfaferek93 Date: Mon, 27 Jul 2026 18:38:14 +0200 Subject: [PATCH 2/8] docs(plc_msgs): state what the publishing side must provide 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. --- src/ros2_medkit_plc_msgs/README.md | 8 ++++ src/ros2_medkit_plc_msgs/design/index.rst | 41 ++++++++++++++++++- src/ros2_medkit_plc_msgs/msg/PlcAlarm.msg | 9 +++- .../msg/PlcDiagnosticsStatus.msg | 5 ++- 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/ros2_medkit_plc_msgs/README.md b/src/ros2_medkit_plc_msgs/README.md index b4c433ae2..ac9fe9569 100644 --- a/src/ros2_medkit_plc_msgs/README.md +++ b/src/ros2_medkit_plc_msgs/README.md @@ -13,6 +13,14 @@ This package defines the interface between them. Alarms declared in a PLC projec in ROS 2 as structured events and become faults; the diagnostics side sends back one small status word for the signal column and for machine logic. +The first target on the PLC side is Siemens SIMATIC ROS Connector (ROXSIE), a code +generator that already produces the PLC blocks and the ROS 2 package for exchanging +declared data between a SIMATIC PLC and ROS 2. Its generated node sits on the ROS 2 graph +already, so it is the natural place to publish alarm events and consume the status +message. Nothing in the definitions depends on it: any producer that can observe a PLC +alarm system interoperates. See the design note for what the publishing side has to +provide. + Two directions, deliberately asymmetric: - **Alarms flow out in full** - every alarm event becomes a `PlcAlarm` message. diff --git a/src/ros2_medkit_plc_msgs/design/index.rst b/src/ros2_medkit_plc_msgs/design/index.rst index d0a1ff1ca..eff9e8302 100644 --- a/src/ros2_medkit_plc_msgs/design/index.rst +++ b/src/ros2_medkit_plc_msgs/design/index.rst @@ -60,7 +60,7 @@ Architecture package "PLC" { [Alarm system] as AS - [Vendor bridge or\ngenerated glue code] as GLUE + [PLC-side producer\n(e.g. ROXSIE generated node)] as GLUE [Status data block] as DB [Signal column\nMachine interlocks] as SIG } @@ -86,6 +86,45 @@ Architecture @enduml +Producer Requirements +--------------------- + +The interface only describes the wire. The publishing side runs on or next to the +PLC and is what makes the alarms reachable at all, so its obligations are part of +the contract: + +* **Observe the alarm system, not a tag mirror.** Alarm events live in the CPU + message system, not in ordinary data blocks. The producer needs a path to them, + either a native subscription or program code that reads pending alarms and + forwards them. +* **Latch values at trigger time.** ``associated_values`` must be the values the + alarm system captured when the alarm fired, not values read later. Re-reading + them afterwards produces a plausible but wrong freeze-frame. +* **Timestamp at the source.** ``plc_timestamp`` comes from the PLC clock in UTC. + It is not the publish time. +* **Re-publish pending alarms after a restart.** A producer that starts fresh must + send the alarms that are currently active, otherwise an active alarm silently + disappears from the diagnostics side for as long as it stays active. +* **Ship the alarm number to text table.** Exported once from the engineering + project, consumed on the ROS 2 side. The table travels with the deployment, not + with each event. +* **Write the status block and let the PLC watch it.** The consumer side writes the + four values into a data block; a watchdog in the PLC program turns a stalled + heartbeat into its own alarm. + +**First target: Siemens SIMATIC ROS Connector (ROXSIE).** It is a code generator +that, from a YAML configuration, produces both the PLC blocks and a ROS 2 package +for exchanging declared data between a SIMATIC PLC and ROS 2. The generated node +already sits on the ROS 2 graph on the host network, so it is the natural place to +publish alarm events and to consume the status message. Its configuration surface +today covers declared data topics; carrying alarm events is an addition on that +side, which is why this interface defines only the wire and leaves the production +of the events to the generator. + +Nothing in the messages depends on that generator. Any producer that can meet the +obligations above interoperates, which is the point of keeping the definitions +vendor-neutral. + Message Definitions ------------------- diff --git a/src/ros2_medkit_plc_msgs/msg/PlcAlarm.msg b/src/ros2_medkit_plc_msgs/msg/PlcAlarm.msg index f9510db7c..49aa80a0f 100644 --- a/src/ros2_medkit_plc_msgs/msg/PlcAlarm.msg +++ b/src/ros2_medkit_plc_msgs/msg/PlcAlarm.msg @@ -14,9 +14,16 @@ # # PlcAlarm.msg - one alarm event as reported by a PLC alarm system. # -# Published by a vendor bridge or by generated PLC glue code, consumed by +# Published by whatever runs on the PLC side and can observe its alarm system: +# generated glue code, a protocol bridge, or a hand-written node. Consumed by # ros2_medkit_plc_bridge, which maps each event onto a SOVD fault. # +# The first target is Siemens SIMATIC ROS Connector (ROXSIE), a code generator +# that already produces the PLC blocks and the ROS 2 package for exchanging +# declared data between a SIMATIC PLC and ROS 2. Its generated node is the +# natural place to publish these events. See the design note for what the +# publishing side has to provide. +# # Design intent: the payload is fixed-size and free of runtime strings that the # PLC would have to build. Alarm texts are not carried here. A generator or an # engineering export provides a static alarm_id -> text table that the bridge diff --git a/src/ros2_medkit_plc_msgs/msg/PlcDiagnosticsStatus.msg b/src/ros2_medkit_plc_msgs/msg/PlcDiagnosticsStatus.msg index 74d43ca0c..35d4e193e 100644 --- a/src/ros2_medkit_plc_msgs/msg/PlcDiagnosticsStatus.msg +++ b/src/ros2_medkit_plc_msgs/msg/PlcDiagnosticsStatus.msg @@ -14,8 +14,9 @@ # # PlcDiagnosticsStatus.msg - aggregate diagnostics state sent towards a PLC. # -# Published by ros2_medkit_plc_bridge, consumed by a vendor bridge or generated -# PLC glue code, which writes it into a small data block. +# Published by ros2_medkit_plc_bridge, consumed by the PLC-side integration - +# for example a node generated by Siemens SIMATIC ROS Connector (ROXSIE) - which +# writes the values into a small data block the PLC program can read. # # Design intent: this is the whole of what the IT side puts inside a PLC. Rich # fault information stays on the diagnostics side and is read over REST by From aecd2657d74b77a3d04f629efc85f9cc571c3b1f Mon Sep 17 00:00:00 2001 From: mfaferek93 Date: Mon, 27 Jul 2026 18:59:24 +0200 Subject: [PATCH 3/8] refactor(roxsie_msgs): scope the interface to the ROXSIE integration Renames the package and the two types, and rewrites the docs around the generated node as the producer instead of a hypothetical vendor bridge. --- docs/design/index.rst | 2 +- docs/design/ros2_medkit_plc_msgs | 1 - docs/design/ros2_medkit_roxsie_msgs | 1 + src/ros2_medkit_plc_msgs/CHANGELOG.rst | 7 - src/ros2_medkit_plc_msgs/README.md | 87 -------- src/ros2_medkit_plc_msgs/design/index.rst | 198 ------------------ .../msg/PlcDiagnosticsStatus.msg | 52 ----- src/ros2_medkit_roxsie_msgs/CHANGELOG.rst | 7 + .../CMakeLists.txt | 6 +- src/ros2_medkit_roxsie_msgs/README.md | 85 ++++++++ src/ros2_medkit_roxsie_msgs/design/index.rst | 193 +++++++++++++++++ .../msg/AlarmEvent.msg} | 53 ++--- .../msg/DiagnosticsStatus.msg | 50 +++++ .../package.xml | 4 +- 14 files changed, 365 insertions(+), 381 deletions(-) delete mode 120000 docs/design/ros2_medkit_plc_msgs create mode 120000 docs/design/ros2_medkit_roxsie_msgs delete mode 100644 src/ros2_medkit_plc_msgs/CHANGELOG.rst delete mode 100644 src/ros2_medkit_plc_msgs/README.md delete mode 100644 src/ros2_medkit_plc_msgs/design/index.rst delete mode 100644 src/ros2_medkit_plc_msgs/msg/PlcDiagnosticsStatus.msg create mode 100644 src/ros2_medkit_roxsie_msgs/CHANGELOG.rst rename src/{ros2_medkit_plc_msgs => ros2_medkit_roxsie_msgs}/CMakeLists.txt (92%) create mode 100644 src/ros2_medkit_roxsie_msgs/README.md create mode 100644 src/ros2_medkit_roxsie_msgs/design/index.rst rename src/{ros2_medkit_plc_msgs/msg/PlcAlarm.msg => ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg} (50%) create mode 100644 src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg rename src/{ros2_medkit_plc_msgs => ros2_medkit_roxsie_msgs}/package.xml (79%) diff --git a/docs/design/index.rst b/docs/design/index.rst index 8b6d27c2a..15ef48ee5 100644 --- a/docs/design/index.rst +++ b/docs/design/index.rst @@ -19,7 +19,7 @@ This section contains design documentation for the ros2_medkit project packages. ros2_medkit_msgs/index ros2_medkit_opcua/index ros2_medkit_param_beacon/index - ros2_medkit_plc_msgs/index + ros2_medkit_roxsie_msgs/index ros2_medkit_serialization/index ros2_medkit_sovd_service_interface/index ros2_medkit_topic_beacon/index diff --git a/docs/design/ros2_medkit_plc_msgs b/docs/design/ros2_medkit_plc_msgs deleted file mode 120000 index f3f3ea237..000000000 --- a/docs/design/ros2_medkit_plc_msgs +++ /dev/null @@ -1 +0,0 @@ -../../src/ros2_medkit_plc_msgs/design \ No newline at end of file diff --git a/docs/design/ros2_medkit_roxsie_msgs b/docs/design/ros2_medkit_roxsie_msgs new file mode 120000 index 000000000..96dc3276c --- /dev/null +++ b/docs/design/ros2_medkit_roxsie_msgs @@ -0,0 +1 @@ +../../src/ros2_medkit_roxsie_msgs/design \ No newline at end of file diff --git a/src/ros2_medkit_plc_msgs/CHANGELOG.rst b/src/ros2_medkit_plc_msgs/CHANGELOG.rst deleted file mode 100644 index 28a5b401b..000000000 --- a/src/ros2_medkit_plc_msgs/CHANGELOG.rst +++ /dev/null @@ -1,7 +0,0 @@ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Changelog for package ros2_medkit_plc_msgs -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Forthcoming ------------ -* Initial interface definitions: ``PlcAlarm.msg`` for alarm events coming out of a PLC alarm system, ``PlcDiagnosticsStatus.msg`` for aggregate diagnostics state going back in. diff --git a/src/ros2_medkit_plc_msgs/README.md b/src/ros2_medkit_plc_msgs/README.md deleted file mode 100644 index ac9fe9569..000000000 --- a/src/ros2_medkit_plc_msgs/README.md +++ /dev/null @@ -1,87 +0,0 @@ -# ros2_medkit_plc_msgs - -Message definitions for exchanging PLC alarms and aggregate diagnostics state with ROS 2. - -## Overview - -A machine that combines a PLC and ROS 2 has two fault stories that never meet. The PLC -has a mature alarm system: numbered alarms, coming and going states, acknowledgement, and -values latched at the moment the alarm fired. ROS 2 has faults, freeze-frames and a -diagnostics tree. - -This package defines the interface between them. Alarms declared in a PLC project arrive -in ROS 2 as structured events and become faults; the diagnostics side sends back one small -status word for the signal column and for machine logic. - -The first target on the PLC side is Siemens SIMATIC ROS Connector (ROXSIE), a code -generator that already produces the PLC blocks and the ROS 2 package for exchanging -declared data between a SIMATIC PLC and ROS 2. Its generated node sits on the ROS 2 graph -already, so it is the natural place to publish alarm events and consume the status -message. Nothing in the definitions depends on it: any producer that can observe a PLC -alarm system interoperates. See the design note for what the publishing side has to -provide. - -Two directions, deliberately asymmetric: - -- **Alarms flow out in full** - every alarm event becomes a `PlcAlarm` message. -- **Only a status word flows back in** - `PlcDiagnosticsStatus` carries four fixed values, - no fault list and no strings the PLC would have to build. - -Fault text is dynamic and unbounded, and string handling inside a PLC costs determinism. -A fault list inside the PLC would also be a second copy that drifts from the first. What -the machine needs from the diagnostics side is a condition it can act on; everything -richer is read over REST by whoever needs it. - -## Messages - -### PlcAlarm.msg - -One alarm event as reported by a PLC alarm system. - -| Field | Type | Description | -|-------|------|-------------| -| `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, e.g. whether acknowledgement is required | -| `state` | uint8 | `STATE_COMING` / `STATE_GOING` | -| `ack_state` | uint8 | `ACK_UNACKNOWLEDGED` / `ACK_ACKNOWLEDGED` | -| `plc_timestamp` | builtin_interfaces/Time | Event time from the PLC clock, UTC | -| `associated_values` | float64[] | Values latched when the alarm fired | - -Alarm texts are not carried at runtime. The mapping from alarm number to text is static, -exported once from the engineering project, and resolved on the ROS 2 side. - -A `STATE_GOING` event is not a delete. It moves the fault towards healing, the same way an -alarm system keeps a condition until it is acknowledged, so the history survives for the -audit trail. - -`ack_state` is the shared acknowledgement state. Whoever acknowledges first, every other -consumer sees the same value rather than keeping a private copy. - -### PlcDiagnosticsStatus.msg - -Aggregate diagnostics state sent towards a PLC. - -| Field | Type | Description | -|-------|------|-------------| -| `heartbeat` | uint16 | Liveness counter of the diagnostics layer, about 1 Hz | -| `aggregate_severity` | uint8 | Highest severity in scope, `Fault` SEVERITY_* semantics | -| `active_fault_count` | uint16 | Active faults in scope | -| `class_bitmask` | uint16 | Coarse fault classes for machine logic | -| `scope` | string | Which entity in the diagnostics tree this status covers | - -A stalled `heartbeat` lets a watchdog in the PLC raise its own alarm, so the plant sees -that diagnostics are offline instead of a stale healthy state. - -`scope` lets one diagnostics instance serve several cells without them seeing each other's -faults. - -## Related packages - -- `ros2_medkit_msgs` - the core fault model these events are mapped onto -- `ros2_medkit_fault_manager` - fault aggregation, debounce and freeze-frame capture -- `ros2_medkit_diagnostic_bridge` - the same bridging pattern for ROS 2 `/diagnostics` - -## License - -Apache-2.0 diff --git a/src/ros2_medkit_plc_msgs/design/index.rst b/src/ros2_medkit_plc_msgs/design/index.rst deleted file mode 100644 index eff9e8302..000000000 --- a/src/ros2_medkit_plc_msgs/design/index.rst +++ /dev/null @@ -1,198 +0,0 @@ -ros2_medkit_plc_msgs -==================== - -This section contains design documentation for the ros2_medkit_plc_msgs package. - -Overview --------- - -A machine that combines a PLC and ROS 2 has two fault stories that never meet. The -PLC has a mature alarm system: numbered alarms, coming and going states, -acknowledgement, and values latched at the moment the alarm fired. ROS 2 has faults, -freeze-frames and a diagnostics tree. Joining them today means reading alarm bits over -a protocol and rebuilding their meaning from an engineering spreadsheet, or copying -alarms by hand into a second list. Both lose semantics, and both end with two fault -lists that drift apart. - -This package defines the interface that removes the guesswork. It has no runtime code - -only ``.msg`` definitions that are compiled by ``rosidl`` into C++ and Python bindings. - -Design ------- - -Two directions, deliberately asymmetric. - -**Alarms flow out in full.** Every alarm event the PLC alarm system produces is -published as ``PlcAlarm``: which alarm, coming or going, acknowledged or not, the PLC's -own timestamp, and the values latched when it fired. A bridge turns each event into a -fault with a freeze-frame and an audit trail. - -**Only a status word flows back in.** The diagnostics side does not push faults into the -PLC. It publishes ``PlcDiagnosticsStatus``, four fixed values written into a small data -block: heartbeat, worst severity, active count, coarse class bits. - -The asymmetry is the point: - -* Fault text is dynamic and unbounded. String handling inside a PLC costs determinism, - which is the one property a PLC exists to provide. -* A fault list inside the PLC would be a second copy of the fault list, and two copies - drift apart. The value of one source of truth disappears the moment it is duplicated. -* What the machine needs from the diagnostics side is a condition it can act on: a lamp - for the operator, a bit for an interlock. Everything richer is read over REST by - whoever needs it, at the moment they need it. - -Alarm texts never travel at runtime. The mapping from alarm number to text is static, -exported once from the engineering project, and resolved on the ROS 2 side. - -Acknowledgement is one shared state. Whoever acknowledges first - an operator on the -panel or a client on the ROS 2 side - every other consumer sees the same value rather -than keeping a private copy. - -Architecture ------------- - -.. plantuml:: - :caption: PLC alarm interface - data flow - - @startuml plc_alarm_interface_architecture - - skinparam linetype ortho - - package "PLC" { - [Alarm system] as AS - [PLC-side producer\n(e.g. ROXSIE generated node)] as GLUE - [Status data block] as DB - [Signal column\nMachine interlocks] as SIG - } - - package "ROS 2" { - [plc_alarm_bridge] as AB - [plc_status_publisher] as SP - [FaultManager] as FM - [SOVD gateway] as GW - } - - [Panel page, dashboards,\nmaintenance clients] as CONS - - AS --> GLUE : alarm events - GLUE --> AB : PlcAlarm - AB --> FM : ReportFault - FM --> SP : fault events - SP --> GLUE : PlcDiagnosticsStatus - GLUE --> DB - DB --> SIG - FM -- GW - GW --> CONS : HTTPS - - @enduml - -Producer Requirements ---------------------- - -The interface only describes the wire. The publishing side runs on or next to the -PLC and is what makes the alarms reachable at all, so its obligations are part of -the contract: - -* **Observe the alarm system, not a tag mirror.** Alarm events live in the CPU - message system, not in ordinary data blocks. The producer needs a path to them, - either a native subscription or program code that reads pending alarms and - forwards them. -* **Latch values at trigger time.** ``associated_values`` must be the values the - alarm system captured when the alarm fired, not values read later. Re-reading - them afterwards produces a plausible but wrong freeze-frame. -* **Timestamp at the source.** ``plc_timestamp`` comes from the PLC clock in UTC. - It is not the publish time. -* **Re-publish pending alarms after a restart.** A producer that starts fresh must - send the alarms that are currently active, otherwise an active alarm silently - disappears from the diagnostics side for as long as it stays active. -* **Ship the alarm number to text table.** Exported once from the engineering - project, consumed on the ROS 2 side. The table travels with the deployment, not - with each event. -* **Write the status block and let the PLC watch it.** The consumer side writes the - four values into a data block; a watchdog in the PLC program turns a stalled - heartbeat into its own alarm. - -**First target: Siemens SIMATIC ROS Connector (ROXSIE).** It is a code generator -that, from a YAML configuration, produces both the PLC blocks and a ROS 2 package -for exchanging declared data between a SIMATIC PLC and ROS 2. The generated node -already sits on the ROS 2 graph on the host network, so it is the natural place to -publish alarm events and to consume the status message. Its configuration surface -today covers declared data topics; carrying alarm events is an addition on that -side, which is why this interface defines only the wire and leaves the production -of the events to the generator. - -Nothing in the messages depends on that generator. Any producer that can meet the -obligations above interoperates, which is the point of keeping the definitions -vendor-neutral. - -Message Definitions -------------------- - -.. list-table:: - :header-rows: 1 - :widths: 30 70 - - * - Message - - Purpose - * - ``PlcAlarm.msg`` - - One alarm event from a PLC alarm system: identifier, source, class, coming or - going, acknowledgement state, PLC timestamp, latched values - * - ``PlcDiagnosticsStatus.msg`` - - Aggregate diagnostics state towards a PLC: heartbeat, worst severity, active - count, coarse class bits, scope - -Lifecycle ---------- - -A PLC alarm and a medkit fault already model the same thing, so the mapping is direct -rather than invented. - -.. list-table:: - :header-rows: 1 - :widths: 40 60 - - * - Alarm event - - Fault behaviour - * - ``STATE_COMING`` - - Reported as a FAILED event; debounce confirms it and a freeze-frame is captured - from the latched values - * - ``STATE_GOING`` - - Reported as a PASSED event; the fault heals but is not deleted, so history stays - for the audit trail - * - ``ACK_ACKNOWLEDGED`` - - The fault is cleared; the acknowledgement is the same state on both sides - -Failure Behaviour ------------------ - -.. list-table:: - :header-rows: 1 - :widths: 40 60 - - * - Failure - - Behaviour - * - Diagnostics layer or link down - - The heartbeat stops and a watchdog in the PLC raises its own alarm, so the plant - sees that diagnostics are offline rather than a stale healthy state - * - PLC down or unreachable - - The bridge reports a communications fault on the ROS 2 side - * - Bridge restarts - - Currently pending alarms are re-published, so a restart does not silently lose - active alarms - -The first row is the load-bearing one: the failure mode of the diagnostics layer must -never be indistinguishable from a healthy machine. - -Open Questions --------------- - -#. Is ``alarm_id`` unique per CPU, or only per block? If only per block, the unique key - has to be ``(source, alarm_id)`` and consumers cannot key on the number alone. -#. Numeric or typed associated values? Numeric keeps the message fixed-size and cheap on - the PLC side; typed is richer but reintroduces strings. -#. Can generated code acknowledge an alarm in the PLC alarm system, or is acknowledgement - panel-only? It decides whether an acknowledgement made on the ROS 2 side can reach the - native alarm list. -#. Who owns the class catalogue behind ``class_bitmask``, and is 16 bits enough? -#. A separate interface package, or these messages in the core message package? Separate - lets a generator depend on the interface alone; core is one less package to release. diff --git a/src/ros2_medkit_plc_msgs/msg/PlcDiagnosticsStatus.msg b/src/ros2_medkit_plc_msgs/msg/PlcDiagnosticsStatus.msg deleted file mode 100644 index 35d4e193e..000000000 --- a/src/ros2_medkit_plc_msgs/msg/PlcDiagnosticsStatus.msg +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2026 mfaferek93 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# PlcDiagnosticsStatus.msg - aggregate diagnostics state sent towards a PLC. -# -# Published by ros2_medkit_plc_bridge, consumed by the PLC-side integration - -# for example a node generated by Siemens SIMATIC ROS Connector (ROXSIE) - which -# writes the values into a small data block the PLC program can read. -# -# Design intent: this is the whole of what the IT side puts inside a PLC. Rich -# fault information stays on the diagnostics side and is read over REST by -# whoever needs it - an HMI page, a dashboard, a maintenance client. What a PLC -# genuinely needs is a lamp and, optionally, a condition it can interlock on. -# Four fixed words, no strings, no lists. - -# Liveness counter of the diagnostics layer, incremented about once per second. -# -# A watchdog on the PLC side turns a stalled counter into its own alarm, so the -# plant sees "diagnostics offline" instead of a stale healthy state. The failure -# mode of the diagnostics layer must never look like a healthy machine. -uint16 heartbeat - -# Highest severity among the active faults in scope. -# Values follow ros2_medkit_msgs/Fault SEVERITY_* semantics: 0 INFO, 1 WARN, -# 2 ERROR, 3 CRITICAL. Typically drives a signal column. -uint8 aggregate_severity - -# Number of active faults in scope. Optional indicator on a panel. -uint16 active_fault_count - -# Coarse fault classes currently active, one bit per class. -# -# Intended for machine logic, for example an interlock that must not depend on -# reading a fault list. The mapping from fault codes to bits is deployment -# configuration on the diagnostics side, not a fixed catalogue in this message. -uint16 class_bitmask - -# Which part of the plant this status covers, expressed as an entity in the -# diagnostics tree, e.g. an area or a component id. Lets one diagnostics -# instance serve several cells without them seeing each other's faults. -string scope diff --git a/src/ros2_medkit_roxsie_msgs/CHANGELOG.rst b/src/ros2_medkit_roxsie_msgs/CHANGELOG.rst new file mode 100644 index 000000000..38f0ae530 --- /dev/null +++ b/src/ros2_medkit_roxsie_msgs/CHANGELOG.rst @@ -0,0 +1,7 @@ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Changelog for package ros2_medkit_roxsie_msgs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Forthcoming +----------- +* Initial interface definitions for the ROXSIE integration: ``AlarmEvent.msg`` for PLC alarm events entering ROS 2, ``DiagnosticsStatus.msg`` for aggregate diagnostics state going back into the PLC. diff --git a/src/ros2_medkit_plc_msgs/CMakeLists.txt b/src/ros2_medkit_roxsie_msgs/CMakeLists.txt similarity index 92% rename from src/ros2_medkit_plc_msgs/CMakeLists.txt rename to src/ros2_medkit_roxsie_msgs/CMakeLists.txt index 4ca63a274..225facb70 100644 --- a/src/ros2_medkit_plc_msgs/CMakeLists.txt +++ b/src/ros2_medkit_roxsie_msgs/CMakeLists.txt @@ -13,7 +13,7 @@ # limitations under the License. cmake_minimum_required(VERSION 3.8) -project(ros2_medkit_plc_msgs) +project(ros2_medkit_roxsie_msgs) # Note: ROS2MedkitWarnings is intentionally NOT included here. # rosidl-generated code triggers strict warnings we cannot fix. @@ -22,8 +22,8 @@ find_package(rosidl_default_generators REQUIRED) find_package(builtin_interfaces REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} - "msg/PlcAlarm.msg" - "msg/PlcDiagnosticsStatus.msg" + "msg/AlarmEvent.msg" + "msg/DiagnosticsStatus.msg" DEPENDENCIES builtin_interfaces ) diff --git a/src/ros2_medkit_roxsie_msgs/README.md b/src/ros2_medkit_roxsie_msgs/README.md new file mode 100644 index 000000000..a3b82f8be --- /dev/null +++ b/src/ros2_medkit_roxsie_msgs/README.md @@ -0,0 +1,85 @@ +# ros2_medkit_roxsie_msgs + +Message definitions for the ROXSIE integration: PLC alarm events into ROS 2, and aggregate +diagnostics state back into the PLC. + +## Overview + +Siemens SIMATIC ROS Connector (ROXSIE) generates, from a YAML configuration, both the +PLC-side blocks and a ROS 2 package that exchange declared data between a SIMATIC PLC and +ROS 2. Its configuration surface today covers data topics. Alarms are the gap: a machine +that combines a PLC and ROS 2 still has two fault stories that never meet. The PLC has a +mature alarm system - numbered alarms, coming and going states, acknowledgement, values +latched at the moment the alarm fired. ROS 2 has faults, freeze-frames and a diagnostics +tree. + +This package defines the two messages that close that gap. Alarms declared in the TIA +project reach ROS 2 as structured events and become faults with freeze-frames; the +diagnostics side sends back one small status word for the signal column and for machine +logic. + +Two directions, deliberately asymmetric: + +- **Alarms flow out in full** - every alarm event becomes an `AlarmEvent` message. +- **Only a status word flows back in** - `DiagnosticsStatus` carries four fixed values, no + fault list and no strings the PLC would have to build. + +Fault text is dynamic and unbounded, and string handling inside a PLC costs determinism. A +fault list inside the PLC would also be a second copy that drifts from the first. What the +machine needs from the diagnostics side is a condition it can act on; everything richer is +read over REST by whoever needs it. + +## Messages + +### AlarmEvent.msg + +One alarm event from the PLC alarm system, published by the ROXSIE generated node. + +| Field | Type | Description | +|-------|------|-------------| +| `alarm_id` | uint32 | Numeric alarm identifier from the TIA project | +| `source` | string | Block or instance that declared the alarm | +| `alarm_class` | string | Alarm class from the project, e.g. whether acknowledgement is required | +| `state` | uint8 | `STATE_COMING` / `STATE_GOING` | +| `ack_state` | uint8 | `ACK_UNACKNOWLEDGED` / `ACK_ACKNOWLEDGED` | +| `plc_timestamp` | builtin_interfaces/Time | Event time from the PLC clock, UTC | +| `associated_values` | float64[] | Values latched when the alarm fired | + +Alarm texts are not carried at runtime. The generator emits a static alarm number to text +table next to the generated code, and the bridge resolves the text on the ROS 2 side. + +A `STATE_GOING` event is not a delete. It moves the fault towards healing, the same way the +alarm system keeps a condition until it is acknowledged, so history survives for the audit +trail. + +`ack_state` is the shared acknowledgement state. Whoever acknowledges first, every other +consumer sees the same value rather than keeping a private copy. + +### DiagnosticsStatus.msg + +Aggregate diagnostics state towards the PLC, consumed by the ROXSIE generated node and +written into a consumed data block. + +| Field | Type | Description | +|-------|------|-------------| +| `heartbeat` | uint16 | Liveness counter of the diagnostics layer, about 1 Hz | +| `aggregate_severity` | uint8 | Highest severity in scope, `Fault` SEVERITY_* semantics | +| `active_fault_count` | uint16 | Active faults in scope | +| `class_bitmask` | uint16 | Coarse fault classes for machine logic | +| `scope` | string | Which entity in the SOVD tree this status covers | + +A stalled `heartbeat` lets a watchdog in the PLC program raise its own alarm, so the plant +sees that diagnostics are offline instead of a stale healthy state. + +`scope` lets one diagnostics instance serve several cells without them seeing each other's +faults. + +## Related packages + +- `ros2_medkit_msgs` - the core fault model these events are mapped onto +- `ros2_medkit_fault_manager` - fault aggregation, debounce and freeze-frame capture +- `ros2_medkit_diagnostic_bridge` - the same bridging pattern for ROS 2 `/diagnostics` + +## License + +Apache-2.0 diff --git a/src/ros2_medkit_roxsie_msgs/design/index.rst b/src/ros2_medkit_roxsie_msgs/design/index.rst new file mode 100644 index 000000000..187af9799 --- /dev/null +++ b/src/ros2_medkit_roxsie_msgs/design/index.rst @@ -0,0 +1,193 @@ +ros2_medkit_roxsie_msgs +======================= + +This section contains design documentation for the ros2_medkit_roxsie_msgs package. + +Overview +-------- + +Siemens SIMATIC ROS Connector (ROXSIE) generates, from a YAML configuration, both the +PLC-side blocks and a ROS 2 package that exchange declared data between a SIMATIC PLC +and ROS 2 over shared memory on the host. The generated node sits on the ROS 2 graph, +so anything already listening to that graph can consume the data. + +Alarms are the gap. The configuration surface covers declared data topics, while the +alarm system is a separate mechanism inside the CPU: numbered alarms, coming and going +states, acknowledgement, and values latched at the moment the alarm fired. A machine +that combines a SIMATIC PLC with ROS 2 therefore still keeps two fault stories that +never meet, and joining them today means reading alarm bits over a protocol and +rebuilding their meaning from an engineering export, or copying alarms by hand into a +second list on the panel. + +This package defines the two messages that close the gap. It has no runtime code - only +``.msg`` definitions compiled by ``rosidl`` into C++ and Python bindings. + +Design +------ + +Two directions, deliberately asymmetric. + +**Alarms flow out in full.** Every alarm event the CPU alarm system produces is published +as ``AlarmEvent``: which alarm, coming or going, acknowledged or not, the PLC's own +timestamp, and the values latched when it fired. The bridge turns each event into a fault +with a freeze-frame and an audit trail. + +**Only a status word flows back in.** The diagnostics side does not push faults into the +PLC. It publishes ``DiagnosticsStatus``, four fixed values that the generated node writes +into a consumed data block: heartbeat, worst severity, active count, coarse class bits. + +The asymmetry is the point: + +* Fault text is dynamic and unbounded. String handling inside a PLC costs determinism, + which is the one property a PLC exists to provide. +* A fault list inside the PLC would be a second copy of the fault list, and two copies + drift apart. The value of one source of truth disappears the moment it is duplicated. +* What the machine needs from the diagnostics side is a condition it can act on: a lamp + for the operator, a bit for an interlock. Everything richer is read over REST by + whoever needs it, at the moment they need it. + +Alarm texts never travel at runtime. The generator emits a static alarm number to text +table next to the generated code, and the bridge resolves the text on the ROS 2 side. + +Acknowledgement is one shared state. Whoever acknowledges first - an operator on the +panel or a client on the ROS 2 side - every other consumer sees the same value rather +than keeping a private copy. + +Architecture +------------ + +.. plantuml:: + :caption: ROXSIE alarm integration - data flow + + @startuml roxsie_alarm_integration_architecture + + skinparam linetype ortho + + package "SIMATIC PLC" { + [CPU alarm system] as AS + [ROXSIE generated blocks] as GEN + [Consumed status block] as DB + [Signal column\nMachine interlocks] as SIG + } + + package "ROS 2" { + [ROXSIE generated node] as NODE + [ros2_medkit_roxsie_bridge] as BR + [FaultManager] as FM + [SOVD gateway] as GW + } + + [Panel page, dashboards,\nmaintenance clients] as CONS + + AS --> GEN : alarm events + GEN --> NODE : shared memory + NODE --> BR : AlarmEvent + BR --> FM : ReportFault + FM --> BR : fault events + BR --> NODE : DiagnosticsStatus + NODE --> GEN : shared memory + GEN --> DB + DB --> SIG + FM -- GW + GW --> CONS : HTTPS + + @enduml + +Deployment note: the generated node exchanges data with the PLC over shared memory on +the same host, but publishes on the ROS 2 graph over the host network. The diagnostics +side therefore does not have to run on the controller; it subscribes like any other ROS 2 +participant. + +What the Generator Has to Provide +--------------------------------- + +The messages describe the wire. The generated code is what makes the alarms reachable at +all, so its obligations are part of the contract: + +* **Observe the alarm system, not a data block mirror.** Alarm events live in the CPU + message system, not in ordinary data blocks. Reaching them needs either a native + subscription or program code that reads pending alarms and forwards them. +* **Latch values at trigger time.** ``associated_values`` must be what the alarm system + captured when the alarm fired. Re-reading them afterwards produces a plausible but + wrong freeze-frame. +* **Timestamp at the source.** ``plc_timestamp`` comes from the PLC clock in UTC, not + from the moment of publishing. +* **Re-publish pending alarms after a restart.** A node that starts fresh must send the + alarms that are currently active, otherwise an active alarm silently disappears from + the diagnostics side for as long as it stays active. +* **Emit the alarm number to text table.** Generated once from the TIA project alongside + the code, consumed on the ROS 2 side. The table travels with the deployment, not with + each event. +* **Write the consumed status block.** Four values into the data block, so that a + watchdog in the PLC program can turn a stalled heartbeat into its own alarm. + +Message Definitions +------------------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Message + - Purpose + * - ``AlarmEvent.msg`` + - One alarm event from the CPU alarm system: identifier, source, class, coming or + going, acknowledgement state, PLC timestamp, latched values + * - ``DiagnosticsStatus.msg`` + - Aggregate diagnostics state towards the PLC: heartbeat, worst severity, active + count, coarse class bits, scope + +Lifecycle +--------- + +A PLC alarm and a medkit fault already model the same thing, so the mapping is direct +rather than invented. + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Alarm event + - Fault behaviour + * - ``STATE_COMING`` + - Reported as a FAILED event; debounce confirms it and a freeze-frame is captured + from the latched values + * - ``STATE_GOING`` + - Reported as a PASSED event; the fault heals but is not deleted, so history stays + for the audit trail + * - ``ACK_ACKNOWLEDGED`` + - The fault is cleared; the acknowledgement is the same state on both sides + +Failure Behaviour +----------------- + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Failure + - Behaviour + * - Diagnostics layer or link down + - The heartbeat stops and a watchdog in the PLC program raises its own alarm, so the + plant sees that diagnostics are offline rather than a stale healthy state + * - PLC or the generated node down + - The bridge reports a communications fault on the ROS 2 side + * - Bridge restarts + - Currently pending alarms are re-published, so a restart does not silently lose + active alarms + +The first row is the load-bearing one: the failure mode of the diagnostics layer must +never be indistinguishable from a healthy machine. + +Open Questions +-------------- + +#. Is ``alarm_id`` unique per CPU, or only per block? If only per block, the unique key + has to be ``(source, alarm_id)`` and the bridge cannot key on the number alone. +#. Numeric or typed associated values? Numeric keeps the message fixed-size and cheap on + the PLC side; typed is richer but reintroduces strings. +#. Can the generated code acknowledge an alarm in the CPU alarm system, or is + acknowledgement panel-only? It decides whether an acknowledgement made on the ROS 2 + side can reach the native alarm list. +#. Who owns the class catalogue behind ``class_bitmask``, and is 16 bits enough? +#. Topic names and QoS for the two topics, and whether they are namespaced per cell. diff --git a/src/ros2_medkit_plc_msgs/msg/PlcAlarm.msg b/src/ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg similarity index 50% rename from src/ros2_medkit_plc_msgs/msg/PlcAlarm.msg rename to src/ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg index 49aa80a0f..f6ef199c8 100644 --- a/src/ros2_medkit_plc_msgs/msg/PlcAlarm.msg +++ b/src/ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg @@ -12,47 +12,40 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# PlcAlarm.msg - one alarm event as reported by a PLC alarm system. +# AlarmEvent.msg - one alarm event from the PLC alarm system, carried onto the +# ROS 2 graph by the node that Siemens SIMATIC ROS Connector (ROXSIE) generates. # -# Published by whatever runs on the PLC side and can observe its alarm system: -# generated glue code, a protocol bridge, or a hand-written node. Consumed by -# ros2_medkit_plc_bridge, which maps each event onto a SOVD fault. -# -# The first target is Siemens SIMATIC ROS Connector (ROXSIE), a code generator -# that already produces the PLC blocks and the ROS 2 package for exchanging -# declared data between a SIMATIC PLC and ROS 2. Its generated node is the -# natural place to publish these events. See the design note for what the -# publishing side has to provide. +# Published by the ROXSIE generated node, consumed by ros2_medkit_roxsie_bridge, +# which maps each event onto a SOVD fault with a freeze-frame and an audit trail. # # Design intent: the payload is fixed-size and free of runtime strings that the -# PLC would have to build. Alarm texts are not carried here. A generator or an -# engineering export provides a static alarm_id -> text table that the bridge -# resolves on the ROS 2 side. String handling in a PLC costs determinism, and -# alarm texts do not change at runtime. +# PLC would have to build. Alarm texts are not carried here. The generator emits +# a static alarm_id -> text table next to the generated code, and the bridge +# resolves the text on the ROS 2 side. String handling in a PLC costs +# determinism, and alarm texts do not change while the machine runs. -# Numeric alarm identifier as declared in the PLC project. +# Numeric alarm identifier as declared in the TIA project. # # OPEN QUESTION: is this identifier unique per CPU, or only per function block? # If it is only unique per block, the unique key must be (source, alarm_id) and -# consumers cannot key on alarm_id alone. +# the bridge cannot key on alarm_id alone. uint32 alarm_id # Origin of the alarm inside the PLC program, e.g. the function block or -# instance name that declared it. Static knowledge of the generator or the -# engineering export, not read from the PLC at runtime. -# Required when alarm_id is not globally unique (see above). +# instance that declared it. Static knowledge of the generator, not read from +# the PLC at runtime. Required when alarm_id is not unique per CPU (see above). string source -# Alarm class as declared in the PLC project, e.g. whether the alarm requires +# Alarm class as declared in the TIA project, e.g. whether the alarm requires # acknowledgement. Carried as an opaque string because the class catalogue is -# project and vendor specific. +# project specific. string alarm_class # Whether the alarm condition appeared or disappeared. Use STATE_* constants. # -# The event is not a level: a GOING event does not delete the fault, it moves -# it towards healing. The fault stays visible for the audit trail until it is -# cleared, which mirrors how PLC alarm systems latch conditions. +# The event is not a level: a GOING event does not delete the fault, it moves it +# towards healing. The fault stays visible for the audit trail until it is +# cleared, which mirrors how the PLC alarm system latches conditions. uint8 state uint8 STATE_COMING = 0 uint8 STATE_GOING = 1 @@ -60,23 +53,23 @@ uint8 STATE_GOING = 1 # Acknowledgement state as seen by the PLC alarm system. Use ACK_* constants. # # This is the shared acknowledgement truth. Whoever acknowledges first - an -# operator on the panel or a consumer on the ROS 2 side - every other consumer +# operator on the panel or a client on the ROS 2 side - every other consumer # sees the same state instead of keeping a private copy. uint8 ack_state uint8 ACK_UNACKNOWLEDGED = 0 uint8 ACK_ACKNOWLEDGED = 1 -# Time of the event as taken from the PLC clock, in UTC. +# Time of the event taken from the PLC clock, in UTC. # -# Deliberately the source time, not the time the message was received. A +# Deliberately the source time, not the time the message was published. A # consumer can compare it against its own clock to detect a PLC that is not # time-synchronised, which is common on plant networks. builtin_interfaces/Time plc_timestamp -# Process values latched by the alarm system at the moment the alarm fired. -# They become the freeze-frame of the resulting fault. +# Values latched by the alarm system at the moment the alarm fired, the +# associated values of the alarm. They become the freeze-frame of the fault. # -# OPEN QUESTION: numeric only, or a typed variant? Alarm systems can attach +# OPEN QUESTION: numeric only, or a typed variant? The alarm system can attach # non-numeric values. Numeric keeps the message fixed-size and cheap on the PLC # side; a typed variant is richer but reintroduces strings. float64[] associated_values diff --git a/src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg b/src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg new file mode 100644 index 000000000..808f4b52a --- /dev/null +++ b/src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg @@ -0,0 +1,50 @@ +# Copyright 2026 mfaferek93 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# DiagnosticsStatus.msg - aggregate diagnostics state sent towards the PLC. +# +# Published by ros2_medkit_roxsie_bridge, consumed by the ROXSIE generated node, +# which writes the values into a consumed data block the PLC program reads. +# +# Design intent: this is the whole of what the diagnostics side puts inside the +# PLC. Faults stay on the ROS 2 side and are read over REST by whoever needs +# them. What the PLC genuinely needs is a lamp and, optionally, a condition it +# can interlock on. Four fixed words, no strings, no lists. + +# Liveness counter of the diagnostics layer, incremented about once per second. +# +# A watchdog in the PLC program turns a stalled counter into its own alarm, so +# the plant sees "diagnostics offline" instead of a stale healthy state. The +# failure mode of the diagnostics layer must never look like a healthy machine. +uint16 heartbeat + +# Highest severity among the active faults in scope. +# Values follow ros2_medkit_msgs/Fault SEVERITY_* semantics: 0 INFO, 1 WARN, +# 2 ERROR, 3 CRITICAL. Typically drives the signal column. +uint8 aggregate_severity + +# Number of active faults in scope. Optional indicator on the panel. +uint16 active_fault_count + +# Coarse fault classes currently active, one bit per class. +# +# Intended for machine logic, for example an interlock that must not depend on +# reading a fault list. The mapping from fault codes to bits is deployment +# configuration on the ROS 2 side, not a fixed catalogue in this message. +uint16 class_bitmask + +# Which part of the plant this status covers, expressed as an entity in the SOVD +# tree, e.g. an area or a component id. Lets one diagnostics instance serve +# several cells without them seeing each other's faults. +string scope diff --git a/src/ros2_medkit_plc_msgs/package.xml b/src/ros2_medkit_roxsie_msgs/package.xml similarity index 79% rename from src/ros2_medkit_plc_msgs/package.xml rename to src/ros2_medkit_roxsie_msgs/package.xml index 8c49d35c4..7427e848a 100644 --- a/src/ros2_medkit_plc_msgs/package.xml +++ b/src/ros2_medkit_roxsie_msgs/package.xml @@ -1,9 +1,9 @@ - ros2_medkit_plc_msgs + ros2_medkit_roxsie_msgs 0.6.0 - Message definitions for exchanging PLC alarms and aggregate diagnostics state with ROS 2 + Message definitions for the ROXSIE integration: PLC alarm events into ROS 2 and aggregate diagnostics state back into the PLC mfaferek93 Apache-2.0 From b02f4b7e1466ecd40abb1028f87129323b5724bd Mon Sep 17 00:00:00 2001 From: mfaferek93 Date: Mon, 27 Jul 2026 19:24:09 +0200 Subject: [PATCH 4/8] docs(roxsie_msgs): plainer wording in the package docs Shorter sentences, direct statements, no rhetorical framing. --- src/ros2_medkit_roxsie_msgs/README.md | 65 +++++----- src/ros2_medkit_roxsie_msgs/design/index.rst | 112 ++++++++---------- .../msg/AlarmEvent.msg | 26 ++-- .../msg/DiagnosticsStatus.msg | 14 +-- 4 files changed, 98 insertions(+), 119 deletions(-) diff --git a/src/ros2_medkit_roxsie_msgs/README.md b/src/ros2_medkit_roxsie_msgs/README.md index a3b82f8be..77e430ac8 100644 --- a/src/ros2_medkit_roxsie_msgs/README.md +++ b/src/ros2_medkit_roxsie_msgs/README.md @@ -1,59 +1,58 @@ # ros2_medkit_roxsie_msgs -Message definitions for the ROXSIE integration: PLC alarm events into ROS 2, and aggregate -diagnostics state back into the PLC. +Messages for the ROXSIE integration: PLC alarm events into ROS 2, and aggregate diagnostics +state back into the PLC. ## Overview -Siemens SIMATIC ROS Connector (ROXSIE) generates, from a YAML configuration, both the -PLC-side blocks and a ROS 2 package that exchange declared data between a SIMATIC PLC and -ROS 2. Its configuration surface today covers data topics. Alarms are the gap: a machine -that combines a PLC and ROS 2 still has two fault stories that never meet. The PLC has a -mature alarm system - numbered alarms, coming and going states, acknowledgement, values -latched at the moment the alarm fired. ROS 2 has faults, freeze-frames and a diagnostics -tree. - -This package defines the two messages that close that gap. Alarms declared in the TIA -project reach ROS 2 as structured events and become faults with freeze-frames; the -diagnostics side sends back one small status word for the signal column and for machine +ROXSIE generates the PLC blocks and a ROS 2 package for the data you declare in the YAML +config. 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. + +This package holds the two message definitions for that exchange. Alarms declared in the TIA +project arrive in ROS 2 as faults, with the values the alarm system latched when the alarm +fired. Back the other way, the PLC gets four numbers for the signal column and for machine logic. -Two directions, deliberately asymmetric: +The two directions are not the same size, on purpose. Every alarm goes out with its full +context. Back the other way we send four numbers. -- **Alarms flow out in full** - every alarm event becomes an `AlarmEvent` message. -- **Only a status word flows back in** - `DiagnosticsStatus` carries four fixed values, no - fault list and no strings the PLC would have to build. +Alarm text has no fixed length and changes with the project. A PLC that handles strings +loses determinism, and determinism is why it is there. Writing the fault list into the PLC +would give us two lists, and two lists drift. -Fault text is dynamic and unbounded, and string handling inside a PLC costs determinism. A -fault list inside the PLC would also be a second copy that drifts from the first. What the -machine needs from the diagnostics side is a condition it can act on; everything richer is -read over REST by whoever needs it. +The PLC only needs something it can switch on: a lamp for the operator, a bit for an +interlock. Whoever wants the detail reads it over REST. ## Messages ### AlarmEvent.msg -One alarm event from the PLC alarm system, published by the ROXSIE generated node. +One alarm event from the CPU alarm system, published by the ROXSIE generated node. | Field | Type | Description | |-------|------|-------------| | `alarm_id` | uint32 | Numeric alarm identifier from the TIA project | | `source` | string | Block or instance that declared the alarm | -| `alarm_class` | string | Alarm class from the project, e.g. whether acknowledgement is required | +| `alarm_class` | string | Alarm class from the project, e.g. whether it needs an ack | | `state` | uint8 | `STATE_COMING` / `STATE_GOING` | | `ack_state` | uint8 | `ACK_UNACKNOWLEDGED` / `ACK_ACKNOWLEDGED` | | `plc_timestamp` | builtin_interfaces/Time | Event time from the PLC clock, UTC | -| `associated_values` | float64[] | Values latched when the alarm fired | +| `associated_values` | float64[] | Values the alarm system latched when the alarm fired | -Alarm texts are not carried at runtime. The generator emits a static alarm number to text -table next to the generated code, and the bridge resolves the text on the ROS 2 side. +Alarm text does not travel at runtime. The generator writes an alarm number to text table +next to the code, and the bridge looks the text up on the ROS 2 side. -A `STATE_GOING` event is not a delete. It moves the fault towards healing, the same way the -alarm system keeps a condition until it is acknowledged, so history survives for the audit -trail. +`STATE_GOING` moves the fault towards healing. We keep the fault, so the history stays for +the audit trail, the same way the alarm system keeps a condition until someone acks it. -`ack_state` is the shared acknowledgement state. Whoever acknowledges first, every other -consumer sees the same value rather than keeping a private copy. +`ack_state` is one shared value. The operator acks on the panel or a client acks over REST, +and both sides see the same thing. ### DiagnosticsStatus.msg @@ -68,8 +67,8 @@ written into a consumed data block. | `class_bitmask` | uint16 | Coarse fault classes for machine logic | | `scope` | string | Which entity in the SOVD tree this status covers | -A stalled `heartbeat` lets a watchdog in the PLC program raise its own alarm, so the plant -sees that diagnostics are offline instead of a stale healthy state. +When `heartbeat` stops, a watchdog in the PLC program raises its own alarm. The plant sees +that diagnostics are down instead of a green light that means nothing. `scope` lets one diagnostics instance serve several cells without them seeing each other's faults. diff --git a/src/ros2_medkit_roxsie_msgs/design/index.rst b/src/ros2_medkit_roxsie_msgs/design/index.rst index 187af9799..ac84357ff 100644 --- a/src/ros2_medkit_roxsie_msgs/design/index.rst +++ b/src/ros2_medkit_roxsie_msgs/design/index.rst @@ -6,52 +6,43 @@ This section contains design documentation for the ros2_medkit_roxsie_msgs packa Overview -------- -Siemens SIMATIC ROS Connector (ROXSIE) generates, from a YAML configuration, both the -PLC-side blocks and a ROS 2 package that exchange declared data between a SIMATIC PLC -and ROS 2 over shared memory on the host. The generated node sits on the ROS 2 graph, -so anything already listening to that graph can consume the data. - -Alarms are the gap. The configuration surface covers declared data topics, while the -alarm system is a separate mechanism inside the CPU: numbered alarms, coming and going -states, acknowledgement, and values latched at the moment the alarm fired. A machine -that combines a SIMATIC PLC with ROS 2 therefore still keeps two fault stories that -never meet, and joining them today means reading alarm bits over a protocol and -rebuilding their meaning from an engineering export, or copying alarms by hand into a -second list on the panel. - -This package defines the two messages that close the gap. It has no runtime code - only -``.msg`` definitions compiled by ``rosidl`` into C++ and Python bindings. +ROXSIE 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. The generated node sits on the ROS 2 +graph, so anything already listening to that graph can read the data. -Design ------- +That covers process values. It does not cover alarms, because alarms live in the CPU +message system and not in a data block: numbered alarms, coming and going states, +acknowledgement, and the values the alarm system latched when the alarm fired. + +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. -Two directions, deliberately asymmetric. +This package holds the two message definitions for that exchange. It has no runtime code, +only ``.msg`` definitions that ``rosidl`` compiles into C++ and Python bindings. -**Alarms flow out in full.** Every alarm event the CPU alarm system produces is published -as ``AlarmEvent``: which alarm, coming or going, acknowledged or not, the PLC's own -timestamp, and the values latched when it fired. The bridge turns each event into a fault -with a freeze-frame and an audit trail. +Design +------ -**Only a status word flows back in.** The diagnostics side does not push faults into the -PLC. It publishes ``DiagnosticsStatus``, four fixed values that the generated node writes -into a consumed data block: heartbeat, worst severity, active count, coarse class bits. +The two directions are not the same size, on purpose. Every alarm goes out with its full +context as ``AlarmEvent``: which alarm, coming or going, acked or not, the PLC timestamp, +and the latched values. The bridge turns each event into a fault with a freeze-frame and +an audit trail. Back the other way we send four numbers as ``DiagnosticsStatus``, and the +generated node writes them into a consumed data block. -The asymmetry is the point: +Alarm text has no fixed length and changes with the project. A PLC that handles strings +loses determinism, and determinism is why it is there. Writing the fault list into the PLC +would give us two lists, and two lists drift. -* Fault text is dynamic and unbounded. String handling inside a PLC costs determinism, - which is the one property a PLC exists to provide. -* A fault list inside the PLC would be a second copy of the fault list, and two copies - drift apart. The value of one source of truth disappears the moment it is duplicated. -* What the machine needs from the diagnostics side is a condition it can act on: a lamp - for the operator, a bit for an interlock. Everything richer is read over REST by - whoever needs it, at the moment they need it. +The PLC only needs something it can switch on: a lamp for the operator, a bit for an +interlock. Whoever wants the detail reads it over REST. -Alarm texts never travel at runtime. The generator emits a static alarm number to text -table next to the generated code, and the bridge resolves the text on the ROS 2 side. +Alarm text does not travel at runtime. The generator writes an alarm number to text table +next to the code, and the bridge looks the text up on the ROS 2 side. -Acknowledgement is one shared state. Whoever acknowledges first - an operator on the -panel or a client on the ROS 2 side - every other consumer sees the same value rather -than keeping a private copy. +Acknowledgement is one shared value. The operator acks on the panel or a client acks over +REST, and both sides see the same thing. Architecture ------------ @@ -93,33 +84,26 @@ Architecture @enduml -Deployment note: the generated node exchanges data with the PLC over shared memory on -the same host, but publishes on the ROS 2 graph over the host network. The diagnostics -side therefore does not have to run on the controller; it subscribes like any other ROS 2 -participant. +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. It subscribes like any other ROS 2 participant. What the Generator Has to Provide --------------------------------- -The messages describe the wire. The generated code is what makes the alarms reachable at -all, so its obligations are part of the contract: - -* **Observe the alarm system, not a data block mirror.** Alarm events live in the CPU - message system, not in ordinary data blocks. Reaching them needs either a native - subscription or program code that reads pending alarms and forwards them. -* **Latch values at trigger time.** ``associated_values`` must be what the alarm system - captured when the alarm fired. Re-reading them afterwards produces a plausible but - wrong freeze-frame. -* **Timestamp at the source.** ``plc_timestamp`` comes from the PLC clock in UTC, not - from the moment of publishing. -* **Re-publish pending alarms after a restart.** A node that starts fresh must send the - alarms that are currently active, otherwise an active alarm silently disappears from - the diagnostics side for as long as it stays active. -* **Emit the alarm number to text table.** Generated once from the TIA project alongside - the code, consumed on the ROS 2 side. The table travels with the deployment, not with - each event. -* **Write the consumed status block.** Four values into the data block, so that a - watchdog in the PLC program can turn a stalled heartbeat into its own alarm. +The messages only describe the wire. The generated code is what gets the alarms out, so +these belong to the contract: + +* **Read the CPU message system, not a data block mirror.** That is where alarm events + are. +* **Put the latched values into the message.** If you read them again later, the + freeze-frame looks right and is wrong. +* **Use the PLC clock for** ``plc_timestamp``, not the publish time. +* **After a restart, send the alarms that are still active.** Otherwise an active alarm + disappears from our side until it goes away by itself. +* **Emit the alarm number to text table** when you generate the code. +* **Write the four values into the consumed data block**, so a watchdog in the program + can raise its own alarm when the heartbeat stops. Message Definitions ------------------- @@ -140,8 +124,7 @@ Message Definitions Lifecycle --------- -A PLC alarm and a medkit fault already model the same thing, so the mapping is direct -rather than invented. +A PLC alarm and a medkit fault model the same thing, so the mapping is direct. .. list-table:: :header-rows: 1 @@ -176,8 +159,7 @@ Failure Behaviour - Currently pending alarms are re-published, so a restart does not silently lose active alarms -The first row is the load-bearing one: the failure mode of the diagnostics layer must -never be indistinguishable from a healthy machine. +The first row matters most. If our layer dies, the plant has to see it. Open Questions -------------- diff --git a/src/ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg b/src/ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg index f6ef199c8..fc00c4db6 100644 --- a/src/ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg +++ b/src/ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg @@ -18,11 +18,11 @@ # Published by the ROXSIE generated node, consumed by ros2_medkit_roxsie_bridge, # which maps each event onto a SOVD fault with a freeze-frame and an audit trail. # -# Design intent: the payload is fixed-size and free of runtime strings that the -# PLC would have to build. Alarm texts are not carried here. The generator emits -# a static alarm_id -> text table next to the generated code, and the bridge -# resolves the text on the ROS 2 side. String handling in a PLC costs -# determinism, and alarm texts do not change while the machine runs. +# The payload is fixed-size and carries no strings the PLC would have to build. +# Alarm text stays out of it: the generator writes an alarm_id -> text table next +# to the code, and the bridge looks the text up on the ROS 2 side. A PLC that +# handles strings loses determinism, and alarm text does not change while the +# machine runs. # Numeric alarm identifier as declared in the TIA project. # @@ -43,27 +43,25 @@ string alarm_class # Whether the alarm condition appeared or disappeared. Use STATE_* constants. # -# The event is not a level: a GOING event does not delete the fault, it moves it -# towards healing. The fault stays visible for the audit trail until it is -# cleared, which mirrors how the PLC alarm system latches conditions. +# This is an event, not a level. GOING moves the fault towards healing; we keep +# the fault until it is cleared, so the history stays for the audit trail. The +# alarm system latches conditions the same way. uint8 state uint8 STATE_COMING = 0 uint8 STATE_GOING = 1 # Acknowledgement state as seen by the PLC alarm system. Use ACK_* constants. # -# This is the shared acknowledgement truth. Whoever acknowledges first - an -# operator on the panel or a client on the ROS 2 side - every other consumer -# sees the same state instead of keeping a private copy. +# One shared value. The operator acks on the panel or a client acks over REST, +# and both sides see the same thing. uint8 ack_state uint8 ACK_UNACKNOWLEDGED = 0 uint8 ACK_ACKNOWLEDGED = 1 # Time of the event taken from the PLC clock, in UTC. # -# Deliberately the source time, not the time the message was published. A -# consumer can compare it against its own clock to detect a PLC that is not -# time-synchronised, which is common on plant networks. +# The source time, so a consumer can compare it against its own clock and spot a +# PLC that is not time-synchronised. That happens often on plant networks. builtin_interfaces/Time plc_timestamp # Values latched by the alarm system at the moment the alarm fired, the diff --git a/src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg b/src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg index 808f4b52a..6e4e2f7f6 100644 --- a/src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg +++ b/src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg @@ -17,16 +17,16 @@ # Published by ros2_medkit_roxsie_bridge, consumed by the ROXSIE generated node, # which writes the values into a consumed data block the PLC program reads. # -# Design intent: this is the whole of what the diagnostics side puts inside the -# PLC. Faults stay on the ROS 2 side and are read over REST by whoever needs -# them. What the PLC genuinely needs is a lamp and, optionally, a condition it -# can interlock on. Four fixed words, no strings, no lists. +# This is everything we put inside the PLC. Faults stay on the ROS 2 side and +# whoever wants them reads them over REST. The PLC only needs something it can +# switch on: a lamp, and a bit it can interlock on. Four numbers, no strings, +# no lists. # Liveness counter of the diagnostics layer, incremented about once per second. # -# A watchdog in the PLC program turns a stalled counter into its own alarm, so -# the plant sees "diagnostics offline" instead of a stale healthy state. The -# failure mode of the diagnostics layer must never look like a healthy machine. +# When it stops, a watchdog in the PLC program raises its own alarm. The plant +# then sees that diagnostics are down instead of a green light that means +# nothing. uint16 heartbeat # Highest severity among the active faults in scope. From 8fdd80ff25608dd0d6706f725c2d7b00898710aa Mon Sep 17 00:00:00 2001 From: mfaferek93 Date: Mon, 27 Jul 2026 19:37:25 +0200 Subject: [PATCH 5/8] docs(roxsie_msgs): correct the audit claim, note the status covers all 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. --- src/ros2_medkit_roxsie_msgs/README.md | 3 +++ src/ros2_medkit_roxsie_msgs/design/index.rst | 10 +++++++--- src/ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg | 3 ++- src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg | 3 ++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/ros2_medkit_roxsie_msgs/README.md b/src/ros2_medkit_roxsie_msgs/README.md index 77e430ac8..f628224fc 100644 --- a/src/ros2_medkit_roxsie_msgs/README.md +++ b/src/ros2_medkit_roxsie_msgs/README.md @@ -19,6 +19,9 @@ project arrive in ROS 2 as faults, with the values the alarm system latched when fired. Back the other way, the PLC gets four numbers for the signal column and for machine logic. +Those four numbers cover every active fault in scope, not only the ones that came from the +PLC. A navigation fault on the robot moves the same severity and the same bits. + The two directions are not the same size, on purpose. Every alarm goes out with its full context. Back the other way we send four numbers. diff --git a/src/ros2_medkit_roxsie_msgs/design/index.rst b/src/ros2_medkit_roxsie_msgs/design/index.rst index ac84357ff..98ef27062 100644 --- a/src/ros2_medkit_roxsie_msgs/design/index.rst +++ b/src/ros2_medkit_roxsie_msgs/design/index.rst @@ -27,9 +27,13 @@ Design The two directions are not the same size, on purpose. Every alarm goes out with its full context as ``AlarmEvent``: which alarm, coming or going, acked or not, the PLC timestamp, -and the latched values. The bridge turns each event into a fault with a freeze-frame and -an audit trail. Back the other way we send four numbers as ``DiagnosticsStatus``, and the -generated node writes them into a consumed data block. +and the latched values. The bridge turns each event into a fault with a freeze-frame, and +into an audit trail entry when the audit log is on. Back the other way we send four numbers +as ``DiagnosticsStatus``, and the generated node writes them into a consumed data block. + +Those four numbers cover every active fault in scope, not only the ones that came from the +PLC. A navigation fault on the robot moves the same severity and the same bits, which is +how a ROS 2 problem reaches the signal column. Alarm text has no fixed length and changes with the project. A PLC that handles strings loses determinism, and determinism is why it is there. Writing the fault list into the PLC diff --git a/src/ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg b/src/ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg index fc00c4db6..ae4726aec 100644 --- a/src/ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg +++ b/src/ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg @@ -16,7 +16,8 @@ # ROS 2 graph by the node that Siemens SIMATIC ROS Connector (ROXSIE) generates. # # Published by the ROXSIE generated node, consumed by ros2_medkit_roxsie_bridge, -# which maps each event onto a SOVD fault with a freeze-frame and an audit trail. +# which maps each event onto a SOVD fault with a freeze-frame. Fault manager also +# writes an audit trail entry when the audit log is enabled. # # The payload is fixed-size and carries no strings the PLC would have to build. # Alarm text stays out of it: the generator writes an alarm_id -> text table next diff --git a/src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg b/src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg index 6e4e2f7f6..05dc19355 100644 --- a/src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg +++ b/src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg @@ -29,7 +29,8 @@ # nothing. uint16 heartbeat -# Highest severity among the active faults in scope. +# Highest severity among the active faults in scope, whatever reported them: +# a PLC alarm, a robot node, or anything else on the ROS 2 side. # Values follow ros2_medkit_msgs/Fault SEVERITY_* semantics: 0 INFO, 1 WARN, # 2 ERROR, 3 CRITICAL. Typically drives the signal column. uint8 aggregate_severity From 977f19ad4a3150e4289b6f367e4f8e568f427fa5 Mon Sep 17 00:00:00 2001 From: mfaferek93 Date: Mon, 27 Jul 2026 23:18:56 +0200 Subject: [PATCH 6/8] feat(roxsie_msgs): carry a picture of the alarms, not a stream 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. --- src/ros2_medkit_roxsie_msgs/CHANGELOG.rst | 2 +- src/ros2_medkit_roxsie_msgs/CMakeLists.txt | 3 +- src/ros2_medkit_roxsie_msgs/README.md | 118 +++++++---- src/ros2_medkit_roxsie_msgs/design/index.rst | 200 +++++++++++------- src/ros2_medkit_roxsie_msgs/msg/Alarm.msg | 77 +++++++ .../msg/AlarmEvent.msg | 74 ------- src/ros2_medkit_roxsie_msgs/msg/AlarmList.msg | 43 ++++ .../msg/DiagnosticsStatus.msg | 46 ++-- 8 files changed, 339 insertions(+), 224 deletions(-) create mode 100644 src/ros2_medkit_roxsie_msgs/msg/Alarm.msg delete mode 100644 src/ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg create mode 100644 src/ros2_medkit_roxsie_msgs/msg/AlarmList.msg diff --git a/src/ros2_medkit_roxsie_msgs/CHANGELOG.rst b/src/ros2_medkit_roxsie_msgs/CHANGELOG.rst index 38f0ae530..1c284c6da 100644 --- a/src/ros2_medkit_roxsie_msgs/CHANGELOG.rst +++ b/src/ros2_medkit_roxsie_msgs/CHANGELOG.rst @@ -4,4 +4,4 @@ Changelog for package ros2_medkit_roxsie_msgs Forthcoming ----------- -* Initial interface definitions for the ROXSIE integration: ``AlarmEvent.msg`` for PLC alarm events entering ROS 2, ``DiagnosticsStatus.msg`` for aggregate diagnostics state going back into the PLC. +* Initial interface definitions for the ROXSIE integration: ``Alarm.msg`` and ``AlarmList.msg`` for the cyclic picture of alarms in the PLC, ``DiagnosticsStatus.msg`` for the condition sent back into the PLC. diff --git a/src/ros2_medkit_roxsie_msgs/CMakeLists.txt b/src/ros2_medkit_roxsie_msgs/CMakeLists.txt index 225facb70..7b7fcd06d 100644 --- a/src/ros2_medkit_roxsie_msgs/CMakeLists.txt +++ b/src/ros2_medkit_roxsie_msgs/CMakeLists.txt @@ -22,7 +22,8 @@ find_package(rosidl_default_generators REQUIRED) find_package(builtin_interfaces REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} - "msg/AlarmEvent.msg" + "msg/Alarm.msg" + "msg/AlarmList.msg" "msg/DiagnosticsStatus.msg" DEPENDENCIES builtin_interfaces ) diff --git a/src/ros2_medkit_roxsie_msgs/README.md b/src/ros2_medkit_roxsie_msgs/README.md index f628224fc..ceacdc5ad 100644 --- a/src/ros2_medkit_roxsie_msgs/README.md +++ b/src/ros2_medkit_roxsie_msgs/README.md @@ -1,84 +1,112 @@ # ros2_medkit_roxsie_msgs -Messages for the ROXSIE integration: PLC alarm events into ROS 2, and aggregate diagnostics -state back into the PLC. +Messages for the ROXSIE integration: PLC alarms into ROS 2, and one status value back +into the PLC. ## Overview ROXSIE generates the PLC blocks and a ROS 2 package for the data you declare in the YAML -config. That covers process values. It does not cover alarms, because alarms live in the CPU -message system and not in a data block. +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. +meaning from an engineering export, or someone copies the alarms by hand into a second +list on the panel. Both drift. -This package holds the two message definitions for that exchange. Alarms declared in the TIA -project arrive in ROS 2 as faults, with the values the alarm system latched when the alarm -fired. Back the other way, the PLC gets four numbers for the signal column and for machine -logic. +This package holds the messages for that exchange. Alarms in the PLC arrive in ROS 2 and +become faults with freeze-frames. Back the other way, the PLC gets one value it can act on. -Those four numbers cover every active fault in scope, not only the ones that came from the -PLC. A navigation fault on the robot moves the same severity and the same bits. +## A picture, not a stream -The two directions are not the same size, on purpose. Every alarm goes out with its full -context. Back the other way we send four numbers. +`AlarmList` carries every alarm that is active or waiting for acknowledgement, published +cyclically. That is a deliberate choice. -Alarm text has no fixed length and changes with the project. A PLC that handles strings -loses determinism, and determinism is why it is there. Writing the fault list into the PLC -would give us two lists, and two lists drift. +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. -The PLC only needs something it can switch on: a lamp for the operator, a bit for an -interlock. Whoever wants the detail reads it over REST. +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. + +## Two ways to fill the list + +**From bits.** The customer declares alarm bits in a data block and ROXSIE carries them +like any other 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 instead. 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. + +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. ## Messages -### AlarmEvent.msg +### Alarm.msg -One alarm event from the CPU alarm system, published by the ROXSIE generated node. +One alarm, as one entry of the list. | Field | Type | Description | |-------|------|-------------| -| `alarm_id` | uint32 | Numeric alarm identifier from the TIA project | +| `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, e.g. whether it needs an ack | -| `state` | uint8 | `STATE_COMING` / `STATE_GOING` | -| `ack_state` | uint8 | `ACK_UNACKNOWLEDGED` / `ACK_ACKNOWLEDGED` | -| `plc_timestamp` | builtin_interfaces/Time | Event time from the PLC clock, UTC | -| `associated_values` | float64[] | Values the alarm system latched when the alarm fired | +| `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 | -Alarm text does not travel at runtime. The generator writes an alarm number to text table -next to the code, and the bridge looks the text up on the ROS 2 side. +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. -`STATE_GOING` moves the fault towards healing. We keep the fault, so the history stays for -the audit trail, the same way the alarm system keeps a condition until someone acks it. +Alarm text does not travel. The text belongs to the project, changes with the project, and +is resolved on the ROS 2 side from a table that ships with the deployment. -`ack_state` is one shared value. The operator acks on the panel or a client acks over REST, -and both sides see the same thing. +### AlarmList.msg -### DiagnosticsStatus.msg +| Field | Type | Description | +|-------|------|-------------| +| `stamp` | builtin_interfaces/Time | When this picture was taken | +| `alarms` | Alarm[] | Alarms active or not yet acknowledged | -Aggregate diagnostics state towards the PLC, consumed by the ROXSIE generated node and -written into a consumed data block. +### DiagnosticsStatus.msg | Field | Type | Description | |-------|------|-------------| | `heartbeat` | uint16 | Liveness counter of the diagnostics layer, about 1 Hz | -| `aggregate_severity` | uint8 | Highest severity in scope, `Fault` SEVERITY_* semantics | -| `active_fault_count` | uint16 | Active faults in scope | -| `class_bitmask` | uint16 | Coarse fault classes for machine logic | -| `scope` | string | Which entity in the SOVD tree this status covers | +| `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 | + +`condition` says what we are asking of the machine, not 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. We hand over a condition and let that logic decide. -When `heartbeat` stops, a watchdog in the PLC program raises its own alarm. The plant sees -that diagnostics are down instead of a green light that means nothing. +When `heartbeat` stops, a watchdog in the PLC program raises its own alarm, so the plant +sees that diagnostics are down instead of a green light that means nothing. -`scope` lets one diagnostics instance serve several cells without them seeing each other's -faults. +`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. ## Related packages -- `ros2_medkit_msgs` - the core fault model these events are mapped onto +- `ros2_medkit_msgs` - the core fault model these alarms are mapped onto - `ros2_medkit_fault_manager` - fault aggregation, debounce and freeze-frame capture - `ros2_medkit_diagnostic_bridge` - the same bridging pattern for ROS 2 `/diagnostics` diff --git a/src/ros2_medkit_roxsie_msgs/design/index.rst b/src/ros2_medkit_roxsie_msgs/design/index.rst index 98ef27062..e1e3ccbdc 100644 --- a/src/ros2_medkit_roxsie_msgs/design/index.rst +++ b/src/ros2_medkit_roxsie_msgs/design/index.rst @@ -11,42 +11,66 @@ config, exchanged over shared memory on the host. The generated node sits on the graph, so anything already listening to that graph can read the data. That covers process values. It does not cover alarms, because alarms live in the CPU -message system and not in a data block: numbered alarms, coming and going states, -acknowledgement, and the values the alarm system latched when the alarm fired. +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. -This package holds the two message definitions for that exchange. It has no runtime code, -only ``.msg`` definitions that ``rosidl`` compiles into C++ and Python bindings. +This package holds the messages for that exchange. It has no runtime code, only ``.msg`` +definitions that ``rosidl`` compiles into C++ and Python bindings. -Design ------- +A Picture, Not a Stream +----------------------- -The two directions are not the same size, on purpose. Every alarm goes out with its full -context as ``AlarmEvent``: which alarm, coming or going, acked or not, the PLC timestamp, -and the latched values. The bridge turns each event into a fault with a freeze-frame, and -into an audit trail entry when the audit log is on. Back the other way we send four numbers -as ``DiagnosticsStatus``, and the generated node writes them into a consumed data block. +``AlarmList`` carries every alarm that is active or waiting for acknowledgement, published +cyclically. Two reasons. -Those four numbers cover every active fault in scope, not only the ones that came from the -PLC. A navigation fault on the robot moves the same severity and the same bits, which is -how a ROS 2 problem reaches the signal column. +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. An event stream would be built on top of that and would +need its own delivery guarantees. -Alarm text has no fixed length and changes with the project. A PLC that handles strings -loses determinism, and determinism is why it is there. Writing the fault list into the PLC -would give us two lists, and two lists drift. +A picture also 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. The gap is not hypothetical: reading alarms +over OPC UA has the same hole, because no table of pending alarms is served on reconnect. -The PLC only needs something it can switch on: a lamp for the operator, a bit for an -interlock. Whoever wants the detail reads it over REST. +The bridge compares consecutive pictures. An entry that appears is reported as a fault, an +entry that disappears heals it. Debounce, freeze-frame capture and the audit trail are the +same ones every other fault source in medkit goes through. -Alarm text does not travel at runtime. The generator writes an alarm number to text table -next to the code, and the bridge looks the text up on the ROS 2 side. +Two Ways to Fill the List +------------------------- -Acknowledgement is one shared value. The operator acks on the panel or a client acks over -REST, and both sides see the same thing. +**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 path +works today. ``alarm_class``, ``priority``, ``producer``, ``plc_timestamp`` and +``associated_values`` stay empty, and the freeze-frame comes from the process topics the +same 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 +message can hold: + +* the event time from the CPU clock, rather than the moment ROS 2 noticed +* the alarm class, which says whether the alarm needs acknowledgement +* the priority the plant already engineers with, 0 to 16 +* the values the alarm system latched when the alarm fired, capped by the CPU at 512 bytes + across all of them + +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, +which 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. Architecture ------------ @@ -60,9 +84,10 @@ Architecture package "SIMATIC PLC" { [CPU alarm system] as AS - [ROXSIE generated blocks] as GEN + [Alarm bits] as BITS + [Alarm list block] as GEN [Consumed status block] as DB - [Signal column\nMachine interlocks] as SIG + [Machine logic\nSignal column] as SIG } package "ROS 2" { @@ -74,14 +99,14 @@ Architecture [Panel page, dashboards,\nmaintenance clients] as CONS - AS --> GEN : alarm events + AS --> GEN : Get_Alarm + BITS --> GEN : bit path GEN --> NODE : shared memory - NODE --> BR : AlarmEvent + NODE --> BR : AlarmList BR --> FM : ReportFault FM --> BR : fault events BR --> NODE : DiagnosticsStatus - NODE --> GEN : shared memory - GEN --> DB + NODE --> DB : shared memory DB --> SIG FM -- GW GW --> CONS : HTTPS @@ -92,22 +117,24 @@ The generated node talks to the PLC over shared memory on the same host, but pub the ROS 2 graph over the network. So the diagnostics side does not have to run on the controller. It subscribes like any other ROS 2 participant. -What the Generator Has to Provide ---------------------------------- +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 that means for the lamp, for an +interlock, or for a state transition. -The messages only describe the wire. The generated code is what gets the alarms out, so -these belong to the contract: +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. -* **Read the CPU message system, not a data block mirror.** That is where alarm events - are. -* **Put the latched values into the message.** If you read them again later, the - freeze-frame looks right and is wrong. -* **Use the PLC clock for** ``plc_timestamp``, not the publish time. -* **After a restart, send the alarms that are still active.** Otherwise an active alarm - disappears from our side until it goes away by itself. -* **Emit the alarm number to text table** when you generate the code. -* **Write the four values into the consumed data block**, so a watchdog in the program - can raise its own alarm when the heartbeat stops. +``class_bitmask`` is there for machine logic that needs to interlock on a kind of fault +without reading a list. Which fault sets which bit is deployment configuration. Message Definitions ------------------- @@ -118,32 +145,17 @@ Message Definitions * - Message - Purpose - * - ``AlarmEvent.msg`` - - One alarm event from the CPU alarm system: identifier, source, class, coming or - going, acknowledgement state, PLC timestamp, latched values + * - ``Alarm.msg`` + - One alarm: identity, source, class, priority, producer, active and acknowledged + flags, PLC timestamp, latched values + * - ``AlarmList.msg`` + - The cyclic picture: every alarm active or not yet acknowledged * - ``DiagnosticsStatus.msg`` - - Aggregate diagnostics state towards the PLC: heartbeat, worst severity, active - count, coarse class bits, scope + - What the diagnostics side asks of the machine: heartbeat, condition, class bits, + scope -Lifecycle ---------- - -A PLC alarm and a medkit fault model the same thing, so the mapping is direct. - -.. list-table:: - :header-rows: 1 - :widths: 40 60 - - * - Alarm event - - Fault behaviour - * - ``STATE_COMING`` - - Reported as a FAILED event; debounce confirms it and a freeze-frame is captured - from the latched values - * - ``STATE_GOING`` - - Reported as a PASSED event; the fault heals but is not deleted, so history stays - for the audit trail - * - ``ACK_ACKNOWLEDGED`` - - The fault is cleared; the acknowledgement is the same state on both sides +Alarm text does not travel at runtime. The text belongs to the project, changes with the +project, and is resolved on the ROS 2 side from a table that ships with the deployment. Failure Behaviour ----------------- @@ -156,24 +168,52 @@ Failure Behaviour - Behaviour * - Diagnostics layer or link down - The heartbeat stops and a watchdog in the PLC program raises its own alarm, so the - plant sees that diagnostics are offline rather than a stale healthy state + plant sees that diagnostics are down rather than a green light that means nothing * - PLC or the generated node down - The bridge reports a communications fault on the ROS 2 side - * - Bridge restarts - - Currently pending alarms are re-published, so a restart does not silently lose - active alarms + * - 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 package 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 of them 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 would leave three things to settle: which signal becomes a fault, where log +records at error level are the obvious answer and metrics the more interesting one, since +``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, since resource +attributes have to map onto the tree or every host lands in one unattached pile; and what +the operator sees, which needs no new wiring, because 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 -------------- -#. Is ``alarm_id`` unique per CPU, or only per block? If only per block, the unique key - has to be ``(source, alarm_id)`` and the bridge cannot key on the number alone. -#. Numeric or typed associated values? Numeric keeps the message fixed-size and cheap on - the PLC side; typed is richer but reintroduces strings. -#. Can the generated code acknowledge an alarm in the CPU alarm system, or is - acknowledgement panel-only? It decides whether an acknowledgement made on the ROS 2 - side can reach the native alarm list. -#. Who owns the class catalogue behind ``class_bitmask``, and is 16 bits enough? -#. Topic names and QoS for the two topics, and whether they are namespaced per cell. +#. Is ``alarm_id`` unique per CPU, or only per block? If only per block, the key has to be + ``(source, alarm_id)``. +#. Where does the alarm number to text table come from - the project export the generator + already reads, or a separate export? +#. 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. +#. 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. +#. Numeric associated values, or typed? Numeric keeps the entry fixed size. Typed carries + more and brings strings back. diff --git a/src/ros2_medkit_roxsie_msgs/msg/Alarm.msg b/src/ros2_medkit_roxsie_msgs/msg/Alarm.msg new file mode 100644 index 000000000..cc271bb0b --- /dev/null +++ b/src/ros2_medkit_roxsie_msgs/msg/Alarm.msg @@ -0,0 +1,77 @@ +# Copyright 2026 mfaferek93 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Alarm.msg - one alarm in the PLC, as one entry of AlarmList. +# +# Not an event. An entry stays in the list while the alarm is active or waiting +# for acknowledgement, and drops out once it is gone and acknowledged. The two +# flags below mirror how the CPU alarm system tracks a condition. +# +# Fields the bit path cannot fill stay empty. A consumer that finds an empty +# alarm_class or no associated values is looking at a bit-filled alarm, not at +# an error. + +# Numeric alarm identifier from the PLC project. +# +# OPEN QUESTION: unique per CPU, or only per block? If only per block, the key +# is (source, alarm_id) and a consumer cannot key on the number alone. +uint32 alarm_id + +# Block or instance that declared the alarm. Static knowledge of the generator. +# Also the natural handle for placing the alarm under an entity in the tree. +string source + +# Alarm class from the project, e.g. whether the alarm needs acknowledgement. +# Empty when the alarm comes from bits, which carry no class. +string alarm_class + +# Priority as the PLC knows it: 0 to 16, higher is more important. This is the +# scale the plant already engineers with, so we carry it instead of inventing +# one. 0 when unknown. +uint8 priority + +# Where the alarm came from inside the CPU: "user_program", "system_diagnostics", +# "prodiag", "graph", "motion", "security". Empty for bit-filled alarms. +# +# Worth carrying because the CPU message system produces more than user alarms. +# A module fault or a drive alarm arrives here without anyone declaring it. +string producer + +# Whether the condition is currently present. +bool active + +# Whether the alarm has been acknowledged. +# +# Both flags together, not one enum: an alarm that is gone but unacknowledged is +# a real state and stays in the list. Same two bits the CPU alarm system tracks. +# +# Who owns this depends on how the list is filled. Bits: shared, either side can +# set it. CPU alarm system: the CPU owns it, we mirror it, and setting it for a +# single alarm needs the panel or OPC UA, because the CPU has no per-alarm +# acknowledge instruction. +bool acknowledged + +# Time the condition last changed, from the PLC clock, UTC. +# +# The source time, so a consumer can compare it against its own clock and spot a +# PLC that is not time-synchronised. That happens often on plant networks. Empty +# in the bit path, where nobody recorded when it happened. +builtin_interfaces/Time plc_timestamp + +# Values the alarm system latched when the alarm fired. They become the +# freeze-frame of the fault. +# +# The CPU caps these at 512 bytes across all of them. In the bit path there are +# none, and the freeze-frame comes from the process topics instead. +float64[] associated_values diff --git a/src/ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg b/src/ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg deleted file mode 100644 index ae4726aec..000000000 --- a/src/ros2_medkit_roxsie_msgs/msg/AlarmEvent.msg +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2026 mfaferek93 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# AlarmEvent.msg - one alarm event from the PLC alarm system, carried onto the -# ROS 2 graph by the node that Siemens SIMATIC ROS Connector (ROXSIE) generates. -# -# Published by the ROXSIE generated node, consumed by ros2_medkit_roxsie_bridge, -# which maps each event onto a SOVD fault with a freeze-frame. Fault manager also -# writes an audit trail entry when the audit log is enabled. -# -# The payload is fixed-size and carries no strings the PLC would have to build. -# Alarm text stays out of it: the generator writes an alarm_id -> text table next -# to the code, and the bridge looks the text up on the ROS 2 side. A PLC that -# handles strings loses determinism, and alarm text does not change while the -# machine runs. - -# Numeric alarm identifier as declared in the TIA project. -# -# OPEN QUESTION: is this identifier unique per CPU, or only per function block? -# If it is only unique per block, the unique key must be (source, alarm_id) and -# the bridge cannot key on alarm_id alone. -uint32 alarm_id - -# Origin of the alarm inside the PLC program, e.g. the function block or -# instance that declared it. Static knowledge of the generator, not read from -# the PLC at runtime. Required when alarm_id is not unique per CPU (see above). -string source - -# Alarm class as declared in the TIA project, e.g. whether the alarm requires -# acknowledgement. Carried as an opaque string because the class catalogue is -# project specific. -string alarm_class - -# Whether the alarm condition appeared or disappeared. Use STATE_* constants. -# -# This is an event, not a level. GOING moves the fault towards healing; we keep -# the fault until it is cleared, so the history stays for the audit trail. The -# alarm system latches conditions the same way. -uint8 state -uint8 STATE_COMING = 0 -uint8 STATE_GOING = 1 - -# Acknowledgement state as seen by the PLC alarm system. Use ACK_* constants. -# -# One shared value. The operator acks on the panel or a client acks over REST, -# and both sides see the same thing. -uint8 ack_state -uint8 ACK_UNACKNOWLEDGED = 0 -uint8 ACK_ACKNOWLEDGED = 1 - -# Time of the event taken from the PLC clock, in UTC. -# -# The source time, so a consumer can compare it against its own clock and spot a -# PLC that is not time-synchronised. That happens often on plant networks. -builtin_interfaces/Time plc_timestamp - -# Values latched by the alarm system at the moment the alarm fired, the -# associated values of the alarm. They become the freeze-frame of the fault. -# -# OPEN QUESTION: numeric only, or a typed variant? The alarm system can attach -# non-numeric values. Numeric keeps the message fixed-size and cheap on the PLC -# side; a typed variant is richer but reintroduces strings. -float64[] associated_values diff --git a/src/ros2_medkit_roxsie_msgs/msg/AlarmList.msg b/src/ros2_medkit_roxsie_msgs/msg/AlarmList.msg new file mode 100644 index 000000000..2c5484c5f --- /dev/null +++ b/src/ros2_medkit_roxsie_msgs/msg/AlarmList.msg @@ -0,0 +1,43 @@ +# Copyright 2026 mfaferek93 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# AlarmList.msg - every alarm in the PLC that is active or waiting for +# acknowledgement, published cyclically. +# +# A picture of the current state, not a stream of events. Two reasons. +# +# The transport is cyclic: the PLC-side node reads a data block at a fixed rate, +# so a picture is what it naturally has. An event stream would have to be built +# on top of it and would then need its own delivery guarantees. +# +# A picture survives a restart. Whoever restarts - the PLC-side node, the bridge, +# the whole diagnostics stack - the next cycle carries the full truth again. An +# event stream loses whatever happened while it was away, and the consumer has no +# way to tell an empty stream from a healthy machine. This is the same gap we hit +# reading alarms over OPC UA, where no table of pending alarms is served on +# reconnect. +# +# The bridge compares consecutive pictures: an entry that appears is reported as a +# fault, an entry that disappears heals it. + +# When this picture was taken, from the PLC clock. +builtin_interfaces/Time stamp + +# Alarms that are active or not yet acknowledged. Order is not significant. +# +# OPEN QUESTION: how many entries can the generated data block hold? A plant can +# declare hundreds of alarms, and the array is fixed size once generated. If the +# ceiling is low, the fill has to prioritise, and then it also has to say that it +# truncated. +Alarm[] alarms diff --git a/src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg b/src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg index 05dc19355..2c988105d 100644 --- a/src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg +++ b/src/ros2_medkit_roxsie_msgs/msg/DiagnosticsStatus.msg @@ -12,15 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# DiagnosticsStatus.msg - aggregate diagnostics state sent towards the PLC. +# DiagnosticsStatus.msg - what the diagnostics side asks of the machine. # -# Published by ros2_medkit_roxsie_bridge, consumed by the ROXSIE generated node, -# which writes the values into a consumed data block the PLC program reads. +# Written into a consumed data block by the PLC-side node. This is everything we +# put inside the PLC. Faults stay on the ROS 2 side and whoever wants them reads +# them over REST. # -# This is everything we put inside the PLC. Faults stay on the ROS 2 side and -# whoever wants them reads them over REST. The PLC only needs something it can -# switch on: a lamp, and a bit it can interlock on. Four numbers, no strings, -# no lists. +# It says what we are asking for, not what to display. Machines already have a +# state machine and, where PackML is used, a block that turns machine state into +# signal column bits. We do not drive lamps; we hand the machine a condition its +# own logic can act on. Two writers on one lamp is a fault of its own. # Liveness counter of the diagnostics layer, incremented about once per second. # @@ -29,23 +30,22 @@ # nothing. uint16 heartbeat -# Highest severity among the active faults in scope, whatever reported them: -# a PLC alarm, a robot node, or anything else on the ROS 2 side. -# Values follow ros2_medkit_msgs/Fault SEVERITY_* semantics: 0 INFO, 1 WARN, -# 2 ERROR, 3 CRITICAL. Typically drives the signal column. -uint8 aggregate_severity +# What we are asking the machine to do about the faults in scope. +# +# The wording follows the colour semantics the signalling standards already use, +# so an integrator can wire it without a translation table. +uint8 condition +uint8 CONDITION_OK = 0 +uint8 CONDITION_ABNORMAL = 1 +uint8 CONDITION_ACTION_REQUIRED = 2 +uint8 CONDITION_STOP = 3 -# Number of active faults in scope. Optional indicator on the panel. -uint16 active_fault_count - -# Coarse fault classes currently active, one bit per class. -# -# Intended for machine logic, for example an interlock that must not depend on -# reading a fault list. The mapping from fault codes to bits is deployment -# configuration on the ROS 2 side, not a fixed catalogue in this message. +# Optional, for machine logic only, one bit per coarse fault class. Meant for an +# interlock that must not depend on reading a fault list. Which fault sets which +# bit is deployment configuration on the ROS 2 side. Zero when unused. uint16 class_bitmask -# Which part of the plant this status covers, expressed as an entity in the SOVD -# tree, e.g. an area or a component id. Lets one diagnostics instance serve -# several cells without them seeing each other's faults. +# Which part of the plant this status covers, as an entity in the SOVD tree, e.g. +# an area or a component id. Lets one diagnostics instance serve several cells +# without them seeing each other's faults. string scope From 7618273073ead2e393647900e84fc08d6b4b7c88 Mon Sep 17 00:00:00 2001 From: mfaferek93 Date: Tue, 28 Jul 2026 08:26:36 +0200 Subject: [PATCH 7/8] docs(roxsie_msgs): ask for the bridge mapping as a generator output 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. --- src/ros2_medkit_roxsie_msgs/design/index.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ros2_medkit_roxsie_msgs/design/index.rst b/src/ros2_medkit_roxsie_msgs/design/index.rst index e1e3ccbdc..851e66b7d 100644 --- a/src/ros2_medkit_roxsie_msgs/design/index.rst +++ b/src/ros2_medkit_roxsie_msgs/design/index.rst @@ -217,3 +217,9 @@ Open Questions acknowledgement from the ROS 2 side stays a bit-path feature. #. Numeric associated values, or typed? Numeric keeps the entry fixed size. Typed carries more and brings strings back. +#. 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? From a26e71cc52761395c88e35c9e8086acc3aeb0084 Mon Sep 17 00:00:00 2001 From: mfaferek93 Date: Tue, 28 Jul 2026 08:32:07 +0200 Subject: [PATCH 8/8] docs(roxsie_msgs): break the OTel paragraph into bullets --- src/ros2_medkit_roxsie_msgs/design/index.rst | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/ros2_medkit_roxsie_msgs/design/index.rst b/src/ros2_medkit_roxsie_msgs/design/index.rst index 851e66b7d..d773bf861 100644 --- a/src/ros2_medkit_roxsie_msgs/design/index.rst +++ b/src/ros2_medkit_roxsie_msgs/design/index.rst @@ -187,13 +187,15 @@ that flaps, the controller that runs hot, the compute in another building that t depends on - none of them speak the PLC alarm system, and most of them 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 would leave three things to settle: which signal becomes a fault, where log -records at error level are the obvious answer and metrics the more interesting one, since -``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, since resource -attributes have to map onto the tree or every host lands in one unattached pile; and what -the operator sees, which needs no new wiring, because an IT fault in the same list already -moves the condition that reaches the signal column. +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