BACnet 简介
BACnet 是一种专为智能建筑设计的通信协议,已被国际标准化组织(ISO)、美国国家标准协会(ANSI)及美国采暖、制冷与空调工程师学会(ASHRAE)等机构标准化。它主要用于智能建筑的控制系统,如暖通空调(HVAC)系统、照明控制、门禁、火警检测等设备。BACnet 的优势在于降低了系统维护成本,简化了安装过程,并提供五种行业标准协议,避免了设备供应商的垄断,极大提升了系统的扩展性和兼容性。
实例概述
本文将介绍两个 C# 与 BACnet 服务器通信的实例,代码和 BACnet 模拟器软件下载地址将在文末提供。
第一个实例 :实现AO类型等点值的读取。
第二个实例:实现BO类型等点开关状态的读写功能。
注意:由于端口冲突等原因,BACnet 模拟器和客户端必须在不同的电脑上,并且位于同一网段才能进行测试。
第一个实例:值的读取
在 BACnet 模拟器中添加几个 AO 点,设定值为 26.5,并通过 C# 代码读取该值,关键代码如下:
cs
// 初始化方法
public void init()
{
try
{
// 获取本机所有可用的IP地址
string[] availableIps = GetAvailableIps();
// 遍历获取到的IP地址列表(目前注释掉了日志记录代码)
for (int i = 0; i < availableIps.Length; i++)
//PUBLIC.pubfun.Add2List("获取到有效IP:" + availableIps[i]);
// 调用设备添加方法
AddDevice();
// 延时 5000 毫秒(目前注释掉了,可能是为了调试)
//Thread.Sleep(5000);
// 调用添加点的方法(目前注释掉了,可能是为了调试)
//AddPoint();
}
catch (Exception err)
{
// 捕获异常并记录错误(目前注释掉了日志记录代码)
//PUBLIC.pubfun.Add2List("init bac err:" + err.ToString());
}
}
// 获取本地所有可用的IP地址
public string[] GetAvailableIps()
{
List<string> list = new List<string>();
// 获取所有网络接口
foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces())
{
// 筛选出有效的网络接口,排除只接收的接口和回环接口
if (!networkInterface.IsReceiveOnly && networkInterface.OperationalStatus == OperationalStatus.Up &&
(networkInterface.SupportsMulticast && networkInterface.NetworkInterfaceType != NetworkInterfaceType.Loopback))
{
// 获取IP属性
IPInterfaceProperties ipProperties = networkInterface.GetIPProperties();
// 检查是否有有效的网关地址
if (ipProperties.GatewayAddresses != null && ipProperties.GatewayAddresses.Count != 0 &&
(ipProperties.GatewayAddresses.Count != 1 || !(ipProperties.GatewayAddresses[0].Address.ToString() == "0.0.0.0")))
{
// 遍历获取所有的Unicast地址(IPv4地址)
foreach (UnicastIPAddressInformation addressInformation in ipProperties.UnicastAddresses)
{
if (addressInformation.Address.AddressFamily == AddressFamily.InterNetwork)
list.Add(addressInformation.Address.ToString()); // 添加IPv4地址到列表
}
}
}
}
// 返回IP地址列表
return list.ToArray();
}
// 发送设备查找指令(WHOIS)
public void SendWhoIs()
{
// 清空设备字典
//m_devices.Clear();
dic_devices.Clear();
m_devices[comm].Devices.Clear();
// 发送WHOIS指令,查找设备
comm.WhoIs(-1, -1);
// 等待设备响应
Thread.Sleep(whowait);
}
// 查找并添加设备下的点(此方法较复杂,涉及到不同的处理模式)
public void AddPoint()
{
try
{
lock (dic_devices) // 加锁,防止多线程操作冲突
{
dic_units.Clear(); // 清空点列表
// 遍历所有设备
foreach (var item in dic_devices)
{
foreach (var it in item.Value)
{
// 尝试将设备信息转换为 KeyValuePair 类型
KeyValuePair<BacnetAddress, uint>? nullable = it as KeyValuePair<BacnetAddress, uint>?;
if (!nullable.HasValue)
return; // 如果转换失败,则退出
BacnetAddress key = nullable.Value.Key; // 获取设备的地址
uint num1 = nullable.Value.Value; // 获取设备的ID
// 如果使用的是 MSTP 协议,且源地址为 -1
if (comm.Transport is BacnetMstpProtocolTransport && (int)((BacnetMstpProtocolTransport)comm.Transport).SourceAddress == -1)
{
// 这里留有注释说明,需要补充处理的逻辑
//PUBLIC.pubfun.Add2List("走到了一个特别的地方 需要补充处理。。。");
//PUBLIC.pubfun.log("走到了一个特别的地方 需要补充处理。。。", "");
}
int timeout = comm.Timeout; // 获取通信超时设置
IList<BacnetValue> value_list1 = null; // 存储设备对象列表的变量
try
{
if (readmode == 1) // 如果是多点模式
{
#region 多点模式处理
// 如果设备对象列表为空,尝试读取对象列表
if (value_list1 == null)
{
try
{
// 发送读取设备对象的请求
bool ret = comm.ReadPropertyRequest(
key,
new BacnetObjectId(BacnetObjectTypes.OBJECT_DEVICE, num1),
BacnetPropertyIds.PROP_OBJECT_LIST,
out value_list1, (byte)0,
uint.MaxValue);
// 如果没有响应,重置 value_list1
if (!ret)
{
value_list1 = null;
}
}
catch (Exception ex)
{
// 捕获异常并处理
value_list1 = null;
}
}
// 如果仍然获取不到设备对象,记录错误
if (value_list1 == null)
{
//PUBLIC.pubfun.Add2List("获取不到设备对象通讯终断!");
//PUBLIC.pubfun.log("获取不到设备对象通讯终断!", "");
}
else
{
// 遍历返回的设备对象列表
foreach (BacnetValue bacnetValue in value_list1)
{
BacnetObjectId object_id = (BacnetObjectId)bacnetValue.Value;
// 将设备对象添加到列表中
this.AddObjectEntry(comm, key, null, object_id, num1);
}
}
#endregion
}
if (readmode == 0) // 如果是单点模式
{
#region 单点模式处理
if (value_list1 == null)
{
try
{
// 发送读取设备对象的请求
if (!comm.ReadPropertyRequest(key, new BacnetObjectId(BacnetObjectTypes.OBJECT_DEVICE, num1), BacnetPropertyIds.PROP_OBJECT_LIST, out value_list1, (byte)0, 0))
{
// 获取不到设备对象,记录错误
return;
}
}
catch (Exception ex)
{
// 捕获异常并处理
return;
}
if (value_list1 != null && value_list1.Count == 1 && value_list1[0].Value is uint)
{
uint count = (uint)value_list1[0].Value;
// 如果获取到对象列表,逐个添加
this.AddObjectListOneByOneAsync(comm, key, num1, count);
}
else
{
// 获取不到对象数量,记录错误
}
}
else
{
// 如果有设备对象,直接添加
foreach (BacnetValue bacnetValue in value_list1)
{
BacnetObjectId object_id = (BacnetObjectId)bacnetValue.Value;
this.AddObjectEntry(comm, key, null, object_id, num1);
}
}
#endregion
}
}
finally
{
// 恢复默认光标(此处可以有其他清理操作)
//this.Cursor = Cursors.Default;
}
}
}
}
}
catch (Exception err)
{
// 发生异常时记录错误
//PUBLIC.pubfun.Add2List("AddPoint err:" + err.ToString());
//PUBLIC.pubfun.log("AddPoint err:" , err.ToString());
}
}
// 读取值的主方法
public void ReadTags()
{
// 清空存储单位值和设备值的字典
dic_unit_value.Clear();
dic_values.Clear();
try
{
// 锁定设备字典,避免多线程冲突
lock (dic_devices)
{
// 锁定单位字典
lock (dic_units)
{
// 遍历每个单位
foreach (var units in dic_units)
{
BacnetAddress key = null; // 存储设备地址
BacnetObjectId object_id = new BacnetObjectId(); // 存储对象ID
string m_tagid = "", m_unitid = ""; // 标签ID和单位ID
KeyValuePair<BacnetAddress, uint>? nullable; // 可空的键值对
#region 取出参数基本信息
// 获取单位对应的设备信息
foreach (var it in dic_devices[string.Format("{0}:{1}", units.Key.Split(':')[0], units.Key.Split(':')[1])])
{
nullable = it as KeyValuePair<BacnetAddress, uint>?;
if (!nullable.HasValue)
return; // 如果转换失败,直接返回
key = nullable.Value.Key; // 获取设备地址
uint num1 = nullable.Value.Value; // 获取设备端口号
m_unitid = units.Key; // 获取单位ID
m_tagid = string.Format("{0}:{1}", units.Key.Split(':')[0], units.Key.Split(':')[1]); // 生成标签ID
object_id = (BacnetObjectId)units.Value; // 获取对象ID(如模拟输入等)
}
#endregion
#region 读值操作
try
{
// 创建属性引用数组,准备读取属性
BacnetPropertyReference[] propertyReferenceArray = new BacnetPropertyReference[1]
{
new BacnetPropertyReference(8U, uint.MaxValue) // 属性ID为8(例如,某个特定数据属性)
};
IList<BacnetReadAccessResult> list = null; // 用于存储读取结果的列表
// 判断读取模式
if (readmode == 0)
{
#region 单个属性读取方式
try
{
// 调用单个属性读取方法
if (!this.ReadAllPropertiesBySingle(comm, key, object_id, out list))
{
// 设备通讯发生错误
// 记录错误信息(注释掉的部分表示日志处理)
continue; // 继续下一个单位
}
}
catch (Exception ex2)
{
// 处理读取时发生的异常
continue; // 继续下一个单位
}
#endregion
}
if (readmode == 1)
{
#region 多个属性一起读取的方式
try
{
// 关键的读取请求
if (!comm.ReadPropertyMultipleRequest(key, object_id, (IList<BacnetPropertyReference>)propertyReferenceArray, out list, (byte)0))
{
// 记录设备通讯错误(注释掉的部分表示日志处理)
continue; // 继续下一个单位
}
}
catch (Exception ex1)
{
// 如果多个属性读取失败,尝试单个属性读取
try
{
if (!this.ReadAllPropertiesBySingle(comm, key, object_id, out list))
{
// 记录设备通讯错误(注释掉的部分表示日志处理)
continue; // 继续下一个单位
}
}
catch (Exception ex2)
{
// 处理读取时发生的异常
continue; // 继续下一个单位
}
}
#endregion
}
// 如果读取成功,处理返回的值
if (list[0].values != null)
{
foreach (BacnetPropertyValue bacnetPropertyValue in (IEnumerable<BacnetPropertyValue>)list[0].values)
{
string m_tag_name = bacnetPropertyValue.property.ToString(); // 获取标签名
string m_tag_value = "";
if (bacnetPropertyValue.value[0].Value != null)
m_tag_value = bacnetPropertyValue.value[0].Value.ToString(); // 获取标签值
// 将值存入单位值字典
dic_2_setvalue(dic_unit_value, m_unitid, m_tag_name, m_tag_value);
}
}
}
finally
{
// 释放光标或其他资源(注释掉的部分可以用于恢复状态)
}
#endregion
}
}
}
// 结构变换,对单位值进行重组
#region 结构变换
foreach (var item_value in dic_unit_value)
{
if (!item_value.Value.ContainsKey("PROP_OBJECT_NAME"))
{
continue; // 如果没有对象名称,跳过
}
string m_temp_key = string.Format("{0}:{1}", item_value.Key.Split(':')[0], item_value.Key.Split(':')[1]);
// 将单位值添加到结果字典
dic_2_setvalue(dic_values, m_temp_key + ":" + item_value.Value["PROP_OBJECT_NAME"], "SOURCENAME", item_value.Key);
foreach (var unit_value in item_value.Value)
{
dic_2_setvalue(dic_values, m_temp_key + ":" + item_value.Value["PROP_OBJECT_NAME"], unit_value.Key, unit_value.Value);
}
}
#endregion
}
catch (Exception err)
{
// 处理全局异常,记录错误(注释掉的部分表示日志处理)
// Refresh(); // 或者其他恢复操作
}
}
第二个实例:开关的读写
模拟器设置一个 BO 点,并实现开关状态的读写功能,主要代码如下:
cs
// 读取属性 -------------------------------------------------------------------------
public bool SendReadProperty(
int deviceidx, // 设备索引
uint instance, // 实例
int arrayidx, // 数组索引
BACnetEnums.BACNET_OBJECT_TYPE objtype, // 对象类型
BACnetEnums.BACNET_PROPERTY_ID objprop, // 属性ID
Property property // 属性对象
)
{
// 参数说明:
// Device index (设备索引,用于网络和MAC地址)
// Object Type (对象类型)
// Property ID (属性ID)
// Value returned (返回值)
// 如果设备索引无效或超出范围,返回false
if ((deviceidx < 0) || (deviceidx >= BACnetData.Devices.Count)) return false;
// 获取设备的远程端点信息
IPEndPoint remoteEP = BACnetData.Devices[deviceidx].ServerEP;
if (remoteEP == null) return false;
// 如果属性对象为空,返回false
if (property == null) return false;
// 定义发送和接收的字节数组
Byte[] sendBytes = new Byte[50];
Byte[] recvBytes = new Byte[512];
uint len;
// 设置 BVLL 部分(BACnet Virtual Link Layer)
sendBytes[0] = BACnetEnums.BACNET_BVLC_TYPE_BIP;
sendBytes[1] = BACnetEnums.BACNET_UNICAST_NPDU;
sendBytes[2] = 0x00;
sendBytes[3] = 0x00; // BVLL长度,稍后修正(可能为24)
// 设置 NPDU 部分(Network Protocol Data Unit)
sendBytes[4] = BACnetEnums.BACNET_PROTOCOL_VERSION;
if (BACnetData.Devices[deviceidx].SourceLength == 0)
sendBytes[5] = 0x04; // 控制标志,无目标地址
else
sendBytes[5] = 0x24; // 控制标志,有广播或目标地址
len = 6;
if (BACnetData.Devices[deviceidx].SourceLength > 0)
{
// 获取设备的网络号(例如:2001)
byte[] temp2 = new byte[2];
temp2 = BitConverter.GetBytes(BACnetData.Devices[deviceidx].Network);
sendBytes[len++] = temp2[1];
sendBytes[len++] = temp2[0];
// 获取设备的MAC地址
byte[] temp4 = new byte[4];
temp4 = BitConverter.GetBytes(BACnetData.Devices[deviceidx].MACAddress);
sendBytes[len++] = 0x01; // MAC地址长度
sendBytes[len++] = temp4[0];
sendBytes[len++] = 0xFF; // 跳数计数 = 255
}
// 设置 APDU 部分(Application Protocol Data Unit)
sendBytes[len++] = 0x00; // 控制标志
sendBytes[len++] = 0x05; // 最大APDU长度(1476)
// 创建调用计数器
sendBytes[len++] = (byte)(InvokeCounter);
InvokeCounter = ((InvokeCounter + 1) & 0xFF);
sendBytes[len++] = 0x0C; // 服务选择:读取属性请求
// 设置服务请求部分(APDU的可变部分):
// 设置对象ID(上下文标记)
len = APDU.SetObjectID(ref sendBytes, len, objtype, instance);
// 设置属性ID(上下文标记)
len = APDU.SetPropertyID(ref sendBytes, len, objprop);
// 可选的数组索引
if (arrayidx >= 0)
len = APDU.SetArrayIdx(ref sendBytes, len, arrayidx);
// 修正BVLL长度
sendBytes[3] = (byte)len;
// 创建计时器(我们也可以使用阻塞的recvFrom方法)
Timer ReadPropTimer = new Timer();
try
{
int Count = 0;
using (ReadPropTimer)
{
// 绑定计时器事件
ReadPropTimer.Tick += new EventHandler(Timer_Tick);
while (Count < 3)
{
// 禁用广播
SendUDP.EnableBroadcast = false;
// 发送数据包
SendUDP.Send(sendBytes, (int)len, remoteEP);
// 启动计时器
TimerDone = false;
ReadPropTimer.Interval = 400; // 100毫秒
ReadPropTimer.Start();
while (!TimerDone)
{
// 等待确认响应
Application.DoEvents();
if (SendUDP.Client.Available > 0)
{
// 接收响应数据
recvBytes = SendUDP.Receive(ref remoteEP);
int APDUOffset = NPDU.Parse(recvBytes, 4); // BVLL始终为4字节
// 检查APDU响应
if (recvBytes[APDUOffset] == 0x30) // 确认请求
{
// 验证Invoke ID是否一致
byte ic = (byte)(InvokeCounter == 0 ? 255 : InvokeCounter - 1);
if (ic == recvBytes[APDUOffset + 1])
{
// 解析返回的属性数据
APDU.ParseProperty(ref recvBytes, APDUOffset, property);
return true; // 成功,跳出循环
}
}
}
}
Count++; // 增加重试次数
BACnetData.PacketRetryCount++; // 计数器增加
ReadPropTimer.Stop(); // 停止计时器,准备下次重试
}
return false; // 重试次数已达到上限,返回失败
}
}
finally
{
// 确保计时器停止
ReadPropTimer.Stop();
}
}
public bool /*BACnetStack*/ SendWriteProperty(
int deviceidx, // 设备索引(用于获取网络和MAC地址)
uint instance, // 实例号
int arrayidx, // 数组索引
BACnetEnums.BACNET_OBJECT_TYPE objtype, // 对象类型
BACnetEnums.BACNET_PROPERTY_ID objprop, // 属性ID
Property property, // 属性值
int priority // 优先级
)
{
// 创建并发送一个确认请求
if ((deviceidx < 0) || (deviceidx >= BACnetData.Devices.Count)) return false; // 如果设备索引无效,返回false
IPEndPoint remoteEP = BACnetData.Devices[deviceidx].ServerEP; // 获取远程设备的服务器端点
if (remoteEP == null) return false; // 如果远程端点为空,返回false
if (property == null) return false; // 如果属性为空,返回false
Byte[] sendBytes = new Byte[50]; // 定义发送字节数组
Byte[] recvBytes = new Byte[512]; // 定义接收字节数组
uint len; // 定义数据长度
// BVLL部分(BACnet Virtual Link Layer)
sendBytes[0] = BACnetEnums.BACNET_BVLC_TYPE_BIP; // BVLC类型,BIP表示IP协议
sendBytes[1] = BACnetEnums.BACNET_UNICAST_NPDU; // NPDU类型,表示单播
sendBytes[2] = 0x00; // 保留字段
sendBytes[3] = 0x00; // BVLL长度(可能是24?)
// NPDU部分(Network Protocol Data Unit,网络协议数据单元)
sendBytes[4] = BACnetEnums.BACNET_PROTOCOL_VERSION; // BACnet协议版本
if (BACnetData.Devices[deviceidx].SourceLength == 0)
sendBytes[5] = 0x04; // 控制标志,表示没有目标地址
else
sendBytes[5] = 0x24; // 控制标志,表示有广播或目标地址
len = 6; // 初始长度为6
if (BACnetData.Devices[deviceidx].SourceLength > 0)
{
// 获取网络号(例如:2001)
byte[] temp2 = new byte[2];
temp2 = BitConverter.GetBytes(BACnetData.Devices[deviceidx].Network); // 获取设备网络号
sendBytes[len++] = temp2[1];
sendBytes[len++] = temp2[0];
// 获取MAC地址(例如:0x0D)
byte[] temp4 = new byte[4];
temp4 = BitConverter.GetBytes(BACnetData.Devices[deviceidx].MACAddress); // 获取设备的MAC地址
sendBytes[len++] = 0x01; // MAC地址长度
sendBytes[len++] = temp4[0];
sendBytes[len++] = 0xFF; // 跳数(Hop Count)= 255
}
// APDU部分(Application Protocol Data Unit,应用协议数据单元)
sendBytes[len++] = 0x00; // 控制标志
sendBytes[len++] = 0x05; // 最大APDU长度(1476字节)
// 创建调用计数器(Invoke Counter)
sendBytes[len++] = (byte)(InvokeCounter); // 使用当前InvokeCounter
InvokeCounter = ((InvokeCounter + 1) & 0xFF); // 更新InvokeCounter,防止溢出
sendBytes[len++] = 0x0F; // 服务选择码:表示Write Property请求
// 设置服务请求部分(APDU的可变部分):
len = APDU.SetObjectID(ref sendBytes, len, objtype, instance); // 设置对象ID(上下文标签)
len = APDU.SetPropertyID(ref sendBytes, len, objprop); // 设置属性ID(上下文标签)
// 如果有数组索引,设置数组索引
if (arrayidx >= 0)
len = APDU.SetArrayIdx(ref sendBytes, len, arrayidx);
// 设置要发送的属性值
len = APDU.SetProperty(ref sendBytes, len, property);
// 如果有优先级,设置优先级
if (priority > 0)
len = APDU.SetPriority(ref sendBytes, len, priority);
// 修正BVLL长度
sendBytes[3] = (byte)len;
// 创建定时器(我们也可以使用阻塞式recvFrom)
Timer ReadPropTimer = new Timer();
try
{
using (ReadPropTimer)
{
int Count = 0;
ReadPropTimer.Tick += new EventHandler(Timer_Tick); // 定时器Tick事件
// 循环最多3次发送请求
while (Count < 3)
{
SendUDP.EnableBroadcast = false; // 禁用广播
SendUDP.Send(sendBytes, (int)len, remoteEP); // 发送数据包
// 启动定时器
TimerDone = false;
ReadPropTimer.Interval = 400; // 设置定时器间隔
ReadPropTimer.Start();
// 等待直到定时器完成
while (!TimerDone)
{
// 等待确认响应
Application.DoEvents();
// 如果收到数据包
if (SendUDP.Client.Available > 0)
{
// 接收数据
recvBytes = SendUDP.Receive(ref remoteEP);
// 解析NPDU部分,返回APDU的偏移量
int APDUOffset = NPDU.Parse(recvBytes, 4); // BVLL始终为4字节
// 检查APDU响应类型,确定如何处理
if (recvBytes[APDUOffset] == 0x20) // 确认请求类型
{
// 验证Invoke ID是否一致
byte ic = (byte)(InvokeCounter == 0 ? 255 : InvokeCounter - 1);
if (ic == recvBytes[APDUOffset + 1])
{
return true; // 如果Invoke ID匹配,返回成功
}
}
}
}
Count++; // 增加重试次数
BACnetData.PacketRetryCount++; // 增加重试计数
ReadPropTimer.Stop(); // 停止定时器,准备下一轮循环
}
return false; // 如果3次重试都没有成功,返回失败
}
}
finally
{
ReadPropTimer.Stop(); // 确保定时器停止
}
}
总结
上述两个实例展示了如何在 C# 中实现与 BACnet 服务器的通信。若需要查看完整的代码和调试程序,请下载源码。
源码下载地址:https://download.csdn.net/download/weixin_44643352/90053081?spm=1001.2014.3001.5501