Skip to content

Easier specification of prefixes; fixes for use with bleak #89

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 16, 2020
4 changes: 2 additions & 2 deletions adafruit_ble/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.

Expand Down
59 changes: 51 additions & 8 deletions adafruit_ble/advertising/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)."""
Expand Down Expand Up @@ -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
Expand All @@ -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."""
Expand Down
17 changes: 9 additions & 8 deletions adafruit_ble/advertising/adafruit.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,15 @@
class AdafruitColor(Advertisement):
"""Broadcast a single RGB color."""

# This prefix matches all
prefix = struct.pack(
"<BBHBH",
0x6,
_MANUFACTURING_DATA_ADT,
_ADAFRUIT_COMPANY_ID,
struct.calcsize("<HI"),
_COLOR_DATA_ID,
# This single prefix matches all color advertisements.
match_prefixes = (
struct.pack(
"<BHBH",
_MANUFACTURING_DATA_ADT,
_ADAFRUIT_COMPANY_ID,
struct.calcsize("<HI"),
_COLOR_DATA_ID,
),
)
manufacturer_data = LazyObjectField(
ManufacturerData,
Expand Down
3 changes: 2 additions & 1 deletion adafruit_ble/advertising/apple.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# class iBeacon(Advertisement):
# prefix = b"\xff\x00\x4c\x02" # Apple manufacturer data with subtype 2
# # Apple manufacturer data with subtype 2
# match_prefixes = (b"\xff\x00\x4c\x02",)
#
# proximity_uuid =
# major =
Expand Down
25 changes: 17 additions & 8 deletions adafruit_ble/advertising/standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def __str__(self):
data.append(str(service_uuid))
for service_uuid in self._vendor_services:
data.append(str(service_uuid))
return " ".join(data)
return "<BoundServiceList: {}>".format(", ".join(data))


class ServiceList(AdvertisingDataField):
Expand All @@ -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]
Expand All @@ -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."""

Expand All @@ -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."""
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions adafruit_ble/characteristics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions adafruit_ble/characteristics/float.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions adafruit_ble/characteristics/int.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 4 additions & 0 deletions adafruit_ble/characteristics/string.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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")
14 changes: 9 additions & 5 deletions adafruit_ble/services/standard/device_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import binascii
import os
import sys
import microcontroller

from .. import Service
from ...uuid import StandardUUID
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion adafruit_ble/services/standard/hid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down