目录
BACnet
使用软件
资源
模拟器
使用Java主动查
引入maven
创建网络对象
获取远程设备
获取设备属性
使用DeviceEventAdapter订阅
初始化本地BACnet设备和IP网络配置:
启动本地设备和添加监听器:
搜寻远程设备:
发送订阅COV报文:
修改值并等待:
SubscribeDevice监听器:
BACnet
BACnet(Building Automation and Control Network)是一种常用于楼宇自动化和控制系统的通信协议,它允许设备在楼宇管理系统中进行相互通信和控制。在Java中进行BACnet物联网操作,我们可以使用BACnet4J库,它是一个用于BACnet通信的Java库。
使用软件
资源
由于是个人学习,所以一般不会有真实硬件可以测试,所以我们还需要准备模拟器。
在这方面,我已经准备好了资源。
链接:https://pan.baidu.com/s/1Pd1cTpOkYZ9p4tbrAUihUg
提取码:w62j
模拟器
拿其中一个模拟器Yabe来配合这次学习
安装好后,我们只需要用到这两个功能即可。
我们点击Simulator功能,会弹出这么一个模拟框。
这是模拟一个真实设备温度设备,底下的deviceId则为设备唯一标识。
我们同时打开多台,就可以发现他们的唯一标识不一样。
接下来打开Yabe
选择Add device,然后输入本机ip地址
然后你就会发现,它把我们刚刚打开的两台模拟设备扫描进去了。
选中其中一台设备后,下面Address Space会显示几行数据,对某个数据右键选择订阅后,你会发现,我们可以拿到模拟设备中的某个值的实时数据。
从而,我们能判断出,哪个属性值对应的是模拟设备中的什么,基于软件对设备的通讯到此就结束了。
重点在于我们如何使用Java来使用BACnet进行设备之间的通讯。
使用Java主动查
引入maven
在开始示例之前,请确保已经下载并配置了BACnet4J库。
<!-- https://github.com/infiniteautomation/BACnet4J -->
<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>bacnet4j</artifactId>
<version>6.0.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/bacnet/bacnet4j-6.0.0.jar</systemPath>
</dependency>
通过测试,单引用bacnet4j依赖,是远远不够的。使用起来会报各种异常,
比如
- slf4j的 NoClassDefFoundError: org/slf4j/LoggerFactory
- warp的 NoClassDefFoundError: lohbihler/warp/WarpScheduledExecutorService
- commons的 NoClassDefFoundError: org/apache/commons/lang3/StringUtils
因此还需要引入以下maven依赖
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.32</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>ai.serotonin.oss</groupId>
<artifactId>sero-scheduler</artifactId>
<version>1.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/ai.serotonin.oss/sero-warp -->
<dependency>
<groupId>ai.serotonin.oss</groupId>
<artifactId>sero-warp</artifactId>
<version>1.0.0</version>
</dependency>
创建网络对象
首先获取网段内的模拟设备,
//创建网络对象
IpNetwork ipNetwork = new IpNetworkBuilder()
//本机的ip
.withLocalBindAddress("192.168.1.12")
//掩码和长度,如果不知道本机的掩码和长度的话,可以使用代码的工具类IpNetworkUtils获取
.withSubnet("255.255.255.0", 24)
//默认的UDP端口
.withPort(47808)
.withReuseAddress(true)
.build();
//创建虚拟的本地设备,随意设置一个deviceId 设备的唯一标识
LocalDevice localDevice = new LocalDevice(LOCAL_DEVICE_ID, new DefaultTransport(ipNetwork));
//初始化本地设备
localDevice.initialize();
//搜寻网段内远程设备
localDevice.startRemoteDeviceDiscovery();
如果是ubuntu的情况下,建议去掉 withLocalBindAddress("192.168.1.12"),它会默认监听"0.0.0.0"这个地址,达到一样的效果。
跨网段的话,修改withSubnet()方法
获取远程设备
可以使用 LocalDevice 对象来获取远程设备,根据业务来选择以下方法:
//某个远程设备的id
private static final Integer REMOTE_DEVICE_ID = 311400;
//获取某个远程设备,REMOTE_DEVICE_ID是远程设备ID
RemoteDevice remoteDevice = localDevice.getRemoteDeviceBlocking(REMOTE_DEVICE_ID);
//获取所有远程设备
List<RemoteDevice> remoteDevices = localDevice.getRemoteDevices();
通过debug得知,获取到的数据没有区别。
获取设备属性
在物联网中,有一个叫物模型的抽象思想,其中有一个概念叫做属性,简单的说,一个温度检测设备里,它的在线离线是一个属性,温度也是一个属性。
接下来我们就去获取这个设备的所有属性。
//获取远程设备的标识符对象
List<ObjectIdentifier> objectList = RequestUtils.getObjectList(localDevice,remoteDevice).getValues();
通过debug可以看到这个属性集合中的数据
是否觉得眼熟呢?是的,他就是对应上面进行订阅步骤的Address Space数据
这里就是它的属性。
通过对比,我们可以知道Analog_Input:0是我们需要的温度属性。
使用Java8获取温度属性对象
List<ObjectIdentifier> filter = objectList.stream().filter(e -> e.getObjectType().equals(ObjectType.analogInput) && e.getInstanceNumber() == 0).collect(Collectors.toList());
然后循环不断的获取该属性值的实时数据
while (true) {
//根据对象属性标识符的类型进行取值操作 [测试工具模拟的设备点位的属性有objectName、description、present-value等等]
//analog-input
PropertyValues pvAiObjectName = readValueByProperty(localDevice, remoteDevice, filter, null, PropertyIdentifier.objectName);
PropertyValues pvAiPresentValue = readValueByProperty(localDevice, remoteDevice, filter, null, PropertyIdentifier.presentValue);
PropertyValues pvAiDescription = readValueByProperty(localDevice, remoteDevice, filter, null, PropertyIdentifier.description);
for (ObjectIdentifier oi : filter) {
//取出点位对象不同类型分别对应的值
System.out.println(oi.getObjectType().toString() + " " + oi.getInstanceNumber() + " Name: " + pvAiObjectName.get(oi, PropertyIdentifier.objectName).toString());
System.out.println(oi.getObjectType().toString() + " " + oi.getInstanceNumber() + " PresentValue: " + pvAiPresentValue.get(oi, PropertyIdentifier.presentValue).toString());
System.out.println(oi.getObjectType().toString() + " " + oi.getInstanceNumber() + " Description: " + pvAiDescription.get(oi, PropertyIdentifier.description).toString());
}
Thread.sleep(1000);
}
抽一个通用的方法 readValueByProperty
public static PropertyValues readValueByProperty(final LocalDevice localDevice, final RemoteDevice d,final List<ObjectIdentifier> ois, final ReadListener callback, PropertyIdentifier propertyIdentifier) throws BACnetException
{
if (ois.size() == 0) {
return new PropertyValues();
}
final PropertyReferences refs = new PropertyReferences();
for (final ObjectIdentifier oid : ois) {
refs.add(oid, propertyIdentifier);
}
return RequestUtils.readProperties(localDevice, d, refs, false, callback);
}
然后不断的读取和打印
这样就可以不断获取最新的实时值。
使用DeviceEventAdapter订阅
-
初始化本地BACnet设备和IP网络配置:
//创建网络对象 IpNetwork ipNetwork = new IpNetworkBuilder() //本机的ip .withLocalBindAddress("192.168.1.12") //掩码和长度,如果不知道本机的掩码和长度的话,可以使用代码的工具类IpNetworkUtils获取 .withSubnet("255.255.255.0", 24) //默认的UDP端口 .withPort(47808) .withReuseAddress(true) .build(); //创建虚拟的本地设备,随意设置一个deviceId 设备的唯一标识 LocalDevice localDevice = new LocalDevice(LOCAL_DEVICE_ID, new DefaultTransport(ipNetwork));
- 首先,它创建了一个本地BACnet设备,并指定设备号为123,并为其分配了一个默认的传输实例(DefaultTransport)。
- 然后,它使用
IpNetworkBuilder
构建了一个IP网络配置,包括本地绑定地址、子网掩码和长度、默认UDP端口和地址重用等,
-
启动本地设备和添加监听器:
- 接下来,它初始化本地设备,然后添加了一个
SubscribeDevice
的监听器,该监听器继承自DeviceEventAdapter
。
//初始化本地设备 localDevice.initialize(); //添加监听器 localDevice.getEventHandler().addListener(new SubscribeDevice());
- 接下来,它初始化本地设备,然后添加了一个
-
搜寻远程设备:
- 调用
localDevice.startRemoteDeviceDiscovery()
方法,搜寻网段内的远程BACnet设备。
//搜寻网段内远程设备 localDevice.startRemoteDeviceDiscovery();
- 调用
-
发送订阅COV报文:
- 接下来,它使用
localDevice.send
方法发送了一个订阅COV请求(SubscribeCOVRequest)到远程设备,以订阅特定对象的变化。 - 订阅COV请求包括订阅标识、被监视对象的对象标识符、是否要发送确认报文以及订阅的时长。
-
//发送订阅COV报文 对应为订阅标识(不可为0),订阅对象,是否要发送确认报文,订阅时长(0为永久) localDevice.send(remoteDevice, new SubscribeCOVRequest(new UnsignedInteger(1), new ObjectIdentifier(ObjectType.analogInput, 0), Boolean.TRUE, new UnsignedInteger(0))).get();
- 接下来,它使用
-
修改值并等待:
- 之后,进入一个无限循环,每隔2秒向远程设备的某个对象(ObjectType.analogInput, 0)写入值77。
- 这会触发远程设备发送COV通知给本地设备。
-
while (true){ //修改值为77 RequestUtils.writePresentValue(localDevice, remoteDevice, new ObjectIdentifier(ObjectType.analogValue, 0), new Real(77)); Thread.sleep(2000); }
-
SubscribeDevice监听器:
SubscribeDevice
是一个自定义的监听器类,继承自DeviceEventAdapter
。- 它实现了
covNotificationReceived
方法,该方法在收到COV通知时被调用,处理实时数据更新。
-
class SubscribeDevice extends DeviceEventAdapter { @Override public void covNotificationReceived(final UnsignedInteger subscriberProcessIdentifier, final ObjectIdentifier initiatingDevice, final ObjectIdentifier monitoredObjectIdentifier, final UnsignedInteger timeRemaining, final SequenceOf<PropertyValue> listOfValues) { if (listOfValues.get(0).getPropertyArrayIndex()!=null) { System.out.println(listOfValues.get(0).getValue()); System.out.println("==========================================================="); } } }
通过这种方法不断的获取最新值。