/bleak/backends/corebluetooth/PeripheralDelegate.py

https://github.com/hbldh/bleak · Python · 436 lines · 410 code · 7 blank · 19 comment · 3 complexity · c91c15af41c98c10d3d1ff8d34cfde7b MD5 · raw file

  1. """
  2. PeripheralDelegate
  3. Created by kevincar <kevincarrolldavis@gmail.com>
  4. """
  5. import asyncio
  6. import logging
  7. from typing import Callable, Any
  8. import objc
  9. from Foundation import (
  10. NSObject,
  11. CBPeripheral,
  12. CBService,
  13. CBCharacteristic,
  14. CBDescriptor,
  15. NSData,
  16. NSError,
  17. )
  18. from CoreBluetooth import (
  19. CBCharacteristicWriteWithResponse,
  20. CBCharacteristicWriteWithoutResponse,
  21. )
  22. from bleak.exc import BleakError
  23. # logging.basicConfig(level=logging.DEBUG)
  24. logger = logging.getLogger(__name__)
  25. CBPeripheralDelegate = objc.protocolNamed("CBPeripheralDelegate")
  26. class _EventDict(dict):
  27. def get_cleared(self, xUUID) -> asyncio.Event:
  28. """Convenience method.
  29. Returns a cleared (False) event. Creates it if doesn't exits.
  30. """
  31. if xUUID not in self:
  32. # init as cleared (False)
  33. self[xUUID] = asyncio.Event()
  34. else:
  35. self[xUUID].clear()
  36. return self[xUUID]
  37. class PeripheralDelegate(NSObject):
  38. """macOS conforming python class for managing the PeripheralDelegate for BLE"""
  39. ___pyobjc_protocols__ = [CBPeripheralDelegate]
  40. def initWithPeripheral_(self, peripheral: CBPeripheral):
  41. """macOS init function for NSObject"""
  42. self = objc.super(PeripheralDelegate, self).init()
  43. if self is None:
  44. return None
  45. self.peripheral = peripheral
  46. self.peripheral.setDelegate_(self)
  47. self._event_loop = asyncio.get_event_loop()
  48. self._services_discovered_event = asyncio.Event()
  49. self._service_characteristic_discovered_events = _EventDict()
  50. self._characteristic_descriptor_discover_events = _EventDict()
  51. self._characteristic_read_events = _EventDict()
  52. self._characteristic_write_events = _EventDict()
  53. self._descriptor_read_events = _EventDict()
  54. self._descriptor_write_events = _EventDict()
  55. self._characteristic_notify_change_events = _EventDict()
  56. self._characteristic_notify_callbacks = {}
  57. return self
  58. async def discoverServices(self, use_cached=True) -> [CBService]:
  59. event = self._services_discovered_event
  60. if event.is_set() and (use_cached is True):
  61. return self.peripheral.services()
  62. event.clear()
  63. self.peripheral.discoverServices_(None)
  64. # wait for peripheral_didDiscoverServices_ to set
  65. await event.wait()
  66. return self.peripheral.services()
  67. async def discoverCharacteristics_(
  68. self, service: CBService, use_cached=True
  69. ) -> [CBCharacteristic]:
  70. if service.characteristics() is not None and use_cached is True:
  71. return service.characteristics()
  72. sUUID = service.UUID().UUIDString()
  73. event = self._service_characteristic_discovered_events.get_cleared(sUUID)
  74. self.peripheral.discoverCharacteristics_forService_(None, service)
  75. await event.wait()
  76. return service.characteristics()
  77. async def discoverDescriptors_(
  78. self, characteristic: CBCharacteristic, use_cached=True
  79. ) -> [CBDescriptor]:
  80. if characteristic.descriptors() is not None and use_cached is True:
  81. return characteristic.descriptors()
  82. cUUID = characteristic.UUID().UUIDString()
  83. event = self._characteristic_descriptor_discover_events.get_cleared(cUUID)
  84. self.peripheral.discoverDescriptorsForCharacteristic_(characteristic)
  85. await event.wait()
  86. return characteristic.descriptors()
  87. async def readCharacteristic_(
  88. self, characteristic: CBCharacteristic, use_cached=True
  89. ) -> NSData:
  90. if characteristic.value() is not None and use_cached is True:
  91. return characteristic.value()
  92. cUUID = characteristic.UUID().UUIDString()
  93. event = self._characteristic_read_events.get_cleared(cUUID)
  94. self.peripheral.readValueForCharacteristic_(characteristic)
  95. await asyncio.wait_for(event.wait(), timeout=5)
  96. if characteristic.value():
  97. return characteristic.value()
  98. else:
  99. return b""
  100. async def readDescriptor_(
  101. self, descriptor: CBDescriptor, use_cached=True
  102. ) -> NSData:
  103. if descriptor.value() is not None and use_cached is True:
  104. return descriptor.value()
  105. dUUID = descriptor.UUID().UUIDString()
  106. event = self._descriptor_read_events.get_cleared(dUUID)
  107. self.peripheral.readValueForDescriptor_(descriptor)
  108. await event.wait()
  109. return descriptor.value()
  110. async def writeCharacteristic_value_type_(
  111. self, characteristic: CBCharacteristic, value: NSData, response: int
  112. ) -> bool:
  113. # TODO: Is the type hint for response correct? Should it be a NSInteger instead?
  114. cUUID = characteristic.UUID().UUIDString()
  115. event = self._characteristic_write_events.get_cleared(cUUID)
  116. self.peripheral.writeValue_forCharacteristic_type_(
  117. value, characteristic, response
  118. )
  119. if response == CBCharacteristicWriteWithResponse:
  120. await event.wait()
  121. return True
  122. async def writeDescriptor_value_(
  123. self, descriptor: CBDescriptor, value: NSData
  124. ) -> bool:
  125. dUUID = descriptor.UUID().UUIDString()
  126. event = self._descriptor_write_events.get_cleared(dUUID)
  127. self.peripheral.writeValue_forDescriptor_(value, descriptor)
  128. await event.wait()
  129. return True
  130. async def startNotify_cb_(
  131. self, characteristic: CBCharacteristic, callback: Callable[[str, Any], Any]
  132. ) -> bool:
  133. c_handle = characteristic.handle()
  134. if c_handle in self._characteristic_notify_callbacks:
  135. raise ValueError("Characteristic notifications already started")
  136. self._characteristic_notify_callbacks[c_handle] = callback
  137. event = self._characteristic_notify_change_events.get_cleared(c_handle)
  138. self.peripheral.setNotifyValue_forCharacteristic_(True, characteristic)
  139. # wait for peripheral_didUpdateNotificationStateForCharacteristic_error_ to set event
  140. # await event.wait()
  141. return True
  142. async def stopNotify_(self, characteristic: CBCharacteristic) -> bool:
  143. c_handle = characteristic.handle()
  144. if c_handle not in self._characteristic_notify_callbacks:
  145. raise ValueError("Characteristic notification never started")
  146. event = self._characteristic_notify_change_events.get_cleared(c_handle)
  147. self.peripheral.setNotifyValue_forCharacteristic_(False, characteristic)
  148. # wait for peripheral_didUpdateNotificationStateForCharacteristic_error_ to set event
  149. # await event.wait()
  150. self._characteristic_notify_callbacks.pop(c_handle)
  151. return True
  152. # Protocol Functions
  153. @objc.python_method
  154. def did_discover_services(self, peripheral: CBPeripheral, error: NSError) -> None:
  155. if error is not None:
  156. raise BleakError("Failed to discover services {}".format(error))
  157. logger.debug("Services discovered")
  158. self._services_discovered_event.set()
  159. def peripheral_didDiscoverServices_(
  160. self, peripheral: CBPeripheral, error: NSError
  161. ) -> None:
  162. logger.debug("peripheral_didDiscoverServices_")
  163. self._event_loop.call_soon_threadsafe(
  164. self.did_discover_services,
  165. peripheral,
  166. error,
  167. )
  168. @objc.python_method
  169. def did_discover_characteristics_for_service(
  170. self, peripheral: CBPeripheral, service: CBService, error: NSError
  171. ):
  172. sUUID = service.UUID().UUIDString()
  173. if error is not None:
  174. raise BleakError(
  175. "Failed to discover services for service {}: {}".format(sUUID, error)
  176. )
  177. logger.debug("Characteristics discovered")
  178. event = self._service_characteristic_discovered_events.get(sUUID)
  179. if event:
  180. event.set()
  181. else:
  182. logger.debug("Unexpected event didDiscoverCharacteristicsForService")
  183. def peripheral_didDiscoverCharacteristicsForService_error_(
  184. self, peripheral: CBPeripheral, service: CBService, error: NSError
  185. ):
  186. logger.debug("peripheral_didDiscoverCharacteristicsForService_error_")
  187. self._event_loop.call_soon_threadsafe(
  188. self.did_discover_characteristics_for_service,
  189. peripheral,
  190. service,
  191. error,
  192. )
  193. @objc.python_method
  194. def did_discover_descriptors_for_characteristic(
  195. self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError
  196. ):
  197. cUUID = characteristic.UUID().UUIDString()
  198. if error is not None:
  199. raise BleakError(
  200. "Failed to discover descriptors for characteristic {}: {}".format(
  201. cUUID, error
  202. )
  203. )
  204. logger.debug("Descriptor discovered {}".format(cUUID))
  205. event = self._characteristic_descriptor_discover_events.get(cUUID)
  206. if event:
  207. event.set()
  208. else:
  209. logger.warning("Unexpected event didDiscoverDescriptorsForCharacteristic")
  210. def peripheral_didDiscoverDescriptorsForCharacteristic_error_(
  211. self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError
  212. ):
  213. logger.debug("peripheral_didDiscoverDescriptorsForCharacteristic_error_")
  214. self._event_loop.call_soon_threadsafe(
  215. self.did_discover_descriptors_for_characteristic,
  216. peripheral,
  217. characteristic,
  218. error,
  219. )
  220. @objc.python_method
  221. def did_update_value_for_characteristic(
  222. self,
  223. peripheral: CBPeripheral,
  224. characteristic: CBCharacteristic,
  225. value: bytes,
  226. error: NSError,
  227. ):
  228. cUUID = characteristic.UUID().UUIDString()
  229. c_handle = characteristic.handle()
  230. if error is not None:
  231. raise BleakError(
  232. "Failed to read characteristic {}: {}".format(cUUID, error)
  233. )
  234. notify_callback = self._characteristic_notify_callbacks.get(c_handle)
  235. if notify_callback:
  236. notify_callback(c_handle, value)
  237. logger.debug("Read characteristic value")
  238. event = self._characteristic_read_events.get(cUUID)
  239. if event:
  240. event.set()
  241. else:
  242. # only expected on read
  243. pass
  244. def peripheral_didUpdateValueForCharacteristic_error_(
  245. self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError
  246. ):
  247. logger.debug("peripheral_didUpdateValueForCharacteristic_error_")
  248. self._event_loop.call_soon_threadsafe(
  249. self.did_update_value_for_characteristic,
  250. peripheral,
  251. characteristic,
  252. characteristic.value(),
  253. error,
  254. )
  255. @objc.python_method
  256. def did_update_value_for_descriptor(
  257. self, peripheral: CBPeripheral, descriptor: CBDescriptor, error: NSError
  258. ):
  259. dUUID = descriptor.UUID().UUIDString()
  260. if error is not None:
  261. raise BleakError("Failed to read descriptor {}: {}".format(dUUID, error))
  262. logger.debug("Read descriptor value")
  263. event = self._descriptor_read_events.get(dUUID)
  264. if event:
  265. event.set()
  266. else:
  267. logger.warning("Unexpected event didUpdateValueForDescriptor")
  268. def peripheral_didUpdateValueForDescriptor_error_(
  269. self, peripheral: CBPeripheral, descriptor: CBDescriptor, error: NSError
  270. ):
  271. logger.debug("peripheral_didUpdateValueForDescriptor_error_")
  272. self._event_loop.call_soon_threadsafe(
  273. self.did_update_value_for_descriptor,
  274. peripheral,
  275. descriptor,
  276. error,
  277. )
  278. @objc.python_method
  279. def did_write_value_for_characteristic(
  280. self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError
  281. ):
  282. cUUID = characteristic.UUID().UUIDString()
  283. if error is not None:
  284. raise BleakError(
  285. "Failed to write characteristic {}: {}".format(cUUID, error)
  286. )
  287. logger.debug("Write Characteristic Value")
  288. event = self._characteristic_write_events.get(cUUID)
  289. if event:
  290. event.set()
  291. else:
  292. # event only expected on write with response
  293. pass
  294. def peripheral_didWriteValueForCharacteristic_error_(
  295. self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError
  296. ):
  297. logger.debug("peripheral_didWriteValueForCharacteristic_error_")
  298. self._event_loop.call_soon_threadsafe(
  299. self.did_write_value_for_characteristic,
  300. peripheral,
  301. characteristic,
  302. error,
  303. )
  304. @objc.python_method
  305. def did_write_value_for_descriptor(
  306. self, peripheral: CBPeripheral, descriptor: CBDescriptor, error: NSError
  307. ):
  308. dUUID = descriptor.UUID().UUIDString()
  309. if error is not None:
  310. raise BleakError("Failed to write descriptor {}: {}".format(dUUID, error))
  311. logger.debug("Write Descriptor Value")
  312. event = self._descriptor_write_events.get(dUUID)
  313. if event:
  314. event.set()
  315. else:
  316. logger.warning("Unexpected event didWriteValueForDescriptor")
  317. def peripheral_didWriteValueForDescriptor_error_(
  318. self, peripheral: CBPeripheral, descriptor: CBDescriptor, error: NSError
  319. ):
  320. logger.debug("peripheral_didWriteValueForDescriptor_error_")
  321. self._event_loop.call_soon_threadsafe(
  322. self.did_write_value_for_descriptor,
  323. peripheral,
  324. descriptor,
  325. error,
  326. )
  327. @objc.python_method
  328. def did_update_notification_for_characteristic(
  329. self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError
  330. ):
  331. cUUID = characteristic.UUID().UUIDString()
  332. c_handle = characteristic.handle()
  333. if error is not None:
  334. raise BleakError(
  335. "Failed to update the notification status for characteristic {}: {}".format(
  336. cUUID, error
  337. )
  338. )
  339. logger.debug("Character Notify Update")
  340. event = self._characteristic_notify_change_events.get(c_handle)
  341. if event:
  342. event.set()
  343. else:
  344. logger.warning(
  345. "Unexpected event didUpdateNotificationStateForCharacteristic"
  346. )
  347. def peripheral_didUpdateNotificationStateForCharacteristic_error_(
  348. self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError
  349. ):
  350. logger.debug("peripheral_didUpdateNotificationStateForCharacteristic_error_")
  351. self._event_loop.call_soon_threadsafe(
  352. self.did_update_notification_for_characteristic,
  353. peripheral,
  354. characteristic,
  355. error,
  356. )