# OPC UA

> 面向第三方服务器的 OPC UA 客户端，带以证书把守的信任、按节点 id 寻址，以及地址空间的设备发现。

OPC UA 驱动是一个面向第三方 OPC UA 服务器的**客户端**，走 `opc.tcp`，对象是 PLC、网关、历史数据库和别的 SCADA 系统。它是那个对外发布本工作站自己地址空间的内置 OPC UA 服务器（在 [OPC UA 服务器](connector-opcua-server)下面讲）在出站方向上的对应物；两者共用工作站的应用身份，但各自独立地信任对端。

## 设备 {#device}

| 字段 | 含义 | 默认值 |
| --- | --- | --- |
| **主机** | 服务器的 DNS 名称、IPv4 或 IPv6 地址。可以照 OPC UA 客户端显示的样子粘贴 endpoint URL（`opc.tcp://192.168.0.10:4841/UA/Server`）：`opc.tcp://` 会被去掉，端口进入端口字段，路径进入资源路径。其他协议的地址会被拒绝，并在字段下方说明原因。 | 空：填写之前设备不会被连接 |
| **端口** | endpoint 的 TCP 端口，1 到 65535。框留空时 4840 以灰色显示，设备就在这个端口上被拨号连接；以 0 保存的设备也是这样显示的。 | 4840 |
| **资源路径** | 有些服务器要求在主机和端口之后再接一段 endpoint 路径，拼出来是 `opc.tcp://host:port/path`。服务器在根上应答时就留空。 | 空 |
| **会话安全** | 关闭时，会话经过签名和加密，拿不出更安全方式的现场设备会被拒绝。**允许没有安全保护的会话**则照样把会话打开，以明文进行，用于不公布任何安全 endpoint 的现场设备。 | 关闭 |
| **用户名** / **密码** | 这次会话所用的用户身份。用户名为空表示匿名会话。密码在静态存储时由 Windows 的每用户 DPAPI 保护，只能在输入过它的那台机器和那个 Windows 账户上读出来。 | 匿名 |
| **轮询间隔（ms）** | 没有自己另作规定的点位所用的默认轮询速率。 | 1000 |

## 准备服务器 {#prepare-the-server}

大多数 OPC UA 服务器出厂时就会拒绝新的客户端，原因是它们自己的两项设置，两项都在服务器上做：

| 服务器上的设置 | 需要什么 | 没有它时 |
| --- | --- | --- |
| 信任本工作站的证书 | 服务器信任本工作站出示的应用证书，也就是内置 [OPC UA 服务器](connector-opcua-server)出示的那一份。工作站把它作为 `.der` 文件保存在自己的数据根目录下的 `ua\pki\own\certs` 里。 | 服务器拒绝安全通道（`BadSecurityChecksFailed` 或 `BadCertificateUntrusted`），或者在握手过程中把它关掉，会话打不开。 |
| 它接受的用户 | 服务器上允许匿名访问，或者设备上填了服务器认识的**用户名**和**密码**。 | 服务器拒绝会话（`BadIdentityTokenRejected`、`BadIdentityTokenInvalid` 或 `BadUserAccessDenied`）。 |

常见服务器信任客户端证书的位置：

| 服务器 | 位置 |
| --- | --- |
| 把被拒绝的证书放在文件夹里的服务器（OPC Foundation 的 SDK 以及基于它的许多服务器） | 被拒绝的证书在它的 `rejected` 文件夹里等着：把它移到 `trusted/certs`，或者把工作站的文件复制到那里。 |
| KEPServerEX | 右键点击通知区域里的 **Administration** 图标，选择 **OPC UA Configuration**；在 Trusted Clients 选项卡中选中证书并点 Trust，或者点 **Import...** 并选择文件；然后从同一个图标执行 **Reinitialize**。 |
| Siemens S7-1500（TIA Portal） | 在 **Global security settings > Certificate manager** 的 **Device certificates** 选项卡中导入文件；在 CPU 的 Properties 中，于 **OPC UA > Server > Security > Secure Channel** 用 `<Add new>` 把它加入 Trusted clients 列表；然后下载到 CPU。 |

服务器因为其中任一原因拒绝时，设备页面会用一条提示代替服务器的状态，说明是哪一个原因。对证书，它给出证书的名称和指纹（SHA-1，服务器列出它时用的那个），每项都带复制按钮，并给出文件和一个打开其所在文件夹的按钮。对用户，它说明服务器是不接受匿名会话，还是拒绝了这个用户，并指向设备的**用户名**和**密码**。会话一打开，提示就会自行消失，日志和智能体读到的也是同样的说明。本工作站的应用程序名称或网络名称改变时，会签发新的证书，服务器随后必须信任新的证书。

## 安全与证书 {#security-and-certificates}

连接是从服务器的 endpoint 列表开始的：客户端挑**服务器公布的那些 endpoint 里最安全的那一个**，并用本工作站自己的证书为应用做认证，也就是内置服务器所用的那同一个 OPC UA 应用身份。许多服务器要求在它们那一侧先信任这份证书，才接受会话；在哪里做，见[准备服务器](#prepare-the-server)。

一台除了**没有安全保护的 endpoint**再拿不出别的现场设备会被拒绝，设备会说明原因。那样的会话会把读数和凭据以明文送出去，也证明不了现场设备的身份，所以它只在你于那台设备上提出要求的地方才打开：连接卡片上的**会话安全**、**允许没有安全保护的会话**。这个选择属于那一台设备，以 `?security=none` 的形式随它的地址走，读取会话和发现设备时的浏览一样适用。把它打开也绝不会让本来提供安全的现场设备被降级：拿到的仍然是公布出来的那些 endpoint 里最安全的一个。

反方向上，信任是**默认拒绝、必须明确给出**的。现场设备的证书在会话打开之前就被检查：

- 一份本工作站从未批准过的证书被拒绝，并记下来等你审阅，设备面板显示一条通知，点名它在等哪一次批准：这台设备不是“离线”，它是在等一个决定。
- 你在本页面的[现场设备访问](#equipment-access)卡片里审阅证书，那是证书信任的出站那一半：一份被记下来的证书待在**等待批准**里，直到你**批准**它（新会话可以打开）或者**阻止**它（被拒绝，而且不会反复弹提示）。那是现场设备的清单；[OPC UA 服务器](connector-opcua-server)面板上的**客户端访问**是另一个方向，也就是谁可以入站连到本工作站自己的服务器上。
- 已批准的那一行上有**撤回批准**，它会关掉基于那份证书跑着的实时会话，并把它送回审阅列表，于是下一次连接会再问你一遍。
- **一份证书都不出示**的现场设备，证明不了应答的是谁，也就没有什么可批准的，那次会话会被拒绝，并把它的地址写进日志。它只有在同一个**允许没有安全保护的会话**之下才连得上，那时这个选择就代替了批准。

批准一台设备服务器，绝不等于授权那份证书入站连到本工作站自己的服务器上：两个方向刻意分开存放。

## 会话的行为 {#session-behavior}

点位是被**轮询**的：每一次读取都向服务器要这个节点当前的值。服务器自己的判断随每一次采样一起过来：被标为 Bad 或 Uncertain 的值会被拒绝，点位读作质量不可用，所以一个可疑的来源绝不会被洗成一次好的读数。服务器上已经不存在的节点读作没有值，而不是让整台设备失败。

服务器的时间戳也随采样一起过来：服务器给一个好值打上的 SourceTimestamp，会在[点位页面](connector-tags#the-live-value-card)上成为这个点位的**源时间戳**。服务器可以一直留着这个时间戳，直到值或质量变化，所以一个不变的值显示的时间戳可能早于**应用时间戳**；**应用时间戳**始终是工作站收到这次读数的那一刻。

会话由保活报文照看着；连着三次保活没到，客户端就发起一次自动重连并继续下去。初次连接由应用后台的重试循环负责，所以一台够不着的服务器在启动时不花什么代价。

## 点位寻址 {#tag-addressing}

一个点位靠它的 **节点 ID** 寻址，以自由文本输入，原样发出去。接受的形式就是 OPC UA 那几种标准记法：

| 例子 | 含义 |
| --- | --- |
| `ns=2;s=Temperature` | 命名空间索引 2 里的字符串标识符 |
| `ns=2;g=5f2c7a1e-8b3d-4c9a-9e61-2d4b7f0a3c58` | 命名空间索引 2 里的 GUID 标识符 |
| `ns=3;i=1042` | 命名空间索引 3 里的数字标识符 |
| `nsu=urn:vendor:app;s=Line1.Speed` | 展开形式，用 URI 而不是索引来点名命名空间，对重启之后索引变化更稳健 |

点位读写的是这个节点的 **Value** 属性。

### 数值类型 {#value-types}

| 提供的类型 |
| --- |
| Boolean、Byte、Int16、Int32、Int64、Float、Double、String |

值**只能是标量**：数组、结构和其他复杂值会被当作读不了而拒绝，不会被压扁成一个误导人的数字，点位读作质量不可用。这样的点位只是它自己失败；这台设备其他的点位照读不误，也不会为它重新连接什么。空字符串是一个值，不是一次失败。提供范围之外的类型在这个驱动上不能用作点位的类型：SByte、Byte 以外的无符号宽类型（UInt16、UInt32、UInt64）、DateTime、Guid 和 ByteString。

点位类型与节点类型不同时，按数值换算，绝不按字节重新解读，读和写遵循同一条规则。实数对应实数（Float 与 Double），整数在值放得下时对应另一种宽度的整数；Boolean 点位对应整数节点时，把 0 和 1 读作关和开，写入时也写成 0 和 1。放不下的值，或者节点持有的是另一类值（整数点位遇到实数、文本点位遇到数字），只在这一个点位上被拒绝，这样被拒绝的写入绝不会发送到服务器。

因此，扫描一台服务器给出的通道会比服务器实际持有的少：声明为一列值、或者扫描读取时持有一列值的节点，以及类型在提供范围之外的节点，都被留在外面。很多服务器即使变量始终只持有一个值，也让它声明的形态保持开放（ValueRank 为 Any 或 ScalarOrOneDimension），所以这样的节点按扫描读取时持有的值来判断，只要这个值不是一列值就会被给出。如果它以后持有一列值，只有这一个点位在自己的读取上失败，和点位持有不了的任何值一样，这台设备其他的点位照读不误。一个节点的类型，是它所声明的类型派生自的那个基本类型：声明为 VersionTime 的节点持有的是 UInt32，声明为 UtcTime 的持有的是 DateTime，所以两者都被留在外面；而声明为 Duration 的节点持有的是 Double，会被给出。让形态保持开放的服务器，往往也让声明的类型保持开放（BaseDataType，或者 Number、Integer、UInteger，没有哪个值会以这些类型传输），所以这样的节点采用扫描读取时它持有的那个值的类型，按这个类型决定是给出还是留在外面。这样的节点如果读取时没有带回任何值，就显示不出类型，会被留在外面。如果服务器以后发来另一种类型的值，这个值按上面的换算规则读取，只有在规则拒绝这个值时，这一个点位才会失败。不过它们不是被悄悄丢掉的。扫描会把它们数出来，收尾的消息会说有多少个、分别是哪一类，例如“有 5 个通道被留在外面：3 个是 UInt32 类型，2 个持有的是一列值而不是一个值。”，这样一份短列表读起来就是一份短列表，而不是一台空空的控制器。

服务器应答时根本不带值的节点也被留在外面，而且不计数：它什么也不持有，导入之后永远读不到。值以 Bad 返回的节点，只要没有声明为一列值，也没有在声明开放类型的同时读不回可以取类型的值，仍然会被给出：这个状态只说明节点背后的现场设备还没有应答，或者扫描用的匿名会话无权读它，而设备是用自己的凭据登录的。

## 写入 {#writes}

可写的点位通过 OPC UA 的 Write 服务写这个节点的 Value 属性。服务器的应答就是判决：被驳回的写入表现为一次失败，缓存的值只在被接受时才往前走。和别处一样，写入字段接收的是工程值，原始值发出去之前会先把[数值转换链](connector-value-pipeline)反过来走一遍。

## 设备发现 {#discovery}

**发现设备**这条命令从旁边那个字段里取一个目标：一个 OPC UA 服务器地址，例如 `opc.tcp://plc-host:4840`。扫描分两个阶段，进度行会点名正在跑的是哪一个：

1. **Endpoint**：向目标的标准发现服务查询，公布出来的每一个发现 URL 都成为一个候选 endpoint。自定义端口和服务器路径会原样回到候选项的连接字段里。
2. **通道**：从 Objects 文件夹往下浏览每个候选项的地址空间，每一个可读的变量都作为候选点位列出来，带着它的类型和读写方式，[数值类型](#value-types)里说被留在外面的节点除外。服务器的诊断区被跳过。

候选点位以它自己那个节点的名字（BrowseName）命名，前面只加上足以把它和服务器提供的其他通道区分开的最少几层上级节点的名字：在另一个站点的服务器上是 `Level` 而不是 `Devices.Simulated.Tank.Level`，只有两个都叫 Speed 的节点会撞在一起的地方才是 `Line1.Speed` 和 `Line2.Speed`。两个名字如果仍然读起来一样（只差大小写或标点），就用一个编号区分。完整的浏览路径留在候选项的小卡上，写在名字下面的第二行，让你知道每个点位读的是哪个节点。点位按它的节点 ID 读取，所以名字只是它的叫法：用在点位表格里，也用在它地址的末尾（`Connector.OpcUa.Peer.Level`）。

浏览是以匿名身份登录的（用同样那套“挑最安全 endpoint”的做法，同样拒绝没有安全保护的会话，也过同样那道现场设备证书的关卡）。一台拒绝匿名会话的服务器、一台不公布任何安全 endpoint 的服务器，或者一次因任何原因失败的浏览，仍然会留下一个点位数为零、可以添加的 endpoint：把它加进来，在设备上填好凭据和会话安全，再手工配置点位。在你**添加**某个候选项之前什么都不会被存下来，而取消会保留那些已经完整的候选项。

已经有的服务器可以在它的设备页面上读到更多节点：点位卡片标题栏里的**发现点位**像设备自己的会话那样登录，浏览这台设备的 endpoint，用的是设备的用户名、密码和会话安全设置，所以拒绝匿名会话的服务器在这里也会给出它的节点。它只列出设备的点位都还没在读的节点，并通过服务器的命名空间表来比较：用命名空间索引写的点位（`ns=2;s=Level`）读的就是浏览用命名空间 URI 命名的那个节点（`nsu=…;s=Level`），所以这个节点不会再被列出。拒绝这个会话的服务器会让扫描结束，给出的原因和设备自己的会话给出的一样（见[发现一台设备的点位](connector-discovery#discovering-a-devices-tags)）。

## 值得知道的限制 {#limits-worth-knowing}

- 读取是对 Value 属性的轮询；驱动不会在远端服务器上创建订阅。
- 只支持标量值：一个点位没法去寻一个数组元素或者一个结构成员。
- 批准这道关卡是按证书算的：轮换了证书的现场设备会回到审阅列表，等一个新的决定。

## 现场设备访问 {#equipment-access}

OPC UA 驱动页面最后是它的 **现场设备访问** 卡片：证书信任朝外的那一半。Connector 拨号连接一台 OPC UA 现场设备服务器时，那个服务器用一份证书证明自己是谁，而证书你没有批准的设备，工作站一个数都不读。卡片抬头就是这么说的：“当 Connector 拨号连接一台 OPC UA 现场设备服务器时，那个服务器会用一份证书证明自己的身份。只批准你已经和调试该设备的人核对过的指纹。”查看 Connector 可以读这张卡片，由配置 Connector 来决定；决定在工作站自己的桌面上做。

抬头下面那条原则：“未经你批准的现场设备，一律不读取。证书还在这里等待的设备保持沉默，并且会在 Connector 中说明。批准它就开始读取；撤回批准会关闭会话并再次询问你。证书上的名称是设备自称的；要核对的身份是 SHA-256 指纹。”

这张卡片有意做成和[客户端访问](connector-opcua-client-access)一样的形状，意思相同的地方用的是相同的话，谁可以决定也是同一条规则：另一台设备上的浏览器看到“现场设备访问只能在工作站本机的桌面应用中更改。”和那条提示条“要更改现场设备访问，请打开桌面应用”；没有配置 Connector 的角色不会被画出这些按钮，而正在运行的单元和已经在执行的一次更改，各自用自己那句话拒绝。每一种结果都落在状态栏上。

### 一台等着的设备在 Connector 里做什么 {#what-a-waiting-device-does-in-the-connector}

Connector 第一次拨号连接一台 OPC UA 设备时，这台现场设备的证书被拒绝，在这里记作等待中，而这台设备一个数都不读。在[设备自己的页面](connector-devices)上，Connector 显示一条警示提示条，**正在等待证书批准**，带一个**查看证书**按钮，按下去就打开这一节并停在这台设备那一行上，而设备的健康状态在树里写着同一个状态。驱动自己那次失败说的是原因，而不是一个没名没姓的握手错误：“这台现场设备的证书（指纹的前 16 个字符）正在 OPC UA 驱动页面的‘现场设备访问’里等待你的批准。批准之后就开始读取。”被阻止的那一份写的是“…在本工作站上被阻止。请到 OPC UA 驱动页面的‘现场设备访问’里查看它。”设备发现的浏览在它那次会话打开之前，过的是同一道关卡。

一经批准，Connector 就在它下一次尝试时开始读取。本来在读的现场设备，如果在同一个地址上开始出示另一份证书，就不再被读取，并立起一张常驻卡片，“证书已更换：`<name>`”：“`<address>` 上的现场设备原先用本工作站批准过的证书供读取，现在出示的是另一份（…），因此不再从它读取任何数据。请与维护该设备的人核对指纹，然后批准它。”，带**审核**，窗口藏起来时还加一条 Windows 通知。正在登记中的设备不会这样宣告：登记它的那个人本来就看着 Connector。

### 等待批准 {#waiting-for-approval}

至少有一份证书等着的时候才画出来，抬头是“在你做出决定之前，这些设备不会被读取。”，带着数量。每一行显示一个警示色调的圆点、证书自称的名称、“自称的设备身份”、指纹、“`<address>` · 最近一次尝试 `<date and time>` · N 次连接尝试”，以及一个**证书详情**折叠块，里面是主体、颁发者、应用程序 URI、域名、有效期起、有效期至和首次出现。一个“审核”按钮带过去的那一行标着“已选中待审核”，并被滚动到视野里。

| 命令 | 作用 | 什么时候变灰（情形） |
| --- | --- | --- |
| 批准 | 把这份证书挪到已批准的现场设备里，并刷新实时校验器：“`<name>` 已获批准。Connector 会在下一次尝试时开始读取。” | 按上面那条规则。 |
| 阻止 | 把它挪到被阻止的现场设备里，并继续拒绝它：“`<name>` 已被阻止。本工作站会继续拒绝这份证书。” | 按上面那条规则。 |
| 丢弃所有待处理的证书（在这一组抬头上） | 先问一句“要丢弃那份等待审核的证书吗？”或者“要丢弃那 N 份等待审核的证书吗？”（“这样做不会批准也不会阻止任何东西：这些请求被移除，列表重新腾出位置。此后 Connector 拨号连接的设备会作为新请求出现在这里。”），带**丢弃**和**保留列表**。 | 按上面那条规则。 |

这份列表最多装 100 份证书。它满着的时候，再来一份证书仍然被拒绝，但不再留存，卡片会这么说：“下面的待审列表已满。在它满着的时候，新证书会被拒绝而不会加进这里，因此你等待的那台设备可能永远不会出现。请丢弃等待审核的证书来腾出位置。”，一旦真的有拒绝，就带上它满了以后的拒绝次数。每一次这样的拒绝都记进日志。

### 已批准的现场设备 {#approved-equipment}

“Connector 可以与之建立会话的证书。”，带着数量。每一行显示一个中性色圆点、自称的名称、“自称的设备身份 · 由 `<user>` 于 `<date and time>` 批准”（或者“在本工作站上”）、指纹，以及一个详情折叠块，里面是地址、主体、颁发者、应用程序 URI、有效期至和最近出现。一个都没有时：“没有已批准的现场设备证书。”

| 命令 | 作用 | 什么时候变灰（情形） |
| --- | --- | --- |
| 导入证书（在这一组抬头上） | 打开一个文件选择框，选这台现场设备的公开证书（`.der`、`.cer`、`.crt` 或 `.pem`，最大 64 KB），在第一次连接之前就批准它：“`<name>` 已从 `<file>` 获得批准。请把下面的指纹与设备文档里的指纹核对。”更大的文件：“`<file>` 比一份设备证书大。请导入公开证书文件（.der、.cer 或 .pem），而不是压缩包。” | 按上面那条规则。 |
| 撤回批准（在行上，危险色调） | 先问一句“要撤回对 `<name>` 的批准吗？”：“与这台设备当前的会话会被关闭，本工作站不再从它读取数据。证书回到待审列表，因此下一次连接会再次询问你。”，带**撤回**和**保持批准**。随后是：“已撤回对 `<name>` 的批准。它的会话已关闭，正在重新等待审核。” | 按上面那条规则。 |

抬头下面那句提示：“已经拿到设备的公开证书了？在这里导入它，就可以在第一次连接之前批准它，而不必先让连接失败一次。请把导入后出现的指纹与设备文档里的指纹核对。”现场设备通常随货带着它的证书，所以这一侧把导入放在前面。导入一份被阻止的证书会解除那次阻止。撤回是把证书退回审核，不是把它阻止：你要的是再被问一次。

### 被阻止的现场设备 {#blocked-equipment}

一个收起着的折叠块，“被阻止的现场设备”，带着数量，只有至少存在一份时才画出来。每一行显示一个红色圆点、自称的名称、“自称的设备身份 · 由 `<user>` 于 `<date and time>` 阻止”、指纹，以及**解除阻止**，按下去它就回到待审列表，而不被批准：“`<name>` 已解除阻止，并回到待审列表。”一份被阻止的证书绝不会自己回来。

### 这些决定存在哪里 {#where-the-decisions-live}

现场设备的那几个库在工作站数据根目录下的 `ua\pki\southbound` 里（`pending`、`trusted`、`blocked` 和 SDK 那个 `rejected`），每份证书是在哪个地址上见到的、尝试次数和各项决定在 `equipment-access.json` 里。它们和朝内的那几个库是分开的，所以一份现场设备的证书绝不可能变成某个应用程序连上工作站自己那个服务器的许可，也绝不会作为一个朝内的请求出现。它们不属于配置[备份](settings-backups)的一部分。

### 这一节不做什么 {#what-this-section-does-not-do}

任何东西都不会被自动批准，而且没有哪个设置能把这一点关掉。这张卡片不配置设备本身（地址、凭据和轮询在 [Connector](connector-devices) 上），不显示这台现场设备的实时数值，也不决定拨号连上这台工作站的那些应用程序的事，那是[客户端访问](connector-opcua-client-access)。
