From 73a840061b17d09c315b0f03bcadb2a2580be5a7 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Thu, 4 Jun 2020 20:04:00 -0400 Subject: [PATCH 1/9] wip --- adafruit_ble/__init__.py | 2 +- adafruit_ble/advertising/__init__.py | 49 +++++++++++++++---- adafruit_ble/advertising/adafruit.py | 17 ++++--- adafruit_ble/advertising/apple.py | 3 +- adafruit_ble/advertising/standard.py | 16 +++--- adafruit_ble/services/standard/device_info.py | 16 ++++-- 6 files changed, 71 insertions(+), 32 deletions(-) diff --git a/adafruit_ble/__init__.py b/adafruit_ble/__init__.py index 10ce0fb..0ceaeac 100755 --- a/adafruit_ble/__init__.py +++ b/adafruit_ble/__init__.py @@ -231,7 +231,7 @@ def start_scan( """ if not advertisement_types: advertisement_types = (Advertisement,) - prefixes = b"".join(adv.prefix for adv in advertisement_types) + prefixes = b"".join(adv.get_prefix_bytes() for adv in advertisement_types) for entry in self._adapter.start_scan( prefixes=prefixes, buffer_size=buffer_size, diff --git a/adafruit_ble/advertising/__init__.py b/adafruit_ble/advertising/__init__.py index 649901d..0e7e489 100644 --- a/adafruit_ble/advertising/__init__.py +++ b/adafruit_ble/advertising/__init__.py @@ -214,9 +214,17 @@ def advertising_data_type(self): class Advertisement: - """Core Advertisement type""" + """Core Advertisement type. + + The class attribute ``match_prefixes``, if not ``None``, is a tuple of + bytestring prefixes to match against the multiple data structures in the advertisement. + """ + + match_prefixes = () + """For Advertisement, `matches` will always return True. Subclasses may override this value.""" + # cached bytes of merged prefixes. + _prefix_bytes = None - prefix = b"\x00" # This is an empty prefix and will match everything. flags = LazyObjectField(AdvertisingFlags, "flags", advertising_data_type=0x01) short_name = String(advertising_data_type=0x08) """Short local device name (shortened to fit).""" @@ -257,7 +265,11 @@ def from_entry(cls, entry): """Create an Advertisement based on the given ScanEntry. This is done automatically by `BLERadio` for all scan results.""" self = cls() - self.data_dict = decode_data(entry.advertisement_bytes) + # If data_dict is available, use it directly. Otherwise decode the bytestring. + if hasattr(entry, "data_dict"): + self.data_dict = entry.data_dict + else: + self.data_dict = decode_data(entry.advertisement_bytes) self.address = entry.address self._rssi = entry.rssi # pylint: disable=protected-access self.connectable = entry.connectable @@ -272,13 +284,32 @@ def rssi(self): return self._rssi @classmethod - def matches(cls, entry): - """Returns true if the given `_bleio.ScanEntry` matches all portions of the Advertisement - type's prefix.""" - if not hasattr(cls, "prefix"): - return True + def get_prefix_bytes(cls): + """Return a merged version of match_prefixes as a single bytes object, + with length headers. + """ + # Do merge once and memoize it. + if cls._prefix_bytes is None: + cls._prefix_bytes = ( + b"" + if cls.match_prefixes is None + else b"".join( + len(prefix).to_bytes(1, "little") + prefix + for prefix in cls.match_prefixes + ) + ) + + return cls._prefix_bytes - return entry.matches(cls.prefix) + @classmethod + def matches(cls, entry, all_=True): + """Returns ``True`` if the given `_bleio.ScanEntry` advertisement fields + match any or all of the given prefixes in the `match_prefixes` tuple attribute. + If `all_` is ``True``, all the prefixes must match. If ``all_`` is ``False``, + returns ``True`` if at least one of the prefixes match. + """ + # Returns True if cls.get_prefix_bytes() is empty. + return entry.matches(cls.get_prefix_bytes(), all=all_) def __bytes__(self): """The raw packet bytes.""" diff --git a/adafruit_ble/advertising/adafruit.py b/adafruit_ble/advertising/adafruit.py index 29d6ff5..d362b01 100755 --- a/adafruit_ble/advertising/adafruit.py +++ b/adafruit_ble/advertising/adafruit.py @@ -48,14 +48,15 @@ class AdafruitColor(Advertisement): """Broadcast a single RGB color.""" - # This prefix matches all - prefix = struct.pack( - "".format(", ".join(data)) class ServiceList(AdvertisingDataField): @@ -156,7 +156,7 @@ def _present(self, obj): def __get__(self, obj, cls): if not self._present(obj) and not obj.mutable: - return None + return () if not hasattr(obj, "adv_service_lists"): obj.adv_service_lists = {} first_adt = self.standard_services[0] @@ -168,8 +168,8 @@ def __get__(self, obj, cls): class ProvideServicesAdvertisement(Advertisement): """Advertise what services that the device makes available upon connection.""" - # This is four prefixes, one for each ADT that can carry service UUIDs. - prefix = b"\x01\x02\x01\x03\x01\x06\x01\x07" + # Prefixes that match each ADT that can carry service UUIDs. + match_prefixes = (b"\x02", b"\x03", b"\x06", b"\x07") services = ServiceList(standard_services=[0x02, 0x03], vendor_services=[0x06, 0x07]) """List of services the device can provide.""" @@ -182,15 +182,15 @@ def __init__(self, *services): self.flags.le_only = True @classmethod - def matches(cls, entry): - return entry.matches(cls.prefix, all=False) + def matches(cls, entry, all_=False): + return super().matches(entry, all_=all_) class SolicitServicesAdvertisement(Advertisement): """Advertise what services the device would like to use over a connection.""" - # This is two prefixes, one for each ADT that can carry solicited service UUIDs. - prefix = b"\x01\x14\x01\x15" + # Prefixes that match each ADT that can carry solicited service UUIDs. + match_prefixes = (b"\x14", b"\x15") solicited_services = ServiceList(standard_services=[0x14], vendor_services=[0x15]) """List of services the device would like to use.""" diff --git a/adafruit_ble/services/standard/device_info.py b/adafruit_ble/services/standard/device_info.py index 7b631da..5a49c8d 100644 --- a/adafruit_ble/services/standard/device_info.py +++ b/adafruit_ble/services/standard/device_info.py @@ -29,7 +29,6 @@ import binascii import os import sys -import microcontroller from .. import Service from ...uuid import StandardUUID @@ -65,11 +64,18 @@ def __init__( if model_number is None: model_number = sys.platform if serial_number is None: - serial_number = binascii.hexlify( - microcontroller.cpu.uid # pylint: disable=no-member - ).decode("utf-8") + try: + import microcontroller + serial_number = binascii.hexlify( + microcontroller.cpu.uid # pylint: disable=no-member + ).decode("utf-8") + except ImportError: + pass if firmware_revision is None: - firmware_revision = os.uname().version + try: + firmware_revision = os.uname().version + except AttributeError: + pass super().__init__( manufacturer=manufacturer, software_revision=software_revision, From e38b153dbcff10a72b7ab422882558e3a406e458 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Fri, 5 Jun 2020 23:28:20 -0400 Subject: [PATCH 2/9] increase connect default timeout; handle CPython descriptor protocol class arg --- adafruit_ble/__init__.py | 2 +- adafruit_ble/advertising/__init__.py | 2 ++ adafruit_ble/advertising/standard.py | 4 ++++ adafruit_ble/characteristics/__init__.py | 8 ++++++++ adafruit_ble/characteristics/float.py | 2 ++ adafruit_ble/characteristics/int.py | 2 ++ adafruit_ble/characteristics/string.py | 4 ++++ 7 files changed, 23 insertions(+), 1 deletion(-) diff --git a/adafruit_ble/__init__.py b/adafruit_ble/__init__.py index 0ceaeac..78f4abd 100755 --- a/adafruit_ble/__init__.py +++ b/adafruit_ble/__init__.py @@ -261,7 +261,7 @@ def stop_scan(self): once empty.""" self._adapter.stop_scan() - def connect(self, advertisement, *, timeout=4): + def connect(self, advertisement, *, timeout=10.0): """ Initiates a `BLEConnection` to the peer that advertised the given advertisement. diff --git a/adafruit_ble/advertising/__init__.py b/adafruit_ble/advertising/__init__.py index 0e7e489..b685318 100644 --- a/adafruit_ble/advertising/__init__.py +++ b/adafruit_ble/advertising/__init__.py @@ -103,6 +103,8 @@ def __init__(self, bit_position): self._bitmask = 1 << bit_position def __get__(self, obj, cls): + if obj is None: + return self return (obj.flags & self._bitmask) != 0 def __set__(self, obj, value): diff --git a/adafruit_ble/advertising/standard.py b/adafruit_ble/advertising/standard.py index d19c3ef..248989e 100644 --- a/adafruit_ble/advertising/standard.py +++ b/adafruit_ble/advertising/standard.py @@ -155,6 +155,8 @@ def _present(self, obj): return False def __get__(self, obj, cls): + if obj is None: + return self if not self._present(obj) and not obj.mutable: return () if not hasattr(obj, "adv_service_lists"): @@ -315,6 +317,8 @@ def __init__(self, service): self._prefix = bytes(service.uuid) def __get__(self, obj, cls): + if obj is None: + return self # If not present at all and mutable, then we init it, otherwise None. if self._adt not in obj.data_dict: if obj.mutable: diff --git a/adafruit_ble/characteristics/__init__.py b/adafruit_ble/characteristics/__init__.py index 6c3e481..fdf8894 100644 --- a/adafruit_ble/characteristics/__init__.py +++ b/adafruit_ble/characteristics/__init__.py @@ -151,6 +151,10 @@ def __bind_locally(self, service, initial_value): ) def __get__(self, service, cls=None): + # CircuitPython doesn't invoke descriptor protocol on obj's class, + # but CPython does. In the CPython case, pretend that it doesn't. + if service is None: + return self self._ensure_bound(service) bleio_characteristic = service.bleio_characteristics[self.field_name] return bleio_characteristic.value @@ -210,6 +214,8 @@ def bind(self, service): ) def __get__(self, service, cls=None): + if service is None: + return self bound_object = self.bind(service) setattr(service, self.field_name, bound_object) return bound_object @@ -253,6 +259,8 @@ def __init__( ) def __get__(self, obj, cls=None): + if obj is None: + return self raw_data = super().__get__(obj, cls) if len(raw_data) < self._expected_size: return None diff --git a/adafruit_ble/characteristics/float.py b/adafruit_ble/characteristics/float.py index eff39e1..5964e83 100644 --- a/adafruit_ble/characteristics/float.py +++ b/adafruit_ble/characteristics/float.py @@ -58,6 +58,8 @@ def __init__( ) def __get__(self, obj, cls=None): + if obj is None: + return self return super().__get__(obj)[0] def __set__(self, obj, value): diff --git a/adafruit_ble/characteristics/int.py b/adafruit_ble/characteristics/int.py index 303c2d3..46caf60 100755 --- a/adafruit_ble/characteristics/int.py +++ b/adafruit_ble/characteristics/int.py @@ -66,6 +66,8 @@ def __init__( ) def __get__(self, obj, cls=None): + if obj is None: + return self return super().__get__(obj)[0] def __set__(self, obj, value): diff --git a/adafruit_ble/characteristics/string.py b/adafruit_ble/characteristics/string.py index e4e7cfa..06569bc 100755 --- a/adafruit_ble/characteristics/string.py +++ b/adafruit_ble/characteristics/string.py @@ -57,6 +57,8 @@ def __init__( ) def __get__(self, obj, cls=None): + if obj is None: + return self return str(super().__get__(obj, cls), "utf-8") def __set__(self, obj, value): @@ -76,4 +78,6 @@ def __init__(self, *, uuid=None, read_perm=Attribute.OPEN): ) def __get__(self, obj, cls=None): + if obj is None: + return self return str(super().__get__(obj, cls), "utf-8") From f9f8b93dff8c7d3ebba1c4e7aee78e5703bd321b Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Sat, 6 Jun 2020 16:17:09 -0400 Subject: [PATCH 3/9] switch back to 4 seconds timeout; 10 seconds was a misunderstanding --- adafruit_ble/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_ble/__init__.py b/adafruit_ble/__init__.py index 78f4abd..75e6e81 100755 --- a/adafruit_ble/__init__.py +++ b/adafruit_ble/__init__.py @@ -261,7 +261,7 @@ def stop_scan(self): once empty.""" self._adapter.stop_scan() - def connect(self, advertisement, *, timeout=10.0): + def connect(self, advertisement, *, timeout=4.0): """ Initiates a `BLEConnection` to the peer that advertised the given advertisement. From 560f0b20bf20cc723b30fec797571546f657cd25 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 10 Jun 2020 15:23:14 -0400 Subject: [PATCH 4/9] clean up matches() --- adafruit_ble/advertising/__init__.py | 10 +++++++++- adafruit_ble/advertising/standard.py | 7 +++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/adafruit_ble/advertising/__init__.py b/adafruit_ble/advertising/__init__.py index b685318..75db614 100644 --- a/adafruit_ble/advertising/__init__.py +++ b/adafruit_ble/advertising/__init__.py @@ -304,7 +304,15 @@ def get_prefix_bytes(cls): return cls._prefix_bytes @classmethod - def matches(cls, entry, all_=True): + def matches(cls, entry): + """Returns ``True`` if the given `_bleio.ScanEntry` advertisement fields + matches all of the given prefixes in the `match_prefixes` tuple attribute. + Subclasses may override this to match any instead of all. + """ + return cls.matches_prefixes(entry, all_=True) + + @classmethod + def matches_prefixes(cls, entry, *, all_): """Returns ``True`` if the given `_bleio.ScanEntry` advertisement fields match any or all of the given prefixes in the `match_prefixes` tuple attribute. If `all_` is ``True``, all the prefixes must match. If ``all_`` is ``False``, diff --git a/adafruit_ble/advertising/standard.py b/adafruit_ble/advertising/standard.py index 248989e..21e9885 100644 --- a/adafruit_ble/advertising/standard.py +++ b/adafruit_ble/advertising/standard.py @@ -184,8 +184,11 @@ def __init__(self, *services): self.flags.le_only = True @classmethod - def matches(cls, entry, all_=False): - return super().matches(entry, all_=all_) + def matches(cls, entry): + """Only one kind of service list need be present in a ProvideServicesAdvertisement, + so override the default behavior and match any prefix, not all. + """ + return cls.matches_prefixes(entry, all_=False) class SolicitServicesAdvertisement(Advertisement): From f466835b4d1e373a989cdb5a9b46ca18540663c4 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 10 Jun 2020 16:33:05 -0400 Subject: [PATCH 5/9] black reformatting --- adafruit_ble/services/standard/device_info.py | 1 + 1 file changed, 1 insertion(+) diff --git a/adafruit_ble/services/standard/device_info.py b/adafruit_ble/services/standard/device_info.py index 5a49c8d..fb5767a 100644 --- a/adafruit_ble/services/standard/device_info.py +++ b/adafruit_ble/services/standard/device_info.py @@ -66,6 +66,7 @@ def __init__( if serial_number is None: try: import microcontroller + serial_number = binascii.hexlify( microcontroller.cpu.uid # pylint: disable=no-member ).decode("utf-8") From 9efa23a823df695421dcc766832f397a236ac1f0 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 10 Jun 2020 16:54:59 -0400 Subject: [PATCH 6/9] pylint --- adafruit_ble/advertising/standard.py | 4 +++- adafruit_ble/services/standard/device_info.py | 2 +- adafruit_ble/services/standard/hid.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/adafruit_ble/advertising/standard.py b/adafruit_ble/advertising/standard.py index 21e9885..4259be0 100644 --- a/adafruit_ble/advertising/standard.py +++ b/adafruit_ble/advertising/standard.py @@ -319,7 +319,9 @@ def __init__(self, service): self._adt = 0x21 self._prefix = bytes(service.uuid) - def __get__(self, obj, cls): + def __get__( + self, obj, cls + ): # pylint: disable=too-many-return-statements,too-many-branches if obj is None: return self # If not present at all and mutable, then we init it, otherwise None. diff --git a/adafruit_ble/services/standard/device_info.py b/adafruit_ble/services/standard/device_info.py index fb5767a..7f6efd5 100644 --- a/adafruit_ble/services/standard/device_info.py +++ b/adafruit_ble/services/standard/device_info.py @@ -65,7 +65,7 @@ def __init__( model_number = sys.platform if serial_number is None: try: - import microcontroller + import microcontroller # pylint: disable=import-outside-toplevel serial_number = binascii.hexlify( microcontroller.cpu.uid # pylint: disable=no-member diff --git a/adafruit_ble/services/standard/hid.py b/adafruit_ble/services/standard/hid.py index 668b0d6..4149992 100755 --- a/adafruit_ble/services/standard/hid.py +++ b/adafruit_ble/services/standard/hid.py @@ -30,8 +30,8 @@ """ import struct -import _bleio from micropython import const +import _bleio from adafruit_ble.characteristics import Attribute from adafruit_ble.characteristics import Characteristic From 674d10d7801321815de046af31d2491a94a744b9 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 10 Jun 2020 20:59:21 -0400 Subject: [PATCH 7/9] sphinx --- adafruit_ble/advertising/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_ble/advertising/__init__.py b/adafruit_ble/advertising/__init__.py index 75db614..5090a89 100644 --- a/adafruit_ble/advertising/__init__.py +++ b/adafruit_ble/advertising/__init__.py @@ -315,7 +315,7 @@ def matches(cls, entry): def matches_prefixes(cls, entry, *, all_): """Returns ``True`` if the given `_bleio.ScanEntry` advertisement fields match any or all of the given prefixes in the `match_prefixes` tuple attribute. - If `all_` is ``True``, all the prefixes must match. If ``all_`` is ``False``, + If ``all_`` is ``True``, all the prefixes must match. If ``all_`` is ``False``, returns ``True`` if at least one of the prefixes match. """ # Returns True if cls.get_prefix_bytes() is empty. From 4c6dbb8f1f8fad00b5853a091356ab7733dec8c8 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Thu, 11 Jun 2020 15:42:27 -0400 Subject: [PATCH 8/9] Update adafruit_ble/services/standard/device_info.py Co-authored-by: Scott Shawcroft --- adafruit_ble/services/standard/device_info.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/adafruit_ble/services/standard/device_info.py b/adafruit_ble/services/standard/device_info.py index 7f6efd5..e77910b 100644 --- a/adafruit_ble/services/standard/device_info.py +++ b/adafruit_ble/services/standard/device_info.py @@ -73,10 +73,7 @@ def __init__( except ImportError: pass if firmware_revision is None: - try: - firmware_revision = os.uname().version - except AttributeError: - pass + firmware_revision = getattr(os.uname(), "version", None) super().__init__( manufacturer=manufacturer, software_revision=software_revision, From 4d86f7b518cdc14678cfbc8b2a266ad9f35718aa Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Fri, 12 Jun 2020 15:18:17 -0400 Subject: [PATCH 9/9] allow use for now of deprecated Advertisement.prefix attribute --- adafruit_ble/advertising/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/adafruit_ble/advertising/__init__.py b/adafruit_ble/advertising/__init__.py index 5090a89..6082015 100644 --- a/adafruit_ble/advertising/__init__.py +++ b/adafruit_ble/advertising/__init__.py @@ -290,6 +290,8 @@ def get_prefix_bytes(cls): """Return a merged version of match_prefixes as a single bytes object, with length headers. """ + # Check for deprecated `prefix` class attribute. + cls._prefix_bytes = getattr(cls, "prefix", None) # Do merge once and memoize it. if cls._prefix_bytes is None: cls._prefix_bytes = (