Skip to content

Commit 3434bfa

Browse files
committed
Tweaks based on feedback from Dan and Thea
This renames Smart* to BLE* and removes the smart recognition. It is replaced by knowing the type of what we're interested at use time only. Only printing Service lists is now dumber. Interal variables to _bleio classes are now public as bleio_* instead so that other classes in the library can access them and its clearer what they are.
1 parent 8e0d39b commit 3434bfa

36 files changed

+658
-612
lines changed

adafruit_ble/__init__.py

Lines changed: 65 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -46,75 +46,61 @@
4646
import _bleio
4747
import board
4848

49-
from .services.core import Service
49+
from .services import Service
5050
from .advertising import Advertisement
5151

5252
__version__ = "0.0.0-auto.0"
5353
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git"
5454

55-
# These are internal data structures used throughout the library to recognize certain Services and
56-
# Advertisements.
57-
# pylint: disable=invalid-name
58-
all_services_by_name = {}
59-
all_services_by_uuid = {}
60-
known_advertisements = set()
61-
# pylint: enable=invalid-name
62-
63-
def recognize_services(*service_classes):
64-
"""Instruct the adafruit_ble library to recognize the given Services.
65-
66-
This will cause the Service related advertisements to show the corresponding class.
67-
`SmartConnection` will automatically have attributes for any recognized service available
68-
from the peer."""
69-
for service_class in service_classes:
70-
if not issubclass(service_class, Service):
71-
raise ValueError("Can only detect subclasses of Service")
72-
all_services_by_name[service_class.default_field_name] = service_class
73-
all_services_by_uuid[service_class.uuid] = service_class
74-
75-
def recognize_advertisement(*advertisements):
76-
"""Instruct the adafruit_ble library to recognize the given `Advertisement` types.
77-
78-
When an advertisement is recognized by the `SmartAdapter`, it will be returned from the
79-
start_scan iterator instead of a generic `Advertisement`."""
80-
known_advertisements.add(*advertisements)
81-
82-
class SmartConnection:
55+
class BLEConnection:
8356
"""This represents a connection to a peer BLE device.
8457
85-
Its smarts come from its ability to recognize Services available on the peer and make them
86-
available as attributes on the Connection. Use `recognize_services` to register all services
87-
of interest. All subsequent Connections will then recognize the service.
88-
89-
``dir(connection)`` will show all attributes including recognized Services.
90-
"""
58+
It acts as a map from a Service type to a Service instance for the connection.
59+
"""
9160
def __init__(self, connection):
9261
self._connection = connection
93-
94-
def __dir__(self):
95-
discovered = []
96-
results = self._connection.discover_remote_services()
97-
for service in results:
98-
uuid = service.uuid
99-
if uuid in all_services_by_uuid:
100-
service = all_services_by_uuid[uuid]
101-
discovered.append(service.default_field_name)
102-
super_dir = dir(super())
103-
super_dir.extend(discovered)
104-
return super_dir
105-
106-
def __getattr__(self, name):
107-
if name in self.__dict__:
108-
return self.__dict__[name]
109-
if name in all_services_by_name:
110-
service = all_services_by_name[name]
111-
uuid = service.uuid._uuid
112-
results = self._connection.discover_remote_services((uuid,))
62+
self._discovered_services = {}
63+
"""These are the bare remote services from _bleio."""
64+
65+
self._constructed_services = {}
66+
"""These are the Service instances from the library that wrap the remote services."""
67+
68+
def _discover_remote(self, uuid):
69+
remote_service = None
70+
if uuid in self._discovered_services:
71+
remote_service = self._discovered_services[uuid]
72+
else:
73+
results = self._connection.discover_remote_services((uuid.bleio_uuid,))
11374
if results:
114-
remote_service = service(service=results[0])
115-
setattr(self, name, remote_service)
116-
return remote_service
117-
raise AttributeError()
75+
remote_service = results[0]
76+
self._discovered_services[uuid] = remote_service
77+
return remote_service
78+
79+
def __contains__(self, key):
80+
uuid = key
81+
if hasattr(key, "uuid"):
82+
uuid = key.uuid
83+
return self._discover_remote(uuid) is not None
84+
85+
def __getitem__(self, key):
86+
uuid = key
87+
maybe_service = False
88+
if hasattr(key, "uuid"):
89+
uuid = key.uuid
90+
maybe_service = True
91+
92+
remote_service = self._discover_remote(uuid)
93+
94+
if uuid in self._constructed_services:
95+
return self._constructed_services[uuid]
96+
if remote_service:
97+
constructed_service = None
98+
if maybe_service:
99+
constructed_service = key(service=remote_service)
100+
self._constructed_services[uuid] = constructed_service
101+
return constructed_service
102+
103+
raise KeyError("{!r} object has no service {}".format(self, key))
118104

119105
@property
120106
def connected(self):
@@ -125,10 +111,11 @@ def disconnect(self):
125111
"""Disconnect from peer."""
126112
self._connection.disconnect()
127113

128-
class SmartAdapter:
129-
"""This BLE Adapter class enhances the normal `_bleio.Adapter`.
114+
class BLERadio:
115+
"""The BLERadio class enhances the normal `_bleio.Adapter`.
116+
117+
It uses the library's `Advertisement` classes and the `BLEConnection` class."""
130118

131-
It uses the library's `Advertisement` classes and the `SmartConnection` class."""
132119
def __init__(self, adapter=None):
133120
if not adapter:
134121
adapter = _bleio.adapter
@@ -143,7 +130,6 @@ def start_advertising(self, advertisement, scan_response=None, **kwargs):
143130
scan_response_data = None
144131
if scan_response:
145132
scan_response_data = bytes(scan_response)
146-
print(advertisement.connectable)
147133
self._adapter.start_advertising(bytes(advertisement),
148134
scan_response=scan_response_data,
149135
connectable=advertisement.connectable,
@@ -153,21 +139,20 @@ def stop_advertising(self):
153139
"""Stops advertising."""
154140
self._adapter.stop_advertising()
155141

156-
def start_scan(self, advertisement_types=None, **kwargs):
157-
"""Starts scanning. Returns an iterator of Advertisements that are either recognized or
158-
in advertisment_types (which will be subsequently recognized.) The iterator will block
159-
until an advertisement is heard or the scan times out.
142+
def start_scan(self, *advertisement_types, **kwargs):
143+
"""Starts scanning. Returns an iterator of advertisement objects of the types given in
144+
advertisement_types. The iterator will block until an advertisement is heard or the scan
145+
times out.
160146
161-
If a list ``advertisement_types`` is given, only Advertisements of that type are produced
162-
by the returned iterator."""
147+
If any ``advertisement_types`` are given, only Advertisements of those types are produced
148+
by the returned iterator. If none are given then `Advertisement` objects will be
149+
returned."""
163150
prefixes = b""
164151
if advertisement_types:
165-
recognize_advertisement(*advertisement_types)
166-
if len(advertisement_types) == 1:
167-
prefixes = advertisement_types[0].prefix
152+
prefixes = b"".join(adv.prefix for adv in advertisement_types)
168153
for entry in self._adapter.start_scan(prefixes=prefixes, **kwargs):
169154
adv_type = Advertisement
170-
for possible_type in known_advertisements:
155+
for possible_type in advertisement_types:
171156
if possible_type.matches(entry) and issubclass(possible_type, adv_type):
172157
adv_type = possible_type
173158
advertisement = adv_type.from_entry(entry)
@@ -182,9 +167,9 @@ def stop_scan(self):
182167
self._adapter.stop_scan()
183168

184169
def connect(self, advertisement, *, timeout=4):
185-
"""Initiates a `SmartConnection` to the peer that advertised the given advertisement."""
170+
"""Initiates a `BLEConnection` to the peer that advertised the given advertisement."""
186171
connection = self._adapter.connect(advertisement.address, timeout=timeout)
187-
self._connection_cache[connection] = SmartConnection(connection)
172+
self._connection_cache[connection] = BLEConnection(connection)
188173
return self._connection_cache[connection]
189174

190175
@property
@@ -194,12 +179,12 @@ def connected(self):
194179

195180
@property
196181
def connections(self):
197-
"""A tuple of active `SmartConnection` objects."""
182+
"""A tuple of active `BLEConnection` objects."""
198183
connections = self._adapter.connections
199-
smart_connections = [None] * len(connections)
184+
wrapped_connections = [None] * len(connections)
200185
for i, connection in enumerate(self._adapter.connections):
201186
if connection not in self._connection_cache:
202-
self._connection_cache[connection] = SmartConnection(connection)
203-
smart_connections[i] = self._connection_cache[connection]
187+
self._connection_cache[connection] = BLEConnection(connection)
188+
wrapped_connections[i] = self._connection_cache[connection]
204189

205-
return tuple(smart_connections)
190+
return tuple(wrapped_connections)

adafruit_ble/advertising/__init__.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
"""
2525

2626
import struct
27-
import gc
2827

2928
def to_hex(b):
3029
"""Pretty prints a byte sequence as hex values."""
@@ -41,9 +40,6 @@ def decode_data(data, *, key_encoding="B"):
4140
encoding."""
4241
i = 0
4342
data_dict = {}
44-
if len(data) > 255:
45-
print("original", data)
46-
raise RuntimeError()
4743
key_size = struct.calcsize(key_encoding)
4844
while i < len(data):
4945
item_length = data[i]
@@ -128,6 +124,9 @@ def __init__(self, advertisement, advertising_data_type):
128124
else:
129125
self.flags = 0
130126

127+
def __len__(self):
128+
return 1
129+
131130
def __bytes__(self):
132131
encoded = bytearray(1)
133132
encoded[0] = self.flags
@@ -185,7 +184,6 @@ def __get__(self, obj, cls):
185184
# Return None if our object is immutable and the data is not present.
186185
if not obj.mutable and self._adt not in obj.data_dict:
187186
return None
188-
print(self._adt, self._cls, repr(obj))
189187
bound_class = self._cls(obj, advertising_data_type=self._adt, **self._kwargs)
190188
setattr(obj, self._attribute_name, bound_class)
191189
obj.data_dict[self._adt] = bound_class
@@ -196,7 +194,7 @@ def __get__(self, obj, cls):
196194

197195
class Advertisement:
198196
"""Core Advertisement type"""
199-
prefix = b"\x00"
197+
prefix = b"\x00" # This is an empty prefix and will match everything.
200198
flags = LazyField(AdvertisingFlags, "flags", advertising_data_type=0x01)
201199
short_name = String(advertising_data_type=0x08)
202200
"""Short local device name (shortened to fit)."""
@@ -240,7 +238,7 @@ def __init__(self):
240238
@classmethod
241239
def from_entry(cls, entry):
242240
"""Create an Advertisement based on the given ScanEntry. This is done automatically by
243-
`SmartAdapter` for all scan results."""
241+
`BLERadio` for all scan results."""
244242
self = cls()
245243
self.data_dict = decode_data(entry.advertisement_bytes)
246244
self.address = entry.address
@@ -258,10 +256,10 @@ def rssi(self):
258256

259257
@classmethod
260258
def matches(cls, entry):
261-
"""Returns true if the given `_bleio.ScanEntry` matches all portions of the Advertisement type's
262-
prefix."""
259+
"""Returns true if the given `_bleio.ScanEntry` matches all portions of the Advertisement
260+
type's prefix."""
263261
if not hasattr(cls, "prefix"):
264-
return False
262+
return True
265263

266264
return entry.matches(cls.prefix)
267265

adafruit_ble/advertising/adafruit.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,35 @@
3131
3232
"""
3333

34+
import struct
35+
from micropython import const
36+
3437
from . import Advertisement, LazyField
3538
from .standard import ManufacturerData, ManufacturerDataField
3639

3740
__version__ = "0.0.0-auto.0"
3841
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git"
3942

43+
_MANUFACTURING_DATA_ADT = const(0xff)
44+
_ADAFRUIT_COMPANY_ID = const(0x0822)
45+
_COLOR_DATA_ID = const(0x0000)
46+
4047
class AdafruitColor(Advertisement):
4148
"""Broadcast a single RGB color."""
42-
prefix = b"\x06\xff\xff\xff\x06\x00\x00"
49+
# This prefix matches all
50+
prefix = struct.pack("<BBHBH",
51+
0x6,
52+
_MANUFACTURING_DATA_ADT,
53+
_ADAFRUIT_COMPANY_ID,
54+
struct.calcsize("<HI"),
55+
_COLOR_DATA_ID)
4356
manufacturer_data = LazyField(ManufacturerData,
4457
"manufacturer_data",
45-
advertising_data_type=0xff,
46-
company_id=0x0822,
58+
advertising_data_type=_MANUFACTURING_DATA_ADT,
59+
company_id=_ADAFRUIT_COMPANY_ID,
4760
key_encoding="<H")
48-
color = ManufacturerDataField(0x0000, "<I")
61+
color = ManufacturerDataField(_COLOR_DATA_ID, "<I")
62+
"""Color to broadcast as RGB integer."""
4963

5064
# TODO: Add radio packets.
5165
#

0 commit comments

Comments
 (0)