diff --git a/adafruit_ble/__init__.py b/adafruit_ble/__init__.py index 10ce0fb..75e6e81 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, @@ -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=4.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 649901d..6082015 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): @@ -214,9 +216,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 +267,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 @@ -271,14 +285,43 @@ def rssi(self): from `BLERadio.start_scan()`. (read-only)""" return self._rssi + @classmethod + 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 = ( + 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 + @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 + """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) - return entry.matches(cls.prefix) + @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``, + 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): @@ -155,8 +155,10 @@ 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 None + return () if not hasattr(obj, "adv_service_lists"): obj.adv_service_lists = {} first_adt = self.standard_services[0] @@ -168,8 +170,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.""" @@ -183,14 +185,17 @@ def __init__(self, *services): @classmethod def matches(cls, entry): - return entry.matches(cls.prefix, all=False) + """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): """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.""" @@ -314,7 +319,11 @@ 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. 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") diff --git a/adafruit_ble/services/standard/device_info.py b/adafruit_ble/services/standard/device_info.py index 7b631da..e77910b 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,16 @@ 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 # pylint: disable=import-outside-toplevel + + 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 + firmware_revision = getattr(os.uname(), "version", None) super().__init__( manufacturer=manufacturer, software_revision=software_revision, 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