最近新進一家公司,主要是做物聯(lián)網(wǎng)這一塊的的,項目需要用到藍牙開發(fā),講真的,挑戰(zhàn)還是挺大的,做了差不多四年的iOS開發(fā),從沒有接觸過藍牙開發(fā)這一領(lǐng)域,我是這樣學習的。
從網(wǎng)上找各種博客(國內(nèi)的,國外的),借鑒別人寫過的Demo以及官方文檔,花了整整的一周時間,對iOS的CoreBluetooth這個框架的使用稍微有一些的了解,請聽我一一道來;
iOS 藍牙
簡稱:BLE(buletouch low energy),藍牙 4.0 設(shè)備因為低耗電,所以也叫做 BLE,CoreBluetooth框架就是蘋果公司為我們提供的一個庫,我們可以使用這個庫和其他支持藍牙4.0的設(shè)備進行數(shù)據(jù)交互。值得注意的是在IOS10之后的APP中,我們需要在 info.plist文件中添加NSBluetoothPeripheralUsageDescription字段否則APP會崩潰
工作模式:藍牙通信中,首先需要提到的就是 central 和 peripheral 兩個概念。這是設(shè)備在通信過程中扮演的兩種角色。直譯過來就是 [中心] 和 [周邊(可以理解為外設(shè))]。iOS 設(shè)備既可以作為 central,也可以作為 peripheral,這主要取決于通信需求。
自己嘗試的寫了個Demo,實現(xiàn)的功能有:
1、通過已知外圍設(shè)備的服務(wù)UUID搜索(這個UUID是指被廣播出來的服務(wù)UUID);
2、連接指定的外圍設(shè)備;
3、獲取指定的服務(wù),發(fā)現(xiàn)需要訂閱的特征;
4、接收外圍設(shè)備發(fā)送的數(shù)據(jù);
5、向外圍設(shè)備寫數(shù)據(jù);
6、實現(xiàn)藍牙服務(wù)的后臺模式;
7、實現(xiàn)藍牙服務(wù)的狀態(tài)保存與恢復(應(yīng)用被系統(tǒng)殺死的時候,系統(tǒng)會自動保存 central manager 的狀態(tài));
中心角色的實現(xiàn):(central)
(1)、初始化中央管理器對象
/**
第一個參數(shù):代理
第二個參數(shù):隊列(nil為不指定隊列,默認為主隊列)
第三個參數(shù):實現(xiàn)狀態(tài)保存的時候需要用到 eg:@{CBCentralManagerOptionRestoreIdentifierKey:@"centralManagerIdentifier"}
*/
centerManager = [[CBCentralManager alloc]initWithDelegate:self queue:queue options:options];
中央管理器會調(diào)用 centralManagerDidUpdateState:通知藍牙的狀態(tài)
(2)、發(fā)現(xiàn)外圍設(shè)備
[centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:SERVICE_UUID]] options:nil];
每次中央管理器發(fā)現(xiàn)外圍設(shè)備時,它都會調(diào)用centralManager:didDiscoverPeripheral:advertisementData:RSSI:其委托對象的方法。
(3)、發(fā)現(xiàn)想要的外圍設(shè)備進行連接
#pragma mark -- 掃描發(fā)現(xiàn)到任何一臺設(shè)備都會通過這個代理方法回調(diào)
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI
{
//過濾掉無效的結(jié)果
if (peripheral == nil||peripheral.identifier == nil/*||peripheral.name == nil*/)
{
return;
}
NSString *pername =[NSString stringWithFormat:@"%@",peripheral.name];
NSLog(@"所有服務(wù)****:%@",peripheral.services);
NSLog(@"藍牙名字:%@ 信號強弱:%@",pername,RSSI);
//連接需要的外圍設(shè)備
[self connectPeripheral:peripheral];
//將搜索到的設(shè)備添加到列表中
[self.peripherals addObject:peripheral];
if (_didDiscoverPeripheralBlock) {
_didDiscoverPeripheralBlock(central,peripheral,advertisementData,RSSI);
}
}
如果連接請求成功,則中央管理器調(diào)用centralManager:didConnectPeripheral:其委托對象的方法。
(4)、發(fā)現(xiàn)所連接的外圍設(shè)備的服務(wù)
#pragma mark -- 連接成功、獲取當前設(shè)備的服務(wù)和特征 并停止掃描
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
NSLog(@"%@",peripheral);
// 設(shè)置設(shè)備代理
[peripheral setDelegate:self];
// 大概獲取服務(wù)和特征
[peripheral discoverServices:@[[CBUUID UUIDWithString:SERVICE_UUID]]];
NSLog(@"Peripheral Connected");
if (_centerManager.isScanning) {
[_centerManager stopScan];
}
NSLog(@"Scanning stopped");
}
發(fā)現(xiàn)指定的服務(wù)時,外圍設(shè)備(CBPeripheral你連接的對象)會調(diào)用peripheral:didDiscoverServices:其委托對象的方法。
(5)、發(fā)現(xiàn)服務(wù)的特征
#pragma mark -- 獲取當前設(shè)備服務(wù)services
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
if (error) {
NSLog(@"Error discovering services: %@", [error localizedDescription]);
return;
}
NSLog(@"所有的servicesUUID%@",peripheral.services);
//遍歷所有service
for (CBService *service in peripheral.services)
{
NSLog(@"服務(wù)%@",service.UUID);
//找到你需要的servicesuuid
if ([[NSString stringWithFormat:@"%@",service.UUID] isEqualToString:SERVICE_UUID])
{
// 根據(jù)UUID尋找服務(wù)中的特征
[peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:CHARACTERISTIC_UUID]] forService:service];
}
}
}
peripheral:didDiscoverCharacteristicsForService:error:當發(fā)現(xiàn)指定服務(wù)的特征時,外圍設(shè)備調(diào)用其委托對象的方法。
(6)、檢索特征價值
閱讀特征的值 ()
[peripheral readValueForCharacteristic:interestingCharacteristic];
注意: 并非所有特征都是可讀的。你可以通過檢查其properties屬性是否包含CBCharacteristicPropertyRead常量來確定特征是否可讀。如果嘗試讀取不可讀的特征值,則peripheral:didUpdateValueForCharacteristic:error:委托方法將返回合適的錯誤。
訂閱特征的值()
雖然使用該readValueForCharacteristic:方法讀取特征值對靜態(tài)值有效,但它不是檢索動態(tài)值的最有效方法。檢索隨時間變化的特征值 - 例如,你的心率 - 通過訂閱它們。訂閱特征值時,您會在值更改時收到外圍設(shè)備的通知。
[peripheral setNotifyValue:YES forCharacteristic:interestingCharacteristic];
注意: 并非所有特征都提供訂閱。你可以通過檢查特性是否properties包含其中一個CBCharacteristicPropertyNotify或多個CBCharacteristicPropertyIndicate常量來確定特征是否提供訂閱。
當你訂閱(或取消訂閱)特征的值時,外圍設(shè)備會調(diào)用peripheral:didUpdateNotificationStateForCharacteristic:error:其委托對象的方法。
寫一個特征的值 ()
有時寫一個特征的值是有意義的。例如,如果你的應(yīng)用程序與藍牙低功耗數(shù)字恒溫器交互,你可能需要為恒溫器提供設(shè)置房間溫度的值。如果特征值是可寫的,則可以NSData通過調(diào)用外設(shè)writeValue:forCharacteristic:type:方法將數(shù)據(jù)值;
[self.discoveredPeripheral writeValue:data forCharacteristic:self.characteristic1 type:CBCharacteristicWriteWithResponse];
寫入特征的值時,指定要執(zhí)行的寫入類型。在上面的示例中,寫入類型CBCharacteristicWriteWithResponse指示外圍設(shè)備通過調(diào)用peripheral:didWriteValueForCharacteristic:error:其委托對象的方法讓您的應(yīng)用程序知道寫入是否成功。
外圍角色的實現(xiàn)
(1)、初始化外圍設(shè)備管理器
peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil];
創(chuàng)建外圍設(shè)備管理器時,外圍設(shè)備管理器會調(diào)用peripheralManagerDidUpdateState:其委托對象的方法。您必須實現(xiàn)此委托方法,以確保支持藍牙低功耗并可在本地外圍設(shè)備上使用。
(2)、設(shè)置服務(wù)和特征
為自定義服務(wù)和特征創(chuàng)建自己的UUID
在終端使用 uuidgen 命令獲取以ASCII字符串形式的128位值的UUID:71DA3FD1-7E10-41C1-B16F-4430B506CDE7
構(gòu)建服務(wù)樹和特征
myCharacteristic =[[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID properties:CBCharacteristicPropertyRead value:myValue permissions:CBAttributePermissionsReadable]; //特征
myService = [[CBMutableService alloc] initWithType:myServiceUUID primary:YES]; //與特征所關(guān)聯(lián)的服務(wù)
myService.characteristics = @ [myCharacteristic]; //設(shè)置服務(wù)的特征數(shù)組,將特征與其關(guān)聯(lián)
(3)、發(fā)布服務(wù)和特征
[peripheralManager addService:myService];
當調(diào)用此方法發(fā)布服務(wù)時,外圍管理器將調(diào)用peripheralManager:didAddService:error:其委托對象的方法。通過error可以知道是否發(fā)布成功;
將服務(wù)及其任何關(guān)聯(lián)特性發(fā)布到外圍設(shè)備的數(shù)據(jù)庫后,該服務(wù)將被緩存,將無法再對其進行更改。
(4)、廣播服務(wù)
[peripheralManager startAdvertising:@ {CBAdvertisementDataServiceUUIDsKey:@[myFirstService.UUID,mySecondService.UUID]}];
當開始在本地外圍設(shè)備上公布某些數(shù)據(jù)時,外圍設(shè)備管理器會調(diào)用peripheralManagerDidStartAdvertising:error:其委托對象的方法。
(5)、響應(yīng)來自中央的讀取和寫入請求
當連接的中央請求讀取某個特征的值時,外圍管理器會調(diào)用peripheralManager:didReceiveReadRequest:其委托對象的方法。
[peripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset];
設(shè)置讀取請求不要求從超出特征值的邊界的索引位置讀取
request.value = [myCharacteristic.value subdataWithRange:NSMakeRange(request.offset,myCharacteristic.value.length - request.offset)];
將請求的特性屬性(默認值為nil)的值設(shè)置為您在本地外圍設(shè)備上創(chuàng)建的特征值,同時考慮讀取請求的偏移量
設(shè)置值后,響應(yīng)遠程中央以指示請求已成功完成。通過調(diào)用類的respondToRequest:withResult:方法CBPeripheralManager,傳回請求(其更新的值)和請求的結(jié)果
當連接的中心發(fā)送寫入一個或多個特征值的請求時,外圍管理器會調(diào)用peripheralManager:didReceiveWriteRequests:其委托對象的方法
(6)、將更新的特征值發(fā)送到訂閱的中心
當連接的中心訂閱某個特征的值時,外圍管理器會調(diào)用peripheralManager:central:didSubscribeToCharacteristic:其委托對象的方法
獲取特征的更新值,并通過調(diào)用類的updateValue:forCharacteristic:onSubscribedCentrals:方法將其發(fā)送到中心CBPeripheralManager。
處理常駐后臺任務(wù)
首先需要在Capabilities-->Background Modes申請中心角色的后臺模式說明
如圖:
(1)、狀態(tài)保存與恢復
因為狀態(tài)的保存和恢復 Core Bluetooth 都為我們封裝好了,所以我們只需要選擇是否需要這個特性即可。系統(tǒng)會保存當前 central manager 或 peripheral manager,并且繼續(xù)執(zhí)行藍牙相關(guān)事件(即使程序已經(jīng)不再運行)。一旦事件執(zhí)行完畢,系統(tǒng)會在后臺重啟 app,這時你有機會去存儲當前狀態(tài),并且處理一些事物。在之前提到的 “門鎖” 的例子中,系統(tǒng)會監(jiān)視連接請求,并在 centralManager:didConnectPeripheral: 回調(diào)時,重啟 app,在用戶回家后,連接操作結(jié)束。
Core Bluetooth 的狀態(tài)保存與恢復在設(shè)備作為 central、peripheral 或者這兩種角色時,都可用。在設(shè)備作為 central 并添加了狀態(tài)保存與恢復支持后,如果 app 被強行關(guān)閉進程,系統(tǒng)會自動保存 central manager 的狀態(tài)(如果 app 有多個 central manager,你可以選擇哪一個需要系統(tǒng)保存)。
對于 CBCentralManager,系統(tǒng)會保存以下信息:
central 準備連接或已經(jīng)連接的 peripheral
central 需要掃描的 service(包括掃描時,配置的 options)
central 訂閱的 characteristic
對于 peripheral 來說,情況也差不多。系統(tǒng)對 CBPeripheralManager 的處理方式如下:
peripheral 在廣播的數(shù)據(jù)
peripheral 存入的 service 和 characteristic 的樹形結(jié)構(gòu)
已經(jīng)被 central 訂閱了的 characteristic 的值
當系統(tǒng)在后臺重新加載程序后(可能是因為找到了要找的 peripheral),你可以重新實例化 central manager 或 peripheral 并恢復他們的狀態(tài)。
(2)、選擇支持存儲和恢復
如果要支持存儲和恢復,則需要在初始化 manager 的時候給一個 restoration identifier。restoration identifier 是 string 類型,并標識了 app 中的 central manager 或 peripheral manager。這個 string 很重要,它將會告訴 Core Bluetooth 需要存儲狀態(tài),畢竟 Core Bluetooth 恢復有 identifier 的對象。
例如,在 central 端,要想支持該特性,可以在調(diào)用 CBCentralManager 的初始化方法時,配置 CBCentralManagerOptionRestoreIdentifierKey:
centralManager = [[CBCentralManager alloc] initWithDelegate:self
queue:nil
options:@{CBCentralManagerOptionRestoreIdentifierKey:@"centralManagerIdentifier"}];
雖然以上代碼沒有展示出來,其實在 peripheral manager 中要設(shè)置 identifier 也是這樣的。只是在初始化時,將 key 改成了 CBPeripheralManagerOptionRestoreIdentifierKey。
因為程序可以有多個 CBCentralManager 和 CBPeripheralManager,所以要確保每個 identifier 都是唯一的。
(3)、重新初始化 central manager 和 peripheral manager
當系統(tǒng)重新在后臺加載程序時,首先需要做的即根據(jù)存儲的 identifier,重新初始化 central manager 或 peripheral manager。如果你只有一個 manager,并且 manager 存在于 app 生命周期中,那這個步驟就不需要做什么了。
.
如果 app 中包含多個 manager,或者 manager 不是在整個 app 生命周期中都存在的,那 app 就必須要區(qū)分你要重新初始化哪個 manager 了。你可以通過從 app delegate 中的 application:didFinishLaunchingWithOptions: 中取出 key(UIApplicationLaunchOptionsBluetoothCentralsKey 或 UIApplicationLaunchOptionsBluetoothPeripheralsKey)中的 value(數(shù)組類型)來得到程序退出之前存儲的 manager identifier 列表:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSArray *centralManagerIdentifiers =
launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey];
if (centralManagerIdentifiers.count) {
//重新初始化所有的 manager
for (NSString *identifier in centralManagerIdentifiers) {
NSLog(@"系統(tǒng)啟動項目");
//在這里創(chuàng)建的藍牙實例一定要被當前類持有,不然出了這個函數(shù)就被銷毀了,藍牙檢測會出現(xiàn)“XPC connection invalid”
self.bluetooth = [[MSBBlueTooth alloc]initWithQueue:nil options:@{CBCentralManagerOptionRestoreIdentifierKey : identifier}];
NSLog(@"");
}
}
return YES;
}
(4)、實現(xiàn)恢復狀態(tài)的代理方法
在重新初始化 manager 之后,接下來需要同步 Core Bluetooth 存儲的他們的狀態(tài)。要想弄清楚在程序被退出時都在做些什么,就需要正確的實現(xiàn)代理方法。對于 central manager 來說,需要實現(xiàn) centralManager:willRestoreState:;對于 peripheral manager 來說,需要實現(xiàn) peripheralManager:willRestoreState:。
.
注意:如果選擇存儲和恢復狀態(tài),當系統(tǒng)在后臺重新加載程序時,首先調(diào)用的方法是 centralManager:willRestoreState: 或 peripheralManager:willRestoreState:。如果沒有選擇存儲的恢復狀態(tài)(或者喚醒時沒有什么內(nèi)容需要恢復),那么首先調(diào)用的方法是 centralManagerDidUpdateState: 或 peripheralManagerDidUpdateState:。
.
無論是以上哪種代理方法,最后一個參數(shù)都是一個包含程序退出前狀態(tài)的字典。字典中,可用的 key ,
central 端有:
NSString *const CBCentralManagerRestoredStatePeripheralsKey;
NSString *const CBCentralManagerRestoredStateScanServicesKey;
NSString *const CBCentralManagerRestoredStateScanOptionsKey;
peripheral 端有:
NSString *const CBPeripheralManagerRestoredStateServicesKey;
NSString *const CBPeripheralManagerRestoredStateAdvertisementDataKey;
要恢復 central manager 的狀態(tài),可以用 centralManager:willRestoreState: 返回字典中的 key 來得到。假如說 central manager 有想要或者已經(jīng)連接的 peripheral,那么可以通過 CBCentralManagerRestoredStatePeripheralsKey 對應(yīng)得到的 peripheral(CBPeripheral 對象)數(shù)組來得到。
- (void)centralManager:(CBCentralManager *)central
willRestoreState:(NSDictionary *)state {
NSArray *peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey];
//講狀態(tài)保存的設(shè)備加入列表,在藍牙檢測狀態(tài)的回調(diào)里實現(xiàn)重連
self.peripherals = [NSMutableArray arrayWithArray:peripherals];
}
具體要對拿到的 peripheral 數(shù)組做什么就要根據(jù)需求來了。如果這是個 central manager 搜索到的 peripheral 數(shù)組,那就可以存儲這個數(shù)組的引用,并且開始建立連接了(注意給這些 peripheral 設(shè)置代理,否則連接后不會走 peripheral 的代理方法)。
.
恢復 peripheral manager 的狀態(tài)和 central manager 的方式類似,就只是把代理方法換成了 peripheralManager:willRestoreState:,并且使用對應(yīng)的 key 即可
寫的不是很好,也算是東拼西湊了,但也是花了時間去整理的,如果看不懂,可以下載我的Demo自己跑一遍;
想要看實現(xiàn)效果,可以下載Demo,看的再多也不如項目跑一遍來的快,療效是不騙人的;
有需要的可以加我微信Jarvis-LLL,一起討論學習
喜歡就點個贊,也可以在下方評論一起討論討論