From c11382c2ef7b6f029a3b892f02acfa2ae3651737 Mon Sep 17 00:00:00 2001 From: idaniel86 Date: Mon, 26 Oct 2020 22:22:14 +0100 Subject: [PATCH 1/5] Added extended BLE commands. (#54) * Added extended BLE commands. * Merged extended and legacy scan commands. Updated documentation. --- beacontools/backend/linux.py | 4 ++ beacontools/const.py | 6 ++ beacontools/scanner.py | 126 ++++++++++++++++++++++++++++------- 3 files changed, 113 insertions(+), 23 deletions(-) diff --git a/beacontools/backend/linux.py b/beacontools/backend/linux.py index b509547..3e77682 100644 --- a/beacontools/backend/linux.py +++ b/beacontools/backend/linux.py @@ -17,3 +17,7 @@ def open_dev(bt_device_id): def send_cmd(socket, group_field, command_field, data): """Send hci command to device.""" return bluez.hci_send_cmd(socket, group_field, command_field, data) + +def send_req(socket, group_field, command_field, event, rlen, params, timeout): + """Send hci request to device.""" + return bluez.hci_send_req(socket, group_field, command_field, event, rlen, params, timeout) diff --git a/beacontools/const.py b/beacontools/const.py index 396d0d7..d2193bf 100644 --- a/beacontools/const.py +++ b/beacontools/const.py @@ -41,6 +41,12 @@ class BluetoothAddressType(IntEnum): OCF_LE_SET_SCAN_PARAMETERS = 0x000B OCF_LE_SET_SCAN_ENABLE = 0x000C EVT_LE_ADVERTISING_REPORT = 0x02 +OCF_LE_SET_EXT_SCAN_PARAMETERS = 0x0041 +OCF_LE_SET_EXT_SCAN_ENABLE = 0x0042 +EVT_LE_EXT_ADVERTISING_REPORT = 0x0D +OGF_INFO_PARAM = 0x04 +OCF_READ_LOCAL_VERSION = 0x01 +EVT_CMD_COMPLETE = 0x0E # for Generic Access Profile parsing FLAGS_DATA_TYPE = 0x01 diff --git a/beacontools/scanner.py b/beacontools/scanner.py index a366fdb..5ee2b0a 100644 --- a/beacontools/scanner.py +++ b/beacontools/scanner.py @@ -3,6 +3,8 @@ import struct import threading from importlib import import_module +from enum import IntEnum +from construct import Struct, Byte, Bytes, GreedyRange, ConstructError from ahocorapy.keywordtree import KeywordTree @@ -13,7 +15,10 @@ LE_META_EVENT, MANUFACTURER_SPECIFIC_DATA_TYPE, MS_FRACTION_DIVIDER, OCF_LE_SET_SCAN_ENABLE, OCF_LE_SET_SCAN_PARAMETERS, OGF_LE_CTL, - BluetoothAddressType, ScanFilter, ScannerMode, ScanType) + BluetoothAddressType, ScanFilter, ScannerMode, ScanType, + OCF_LE_SET_EXT_SCAN_PARAMETERS, OCF_LE_SET_EXT_SCAN_ENABLE, + EVT_LE_EXT_ADVERTISING_REPORT, OGF_INFO_PARAM, + OCF_READ_LOCAL_VERSION, EVT_CMD_COMPLETE) from .device_filters import BtAddrFilter, DeviceFilter from .packet_types import (EddystoneEIDFrame, EddystoneEncryptedTLMFrame, EddystoneTLMFrame, EddystoneUIDFrame, @@ -22,6 +27,26 @@ from .utils import (bin_to_int, bt_addr_to_string, get_mode, is_one_of, is_packet_type, to_int) + +class HCIVersion(IntEnum): + """HCI version enumeration + + https://www.bluetooth.com/specifications/assigned-numbers/host-controller-interface/ + """ + BT_CORE_SPEC_1_0 = 0 + BT_CODE_SPEC_1_1 = 1 + BT_CODE_SPEC_1_2 = 2 + BT_CORE_SPEC_2_0 = 3 + BT_CORE_SPEC_2_1 = 4 + BT_CORE_SPEC_3_0 = 5 + BT_CORE_SPEC_4_0 = 6 + BT_CORE_SPEC_4_1 = 7 + BT_CORE_SPEC_4_2 = 8 + BT_CORE_SPEC_5_0 = 9 + BT_CORE_SPEC_5_1 = 10 + BT_CORE_SPEC_5_2 = 11 + + _LOGGER = logging.getLogger(__name__) _LOGGER.setLevel(logging.DEBUG) @@ -95,6 +120,8 @@ def __init__(self, callback, bt_device_id, device_filter, packet_filter, scan_pa self.eddystone_mappings = [] # parameters to pass to bt device self.scan_parameters = scan_parameters + # hci version + self.hci_version = HCIVersion.BT_CORE_SPEC_1_0 # construct an aho-corasick search tree for efficient prefiltering service_uuid_prefix = b"\x03\x03" @@ -116,6 +143,7 @@ def run(self): """Continously scan for BLE advertisements.""" self.socket = self.backend.open_dev(self.bt_device_id) + self.hci_version = self.get_hci_version() self.set_scan_parameters(**self.scan_parameters) self.toggle_scan(True) @@ -123,21 +151,44 @@ def run(self): pkt = self.socket.recv(255) event = to_int(pkt[1]) subevent = to_int(pkt[3]) - if event == LE_META_EVENT and subevent == EVT_LE_ADVERTISING_REPORT: + if event == LE_META_EVENT and subevent in [EVT_LE_ADVERTISING_REPORT, EVT_LE_EXT_ADVERTISING_REPORT]: # we have an BLE advertisement self.process_packet(pkt) self.socket.close() + def get_hci_version(self): + """Gets the HCI version""" + local_version = Struct( + "status" / Byte, + "hci_version" / Byte, + "hci_revision" / Bytes(2), + "lmp_version" / Byte, + "manufacturer_name" / Bytes(2), + "lmp_subversion" / Bytes(2), + ) + + resp = self.backend.send_req(self.socket, OGF_INFO_PARAM, OCF_READ_LOCAL_VERSION, + EVT_CMD_COMPLETE, local_version.sizeof(), bytes(), 0) + try: + return HCIVersion(GreedyRange(local_version).parse(resp)[0]["hci_version"]) + except ConstructError: + return HCIVersion.BT_CORE_SPEC_1_0 + def set_scan_parameters(self, scan_type=ScanType.ACTIVE, interval_ms=10, window_ms=10, address_type=BluetoothAddressType.RANDOM, filter_type=ScanFilter.ALL): - """"sets the le scan parameters + """"Sets the le scan parameters + + For extended set scan parameters command additional parameter scanning PHYs has to be provided. + The parameter indicates the PHY(s) on which the advertising packets should be received on the + primary advertising physical channel. For further information have a look on BT Core 5.1 Specification, + page 1439 ( LE Set Extended Scan Parameters command). Args: scan_type: ScanType.(PASSIVE|ACTIVE) - interval: ms (as float) between scans (valid range 2.5ms - 10240ms) + interval: ms (as float) between scans (valid range 2.5ms - 10240ms or 40.95s for extended version) ..note:: when interval and window are equal, the scan runs continuos - window: ms (as float) scan duration (valid range 2.5ms - 10240ms) + window: ms (as float) scan duration (valid range 2.5ms - 10240ms or 40.95s for extended version) address_type: Bluetooth address type BluetoothAddressType.(PUBLIC|RANDOM) * PUBLIC = use device MAC address * RANDOM = generate a random MAC address and use that @@ -148,48 +199,77 @@ def set_scan_parameters(self, scan_type=ScanType.ACTIVE, interval_ms=10, window_ Raises: ValueError: A value had an unexpected format or was not in range """ + max_interval = (0x0004 if self.hci_version < HCIVersion.BT_CORE_SPEC_5_0 else 0xFFFF) interval_fractions = interval_ms / MS_FRACTION_DIVIDER - if interval_fractions < 0x0004 or interval_fractions > 0x4000: + if interval_fractions < 0x0004 or interval_fractions > max_interval: raise ValueError( - "Invalid interval given {}, must be in range of 2.5ms to 10240ms!".format( - interval_fractions)) + "Invalid interval given {}, must be in range of 2.5ms to {}ms!".format( + interval_fractions, max_interval * MS_FRACTION_DIVIDER)) window_fractions = window_ms / MS_FRACTION_DIVIDER - if window_fractions < 0x0004 or window_fractions > 0x4000: + if window_fractions < 0x0004 or window_fractions > max_interval: raise ValueError( - "Invalid window given {}, must be in range of 2.5ms to 10240ms!".format( - window_fractions)) + "Invalid window given {}, must be in range of 2.5ms to {}ms!".format( + window_fractions, max_interval * MS_FRACTION_DIVIDER)) interval_fractions, window_fractions = int(interval_fractions), int(window_fractions) - scan_parameter_pkg = struct.pack( - " Date: Mon, 26 Oct 2020 22:35:52 +0100 Subject: [PATCH 2/5] fix max_interval value --- beacontools/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacontools/scanner.py b/beacontools/scanner.py index 5ee2b0a..20adf7b 100644 --- a/beacontools/scanner.py +++ b/beacontools/scanner.py @@ -199,7 +199,7 @@ def set_scan_parameters(self, scan_type=ScanType.ACTIVE, interval_ms=10, window_ Raises: ValueError: A value had an unexpected format or was not in range """ - max_interval = (0x0004 if self.hci_version < HCIVersion.BT_CORE_SPEC_5_0 else 0xFFFF) + max_interval = (0x4000 if self.hci_version < HCIVersion.BT_CORE_SPEC_5_0 else 0xFFFF) interval_fractions = interval_ms / MS_FRACTION_DIVIDER if interval_fractions < 0x0004 or interval_fractions > max_interval: raise ValueError( From b9770b4fea350090116ae83e381e9f2b9d7b3e65 Mon Sep 17 00:00:00 2001 From: Felix Seele Date: Mon, 26 Oct 2020 23:49:39 +0100 Subject: [PATCH 3/5] added too-many-arguments to pylint whitelist --- beacontools/scanner.py | 2 +- pylintrc | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/beacontools/scanner.py b/beacontools/scanner.py index 20adf7b..7b7928a 100644 --- a/beacontools/scanner.py +++ b/beacontools/scanner.py @@ -50,7 +50,7 @@ class HCIVersion(IntEnum): _LOGGER = logging.getLogger(__name__) _LOGGER.setLevel(logging.DEBUG) -# pylint: disable=no-member,too-many-arguments +# pylint: disable=no-member class BeaconScanner(object): diff --git a/pylintrc b/pylintrc index aa1ad37..9740e75 100644 --- a/pylintrc +++ b/pylintrc @@ -1,6 +1,10 @@ [MASTER] reports=no -disable=cyclic-import,too-many-instance-attributes,too-few-public-methods,too-many-branches,locally-disabled,fixme,too-many-boolean-expressions,no-else-return,len-as-condition,inconsistent-return-statements,useless-object-inheritance +disable=cyclic-import,too-many-instance-attributes, + too-few-public-methods,too-many-branches,locally-disabled, + fixme,too-many-boolean-expressions,no-else-return, + len-as-condition,inconsistent-return-statements, + useless-object-inheritance,too-many-arguments max-line-length=120 From 51076ff6fbb51c9c2b4fad33737a6bdae4382674 Mon Sep 17 00:00:00 2001 From: Felix Seele Date: Mon, 26 Oct 2020 23:58:34 +0100 Subject: [PATCH 4/5] added NotImplementedError for FreeBSD backend --- beacontools/backend/freebsd.py | 4 ++++ beacontools/scanner.py | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/beacontools/backend/freebsd.py b/beacontools/backend/freebsd.py index 497ff8b..e8df94c 100644 --- a/beacontools/backend/freebsd.py +++ b/beacontools/backend/freebsd.py @@ -55,3 +55,7 @@ def send_cmd(sock, group_field, command_field, data): """Send hci command to device.""" opcode = (((group_field & 0x3f) << 10) | (command_field & 0x3ff)) sock.send(struct.pack(' Date: Tue, 27 Oct 2020 22:43:36 +0100 Subject: [PATCH 5/5] updated readme for version 2.1 and updated dependencies --- CONTRIBUTORS.md | 1 + README.rst | 2 ++ setup.py | 6 +++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index b9c1dd9..fb7e04c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -9,3 +9,4 @@ Many thanks to everyone who contributed to this project: - clydebarrow (https://github.com/clydebarrow) - myfreeweb (https://github.com/myfreeweb) - cleitonbueno (https://github.com/cleitonbueno) +- idaniel86 (https://github.com/idaniel86) diff --git a/README.rst b/README.rst index 2941647..54b0976 100644 --- a/README.rst +++ b/README.rst @@ -126,6 +126,8 @@ Changelog --------- Beacontools follows the `semantic versioning `__ scheme. +* 2.1.0 + * Added support for extended BLE commands for devices using HCI >= 5.0 (Linux only, thanks to `idaniel86 `__) * 2.0.2 * Improved prefiltering of packets, fixes #48 * 2.0.1 diff --git a/setup.py b/setup.py index 529e08b..819ff09 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='beacontools', - version='2.0.2', + version='2.1.0', description='A Python library for working with various types of Bluetooth LE Beacons.', long_description=long_description, @@ -61,14 +61,14 @@ # for example: # $ pip install -e .[dev,test] extras_require={ - 'scan': ['PyBluez==0.22'] if sys.platform.startswith("linux") else [], + 'scan': ['PyBluez==0.23'] if sys.platform.startswith("linux") else [], 'dev': ['check-manifest'], 'test': [ 'coveralls~=2.1', 'pytest~=6.0', 'pytest-cov~=2.10', 'mock~=4.0', - 'check-manifest==0.42', + 'check-manifest', 'pylint', 'readme_renderer', 'docutils'