OpexFlow can **serve** OPC-UA as well as read it. A SCADA, MES, historian or any OPC-UA
client (UaExpert, Ignition, WinCC, KEPServerEX, a script) connects to the connector, browses
the machines and reads or subscribes to their tags. The server is built into the binary
(feature `opcua-server`, on by default) and off until you enable it.

What it serves is exactly what the flow's **`hub` sink** emits: every record a flow routes to
the hub also updates the OPC-UA address space. A record that never reaches the hub sink is not
visible to OPC-UA clients.

> **Read-only**
>
> Clients can browse, read and subscribe. They **cannot write**: every variable has the access
> level *current read* only, a Write is refused (`BadUserAccessDenied`), and AddNodes / DeleteNodes /
> method calls are unsupported. Nothing is ever sent to a machine through this server.

## Enable it

```yaml
opcua_server:
  enabled: true
  endpoint: "opc.tcp://0.0.0.0:4840"   # default opc.tcp://127.0.0.1:4840 (loopback, like the API)
  advertised_host: "flow.plant.local"  # name clients use when binding a wildcard address
  users:
- { name: scada, password: "${OPCUA_SCADA_PASSWORD}" }
```

`opcua_server:` is a **top-level** section (a sibling of `adapters:`). It needs a restart to
change (`/api/config/reload` does not touch it). `GET /api/status` has an `opcua_server`
object: `{enabled:false}` when it is off, else the endpoint, `sessions`, `devices`,
`variables`, `records`, `lagged_records` and `skipped`.

| Key | Default | Meaning |
| --- | --- | --- |
| `enabled` | `false` | Start the server. |
| `endpoint` | `opc.tcp://127.0.0.1:4840` | Address to bind. Loopback by default; `0.0.0.0` serves the network. Port `0` picks a free port. |
| `advertised_host` | machine host name | Host in the endpoint URLs the server hands to clients when `endpoint` is a wildcard (`0.0.0.0`, `::`). |
| `application_name` | `OpexFlow` | Shown by clients. |
| `application_uri` | `urn:opex-flow:<connector.name>` | Application URI, also inside the certificate. |
| `namespace_uri` | `urn:opex-flow:machines` | Namespace of the machine nodes. |
| `security_policies` | `[Basic256Sha256]` | `Basic256Sha256`, `Aes128Sha256RsaOaep`, `Aes256Sha256RsaPss`, and `None`. `None` is offered only when listed. The deprecated `Basic128Rsa15` and `Basic256` are not available. |
| `security_modes` | `[Sign, SignAndEncrypt]` | Modes offered for every policy except `None`. |
| `anonymous` | `false` | Accept clients without a login. |
| `users` | none | `{name, password}` logins; passwords as `${ENV}` or `${secret:name}`. Redacted in `GET /api/config`. |
| `pki_dir` | `<data_dir>/opcua-server-pki` | Certificate and trust store (relative paths are under `data_dir`). |
| `trust_client_certs` | `false` | Accept every client certificate without it being in `<pki_dir>/trusted`. |
| `allow_insecure` | `false` | Allow the two unsafe exposures below on a non-loopback bind. |
| `max_devices` | `1000` | Machine folders the address space may hold. |
| `max_metadata_per_device` | `200` | `metadata.*` variables per machine. |
| `max_sessions` | `50` | Concurrent client sessions. |

At least one of `anonymous: true` or a `users` entry is required; a server nobody can log in
to is a configuration error.

## Security

- **Secure by default.** The default offers `Basic256Sha256` with `Sign` and `SignAndEncrypt`,
  bound to loopback. A user name and password are always encrypted: even on a `None` endpoint
  the login is encrypted with the server certificate.
- **Certificate.** A self-signed certificate is generated into `<pki_dir>/own` on the first
  start and reused afterwards. It is replaced automatically (and clients must trust it again)
  when it has less than 30 days left or no longer matches `application_uri`.
- **Client trust.** With `trust_client_certs: false` a client's certificate is rejected until
  you move it from `<pki_dir>/rejected` to `<pki_dir>/trusted`. Clients also verify the server
  certificate: trust `<pki_dir>/own/cert.der` in the client.
- **Refused at startup** (also reported by `--check-config`), unless `allow_insecure: true`:
  1. policy `None` together with `anonymous: true` on a non-loopback bind (every tag readable
     unauthenticated and unencrypted from the network);
  2. `trust_client_certs: true` together with `anonymous: true` on a non-loopback bind (any
     client with a self-made certificate can read everything).

  With `allow_insecure: true` the server starts and logs a `SECURITY:` warning at every start.
  Policy `None`, anonymous access and `trust_client_certs` on their own also log a warning.
- An unwritable `pki_dir` or a port already in use stops OpexFlow at startup rather than
  silently running without the server.

## What clients see

One **folder per machine** under `Objects`, browse name = `machine.id`, display name =
`machine.name` when the record has one. Folders and variables are created when the first record
of a machine arrives, so an empty connector shows nothing. Node ids are strings in the
namespace `namespace_uri`: `ns=2;s=<machine>` for the folder and
`ns=2;s=<machine>.<variable>` for a variable (the namespace index is not fixed; resolve it from
the namespace array, or browse).

| Variable | Data type | From |
| --- | --- | --- |
| `status.state` | String | `running`, `idle`, `alarm`, `offline`, `unknown` |
| `status.alarmCode`, `status.alarmMessage` | String | Created when a record first carries them; set to `""` when a later record has none |
| `spindle.speed`, `spindle.load` | Double | `spindle` (load only when present) |
| `axis.<name>.position` | Double | `axes[]`; the unit is the variable's description |
| `metadata.<key>` | Boolean, Int64, UInt64, Double or String | `metadata`; arrays and objects as JSON text, `null` values are skipped |
| `quality` | String | `good`, `uncertain`, `bad` |
| `source` | String | The record's `source` |
| `timestamp` | DateTime | The record timestamp |

A `metadata` variable keeps the type of its first value and only widens: Int64 and UInt64
become Double when a float arrives, anything else mixed becomes String (the DataType attribute
changes accordingly). A variable whose field is missing from a later record keeps its last value
and its older timestamp.

**Quality and time.** Each value's `StatusCode` comes from the record's `quality` (absent =
good): `good` → `Good`, `uncertain` → `Uncertain`, `bad` → `BadCommunicationError`. A
per-value `valueQuality` entry can only make its own value worse, never better. The value is
still delivered with a bad status. `SourceTimestamp` is the record timestamp, `ServerTimestamp`
is the time the server applied the record.

A record of a degraded device (for example the offline record: state `offline`, quality `bad`,
no axes or spindle) also degrades every variable it does not carry: they keep their last value
and their last real timestamp, but their StatusCode becomes the record's. The next good record
that carries them makes them good again.

**Subscriptions.** Values are pushed from the data hub, so a client subscription gets a data
change whenever a value changes. A change of the StatusCode alone is reported only to
subscriptions that request a data change filter with trigger *Status/Value* (the async-opcua
server applies the OPC-UA default only when the filter is sent; most SCADA clients have this
as a per-item option). The `quality` and `status.state` variables change value with the
quality, so they notify either way. If the server falls behind the hub's broadcast buffer,
the missed records are counted in `lagged_records`.

## Limits and what is not included

- Read-only by design; there is no flow node that writes to an OPC-UA variable path, and no
  history (`HistoryRead` is unsupported): use the storage and historian sinks for history.
- Records of machines beyond `max_devices` and `metadata` beyond `max_metadata_per_device`
  are not served; they are counted in `skipped` in `GET /api/status`.
- Two machines whose ids make the same node id (`m1` with a variable `status.state` against a
  machine called `m1.status.state`) clash: the later node is not created and a warning is logged.
- On an HA pair each node runs its own server; the standby has no data until it is promoted.