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.
Enable it
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
Basic256Sha256withSignandSignAndEncrypt, bound to loopback. A user name and password are always encrypted: even on aNoneendpoint the login is encrypted with the server certificate. -
Certificate. A self-signed certificate is generated into
<pki_dir>/ownon 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 matchesapplication_uri. -
Client trust. With
trust_client_certs: falsea client’s certificate is rejected until you move it from<pki_dir>/rejectedto<pki_dir>/trusted. Clients also verify the server certificate: trust<pki_dir>/own/cert.derin the client. -
Refused at startup (also reported by
--check-config), unlessallow_insecure: true:- policy
Nonetogether withanonymous: trueon a non-loopback bind (every tag readable unauthenticated and unencrypted from the network); trust_client_certs: truetogether withanonymous: trueon a non-loopback bind (any client with a self-made certificate can read everything).
With
allow_insecure: truethe server starts and logs aSECURITY:warning at every start. PolicyNone, anonymous access andtrust_client_certson their own also log a warning. - policy
-
An unwritable
pki_diror 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 (
HistoryReadis unsupported): use the storage and historian sinks for history. - Records of machines beyond
max_devicesandmetadatabeyondmax_metadata_per_deviceare not served; they are counted inskippedinGET /api/status. - Two machines whose ids make the same node id (
m1with a variablestatus.stateagainst a machine calledm1.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.