From 85635e3b8a3e4f3e08f421180a03173f6acd5a2f Mon Sep 17 00:00:00 2001 From: Wayne Keenan Date: Sun, 5 Nov 2017 17:27:47 +0000 Subject: [PATCH 01/13] Inital implementation of draft api. --- LICENSE | 2 +- README.rst | 97 +++++++- bleson/__init__.py | 7 + bleson/beacons/__init__.py | 0 bleson/beacons/eddystone.py | 103 ++++++++ bleson/core/__init__.py | 0 bleson/core/hci/__init__.py | 28 +++ bleson/core/hci/constants.py | 232 +++++++++++++++++ bleson/core/hci/type_converters.py | 122 +++++++++ bleson/core/hci/types.py | 45 ++++ bleson/core/roles.py | 43 ++++ bleson/core/types.py | 124 ++++++++++ bleson/interfaces/__init__.py | 0 bleson/interfaces/adapter.py | 28 +++ bleson/interfaces/provider.py | 10 + bleson/interfaces/role.py | 20 ++ bleson/logger.py | 13 + bleson/providers/__init__.py | 22 ++ bleson/providers/linux/__init__.py | 0 bleson/providers/linux/constants.py | 21 ++ bleson/providers/linux/linux_adapter.py | 301 +++++++++++++++++++++++ bleson/providers/linux/linux_provider.py | 15 ++ bleson/utils.py | 14 ++ examples/basic_advertiser.py | 23 ++ examples/basic_eddystone_beacon.py | 22 ++ examples/basic_observer.py | 22 ++ examples/context_advertiser.py | 14 ++ examples/context_eddystone_beacon.py | 11 + examples/context_observer.py | 16 ++ setup.py | 5 +- tests/test_examples.py | 35 +++ tests/test_internal_advertising_data.py | 23 ++ tests/test_internal_hci_packet_parse.py | 91 +++++++ tests/test_role_advertiser.py | 35 +++ tests/test_role_dual_adapters.py | 128 ++++++++++ tests/test_role_eddystone_beacon.py | 23 ++ tests/test_role_observer.py | 49 ++++ 37 files changed, 1740 insertions(+), 4 deletions(-) create mode 100644 bleson/beacons/__init__.py create mode 100644 bleson/beacons/eddystone.py create mode 100644 bleson/core/__init__.py create mode 100644 bleson/core/hci/__init__.py create mode 100644 bleson/core/hci/constants.py create mode 100644 bleson/core/hci/type_converters.py create mode 100644 bleson/core/hci/types.py create mode 100644 bleson/core/roles.py create mode 100644 bleson/core/types.py create mode 100644 bleson/interfaces/__init__.py create mode 100644 bleson/interfaces/adapter.py create mode 100644 bleson/interfaces/provider.py create mode 100644 bleson/interfaces/role.py create mode 100644 bleson/logger.py create mode 100644 bleson/providers/__init__.py create mode 100644 bleson/providers/linux/__init__.py create mode 100644 bleson/providers/linux/constants.py create mode 100644 bleson/providers/linux/linux_adapter.py create mode 100644 bleson/providers/linux/linux_provider.py create mode 100644 bleson/utils.py create mode 100644 examples/basic_advertiser.py create mode 100644 examples/basic_eddystone_beacon.py create mode 100644 examples/basic_observer.py create mode 100644 examples/context_advertiser.py create mode 100644 examples/context_eddystone_beacon.py create mode 100644 examples/context_observer.py create mode 100644 tests/test_examples.py create mode 100644 tests/test_internal_advertising_data.py create mode 100644 tests/test_internal_hci_packet_parse.py create mode 100644 tests/test_role_advertiser.py create mode 100644 tests/test_role_dual_adapters.py create mode 100644 tests/test_role_eddystone_beacon.py create mode 100644 tests/test_role_observer.py diff --git a/LICENSE b/LICENSE index 222a54a..f87b3e9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 The Cellule +Copyright (c) 2017 The Cellule Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst b/README.rst index 439a834..d446405 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,98 @@ ============= python-bleson -============= \ No newline at end of file +============= + +Status +====== + +Experimental. + + +Public API +========== + +Outline of the 'bleson' Python packages an end-user would use to play with Bluetooth LE. + +.. code:: python + + bleson + get_provider() # Obtain the provider for the current platfrom (currently Linux HCI only) + + bleson.core.types # Value objects representing Bluetooth Core Spec data types (not HCI specific) + BDAddress + Device + Advertisement + + bleson.core.roles # Core Bluetooth roles + Advertiser + Observer + + bleson.beacons.eddystone + EddystoneBeacon # Physical Web beacon, is a subclass of Advertiser role + + bleson.logger + log # Simple convenience wrapper around Python's standard logging. + + + +Examples +======== + +Examples are abbreviated below, plese see 'examples' folder for more details. + +Example - Advertiser +-------------------- + +.. code:: python + + from bleson import get_provider, Advertiser, Advertisement + + adapter = get_provider().get_adapter() + advertiser = Advertiser(adapter) + advertisement = Advertisement(name = "bleson") + advertiser.start() + advertiser.stop() + + +Example - Eddystone Beacon +-------------------------- + +.. code:: python + + from bleson import get_provider, EddystoneBeacon + + adapter = get_provider().get_adapter() + beacon = EddystoneBeacon(adapter) + beacon.url = 'https://www.bluetooth.com/' + beacons.start() + beacons.stop() + + +Example - Observer +------------------ + +.. code:: python + + from bleson import get_provider, Observer + + adapter = get_provider().get_adapter() + + def on_advertisement(advertisement): + print(advertisement) + + observer = Observer(adapter) + observer.on_advertising_data = on_advertisement + observer.start() + observer.stop() + +Tests +===== + +Please see the testsuite in the 'tests' folder. + + +Internal API +============ + +To be continued... + diff --git a/bleson/__init__.py b/bleson/__init__.py index e69de29..7518347 100644 --- a/bleson/__init__.py +++ b/bleson/__init__.py @@ -0,0 +1,7 @@ +from .providers import get_provider +from .core.roles import * +from .core.types import * +from .beacons.eddystone import * +from .logger import set_level + + diff --git a/bleson/beacons/__init__.py b/bleson/beacons/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bleson/beacons/eddystone.py b/bleson/beacons/eddystone.py new file mode 100644 index 0000000..66696a7 --- /dev/null +++ b/bleson/beacons/eddystone.py @@ -0,0 +1,103 @@ +from bleson.core.roles import Advertiser +from bleson.core.types import Advertisement +from bleson.logger import log + + +class EddystoneBeacon(Advertiser): + + def __init__(self, adapter, url=None): + super().__init__(adapter) + self.advertisement=Advertisement() + self.url = url + + @property + def url(self): + return self._url + + @url.setter + def url(self, url): + self._url = url + if url: + self.advertisement.raw_data=self.eddystone_url_adv_data(url) + log.debug("Beacon Adv raw data = {}".format(self.advertisement.raw_data)) + + # ------------------------------------------- + # Eddystone (pretty much as-is from the Google source) + # see: https://github.com/google/eddystone/blob/master/eddystone-url/implementations/PyBeacon/PyBeacon/PyBeacon.py + + schemes = [ + "http://www.", + "https://www.", + "http://", + "https://", + ] + + extensions = [ + ".com/", ".org/", ".edu/", ".net/", ".info/", ".biz/", ".gov/", + ".com", ".org", ".edu", ".net", ".info", ".biz", ".gov", + ] + + @classmethod + def encode_url(cls, url): + i = 0 + data = [] + + for s in range(len(cls.schemes)): + scheme = cls.schemes[s] + if url.startswith(scheme): + data.append(s) + i += len(scheme) + break + else: + raise Exception("Invalid url scheme") + + while i < len(url): + if url[i] == '.': + for e in range(len(cls.extensions)): + expansion = cls.extensions[e] + if url.startswith(expansion, i): + data.append(e) + i += len(expansion) + break + else: + data.append(0x2E) + i += 1 + else: + data.append(ord(url[i])) + i += 1 + + return data + + @classmethod + def eddystone_url_adv_data(cls, url): + log.info("Encoding URL for Eddystone beacon: '{}'".format(url)) + encodedurl = cls.encode_url(url) + encodedurlLength = len(encodedurl) + + if encodedurlLength > 18: + raise ValueError("Encoded url length {} is > 18 maximum length.".format(encodedurlLength)) + + message = [ + 0x02, # Flags length + 0x01, # Flags data type value + 0x1a, # Flags data + + 0x03, # Service UUID length + 0x03, # Service UUID data type value + 0xaa, # 16-bit Eddystone UUID + 0xfe, # 16-bit Eddystone UUID + + 5 + len(encodedurl), # Service Data length + 0x16, # Service Data data type value + 0xaa, # 16-bit Eddystone UUID + 0xfe, # 16-bit Eddystone UUID + + 0x10, # Eddystone-url frame type + 0xed, # txpower + ] + + message += encodedurl + + return bytearray(message) + + diff --git a/bleson/core/__init__.py b/bleson/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bleson/core/hci/__init__.py b/bleson/core/hci/__init__.py new file mode 100644 index 0000000..68ed6a6 --- /dev/null +++ b/bleson/core/hci/__init__.py @@ -0,0 +1,28 @@ +import struct + + +from bleson.core.hci.constants import EVT_LE_META_EVENT +from .constants import HCI_EVENTS +from .types import HCIPacket + +def event_to_string(event_code): + return HCI_EVENTS[event_code] + + +def parse_hci_event_packet(data): + evtcode, length = struct.unpack(" 127 else rssi_unsigned + if rssi == 127: + rssi = None # RSSI Not available + elif rssi >=20: + rssi = None # Reserverd range 20-126 + + return rssi \ No newline at end of file diff --git a/bleson/core/hci/constants.py b/bleson/core/hci/constants.py new file mode 100644 index 0000000..09391d6 --- /dev/null +++ b/bleson/core/hci/constants.py @@ -0,0 +1,232 @@ + +LE_PUBLIC_ADDRESS = 0x00 +LE_RANDOM_ADDRESS = 0x01 + +# Types of bluetooth scan + +SCAN_TYPE_PASSIVE = 0x00 +SCAN_TYPE_ACTIVE = 0x01 +SCAN_FILTER_DUPLICATES = 0x01 +SCAN_DISABLE = 0x00 +SCAN_ENABLE = 0x01 + + +# Advertisement event types +ADV_IND = 0x00 +ADV_DIRECT_IND = 0x01 +ADV_SCAN_IND = 0x02 +ADV_NONCONN_IND = 0x03 +ADV_SCAN_RSP = 0x04 + +FILTER_POLICY_NO_WHITELIST = 0x00 # Allow Scan Request from Any, Connect Request from Any +FILTER_POLICY_SCAN_WHITELIST = 0x01 # Allow Scan Request from White List Only, Connect Request from Any +FILTER_POLICY_CONN_WHITELIST = 0x02 # Allow Scan Request from Any, Connect Request from White List Only +FILTER_POLICY_SCAN_AND_CONN_WHITELIST = 0x03 # Allow Scan Request from White List Only, Connect Request from White List Only + + +EVT_CMD_COMPLETE = 0x0e +EVT_CMD_STATUS = 0x0f + + + +# 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 +EVT_DISCONN_COMPLETE = 0x05 +EVT_LE_META_EVENT = 0x3e # Core_4.2.pdf section: 7.7.65 LE Meta Event + + +ATT_CID = 0x0004 + + +ACL_START = 0x02 + + +OGF_LE_CTL = 0x08 +OGF_LINK_CTL = 0x01 + +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 +OCF_LE_SET_SCAN_RESPONSE_DATA = 0x0009 +OCF_DISCONNECT = 0x0006 + +LE_SET_SCAN_PARAMETERS_CMD = OCF_LE_SET_SCAN_PARAMETERS | OGF_LE_CTL << 10 +LE_SET_SCAN_ENABLE_CMD = OCF_LE_SET_SCAN_ENABLE | OGF_LE_CTL << 10 +LE_SET_ADVERTISING_PARAMETERS_CMD = OCF_LE_SET_ADVERTISING_PARAMETERS | OGF_LE_CTL << 10 +LE_SET_ADVERTISING_DATA_CMD = OCF_LE_SET_ADVERTISING_DATA | OGF_LE_CTL << 10 +LE_SET_SCAN_RESPONSE_DATA_CMD = OCF_LE_SET_SCAN_RESPONSE_DATA | OGF_LE_CTL << 10 +LE_SET_ADVERTISE_ENABLE_CMD = OCF_LE_SET_ADVERTISE_ENABLE | OGF_LE_CTL << 10 + +LE_CREATE_CONN_CMD = OCF_LE_CREATE_CONN | OGF_LE_CTL << 10 +DISCONNECT_CMD = OCF_DISCONNECT | OGF_LINK_CTL << 10 + + + +# Generic Access Profile +# BT Spec V4.0, Volume 3, Part C, Section 18 + +# https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile + + +""" +0x01 «Flags» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.3 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, sections 11.1.3 and 18.1 (v4.0)Core Specification Supplement, Part A, section 1.3 +0x02 «Incomplete List of 16-bit Service Class UUIDs» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.1 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, sections 11.1.1 and 18.2 (v4.0)Core Specification Supplement, Part A, section 1.1 +0x03 «Complete List of 16-bit Service Class UUIDs» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.1 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, sections 11.1.1 and 18.2 (v4.0)Core Specification Supplement, Part A, section 1.1 +0x04 «Incomplete List of 32-bit Service Class UUIDs» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.1 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, section 18.2 (v4.0)Core Specification Supplement, Part A, section 1.1 +0x05 «Complete List of 32-bit Service Class UUIDs» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.1 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, section 18.2 (v4.0)Core Specification Supplement, Part A, section 1.1 +0x06 «Incomplete List of 128-bit Service Class UUIDs» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.1 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, sections 11.1.1 and 18.2 (v4.0)Core Specification Supplement, Part A, section 1.1 +0x07 «Complete List of 128-bit Service Class UUIDs» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.1 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, sections 11.1.1 and 18.2 (v4.0)Core Specification Supplement, Part A, section 1.1 +0x08 «Shortened Local Name» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.2 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, sections 11.1.2 and 18.4 (v4.0)Core Specification Supplement, Part A, section 1.2 +0x09 «Complete Local Name» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.2 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, sections 11.1.2 and 18.4 (v4.0)Core Specification Supplement, Part A, section 1.2 +0x0A «Tx Power Level» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.5 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, sections 11.1.5 and 18.3 (v4.0)Core Specification Supplement, Part A, section 1.5 +0x0D «Class of Device» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.6 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, sections 11.1.5 and 18.5 (v4.0)Core Specification Supplement, Part A, section 1.6 +0x0E «Simple Pairing Hash C» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.6 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, sections 11.1.5 and 18.5 (v4.0) +0x0E «Simple Pairing Hash C-192» Core Specification Supplement, Part A, section 1.6 +0x0F «Simple Pairing Randomizer R» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.6 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, sections 11.1.5 and 18.5 (v4.0) +0x0F «Simple Pairing Randomizer R-192» Core Specification Supplement, Part A, section 1.6 +0x10 «Device ID» Device ID Profile v1.3 or later +0x10 «Security Manager TK Value» Bluetooth Core Specification:Vol. 3, Part C, sections 11.1.7 and 18.6 (v4.0)Core Specification Supplement, Part A, section 1.8 +0x11 «Security Manager Out of Band Flags» Bluetooth Core Specification:Vol. 3, Part C, sections 11.1.6 and 18.7 (v4.0)Core Specification Supplement, Part A, section 1.7 +0x12 «Slave Connection Interval Range» Bluetooth Core Specification:Vol. 3, Part C, sections 11.1.8 and 18.8 (v4.0)Core Specification Supplement, Part A, section 1.9 +0x14 «List of 16-bit Service Solicitation UUIDs» Bluetooth Core Specification:Vol. 3, Part C, sections 11.1.9 and 18.9 (v4.0)Core Specification Supplement, Part A, section 1.10 +0x15 «List of 128-bit Service Solicitation UUIDs» Bluetooth Core Specification:Vol. 3, Part C, sections 11.1.9 and 18.9 (v4.0)Core Specification Supplement, Part A, section 1.10 +0x16 «Service Data» Bluetooth Core Specification:Vol. 3, Part C, sections 11.1.10 and 18.10 (v4.0) +0x16 «Service Data - 16-bit UUID» Core Specification Supplement, Part A, section 1.11 +0x17 «Public Target Address» Bluetooth Core Specification:Core Specification Supplement, Part A, section 1.13 +0x18 «Random Target Address» Bluetooth Core Specification:Core Specification Supplement, Part A, section 1.14 +0x19 «Appearance» Bluetooth Core Specification:Core Specification Supplement, Part A, section 1.12 +0x1A «Advertising Interval» Bluetooth Core Specification:Core Specification Supplement, Part A, section 1.15 +0x1B «LE Bluetooth Device Address» Core Specification Supplement, Part A, section 1.16 +0x1C «LE Role» Core Specification Supplement, Part A, section 1.17 +0x1D «Simple Pairing Hash C-256» Core Specification Supplement, Part A, section 1.6 +0x1E «Simple Pairing Randomizer R-256» Core Specification Supplement, Part A, section 1.6 +0x1F «List of 32-bit Service Solicitation UUIDs» Core Specification Supplement, Part A, section 1.10 +0x20 «Service Data - 32-bit UUID» Core Specification Supplement, Part A, section 1.11 +0x21 «Service Data - 128-bit UUID» Core Specification Supplement, Part A, section 1.11 +0x22 «LE Secure Connections Confirmation Value» Core Specification Supplement Part A, Section 1.6 +0x23 «LE Secure Connections Random Value» Core Specification Supplement Part A, Section 1.6 +0x24 «URI» Bluetooth Core Specification:Core Specification Supplement, Part A, section 1.18 +0x25 «Indoor Positioning» Indoor Posiioning Service v1.0 or later +0x26 «Transport Discovery Data» Transport Discovery Service v1.0 or later +0x27 «LE Supported Features» Core Specification Supplement, Part A, Section 1.19 +0x28 «Channel Map Update Indication» Core Specification Supplement, Part A, Section 1.20 +0x29 «PB-ADV» +Mesh Profile Specification Section 5.2.1 +0x2A «Mesh Message» +Mesh Profile Specification Section 3.3.1 +0x2B «Mesh Beacon» +Mesh Profile Specification Section 3.9 +0x3D «3D Information Data» 3D Synchronization Profile, v1.0 or later +0xFF «Manufacturer Specific Data» Bluetooth Core Specification:Vol. 3, Part C, section 8.1.4 (v2.1 + EDR, 3.0 + HS and 4.0)Vol. 3, Part C, sections 11.1.4 and 18.11 (v4.0)Core Specification Supplement, Part A, section 1.4""" + +GAP_FLAGS = 0x01 +GAP_UUID_16BIT_INCOMPLETE = 0x02 +GAP_UUID_16BIT_COMPLETE = 0x03 +GAP_UUID_128BIT_INCOMPLETE = 0x06 +GAP_UUID_128BIT_COMPLETE = 0x07 +GAP_NAME_INCOMPLETE = 0x08 +GAP_NAME_COMPLETE = 0x09 +GAP_SERVICE_DATA = 0x16 +GAP_TX_POWER = 0x0A +GAP_MFG_DATA = 0xFF + + + +HCI_LE_META_EVENT = 0x3e + +HCI_LE_META_EVENTS = { + 0x01 : "LE_Connection_Complete", + 0x02 : "LE_Advertising_Report", + 0x03 : "LE_Connection_Update_Complete", + 0x04 : "LE_Read_Remote_Used_Features_Complete", + 0x05 : "LE_Long_Term_Key_Request", + 0x06 : "LE_Remote_Connection_Parameter_Request" + } + + +HCI_EVENTS = { + 0x01 : "Inquiry_Complete", + 0x02 : "Inquiry_Result", + 0x03 : "Connection_Complete", + 0x04 : "Connection_Request", + 0x05 : "Disconnection_Complete", + 0x06 : "Authentication_Complete", + 0x07 : "Remote_Name_Request_Complete", + 0x08 : "Encryption_Change", + 0x09 : "Change_Connection_Link_Key_Complete", + 0x0a : "Master_Link_Key_Complete", + 0x0b : "Read_Remote_Supported_Features_Complete", + 0x0c : "Read_Remote_Version_Information_Complete", + 0x0d : "QoS_Setup_Complete", + 0x0e : "Command_Complete", + 0x0f : "Command_Status", + 0x10 : "Hardware_Error", + 0x11 : "Flush_Occurred", + 0x12 : "Role_Change", + 0x13 : "Number_Of_Completed_Packets", + 0x14 : "Mode_Change", + 0x15 : "Return_Link_Keys", + 0x16 : "PIN_Code_Request", + 0x17 : "Link_Key_Request", + 0x18 : "Link_Key_Notification", + 0x19 : "Loopback_Command", + 0x1a : "Data_Buffer_Overflow", + 0x1b : "Max_Slots_Change", + 0x1c : "Read_Clock_Offset_Complete", + 0x1d : "Connection_Packet_Type_Changed", + 0x1e : "QoS_Violation", + 0x20 : "Page_Scan_Repetition_Mode_Change", + 0x21 : "Flow_Specification_Complete", + 0x22 : "Inquiry_Result_with_RSSI", + 0x23 : "Read_Remote_Extended_Features_Complete", + 0x2c : "Synchronous_Connection_Complete", + 0x2d : "Synchronous_Connection_Changed", + 0x2e : "Sniff_Subrating", + 0x2f : "Extended_Inquiry_Result", + 0x30 : "Encryption_Key_Refresh_Complete", + 0x31 : "IO_Capability_Request", + 0x32 : "IO_Capability_Response", + 0x33 : "User_Confirmation_Request", + 0x34 : "User_Passkey_Request", + 0x35 : "Remote_OOB_Data_Request", + 0x36 : "Simple_Pairing_Complete", + 0x38 : "Link_Supervision_Timeout_Changed", + 0x39 : "Enhanced_Flush_Complete", + 0x3b : "User_Passkey_Notification", + 0x3c : "Keypress_Notification", + 0x3d : "Remote_Host_Supported_Features_Notification", + HCI_LE_META_EVENT : "LE_Meta_Event", + 0x40 : "Physical_Link_Complete", + 0x41 : "Channel_Selected", + 0x42 : "Disconnection_Physical_Link_Complete", + 0x43 : "Physical_Link_Loss_Early_Warning", + 0x44 : "Physical_Link_Recovery", + 0x45 : "Logical_Link_Complete", + 0x46 : "Disconnection_Logical_Link_Complete", + 0x47 : "Flow_Spec_Modify_Complete", + 0x48 : "Number_Of_Completed_Data_Blocks", + 0x4c : "Short_Range_Mode_Change_Complete", + 0x4d : "AMP_Status_Change", + 0x49 : "AMP_Start_Test", + 0x4a : "AMP_Test_End", + 0x4b : "AMP_Receiver_Report", + 0x4e : "Triggered_Clock_Capture", + 0x4f : "Synchronization_Train_Complete", + 0x50 : "Synchronization_Train_Received", + 0x51 : "Connectionless_Slave_Broadcast_Receive", + 0x52 : "Connectionless_Slave_Broadcast_Timeout", + 0x53 : "Truncated_Page_Complete", + 0x54 : "Slave_Page_Response_Timeout", + 0x55 : "Connectionless_Slave_Broadcast_Channel_Map_Change", + 0x56 : "Inquiry_Response_Notification", + 0x57 : "Authenticated_Payload_Timeout_Expired", + } + + diff --git a/bleson/core/hci/type_converters.py b/bleson/core/hci/type_converters.py new file mode 100644 index 0000000..76929f9 --- /dev/null +++ b/bleson/core/hci/type_converters.py @@ -0,0 +1,122 @@ +from bleson.core.hci import rssi_from_byte +from bleson.core.hci.constants import * +from bleson.core.hci.types import HCIPacket, HCIPayload +from bleson.core.types import Advertisement, BDAddress +from bleson.logger import log +from bleson.utils import hex_string, bytearray_to_hexstring + +# meh... + +class AdvertisingDataConverters(object): + + @classmethod + def from_advertisement(cls, advertisement): + if advertisement.raw_data: + return advertisement.raw_data + + hci_payload = HCIPayload() + + #TODO: Advertisement should support __iter__ + + if advertisement.flags: + hci_payload.add_item(GAP_FLAGS, [advertisement.flags]) + if advertisement.name: + hci_payload.add_item(GAP_NAME_COMPLETE, advertisement.name.encode('ascii')) + + # TODO: more!! + + return hci_payload.data + + @classmethod + def from_hcipacket(cls, hci_packet: HCIPacket): + """ + uint8_t evt_type; + uint8_t bdaddr_type; + bdaddr_t bdaddr; + uint8_t length; + uint8_t data[0]; + """ + + if hci_packet.event_code != HCI_LE_META_EVENT and hci_packet.subevent_code != EVT_LE_ADVERTISING_REPORT: + raise ValueError("Invalid HCI Advertising Packet {}".format(hci_packet)) + + return cls.from_hcipayload(hci_packet.data) + + @classmethod + def from_hcipayload(cls, data): + data_info = "Data: {}".format(hex_string(data)) + pos_info = "POS : {}".format(''.join('{:02} '.format(x) for x in range(0, len(data)))) + log.debug(data_info) + log.debug(pos_info) + + num_reports = data[0] + log.debug("Num Reports {}".format(num_reports)) + + if num_reports !=1: + log.error("TODO: Only 1 Advertising report is supported, creating emtpy Advertisement") + # don't make it fatal + return Advertisement() + + gap_adv_type = ['ADV_IND', 'ADV_DIRECT_IND', 'ADV_SCAN_IND', 'ADV_NONCONN_IND', 'SCAN_RSP'][data[1]] + gap_addr_type = ['PUBLIC', 'RANDOM', 'PUBLIC_IDENTITY', 'RANDOM_STATIC'][data[2]] + gap_addr = data[8:2:-1] + rssi = rssi_from_byte(data[-1]) + + + advertisement = Advertisement(address=BDAddress(gap_addr), rssi=rssi) + advertisement.type = gap_adv_type + advertisement.address_type = gap_addr_type + + pos = 10 + while pos < len(data)-1: + log.debug("POS={}".format(pos)) + length = data[pos] + gap_type = data[pos+1] + payload = data[pos+2:pos+2+length-1] + log.debug("Pos={} Type=0x{:02x} Len={} Payload={}".format(pos, gap_type, length, hex_string(payload))) + + if GAP_FLAGS == gap_type: + advertisement.flags = payload[0] + log.debug("Flags={:02x}".format(advertisement.flags)) + + elif GAP_UUID_16BIT_COMPLETE == gap_type: + # + if length-1 > 2: + log.warning("TODO: >1 UUID16's found, not yet split into individual elements") + advertisement.uuid16s=[payload] + + elif GAP_UUID_128BIT_COMPLETE == gap_type: + # + #if length-1 > 2: + # log.warning("TODO: >1 UUID128's found, not yet split into individual elements") + advertisement.uuid128s=[payload] + log.debug(bytearray_to_hexstring(advertisement.uuid128s[0])) + + elif GAP_NAME_INCOMPLETE == gap_type: + advertisement.name = payload + advertisement.name_is_complete = False + log.debug("Incomplete Name={}".format(advertisement.name)) + + elif GAP_NAME_COMPLETE == gap_type: + advertisement.name = payload + advertisement.name_is_complete = True + log.debug("Complete Name={}".format(advertisement.name)) + + elif GAP_SERVICE_DATA == gap_type: + advertisement.service_data = payload + log.debug("Service Data={}".format(advertisement.service_data)) + + elif GAP_MFG_DATA == gap_type: + advertisement.mfg_data = payload + log.debug("Manufacturer Data={}".format(advertisement.mfg_data)) + + else: + log.warning("TODO: Unhandled GAP type, pos={} type=0x{:02x} len={}".format(pos, gap_type, length)) + log.warning(data_info) + log.warning(pos_info) + + pos += length +1 + + + log.debug(advertisement) + return advertisement diff --git a/bleson/core/hci/types.py b/bleson/core/hci/types.py new file mode 100644 index 0000000..c124d8f --- /dev/null +++ b/bleson/core/hci/types.py @@ -0,0 +1,45 @@ +import struct +from collections import namedtuple + +from bleson import log + +HCIPacket = namedtuple('HCIPacket', 'event_name event_code subevent_code data length') + + +class HCIPayload: + + def __init__(self, with_data=b''): + self.data = bytes(with_data) + self._parse_data() + + def add_item(self, tag, value): + log.debug("'{}' = {}".format(tag, value)) + newdata = self.data + struct.pack(" maxlen: + raise IndexError("Bad length byte 0x%02X in advertising data" % ll) + yield (tag, self.data[ofs+2:ofs+1+ll]) + ofs = ofs + 1 + ll + + def _parse_data(self): + self.tags={} + for (tag,value) in self: + self.tags[tag] = value + + def __str__(self): + return repr(self.tags) # TODO... \ No newline at end of file diff --git a/bleson/core/roles.py b/bleson/core/roles.py new file mode 100644 index 0000000..836f60f --- /dev/null +++ b/bleson/core/roles.py @@ -0,0 +1,43 @@ + +from bleson.interfaces.adapter import Adapter +from bleson.interfaces.role import Role + + +class Observer(Role): + + def __init__(self, adapter : Adapter, on_advertising_data=None): + self.adapter = adapter + self.on_advertising_data = on_advertising_data + + def start(self): + self.adapter.start_scanning() + + def stop(self): + self.adapter.stop_scanning() + + @property + def on_advertising_data(self): + return self.adapter.on_advtising_data + + @on_advertising_data.setter + def on_advertising_data(self, cb): + self.adapter.on_advertising_data=cb + + # Potential candidate to be a generator, maybe even an asyncio coroutine-generator? + # def discover_devices(self, timeout=None, filter=lambda advertisement: True): + # pass + +class Advertiser(Role): + + def __init__(self, adapter : Adapter, advertisement=None, scan_response=None): + self.adapter = adapter + self.advertisement = advertisement + self.scan_response = scan_response + + def start(self): + self.adapter.start_advertising(self.advertisement, self.scan_response) + + def stop(self): + self.adapter.stop_advertising() + + diff --git a/bleson/core/types.py b/bleson/core/types.py new file mode 100644 index 0000000..d26d30c --- /dev/null +++ b/bleson/core/types.py @@ -0,0 +1,124 @@ +from bleson.logger import log + +# Collection of value objects. + +# They represent data types in the bluetooth spec. + +# Object equlity is not based on the objects address, but instead for example, may be based on a MAC (BDAddress) + +class ValueObject(object): + pass + + # speculative... better placed elsewhere... + # def from_hci(self): + # pass + # + # def to_hci(self): + # pass + + +class BDAddress(ValueObject): + + def __init__(self, address:str=None): + # expect a string of the format "xx:xx:xx:xx:xx:xx" + # or a 6 element; tuple, list, bytes or bytearray + if isinstance(address, str): + # TODO: regex based check + if len(address) != 6*2+5: + raise ValueError('Unexpected address length of {} for {}'.format(len(address, address))) + address = address + elif isinstance(address, list) or isinstance(address, tuple): + if len(address) != 6: + raise ValueError('Unexpected address length of {} for {}'.format(len(address, address))) + address = "%02x:%02x:%02x:%02x:%02x:%02x" % address + elif isinstance(address, bytes) or isinstance(address, bytearray): + if len(address) != 6: + raise ValueError('Unexpected address length of {} for {}'.format(len(address, address))) + address = ':'.join([format(c, '02x') for c in address]) + else: + raise TypeError('Unsupported address type: {}'.format(type(address))) + + # TODO: keep this for hashing but return bytearray for getter (defaulted to little endian) + self.address=address.upper() + log.debug(self) + + def __repr__(self): + return "BDAddress('{}')".format(self.address) + + def __eq__(self, other): + return isinstance(other, BDAddress) and self.address == other.address + + def __hash__(self): + return hash(self.address) + + +class Device(ValueObject): + + def __init__(self, address:BDAddress=None, name=None, rssi=None): + self.address = address + self.name = name + self.rssi = rssi + log.debug(self) + + def __repr__(self): + return "Device(address={}, name={}, rssi={})".format(self.address, self.name, self.rssi) + + def __eq__(self, other): + return isinstance(other, Device) and self.address == other.address + + def __hash__(self): + return hash(self.address) + + +class Advertisement(ValueObject): + + def __init__(self, name=None, address=None, rssi=None, tx_power=None, raw_data=None): + #TODO: use kwargs + self.flags = 6 # uint8 # default to LE_GENERAL_DISCOVERABLE | BREDR_NOT_SUPPORTED + self.uuid16s = None # uuid16[] + self.uuid32s = None # uuid32[] + self.uuid128s = None # uuid12[] + self.name = name + self.name_is_complete = False # unsigned + self.tx_pwr_lvl = tx_power # unsigned + #slave_itvl_range + self.svc_data_uuid16 = None # uuid16[] + self.public_tgt_addr = None # uint8[] + self.appearance = None # unit16 + self.adv_itvl = None # uint16 + self.svc_data_uuid32 = None # uint8 + self.svc_data_uuid128 = None # uint8 + self.uri = None # unit8 + self.mfg_data = None # unit8 + self.service_data = None # TODO: check this... + self.type = None + self.address_type = None + self.address = address + self.eir = None + self.rssi = rssi + + self.raw_data = raw_data + log.debug(self) + + + def __repr__(self): + # TODO: UUID's, appearance etc... + return "Advertisement(flags=0x{:02x}, name={}, rssi={})".format(self.flags, self.name, self.rssi) + + + @property + def name(self): + return self._name + + @name.setter + def name(self, name): + if isinstance(name, bytearray): + self._name = str(name,'ascii') # TODO: is the name really UTF8 encoded according to the Core spec? + else: + self._name = str(name) # unit8 + + +class ScanResponse(ValueObject): + pass + + diff --git a/bleson/interfaces/__init__.py b/bleson/interfaces/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bleson/interfaces/adapter.py b/bleson/interfaces/adapter.py new file mode 100644 index 0000000..aa6581b --- /dev/null +++ b/bleson/interfaces/adapter.py @@ -0,0 +1,28 @@ +import abc + +class Adapter(object): + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def on(self): + raise NotImplementedError + + @abc.abstractmethod + def off(self): + raise NotImplementedError + + @abc.abstractmethod + def start_scanning(self): + raise NotImplementedError + + @abc.abstractmethod + def stop_scanning(self): + raise NotImplementedError + + @abc.abstractmethod + def start_advertising(self, advertisement, scan_response=None): + raise NotImplementedError + + @abc.abstractmethod + def stop_advertising(self): + raise NotImplementedError diff --git a/bleson/interfaces/provider.py b/bleson/interfaces/provider.py new file mode 100644 index 0000000..76ed7f6 --- /dev/null +++ b/bleson/interfaces/provider.py @@ -0,0 +1,10 @@ +import abc + + +class Provider(object): + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def get_adapter(cls, adapter_id=None): + """Return an Adapter instance, default to first one available""" + raise NotImplementedError diff --git a/bleson/interfaces/role.py b/bleson/interfaces/role.py new file mode 100644 index 0000000..dd38800 --- /dev/null +++ b/bleson/interfaces/role.py @@ -0,0 +1,20 @@ +import abc + +class Role(object): + __metaclass__ = abc.ABCMeta + + def __enter__(self): + self.start() + + def __exit__(self, type, value, traceback): + self.stop() + + @abc.abstractmethod + def start(self): + """Start the role.""" + raise NotImplementedError + + @abc.abstractmethod + def stop(self): + """Stop the role.""" + raise NotImplementedError diff --git a/bleson/logger.py b/bleson/logger.py new file mode 100644 index 0000000..66b3621 --- /dev/null +++ b/bleson/logger.py @@ -0,0 +1,13 @@ +from logging import basicConfig, getLogger, INFO, DEBUG, WARN, WARNING, ERROR + +LOGGER_NAME='bleson' +LOG_FORMAT= '%(asctime)s %(levelname)6s - %(filename)24s:%(lineno)3s - %(funcName)24s(): %(message)s' + +basicConfig(level=INFO, format=LOG_FORMAT) +log = getLogger(LOGGER_NAME) + +def set_level(level): + logger = getLogger(LOGGER_NAME) + previous_level = logger.level + logger.setLevel(level) + return previous_level \ No newline at end of file diff --git a/bleson/providers/__init__.py b/bleson/providers/__init__.py new file mode 100644 index 0000000..2913e92 --- /dev/null +++ b/bleson/providers/__init__.py @@ -0,0 +1,22 @@ +import logging +import sys + +from bleson.logger import log + +_provider = None + +def get_provider(): + global _provider + + if _provider is None: + if sys.platform.startswith('linux'): + from bleson.providers.linux.linux_provider import LinuxProvider + _provider = LinuxProvider() + elif sys.startswith('darwin'): + raise NotImplementedError() + elif sys.startswith('win32'): + raise NotImplementedError() + else: + raise RuntimeError('Platform {0} is not supported!'.format(sys.platform)) + + return _provider diff --git a/bleson/providers/linux/__init__.py b/bleson/providers/linux/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bleson/providers/linux/constants.py b/bleson/providers/linux/constants.py new file mode 100644 index 0000000..f743c1d --- /dev/null +++ b/bleson/providers/linux/constants.py @@ -0,0 +1,21 @@ + +HCIPY_HCI_CMD_STRUCT_HEADER = "1 else 10 + +adapter = get_provider().get_adapter() + +advertiser = Advertiser(adapter) +advertisement = Advertisement() +advertisement.name = "bleson" + +advertiser.advertisement = advertisement + +advertiser.start() +sleep(WAIT_TIME) +advertiser.stop() + diff --git a/examples/basic_eddystone_beacon.py b/examples/basic_eddystone_beacon.py new file mode 100644 index 0000000..4e9587a --- /dev/null +++ b/examples/basic_eddystone_beacon.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import sys +from time import sleep +from bleson import get_provider, EddystoneBeacon + +# Get the wait time from the first script argument or default it to 10 seconds +WAIT_TIME = int(sys.argv[1]) if len(sys.argv)>1 else 10 + +try: + adapter = get_provider().get_adapter() + + beacon = EddystoneBeacon(adapter) + beacon.url = 'https://www.bluetooth.com/' + beacon.start() + sleep(WAIT_TIME) + +except KeyboardInterrupt: + pass + +finally: + beacon.stop() diff --git a/examples/basic_observer.py b/examples/basic_observer.py new file mode 100644 index 0000000..56d5aea --- /dev/null +++ b/examples/basic_observer.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import sys +from time import sleep +from bleson import get_provider, Observer + +# Get the wait time from the first script argument or default it to 10 seconds +WAIT_TIME = int(sys.argv[1]) if len(sys.argv)>1 else 10 + + +def on_advertisement(advertisement): + print(advertisement) + +adapter = get_provider().get_adapter() + +observer = Observer(adapter) +observer.on_advertising_data = on_advertisement + +observer.start() +sleep(WAIT_TIME) +observer.stop() + diff --git a/examples/context_advertiser.py b/examples/context_advertiser.py new file mode 100644 index 0000000..87b53e9 --- /dev/null +++ b/examples/context_advertiser.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +import sys +from time import sleep + +from bleson import get_provider, Advertiser, Advertisement + +# Get the wait time from the first script argument or default it to 10 seconds +WAIT_TIME = int(sys.argv[1]) if len(sys.argv)>1 else 10 + + +with Advertiser(get_provider().get_adapter(), Advertisement(name='bleson')): + sleep(WAIT_TIME) + diff --git a/examples/context_eddystone_beacon.py b/examples/context_eddystone_beacon.py new file mode 100644 index 0000000..e1039c5 --- /dev/null +++ b/examples/context_eddystone_beacon.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 + +import sys +from time import sleep +from bleson import get_provider, EddystoneBeacon + +# Get the wait time from the first script argument or default it to 10 seconds +WAIT_TIME = int(sys.argv[1]) if len(sys.argv)>1 else 10 + +with EddystoneBeacon(get_provider().get_adapter(), 'https://www.bluetooth.com/'): + sleep(WAIT_TIME) diff --git a/examples/context_observer.py b/examples/context_observer.py new file mode 100644 index 0000000..c459427 --- /dev/null +++ b/examples/context_observer.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +import sys +from time import sleep +from bleson import get_provider, Observer +from bleson.logger import log, set_level, DEBUG + +set_level(DEBUG) + + +# Get the wait time from the first script argument or default it to 10 seconds +WAIT_TIME = int(sys.argv[1]) if len(sys.argv)>1 else 10 + +with Observer(get_provider().get_adapter(), lambda device: log.info(device)): + sleep(WAIT_TIME) + diff --git a/setup.py b/setup.py index fd36b49..ac03fa9 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,10 @@ from distutils.core import setup +from setuptools import find_packages setup( name='bleson', - version='0.0.1', - packages=['bleson'], + version='0.0.2', + packages= find_packages(), url='https://github.com/TheCellule/python-bleson', license='MIT', author='TheCellule', diff --git a/tests/test_examples.py b/tests/test_examples.py new file mode 100644 index 0000000..7e95742 --- /dev/null +++ b/tests/test_examples.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + +# A test to run all the example scripts to check for basic compilation or unexpected runtime exceptions +# Does not validate the correctness of any example +# All examples should return in a timely manor. TODO: add a forced timeout to this script executor. + +import sys +import os +import glob + +import unittest +import runpy + +from bleson.logger import log + +TEST_DURATION=1 + +class TestExamples(unittest.TestCase): + + @classmethod + def setUpClass(cls): + path = os.path.abspath(__file__) + dir_path = os.path.dirname(path) + examples_path = os.path.join(dir_path, '..', 'examples') + examples_glob = os.path.join(examples_path, '*.py') + + cls.scripts = glob.glob(examples_glob) + log.debug(cls.scripts) + + def test_all_examples(self): + for script in self.scripts: + log.info("Running {}".format(script)) + sys.argv = ['', str(TEST_DURATION)] + runpy.run_path(script) + diff --git a/tests/test_internal_advertising_data.py b/tests/test_internal_advertising_data.py new file mode 100644 index 0000000..7c5a225 --- /dev/null +++ b/tests/test_internal_advertising_data.py @@ -0,0 +1,23 @@ +import unittest + +from bleson.logger import log, set_level, DEBUG +from bleson.core.types import Advertisement + +from bleson.core.hci.type_converters import AdvertisingDataConverters + +set_level(DEBUG) + +class TestAdvertisingData(unittest.TestCase): + + + def test_create_hcipayload_from_advertising(self): + + adv = Advertisement() + adv.name = 'blename' + log.info(adv.raw_data) + + hci_payload = AdvertisingDataConverters.from_advertisement(adv) + log.info(hci_payload) + + self.assertEqual(b'\x02\x01\x06\x08\x09blename', hci_payload) + diff --git a/tests/test_internal_hci_packet_parse.py b/tests/test_internal_hci_packet_parse.py new file mode 100644 index 0000000..8e117b1 --- /dev/null +++ b/tests/test_internal_hci_packet_parse.py @@ -0,0 +1,91 @@ + +import unittest + +from bleson.core.hci import parse_hci_event_packet +from bleson.core.hci.type_converters import AdvertisingDataConverters +from bleson.logger import set_level, DEBUG +from bleson.utils import hexstring_to_bytearray + +set_level(DEBUG) + +EDDYSTONE_ADV_REPORT = hexstring_to_bytearray('3e 21 02 01 00 00 1e ac e7 eb 27 b8 15 02 01 1a 03 03 aa fe 0d 16 aa fe 10 ed 00 67 6f 6f 67 6c 65 00 bc') +BBC_MICROBIT_ADV_REPORT = hexstring_to_bytearray('3e 26 02 01 00 01 f6 15 b0 c9 3a f5 1a 02 01 06 16 09 42 42 43 20 6d 69 63 72 6f 3a 62 69 74 20 5b 74 65 67 69 70 5d e4') +JSPUCK_ADV_REPORT = hexstring_to_bytearray('3e 1d 02 01 00 01 43 7b 30 8e 58 f4 11 02 01 05 0d 09 50 75 63 6b 2e 6a 73 20 37 62 34 33 ac') +UUID128_ADV_REPORT = hexstring_to_bytearray('3e 1d 02 01 04 01 43 7b 30 8e 58 f4 12 11 07 9e ca dc 24 0e e5 a9 e0 93 f3 a3 b5 01 00 40 6e') + +class TestHCIParsers(unittest.TestCase): + + def test_parse_eddystone_advertisment_report(self): + # Some context... + # eddystone adv data = [ + # 0x02, # Flags length + # 0x01, # Flags data type value + # 0x1a, # Flags data + # + # 0x03, # Service UUID length + # 0x03, # Service UUID data type value + # 0xaa, # 16-bit Eddystone UUID + # 0xfe, # 16-bit Eddystone UUID + # + # 0x0d, #5 + len(encodedurl), # Service Data length + # 0x16, # Service Data data type value + # 0xaa, # 16-bit Eddystone UUID + # 0xfe, # 16-bit Eddystone UUID + # + # 0x10, # Eddystone-url frame type + # 0xed, # txpower + # + # 0x00, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x00 # message += encode_url("http://www.google.com/") + # #from bleson.beacons.eddystone import EddystoneBeacon + # #log.info(hex_string(EddystoneBeacon.encode_url("http://www.google.com/"))) + # + # ] + + # running : context_eddystone_beacon.py, + # output from : sudo hcidump -i hci0 -X -R + + # > 0000: 04 3e 21 02 01 00 00 1e ac e7 eb 27 b8 15 02 01 .>!........'.... + # 0010: 1a 03 03 aa fe 0d 16 aa fe 10 ed 00 67 6f 6f 67 ............goog + # 0020: 6c 65 00 bc le.. + + # hci_packet = '04 3e 21 02 01 00 00 1e ac e7 eb 27 b8 15 02 01 1a 03 03 aa fe 0d 16 aa fe 10 ed 00 67 6f 6f 67 6c 65 00 bc' + # 04 is HCI_EVENT_PACKET type, one of a handful of types recevied by the Socket listener + + hci_packet = parse_hci_event_packet(EDDYSTONE_ADV_REPORT) + + advertisement = AdvertisingDataConverters.from_hcipacket(hci_packet) + + self.assertEqual(0x1a, advertisement.flags) + self.assertEqual(-68, advertisement.rssi) + self.assertEqual([bytearray([0xaa, 0xfe])], advertisement.uuid16s) + self.assertEqual(b'\xaa\xfe\x10\xed\x00google\x00', advertisement.service_data) + + + def test_parse_microbit_advertisment_report(self): + + hci_packet = parse_hci_event_packet( BBC_MICROBIT_ADV_REPORT) + + advertisement = AdvertisingDataConverters.from_hcipacket(hci_packet) + + self.assertEqual(0x06, advertisement.flags) + self.assertEqual(-28, advertisement.rssi) + self.assertEqual('BBC micro:bit [tegip]', advertisement.name) + self.assertEqual(True, advertisement.name_is_complete) + + def test_parse_jspuck_advertisment_report(self): + + hci_packet = parse_hci_event_packet(JSPUCK_ADV_REPORT) + + advertisement = AdvertisingDataConverters.from_hcipacket(hci_packet) + + self.assertEqual(0x05, advertisement.flags) + self.assertEqual(-84, advertisement.rssi) + self.assertEqual('Puck.js 7b43', advertisement.name) + self.assertEqual(True, advertisement.name_is_complete) + + def test_parse_jspuck_advertisment_report(self): + + hci_packet = parse_hci_event_packet(UUID128_ADV_REPORT) + + advertisement = AdvertisingDataConverters.from_hcipacket(hci_packet) + diff --git a/tests/test_role_advertiser.py b/tests/test_role_advertiser.py new file mode 100644 index 0000000..686d012 --- /dev/null +++ b/tests/test_role_advertiser.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + +import unittest +from time import sleep + +from bleson import get_provider +from bleson.core.types import BDAddress, Advertisement +from bleson.core.roles import Advertiser +from bleson.logger import log + +ADVERTISE_TIME = 5 # seconds + +MICROBIT1_BDADDR=BDAddress('f5:3a:c9:b0:15:f6') + + +class TestAdvertiser(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.adapter = get_provider().get_adapter() + + def test_advertiser(self): + + advertisement = Advertisement() + advertisement.name = 'bleson' + + advertiser = Advertiser(self.adapter) + advertiser.advertisement = advertisement + + advertiser.start() + log.info("Starting Advertiser role for {} seconds, advertised name={}".format(ADVERTISE_TIME, advertisement.name)) + + sleep(ADVERTISE_TIME) + advertiser.stop() + diff --git a/tests/test_role_dual_adapters.py b/tests/test_role_dual_adapters.py new file mode 100644 index 0000000..55beedb --- /dev/null +++ b/tests/test_role_dual_adapters.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 + +# This test expects the host to have 2 Bluetooth LE adapters available. +# e.g. a Pi3/ZeroW with a BluetoothLE USB dongle. + +import unittest +from time import sleep + +from bleson import get_provider +from bleson.core.types import Advertisement +from bleson.core.roles import Observer, Advertiser +from bleson.beacons.eddystone import EddystoneBeacon +from bleson.logger import log, set_level, DEBUG, INFO + + +TEST_DURATION_SECS = 5 + + +class TestDualAdapters(unittest.TestCase): + + @classmethod + def setUpClass(cls): + provider = get_provider() + cls.adapter0 = provider.get_adapter(0) # Used for the Observer role + cls.adapter1 = provider.get_adapter(1) # Used for the Advertiser role + + # Turn on bleson DEBUG logging, keeping note of the previous setting. + cls.previous_log_level = set_level(DEBUG) + + @classmethod + def tearDownClass(cls): + # Restore logging level, in case we're running in a test suite + set_level(cls.previous_log_level) + + def test_observer_advertiser_pair(self): + + # ---------------------------------------- + # Setup the Observer, on adatper 0 + + observer = Observer(self.adapter0) + + # The observer callback records the found device BDAddress + + found_addresses = set() + + def advertisement_update(advertisement): + log.info("Found: {}".format(advertisement)) + found_addresses.add(advertisement.address) # instance of 'BDAddress' + + observer.on_advertising_data = advertisement_update + + + # ---------------------------------------- + # Setup the Advertiser, on adapter 1 + + advertiser = Advertiser(self.adapter1) + + advertisement = Advertisement() + advertisement.name = "bleson" + + advertiser.advertisement = advertisement + + + # ---------------------------------------- + # Start the Observer and the Advertiser. + + observer.start() + advertiser.start() + + # wait... + log.info("Starting Advertiser & Observer roles for {} seconds, advertised name={}".format(TEST_DURATION_SECS, advertisement.name)) + + sleep(TEST_DURATION_SECS) + + # Stop + advertiser.stop() + observer.stop() + + + # ---------------------------------------- + # Did the left hand find the right hand? + + log.info("Observer's address: {}".format(self.adapter0.device.address)) + log.info("Advertiser's address: {}".format(self.adapter1.device.address)) + log.info("Observed BDAddresses: {}".format(found_addresses)) + + self.assertTrue(self.adapter1.device.address in found_addresses) + + + + def test_observer_beacon_pair(self): + + observer = Observer(self.adapter0) + found_addresses = set() + + def advertisement_update(advertisement): + log.info("Found: {}".format(advertisement)) + found_addresses.add(advertisement.address) # instance of 'BDAddress' + + observer.on_advertising_data = advertisement_update + + beacon = EddystoneBeacon(self.adapter1) + + + # ---------------------------------------- + # Start the Observer and the Advertiser. + + observer.start() + beacon.start() + + # wait... + log.info("Starting Advertiser & Observer roles for {} seconds, advertised url={}".format(TEST_DURATION_SECS, beacon.url)) + + sleep(TEST_DURATION_SECS) + + # Stop + beacon.stop() + observer.stop() + + + # ---------------------------------------- + # Did the left hand find the right hand? + + log.info("Observer's address: {}".format(self.adapter0.device.address)) + log.info("Beacons's address: {}".format(self.adapter1.device.address)) + log.info("Observed BDAddresses: {}".format(found_addresses)) + + self.assertTrue(self.adapter1.device.address in found_addresses) diff --git a/tests/test_role_eddystone_beacon.py b/tests/test_role_eddystone_beacon.py new file mode 100644 index 0000000..4575c8a --- /dev/null +++ b/tests/test_role_eddystone_beacon.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +import unittest +from time import sleep + +from bleson import get_provider +from bleson.beacons.eddystone import EddystoneBeacon +from bleson.logger import log + +BEACON_TIME = 10 + +class TestBeacons(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.adapter = get_provider().get_adapter() + + def test_eddystone(self): + beacon = EddystoneBeacon(self.adapter, 'https://www.google.com/') + beacon.start() + log.info("Starting beacon for {} seconds, url={}".format(BEACON_TIME, beacon.url)) + sleep(BEACON_TIME) + beacon.stop() diff --git a/tests/test_role_observer.py b/tests/test_role_observer.py new file mode 100644 index 0000000..194a361 --- /dev/null +++ b/tests/test_role_observer.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +import unittest +from time import sleep + + +from bleson import get_provider +from bleson.core.types import BDAddress +from bleson.core.roles import Observer +from bleson.logger import log, set_level, DEBUG + +SCANTIME = 5 # seconds + +MICROBIT1_BDADDR=BDAddress('f5:3a:c9:b0:15:f6') + + +class TestRoles(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.adapter = get_provider().get_adapter() + # Turn on bleson DEBUG logging, keeping note of the previous setting. + cls.previous_log_level = set_level(DEBUG) + + @classmethod + def tearDownClass(cls): + # Restore logging level, in case we're running in a test suite + set_level(cls.previous_log_level) + + def test_observer(self): + + found_devices = set() + observer = Observer(self.adapter) + + def advertisement_update(advertisement): + found_devices.add(advertisement.address) + + observer.on_advertising_data = advertisement_update + + observer.start() + log.info("Starting Observer role for {} seconds".format(SCANTIME)) + + sleep(SCANTIME) + observer.stop() + + log.info("Found: {}".format(found_devices)) + + self.assertTrue(MICROBIT1_BDADDR in found_devices) + From c6f9f5b1c5ce092b1d30741ed01c5976f7080c08 Mon Sep 17 00:00:00 2001 From: Wayne Keenan Date: Mon, 6 Nov 2017 06:39:21 +0000 Subject: [PATCH 02/13] Expanded, reworded and removed sample code from the Examples section. --- README.rst | 42 ++++++++---------------------------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/README.rst b/README.rst index d446405..e07c627 100644 --- a/README.rst +++ b/README.rst @@ -38,57 +38,31 @@ Outline of the 'bleson' Python packages an end-user would use to play with Bluet Examples ======== -Examples are abbreviated below, plese see 'examples' folder for more details. +Please see 'examples' folder for more details. +Examples prefixed with 'basic_' shows basic API usage. +Examples prefixed with 'context_' shows 'with context' API usage. + Example - Advertiser -------------------- -.. code:: python - - from bleson import get_provider, Advertiser, Advertisement - - adapter = get_provider().get_adapter() - advertiser = Advertiser(adapter) - advertisement = Advertisement(name = "bleson") - advertiser.start() - advertiser.stop() - +Shows how to create custom advertisement. Example - Eddystone Beacon -------------------------- -.. code:: python - - from bleson import get_provider, EddystoneBeacon - - adapter = get_provider().get_adapter() - beacon = EddystoneBeacon(adapter) - beacon.url = 'https://www.bluetooth.com/' - beacons.start() - beacons.stop() - +Shows how to setup a Physical Web beacon Example - Observer ------------------ -.. code:: python - - from bleson import get_provider, Observer - - adapter = get_provider().get_adapter() - - def on_advertisement(advertisement): - print(advertisement) +Shows how to scan for local devices. - observer = Observer(adapter) - observer.on_advertising_data = on_advertisement - observer.start() - observer.stop() Tests ===== -Please see the testsuite in the 'tests' folder. +Please see the 'tests' folder. Internal API From 8eafa9a96dd34e250e0004ebb51f9f5a56eb2c3b Mon Sep 17 00:00:00 2001 From: Wayne Keenan Date: Tue, 7 Nov 2017 07:30:16 +0000 Subject: [PATCH 03/13] minro Readme examples rewording --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index e07c627..a0d2428 100644 --- a/README.rst +++ b/README.rst @@ -39,8 +39,8 @@ Examples ======== Please see 'examples' folder for more details. -Examples prefixed with 'basic_' shows basic API usage. -Examples prefixed with 'context_' shows 'with context' API usage. +Examples prefixed with 'basic_' shows basic Bleson API usage. +Examples prefixed with 'context_' shows Blesons context maanger ('with' keyword) API usage. Example - Advertiser From e1b9a565cc4cfabd0d2f5ebb0cdb7d3119b5b402 Mon Sep 17 00:00:00 2001 From: Wayne Keenan Date: Wed, 8 Nov 2017 11:19:00 +0000 Subject: [PATCH 04/13] added UUID16 and UUID128 type along with advertising and parsing support. added HRM advertising only example --- bleson/__init__.py | 2 +- bleson/core/hci/__init__.py | 28 +---- bleson/core/hci/constants.py | 13 ++ bleson/core/hci/type_converters.py | 111 ++++++++++++++--- bleson/core/hci/types.py | 29 ++++- bleson/core/types.py | 144 +++++++++++++++++++---- bleson/providers/__init__.py | 5 +- bleson/providers/linux/linux_adapter.py | 4 +- bleson/providers/linux/linux_provider.py | 2 +- bleson/utils.py | 14 --- examples/basic_advertiser_heartrate.py | 28 +++++ tests/test_internal_advertising_data.py | 37 +++++- tests/test_internal_hci_packet_parse.py | 78 ++++++++++-- tests/test_internal_type_uuid.py | 25 ++++ 14 files changed, 422 insertions(+), 98 deletions(-) delete mode 100644 bleson/utils.py create mode 100644 examples/basic_advertiser_heartrate.py create mode 100644 tests/test_internal_type_uuid.py diff --git a/bleson/__init__.py b/bleson/__init__.py index 7518347..f22aa4f 100644 --- a/bleson/__init__.py +++ b/bleson/__init__.py @@ -3,5 +3,5 @@ from .core.types import * from .beacons.eddystone import * from .logger import set_level - +from .core.hci.constants import * diff --git a/bleson/core/hci/__init__.py b/bleson/core/hci/__init__.py index 68ed6a6..24be18e 100644 --- a/bleson/core/hci/__init__.py +++ b/bleson/core/hci/__init__.py @@ -1,28 +1,4 @@ -import struct +from .constants import * +from .types import * -from bleson.core.hci.constants import EVT_LE_META_EVENT -from .constants import HCI_EVENTS -from .types import HCIPacket - -def event_to_string(event_code): - return HCI_EVENTS[event_code] - - -def parse_hci_event_packet(data): - evtcode, length = struct.unpack(" 127 else rssi_unsigned - if rssi == 127: - rssi = None # RSSI Not available - elif rssi >=20: - rssi = None # Reserverd range 20-126 - - return rssi \ No newline at end of file diff --git a/bleson/core/hci/constants.py b/bleson/core/hci/constants.py index 09391d6..5cbfbdd 100644 --- a/bleson/core/hci/constants.py +++ b/bleson/core/hci/constants.py @@ -139,6 +139,19 @@ +# see: https://github.com/ARMmbed/ble/blob/8d97fced5440d78c9557693b6d1632f1ab5d77b7/ble/GapAdvertisingData.h + +LE_LIMITED_DISCOVERABLE = 0x01 #, /**< Peripheral device is discoverable for a limited period of time. */ +LE_GENERAL_DISCOVERABLE = 0x02 #, /**< Peripheral device is discoverable at any moment. */ +BREDR_NOT_SUPPORTED = 0x04 #, /**< Peripheral device is LE only. */ +SIMULTANEOUS_LE_BREDR_C = 0x08 #, /**< Not relevant - central mode only. */ +SIMULTANEOUS_LE_BREDR_H = 0x10 # /**< Not relevant - central mode only. */ + + +# https://github.com/ARMmbed/ble/blob/8d97fced5440d78c9557693b6d1632f1ab5d77b7/ble/GapAdvertisingData.h + + + HCI_LE_META_EVENT = 0x3e HCI_LE_META_EVENTS = { diff --git a/bleson/core/hci/type_converters.py b/bleson/core/hci/type_converters.py index 76929f9..3acb5f1 100644 --- a/bleson/core/hci/type_converters.py +++ b/bleson/core/hci/type_converters.py @@ -1,11 +1,49 @@ -from bleson.core.hci import rssi_from_byte +import struct + from bleson.core.hci.constants import * from bleson.core.hci.types import HCIPacket, HCIPayload -from bleson.core.types import Advertisement, BDAddress +from bleson.core.types import Advertisement, BDAddress, UUID16, UUID128 from bleson.logger import log -from bleson.utils import hex_string, bytearray_to_hexstring -# meh... + + +def hex_string(data): + return ''.join('{:02x} '.format(x) for x in data) + + +def bytearray_to_hexstring(ba): + return hex_string(ba) + #return binascii.hexlify(ba) + + +def hexstring_to_bytearray(hexstr): + """" + hexstr: e.g. "de ad be ef 00" + """ + return bytearray.fromhex(hexstr) + +def event_to_string(event_code): + return HCI_EVENTS[event_code] + + +def parse_hci_event_packet(data): + evtcode, length = struct.unpack(" 127 else rssi_unsigned + if rssi == 127: + rssi = None # RSSI Not available + elif rssi >=20: + rssi = None # Reserverd range 20-126 + + return rssi + class AdvertisingDataConverters(object): @@ -16,12 +54,27 @@ def from_advertisement(cls, advertisement): hci_payload = HCIPayload() - #TODO: Advertisement should support __iter__ + # TODO: Advertisement should support __iter__ + + # WARN: Although the order isn't important, the Python tests are (currently) order sensitive. if advertisement.flags: hci_payload.add_item(GAP_FLAGS, [advertisement.flags]) + + if advertisement.uuid16s: + hci_payload.add_item(GAP_UUID_16BIT_COMPLETE, advertisement.uuid16s) + # TODO: incompleteuuid16s + + if advertisement.uuid128s: + hci_payload.add_item(GAP_UUID_128BIT_COMPLETE, advertisement.uuid128s) + # TODO: incompleteuuid128s + + if advertisement.tx_pwr_lvl: + hci_payload.add_item(GAP_TX_POWER, advertisement.tx_pwr_lvl) + if advertisement.name: - hci_payload.add_item(GAP_NAME_COMPLETE, advertisement.name.encode('ascii')) + # TODO: 'incomplete' name + hci_payload.add_item(GAP_NAME_COMPLETE, advertisement.name.encode('ascii')) # TODO: check encoding is ok # TODO: more!! @@ -57,6 +110,7 @@ def from_hcipayload(cls, data): # don't make it fatal return Advertisement() + # TODO: move these 2 LUTs to a better place gap_adv_type = ['ADV_IND', 'ADV_DIRECT_IND', 'ADV_SCAN_IND', 'ADV_NONCONN_IND', 'SCAN_RSP'][data[1]] gap_addr_type = ['PUBLIC', 'RANDOM', 'PUBLIC_IDENTITY', 'RANDOM_STATIC'][data[2]] gap_addr = data[8:2:-1] @@ -80,17 +134,42 @@ def from_hcipayload(cls, data): log.debug("Flags={:02x}".format(advertisement.flags)) elif GAP_UUID_16BIT_COMPLETE == gap_type: - # - if length-1 > 2: - log.warning("TODO: >1 UUID16's found, not yet split into individual elements") - advertisement.uuid16s=[payload] + uuids = [] + byte_pos = 0 + if len(payload) % 2 !=0: + raise ValueError("PAyload is not divisible by 2 for UUID16") + + while byte_pos < len(payload): + log.debug('byte_pos={}'.format(byte_pos)) + byte_pair = payload[byte_pos:byte_pos+2] + log.debug('byte pair = {}'.format(byte_pair)) + uuid = UUID16(byte_pair) + uuids.append(uuid) + byte_pos += 2 + + advertisement.uuid16s=uuids elif GAP_UUID_128BIT_COMPLETE == gap_type: - # - #if length-1 > 2: - # log.warning("TODO: >1 UUID128's found, not yet split into individual elements") - advertisement.uuid128s=[payload] - log.debug(bytearray_to_hexstring(advertisement.uuid128s[0])) + + # if length-1 > 16: + # log.warning("TODO: >1 UUID128's found, not yet split into individual elements") + #advertisement.uuid128s=[UUID128(payload)] + uuids = [] + byte_pos = 0 + if len(payload) % 16 !=0: + raise ValueError("Payload is not divisible by 16 for UUID128") + + while byte_pos < len(payload): + log.debug('byte_pos={}'.format(byte_pos)) + byte_list = payload[byte_pos:byte_pos+16] + log.debug('byte_list = {}'.format(byte_list)) + uuid = UUID128(byte_list) + uuids.append(uuid) + byte_pos += 16 + + advertisement.uuid128s=uuids + + log.debug(advertisement.uuid128s) elif GAP_NAME_INCOMPLETE == gap_type: advertisement.name = payload @@ -120,3 +199,5 @@ def from_hcipayload(cls, data): log.debug(advertisement) return advertisement + + diff --git a/bleson/core/hci/types.py b/bleson/core/hci/types.py index c124d8f..8610e79 100644 --- a/bleson/core/hci/types.py +++ b/bleson/core/hci/types.py @@ -13,14 +13,31 @@ def __init__(self, with_data=b''): self._parse_data() def add_item(self, tag, value): - log.debug("'{}' = {}".format(tag, value)) - newdata = self.data + struct.pack(">8) & 0xff]) + + def __len__(self): + return 2 + + @property + def uuid(self): + return self._uuid + + @uuid.setter + def uuid(self, uuid): + raise NotImplemented + + +class UUID128(ValueObject): + + def __init__(self, uuid): + """ Crate UUID128, non-int types are assumed to be little endian (e.g. 'reversed' order w.r.t. displayed UUID)""" + + # TODO: accept 32bit and convert, xxxxxxxx-0000-1000-8000-00805F9B34FB + log.debug(("UUID128 type: {}".format(type(uuid)))) + + if isinstance(uuid, int): + if not 0 < uuid < 0xffff: + raise ValueError('Invalid UUID16 value {} fro UUID128 promotion'.format(uuid)) + uuid = UUID("0000{:04x}-0000-1000-8000-00805F9B34FB".format(uuid)).hex + + elif isinstance(uuid, str): + if not len(uuid) == 36: + raise ValueError('Invalid UUID128 value {}'.format(uuid)) + + elif isinstance(uuid, list) or isinstance(uuid, tuple) or isinstance(uuid, bytes) or isinstance(uuid, bytearray): + if len(uuid) != 16: + raise ValueError('Unexpected address length of {} for {}'.format(len(uuid), uuid)) + uuid = ''.join([format(c, '02x') for c in reversed(uuid)] ) + else: + raise TypeError('Unsupported UUID128 initialiser type: {}'.format(type(uuid))) + + self._uuid_obj = UUID(uuid) + + self._uuid=self._uuid_obj.urn.replace('urn:uuid:', '') + log.debug(self) + + def __hash__(self): + return self._uuid + + def __eq__(self, other): + return isinstance(other, UUID128) and self._uuid == other._uuid + + def __repr__(self): + return "UUID128('{}')".format(self._uuid) + + + def __bytes__(self): + return bytes(reversed(self._uuid_obj.bytes)) + + def __len__(self): + return len(self._uuid_obj.bytes) + + @property + def uuid(self): + return self._uuid + + @uuid.setter + def uuid(self, uuid): + raise NotImplemented class BDAddress(ValueObject): + # TODO: add an overide for little_endian=True\False flag, default True, for list-like types def __init__(self, address:str=None): # expect a string of the format "xx:xx:xx:xx:xx:xx" # or a 6 element; tuple, list, bytes or bytearray @@ -75,35 +179,35 @@ class Advertisement(ValueObject): def __init__(self, name=None, address=None, rssi=None, tx_power=None, raw_data=None): #TODO: use kwargs self.flags = 6 # uint8 # default to LE_GENERAL_DISCOVERABLE | BREDR_NOT_SUPPORTED + self.type = None + self.address_type = None + self.address = address + self._name = name + self.name_is_complete = False # unsigned + self.tx_pwr_lvl = tx_power # unsigned + self.appearance = None # unit16 self.uuid16s = None # uuid16[] self.uuid32s = None # uuid32[] self.uuid128s = None # uuid12[] - self.name = name - self.name_is_complete = False # unsigned - self.tx_pwr_lvl = tx_power # unsigned + self.service_data = None #slave_itvl_range self.svc_data_uuid16 = None # uuid16[] self.public_tgt_addr = None # uint8[] - self.appearance = None # unit16 self.adv_itvl = None # uint16 self.svc_data_uuid32 = None # uint8 self.svc_data_uuid128 = None # uint8 self.uri = None # unit8 self.mfg_data = None # unit8 - self.service_data = None # TODO: check this... - self.type = None - self.address_type = None - self.address = address - self.eir = None - self.rssi = rssi + self.rssi = rssi # really only part of an Advertisement Report... self.raw_data = raw_data log.debug(self) def __repr__(self): - # TODO: UUID's, appearance etc... - return "Advertisement(flags=0x{:02x}, name={}, rssi={})".format(self.flags, self.name, self.rssi) + # TODO: appearance etc... + return "Advertisement(flags=0x{:02x}, name={}, txpower={} uuid16s={} uuid128s={} rssi={})".format( + self.flags, self.name, self.tx_pwr_lvl, self.uuid16s, self.uuid128s, self.rssi) @property @@ -118,7 +222,7 @@ def name(self, name): self._name = str(name) # unit8 -class ScanResponse(ValueObject): +class ScanResponse(Advertisement): pass diff --git a/bleson/providers/__init__.py b/bleson/providers/__init__.py index 2913e92..c3d15be 100644 --- a/bleson/providers/__init__.py +++ b/bleson/providers/__init__.py @@ -1,11 +1,12 @@ -import logging import sys from bleson.logger import log _provider = None + def get_provider(): + global _provider if _provider is None: @@ -19,4 +20,6 @@ def get_provider(): else: raise RuntimeError('Platform {0} is not supported!'.format(sys.platform)) + log.info("Provider is {}".format(_provider)) + return _provider diff --git a/bleson/providers/linux/linux_adapter.py b/bleson/providers/linux/linux_adapter.py index b43fc42..bd41d22 100644 --- a/bleson/providers/linux/linux_adapter.py +++ b/bleson/providers/linux/linux_adapter.py @@ -4,13 +4,11 @@ import struct import threading -from bleson.core.hci import parse_hci_event_packet from bleson.core.hci.constants import * -from bleson.core.hci.type_converters import AdvertisingDataConverters +from bleson.core.hci.type_converters import AdvertisingDataConverters, parse_hci_event_packet, hex_string from bleson.core.types import Device, BDAddress from bleson.interfaces.adapter import Adapter from bleson.logger import log -from bleson.utils import hex_string from .constants import * diff --git a/bleson/providers/linux/linux_provider.py b/bleson/providers/linux/linux_provider.py index c430c85..84b0dea 100644 --- a/bleson/providers/linux/linux_provider.py +++ b/bleson/providers/linux/linux_provider.py @@ -12,4 +12,4 @@ def get_adapter(self, adapter_id=0): adapter.on() return adapter - + # TODO: have a 'get_converter(for_type)' and registry of ValueObject to provider data converters (Linux uses core HCI) diff --git a/bleson/utils.py b/bleson/utils.py deleted file mode 100644 index 0c6314d..0000000 --- a/bleson/utils.py +++ /dev/null @@ -1,14 +0,0 @@ -import binascii - -def hex_string(data): - return ''.join('{:02x} '.format(x) for x in data) - -def bytearray_to_hexstring(ba): - return hex_string(ba) - #return binascii.hexlify(ba) - -def hexstring_to_bytearray(hexstr): - """" - hexstr: e.g. de ad be ef 00" - """ - return bytearray.fromhex(hexstr) diff --git a/examples/basic_advertiser_heartrate.py b/examples/basic_advertiser_heartrate.py new file mode 100644 index 0000000..97a9d0b --- /dev/null +++ b/examples/basic_advertiser_heartrate.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +import sys +from time import sleep + +from bleson.logger import log, set_level, DEBUG +from bleson import get_provider, Advertiser, Advertisement, UUID16, LE_GENERAL_DISCOVERABLE, BREDR_NOT_SUPPORTED + +previous_log_level = set_level(DEBUG) +# Get the wait time from the first script argument or default it to 10 seconds +WAIT_TIME = int(sys.argv[1]) if len(sys.argv)>1 else 10 + +adapter = get_provider().get_adapter() + +advertiser = Advertiser(adapter) + +advertisement = Advertisement() +advertisement.name = "Heart Rate" +advertisement.flags = LE_GENERAL_DISCOVERABLE | BREDR_NOT_SUPPORTED +advertisement.uuid16s = [UUID16(0x180a), UUID16(0x180d)] + +advertiser.advertisement = advertisement + +advertiser.start() +sleep(WAIT_TIME) +advertiser.stop() + +set_level(previous_log_level) \ No newline at end of file diff --git a/tests/test_internal_advertising_data.py b/tests/test_internal_advertising_data.py index 7c5a225..42b2155 100644 --- a/tests/test_internal_advertising_data.py +++ b/tests/test_internal_advertising_data.py @@ -1,23 +1,52 @@ import unittest from bleson.logger import log, set_level, DEBUG -from bleson.core.types import Advertisement +from bleson.core.types import Advertisement, UUID16, UUID128 -from bleson.core.hci.type_converters import AdvertisingDataConverters +from bleson.core.hci.type_converters import AdvertisingDataConverters, hexstring_to_bytearray set_level(DEBUG) class TestAdvertisingData(unittest.TestCase): - def test_create_hcipayload_from_advertising(self): + def test_create_adv_name(self): adv = Advertisement() adv.name = 'blename' - log.info(adv.raw_data) hci_payload = AdvertisingDataConverters.from_advertisement(adv) log.info(hci_payload) + self.assertEqual(adv.raw_data, None) self.assertEqual(b'\x02\x01\x06\x08\x09blename', hci_payload) + + def test_create_adv_uuid16_single(self): + + adv = Advertisement() + adv.name = 'blename' + adv.uuid16s = [UUID16(0xfeff)] + + + hci_payload = AdvertisingDataConverters.from_advertisement(adv) + log.info(hci_payload) + + self.assertEqual(adv.raw_data, None) + self.assertEqual(b'\x02\x01\x06\x03\x03\xff\xfe\x08\x09blename', hci_payload) + + + def test_create_adv_uuid128_single(self): + NRF51X_LEGACY_DFU_ADV_DATA = hexstring_to_bytearray( + '02 01 1a 11 07 23 d1 bc ea 5f 78 23 15 de ef 12 12 30 15 00 00 06 09 6e 52 46 35 78') + + adv = Advertisement() + adv.flags = 0x1a + adv.name = 'nRF5x' + adv.uuid128s = [ UUID128('00001530-1212-efde-1523-785feabcd123')] + + hci_payload = AdvertisingDataConverters.from_advertisement(adv) + log.info(hci_payload) + + self.assertEqual(adv.raw_data, None) + self.assertEqual(NRF51X_LEGACY_DFU_ADV_DATA, hci_payload) \ No newline at end of file diff --git a/tests/test_internal_hci_packet_parse.py b/tests/test_internal_hci_packet_parse.py index 8e117b1..54188b0 100644 --- a/tests/test_internal_hci_packet_parse.py +++ b/tests/test_internal_hci_packet_parse.py @@ -1,20 +1,34 @@ import unittest -from bleson.core.hci import parse_hci_event_packet -from bleson.core.hci.type_converters import AdvertisingDataConverters -from bleson.logger import set_level, DEBUG -from bleson.utils import hexstring_to_bytearray - -set_level(DEBUG) +from bleson.core.types import UUID16, UUID128 +from bleson.core.hci.type_converters import AdvertisingDataConverters, parse_hci_event_packet, hexstring_to_bytearray +from bleson.logger import log, set_level, DEBUG EDDYSTONE_ADV_REPORT = hexstring_to_bytearray('3e 21 02 01 00 00 1e ac e7 eb 27 b8 15 02 01 1a 03 03 aa fe 0d 16 aa fe 10 ed 00 67 6f 6f 67 6c 65 00 bc') BBC_MICROBIT_ADV_REPORT = hexstring_to_bytearray('3e 26 02 01 00 01 f6 15 b0 c9 3a f5 1a 02 01 06 16 09 42 42 43 20 6d 69 63 72 6f 3a 62 69 74 20 5b 74 65 67 69 70 5d e4') JSPUCK_ADV_REPORT = hexstring_to_bytearray('3e 1d 02 01 00 01 43 7b 30 8e 58 f4 11 02 01 05 0d 09 50 75 63 6b 2e 6a 73 20 37 62 34 33 ac') UUID128_ADV_REPORT = hexstring_to_bytearray('3e 1d 02 01 04 01 43 7b 30 8e 58 f4 12 11 07 9e ca dc 24 0e e5 a9 e0 93 f3 a3 b5 01 00 40 6e') +BLUEFRUIT52_ADV_REPORT = hexstring_to_bytearray('3e 19 02 01 04 00 ad af ad af ad af 0d 0c 09 42 6c 75 65 66 72 75 69 74 35 32 d6') + +HEARTRATE_ADV_REPORT = hexstring_to_bytearray('3e 29 02 01 00 01 3f 55 b5 74 6e 4e 1d 02 01 1a 05 03 0a 18 0d 18 0b 09 48 65 61 72 74 20 52 61 74 65 07 ff 4c 00 10 02 0b 00 c9') +THERMOMETER_ADV_REPORT = hexstring_to_bytearray('3e 29 02 01 00 01 28 5f 74 ec 4f 6e 1d 02 01 1a 05 03 0a 18 09 18 13 09 48 65 61 6c 74 68 20 54 68 65 72 6d 6f 6d 65 74 65 72 b6') +NRF51X_LEGACY_DFU_ADV_REPORT = hexstring_to_bytearray('3e 28 02 01 00 01 bd ac 2c fb 4e 51 1c 02 01 1a 11 07 23 d1 bc ea 5f 78 23 15 de ef 12 12 30 15 00 00 06 09 6e 52 46 35 78 b9') class TestHCIParsers(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Turn on bleson DEBUG logging, keeping note of the previous setting. + cls.previous_log_level = set_level(DEBUG) + + @classmethod + def tearDownClass(cls): + # Restore logging level, in case we're running in a test suite + set_level(cls.previous_log_level) + + + def test_parse_eddystone_advertisment_report(self): # Some context... # eddystone adv data = [ @@ -57,7 +71,9 @@ def test_parse_eddystone_advertisment_report(self): self.assertEqual(0x1a, advertisement.flags) self.assertEqual(-68, advertisement.rssi) - self.assertEqual([bytearray([0xaa, 0xfe])], advertisement.uuid16s) + self.assertEqual(True, UUID16(0xfeaa) in advertisement.uuid16s) + self.assertEqual(1, len(advertisement.uuid16s)) + self.assertEqual(b'\xaa\xfe\x10\xed\x00google\x00', advertisement.service_data) @@ -86,6 +102,54 @@ def test_parse_jspuck_advertisment_report(self): def test_parse_jspuck_advertisment_report(self): hci_packet = parse_hci_event_packet(UUID128_ADV_REPORT) + advertisement = AdvertisingDataConverters.from_hcipacket(hci_packet) + + + def test_parse_bluefruit52_advertisment_report(self): + + hci_packet = parse_hci_event_packet(BLUEFRUIT52_ADV_REPORT) + advertisement = AdvertisingDataConverters.from_hcipacket(hci_packet) + + + def test_parse_heartrate_advertisment_report(self): + hci_packet = parse_hci_event_packet(HEARTRATE_ADV_REPORT) + log.debug(hci_packet) advertisement = AdvertisingDataConverters.from_hcipacket(hci_packet) + self.assertEqual(0x1a, advertisement.flags) + self.assertEqual(-55, advertisement.rssi) + self.assertEqual('Heart Rate', advertisement.name) + self.assertEqual(True, advertisement.name_is_complete) + self.assertEqual(True, UUID16(0x180a) in advertisement.uuid16s) + self.assertEqual(True, UUID16(0x180d) in advertisement.uuid16s) + self.assertEqual(2, len(advertisement.uuid16s)) + + def test_parse_thermometer_advertisment_report(self): + + hci_packet = parse_hci_event_packet(THERMOMETER_ADV_REPORT) + log.debug(hci_packet) + advertisement = AdvertisingDataConverters.from_hcipacket(hci_packet) + self.assertEqual(0x1a, advertisement.flags) + self.assertEqual(-74, advertisement.rssi) + self.assertEqual('Health Thermometer', advertisement.name) + self.assertEqual(True, advertisement.name_is_complete) + log.debug(advertisement.uuid16s) + + self.assertEqual(True, UUID16(0x180a) in advertisement.uuid16s) + self.assertEqual(True, UUID16(0x1809) in advertisement.uuid16s) + self.assertEqual(2, len(advertisement.uuid16s)) + + + def test_parse_nrf5x_lefacy_dfu_advertisement_report(self): + + hci_packet = parse_hci_event_packet(NRF51X_LEGACY_DFU_ADV_REPORT) + log.debug(hci_packet) + + advertisement = AdvertisingDataConverters.from_hcipacket(hci_packet) + self.assertEqual(0x1a, advertisement.flags) + self.assertEqual(-71, advertisement.rssi) + self.assertEqual('nRF5x', advertisement.name) + log.debug(advertisement.uuid128s) + self.assertEqual(True, UUID128('00001530-1212-efde-1523-785feabcd123') in advertisement.uuid128s) + self.assertEqual(1, len(advertisement.uuid128s)) \ No newline at end of file diff --git a/tests/test_internal_type_uuid.py b/tests/test_internal_type_uuid.py new file mode 100644 index 0000000..689730f --- /dev/null +++ b/tests/test_internal_type_uuid.py @@ -0,0 +1,25 @@ +import unittest + +from bleson.core.types import UUID16, UUID128 + + +class TestUUID(unittest.TestCase): + + def test_uuid16_init(self): + u1 = UUID16(bytearray([0x0d, 0x1a])) + u2 = UUID16(0x1a0d) + + self.assertEqual(0x1a0d, u1.uuid) + self.assertEqual(0x1a0d, u2.uuid) + self.assertEqual(u1, u2) + + def test_uuid128_init(self): + u1 = UUID128([0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF, 0x12, 0x12, 0x30, 0x15, 0x00, 0x00]) + u2 = UUID128('00001530-1212-efde-1523-785feabcd123') + u3 = UUID128('12345678-1212-efde-1523-785feabcd123') + u4 = UUID128(0xFEFF) + self.assertEqual('00001530-1212-efde-1523-785feabcd123', u1.uuid) + self.assertEqual('00001530-1212-efde-1523-785feabcd123', u2.uuid) + self.assertEqual(u1, u2) + self.assertNotEqual(u3, u1) + self.assertNotEqual(u4, u1) From 65adbcd0925aae8a1a0ba66175144281cbd06026 Mon Sep 17 00:00:00 2001 From: Wayne Keenan Date: Thu, 9 Nov 2017 11:14:43 +0000 Subject: [PATCH 05/13] first pass at redimentary macOS provider, observing only, very alpha. --- bleson/interfaces/adapter.py | 5 + bleson/providers/__init__.py | 8 +- bleson/providers/macos/__init__.py | 0 bleson/providers/macos/macos_adapter.py | 114 +++++++++++++++++++++++ bleson/providers/macos/macos_provider.py | 15 +++ tests/test_macos_corebluetooth.py | 0 6 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 bleson/providers/macos/__init__.py create mode 100644 bleson/providers/macos/macos_adapter.py create mode 100644 bleson/providers/macos/macos_provider.py create mode 100644 tests/test_macos_corebluetooth.py diff --git a/bleson/interfaces/adapter.py b/bleson/interfaces/adapter.py index aa6581b..4299a4f 100644 --- a/bleson/interfaces/adapter.py +++ b/bleson/interfaces/adapter.py @@ -3,6 +3,11 @@ class Adapter(object): __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def open(self): + raise NotImplementedError + @abc.abstractmethod def on(self): raise NotImplementedError diff --git a/bleson/providers/__init__.py b/bleson/providers/__init__.py index c3d15be..8a28559 100644 --- a/bleson/providers/__init__.py +++ b/bleson/providers/__init__.py @@ -13,9 +13,11 @@ def get_provider(): if sys.platform.startswith('linux'): from bleson.providers.linux.linux_provider import LinuxProvider _provider = LinuxProvider() - elif sys.startswith('darwin'): - raise NotImplementedError() - elif sys.startswith('win32'): + elif sys.platform.startswith('darwin'): + from bleson.providers.macos.macos_provider import MacOSProvider + _provider = MacOSProvider() + + elif sys.platform.startswith('win32'): raise NotImplementedError() else: raise RuntimeError('Platform {0} is not supported!'.format(sys.platform)) diff --git a/bleson/providers/macos/__init__.py b/bleson/providers/macos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bleson/providers/macos/macos_adapter.py b/bleson/providers/macos/macos_adapter.py new file mode 100644 index 0000000..f500c2b --- /dev/null +++ b/bleson/providers/macos/macos_adapter.py @@ -0,0 +1,114 @@ +from bleson.interfaces.adapter import Adapter +from bleson.core.types import Advertisement +from bleson.logger import log + +import objc +from PyObjCTools import AppHelper +objc.loadBundle("CoreBluetooth", globals(), + bundle_path=objc.pathForFramework( + u'/System/Library/Frameworks/IOBluetooth.framework/Versions/A/Frameworks/CoreBluetooth.framework')) + + +class CoreBluetoothAdapter(Adapter): + + def __init__(self, device_id=0): + self.device_id = device_id + self.connected = False + self._keep_running = True + self._manager = CBCentralManager.alloc() + + + def open(self): + self._manager.initWithDelegate_queue_options_(self, None, None) + + + def on(self): + log.warn("TODO: adatper on") + + def off(self): + log.warn("TODO: adatper off") + + def start_scanning(self): + log.info("start scanning") + try: + AppHelper.runConsoleEventLoop(installInterrupt=True) + except KeyboardInterrupt: + AppHelper.stopEventLoop() + + def stop_scanning(self): + self.manager.stopScan() + #self.peripheral = peripheral + #manager.connectPeripheral_options_(self.peripheral, None) + + def start_advertising(self, advertisement, scan_response=None): + raise NotImplementedError + + def stop_advertising(self): + raise NotImplementedError + + + # CoreBluetooth Protocol + + def centralManagerDidUpdateState_(self, manager): + log.debug("centralManagerDidUpdateState_") + + if self.connected == False: + self.manager = manager + manager.scanForPeripheralsWithServices_options_(None, None) + + def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(self, manager, peripheral, data, rssi): + log.debug("centralManager_didDiscoverPeripheral_advertisementData_RSSI_") + log.debug('Found: name={} rssi={} data={} '.format(peripheral.name(), rssi, data)) + + if self.on_advertising_data: + advertisement = Advertisement() + advertisement.name = peripheral.name() + advertisement.rssi = rssi + self.on_advertising_data(advertisement) + + + def centralManager_didConnectPeripheral_(self, manager, peripheral): + log.debug("centralManager_didConnectPeripheral_") + log.debug('Connected: ' + peripheral.name()) + self.connected = True + self.peripheral.setDelegate_(self) + self.peripheral.readRSSI() + #self.peripheral.discoverServices_([CBUUID(...)]) + + def centralManager_didFailToConnectPeripheral_error_(self, manager, peripheral, error): + log.debug("centralManager_didFailToConnectPeripheral_error_") + + log.error(repr(error)) + + def centralManager_didDisconnectPeripheral_error_(self, manager, peripheral, error): + log.debug("centralManager_didDisconnectPeripheral_error_") + self.connected = False + AppHelper.stopEventLoop() + + def peripheral_didDiscoverServices_(self, peripheral, error): + log.debug("peripheral_didDiscoverServices_") + if (error == None): + self.service = self.peripheral.services()[0] + #self.peripheral.discoverCharacteristics_forService_([CBUUD(...)], self.service) + + def peripheral_didDiscoverCharacteristicsForService_error_(self, peripheral, service, error): + log.debug("peripheral_didDiscoverCharacteristicsForService_error_") + + for characteristic in self.service.characteristics(): + if characteristic.UUID().UUIDString() == crtp_characteristic.UUIDString(): + self.crtp_characteristic = characteristic + self.peripheral.setNotifyValue_forCharacteristic_(True, self.crtp_characteristic) + + def peripheral_didWriteValueForCharacteristic_error_(self, peripheral, characteristic, error): + log.debug("peripheral_didWriteValueForCharacteristic_error_") + + if error != None: + log.error(repr(error)) + + def peripheral_didUpdateNotificationStateForCharacteristic_error_(self, peripheral, characteristic, error): + log.debug("peripheral_didUpdateNotificationStateForCharacteristic_error_") + + + def peripheral_didUpdateValueForCharacteristic_error_(self, peripheral, characteristic, error): + log.debug("peripheral_didUpdateValueForCharacteristic_error_") + repr(characteristic.value().bytes().tobytes()) diff --git a/bleson/providers/macos/macos_provider.py b/bleson/providers/macos/macos_provider.py new file mode 100644 index 0000000..98da1bf --- /dev/null +++ b/bleson/providers/macos/macos_provider.py @@ -0,0 +1,15 @@ +from bleson.interfaces.provider import Provider +from .macos_adapter import CoreBluetoothAdapter +from bleson.logger import log + + +class MacOSProvider(Provider): + + def get_adapter(self, adapter_id=0): + adapter = CoreBluetoothAdapter(adapter_id) + adapter.open() + adapter.off() + adapter.on() + return adapter + + # TODO: have a 'get_converter(for_type)' and registry of ValueObject to provider data converters (Linux uses core HCI) diff --git a/tests/test_macos_corebluetooth.py b/tests/test_macos_corebluetooth.py new file mode 100644 index 0000000..e69de29 From 5eca11f1e1553f495ce8b6dc8a638387e3f3b672 Mon Sep 17 00:00:00 2001 From: Wayne Keenan Date: Fri, 10 Nov 2017 13:43:27 +0000 Subject: [PATCH 06/13] Added macOS Advertisement support for: txpower, service uuid16s and uuid128s, manufacturer data --- bleson/core/types.py | 34 +++-- bleson/providers/macos/macos_adapter.py | 55 ++++++-- tests/test_macos_corebluetooth.py | 166 ++++++++++++++++++++++++ 3 files changed, 235 insertions(+), 20 deletions(-) diff --git a/bleson/core/types.py b/bleson/core/types.py index 2e868b9..9f10909 100644 --- a/bleson/core/types.py +++ b/bleson/core/types.py @@ -20,10 +20,13 @@ def __len__(self): class UUID16(ValueObject): - def __init__(self, uuid): - """ Crate UUID16, non-int types are assumed to be little endian (reverse order)""" + def __init__(self, uuid, little_endian=True): + """ Create UUID16, non-int types are assumed to be little endian (reverse order)""" log.debug(("UUID16 type: {}".format(type(uuid)))) + if isinstance(uuid, memoryview): + uuid = uuid.tolist() + if isinstance(uuid, int): if not 0 < uuid < 0xffff: raise ValueError('Invalid UUID16 value {}'.format(uuid)) @@ -39,6 +42,8 @@ def __init__(self, uuid): else: raise TypeError('Unsupported UUID16 initialiser type: {}'.format(type(uuid))) + if not little_endian: + uuid = ( (uuid & 0x00ff) << 8 ) + ( (uuid & 0xff00) >>8) self._uuid=uuid log.debug(self) @@ -68,12 +73,19 @@ def uuid(self, uuid): class UUID128(ValueObject): - def __init__(self, uuid): - """ Crate UUID128, non-int types are assumed to be little endian (e.g. 'reversed' order w.r.t. displayed UUID)""" + def __init__(self, uuid, little_endian=True): + """ Create UUID128, non-int types are assumed to be little endian (e.g. 'reversed' order w.r.t. displayed UUID)""" # TODO: accept 32bit and convert, xxxxxxxx-0000-1000-8000-00805F9B34FB - log.debug(("UUID128 type: {}".format(type(uuid)))) + log.debug(("UUID128 type: {} value={}".format(type(uuid), uuid))) + + if isinstance(uuid, memoryview): + if little_endian: + uuid = uuid.tolist() + else: + uuid = list(reversed(uuid.tolist())) + # Massage 'uuid' into a string that the built-in UUID() contructor accepts if isinstance(uuid, int): if not 0 < uuid < 0xffff: raise ValueError('Invalid UUID16 value {} fro UUID128 promotion'.format(uuid)) @@ -90,8 +102,10 @@ def __init__(self, uuid): else: raise TypeError('Unsupported UUID128 initialiser type: {}'.format(type(uuid))) + self._uuid_obj = UUID(uuid) + self._uuid=self._uuid_obj.urn.replace('urn:uuid:', '') log.debug(self) @@ -186,9 +200,9 @@ def __init__(self, name=None, address=None, rssi=None, tx_power=None, raw_data=N self.name_is_complete = False # unsigned self.tx_pwr_lvl = tx_power # unsigned self.appearance = None # unit16 - self.uuid16s = None # uuid16[] - self.uuid32s = None # uuid32[] - self.uuid128s = None # uuid12[] + self.uuid16s = [] # uuid16[] + self.uuid32s = [] # uuid32[] + self.uuid128s = [] # uuid12[] self.service_data = None #slave_itvl_range self.svc_data_uuid16 = None # uuid16[] @@ -206,8 +220,8 @@ def __init__(self, name=None, address=None, rssi=None, tx_power=None, raw_data=N def __repr__(self): # TODO: appearance etc... - return "Advertisement(flags=0x{:02x}, name={}, txpower={} uuid16s={} uuid128s={} rssi={})".format( - self.flags, self.name, self.tx_pwr_lvl, self.uuid16s, self.uuid128s, self.rssi) + return "Advertisement(flags=0x{:02x}, name={}, txpower={} uuid16s={} uuid128s={} rssi={} mfg_data={})".format( + self.flags, self.name, self.tx_pwr_lvl, self.uuid16s, self.uuid128s, self.rssi, self.mfg_data) @property diff --git a/bleson/providers/macos/macos_adapter.py b/bleson/providers/macos/macos_adapter.py index f500c2b..285df68 100644 --- a/bleson/providers/macos/macos_adapter.py +++ b/bleson/providers/macos/macos_adapter.py @@ -1,7 +1,8 @@ from bleson.interfaces.adapter import Adapter -from bleson.core.types import Advertisement +from bleson.core.types import Advertisement, UUID16, UUID128 +from bleson.core.hci.constants import * from bleson.logger import log - +from bleson.core.hci.type_converters import bytearray_to_hexstring import objc from PyObjCTools import AppHelper objc.loadBundle("CoreBluetooth", globals(), @@ -57,14 +58,48 @@ def centralManagerDidUpdateState_(self, manager): manager.scanForPeripheralsWithServices_options_(None, None) def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(self, manager, peripheral, data, rssi): - log.debug("centralManager_didDiscoverPeripheral_advertisementData_RSSI_") - log.debug('Found: name={} rssi={} data={} '.format(peripheral.name(), rssi, data)) - - if self.on_advertising_data: - advertisement = Advertisement() - advertisement.name = peripheral.name() - advertisement.rssi = rssi - self.on_advertising_data(advertisement) + try: + log.debug("centralManager_didDiscoverPeripheral_advertisementData_RSSI_") + log.debug('Found: name={} rssi={} data={} '.format(peripheral.name(), rssi, data)) + + if self.on_advertising_data: + advertisement = Advertisement() + advertisement.flags = 0 # Not available + advertisement.name = peripheral.name() + advertisement.rssi = rssi + + if 'kCBAdvDataTxPowerLevel' in data: + advertisement.tx_pwr_lvl = int(data['kCBAdvDataTxPowerLevel']) + + if data['kCBAdvDataIsConnectable']: + # TODO: handle: kCBAdvDataIsConnectable correctly + advertisement.type = 0x01 # BLE_GAP_ADV_TYPE_ADV_DIRECT_IND + + if 'kCBAdvDataServiceUUIDs' in data: + log.debug('kCBAdvDataServiceUUIDs:') + for cbuuid in data['kCBAdvDataServiceUUIDs']: + uuid_bytes = cbuuid.data().bytes() + if 2 == len(uuid_bytes): + uuid = UUID16(uuid_bytes, little_endian=False) + advertisement.uuid16s.append(uuid) + + elif 16 == len(uuid_bytes): + uuid = UUID128(uuid_bytes, little_endian=False) + advertisement.uuid128s.append(uuid) + else: + log.error("Unsupporten UUID length for UUID bytes={}".format(uuid_bytes)) + + log.debug('Service UUID: {} {}'.format(type(cbuuid), cbuuid)) + + if 'kCBAdvDataManufacturerData' in data: + mfg_data=data['kCBAdvDataManufacturerData'] + log.debug('kCBAdvDataManufacturerData={}'.format(mfg_data)) + advertisement.mfg_data=mfg_data + + self.on_advertising_data(advertisement) + + except Exception as e: + log.exception(e) def centralManager_didConnectPeripheral_(self, manager, peripheral): diff --git a/tests/test_macos_corebluetooth.py b/tests/test_macos_corebluetooth.py index e69de29..8e68254 100644 --- a/tests/test_macos_corebluetooth.py +++ b/tests/test_macos_corebluetooth.py @@ -0,0 +1,166 @@ +import time +import struct +from threading import Timer +import objc +from PyObjCTools import AppHelper + +# install: https://www.python.org/downloads/release/python-363/ +# pip3 install obcj + +# see: https://github.com/ppossemiers/BLECrazyflie_Python/blob/master/ble_crazyflie.py + + +objc.loadBundle("CoreBluetooth", globals(), + bundle_path=objc.pathForFramework( + u'/System/Library/Frameworks/IOBluetooth.framework/Versions/A/Frameworks/CoreBluetooth.framework')) + +#dfu_service = CBUUID.UUIDWithString_(u'00001530-1212-efde-1523-785feabcd123') +#dfu_characteristic = CBUUID.UUIDWithString_(u'00001531-1212-efde-1523-785feabcd123') +crazyflie_service = CBUUID.UUIDWithString_(u'6E400001-B5A3-F393-E0A9-E50E24DCCA9E') +crtp_characteristic = CBUUID.UUIDWithString_(u'6E400002-B5A3-F393-E0A9-E50E24DCCA9E') # TX char + +print(crazyflie_service) + +def main(): + # Ctrl + \ kills the script on MacOS + cf = BLECrazyFlie() + # add methods that the crazyflie executes + cf.add_callback(hover) + manager = CBCentralManager.alloc() + manager.initWithDelegate_queue_options_(cf, None, None) + + try: + print("starting") + AppHelper.runConsoleEventLoop(installInterrupt=True) + except KeyboardInterrupt: + AppHelper.stopEventLoop() + + +def hover(cf): + print('hover') + # take off + for i in range(2): + cf.send_setpoint(0, 0, 0, 50000) + time.sleep(0.5) + + # stop thrust, start hover + cf.set_param(11, 'b', 1) + cf.send_setpoint(0, 0, 0, 32767) + while 1: + cf.send_setpoint(0, 0, 0, 32767) + time.sleep(0.5) + + +class BLECrazyFlie(): + def __init__(self): + self.manager = None + self.peripheral = None + self.service = None + self.crtp_characteristic = None + self.connected = False + self.callbacks = [] + + self.init = False + + def send_setpoint(self, roll, pitch, yaw, thrust): + data = struct.pack(' Date: Sun, 12 Nov 2017 05:34:58 +0000 Subject: [PATCH 07/13] uuid endiian fixes. macos advertising and other experiments --- bleson/core/types.py | 16 +-- bleson/providers/macos/macos_adapter.py | 86 +++++++++++--- sandpit/test_xpc_observer.py | 115 +++++++++++++++++++ tests/test_macos_corebluetooth_advertiser.py | 54 +++++++++ 4 files changed, 249 insertions(+), 22 deletions(-) create mode 100644 sandpit/test_xpc_observer.py create mode 100644 tests/test_macos_corebluetooth_advertiser.py diff --git a/bleson/core/types.py b/bleson/core/types.py index 9f10909..c7725cd 100644 --- a/bleson/core/types.py +++ b/bleson/core/types.py @@ -23,7 +23,7 @@ class UUID16(ValueObject): def __init__(self, uuid, little_endian=True): """ Create UUID16, non-int types are assumed to be little endian (reverse order)""" - log.debug(("UUID16 type: {}".format(type(uuid)))) + log.debug(("UUID16 type: {} value={}".format(type(uuid), uuid))) if isinstance(uuid, memoryview): uuid = uuid.tolist() @@ -42,8 +42,8 @@ def __init__(self, uuid, little_endian=True): else: raise TypeError('Unsupported UUID16 initialiser type: {}'.format(type(uuid))) - if not little_endian: - uuid = ( (uuid & 0x00ff) << 8 ) + ( (uuid & 0xff00) >>8) + if not little_endian: # swap + uuid = ( (uuid & 0xff) << 8 ) | ( (uuid & 0xff00) >>8) self._uuid=uuid log.debug(self) @@ -74,16 +74,13 @@ def uuid(self, uuid): class UUID128(ValueObject): def __init__(self, uuid, little_endian=True): - """ Create UUID128, non-int types are assumed to be little endian (e.g. 'reversed' order w.r.t. displayed UUID)""" + """ Create UUID128, non-int types must be little endian (e.g. 'reversed' order w.r.t. displayed UUID)""" # TODO: accept 32bit and convert, xxxxxxxx-0000-1000-8000-00805F9B34FB log.debug(("UUID128 type: {} value={}".format(type(uuid), uuid))) if isinstance(uuid, memoryview): - if little_endian: - uuid = uuid.tolist() - else: - uuid = list(reversed(uuid.tolist())) + uuid = uuid.tobytes() # Massage 'uuid' into a string that the built-in UUID() contructor accepts if isinstance(uuid, int): @@ -105,6 +102,9 @@ def __init__(self, uuid, little_endian=True): self._uuid_obj = UUID(uuid) + if not little_endian: + self._uuid_obj = UUID(bytes=bytes(reversed(self._uuid_obj.bytes))) + self._uuid=self._uuid_obj.urn.replace('urn:uuid:', '') log.debug(self) diff --git a/bleson/providers/macos/macos_adapter.py b/bleson/providers/macos/macos_adapter.py index 285df68..b89d43d 100644 --- a/bleson/providers/macos/macos_adapter.py +++ b/bleson/providers/macos/macos_adapter.py @@ -1,13 +1,42 @@ +import threading from bleson.interfaces.adapter import Adapter from bleson.core.types import Advertisement, UUID16, UUID128 from bleson.core.hci.constants import * from bleson.logger import log from bleson.core.hci.type_converters import bytearray_to_hexstring import objc +from Foundation import * from PyObjCTools import AppHelper -objc.loadBundle("CoreBluetooth", globals(), - bundle_path=objc.pathForFramework( - u'/System/Library/Frameworks/IOBluetooth.framework/Versions/A/Frameworks/CoreBluetooth.framework')) +import CoreBluetooth + +# https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html + +# pyobjc: https://bitbucket.org/ronaldoussoren/pyobjc +# https://bitbucket.org/ronaldoussoren/pyobjc/src/87ce46672615ef127f40850cc78401ca62cc336a/pyobjc-framework-CoreBluetooth/PyObjCTest/?at=default + +# Issue: https://bitbucket.org/ronaldoussoren/pyobjc/issues/215/starting-runconsoleeventloop-from-a + +# ObjC diest support dispatchQueues... + +# use XPC instead: https://github.com/sandeepmistry/noble/blob/master/lib/mac/yosemite.js + + + +# Python XPC + +# brew install python3 +# brew install boost-python --with-python3 +# ln -s /usr/local/lib/libboost_python3.a /usr/local/lib/libboost_python-py34.a +# git clone https://github.com/matthewelse/pyxpcconnection +# cd pyxpcconnection +# edit setup.py: +""" +- extra_compile_args=['-std=c++11'], ++ extra_compile_args=['-std=c++11', '-stdlib=libc++', '-mmacosx-version-min=10.9'], + libraries = boost_libs +""" +# python3 setup.py install + class CoreBluetoothAdapter(Adapter): @@ -16,11 +45,12 @@ def __init__(self, device_id=0): self.device_id = device_id self.connected = False self._keep_running = True - self._manager = CBCentralManager.alloc() - + self._socket_poll_thread = threading.Thread(target=self._runloop_thread, name='BlesonObjCRunLoop') + self._socket_poll_thread.setDaemon(True) + self._manager = None def open(self): - self._manager.initWithDelegate_queue_options_(self, None, None) + pass def on(self): @@ -31,15 +61,14 @@ def off(self): def start_scanning(self): log.info("start scanning") - try: - AppHelper.runConsoleEventLoop(installInterrupt=True) - except KeyboardInterrupt: - AppHelper.stopEventLoop() + self._runloop_thread() + #self._socket_poll_thread.start() + def stop_scanning(self): - self.manager.stopScan() - #self.peripheral = peripheral - #manager.connectPeripheral_options_(self.peripheral, None) + log.info("stopping") + rc = AppHelper.stopEventLoop() + log.info("done: AppHelper.stopEventLoop, successful?={}".format(rc)) def start_advertising(self, advertisement, scan_response=None): raise NotImplementedError @@ -47,6 +76,30 @@ def start_advertising(self, advertisement, scan_response=None): def stop_advertising(self): raise NotImplementedError + # Obj Runloop + + def _init_cb_manager(self): + # objc.loadBundle("CoreBluetooth", globals(), + # bundle_path=objc.pathForFramework( + # u'/System/Library/Frameworks/IOBluetooth.framework/Versions/A/Frameworks/CoreBluetooth.framework')) + + self._manager = CoreBluetooth.CBCentralManager.alloc() + self._manager.initWithDelegate_queue_options_(self, None, None) + + # https://pythonhosted.org/pyobjc/core/intro.html#working-with-threads + def _runloop_thread(self): + try: + log.info('AppHelper.runConsoleEventLoop()') + pool = NSAutoreleasePool.alloc().init() + self._init_cb_manager() + rc = AppHelper.runConsoleEventLoop(installInterrupt=True) + log.info(rc) + except Exception as e: + log.exception(e) + finally: + AppHelper.stopEventLoop() + del pool + log.info("leaving") # CoreBluetooth Protocol @@ -78,7 +131,12 @@ def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(self, manager, if 'kCBAdvDataServiceUUIDs' in data: log.debug('kCBAdvDataServiceUUIDs:') for cbuuid in data['kCBAdvDataServiceUUIDs']: - uuid_bytes = cbuuid.data().bytes() + uuid_bytes2 = cbuuid.data().bytes() + uuid_bytes = cbuuid.data().bytes().tobytes() + log.debug("--------------") + log.debug(bytearray_to_hexstring(uuid_bytes)) + log.debug(bytearray_to_hexstring(uuid_bytes2)) + if 2 == len(uuid_bytes): uuid = UUID16(uuid_bytes, little_endian=False) advertisement.uuid16s.append(uuid) diff --git a/sandpit/test_xpc_observer.py b/sandpit/test_xpc_observer.py new file mode 100644 index 0000000..2df5ed5 --- /dev/null +++ b/sandpit/test_xpc_observer.py @@ -0,0 +1,115 @@ +# xpcconnection: OS X XPC Bindings for Python +# +# Copyright (c) 2015 Matthew Else +# +# 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 +# http://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. + +from xpcconnection import XpcConnection +from threading import Event + + +import time + +from uuid import UUID + +class Connection(XpcConnection): + def __init__(self, target, event): + super(Connection, self).__init__(target) + self.event = event + + def onEvent(self, data): + print('onEvent, data={}'.format(data)) + + msg_id = data['kCBMsgId'] + args = data['kCBMsgArgs'] + + if msg_id == 6: + # state changed + STATE_TYPES = ['unknown', 'resetting', 'unsupported', 'unauthorized', 'poweredOff', 'poweredOn'] + print('State Changed: %s' % STATE_TYPES[args['kCBMsgArgState']]) + elif msg_id == 37: + print('discovered a device') + args.setdefault(None) + + rssi = args['kCBMsgArgRssi'] + uuid = UUID(bytes=args['kCBMsgArgDeviceUUID']) + ad_data = args['kCBMsgArgAdvertisementData'] + + print(rssi) + print(uuid) + print(ad_data) + + def onError(self, data): + print('error') + + def handler(self, event): + e_type, data = event + + if e_type == 'event': + self.onEvent(data) + elif e_type == 'error': + self.onError(data) + else: + # que? + pass + + self.event.set() + +e = Event() + +conn = Connection('com.apple.blued', e) + +def init(): + # init + init_data = { + 'kCBMsgArgName': 'py-' + str(time.time()), + 'kCBMsgArgOptions': { + 'kCBInitOptionShowPowerAlert': 0 + }, + 'kCBMsgArgType': 0 + } + + conn.sendMessage({ + 'kCBMsgId': 1, + 'kCBMsgArgs': init_data + }) + + e.wait() + e.clear() + +def startScanning(): + scan_data = { + 'kCBMsgArgOptions': {}, + 'kCBMsgArgUUIDs': [] + } + + conn.sendMessage({ + 'kCBMsgId': 29, + 'kCBMsgArgs': scan_data + }) + + e.wait() + e.clear() + +def stopScanning(): + conn.sendMessage({ + 'kCBMsgId': 30, + 'kCBMsgArgs': None + }) + + e.wait() + e.clear() + + + +init() +startScanning() +stopScanning() \ No newline at end of file diff --git a/tests/test_macos_corebluetooth_advertiser.py b/tests/test_macos_corebluetooth_advertiser.py new file mode 100644 index 0000000..86dc9d4 --- /dev/null +++ b/tests/test_macos_corebluetooth_advertiser.py @@ -0,0 +1,54 @@ +import uuid +import threading +import objc +from PyObjCTools import AppHelper + +# https://gist.github.com/jeamland/11284662 + +objc.loadBundle("CoreBluetooth", globals(), + bundle_path=objc.pathForFramework( + u'/System/Library/Frameworks/IOBluetooth.framework/Versions/A/Frameworks/CoreBluetooth.framework')) + + +def main(): + delegate = TestCoreBluetoothAdvertisement() + manager = CBPeripheralManager.alloc() + + # https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393295-init + manager.initWithDelegate_queue_options_(delegate, None, None) + + try: + print("starting") + AppHelper.runConsoleEventLoop(installInterrupt=True) + except KeyboardInterrupt: + AppHelper.stopEventLoop() + + + +class TestCoreBluetoothAdvertisement(): + + def __init__(self): + self.manager = None + self.peripheral = None + self._connected = threading.Event() + + def peripheralManagerDidUpdateState_(self, manager): + print("peripheralManagerDidUpdateState_") + self.manager = manager + self.start_advertising() + + def start_advertising(self): + adv_data = { + 'CBAdvertisementDataLocalNameKey': 'bleson', + #'CBAdvertisementDataServiceUUIDsKey': CBUUID.UUIDWithString_(u'6E400001-B5A3-F393-E0A9-E50E24DCCA9E') + 'CBAdvertisementDataServiceUUIDsKey': CBUUID.UUIDWithString_(u'0xFFEF') + } + self.manager.startAdvertising_(adv_data) + + + def peripheralManagerDidStartAdvertising_error_(self, peripheral, error): + print("peripheralManagerDidStartAdvertising_error_ {} {}".format(peripheral, error)) + + +if __name__ == "__main__": + main() \ No newline at end of file From c18bc77afe9da90b813140651e8fb834ae602117 Mon Sep 17 00:00:00 2001 From: Wayne Keenan Date: Sun, 12 Nov 2017 05:38:32 +0000 Subject: [PATCH 08/13] moved experimental examples to the sandpit --- {tests => sandpit}/test_macos_corebluetooth.py | 0 {tests => sandpit}/test_macos_corebluetooth_advertiser.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {tests => sandpit}/test_macos_corebluetooth.py (100%) rename {tests => sandpit}/test_macos_corebluetooth_advertiser.py (100%) diff --git a/tests/test_macos_corebluetooth.py b/sandpit/test_macos_corebluetooth.py similarity index 100% rename from tests/test_macos_corebluetooth.py rename to sandpit/test_macos_corebluetooth.py diff --git a/tests/test_macos_corebluetooth_advertiser.py b/sandpit/test_macos_corebluetooth_advertiser.py similarity index 100% rename from tests/test_macos_corebluetooth_advertiser.py rename to sandpit/test_macos_corebluetooth_advertiser.py From 546b5d907b0a4495362843237c947f3e8f89f0bc Mon Sep 17 00:00:00 2001 From: Wayne Keenan Date: Mon, 13 Nov 2017 12:49:43 +0000 Subject: [PATCH 09/13] added pure Pytohn XPC experiment. added pyobj dispatch expermiment --- bleson/providers/macos/macos_adapter.py | 89 ++++++++++++--- ...ser.py => test_macos_pyobjc_advertiser.py} | 0 ...tooth.py => test_macos_pyobjc_observer.py} | 0 sandpit/test_macos_xpc_ctypes_observer.py | 106 ++++++++++++++++++ ...r.py => test_macos_xpc_native_observer.py} | 22 ++-- 5 files changed, 193 insertions(+), 24 deletions(-) rename sandpit/{test_macos_corebluetooth_advertiser.py => test_macos_pyobjc_advertiser.py} (100%) rename sandpit/{test_macos_corebluetooth.py => test_macos_pyobjc_observer.py} (100%) create mode 100644 sandpit/test_macos_xpc_ctypes_observer.py rename sandpit/{test_xpc_observer.py => test_macos_xpc_native_observer.py} (80%) diff --git a/bleson/providers/macos/macos_adapter.py b/bleson/providers/macos/macos_adapter.py index b89d43d..0172c3e 100644 --- a/bleson/providers/macos/macos_adapter.py +++ b/bleson/providers/macos/macos_adapter.py @@ -8,6 +8,60 @@ from Foundation import * from PyObjCTools import AppHelper import CoreBluetooth +import ctypes + + +# No experiments, no background thread. +(USE_BACKGROUND_THREAD_EXPERIMENT, USE_DISPATCH_EXPERIMENT, USE_DISPATCH_EXPERIMENT_MAIN_QUEUE) = (False, False, False) + + +# Background thread, no dispatch queue. Donest crash, bu tnot CoreBluetooth messages +#(USE_BACKGROUND_THREAD_EXPERIMENT, USE_DISPATCH_EXPERIMENT, USE_DISPATCH_EXPERIMENT_MAIN_QUEUE) = (True, False, False) + + +# Background thread, thread local dispatch queue. crash +#(USE_BACKGROUND_THREAD_EXPERIMENT, USE_DISPATCH_EXPERIMENT, USE_DISPATCH_EXPERIMENT_MAIN_QUEUE) = (True, True, False) + + +# Background thread, main dispatch queue. crash +#(USE_BACKGROUND_THREAD_EXPERIMENT, USE_DISPATCH_EXPERIMENT, USE_DISPATCH_EXPERIMENT_MAIN_QUEUE) = (True, True, True) + + +###################################### +# Dispatch Queue experimetn begins + +# see: https://bitbucket.org/ronaldoussoren/pyobjc/issues/215/starting-runconsoleeventloop-from-a + + +# Opaque structure use to pass areound 'dispatch_queue_t' C type +# see: https://stackoverflow.com/questions/5030730/is-it-acceptable-to-subclass-c-void-p-in-ctypes +class dispatch_queue_t(ctypes.Structure): + pass + +# Load the dispatch library +_lib = ctypes.cdll.LoadLibrary("/usr/lib/system/libdispatch.dylib") + +_dispatch_queue_create = _lib.dispatch_queue_create +_dispatch_queue_create.argtypes = [ctypes.c_char_p, ctypes.c_int32] # 2nd param is stuct, but we don't use it. +_dispatch_queue_create.restype = ctypes.POINTER(dispatch_queue_t) +# https://developer.apple.com/documentation/dispatch/1453030-dispatch_queue_create + + +# https://docs.python.org/3/library/ctypes.html#accessing-values-exported-from-dlls +_dispatch_main_q = ctypes.POINTER(dispatch_queue_t).in_dll(_lib, '_dispatch_main_q') + +def dispatch_queue_create(name): + b_name = name.encode('utf-8') + c_name = ctypes.c_char_p(b_name) + return _dispatch_queue_create(c_name, ctypes.c_int32(0)) + +def dispatch_get_main_queue(): + return _dispatch_main_q + +# Dispatch Queue experimetn ends +###################################### + + # https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html @@ -21,22 +75,10 @@ # use XPC instead: https://github.com/sandeepmistry/noble/blob/master/lib/mac/yosemite.js - # Python XPC -# brew install python3 -# brew install boost-python --with-python3 -# ln -s /usr/local/lib/libboost_python3.a /usr/local/lib/libboost_python-py34.a -# git clone https://github.com/matthewelse/pyxpcconnection -# cd pyxpcconnection -# edit setup.py: -""" -- extra_compile_args=['-std=c++11'], -+ extra_compile_args=['-std=c++11', '-stdlib=libc++', '-mmacosx-version-min=10.9'], - libraries = boost_libs -""" -# python3 setup.py install - +# see: https://github.com/TheCellule/pyxpcconnection/tree/py3_fix +# python3 setup.py install --user class CoreBluetoothAdapter(Adapter): @@ -48,6 +90,7 @@ def __init__(self, device_id=0): self._socket_poll_thread = threading.Thread(target=self._runloop_thread, name='BlesonObjCRunLoop') self._socket_poll_thread.setDaemon(True) self._manager = None + self._dispatch_queue = None def open(self): pass @@ -61,8 +104,13 @@ def off(self): def start_scanning(self): log.info("start scanning") - self._runloop_thread() - #self._socket_poll_thread.start() + + if USE_BACKGROUND_THREAD_EXPERIMENT: + # spawn background thread + self._socket_poll_thread.start() + else: + # Start on main thread + self._runloop_thread() def stop_scanning(self): @@ -83,8 +131,15 @@ def _init_cb_manager(self): # bundle_path=objc.pathForFramework( # u'/System/Library/Frameworks/IOBluetooth.framework/Versions/A/Frameworks/CoreBluetooth.framework')) + if USE_DISPATCH_EXPERIMENT: + if USE_DISPATCH_EXPERIMENT_MAIN_QUEUE: + self._dispatch_queue = dispatch_get_main_queue() + else: + self._dispatch_queue = dispatch_queue_create('blesonq') + + print(self._dispatch_queue) self._manager = CoreBluetooth.CBCentralManager.alloc() - self._manager.initWithDelegate_queue_options_(self, None, None) + self._manager.initWithDelegate_queue_options_(self, self._dispatch_queue, None) # https://pythonhosted.org/pyobjc/core/intro.html#working-with-threads def _runloop_thread(self): diff --git a/sandpit/test_macos_corebluetooth_advertiser.py b/sandpit/test_macos_pyobjc_advertiser.py similarity index 100% rename from sandpit/test_macos_corebluetooth_advertiser.py rename to sandpit/test_macos_pyobjc_advertiser.py diff --git a/sandpit/test_macos_corebluetooth.py b/sandpit/test_macos_pyobjc_observer.py similarity index 100% rename from sandpit/test_macos_corebluetooth.py rename to sandpit/test_macos_pyobjc_observer.py diff --git a/sandpit/test_macos_xpc_ctypes_observer.py b/sandpit/test_macos_xpc_ctypes_observer.py new file mode 100644 index 0000000..99748c8 --- /dev/null +++ b/sandpit/test_macos_xpc_ctypes_observer.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 + +# Experiment with calling XPC using the built-in Python 'ctypes' module. +# e.g. an attempt at a pure python port of https://github.com/matthewelse/pyxpcconnection + + +import ctypes +from time import sleep + +############################################################################# +# libDispatch Wrapper + + +# Opaque structure use to pass areound 'dispatch_queue_t' C type +# see: https://stackoverflow.com/questions/5030730/is-it-acceptable-to-subclass-c-void-p-in-ctypes +class dispatch_queue_t(ctypes.Structure): + pass + +# Load the dispatch library +_lib = ctypes.cdll.LoadLibrary("/usr/lib/system/libdispatch.dylib") + +_dispatch_queue_create = _lib.dispatch_queue_create +_dispatch_queue_create.argtypes = [ctypes.c_char_p, ctypes.c_int32] # 2nd param is stuct, but we don't use it. +_dispatch_queue_create.restype = ctypes.POINTER(dispatch_queue_t) + +def dispatch_queue_create(name): + b_name = name.encode('utf-8') + c_name = ctypes.c_char_p(b_name) + return _dispatch_queue_create(c_name, ctypes.c_int32(0)) + + + + +############################################################################# +# XPC Wrapper + +# Opaque type +class xpc_connection(ctypes.Structure): + pass + +# Constants from header file /usr/include/xpc/connection.h +XPC_CONNECTION_MACH_SERVICE_PRIVILEGED = 1 << 1 + + +xpc = ctypes.cdll.LoadLibrary("/usr/lib/system/libxpc.dylib") + +_xpc_connection_create_mach_service = xpc.xpc_connection_create_mach_service +_xpc_connection_create_mach_service.argtypes = [ctypes.c_char_p, ctypes.POINTER(dispatch_queue_t), ctypes.c_int32] +_xpc_connection_create_mach_service.restype = ctypes.POINTER(xpc_connection) + +def xpc_connection_create_mach_service(name, queue, flags): + b_name = name.encode('utf-8') + c_name = ctypes.c_char_p(b_name) + return _xpc_connection_create_mach_service(c_name, queue, flags) + + + + + +# see: https://developer.apple.com/documentation/xpc/1448781-xpc_connection_resume +_xpc_connection_resume = xpc.xpc_connection_resume +_xpc_connection_resume.argtypes = [ctypes.POINTER(xpc_connection)] +#_xpc_connection_resume.restype = None + +def xpc_connection_resume(connection): + _xpc_connection_resume(connection) + + + +# Opaque type +class xpc_object_t(ctypes.Structure): + pass + + +xpc_handler_t = ctypes.CFUNCTYPE(None, ctypes.POINTER(xpc_object_t)) # 1st parameter is funtion return type + + +# see: https://developer.apple.com/documentation/xpc/1448805-xpc_connection_set_event_handler + +_xpc_connection_set_event_handler = xpc.xpc_connection_set_event_handler +_xpc_connection_set_event_handler.argtypes = [ctypes.POINTER(xpc_connection), ctypes.POINTER(xpc_handler_t)] +#_xpc_connection_set_event_handler.argtypes = [ctypes.POINTER(xpc_connection), xpc_handler_t] + +def xpc_connection_set_event_handler(connection, handler): + _xpc_connection_set_event_handler(connection, xpc_handler_t(handler)) + +############################################################################# + +queue = dispatch_queue_create('myqueue') +print(queue) + + +def handler(object): + print('XPC handler') + pass + +xpc_connection = xpc_connection_create_mach_service('myxpc', queue, XPC_CONNECTION_MACH_SERVICE_PRIVILEGED) + +xpc_connection_set_event_handler(xpc_connection, handler) + + +xpc_connection_resume(xpc_connection) + + +sleep(10) + diff --git a/sandpit/test_xpc_observer.py b/sandpit/test_macos_xpc_native_observer.py similarity index 80% rename from sandpit/test_xpc_observer.py rename to sandpit/test_macos_xpc_native_observer.py index 2df5ed5..0c08d20 100644 --- a/sandpit/test_xpc_observer.py +++ b/sandpit/test_macos_xpc_native_observer.py @@ -28,6 +28,7 @@ def __init__(self, target, event): def onEvent(self, data): print('onEvent, data={}'.format(data)) + msg_id = data['kCBMsgId'] args = data['kCBMsgArgs'] @@ -39,18 +40,24 @@ def onEvent(self, data): print('discovered a device') args.setdefault(None) - rssi = args['kCBMsgArgRssi'] - uuid = UUID(bytes=args['kCBMsgArgDeviceUUID']) - ad_data = args['kCBMsgArgAdvertisementData'] + rssi = args['kCBMsgArgRssi'] if 'kCBMsgArgRssi' in args else None + ad_data = args['kCBMsgArgAdvertisementData'] if 'kCBMsgArgAdvertisementData' in args else None + + if 'kCBMsgArgDeviceUUID' in args and args['kCBMsgArgDeviceUUID'] is not None: + uuid = UUID(bytes=args['kCBMsgArgDeviceUUID']) + else: + uuid = None - print(rssi) - print(uuid) - print(ad_data) + print("RSSI", rssi) + print("UUID", uuid) + print("ADV_DATA", ad_data) def onError(self, data): - print('error') + print('ERROR:', data) def handler(self, event): + print("handler") + e_type, data = event if e_type == 'event': @@ -58,6 +65,7 @@ def handler(self, event): elif e_type == 'error': self.onError(data) else: + print("WARNING: Unhandled message", event) # que? pass From 1ff5ec69937804fe7a418eaf8c6466a9f3c35171 Mon Sep 17 00:00:00 2001 From: Wayne Keenan Date: Tue, 14 Nov 2017 08:42:25 +0000 Subject: [PATCH 10/13] CoreBluetooth now runs in a background thread --- bleson/providers/__init__.py | 2 +- bleson/providers/macos/macos_adapter.py | 111 +++------------ .../test_macos_pyobjc_dispatch_observer.py | 129 ++++++++++++++++++ 3 files changed, 150 insertions(+), 92 deletions(-) create mode 100644 sandpit/test_macos_pyobjc_dispatch_observer.py diff --git a/bleson/providers/__init__.py b/bleson/providers/__init__.py index 8a28559..fb55b8b 100644 --- a/bleson/providers/__init__.py +++ b/bleson/providers/__init__.py @@ -22,6 +22,6 @@ def get_provider(): else: raise RuntimeError('Platform {0} is not supported!'.format(sys.platform)) - log.info("Provider is {}".format(_provider)) + log.debug("Provider is {}".format(_provider)) return _provider diff --git a/bleson/providers/macos/macos_adapter.py b/bleson/providers/macos/macos_adapter.py index 0172c3e..2d2807f 100644 --- a/bleson/providers/macos/macos_adapter.py +++ b/bleson/providers/macos/macos_adapter.py @@ -11,74 +11,34 @@ import ctypes -# No experiments, no background thread. -(USE_BACKGROUND_THREAD_EXPERIMENT, USE_DISPATCH_EXPERIMENT, USE_DISPATCH_EXPERIMENT_MAIN_QUEUE) = (False, False, False) - - -# Background thread, no dispatch queue. Donest crash, bu tnot CoreBluetooth messages -#(USE_BACKGROUND_THREAD_EXPERIMENT, USE_DISPATCH_EXPERIMENT, USE_DISPATCH_EXPERIMENT_MAIN_QUEUE) = (True, False, False) - - -# Background thread, thread local dispatch queue. crash -#(USE_BACKGROUND_THREAD_EXPERIMENT, USE_DISPATCH_EXPERIMENT, USE_DISPATCH_EXPERIMENT_MAIN_QUEUE) = (True, True, False) - - -# Background thread, main dispatch queue. crash -#(USE_BACKGROUND_THREAD_EXPERIMENT, USE_DISPATCH_EXPERIMENT, USE_DISPATCH_EXPERIMENT_MAIN_QUEUE) = (True, True, True) - - ###################################### -# Dispatch Queue experimetn begins +# Dispatch Queue support # see: https://bitbucket.org/ronaldoussoren/pyobjc/issues/215/starting-runconsoleeventloop-from-a - # Opaque structure use to pass areound 'dispatch_queue_t' C type # see: https://stackoverflow.com/questions/5030730/is-it-acceptable-to-subclass-c-void-p-in-ctypes -class dispatch_queue_t(ctypes.Structure): - pass +#class dispatch_queue_t(ctypes.Structure): +# pass +# The above is not used as the PyObcC doest accept it as a type to create the pointer to the dispatch_queu_ + +NULL_PTR = ctypes.POINTER(ctypes.c_int)() + # Load the dispatch library _lib = ctypes.cdll.LoadLibrary("/usr/lib/system/libdispatch.dylib") _dispatch_queue_create = _lib.dispatch_queue_create -_dispatch_queue_create.argtypes = [ctypes.c_char_p, ctypes.c_int32] # 2nd param is stuct, but we don't use it. -_dispatch_queue_create.restype = ctypes.POINTER(dispatch_queue_t) +_dispatch_queue_create.argtypes = [ctypes.c_char_p, ctypes.c_void_p] # 2nd param is stuct, but we don't use it. +_dispatch_queue_create.restype = ctypes.c_void_p # https://developer.apple.com/documentation/dispatch/1453030-dispatch_queue_create -# https://docs.python.org/3/library/ctypes.html#accessing-values-exported-from-dlls -_dispatch_main_q = ctypes.POINTER(dispatch_queue_t).in_dll(_lib, '_dispatch_main_q') def dispatch_queue_create(name): b_name = name.encode('utf-8') c_name = ctypes.c_char_p(b_name) - return _dispatch_queue_create(c_name, ctypes.c_int32(0)) - -def dispatch_get_main_queue(): - return _dispatch_main_q - -# Dispatch Queue experimetn ends -###################################### - - - -# https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html - -# pyobjc: https://bitbucket.org/ronaldoussoren/pyobjc -# https://bitbucket.org/ronaldoussoren/pyobjc/src/87ce46672615ef127f40850cc78401ca62cc336a/pyobjc-framework-CoreBluetooth/PyObjCTest/?at=default - -# Issue: https://bitbucket.org/ronaldoussoren/pyobjc/issues/215/starting-runconsoleeventloop-from-a - -# ObjC diest support dispatchQueues... - -# use XPC instead: https://github.com/sandeepmistry/noble/blob/master/lib/mac/yosemite.js - - -# Python XPC - -# see: https://github.com/TheCellule/pyxpcconnection/tree/py3_fix -# python3 setup.py install --user + return _dispatch_queue_create(c_name, NULL_PTR) class CoreBluetoothAdapter(Adapter): @@ -89,8 +49,9 @@ def __init__(self, device_id=0): self._keep_running = True self._socket_poll_thread = threading.Thread(target=self._runloop_thread, name='BlesonObjCRunLoop') self._socket_poll_thread.setDaemon(True) - self._manager = None - self._dispatch_queue = None + self._dispatch_queue = dispatch_queue_create('blesonq') + self._manager = CoreBluetooth.CBCentralManager.alloc() + def open(self): pass @@ -103,18 +64,10 @@ def off(self): log.warn("TODO: adatper off") def start_scanning(self): - log.info("start scanning") - - if USE_BACKGROUND_THREAD_EXPERIMENT: - # spawn background thread - self._socket_poll_thread.start() - else: - # Start on main thread - self._runloop_thread() + self._socket_poll_thread.start() def stop_scanning(self): - log.info("stopping") rc = AppHelper.stopEventLoop() log.info("done: AppHelper.stopEventLoop, successful?={}".format(rc)) @@ -124,37 +77,18 @@ def start_advertising(self, advertisement, scan_response=None): def stop_advertising(self): raise NotImplementedError - # Obj Runloop - - def _init_cb_manager(self): - # objc.loadBundle("CoreBluetooth", globals(), - # bundle_path=objc.pathForFramework( - # u'/System/Library/Frameworks/IOBluetooth.framework/Versions/A/Frameworks/CoreBluetooth.framework')) - - if USE_DISPATCH_EXPERIMENT: - if USE_DISPATCH_EXPERIMENT_MAIN_QUEUE: - self._dispatch_queue = dispatch_get_main_queue() - else: - self._dispatch_queue = dispatch_queue_create('blesonq') - - print(self._dispatch_queue) - self._manager = CoreBluetooth.CBCentralManager.alloc() - self._manager.initWithDelegate_queue_options_(self, self._dispatch_queue, None) # https://pythonhosted.org/pyobjc/core/intro.html#working-with-threads def _runloop_thread(self): try: - log.info('AppHelper.runConsoleEventLoop()') - pool = NSAutoreleasePool.alloc().init() - self._init_cb_manager() - rc = AppHelper.runConsoleEventLoop(installInterrupt=True) - log.info(rc) + with objc.autorelease_pool(): + queue_ptr = objc.objc_object(c_void_p=self._dispatch_queue) + self._manager.initWithDelegate_queue_options_(self, queue_ptr, None) + AppHelper.runConsoleEventLoop(installInterrupt=True) except Exception as e: log.exception(e) finally: AppHelper.stopEventLoop() - del pool - log.info("leaving") # CoreBluetooth Protocol @@ -162,12 +96,10 @@ def centralManagerDidUpdateState_(self, manager): log.debug("centralManagerDidUpdateState_") if self.connected == False: - self.manager = manager - manager.scanForPeripheralsWithServices_options_(None, None) + self._manager.scanForPeripheralsWithServices_options_(None, None) def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(self, manager, peripheral, data, rssi): try: - log.debug("centralManager_didDiscoverPeripheral_advertisementData_RSSI_") log.debug('Found: name={} rssi={} data={} '.format(peripheral.name(), rssi, data)) if self.on_advertising_data: @@ -216,7 +148,6 @@ def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(self, manager, def centralManager_didConnectPeripheral_(self, manager, peripheral): - log.debug("centralManager_didConnectPeripheral_") log.debug('Connected: ' + peripheral.name()) self.connected = True self.peripheral.setDelegate_(self) @@ -224,8 +155,6 @@ def centralManager_didConnectPeripheral_(self, manager, peripheral): #self.peripheral.discoverServices_([CBUUID(...)]) def centralManager_didFailToConnectPeripheral_error_(self, manager, peripheral, error): - log.debug("centralManager_didFailToConnectPeripheral_error_") - log.error(repr(error)) def centralManager_didDisconnectPeripheral_error_(self, manager, peripheral, error): @@ -259,4 +188,4 @@ def peripheral_didUpdateNotificationStateForCharacteristic_error_(self, peripher def peripheral_didUpdateValueForCharacteristic_error_(self, peripheral, characteristic, error): log.debug("peripheral_didUpdateValueForCharacteristic_error_") - repr(characteristic.value().bytes().tobytes()) + log.debug(repr(characteristic.value().bytes().tobytes())) diff --git a/sandpit/test_macos_pyobjc_dispatch_observer.py b/sandpit/test_macos_pyobjc_dispatch_observer.py new file mode 100644 index 0000000..643fbe7 --- /dev/null +++ b/sandpit/test_macos_pyobjc_dispatch_observer.py @@ -0,0 +1,129 @@ +import threading +from logging import getLogger, basicConfig, DEBUG +from time import sleep +import objc +from Foundation import * +from PyObjCTools import AppHelper +import CoreBluetooth + +from ctypes import cdll, Structure, POINTER, pointer, c_int, c_char_p, c_void_p, cast +# http://python.net/crew/theller/ctypes/tutorial.html + +#from objc import se +#objc.setObjCPointerIsError(True) + +OpaqueType = objc.createOpaquePointerType("OpaqueType", b"^{OpaqueType}", None) + + +basicConfig(level=DEBUG, format='%(asctime)s %(levelname)6s - %(funcName)24s(): %(message)s') +log = getLogger(__name__) + +USE_BACKGROUND_THREAD = True # When false OK, when true no CoreBluetooth events +USE_DISPATCH_QUEUE = True # When false OK, when true SIG + + +NULL_PTR = POINTER(c_int)() + +# Opaque structure that can be used to pass areound 'dispatch_queue_t' C type in a type-safe way +# see: https://stackoverflow.com/questions/5030730/is-it-acceptable-to-subclass-c-void-p-in-ctypes +class dispatch_queue_t(Structure): + pass + + +# Load the dispatch library +_lib = cdll.LoadLibrary("/usr/lib/system/libdispatch.dylib") + +_dispatch_queue_create = _lib.dispatch_queue_create +_dispatch_queue_create.argtypes = [c_char_p, c_void_p] +#_dispatch_queue_create.restype = POINTER(dispatch_queue_t) +_dispatch_queue_create.restype = c_void_p + +def dispatch_queue_create(name): + # https://developer.apple.com/documentation/dispatch/1453030-dispatch_queue_create + b_name = name.encode('utf-8') + c_name = c_char_p(b_name) + queue = _dispatch_queue_create(c_name, NULL_PTR) + + return queue + + +class TestCoreBluetooth(): + + def __init__(self): + self._manager = None + self._dispatch_queue = dispatch_queue_create('myq') if USE_DISPATCH_QUEUE else None + + + def start(self): + if USE_BACKGROUND_THREAD: + self._socket_poll_thread = threading.Thread(target=self._runloop, name='mythread') + self._socket_poll_thread.setDaemon(True) + self._socket_poll_thread.start() + else: + self._runloop() + + + # https://pythonhosted.org/pyobjc/core/intro.html#working-with-threads + def _runloop(self): + log.info("") + try: + pool = NSAutoreleasePool.alloc().init() + + self._manager = CoreBluetooth.CBCentralManager.alloc() + + # queue_ptr = self._dispatch_queue # the ctypes object of dispatch_queue_t* returned from dispatch_queue_create() + # queue_ptr = OpaqueType(c_void_p=self._dispatch_queue) + queue_ptr = objc.objc_object(c_void_p=self._dispatch_queue) + self._manager.initWithDelegate_queue_options_(self, queue_ptr, None) + + log.info('Starting RunLoop, dispatch_queue={}'.format(self._dispatch_queue)) + rc = AppHelper.runConsoleEventLoop(installInterrupt=True) + log.info("RC=",rc) + except Exception as e: + log.exception(e) + finally: + AppHelper.stopEventLoop() + if pool: + del pool + log.info("Ending") + + + # CoreBluetooth Protocol + + def centralManagerDidUpdateState_(self, manager): + log.info("") + self._manager.scanForPeripheralsWithServices_options_(None, None) + + def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(self, manager, peripheral, data, rssi): + log.info('Found: name={} rssi={} data={} '.format(peripheral.name(), rssi, data)) + + def centralManager_didConnectPeripheral_(self, manager, peripheral): + log.info("") + + def centralManager_didFailToConnectPeripheral_error_(self, manager, peripheral, error): + log.error(repr(error)) + + def centralManager_didDisconnectPeripheral_error_(self, manager, peripheral, error): + log.info("") + + def peripheral_didDiscoverServices_(self, peripheral, error): + log.info("") + + def peripheral_didDiscoverCharacteristicsForService_error_(self, peripheral, service, error): + log.info("") + + def peripheral_didWriteValueForCharacteristic_error_(self, peripheral, characteristic, error): + log.info("") + + def peripheral_didUpdateNotificationStateForCharacteristic_error_(self, peripheral, characteristic, error): + log.info("peripheral_didUpdateNotificationStateForCharacteristic_error_") + + + def peripheral_didUpdateValueForCharacteristic_error_(self, peripheral, characteristic, error): + log.error(repr(error)) + + +if __name__ == "__main__": + bt = TestCoreBluetooth() + bt.start() + sleep(10) \ No newline at end of file From 6335d04b99059200f85c553c4428e67685b863eb Mon Sep 17 00:00:00 2001 From: Wayne Keenan Date: Tue, 14 Nov 2017 08:56:09 +0000 Subject: [PATCH 11/13] Quick tidy --- bleson/providers/macos/macos_adapter.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/bleson/providers/macos/macos_adapter.py b/bleson/providers/macos/macos_adapter.py index 2d2807f..b9fb370 100644 --- a/bleson/providers/macos/macos_adapter.py +++ b/bleson/providers/macos/macos_adapter.py @@ -68,8 +68,7 @@ def start_scanning(self): def stop_scanning(self): - rc = AppHelper.stopEventLoop() - log.info("done: AppHelper.stopEventLoop, successful?={}".format(rc)) + AppHelper.stopEventLoop() def start_advertising(self, advertisement, scan_response=None): raise NotImplementedError @@ -118,11 +117,7 @@ def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(self, manager, if 'kCBAdvDataServiceUUIDs' in data: log.debug('kCBAdvDataServiceUUIDs:') for cbuuid in data['kCBAdvDataServiceUUIDs']: - uuid_bytes2 = cbuuid.data().bytes() uuid_bytes = cbuuid.data().bytes().tobytes() - log.debug("--------------") - log.debug(bytearray_to_hexstring(uuid_bytes)) - log.debug(bytearray_to_hexstring(uuid_bytes2)) if 2 == len(uuid_bytes): uuid = UUID16(uuid_bytes, little_endian=False) From 43317cd2596e03ccddd58cd2fa5a3ea66541794e Mon Sep 17 00:00:00 2001 From: Wayne Keenan Date: Tue, 14 Nov 2017 13:56:59 +0000 Subject: [PATCH 12/13] inital win32 provider --- bleson/providers/__init__.py | 3 +- bleson/providers/win32/__init__.py | 0 bleson/providers/win32/win32_adapter.py | 44 ++++++++++++++++++++++++ bleson/providers/win32/win32_provider.py | 15 ++++++++ 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 bleson/providers/win32/__init__.py create mode 100644 bleson/providers/win32/win32_adapter.py create mode 100644 bleson/providers/win32/win32_provider.py diff --git a/bleson/providers/__init__.py b/bleson/providers/__init__.py index 8a28559..ba20c0f 100644 --- a/bleson/providers/__init__.py +++ b/bleson/providers/__init__.py @@ -18,7 +18,8 @@ def get_provider(): _provider = MacOSProvider() elif sys.platform.startswith('win32'): - raise NotImplementedError() + from bleson.providers.win32.win32_provider import Win32Provider + _provider = Win32Provider() else: raise RuntimeError('Platform {0} is not supported!'.format(sys.platform)) diff --git a/bleson/providers/win32/__init__.py b/bleson/providers/win32/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bleson/providers/win32/win32_adapter.py b/bleson/providers/win32/win32_adapter.py new file mode 100644 index 0000000..be84e20 --- /dev/null +++ b/bleson/providers/win32/win32_adapter.py @@ -0,0 +1,44 @@ +import threading +from bleson.interfaces.adapter import Adapter +from bleson.core.types import Advertisement, UUID16, UUID128 +from bleson.core.hci.constants import * +from bleson.logger import log +from bleson.core.hci.type_converters import bytearray_to_hexstring + +#Minimum version for BLE scanning without paring and GATT Peripheral : https://docs.microsoft.com/en-gb/windows/uwp/whats-new/windows-10-build-15063 + + +#https://docs.microsoft.com/en-gb/uwp/api/windows.devices.bluetooth.genericattributeprofile.gattserviceprovider +#https://docs.microsoft.com/en-gb/uwp/api/windows.devices.bluetooth.bluetoothledevice + + +class BluetoothAdapter(Adapter): + + def __init__(self, device_id=0): + self.device_id = device_id + self.connected = False + self._keep_running = True + + + def open(self): + pass + + + def on(self): + log.warn("TODO: adatper on") + + def off(self): + log.warn("TODO: adatper off") + + def start_scanning(self): + log.info("start scanning") + + def stop_scanning(self): + log.info("stopping") + + def start_advertising(self, advertisement, scan_response=None): + raise NotImplementedError + + def stop_advertising(self): + raise NotImplementedError + diff --git a/bleson/providers/win32/win32_provider.py b/bleson/providers/win32/win32_provider.py new file mode 100644 index 0000000..8a30ed5 --- /dev/null +++ b/bleson/providers/win32/win32_provider.py @@ -0,0 +1,15 @@ +from bleson.interfaces.provider import Provider +from .win32_adapter import BluetoothAdapter +from bleson.logger import log + + +class Win32Provider(Provider): + + def get_adapter(self, adapter_id=0): + adapter = BluetoothAdapter(adapter_id) + adapter.open() + adapter.off() + adapter.on() + return adapter + + # TODO: have a 'get_converter(for_type)' and registry of ValueObject to provider data converters (Linux uses core HCI) From 5e814cb4cb040f8504b618941716ebb294b2d286 Mon Sep 17 00:00:00 2001 From: Wayne Keenan Date: Thu, 16 Nov 2017 18:01:26 +0000 Subject: [PATCH 13/13] Initial Win10 BLE scanning support, depends on the new blesonwin native Python module --- bleson/providers/win32/win32_adapter.py | 36 ++++++++++++++++++++---- bleson/providers/win32/win32_provider.py | 1 - 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/bleson/providers/win32/win32_adapter.py b/bleson/providers/win32/win32_adapter.py index be84e20..bfd199a 100644 --- a/bleson/providers/win32/win32_adapter.py +++ b/bleson/providers/win32/win32_adapter.py @@ -5,12 +5,7 @@ from bleson.logger import log from bleson.core.hci.type_converters import bytearray_to_hexstring -#Minimum version for BLE scanning without paring and GATT Peripheral : https://docs.microsoft.com/en-gb/windows/uwp/whats-new/windows-10-build-15063 - - -#https://docs.microsoft.com/en-gb/uwp/api/windows.devices.bluetooth.genericattributeprofile.gattserviceprovider -#https://docs.microsoft.com/en-gb/uwp/api/windows.devices.bluetooth.bluetoothledevice - +import blesonwin class BluetoothAdapter(Adapter): @@ -18,6 +13,8 @@ def __init__(self, device_id=0): self.device_id = device_id self.connected = False self._keep_running = True + self.on_advertising_data = None + blesonwin.initialise() def open(self): @@ -32,9 +29,17 @@ def off(self): def start_scanning(self): log.info("start scanning") + if self.on_advertising_data: + blesonwin.on_advertisement(self._on_advertising_data) + else: + log.warning("on_advertising_data is not set") + + log.info(self.on_advertising_data) + blesonwin.start_observer() def stop_scanning(self): log.info("stopping") + blesonwin.stop_observer() def start_advertising(self, advertisement, scan_response=None): raise NotImplementedError @@ -42,3 +47,22 @@ def start_advertising(self, advertisement, scan_response=None): def stop_advertising(self): raise NotImplementedError + def _on_advertising_data(self, data): + try: + log.debug('Found: {}'.format(data)) + + if self.on_advertising_data: + advertisement = Advertisement() + advertisement.flags = 0 + advertisement.rssi = data['RSSI'] + + if 'LOCALNAME' in data: + advertisement.name = int(data['LOCALNAME']) + + if 'TXPOWER' in data: + advertisement.tx_pwr_lvl = int(data['TXPOWER']) + + self.on_advertising_data(advertisement) + + except Exception as e: + log.exception(e) \ No newline at end of file diff --git a/bleson/providers/win32/win32_provider.py b/bleson/providers/win32/win32_provider.py index 8a30ed5..5108324 100644 --- a/bleson/providers/win32/win32_provider.py +++ b/bleson/providers/win32/win32_provider.py @@ -2,7 +2,6 @@ from .win32_adapter import BluetoothAdapter from bleson.logger import log - class Win32Provider(Provider): def get_adapter(self, adapter_id=0):