central.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. # MicroPython aioble module
  2. # MIT license; Copyright (c) 2021 Jim Mussared
  3. from micropython import const
  4. import bluetooth
  5. import struct
  6. import asyncio
  7. from .core import (
  8. ensure_active,
  9. ble,
  10. log_info,
  11. log_error,
  12. log_warn,
  13. register_irq_handler,
  14. )
  15. from .device import Device, DeviceConnection, DeviceTimeout
  16. _IRQ_SCAN_RESULT = const(5)
  17. _IRQ_SCAN_DONE = const(6)
  18. _IRQ_PERIPHERAL_CONNECT = const(7)
  19. _IRQ_PERIPHERAL_DISCONNECT = const(8)
  20. _ADV_IND = const(0)
  21. _ADV_DIRECT_IND = const(1)
  22. _ADV_SCAN_IND = const(2)
  23. _ADV_NONCONN_IND = const(3)
  24. _SCAN_RSP = const(4)
  25. _ADV_TYPE_FLAGS = const(0x01)
  26. _ADV_TYPE_NAME = const(0x09)
  27. _ADV_TYPE_SHORT_NAME = const(0x08)
  28. _ADV_TYPE_UUID16_INCOMPLETE = const(0x2)
  29. _ADV_TYPE_UUID16_COMPLETE = const(0x3)
  30. _ADV_TYPE_UUID32_INCOMPLETE = const(0x4)
  31. _ADV_TYPE_UUID32_COMPLETE = const(0x5)
  32. _ADV_TYPE_UUID128_INCOMPLETE = const(0x6)
  33. _ADV_TYPE_UUID128_COMPLETE = const(0x7)
  34. _ADV_TYPE_APPEARANCE = const(0x19)
  35. _ADV_TYPE_MANUFACTURER = const(0xFF)
  36. # Keep track of the active scanner so IRQs can be delivered to it.
  37. _active_scanner = None
  38. # Set of devices that are waiting for the peripheral connect IRQ.
  39. _connecting = set()
  40. def _central_irq(event, data):
  41. # Send results and done events to the active scanner instance.
  42. if event == _IRQ_SCAN_RESULT:
  43. addr_type, addr, adv_type, rssi, adv_data = data
  44. if not _active_scanner:
  45. return
  46. _active_scanner._queue.append((addr_type, bytes(addr), adv_type, rssi, bytes(adv_data)))
  47. _active_scanner._event.set()
  48. elif event == _IRQ_SCAN_DONE:
  49. if not _active_scanner:
  50. return
  51. _active_scanner._done = True
  52. _active_scanner._event.set()
  53. # Peripheral connect must be in response to a pending connection, so find
  54. # it in the pending connection set.
  55. elif event == _IRQ_PERIPHERAL_CONNECT:
  56. conn_handle, addr_type, addr = data
  57. for d in _connecting:
  58. if d.addr_type == addr_type and d.addr == addr:
  59. # Allow connect() to complete.
  60. connection = d._connection
  61. connection._conn_handle = conn_handle
  62. connection._event.set()
  63. break
  64. # Find the active device connection for this connection handle.
  65. elif event == _IRQ_PERIPHERAL_DISCONNECT:
  66. conn_handle, _, _ = data
  67. if connection := DeviceConnection._connected.get(conn_handle, None):
  68. # Tell the device_task that it should terminate.
  69. connection._event.set()
  70. def _central_shutdown():
  71. global _active_scanner, _connecting
  72. _active_scanner = None
  73. _connecting = set()
  74. register_irq_handler(_central_irq, _central_shutdown)
  75. # Cancel an in-progress scan.
  76. async def _cancel_pending():
  77. if _active_scanner:
  78. await _active_scanner.cancel()
  79. # Start connecting to a peripheral.
  80. # Call device.connect() rather than using method directly.
  81. async def _connect(
  82. connection, timeout_ms, scan_duration_ms, min_conn_interval_us, max_conn_interval_us
  83. ):
  84. device = connection.device
  85. if device in _connecting:
  86. return
  87. # Enable BLE and cancel in-progress scans.
  88. ensure_active()
  89. await _cancel_pending()
  90. # Allow the connected IRQ to find the device by address.
  91. _connecting.add(device)
  92. # Event will be set in the connected IRQ, and then later
  93. # re-used to notify disconnection.
  94. connection._event = connection._event or asyncio.ThreadSafeFlag()
  95. try:
  96. with DeviceTimeout(None, timeout_ms):
  97. ble.gap_connect(
  98. device.addr_type,
  99. device.addr,
  100. scan_duration_ms,
  101. min_conn_interval_us,
  102. max_conn_interval_us,
  103. )
  104. # Wait for the connected IRQ.
  105. await connection._event.wait()
  106. assert connection._conn_handle is not None
  107. # Register connection handle -> device.
  108. DeviceConnection._connected[connection._conn_handle] = connection
  109. finally:
  110. # After timeout, don't hold a reference and ignore future events.
  111. _connecting.remove(device)
  112. # Represents a single device that has been found during a scan. The scan
  113. # iterator will return the same ScanResult instance multiple times as its data
  114. # changes (i.e. changing RSSI or advertising data).
  115. class ScanResult:
  116. def __init__(self, device):
  117. self.device = device
  118. self.adv_data = None
  119. self.resp_data = None
  120. self.rssi = None
  121. self.connectable = False
  122. # New scan result available, return true if it changes our state.
  123. def _update(self, adv_type, rssi, adv_data):
  124. updated = False
  125. if rssi != self.rssi:
  126. self.rssi = rssi
  127. updated = True
  128. if adv_type in (_ADV_IND, _ADV_NONCONN_IND):
  129. if adv_data != self.adv_data:
  130. self.adv_data = adv_data
  131. self.connectable = adv_type == _ADV_IND
  132. updated = True
  133. elif adv_type == _ADV_SCAN_IND:
  134. if adv_data != self.adv_data and self.resp_data:
  135. updated = True
  136. self.adv_data = adv_data
  137. elif adv_type == _SCAN_RSP and adv_data:
  138. if adv_data != self.resp_data:
  139. self.resp_data = adv_data
  140. updated = True
  141. return updated
  142. def __str__(self):
  143. return "Scan result: {} {}".format(self.device, self.rssi)
  144. # Gets all the fields for the specified types.
  145. def _decode_field(self, *adv_type):
  146. # Advertising payloads are repeated packets of the following form:
  147. # 1 byte data length (N + 1)
  148. # 1 byte type (see constants below)
  149. # N bytes type-specific data
  150. for payload in (self.adv_data, self.resp_data):
  151. if not payload:
  152. continue
  153. i = 0
  154. while i + 1 < len(payload):
  155. if payload[i + 1] in adv_type:
  156. yield payload[i + 2 : i + payload[i] + 1]
  157. i += 1 + payload[i]
  158. # Returns the value of the complete (or shortened) advertised name, if available.
  159. def name(self):
  160. for n in self._decode_field(_ADV_TYPE_NAME, _ADV_TYPE_SHORT_NAME):
  161. return str(n, "utf-8") if n else ""
  162. # Generator that enumerates the service UUIDs that are advertised.
  163. def services(self):
  164. for uuid_len, codes in (
  165. (2, (_ADV_TYPE_UUID16_INCOMPLETE, _ADV_TYPE_UUID16_COMPLETE)),
  166. (4, (_ADV_TYPE_UUID32_INCOMPLETE, _ADV_TYPE_UUID32_COMPLETE)),
  167. (16, (_ADV_TYPE_UUID128_INCOMPLETE, _ADV_TYPE_UUID128_COMPLETE)),
  168. ):
  169. for u in self._decode_field(*codes):
  170. for i in range(0, len(u), uuid_len):
  171. yield bluetooth.UUID(u[i : i + uuid_len])
  172. # Generator that returns (manufacturer_id, data) tuples.
  173. def manufacturer(self, filter=None):
  174. for u in self._decode_field(_ADV_TYPE_MANUFACTURER):
  175. if len(u) < 2:
  176. continue
  177. m = struct.unpack("<H", u[0:2])[0]
  178. if filter is None or m == filter:
  179. yield (m, u[2:])
  180. # Use with:
  181. # async with aioble.scan(...) as scanner:
  182. # async for result in scanner:
  183. # ...
  184. class scan:
  185. def __init__(self, duration_ms, interval_us=None, window_us=None, active=False):
  186. self._queue = []
  187. self._event = asyncio.ThreadSafeFlag()
  188. self._done = False
  189. # Keep track of what we've already seen.
  190. self._results = set()
  191. # Ideally we'd start the scan here and avoid having to save these
  192. # values, but we need to stop any previous scan first via awaiting
  193. # _cancel_pending(), but __init__ isn't async.
  194. self._duration_ms = duration_ms
  195. self._interval_us = interval_us or 1280000
  196. self._window_us = window_us or 11250
  197. self._active = active
  198. async def __aenter__(self):
  199. global _active_scanner
  200. ensure_active()
  201. await _cancel_pending()
  202. _active_scanner = self
  203. ble.gap_scan(self._duration_ms, self._interval_us, self._window_us, self._active)
  204. return self
  205. async def __aexit__(self, exc_type, exc_val, exc_traceback):
  206. # Cancel the current scan if we're still the active scanner. This will
  207. # happen if the loop breaks early before the scan duration completes.
  208. if _active_scanner == self:
  209. await self.cancel()
  210. def __aiter__(self):
  211. assert _active_scanner == self
  212. return self
  213. async def __anext__(self):
  214. global _active_scanner
  215. if _active_scanner != self:
  216. # The scan has been canceled (e.g. a connection was initiated).
  217. raise StopAsyncIteration
  218. while True:
  219. while self._queue:
  220. addr_type, addr, adv_type, rssi, adv_data = self._queue.pop()
  221. # Try to find an existing ScanResult for this device.
  222. for r in self._results:
  223. if r.device.addr_type == addr_type and r.device.addr == addr:
  224. result = r
  225. break
  226. else:
  227. # New device, create a new Device & ScanResult.
  228. device = Device(addr_type, addr)
  229. result = ScanResult(device)
  230. self._results.add(result)
  231. # Add the new information from this event.
  232. if result._update(adv_type, rssi, adv_data):
  233. # It's new information, so re-yield this result.
  234. return result
  235. if self._done:
  236. # _IRQ_SCAN_DONE event was fired.
  237. _active_scanner = None
  238. raise StopAsyncIteration
  239. # Wait for either done or result IRQ.
  240. await self._event.wait()
  241. # Cancel any in-progress scan. We need to do this before starting any other operation.
  242. async def cancel(self):
  243. if self._done:
  244. return
  245. ble.gap_scan(None)
  246. while not self._done:
  247. await self._event.wait()
  248. global _active_scanner
  249. _active_scanner = None
  250. __version__ = '0.3.0'