> ## 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

> [!NOTE]
>
> 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).

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.

## 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

**ENDPOINT\_URL** `string` *required*

OPC UA server endpoint URL (e.g., `opc.tcp://localhost:4840`).

**AUTH\_MODE** `integer`

Authentication mode:

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

Default: 0.

**USERNAME** `string`

Username for authentication (required if AUTH\_MODE is 1).

**PASSWORD** `string`

Password for authentication (required if AUTH\_MODE is 1).

**USE\_SECURITY** `boolean`

Enable security mode. Default: false.

**SECURITY\_POLICY** `integer`

Security policy level:

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

Default: 0.

**MESSAGE\_SECURITY** `integer`

Message security mode:

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

Default: 0.

**PKI\_DIR** `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).

**CLIENT\_CERT\_PATH** `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.

**CLIENT\_KEY\_PATH** `string`

Path to the private key PEM file when `CLIENT_CERT_PATH` points to a certificate-only PEM. Not needed for PKCS#12 bundles.

**CLIENT\_CERT\_PASSWORD** `string`

Password for the PKCS#12 bundle or encrypted private key. Use `GET SECRET`—never hard-code passwords in route definitions.

**AUTO\_ACCEPT\_UNTRUSTED\_CERTIFICATES** `boolean`

Automatically accept untrusted server certificates. Default: false. For production, leave false and trust the server certificate explicitly—see [Certificate Management](#certificate-management).

**SUPPRESS\_NONCE\_VALIDATION\_ERRORS** `boolean`

Suppress nonce validation errors. Default: false.

**TIMEOUT** `integer`

Connection timeout in milliseconds. Default: 90000.

***

## Security Configuration

**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"
```

**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
```

**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).

***

## 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.

### 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.**

### 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.

### Supply your own client certificate

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

**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
```

**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"
```

**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"
```

### Trust the server's certificate

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

**1. Deploy the route**

The first handshake fails and the server's certificate is written to `PKI_DIR/rejected/certs`.

**2. 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/
```

**3. Reconnect**

The route reconnects automatically—the handshake now succeeds.

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:

**1. 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
```

**2. 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.

**3. Reconnect**

The server now accepts the certificate identity.

### 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:

**USE\_BROWSE\_PATH** `boolean`

Enable BrowsePath mode - treat all TAG addresses as browse paths instead of NodeIds.

**BROWSE\_ROOT\_PATH** `string`

Root path for browsing when USE\_BROWSE\_PATH is enabled. Default: Objects.

#### 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; sized with `LENGTH` (default 256 bytes) |

### String sizing

For `STRING` tags, `WITH LENGTH` sets the maximum string size in **bytes** (default **256**). `STRING_SIZE` is accepted as an alias for the same field.

```lot wrap focus={11} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OpcUaStrings 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 ProductData
        WITH EVERY 1 SECOND
        ADD TAG ProductName
            WITH ADDRESS "ns=2;s=Product.Name"
            WITH DATA_TYPE "STRING"
            WITH LENGTH 128
            WITH SOURCE_TOPIC "opcua/product/name"
```

For cross-protocol defaults and caps, see [String sizing by Route](./overview#string-sizing-by-route) on the industrial overview.

***

## 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

> **Address Configuration**
>
> **ADDRESS** `string` *required*
>
> OPC UA NodeId (e.g., `ns=2;s=MyVariable` or `ns=2;i=1234`) or BrowsePath if USE\_BROWSE\_PATH is enabled.
>
> **DATA\_TYPE** `string` *required*
>
> OPC UA data type: `BOOLEAN`, `BYTE`, `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64`, `UINT64`, `FLOAT`, `DOUBLE`, `STRING`.
>
> **IS\_ARRAY** `boolean`
>
> Indicates if the value is an array. Default: false.
>
> **LENGTH** `integer` (default: 256)
>
> **STRING only.** Maximum string size in bytes. Alias: `STRING_SIZE`.

> **Value Transformation**
>
> **SCALING** `double`
>
> Multiplier for value transformation. Default: 1.0.
>
> **OFFSET** `double`
>
> Offset added after scaling. Default: 0.0.
>
> **DECIMAL\_PLACES** `integer`
>
> Decimal places in output. Default: 2.

> **Filtering**
>
> **MIN\_VALUE** `double`
>
> Minimum allowed value.
>
> **MAX\_VALUE** `double`
>
> Maximum allowed value.
>
> **DEADBAND** `double`
>
> Minimum change to trigger publish. Default: 0.0.

> **Publishing**
>
> **SOURCE\_TOPIC** `string`
>
> Topic where PLC values are published. Subscribe here to receive sensor data.
>
> **PUBLISH\_MODE** `string`
>
> Output format: `VALUE_ONLY` or `JSON`. Default: VALUE\_ONLY.
>
> **UNIT** `string`
>
> Engineering unit for documentation.
>
> **DESCRIPTION** `string`
>
> Human-readable description.

> **Write Configuration**
>
> **WRITABLE** `boolean`
>
> Allow writing to this node. Default: false.
>
> **DESTINATION\_TOPIC** `string`
>
> Topic to send write commands. Publish a value here to write it to the PLC.

***

## 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

**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"
```

**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"
```

**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"
```

**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"
```

**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"
```

**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`.

***

## Security Best Practices

> **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
> ```

> **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).

> **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"
> ```

***

## Troubleshooting

> **Connection Timeout**
>
> * Verify endpoint URL is correct
> * Check firewall allows TCP port (usually 4840)
> * Ensure server is running and accessible
> * Try increasing TIMEOUT value

> **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

> **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

> **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

> **Authentication Failed**
>
> * Verify username and password
> * Check user has appropriate permissions
> * Ensure AUTH\_MODE matches server requirements

> **Incorrect Values**
>
> * Verify DATA\_TYPE matches server variable type
> * Check IS\_ARRAY setting for array values
> * Verify SCALING and OFFSET calculations

***

## Next Steps

> **[OPC UA Server Route](./opcua-server)**
>
> Expose MQTT topics as OPC UA nodes for SCADA and OT clients.

> **[ADS (Beckhoff)](./ads)**
>
> Connect to TwinCAT systems.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.