From c671de0aa38b7619ee3e38c25e77930942f0491f Mon Sep 17 00:00:00 2001 From: Aliaksandr Nikitsin Date: Sat, 30 May 2026 17:58:26 +0000 Subject: [PATCH 1/9] add whois request --- .../protocol/python/src/protocol_bacnet.py | 47 ++++++++++++++++--- .../protocol/python/src/protocol_module.py | 18 +++---- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/modules/test/protocol/python/src/protocol_bacnet.py b/modules/test/protocol/python/src/protocol_bacnet.py index aaa41b224..359e1a6c1 100644 --- a/modules/test/protocol/python/src/protocol_bacnet.py +++ b/modules/test/protocol/python/src/protocol_bacnet.py @@ -14,6 +14,7 @@ """Module to run all the BACnet related methods for testing""" import BAC0 +from bacpypes3.pdu import Address from dataclasses import dataclass import logging import json @@ -64,12 +65,13 @@ def __init__(self, self.device_hw_addr = device_hw_addr self._bin_dir = bin_dir - async def discover(self, local_ip): + async def discover(self, local_ip, device_ip): LOGGER.info('Performing BACnet discovery...') - self.bacnet = BAC0.lite(local_ip) + self.devices = [] + self.bacnet = BAC0.connect(local_ip) LOGGER.info('Local BACnet object: ' + str(self.bacnet)) try: - await self.bacnet._discover(global_broadcast=True) # pylint: disable=protected-access + await self.bacnet._discover(global_broadcast=True, timeout=10) # pylint: disable=protected-access except Exception as e: # pylint: disable=W0718 LOGGER.error(e) LOGGER.info('BACnet discovery complete') @@ -77,16 +79,47 @@ async def discover(self, local_ip): bac0_log = f.read() LOGGER.info('BAC0 Log:\n' + bac0_log) # Extract discovered devices as a BACnetDevice. - self.devices = [] + LOGGER.info('discoveredDevices: ' + str(self.bacnet.discoveredDevices)) if self.bacnet.discoveredDevices is not None: for device_info in self.bacnet.discoveredDevices.values(): - self.devices.append( - BACnetDevice( + device = BACnetDevice( device_id=str(device_info['object_instance'][1]), ip=str(device_info['address']) ) - ) + LOGGER.info(f'Discovered BACnet device: {device}') + self.devices.append(device) LOGGER.info('BACnet devices found: ' + str(len(self.devices))) + self.bacnet.discoveredDevices = {} + try: + await self.bacnet._discover(timeout=10) # pylint: disable=protected-access + except Exception as e: # pylint: disable=W0718 + LOGGER.error(e) + LOGGER.info('BACnet discovery complete') + with open(BAC0_LOG, 'r', encoding='utf-8') as f: + bac0_log = f.read() + LOGGER.info('BAC0 Log:\n' + bac0_log) + LOGGER.info('discoveredDevices: ' + str(self.bacnet.discoveredDevices)) + if self.bacnet.discoveredDevices is not None: + for device_info in self.bacnet.discoveredDevices.values(): + device = BACnetDevice( + device_id=str(device_info['object_instance'][1]), + ip=str(device_info['address']) + ) + LOGGER.info(f'Discovered BACnet device: {device}') + res = await self.bacnet.this_application.app.who_is( + low_limit=0, + high_limit=4194303, + address=Address(f'{device_ip}:47808'), + timeout=10, + ) + LOGGER.info(f'WhoIs result: {res}') + for iam in res: + instance = iam.iAmDeviceIdentifier[1] + obj_type = iam.iAmDeviceIdentifier[0] + address = str(iam.pduSource) + vendor_id = getattr(iam, 'vendorID', None) + LOGGER.info(f'''Device type: {obj_type}, instance: {instance}, + address: {address}, vendor_id: {vendor_id}''') # Check if the device being tested is in the discovered devices list # discover needs to be called before this method is invoked diff --git a/modules/test/protocol/python/src/protocol_module.py b/modules/test/protocol/python/src/protocol_module.py index 95a43a7c9..8077b88ee 100644 --- a/modules/test/protocol/python/src/protocol_module.py +++ b/modules/test/protocol/python/src/protocol_module.py @@ -27,17 +27,11 @@ class ProtocolModule(TestModule): def __init__(self, module): self._supports_bacnet = False - self._bacnet_loop = None super().__init__(module_name=module, log_name=LOG_NAME) global LOGGER LOGGER = self._get_logger() self._bacnet = BACnet(log=LOGGER, device_hw_addr=self._device_mac) - def _get_bacnet_loop(self): - if self._bacnet_loop is None: - self._bacnet_loop = asyncio.new_event_loop() - return self._bacnet_loop - def _protocol_valid_bacnet(self): LOGGER.info('Running protocol.valid_bacnet') result = None @@ -53,8 +47,11 @@ def _protocol_valid_bacnet(self): local_address = self.get_local_ip(interface_name) if local_address: local_address += '/24' - self._get_bacnet_loop().run_until_complete( - self._bacnet.discover(local_address)) + try: + loop = asyncio.get_running_loop() + loop.run_until_complete(self._bacnet.discover(local_address, self._device_ipv4_addr)) + except RuntimeError: + asyncio.run(self._bacnet.discover(local_address, self._device_ipv4_addr)) result = self._bacnet.validate_device() if result[0]: self._supports_bacnet = True @@ -81,11 +78,8 @@ def _protocol_bacnet_version(self): if len(self._bacnet.devices) > 0: for device in self._bacnet.devices: LOGGER.debug(f'Checking BACnet version for device: {device}') - loop = self._get_bacnet_loop() result_status, result_description = \ - loop.run_until_complete( - self._bacnet.validate_protocol_version(device) - ) + self._bacnet.validate_protocol_version(device) break LOGGER.info(result_description) From 9f82b77573d0057b10c04c7d178032b711643e14 Mon Sep 17 00:00:00 2001 From: Aliaksandr Nikitsin Date: Sat, 30 May 2026 20:03:37 +0200 Subject: [PATCH 2/9] pylint --- modules/test/protocol/python/src/protocol_module.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/test/protocol/python/src/protocol_module.py b/modules/test/protocol/python/src/protocol_module.py index 8077b88ee..38a1b4031 100644 --- a/modules/test/protocol/python/src/protocol_module.py +++ b/modules/test/protocol/python/src/protocol_module.py @@ -49,9 +49,13 @@ def _protocol_valid_bacnet(self): local_address += '/24' try: loop = asyncio.get_running_loop() - loop.run_until_complete(self._bacnet.discover(local_address, self._device_ipv4_addr)) + loop.run_until_complete( + self._bacnet.discover(local_address, self._device_ipv4_addr) + ) except RuntimeError: - asyncio.run(self._bacnet.discover(local_address, self._device_ipv4_addr)) + asyncio.run(self._bacnet.discover( + local_address, self._device_ipv4_addr) + ) result = self._bacnet.validate_device() if result[0]: self._supports_bacnet = True From a638fb29a5c7c6992c8ee21badd07dfc3708a213 Mon Sep 17 00:00:00 2001 From: Aliaksandr Nikitsin Date: Fri, 5 Jun 2026 17:42:03 +0200 Subject: [PATCH 3/9] device discovery from captured packets --- .../protocol/bin/get_bacnet_i-am_packets.sh | 27 ++++++ .../protocol/python/src/protocol_bacnet.py | 88 ++++++++++++------- 2 files changed, 85 insertions(+), 30 deletions(-) create mode 100644 modules/test/protocol/bin/get_bacnet_i-am_packets.sh diff --git a/modules/test/protocol/bin/get_bacnet_i-am_packets.sh b/modules/test/protocol/bin/get_bacnet_i-am_packets.sh new file mode 100644 index 000000000..c26066988 --- /dev/null +++ b/modules/test/protocol/bin/get_bacnet_i-am_packets.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#!/bin/bash + +CAPTURE_FILE="$1" +SRC_IP="$2" + +TSHARK_FILTER="bacnet && ip.src == $SRC_IP" +TSHARK_OUTPUT="-T json -e ip.src -e ip.dst -e eth.src -e eth.dst -e bacapp.instance_number -e _ws.col.Info" + +response=$(tshark -r "$CAPTURE_FILE" $TSHARK_OUTPUT -Y "$TSHARK_FILTER") + +echo "$response" \ No newline at end of file diff --git a/modules/test/protocol/python/src/protocol_bacnet.py b/modules/test/protocol/python/src/protocol_bacnet.py index 359e1a6c1..f565a3bd8 100644 --- a/modules/test/protocol/python/src/protocol_bacnet.py +++ b/modules/test/protocol/python/src/protocol_bacnet.py @@ -89,37 +89,41 @@ async def discover(self, local_ip, device_ip): LOGGER.info(f'Discovered BACnet device: {device}') self.devices.append(device) LOGGER.info('BACnet devices found: ' + str(len(self.devices))) - self.bacnet.discoveredDevices = {} - try: - await self.bacnet._discover(timeout=10) # pylint: disable=protected-access - except Exception as e: # pylint: disable=W0718 - LOGGER.error(e) - LOGGER.info('BACnet discovery complete') - with open(BAC0_LOG, 'r', encoding='utf-8') as f: - bac0_log = f.read() - LOGGER.info('BAC0 Log:\n' + bac0_log) - LOGGER.info('discoveredDevices: ' + str(self.bacnet.discoveredDevices)) - if self.bacnet.discoveredDevices is not None: - for device_info in self.bacnet.discoveredDevices.values(): + if not self.devices: + try: + await self.bacnet._discover(timeout=10) # pylint: disable=protected-access + except Exception as e: # pylint: disable=W0718 + LOGGER.error(e) + LOGGER.info('BACnet discovery complete') + with open(BAC0_LOG, 'r', encoding='utf-8') as f: + bac0_log = f.read() + LOGGER.info('BAC0 Log:\n' + bac0_log) + LOGGER.info('discoveredDevices: ' + str(self.bacnet.discoveredDevices)) + if self.bacnet.discoveredDevices is not None: + for device_info in self.bacnet.discoveredDevices.values(): + device = BACnetDevice( + device_id=str(device_info['object_instance'][1]), + ip=str(device_info['address']) + ) + self.devices.append(device) + LOGGER.info(f'Discovered BACnet device: {device}') + if not self.devices: + res = await self.bacnet.this_application.app.who_is( + low_limit=0, + high_limit=4194303, + address=Address(f'{device_ip}:47808'), + timeout=10, + ) + for iam in res: + instance = iam.iAmDeviceIdentifier[1] + address = str(iam.pduSource) device = BACnetDevice( - device_id=str(device_info['object_instance'][1]), - ip=str(device_info['address']) - ) - LOGGER.info(f'Discovered BACnet device: {device}') - res = await self.bacnet.this_application.app.who_is( - low_limit=0, - high_limit=4194303, - address=Address(f'{device_ip}:47808'), - timeout=10, - ) - LOGGER.info(f'WhoIs result: {res}') - for iam in res: - instance = iam.iAmDeviceIdentifier[1] - obj_type = iam.iAmDeviceIdentifier[0] - address = str(iam.pduSource) - vendor_id = getattr(iam, 'vendorID', None) - LOGGER.info(f'''Device type: {obj_type}, instance: {instance}, - address: {address}, vendor_id: {vendor_id}''') + device_id=str(instance), + ip=str(address) + ) + self.devices.append(device) + if not self.devices: + self.devices = self._discover_from_packets(device_ip) # Check if the device being tested is in the discovered devices list # discover needs to be called before this method is invoked @@ -227,3 +231,27 @@ def get_bacnet_packets( command = f'{bin_file} {args}' response = util.run_command(command) return json.loads(response[0].strip()) + + def _discover_from_packets(self, device_ip: str) -> list[BACnetDevice]: + discovered = set() + capture_file = os.path.join(self._captures_dir, self._capture_file) + LOGGER.info(f'Discovering BACnet devices from packets in {capture_file}...') + bin_file = self._bin_dir + '/get_bacnet_i-am_packets.sh' + args = f'"{capture_file}" {device_ip}' + command = f'{bin_file} {args}' + response = util.run_command(command) + packets = json.loads(response[0].strip()) + for packet in packets: + info = packet['_source']['layers']['_ws.col.info'][0] + if 'i-Am' in info: + discovered.add( + ( + packet['_source']['layers']['bacapp.instance_number'][0], + packet['_source']['layers']['ip.src'][0] + ) + ) + LOGGER.info(f'Discovered BACnet devices from packets: {discovered}') + return [ + BACnetDevice(device_id=device_id, ip=ip) + for device_id, ip in discovered + ] From 28bfdcf701915b3b0e29a7a3e4c48ba1bf8d8587 Mon Sep 17 00:00:00 2001 From: Aliaksandr Nikitsin Date: Tue, 16 Jun 2026 19:38:09 +0200 Subject: [PATCH 4/9] readmultiple --- .../protocol/python/src/protocol_bacnet.py | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/modules/test/protocol/python/src/protocol_bacnet.py b/modules/test/protocol/python/src/protocol_bacnet.py index f565a3bd8..46798d6d8 100644 --- a/modules/test/protocol/python/src/protocol_bacnet.py +++ b/modules/test/protocol/python/src/protocol_bacnet.py @@ -130,6 +130,7 @@ async def discover(self, local_ip, device_ip): def validate_device(self): result = None description = '' + try: if len(self.devices) > 0: result = True @@ -157,17 +158,29 @@ async def validate_protocol_version( f'Resolving protocol version for BACnet device: {device.device_id}' ) try: - dut = await BAC0.device( - device.ip, - device.device_id, - self.bacnet - ) - version = await dut.read_property( - ('device', device.device_id, 'protocolVersion') - ) - revision = await dut.read_property( - ('device', device.device_id, 'protocolRevision') - ) + + + try: + dut = await BAC0.device( + device.ip, + device.device_id, + self.bacnet + ) + version = await dut.read_property( + ('device', device.device_id, 'protocolVersion') + ) + revision = await dut.read_property( + ('device', device.device_id, 'protocolRevision') + ) + except AttributeError: + cmd = f'{device.ip} device {device.device_id} protocolVersion protocolRevision' + + results = await self.bacnet.readMultiple(cmd) + + LOGGER.info(f'BACnet readMultiple results: {results}') + if len(results) == 2: + version = results[0] + revision = results[1] if version is None or revision is None: result = False result_description = ( From 43d92cc915fd545a1431bb35e0711f12e12a4b34 Mon Sep 17 00:00:00 2001 From: Aliaksandr Nikitsin Date: Wed, 17 Jun 2026 10:33:21 +0200 Subject: [PATCH 5/9] pylint --- modules/test/protocol/python/src/protocol_bacnet.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/test/protocol/python/src/protocol_bacnet.py b/modules/test/protocol/python/src/protocol_bacnet.py index 46798d6d8..6ca834d00 100644 --- a/modules/test/protocol/python/src/protocol_bacnet.py +++ b/modules/test/protocol/python/src/protocol_bacnet.py @@ -158,8 +158,6 @@ async def validate_protocol_version( f'Resolving protocol version for BACnet device: {device.device_id}' ) try: - - try: dut = await BAC0.device( device.ip, @@ -173,8 +171,9 @@ async def validate_protocol_version( ('device', device.device_id, 'protocolRevision') ) except AttributeError: - cmd = f'{device.ip} device {device.device_id} protocolVersion protocolRevision' - + ip = device.ip + d_id = device.device_id + cmd = f'{ip} device {d_id} protocolVersion protocolRevision' results = await self.bacnet.readMultiple(cmd) LOGGER.info(f'BACnet readMultiple results: {results}') From 37a98d63d06f040a393e5279418902d296a73f3c Mon Sep 17 00:00:00 2001 From: Aliaksandr Nikitsin Date: Wed, 17 Jun 2026 18:49:12 +0200 Subject: [PATCH 6/9] clear devices cache --- .../protocol/python/src/protocol_bacnet.py | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/modules/test/protocol/python/src/protocol_bacnet.py b/modules/test/protocol/python/src/protocol_bacnet.py index 6ca834d00..d34b06c64 100644 --- a/modules/test/protocol/python/src/protocol_bacnet.py +++ b/modules/test/protocol/python/src/protocol_bacnet.py @@ -15,6 +15,8 @@ import BAC0 from bacpypes3.pdu import Address +from bacpypes3.app import DeviceInfo +from bacpypes3.basetypes import Segmentation from dataclasses import dataclass import logging import json @@ -157,29 +159,29 @@ async def validate_protocol_version( LOGGER.info( f'Resolving protocol version for BACnet device: {device.device_id}' ) + version = None + revision = None try: - try: - dut = await BAC0.device( - device.ip, - device.device_id, - self.bacnet - ) - version = await dut.read_property( - ('device', device.device_id, 'protocolVersion') - ) - revision = await dut.read_property( - ('device', device.device_id, 'protocolRevision') - ) - except AttributeError: - ip = device.ip - d_id = device.device_id - cmd = f'{ip} device {d_id} protocolVersion protocolRevision' - results = await self.bacnet.readMultiple(cmd) - - LOGGER.info(f'BACnet readMultiple results: {results}') - if len(results) == 2: - version = results[0] - revision = results[1] + dev_info = DeviceInfo() + dev_info.device_instance = device.device_id + dev_info.device_address = Address(device.ip) + dev_info.max_apdu_length_accepted = 1476 # Стандарт для BACnet/IP + dev_info.segmentation_supported = Segmentation.noSegmentation + dev_info.vendor_id = 0 + + await self.bacnet.this_application.app.device_info_cache.set_device_info(dev_info) + LOGGER.info(f"Manually injected device {device.device_id} ({device.ip}) into BAC0 cache.") + except Exception as cache_err: + LOGGER.warning(f"Failed to pre-populate BAC0 cache: {cache_err}") + try: + ip = device.ip + d_id = device.device_id + cmd = f'{ip} device {d_id} protocolVersion protocolRevision' + results = await self.bacnet.readMultiple(cmd) + LOGGER.info(f'BACnet readMultiple results: {results}') + if len(results) == 2: + version = results[0] + revision = results[1] if version is None or revision is None: result = False result_description = ( From 9857f7e6ae59806558f8e0e67a11b13c6c245982 Mon Sep 17 00:00:00 2001 From: Aliaksandr Nikitsin Date: Wed, 17 Jun 2026 18:52:07 +0200 Subject: [PATCH 7/9] pylint --- .../test/protocol/python/src/protocol_bacnet.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/modules/test/protocol/python/src/protocol_bacnet.py b/modules/test/protocol/python/src/protocol_bacnet.py index d34b06c64..f67284efd 100644 --- a/modules/test/protocol/python/src/protocol_bacnet.py +++ b/modules/test/protocol/python/src/protocol_bacnet.py @@ -161,21 +161,22 @@ async def validate_protocol_version( ) version = None revision = None + ip = device.ip + d_id = device.device_id try: dev_info = DeviceInfo() - dev_info.device_instance = device.device_id - dev_info.device_address = Address(device.ip) + dev_info.device_instance = d_id + dev_info.device_address = Address(ip) dev_info.max_apdu_length_accepted = 1476 # Стандарт для BACnet/IP dev_info.segmentation_supported = Segmentation.noSegmentation dev_info.vendor_id = 0 - - await self.bacnet.this_application.app.device_info_cache.set_device_info(dev_info) - LOGGER.info(f"Manually injected device {device.device_id} ({device.ip}) into BAC0 cache.") + await self.bacnet.this_application.app.device_info_cache.set_device_info( + dev_info + ) + LOGGER.info(f"Manually injected device {d_id} {ip} into BAC0 cache.") except Exception as cache_err: LOGGER.warning(f"Failed to pre-populate BAC0 cache: {cache_err}") try: - ip = device.ip - d_id = device.device_id cmd = f'{ip} device {d_id} protocolVersion protocolRevision' results = await self.bacnet.readMultiple(cmd) LOGGER.info(f'BACnet readMultiple results: {results}') From 67ba3e7af71d09920bff447328ed55cadb882544 Mon Sep 17 00:00:00 2001 From: Aliaksandr Nikitsin Date: Wed, 17 Jun 2026 18:58:16 +0200 Subject: [PATCH 8/9] pylint --- modules/test/protocol/python/src/protocol_bacnet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/test/protocol/python/src/protocol_bacnet.py b/modules/test/protocol/python/src/protocol_bacnet.py index f67284efd..bb968c824 100644 --- a/modules/test/protocol/python/src/protocol_bacnet.py +++ b/modules/test/protocol/python/src/protocol_bacnet.py @@ -173,9 +173,9 @@ async def validate_protocol_version( await self.bacnet.this_application.app.device_info_cache.set_device_info( dev_info ) - LOGGER.info(f"Manually injected device {d_id} {ip} into BAC0 cache.") + LOGGER.info(f'Manually injected device {d_id} {ip} into BAC0 cache.') except Exception as cache_err: - LOGGER.warning(f"Failed to pre-populate BAC0 cache: {cache_err}") + LOGGER.warning(f'Failed to pre-populate BAC0 cache: {cache_err}' ) try: cmd = f'{ip} device {d_id} protocolVersion protocolRevision' results = await self.bacnet.readMultiple(cmd) From d0051f49ef31a7e937b2583b367f9451ceffa726 Mon Sep 17 00:00:00 2001 From: Aliaksandr Nikitsin Date: Mon, 22 Jun 2026 17:58:36 +0200 Subject: [PATCH 9/9] device info --- modules/test/protocol/python/src/protocol_bacnet.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/test/protocol/python/src/protocol_bacnet.py b/modules/test/protocol/python/src/protocol_bacnet.py index bb968c824..d26581b6e 100644 --- a/modules/test/protocol/python/src/protocol_bacnet.py +++ b/modules/test/protocol/python/src/protocol_bacnet.py @@ -164,12 +164,13 @@ async def validate_protocol_version( ip = device.ip d_id = device.device_id try: - dev_info = DeviceInfo() - dev_info.device_instance = d_id - dev_info.device_address = Address(ip) - dev_info.max_apdu_length_accepted = 1476 # Стандарт для BACnet/IP - dev_info.segmentation_supported = Segmentation.noSegmentation - dev_info.vendor_id = 0 + dev_info = DeviceInfo( + device_instance=int(d_id), + device_address=Address(ip), + max_apdu_length_accepted=1476, + segmentation_supported=Segmentation.noSegmentation, + vendor_identifier=0 + ) await self.bacnet.this_application.app.device_info_cache.set_device_info( dev_info )