/* -*- Mode: C; tab-width: 4 -*- * * Copyright (c) 2015-2016 Apple Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #if ENABLE_BLE_TRIGGERED_BONJOUR #include "mDNSEmbeddedAPI.h" #include "DNSCommon.h" #import #import #import #import "mDNSMacOSX.h" #import "BLE.h" #import "coreBLE.h" static coreBLE * coreBLEptr; // Call Bluetooth subsystem to start/stop the the Bonjour BLE beacon and // beacon scanning based on the current Bloom filter. void updateBLEBeacon(serviceHash_t bloomFilter) { if (coreBLEptr == 0) coreBLEptr = [[coreBLE alloc] init]; LogInfo("updateBLEBeacon: bloomFilter = 0x%lx", bloomFilter); [coreBLEptr updateBeacon:bloomFilter]; } // Stop the current BLE beacon. void stopBLEBeacon(void) { if (coreBLEptr == 0) coreBLEptr = [[coreBLE alloc] init]; [coreBLEptr stopBeacon]; } bool currentlyBeaconing(void) { if (coreBLEptr == 0) coreBLEptr = [[coreBLE alloc] init]; return [coreBLEptr isBeaconing]; } // Start the scan. void startBLEScan(void) { if (coreBLEptr == 0) coreBLEptr = [[coreBLE alloc] init]; [coreBLEptr startScan]; } // Stop the scan. void stopBLEScan(void) { if (coreBLEptr == 0) coreBLEptr = [[coreBLE alloc] init]; [coreBLEptr stopScan]; } @implementation coreBLE { CBCentralManager *_centralManager; CBPeripheralManager *_peripheralManager; NSData *_currentlyAdvertisedData; // [_centralManager isScanning] is only available on iOS and not OSX, // so track scanning state locally. BOOL _isScanning; BOOL _centralManagerIsOn; BOOL _peripheralManagerIsOn; } - (id)init { self = [super init]; if (self) { _centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:dispatch_get_main_queue()]; _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:dispatch_get_main_queue()]; _currentlyAdvertisedData = nil; _isScanning = NO; _centralManagerIsOn = NO; _peripheralManagerIsOn = NO; if (_centralManager == nil || _peripheralManager == nil ) { LogMsg("coreBLE initialization failed!"); } else { LogInfo("coreBLE initialized"); } } return self; } #define ADVERTISEMENTDATALENGTH 28 // 31 - 3 (3 bytes for flags) // TODO: // Define DBDeviceTypeBonjour for prototyping until we move to the TDS beacon format. // The Bluetooth team recommended using a value < 32 for prototyping, since 32 is the number of // beacon types they can track in their duplicate beacon filtering logic. #define DBDeviceTypeBonjour 26 // Beacon flags and version byte #define BonjourBLEVersion 1 extern mDNS mDNSStorage; extern mDNSInterfaceID AWDLInterfaceID; // Transmit the last beacon indicating we are no longer advertising or browsing any services for two seconds. #define LastBeaconTime 2 - (void) updateBeacon:(serviceHash_t) bloomFilter { uint8_t advertisingData[ADVERTISEMENTDATALENGTH] = {0, 0xff, 0x4c, 0x00 }; uint8_t advertisingLength = 4; // If no longer browsing or advertising, beacon this state for 'LastBeaconTime' seconds // so that peers have a chance to notice the state change. if (bloomFilter == 0) { LogInfo("updateBeacon: Stopping beacon in %d seconds", LastBeaconTime); if (mDNSStorage.timenow == 0) { // This should never happen since all calling code paths should have called mDNS_Lock(), which // initializes the mDNSStorage.timenow value. LogMsg("updateBeacon: NOTE, timenow == 0 ??"); } mDNSStorage.NextBLEServiceTime = NonZeroTime(mDNSStorage.timenow + (LastBeaconTime * mDNSPlatformOneSecond)); finalBeacon = true; } else { // Cancel any pending final beacon processing. finalBeacon = false; } // The beacon type. advertisingData[advertisingLength++] = DBDeviceTypeBonjour; // Flags and Version field advertisingData[advertisingLength++] = BonjourBLEVersion; memcpy(& advertisingData[advertisingLength], & bloomFilter, sizeof(serviceHash_t)); advertisingLength += sizeof(serviceHash_t); // Add the MAC address of the awdl0 interface. Don't cache it since // it can get updated periodically. if (AWDLInterfaceID) { NetworkInterfaceInfoOSX *intf = IfindexToInterfaceInfoOSX(AWDLInterfaceID); if (intf) memcpy(& advertisingData[advertisingLength], & intf->ifinfo.MAC, sizeof(mDNSEthAddr)); else memset( & advertisingData[advertisingLength], 0, sizeof(mDNSEthAddr)); } else { // Just use zero if not avaiblable. memset( & advertisingData[advertisingLength], 0, sizeof(mDNSEthAddr)); } advertisingLength += sizeof(mDNSEthAddr); // Total length of data advertised, minus this length byte. advertisingData[0] = (advertisingLength - 1); LogInfo("updateBeacon: advertisingLength = %d", advertisingLength); if (_currentlyAdvertisedData) [_currentlyAdvertisedData release]; _currentlyAdvertisedData = [[NSData alloc] initWithBytes:advertisingData length:advertisingLength]; [self startBeacon]; } - (void) startBeacon { if (!_peripheralManagerIsOn) { LogInfo("startBeacon: Not starting beacon, CBPeripheralManager not powered on"); return; } if (_currentlyAdvertisedData == nil) { LogInfo("startBeacon: Not starting beacon, no data to advertise"); return; } if ([_peripheralManager isAdvertising]) { LogInfo("startBeacon: Stop current beacon transmission before restarting"); [_peripheralManager stopAdvertising]; } LogInfo("startBeacon: Starting beacon"); #if 0 // Move to this code during Fall 2018 develelopment if still using these APIs. [_peripheralManager startAdvertising:@{ CBAdvertisementDataAppleMfgData : _currentlyAdvertisedData, CBManagerIsPrivilegedDaemonKey : @YES, @"kCBAdvOptionUseFGInterval" : @YES }]; #else // While CBCentralManagerScanOptionIsPrivilegedDaemonKey is deprecated in current MobileBluetooth project, it's still defined in the current and // previous train SDKs. Suppress deprecated warning for now since we intend to move to a different Bluetooth API to manage the BLE Triggered Bonjour // beacons when this code is enabled by default. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" [_peripheralManager startAdvertising:@{ CBAdvertisementDataAppleMfgData : _currentlyAdvertisedData, CBCentralManagerScanOptionIsPrivilegedDaemonKey : @YES, @"kCBAdvOptionUseFGInterval" : @YES }]; #pragma GCC diagnostic pop #endif } - (bool) isBeaconing { return (_currentlyAdvertisedData != nil); } - (void) stopBeacon { if (!_peripheralManagerIsOn) { LogInfo("stopBeacon: CBPeripheralManager is not powered on"); return; } // Only beaconing if we have advertised data to send. if (_currentlyAdvertisedData) { LogInfo("stoptBeacon: Stopping beacon"); [_peripheralManager stopAdvertising]; [_currentlyAdvertisedData release]; _currentlyAdvertisedData = nil; } else LogInfo("stoptBeacon: Note currently beaconing"); } - (void) startScan { if (!_centralManagerIsOn) { LogInfo("startScan: Not starting scan, CBCentralManager is not powered on"); return; } if (_isScanning) { LogInfo("startScan: already scanning, stopping scan before restarting"); [_centralManager stopScan]; } LogInfo("startScan: Starting scan"); _isScanning = YES; #if 0 // Move to this code during Fall 2018 develelopment if still using these APIs. [_centralManager scanForPeripheralsWithServices:nil options:@{ CBCentralManagerScanOptionAllowDuplicatesKey : @YES , CBManagerIsPrivilegedDaemonKey : @YES}]; #else // While CBCentralManagerScanOptionIsPrivilegedDaemonKey is deprecated in current MobileBluetooth project, it's still defined in the current and // previous train SDKs. Suppress deprecated warning for now since we intend to move to a different Bluetooth API to manage the BLE Triggered Bonjour // beacons when this code is enabled by default. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" [_centralManager scanForPeripheralsWithServices:nil options:@{ CBCentralManagerScanOptionAllowDuplicatesKey : @YES , CBCentralManagerScanOptionIsPrivilegedDaemonKey : @YES}]; #pragma GCC diagnostic pop #endif } - (void) stopScan { if (!_centralManagerIsOn) { LogInfo("stopScan: Not stopping scan, CBCentralManager is not powered on"); return; } if (_isScanning) { LogInfo("stopScan: Stopping scan"); [_centralManager stopScan]; _isScanning = NO; } else { LogInfo("stopScan: Not currently scanning"); } } #pragma mark - CBCentralManagerDelegate protocol - (void)centralManagerDidUpdateState:(CBCentralManager *)central { switch (central.state) { case CBManagerStateUnknown: LogInfo("centralManagerDidUpdateState: CBManagerStateUnknown"); break; case CBManagerStateResetting: LogInfo("centralManagerDidUpdateState: CBManagerStateResetting"); break; case CBManagerStateUnsupported: LogInfo("centralManagerDidUpdateState: CBManagerStateUnsupported"); break; case CBManagerStateUnauthorized: LogInfo("centralManagerDidUpdateState: CBManagerStateUnauthorized"); break; case CBManagerStatePoweredOff: LogInfo("centralManagerDidUpdateState: CBManagerStatePoweredOff"); break; case CBManagerStatePoweredOn: // Hold lock to synchronize with main thread from this callback thread. KQueueLock(); LogInfo("centralManagerDidUpdateState: CBManagerStatePoweredOn"); _centralManagerIsOn = YES; // Only start scan if we have data we will be transmitting or if "suppressBeacons" // is set, indicating we should be scanning, but not beaconing. if (_currentlyAdvertisedData || suppressBeacons) [self startScan]; else LogInfo("centralManagerDidUpdateState:: Not starting scan"); KQueueUnlock("CBManagerStatePoweredOn"); break; default: LogInfo("centralManagerDidUpdateState: Unknown state ??"); break; } } #define beaconTypeByteIndex 2 // Offset of beacon type in received CBAdvertisementDataManufacturerDataKey byte array. #define beaconDataLength 18 // Total number of bytes in the CBAdvertisementDataManufacturerDataKey. - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI { (void) central; (void) peripheral; (void) RSSI; NSData *data = [advertisementData objectForKey:CBAdvertisementDataManufacturerDataKey]; // Just return if the beacon data does not match what we are looking for. if (!data || ([data length] != beaconDataLength)) { return; } unsigned char *bytes = (unsigned char *)data.bytes; // Just parse the DBDeviceTypeBonjour beacons. if (bytes[beaconTypeByteIndex] == DBDeviceTypeBonjour) { serviceHash_t peerBloomFilter; mDNSEthAddr peerMAC; unsigned char flagsAndVersion; unsigned char *ptr; #if VERBOSE_BLE_DEBUG LogInfo("didDiscoverPeripheral: received DBDeviceTypeBonjour beacon, length = %d", [data length]); LogInfo("didDiscoverPeripheral: central = 0x%x, peripheral = 0x%x", central, peripheral); #endif // VERBOSE_BLE_DEBUG // The DBDeviceTypeBonjour beacon bytes will be: // 0x4C (1 byte), 0x0 (1 byte), DBDeviceTypeBonjour byte, flags and version byte, 8 byte Bloom filter, // 6 byte sender AWDL MAC address ptr = & bytes[beaconTypeByteIndex + 1]; flagsAndVersion = *ptr++; memcpy(& peerBloomFilter, ptr, sizeof(serviceHash_t)); ptr += sizeof(serviceHash_t); memcpy(& peerMAC, ptr, sizeof(peerMAC)); #if VERBOSE_BLE_DEBUG LogInfo("didDiscoverPeripheral: version = 0x%x, peerBloomFilter = 0x%x", flagsAndVersion, peerBloomFilter); LogInfo("didDiscoverPeripheral: sender MAC = 0x%x 0x%x 0x%x 0x%x 0x%x 0x%x", peerMAC.b[0], peerMAC.b[1], peerMAC.b[2], peerMAC.b[3], peerMAC.b[4], peerMAC.b[5]); #else (void)flagsAndVersion; // Unused #endif // VERBOSE_BLE_DEBUG responseReceived(peerBloomFilter, & peerMAC); } } #pragma mark - CBPeripheralManagerDelegate protocol - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral { switch (peripheral.state) { case CBManagerStateUnknown: LogInfo("peripheralManagerDidUpdateState: CBManagerStateUnknown"); break; case CBManagerStateResetting: LogInfo("peripheralManagerDidUpdateState: CBManagerStateResetting"); break; case CBManagerStateUnsupported: LogInfo("peripheralManagerDidUpdateState: CBManagerStateUnsupported"); break; case CBManagerStateUnauthorized: LogInfo("peripheralManagerDidUpdateState: CBManagerStateUnauthorized"); break; case CBManagerStatePoweredOff: LogInfo("peripheralManagerDidUpdateState: CBManagerStatePoweredOff"); break; case CBManagerStatePoweredOn: // Hold lock to synchronize with main thread from this callback thread. KQueueLock(); LogInfo("peripheralManagerDidUpdateState: CBManagerStatePoweredOn"); _peripheralManagerIsOn = YES; // Start beaconing if we have initialized beacon data to send. if (_currentlyAdvertisedData) [self startBeacon]; KQueueUnlock("CBManagerStatePoweredOn"); break; default: LogInfo("peripheralManagerDidUpdateState: Unknown state ??"); break; } } - (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(nullable NSError *)error { (void) peripheral; if (error) { const char * errorString = [[error localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]; LogInfo("peripheralManagerDidStartAdvertising: error = %s", errorString ? errorString: "unknown"); } else { LogInfo("peripheralManagerDidStartAdvertising:"); } } @end #endif // ENABLE_BLE_TRIGGERED_BONJOUR