RCAN ↔ OPC UA Bridge Specification¶
Status: Draft v1.0
Closes: Issue #2
Namespace URI: https://rcan.dev/opcua/v1
Reference Implementation: castor/bridges/opcua_bridge.py
1. Overview¶
OPC UA (IEC 62541) is the dominant communication standard in industrial automation. SCADA systems, Manufacturing Execution Systems (MES), and factory floor controllers speak OPC UA natively. RCAN-compliant robots that also expose an OPC UA interface can participate in existing factory infrastructure on day one — without requiring any changes to the OPC UA clients or SCADA software.
The RCAN ↔ OPC UA bridge is a bidirectional translation layer. It does not replace either protocol:
- OPC UA clients (SCADA, MES, HMI) can discover, monitor, and command RCAN robots as if they were native OPC UA devices.
- RCAN governs all AI accountability, RBAC enforcement, and audit chain requirements regardless of which side initiates an action.
An RCAN robot exposing an OPC UA interface inherits the full RCAN governance model (§5 identity, §6 audit, §8 safety invariants, §12 RBAC) while remaining visible to every OPC UA client in the facility.
1.1 Design Principles¶
- RCAN is the authority — OPC UA is the transport surface. Security decisions are made in RCAN; OPC UA presents the result.
- No audit gap — Every OPC UA method call that maps to a RCAN COMMAND generates a §6-compliant audit entry. No action reaches the robot without an audit record.
- Namespace isolation — All RCAN-specific nodes live under the registered custom namespace
https://rcan.dev/opcua/v1, keeping them cleanly separated from vendor namespaces. - Incremental adoption — Existing OPC UA infrastructure requires zero modification to discover and monitor RCAN robots.
2. Conceptual Mapping¶
| RCAN Concept | OPC UA Equivalent | Notes |
|---|---|---|
RURI (rcan://registry/mfr/model/id) |
OPC UA NodeId in custom namespace (ns=2;s=rcan:<ruri>) |
Full RURI embedded in the NodeId string. The registry hostname becomes the namespace URI. |
| RBAC roles (GUEST → CREATOR, levels 1–5) | UserTokenPolicy + RolePermissionType |
GUEST → Anonymous; USER → Operator; OWNER → Supervisor; CREATOR → Engineer |
| RCAN Registry | OPC UA Local Discovery Server (LDS) | mDNS _rcan._tcp maps to OPC UA mDNS discovery; robots register with both simultaneously |
| COMMAND message | OPC UA Method Call on robot node | Method input args: action_type + params; output: outcome + audit_id |
| STATUS telemetry | OPC UA MonitoredItem / Subscription |
Robot state variables exposed as OPC UA Variable nodes; sampling interval set in bridge config |
| §6 RCAN Audit record | AuditEventType (extended) |
Custom AuditRCANEventType adds: ai_provider, ai_model, confidence, thought_id fields |
Capability manifest (capabilities[]) |
OPC UA Address Space Variable nodes | Each capability exposed as a typed Variable under the robot's OPC UA object node |
| PENDING_AUTH (HiTL gate) | ConditionType / Alarm |
HiTL pending gate fires an OPC UA alarm; AUTHORIZE action maps to alarm Acknowledge |
| COMMAND outcome | OPC UA Method return StatusCode |
Good = executed; BadRequestNotAllowed = RBAC denied; BadWaitingForResponse = HiTL pending |
2.1 RBAC Role Mapping Detail¶
| RCAN Role | Level | OPC UA Role | OPC UA Permission Set |
|---|---|---|---|
| GUEST | 1 | Anonymous | Read-only: status, telemetry, capabilities |
| USER | 2 | Operator | Read + invoke non-safety commands |
| OPERATOR | 3 | Supervisor | Read + invoke all non-override commands |
| OWNER | 4 | Supervisor (elevated) | All commands + configuration reads |
| CREATOR | 5 | Engineer | Full access including firmware, profile, audit export |
RBAC translation occurs at session establishment. The OPC UA session identity (username/certificate CN) is resolved to an RCAN role via the bridge's identity map. The RCAN role is enforced before any command is forwarded to OpenCastor.
3. RCAN OPC UA Namespace Definition¶
The custom namespace URI is https://rcan.dev/opcua/v1. This namespace must be registered as index 2 in the bridge server's namespace array (index 0 = OPC UA base, index 1 = vendor).
<?xml version="1.0" encoding="UTF-8"?>
<!-- RCAN OPC UA NodeSet2 — namespace: https://rcan.dev/opcua/v1 -->
<!-- Full XML generated by UA Model Designer or opcua-modeler tooling -->
<UANodeSet xmlns="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:uax="http://opcfoundation.org/UA/2008/02/Types.xsd">
<NamespaceUris>
<Uri>https://rcan.dev/opcua/v1</Uri>
</NamespaceUris>
<!-- ═══════════════════════════════════════════════════════════════
AuditRCANEventType — extends OPC UA AuditEventType (NodeId i=2052)
Adds AI accountability fields required by RCAN §6
═══════════════════════════════════════════════════════════════ -->
<UAObjectType NodeId="ns=2;i=1001" BrowseName="2:AuditRCANEventType"
IsAbstract="false">
<DisplayName>AuditRCANEventType</DisplayName>
<Description>Extends AuditEventType with RCAN §6 AI accountability fields</Description>
<References>
<Reference ReferenceType="HasSubtype" IsForward="false">i=2052</Reference>
<!-- HasProperty: AiProvider -->
<Reference ReferenceType="HasProperty">ns=2;i=1002</Reference>
<!-- HasProperty: AiModel -->
<Reference ReferenceType="HasProperty">ns=2;i=1003</Reference>
<!-- HasProperty: Confidence -->
<Reference ReferenceType="HasProperty">ns=2;i=1004</Reference>
<!-- HasProperty: ThoughtId -->
<Reference ReferenceType="HasProperty">ns=2;i=1005</Reference>
<!-- HasProperty: RcanAuditId -->
<Reference ReferenceType="HasProperty">ns=2;i=1006</Reference>
<!-- HasProperty: RcanRole -->
<Reference ReferenceType="HasProperty">ns=2;i=1007</Reference>
</References>
</UAObjectType>
<UAVariable NodeId="ns=2;i=1002" BrowseName="2:AiProvider"
DataType="String" ValueRank="-1">
<DisplayName>AiProvider</DisplayName>
<Description>AI provider name (e.g., "openai", "anthropic", "local")</Description>
</UAVariable>
<UAVariable NodeId="ns=2;i=1003" BrowseName="2:AiModel"
DataType="String" ValueRank="-1">
<DisplayName>AiModel</DisplayName>
<Description>AI model identifier (e.g., "gpt-4o", "claude-3-5-sonnet")</Description>
</UAVariable>
<UAVariable NodeId="ns=2;i=1004" BrowseName="2:Confidence"
DataType="Double" ValueRank="-1">
<DisplayName>Confidence</DisplayName>
<Description>AI confidence score [0.0–1.0] from RCAN §6 audit record</Description>
</UAVariable>
<UAVariable NodeId="ns=2;i=1005" BrowseName="2:ThoughtId"
DataType="String" ValueRank="-1">
<DisplayName>ThoughtId</DisplayName>
<Description>Opaque thought/trace identifier from AI provider</Description>
</UAVariable>
<UAVariable NodeId="ns=2;i=1006" BrowseName="2:RcanAuditId"
DataType="String" ValueRank="-1">
<DisplayName>RcanAuditId</DisplayName>
<Description>SHA-256 audit chain entry ID from RCAN §6</Description>
</UAVariable>
<UAVariable NodeId="ns=2;i=1007" BrowseName="2:RcanRole"
DataType="ns=2;i=2001" ValueRank="-1">
<DisplayName>RcanRole</DisplayName>
<Description>RCAN RBAC role of the session that issued this command</Description>
</UAVariable>
<!-- ═══════════════════════════════════════════════════════════════
RCANRoleType — enumeration matching §12 RBAC levels
═══════════════════════════════════════════════════════════════ -->
<UADataType NodeId="ns=2;i=2001" BrowseName="2:RCANRoleType">
<DisplayName>RCANRoleType</DisplayName>
<Description>RCAN RBAC role enumeration (GUEST=1 through CREATOR=5)</Description>
<Definition Name="RCANRoleType">
<Field Name="GUEST" Value="1"/>
<Field Name="USER" Value="2"/>
<Field Name="OPERATOR" Value="3"/>
<Field Name="OWNER" Value="4"/>
<Field Name="CREATOR" Value="5"/>
</Definition>
</UADataType>
<!-- ═══════════════════════════════════════════════════════════════
RCANCommandMethod — OPC UA Method representing a RCAN COMMAND
Defined on every robot object node
═══════════════════════════════════════════════════════════════ -->
<UAMethod NodeId="ns=2;i=3001" BrowseName="2:RCANCommand"
Executable="true" UserExecutable="true">
<DisplayName>RCANCommand</DisplayName>
<Description>Issue a RCAN COMMAND to this robot. RBAC enforced by bridge.</Description>
<References>
<Reference ReferenceType="HasProperty">ns=2;i=3002</Reference>
<Reference ReferenceType="HasProperty">ns=2;i=3003</Reference>
</References>
</UAMethod>
<!-- Input arguments -->
<UAVariable NodeId="ns=2;i=3002" BrowseName="InputArguments" DataType="i=296"
ValueRank="1">
<DisplayName>InputArguments</DisplayName>
<Value>
<uax:ListOfExtensionObject>
<!-- action_type: String — RCAN action type (e.g., "move", "stop", "arm_joint") -->
<uax:ExtensionObject><uax:TypeId><uax:Identifier>i=297</uax:Identifier></uax:TypeId>
<uax:Body><uax:Argument><uax:Name>action_type</uax:Name>
<uax:DataType><uax:Identifier>i=12</uax:Identifier></uax:DataType>
<uax:ValueRank>-1</uax:ValueRank></uax:Argument></uax:Body>
</uax:ExtensionObject>
<!-- params: String (JSON-encoded RCAN command params) -->
<uax:ExtensionObject><uax:TypeId><uax:Identifier>i=297</uax:Identifier></uax:TypeId>
<uax:Body><uax:Argument><uax:Name>params_json</uax:Name>
<uax:DataType><uax:Identifier>i=12</uax:Identifier></uax:DataType>
<uax:ValueRank>-1</uax:ValueRank></uax:Argument></uax:Body>
</uax:ExtensionObject>
<!-- thought_id: String (optional, from AI provider) -->
<uax:ExtensionObject><uax:TypeId><uax:Identifier>i=297</uax:Identifier></uax:TypeId>
<uax:Body><uax:Argument><uax:Name>thought_id</uax:Name>
<uax:DataType><uax:Identifier>i=12</uax:Identifier></uax:DataType>
<uax:ValueRank>-1</uax:ValueRank></uax:Argument></uax:Body>
</uax:ExtensionObject>
</uax:ListOfExtensionObject>
</Value>
</UAVariable>
<!-- Output arguments -->
<UAVariable NodeId="ns=2;i=3003" BrowseName="OutputArguments" DataType="i=296"
ValueRank="1">
<DisplayName>OutputArguments</DisplayName>
<Value>
<uax:ListOfExtensionObject>
<!-- outcome: String — "executed" | "denied" | "pending_auth" | "error" -->
<uax:ExtensionObject><uax:TypeId><uax:Identifier>i=297</uax:Identifier></uax:TypeId>
<uax:Body><uax:Argument><uax:Name>outcome</uax:Name>
<uax:DataType><uax:Identifier>i=12</uax:Identifier></uax:DataType>
<uax:ValueRank>-1</uax:ValueRank></uax:Argument></uax:Body>
</uax:ExtensionObject>
<!-- audit_id: String — §6 audit chain entry SHA-256 -->
<uax:ExtensionObject><uax:TypeId><uax:Identifier>i=297</uax:Identifier></uax:TypeId>
<uax:Body><uax:Argument><uax:Name>audit_id</uax:Name>
<uax:DataType><uax:Identifier>i=12</uax:Identifier></uax:DataType>
<uax:ValueRank>-1</uax:ValueRank></uax:Argument></uax:Body>
</uax:ExtensionObject>
</uax:ListOfExtensionObject>
</Value>
</UAVariable>
</UANodeSet>
Note: The above XML shows the structural definition. Production-ready NodeSet2 XML is generated by OPC UA model designer tooling (e.g., UA-ModelCompiler) from the
.xmlmodel file committed atcastor/bridges/opcua/rcan-model.xml.
4. OPC UA Address Space Layout¶
For each RCAN robot, the bridge creates the following address space hierarchy:
Objects/
└── RCAN/
└── {mfr}/{model}/{id}/ ← robot object node (ns=2;s=rcan:<ruri>)
├── Identity/
│ ├── RURI ← String Variable
│ ├── Manufacturer ← String Variable
│ ├── Model ← String Variable
│ ├── FirmwareVersion ← String Variable
│ └── SpecVersion ← String Variable (e.g. "1.2")
├── Status/ ← MonitoredItem targets
│ ├── State ← Enum: IDLE/MOVING/FAULT/ESTOPPED
│ ├── BatteryPct ← Double [0–100]
│ ├── PositionX ← Double (m)
│ ├── PositionY ← Double (m)
│ ├── PositionZ ← Double (m)
│ ├── LinearVelocity ← Double (m/s)
│ └── AngularVelocity ← Double (rad/s)
├── Capabilities/
│ ├── move ← Boolean Variable (true = supported)
│ ├── arm_joint ← Boolean Variable
│ ├── navigate ← Boolean Variable
│ └── ... ← one node per capability
├── Methods/
│ └── RCANCommand ← Method (ns=2;i=3001 instance)
└── Alarms/
└── HiTLPendingCondition ← ConditionType alarm node
5. Discovery Flow¶
RCAN robots advertise via mDNS as _rcan._tcp.local. The bridge simultaneously registers the robot with an OPC UA Local Discovery Server (LDS):
1. Robot boots → OpenCastor registers mDNS record: _rcan._tcp.local
2. Bridge starts → registers OPC UA endpoint with LDS at opc.tcp://localhost:4840
3. OPC UA client browses LDS → discovers robot endpoint
4. Client connects → bridge establishes RCAN session, maps OPC UA identity to RCAN role
5. Client subscribes to Status/ nodes → bridge creates MonitoredItems backed by RCAN telemetry
6. Client calls RCANCommand method → bridge validates RBAC, forwards to OpenCastor, returns audit_id
For LDS-ME (multi-endpoint) deployments, the bridge registers the robot's full RURI as the application URI.
6. HiTL Gate ↔ OPC UA Alarm Flow¶
When a RCAN COMMAND triggers the §9 HiTL gate (confidence below threshold or action in supervised scope):
RCAN COMMAND received
→ Bridge evaluates §8 invariants + §9 HiTL gate
→ Gate OPEN: forward command, return outcome="executed", audit_id=<hash>
→ Gate CLOSED (PENDING_AUTH):
1. Bridge fires HiTLPendingCondition alarm (ConditionType, Severity=800)
2. Alarm payload includes: action_type, params_json, thought_id, rcan_role
3. OPC UA method returns immediately with outcome="pending_auth"
4. OPC UA operator Acknowledges alarm in HMI
→ Bridge receives OPC UA Acknowledge event
→ Bridge sends AUTHORIZE to OpenCastor
→ OpenCastor executes command, creates audit entry
→ HiTLPendingCondition transitions to Inactive
Alarm severity mapping:
- PENDING_AUTH on safety-critical commands → Severity 900 (high)
- PENDING_AUTH on standard commands → Severity 700 (medium)
- FAULT state → Severity 1000 (critical, OPC UA maximum)
7. Audit Bridge¶
Every OPC UA method call that maps to a RCAN COMMAND generates a §6-compliant audit entry via OpenCastor. The bridge emits both a standard OPC UA AuditEventType and a custom AuditRCANEventType:
# Pseudocode — see castor/bridges/opcua_bridge.py for full implementation
async def handle_rcan_command(session, action_type, params_json, thought_id):
rcan_role = session.rcan_role # resolved at session establishment
# 1. RCAN §12 RBAC check
if not rbac_check(rcan_role, action_type):
emit_audit_event(AuditRCANEventType, outcome="denied", ...)
return "denied", None
# 2. Forward to OpenCastor (enforces §8 invariants, §9 HiTL)
result = await castor.command(action_type, params_json, thought_id, role=rcan_role)
# 3. Emit OPC UA audit event with RCAN fields
emit_audit_event(AuditRCANEventType,
ClientAuditEntryId=result.audit_id,
ClientUserId=session.opc_identity,
MethodId=RCANCommandMethodId,
ai_provider=result.ai_provider,
ai_model=result.ai_model,
confidence=result.confidence,
thought_id=thought_id,
rcan_role=rcan_role,
rcan_audit_id=result.audit_id,
)
return result.outcome, result.audit_id
8. Bridge Implementation¶
8.1 Technology Stack¶
| Component | Technology |
|---|---|
| OPC UA server | asyncua (Python, MIT license) |
| RCAN client | OpenCastor Python SDK (castor) |
| Identity resolution | Configurable: LDAP, local map, or X.509 certificate CN |
| Transport security | OPC UA SecurityMode: SignAndEncrypt, policy: Basic256Sha256 |
8.2 Configuration¶
# opcua-bridge.yaml
rcan:
registry: rcan://registry.local
castor_socket: /run/castor/castor.sock # OpenCastor IPC
opcua:
endpoint: opc.tcp://0.0.0.0:4840/rcan
security_mode: SignAndEncrypt
security_policy: Basic256Sha256
certificate: /etc/rcan/opcua-cert.pem
private_key: /etc/rcan/opcua-key.pem
lds_url: opc.tcp://localhost:4840 # Local Discovery Server
identity_map:
# OPC UA username → RCAN role
anonymous: GUEST
operator1: USER
supervisor1: OWNER
engineer1: CREATOR
# Certificate CN patterns also supported:
# "CN=robot-console*": OWNER
8.3 Running the Bridge¶
# Install
pip install asyncua castor-sdk
# Run alongside OpenCastor
python -m castor.bridges.opcua_bridge --config opcua-bridge.yaml
# Or as a systemd service
systemctl enable --now rcan-opcua-bridge
8.4 Reference Implementation Target¶
castor/bridges/opcua_bridge.py in the OpenCastor repository.
9. Registering the RCAN OPC UA Namespace¶
Short-term: The namespace URI https://rcan.dev/opcua/v1 is self-hosted at rcan.dev. The NodeSet2 XML file is served at https://rcan.dev/opcua/v1/nodeset.xml.
Long-term path: Submit the namespace URI to the OPC Foundation Namespace Registry for official registration. This requires: 1. Published NodeSet2 XML (complete, toolchain-validated) 2. Companion specification document (this document serves as the basis) 3. OPC Foundation membership or working group sponsorship
The RCAN OPC UA namespace is intended to be submitted alongside an ISO/TC 299 liaison engagement (see docs/engagement/iso-tc299-roadmap.md).
10. Security Considerations¶
- Transport encryption: Bridge MUST use
SignAndEncryptmode in production.Nonemode permitted only on isolated test networks. - Certificate management: OPC UA server certificate and client certificates managed independently of RCAN mTLS certificates.
- RBAC is RCAN-authoritative: OPC UA
RolePermissionTypeis informational only; the bridge enforces RCAN RBAC before forwarding any command. Misconfiguring OPC UA roles cannot bypass RCAN access control. - Audit gap prevention: Bridge MUST NOT forward any command to OpenCastor without first obtaining an audit_id. Failed audit writes → command rejected.
- Session timeouts: OPC UA sessions inherit RCAN session TTL. Long-lived OPC UA subscriptions must re-authenticate when the RCAN session expires.
11. Conformance¶
A RCAN OPC UA bridge implementation MUST:
- Expose the RCAN robot object under namespace
https://rcan.dev/opcua/v1 - Embed the full RURI in the robot object NodeId (
ns=2;s=rcan:<ruri>) - Enforce RCAN §12 RBAC before forwarding any method call
- Generate a §6-compliant audit entry for every COMMAND forwarded to OpenCastor
- Fire a
ConditionTypealarm for every PENDING_AUTH HiTL gate event - Return
audit_idin every successful method call response - Register with OPC UA LDS when LDS is available on the network
- Support OPC UA SecurityMode
SignAndEncryptwith policyBasic256Sha256
See also: docs/bridges/ros2-bridge.md | docs/engagement/iso-tc299-roadmap.md | docs/compliance/iso-10218-alignment.md