Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions demos/openplc_tank/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
demos/openplc_tank/openplc/generated/
103 changes: 103 additions & 0 deletions demos/openplc_tank/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# OpenPLC Tank Demo

PLC diagnostics demo using OpenPLC v4 + ros2_medkit OPC-UA plugin.

## Architecture

```
OpenPLC v4 Runtime (Docker) ros2_medkit Gateway
[Tank ST program] [opcua_plugin.so]
OPC-UA Server :4840 <-- sub --> open62541pp client
|
OpcuaPoller
|
+---------+---------+
| | |
IntrospectionProvider REST FaultManager
(PLC entity tree) routes (alarm->fault)
```

## SOVD Entity Tree

```
Area: plc_systems
Component: openplc_runtime
App: tank_process (level, temperature, pressure + alarms)
App: fill_pump (speed control)
App: drain_valve (position control)
```

## Quick Start

```bash
./scripts/run-demo.sh

# Wait ~30s for startup, then:
curl http://localhost:8080/api/v1/health
curl http://localhost:8080/api/v1/apps | jq '.items[].id'
curl http://localhost:8080/api/v1/apps/tank_process/x-plc-data | jq .
```

## Endpoints

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/v1/areas` | List areas (plc_systems) |
| GET | `/api/v1/components` | List components (openplc_runtime) |
| GET | `/api/v1/apps` | List apps (tank_process, fill_pump, drain_valve) |
| GET | `/apps/{id}/x-plc-data` | All OPC-UA values for entity |
| GET | `/apps/{id}/x-plc-data/{node}` | Single value |
| POST | `/apps/{id}/x-plc-operations/{op}` | Write value `{"value": 75.0}` |
| GET | `/components/{id}/x-plc-status` | Connection state, stats |
| GET | `/apps/{id}/faults` | SOVD faults (from PLC alarms) |

## Fault Injection

```bash
# Inject high temperature (>80C threshold)
./scripts/inject-fault.sh high_temp

# Inject low level (<100mm threshold)
./scripts/inject-fault.sh low_level

# Inject overpressure (>5bar threshold)
./scripts/inject-fault.sh overpressure

# Restore normal operation
./scripts/restore-normal.sh
```

## Control PLC via REST

```bash
# Set pump speed to 75%
curl -X POST http://localhost:8080/api/v1/apps/fill_pump/x-plc-operations/set_pump_speed \
-H "Content-Type: application/json" -d '{"value": 75.0}'

# Set valve position to 50%
curl -X POST http://localhost:8080/api/v1/apps/drain_valve/x-plc-operations/set_valve_position \
-H "Content-Type: application/json" -d '{"value": 50.0}'
```

## Integration Tests

```bash
./scripts/run_integration_tests.sh
```

Tests cover: entity discovery, live data, control writes, fault lifecycle, connection resilience, error responses.

## Ports

| Service | Port | Protocol |
|---------|------|----------|
| OpenPLC OPC-UA | 4840 | OPC-UA TCP |
| Gateway REST | 8080 | HTTP |
| Web UI | 3000 | HTTP |

## Stopping

```bash
./scripts/stop-demo.sh # stop containers
./scripts/stop-demo.sh --volumes # stop + remove volumes
```
57 changes: 57 additions & 0 deletions demos/openplc_tank/config/gateway_params.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Gateway parameters for OpenPLC Tank Demo
# Namespace: diagnostics/ros2_medkit_gateway
diagnostics:
ros2_medkit_gateway:
ros__parameters:
server:
host: "0.0.0.0"
port: 8080

cors:
allowed_origins: ["*"]
allowed_methods: [GET, PUT, POST, DELETE, OPTIONS]
allowed_headers: [Content-Type, Accept]
allow_credentials: false

discovery:
mode: "hybrid"
manifest_path: "/config/tank_manifest.yaml"
manifest_strict_validation: true
runtime:
create_synthetic_components: false

max_parallel_topic_samples: 10

# Plugin configuration
plugins: ["opcua"]
plugins.opcua.path: "/plugin_ws/install/opcua_gateway_plugin/lib/opcua_gateway_plugin/opcua_gateway_plugin.so"
plugins.opcua.endpoint_url: "opc.tcp://openplc:4840"
plugins.opcua.node_map_path: "/config/tank_nodes.yaml"
plugins.opcua.prefer_subscriptions: true
plugins.opcua.subscription_interval_ms: 500.0
plugins.opcua.poll_interval_ms: 1000

# Triggers
triggers:
enabled: true
max_triggers: 100

# Documentation
docs:
enabled: true

# Fault Manager parameters
fault_manager:
ros__parameters:
storage_type: "sqlite"
database_path: "/var/lib/ros2_medkit/faults.db"

confirmation_threshold: 0
healing_enabled: false
auto_confirm_after_sec: 0.0

snapshots:
enabled: false

rosbag:
enabled: false
47 changes: 47 additions & 0 deletions demos/openplc_tank/config/tank_manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# SOVD Entity Manifest for OpenPLC Tank Demo
# Defines the static entity tree structure
# Dynamic data comes from the OPC-UA plugin at runtime

areas:
- id: plc_systems
name: PLC Systems
description: PLC systems connected via OPC-UA

components:
- id: openplc_runtime
name: OpenPLC Runtime
area: plc_systems
description: OpenPLC v4 runtime with OPC-UA server
tags:
- plc
- opcua
- simulation

apps:
- id: tank_process
name: Tank Process
component_id: openplc_runtime
external: true
description: Tank simulation - level, temperature, pressure sensors with alarms
tags:
- sensors
- alarms
- tank

- id: fill_pump
name: Fill Pump
component_id: openplc_runtime
external: true
description: Fill pump with speed control (0-100%)
tags:
- actuator
- pump

- id: drain_valve
name: Drain Valve
component_id: openplc_runtime
external: true
description: Drain valve with position control (0-100%)
tags:
- actuator
- valve
84 changes: 84 additions & 0 deletions demos/openplc_tank/config/tank_nodes.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# OPC-UA Node Map for OpenPLC Tank Demo
# Maps OPC-UA NodeIds to SOVD entity data points

area_id: plc_systems
area_name: PLC Systems
component_id: openplc_runtime
component_name: OpenPLC Runtime
auto_browse: false

nodes:
# === Tank Process - Sensor Readings ===
- node_id: "ns=1;s=TankLevel"
entity_id: tank_process
data_name: tank_level
display_name: Tank Level
unit: mm
data_type: float
writable: false
alarm:
fault_code: PLC_LOW_LEVEL
severity: WARNING
message: Tank level below minimum (100mm)
threshold: 100.0
above_threshold: false

- node_id: "ns=1;s=TankTemperature"
entity_id: tank_process
data_name: tank_temperature
display_name: Tank Temperature
unit: C
data_type: float
writable: false
alarm:
fault_code: PLC_HIGH_TEMP
severity: ERROR
message: Tank temperature above maximum (80C)
threshold: 80.0
above_threshold: true

- node_id: "ns=1;s=TankPressure"
entity_id: tank_process
data_name: tank_pressure
display_name: Tank Pressure
unit: bar
data_type: float
writable: false
alarm:
fault_code: PLC_OVERPRESSURE
severity: ERROR
message: Tank pressure above maximum (5 bar)
threshold: 5.0
above_threshold: true

# === Fill Pump - Speed Control ===
- node_id: "ns=1;s=PumpSpeed"
entity_id: fill_pump
data_name: pump_speed
display_name: Pump Speed Setpoint
unit: "%"
data_type: float
writable: true

- node_id: "ns=1;s=PumpRunning"
entity_id: fill_pump
data_name: pump_running
display_name: Pump Running Status
data_type: bool
writable: false

# === Drain Valve - Position Control ===
- node_id: "ns=1;s=ValvePosition"
entity_id: drain_valve
data_name: valve_position
display_name: Valve Position Setpoint
unit: "%"
data_type: float
writable: true

- node_id: "ns=1;s=ValveOpen"
entity_id: drain_valve
data_name: valve_open
display_name: Valve Open Status
data_type: bool
writable: false
75 changes: 75 additions & 0 deletions demos/openplc_tank/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
services:
# OpenPLC v4 runtime with tank simulation program
openplc:
build:
context: openplc/
dockerfile: Dockerfile
container_name: openplc_tank
ports:
- "4840:4840" # OPC-UA server
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/api/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s

# ros2_medkit Gateway with OPC-UA plugin
gateway:
build:
context: gateway/
dockerfile: Dockerfile
additional_contexts:
ros2_medkit: ../../../ros2_medkit
opcua_gateway_plugin: ../../../platform/commercial/opcua_gateway_plugin
config: config/
container_name: openplc_gateway
environment:
- ROS_DOMAIN_ID=50
ports:
- "8080:8080"
depends_on:
openplc:
condition: service_started
restart: unless-stopped

# SOVD Web UI
sovd-web-ui:
image: ghcr.io/selfpatch/sovd_web_ui:latest
container_name: sovd_web_ui
ports:
- "3000:80"
depends_on:
- gateway

# CI testing profile - headless validation
gateway-ci:
profiles: ["ci"]
build:
context: gateway/
dockerfile: Dockerfile
additional_contexts:
ros2_medkit: ../../../ros2_medkit
opcua_gateway_plugin: ../../../platform/commercial/opcua_gateway_plugin
config: config/
container_name: openplc_gateway_ci
environment:
- ROS_DOMAIN_ID=50
ports:
- "8080:8080"
depends_on:
openplc:
condition: service_started
command: >
bash -c "mkdir -p /var/lib/ros2_medkit/rosbags &&
source /opt/ros/jazzy/setup.bash &&
source /medkit_ws/install/setup.bash &&
source /plugin_ws/install/setup.bash &&
ros2 run ros2_medkit_gateway gateway_node \
--ros-args --params-file /config/gateway_params.yaml &
sleep 15 &&
curl -sf http://localhost:8080/api/v1/health &&
curl -sf http://localhost:8080/api/v1/areas | jq '.items[] | .id' &&
curl -sf http://localhost:8080/api/v1/apps | jq '.items[] | .id' &&
echo 'CI validation passed!'"
Loading
Loading