# 数值转换链

> 每个点位共用的那条带类型的转换链，从原始读数到工程值、两条无效读数策略、越限之后的处理方式、布尔和文本的变换，以及一次写入是怎么把各个环节反算回去的。

每个点位都共用同一条带类型的转换链，把原始读数变成工程值。这条链住在点位自己身上：[点位](connector-tags#the-conversion-card)页面上的“转换”卡片和设备表格显示的是同样那些环节，而落到 Logic、仪表画面、记录器和工作站对外发布的内容里的那个值，就是从最后一个环节出来的那个。对外 OPC UA 节点承载的是原始值。

## 先有原始值 {#raw-first}

这条链是从按点位数据类型强制转换过的**原始值**开始的，不是从驱动自己的那个对象开始的：一个驱动把 3.7 交给一个 Int32 点位，卡片顶上读到的就是 4，而后面每一个环节都从这个 4 往下走。声明的类型根本装不下的读数（声明为数值却来了文本）算作一次空读数，绝不会变成一个凑数的零。各环节按实数计算，到了另一头，工程值取它自己的类型：

| 点位 | 工程值 |
| --- | --- |
| 整数数据类型，量程换算和校准都关闭，或者以斜率 1、截距 0 打开（每个点位新建时都是这样） | 就是那个整数本身，保持点位的数据类型。 |
| 整数数据类型，量程换算或校准改变了数值 | 一个实数（Double），由小数位环节舍入：一个把温度乘以十存放的 Int16 点位，在设备上读作 235，量程换算斜率为 0.1 时读作 23.5。 |
| Float 或 Double | 点位的数据类型，所以一个 Float 点位会舍入到 float 能表示的那个值。 |

数据类型仍然是设备和对外 OPC UA 节点承载的那个类型。打开或关闭量程换算或校准，从那一刻起就会改变工程值的类型：记录器已经记下的值保持原样，通道保持它自己声明的类型，所以整数通道记录四舍五入为最接近整数的值，小数通道保留小数。“转换”卡片以给出的值收尾，并在量程换算和校准都不改变数值的整数类型把限值钳位所用边界的小数舍掉时加以说明。

## 各个环节 {#the-stages}

对数值类型来说，这条链是一串环节，每个环节各有自己的勾选框；关掉的环节把输入原样放过去，而卡片会在每个环节末尾显示当时的值，好让你跟着一次实时读数走完整套算法：

| 环节 | 默认 | 作用 |
| --- | --- | --- |
| **量程换算** | 开 | `scaled = raw × Angular + Linear`。斜率默认 1，截距默认 0。斜率填 0 会在打字的地方就被拒绝（“换算系数填 0 会把每一次读数都变成零。请填写要乘的系数（填 1 表示保持原值）。”），因为它会把每一次读数都归零，而预览和存下来的转换链绝不能互相矛盾。 |
| **校准** | 关 | 作用在换算结果上的第二对线性系数：`calibrated = scaled × Angular + Linear`。它和量程换算分开，好让一次校前/校后修正不去搅动量程那套算法，而且它由自己的权限把守，见[校准](connector-calibration)。斜率填 0 同样被拒绝。 |
| **无效读数** | 始终显示 | 给拿不出可用数字的读数用的两条策略（见下）。它不是勾选框：它的“关”本身就是其中一个答案。 |
| **限值** | 关 | 拿这个值去和**最小**（默认 0）与**最大**（默认 100）比较，并执行选中的越限处理方式（见下）。上下界会被规整，所以下限填得比上限高也不会出错：两者中小的那个就是底。 |
| **小数位** | 开 | 舍入到**小数位**，默认 2，接受 0 到 15。 |

两个线性环节都带一个**两点法**助手，用两个参考点算出那一对系数；它在[校准](connector-calibration#the-two-point-assistant)里一步一步地讲。

## 设备回答了，但没有给出数字 {#the-device-answered-without-a-number}

热电偶断了，某个通道上没接传感器：读数到了，现场设备也在说话，但这次测量是 NaN。只对数值点位有效，而且不管选哪一条，质量都保持良好：

| 策略 | 效果 |
| --- | --- |
| **原样传递**（默认） | 这个 NaN 原封不动地往下走，并且完全跳过限值环节，不管那个环节带的是哪种处理方式：限值判的是数字，而这里没有数字。无穷不是 NaN，仍然会在量程上饱和。 |
| **用一个值代替** | 从这个环节起，声明的那个数字顶上去，并且像任何一次读数一样进入限值、小数位和最后的强制转换。 |

## 设备没有回答 {#the-device-did-not-answer}

一次超时，一根被切断的线缆，一台被关掉的现场设备，一次驱动异常。所有类型都适用，只不过读文本、时间戳、标识符或字节块的点位只提供前两条策略：声明的替代值是一个数字，而这些类型的读数都不是数字。

| 策略 | 效果 |
| --- | --- |
| **显示错误**（默认） | 点位读作空，它的质量转为不可用，于是这次失败一下子在所有地方都浮现出来。 |
| **保持最近一次的有效值** | 最后一次落地的读数继续显示，不设过期，直到现场设备重新应答为止。时间戳仍然指着那次读数，因为那才是这个数字被测出来的时刻。第一次读数落地之前什么都不保持，那时点位读作空。 |
| **用一个值代替** | 声明的那个数字按声明的样子呈现：没有读数可以走完各个环节，所以它不做量程换算、不做校准、也不做舍入，只是被强制转换成工程值的类型。只对数值类型和布尔类型提供这一条，它们的读数可以由一个数字得出；String、DateTime、Guid 和 ByteString 都不提供。 |

替代值从不悄无声息：“实时数值”卡片会打出原因（“最近一次有效值，在设备不响应期间保持。”），实时圆点转成琥珀色，内置 OPC UA 服务器发布这个值时带的是“替代”状态，而不是单纯的 Good，而用 `tag_read` 读这个点位的代理会被告知它是哪一种替代值（`substitution`）。策略要求它待多久，它就一直被发布多久：一台一直不应答的设备，不会让外部客户端在替代值和读取失败之间来回跳，所以那个客户端读到的就是面板显示的东西。设备一应答，读数本身立刻把这个点收回去；把替代值关掉也一样。选择一个替代值就是选择可用性，所以报警和表达式会继续把这个点当作有值来判；这么做的代价是摆在明面上还的，因为诊断日志照样记下那次失败的读取，设备健康状态也照样报告设备离线。替代值回答的是“这个点应该显示什么”，从来不是“现场设备在不在”。声明的替代值落在已启用的限值之外时，会在打字的地方就被提醒：“超出限值（0 到 100）：用来代替读取失败的那个值。”

停止更新的读数算作一次失败的读取。比如设备接受了连接之后就不再应答，在点位的轮询间隔再加 10 秒之内没有新的读数到来时，上面的策略就像读取失败了一样，对所有读这个点位的地方生效：Logic 表达式、仪表画面、记录下来的值和代理。选**显示错误**时，点位读作空，质量转为不可用，而内置 OPC UA 服务器在节点上保留设备最后的值，状态是 `UncertainLastUsableValue` 而不是 Good，这样外部客户端既看到它原有的数字，也知道已经没有东西在更新它；有替代值时，替代值照上面那样呈现和发布。质量为不可用期间，报警照常保持自己的状态。“实时数值”卡片会写出原因（“应用时间戳之后，再没有收到新的读数。点位将此视为一次失败的读取。”），时间戳继续标明最后一次到达的读数，下一次到达的读数让点位恢复。

只写点位没有这里的任何一条策略。工作站从不读它，所以它不会有失败的读取：在工作站写入之前，点位没有数值，也如实显示没有数值，没有替代值，没有琥珀色圆点，也不会有东西代替它发布到 OPC UA 上；面板也不为它画出这一行。点位还在被读取时选定的策略会原样保存，在点位为只写期间被忽略；访问方式一回到只读或读写，它就重新生效。

## 越限的值 {#out-of-range-values}

值落在下限和上限之外时，**限值**的处理方式有：

| 处理方式 | 值变成 | 质量 |
| --- | --- | --- |
| **空值** | 一个空档（没有值）。 | 良好 |
| **钳位**（默认） | 最近的那个界。 | 良好 |
| **报错并置空** | 一个空档。 | 不可用（越限故障） |
| **报错并钳位** | 最近的那个界。 | 不可用（越限故障） |
| **NaN** | NaN。 | 良好。只对 Float 和 Double 点位提供，因为只有这两种类型表示得了它；它让一个 Process 通道能把这个无效值分门别类。整数点位上被一次代理写入或一次还原的备份留下的 NaN 处理方式，会退化成“空值”那个空档。 |

越限故障会并进点位的实际质量，所以实时圆点、属性表达式和报警都直接看到这个越限的值是不可用的，不必再接什么线，“转换”卡片也会打出“数值 130 超出了限值（0 到 100）。”不过它终究是一个过程状况，而不是连接问题，[设备健康状态](connector-health)不会因此把设备算成离线。

## 布尔、文本和其他类型 {#boolean-string-and-other-types}

非数值类型在同一张“转换”卡片里有自己的变换：

- **布尔值**：**取反**报告原始值的逻辑非。
- **文本**：**子串起始位置**（从 0 起）和**长度（留空为到末尾）**从原始字符串里切一个窗口出来；长度留空就一直切到末尾，越界的边界会被钳位而不是报错。
- 其余每一种类型（DateTime、Guid、ByteString）都原样直通。它们的“转换”卡片里只有无效读数环节，用来选设备没有回答时的策略；只写点位则不显示这张卡片。

“显示错误”和“保持最近一次的有效值”对每一种类型都适用；“用一个值代替”只对数值点位和布尔点位提供，所以 String、DateTime、Guid 和 ByteString 的点位不列它。这些类型的点位上如果已经存着早先的版本可能存下的“用一个值代替”，它按**显示错误**执行；下一次保存这个点位时（不论是在面板上还是由代理保存），存进去的就换成显示错误。NaN 那条策略只对数值点位适用。

## 写入把转换链反过来走 {#writes-reverse-the-pipeline}

“实时数值”卡片上的写入字段接收一个工程值，并按顺序把那些已启用、可反算的环节反过来走一遍：限值环节启用时先按下限和上限钳位（写入永远是钳位，绝不丢弃，不管越限处理方式是哪一种），再反校准（`(value − Linear) ÷ Angular`），再照样反换算，最后强制转换成点位的类型。量程换算或校准会改变数值的整数点位可以写入小数：在量程换算斜率为 0.1 的 Int16 点位上写入 123.4，写下去的是 1234。没有量程换算和校准时，向整数点位写入小数会四舍五入到最近的整数，一半时远离零（2.5 写下去的是 3）。舍入是有损的，不做反算。布尔输入会重新应用一次取反。文本的子串只影响显示，所以写文本时是把整个值原样发出去；DateTime、Guid 和 ByteString 直通。外部 OPC UA 客户端写到对外节点上的值本来就是原始值，这一整套一个都不经过。
