|
1 | 1 | # The MIT License (MIT)
|
2 | 2 | #
|
3 | 3 | # Copyright (c) 2019 Dan Halbert for Adafruit Industries
|
| 4 | +# Copyright (c) 2019 Scott Shawcroft for Adafruit Industries |
4 | 5 | #
|
5 | 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 | 7 | # of this software and associated documentation files (the "Software"), to deal
|
|
26 | 27 | This module provides higher-level BLE (Bluetooth Low Energy) functionality,
|
27 | 28 | building on the native `_bleio` module.
|
28 | 29 |
|
29 |
| -* Author(s): Dan Halbert for Adafruit Industries |
| 30 | +* Author(s): Dan Halbert and Scott Shawcroft for Adafruit Industries |
30 | 31 |
|
31 | 32 | Implementation Notes
|
32 | 33 | --------------------
|
|
42 | 43 |
|
43 | 44 | """
|
44 | 45 |
|
45 |
| -# imports |
| 46 | +import board |
| 47 | +import _bleio |
| 48 | + |
| 49 | +from .services import Service |
| 50 | +from .advertising import Advertisement |
46 | 51 |
|
47 | 52 | __version__ = "0.0.0-auto.0"
|
48 | 53 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE.git"
|
| 54 | + |
| 55 | +class BLEConnection: |
| 56 | + """This represents a connection to a peer BLE device. |
| 57 | +
|
| 58 | + It acts as a map from a Service type to a Service instance for the connection. |
| 59 | + """ |
| 60 | + def __init__(self, connection): |
| 61 | + self._connection = connection |
| 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,)) |
| 74 | + if results: |
| 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)) |
| 104 | + |
| 105 | + @property |
| 106 | + def connected(self): |
| 107 | + """True if the connection to the peer is still active.""" |
| 108 | + return self._connection.connected |
| 109 | + |
| 110 | + def disconnect(self): |
| 111 | + """Disconnect from peer.""" |
| 112 | + self._connection.disconnect() |
| 113 | + |
| 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.""" |
| 118 | + |
| 119 | + def __init__(self, adapter=None): |
| 120 | + if not adapter: |
| 121 | + adapter = _bleio.adapter |
| 122 | + self._adapter = adapter |
| 123 | + self._current_advertisement = None |
| 124 | + self._connection_cache = {} |
| 125 | + |
| 126 | + def start_advertising(self, advertisement, scan_response=None, **kwargs): |
| 127 | + """Starts advertising the given advertisement. |
| 128 | +
|
| 129 | + It takes most kwargs of `_bleio.Adapter.start_advertising`.""" |
| 130 | + scan_response_data = None |
| 131 | + if scan_response: |
| 132 | + scan_response_data = bytes(scan_response) |
| 133 | + self._adapter.start_advertising(bytes(advertisement), |
| 134 | + scan_response=scan_response_data, |
| 135 | + connectable=advertisement.connectable, |
| 136 | + **kwargs) |
| 137 | + |
| 138 | + def stop_advertising(self): |
| 139 | + """Stops advertising.""" |
| 140 | + self._adapter.stop_advertising() |
| 141 | + |
| 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. |
| 146 | +
|
| 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.""" |
| 150 | + prefixes = b"" |
| 151 | + if advertisement_types: |
| 152 | + prefixes = b"".join(adv.prefix for adv in advertisement_types) |
| 153 | + for entry in self._adapter.start_scan(prefixes=prefixes, **kwargs): |
| 154 | + adv_type = Advertisement |
| 155 | + for possible_type in advertisement_types: |
| 156 | + if possible_type.matches(entry) and issubclass(possible_type, adv_type): |
| 157 | + adv_type = possible_type |
| 158 | + advertisement = adv_type.from_entry(entry) |
| 159 | + if advertisement: |
| 160 | + yield advertisement |
| 161 | + |
| 162 | + def stop_scan(self): |
| 163 | + """Stops any active scan. |
| 164 | +
|
| 165 | + The scan results iterator will return any buffered results and then raise StopIteration |
| 166 | + once empty.""" |
| 167 | + self._adapter.stop_scan() |
| 168 | + |
| 169 | + def connect(self, advertisement, *, timeout=4): |
| 170 | + """Initiates a `BLEConnection` to the peer that advertised the given advertisement.""" |
| 171 | + connection = self._adapter.connect(advertisement.address, timeout=timeout) |
| 172 | + self._connection_cache[connection] = BLEConnection(connection) |
| 173 | + return self._connection_cache[connection] |
| 174 | + |
| 175 | + @property |
| 176 | + def connected(self): |
| 177 | + """True if any peers are connected to the adapter.""" |
| 178 | + return self._adapter.connected |
| 179 | + |
| 180 | + @property |
| 181 | + def connections(self): |
| 182 | + """A tuple of active `BLEConnection` objects.""" |
| 183 | + connections = self._adapter.connections |
| 184 | + wrapped_connections = [None] * len(connections) |
| 185 | + for i, connection in enumerate(self._adapter.connections): |
| 186 | + if connection not in self._connection_cache: |
| 187 | + self._connection_cache[connection] = BLEConnection(connection) |
| 188 | + wrapped_connections[i] = self._connection_cache[connection] |
| 189 | + |
| 190 | + return tuple(wrapped_connections) |
0 commit comments