# MQTT

> 通过外部 MQTT Broker 以普通 MQTT 或 Sparkplug B 发布数据的设备，Broker 作为其下所有设备共用的节点来建模，并能把工作站自己的数据发布到 Broker。

MQTT 驱动读写那些通过**外部 MQTT Broker**发布数据的装置的点位：网关、无线传感器、Node-RED 流程、讲 Sparkplug B 的边缘节点。工作站是这个 Broker 的客户端：它订阅点位要读的主题，并发布你做的写入；每个 Broker 还能发布工作站自己的数据，也就是它的[发布卡片](#the-publish-card)上列出的点位、Logic 变量和报警，以 JSON 或 Sparkplug B 发布。工作站是客户端而不是 Broker，所以装置和工作站都要连到一个 Broker 上；手上没有 Broker 时，工作站自己的 [MQTT Broker](connector-mqtt-broker)就可以充当这个 Broker，打开它后，这个驱动会多出 Broker **本工作站**；[安装一个 Broker](#installing-a-broker)说明怎样另外搭一个。

一个 Broker 后面的所有设备都走同一条连接，所以结构是**驱动 → Broker → 设备 → 点位**：**Broker**持有它在哪里应答、工作站如何登录，只填一次；其下的每台**设备**只持有自己的发布方式（**普通 MQTT** 或 **Sparkplug B**）和自己的点位。两种协议可以在同一个 Broker 下并存。数值靠订阅到达，而不是轮询，所以 MQTT 的设备和点位都没有轮询间隔。

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

MQTT 驱动的页面列出的是 Broker，而不是设备。页头下面那句话是“每个 Broker 是到一个外部 MQTT Broker 的一条连接。先配置它的应答地址和工作站的登录方式，再添加通过它发布数据的设备。”**MQTT Broker**表格在标题右边带着**添加 Broker**，每个 Broker 一行：**名称**、**地址**（`mqtt://host:port`，走 TLS 时是 `mqtts://host:port`，Broker 还没有主机时为空）、**设备**、**状态**（一个圆点，加上 Broker 的状态和它的设备在做什么，例如“在线 · 2/2 台设备在线”），然后是**打开**，有“配置 Connector”权限时还有**启用**、**禁用**和**删除**。表格为空时显示“还没有 Broker。”拒绝了工作站的 Broker，名称后面带着树里同样的红色标记，页面标题栏下面点名有错误的 Broker（“1 个 Broker 有错误：Plant”）。

在树里，驱动那一行把两层都数出来（“1 个 Broker · 2 台设备”），页面的页头把 Broker 的个数写在括号里。在列底部的“新建…”菜单里，MQTT 那一项提供**新建 Broker**，然后是每个 Broker，各带自己的**新建设备**和设备；右键点驱动那一行则提供**添加 Broker**。驱动的命令栏上没有发现设备：一次扫描只在一个 Broker 上监听，所以它在 Broker 的页面上。驱动添加之后就出现在树里的“驱动”文件夹下面，不论有没有 Broker。

## Broker {#the-broker}

**添加 Broker**、“新建…”菜单里的**新建 Broker**，以及驱动那一行的右键菜单，都会创建一个名叫“新建 Broker”（接下来是“新建 Broker 2”）的 Broker，并且处于禁用状态，好让你在工作站连接之前先把连接填好。操作记录条也这样说：“已添加处于禁用状态的 Broker‘新建 Broker’。请填写它的应答地址，然后启用它。”这个 Broker 不会被打开：它在驱动页面的“MQTT Broker”表格里突出显示，从“新建…”或右键菜单新建时画面会转到那个页面。用它那一行的**打开**或者树里它那一行打开的 Broker 页面，显示一句“一个 MQTT Broker：它在哪里应答，以及工作站如何登录它。其下的每台设备都共用这条连接。”，页头里是状态、它的圆点和地址。标题栏下面点名 Broker 下有自身错误的设备（“1 台设备有错误：Pump”）；Broker 本身有错误期间，设备都指向 Broker：设备页面显示“此设备经由 Broker Plant，该 Broker 有错误。原因见 Broker 页面。”

### Broker 连接卡片 {#the-broker-connection-card}

| 字段 | 含义 | 默认值 |
| --- | --- | --- |
| **名称** | Broker 在树里以及其下每台设备上显示的名字。它从不进入任何地址。留空会被拒绝：“Broker 名称不能为空。” | “新建 Broker” |
| **主机** | Broker 的主机名或 IP 地址。可以粘贴整条地址：前面是 `mqtt://` 或 `tcp://` 时传输方式为明文，是 `mqtts://`、`ssl://` 或 `tls://` 时为 TLS，主机后面的端口会进入**端口**，主机本身单独保存。地址里写了别的协议头、路径或登录信息，会在输入框下面被拒绝，各字段保持原来的值：“‘http://192.168.0.20’不是 Broker 地址。请填写 Broker 的主机名或 IP 地址，也可以写成 mqtt://host:port 或 mqtts://host:port。” | 空 |
| **端口** | Broker 应答的 TCP 端口，1 到 65535。 | 1883 |
| **传输方式** | 复选框**使用 TLS**。勾选时连接被加密，Broker 的证书要对照 Windows 证书存储或[现场设备访问](#equipment-access)里已批准的证书检查：“已加密：Broker 的证书必须受 Windows 信任，或已在 MQTT 驱动页面的‘现场设备访问’里获得批准。”不勾选时提示会警告“未加密：数值和密码以明文传输。”切换它会把停在 1883 的端口改成 8883，反过来也一样；其他端口保持不变。 | 不勾选 |
| **用户名**、**密码** | 工作站登录 Broker 所用的账户。用户名为空就匿名登录。密码像设备的凭据一样在存储时受保护，从不出现在地址、诊断日志或 AI 助手的回答里。 | 空 |
| **客户端 ID** | 工作站的会话在 Broker 上的名字，1 到 128 个字符，不含控制字符。Broker 按客户端 ID 只保留一个会话，有两个时把旧的那个踢掉，所以一个客户端 ID 只属于本工作站的一个 Broker：“客户端 ID‘GanterLab01’已属于 Broker‘Plant’。同一客户端 ID 的两个会话会互相把对方挤下 Broker：请另选一个。”两台工作站共用一个 Broker 时，各用各的 ID。 | `GanterLab` 加 14 位十六进制数字，自动生成 |
| **保活间隔 (s)** | 没有别的数据往来时，会话多久证明一次自己还活着：“保活间隔必须在 5 到 3600 秒之间。” | 60 |

字段下面有一句话，说明 Broker 和它的设备在做什么、Broker 还缺什么，接着是“此 Broker 下的所有设备以及工作站发布到它的内容共用这条连接：第一台已启用的设备或第一行发布内容需要时，会话才会打开。”Broker 启用期间，每个字段都仍然可以编辑。保存对应答地址、传输方式、登录信息、客户端 ID 或保活间隔的修改后，Broker 下所有设备共用的那一个会话会重新连接。

### 命令栏 {#the-command-bar}

| 命令 | 作用 | 何时置灰（情况） | 何时不画出来（角色） |
| --- | --- | --- | --- |
| **启用** | 让 Broker 下的设备使用它的会话。没有主机的 Broker 会被拒绝：“启用 Broker 之前，请先填写它的主机名或 IP 地址。” | 已经是启用的，连接不完整（“此 Broker 的连接设置还不完整。”），正在扫描，或工作站拒绝改动。 | 没有“配置 Connector”权限时。 |
| **禁用** | 对其下每台设备关闭会话；每台设备保留自己的“已启用”选择，Broker 重新启用后照原样回来。 | 已经是禁用的，正在扫描，或工作站拒绝改动。 | 没有“配置 Connector”权限时。 |
| **删除 Broker**（危险色，靠右） | 先问一句。空 Broker：“此 Broker 下没有设备。”；有设备的 Broker：“这同时会删除此 Broker 的 2 台设备以及它们的全部点位。”删完之后选中 MQTT 驱动，操作记录条显示“已删除 Broker‘Plant’及其 2 台设备。” | 正在扫描（“一次设备发现扫描正在使用此 Broker。请等它结束。”），或工作站拒绝改动。 | 没有“配置 Connector”权限时。 |

右键点 Broker 那一行，会出现**添加设备**、**启用**、**禁用**和**删除**。

### Broker 的状态 {#broker-states}

Broker 把自己的会话和设备分开报告，出现在树的那一行、驱动的 Broker 表格以及它自己的页头里：

| 状态 | 圆点 | 含义 |
| --- | --- | --- |
| **关** | 灰色 | Broker 已禁用。 |
| **无会话** | 灰色 | 已启用，但没有已启用的设备使用它，它的发布卡片也不发布任何内容，所以没有打开会话。 |
| **正在连接…** | 琥珀色 | 有设备已启用或 Broker 在发布，会话正在连接，或在断开后重新连接。 |
| **在线** | 绿色 | 会话已连接。 |
| **无法连接** | 红色 | Broker 连不上，或者拒绝了工作站。原因用工作站的语言以红字写在页头下面，也按驱动的原话作为该行错误标记的提示文字，用“该改什么”的说法来写：那个端口上没有任何东西应答，这个名字在本网络上不认识，用户名或密码被拒绝，证书不受信任或正在[现场设备访问](#equipment-access)里等待批准，另一个客户端用同一个客户端 ID 抢走了会话。拒绝端口或拒绝没有用户名的客户端的 Broker，会改为说明它的设置（见[准备 Broker](#prepare-the-broker)）。 |

树的那一行显示状态和设备数量（“在线 · 2 台设备”），卡片下面的摘要再补上设备在做什么：“在线 · 2/2 台设备在线”“在线 · 1 台在线 · 1 台有问题”“无会话 · 没有已启用的设备”。宣告自己死亡的 Sparkplug B 设备只让它自己下线：Broker 仍然在线。会话讲 MQTT 5，遇到不支持的 Broker 就退回 MQTT 3.1.1，以干净状态开始，并自行重连；Broker 一直不回来时，两次尝试之间等得越来越久。

### 发现设备卡片 {#the-discover-devices-card}

只为有“配置 Connector”权限的角色画出来，位于这句话下面：“在此 Broker 上监听一段时间：收集自行宣告的 Sparkplug B 边缘节点和设备，以及过滤器下的主题。”

| 字段 | 含义 | 默认值 |
| --- | --- | --- |
| **监听时长 (s)** | 扫描监听多久，3 到 60 秒。 | 10 |
| **主题过滤器** | 要作为一台设备的点位列出的普通主题。可以用通配符 `+`（一级）和 `#`（这一级及其下所有级，只能放在最后），所以写成“例如 plant/#”。留空则只列出 Sparkplug B 设备。 | 空 |

**发现设备**开始扫描，扫描进行时变成**取消**。字段下面那句话说明它要做什么（“在 192.168.0.20:1883 上监听 10 s。”），进度那一行给时间窗计数（“正在监听 192.168.0.20:1883 · 4/10 s…”）。扫描需要 Broker 上已保存的主机，在那之前卡片显示“设备发现之前，请填写 Broker 的主机，并等修改保存。”Broker 上尚未保存的修改会先保存；保存不了就什么也不开始：“设备发现没有开始，因为 Broker 的修改无法保存。”

扫描在 Broker 上做的事：

- 它用自己的客户端 ID 连接，所以绝不会把设备的会话挤下 Broker，而且可以在 Broker 已启用、正在使用时运行。
- 它订阅所有 Sparkplug B 主题（`spBv1.0/#`）和主题过滤器。Sparkplug B 的出生证书不会被保留，所以对听到了却没有出生证书的边缘节点，会请它重新宣告一次（`Node Control/Rebirth` 命令，这是扫描唯一发布的消息）；正在运行的边缘节点会在时间窗内回应。
- 每个宣告自己的边缘节点及其后面的每台设备（NBIRTH、DBIRTH）都成为一个候选，带着它的指标作为点位，类型按出生声明的来。边缘节点自身的管道（`bdSeq` 以及 `Node Control/…` 和 `Device Control/…` 指标）被排除，没有点位能读的指标（数组、数据集、模板、属性集、字节串、文件）也被排除，并按原因计数。听到了但在时间窗内没有出生的边缘节点，会以不带点位的方式列出。
- 有主题过滤器时，在其下听到的普通主题单独成为一个候选：每个主题是一个点位；如果负载是 JSON，其中每个值按数值路径各成一个点位（每个主题最多 32 个，最深 4 级，数组只取前 8 个元素）。数字作为 Double，`true` 或 `false` 作为 Boolean，其余作为 String。列表到 500 个主题为止。发布者保留了最后一条消息的主题会立刻被听到；其他主题只有在时间窗内发布时才会被听到。
- 在你添加某个候选之前，什么都不保存。取消会立刻结束时间窗。

结果卡片**发现的设备**列出**发布身份**（`group/edge node`，边缘节点后面的设备再加 `/device`，或者“普通主题”）、**设备**（添加时用的名字，可以编辑：设备 ID 或边缘节点 ID，或者过滤器在第一个通配符之前的各级）、**结果**（“已选中 12 个点位中的 12 个”）以及**添加设备**，它会在这个 Broker 下用这些点位创建设备。Broker 上已经配置过的 Sparkplug B 身份显示“已经配置过”，改为提供**打开**。什么都没听到的扫描会说“扫描监听期间，此 Broker 上没有任何设备宣告自己。”

已经有的设备可以在自己的页面上找出它还没在读的东西：点位卡片标题栏里的**发现点位**在它的 Broker 上监听 10 秒。Sparkplug B 设备只听它自己的边缘节点，并请这个节点重新发出出生消息，其他边缘节点一概不打扰；普通设备听它的点位共有的层级下面的主题（`plant/oven/temperature` 和 `plant/oven/count` 就是 `plant/oven/#`），没有共有层级时就听 Broker 上的所有主题。只列出它还没有点位的值（见[发现一台设备的点位](connector-discovery#discovering-a-devices-tags)）。

### 设备卡片 {#the-devices-card}

“每台设备读取普通主题，或一个 Sparkplug B 边缘节点或设备。连接继承自 Broker，在这里无法更改。”**添加设备**在这个 Broker 下创建“新建设备”，已启用、普通 MQTT，在这张表格里突出显示并滚动到可见处：“已在‘Plant’上添加设备‘新建设备’。”表格列出**设备**、**发布身份**、**点位**和**健康状态**，每一行有打开、启用、禁用和删除；为空时显示“此 Broker 下没有配置任何设备。”有自身错误的设备，名称后面带着树里同样的红色标记。

### 发布卡片 {#the-publish-card}

最后一张卡片把工作站自己的数据发布到这个 Broker，卡片上写着“每当数值或其质量变化时，工作站就把每一行以 JSON 发布到此 Broker 上该行的主题，并在每次连接建立时重新发布所有行。Broker 为之后订阅的人保留每个主题的最后一条消息。工作站只订阅设为读写的各行的写入主题。”能查看 Connector 的人可以读这张卡片，能配置 Connector 的人可以修改它。工作站自己的 [MQTT Broker](connector-mqtt-broker)对应的 Broker 本工作站 也有同样的卡片。

| 字段 | 含义 | 默认值 |
| --- | --- | --- |
| **发布格式** | 各行的发出方式：在每行的主题上以 **JSON** 发出（[发布](#publishing)），或者以 **Sparkplug B** 发出，工作站作为 Broker 的边缘节点（[以 Sparkplug B 发布](#publishing-in-sparkplug-b)）。它的提示：“各行的发出方式：在每行自己的主题上以 JSON 发出，或以 Sparkplug B 发出。在 Sparkplug B 中，本工作站是一个边缘节点，Ignition 等 Sparkplug B 消费者会自行找到它。” | JSON |
| **主题前缀** | 仅用于 JSON。每一新行的主题建议放在这一层之下。留空时，字段显示默认值并使用它。“主题前缀不能含 + 或 #，不能以 $ 开头，也不能包含主题不能包含的字符。”更改它不会改动列表中已有的主题。 | `ganterlab/` 加工作站名称 |
| **组 ID** | 仅用于 Sparkplug B，出现在前缀的位置。工作站的边缘节点在此 Broker 上的 Sparkplug B 组；留空则为 `GanterLab`。含有 `/`、`+` 或 `#` 的组会在字段下方被拒绝，改正之前 Broker 不会保存。 | `GanterLab` |

在 Sparkplug B 中，标题下的那句话改为“在此 Broker 上，工作站是一个 Sparkplug B 边缘节点：Logic 的变量和报警是它自己的指标，每个有行的设备都是一个 Sparkplug B 设备。连接打开时以及列表每次变化时，它会宣告所有指标，之后发布每个变化的值或质量。只有设为读写的行才接受来自命令的值。”，字段下方的 **边缘节点 ID（工作站的名称）** 显示它发布时所用的边缘节点。

表格列出 **信号源**、**主题** 和 **访问**，按主题排序；还没有选信号源的行显示“尚无信号源”。一行都没有时显示“还没有向此 Broker 发布任何内容。用新建行添加一行。”Broker 已停用而列表中有行时，会有一条提示：“Broker 已停用，因此在启用之前不会发布任何内容。”Broker 的某一行设为读写时，还会有第二条提示：“设为读写的行接受发布到其写入主题的值。任何能向此 Broker 发布消息的人都能更改这些信号源，因此 Broker 的用户名和密码是它们唯一的保护。”在 Sparkplug B 中它显示：“设为读写的行接受来自 Sparkplug B 命令（NCMD、DCMD）的值。任何能向此 Broker 发布消息的人都能更改这些信号源，因此 Broker 的用户名和密码是它们唯一的保护。”在 Sparkplug B 中，**主题** 列变为 **指标**：点位显示在它的设备之后（`Oven · Temperature`），变量和报警按它在边缘节点上的名称显示（`Logic/Oven/Setpoint`、`Alarms/High`）。

| 命令 | 作用 | 何时置灰（情况） | 何时不画出来（角色） |
| --- | --- | --- | --- |
| **新建行** | 添加一行还不发布任何内容的行并选中它：“已在 Broker 的发布列表中添加行。请选择它的信号源。” | 有运行记录占用着工作站，或者运行时已停止。 | 没有“配置 Connector”时。 |
| **删除行**（危险色） | 立即删除选中的行，它的提示会说出这一行发布的是什么。工作站不再发布它；Broker 在它的主题上保留的消息仍然留在那里。 | 没有选中行（“请在列表中选择要删除的行。”）、有运行记录占用着工作站，或者运行时已停止。 | 没有“配置 Connector”时。 |

一行的字段在改动的那一刻就会保存：

| 字段 | 做什么 |
| --- | --- |
| **信号源** | 这一行发布的内容：“（无）”，然后是已启用设备上每一个已启用的点位（“点位 · Simulated · Oven · Temperature”）、每一个持有值的 Logic 变量和每一个报警，按所在文件夹命名。设备命令不发布任何内容，所以不在列表里。这一行按标识保存信号源，所以重命名之后它仍然指向它，信号源的[使用位置](connector-tags#the-used-by-card)卡片会列出这一行。 |
| **主题** | 仅用于 JSON。这一行发布到哪里。选择信号源时会按它的名称建议一个主题：点位是 `<prefix>/<device>/<tag>`，变量是 `<prefix>/logic/<folders>/<variable>`，报警是 `<prefix>/alarms/<folders>/<alarm>`，每个名称占一层主题（空格、斜杠或通配符会变成 `_`）。可以随意修改：仍是建议值的主题会跟着之后选的信号源变化，你自己写的主题会保留，重命名信号源也不会改变它。 |
| **访问** | **只读** 是每一行的默认值：工作站发布这一行，不为它订阅任何主题。**读写**：工作站还接受发布到这一行写入主题（这一行的主题后加 `/set`）的值，并把它们写入信号源（[回写](#writing-back)）。只有写入型驱动上有写访问权限的点位或内部存储值变量才提供这一项；其他信号源上它仍在列表中，置灰显示：“该信号源不接受写入：只有有写访问权限的点位或内部存储值变量才接受。”选择报警会让这一行回到只读。在 Sparkplug B 中，读写接受的不是写入主题，而是 Sparkplug B 命令发给这一行指标的值。 |

字段下面有一句话，说明这一行发布什么：

| 卡片显示 | 什么时候 |
| --- | --- |
| “这一行还没有发布任何内容：请选择信号源。” | 还没有选择信号源。 |
| “每当数值或质量变化时，以 JSON 发布数值、其质量和时间。” | 只读的点位或变量。 |
| “每当数值或质量变化时，以 JSON 发布数值、其质量和时间，并把发布到写入主题的每个值写入信号源：每次写入都会连同 Broker 的名称记入诊断日志。” | 设为读写的点位或变量。 |
| “每当其中任一项变化时，以 JSON 发布报警是否激活和已确认、其状态、优先级、消息和时间。” | 报警。 |
| “这一行的信号源已不存在，因此不再为它发布任何内容。Broker 保留它最后保存的消息。” | 点位、变量或报警已被删除。 |
| “每当数值或质量变化时，以 Sparkplug B 发布数值及其质量。” | Sparkplug B：只读的点位或变量。 |
| “每当数值或质量变化时，以 Sparkplug B 发布数值及其质量，并把 Sparkplug B 命令发给其指标的每个值写入信号源：每次写入都会连同 Broker 的名称记入诊断日志。” | Sparkplug B：设为读写的点位或变量。 |
| “每当其中任一项变化时，以 Sparkplug B 发布报警是否激活和已确认、其状态、优先级和消息。” | Sparkplug B：报警。 |
| “这一行的信号源已不存在，因此边缘节点会不带它重新宣告自己。” | Sparkplug B：点位、变量或报警已被删除。 |

这句话下面，设为读写的行会显示它的 **写入主题**，即这一行的主题后加 `/set`。Sparkplug B 没有写入主题：第二句话说明这一行作为哪个指标发布。点位是它的 Sparkplug B 设备的指标，变量是工作站边缘节点的指标，报警则是它名称下的五个指标（Active、Acked、State、Priority 和 Message）。

列表拒绝的修改不会保存：原因以红字写在这一行的字段上方，修改留在屏幕上，方便你改正。

| 卡片显示 | 什么时候 |
| --- | --- |
| “此信号源已发布到此 Broker：请编辑发布它的那一行。” | 这个 Broker 的另一行已经发布这个信号源。 |
| “此 Broker 的另一行已在这个主题上发布：请给这一行一个自己的主题。” | 一个 Broker 的两行会发布到同一个主题。 |
| “过滤器上无法发布任何内容：主题中不能含 + 或 #。” | 主题含有通配符。 |
| “以 $ 开头的主题属于 Broker：请写另一个。” | 主题以 `$` 开头。 |
| “此主题无法传输：它包含主题不能包含的字符，或者太长。” | 含有控制字符，或者超过 MQTT 主题能承载的长度。 |
| “接受写入的行需要指定写入目标点位或变量。” | 在选择信号源之前就选了读写。 |
| “该信号源不能写入：请选择有写访问权限的点位或内部存储值变量，或者让这一行保持只读。” | 在报警、表达式变量、只读点位或驱动不写入任何内容的点位上选择读写。 |
| “这个主题与此 Broker 另一行的主题只差一个 /set，即行接受写入的位置：请给这一行另一个主题。” | 一行的主题是另一行的主题后加 `/set`。 |
| “列表中的这一行已不存在。请重新加载后重试。” | 你编辑时，这一行在别处被删除了。 |

发布什么、什么时候发布，见[发布](#publishing)。

## 设备 {#device}

| 字段 | 含义 | 默认值 |
| --- | --- | --- |
| **Broker · 固定** | Broker 的名称和它下面的地址，只读：设备在它的 Broker 下创建，并一直留在那里。地址的位置可能显示“Broker 还没有主机地址”。“缺少 Broker”表示设备丢了它的 Broker，无法保存：“必须指定一个 MQTT Broker。” | 添加时所在的 Broker |
| **协议** | 设备的发布方式。**普通 MQTT**：“每个点位指明它读取的主题，若负载是 JSON，还要指明到值的路径。”**Sparkplug B**：一个边缘节点，或它后面的一台设备：“每个点位按边缘节点的出生证书（birth）声明的名称指向一个指标；类型随指标一起到达。”切换它会重新检查每个点位，因为地址从此按主题或按指标来读。 | 普通 MQTT |
| **无消息的最长时间（秒）** | 仅普通 MQTT，0 到 86400，0 表示从不：“在这段时间内主题上收不到消息时，其点位不再算作读数。0：从不过期，适合只在值变化时才发布的主题。对按固定节奏发布的设备，例如每隔几秒上报一次的传感器，请设置一个值。”超出这个范围，设备不会被保存：“无消息的最长时间必须在 0 到 86400 秒之间。”Sparkplug B 设备没有这一项：它的死亡证书已经说明它何时消失。 | 0 |
| **组 ID** | 仅 Sparkplug B：边缘节点发布时所在的组，也就是主题里 `spBv1.0` 后面那一级。“Sparkplug B 设备需要组 ID。” | 空 |
| **边缘节点 ID** | 仅 Sparkplug B：发布这些指标的边缘节点，按它的 NBIRTH 所起的名字。“Sparkplug B 设备需要边缘节点 ID。” | 空 |
| **设备 ID** | 仅 Sparkplug B：边缘节点后面的设备，按它的 DBIRTH 所起的名字。留空（显示为“边缘节点本身”）就读边缘节点自己的指标。 | 空 |

设备没有自己的主机、端口、凭据或轮询间隔：这些都属于 Broker。每个 Sparkplug B ID 是主题中的一级：“Sparkplug B ID 不能包含 '/'、'+' 或 '#'。”一个 Broker 只认一次某个 Sparkplug B 边缘节点或设备，所以在同一个 Broker 上组 ID、边缘节点 ID 和设备 ID 都相同的第二台设备会被拒绝：“Broker‘Plant’上的 Sparkplug B 设备 Plant/Line1/Press1 已属于设备‘Press’。”普通 MQTT 设备在 Broker 上不指明任何属于自己的东西，两台这样的设备可以读同样的主题。

页头显示的基础 URI 就是运行时地址：`mqtt://<broker id>?host=…&port=…&tls=0&keepAlive=60&clientId=…&mode=plain`（设备设置了无消息的最长时间时再加 `&silence=…`），Sparkplug B 设备则是 `mode=sparkplug&group=…&node=…`，边缘节点后面的设备再加 `&device=…`。它用 Broker 的稳定标识来指代 Broker，从不用显示名称，也从不带用户名或密码。

MQTT 设备的副本留在同一个 Broker 上。Sparkplug B 副本取该 Broker 上还没有设备使用的第一个身份：它最内层的 ID（设备 ID，读边缘节点自己的指标时则是边缘节点 ID）像副本的名称那样加上编号（“Press1 (2)”）。

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

地址字段在普通 MQTT 设备上是**主题**，在 Sparkplug B 设备上是**指标**，而且必填（“这个驱动必须填写主题。”）。普通点位还多三个字段；Sparkplug B 指标一个都没有，因为它自带类型，并通过协议自己的命令写入。

| 字段 | 含义 | 默认值 |
| --- | --- | --- |
| **主题** | 点位读取的那一个主题，例如 `plant/line1/temperature`。“一个点位只读取一个主题，因此主题中不能含通配符 + 或 #。” | 必填 |
| **数值路径** | 值在 JSON 负载中的位置：用点分隔的成员和方括号中的数组下标，例如 `sensors[0].value`。名字里含点或方括号的成员，要写在方括号里并加引号（`['flow.rate']`），开头的 `$.` 也接受。留空（显示为“整个负载”）就把整个负载当作值来读。路径格式不对时：“请把数值路径写成用点分隔的成员和方括号中的数组下标，例如 sensors[0].value。” | 空 |
| **命令主题** | 写入发布到哪里。留空则点位保持只读：访问方式包含写入的点位必须有它（“可写点位需要指定写入发布到的主题。”），而且其中不能有通配符（“过滤器上无法发布任何内容：命令主题中不能含 + 或 #。”）。装置很少在它上报的主题上等指令，所以这是一个单独的字段。 | 空 |
| **负载模板** | 写入发布的内容：一段 JSON，在写入值的位置、引号之外恰好放一次 `{value}`，例如 `{"setpoint": {value}}`。留空（显示为“以文本形式的值”）则把值作为文本发布。“负载模板必须恰好包含一次 {value}，放在写入值所在的位置。” / “请用 JSON 写负载模板，并把 {value} 放在 JSON 值的位置，不加引号。” | 空 |
| **指标** | Sparkplug B：出生证书声明的指标名称，例如 `Inputs/Temperature`。 | 必填 |

数据类型有 Boolean、Int16、Int32、Int64、Float、Double、String 和 DateTime。只读的线路地址预览是主题后面跟上 `path`、`command`、`payload` 中适用的部分（`plant/line1/temperature?path=sensors[0].value&command=plant/line1/setpoint`），或者是指标名称；会结束某一部分的字符（`%`、`?`、`&`、`=`、`#`、`+`、空格）写作 `%XX`。

## 读取 {#reading}

会话为它订阅的每个主题保留最后一条消息，点位从那里读：

| 情况 | 普通 MQTT 点位 | Sparkplug B 点位 |
| --- | --- | --- |
| 值 | 主题上的最后一条消息：整个负载作为文本，或数值路径处的 JSON 值，转换成点位的类型。 | 边缘节点最后一条出生消息或数据消息里该指标的值。 |
| 时间戳 | 消息到达的时刻。发布者保留的消息，在工作站订阅后立刻给出第一个值。 | 指标自己的时间戳。 |
| 还什么都没收到 | 点位等待：没有值也没有错误，跟任何驱动在第一次应答之前一样。发布者不保留消息的主题，要等它再发布一次才有值。设置了**无消息的最长时间（秒）**时，从工作站开始监听算起最多等这么久，之后点位读取失败。 | 同样，直到出生证书到达；工作站会请边缘节点重新宣告。 |
| 发布者沉默，Broker 仍在线 | 设置了**无消息的最长时间（秒）**时，主题上超过这段时间没有消息的点位读取失败，由点位针对[设备没有应答](connector-value-pipeline#the-device-did-not-answer)的策略决定显示什么；下一条消息到达后恢复。时间从最后一条消息（包括保留消息）到达时算起。设为 0 时，点位一直保留最后收到的值。 | 不适用：死亡证书会说明。 |
| 点位读不了的消息 | 该点位一次读取失败，并附上原因：空消息、有数值路径但负载不是 JSON、路径处什么都没有，或类型装不下的值。 | 该点位一次读取失败：出生证书没有声明的指标，或类型装不下的值。 |
| Broker 无法连接 | 该 Broker 下的所有设备都断开，它们的点位像任何设备的点位一样失败。 | 同样。 |
| 死亡证书（NDEATH、DDEATH） | 不适用。 | 边缘节点及其设备，或者只是那一台设备，在重新出生之前都没有值；该 Broker 上的其他设备照常进行。 |

Boolean 点位接受 `true` 或 `false`、`1` 或 `0`、`on` 或 `off`。整数点位接受小数并四舍五入，超出范围的数会被拒绝。DateTime 点位接受 ISO 8601 文本或 Unix 毫秒。String 点位按原样接受文本，JSON 对象或数组则按原始 JSON 接受。无符号类型的 Sparkplug B 指标按宽一级的有符号类型来读（UInt8 读作 Int16，UInt16 读作 Int32，UInt32 和 UInt64 读作 Int64），这样整个范围都能读到。

## 写入 {#writing}

写入走的路径和其他任何驱动相同：写入框接收工程值，在发布原始值之前，转换的各级被倒过来执行（见 [Connector](connector)）。

| 点位 | 发布的内容 | QoS | 写入报告什么 |
| --- | --- | --- | --- |
| 普通 MQTT | 把值发布到点位的**命令主题**上，作为文本（`21.5`、`true`、文本本身、ISO 8601 日期），或者作为 JSON 值放进**负载模板**（数字就是数字，布尔值是 `true` 或 `false`，文本加引号并转义）。 | 1，不保留 | Broker 确认收到即完成；MQTT 5 Broker 拒收时为被拒绝，并附上 Broker 的原因；确认没有按时到达则为无应答。 |
| Sparkplug B | 用 NCMD（边缘节点的指标）或 DCMD（它后面某台设备的指标）携带该指标，使用出生时给它的别名，以及出生时声明的类型。 | 0，不保留，这是 Sparkplug B 规范对命令的要求 | 发出即完成。边缘节点会发布新值作为回应，点位接下来读到的就是它。 |

Broker 无法连接时，或者边缘节点或设备已经宣告死亡时，写入会作为已断开被拒绝，什么也不发送。对出生证书没有声明的指标进行的 Sparkplug B 写入会被拒绝，因为边缘节点不会接受；指标类型装不下的值也一样。写入从不保留：被保留的设定值会被之后连上的每个订阅者再执行一次。

## 发布 {#publishing}

本节说明 **发布格式** 为 **JSON** 时的情况；Sparkplug B 见下一节。

Broker 的[发布卡片](#the-publish-card)上的每一行都会发布到它的主题，作为保留消息，QoS 为 1，走的是 Broker 的设备所用的那条连接：工作站从不对一个 Broker 开第二条连接。只用来发布的 Broker 不需要任何设备。它仍然在 MQTT 驱动下面，它的状态（正在连接…、在线、无法连接）就是这条连接的状态。只要它已启用且列表中有行，它就发布；它没有自己的开关。

| 信号源 | 消息（JSON） |
| --- | --- |
| 点位 | `{"value": 21.5, "quality": "good", "timestamp": "2026-10-04T12:30:15.250Z"}`：点位显示的工程值；读到的读数 `quality` 为 `good`，没读到的读数为 `bad` 且 `value` 为 null，替代值、保持的上一个值以及不再到来的读数为 `uncertain`（保留上次发布的值）；时间是读数的时间戳，用 UTC。 |
| Logic 变量 | 同样的成员：变量良好时是它的值和 `good`，否则是 `null` 和 `bad`；时间是它上一次写入或计算的时间戳。 |
| 报警 | `{"active": true, "acked": false, "state": 1, "priority": "High", "message": "Oven above 80 °C", "timestamp": "2026-10-04T12:30:15.250Z"}`：是否激活、是否已确认，`state` 为数字（0 正常，1 激活未确认，2 激活已确认，3 已恢复未确认），优先级用名称（Critical、High、Medium、Low），消息（没有时为空），以及上一次状态变化的时间戳，与 [OPC UA 服务器](connector-opcua-served-nodes#where-a-client-finds-each-source)提供的一致。 |

值按它的 JSON 类型传送：数字、`true` 或 `false`、文本，日期时间用 ISO 8601 文本；JSON 无法表示的数字（NaN、无穷大）传送为 `null`。已停用设备的点位发布 `null` 和 `bad`。

| 情况 | 发布什么 |
| --- | --- |
| 值或质量变化 | 那一行，每秒检查五次。同一个值再读一次不会再次发布：只有时间戳变化不算变化。 |
| 连接建立，或重新建立 | 所有行，带当前状态。 |
| Broker 连不上 | 什么都不发布，也没有任何东西在队列里等待：Broker 回来时，每一行都以那一刻的状态发布。 |
| Broker 拒绝一条消息，或不确认它 | 30 秒后重试。诊断日志在 Connector 区域、组件`MQTT publication`下记录，先记一次，持续期间每隔几分钟记一条汇总。 |
| 信号源被删除 | 不再发布；Broker 保留它最后保存的消息。 |
| 一行被删除 | 不再发布；它的保留消息留在主题上，直到那里发布了别的内容。 |

工作站读取它要发布的内容，不为此计算任何东西：点位最后一次读数、变量的值、报警的状态。它不订阅只读行的任何主题，所以别的系统在那里发布的消息永远不会到达工作站；设为读写的行在写入主题上接受值（[回写](#writing-back)）。代理通过 `mqtt_publish_list` 读取列表，通过 `mqtt_publish_upsert` 和 `mqtt_publish_delete` 修改列表（[工具](agent-tools#connector--configure)）：

```text
mqtt_publish_upsert(brokerId: "<id from mqtt_broker_list>", source: "Connector.Simulated.Oven.Temperature")
mqtt_publish_upsert(brokerId: "<id from mqtt_broker_list>", source: "Alarms.Oven.HighTemperature", topic: "plant/oven/alarms/high")
mqtt_publish_upsert(id: "<id from mqtt_publish_list>", access: "ReadAndWrite")
mqtt_publish_list()
```

## 以 Sparkplug B 发布 {#publishing-in-sparkplug-b}

**发布格式** 设为 **Sparkplug B** 时，Broker 按 Sparkplug B 规范发布同一个列表，工作站作为边缘节点：Sparkplug B 主机（例如 Ignition 的 MQTT Engine）会把工作站列在它的组和边缘节点之下，每个设备列在其下，每个指标都带着它的类型，不需要任何手工建立的映射。连接与 JSON 时一样，是 Broker 的设备所用的那条连接。

| 名称 | 是什么 |
| --- | --- |
| 组 | **组 ID**，留空时为 `GanterLab`。 |
| 边缘节点 | 工作站的名称，其中每个 `/`、`+` 和 `#`（Sparkplug B ID 不能包含的三个字符）都变成 `_`。 |
| 设备 | 每个有点位行的 Connector 设备，按同样的规则命名。 |
| 点位的指标 | 点位的名称，位于它的设备上。 |
| 变量的指标 | `Logic/<folders>/<variable>`，位于边缘节点上。 |
| 报警的指标 | 五个，位于边缘节点上的 `Alarms/<folders>/<alarm>/` 之下：`Active` 和 `Acked`（Boolean），`State`（数字：0 正常，1 激活未确认，2 激活已确认，3 已恢复未确认），`Priority` 和 `Message`（文本）。 |

名称中的 `/` 会变成 `_`，所以只有文件夹构成层级。名称变得相同的两个设备或两个指标，会按各自原名的顺序加上 `_2`、`_3`。各行保留自己的主题：在 Sparkplug B 中这些主题上不发布任何内容，Broker 改回 JSON 后，列表会重新在这些主题上发布。

| 消息 | 何时 |
| --- | --- |
| NDEATH，边缘节点的死亡 | 每次连接建立时作为连接的遗嘱交给 Broker，工作站掉线时由 Broker 发布。工作站在停止发布之前也会自己发布它：Broker 改回 JSON、被停用或删除、列表被清空、组或工作站名称改变、运行时停止。 |
| NBIRTH，边缘节点的出生 | 每次连接建立时、主机请求重新宣告时（`Node Control/Rebirth`），以及变量或报警加入、移出或改名时：边缘节点的每个指标及其当前值，连同 `bdSeq` 和 `Node Control/Rebirth`。 |
| DBIRTH，设备的出生 | 每个 NBIRTH 之后对每个设备发送，设备加入列表时也发送：设备的每个点位及其当前值。 |
| NDATA、DDATA | 每秒五次，发送值或质量发生变化的指标：边缘节点一条消息，每个设备一条。 |
| DDEATH，设备的死亡 | 设备的最后一行移出列表时，以及点位有变化的设备重新出生之前。 |

除 NDEATH 外，所有消息都以 QoS 0 发出，从不保留。序号 `seq` 在每个 NBIRTH 时从 0 开始，之后每条消息加一，到 255 后重新开始；`bdSeq` 计数连接，所以同一条连接的 NBIRTH 和 NDEATH 带着相同的编号。Broker 不可用期间，不会有任何内容在队列中等待。

| 信号源的类型 | Sparkplug B 类型 |
| --- | --- |
| `Boolean` | `Boolean` |
| `SByte`, `Byte` | `Int8`, `UInt8` |
| `Int16`, `UInt16` | `Int16`, `UInt16` |
| `Int32`, `UInt32` | `Int32`, `UInt32` |
| `Int64`, `UInt64` | `Int64`, `UInt64` |
| `Float`, `Double` | `Float`, `Double` |
| `String` | `String` |
| `DateTime` | `DateTime` |
| `Guid`, `ByteString` | `UUID`, `Bytes` |

点位按它自己的类型发布；变量按 [OPC UA 服务器](connector-opcua-served-nodes#where-a-client-finds-each-source)提供它时的类型发布（整数为 Int64，实数为 Double，显示用的值为文本）。每个指标在属性 `Quality` 中带着它的质量，这是 Sparkplug B 主机读取的惯例：192 为良好，64 为不确定（读数不再到来时带着上次发布的值），0 为不良（这时指标没有值）。

| 命令 | 工作站做什么 |
| --- | --- |
| NCMD `Node Control/Rebirth` | 用当前值重新发布 NBIRTH 和所有 DBIRTH。 |
| 指向设为读写的行的指标的 NCMD 或 DCMD | 像[回写](#writing-back)那样把值写入信号源：按命令到达的顺序，以来源 `MQTT <broker name>` 记入诊断日志。 |
| 其他任何指标、没有值的命令，或 Broker 以保留消息交出的命令 | 不写入任何内容；诊断日志在 Connector 区域、组件`MQTT publication`下记录。 |

工作站始终监听发给边缘节点的命令，以便主机随时可以请求重新宣告；发给设备的命令只在该设备的某一行设为读写时才监听。把 Broker 切换为 Sparkplug B 会让它的连接重新建立一次，以便 Broker 持有边缘节点的遗嘱；Broker 的设备会像经历任何重连一样经历这一次。工作站是边缘节点，不是主要主机应用：它在出生前不等待主机的 STATE 消息。

```text
mqtt_broker_upsert(id: "<id from mqtt_broker_list>", publishFormat: "SparkplugB", sparkplugGroupId: "Plant1")
mqtt_publish_list()
```

## 回写 {#writing-back}

[发布卡片](#the-publish-card)上设为 **读写** 的行，会在它的写入主题（这一行的主题后加 `/set`）上接受值：例如读取 `plant/oven/setpoint` 的云端仪表板会把新的设定值发到 `plant/oven/setpoint/set`。工作站写入它的方式，与 [Modbus 服务器](connector-modbus-server)或 [OPC UA 服务器](connector-opcua-served-nodes)的客户端写入标为读写的行相同。

| 情况 | 会发生什么 |
| --- | --- |
| 把一行设为读写 | 工作站通过 Broker 的设备所用的那条连接，以 QoS 1 订阅它的写入主题，并在这条连接每次建立时重新订阅。只读的行不会被订阅，所以它写入主题上的消息永远不会到达工作站。 |
| 写入主题上收到一条消息 | `{"value": 65}` 或单纯的值（`65`、`true`、`配方 A`）会按消息到达的顺序写入信号源：点位走与操作员写入相同的路径，内部存储值变量直接写入。JSON 数字、`true` 或 `false` 和 JSON 文本按它们承载的值到达；不是 JSON 的内容按文本读取。值会像所有写入一样转换为信号源的类型，该类型无法承载的值会被拒绝。 |
| 消息没有承载值 | 不写入任何内容：空消息、`null`、没有 `value` 的 JSON 对象、数组。 |
| Broker 交出它保留的消息 | 不写入任何内容。Broker 会在工作站订阅时交出它，而一小时前留在那里的设定值并不是有人现在发送的值。工作站监听期间发布的值都会写入，无论是否保留。 |
| 信号源不再接受写入 | 不写入任何内容：改回只读的点位、已删除的变量。 |
| 这一行改回只读、改了主题或被删除，或者 Broker 被停用 | 工作站很快就不再监听那个写入主题，这期间到达的消息不会写入任何内容。 |

诊断日志会像记录操作员的写入一样，把每次写入记在 Connector 区域，来源为 `MQTT <Broker 名称>`。没有写入的消息会在诊断日志的 Connector 区域、组件`MQTT publication`下记录，先记一次，重复期间每隔几分钟记一条汇总。

唯一的保护是 Broker 的用户名和密码：Broker 允许谁在写入主题上发布，谁就能更改那个信号源；在与他人共用的云端 Broker 上，这些人会多于工厂里的人。请只给 Broker 上仪表板的账户它需要的主题。在设为 Sparkplug B 的 Broker 上，同样的行接受 Sparkplug B 命令（NCMD、DCMD），而不是写入主题（[以 Sparkplug B 发布](#publishing-in-sparkplug-b)）。

```text
"C:\Program Files\mosquitto\mosquitto_pub" -h localhost -u dashboard -P <password> -t plant/oven/setpoint/set -m 65
```

## 安全 {#security}

勾选**使用 TLS** 时，Broker 的证书通过两种方式之一被接受：它的证书链和主机名对照 Windows 证书存储验证通过，和这台计算机上的任何 HTTPS 连接一样；或者你在本驱动页面的[现场设备访问](#equipment-access)里批准了这份证书本身。证书来自你自己的证书颁发机构的 Broker，只要把该颁发机构安装到 Windows 存储中就会受信任；使用自签名证书的 Broker，在它的证书获得批准后无需安装任何东西。不检查吊销；也没有任何东西会不看就接受一份证书：陌生的证书被拒绝，Broker 显示无法连接并附上原因，它的设备则等待批准。密码在 TLS 连接内传输；不用 TLS 时就像卡片警告的那样以明文传输。

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

使用自签名证书或由自建颁发机构签发证书的 Broker，会出示一份 Windows 中已安装的任何根证书都不为其担保的证书。无需在这台计算机上安装那个根证书。勾选**使用 TLS** 时，第一次连接会被拒绝，Broker 显示无法连接并附上原因，它的设备在树里和各自的页面上显示**正在等待证书批准**，页面上的**查看证书**按钮会在那份证书上打开这张卡片；证书则在 MQTT 驱动页面末尾的**现场设备访问**里等你决定。这张卡片和 OPC UA 驱动页面上的是同一张。

| 分组 | 列出什么 | 你能做什么 |
|---|---|---|
| 等待批准 | 每一份被拒绝的证书，按它自称的名字列出：它的 SHA-256 指纹、遇到它的 Broker 地址、最后一次尝试的时间和次数，以及详情（主体、颁发者、域名、有效期）。设备的**查看证书**打开的那一份会标为“已选中待审核”。 | **批准**从会话的下一次连接尝试起接受这份证书。**阻止**继续拒绝它，且不再在这里列出。**丢弃所有待处理的证书**清空列表，什么也不批准、不阻止。 |
| 已批准的现场设备 | 你批准或导入的证书，连同谁在何时做的决定，以及每份证书最后一次被遇到的 Broker 地址。 | **撤回批准**关闭 Broker 的会话，并把它的证书放回列表。**导入证书**在第一次连接之前批准一份你手头已有的公开证书文件（.der、.cer、.crt 或 .pem）。 |
| 被阻止的现场设备 | 你阻止的证书。被阻止的证书即使受 Windows 信任也会被拒绝。 | **解除阻止**把证书放回列表，但不批准它。 |

批准之前，请与管理该 Broker 的人核对指纹。批准只接受这一份证书，而且只在它的有效期内；它代替 Windows 缺少的根证书和证书上写的名字，所以按 IP 地址连接的 Broker，即使证书上写的是别的主机也能连上，而证书的其他方面一概不放宽。如果 Broker 在你批准过证书的地址上换出另一份证书，它的设备就会停止读取，新证书在这里等待，工作站会带着**审核**按钮提示“证书已更换：`<名称>`”。从“发现设备”卡片对这个 Broker 做的发现也遵循同样的决定。Windows 已经信任的证书照旧被接受，永远不会出现在这里；没有任何东西会被自动批准。

Broker 的原因会说明理由，而不是一个没有名字的 TLS 错误：“位于 `<主机>:<端口>` 的 Broker 的证书（指纹的前 16 个字符）正在 MQTT 驱动页面的‘现场设备访问’里等待你的批准。批准之后就开始读取。”被阻止的则显示“……在本工作站已被阻止。请在 MQTT 驱动页面的‘现场设备访问’里审核它。”超出有效期、已吊销或签名损坏的证书会以这个原因被拒绝，且永远不会被列出，因为批准也不能让它变得可靠。

这些决定保存在工作站数据文件夹的 `ua\pki\mqtt` 下，重启后依然有效。操作按钮只对能配置 Connector 的角色显示，而且只在工作站自己的窗口里生效：其他设备上的浏览器只能查看卡片、不能修改，也没有任何代理工具能批准证书（`connector_browse` 用 `awaitingCertificate` 标出这个 Broker 的设备）。列表最多保留 100 份等待批准的证书；列表满了以后，新的证书仍会被拒绝但不会列出，卡片会说明这一点。

## 准备 Broker {#prepare-the-broker}

Broker 必须接受来自工作站所在计算机的连接，并接受工作站的登录。最常见的 Broker Eclipse Mosquitto 2 出厂时两样都不做：mosquitto.conf 中没有 listener 时它只在本机上侦听，有了 listener 后又会拒绝没有用户名的客户端。MQTT 不会说明是哪个 Broker 应答的，所以工作站只把 Mosquitto 当作常见情形来提，从不断定。

| Broker 的应答 | Broker 页面的说明 | 改变它的办法 |
| --- | --- | --- |
| 它所在的计算机拒绝端口 | Broker 只接受来自本机的连接 | 在 mosquitto.conf 中加入 listener 加端口的一行（listener 1883），然后重启 Broker；并在该计算机的防火墙中允许该端口 |
| 它拒绝没有用户名的客户端 | Broker 不接受没有用户名的客户端 | 在“Broker 连接”卡片的用户名和密码中填入 Broker 的账户；在 Mosquitto 2 上，该账户用 mosquitto_passwd 创建，并在 mosquitto.conf 中用 password_file 指定，或者用 allow_anonymous true 放行匿名客户端 |
| 它拒绝用户名或密码 | Broker 自己的拒绝，指向“Broker 连接”卡片上的用户名和密码 | 在 Broker 上或卡片上修改账户 |

前两种会在 Broker 页面上代替其英文错误说明，并附上这些步骤和端口；它下面的设备继续指向 Broker。会话一旦连接，此提示就会自行消失。树中的标记和日志保留英文原因，原因末尾附有同样的 Mosquitto 步骤。

## 安装一个 Broker {#installing-a-broker}

工作站需要一个 Broker 来连接，装置也一样。手上没有的话，Eclipse Mosquitto 是一个以 Windows 服务方式运行的小型 Broker：

1. 从 mosquitto.org 的下载页面获取 Windows 安装程序并运行。它会在 `C:\Program Files\mosquitto` 中安装名为 **Mosquitto Broker** 的服务。
2. Mosquitto 默认只在它运行的那台计算机上应答。要让网络上的装置连进来，在该文件夹的 `mosquitto.conf` 中加上一个监听器和一个密码文件：

   ```
   listener 1883
   password_file C:\Program Files\mosquitto\passwd
   ```

   用工作站和装置将要使用的账户创建这个文件（命令会询问密码）：

   ```
   "C:\Program Files\mosquitto\mosquitto_passwd" -c "C:\Program Files\mosquitto\passwd" station
   ```

3. 在管理员命令提示符中重启服务，并在 Windows Defender 防火墙中允许来自装置网络的 TCP 1883 端口入站：

   ```
   net stop mosquitto
   net start mosquitto
   ```

4. 在 Broker 的页面上，Broker 与工作站在同一台计算机上运行时，在**主机**里填 `localhost`，否则填那台计算机的地址；**端口**填 1883，再填上你创建的账户，然后**启用**。

装置连上之前想先试一个普通点位，可以在同一文件夹中发布一个保留的测试值，再把某个点位的**主题**指向它：

```
"C:\Program Files\mosquitto\mosquitto_pub" -h localhost -u station -P <password> -t plant/line1/temperature -m 21.5 -r
```

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

- MQTT 驱动是客户端，不是 Broker：装置和工作站都连到一个 Broker 上，可以是工作站自己的 [MQTT Broker](connector-mqtt-broker)，也可以是别的 Broker。
- Broker 的发布卡片以 JSON 或 Sparkplug B 之一发出，从不同时发出两种。在 Sparkplug B 中，工作站是边缘节点而不是主要主机应用，也不使用别名、模板或历史值。
- 工作站不是 Sparkplug B 主机应用：它不发布 STATE 消息。设置为发布前先等待主要主机的边缘节点会一直保持沉默。
- 普通 MQTT 不会告诉你某个发布者已经沉默：只要与 Broker 的连接还在，普通点位就一直保留最后收到的值，并以它到达的时刻注明时间，除非设备设置了**无消息的最长时间（秒）**。只在值变化时才发布的主题分不清发布者沉默和数值不变，所以这类设备保持 0。Sparkplug B 会通过死亡证书说出来。
- 会话以干净状态开始：工作站未连接期间发布的内容不会重发，让点位重新有值的是该主题的保留消息。
- 一个点位读一个主题、读其中的一个值，从不读过滤器；属于数组、数据集、模板或属性集的 Sparkplug B 指标不会被读取。
- 只用 TCP，可带或不带 TLS：不支持基于 WebSocket 的 MQTT，不支持客户端证书，也不支持比 MQTT 3.1.1 更旧的 Broker。
- 设备不能移到另一个 Broker 下：在另一个 Broker 下新建，再删除旧的设备。
