Skip to content

Commit 7dcb774

Browse files
authored
Merge pull request #89 from dhalbert/easier-prefixes
Easier specification of prefixes; fixes for use with bleak
2 parents 3585131 + 4d86f7b commit 7dcb774

File tree

11 files changed

+107
-33
lines changed

11 files changed

+107
-33
lines changed

adafruit_ble/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ def start_scan(
231231
"""
232232
if not advertisement_types:
233233
advertisement_types = (Advertisement,)
234-
prefixes = b"".join(adv.prefix for adv in advertisement_types)
234+
prefixes = b"".join(adv.get_prefix_bytes() for adv in advertisement_types)
235235
for entry in self._adapter.start_scan(
236236
prefixes=prefixes,
237237
buffer_size=buffer_size,
@@ -261,7 +261,7 @@ def stop_scan(self):
261261
once empty."""
262262
self._adapter.stop_scan()
263263

264-
def connect(self, advertisement, *, timeout=4):
264+
def connect(self, advertisement, *, timeout=4.0):
265265
"""
266266
Initiates a `BLEConnection` to the peer that advertised the given advertisement.
267267

adafruit_ble/advertising/__init__.py

+51-8
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ def __init__(self, bit_position):
103103
self._bitmask = 1 << bit_position
104104

105105
def __get__(self, obj, cls):
106+
if obj is None:
107+
return self
106108
return (obj.flags & self._bitmask) != 0
107109

108110
def __set__(self, obj, value):
@@ -214,9 +216,17 @@ def advertising_data_type(self):
214216

215217

216218
class Advertisement:
217-
"""Core Advertisement type"""
219+
"""Core Advertisement type.
220+
221+
The class attribute ``match_prefixes``, if not ``None``, is a tuple of
222+
bytestring prefixes to match against the multiple data structures in the advertisement.
223+
"""
224+
225+
match_prefixes = ()
226+
"""For Advertisement, `matches` will always return True. Subclasses may override this value."""
227+
# cached bytes of merged prefixes.
228+
_prefix_bytes = None
218229

219-
prefix = b"\x00" # This is an empty prefix and will match everything.
220230
flags = LazyObjectField(AdvertisingFlags, "flags", advertising_data_type=0x01)
221231
short_name = String(advertising_data_type=0x08)
222232
"""Short local device name (shortened to fit)."""
@@ -257,7 +267,11 @@ def from_entry(cls, entry):
257267
"""Create an Advertisement based on the given ScanEntry. This is done automatically by
258268
`BLERadio` for all scan results."""
259269
self = cls()
260-
self.data_dict = decode_data(entry.advertisement_bytes)
270+
# If data_dict is available, use it directly. Otherwise decode the bytestring.
271+
if hasattr(entry, "data_dict"):
272+
self.data_dict = entry.data_dict
273+
else:
274+
self.data_dict = decode_data(entry.advertisement_bytes)
261275
self.address = entry.address
262276
self._rssi = entry.rssi # pylint: disable=protected-access
263277
self.connectable = entry.connectable
@@ -271,14 +285,43 @@ def rssi(self):
271285
from `BLERadio.start_scan()`. (read-only)"""
272286
return self._rssi
273287

288+
@classmethod
289+
def get_prefix_bytes(cls):
290+
"""Return a merged version of match_prefixes as a single bytes object,
291+
with length headers.
292+
"""
293+
# Check for deprecated `prefix` class attribute.
294+
cls._prefix_bytes = getattr(cls, "prefix", None)
295+
# Do merge once and memoize it.
296+
if cls._prefix_bytes is None:
297+
cls._prefix_bytes = (
298+
b""
299+
if cls.match_prefixes is None
300+
else b"".join(
301+
len(prefix).to_bytes(1, "little") + prefix
302+
for prefix in cls.match_prefixes
303+
)
304+
)
305+
306+
return cls._prefix_bytes
307+
274308
@classmethod
275309
def matches(cls, entry):
276-
"""Returns true if the given `_bleio.ScanEntry` matches all portions of the Advertisement
277-
type's prefix."""
278-
if not hasattr(cls, "prefix"):
279-
return True
310+
"""Returns ``True`` if the given `_bleio.ScanEntry` advertisement fields
311+
matches all of the given prefixes in the `match_prefixes` tuple attribute.
312+
Subclasses may override this to match any instead of all.
313+
"""
314+
return cls.matches_prefixes(entry, all_=True)
280315

281-
return entry.matches(cls.prefix)
316+
@classmethod
317+
def matches_prefixes(cls, entry, *, all_):
318+
"""Returns ``True`` if the given `_bleio.ScanEntry` advertisement fields
319+
match any or all of the given prefixes in the `match_prefixes` tuple attribute.
320+
If ``all_`` is ``True``, all the prefixes must match. If ``all_`` is ``False``,
321+
returns ``True`` if at least one of the prefixes match.
322+
"""
323+
# Returns True if cls.get_prefix_bytes() is empty.
324+
return entry.matches(cls.get_prefix_bytes(), all=all_)
282325

283326
def __bytes__(self):
284327
"""The raw packet bytes."""

adafruit_ble/advertising/adafruit.py

+9-8
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,15 @@
4848
class AdafruitColor(Advertisement):
4949
"""Broadcast a single RGB color."""
5050

51-
# This prefix matches all
52-
prefix = struct.pack(
53-
"<BBHBH",
54-
0x6,
55-
_MANUFACTURING_DATA_ADT,
56-
_ADAFRUIT_COMPANY_ID,
57-
struct.calcsize("<HI"),
58-
_COLOR_DATA_ID,
51+
# This single prefix matches all color advertisements.
52+
match_prefixes = (
53+
struct.pack(
54+
"<BHBH",
55+
_MANUFACTURING_DATA_ADT,
56+
_ADAFRUIT_COMPANY_ID,
57+
struct.calcsize("<HI"),
58+
_COLOR_DATA_ID,
59+
),
5960
)
6061
manufacturer_data = LazyObjectField(
6162
ManufacturerData,

adafruit_ble/advertising/apple.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# class iBeacon(Advertisement):
2-
# prefix = b"\xff\x00\x4c\x02" # Apple manufacturer data with subtype 2
2+
# # Apple manufacturer data with subtype 2
3+
# match_prefixes = (b"\xff\x00\x4c\x02",)
34
#
45
# proximity_uuid =
56
# major =

adafruit_ble/advertising/standard.py

+17-8
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def __str__(self):
135135
data.append(str(service_uuid))
136136
for service_uuid in self._vendor_services:
137137
data.append(str(service_uuid))
138-
return " ".join(data)
138+
return "<BoundServiceList: {}>".format(", ".join(data))
139139

140140

141141
class ServiceList(AdvertisingDataField):
@@ -155,8 +155,10 @@ def _present(self, obj):
155155
return False
156156

157157
def __get__(self, obj, cls):
158+
if obj is None:
159+
return self
158160
if not self._present(obj) and not obj.mutable:
159-
return None
161+
return ()
160162
if not hasattr(obj, "adv_service_lists"):
161163
obj.adv_service_lists = {}
162164
first_adt = self.standard_services[0]
@@ -168,8 +170,8 @@ def __get__(self, obj, cls):
168170
class ProvideServicesAdvertisement(Advertisement):
169171
"""Advertise what services that the device makes available upon connection."""
170172

171-
# This is four prefixes, one for each ADT that can carry service UUIDs.
172-
prefix = b"\x01\x02\x01\x03\x01\x06\x01\x07"
173+
# Prefixes that match each ADT that can carry service UUIDs.
174+
match_prefixes = (b"\x02", b"\x03", b"\x06", b"\x07")
173175
services = ServiceList(standard_services=[0x02, 0x03], vendor_services=[0x06, 0x07])
174176
"""List of services the device can provide."""
175177

@@ -183,14 +185,17 @@ def __init__(self, *services):
183185

184186
@classmethod
185187
def matches(cls, entry):
186-
return entry.matches(cls.prefix, all=False)
188+
"""Only one kind of service list need be present in a ProvideServicesAdvertisement,
189+
so override the default behavior and match any prefix, not all.
190+
"""
191+
return cls.matches_prefixes(entry, all_=False)
187192

188193

189194
class SolicitServicesAdvertisement(Advertisement):
190195
"""Advertise what services the device would like to use over a connection."""
191196

192-
# This is two prefixes, one for each ADT that can carry solicited service UUIDs.
193-
prefix = b"\x01\x14\x01\x15"
197+
# Prefixes that match each ADT that can carry solicited service UUIDs.
198+
match_prefixes = (b"\x14", b"\x15")
194199

195200
solicited_services = ServiceList(standard_services=[0x14], vendor_services=[0x15])
196201
"""List of services the device would like to use."""
@@ -314,7 +319,11 @@ def __init__(self, service):
314319
self._adt = 0x21
315320
self._prefix = bytes(service.uuid)
316321

317-
def __get__(self, obj, cls):
322+
def __get__(
323+
self, obj, cls
324+
): # pylint: disable=too-many-return-statements,too-many-branches
325+
if obj is None:
326+
return self
318327
# If not present at all and mutable, then we init it, otherwise None.
319328
if self._adt not in obj.data_dict:
320329
if obj.mutable:

adafruit_ble/characteristics/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ def __bind_locally(self, service, initial_value):
151151
)
152152

153153
def __get__(self, service, cls=None):
154+
# CircuitPython doesn't invoke descriptor protocol on obj's class,
155+
# but CPython does. In the CPython case, pretend that it doesn't.
156+
if service is None:
157+
return self
154158
self._ensure_bound(service)
155159
bleio_characteristic = service.bleio_characteristics[self.field_name]
156160
return bleio_characteristic.value
@@ -210,6 +214,8 @@ def bind(self, service):
210214
)
211215

212216
def __get__(self, service, cls=None):
217+
if service is None:
218+
return self
213219
bound_object = self.bind(service)
214220
setattr(service, self.field_name, bound_object)
215221
return bound_object
@@ -253,6 +259,8 @@ def __init__(
253259
)
254260

255261
def __get__(self, obj, cls=None):
262+
if obj is None:
263+
return self
256264
raw_data = super().__get__(obj, cls)
257265
if len(raw_data) < self._expected_size:
258266
return None

adafruit_ble/characteristics/float.py

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ def __init__(
5858
)
5959

6060
def __get__(self, obj, cls=None):
61+
if obj is None:
62+
return self
6163
return super().__get__(obj)[0]
6264

6365
def __set__(self, obj, value):

adafruit_ble/characteristics/int.py

+2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ def __init__(
6666
)
6767

6868
def __get__(self, obj, cls=None):
69+
if obj is None:
70+
return self
6971
return super().__get__(obj)[0]
7072

7173
def __set__(self, obj, value):

adafruit_ble/characteristics/string.py

+4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ def __init__(
5757
)
5858

5959
def __get__(self, obj, cls=None):
60+
if obj is None:
61+
return self
6062
return str(super().__get__(obj, cls), "utf-8")
6163

6264
def __set__(self, obj, value):
@@ -76,4 +78,6 @@ def __init__(self, *, uuid=None, read_perm=Attribute.OPEN):
7678
)
7779

7880
def __get__(self, obj, cls=None):
81+
if obj is None:
82+
return self
7983
return str(super().__get__(obj, cls), "utf-8")

adafruit_ble/services/standard/device_info.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import binascii
3030
import os
3131
import sys
32-
import microcontroller
3332

3433
from .. import Service
3534
from ...uuid import StandardUUID
@@ -65,11 +64,16 @@ def __init__(
6564
if model_number is None:
6665
model_number = sys.platform
6766
if serial_number is None:
68-
serial_number = binascii.hexlify(
69-
microcontroller.cpu.uid # pylint: disable=no-member
70-
).decode("utf-8")
67+
try:
68+
import microcontroller # pylint: disable=import-outside-toplevel
69+
70+
serial_number = binascii.hexlify(
71+
microcontroller.cpu.uid # pylint: disable=no-member
72+
).decode("utf-8")
73+
except ImportError:
74+
pass
7175
if firmware_revision is None:
72-
firmware_revision = os.uname().version
76+
firmware_revision = getattr(os.uname(), "version", None)
7377
super().__init__(
7478
manufacturer=manufacturer,
7579
software_revision=software_revision,

adafruit_ble/services/standard/hid.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
"""
3131
import struct
3232

33-
import _bleio
3433
from micropython import const
34+
import _bleio
3535

3636
from adafruit_ble.characteristics import Attribute
3737
from adafruit_ble.characteristics import Characteristic

0 commit comments

Comments
 (0)