2 # Copyright (c) 2020 Project CHIP Authors
3 # Copyright (c) 2019-2020 Google LLC.
4 # Copyright (c) 2015-2018 Nest Labs, Inc.
7 # Licensed under the Apache License, Version 2.0 (the "License");
8 # you may not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
11 # http://www.apache.org/licenses/LICENSE-2.0
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS,
15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
22 # BLE Central support for Chip Device Controller via OSX CoreBluetooth APIs.
25 from __future__ import absolute_import
26 from __future__ import print_function
28 from Foundation import *
35 from .ChipBleUtility import (
36 BLE_SUBSCRIBE_OPERATION_SUBSCRIBE,
37 BLE_SUBSCRIBE_OPERATION_UNSUBSCRIBE,
38 BLE_ERROR_REMOTE_DEVICE_DISCONNECTED,
44 BleDisconnectEventStruct,
46 BleSubscribeEventStruct,
47 BleDeviceIdentificationInfo,
51 from .ChipUtility import ChipUtility
52 from .ChipBleBase import ChipBleBase
60 bundle_path=objc.pathForFramework(
61 u"/System/Library/Frameworks/IOBluetooth.framework/Versions/A/Frameworks/CoreBluetooth.framework"
64 except Exception as ex:
68 bundle_path=objc.pathForFramework(
69 u"/System/Library/Frameworks/CoreBluetooth.framework"
73 BLE_PERIPHERAL_STATE_DISCONNECTED = 0
74 CBCharacteristicWriteWithResponse = 0
75 CBCharacteristicWriteWithoutResponse = 1
77 CHIP_SERVICE = CBUUID.UUIDWithString_(u"0000FEAF-0000-1000-8000-00805F9B34FB")
78 CHIP_SERVICE_SHORT = CBUUID.UUIDWithString_(u"FEAF")
79 CHIP_TX = CBUUID.UUIDWithString_(u"18EE2EF5-263D-4559-959F-4F9C429F9D11")
80 CHIP_RX = CBUUID.UUIDWithString_(u"18EE2EF5-263D-4559-959F-4F9C429F9D12")
81 CHROMECAST_SETUP_SERVICE = CBUUID.UUIDWithString_(
82 u"0000FEA0-0000-1000-8000-00805F9B34FB"
84 CHROMECAST_SETUP_SERVICE_SHORT = CBUUID.UUIDWithString_(u"FEA0")
87 def _VoidPtrToCBUUID(ptr, len):
89 ptr = ChipUtility.VoidPtrToByteArray(ptr, len)
90 ptr = ChipUtility.Hexlify(ptr)
102 ptr = CBUUID.UUIDWithString_(ptr)
103 except Exception as ex:
104 print("ERROR: failed to convert void * to CBUUID")
110 def __init__(self, op, timelimit, arg = None):
112 self.due = time.time() + timelimit
115 def TimeLimitExceeded(self):
116 return time.time() > self.due
119 def __init__(self, peripheral, advData):
120 self.peripheral = peripheral
121 self.advData = dict(advData)
123 def __eq__(self, another):
124 return self.peripheral == another.peripheral
126 def getPeripheralDevIdInfo(self):
130 servDataDict = self.advData.get("kCBAdvDataServiceData", None)
133 servDataDict = dict(servDataDict)
134 for i in servDataDict.keys():
135 if str(i).lower() == str(CHIP_SERVICE_SHORT).lower():
136 return ParseServiceData(bytes(servDataDict[i]))
139 class CoreBluetoothManager(ChipBleBase):
140 def __init__(self, devCtrl, logger=None):
144 self.logger = logging.getLogger("ChipBLEMgr")
147 format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
150 self.peripheral = None
152 self.scan_quiet = False
153 self.characteristics = {}
154 self.peripheral_list = []
155 self.peripheral_adv_list = []
156 self.bg_peripheral_name = None
157 self.chip_queue = queue.Queue()
159 self.manager = CBCentralManager.alloc()
160 self.manager.initWithDelegate_queue_options_(self, None, None)
162 self.ready_condition = False
163 self.loop_condition = (
164 False # indicates whether the cmd requirement has been met in the runloop.
166 self.connect_state = False # reflects whether or not there is a connection.
167 self.send_condition = False
168 self.subscribe_condition = False
170 self.runLoopUntil(LoopCondition("ready", 10.0))
172 self.orig_input_hook = None
173 self.hookFuncPtr = None
175 self.setInputHook(self.readlineCB)
176 self.devCtrl = devCtrl
178 # test if any connections currently exist (left around from a previous run) and disconnect if need be.
179 peripherals = self.manager.retrieveConnectedPeripheralsWithServices_(
180 [CHIP_SERVICE_SHORT, CHIP_SERVICE]
183 if peripherals and len(peripherals):
184 for periph in peripherals:
185 self.logger.info("disconnecting old connection.")
186 self.loop_condition = False
187 self.manager.cancelPeripheralConnection_(periph)
188 self.runLoopUntil(LoopCondition("disconnect", 5.0))
190 self.connect_state = False
191 self.loop_condition = False
195 self.setInputHook(self.orig_input_hook)
196 self.devCtrl.SetBlockingCB(None)
197 self.devCtrl.SetBleEventCB(None)
200 """A callback used by ChipDeviceCtrl.py to drive the OSX runloop while the
201 main thread waits for the Chip thread to complete its operation."""
202 runLoop = NSRunLoop.currentRunLoop()
203 runLoop.limitDateForMode_(NSDefaultRunLoopMode)
205 def readlineCB(self):
206 """A callback used by readline to drive the OSX runloop while the main thread
207 waits for commandline input from the user."""
208 runLoop = NSRunLoop.currentRunLoop()
209 runLoop.limitDateForMode_(NSDefaultRunLoopMode)
211 if self.orig_input_hook:
212 self.orig_input_hook()
214 def setInputHook(self, hookFunc):
215 """Set the PyOS_InputHook to call the specific function."""
216 hookFunctionType = CFUNCTYPE(None)
217 self.hookFuncPtr = hookFunctionType(hookFunc)
218 pyos_inputhook_ptr = c_void_p.in_dll(pythonapi, "PyOS_InputHook")
219 # save the original so that on del we can revert it back to the way it was.
220 self.orig_input_hook = cast(pyos_inputhook_ptr.value, PYFUNCTYPE(c_int))
221 # set the new hook. readLine will call this periodically as it polls for input.
222 pyos_inputhook_ptr.value = cast(self.hookFuncPtr, c_void_p).value
224 def shouldLoop(self, cond:LoopCondition):
225 """ Used by runLoopUntil to determine whether it should exit the runloop. """
227 if cond.TimeLimitExceeded():
230 if cond.op == "ready":
231 return not self.ready_condition
232 elif cond.op == "scan":
233 for peripheral in self.peripheral_adv_list:
234 if cond.arg and str(peripheral.peripheral._.name) == cond.arg:
236 devIdInfo = peripheral.getPeripheralDevIdInfo()
237 if devIdInfo and cond.arg and str(devIdInfo.discriminator) == cond.arg:
239 elif cond.op == "connect":
240 return (not self.loop_condition)
241 elif cond.op == "disconnect":
242 return (not self.loop_condition)
243 elif cond.op == "send":
244 return (not self.send_condition)
245 elif cond.op == "subscribe":
246 return (not self.subscribe_condition)
247 elif cond.op == "unsubscribe":
248 return self.subscribe_condition
252 def runLoopUntil(self, cond:LoopCondition):
253 """Helper function to drive OSX runloop until an expected event is received or
254 the timeout expires."""
255 runLoop = NSRunLoop.currentRunLoop()
258 while nextfire and self.shouldLoop(cond):
259 nextfire = runLoop.limitDateForMode_(NSDefaultRunLoopMode)
261 def centralManagerDidUpdateState_(self, manager):
262 """ IO Bluetooth initialization is successful."""
264 state = manager.state()
265 string = "BLE is ready!" if state > 4 else "BLE is not ready!"
266 self.logger.info(string)
267 self.manager = manager
268 self.ready_condition = True if state > 4 else False
270 def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(
271 self, manager, peripheral, data, rssi
273 """ Called for each peripheral discovered during scan."""
274 if self.bg_peripheral_name is None:
275 if peripheral not in self.peripheral_list:
276 if not self.scan_quiet:
277 self.logger.info("adding to scan list:")
280 "{0:<16}= {1:<80}".format("Name", str(peripheral._.name))
283 "{0:<16}= {1:<80}".format(
284 "ID", str(peripheral._.identifier.UUIDString())
287 self.logger.info("{0:<16}= {1:<80}".format("RSSI", rssi))
288 devIdInfo = BlePeripheral(peripheral, data).getPeripheralDevIdInfo()
290 self.logger.info("{0:<16}= {1}".format("Pairing State", devIdInfo.pairingState))
291 self.logger.info("{0:<16}= {1}".format("Discriminator", devIdInfo.discriminator))
292 self.logger.info("{0:<16}= {1}".format("Vendor Id", devIdInfo.vendorId))
293 self.logger.info("{0:<16}= {1}".format("Product Id", devIdInfo.productId))
294 self.logger.info("ADV data: " + repr(data))
297 self.peripheral_list.append(peripheral)
298 self.peripheral_adv_list.append(BlePeripheral(peripheral, data))
300 if (peripheral._.name == self.bg_peripheral_name) or (str(devIdInfo.discriminator) == self.bg_peripheral_name):
301 if len(self.peripheral_list) == 0:
302 self.logger.info("found background peripheral")
303 self.peripheral_list = [peripheral]
304 self.peripheral_adv_list = [BlePeripheral(peripheral, data)]
306 def centralManager_didConnectPeripheral_(self, manager, peripheral):
307 """Called by CoreBluetooth via runloop when a connection succeeds."""
308 self.logger.debug(repr(peripheral))
309 # make this class the delegate for peripheral events.
310 self.peripheral.setDelegate_(self)
311 # invoke service discovery on the periph.
312 self.logger.info("Discovering services")
313 self.peripheral.discoverServices_([CHIP_SERVICE_SHORT, CHIP_SERVICE])
315 def centralManager_didFailToConnectPeripheral_error_(
316 self, manager, peripheral, error
318 """Called by CoreBluetooth via runloop when a connection fails."""
319 self.logger.info("Failed to connect error = " + repr(error))
320 self.loop_condition = True
321 self.connect_state = False
323 def centralManager_didDisconnectPeripheral_error_(self, manager, peripheral, error):
324 """Called by CoreBluetooth via runloop when a disconnect completes. error = None on success."""
325 self.loop_condition = True
326 self.connect_state = False
328 self.logger.info("BLE disconnected, error = " + repr(error))
329 dcEvent = BleDisconnectEvent(BLE_ERROR_REMOTE_DEVICE_DISCONNECTED)
330 self.chip_queue.put(dcEvent)
331 self.devCtrl.DriveBleIO()
333 def peripheral_didDiscoverServices_(self, peripheral, services):
334 """Called by CoreBluetooth via runloop when peripheral services are discovered."""
335 if len(self.peripheral.services()) == 0:
336 self.logger.error("Chip service not found")
337 self.connect_state = False
339 # in debugging, we found connect being called twice. This
340 # would trigger discovering the services twice, and
341 # consequently, discovering characteristics twice. We use the
342 # self.service as a flag to indicate whether the
343 # characteristics need to be invalidated immediately.
344 if self.service == self.peripheral.services()[0]:
345 self.logger.debug("didDiscoverServices already happened")
347 self.service = self.peripheral.services()[0]
348 self.characteristics[self.service.UUID()] = []
349 # NOTE: currently limiting discovery to only the pair of Chip characteristics.
350 self.peripheral.discoverCharacteristics_forService_(
351 [CHIP_RX, CHIP_TX], self.service
354 def peripheral_didDiscoverCharacteristicsForService_error_(
355 self, peripheral, service, error
357 """Called by CoreBluetooth via runloop when a characteristic for a service is discovered."""
359 "didDiscoverCharacteristicsForService:error "
360 + str(repr(peripheral))
364 self.logger.debug(repr(service))
365 self.logger.debug(repr(error))
368 self.characteristics[service.UUID()] = [
369 char for char in self.service.characteristics()
372 self.connect_state = True
375 self.logger.error("ERROR: failed to discover characteristics for service.")
376 self.connect_state = False
378 self.loop_condition = True
380 def peripheral_didWriteValueForCharacteristic_error_(
381 self, peripheral, characteristic, error
383 """Called by CoreBluetooth via runloop when a write to characteristic
384 operation completes. error = None on success."""
385 self.logger.debug("didWriteValue error = " + repr(error))
386 self.send_condition = True
387 charId = bytearray(characteristic.UUID().data().bytes().tobytes())
388 svcId = bytearray(CHIP_SERVICE.data().bytes().tobytes())
391 txEvent = BleTxEvent(
392 charId=charId, svcId=svcId, status=True if not error else False
394 self.chip_queue.put(txEvent)
395 self.devCtrl.DriveBleIO()
397 def peripheral_didUpdateNotificationStateForCharacteristic_error_(
398 self, peripheral, characteristic, error
400 """Called by CoreBluetooth via runloop when a subscribe for notification operation completes.
401 Error = None on success."""
402 self.logger.debug("Receiving notifications")
403 charId = bytearray(characteristic.UUID().data().bytes().tobytes())
404 svcId = bytearray(CHIP_SERVICE.data().bytes().tobytes())
405 # look at error and send True/False on Success/Failure
406 success = True if not error else False
407 if characteristic.isNotifying():
408 operation = BLE_SUBSCRIBE_OPERATION_SUBSCRIBE
409 self.subscribe_condition = True
411 operation = BLE_SUBSCRIBE_OPERATION_UNSUBSCRIBE
412 self.subscribe_condition = False
414 self.logger.debug("Operation = " + repr(operation))
415 self.logger.debug("success = " + repr(success))
418 subscribeEvent = BleSubscribeEvent(
419 charId=charId, svcId=svcId, status=success, operation=operation
421 self.chip_queue.put(subscribeEvent)
422 self.devCtrl.DriveBleIO()
424 def peripheral_didUpdateValueForCharacteristic_error_(
425 self, peripheral, characteristic, error
427 """Called by CoreBluetooth via runloop when a new characteristic value is received for a
428 characteristic to which this device has subscribed."""
429 # len = characteristic.value().length()
430 bytes = bytearray(characteristic.value().bytes().tobytes())
431 charId = bytearray(characteristic.UUID().data().bytes().tobytes())
432 svcId = bytearray(CHIP_SERVICE.data().bytes().tobytes())
434 # Kick Chip thread to retrieve the saved packet.
436 # Save buffer, length, service UUID and characteristic UUID
437 rxEvent = BleRxEvent(charId=charId, svcId=svcId, buffer=bytes)
438 self.chip_queue.put(rxEvent)
439 self.devCtrl.DriveBleIO()
441 self.logger.debug("received")
446 + repr(characteristic.value().bytes().tobytes())
449 def GetBleEvent(self):
450 """ Called by ChipDeviceMgr.py on behalf of Chip to retrieve a queued message."""
451 if not self.chip_queue.empty():
452 ev = self.chip_queue.get()
454 if isinstance(ev, BleRxEvent):
455 eventStruct = BleRxEventStruct.fromBleRxEvent(ev)
456 return cast(pointer(eventStruct), c_void_p).value
457 elif isinstance(ev, BleTxEvent):
458 eventStruct = BleTxEventStruct.fromBleTxEvent(ev)
459 return cast(pointer(eventStruct), c_void_p).value
460 elif isinstance(ev, BleSubscribeEvent):
461 eventStruct = BleSubscribeEventStruct.fromBleSubscribeEvent(ev)
462 return cast(pointer(eventStruct), c_void_p).value
463 elif isinstance(ev, BleDisconnectEvent):
464 eventStruct = BleDisconnectEventStruct.fromBleDisconnectEvent(ev)
465 return cast(pointer(eventStruct), c_void_p).value
469 def scan(self, line):
470 """ API to initiatae BLE scanning for -t user_timeout seconds."""
472 args = self.ParseInputLine(line, "scan")
476 self.scan_quiet = args[1]
477 self.bg_peripheral_name = None
478 del self.peripheral_list[:]
479 del self.peripheral_adv_list[:]
480 self.peripheral_list = []
481 self.peripheral_adv_list = []
482 # Filter on the service UUID Array or None to accept all scan results.
483 self.manager.scanForPeripheralsWithServices_options_(
487 CHROMECAST_SETUP_SERVICE_SHORT,
488 CHROMECAST_SETUP_SERVICE,
492 # self.manager.scanForPeripheralsWithServices_options_(None, None)
494 self.runLoopUntil(LoopCondition("scan", args[0], args[2]))
496 self.manager.stopScan()
497 self.logger.info("scanning stopped")
499 def bgScanStart(self, name):
500 """ API to initiate background BLE scanning."""
501 self.logger.info("scanning started")
502 self.bg_peripheral_name = name
503 del self.peripheral_list[:]
504 self.peripheral_list = []
505 # Filter on the service UUID Array or None to accept all scan results.
506 self.manager.scanForPeripheralsWithServices_options_(
510 CHROMECAST_SETUP_SERVICE_SHORT,
511 CHROMECAST_SETUP_SERVICE,
516 def bgScanStop(self):
517 """ API to stop background BLE scanning."""
518 self.manager.stopScan()
519 self.bg_peripheral_name = None
520 self.logger.info("scanning stopped")
522 def ble_debug_log(self, line):
523 args = self.ParseInputLine(line)
524 if int(args[0]) == 1:
525 self.logger.setLevel(logging.DEBUG)
526 self.logger.debug("current logging level is debug")
528 self.logger.setLevel(logging.INFO)
529 self.logger.info("current logging level is info")
532 def CloseBle(self, connObj):
533 """ Called by Chip to close the BLE connection."""
535 self.manager.cancelPeripheralConnection_(self.peripheral)
536 self.characteristics = {}
537 # del self.peripheral_list[:]
538 # self.peripheral_list = []
539 self.peripheral = None
541 self.connect_state = False
545 def updateCharacteristic(self, bytes, svcId, charId):
546 # TODO: implement this for Peripheral support.