1 | /* -*- Mode: C; tab-width: 4 -*- |
---|
2 | * |
---|
3 | * Copyright (c) 2015-2016 Apple Inc. All rights reserved. |
---|
4 | * |
---|
5 | * Licensed under the Apache License, Version 2.0 (the "License"); |
---|
6 | * you may not use this file except in compliance with the License. |
---|
7 | * You may obtain a copy of the License at |
---|
8 | * |
---|
9 | * http://www.apache.org/licenses/LICENSE-2.0 |
---|
10 | * |
---|
11 | * Unless required by applicable law or agreed to in writing, software |
---|
12 | * distributed under the License is distributed on an "AS IS" BASIS, |
---|
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
---|
14 | * See the License for the specific language governing permissions and |
---|
15 | * limitations under the License. |
---|
16 | */ |
---|
17 | |
---|
18 | #if ENABLE_BLE_TRIGGERED_BONJOUR |
---|
19 | |
---|
20 | #include "mDNSEmbeddedAPI.h" |
---|
21 | #include "DNSCommon.h" |
---|
22 | |
---|
23 | #import <Foundation/Foundation.h> |
---|
24 | #import <CoreBluetooth/CoreBluetooth.h> |
---|
25 | #import <CoreBluetooth/CoreBluetooth_Private.h> |
---|
26 | #import "mDNSMacOSX.h" |
---|
27 | #import "BLE.h" |
---|
28 | #import "coreBLE.h" |
---|
29 | |
---|
30 | static coreBLE * coreBLEptr; |
---|
31 | |
---|
32 | // Call Bluetooth subsystem to start/stop the the Bonjour BLE beacon and |
---|
33 | // beacon scanning based on the current Bloom filter. |
---|
34 | void updateBLEBeacon(serviceHash_t bloomFilter) |
---|
35 | { |
---|
36 | if (coreBLEptr == 0) |
---|
37 | coreBLEptr = [[coreBLE alloc] init]; |
---|
38 | |
---|
39 | LogInfo("updateBLEBeacon: bloomFilter = 0x%lx", bloomFilter); |
---|
40 | |
---|
41 | [coreBLEptr updateBeacon:bloomFilter]; |
---|
42 | } |
---|
43 | |
---|
44 | // Stop the current BLE beacon. |
---|
45 | void stopBLEBeacon(void) |
---|
46 | { |
---|
47 | if (coreBLEptr == 0) |
---|
48 | coreBLEptr = [[coreBLE alloc] init]; |
---|
49 | |
---|
50 | [coreBLEptr stopBeacon]; |
---|
51 | } |
---|
52 | |
---|
53 | bool currentlyBeaconing(void) |
---|
54 | { |
---|
55 | if (coreBLEptr == 0) |
---|
56 | coreBLEptr = [[coreBLE alloc] init]; |
---|
57 | |
---|
58 | return [coreBLEptr isBeaconing]; |
---|
59 | } |
---|
60 | |
---|
61 | // Start the scan. |
---|
62 | void startBLEScan(void) |
---|
63 | { |
---|
64 | if (coreBLEptr == 0) |
---|
65 | coreBLEptr = [[coreBLE alloc] init]; |
---|
66 | [coreBLEptr startScan]; |
---|
67 | } |
---|
68 | |
---|
69 | // Stop the scan. |
---|
70 | void stopBLEScan(void) |
---|
71 | { |
---|
72 | if (coreBLEptr == 0) |
---|
73 | coreBLEptr = [[coreBLE alloc] init]; |
---|
74 | |
---|
75 | [coreBLEptr stopScan]; |
---|
76 | } |
---|
77 | |
---|
78 | @implementation coreBLE |
---|
79 | { |
---|
80 | CBCentralManager *_centralManager; |
---|
81 | CBPeripheralManager *_peripheralManager; |
---|
82 | |
---|
83 | NSData *_currentlyAdvertisedData; |
---|
84 | |
---|
85 | // [_centralManager isScanning] is only available on iOS and not OSX, |
---|
86 | // so track scanning state locally. |
---|
87 | BOOL _isScanning; |
---|
88 | BOOL _centralManagerIsOn; |
---|
89 | BOOL _peripheralManagerIsOn; |
---|
90 | } |
---|
91 | |
---|
92 | - (id)init |
---|
93 | { |
---|
94 | self = [super init]; |
---|
95 | |
---|
96 | if (self) |
---|
97 | { |
---|
98 | _centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:dispatch_get_main_queue()]; |
---|
99 | _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:dispatch_get_main_queue()]; |
---|
100 | _currentlyAdvertisedData = nil; |
---|
101 | _isScanning = NO; |
---|
102 | _centralManagerIsOn = NO; |
---|
103 | _peripheralManagerIsOn = NO; |
---|
104 | |
---|
105 | if (_centralManager == nil || _peripheralManager == nil ) |
---|
106 | { |
---|
107 | LogMsg("coreBLE initialization failed!"); |
---|
108 | } |
---|
109 | else |
---|
110 | { |
---|
111 | LogInfo("coreBLE initialized"); |
---|
112 | } |
---|
113 | } |
---|
114 | |
---|
115 | return self; |
---|
116 | } |
---|
117 | |
---|
118 | #define ADVERTISEMENTDATALENGTH 28 // 31 - 3 (3 bytes for flags) |
---|
119 | |
---|
120 | // TODO: |
---|
121 | // Define DBDeviceTypeBonjour for prototyping until we move to the TDS beacon format. |
---|
122 | // The Bluetooth team recommended using a value < 32 for prototyping, since 32 is the number of |
---|
123 | // beacon types they can track in their duplicate beacon filtering logic. |
---|
124 | #define DBDeviceTypeBonjour 26 |
---|
125 | |
---|
126 | // Beacon flags and version byte |
---|
127 | #define BonjourBLEVersion 1 |
---|
128 | |
---|
129 | extern mDNS mDNSStorage; |
---|
130 | extern mDNSInterfaceID AWDLInterfaceID; |
---|
131 | |
---|
132 | // Transmit the last beacon indicating we are no longer advertising or browsing any services for two seconds. |
---|
133 | #define LastBeaconTime 2 |
---|
134 | |
---|
135 | - (void) updateBeacon:(serviceHash_t) bloomFilter |
---|
136 | { |
---|
137 | uint8_t advertisingData[ADVERTISEMENTDATALENGTH] = {0, 0xff, 0x4c, 0x00 }; |
---|
138 | uint8_t advertisingLength = 4; |
---|
139 | |
---|
140 | // If no longer browsing or advertising, beacon this state for 'LastBeaconTime' seconds |
---|
141 | // so that peers have a chance to notice the state change. |
---|
142 | if (bloomFilter == 0) |
---|
143 | { |
---|
144 | LogInfo("updateBeacon: Stopping beacon in %d seconds", LastBeaconTime); |
---|
145 | |
---|
146 | if (mDNSStorage.timenow == 0) |
---|
147 | { |
---|
148 | // This should never happen since all calling code paths should have called mDNS_Lock(), which |
---|
149 | // initializes the mDNSStorage.timenow value. |
---|
150 | LogMsg("updateBeacon: NOTE, timenow == 0 ??"); |
---|
151 | } |
---|
152 | |
---|
153 | mDNSStorage.NextBLEServiceTime = NonZeroTime(mDNSStorage.timenow + (LastBeaconTime * mDNSPlatformOneSecond)); |
---|
154 | finalBeacon = true; |
---|
155 | } |
---|
156 | else |
---|
157 | { |
---|
158 | // Cancel any pending final beacon processing. |
---|
159 | finalBeacon = false; |
---|
160 | } |
---|
161 | |
---|
162 | // The beacon type. |
---|
163 | advertisingData[advertisingLength++] = DBDeviceTypeBonjour; |
---|
164 | |
---|
165 | // Flags and Version field |
---|
166 | advertisingData[advertisingLength++] = BonjourBLEVersion; |
---|
167 | |
---|
168 | memcpy(& advertisingData[advertisingLength], & bloomFilter, sizeof(serviceHash_t)); |
---|
169 | advertisingLength += sizeof(serviceHash_t); |
---|
170 | |
---|
171 | // Add the MAC address of the awdl0 interface. Don't cache it since |
---|
172 | // it can get updated periodically. |
---|
173 | if (AWDLInterfaceID) |
---|
174 | { |
---|
175 | NetworkInterfaceInfoOSX *intf = IfindexToInterfaceInfoOSX(AWDLInterfaceID); |
---|
176 | if (intf) |
---|
177 | memcpy(& advertisingData[advertisingLength], & intf->ifinfo.MAC, sizeof(mDNSEthAddr)); |
---|
178 | else |
---|
179 | memset( & advertisingData[advertisingLength], 0, sizeof(mDNSEthAddr)); |
---|
180 | } |
---|
181 | else |
---|
182 | { |
---|
183 | // Just use zero if not avaiblable. |
---|
184 | memset( & advertisingData[advertisingLength], 0, sizeof(mDNSEthAddr)); |
---|
185 | } |
---|
186 | advertisingLength += sizeof(mDNSEthAddr); |
---|
187 | |
---|
188 | // Total length of data advertised, minus this length byte. |
---|
189 | advertisingData[0] = (advertisingLength - 1); |
---|
190 | |
---|
191 | LogInfo("updateBeacon: advertisingLength = %d", advertisingLength); |
---|
192 | |
---|
193 | if (_currentlyAdvertisedData) |
---|
194 | [_currentlyAdvertisedData release]; |
---|
195 | _currentlyAdvertisedData = [[NSData alloc] initWithBytes:advertisingData length:advertisingLength]; |
---|
196 | [self startBeacon]; |
---|
197 | } |
---|
198 | |
---|
199 | - (void) startBeacon |
---|
200 | { |
---|
201 | if (!_peripheralManagerIsOn) |
---|
202 | { |
---|
203 | LogInfo("startBeacon: Not starting beacon, CBPeripheralManager not powered on"); |
---|
204 | return; |
---|
205 | } |
---|
206 | |
---|
207 | if (_currentlyAdvertisedData == nil) |
---|
208 | { |
---|
209 | LogInfo("startBeacon: Not starting beacon, no data to advertise"); |
---|
210 | return; |
---|
211 | } |
---|
212 | |
---|
213 | if ([_peripheralManager isAdvertising]) |
---|
214 | { |
---|
215 | LogInfo("startBeacon: Stop current beacon transmission before restarting"); |
---|
216 | [_peripheralManager stopAdvertising]; |
---|
217 | } |
---|
218 | LogInfo("startBeacon: Starting beacon"); |
---|
219 | |
---|
220 | #if 0 // Move to this code during Fall 2018 develelopment if still using these APIs. |
---|
221 | [_peripheralManager startAdvertising:@{ CBAdvertisementDataAppleMfgData : _currentlyAdvertisedData, CBManagerIsPrivilegedDaemonKey : @YES, @"kCBAdvOptionUseFGInterval" : @YES }]; |
---|
222 | #else |
---|
223 | // While CBCentralManagerScanOptionIsPrivilegedDaemonKey is deprecated in current MobileBluetooth project, it's still defined in the current and |
---|
224 | // previous train SDKs. Suppress deprecated warning for now since we intend to move to a different Bluetooth API to manage the BLE Triggered Bonjour |
---|
225 | // beacons when this code is enabled by default. |
---|
226 | #pragma GCC diagnostic push |
---|
227 | #pragma GCC diagnostic ignored "-Wdeprecated-declarations" |
---|
228 | [_peripheralManager startAdvertising:@{ CBAdvertisementDataAppleMfgData : _currentlyAdvertisedData, CBCentralManagerScanOptionIsPrivilegedDaemonKey : @YES, @"kCBAdvOptionUseFGInterval" : @YES }]; |
---|
229 | #pragma GCC diagnostic pop |
---|
230 | #endif |
---|
231 | } |
---|
232 | |
---|
233 | - (bool) isBeaconing |
---|
234 | { |
---|
235 | return (_currentlyAdvertisedData != nil); |
---|
236 | } |
---|
237 | |
---|
238 | - (void) stopBeacon |
---|
239 | { |
---|
240 | if (!_peripheralManagerIsOn) |
---|
241 | { |
---|
242 | LogInfo("stopBeacon: CBPeripheralManager is not powered on"); |
---|
243 | return; |
---|
244 | } |
---|
245 | |
---|
246 | // Only beaconing if we have advertised data to send. |
---|
247 | if (_currentlyAdvertisedData) |
---|
248 | { |
---|
249 | LogInfo("stoptBeacon: Stopping beacon"); |
---|
250 | [_peripheralManager stopAdvertising]; |
---|
251 | [_currentlyAdvertisedData release]; |
---|
252 | _currentlyAdvertisedData = nil; |
---|
253 | } |
---|
254 | else |
---|
255 | LogInfo("stoptBeacon: Note currently beaconing"); |
---|
256 | } |
---|
257 | |
---|
258 | - (void) startScan |
---|
259 | { |
---|
260 | if (!_centralManagerIsOn) |
---|
261 | { |
---|
262 | LogInfo("startScan: Not starting scan, CBCentralManager is not powered on"); |
---|
263 | return; |
---|
264 | } |
---|
265 | |
---|
266 | if (_isScanning) |
---|
267 | { |
---|
268 | LogInfo("startScan: already scanning, stopping scan before restarting"); |
---|
269 | [_centralManager stopScan]; |
---|
270 | } |
---|
271 | |
---|
272 | LogInfo("startScan: Starting scan"); |
---|
273 | |
---|
274 | _isScanning = YES; |
---|
275 | |
---|
276 | #if 0 // Move to this code during Fall 2018 develelopment if still using these APIs. |
---|
277 | [_centralManager scanForPeripheralsWithServices:nil options:@{ CBCentralManagerScanOptionAllowDuplicatesKey : @YES , CBManagerIsPrivilegedDaemonKey : @YES}]; |
---|
278 | #else |
---|
279 | // While CBCentralManagerScanOptionIsPrivilegedDaemonKey is deprecated in current MobileBluetooth project, it's still defined in the current and |
---|
280 | // previous train SDKs. Suppress deprecated warning for now since we intend to move to a different Bluetooth API to manage the BLE Triggered Bonjour |
---|
281 | // beacons when this code is enabled by default. |
---|
282 | #pragma GCC diagnostic push |
---|
283 | #pragma GCC diagnostic ignored "-Wdeprecated-declarations" |
---|
284 | [_centralManager scanForPeripheralsWithServices:nil options:@{ CBCentralManagerScanOptionAllowDuplicatesKey : @YES , CBCentralManagerScanOptionIsPrivilegedDaemonKey : @YES}]; |
---|
285 | #pragma GCC diagnostic pop |
---|
286 | #endif |
---|
287 | } |
---|
288 | |
---|
289 | - (void) stopScan |
---|
290 | { |
---|
291 | if (!_centralManagerIsOn) |
---|
292 | { |
---|
293 | LogInfo("stopScan: Not stopping scan, CBCentralManager is not powered on"); |
---|
294 | return; |
---|
295 | } |
---|
296 | |
---|
297 | if (_isScanning) |
---|
298 | { |
---|
299 | LogInfo("stopScan: Stopping scan"); |
---|
300 | [_centralManager stopScan]; |
---|
301 | _isScanning = NO; |
---|
302 | } |
---|
303 | else |
---|
304 | { |
---|
305 | LogInfo("stopScan: Not currently scanning"); |
---|
306 | } |
---|
307 | } |
---|
308 | |
---|
309 | #pragma mark - CBCentralManagerDelegate protocol |
---|
310 | |
---|
311 | - (void)centralManagerDidUpdateState:(CBCentralManager *)central |
---|
312 | { |
---|
313 | switch (central.state) { |
---|
314 | case CBManagerStateUnknown: |
---|
315 | LogInfo("centralManagerDidUpdateState: CBManagerStateUnknown"); |
---|
316 | break; |
---|
317 | |
---|
318 | case CBManagerStateResetting: |
---|
319 | LogInfo("centralManagerDidUpdateState: CBManagerStateResetting"); |
---|
320 | break; |
---|
321 | |
---|
322 | case CBManagerStateUnsupported: |
---|
323 | LogInfo("centralManagerDidUpdateState: CBManagerStateUnsupported"); |
---|
324 | break; |
---|
325 | |
---|
326 | case CBManagerStateUnauthorized: |
---|
327 | LogInfo("centralManagerDidUpdateState: CBManagerStateUnauthorized"); |
---|
328 | break; |
---|
329 | |
---|
330 | case CBManagerStatePoweredOff: |
---|
331 | LogInfo("centralManagerDidUpdateState: CBManagerStatePoweredOff"); |
---|
332 | break; |
---|
333 | |
---|
334 | case CBManagerStatePoweredOn: |
---|
335 | // Hold lock to synchronize with main thread from this callback thread. |
---|
336 | KQueueLock(); |
---|
337 | |
---|
338 | LogInfo("centralManagerDidUpdateState: CBManagerStatePoweredOn"); |
---|
339 | _centralManagerIsOn = YES; |
---|
340 | // Only start scan if we have data we will be transmitting or if "suppressBeacons" |
---|
341 | // is set, indicating we should be scanning, but not beaconing. |
---|
342 | if (_currentlyAdvertisedData || suppressBeacons) |
---|
343 | [self startScan]; |
---|
344 | else |
---|
345 | LogInfo("centralManagerDidUpdateState:: Not starting scan"); |
---|
346 | |
---|
347 | KQueueUnlock("CBManagerStatePoweredOn"); |
---|
348 | break; |
---|
349 | |
---|
350 | default: |
---|
351 | LogInfo("centralManagerDidUpdateState: Unknown state ??"); |
---|
352 | break; |
---|
353 | } |
---|
354 | } |
---|
355 | |
---|
356 | #define beaconTypeByteIndex 2 // Offset of beacon type in received CBAdvertisementDataManufacturerDataKey byte array. |
---|
357 | #define beaconDataLength 18 // Total number of bytes in the CBAdvertisementDataManufacturerDataKey. |
---|
358 | |
---|
359 | - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI |
---|
360 | { |
---|
361 | (void) central; |
---|
362 | (void) peripheral; |
---|
363 | (void) RSSI; |
---|
364 | |
---|
365 | NSData *data = [advertisementData objectForKey:CBAdvertisementDataManufacturerDataKey]; |
---|
366 | |
---|
367 | // Just return if the beacon data does not match what we are looking for. |
---|
368 | if (!data || ([data length] != beaconDataLength)) |
---|
369 | { |
---|
370 | return; |
---|
371 | } |
---|
372 | |
---|
373 | unsigned char *bytes = (unsigned char *)data.bytes; |
---|
374 | |
---|
375 | // Just parse the DBDeviceTypeBonjour beacons. |
---|
376 | if (bytes[beaconTypeByteIndex] == DBDeviceTypeBonjour) |
---|
377 | { |
---|
378 | serviceHash_t peerBloomFilter; |
---|
379 | mDNSEthAddr peerMAC; |
---|
380 | unsigned char flagsAndVersion; |
---|
381 | unsigned char *ptr; |
---|
382 | |
---|
383 | #if VERBOSE_BLE_DEBUG |
---|
384 | LogInfo("didDiscoverPeripheral: received DBDeviceTypeBonjour beacon, length = %d", [data length]); |
---|
385 | LogInfo("didDiscoverPeripheral: central = 0x%x, peripheral = 0x%x", central, peripheral); |
---|
386 | #endif // VERBOSE_BLE_DEBUG |
---|
387 | |
---|
388 | // The DBDeviceTypeBonjour beacon bytes will be: |
---|
389 | // 0x4C (1 byte), 0x0 (1 byte), DBDeviceTypeBonjour byte, flags and version byte, 8 byte Bloom filter, |
---|
390 | // 6 byte sender AWDL MAC address |
---|
391 | |
---|
392 | ptr = & bytes[beaconTypeByteIndex + 1]; |
---|
393 | flagsAndVersion = *ptr++; |
---|
394 | memcpy(& peerBloomFilter, ptr, sizeof(serviceHash_t)); |
---|
395 | ptr += sizeof(serviceHash_t); |
---|
396 | memcpy(& peerMAC, ptr, sizeof(peerMAC)); |
---|
397 | |
---|
398 | #if VERBOSE_BLE_DEBUG |
---|
399 | LogInfo("didDiscoverPeripheral: version = 0x%x, peerBloomFilter = 0x%x", |
---|
400 | flagsAndVersion, peerBloomFilter); |
---|
401 | LogInfo("didDiscoverPeripheral: sender MAC = 0x%x 0x%x 0x%x 0x%x 0x%x 0x%x", |
---|
402 | peerMAC.b[0], peerMAC.b[1], peerMAC.b[2], peerMAC.b[3], peerMAC.b[4], peerMAC.b[5]); |
---|
403 | #else |
---|
404 | (void)flagsAndVersion; // Unused |
---|
405 | #endif // VERBOSE_BLE_DEBUG |
---|
406 | |
---|
407 | responseReceived(peerBloomFilter, & peerMAC); |
---|
408 | } |
---|
409 | } |
---|
410 | |
---|
411 | #pragma mark - CBPeripheralManagerDelegate protocol |
---|
412 | |
---|
413 | - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral |
---|
414 | { |
---|
415 | |
---|
416 | switch (peripheral.state) { |
---|
417 | case CBManagerStateUnknown: |
---|
418 | LogInfo("peripheralManagerDidUpdateState: CBManagerStateUnknown"); |
---|
419 | break; |
---|
420 | |
---|
421 | case CBManagerStateResetting: |
---|
422 | LogInfo("peripheralManagerDidUpdateState: CBManagerStateResetting"); |
---|
423 | break; |
---|
424 | |
---|
425 | case CBManagerStateUnsupported: |
---|
426 | LogInfo("peripheralManagerDidUpdateState: CBManagerStateUnsupported"); |
---|
427 | break; |
---|
428 | |
---|
429 | case CBManagerStateUnauthorized: |
---|
430 | LogInfo("peripheralManagerDidUpdateState: CBManagerStateUnauthorized"); |
---|
431 | break; |
---|
432 | |
---|
433 | case CBManagerStatePoweredOff: |
---|
434 | LogInfo("peripheralManagerDidUpdateState: CBManagerStatePoweredOff"); |
---|
435 | break; |
---|
436 | |
---|
437 | case CBManagerStatePoweredOn: |
---|
438 | // Hold lock to synchronize with main thread from this callback thread. |
---|
439 | KQueueLock(); |
---|
440 | |
---|
441 | LogInfo("peripheralManagerDidUpdateState: CBManagerStatePoweredOn"); |
---|
442 | _peripheralManagerIsOn = YES; |
---|
443 | |
---|
444 | // Start beaconing if we have initialized beacon data to send. |
---|
445 | if (_currentlyAdvertisedData) |
---|
446 | [self startBeacon]; |
---|
447 | |
---|
448 | KQueueUnlock("CBManagerStatePoweredOn"); |
---|
449 | break; |
---|
450 | |
---|
451 | default: |
---|
452 | LogInfo("peripheralManagerDidUpdateState: Unknown state ??"); |
---|
453 | break; |
---|
454 | } |
---|
455 | } |
---|
456 | |
---|
457 | - (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(nullable NSError *)error |
---|
458 | { |
---|
459 | (void) peripheral; |
---|
460 | |
---|
461 | if (error) |
---|
462 | { |
---|
463 | const char * errorString = [[error localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]; |
---|
464 | LogInfo("peripheralManagerDidStartAdvertising: error = %s", errorString ? errorString: "unknown"); |
---|
465 | } |
---|
466 | else |
---|
467 | { |
---|
468 | LogInfo("peripheralManagerDidStartAdvertising:"); |
---|
469 | } |
---|
470 | } |
---|
471 | |
---|
472 | @end |
---|
473 | #endif // ENABLE_BLE_TRIGGERED_BONJOUR |
---|