简介
BACnet(A Data Communication Protocol for Building Automation and Control Network)是
一种为楼宇自动控制网络所制定的数据通信协议,它由美国冷暖空调工程师协会组织的标准
项目委员会 135P(Standard Project Committee: SPC 135P)于 1995 年 6 月制定。BACnet 标
准产生的背景是用户对楼宇自动控制设备互操作性(Interoperability)的广泛要求,即将不
同厂家的设备组成一个一致的自控系统。BACnet 实现楼宇自控设备的互操作性的思想是这
样的,一般楼宇自控设备从功能上讲分为两部分,一部分专门处理设备的控制功能,另一部
分专门处理设备的数据通信功能,不同厂商生产的设备使用各自专门的数据通信的方式,所
以不同厂商的设备之间没有很好的互操作性。BACnet 就是要建立一种统一的数据通信的标
准,用于设备的通信部分,从而使得按这种标准生产的设备,都可以进行通信,实现互操作
性。BACnet 标准只是规定了楼宇自控设备之间要进行“对话”所必须遵守的规则,并不涉
及如何实现这些规则,各厂商可以用不断进步的技术来开发,从而使得整个领域的技术不断
进步。
具体的网络协议文档可以私聊楼主获取
Java+BACnet获取写入设备数据
1导入maven依赖
<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>bacnet4j</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>lohbihler</groupId>
<artifactId>sero-scheduler</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>lohbihler</groupId>
<artifactId>sero-warp</artifactId>
<version>1.0.0</version>
</dependency>
2读取设备数据参考
public static LocalDevice localDevice;
/**
* 初始化本地虚拟设备
* @param localDevice
* @throws Exception
*/
public static LocalDevice initLocalDevice(LocalDevice localDevice) throws Exception {
log.info("初始化本地虚拟设备");
if (localDevice == null || !localDevice.isInitialized()) {
//创建网络对象
IpNetwork ipNetwork = new IpNetworkBuilder()
//本机的ip
.withLocalBindAddress(GlobalConstants.LOCAL_IP)
//掩码和长度,如果不知道本机的掩码和长度的话,可以使用代码的工具类IpNetworkUtils获取
//本来是 255.255.255.0改成 网段的网关
.withSubnet(GlobalConstants.LOCAL_GATEWAY, 24)
//默认的UDP端口
.withPort(47808)
.withReuseAddress(true)
.build();
//创建虚拟的本地设备,deviceNumber随意
localDevice = new LocalDevice(223, new DefaultTransport(ipNetwork));
//初始化本地设备
localDevice.initialize();
//搜寻网段内远程设备
localDevice.startRemoteDeviceDiscovery();
}
return localDevice;
}
/**
* 获取设备数据
*/
public static List<Map<String, String>> requestDeviceData(Integer deviceId, LocalDevice localDevice) throws Exception {
Integer REMOTE_DEVICE_ID = deviceId;
try {
//获取远程设备,instanceNumber是远程设备ID
RemoteDevice remoteDevice = localDevice.getRemoteDeviceBlocking(REMOTE_DEVICE_ID);
//获取远程设备的标识符对象
List<ObjectIdentifier> objectList = RequestUtils.getObjectList(localDevice, remoteDevice).getValues();
List<ObjectIdentifier> aiList = new ArrayList<>();
log.info("<===================对象标识符的对象类型,实例数(下标)===================>");
//Object所有标识符
for (ObjectIdentifier oi : objectList) {
log.info(oi.getObjectType().toString() + "," + oi.getInstanceNumber());
aiList.add(new ObjectIdentifier(oi.getObjectType(), oi.getInstanceNumber()));
}
log.info("<==================================================================>");
log.info("取值开始");
//根据对象属性标识符的类型进行取值操作 [测试工具模拟的设备点位的属性有objectName、description、present-value等等]
//analog-input
log.info("属性读取封装成PropertyValues对象开始");
PropertyValues pvAiObjectName = readValueByPropertyIdentifier(localDevice, remoteDevice, aiList, null, PropertyIdentifier.objectName);
PropertyValues pvAiPresentValue = readValueByPropertyIdentifier(localDevice, remoteDevice, aiList, null, PropertyIdentifier.presentValue);
log.info("属性读取封装成PropertyValues对象结束");
List<Map<String, String>> res = new ArrayList<>();
log.info("属性读取封装成Map开始==>");
for (ObjectIdentifier oi : aiList) {
String presentValue=pvAiPresentValue.getString(oi, PropertyIdentifier.presentValue);
String objectName=pvAiObjectName.getString(oi, PropertyIdentifier.objectName);
//取出点位对象不同类型分别对应的值
String logStr="电箱ID:"+deviceId+",instanceNumber:"+oi.getInstanceNumber()+",object-name:"+objectName+",PresentValue:" + presentValue;
log.info("过滤前的数据:电箱ID:"+deviceId+",object-type:" + oi.getObjectType().toString() + ",instanceNumber" + oi.getInstanceNumber() + ",Name:" + objectName + ",PresentValue:" + presentValue);
HashMap<String, String> objectObjectHashMap = new HashMap<>();
objectObjectHashMap.put(PropertyIdentifier.objectName.toString(), objectName);
objectObjectHashMap.put(PropertyIdentifier.presentValue.toString(), presentValue);
res.add(objectObjectHashMap);
}
log.info("属性读取封装成Map结束");
log.info("取值结束");
return res;
} catch (Exception e) {
log.info("异常信息:" + e.getMessage());
} finally {
// if (null != localDevice) {
// //shutdown
// localDevice.terminate();
// }
}
return null;
}
/**
* 写入设备状态
*/
public static void writeDeviceProperty(Integer deviceId, LocalDevice localDevice, Integer instanceNumber, Integer status) throws Exception{
//获取远程设备,instanceNumber是远程设备ID
log.info("开始获取远程设备");
RemoteDevice remoteDevice = localDevice.getRemoteDeviceBlocking(deviceId);
log.info("开始写入数据");
//修改属性值
try {
log.info("ObjectIdentifier:{}",new ObjectIdentifier(ObjectType.multiStateValue, instanceNumber));
RequestUtils.writePresentValue(localDevice, remoteDevice,new ObjectIdentifier(ObjectType.multiStateValue, instanceNumber),new UnsignedInteger(status));
log.info("写入成功:");
Thread.sleep(2000);
}catch (Exception e){
log.info("写入失败:"+e.getMessage());
}
}
3通过订阅设备某个属性后动态监听值的变化接收
//发送订阅COV报文 对应为订阅标识(不可为0),订阅对象,是否要发送确认报文,订阅时长(0为永久)
localDevice.send(remoteDevice, new SubscribeCOVRequest(new UnsignedInteger(1), new ObjectIdentifier(ObjectType.analogInput, 0), Boolean.TRUE, new UnsignedInteger(0))).get();
localDevice.send(remoteDevice, new SubscribeCOVRequest(new UnsignedInteger(2), new ObjectIdentifier(ObjectType.analogInput, 1), Boolean.TRUE, new UnsignedInteger(0))).get();
localDevice.send(remoteDevice, new SubscribeCOVRequest(new UnsignedInteger(3), new ObjectIdentifier(ObjectType.analogInput, 2), Boolean.TRUE, new UnsignedInteger(0))).get();
//加入监听器
localDevice.getEventHandler().addListener(new SubscribeDevice());
package com.zwh.test;
import com.serotonin.bacnet4j.event.DeviceEventAdapter;
import com.serotonin.bacnet4j.type.constructed.PropertyValue;
import com.serotonin.bacnet4j.type.constructed.SequenceOf;
import com.serotonin.bacnet4j.type.primitive.ObjectIdentifier;
import com.serotonin.bacnet4j.type.primitive.UnsignedInteger;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SubscribeDevice extends DeviceEventAdapter {
@Override
public void covNotificationReceived(final UnsignedInteger subscriberProcessIdentifier,
final ObjectIdentifier initiatingDevice, final ObjectIdentifier monitoredObjectIdentifier,
final UnsignedInteger timeRemaining, final SequenceOf<PropertyValue> listOfValues) {
log.info(">>>>>>>>>>>>>>>>>监听到数据变化");
listOfValues.getValues().forEach(item->{
System.out.println(monitoredObjectIdentifier+","+item.getPropertyIdentifier()+","+item.getValue());
});
}
}
使用BacnetScan客户端软件模拟获取设备数据
1下载BacnetScan软件
2打开,设置网口
确定后双击,可以看到已经获取到设备数据了
随机点开一个
可以看到每个设备里面有很多不通类型的属性,对于我们要关注的属性主要是两个object-name,present-value
一个对于属性名和其对应的值
注意点踩坑
1bacnet协议不支持跨网段,也就是说你设备读取数据服务端软件和你写的代码对应客户端所在服务器要在同一网段上,如果需要跨网段的话则需要一种设备支持,那个设备就是BBMD,不过一般情况下是没有采购该设备的。
2客户端冲突问题,比如我们在服务器上使用BacnetScan客户端软件打开获取设备数据,然后没有关掉,这时我们在编写代码放到服务器上运行,发现设备初始化后连接设备是失败的,要特别注意。两个客户端是会冲突的,这时只要关闭BacnetScan然后重新运行代码
3在写入设备状态值更改设备状态时
RequestUtils.writePresentValue(localDevice, remoteDevice,new ObjectIdentifier(ObjectType.multiStateValue, instanceNumber),new UnsignedInteger(status));
注意ObjectType.multiStateValue,new UnsignedInteger(status)
这个设备状态值的类型和对应封装的类型,ObjectType对于的类型有很多,发现写入失败后就多尝试一下设置几种类型都试一下,很多情况下可能是设备类型不匹配导致写入失败
4bacnet对于maven依赖对于jar包下载问题,由于各种原因可能导致一直下载不来,这时可以去网上把对应jar包下载到自己本地仓库上然后导入对应依赖
5通过BacnetScan客户端软件还是代码运行获取设备数据我们可以看到有很多都是无用的数据,注意过滤掉
6初始化本地虚拟设备去搜寻远端设备信息时配置
//创建网络对象
IpNetwork ipNetwork = new IpNetworkBuilder()
//本机的ip
.withLocalBindAddress(GlobalConstants.LOCAL_IP)
//掩码和长度,如果不知道本机的掩码和长度的话,可以使用代码的工具类IpNetworkUtils获取
//本来是 255.255.255.0改成 网段的网关
.withSubnet(GlobalConstants.LOCAL_GATEWAY, 24)
//默认的UDP端口
.withPort(47808)
.withReuseAddress(true)
.build();
通过对接不同厂商发现有几种配置
1基于统一网段的网关地址+本地电脑ip
2基于统一网段的子网掩码+广播地址+本地电脑ip
这里广播地址可以通过BacnetScan工具测试的日志中找到
感兴趣的一起可以交流https://t.zsxq.com/7AspS,平时也会分享一些公司碰到的问题和具体解决办法。