Skip to content

Commit 66b043f

Browse files
committed
feat(demos/manymove_industrial): add PLC OPC UA cross-source fault demo
Adds a PLC simulator (asyncua-based OPC UA server) and an OPC UA -> medkit fault bridge so PLC AlarmConditionType events land in the same medkit FaultManager that aggregates manymove BT-side faults. Both faults appear in one dashboard with distinct source_ids, demonstrating cross-source correlation as the actual differentiator over single-source logging. The PLC sim exposes three canonical alarms (photoeye_flicker / WARN, conveyor_overspeed / ERROR, estop_engaged / CRITICAL) plus an admin HTTP endpoint so container_scripts and demo orchestrators can raise/clear alarms without speaking OPC UA themselves. Designed to be swappable with a real OpenPLC v3 + ST program once the IEC 61131-3 build pipeline is set up; the OPC UA surface (AlarmConditionType events on namespace 2) stays identical. The bridge is a ROS 2 Python node (rclpy + asyncua) that subscribes to AlarmConditionType events and calls /fault_manager/report_fault for each, with SourceName -> MANYMOVE_PLC_* fault code mapping. Loopback prevention drops events whose SourceName starts with our own source_id. Manifest gains a conveyor-line area, four PLC-side components (openplc, photoeye-pick, photoeye-drop, conveyor-motor), an opcua-bridge component, plus matching apps and a fault-aggregation function tying the bridge to the existing FaultManager + gateway. Smoke test now exercises the conveyor-line container_scripts (inject-photoeye-flicker, restore-line) and asserts MANYMOVE_PLC_* faults round-trip through the bridge into medkit.
1 parent 5b860d7 commit 66b043f

15 files changed

Lines changed: 708 additions & 8 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,17 +170,20 @@ jobs:
170170
- name: Checkout repository
171171
uses: actions/checkout@v4
172172

173-
- name: Build and start manymove_industrial demo
173+
- name: Build and start manymove_industrial demo (sim + plc + bridge)
174174
working-directory: demos/manymove_industrial
175-
run: docker compose --profile ci up -d --build manymove-sim-ci
175+
run: docker compose --profile ci up -d --build
176176

177177
- name: Run smoke tests
178178
run: ./tests/smoke_test_manymove_industrial.sh
179179

180180
- name: Show container logs on failure
181181
if: failure()
182182
working-directory: demos/manymove_industrial
183-
run: docker compose --profile ci logs manymove-sim-ci --tail=200
183+
run: |
184+
docker compose --profile ci logs manymove-sim-ci --tail=200 || true
185+
docker compose --profile ci logs plc-sim --tail=100 || true
186+
docker compose --profile ci logs opcua-bridge --tail=100 || true
184187
185188
- name: Teardown
186189
if: always()

demos/manymove_industrial/config/manymove_industrial_manifest.yaml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ areas:
2121
name: "Diagnostics"
2222
description: "ros2_medkit fault management"
2323
namespace: /
24+
- id: conveyor-line
25+
name: "Conveyor line"
26+
description: "PLC-driven conveyor line: photoeyes, motor, e-stop"
27+
namespace: /
28+
- id: bridge
29+
name: "Bridges"
30+
description: "Cross-protocol bridges (OPC UA, etc.)"
31+
namespace: /
2432

2533
components:
2634
- id: xarm7-arm
@@ -68,6 +76,31 @@ components:
6876
type: "diagnostics"
6977
description: "SQLite-backed fault store"
7078
area: diagnostics
79+
- id: openplc
80+
name: "Line PLC (OPC UA)"
81+
type: "controller"
82+
description: "IEC 61131-3 soft PLC exposing photoeye / conveyor / e-stop tags + AlarmConditionType events"
83+
area: conveyor-line
84+
- id: photoeye-pick
85+
name: "Pick photoeye"
86+
type: "sensor"
87+
description: "Diffuse-mode photoelectric sensor at pick position"
88+
area: conveyor-line
89+
- id: photoeye-drop
90+
name: "Drop photoeye"
91+
type: "sensor"
92+
description: "Diffuse-mode photoelectric sensor at drop position"
93+
area: conveyor-line
94+
- id: conveyor-motor
95+
name: "Conveyor motor"
96+
type: "actuator"
97+
description: "Belt drive motor"
98+
area: conveyor-line
99+
- id: opcua-bridge
100+
name: "OPC UA -> medkit bridge"
101+
type: "bridge"
102+
description: "Subscribes to PLC AlarmConditionType events and forwards them as medkit faults"
103+
area: bridge
71104

72105
apps:
73106
- id: bt-client-xarm7
@@ -126,6 +159,20 @@ apps:
126159
ros_binding:
127160
namespace: /
128161
node_name: ros2_medkit_gateway
162+
- id: openplc-runtime
163+
name: "OpenPLC runtime"
164+
category: "controller"
165+
is_located_on: openplc
166+
ros_binding:
167+
namespace: /plc
168+
node_name: openplc_runtime
169+
- id: opcua-bridge-app
170+
name: "opcua_bridge"
171+
category: "bridge"
172+
is_located_on: opcua-bridge
173+
ros_binding:
174+
namespace: /plc
175+
node_name: sensor_io
129176

130177
functions:
131178
- id: pick-and-place
@@ -151,3 +198,17 @@ functions:
151198
hosted_by:
152199
- fault-manager-app
153200
- gateway-app
201+
- id: line-control
202+
name: "Line control"
203+
category: "controller"
204+
description: "PLC-driven conveyor sequencing with safety interlocks"
205+
hosted_by:
206+
- openplc-runtime
207+
- id: fault-aggregation
208+
name: "Cross-source fault aggregation"
209+
category: "diagnostics"
210+
description: "Bridge PLC AlarmConditionType events into the same medkit FaultManager that aggregates ROS-side faults"
211+
hosted_by:
212+
- opcua-bridge-app
213+
- fault-manager-app
214+
- gateway-app
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "Inject PLC e-stop (CRITICAL)",
3+
"description": "Raises the EstopEngaged AlarmConditionType on the PLC OPC UA server. The opcua_bridge forwards it as MANYMOVE_PLC_ESTOP_ENGAGED (CRITICAL) into the medkit FaultManager.",
4+
"format": "bash"
5+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/bash
2+
# Raise EstopEngaged AlarmConditionType (CRITICAL severity) via the PLC
3+
# admin endpoint. opcua_bridge forwards it to medkit as
4+
# MANYMOVE_PLC_ESTOP_ENGAGED.
5+
set -e
6+
7+
PLC_ADMIN="${PLC_ADMIN_URL:-http://plc-sim:8500}"
8+
9+
curl -fsS -X POST -H "Content-Type: application/json" \
10+
-d '{"message":"Line e-stop pressed at station 2"}' \
11+
"${PLC_ADMIN}/alarm/estop_engaged/raise" >/dev/null
12+
13+
echo "EstopEngaged alarm raised on ${PLC_ADMIN}"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "Inject PLC photoeye flicker (WARN)",
3+
"description": "Raises the PhotoeyeFlicker AlarmConditionType on the PLC OPC UA server. The opcua_bridge subscribes and forwards it as MANYMOVE_PLC_PHOTOEYE_FLICKER (WARN) into the medkit FaultManager.",
4+
"format": "bash"
5+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/bash
2+
# Raise PhotoeyeFlicker AlarmConditionType via the PLC admin endpoint.
3+
# The opcua_bridge service is subscribed and forwards it as
4+
# MANYMOVE_PLC_PHOTOEYE_FLICKER (WARN) to the medkit FaultManager.
5+
set -e
6+
7+
PLC_ADMIN="${PLC_ADMIN_URL:-http://plc-sim:8500}"
8+
9+
curl -fsS -X POST -H "Content-Type: application/json" \
10+
-d '{"message":"Pick photoeye toggled 12 times in 2s, expected <=3"}' \
11+
"${PLC_ADMIN}/alarm/photoeye_flicker/raise" >/dev/null
12+
13+
echo "PhotoeyeFlicker alarm raised on ${PLC_ADMIN}"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "Clear PLC alarms",
3+
"description": "Clears all three PLC AlarmConditionType alarms (photoeye_flicker, conveyor_overspeed, estop_engaged). The opcua_bridge forwards PASSED events that heal the corresponding medkit faults.",
4+
"format": "bash"
5+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/bash
2+
# Clear every PLC AlarmConditionType. opcua_bridge forwards a PASSED
3+
# event per alarm, healing the corresponding medkit fault.
4+
set -e
5+
6+
PLC_ADMIN="${PLC_ADMIN_URL:-http://plc-sim:8500}"
7+
8+
for alarm in photoeye_flicker conveyor_overspeed estop_engaged; do
9+
curl -fsS -X POST "${PLC_ADMIN}/alarm/${alarm}/clear" >/dev/null || true
10+
echo "cleared ${alarm}"
11+
done

demos/manymove_industrial/docker-compose.yml

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ services:
1919
ports:
2020
- "8080:8080" # medkit gateway HTTP
2121
- "8765:8765" # foxglove-bridge
22-
networks: [medkit-net]
22+
networks:
23+
medkit-net:
24+
aliases: [medkit-gateway]
2325
stdin_open: true
2426
tty: true
2527
command: >
@@ -37,6 +39,40 @@ services:
3739
- manymove-sim
3840
networks: [medkit-net]
3941

42+
# PLC simulator: OPC UA server emulating a line PLC (photoeye, conveyor,
43+
# e-stop). Publishes AlarmConditionType events the bridge subscribes to.
44+
# Swappable with a real OpenPLC v3 + ST program once the IEC 61131-3
45+
# build pipeline is in place; the OPC UA surface stays identical.
46+
plc-sim:
47+
profiles: ["cpu", "ci"]
48+
build:
49+
context: ./plc_sim
50+
dockerfile: Dockerfile
51+
container_name: manymove_industrial_plc_sim
52+
ports:
53+
- "4840:4840" # OPC UA endpoint
54+
- "8500:8500" # admin (raise/clear alarm via curl)
55+
networks: [medkit-net]
56+
57+
# OPC UA -> medkit fault bridge. Subscribes to PLC alarm conditions and
58+
# POSTs them to the FaultManager so PLC faults land in the same
59+
# dashboard / SOVD entity tree as the manymove BT-side faults.
60+
opcua-bridge:
61+
profiles: ["cpu", "ci"]
62+
build:
63+
context: ./opcua_bridge
64+
dockerfile: Dockerfile
65+
container_name: manymove_industrial_opcua_bridge
66+
environment:
67+
- OPCUA_ENDPOINT=opc.tcp://plc-sim:4840/manymove_plc/server
68+
- BRIDGE_SOURCE_ID=/plc/sensor_io
69+
- REPORT_FAULT_SERVICE=/fault_manager/report_fault
70+
- ROS_DOMAIN_ID=42
71+
- RMW_IMPLEMENTATION=rmw_fastrtps_cpp
72+
depends_on:
73+
- plc-sim
74+
networks: [medkit-net]
75+
4076
# CI profile: headless, no web UI, no X11. Suitable for the smoke test.
4177
manymove-sim-ci:
4278
profiles: ["ci"]
@@ -51,15 +87,14 @@ services:
5187
- "8080:8080"
5288
volumes:
5389
- manymove_faults_ci:/var/lib/ros2_medkit
54-
networks: [medkit-net]
90+
networks:
91+
medkit-net:
92+
aliases: [medkit-gateway]
5593
command: >
5694
bash -c "source /opt/ros/jazzy/setup.bash &&
5795
source /opt/manymove_ws/install/setup.bash &&
5896
ros2 launch manymove_industrial demo.launch.py headless:=true"
5997
60-
# OpenPLC + OPC UA bridge for the tier-2 PLC correlation narrative are
61-
# planned in a follow-up; see README "TODO: PLC tier".
62-
6398
networks:
6499
medkit-net:
65100
driver: bridge
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
FROM ros:jazzy-ros-base
2+
3+
ENV DEBIAN_FRONTEND=noninteractive \
4+
PYTHONUNBUFFERED=1 \
5+
OPCUA_ENDPOINT=opc.tcp://plc-sim:4840/manymove_plc/server \
6+
BRIDGE_SOURCE_ID=/plc/sensor_io \
7+
REPORT_FAULT_SERVICE=/fault_manager/report_fault
8+
9+
RUN apt-get update && apt-get install -y --no-install-recommends \
10+
python3-pip \
11+
ros-jazzy-ros2-medkit-msgs=0.4.0-1* \
12+
ros-jazzy-ros2-medkit-fault-reporter=0.4.0-1* \
13+
&& rm -rf /var/lib/apt/lists/* \
14+
&& pip install --no-cache-dir --break-system-packages \
15+
"asyncua==1.1.5"
16+
17+
WORKDIR /opt/opcua_bridge
18+
COPY bridge.py ./
19+
20+
CMD ["bash", "-c", "source /opt/ros/jazzy/setup.bash && exec python3 -u bridge.py"]

0 commit comments

Comments
 (0)