BACnet协议读取与发送
因为项目的需求,需要对接某个厂商的BACnet协议。
…
可以说是这协议的坑真的不少,自学的时候遇到了一大堆问题…
注意
本文档仅对python完成读取与发送BACnet协议数据的流程做出教程,不对其中的BUG做出解释。一切以本人踩坑为准.jpg
我的提问:
我这边的需求:因为BACnet没有主动推送数据的方式,我现在是写了个循环,对device设备号进行轮巡读取。但是每轮光是读取就需要花费3秒时间(600+个device实例)。各位如果有什么方法优化这个轮巡的话可以直接写在评论区进行交流,先感谢各位了!
更新
因为BACnet的扫描与通信注定不能跨网段,如果要实现跨网段搜索,需要BBMD设备。
开发环境
依旧是python≥3.6,使用了BAC0库。直接PIP安装,这里放上相关的GitHub源码,调试的时候可能会用的上。
BAC0-GitHub
BACpypes(BAC0的依赖库)
BACnet相关基础知识
楼宇自动控制网络数据通讯协议(即: A Data Communication Protocol for Building Automation and Control Networks,简称《BACnet协议》)。其相关的基础知识,这里放上百度百科链接,感兴趣的可以去查询,我这边也只是做了初步的了解。
BACnet百度百科
BACnet格式
我这里直接使用了BACnet模拟器创建了一组虚拟的设备,设置了虚拟的参数以供自己调试。感谢网上大神们无私奉献的链接,直接拿来下载用了,这里也提供给大家。
BACnet模拟器:BACnet Simulator,验证码=gcfb
BACnet调试工具:BACnet调试工具,验证码=mjp7
其中,“Device X”称作device,子项目被称作type,子项目中的各项被称作属性和值,也就是property和value。在代码中,因为“-”具有很多特殊含义,于是代码中规定这些属性的名称按照小驼峰式命名规则。(例:present-value属性,在代码中的名称为presentValue)
BACnet代码
BACnet设备查找
BACnet协议允许通过whois方法扫描某一局域网下的BACnet设备。lite参数中的IP表示运行代码的本机IP,并非是BACnet设备或BACnet服务器的IP。
第一大坑:如果使用BACnet模拟器测试,那么BACnet模拟器和代码不能运行在同一电脑上,他们会互相占用对应端口!
顺便测试代码之前,把你的BACnet调试设备关掉,不然它也会占用端口。
import BAC0
myIPAddr = '192.168.1.1/24'
bacnet = BAC0.lite(ip=myIPAddr, )
bacnet.whois()
print(bacnet.whois())
打印输出:
2023-03-27 16:02:35,756 - INFO | Starting BAC0 version 22.9.21 (Lite)
2023-03-27 16:02:35,756 - INFO | Use BAC0.log_level to adjust verbosity of the app.
2023-03-27 16:02:35,756 - INFO | Ex. BAC0.log_level('silence') or BAC0.log_level('error')
2023-03-27 16:02:35,756 - INFO | Starting TaskManager
2023-03-27 16:02:35,757 - INFO | Using ip : 192.169.1.1
2023-03-27 16:02:35,934 - INFO | Starting app...
2023-03-27 16:02:35,935 - INFO | BAC0 started
2023-03-27 16:02:35,935 - INFO | Registered as Simple BACnet/IP App
2023-03-27 16:02:35,966 - INFO | Update Local COV Task started
[('20:0x000000000000', 0), ('20:0x010000000000', 1), ('192.168.1.2', 4194302)]
进程已结束,退出代码0
我这里用的是模拟器,我在模拟器中创建了两个device,显示出了两个模拟设备的网络地址和一个模拟器的地址。
BACnet设备读取
使用bacnet.read()方法。让我们先来读一读源码中的说明:
# 节选自C:\PycharmProjects\bacnet_test\venv\Lib\site-packages\BAC0\core\io\Read.py
def read(
self,
args,
arr_index=None,
vendor_id=0,
bacoid=None,
timeout=10,
show_property_name=False,
):
"""
Build a ReadProperty request, wait for the answer and return the value
:param args: String with <addr> <type> <inst> <prop> [ <indx> ]
:returns: data read from device (str representing data like 10 or True)
*Example*::
import BAC0
myIPAddr = '192.168.1.10/24'
bacnet = BAC0.connect(ip = myIPAddr)
bacnet.read('2:5 analogInput 1 presentValue')
Requests the controller at (Network 2, address 5) for the presentValue of
its analog input 1 (AI:1).
"""
bacnet.read()函数接收一个类型为str的参数,字符串中的参数以空格为分隔,包含 addr type inst prop 四个参数.
之前我的模拟器输出了以下的device:
[('20:0x000000000000', 0), ('20:0x010000000000', 1), ('192.168.1.2', 4194302)]
在模拟器这里实际有用的只有前两个。我们以(‘20:0x010000000000’, 1) → AnalogInput 2 → presentValue作为测试项,那么
addr:20:0x010000000000
type:analogValue
inst:2
prop:presentValue
这就是最后的答案了。将其变成read函数需要的字符串参数,就是:'20:0x010000000000 analogValue 2 presentValue'
最终读取代码:
import BAC0
myIPAddr = '192.168.1.1/24'
bacnet = BAC0.lite(ip=myIPAddr, )
# bacnet.whois() # 可以注释掉,这个的扫描时间比较长
# print(bacnet.whois())
data = bacnet.read('20:0x010000000000 analogValue 2 presentValue')
print(data)
完事了,你学会基本读取操作了
BACnet写入操作
同上,看看bacnet.write()源码说明:
def write(self, args, vendor_id=0, timeout=10):
"""Build a WriteProperty request, wait for an answer, and return status [True if ok, False if not].
:param args: String with <addr> <type> <inst> <prop> <value> [ <indx> ] - [ <priority> ]
:returns: return status [True if ok, False if not]
*Example*::
import BAC0
bacnet = BAC0.lite()
bacnet.write('2:5 analogValue 1 presentValue 100 - 8')
Direct the controller at (Network 2, address 5) to write 100 to the presentValues of
its analogValue 1 (AV:1) at priority 8
"""
和上面读取没啥大区别,多了一个‘-’和一个优先级。
bacnet.write('20:0x010000000000 analogValue 2 presentValue 12345.0 - 1')
就直接这么写入就可以了。
AND其他…
没错,测试环境很快就通过了,我就信心满满的实际测试了。然后就报错了
bacpypes.errors.InvalidTag: integer application tag required
…TAG标签错误???通过对代码的debug,找到了报错的触发代码:
def decode(self, tag):
if (tag.tagClass != Tag.applicationTagClass) or (tag.tagNumber != Tag.realAppTag):
raise InvalidTag("real application tag required")
if len(tag.tagData) != 4:
raise InvalidTag("invalid tag length")
就在(tag.tagNumber != Tag.realAppTag)这个判断上。debug告诉我,tag.tagNumber的值是3,Tag.realAppTag是源码中规定的值4。我到这里才去审查厂家给我提供的BACnet服务器的对接数据,结果发现他们的presentValue类型是Long,而不是Real。
这个时候,我坚定的认为自己在read和write函数里少填写了参数,或者用错函数了,导致数据类型不匹配(你想想,传的参数是个字符串啊,哪能规定数据类型),找到最后,除了在GitHub上找到了一个同样的问题但是没有回答,完全找不到任何的踪迹。于是这个时候我才死心塌地的去对着源码一步一步看。
BACpypes库中没有提供非标准数据类型接口!!!!!
我谢谢他啊…于是没办法,最后根据源码,直接修改了venv/lib/python3.6/site-packages/bacpypes/object.py
源码中的数据类型:
@register_object_type
class AnalogValueObject(Object):
objectType = 'analogValue'
_object_supports_cov = True
properties = \
[ ReadableProperty('presentValue', Integer) # 原数据类型为Real
, ReadableProperty('statusFlags', StatusFlags)
, ReadableProperty('eventState', EventState)
, OptionalProperty('reliability', Reliability)
, ReadableProperty('outOfService', Boolean)
, ReadableProperty('units', EngineeringUnits)
, OptionalProperty('minPresValue', Real)
, OptionalProperty('maxPresValue', Real)
, OptionalProperty('resolution', Real)
, OptionalProperty('priorityArray', PriorityArray)
, OptionalProperty('relinquishDefault', Real)
, OptionalProperty('covIncrement', Real)
, OptionalProperty('timeDelay', Unsigned)
, OptionalProperty('notificationClass', Unsigned)
, OptionalProperty('highLimit', Real)
, OptionalProperty('lowLimit', Real)
, OptionalProperty('deadband', Real)
, OptionalProperty('limitEnable', LimitEnable)
, OptionalProperty('eventEnable', EventTransitionBits)
, OptionalProperty('ackedTransitions', EventTransitionBits)
, OptionalProperty('notifyType', NotifyType)
, OptionalProperty('eventTimeStamps', ArrayOf(TimeStamp, 3))
, OptionalProperty('eventMessageTexts', ArrayOf(CharacterString, 3))
, OptionalProperty('eventMessageTextsConfig', ArrayOf(CharacterString, 3))
, OptionalProperty('eventDetectionEnable', Boolean)
, OptionalProperty('eventAlgorithmInhibitRef', ObjectPropertyReference)
, OptionalProperty('eventAlgorithmInhibit', Boolean)
, OptionalProperty('timeDelayNormal', Unsigned)
, OptionalProperty('reliabilityEvaluationInhibit', Boolean)
, OptionalProperty('minPresValue', Real)
, OptionalProperty('maxPresValue', Real)
, OptionalProperty('resolution', Real)
, OptionalProperty('faultHighLimit', Real)
, OptionalProperty('faultLowLimit', Real)
, OptionalProperty('currentCommandPriority', OptionalUnsigned)
, OptionalProperty('valueSource', ValueSource)
, OptionalProperty('valueSourceArray', ArrayOf(ValueSource, 16))
, OptionalProperty('lastCommandTime', TimeStamp)
, OptionalProperty('commandTimeArray', ArrayOf(TimeStamp, 16))
, OptionalProperty('auditablePriorityFilter', OptionalPriorityFilter)
]
就是我写了注释的那行,改完,跑起来了。
总结一下就是,我再也不想用python碰BACnet了,为什么都21.9.21版本了,BAC0库还是没有去尝试修改依赖库或是重写这部分的代码,传参竟然用的是字符串,以空格作为split的参数去分割参数…现在BACnet很多名称中都带有空格了,这库完全没有考虑到兼容性问题。
结束
国内基本都是各种复制粘贴的那三行代码,上面这么多很简单的问题,其实踩过一次坑就明白什么情况了,包括也没人对其中的参数做出一个说明,可惜没有人带我了解一遍这库。希望这篇文章能解决一部分人的问题吧。