# File

> 把其他程序写在本地或网络文件夹中某个文件里的值（试验机的 CSV、数据记录仪的日志、JSON 或 XML 格式的结果）作为点位读取，并为其他程序写入行或整个文件：“文件夹和文件”“文件文本”“写入”三张卡片，点位在每种格式中读取什么，收件文件夹，为每一列或每个字段提供一个点位的扫描，三个完整示例，以及驱动不做的事。

**File** 驱动读取其他程序写入文件的值：试验机在每个样品之后追加一行的 CSV、数据记录仪的日志、JSON 或 XML 格式的结果文件、只含一个批号的文本文件。每个点位读取文件中的一个值，它的值和其他任何点位的值一样送达各处：Logic、仪表画面，以及 [Process](process) 测试台的运行记录。在设备允许时，驱动也为其他程序写入：向文件追加一行，或替换整个文件。

读取是主要用途，只需要三个答案：文件夹、文件和它的格式。文件文本的读法（编码、列分隔符、小数分隔符）在保持识别时从文件本身读出，扫描为每一列或每个字段提供一个点位，每个点位新建时都是只读。属于写入的内容，只有在设备打开写入之后才会出现。

本驱动的一台设备就是一个文件夹以及它在其中读取的文件：同一文件夹中的两个文件是两台设备。文件夹是驱动器加文件夹（`C:\TestMachine\Logs`），或网络共享加其下的文件夹（`\\lab-pc\results`），工作站用运行它的账户的 Windows 权限访问它。设备位于驱动之下，可以归入文件夹，与任何没有串行线路也没有 MQTT Broker 的驱动一样，形成 **驱动 → 文件夹 → 设备 → 点位**。

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

标题下的一行写着“每台设备读取一个文件，或到达某个文件夹的文件。添加设备，填写其文件夹、文件和格式，然后扫描它，把其中的值添加为点位。”对有“配置 Connector”权限的角色，页面上有本驱动自己的扫描范围卡片，里面是扫描要读取的文件（见[设备发现](#discovery)）。它下面是任何驱动都有的**设备**表（见[设备表](connector-drivers#the-device-table)），用标题右侧的**添加设备**增加；为空时显示“还没有设备。”驱动的命令栏上有**发现设备**，在扫描范围卡片填写完整之前呈灰色。本驱动列在**文件和数据库**之下，在每个驱动列表中，这个类别位于 Web 与 API 和测试之间。

## 设备 {#device}

本驱动的设备没有“连接”卡片：**文件夹和文件**卡片占据它的位置，后面是**文件文本**卡片，在读取单个文件的设备上还有**写入**卡片。“设备”卡片的**轮询间隔（ms）**默认为 1000，表示点位多久读取一次文件，与任何驱动一样，每个点位都可以覆盖它。

新设备没有文件夹：显示“尚未填写文件夹”，它的页面写着“在这台设备填写文件夹之前，工作站不会读取它，因此暂时不读取任何数据，也没有错误可显示。在下面的‘文件夹和文件’卡片里填写文件夹，工作站就会开始读取。”在填写文件夹之前不读取任何内容。标题栏显示的 Base URI 就是运行时地址，文件夹和名称以 URI 形式书写，后面是没有设为识别的设置：`file://single?folder=C%3A%5CTestMachine%5CLogs&name=Log_%7Byyyy-MM-dd%7D.csv&format=delimited`，收件文件夹则为 `file://inbox?...`。

### “文件夹和文件”卡片 {#the-folder-and-file-card}

| 字段 | 含义 | 默认值 |
| --- | --- | --- |
| **文件夹路径** | “驱动器加文件夹，或网络共享。访问文件夹时使用运行工作站的账户的 Windows 权限。”框中以 `C:\TestMachine\Logs` 作为提示。 | 空 |
| **文件名** | 在读取单个文件的设备上：“文件夹内的名称。花括号中的部分是工作站的日期和时间：Log_{yyyy-MM-dd}.csv 每天都是一个新文件。”名称可以包含该文件夹的子文件夹（`results\last.csv`）。 | 空 |
| **要读取的文件** | 收件文件夹上的同一个框：“要读取的文件，直接位于文件夹中：* 代表任意字符，? 代表一个字符，例如 Test_*.csv。” | 空 |
| **读取来源** | **单个文件**：“文件每次变化都会重新读取。不断增长的文件从上次读取停止的位置继续读取。”**收件文件夹**：“每个新文件按到达顺序读取一次，然后移到子文件夹 Processed。不会删除任何文件。” | **单个文件** |
| **文件格式** | **CSV 与分列文本**、**自由文本**、**JSON** 或 **XML**：点位在文件中找到其值的方式（见[点位寻址](#tag-addressing)）。 | **CSV 与分列文本** |

带日期部分的名称用花括号书写，里面是 .NET 日期时间格式，按工作站自己的时钟：`Log_{yyyy-MM-dd}.csv` 今天是 `Log_2026-10-03.csv`，过了午夜就是第二天的文件，而 `Data_{yyyy-MM}.txt` 每月更换一次。框下方，卡片显示它现在对应哪个文件：“今天：Log_2026-10-03.csv”。双写的花括号 `{{` 和 `}}` 是名称中的花括号。日期部分从不写冒号，因为文件名不允许冒号：写 `{HH-mm}`，不写 `{HH:mm}`。

设备运行后，卡片会说明它读取的是哪个文件：

| 卡片显示 | 时机 |
| --- | --- |
| “正在读取：Log_2026-10-03.csv” | 正在使用的文件，即设备现在读取的文件。 |
| “正在读取：Log_2026-10-03.csv（尚不存在）” | 文件尚不存在：它的点位等待，设备保持在线。 |
| “此名称由点位写入。重启后，设备会重新读取配置的文件。” | 读取文件名的点位被写入，把设备指向了另一个文件（见[写入](#writing)）。 |
| “收件文件夹中有 3 个文件等待处理” | 收件文件夹中，还有符合其模式、尚待读取的文件（只有一个时为“收件文件夹中有 1 个文件等待处理”）。 |

卡片无法接受的设置会在卡片下方说明，改正之前设备不会保存：

| 你输入 | 卡片显示 |
| --- | --- |
| 不是完整路径的文件夹，例如 `Logs` | “文件夹必须是完整路径：驱动器加文件夹（C:\Test），或网络共享加其下的文件夹（\\server\tests）。” |
| 含有文件夹名称不允许的字符的文件夹，或下面没有文件夹的共享（`\\server`） | “文件夹包含文件夹名称不允许的字符，或者是下面没有文件夹的网络共享。” |
| 没有文件名 | 不会被拒绝：设备照常保存并等待，显示“尚未填写文件名”（收件文件夹显示“尚未填写要读取的文件”），直到填写名称。 |
| 把完整路径作为名称，例如 `C:\Logs\last.csv` | “文件名是文件夹内的名称，而不是完整路径：文件夹应填写在‘文件夹路径’框中。” |
| 含 `..` 的名称，例如 `..\other\last.csv` | “文件名用 ".." 跳出了文件夹。驱动从不离开自己的文件夹。” |
| 含 `:` 或其他文件名不允许的字符的名称，或某一部分以句点或空格结尾 | “文件名包含文件名不允许的字符（例如 : 或 \|），或者某一部分以句点或空格结尾。日期部分不能写入冒号：请使用 {HH-mm}，而不是 {HH:mm}。” |
| Windows 保留给设备的名称，例如 `CON` 或 `COM1` | “Windows 将此名称保留给设备使用（CON、NUL、COM1 等）。请另选一个。” |
| 读取单个文件的设备上出现 `*` 或 `?` | “只有收件文件夹的模式可以使用 * 或 ?。请指定一个文件的名称，或改为从收件文件夹读取。” |
| 收件文件夹的模式中含子文件夹 | “收件文件夹就是文件夹本身：请把子文件夹填在‘文件夹路径’框中，这里只保留模式。” |
| 日期部分没有闭合，或不是日期格式 | “名称中的日期部分没有闭合，或不是日期格式。请把它写在花括号中，例如 Log_{yyyy-MM-dd}.csv。” |

保存时遵守相同的规则，拒绝时操作记录条会这样说：“设备‘Tensile tester’有驱动无法使用的文件设置。请打开它并修正面板标出的字段。”

### “文件文本”卡片 {#the-text-of-the-file-card}

文件的字符、列和数字如何读取。每项设置一开始都是**从文件识别**，驱动从正在使用的文件中读出它。保持这样的设置下方，在设备读过文件之后，会有一行说明它发现了什么，例如“在文件中发现：UTF-8”。对于前几行看不出来的文件，可以选择任一设置把它固定下来。

| 字段 | 显示条件 | 选项 | 默认值 |
| --- | --- | --- | --- |
| **编码** | 始终 | **从文件识别**、`UTF-8`、`Windows-1252 (ANSI)`（旧 Windows 程序的编码） | **从文件识别** |
| **列分隔符** | CSV | **从文件识别**、**分号 (;)**、**逗号 (,)**、**制表符** | **从文件识别** |
| **小数分隔符** | 始终 | **从文件识别**、**小数点 (12.5)**、**小数逗号 (12,5)** | **从文件识别** |
| **标题行** | CSV | 0 到 1000：“包含列名的行，从 1 开始计数；文件没有标题行时为 0。”它之前的行被跳过。 | 1 |

| 设置 | 识别时读取的内容 |
| --- | --- |
| 编码 | 文件开头的字节顺序标记，它优先于任何选择；否则，字节是有效的 UTF-8 时为 UTF-8；否则为 Windows-1252。 |
| 列分隔符 | 制表符、分号和逗号中，第一个能把前几行分成相同列数的那个；第一行不含其中任何一个时，文件只有一列。 |
| 小数分隔符 | 文件中数字之间带逗号的比带点的多时为逗号；否则为点。JSON 数字总是带点；该设置适用于以文本书写的数字。 |

### “写入”卡片 {#the-writing-card}

读取单个文件的设备有**写入**卡片；收件文件夹只被读取，没有这张卡片。

| 字段 | 显示条件 | 含义 | 默认值 |
| --- | --- | --- | --- |
| **允许写入** | 始终 | 关闭时：“该设备只读取。要追加行、替换文件或在工作站运行时切换到另一个文件，请打开写入。”打开时：“此设备的点位可以写入文件。在访问方式另行设置之前，每个点位仍然只读取。” | 关闭 |
| **标题文本** | 写入打开，CSV 或自由文本 | “写入操作新建文件时首先写入的内容，例如 Time;Pressure。”一行，不含换行。 | 空 |
| **行尾** | 写入打开 | **从文件识别**（文件自己的行尾，新文件为 CR LF）、**CR LF（与 Windows 相同）** 或 **LF**。 | **从文件识别** |

只有在设备没有任何点位写入时，才能关闭写入。仍有点位写入时，开关保持显示并呈灰色，并说明原因：“此设备有 2 个点位会写入文件。关闭写入前，请先把它们设为只读。”（只有一个点位时为“此设备有一个点位会写入文件。关闭写入前，请先把它设为只读。”）。

### 在线、等待和离线 {#online-waiting-and-offline}

| 情况 | 设备及其点位的行为 |
| --- | --- |
| 没有填写文件夹 | 设备等待：“尚未填写文件夹”，灰点，不读取任何内容，也没有错误。 |
| 已填写文件夹，没有文件名 | 设备等待：“尚未填写文件名”，灰点，不读取任何内容，也没有错误；它的页面写着“在这台设备填写文件名之前，工作站不会读取它，因此暂时不读取任何数据，也没有错误可显示。在下面的‘文件夹和文件’卡片里填写文件名，工作站就会开始读取。” |
| 收件文件夹，已填写文件夹，没有要读取的文件 | 设备等待：“尚未填写要读取的文件”，灰点，不读取任何内容，也没有错误；它的页面写着“在这台设备填写要读取的文件之前，工作站不会读取它，因此暂时不读取任何数据，也没有错误可显示。在下面的‘文件夹和文件’卡片的‘要读取的文件’里填写（例如 Test_*.csv），工作站就会开始读取。” |
| 无法访问文件夹（共享已停止、驱动器不见了、账户没有访问权限） | 设备离线，其点位读取失败；最多每秒重新查看一次文件夹，文件夹恢复后设备立即连接。 |
| 能访问文件夹，但文件尚不存在 | 设备保持在线。读取值的点位没有读数并等待；**文件是否存在**读为假。 |
| 另一个程序占用文件且不共享 | 这一次没有读数；下一次读取再试。点位保留上一次读数，读数比其轮询间隔加 10 秒更旧时，计为读取失败，与任何驱动相同。 |
| 某个值不存在，或放不进点位的类型 | 该点位读取失败并附原因；设备继续读取其他点位。 |

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

### 点位读取什么 {#what-a-tag-reads}

本驱动的点位没有自己的地址框：它的**信号源**卡片询问它读取什么，对于值，还询问值在设备格式中的位置。

| **读取内容** | 点位显示的内容 | 数据类型 |
| --- | --- | --- |
| **数值** | 文件中的一个值，按下面设备格式的字段找到。 | Boolean、Int32、Int64、Float、Double 或 String；新点位为 Double |
| **整个文件** | 全部内容，去掉末尾的行尾：只含一个批号或一个状态的文件。最多 4 MiB。 | 同上；新点位为 String |
| **文件是否存在** | 正在使用的文件现在是否存在。 | Boolean |
| **文件大小** | 文件的字节数。 | Int64 |
| **最后修改** | 文件最后一次被写入的时间：看它变化，Logic 就知道有新数据到达。 | DateTime |
| **文件名** | 正在使用的文件在文件夹内的名称。写入时，它指定另一个文件（见[写入](#writing)）。 | String |
| **行数** | “统计文件中的所有行，包括标题行和空行。” | Int64 |

信号源不能读取为的类型会被拒绝，并列出可以读取为的类型：“请从可读取为的数据类型中选择一个：Int64。”选择另一个信号源会带来它自己的类型。从文件中取出的文本，按点位的类型读取，方式与 [Generic serial and TCP 驱动](driver-generic-serial-tcp#tag-addressing)读取消息相同，并使用文件的小数分隔符：Boolean 读取 `1`、`true`、`on` 或 `yes` 和 `0`、`false`、`off` 或 `no`，整数读取整数，String 按原样取字符，所以像 `0412` 这样的代码只有作为 String 才保留开头的零。

每个点位新建时都是**只读**，在不允许写入的设备上，**访问**字段不提供其他选项。设备允许写入后，读取**数值**或**整个文件**的点位可以设为**读写**或**只写**，读取**文件名**的点位可以设为**读写**；文件信息（是否存在、大小、最后修改、行数）永远不会被写入：“文件信息无法写入。请让点位保持只读。”

### CSV 与分列文本 {#csv-and-text-in-columns}

| 字段 | 显示条件 | 含义 | 默认值 |
| --- | --- | --- | --- |
| **列** | 读取数值 | “标题行中的列名，或从 1 开始计数的列号。”只含数字的文本是列号；名称不区分大小写，也忽略前后的空格。在没有标题行的文件上：“从 1 开始计数的列号。该文件没有标题行，因此列没有名称。” | 空 |
| **要读取的行** | 读取数值 | **最后**、**按编号**或**按内容**。“行从标题行之后开始计数。‘按内容’读取键列包含键值的最后一行。” | **最后** |
| **行号** | 按编号 | 数据行，从标题行之后的 1 开始计数。 | 空 |
| **键列** | 按内容 | 用其值选择行的列，按名称或列号。 | 空 |
| **键值** | 按内容 | 要读取的行中键列所含的值，例如 `0412`。 | 空 |

用双引号括起来的字段可以包含分隔符；包含换行的字段不会被读取。字段无法工作的点位不会保存，它的页面会说明缺少什么：

| 你留空或输入 | 点位页面显示 |
| --- | --- |
| 没有列 | “请输入列：使用标题行中的列名或列号。” |
| 第 0 列 | “列从 1 开始计数。” |
| 在没有标题行的文件上按名称指定列 | “该文件没有标题行，因此列需用列号指定。” |
| 按编号，但没有行号 | “请输入从 1 开始计数的编号。” |
| 按内容，但没有键列 | “请输入键列：用该列的值选择行。” |
| 按内容，但没有键值 | “请输入该行包含的内容。” |

只读的**源地址（总线）**一行显示驱动收到的内容：`column?name=Force` 表示最后一行的 Force，`column?number=3&row=number&rowNumber=5` 表示第五行的第三列，`column?name=Force&row=match&key=Sample&equals=0412` 表示样品 0412 的那一行。

### 自由文本 {#free-text}

| 字段 | 显示条件 | 含义 | 默认值 |
| --- | --- | --- | --- |
| **要读取的行** | 读取数值 | **最后**（最后一个非空行）、**按编号**或**按内容**。“行从文件开头开始计数。‘按内容’读取包含该文本的最后一行。” | **最后** |
| **行号** | 按编号 | 行号，从文件开头的 1 开始计数。 | 空 |
| **行中包含** | 按内容 | 要读取的行中包含的一段文本，例如 `Result`。 | 空 |
| **数值取自** | 读取数值 | “与 Generic serial and TCP 驱动的选项相同：整行、固定位置、分隔符之间的字段，或正则表达式。”选项与该驱动相同，只是第一项按行命名：**整行**、**固定位置**（**起始字符**和**长度**）、**分隔符之间的字段**（**分隔符**和**字段序号**）和**模式中的捕获组**（**模式**）。 | **整行** |

位置和字段从 1 开始计数，模式是 .NET 正则表达式，恰好有一个包住数值的捕获组，读取时间超过 250 ms 的行不会被读取。拒绝信息与通用驱动本身相同，例如“请填写数值的第一个字符位置，从 1 开始数。”或“模式必须恰好有一个包住数值的捕获组。”总线地址写作 `line` 加上适用的部分，例如 `line?row=match&contains=Result&extract=regex&pattern=...`。

### JSON 与 XML {#json-and-xml}

| 字段 | 格式 | 含义 |
| --- | --- | --- |
| **JSON 路径** | JSON | “成员之间用点分隔，位置写在方括号中并从 0 开始，例如 result.force 或 items[0].value。”名称中含点的成员写作 `['flow.rate']`。 |
| **XML 路径** | XML | “从根开始的元素用斜杠分隔，元素重复处写从 1 开始的位置，最后是属性，例如 Result/Force 或 Result/@unit。”名称按本地名称匹配，忽略命名空间前缀。 |

JSON 数字、`true` 或 `false` 按原样取用，字符串按点位的类型读取；对象或列表只能作为 String 读取，即它的 JSON。JSON 或 XML 文档在变化时整体读取，最多 4 MiB，不读取任何 DTD。总线地址为 `json?path=result.force` 或 `xml?path=Result%2FForce`。

| 你留空或输入 | 点位页面显示 |
| --- | --- |
| 没有路径 | “请输入值的路径。要读取整个文件，请选择‘整个文件’。” |
| 无法解析的 JSON 路径 | “这不是 JSON 路径：成员用点分隔，位置写在方括号中，例如 result.items[0].value。” |
| 无法解析的 XML 路径 | “这不是 XML 路径：元素用斜杠分隔，位置写成 Item[2] 这样，最后是属性，例如 @unit。” |

## 读取 {#reading}

每次读取时，驱动向 Windows 询问文件的大小和时间，这在网络共享上同样有效，而 Windows 的变更通知在那里无效；只有其中之一变化时才打开文件。它总是以共享读、写和删除的方式打开文件，因此写入该文件的程序从不会被阻塞。

| 文件 | 读取的做法 |
| --- | --- |
| 变大了，并且开头和结尾仍在原处 | 只读取新增的部分，从不重新读取整个文件：几兆字节的日志，代价只是它的新行。 |
| 变短了、被重写或被重新创建 | 从头重新读取。 |
| 以尚未被行尾结束的一行结尾 | 这一行等待，确保从不读到写了一半的值；文件 2 秒内没有变化后，它作为最后一行读取。 |
| JSON、XML，或读取整个文件的点位 | 变化时整体读取，最多 4 MiB；无法解析且变化不到 2 秒的文档视为仍在写入，点位等待。 |
| 有超过 1 MiB 的行 | 不是按行的文本：它的点位读取失败。 |

每个读数的时间戳是文件的最后修改时间，而不是读取的时刻，所以运行记录会把样品放在另一个程序写入它的时刻。

## 收件文件夹 {#the-inbox-folder}

**读取来源**设为**收件文件夹**，用于每个样品写一个文件的设备（`Test_0412.json`、`Test_0413.json`……）：每个符合模式的新文件按到达顺序读取一次。

| 步骤 | 发生的事 |
| --- | --- |
| 文件到达 | 文件的大小和时间保持 1 秒不变，并且没有程序为写入而打开它时，才会被取用。 |
| 先读哪个 | 先创建的，其次先写入的，再按名称。同时到达的两个文件分两轮读取：不会跳过任何一个。 |
| 它的读数 | 设备的每个点位都读取这个文件，该文件就是一个读数，时间戳为文件的时间：运行记录会记录每一个。**文件名**说明这些值来自哪个文件。 |
| 每个点位都读过之后 | 文件被移到子文件夹 `Processed`，该子文件夹由驱动创建，名称在每种语言中都相同，因为其他程序可能会查找它；那里已有同名文件时，改名为 `name (2).json`。在下一个文件之前，它的值仍是点位的读数。 |
| 移动失败 | 每次读取时重试；在此期间不会再次读取该文件。 |
| 移动前工作站重启 | 文件会被再次读取：宁可一个读数计两次，也不丢失一个。 |

从不删除任何内容：子文件夹 `Processed` 会一直增长，直到有人清空它。收件文件夹从不被写入。

## 写入 {#writing}

在读取单个文件且允许写入的设备上，读取**数值**或**整个文件**、访问方式包含写的点位，会说明一次写入做什么：

| 字段 | 显示条件 | 含义 | 默认值 |
| --- | --- | --- | --- |
| **写入方式** | 点位会写入 | **追加一行**：“在文件末尾追加一行。每次写入都有效，不会因为更新的写入而丢弃。”**替换文件**：“通过临时文件用该值替换整个文件，因此不会有程序读到写了一半的文件。” | **追加一行** |
| **行模板** | 追加一行 | “留空时，写入的值就是整行。{value} 表示值，{datetime} 表示写入时刻，需要时在冒号后加格式，例如 {value:0.00}。”框中以 `{datetime};{value}` 作为提示。 | 空 |

| 模板 | 写入 12.5 时追加 |
| --- | --- |
| 空 | `12.5` |
| `{datetime};{value}` | `2026-10-03 14:05:12;12.5` |
| `{datetime:dd/MM/yyyy HH:mm};{value:0.00}` | `03/10/2026 14:05;12.50` |

`{value:FORMAT}` 接受 .NET 数字格式，`{datetime:FORMAT}` 接受 .NET 日期时间格式，按工作站的时钟，未给出时为 `yyyy-MM-dd HH:mm:ss`。数字用文件的小数分隔符书写，所以同一个模板在使用逗号的文件中写成 `12,50`。Boolean 点位的模板可以省略 `{value}`：写入真时会追加该行，就像一条命令。驱动无法填充的模板会在点位页面上被拒绝：

| 你输入 | 点位页面显示 |
| --- | --- |
| 既不是 `{value}` 也不是 `{datetime}` 的部分，或未闭合的花括号 | “模板可使用 {value} 和 {datetime}，需要时在冒号后加格式。{{ 和 }} 表示行中的花括号。” |
| 不是 Boolean 的点位上没有 `{value}` | “模板中没有 {value}，因此写入的值永远不会进入文件。只有布尔型点位可以省略它：写入真时会追加该行。” |
| 换行 | “模板只能是一行，不能包含换行。” |
| 在不允许写入的设备上会写入的点位 | “该设备不允许写入。请在设备中打开写入，或让点位保持只读。” |

| 一次写入 | 如何进行 |
| --- | --- |
| 追加一行 | 这一行一次写完，并在写入计为完成之前刷新到磁盘。写入新建的文件先写入**标题文本**；最后一行没有行尾的文件，先补上一个行尾，使这一行完整。含换行的值会被拒绝。每次写入都有效：仍在设备队列中等待的写入，从不会因为发往同一点位的更新写入而被丢弃，与命令从不被丢弃一样。 |
| 替换文件 | 值写入文件旁边的临时文件并刷新到磁盘，然后一步取代原文件。仍在等待的较新值会取代较旧的值，与任何设定值一样。 |
| 文件名 | 读取**文件名**、设为**读写**的点位，写入另一个名称后，设备从此改为读写同一文件夹中的那个文件：“在此写入一个名称后，设备从此读写同一文件夹中的该文件。文件夹之外的名称会被拒绝。”完整路径、`..`、`:` 或 Windows 保留的名称会被拒绝。设备重新连接时（包括重启），它会重新读取配置的文件。 |
| 文本 | 使用文件自身的编码、行尾和小数分隔符，在设备保持识别的地方从文件中读出。新文件为不带字节顺序标记的 UTF-8，使用 CR LF 和小数点，除非设备另有设置。 |
| 应答 | 完成，或附原因拒绝：设备不允许写入、账户没有权限、文件已标记为只读（清除该属性后，下一次写入即可完成）、磁盘已满，或文件在另一个程序中打开。只要有任何程序打开该文件，替换文件就会被拒绝；追加一行只在某个程序不共享地占用该文件时被拒绝。该程序关闭文件后，下一次写入即可完成。 |

名称所指向的文件夹和子文件夹，会在写入需要时创建，并且总在设备的文件夹之内。

## 设备发现 {#discovery}

### “扫描范围”卡片 {#the-scan-scope-card}

对有“配置 Connector”权限的角色，驱动页面上有它的**扫描范围**卡片，卡片下方的一行是“读取一个文件，为找到的每个列或字段以及文件信息各提供一个点位。请选择设备以读取其文件，或输入文件夹、文件和格式。”

| 字段 | 含义 | 默认值 |
| --- | --- | --- |
| **要扫描的文件** | **在此输入的文件**，或按名称选择本驱动的一台设备。设备的文件按设备保存的设置读取，设备尚待保存的修改会先保存。 | **在此输入的文件** |
| **文件夹路径** | 输入的文件：文件夹，与设备上相同。 | 空 |
| **文件名** 或 **要读取的文件** | 输入的文件：名称，或收件文件夹的模式。 | 空 |
| **读取来源** | 输入的文件：**单个文件**或**收件文件夹**。 | **单个文件** |
| **文件格式** | 输入的文件：格式。输入的文件的所有文本设置都会被识别，包括标题行：第一行的字段都不是数字而下一行有数字时，第一行就是标题行。 | **CSV 与分列文本** |

范围仍缺少的内容会以红字显示在卡片下方，范围完整之前，发现设备一直呈灰色：

| 范围 | 卡片显示 |
| --- | --- |
| 输入的文件没有文件夹 | “请输入文件所在的文件夹。” |
| 输入的文件夹不是完整路径 | “‘Logs’不是本工作站可以读取的文件夹。请填写完整路径，例如 C:\Test 或 \\server\tests。” |
| 输入的名称不是该文件夹中的文件 | “‘..\last.csv’不是文件夹内某个文件的名称。” |
| 已不存在的设备 | “所选的扫描设备已不存在。请另选一个要扫描的文件。” |
| 没有文件夹或文件的设备 | “设备‘Tensile tester’尚未设置文件夹或文件，或有驱动无法使用的设置。请先填写其‘文件夹和文件’卡片。” |

要扫描的设备有无法保存的修改时，扫描不会开始：“设备发现没有开始，因为要扫描的设备的修改无法保存。”

### 扫描提供的内容 {#what-the-scan-offers}

扫描读取单文件设备现在读取的文件，或收件文件夹中最新的文件（扫描不移动它）；文件不存在、文件夹无法访问、文档无法解析，都会使扫描附原因结束。它提供一个候选项，把找到的值作为候选点位，全部为**只读**：

| 格式 | 候选点位 |
| --- | --- |
| CSV | 每列一个，读取最后一行，以标题命名（标题没有为它命名或命名重复时用列号），类型按其前几行的值确定：所有值都是数字时为 Double，都是 true 或 false 时为 Boolean，否则为 String。开头带零的代码，例如 `0412`，仍是 String。 |
| 自由文本 | 最后一行，String。 |
| JSON、XML | 每个值一个，按其路径命名和寻址，最多 500 个；JSON 列表提供前 20 项，遍历深入 8 层。 |
| 每种格式 | 文件信息：**文件是否存在**、**文件大小**、**最后修改**、**文件名**和**行数**。 |

一次扫描最多提供 500 个值，CSV 文件的列也算在内；在此截断的扫描会在结束扫描的那一行和扫描结果上方显示“result.json 的扫描提前停止。一次扫描最多提供 500 个值：请按列、JSON 路径或 XML 路径将其余的值添加为点位。”

输入的文件的候选项以文件命名（`run.csv` 为 `run`），名称带日期部分或是收件文件夹时，以文件夹命名。文件夹、名称和**读取来源**都已被本驱动某台设备读取的候选项（被扫描的设备，或另一台相同的设备），标为**已经配置过**：它的卡片只列出该设备尚无点位读取的值，并已勾选，**添加所选点位**位于**打开**之前，把勾选的值添加到该设备，均为只读。扫描的其余行为见[设备发现](connector-discovery)。

设备自己的页面不经过驱动页面也能做同样的事：“点位”卡片标题栏里的**发现点位**按设备保存的样子读它的文件，只列出它还没有点位的值（见[发现一台设备的点位](connector-discovery#discovering-a-devices-tags)）。

## 示例：试验机的每日 CSV {#example-a-test-machines-daily-csv}

一台拉伸试验机每天在 `C:\TestMachine\Logs` 中写一个 CSV，名为 `Log_2026-10-03.csv`，标题行为 `Sample;Force;Elongation`，每个样品之后写一行，例如 `0412;12,5;3,1`。按简单路径读取它：

1. 在驱动页面点**添加设备**；命名为 `Tensile tester`。
2. 在**文件夹和文件**卡片中，**文件夹路径**填写 `C:\TestMachine\Logs`，**文件名**填写 `Log_{yyyy-MM-dd}.csv`；保持**单个文件**和 **CSV 与分列文本**。卡片显示“今天：Log_2026-10-03.csv”，设备读取它之后显示“正在读取：Log_2026-10-03.csv”。**文件文本**卡片保持**从文件识别**：它会找出分号和小数逗号。
3. 在驱动页面的**要扫描的文件**中选择 `Tensile tester`，然后点**发现设备**。候选项标为**已经配置过**，列出 `Sample`（String，保留 `0412` 的零）、`Force` 和 `Elongation`（Double），以及文件信息。点**添加所选点位**。

每个点位读取最后一行；**最后修改**随每个新样品变化，Logic 的动作由此知道有样品到达。到了午夜，设备转向第二天的文件，在试验机写出它之前，这些值没有读数，**文件是否存在**读为假。智能体用工具也能做到：

```
device_upsert name="Tensile tester" driver=File fileFolder="C:\TestMachine\Logs" fileName="Log_{yyyy-MM-dd}.csv" fileFormat=Delimited
device_discover driver=File fileDevice="Tensile tester"
tag_upsert device="Tensile tester" name=Force address=Force opcUaType=Double
tag_upsert device="Tensile tester" name=Sample address=Sample opcUaType=String
tag_upsert device="Tensile tester" name="Last change" fileSource=Modified
```

## 示例：JSON 结果文件的收件文件夹 {#example-an-inbox-of-json-result-files}

一个试验单元每个样品在 `C:\Results\Inbox` 中放一个结果文件，例如内容为 `{"sample":"0413","result":{"force":12.5,"ok":true}}` 的 `Test_0413.json`：

1. 点**添加设备**；命名为 `Results`，**文件夹路径**为 `C:\Results\Inbox`，**读取来源**为**收件文件夹**，**要读取的文件**为 `Test_*.json`，**文件格式**为 **JSON**。
2. **添加点位** `Force`，**JSON 路径** `result.force`，Double；**添加点位** `Pass`，**JSON 路径** `result.ok`，Boolean；**添加点位** `Sample`，**JSON 路径** `sample`，String。
3. **添加点位** `Source file`，**读取内容**为**文件名**：它显示这些值来自哪个文件。

每个文件在静止 1 秒且没有程序为写入而打开它之后读取一次，按文件到达的顺序；每个文件都是单独的一个读数，时间戳为文件的时间；每个点位都读过之后，它被移到 `C:\Results\Inbox\Processed`。不会删除任何内容。用工具：

```
device_upsert name=Results driver=File fileFolder="C:\Results\Inbox" fileName="Test_*.json" fileMode=Inbox fileFormat=Json
tag_upsert device=Results name=Force address=result.force opcUaType=Double
tag_upsert device=Results name=Pass address=result.ok opcUaType=Boolean
tag_upsert device=Results name=Sample address=sample opcUaType=String
tag_upsert device=Results name="Source file" fileSource=Name
```

## 示例：为另一个程序追加一行 {#example-appending-a-line-for-another-program}

另一个程序读取 `D:\Records\run.csv`，每秒一行，包含时间和压力：

1. 点**添加设备**；命名为 `Run log`，**文件夹路径**为 `D:\Records`，**文件名**为 `run.csv`。
2. 在**写入**卡片中打开**允许写入**，**标题文本**填写 `Time;Pressure`。
3. **添加点位** `Line`，Double，**访问**为**只写**，**写入方式**为**追加一行**，**行模板**为 `{datetime};{value:0.00}`。

向 `Line` 写入 12.5 会追加 `2026-10-03 14:05:12;12.50`，文件已使用逗号或**小数分隔符**设为**小数逗号 (12,5)** 时使用逗号；写入新建的文件会先写入 `Time;Pressure`。快速连续的写入全部保留，并保持顺序。要每次运行写一个文件，再添加一个点位 `File`，**读取内容**为**文件名**，**读写**：运行开始时，Logic 写入序列号加 `.csv`，之后的行就写进那个文件。用工具：

```
device_upsert name="Run log" driver=File fileFolder="D:\Records" fileName=run.csv fileFormat=Delimited fileWrites=true fileHeaderText="Time;Pressure"
tag_upsert device="Run log" name=Line opcUaType=Double accessType=WriteOnly fileWriteMode=AppendLine fileLineTemplate="{datetime};{value:0.00}"
tag_upsert device="Run log" name=File fileSource=Name accessType=ReadAndWrite
```

## 智能体工具 {#agent-tools}

`driver_capabilities` 在 `file` 下描述本驱动：简单路径、设备的每个字段、文件夹规则以及只能在该文件夹内操作的限制、文件名的各种形式、两种读取方式、各格式及其地址语法、各信号源及其类型、文本设置、读取与写入的行为、设备发现，以及三个完整示例。`device_upsert driver=File` 接受 `fileFolder`、`fileName`、`fileMode`（默认的 `SingleFile` 或 `Inbox`）、`fileFormat`（默认的 `Delimited`、`Text`、`Json` 或 `Xml`）、`fileEncoding`（`Automatic`、`Utf8` 或 `Windows1252`）、`fileSeparator`（`Automatic`、`Semicolon`、`Comma` 或 `Tab`）、`fileDecimal`（`Automatic`、`Point` 或 `Comma`）、`fileHeaderRow`（0 到 1000）、`fileWrites`，以及写入打开时的 `fileHeaderText` 和 `fileLineEnd`（`Automatic`、`CrLf` 或 `Lf`）；它返回的设备带有 `file`，其中的 `fileStatus` 是运行中的驱动发现的内容：正在使用的文件、它是否存在、读到的编码、分隔符和行尾，以及收件文件夹中等待的文件。`tag_upsert` 接受 `fileSource`（`Value`、`WholeFile`、`Exists`、`Size`、`Modified`、`Name` 或 `Lines`）、`fileRow`（`Last`、`Number` 或 `Matching`）、`fileRowNumber`、`fileKeyColumn`、`fileMatchText`、`fileWriteMode`（`AppendLine` 或 `ReplaceFile`）和 `fileLineTemplate`；列或路径就是 `address`，自由文本的值使用通用驱动的 `valueExtraction`、`valueStart`、`valueLength`、`valueSeparator`、`valueField` 和 `valuePattern`，新点位为只读，除非 `opcUaType` 另行指定，否则取信号源自身的类型。`device_discover driver=File` 读取 `fileDevice`（本驱动一台设备的名称），或者为扫描输入的 `fileFolder`、`fileName`、`fileMode` 和 `fileFormat`；已配置设备缺少的点位用 `tag_upsert` 添加。`connector_browse` 为每台设备返回 `fileFolder`、`fileName`、`fileMode` 和 `fileFormat`。其他工具见[智能体工具](agent-tools)。

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

- 不读取 Excel 工作簿：工作簿要等它的程序另存为 CSV 后才能读取。
- 驱动从不删除文件。收件文件夹把读过的文件移到 `Processed`，唯一会被移除的只有替换失败时的临时文件。
- “读取文件夹中最新的文件”不是一种读取方式：它会丢失同时到达的文件，还可能显示上一个样品的值。每个样品写一个文件的设备，用收件文件夹读取。
- 不会在文件内原地修改某个值（CSV 的一个单元格、另一个程序的 JSON 中的一个成员）：写入只追加一行或替换整个文件，因为两个程序写同一个文件，可能会丢失对方的修改。
- 网络共享用运行工作站的账户的 Windows 权限访问：设备不保存用户名和密码，所以共享必须允许该账户进入（读取；对于会写入的设备或收件文件夹，还要能写入和移动）。
- 驱动从不触碰其文件夹以外的任何内容：指向别处的名称，在输入时和写入时都会被拒绝。
- 每次读取时查看文件：变化在下一次读取时才会看到，而不是另一个程序写入的那一刻。
- 包含换行的 CSV 字段不会被读取，JSON、XML 或整个文件的点位最多读取 4 MiB。
