BACnet 简介
BACnet 是一种专为智能建筑设计的通信协议,已被国际标准化组织(ISO)、美国国家标准协会(ANSI)及美国采暖、制冷与空调工程师学会(ASHRAE)等机构标准化。它主要用于智能建筑的控制系统,如暖通空调(HVAC)系统、照明控制、门禁、火警检测等设备。BACnet 的优势在于降低了系统维护成本,简化了安装过程,并提供五种行业标准协议,避免了设备供应商的垄断,极大提升了系统的扩展性和兼容性。
实例概述
本文将介绍两个 C# 与 BACnet 服务器通信的实例,代码和 BACnet 模拟器软件下载地址将在文末提供。
第一个实例:实现AO类型等点值的读取。
第二个实例:实现BO类型等点开关状态的读写功能。
注意:由于端口冲突等原因,BACnet 模拟器和客户端必须在不同的电脑上,并且位于同一网段才能进行测试。
第一个实例:值的读取
在 BACnet 模拟器中添加几个 AO 点,设定值为 26.5,并通过 C# 代码读取该值,关键代码如下:
// 初始化方法
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 协议,且源地址为 -1if (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_list1if (!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(); // 存储对象IDstring m_tagid = "", m_unitid = ""; // 标签ID和单位IDKeyValuePair<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; // 获取单位IDm_tagid = string.Format("{0}:{1}", units.Key.Split(':')[0], units.Key.Split(':')[1]); // 生成标签IDobject_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 点,并实现开关状态的读写功能,主要代码如下:
// 读取属性 -------------------------------------------------------------------------
public bool SendReadProperty(int deviceidx, // 设备索引uint instance, // 实例int arrayidx, // 数组索引BACnetEnums.BACNET_OBJECT_TYPE objtype, // 对象类型BACnetEnums.BACNET_PROPERTY_ID objprop, // 属性IDProperty property // 属性对象
)
{// 参数说明:// Device index (设备索引,用于网络和MAC地址)// Object Type (对象类型)// Property ID (属性ID)// Value returned (返回值)// 如果设备索引无效或超出范围,返回falseif ((deviceidx < 0) || (deviceidx >= BACnetData.Devices.Count)) return false;// 获取设备的远程端点信息IPEndPoint remoteEP = BACnetData.Devices[deviceidx].ServerEP;if (remoteEP == null) return false;// 如果属性对象为空,返回falseif (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; // 控制标志,无目标地址elsesendBytes[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, // 属性IDProperty property, // 属性值int priority // 优先级
)
{// 创建并发送一个确认请求if ((deviceidx < 0) || (deviceidx >= BACnetData.Devices.Count)) return false; // 如果设备索引无效,返回falseIPEndPoint remoteEP = BACnetData.Devices[deviceidx].ServerEP; // 获取远程设备的服务器端点if (remoteEP == null) return false; // 如果远程端点为空,返回falseif (property == null) return false; // 如果属性为空,返回falseByte[] 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; // 控制标志,表示没有目标地址elsesendBytes[5] = 0x24; // 控制标志,表示有广播或目标地址len = 6; // 初始长度为6if (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); // 使用当前InvokeCounterInvokeCounter = ((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