Fix for x86_64 build fail
[platform/upstream/connectedhomeip.git] / src / controller / python / chip / ChipCoreBluetoothMgr.py
1 #
2 #    Copyright (c) 2020 Project CHIP Authors
3 #    Copyright (c) 2019-2020 Google LLC.
4 #    Copyright (c) 2015-2018 Nest Labs, Inc.
5 #    All rights reserved.
6 #
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
10 #
11 #        http://www.apache.org/licenses/LICENSE-2.0
12 #
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.
18 #
19
20 #
21 #    @file
22 #      BLE Central support for Chip Device Controller via OSX CoreBluetooth APIs.
23 #
24
25 from __future__ import absolute_import
26 from __future__ import print_function
27 from ctypes import *
28 from Foundation import *
29
30 import logging
31 import objc
32 import queue
33 import time
34
35 from .ChipBleUtility import (
36     BLE_SUBSCRIBE_OPERATION_SUBSCRIBE,
37     BLE_SUBSCRIBE_OPERATION_UNSUBSCRIBE,
38     BLE_ERROR_REMOTE_DEVICE_DISCONNECTED,
39     BleTxEvent,
40     BleDisconnectEvent,
41     BleRxEvent,
42     BleSubscribeEvent,
43     BleTxEventStruct,
44     BleDisconnectEventStruct,
45     BleRxEventStruct,
46     BleSubscribeEventStruct,
47     BleDeviceIdentificationInfo,
48     ParseServiceData,
49 )
50
51 from .ChipUtility import ChipUtility
52 from .ChipBleBase import ChipBleBase
53
54
55 try:
56
57     objc.loadBundle(
58         "CoreBluetooth",
59         globals(),
60         bundle_path=objc.pathForFramework(
61             u"/System/Library/Frameworks/IOBluetooth.framework/Versions/A/Frameworks/CoreBluetooth.framework"
62         ),
63     )
64 except Exception as ex:
65     objc.loadBundle(
66         "CoreBluetooth",
67         globals(),
68         bundle_path=objc.pathForFramework(
69             u"/System/Library/Frameworks/CoreBluetooth.framework"
70         ),
71     )
72
73 BLE_PERIPHERAL_STATE_DISCONNECTED = 0
74 CBCharacteristicWriteWithResponse = 0
75 CBCharacteristicWriteWithoutResponse = 1
76
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"
83 )
84 CHROMECAST_SETUP_SERVICE_SHORT = CBUUID.UUIDWithString_(u"FEA0")
85
86
87 def _VoidPtrToCBUUID(ptr, len):
88     try:
89         ptr = ChipUtility.VoidPtrToByteArray(ptr, len)
90         ptr = ChipUtility.Hexlify(ptr)
91         ptr = (
92             ptr[:8]
93             + "-"
94             + ptr[8:12]
95             + "-"
96             + ptr[12:16]
97             + "-"
98             + ptr[16:20]
99             + "-"
100             + ptr[20:]
101         )
102         ptr = CBUUID.UUIDWithString_(ptr)
103     except Exception as ex:
104         print("ERROR: failed to convert void * to CBUUID")
105         ptr = None
106
107     return ptr
108
109 class LoopCondition:
110     def __init__(self, op, timelimit, arg = None):
111         self.op = op
112         self.due = time.time() + timelimit
113         self.arg = arg
114     
115     def TimeLimitExceeded(self):
116         return time.time() > self.due
117
118 class BlePeripheral:
119     def __init__(self, peripheral, advData):
120         self.peripheral = peripheral
121         self.advData = dict(advData)
122     
123     def __eq__(self, another):
124         return self.peripheral == another.peripheral
125
126     def getPeripheralDevIdInfo(self):
127         # CHIP_SERVICE_SHORT
128         if not self.advData:
129             return None
130         servDataDict = self.advData.get("kCBAdvDataServiceData", None)
131         if not servDataDict:
132             return 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]))
137         return None
138
139 class CoreBluetoothManager(ChipBleBase):
140     def __init__(self, devCtrl, logger=None):
141         if logger:
142             self.logger = logger
143         else:
144             self.logger = logging.getLogger("ChipBLEMgr")
145             logging.basicConfig(
146                 level=logging.INFO,
147                 format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
148             )
149         self.manager = None
150         self.peripheral = None
151         self.service = 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()
158
159         self.manager = CBCentralManager.alloc()
160         self.manager.initWithDelegate_queue_options_(self, None, None)
161
162         self.ready_condition = False
163         self.loop_condition = (
164             False  # indicates whether the cmd requirement has been met in the runloop.
165         )
166         self.connect_state = False  # reflects whether or not there is a connection.
167         self.send_condition = False
168         self.subscribe_condition = False
169
170         self.runLoopUntil(LoopCondition("ready", 10.0))
171
172         self.orig_input_hook = None
173         self.hookFuncPtr = None
174
175         self.setInputHook(self.readlineCB)
176         self.devCtrl = devCtrl
177
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]
181         )
182
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))
189
190             self.connect_state = False
191             self.loop_condition = False
192
193     def __del__(self):
194         self.disconnect()
195         self.setInputHook(self.orig_input_hook)
196         self.devCtrl.SetBlockingCB(None)
197         self.devCtrl.SetBleEventCB(None)
198
199     def devMgrCB(self):
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)
204
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)
210
211         if self.orig_input_hook:
212             self.orig_input_hook()
213
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
223
224     def shouldLoop(self, cond:LoopCondition):
225         """ Used by runLoopUntil to determine whether it should exit the runloop. """
226
227         if cond.TimeLimitExceeded():
228             return False
229
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:
235                     return False
236                 devIdInfo = peripheral.getPeripheralDevIdInfo()
237                 if devIdInfo and cond.arg and str(devIdInfo.discriminator) == cond.arg:
238                     return False
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
249
250         return True
251
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()
256         nextfire = 1
257
258         while nextfire and self.shouldLoop(cond):
259             nextfire = runLoop.limitDateForMode_(NSDefaultRunLoopMode)
260
261     def centralManagerDidUpdateState_(self, manager):
262         """ IO Bluetooth initialization is successful."""
263
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
269
270     def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(
271         self, manager, peripheral, data, rssi
272     ):
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:")
278                     self.logger.info("")
279                     self.logger.info(
280                         "{0:<16}= {1:<80}".format("Name", str(peripheral._.name))
281                     )
282                     self.logger.info(
283                         "{0:<16}= {1:<80}".format(
284                             "ID", str(peripheral._.identifier.UUIDString())
285                         )
286                     )
287                     self.logger.info("{0:<16}= {1:<80}".format("RSSI", rssi))
288                     devIdInfo = BlePeripheral(peripheral, data).getPeripheralDevIdInfo()
289                     if devIdInfo:
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))
295                     self.logger.info("")
296
297                 self.peripheral_list.append(peripheral)
298                 self.peripheral_adv_list.append(BlePeripheral(peripheral, data))
299         else:
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)]
305
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])
314
315     def centralManager_didFailToConnectPeripheral_error_(
316         self, manager, peripheral, error
317     ):
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
322
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
327         if self.devCtrl:
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()
332
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
338         else:
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")
346             else:
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
352             )
353
354     def peripheral_didDiscoverCharacteristicsForService_error_(
355         self, peripheral, service, error
356     ):
357         """Called by CoreBluetooth via runloop when a characteristic for a service is discovered."""
358         self.logger.debug(
359             "didDiscoverCharacteristicsForService:error "
360             + str(repr(peripheral))
361             + " "
362             + str(repr(service))
363         )
364         self.logger.debug(repr(service))
365         self.logger.debug(repr(error))
366
367         if not error:
368             self.characteristics[service.UUID()] = [
369                 char for char in self.service.characteristics()
370             ]
371
372             self.connect_state = True
373
374         else:
375             self.logger.error("ERROR: failed to discover characteristics for service.")
376             self.connect_state = False
377
378         self.loop_condition = True
379
380     def peripheral_didWriteValueForCharacteristic_error_(
381         self, peripheral, characteristic, error
382     ):
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())
389
390         if self.devCtrl:
391             txEvent = BleTxEvent(
392                 charId=charId, svcId=svcId, status=True if not error else False
393             )
394             self.chip_queue.put(txEvent)
395             self.devCtrl.DriveBleIO()
396
397     def peripheral_didUpdateNotificationStateForCharacteristic_error_(
398         self, peripheral, characteristic, error
399     ):
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
410         else:
411             operation = BLE_SUBSCRIBE_OPERATION_UNSUBSCRIBE
412             self.subscribe_condition = False
413
414         self.logger.debug("Operation = " + repr(operation))
415         self.logger.debug("success = " + repr(success))
416
417         if self.devCtrl:
418             subscribeEvent = BleSubscribeEvent(
419                 charId=charId, svcId=svcId, status=success, operation=operation
420             )
421             self.chip_queue.put(subscribeEvent)
422             self.devCtrl.DriveBleIO()
423
424     def peripheral_didUpdateValueForCharacteristic_error_(
425         self, peripheral, characteristic, error
426     ):
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())
433
434         # Kick Chip thread to retrieve the saved packet.
435         if self.devCtrl:
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()
440
441         self.logger.debug("received")
442         self.logger.debug(
443             "received ("
444             + str(len)
445             + ") bytes: "
446             + repr(characteristic.value().bytes().tobytes())
447         )
448
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()
453
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
466
467         return None
468
469     def scan(self, line):
470         """ API to initiatae BLE scanning for -t user_timeout seconds."""
471
472         args = self.ParseInputLine(line, "scan")
473
474         if not args:
475             return
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_(
484             [
485                 CHIP_SERVICE_SHORT,
486                 CHIP_SERVICE,
487                 CHROMECAST_SETUP_SERVICE_SHORT,
488                 CHROMECAST_SETUP_SERVICE,
489             ],
490             None,
491         )
492         # self.manager.scanForPeripheralsWithServices_options_(None, None)
493
494         self.runLoopUntil(LoopCondition("scan", args[0], args[2]))
495
496         self.manager.stopScan()
497         self.logger.info("scanning stopped")
498
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_(
507             [
508                 CHIP_SERVICE_SHORT,
509                 CHIP_SERVICE,
510                 CHROMECAST_SETUP_SERVICE_SHORT,
511                 CHROMECAST_SETUP_SERVICE,
512             ],
513             None,
514         )
515
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")
521
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")
527         else:
528             self.logger.setLevel(logging.INFO)
529             self.logger.info("current logging level is info")
530         return True
531
532     def CloseBle(self, connObj):
533         """ Called by Chip to close the BLE connection."""
534         if self.peripheral:
535             self.manager.cancelPeripheralConnection_(self.peripheral)
536             self.characteristics = {}
537             # del self.peripheral_list[:]
538             # self.peripheral_list = []
539             self.peripheral = None
540             self.service = None
541             self.connect_state = False
542
543         return True
544
545     def updateCharacteristic(self, bytes, svcId, charId):
546         # TODO: implement this for Peripheral support.
547         return False