# BACnet

> 连接 BACnet/IP 楼宇自动化设备：设备实例号、按对象类型和实例号寻址、Who-Is 设备发现，以及 presentValue。

**BACnet** 驱动通过 BACnet/IP 和楼宇自动化设备对话。每个点位读的是某一个 BACnet 对象的**当前值**：一个模拟输入的实时读数、一个二进制输出的状态、一个多状态值的位置。在 [Connector](connector) 里的 BACnet 驱动下面添加一台设备；面板只显示这个协议用得到的那些字段。

## 连接 {#connection}

| 字段 | 含义 | 默认值 |
| --- | --- | --- |
| **主机** | BACnet 设备（或者替它应答的那个路由器）的 IP 地址或主机名。设备连接时，名称会被解析成它的 IPv4 地址；这台计算机解析不了的名称会让设备连不上，原因里会写明这一点。 | 空 |
| **端口** | 设备的 UDP 端口，1 到 65535。框留空时 47808（也就是 BACnet/IP 的标准端口）以灰色显示，设备就通过这个端口连上；以 0 保存的设备也是这样显示的。 | 47808 |
| **设备 Id** | 这台设备的实例号，也就是它 Device 对象的编号，在整个 BACnet 网络上唯一。 | 0 |

设备标题栏显示拼好的 endpoint，例如 `bacnet://192.168.0.30:47808/1234`。连接时会发一个请求去问这台设备对象列表的大小，用来确认仪器确实在应答；对象列表本身并不会被走一遍。那个回答就是连接：不回话的设备保持离线，而不是显示成已连接，原因（UDP 端口已被别人占着、一个不是地址的地址）会写进事件页面和当天的日志文件。这个驱动没有轮询间隔字段：它的点位每秒读一次。

## 寻址 {#addressing}

BACnet 的点位靠点位“信号源”卡片里的两个定型字段寻址，没有自由文本地址：

| 字段 | 含义 | 接受的取值 | 默认值 |
| --- | --- | --- | --- |
| **对象类型** | 要读哪一种 BACnet 对象。 | 下面那九种类型 | `AnalogInput` |
| **实例号** | 这个对象在它那一类里的实例编号。 | 0 及以上 | 0 |

提供的对象类型和它们标准的 BACnet 类型码：

| 对象类型 | 类型码 | 当前值里装着 |
| --- | --- | --- |
| `AnalogInput` | 0 | 一个测得的数 |
| `AnalogOutput` | 1 | 一个下达的数 |
| `AnalogValue` | 2 | 一个存着的数 |
| `BinaryInput` | 3 | inactive/active |
| `BinaryOutput` | 4 | inactive/active |
| `BinaryValue` | 5 | inactive/active |
| `MultiStateInput` | 13 | 一个从 1 起的状态号 |
| `MultiStateOutput` | 14 | 一个从 1 起的状态号 |
| `MultiStateValue` | 19 | 一个从 1 起的状态号 |

设备 1234 上的 `AnalogInput` 5，意思就是“那台设备第 5 号模拟输入的当前值”。每一次读取都去要这个对象的 `presentValue` 属性；别的属性寻不了址。“信号源”卡片里那行只读的**源地址（总线）**会随着你编辑显示拼好的 `type,instance` 这一对。

## 数据类型 {#data-types}

**数据类型**选择器提供四种类型。挑和那个对象对得上的那一种：

| 数据类型 | 适合 | 说明 |
| --- | --- | --- |
| Float | 模拟类对象 | Real 型的当前值；double 会被收窄成 Float |
| Boolean | 二进制类对象 | Boolean 型的当前值，或者二进制对象用来应答的枚举值 0 和 1 |
| Int32 | 多状态类以及别的整数值 | 有符号、无符号和枚举的回答都落在这里 |
| String | 字符串类的值 | 设备发来什么文本就是什么文本 |

挑的类型必须和这个对象装着的东西对上。设备答的是别的东西时，那个点位这一次读取就失败：点位转为质量不可用，原因点名到来的是什么（“设备为一个声明为整数的点答了一个 Single 值。”），而不是让一个换算过的数字进到趋势、历史和报警里去。把模拟对象读成布尔，或者把 Real 读成 Int32，正是这种情形。这台设备其余部分照读不误；也不会为一个类型填错的点位重新连接什么。

点位指向的对象设备里没有时，这次读取会带着设备自己的回答失败：原因说明设备在那个地址上没有对象，并点名它发来的错误（`ERROR_CODE_UNKNOWN_OBJECT`），这样地址填错和设备不再应答就能分得开。设备保持连接。

## 写入 {#writes}

**访问**为读写或只写的点位可以被写。一次写入会去设这个对象的 `presentValue`，用的是 BACnet 标准给这类对象规定的类型，不管点位的数据类型是什么：

| 对象 | 写成 | 接受 |
| --- | --- | --- |
| 二进制输入、输出或值 | Enumerated | inactive (0) 或 active (1)，来自 Boolean 点位，或等于 0 或 1 的 Int32 |
| 多状态输入、输出或值 | Unsigned | 一个从 1 起的状态号 |
| 模拟输入、输出或值 | Real | 来自 Float 或 Int32 点位的一个数 |
| 其他任何对象类型 | 点位的数据类型 | Float 写 Real，Boolean 写 Boolean，Int32 写有符号整数，String 写字符串 |

写入时不带任何命令优先级。对象装不下的值，比如给多状态对象写 0，或给二进制对象写 2，在发出任何东西之前就被拒绝，拒绝会说明原因。设备用一个错误来回答这次写入时，点位报告设备发来的错误类别和错误码，而不是假装成功，设备也保持连接。新点位一开始是只读。

## 设备发现 {#discovery}

Connector 的“发现设备”按钮会广播一个 BACnet **Who-Is**，并监听 **I-Am** 回答 10 秒。每一台回答的设备都成为一个候选项，带着它的地址、端口和设备实例号，连接字段都已经填好，可以直接添加；而且它是一答话就出现在列表里，不是等到十秒结束才出现：中途取消的扫描会保留所有已经答过话的东西。扫描不去浏览对象，所以候选项到来时点位数为零；对象类型和实例号那一对，请你自己填。

Who-Is 从 **扫描范围** 卡片指定的网络发出。它的输入框接受该网络上的一个 IPv4 地址，即某台设备的地址，或这台计算机自己在该网络上的地址（例如 `192.168.1.20`），扫描随后只在该网络上广播。留空时，Who-Is 从这台计算机的所有网络发出，因此装有两块网卡、VPN 或 Docker 的计算机也能找到每个网络上的设备。不是完整写出的 IPv4 地址会在输入框下方被拒绝，改正之前“发现设备”按钮保持灰色。

**通过 BACnet 路由器**应答的设备也是同样添加的。它的候选项带着路由器的地址，并在旁边带着这台设备自己所在的网络号和它在那个网络上的地址；你添加它时两者都保留下来，之后每一次读取都发给路由器，并在里面点名那台设备。因此，这样找到的设备读起来和别的没有两样，它的 endpoint 读作“device via router”。手工添加的设备没有路由，直接在它的主机上够到。

## 需要知道的事 {#good-to-know}

- 只支持 BACnet/IP：MS/TP 或 BACnet/SC 的仪器只有通过一个把它们呈现在 BACnet/IP 上的路由器才够得着，而找到它们的正是扫描：设备应答时所走的那条路由会从扫描里保留下来，所以请从发现出来的列表里添加它，而不是手工去打那些字段。
- 在有多个网络接口的计算机上（两块网卡、VPN、Docker），每台设备都通过这台计算机路由其地址所用的接口到达，也就是这里任何其他程序都会用的那个接口；无需选择。
- 本工作站上只有监听设备消息的那个套接字占用 BACnet 标准 UDP 端口 47808，所以同一台计算机上另一个独占该端口的 BACnet 应用会和它冲突。发给设备的请求从系统为每个客户端挑选的端口发出。
- 这个驱动上每一个点位的轮询都固定为一秒；平常那些轮询间隔字段不显示。
