# OPC UA

> An OPC UA client for third-party servers, with certificate-gated trust, node-id addressing and address-space discovery.

The OPC UA driver is a **client** for third-party OPC UA servers (PLCs, gateways,
historians, other SCADA systems) over `opc.tcp`. It is the outgoing counterpart of the
embedded OPC UA server that publishes this station's own address space (described under
[OPC UA server](connector-opcua-server)); the two share the station's application identity but
trust their peers independently.

## Device {#device}

| Field | Meaning | Default |
| --- | --- | --- |
| **Host** | DNS name, IPv4 or IPv6 address of the server. An endpoint URL can be pasted as an OPC UA client shows it (`opc.tcp://192.168.0.10:4841/UA/Server`): `opc.tcp://` is dropped, the port goes to the port field and the path to the resource path. An address of another protocol is refused, with the reason under the box. | empty: the device is not connected until one is entered |
| **Port** | TCP port of the endpoint, 1 to 65535. An empty box shows 4840 in grey and the device is dialed on it, which is also how a device saved with 0 reads. | 4840 |
| **Resource path** | The endpoint path some servers require after host and port, producing `opc.tcp://host:port/path`. Leave empty when the server answers at the root. | empty |
| **Session security** | Off, the session is signed and encrypted and equipment that offers nothing better is refused. **Allow a session without security** opens it anyway, in the clear, for equipment that advertises no secure endpoint. | off |
| **Username** / **Password** | The user identity for the session. Empty username means an anonymous session. The password is protected at rest with per-user Windows DPAPI and can only be read on the machine and Windows account that entered it. | anonymous |
| **Poll interval (ms)** | Default polling rate for tags that do not override it. | 1000 |

## Prepare the server {#prepare-the-server}

Most OPC UA servers turn a new client away out of the box, by two settings of
their own, both made on the server:

| Setting on the server | What it takes | Without it |
| --- | --- | --- |
| Trust in this station's certificate | The server trusts the application certificate this station presents, the same one the embedded [OPC UA server](connector-opcua-server) presents. The station keeps it as a `.der` file in its data root, under `ua\pki\own\certs`. | The server refuses the secure channel (`BadSecurityChecksFailed` or `BadCertificateUntrusted`) or closes it during the handshake, and no session opens. |
| A user it accepts | Anonymous access allowed on the server, or a **Username** and **Password** on the device that the server knows. | The server refuses the session (`BadIdentityTokenRejected`, `BadIdentityTokenInvalid` or `BadUserAccessDenied`). |

Where common servers trust a client's certificate:

| Server | Where |
| --- | --- |
| A server that keeps refused certificates in folders, as the OPC Foundation's SDK and many servers built on it do | The refused certificate waits in its `rejected` folder: move it to `trusted/certs`, or copy the station's file there. |
| KEPServerEX | Right-click the **Administration** icon in the notification area and choose **OPC UA Configuration**; on the Trusted Clients tab, select the certificate and click Trust, or click **Import...** and choose the file; then **Reinitialize** from the same icon. |
| Siemens S7-1500, in TIA Portal | Import the file under **Global security settings > Certificate manager**, tab **Device certificates**; in the CPU's Properties, under **OPC UA > Server > Security > Secure Channel**, add it to Trusted clients with `<Add new>`; then download to the CPU. |

When the server refuses for either reason, the device's page says which in a
notice that stands in place of the server's status. For the certificate it
gives the certificate's name and its thumbprint (SHA-1, the one servers list it
by), each with a copy button, and the file with a button that opens its folder.
For the user it says whether the server takes no anonymous session or refused
the user, and points to the device's **Username** and **Password**. The notice
goes away by itself once a session opens, and the journal and the agent read
the same instruction. This station issues a new certificate when its
application name or network name changes, and the server then has to trust the
new one.

## Security and certificates {#security-and-certificates}

Connecting starts with the server's endpoint list: the client picks the **most
secure endpoint the server advertises** and authenticates the application with
this station's own certificate, the same OPC UA application identity the
embedded server uses. Many servers require that certificate to be trusted on
their side before they accept the session: [Prepare the server](#prepare-the-server)
says where.

Equipment that advertises nothing better than an **unsecured endpoint** is
refused, and the device says why. That session would carry the readings and the
sign-in in the clear and prove no equipment identity, so it opens only where you
asked for it on that device: **Session security**, **Allow a session without
security**, on the connection card. The choice belongs to that one device,
travels in its address as `?security=none`, and covers the reading session and
the discovery browse alike. Turning it on never downgrades equipment that does
offer security: the most secure endpoint advertised is still the one taken.

In the other direction, trust is **fail-closed and explicit**. The equipment's
certificate is checked before the session opens:

- A certificate this station has never approved is refused, recorded for review,
  and the device panel shows a notice naming the approval it is waiting for:
  the device is not "offline", it is waiting for a decision.
- You review certificates on this page's [Equipment access](#equipment-access) card,
  the outgoing half of certificate trust: a recorded certificate sits under
  **Waiting for approval** until you **Approve** it (new sessions may open) or
  **Block** it (refused without raising repeated prompts). This is the equipment
  list; **Client access**, on the [OPC UA server](connector-opcua-server)'s panel, is
  the other direction, who may connect inbound to this station's own server.
- **Withdraw approval**, on an approved row, closes the live sessions running on
  that certificate and returns it to the review list, so the next connection asks
  you again.
- Equipment that presents **no certificate at all** proves nothing about who
  answered, so there is nothing to approve and the session is refused, with its
  address written to the journal. It connects only under the same **Allow a
  session without security**, which is then what stands in for the approval.

Approving a device server never authorizes that certificate to connect inbound to
this station's own server: the two directions keep separate stores on purpose.

## Session behavior {#session-behavior}

Tags are **polled**: each read asks the server for the node's current value. The
server's own verdict travels with every sample: a value stamped Bad or
Uncertain is refused and the tag reads bad quality, so a doubtful source is
never laundered into a good reading. A node that no longer exists on the server
reads as no value rather than failing the whole device.

The server's stamp travels with the sample too: the SourceTimestamp it gave a good value becomes the tag's **Source timestamp** on the [tag page](connector-tags#the-live-value-card). A server may keep that stamp until the value or its quality changes, so a steady value can show an earlier stamp than the **Application timestamp**, which is always the instant the station received the reading.

The session is supervised with keep-alives; after three missed keep-alives the
client starts an automatic reconnect and carries on. The app's background retry
loop owns initial connection, so an unreachable server costs nothing at startup.

## Tag addressing {#tag-addressing}

A tag is addressed by its **Node id**, typed as free text and sent verbatim.
Accepted forms are the standard OPC UA notations:

| Example | Meaning |
| --- | --- |
| `ns=2;s=Temperature` | String identifier in namespace index 2 |
| `ns=2;g=5f2c7a1e-8b3d-4c9a-9e61-2d4b7f0a3c58` | GUID identifier in namespace index 2 |
| `ns=3;i=1042` | Numeric identifier in namespace index 3 |
| `nsu=urn:vendor:app;s=Line1.Speed` | Expanded form naming the namespace by URI instead of index, robust against index changes across restarts |

The tag reads and writes the node's **Value** attribute.

### Value types {#value-types}

| Offered types |
| --- |
| Boolean, Byte, Int16, Int32, Int64, Float, Double, String |

Values are **scalars only**: arrays, structures and other complex values are
refused as unreadable rather than flattened into a misleading number, and the
tag reads bad quality. A tag like that fails on its own; the device's other tags
keep reading, and nothing is reconnected over it. An empty string is a value, not
a failure. Types outside the offered set are not available as a tag's type on
this driver: SByte, the unsigned widths other than Byte (UInt16, UInt32, UInt64),
DateTime, Guid and ByteString.

A tag whose type differs from the node's is converted by its number, never reread through its
bytes, and the same rule holds for reads and writes. A real goes to a real (Float and Double),
and a whole number to a whole number of another width when the value fits; a Boolean tag over a
whole-number node reads 0 and 1 as off and on and writes them as 0 and 1. A value that does not
fit, or a node that holds another kind of value (a real for an integer tag, a number for a text
tag), is refused on that one point, and a write refused this way is never sent to the server.

A scan of a server therefore offers fewer channels than the server holds: a node
that declares a list of values or holds one when the scan reads it, and a node whose
type is outside the offered set, are left out. Many servers leave a variable's
declared shape open (a ValueRank of Any or ScalarOrOneDimension) even where it only
ever holds one value, so such a node is judged by the value it holds when the scan
reads it, and is offered unless that value is a list. Should it hold a list later,
that one tag fails on its own reads, like any value a tag cannot hold, and the
device's other tags keep reading. A node's type is the built-in type its declared
type derives from: a node declared as VersionTime holds a UInt32 and one declared
as UtcTime a DateTime,
so both are left out, while one declared as Duration holds a Double and is offered.
Servers that leave the shape open often leave the declared type open too
(BaseDataType, or Number, Integer or UInteger, which no value travels as), so such
a node takes the type of the value it holds when the scan reads it, and is offered
or left out by that type. One whose read brings back no value shows no type and is
left out. Should the server later send a value of another type, that value is read
by the conversion rule above, and that one tag fails only where the rule refuses it.
They are not dropped in silence, though. The scan counts them and
its closing message says how many and of which type, for example "5 channels
were left out: 3 of type UInt32, 2 that hold a list of values instead of one
value.", so a short list reads as a short list rather than as a bare controller.

A node the server answers with no value at all is left out too, and not counted:
it holds nothing, and imported it would never read. A node whose value comes back
Bad is still offered unless it declares a list, or declares an open type and brings
back no value to take one from: that status says only that the
equipment behind it has not answered yet, or that the scan's anonymous session may not read it, and the device
signs in with its own credentials.

## Writes {#writes}

A writable tag writes the node's Value attribute through the OPC UA Write
service. The server's response is the verdict: a rejected write surfaces as a
failure, and the cached value advances only on acceptance. As everywhere, the
write field takes the engineering value and the [value pipeline](connector-value-pipeline) is
reversed before the raw value is sent.

## Discovery {#discovery}

The **Discover** command takes a target in the field beside it: an OPC UA server
address such as `opc.tcp://plc-host:4840`. The scan has two stages, and the
progress line names the one running:

1. **Endpoints**: the target's standard discovery service is queried and every
   advertised discovery URL becomes a candidate endpoint. Custom ports and
   server paths round-trip into the candidate's connection fields.
2. **Channels**: each candidate's address space is browsed from the Objects
   folder down, and every readable variable is listed as a candidate tag with
   its type and access, except the nodes [Value types](#value-types) says are left
   out. The server's diagnostics areas are skipped.

A candidate tag is named by its own node's browse name, with the fewest parent
names in front of it that keep it apart from the other channels the server offers:
`Level` rather than `Devices.Simulated.Tank.Level` on another
station's server, and `Line1.Speed` beside `Line2.Speed` only where two nodes named
Speed would meet. Two names that would still read alike, differing only in case or
punctuation, are told apart by a number. The whole browse path stays on the
candidate card, on a second line under the name, so you can tell which node each
tag reads. The tag reads its node id, so the name is only what it is called: in the
tag grid and at the end of its address (`Connector.OpcUa.Peer.Level`).

Browsing signs in anonymously (with the same most-secure-endpoint selection, the
same refusal of an unsecured session, and the same equipment-certificate gate). A
server that refuses anonymous sessions, a server that advertises no secure
endpoint, or a browse that fails for any reason, still leaves the endpoint
addable with zero tags: add it, set the credentials and the session security on
the device, and configure tags by hand. Nothing is persisted until you **Add** a
candidate, and canceling keeps the candidates that were already complete.

A server you already have reads more of its nodes from its device's own page: **Discover tags**, on the head of the Tags card, browses that device's endpoint signed in as the device's own session is, with its username, its password and its session security, so a server that refuses anonymous sessions lists its nodes there too. It offers only the nodes no tag of the device reads yet, compared through the server's namespace table: a tag typed with a namespace index (`ns=2;s=Level`) reads the node the browse names by its namespace URI (`nsu=…;s=Level`), so that node is not offered again. A server that refuses the session ends the scan with the cause the device's own session gives (see [Discovering a device's tags](connector-discovery#discovering-a-devices-tags)).

## Limits worth knowing {#limits-worth-knowing}

- Reads are polls of the Value attribute; the driver does not create
  subscriptions on the remote server.
- Scalar values only: a tag cannot address an array element or a structure
  field.
- The approval gate applies per certificate: equipment that rotates its certificate
  returns to the review list and waits for a new decision.

## Equipment access {#equipment-access}

The OPC UA driver's page ends with its **Equipment access** card: the outgoing half of certificate trust. When the Connector dials an OPC UA equipment server, that server proves who it is with a certificate, and the station reads nothing from equipment whose certificate you have not approved. The card head says it: "When the Connector dials an OPC UA equipment server, that server proves who it is with a certificate. Approve only a fingerprint you have checked with whoever commissioned the equipment." View connector reads the card; Configure connector decides, and decisions are taken on the station's own desktop.

The principle under the head: "Nothing is read from equipment you have not approved. A device whose certificate is waiting here stays silent and says so in the Connector. Approving it starts the reading; withdrawing the approval closes the session and asks you again. Certificate names are claimed by the equipment; the SHA-256 fingerprint is the identity to verify."

The card is deliberately the same shape as [Client access](connector-opcua-client-access), with the same words wherever the idea is the same, and the same rule for who may decide: a browser on another device sees "Equipment access can only be changed in the station's desktop app." and the banner "Open the desktop app to change equipment access"; a role without Configure connector is not drawn the buttons, and a running unit and a change already in progress each refuse with their own sentence. Every outcome lands in the status bar.

### What a waiting device does in the Connector {#what-a-waiting-device-does-in-the-connector}

The first time the Connector dials an OPC UA device, the equipment's certificate is refused, recorded here as waiting, and the device reads nothing. On the [device's page](connector-devices) the Connector shows a caution banner, **Waiting for certificate approval**, with a **Review certificate** button that opens this section on the device's row, and the device's health names the same state in the tree. The driver's own failure says the reason instead of a nameless handshake error: "This equipment's certificate (the first 16 characters of the fingerprint) is waiting for your approval under Equipment access on the OPC UA driver's page. Reading starts once you approve it." A blocked one reads "…is blocked at this station. Review it under Equipment access on the OPC UA driver's page." The discovery browse passes the same gate before its session opens.

Once approved, the Connector starts reading on its next attempt. Equipment that was reading and starts presenting a different certificate at the same address stops being read and raises a sticky card, "Certificate changed: `<name>`": "The equipment at `<address>` was reading with a certificate this station approved and is now presenting a different one (…), so nothing is read from it. Check the fingerprint with whoever maintains the equipment, then approve it.", with **Review**, plus a Windows notification for a hidden window. A device being registered right now is not announced this way: whoever registers it is already looking at the Connector.

### Waiting for approval {#waiting-for-approval}

Drawn while at least one certificate waits, headed "These devices stay unread until you decide." with the count. Each row shows an amber dot, the name the certificate claims, "Claimed equipment identity", the fingerprint, "`<address>` · Last attempt `<date and time>` · N connection attempts", and a **Certificate details** disclosure with Subject, Issuer, Application URI, Domain names, Valid from, Valid until and First seen. The row a Review button led to is marked "Selected for review" and scrolled into view.

| Command | What it does | Greyed when (situation) |
| --- | --- | --- |
| Approve | Moves the certificate to Approved equipment and refreshes the live validator: "`<name>` is approved. The Connector starts reading on its next attempt." | By the rule above. |
| Block | Moves it to Blocked equipment and keeps refusing it: "`<name>` is blocked. The station will keep refusing this certificate." | By the rule above. |
| Discard all pending certificates (on the group head) | Asks "Discard the certificate waiting for review?" or "Discard the N certificates waiting for review?" ("Nothing is approved or blocked by this: the requests are removed and the list makes room again. Equipment the Connector dials after this appears here as a new request."), with **Discard** and **Keep the list**. | By the rule above. |

The list holds at most 100 certificates. While it is full, a further certificate stays refused but is not retained, and the card says so: "The review list below is full. While it is, a new certificate is refused without being added here, so the equipment you are waiting for may never appear. Discard the certificates waiting for review to make room.", with the count of refusals since it filled once there are any. Every such refusal is journaled.

### Approved equipment {#approved-equipment}

"Certificates the Connector may open sessions with.", with the count. Each row shows a neutral dot, the claimed name, "Claimed equipment identity · Approved by `<user>` on `<date and time>`" (or "at this station"), the fingerprint, and a details disclosure with Address, Subject, Issuer, Application URI, Valid until and Last seen. With none: "No equipment certificate approved."

| Command | What it does | Greyed when (situation) |
| --- | --- | --- |
| Import certificate (on the group head) | Opens a file picker for the equipment's public certificate (`.der`, `.cer`, `.crt` or `.pem`, up to 64 KB) and approves it before the first connection: "`<name>` is approved from `<file>`. Check the fingerprint below against the one in the equipment's documentation." A larger file: "`<file>` is larger than an equipment certificate. Import the public certificate file (.der, .cer or .pem), not an archive." | By the rule above. |
| Withdraw approval (on the row, danger ink) | Asks "Withdraw approval for `<name>`?": "The current session with this equipment is closed and the station stops reading from it. The certificate returns to the review list, so the next connection asks you again.", with **Withdraw** and **Keep approved**. Then: "Approval withdrawn for `<name>`. Its session was closed and it is waiting for review again." | By the rule above. |

The hint under the head: "Have the equipment's public certificate already? Import it here to approve it before the first connection, instead of letting the connection fail once. Check the fingerprint it adds against the one in the equipment's documentation." Equipment usually ships with its certificate, which is why this side has the import first. Importing a blocked certificate lifts the block. Withdrawing sends the certificate back to review, not to a block: you asked to be asked again.

### Blocked equipment {#blocked-equipment}

A collapsed disclosure, "Blocked equipment" with the count, drawn only while at least one exists. Each row shows a red dot, the claimed name, "Claimed equipment identity · Blocked by `<user>` on `<date and time>`", the fingerprint, and **Unblock**, which returns it to the review list without approving it: "`<name>` is unblocked and has returned to the review list." A blocked certificate never returns on its own.

### Where the decisions live {#where-the-decisions-live}

The equipment stores sit under `ua\pki\southbound` in the station's data root (`pending`, `trusted`, `blocked` and the SDK's `rejected`), with the address each certificate was seen at, attempt counts and decisions in `equipment-access.json`. They are separate from the inbound stores, so an equipment certificate can never become permission for an application to connect to the station's own server, nor appear as an inbound request. They are not part of a configuration [backup](settings-backups).

### What this section does not do {#what-this-section-does-not-do}

Nothing is approved automatically, and no setting turns that off. The card does not configure the device itself (address, credentials and polling are on the [Connector](connector-devices)), does not show the equipment's live values, and does not decide about applications dialing this station, which is [Client access](connector-opcua-client-access).
