Skip to content

Commit

Permalink
Merge pull request #55 from citruz/release2.1
Browse files Browse the repository at this point in the history
Release2.1
  • Loading branch information
citruz authored Oct 27, 2020
2 parents 360b1ef + 1a772d8 commit 513e1c7
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 28 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ Changelog
---------
Beacontools follows the `semantic versioning <https://semver.org/>`__ scheme.

* 2.1.0
* Added support for extended BLE commands for devices using HCI >= 5.0 (Linux only, thanks to `idaniel86 <https://github.com/idaniel86>`__)
* 2.0.2
* Improved prefiltering of packets, fixes #48
* 2.0.1
Expand Down
4 changes: 4 additions & 0 deletions beacontools/backend/freebsd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('<BHB', 1, opcode, len(data)) + data)

def send_req(_socket, _group_field, _command_field, _event, _rlen, _params, _timeout):
"""Support for HCI 5 has not been implemented yet for FreeBSD, pull requests are wellcome"""
raise NotImplementedError("send_req has not been implemented yet for FreeBSD")
4 changes: 4 additions & 0 deletions beacontools/backend/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
6 changes: 6 additions & 0 deletions beacontools/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
128 changes: 104 additions & 24 deletions beacontools/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand All @@ -22,10 +27,30 @@
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)

# pylint: disable=no-member,too-many-arguments
# pylint: disable=no-member


class BeaconScanner(object):
Expand Down Expand Up @@ -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"
Expand All @@ -116,28 +143,52 @@ 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)

while self.keep_going:
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),
)

try:
resp = self.backend.send_req(self.socket, OGF_INFO_PARAM, OCF_READ_LOCAL_VERSION,
EVT_CMD_COMPLETE, local_version.sizeof(), bytes(), 0)
return HCIVersion(GreedyRange(local_version).parse(resp)[0]["hci_version"])
except (ConstructError, NotImplementedError):
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
Expand All @@ -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 = (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 > 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(
"<BHHBB",
scan_type,
interval_fractions,
window_fractions,
address_type,
filter_type)
self.backend.send_cmd(self.socket, OGF_LE_CTL, OCF_LE_SET_SCAN_PARAMETERS, scan_parameter_pkg)
if self.hci_version < HCIVersion.BT_CORE_SPEC_5_0:
command_field = OCF_LE_SET_SCAN_PARAMETERS
scan_parameter_pkg = struct.pack(
"<BHHBB",
scan_type,
interval_fractions,
window_fractions,
address_type,
filter_type)
else:
command_field = OCF_LE_SET_EXT_SCAN_PARAMETERS
scan_parameter_pkg = struct.pack(
"<BBBBHH",
address_type,
filter_type,
1, # scan advertisements on the LE 1M PHY
scan_type,
interval_fractions,
window_fractions)

self.backend.send_cmd(self.socket, OGF_LE_CTL, command_field, scan_parameter_pkg)

def toggle_scan(self, enable, filter_duplicates=False):
"""Enables or disables BLE scanning
For extended set scan enable command additional parameters duration and period have
to be provided. When both are zero, the controller shall continue scanning until
scanning is disabled. For non-zero values have a look on BT Core 5.1 Specification,
page 1442 (LE Set Extended Scan Enable command).
Args:
enable: boolean value to enable (True) or disable (False) scanner
filter_duplicates: boolean value to enable/disable filter, that
omits duplicated packets"""
command = struct.pack("BB", enable, filter_duplicates)
self.backend.send_cmd(self.socket, OGF_LE_CTL, OCF_LE_SET_SCAN_ENABLE, command)
if self.hci_version < HCIVersion.BT_CORE_SPEC_5_0:
command_field = OCF_LE_SET_SCAN_ENABLE
command = struct.pack("BB", enable, filter_duplicates)
else:
command_field = OCF_LE_SET_EXT_SCAN_ENABLE
command = struct.pack("<BBHH", enable, filter_duplicates,
0, # duration
0 # period
)

self.backend.send_cmd(self.socket, OGF_LE_CTL, command_field, command)

def process_packet(self, pkt):
"""Parse the packet and call callback if one of the filters matches."""
payload = pkt[14:-1]
payload = pkt[14:-1] if self.hci_version < HCIVersion.BT_CORE_SPEC_5_0 else pkt[29:]

# check if this could be a valid packet before parsing
# this reduces the CPU load significantly
if not self.kwtree.search(payload):
return

bt_addr = bt_addr_to_string(pkt[7:13])
rssi = bin_to_int(pkt[-1])
rssi = bin_to_int(pkt[-1] if self.hci_version < HCIVersion.BT_CORE_SPEC_5_0 else pkt[18])
# strip bluetooth address and parse packet
packet = parse_packet(payload)

Expand Down
6 changes: 5 additions & 1 deletion pylintrc
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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'
Expand Down

0 comments on commit 513e1c7

Please sign in to comment.