client.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. # MicroPython aioble module
  2. # MIT license; Copyright (c) 2021 Jim Mussared
  3. from micropython import const
  4. from collections import deque
  5. import asyncio
  6. import struct
  7. import bluetooth
  8. from .core import ble, GattError, register_irq_handler
  9. from .device import DeviceConnection
  10. _IRQ_GATTC_SERVICE_RESULT = const(9)
  11. _IRQ_GATTC_SERVICE_DONE = const(10)
  12. _IRQ_GATTC_CHARACTERISTIC_RESULT = const(11)
  13. _IRQ_GATTC_CHARACTERISTIC_DONE = const(12)
  14. _IRQ_GATTC_DESCRIPTOR_RESULT = const(13)
  15. _IRQ_GATTC_DESCRIPTOR_DONE = const(14)
  16. _IRQ_GATTC_READ_RESULT = const(15)
  17. _IRQ_GATTC_READ_DONE = const(16)
  18. _IRQ_GATTC_WRITE_DONE = const(17)
  19. _IRQ_GATTC_NOTIFY = const(18)
  20. _IRQ_GATTC_INDICATE = const(19)
  21. _CCCD_UUID = const(0x2902)
  22. _CCCD_NOTIFY = const(1)
  23. _CCCD_INDICATE = const(2)
  24. _FLAG_READ = const(0x0002)
  25. _FLAG_WRITE_NO_RESPONSE = const(0x0004)
  26. _FLAG_WRITE = const(0x0008)
  27. _FLAG_NOTIFY = const(0x0010)
  28. _FLAG_INDICATE = const(0x0020)
  29. # Forward IRQs directly to static methods on the type that handles them and
  30. # knows how to map handles to instances. Note: We copy all uuid and data
  31. # params here for safety, but a future optimisation might be able to avoid
  32. # these copies in a few places.
  33. def _client_irq(event, data):
  34. if event == _IRQ_GATTC_SERVICE_RESULT:
  35. conn_handle, start_handle, end_handle, uuid = data
  36. ClientDiscover._discover_result(
  37. conn_handle, start_handle, end_handle, bluetooth.UUID(uuid)
  38. )
  39. elif event == _IRQ_GATTC_SERVICE_DONE:
  40. conn_handle, status = data
  41. ClientDiscover._discover_done(conn_handle, status)
  42. elif event == _IRQ_GATTC_CHARACTERISTIC_RESULT:
  43. conn_handle, end_handle, value_handle, properties, uuid = data
  44. ClientDiscover._discover_result(
  45. conn_handle, end_handle, value_handle, properties, bluetooth.UUID(uuid)
  46. )
  47. elif event == _IRQ_GATTC_CHARACTERISTIC_DONE:
  48. conn_handle, status = data
  49. ClientDiscover._discover_done(conn_handle, status)
  50. elif event == _IRQ_GATTC_DESCRIPTOR_RESULT:
  51. conn_handle, dsc_handle, uuid = data
  52. ClientDiscover._discover_result(conn_handle, dsc_handle, bluetooth.UUID(uuid))
  53. elif event == _IRQ_GATTC_DESCRIPTOR_DONE:
  54. conn_handle, status = data
  55. ClientDiscover._discover_done(conn_handle, status)
  56. elif event == _IRQ_GATTC_READ_RESULT:
  57. conn_handle, value_handle, char_data = data
  58. ClientCharacteristic._read_result(conn_handle, value_handle, bytes(char_data))
  59. elif event == _IRQ_GATTC_READ_DONE:
  60. conn_handle, value_handle, status = data
  61. ClientCharacteristic._read_done(conn_handle, value_handle, status)
  62. elif event == _IRQ_GATTC_WRITE_DONE:
  63. conn_handle, value_handle, status = data
  64. ClientCharacteristic._write_done(conn_handle, value_handle, status)
  65. elif event == _IRQ_GATTC_NOTIFY:
  66. conn_handle, value_handle, notify_data = data
  67. ClientCharacteristic._on_notify(conn_handle, value_handle, bytes(notify_data))
  68. elif event == _IRQ_GATTC_INDICATE:
  69. conn_handle, value_handle, indicate_data = data
  70. ClientCharacteristic._on_indicate(conn_handle, value_handle, bytes(indicate_data))
  71. register_irq_handler(_client_irq, None)
  72. # Async generator for discovering services, characteristics, descriptors.
  73. class ClientDiscover:
  74. def __init__(self, connection, disc_type, parent, timeout_ms, *args):
  75. self._connection = connection
  76. # Each result IRQ will append to this.
  77. self._queue = []
  78. # This will be set by the done IRQ.
  79. self._status = None
  80. # Tell the generator to process new events.
  81. self._event = asyncio.ThreadSafeFlag()
  82. # Must implement the _start_discovery static method. Instances of this
  83. # type are returned by __anext__.
  84. self._disc_type = disc_type
  85. # This will be the connection for a service discovery, and the service for a characteristic discovery.
  86. self._parent = parent
  87. # Timeout for the discovery process.
  88. # TODO: Not implemented.
  89. self._timeout_ms = timeout_ms
  90. # Additional arguments to pass to the _start_discovery method on disc_type.
  91. self._args = args
  92. async def _start(self):
  93. if self._connection._discover:
  94. # TODO: cancel existing? (e.g. perhaps they didn't let the loop run to completion)
  95. raise ValueError("Discovery in progress")
  96. # Tell the connection that we're the active discovery operation (the IRQ only gives us conn_handle).
  97. self._connection._discover = self
  98. # Call the appropriate ubluetooth.BLE method.
  99. self._disc_type._start_discovery(self._parent, *self._args)
  100. def __aiter__(self):
  101. return self
  102. async def __anext__(self):
  103. if self._connection._discover != self:
  104. # Start the discovery if necessary.
  105. await self._start()
  106. # Keep returning items from the queue until the status is set by the
  107. # done IRQ.
  108. while True:
  109. while self._queue:
  110. return self._disc_type(self._parent, *self._queue.pop())
  111. if self._status is not None:
  112. self._connection._discover = None
  113. raise StopAsyncIteration
  114. # Wait for more results to be added to the queue.
  115. await self._event.wait()
  116. # Tell the active discovery instance for this connection to add a new result
  117. # to the queue.
  118. def _discover_result(conn_handle, *args):
  119. if connection := DeviceConnection._connected.get(conn_handle, None):
  120. if discover := connection._discover:
  121. discover._queue.append(args)
  122. discover._event.set()
  123. # Tell the active discovery instance for this connection that it is complete.
  124. def _discover_done(conn_handle, status):
  125. if connection := DeviceConnection._connected.get(conn_handle, None):
  126. if discover := connection._discover:
  127. discover._status = status
  128. discover._event.set()
  129. # Represents a single service supported by a connection. Do not construct this
  130. # class directly, instead use `async for service in connection.services([uuid])` or
  131. # `await connection.service(uuid)`.
  132. class ClientService:
  133. def __init__(self, connection, start_handle, end_handle, uuid):
  134. self.connection = connection
  135. # Used for characteristic discovery.
  136. self._start_handle = start_handle
  137. self._end_handle = end_handle
  138. # Allows comparison to a known uuid.
  139. self.uuid = uuid
  140. def __str__(self):
  141. return "Service: {} {} {}".format(self._start_handle, self._end_handle, self.uuid)
  142. # Search for a specific characteristic by uuid.
  143. async def characteristic(self, uuid, timeout_ms=2000):
  144. result = None
  145. # Make sure loop runs to completion.
  146. async for characteristic in self.characteristics(uuid, timeout_ms):
  147. if not result and characteristic.uuid == uuid:
  148. # Keep first result.
  149. result = characteristic
  150. return result
  151. # Search for all services (optionally by uuid).
  152. # Use with `async for`, e.g.
  153. # async for characteristic in service.characteristics():
  154. # Note: must allow the loop to run to completion.
  155. def characteristics(self, uuid=None, timeout_ms=2000):
  156. return ClientDiscover(self.connection, ClientCharacteristic, self, timeout_ms, uuid)
  157. # For ClientDiscover
  158. def _start_discovery(connection, uuid=None):
  159. ble.gattc_discover_services(connection._conn_handle, uuid)
  160. class BaseClientCharacteristic:
  161. def __init__(self, value_handle, properties, uuid):
  162. # Used for read/write/notify ops.
  163. self._value_handle = value_handle
  164. # Which operations are supported.
  165. self.properties = properties
  166. # Allows comparison to a known uuid.
  167. self.uuid = uuid
  168. if properties & _FLAG_READ:
  169. # Fired for each read result and read done IRQ.
  170. self._read_event = None
  171. self._read_data = None
  172. # Used to indicate that the read is complete.
  173. self._read_status = None
  174. if (properties & _FLAG_WRITE) or (properties & _FLAG_WRITE_NO_RESPONSE):
  175. # Fired for the write done IRQ.
  176. self._write_event = None
  177. # Used to indicate that the write is complete.
  178. self._write_status = None
  179. # Register this value handle so events can find us.
  180. def _register_with_connection(self):
  181. self._connection()._characteristics[self._value_handle] = self
  182. # Map an incoming IRQ to an registered characteristic.
  183. def _find(conn_handle, value_handle):
  184. if connection := DeviceConnection._connected.get(conn_handle, None):
  185. if characteristic := connection._characteristics.get(value_handle, None):
  186. return characteristic
  187. else:
  188. # IRQ for a characteristic that we weren't expecting. e.g.
  189. # notification when we're not waiting on notified().
  190. # TODO: This will happen on btstack, which doesn't give us
  191. # value handle for the done event.
  192. return None
  193. def _check(self, flag):
  194. if not (self.properties & flag):
  195. raise ValueError("Unsupported")
  196. # Issue a read to the characteristic.
  197. async def read(self, timeout_ms=1000):
  198. self._check(_FLAG_READ)
  199. # Make sure this conn_handle/value_handle is known.
  200. self._register_with_connection()
  201. # This will be set by the done IRQ.
  202. self._read_status = None
  203. # This will be set by the result and done IRQs. Re-use if possible.
  204. self._read_event = self._read_event or asyncio.ThreadSafeFlag()
  205. # Issue the read.
  206. ble.gattc_read(self._connection()._conn_handle, self._value_handle)
  207. with self._connection().timeout(timeout_ms):
  208. # The event will be set for each read result, then a final time for done.
  209. while self._read_status is None:
  210. await self._read_event.wait()
  211. if self._read_status != 0:
  212. raise GattError(self._read_status)
  213. return self._read_data
  214. # Map an incoming result IRQ to a registered characteristic.
  215. def _read_result(conn_handle, value_handle, data):
  216. if characteristic := ClientCharacteristic._find(conn_handle, value_handle):
  217. characteristic._read_data = data
  218. characteristic._read_event.set()
  219. # Map an incoming read done IRQ to a registered characteristic.
  220. def _read_done(conn_handle, value_handle, status):
  221. if characteristic := ClientCharacteristic._find(conn_handle, value_handle):
  222. characteristic._read_status = status
  223. characteristic._read_event.set()
  224. async def write(self, data, response=None, timeout_ms=1000):
  225. self._check(_FLAG_WRITE | _FLAG_WRITE_NO_RESPONSE)
  226. # If the response arg is unset, then default it to true if we only support write-with-response.
  227. if response is None:
  228. p = self.properties
  229. response = (p & _FLAG_WRITE) and not (p & _FLAG_WRITE_NO_RESPONSE)
  230. if response:
  231. # Same as read.
  232. self._register_with_connection()
  233. self._write_status = None
  234. self._write_event = self._write_event or asyncio.ThreadSafeFlag()
  235. # Issue the write.
  236. ble.gattc_write(self._connection()._conn_handle, self._value_handle, data, response)
  237. if response:
  238. with self._connection().timeout(timeout_ms):
  239. # The event will be set for the write done IRQ.
  240. await self._write_event.wait()
  241. if self._write_status != 0:
  242. raise GattError(self._write_status)
  243. # Map an incoming write done IRQ to a registered characteristic.
  244. def _write_done(conn_handle, value_handle, status):
  245. if characteristic := ClientCharacteristic._find(conn_handle, value_handle):
  246. characteristic._write_status = status
  247. characteristic._write_event.set()
  248. # Represents a single characteristic supported by a service. Do not construct
  249. # this class directly, instead use `async for characteristic in
  250. # service.characteristics([uuid])` or `await service.characteristic(uuid)`.
  251. class ClientCharacteristic(BaseClientCharacteristic):
  252. def __init__(self, service, end_handle, value_handle, properties, uuid):
  253. self.service = service
  254. self.connection = service.connection
  255. # Used for descriptor discovery. If available, otherwise assume just
  256. # past the value handle (enough for two descriptors without risking
  257. # going into the next characteristic).
  258. self._end_handle = end_handle if end_handle > value_handle else value_handle + 2
  259. super().__init__(value_handle, properties, uuid)
  260. if properties & _FLAG_NOTIFY:
  261. # Fired when a notification arrives.
  262. self._notify_event = asyncio.ThreadSafeFlag()
  263. # Data for the most recent notification.
  264. self._notify_queue = deque((), 1)
  265. if properties & _FLAG_INDICATE:
  266. # Same for indications.
  267. self._indicate_event = asyncio.ThreadSafeFlag()
  268. self._indicate_queue = deque((), 1)
  269. def __str__(self):
  270. return "Characteristic: {} {} {} {}".format(
  271. self._end_handle, self._value_handle, self.properties, self.uuid
  272. )
  273. def _connection(self):
  274. return self.service.connection
  275. # Search for a specific descriptor by uuid.
  276. async def descriptor(self, uuid, timeout_ms=2000):
  277. result = None
  278. # Make sure loop runs to completion.
  279. async for descriptor in self.descriptors(timeout_ms):
  280. if not result and descriptor.uuid == uuid:
  281. # Keep first result.
  282. result = descriptor
  283. return result
  284. # Search for all services (optionally by uuid).
  285. # Use with `async for`, e.g.
  286. # async for descriptor in characteristic.descriptors():
  287. # Note: must allow the loop to run to completion.
  288. def descriptors(self, timeout_ms=2000):
  289. return ClientDiscover(self.connection, ClientDescriptor, self, timeout_ms)
  290. # For ClientDiscover
  291. def _start_discovery(service, uuid=None):
  292. ble.gattc_discover_characteristics(
  293. service.connection._conn_handle,
  294. service._start_handle,
  295. service._end_handle,
  296. uuid,
  297. )
  298. # Helper for notified() and indicated().
  299. async def _notified_indicated(self, queue, event, timeout_ms):
  300. # Ensure that events for this connection can route to this characteristic.
  301. self._register_with_connection()
  302. # If the queue is empty, then we need to wait. However, if the queue
  303. # has a single item, we also need to do a no-op wait in order to
  304. # clear the event flag (because the queue will become empty and
  305. # therefore the event should be cleared).
  306. if len(queue) <= 1:
  307. with self._connection().timeout(timeout_ms):
  308. await event.wait()
  309. # Either we started > 1 item, or the wait completed successfully, return
  310. # the front of the queue.
  311. return queue.popleft()
  312. # Wait for the next notification.
  313. # Will return immediately if a notification has already been received.
  314. async def notified(self, timeout_ms=None):
  315. self._check(_FLAG_NOTIFY)
  316. return await self._notified_indicated(self._notify_queue, self._notify_event, timeout_ms)
  317. def _on_notify_indicate(self, queue, event, data):
  318. # If we've gone from empty to one item, then wake something
  319. # blocking on `await char.notified()` (or `await char.indicated()`).
  320. wake = len(queue) == 0
  321. # Append the data. By default this is a deque with max-length==1, so it
  322. # replaces. But if capture is enabled then it will append.
  323. queue.append(data)
  324. if wake:
  325. # Queue is now non-empty. If something is waiting, it will be
  326. # worken. If something isn't waiting right now, then a future
  327. # caller to `await char.written()` will see the queue is
  328. # non-empty, and wait on the event if it's going to empty the
  329. # queue.
  330. event.set()
  331. # Map an incoming notify IRQ to a registered characteristic.
  332. def _on_notify(conn_handle, value_handle, notify_data):
  333. if characteristic := ClientCharacteristic._find(conn_handle, value_handle):
  334. characteristic._on_notify_indicate(
  335. characteristic._notify_queue, characteristic._notify_event, notify_data
  336. )
  337. # Wait for the next indication.
  338. # Will return immediately if an indication has already been received.
  339. async def indicated(self, timeout_ms=None):
  340. self._check(_FLAG_INDICATE)
  341. return await self._notified_indicated(
  342. self._indicate_queue, self._indicate_event, timeout_ms
  343. )
  344. # Map an incoming indicate IRQ to a registered characteristic.
  345. def _on_indicate(conn_handle, value_handle, indicate_data):
  346. if characteristic := ClientCharacteristic._find(conn_handle, value_handle):
  347. characteristic._on_notify_indicate(
  348. characteristic._indicate_queue, characteristic._indicate_event, indicate_data
  349. )
  350. # Write to the Client Characteristic Configuration to subscribe to
  351. # notify/indications for this characteristic.
  352. async def subscribe(self, notify=True, indicate=False):
  353. # Ensure that the generated notifications are dispatched in case the app
  354. # hasn't awaited on notified/indicated yet.
  355. self._register_with_connection()
  356. if cccd := await self.descriptor(bluetooth.UUID(_CCCD_UUID)):
  357. await cccd.write(struct.pack("<H", _CCCD_NOTIFY * notify + _CCCD_INDICATE * indicate))
  358. else:
  359. raise ValueError("CCCD not found")
  360. # Represents a single descriptor supported by a characteristic. Do not construct
  361. # this class directly, instead use `async for descriptors in
  362. # characteristic.descriptors([uuid])` or `await characteristic.descriptor(uuid)`.
  363. class ClientDescriptor(BaseClientCharacteristic):
  364. def __init__(self, characteristic, dsc_handle, uuid):
  365. self.characteristic = characteristic
  366. super().__init__(dsc_handle, _FLAG_READ | _FLAG_WRITE, uuid)
  367. def __str__(self):
  368. return "Descriptor: {} {} {}".format(self._value_handle, self.properties, self.uuid)
  369. def _connection(self):
  370. return self.characteristic.service.connection
  371. # For ClientDiscover
  372. def _start_discovery(characteristic, uuid=None):
  373. ble.gattc_discover_descriptors(
  374. characteristic._connection()._conn_handle,
  375. characteristic._value_handle,
  376. characteristic._end_handle,
  377. )
  378. __version__ = '0.3.0'