最近新進(jìn)一家公司液兽,主要是做物聯(lián)網(wǎng)這一塊的的拂酣,項目需要用到藍(lán)牙開發(fā)奠涌,講真的娃殖,挑戰(zhàn)還是挺大的定庵,做了差不多四年的iOS開發(fā)衰粹,從沒有接觸過藍(lán)牙開發(fā)這一領(lǐng)域锣光,我是這樣學(xué)習(xí)的。
從網(wǎng)上找各種博客(國內(nèi)的铝耻,國外的)誊爹,借鑒別人寫過的Demo以及官方文檔蹬刷,花了整整的一周時間,對iOS的CoreBluetooth這個框架的使用稍微有一些的了解频丘,請聽我一一道來办成;
iOS 藍(lán)牙
簡稱:BLE(buletouch low energy),藍(lán)牙 4.0 設(shè)備因為低耗電搂漠,所以也叫做 BLE迂卢,CoreBluetooth框架就是蘋果公司為我們提供的一個庫,我們可以使用這個庫和其他支持藍(lán)牙4.0的設(shè)備進(jìn)行數(shù)據(jù)交互桐汤。值得注意的是在IOS10之后的APP中而克,我們需要在 info.plist文件中添加NSBluetoothPeripheralUsageDescription字段否則APP會崩潰
工作模式:藍(lán)牙通信中,首先需要提到的就是 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)藍(lán)牙服務(wù)的后臺模式副女;
7蛤高、實現(xiàn)藍(lán)牙服務(wù)的狀態(tài)保存與恢復(fù)(應(yīng)用被系統(tǒng)殺死的時候,系統(tǒng)會自動保存 central manager 的狀態(tài))碑幅;
中心角色的實現(xiàn):(central)
(1)戴陡、初始化中央管理器對象
/**
第一個參數(shù):代理
第二個參數(shù):隊列(nil為不指定隊列,默認(rèn)為主隊列)
第三個參數(shù):實現(xiàn)狀態(tài)保存的時候需要用到 eg:@{CBCentralManagerOptionRestoreIdentifierKey:@"centralManagerIdentifier"}
*/
centerManager = [[CBCentralManager alloc]initWithDelegate:self queue:queue options:options];
中央管理器會調(diào)用 centralManagerDidUpdateState:通知藍(lán)牙的狀態(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è)備進(jìn)行連接
#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(@"藍(lán)牙名字:%@ 信號強(qiáng)弱:%@",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 -- 連接成功延都、獲取當(dāng)前設(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 -- 獲取當(dāng)前設(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:當(dāng)發(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常量來確定特征是否提供訂閱刑然。
當(dāng)你訂閱(或取消訂閱)特征的值時寺擂,外圍設(shè)備會調(diào)用peripheral:didUpdateNotificationStateForCharacteristic:error:其委托對象的方法。
寫一個特征的值 ()
有時寫一個特征的值是有意義的泼掠。例如怔软,如果你的應(yīng)用程序與藍(lán)牙低功耗數(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)此委托方法憔涉,以確保支持藍(lá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];
當(dāng)調(diào)用此方法發(fā)布服務(wù)時穿扳,外圍管理器將調(diào)用peripheralManager:didAddService:error:其委托對象的方法。通過error可以知道是否發(fā)布成功;
將服務(wù)及其任何關(guān)聯(lián)特性發(fā)布到外圍設(shè)備的數(shù)據(jù)庫后国旷,該服務(wù)將被緩存矛物,將無法再對其進(jìn)行更改。
(4)跪但、廣播服務(wù)
[peripheralManager startAdvertising:@ {CBAdvertisementDataServiceUUIDsKey:@[myFirstService.UUID履羞,mySecondService.UUID]}];
當(dāng)開始在本地外圍設(shè)備上公布某些數(shù)據(jù)時,外圍設(shè)備管理器會調(diào)用peripheralManagerDidStartAdvertising:error:其委托對象的方法屡久。
(5)忆首、響應(yīng)來自中央的讀取和寫入請求
當(dāng)連接的中央請求讀取某個特征的值時,外圍管理器會調(diào)用peripheralManager:didReceiveReadRequest:其委托對象的方法被环。
[peripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset];
設(shè)置讀取請求不要求從超出特征值的邊界的索引位置讀取
request.value = [myCharacteristic.value subdataWithRange:NSMakeRange(request.offset糙及,myCharacteristic.value.length - request.offset)];
將請求的特性屬性(默認(rèn)值為nil)的值設(shè)置為您在本地外圍設(shè)備上創(chuàng)建的特征值,同時考慮讀取請求的偏移量
設(shè)置值后筛欢,響應(yīng)遠(yuǎn)程中央以指示請求已成功完成浸锨。通過調(diào)用類的respondToRequest:withResult:方法CBPeripheralManager,傳回請求(其更新的值)和請求的結(jié)果
當(dāng)連接的中心發(fā)送寫入一個或多個特征值的請求時版姑,外圍管理器會調(diào)用peripheralManager:didReceiveWriteRequests:其委托對象的方法
(6)柱搜、將更新的特征值發(fā)送到訂閱的中心
當(dāng)連接的中心訂閱某個特征的值時,外圍管理器會調(diào)用peripheralManager:central:didSubscribeToCharacteristic:其委托對象的方法
獲取特征的更新值剥险,并通過調(diào)用類的updateValue:forCharacteristic:onSubscribedCentrals:方法將其發(fā)送到中心CBPeripheralManager聪蘸。
處理常駐后臺任務(wù)
首先需要在Capabilities-->Background Modes申請中心角色的后臺模式說明
如圖:
(1)、狀態(tài)保存與恢復(fù)
因為狀態(tài)的保存和恢復(fù) Core Bluetooth 都為我們封裝好了表制,所以我們只需要選擇是否需要這個特性即可宇姚。系統(tǒng)會保存當(dāng)前 central manager 或 peripheral manager,并且繼續(xù)執(zhí)行藍(lán)牙相關(guān)事件(即使程序已經(jīng)不再運(yùn)行)夫凸。一旦事件執(zhí)行完畢浑劳,系統(tǒng)會在后臺重啟 app,這時你有機(jī)會去存儲當(dāng)前狀態(tài)夭拌,并且處理一些事物魔熏。在之前提到的 “門鎖” 的例子中,系統(tǒng)會監(jiān)視連接請求鸽扁,并在 centralManager:didConnectPeripheral: 回調(diào)時蒜绽,重啟 app,在用戶回家后桶现,連接操作結(jié)束躲雅。
Core Bluetooth 的狀態(tài)保存與恢復(fù)在設(shè)備作為 central、peripheral 或者這兩種角色時骡和,都可用相赁。在設(shè)備作為 central 并添加了狀態(tài)保存與恢復(fù)支持后相寇,如果 app 被強(qiáng)行關(guān)閉進(jìn)程,系統(tǒng)會自動保存 central manager 的狀態(tài)(如果 app 有多個 central manager钮科,你可以選擇哪一個需要系統(tǒng)保存)唤衫。
對于 CBCentralManager,系統(tǒng)會保存以下信息:
central 準(zhǔn)備連接或已經(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 的值
當(dāng)系統(tǒng)在后臺重新加載程序后(可能是因為找到了要找的 peripheral)蛆挫,你可以重新實例化 central manager 或 peripheral 并恢復(fù)他們的狀態(tài)赃承。
(2)、選擇支持存儲和恢復(fù)
如果要支持存儲和恢復(fù)悴侵,則需要在初始化 manager 的時候給一個 restoration identifier瞧剖。restoration identifier 是 string 類型,并標(biāo)識了 app 中的 central manager 或 peripheral manager畜挨。這個 string 很重要筒繁,它將會告訴 Core Bluetooth 需要存儲狀態(tài),畢竟 Core Bluetooth 恢復(fù)有 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
當(dāng)系統(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)建的藍(lán)牙實例一定要被當(dāng)前類持有,不然出了這個函數(shù)就被銷毀了咖熟,藍(lán)牙檢測會出現(xiàn)“XPC connection invalid”
self.bluetooth = [[MSBBlueTooth alloc]initWithQueue:nil options:@{CBCentralManagerOptionRestoreIdentifierKey : identifier}];
NSLog(@"");
}
}
return YES;
}
(4)圃酵、實現(xiàn)恢復(fù)狀態(tài)的代理方法
在重新初始化 manager 之后,接下來需要同步 Core Bluetooth 存儲的他們的狀態(tài)球恤。要想弄清楚在程序被退出時都在做些什么辜昵,就需要正確的實現(xiàn)代理方法荸镊。對于 central manager 來說咽斧,需要實現(xiàn) centralManager:willRestoreState:;對于 peripheral manager 來說躬存,需要實現(xiàn) peripheralManager:willRestoreState:张惹。
.
注意:如果選擇存儲和恢復(fù)狀態(tài),當(dāng)系統(tǒng)在后臺重新加載程序時岭洲,首先調(diào)用的方法是 centralManager:willRestoreState: 或 peripheralManager:willRestoreState:宛逗。如果沒有選擇存儲的恢復(fù)狀態(tài)(或者喚醒時沒有什么內(nèi)容需要恢復(fù)),那么首先調(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;
要恢復(fù) 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è)備加入列表蜀撑,在藍(lán)牙檢測狀態(tài)的回調(diào)里實現(xiàn)重連
self.peripherals = [NSMutableArray arrayWithArray:peripherals];
}
具體要對拿到的 peripheral 數(shù)組做什么就要根據(jù)需求來了挤巡。如果這是個 central manager 搜索到的 peripheral 數(shù)組,那就可以存儲這個數(shù)組的引用酷麦,并且開始建立連接了(注意給這些 peripheral 設(shè)置代理矿卑,否則連接后不會走 peripheral 的代理方法)。
.
恢復(fù) peripheral manager 的狀態(tài)和 central manager 的方式類似沃饶,就只是把代理方法換成了 peripheralManager:willRestoreState:母廷,并且使用對應(yīng)的 key 即可
寫的不是很好,也算是東拼西湊了绍坝,但也是花了時間去整理的徘意,如果看不懂,可以下載我的Demo自己跑一遍轩褐;
想要看實現(xiàn)效果椎咧,可以下載Demo,看的再多也不如項目跑一遍來的快,療效是不騙人的;
有需要的可以加我微信Jarvis-LLL勤讽,一起討論學(xué)習(xí)
喜歡就點(diǎn)個贊蟋座,也可以在下方評論一起討論討論