Skip to content

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

  1. RCAN is the authority — OPC UA is the transport surface. Security decisions are made in RCAN; OPC UA presents the result.
  2. 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.
  3. Namespace isolation — All RCAN-specific nodes live under the registered custom namespace https://rcan.dev/opcua/v1, keeping them cleanly separated from vendor namespaces.
  4. 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 .xml model file committed at castor/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 SignAndEncrypt mode in production. None mode 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 RolePermissionType is 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:

  1. Expose the RCAN robot object under namespace https://rcan.dev/opcua/v1
  2. Embed the full RURI in the robot object NodeId (ns=2;s=rcan:<ruri>)
  3. Enforce RCAN §12 RBAC before forwarding any method call
  4. Generate a §6-compliant audit entry for every COMMAND forwarded to OpenCastor
  5. Fire a ConditionType alarm for every PENDING_AUTH HiTL gate event
  6. Return audit_id in every successful method call response
  7. Register with OPC UA LDS when LDS is available on the network
  8. Support OPC UA SecurityMode SignAndEncrypt with policy Basic256Sha256

See also: docs/bridges/ros2-bridge.md | docs/engagement/iso-tc299-roadmap.md | docs/compliance/iso-10218-alignment.md