> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# OPC UA Client Route

> Connect to remote OPC UA servers and poll device data into MQTT with security and BrowsePath support

## OPC UA Client

<Info>
  This page documents the **client** route (`OPCUA`)—the broker connects **to** a remote OPC UA server and reads or writes device data over MQTT. To expose MQTT topics **to** OPC UA clients instead (SCADA, UaExpert, PLCs connect to the broker), see [OPC UA Server](./opcua-server).
</Info>

The `OPCUA` route enables communication with OPC UA servers, providing a standardized, cross-platform protocol for industrial automation. It supports multiple authentication modes, security policies, and both NodeId and BrowsePath addressing.

<Tip>
  OPC UA is vendor-neutral and widely supported by modern industrial equipment. Use it when you need secure, standardized communication across different manufacturer's devices.
</Tip>

## Basic Syntax

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OPCServer WITH TYPE OPCUA
    ADD OPCUA_CONFIG
        WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
        WITH AUTH_MODE 0
        WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES "true"
    ADD MAPPING ProcessData
        WITH EVERY 1 SECOND
        ADD TAG Temperature
            WITH ADDRESS "ns=2;s=Channel1.Device1.Temperature"
            WITH DATA_TYPE "FLOAT"
            WITH SOURCE_TOPIC "opcua/temperature"
```

***

## Connection Configuration

### OPCUA\_CONFIG Parameters

<ParamField path="ENDPOINT_URL" type="string" required>
  OPC UA server endpoint URL (e.g., `opc.tcp://localhost:4840`).
</ParamField>

<ParamField path="AUTH_MODE" type="integer">
  Authentication mode:

  * `0` - Anonymous (no authentication)
  * `1` - Username/Password
  * `2` - Certificate

  Default: 0.
</ParamField>

<ParamField path="USERNAME" type="string">
  Username for authentication (required if AUTH\_MODE is 1).
</ParamField>

<ParamField path="PASSWORD" type="string">
  Password for authentication (required if AUTH\_MODE is 1).
</ParamField>

<ParamField path="USE_SECURITY" type="boolean">
  Enable security mode. Default: false.
</ParamField>

<ParamField path="SECURITY_POLICY" type="integer">
  Security policy level:

  * `0` - None
  * `1` - Basic128Rsa15
  * `2` - Basic256
  * `3` - Basic256Sha256
  * `4` - Aes128\_Sha256\_RsaOaep
  * `5` - Aes256\_Sha256\_RsaPss

  Default: 0.
</ParamField>

<ParamField path="MESSAGE_SECURITY" type="integer">
  Message security mode:

  * `0` - None
  * `1` - Sign
  * `2` - SignAndEncrypt

  Default: 0.
</ParamField>

<ParamField path="PKI_DIR" type="string">
  Root directory for the four-folder PKI store (`own`, `issuer`, `trusted`, `rejected`). Defaults to the OPC Foundation layout under local application data. Set an explicit path—and mount it as a volume in Docker—so certificates persist across restarts. See [Certificate Management](#certificate-management).
</ParamField>

<ParamField path="CLIENT_CERT_PATH" type="string">
  Path to a PKCS#12 (`.pfx`) or PEM client certificate used when `AUTH_MODE` is `2`. The broker loads it, adopts its application URI, and stores it under `PKI_DIR/own`. When omitted, a self-signed certificate is auto-generated on first connect.
</ParamField>

<ParamField path="CLIENT_KEY_PATH" type="string">
  Path to the private key PEM file when `CLIENT_CERT_PATH` points to a certificate-only PEM. Not needed for PKCS#12 bundles.
</ParamField>

<ParamField path="CLIENT_CERT_PASSWORD" type="string">
  Password for the PKCS#12 bundle or encrypted private key. Use `GET SECRET`—never hard-code passwords in route definitions.
</ParamField>

<ParamField path="AUTO_ACCEPT_UNTRUSTED_CERTIFICATES" type="boolean">
  Automatically accept untrusted server certificates. Default: false. For production, leave false and trust the server certificate explicitly—see [Certificate Management](#certificate-management).
</ParamField>

<ParamField path="SUPPRESS_NONCE_VALIDATION_ERRORS" type="boolean">
  Suppress nonce validation errors. Default: false.
</ParamField>

<ParamField path="TIMEOUT" type="integer">
  Connection timeout in milliseconds. Default: 90000.
</ParamField>

***

## Security Configuration

<Tabs>
  <Tab title="Anonymous (No Security)">
    For development or trusted networks:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    ADD OPCUA_CONFIG
        WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
        WITH AUTH_MODE 0
        WITH USE_SECURITY "false"
        WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES "true"
    ```
  </Tab>

  <Tab title="Username/Password">
    Basic authentication:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    ADD OPCUA_CONFIG
        WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
        WITH AUTH_MODE 1
        WITH USERNAME "opcuser"
        WITH PASSWORD "secure_password"
        WITH USE_SECURITY "true"
        WITH SECURITY_POLICY 3
        WITH MESSAGE_SECURITY 2
    ```
  </Tab>

  <Tab title="Certificate Authentication">
    Authenticate with an X.509 certificate identity. The application instance certificate secures the channel and identifies the client to the server:

    ```lot wrap focus={4-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    ADD OPCUA_CONFIG
        WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
        WITH AUTH_MODE 2
        WITH USE_SECURITY true
        WITH SECURITY_POLICY 3
        WITH MESSAGE_SECURITY 2
        WITH PKI_DIR "/data/opcua/pki"
        WITH CLIENT_CERT_PATH "/certs/client.pfx"
        WITH CLIENT_CERT_PASSWORD GET SECRET "OPCUA_CLIENT_CERT_PASSWORD"
    ```

    Omit `CLIENT_CERT_PATH` to auto-generate a self-signed certificate. You still need mutual trust between broker and server—see [Certificate Management](#certificate-management).
  </Tab>
</Tabs>

***

## Certificate Management

A secured OPC UA client connection requires **two certificates that trust each other**: the broker's **application instance certificate** (channel security and, with `AUTH_MODE 2`, user identity) and the **server's certificate** (must be in your `trusted` store). For server-side trust workflows, see [OPC UA Server](./opcua-server#certificate-management).

<Tip>
  **Like exchanging ID badges at a secure facility.** Both sides must recognize each other's badge before the door opens—moving a certificate from `rejected` to `trusted` is how you approve a new badge.
</Tip>

### PKI directory layout

Both client and server routes store certificates in four folders under `PKI_DIR`:

| Folder     | Holds                                                                        | File form         |
| ---------- | ---------------------------------------------------------------------------- | ----------------- |
| `own`      | This application's certificate (`own/certs`) and private key (`own/private`) | `*.der` + `*.pfx` |
| `issuer`   | Trusted CA / issuer certificates for chain validation                        | `*.der`           |
| `trusted`  | Trusted peer certificates—the server you accept goes here (`trusted/certs`)  | `*.der`           |
| `rejected` | Certificates seen during a handshake but not yet trusted                     | `*.der`           |

### Where certificates live

When `PKI_DIR` is not set, it defaults to `%LocalApplicationData%/OPC Foundation/pki`:

| OS      | Default path                                                               |
| ------- | -------------------------------------------------------------------------- |
| macOS   | `~/Library/Application Support/OPC Foundation/pki`                         |
| Linux   | `$XDG_DATA_HOME/OPC Foundation/pki` or `~/.local/share/OPC Foundation/pki` |
| Windows | `%LOCALAPPDATA%\OPC Foundation\pki`                                        |

Relocate the store to a path you control:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD OPCUA_CONFIG
    WITH ENDPOINT_URL "opc.tcp://server:4840"
    WITH PKI_DIR "/data/opcua/pki"
```

<Warning>
  In Docker, the default PKI location is ephemeral—the broker regenerates its certificate on every restart and any server that trusted the previous one will reject the new one. **Always set `PKI_DIR` to a mounted volume.**
</Warning>

### Inspecting and exporting certificates

Certificates are plain files you can inspect with `openssl`:

```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# The broker's own certificate and private key
ls "$PKI_DIR"/own/certs/*.der
ls "$PKI_DIR"/own/private/*.pfx

# Inspect subject, validity, and application URI SAN
openssl x509 -inform der -in "$PKI_DIR"/own/certs/<name>.der -noout -subject -dates -ext subjectAltName

# Export public cert for the server's trust list
openssl x509 -inform der -in "$PKI_DIR"/own/certs/<name>.der -out broker-client.pem
```

Give peers the **public** certificate (`own/certs/*.der`). The private key (`own/private/*.pfx`) never leaves the broker.

### Auto-generated client certificate

With `AUTH_MODE 2` and no `CLIENT_CERT_PATH`, the broker auto-generates a self-signed certificate (`CN=CorefluxBroker_OpcUaClient, O=Coreflux`) in `PKI_DIR/own` on first connect:

```lot wrap  theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantClient WITH TYPE OPCUA
    ADD OPCUA_CONFIG
        WITH ENDPOINT_URL "opc.tcp://server:4840"
        WITH AUTH_MODE 2
        WITH USE_SECURITY true
        WITH SECURITY_POLICY 3
        WITH MESSAGE_SECURITY 2
        WITH PKI_DIR "/data/opcua/pki"
```

You still must (1) get this broker certificate trusted by the server and (2) trust the server's certificate—see the workflows below.

<Note>
  An application instance certificate **must** carry an application URI in its Subject Alternative Name. The OPC UA stack matches certificates by that URI and rejects certificates without one.
</Note>

### Supply your own client certificate

Use a certificate from your PKI via `CLIENT_CERT_PATH`. Generate it with a mandatory URI SAN:

<Tabs>
  <Tab title="Generate certificate">
    ```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    # Self-signed example—use your CA in production. The URI SAN is mandatory.
    openssl req -x509 -newkey rsa:2048 -keyout client-key.pem -out client-cert.pem -days 825 -nodes \
      -subj "/CN=PlantClient/O=ACME" \
      -addext "subjectAltName=URI:urn:acme:opcua:plant-client" \
      -addext "keyUsage=digitalSignature,keyEncipherment,dataEncipherment,nonRepudiation" \
      -addext "extendedKeyUsage=clientAuth,serverAuth"

    # Bundle into PKCS#12 (.pfx)
    openssl pkcs12 -export -inkey client-key.pem -in client-cert.pem \
      -out client.pfx -passout pass:CHANGEME -name plantclient
    ```
  </Tab>

  <Tab title="Reference in route (PKCS#12)">
    ```lot wrap focus={6-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE PlantClient WITH TYPE OPCUA
        ADD OPCUA_CONFIG
            WITH ENDPOINT_URL "opc.tcp://server:4840"
            WITH AUTH_MODE 2
            WITH USE_SECURITY true
            WITH SECURITY_POLICY 3
            WITH MESSAGE_SECURITY 2
            WITH CLIENT_CERT_PATH "/certs/client.pfx"
            WITH CLIENT_CERT_PASSWORD GET SECRET "OPCUA_CLIENT_CERT_PASSWORD"
            WITH PKI_DIR "/data/opcua/pki"
    ```
  </Tab>

  <Tab title="Reference in route (PEM)">
    Point `CLIENT_CERT_PATH` at the certificate PEM. If the key is in a separate file, add `CLIENT_KEY_PATH`:

    ```lot wrap focus={6-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE PlantClient WITH TYPE OPCUA
        ADD OPCUA_CONFIG
            WITH ENDPOINT_URL "opc.tcp://server:4840"
            WITH AUTH_MODE 2
            WITH USE_SECURITY true
            WITH CLIENT_CERT_PATH "/certs/client-cert.pem"
            WITH CLIENT_KEY_PATH "/certs/client-key.pem"
            WITH CLIENT_CERT_PASSWORD GET SECRET "OPCUA_CLIENT_KEY_PASSWORD"
            WITH PKI_DIR "/data/opcua/pki"
    ```
  </Tab>
</Tabs>

### Trust the server's certificate

Required when `USE_SECURITY true` and `AUTO_ACCEPT_UNTRUSTED_CERTIFICATES false`:

<Steps>
  <Step title="Deploy the route">
    The first handshake fails and the server's certificate is written to `PKI_DIR/rejected/certs`.
  </Step>

  <Step title="Move to trusted">
    ```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    mv "$PKI_DIR"/rejected/certs/<server-cert>.der "$PKI_DIR"/trusted/certs/
    ```
  </Step>

  <Step title="Reconnect">
    The route reconnects automatically—the handshake now succeeds.
  </Step>
</Steps>

For testing only, skip this with `WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES true`.

### Get the broker trusted by the server

For `AUTH_MODE 2`, the server must trust the broker's client certificate:

<Steps>
  <Step title="Export the broker certificate">
    ```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    openssl x509 -inform der -in "$PKI_DIR"/own/certs/<name>.der -out broker-client.pem
    ```
  </Step>

  <Step title="Add to the server's trust list">
    Import `broker-client.pem` (or the `.der`) into the server's trusted certificate store—via its PKI `trusted` folder or vendor certificate manager.
  </Step>

  <Step title="Reconnect">
    The server now accepts the certificate identity.
  </Step>
</Steps>

### Rotation and renewal

* **Auto-generated certificates** — Delete files in `PKI_DIR/own/certs` and `PKI_DIR/own/private`, then reconnect; the broker regenerates a new self-signed certificate.
* **Supplied certificates** — Replace the `.pfx`/PEM file, remove the old copy from `PKI_DIR/own`, and redeploy. Re-distribute the new public certificate to the server's trust list before the old one expires.
* Plan rotation **before expiry**—an expired application instance certificate fails the handshake.

### Verify the connection

After mutual trust is configured, confirm the route connects:

```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-checkRouteConnection PlantClient
```

Publish that command to `$SYS/Coreflux/Command`. A response with `"success": true` means the certificate-authenticated session is established.

***

## NodeId Addressing

OPC UA uses NodeIds to identify variables. The format is: `ns=<namespace>;s=<identifier>` or `ns=<namespace>;i=<numeric_id>`

| Format             | Example                                       | Description                          |
| ------------------ | --------------------------------------------- | ------------------------------------ |
| String identifier  | `ns=2;s=Channel1.Device1.Tag1`                | Most common, human-readable          |
| Numeric identifier | `ns=2;i=1234`                                 | More efficient, used by some servers |
| GUID identifier    | `ns=2;g=12345678-1234-1234-1234-123456789012` | Globally unique identifiers          |

### Finding NodeIds

Use an OPC UA browser tool (like UaExpert, Prosys OPC UA Browser, or similar) to explore the server's address space and find the correct NodeIds.

### BrowsePath Mode

For servers that support it, you can use symbolic paths instead of NodeIds:

<ParamField path="USE_BROWSE_PATH" type="boolean">
  Enable BrowsePath mode - treat all TAG addresses as browse paths instead of NodeIds.
</ParamField>

<ParamField path="BROWSE_ROOT_PATH" type="string">
  Root path for browsing when USE\_BROWSE\_PATH is enabled. Default: Objects.
</ParamField>

#### BrowsePath Example

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD OPCUA_CONFIG
    WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
    WITH USE_BROWSE_PATH "true"
    WITH BROWSE_ROOT_PATH "Objects"
ADD MAPPING Tags
    WITH EVERY 1 SECOND
    ADD TAG Temperature
        WITH ADDRESS "Aliases/MyDevice/Temperature"
        WITH DATA_TYPE "FLOAT"
        WITH SOURCE_TOPIC "opcua/temperature"
```

***

## Data Types

| Data Type                      | OPC UA Type | Description                                 |
| ------------------------------ | ----------- | ------------------------------------------- |
| `BOOLEAN` / `BOOL`             | Boolean     | True/False value                            |
| `SBYTE` / `SINT`               | SByte       | Signed 8-bit integer                        |
| `BYTE` / `USINT`               | Byte        | Unsigned 8-bit integer                      |
| `INT16` / `INT` / `WORD`       | Int16       | 16-bit integer                              |
| `UINT16` / `UINT`              | UInt16      | Unsigned 16-bit integer                     |
| `INT32` / `DINT`               | Int32       | Signed 32-bit integer                       |
| `UINT32` / `UDINT` / `DWORD`   | UInt32      | Unsigned 32-bit integer                     |
| `INT64` / `LINT`               | Int64       | Signed 64-bit integer                       |
| `UINT64` / `ULINT` / `LWORD`   | UInt64      | Unsigned 64-bit integer                     |
| `FLOAT` / `FLOAT32` / `REAL`   | Float       | 32-bit floating point                       |
| `DOUBLE` / `FLOAT64` / `LREAL` | Double      | 64-bit floating point                       |
| `CHAR`                         | Char        | Single character                            |
| `WCHAR`                        | WChar       | Wide character                              |
| `TIME`                         | Time        | Time duration                               |
| `DATE`                         | Date        | Date value                                  |
| `TOD`                          | TimeOfDay   | Time of day                                 |
| `STRING`                       | String      | Unicode string (configurable `STRING_SIZE`) |

***

## TAG Configuration

### Complete TAG Example

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG ProcessTemperature
    WITH ADDRESS "ns=2;s=Process.Temperature"
    WITH DATA_TYPE "FLOAT"
    WITH SOURCE_TOPIC "opcua/process/temperature"
    WITH SCALING 1
    WITH OFFSET 0
    WITH UNIT "°C"
    WITH DECIMAL_PLACES 2
    WITH MIN_VALUE -50
    WITH MAX_VALUE 500
    WITH DEADBAND 0.5
    WITH PUBLISH_MODE "JSON"
    WITH WRITABLE "true"
    WITH DESTINATION_TOPIC "opcua/process/temperature/set"
    WITH IS_ARRAY "false"
    WITH DESCRIPTION "Main process temperature sensor"
```

### TAG Parameters

<AccordionGroup>
  <Accordion title="Address Configuration">
    <ParamField path="ADDRESS" type="string" required>
      OPC UA NodeId (e.g., `ns=2;s=MyVariable` or `ns=2;i=1234`) or BrowsePath if USE\_BROWSE\_PATH is enabled.
    </ParamField>

    <ParamField path="DATA_TYPE" type="string" required>
      OPC UA data type: `BOOLEAN`, `BYTE`, `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64`, `UINT64`, `FLOAT`, `DOUBLE`, `STRING`.
    </ParamField>

    <ParamField path="IS_ARRAY" type="boolean">
      Indicates if the value is an array. Default: false.
    </ParamField>
  </Accordion>

  <Accordion title="Value Transformation">
    <ParamField path="SCALING" type="double">
      Multiplier for value transformation. Default: 1.0.
    </ParamField>

    <ParamField path="OFFSET" type="double">
      Offset added after scaling. Default: 0.0.
    </ParamField>

    <ParamField path="DECIMAL_PLACES" type="integer">
      Decimal places in output. Default: 2.
    </ParamField>
  </Accordion>

  <Accordion title="Filtering">
    <ParamField path="MIN_VALUE" type="double">
      Minimum allowed value.
    </ParamField>

    <ParamField path="MAX_VALUE" type="double">
      Maximum allowed value.
    </ParamField>

    <ParamField path="DEADBAND" type="double">
      Minimum change to trigger publish. Default: 0.0.
    </ParamField>
  </Accordion>

  <Accordion title="Publishing">
    <ParamField path="SOURCE_TOPIC" type="string">
      Topic where PLC values are published. Subscribe here to receive sensor data.
    </ParamField>

    <ParamField path="PUBLISH_MODE" type="string">
      Output format: `VALUE_ONLY` or `JSON`. Default: VALUE\_ONLY.
    </ParamField>

    <ParamField path="UNIT" type="string">
      Engineering unit for documentation.
    </ParamField>

    <ParamField path="DESCRIPTION" type="string">
      Human-readable description.
    </ParamField>
  </Accordion>

  <Accordion title="Write Configuration">
    <ParamField path="WRITABLE" type="boolean">
      Allow writing to this node. Default: false.
    </ParamField>

    <ParamField path="DESTINATION_TOPIC" type="string">
      Topic to send write commands. Publish a value here to write it to the PLC.
    </ParamField>
  </Accordion>
</AccordionGroup>

***

## Event-Based Operations

For on-demand OPC UA operations (not polling), use the EVENT syntax. Publish a message to SOURCE\_TOPIC to trigger the operation; the route executes it and publishes the result to DESTINATION\_TOPIC.

### Supported Operations

| Operation | Description                 | Query Parameters                |
| --------- | --------------------------- | ------------------------------- |
| `READ`    | Read a node value on demand | `node_id`, `data_type`          |
| `WRITE`   | Write a value to a node     | `node_id`, `data_type`, `value` |

### Query Parameters

| Parameter   | Description                       | Example                              |
| ----------- | --------------------------------- | ------------------------------------ |
| `operation` | Operation type: `READ` or `WRITE` | `READ`                               |
| `node_id`   | OPC UA NodeId (string or numeric) | `ns=2;i=2`, `ns=2;s=Process.Temp`    |
| `data_type` | OPC UA data type                  | `INT32`, `FLOAT`, `DOUBLE`, `STRING` |
| `value`     | Value to write (WRITE only)       | `42`, `23.5`, `"hello"`              |

### Read Example

Read a node value on demand:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT ReadOnDemand
    WITH SOURCE_TOPIC "opcua/commands/read"
    WITH DESTINATION_TOPIC "opcua/responses/read"
    WITH QUERY "{operation: READ, node_id: ns=2;i=2, data_type: INT32}"
```

### Write Example

Write a FLOAT value to a node on demand:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT WriteSetpoint
    WITH SOURCE_TOPIC "opcua/commands/write"
    WITH DESTINATION_TOPIC "opcua/responses/write"
    WITH QUERY "{operation: WRITE, node_id: ns=2;s=Process.Setpoint, data_type: FLOAT, value: 75.0}"
```

***

## Complete Examples

<Tabs>
  <Tab title="Basic Connection">
    Simple OPC UA connection with anonymous access:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE OPCServer WITH TYPE OPCUA
        ADD OPCUA_CONFIG
            WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
            WITH AUTH_MODE 0
            WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES "true"
        ADD MAPPING ProcessData
            WITH EVERY 1 SECOND
            ADD TAG Temperature
                WITH ADDRESS "ns=2;s=Channel1.Device1.Temperature"
                WITH DATA_TYPE "FLOAT"
                WITH SOURCE_TOPIC "opcua/temperature"
                WITH UNIT "°C"
            ADD TAG Pressure
                WITH ADDRESS "ns=2;s=Channel1.Device1.Pressure"
                WITH DATA_TYPE "FLOAT"
                WITH SOURCE_TOPIC "opcua/pressure"
                WITH UNIT "bar"
            ADD TAG Status
                WITH ADDRESS "ns=2;s=Channel1.Device1.Status"
                WITH DATA_TYPE "INT32"
                WITH SOURCE_TOPIC "opcua/status"
    ```
  </Tab>

  <Tab title="Secure Connection">
    Production setup with encryption:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE SecureOPC WITH TYPE OPCUA
        ADD OPCUA_CONFIG
            WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
            WITH AUTH_MODE 1
            WITH USERNAME "operator"
            WITH PASSWORD "secure_password_here"
            WITH USE_SECURITY "true"
            WITH SECURITY_POLICY 3
            WITH MESSAGE_SECURITY 2
            WITH TIMEOUT 30000
        ADD MAPPING SecureData
            WITH EVERY 500 MILLISECONDS
            ADD TAG CriticalValue
                WITH ADDRESS "ns=2;s=Safety.CriticalValue"
                WITH DATA_TYPE "FLOAT"
                WITH SOURCE_TOPIC "secure/critical"
                WITH PUBLISH_MODE "JSON"
    ```
  </Tab>

  <Tab title="BrowsePath Mode">
    Using symbolic paths:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE BrowsePathOPC WITH TYPE OPCUA
        ADD OPCUA_CONFIG
            WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
            WITH AUTH_MODE 0
            WITH USE_BROWSE_PATH "true"
            WITH BROWSE_ROOT_PATH "Objects"
            WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES "true"
        ADD MAPPING ProcessTags
            WITH EVERY 1 SECOND
            ADD TAG Temperature
                WITH ADDRESS "DataAccess/AnalogItems/Temperature"
                WITH DATA_TYPE "DOUBLE"
                WITH SOURCE_TOPIC "opcua/temperature"
            ADD TAG Setpoint
                WITH ADDRESS "DataAccess/AnalogItems/Setpoint"
                WITH DATA_TYPE "DOUBLE"
                WITH SOURCE_TOPIC "opcua/setpoint"
                WITH WRITABLE "true"
                WITH DESTINATION_TOPIC "opcua/setpoint/write"
    ```
  </Tab>

  <Tab title="Array Values">
    Reading array data:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE ArrayOPC WITH TYPE OPCUA
        ADD OPCUA_CONFIG
            WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
            WITH AUTH_MODE 0
            WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES "true"
        ADD MAPPING ArrayData
            WITH EVERY 1 SECOND
            ADD TAG TemperatureArray
                WITH ADDRESS "ns=2;s=Process.TemperatureArray"
                WITH DATA_TYPE "FLOAT"
                WITH IS_ARRAY "true"
                WITH SOURCE_TOPIC "opcua/temperatures"
                WITH PUBLISH_MODE "JSON"
            ADD TAG StatusBits
                WITH ADDRESS "ns=2;s=Process.StatusBits"
                WITH DATA_TYPE "BOOLEAN"
                WITH IS_ARRAY "true"
                WITH SOURCE_TOPIC "opcua/status/bits"
    ```
  </Tab>

  <Tab title="Kepware Connection">
    Connecting to Kepware OPC UA server:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE KepwareServer WITH TYPE OPCUA
        ADD OPCUA_CONFIG
            WITH ENDPOINT_URL "opc.tcp://192.168.1.50:49320"
            WITH AUTH_MODE 0
            WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES "true"
        ADD MAPPING KepwareTags
            WITH EVERY 500 MILLISECONDS
            ADD TAG Motor1Speed
                WITH ADDRESS "ns=2;s=Channel1.Device1.Motor1_Speed"
                WITH DATA_TYPE "FLOAT"
                WITH SOURCE_TOPIC "kepware/motor1/speed"
                WITH UNIT "RPM"
            ADD TAG Motor1Current
                WITH ADDRESS "ns=2;s=Channel1.Device1.Motor1_Current"
                WITH DATA_TYPE "FLOAT"
                WITH SOURCE_TOPIC "kepware/motor1/current"
                WITH UNIT "A"
            ADD TAG Motor1Running
                WITH ADDRESS "ns=2;s=Channel1.Device1.Motor1_Running"
                WITH DATA_TYPE "BOOLEAN"
                WITH SOURCE_TOPIC "kepware/motor1/running"
    ```
  </Tab>

  <Tab title="Combined (Cyclic + On-Demand)">
    Continuous monitoring with on-demand read and write events in the same route:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE FullOPCSetup WITH TYPE OPCUA
        ADD OPCUA_CONFIG
            WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
            WITH AUTH_MODE 0
            WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES "true"
        
        ADD MAPPING ProcessData
            WITH EVERY 1 SECOND
            ADD TAG Temperature
                WITH ADDRESS "ns=2;s=Channel1.Device1.Temperature"
                WITH DATA_TYPE "FLOAT"
                WITH SOURCE_TOPIC "opcua/temperature"
                WITH UNIT "°C"
            ADD TAG Pressure
                WITH ADDRESS "ns=2;s=Channel1.Device1.Pressure"
                WITH DATA_TYPE "FLOAT"
                WITH SOURCE_TOPIC "opcua/pressure"
                WITH UNIT "bar"
        
        ADD EVENT ReadNode
            WITH SOURCE_TOPIC "opcua/commands/read"
            WITH DESTINATION_TOPIC "opcua/responses/read"
            WITH QUERY "{operation: READ, node_id: ns=2;s=Channel1.Device1.Status, data_type: INT32}"
        
        ADD EVENT WriteSetpoint
            WITH SOURCE_TOPIC "opcua/commands/write"
            WITH DESTINATION_TOPIC "opcua/responses/write"
            WITH QUERY "{operation: WRITE, node_id: ns=2;s=Channel1.Device1.Setpoint, data_type: FLOAT, value: 0}"
    ```

    The MAPPING continuously reads temperature and pressure. To trigger an on-demand status read, publish any message to `opcua/commands/read`. To write a setpoint, publish to `opcua/commands/write`.
  </Tab>
</Tabs>

***

## Security Best Practices

<AccordionGroup>
  <Accordion title="Use Encryption in Production">
    Always enable security for production deployments:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    WITH USE_SECURITY "true"
    WITH SECURITY_POLICY 3  // Basic256Sha256
    WITH MESSAGE_SECURITY 2  // SignAndEncrypt
    ```
  </Accordion>

  <Accordion title="Don't Auto-Accept Certificates">
    In production, properly configure certificate trust:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES false
    ```

    Move server certificates from `PKI_DIR/rejected/certs` to `PKI_DIR/trusted/certs` during commissioning. See [Certificate Management](#certificate-management).
  </Accordion>

  <Accordion title="Use Strong Authentication">
    Prefer username/password or certificate authentication over anonymous:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    WITH AUTH_MODE 1  // or '2' for certificate
    WITH USERNAME "operator"
    WITH PASSWORD "strong_password_here"
    ```
  </Accordion>
</AccordionGroup>

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="Connection Timeout">
    * Verify endpoint URL is correct
    * Check firewall allows TCP port (usually 4840)
    * Ensure server is running and accessible
    * Try increasing TIMEOUT value
  </Accordion>

  <Accordion title="Security Rejection">
    * Verify security policy matches server configuration
    * **Server certificate not trusted** — Move it from `PKI_DIR/rejected/certs` to `PKI_DIR/trusted/certs`, or set `AUTO_ACCEPT_UNTRUSTED_CERTIFICATES true` for testing only
    * **Supplied certificate ignored** — Reissue with a `subjectAltName=URI:...` SAN; certificates without an application URI are rejected
    * **Server rejects client certificate** — Export the broker's public cert from `PKI_DIR/own/certs` and add it to the server's trust list
    * **Certificate regenerates every restart** — Set `PKI_DIR` to a persistent volume (especially in Docker)
    * **`BadCertificateUriInvalid`** — Regenerate the certificate or set a stable application URI in the SAN
  </Accordion>

  <Accordion title="Route with CLIENT_CERT_PATH fails to start">
    * Verify the file path is correct and readable by the broker
    * Check `CLIENT_CERT_PASSWORD` matches the PKCS#12 bundle or encrypted key
    * For a split PEM (certificate and key in separate files), set `CLIENT_KEY_PATH`
    * The broker log names the failing file—check it for the exact cause
  </Accordion>

  <Accordion title="NodeId Not Found">
    * Use OPC UA browser to verify correct NodeId
    * Check namespace index (ns=) is correct
    * Verify node exists in server's address space
    * Try BrowsePath mode if NodeId is unclear
  </Accordion>

  <Accordion title="Authentication Failed">
    * Verify username and password
    * Check user has appropriate permissions
    * Ensure AUTH\_MODE matches server requirements
  </Accordion>

  <Accordion title="Incorrect Values">
    * Verify DATA\_TYPE matches server variable type
    * Check IS\_ARRAY setting for array values
    * Verify SCALING and OFFSET calculations
  </Accordion>
</AccordionGroup>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="OPC UA Server Route" icon="server" href="./opcua-server">
    Expose MQTT topics as OPC UA nodes for SCADA and OT clients.
  </Card>

  <Card title="ADS (Beckhoff)" icon="gear" href="./ads">
    Connect to TwinCAT systems.
  </Card>
</CardGroup>
