# Generic serial and TCP

> 通过串行线路、TCP 或 UDP 接入说自有文本或字节协议的现场设备，例如称重仪表和条码阅读器：它们的消息如何结束、如何校验，以及每个点位如何从中取值。

Generic serial and TCP 驱动读写那些说自有协议的现场设备，协议由文本或简单字节组成，没有哪个标准协议适用：称重仪表、条码或 RFID 阅读器、老式仪表、标签打印机。这类现场设备无法被问出自己是什么，所以不做任何发现。你要告诉设备，它发来的消息如何结束、校验和如何核对；还要告诉每个点位，它如何得到数值、哪些消息属于它、消息中的哪些字符是数值。

设备通过以下三种方式之一到达现场设备，在创建时选定：

| 传输方式 | 设备如何到达 | 它的点位能做什么 |
| --- | --- | --- |
| 在串行线路上 | 本驱动的一条串行线路，与线路上的其他设备共用，一次只发一个请求。 | 请求与应答、监听、写入。 |
| **TCP 客户端** | 到现场设备主机和端口的独立连接。 | 请求与应答、监听、写入。 |
| **UDP 监听** | 本工作站上的一个端口，现场设备向它发送数据报。工作站不发送任何东西。 | 只能监听，只读。 |

串行线路上的设备挂在它的线路下面，所以树的这一部分和 [Modbus RTU](driver-modbus-rtu) 一样读作**驱动 → 线路 → 设备 → 点位**。网络设备挂在驱动下面，可以像任何其他驱动的设备一样归入文件夹，即**驱动 → 文件夹 → 设备 → 点位**。设备留在创建时所在的一侧：TCP 和 UDP 可以互换，但设备绝不会在线路和网络之间移动。

## 驱动页面 {#the-drivers-page}

标题栏下面那行写着“串行线路下挂共用一条总线的仪表，网络设备直接挂在驱动下。添加一条线路或一台设备，然后打开它描述其消息。”，页面上下排着两张表：

| 表 | 用什么增加 | 列出什么 |
| --- | --- | --- |
| **串行线路** | 标题右侧的**添加线路**。 | 驱动的线路：**线路**、**物理参数组**（端口 · 波特率 · 格式，不含超时，因为本驱动的线路没有超时）、**设备**、**状态**，以及打开、启用、禁用和删除操作。为空时显示“还没有串行线路。” |
| **设备** | 标题右侧的**添加设备**。它创建一台网络设备，即 **TCP 客户端**。 | 驱动的所有设备，包括线路上的设备，列与任何驱动的[设备表](connector-drivers#the-device-table)相同。为空时显示“还没有设备。” |

标题栏在括号里显示设备数；只要有一条线路，树中的驱动行就会同时计数两层（“1 条线路 · 2 台设备”）。驱动的命令栏上没有发现。右键单击驱动行会提供**添加设备**、**添加线路**、**新建文件夹**和**刷新**，一条分隔线之后是**移除驱动**；在栏底的“新建…”菜单里，本驱动那一项提供**新建设备**、**新建串行线路**和**新建文件夹**，然后是它的线路、文件夹和设备。驱动添加之后就出现在树里的“驱动”文件夹下面，不论有没有线路或设备。

## 串行线路 {#serial-line}

本驱动的线路就是[串行线路](connector-lines)中描述的那种线路，只是字段更少。它的页面写着“一条物理串行总线：其上仪表共用的端口设置，一次只发一个请求。”，“物理线路”卡片包含：

| 字段 | 含义 | 默认值 |
| --- | --- | --- |
| **名称** | 总线的易读名称，显示在树中和线路上的每台设备上。 | “新建线路” |
| **串行端口** | Windows 的 COM 端口，物理、虚拟或重定向的都可以，手动输入或从 Windows 报告的端口中挑选（**刷新**重新读取）。一个端口只属于一条线路，无论线路属于哪个驱动。 | 第一个空闲端口 |
| **波特率** | 总线的速度，由线路上的所有设备共用。 | 9600 |
| **串行格式** | 数据位、校验位和停止位：`8N1`、`8N2`、`8E1`、`8E2`、`8O1`、`8O2`、`7N2`、`7E1`、`7E2` 或 `7O1`。十种全部可选，因为本驱动不要求 8 个数据位。 | `8E1` |

在这里创建的线路一开始是禁用的，好让你在端口打开之前填好端口设置。端口、波特率和格式只能在线路禁用时更改，名称随时可改。本驱动的线路没有传输模式、响应超时、最小事务间隔和自动静默：线路上的每台设备在自己的面板中说明其消息如何结束、等待应答多久，线路下面的“设备”卡片也这样写：“这条线路上的所有设备共用它的端口设置。每个设备在自己的面板中说明其消息如何结束、等待应答多久。”它没有“发现从站”卡片，设备表也没有单元 ID 列，因为仪表靠请求的内容区分，而不是靠地址。

在线路上，或在它的右键菜单中点**添加设备**，会在这条线路上创建一台本驱动的设备。线路永远属于创建它时所在的驱动：Modbus RTU 设备和本驱动的设备绝不共用一条线路，把设备指到另一个驱动线路上的智能体会被拒绝。线路的状态（已关闭、空闲、正在连接…、在线、端口故障）与所有串行线路相同，见[启用与健康状态](connector-health#serial-line-states)。

在线路上，驱动为线路上的所有设备只打开一次端口，一次只发一个请求，等到它的应答后再发下一个。请求之间到达的内容会交给线路上的每台设备，各自按自己的规则切分成消息。端口出故障时，线路会为它的所有设备重新打开端口，故障持续期间两次尝试之间的等待越来越长。

## 设备 {#device}

### 连接 {#connection}

| 字段 | 含义 | 默认值 |
| --- | --- | --- |
| **传输方式** | 只用于网络设备。**TCP 客户端**：“工作站连接到设备的主机和端口。”**UDP 监听**：“工作站在本地端口上接收设备发送的内容，自身不发送任何东西。”两者可以互换；线路上的设备在这里显示它的线路。 | **TCP 客户端** |
| **主机** | TCP：现场设备的主机名或 IP 地址。可以粘贴完整地址，前面带 `tcp://`、主机后面带端口。 | 空 |
| **端口** | TCP：现场设备应答的端口，1 到 65535。这类现场设备没有标准端口，所以新设备没有端口。 | 空 |
| **仅接受来自** | UDP 时代替主机出现：“只读取来自这个地址的数据报。留空：接受任何发送方。” | 空 |
| **本地端口** | UDP 时代替端口出现：“设备发往的本工作站端口。Windows 防火墙必须放行它。” | 空 |
| **串行线路 · 固定** | 线路上的设备：线路名称，下面是它的端口、波特率和格式，只读。 | 添加时所在的线路 |

新的 TCP 设备显示“尚未填写主机”，在有了主机和端口之前不会连接。有主机却没有端口的设备，以及没有本地端口的 UDP 设备，都无法保存：“请填写端口：TCP 时是设备的端口，UDP 时是用于接收的本地端口。”线路上的设备没有主机、端口、文件夹，也没有单元 ID。

标题栏显示的 Base URI 是运行时地址：`generic://tcp/<host>:<port>`、`generic://udp/<local port>`（填了“仅接受来自”时带 `from=<sender>`）或 `generic://serial/<line id>/<port>/<baud>/<format>`，后面跟着消息如何结束和校验（`end`、`start`、`terminator`、`length`、`gap`、`timeout`、`checksum` 等）。其中携带的是字节本身，而不是设备的输入方式。线路上设备的副本留在那条线路上；网络设备的副本落在驱动本身这一层。

### 消息卡片 {#the-messages-card}

在连接卡片下面，**消息**卡片规定字节如何输入、消息如何结束。

| 字段 | 含义 | 默认值 |
| --- | --- | --- |
| **输入方式** | 起始标记、结束符，以及每个点位的请求、过滤条件、分隔符、写入消息和预期回复如何书写。**带转义的文本**：可打印字符照原样写，其他字节用 `\r`、`\n`、`\t`、`\\` 和 `\xNN`。**十六进制字节**：每个字节两位数字，例如 `02 30 31 0D`。切换时会改写设备及其点位的每个字节字段，让它们继续发送相同的字节；只要其中有一个不是有效字节，切换就会被拒绝：“表示法未更改：这台设备或其点位有一个字节字段在当前表示法下无效。请先改正。” | **带转义的文本** |
| **消息结束方式** | **结束符**、**固定长度**或**静默间隔**。走 UDP 时，无论这里怎么选，一个数据报就是一条消息。 | **结束符** |
| **结束符** | 选结束符时：结束每条消息的字节。它们不属于点位读取的内容。 | `\r\n` |
| **消息长度（字节）** | 选固定长度时，1 到 1024：“每条消息都是这么多字节，包括起始标记和校验和。” | |
| **静默间隔（ms）** | 选静默间隔时，5 到 10000：“这段时间内没有收到任何内容，即视为消息结束。” | 50 |
| **起始标记** | 可选。每条消息开头的字节，例如 `\x02`。它之前的字节会被丢弃，这也让从消息中途开始接收的数据流重新对齐；它本身计为消息的最前面几个字符。 | 空 |
| **响应超时（ms）** | 50 到 60000：“请求等待应答、写入等待预期回复的时间。” | 1000 |
| **无消息的最长时间（秒）** | 0 到 86400，0 表示从不：“在这段时间内收不到任何内容时，监听的点位不再算作读数。0：从不过期，适合条码阅读器这类只偶尔发送的设备。对秤这类持续发送的设备，请设置一个值。” | 0 |

所选结束方式不读取的字段不会显示，并保留其值，以备换回那种结束方式时使用。设备无法使用的值不会保存，记录条会说明原因，例如“请填写消息结尾的结束符，例如 \r\n。”或“消息长度必须在 1 到 1024 字节之间。”

### 校验和卡片 {#the-checksum-card}

| 字段 | 含义 | 默认值 |
| --- | --- | --- |
| **算法** | 每条消息携带的校验和：**无**、**求和（8 位）**、**和的二进制补码（LRC）**、**7 位和的二进制补码（Toledo）**、**异或（8 位）**、**CRC-16/MODBUS** 或 **CRC-16/CCITT**。“校验和不符的消息会被丢弃并在诊断日志中计数，绝不当作数值读取。” | **无** |
| **写法** | **十六进制文本**是用十六进制数字写出的校验和，**原始字节**是校验和本身的字节。 | **十六进制文本** |
| **位置** | **结尾之前**：结束符之前的最后几个字节，或消息的最后几个字节。**结束符之后**：紧跟在结束符后面，此时结束符也在校验范围内；只有消息以结束符结束时才提供。 | **结尾之前** |
| **开头不计的字节数** | 0 到 255：“消息开头不在校验和范围内的字节，例如 STX 或 $。” | 0 |
| **紧挨其前不计的字节数** | 0 到 255：“紧挨校验和之前、不在其范围内的字节，例如 NMEA 语句中的 *。点位也不读取它们。” | 0 |

除算法外的字段只在选定校验和后才显示。校验和覆盖从消息第一个字节到校验和前一个字节的范围，不计的字节除外。校验和不符的消息绝不会被当作数值读取：它会被丢弃，诊断日志在设备名下记录，每分钟最多一次。校验和只做核对：请求和写入完全按输入的样子发出，所以需要校验和的请求要在自己的字节里带上它。

| 算法 | 常见用途 |
| --- | --- |
| 求和（8 位） | 仪表。 |
| 和的二进制补码（LRC） | 仪表、Modbus ASCII 风格的帧。 |
| 7 位和的二进制补码（Toledo） | Toledo 仪表的连续输出，在 CR 之后以原始字节出现。 |
| 异或（8 位） | NMEA 0183 语句（`$…*hh`）、仪表、RFID 阅读器。 |
| CRC-16/MODBUS | 二进制帧；以原始字节写时低字节在前。 |
| CRC-16/CCITT | 二进制帧（CCITT-FALSE 变体）；以原始字节写时高字节在前。 |

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

本驱动的点位没有要输入的地址：它的**信号源**卡片用下面的字段询问它如何得到数值。字节按设备的输入方式输入。

| 字段 | 含义 | 默认值 |
| --- | --- | --- |
| **取值方式** | **请求与应答**：“工作站按点位的轮询间隔发送请求，并从应答中读取数值。”**监听**：“设备自行发送，点位保留其过滤条件接受的最后一条消息。”UDP 设备只提供监听：“这台设备通过 UDP 只接收不发送：它的点位只能监听，并且只读。” | **请求与应答** |
| **请求** | 仅用于请求与应答：“工作站按点位的轮询间隔发送的内容，按设备规定的方式输入。在响应超时内被过滤条件接受的第一条消息即为应答。” | 必填 |
| **消息过滤条件** | 哪些消息属于这个点位：**所有消息**、**开头为**（填写开头的字节）或**匹配正则表达式**（在**正则表达式**中填写）。用于请求时：“哪条消息作为请求的应答。其余的交给监听的点位。” | **所有消息** |
| **数值取自** | 如何从消息中切出数值：**整条消息**、**固定位置**（**起始字符**和**长度**）、**分隔符之间的字段**（**分隔符**和**字段序号**）或**模式中的捕获组**（**模式**）。“位置和字段都从 1 开始数。模式必须恰好有一个捕获组，即数值。数字按小数点为句点读取。” | **整条消息** |
| **设备用逗号作小数点** | 仅限 Float 和 Double 点位。读取和写入时都把逗号当作小数点。 | 关 |
| **写入消息** | 用于会被写入的点位，UDP 上没有：“写入时发送的内容，在数值所在位置放 {value}，例如 SP {value}\r；要格式化时写 {value:0.00}。对 Boolean 点位，不含 {value} 的写入消息是一条命令，写入 true 时发送。” | 空 |
| **预期回复** | “可选。设备对写入的回复以什么开头。只有在响应超时内收到这样的回复，写入才算成功。” | 空 |

字符从消息开头算起，包括起始标记；结束符、校验和以及校验和紧挨其前不计的字节都不属于点位读取的内容。模式是 .NET 正则表达式，读取一条消息超过 250 ms 时不读取这条消息。只写的点位没有请求、过滤条件和取值字段；没有写入消息的点位是只读的。

数据类型有 Boolean、Int32、Int64、Float、Double 和 String。切出的文本先按点位的类型读取，再由点位自己的各个环节换算，和所有驱动一样（见[数值转换链](connector-value-pipeline)）：Boolean 读 `1`、`true`、`on`、`yes` 和 `0`、`false`、`off`、`no`；整数读整数（`12.0` 读作 12，`12.5` 被拒绝）；Float 或 Double 读小数；String 照原样取字符。仪表会用空格填充数字，所以数字前后的空格以及符号和数字之间的空格都会忽略。字段无法成立的点位不会保存，点位页面会说明缺少什么，例如“请填写这个点位发送的请求。”或“模式必须恰好有一个包住数值的捕获组。”

只读的线上地址预览显示 `ask` 或 `listen`，后面跟着适用的部分，例如 `listen?extract=position&start=5&length=6`；发送 `01RD` 和 CR、读取应答中第三个逗号分隔字段的点位则是 `ask?send=01RD%0D&extract=field&separator=,&field=3`。

## 读取 {#reading}

| 情形 | 请求与应答的点位 | 监听的点位 |
| --- | --- | --- |
| 数值 | 取自请求之后、在响应超时内被过滤条件接受的第一条已校验消息。 | 取自被过滤条件接受的最后一条已校验消息，时间戳为它的到达时间。 |
| 何时读取 | 按点位的轮询间隔。 | 每当现场设备发送时；点位按轮询间隔显示最后一条消息。 |
| 第一条消息之前 | 不适用。 | 点位等待：没有数值也没有错误，与任何点位在第一次应答之前一样。 |
| 没有应答，或现场设备沉默 | 响应超时内没有应答就是一次读取失败，由点位针对[设备没有应答](connector-value-pipeline#the-device-did-not-answer)的策略决定显示什么。 | 超过设备的**无消息的最长时间（秒）**后，每个监听的点位都读取失败，由各自的策略决定显示什么。TCP 连接断开时，无论这个时间怎么设，它们都会失败。 |
| 点位读不了的消息 | 一次带原因的读取失败：缺少字段、数值超出消息末尾，或类型容纳不了的文本。 | 相同。 |

没有回应任何请求的消息会交给同一设备的监听点位，所以一台设备可以同时有两种点位。过滤条件相同的监听点位共享收到的内容，可以从同一条消息中取不同的字段。

## 写入 {#writing}

写入和其他任何驱动走同一个通道：写入框接受工程值，各个换算环节按相反顺序执行后再发送原始值（见 [Connector](connector)）。

| 写入消息 | 发送什么 | 写入报告什么 |
| --- | --- | --- |
| 含 `{value}` | 输入的字节，`{value}` 的位置放入数值：数字按固定区域格式（点位这样指定时以逗号作小数点），或按 `{value:FORMAT}` 用 .NET 数字格式书写（`W{value:000.0}\r` 把 12.5 发成 `W012.5` 和一个 CR）；Boolean 发成 `1` 或 `0`；文本照原样。 | 发出即完成。设了**预期回复**时，只有在响应超时内收到以它开头的回复才算完成；收到别的回复则以那条回复为原因拒绝写入，什么都没收到则为无应答。 |
| 不含 `{value}`，Boolean 点位 | 一条命令，例如去皮或打印：写入 true 时发送，写入 false 被拒绝。 | 相同。 |
| 无 | 什么都不发：点位只读。 | |

写入和所有驱动一样在设备的队列中排队：在线路上，它们共用线路一次一个请求的轮次；在 TCP 上，使用设备自己的连接。UDP 设备什么都不发送，所以它的点位不能写入。

## 示例：RS-232 上的称重仪表 {#example-a-weighing-indicator-on-rs-232}

采用 Toledo 连续输出格式的仪表每秒多次发送：一个 STX、三个状态字节、六位重量数字、六位皮重数字、一个 CR 和一个校验和字节。在工作站上：

1. 在驱动页面上点**添加线路**；命名为 `Scale line`，选 `COM3`，**波特率**保持 9600，**串行格式**选 `7E1`，然后**启用**这条线路。
2. 在线路上点**添加设备**；命名为 `Scale`。在**消息**卡片上保持**带转义的文本**和**结束符**，把**结束符**设为 `\r`，**起始标记**设为 `\x02`，**无消息的最长时间（秒）**设为 10，这样拔掉电缆就会显示为通信中断。
3. 在**校验和**卡片上选**7 位和的二进制补码（Toledo）**、**原始字节**和**结束符之后**，两个不计字节数都为 0。
4. 点**添加点位**；命名为 `Weight`，Double，只读。在**信号源**卡片上选**监听**、**所有消息**和**固定位置**，**起始字符**为 5，**长度**为 6：STX 是第 1 个字符，三个状态字节是 2 到 4，重量是 5 到 10。
5. 这些数字不带小数点，所以由点位的量程换算环节放上小数点，例如仪表设为两位小数时用系数 0.01。

智能体用三次工具调用完成同样的事：

```
serial_line_upsert driver=GenericSerialTcp name="Scale line" serialPort=COM3 baudRate=9600 serialFormat=7E1
device_upsert driver=GenericSerialTcp name=Scale transport=SerialLine serialLineId=<id> messageEnd=Terminator terminator=\r startMarker=\x02 checksum=SumTwosComplement7Bit checksumFormat=Raw checksumPlacement=AfterEnd silenceTimeoutSeconds=10
tag_upsert device=Scale name=Weight opcUaType=Double tagMode=Listen valueExtraction=FixedPosition valueStart=5 valueLength=6
```

## 示例：TCP 上的条码阅读器 {#example-a-barcode-reader-on-tcp}

带以太网口的条码阅读器每行发送一个条码，而且只在读到条码时发送：

1. 在驱动页面上点**添加设备**；命名为 `Reader`，保持 **TCP 客户端**，**主机**填 192.168.0.40，**端口**填 2001。
2. 在**消息**卡片上保持**带转义的文本**、**结束符** `\r\n`，**无消息的最长时间（秒）**为 0：一个小时什么都不发的阅读器并没有故障。**校验和**卡片保持**无**。
3. 点**添加点位**；命名为 `Code`，String，只读。在**信号源**卡片上选**监听**、**所有消息**和**整条消息**。

用智能体的工具也一样：

```
device_upsert driver=GenericSerialTcp name=Reader transport=TcpClient host=192.168.0.40 port=2001 messageEnd=Terminator terminator=\r\n
tag_upsert device=Reader name=Code opcUaType=String tagMode=Listen valueExtraction=WholeMessage
```

`driver_capabilities` 在 `genericSerialTcp` 下描述本驱动，并附上这两个示例；`serial_line_upsert` 用 `driver=GenericSerialTcp` 创建本驱动的线路；其他工具见[工具，按范围列出](agent-tools)。

## 值得了解的限制 {#limits-worth-knowing}

- 不做任何发现：没有协议可以向这类现场设备询问它是什么、有什么。
- 在帧头中声明自身长度的二进制帧无法读取；消息以结束符、固定长度、静默间隔或数据报结束。
- 校验和只做核对：工作站发送的内容不会被加上校验和。
- 点位从消息中取出一个数值，作为文本按其类型读取；消息中以二进制整数或浮点数打包的数字不会被解码。
- UDP 设备只监听：不发送请求，也不接受写入。
- 设备不能在串行线路和网络之间移动，也不能移到另一条线路：请在它该在的地方新建，再删除旧的。
