Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c72964b
feat(opcua): event subscription primitive with generation counter
mfaferek93 Apr 25, 2026
bb582d7
feat(opcua): AlarmConditionType state machine, poller wiring, ack/con…
mfaferek93 Apr 25, 2026
c57feef
test(opcua): test_alarm_server fixture, docker integration, CI workfl…
mfaferek93 Apr 25, 2026
d00a033
test(opcua): CTest smoke wrapper for test_alarm_server fixture
mfaferek93 Apr 25, 2026
b86aade
test(opcua): exercise SOVD ack/confirm + cover shelve/disable/reconne…
mfaferek93 Apr 25, 2026
ba8e956
fix(opcua,test): unblock alarm test FIFO + pre-write discovery manifest
mfaferek93 Apr 25, 2026
9aa26ac
fix(opcua,test): dump container logs in cleanup trap before removing …
mfaferek93 Apr 25, 2026
1a0273c
fix(opcua,test): keep docker stdin alive + stage gateway_params for b…
mfaferek93 Apr 25, 2026
9233289
debug(opcua): trace NodeId + status in add_event_monitored_item
mfaferek93 Apr 25, 2026
424e2bc
debug(opcua): use deep-copy NodeId + AlarmConditionType filter type +…
mfaferek93 Apr 25, 2026
a7b09e9
fix(opcua,#386): wire SOVD ack/confirm E2E + cover shelve/disable/rec…
mfaferek93 Apr 25, 2026
6fead31
style(opcua): apply clang-format-18 to diagnostic stderr logs
mfaferek93 Apr 25, 2026
2dae7c7
chore(opcua,#386): post-review quick wins (idempotence, scenario name…
mfaferek93 Apr 26, 2026
12b3406
fix(opcua,#386): operator-visible warn when ConditionRefresh is rejected
mfaferek93 Apr 26, 2026
74b8460
test(opcua,#386): unit cover the call_method per-arg result classifier
mfaferek93 Apr 26, 2026
b490c2a
test(opcua,#386): cover the missing AlarmStateMachine transition cells
mfaferek93 Apr 26, 2026
f66bb5b
fix(opcua,#386): per-MI active flag for event MI removal (Copilot rev…
mfaferek93 Apr 26, 2026
9e7d2c8
fix(opcua,#386): Copilot review feedback batch (observability + hygiene)
mfaferek93 Apr 26, 2026
dba586d
fix(opcua,#386): replace std::cerr traces with RCLCPP_DEBUG_STREAM (b…
mfaferek93 Apr 26, 2026
71473f0
docs(opcua,#386): document HEALED as internal-only + ShelvingState pr…
mfaferek93 Apr 26, 2026
0ad0306
fix(opcua,#386): input validation + cross-pipeline alarm collision ch…
mfaferek93 Apr 26, 2026
0c13920
fix(opcua,#386): use rcutils logging API for Humble compat
mfaferek93 Apr 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/opcua-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,32 @@ jobs:
run: |
docker rm -f gateway openplc 2>/dev/null || true
docker network rm plc-demo 2>/dev/null || true

integration-alarms:
name: Integration (AlarmConditionType)
# Issue #386: tests the native OPC-UA AlarmCondition subscription bridge
# against the test_alarm_server fixture (open62541 with FULL ns0 + alarms
# ON). Independent of the OpenPLC threshold-mode integration above; runs
# in parallel.
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install jq + asyncua (smoke test prerequisite)
run: |
sudo apt-get update
sudo apt-get install -y jq python3-pip
pip3 install --break-system-packages asyncua

- name: Run alarm integration suite
run: bash src/ros2_medkit_plugins/ros2_medkit_opcua/docker/scripts/run_alarm_tests.sh

- name: Dump container logs on failure
if: failure()
run: |
for c in alarm-test-server alarm-test-gateway; do
echo "=== ${c} logs ==="
docker logs "${c}" 2>&1 | tail -80 || true
done
2 changes: 2 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ This page aggregates changelogs from all ros2_medkit packages.
.. include:: ../src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/CHANGELOG.rst

.. include:: ../src/ros2_medkit_plugins/ros2_medkit_graph_provider/CHANGELOG.rst

.. include:: ../src/ros2_medkit_plugins/ros2_medkit_opcua/CHANGELOG.rst
1 change: 1 addition & 0 deletions docs/design/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This section contains design documentation for the ros2_medkit project packages.
ros2_medkit_integration_tests/index
ros2_medkit_linux_introspection/index
ros2_medkit_msgs/index
ros2_medkit_opcua/index
ros2_medkit_param_beacon/index
ros2_medkit_serialization/index
ros2_medkit_sovd_service_interface/index
Expand Down
6 changes: 6 additions & 0 deletions docs/design/ros2_medkit_opcua/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.. Aggregator stub - actual content lives in
.. ``src/ros2_medkit_plugins/ros2_medkit_opcua/design/index.rst`` and is
.. pulled in here so the published docs match what package maintainers
.. edit alongside the code (bburda review on PR #387).

.. include:: ../../../src/ros2_medkit_plugins/ros2_medkit_opcua/design/index.rst
10 changes: 10 additions & 0 deletions src/ros2_medkit_plugins/ros2_medkit_opcua/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
Changelog for package ros2_medkit_opcua
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Forthcoming
-----------
Comment thread
mfaferek93 marked this conversation as resolved.
* Native OPC-UA Part 9 ``AlarmConditionType`` event subscription. The plugin now subscribes to vendor-defined alarms (Siemens S7-1500 ``Program_Alarm`` / ProDiag, Beckhoff TF6100, CodeSys 3.5+, Rockwell via FactoryTalk Linx) and bridges each event into the SOVD fault lifecycle. Configured via a new top-level ``event_alarms:`` block in the node map YAML; mutually exclusive per entry with the existing threshold-based ``alarm`` form. (issue #386)
* New SOVD operations on entities that host alarm sources: ``acknowledge_fault`` invokes the inherited ``Acknowledge`` method on the live ``ConditionId`` (i=9111, EventId tracked per Part 9 §5.7.3); ``confirm_fault`` invokes ``Confirm`` (i=9113). Both accept an optional ``comment`` rendered as ``LocalizedText`` on the server.
* ``OpcuaClient`` gains ``add_event_monitored_item`` / ``remove_event_monitored_item`` / ``call_method`` and a generation counter that filters callbacks fired from defunct subscriptions after a reconnect. Heap-owned ``EventCallbackContext`` resolves the open62541pp / raw-C lifetime hazard.
* Header-only ``AlarmStateMachine`` mapping ``EnabledState x ShelvingState x ActiveState x AckedState x ConfirmedState x BranchId`` to SOVD ``CONFIRMED / HEALED / CLEARED / Suppressed``. Full transition table documented in ``design/index.rst``.
* ``ConditionRefresh`` (Server method i=3875) is invoked on subscribe and on every reconnect, with ``RefreshStartEvent`` / ``RefreshEndEvent`` bracketing tracked for diagnostics.
* New ``test_alarm_server`` fixture (open62541-based, full namespace 0 + alarms enabled) emits AlarmConditionType events on stdin commands; integration test ``run_alarm_tests.sh`` runs in CI alongside the existing OpenPLC threshold suite. The fixture builds by default via the workspace ``colcon build`` (gated on ``MEDKIT_OPCUA_BUILD_ALARM_SERVER`` which defaults to ON; ``ExternalProject_Add`` rebuilds open62541 with ``UA_NAMESPACE_ZERO=FULL`` and alarms ON, with a serial sub-build to dodge the upstream ``-j`` race on ``namespace0_generated.c``).
* New CTest wrapper ``test_alarm_server_smoke`` boots the fixture on an ephemeral port and runs the asyncua smoke test against it; skips with CTest exit 77 (treated as pass) when ``asyncua`` is not importable, so iterating on plugin code without the Python dependency does not fail the suite.

0.4.0 (2026-04-11)
------------------
* Initial release
Expand Down
102 changes: 102 additions & 0 deletions src/ros2_medkit_plugins/ros2_medkit_opcua/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -194,15 +194,117 @@ if(BUILD_TESTING)
target_include_directories(test_opcua_client PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
# rclcpp needed because opcua_client.cpp uses RCLCPP_DEBUG_STREAM for the
# per-event diagnostics (bburda review on PR #387). The unit tests only
# call public methods that never touch the trace path under test, so the
# dep is link-only - no rclcpp::init / Node spin-up in the suite.
medkit_target_dependencies(test_opcua_client
ros2_medkit_gateway
rclcpp
)
target_link_libraries(test_opcua_client
open62541pp::open62541pp
nlohmann_json::nlohmann_json
)
medkit_set_test_domain(test_opcua_client)

# Issue #386: pure-function state machine tests. Header-only target -
# no opcua dependency at link time so it runs fast and is sanitizer
# clean independent of the open62541pp build flavour.
ament_add_gtest(test_alarm_state_machine
test/test_alarm_state_machine.cpp
)
target_include_directories(test_alarm_state_machine PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
medkit_set_test_domain(test_alarm_state_machine)

# ---- test_alarm_server fixture ------------------------------------------
# Standalone OPC-UA server emitting AlarmConditionType events for
# integration testing of native alarm subscriptions (issue #386).
#
# The plugin's main open62541pp build is configured with
# UA_NAMESPACE_ZERO=REDUCED and UA_ENABLE_SUBSCRIPTIONS_ALARMS_CONDITIONS=OFF
# because the runtime client path uses neither. Re-enabling alarms there
# would force every consumer of open62541pp to pull in the full namespace 0
# (~5MB of generated source) and the EXPERIMENTAL A&C subsystem.
#
# Instead we build a second open62541 statically via ExternalProject_Add
# using the source already on disk from FetchContent, pinned to FULL ns0
# and alarms ON. The fixture binary links only against this private copy.
# Defaults ON because the fixture is part of the standard test suite for
# issue #386. The earlier `-j` race on open62541 namespace0_generated.c is
# fixed by forcing a serial sub-build (BUILD_COMMAND below).
option(MEDKIT_OPCUA_BUILD_ALARM_SERVER
"Build the OPC-UA AlarmCondition test fixture server (issue #386)" ON)
if(MEDKIT_OPCUA_BUILD_ALARM_SERVER)
find_package(Threads REQUIRED)
include(ExternalProject)
set(_alarm_o62_src "${open62541pp_SOURCE_DIR}/3rdparty/open62541")
set(_alarm_o62_install "${CMAKE_BINARY_DIR}/_alarm_open62541")
externalproject_add(alarm_open62541_ep
SOURCE_DIR "${_alarm_o62_src}"
PREFIX "${CMAKE_BINARY_DIR}/_alarm_open62541_ep"
INSTALL_DIR "${_alarm_o62_install}"
CMAKE_ARGS
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_INSTALL_PREFIX=${_alarm_o62_install}
-DBUILD_SHARED_LIBS=OFF
-DUA_ENABLE_SUBSCRIPTIONS_EVENTS=ON
-DUA_ENABLE_SUBSCRIPTIONS_ALARMS_CONDITIONS=ON
-DUA_NAMESPACE_ZERO=FULL
-DUA_BUILD_EXAMPLES=OFF
-DUA_BUILD_TOOLS=OFF
-DUA_FORCE_WERROR=OFF
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
-DCMAKE_C_FLAGS=-w
# Serial build avoids a -j race in open62541's Ninja-style codegen
# where a parallel writer can clobber namespace0_generated.c.o.d
# before the .d file is materialized. The build is one-shot, the
# 30-60s overhead is well worth predictability.
BUILD_COMMAND ${CMAKE_COMMAND} --build <BINARY_DIR>
BUILD_BYPRODUCTS "${_alarm_o62_install}/lib/libopen62541.a"
UPDATE_DISCONNECTED 1
)
set(_alarm_o62_lib "${_alarm_o62_install}/lib/libopen62541.a")
set(_alarm_o62_include "${_alarm_o62_install}/include")
file(MAKE_DIRECTORY "${_alarm_o62_include}")

add_executable(test_alarm_server
test/fixtures/test_alarm_server/test_alarm_server.cpp)
add_dependencies(test_alarm_server alarm_open62541_ep)
target_include_directories(test_alarm_server SYSTEM PRIVATE
"${_alarm_o62_include}")
target_link_libraries(test_alarm_server PRIVATE
"${_alarm_o62_lib}" Threads::Threads)
set_target_properties(test_alarm_server PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
target_compile_options(test_alarm_server PRIVATE -w)

install(TARGETS test_alarm_server
RUNTIME DESTINATION lib/${PROJECT_NAME})
install(PROGRAMS
test/fixtures/test_alarm_server/smoke_test.py
test/fixtures/test_alarm_server/run_ctest.py
DESTINATION lib/${PROJECT_NAME})

# CTest wrapper that boots test_alarm_server on an ephemeral port and runs
# the asyncua-based smoke test against it. Skips with CTest exit 77 when
# asyncua is not installed, so iterating on plugin code without the python
# dep does not fail the suite. CI installs asyncua and observes a real
# pass / fail.
find_package(Python3 REQUIRED COMPONENTS Interpreter)
add_test(NAME test_alarm_server_smoke
COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/test/fixtures/test_alarm_server/run_ctest.py"
"${CMAKE_BINARY_DIR}/test_alarm_server"
"${CMAKE_CURRENT_SOURCE_DIR}/test/fixtures/test_alarm_server/smoke_test.py")
set_tests_properties(test_alarm_server_smoke PROPERTIES
LABELS "integration"
SKIP_RETURN_CODE 77
TIMEOUT 60)
endif()

ros2_medkit_relax_vendor_warnings()
endif()

Expand Down
20 changes: 20 additions & 0 deletions src/ros2_medkit_plugins/ros2_medkit_opcua/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,28 @@ nodes:
message: Tank level below minimum
threshold: 100.0
above_threshold: false # Alarm when value < threshold

# Native OPC-UA AlarmConditionType events (issue #386). Subscribes to alarms
# defined inside the PLC (Siemens Program_Alarm / ProDiag, Beckhoff TF6100,
# CodeSys 3.5+, Rockwell via FactoryTalk Linx). Mutually exclusive per entry
# with the threshold-based alarm form above.
event_alarms:
- alarm_source: "ns=4;s=Alarms.Overpressure"
entity_id: tank_process
fault_code: PLC_OVERPRESSURE
```

The plugin auto-registers `acknowledge_fault` and `confirm_fault` operations
on every entity that has at least one `event_alarms` entry. Invoke them with:

```bash
curl -X POST http://localhost:8080/api/v1/apps/tank_process/operations/acknowledge_fault/executions \
-H 'Content-Type: application/json' \
-d '{"fault_code":"PLC_OVERPRESSURE","comment":"operator on radio"}'
```

See `design/index.rst` for the full state machine table and vendor matrix.

### Gateway Parameters

```yaml
Expand Down
Loading
Loading