# -*- coding: utf-8 -*- # This file is from https://github.com/colin-guyon/py-bluetooth-utils # published under MIT License # MIT License # Copyright (c) 2020 Colin GUYON # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Module containing some bluetooth utility functions (linux only). It either uses HCI commands using PyBluez, or does ioctl calls like it's done in Bluez tools such as hciconfig. Main functions: - toggle_device : enable or disable a bluetooth device - set_scan : set scan type on a device ("noscan", "iscan", "pscan", "piscan") - enable/disable_le_scan : enable BLE scanning - parse_le_advertising_events : parse and read BLE advertisements packets - start/stop_le_advertising : advertise custom data using BLE Bluez : http://www.bluez.org/ PyBluez : http://karulis.github.io/pybluez/ The module was in particular inspired from 'iBeacon-Scanner-' https://github.com/switchdoclabs/iBeacon-Scanner-/blob/master/blescan.py and sometimes directly from the Bluez sources. """ from __future__ import absolute_import import sys import struct import fcntl import array import socket from errno import EALREADY # import PyBluez import bluetooth._bluetooth as bluez __all__ = ('toggle_device', 'set_scan', 'enable_le_scan', 'disable_le_scan', 'parse_le_advertising_events', 'start_le_advertising', 'stop_le_advertising', 'raw_packet_to_str') LE_META_EVENT = 0x3E LE_PUBLIC_ADDRESS = 0x00 LE_RANDOM_ADDRESS = 0x01 OGF_LE_CTL = 0x08 OCF_LE_SET_SCAN_PARAMETERS = 0x000B OCF_LE_SET_SCAN_ENABLE = 0x000C OCF_LE_CREATE_CONN = 0x000D OCF_LE_SET_ADVERTISING_PARAMETERS = 0x0006 OCF_LE_SET_ADVERTISE_ENABLE = 0x000A OCF_LE_SET_ADVERTISING_DATA = 0x0008 SCAN_TYPE_PASSIVE = 0x00 SCAN_FILTER_DUPLICATES = 0x01 SCAN_DISABLE = 0x00 SCAN_ENABLE = 0x01 # sub-events of LE_META_EVENT EVT_LE_CONN_COMPLETE = 0x01 EVT_LE_ADVERTISING_REPORT = 0x02 EVT_LE_CONN_UPDATE_COMPLETE = 0x03 EVT_LE_READ_REMOTE_USED_FEATURES_COMPLETE = 0x04 # Advertisement event types ADV_IND = 0x00 ADV_DIRECT_IND = 0x01 ADV_SCAN_IND = 0x02 ADV_NONCONN_IND = 0x03 ADV_SCAN_RSP = 0x04 # Allow Scan Request from Any, Connect Request from Any FILTER_POLICY_NO_WHITELIST = 0x00 # Allow Scan Request from White List Only, Connect Request from Any FILTER_POLICY_SCAN_WHITELIST = 0x01 # Allow Scan Request from Any, Connect Request from White List Only FILTER_POLICY_CONN_WHITELIST = 0x02 # Allow Scan Request from White List Only, Connect Request from White List Only FILTER_POLICY_SCAN_AND_CONN_WHITELIST = 0x03 def toggle_device(dev_id, enable): """ Power ON or OFF a bluetooth device. :param dev_id: Device id. :type dev_id: ``int`` :param enable: Whether to enable of disable the device. :type enable: ``bool`` """ hci_sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_RAW, socket.BTPROTO_HCI) print("Power %s bluetooth device %d" % ('ON' if enable else 'OFF', dev_id)) # di = struct.pack("HbBIBBIIIHHHH10I", dev_id, *((0,) * 22)) # fcntl.ioctl(hci_sock.fileno(), bluez.HCIGETDEVINFO, di) req_str = struct.pack("H", dev_id) request = array.array("b", req_str) try: fcntl.ioctl(hci_sock.fileno(), bluez.HCIDEVUP if enable else bluez.HCIDEVDOWN, request[0]) except IOError as e: if e.errno == EALREADY: print("Bluetooth device %d is already %s" % ( dev_id, 'enabled' if enable else 'disabled')) else: raise finally: hci_sock.close() # Types of bluetooth scan SCAN_DISABLED = 0x00 SCAN_INQUIRY = 0x01 SCAN_PAGE = 0x02 def set_scan(dev_id, scan_type): """ Set scan type on a given bluetooth device. :param dev_id: Device id. :type dev_id: ``int`` :param scan_type: One of ``'noscan'`` ``'iscan'`` ``'pscan'`` ``'piscan'`` :type scan_type: ``str`` """ hci_sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_RAW, socket.BTPROTO_HCI) if scan_type == "noscan": dev_opt = SCAN_DISABLED elif scan_type == "iscan": dev_opt = SCAN_INQUIRY elif scan_type == "pscan": dev_opt = SCAN_PAGE elif scan_type == "piscan": dev_opt = SCAN_PAGE | SCAN_INQUIRY else: raise ValueError("Unknown scan type %r" % scan_type) req_str = struct.pack("HI", dev_id, dev_opt) print("Set scan type %r to bluetooth device %d" % (scan_type, dev_id)) try: fcntl.ioctl(hci_sock.fileno(), bluez.HCISETSCAN, req_str) finally: hci_sock.close() def raw_packet_to_str(pkt): """ Returns the string representation of a raw HCI packet. """ if sys.version_info > (3, 0): return ''.join('%02x' % struct.unpack("B", bytes([x]))[0] for x in pkt) else: return ''.join('%02x' % struct.unpack("B", x)[0] for x in pkt) def enable_le_scan(sock, interval=0x0800, window=0x0800, filter_policy=FILTER_POLICY_NO_WHITELIST, filter_duplicates=True): """ Enable LE passive scan (with filtering of duplicate packets enabled). :param sock: A bluetooth HCI socket (retrieved using the ``hci_open_dev`` PyBluez function). :param interval: Scan interval. :param window: Scan window (must be less or equal than given interval). :param filter_policy: One of ``FILTER_POLICY_NO_WHITELIST`` (default value) ``FILTER_POLICY_SCAN_WHITELIST`` ``FILTER_POLICY_CONN_WHITELIST`` ``FILTER_POLICY_SCAN_AND_CONN_WHITELIST`` .. note:: Scan interval and window are to multiply by 0.625 ms to get the real time duration. """ print("Enable LE scan") own_bdaddr_type = LE_PUBLIC_ADDRESS # does not work with LE_RANDOM_ADDRESS cmd_pkt = struct.pack(" 31: raise ValueError("data is too long (%d but max is 31 bytes)", data_length) cmd_pkt = struct.pack("