公司是一家做藍牙只能硬件的公司,所以整理一些藍牙開發(fā)的筆記。
一個好用的第三方藍牙庫:BabyBluetooth
這個庫對藍牙進行了一些封裝,提供了豐富的接口,基本可以滿足藍牙的很多操作,大家可以去學習一下。
但是由于公司要求,這里只能使用原生的藍牙框架<CoreBluetooth/CoreBluetooth.h>,廢話不多說,開始吧。PS.這里是將手機當做中央,設(shè)備作為周邊的實現(xiàn)。
為什么要用單例?
首先大家來思考這個問題:為什么我們要用單例來封裝藍牙模塊呢?
如果大家開發(fā)藍牙的話就一定知道,我們會有很多頁面需要用到連接,或者是寫入相應的數(shù)據(jù),所以這個時候我們就想到了一個單例,將藍牙模塊封裝到單例里去,就可以實現(xiàn)在每個控制器中都可以拿到唯一的藍牙對象,從而保證藍牙連接的穩(wěn)定性。
在這里吐個槽,在剛來公司時,接手了一個項目,讓我改進里面的藍牙功能,拿到手里整個傻眼,我類個去,每個用到藍牙的地方都寫了一整套藍牙協(xié)議的方法,臃腫度令人發(fā)指,而且導致再切換不同的頁面會走不同的藍牙協(xié)議方法,導致藍牙連接的不穩(wěn)定。(=、=讓我做一秒怨婦)。
創(chuàng)建單例
由于看我的文章的有一部分是基礎(chǔ)不是很好的(其實我基礎(chǔ)也不好o(╯□╰)o),這里簡單提一下單例的創(chuàng)建。
@interface BLETool () <CBCentralManagerDelegate,CBPeripheralDelegate>
@property (nonatomic ,strong) NSMutableArray *deviceArr;
//中央設(shè)備的屬性,全部操作都是通過這個來
@property (nonatomic ,strong)CBCentralManager *myCentralManager;
@end
#pragma mark - Singleton
static BLETool *bleTool = nil;
- (instancetype)init
{
self = [super init];
if (self) {
//這里centralManager需要設(shè)置CBCentralManagerDelegate,CBPeripheralDelegate這兩個代理
_myCentralManager = [[CBCentralManager alloc]initWithDelegate:self queue:nil options:nil];
}
return self;
}
+ (instancetype)shareInstance
{
//這里是通過GCD的once方法實現(xiàn)單例的創(chuàng)建,該方法在整個應用程序中只會執(zhí)行一次,所以經(jīng)常用作單例的創(chuàng)建。
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
bleTool = [[self alloc] init];
});
return bleTool;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
bleTool = [super allocWithZone:zone];
});
return bleTool;
}
- (id)copyWithZone:(NSZone *)zone
{
return self;
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
return self;
}
#pragma mark - 懶加載
- (NSMutableArray *)deviceArr
{
if (!_deviceArr) {
_deviceArr = [NSMutableArray array];
}
return _deviceArr;
}
我們通過shareInstance的方法,去創(chuàng)建BLETool的單例對象,去執(zhí)行所有的掃描,連接,寫入特征等等操作。
掃描設(shè)備
由于我們需要在外界的控制器中通過BLETool這個單例來進行所有的操作,所以這里我們需要在BLETool的.h文件中給出方法接口。
@interface BLETool : NSObject
+ (instancetype)shareInstance;
//可保存當前連接的設(shè)備,根據(jù)需要放在.h或者.m文件中
@property (nonatomic ,strong) CBPeripheral *currentPer;
#pragma mark - action of connecting layer -連接層操作
//掃描設(shè)備
- (void)scanDevice;
//停止掃描
- (void)stopScan;
//連接設(shè)備
- (void)connectDevice:(CBPeripheral *)peripheral;
//斷開設(shè)備連接
- (void)unConnectDevice;
//重連設(shè)備
- (void)reConnectDevice:(BOOL)isConnect;
//檢索已連接的外接設(shè)備
- (NSArray *)retrieveConnectedPeripherals;
好了,我們已經(jīng)向外界提供了一些常用的接口,做一些藍牙的普通操作等,下面來看看這些方法的實現(xiàn)過程:
#pragma mark - action of connecting layer -連接層操作
- (void)scanDevice
{
[self.deviceArr removeAllObjects];
//這里的第一個參數(shù)設(shè)置為nil,就掃描所有的設(shè)備,如果只想返回特定的服務(wù)的設(shè)備,就給服務(wù)的數(shù)組
[_myCentralManager scanForPeripheralsWithServices:nil options:nil];
}
- (void)stopScan
{
[_myCentralManager stopScan];
}
- (void)connectPeripheral:(CBPeripheral *)peripheral
{
self.currentPer = peripheral;
//請求連接到此外設(shè)
[_myCentralManager connectPeripheral:peripheral options:nil];
}
- (void)unConnectPeripheral
{
[self.myCentralManager cancelPeripheralConnection:self.currentPer];
}
- (NSArray *)retrieveConnectedPeripherals
{
//這里值得注意,在ios9.0之前,可以用retrieveConnectedPeripherals這個方法來返回手機已經(jīng)連接的設(shè)備,
//但是在9.0被廢棄了,現(xiàn)在需要特定的服務(wù)UUID才能返回特定的已連接設(shè)備。
return [_myCentralManager retrieveConnectedPeripheralsWithServices:@[[CBUUID UUIDWithString:kServiceUUID]]];
}
在我們調(diào)用了scanDevice的方法
[[BLETool shareInstance] scanDevice];
就會觸發(fā)BLETool的代理方法:
#pragma mark - CBCentralManagerDelegate
//檢查設(shè)備藍牙開關(guān)的狀態(tài)
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
if (central.state == CBCentralManagerStatePoweredOn) {
NSLog(@"藍牙已打開");
[_myCentralManager scanForPeripheralsWithServices:nil options:nil];
}else {
NSLog(@"藍牙已關(guān)閉");
}
}
//查找到正在廣播的外設(shè)
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
{
NSLog(@"Discovered %@", peripheral.name);
//當你發(fā)現(xiàn)你感興趣的外圍設(shè)備,停止掃描其他設(shè)備,以節(jié)省電能。
if (![self.deviceArr containsObject:peripheral]) {
[self.deviceArr addObject:peripheral];
}
}
這里我們掃描到了一些外設(shè),并將他們保存在了self.deviceArr數(shù)組中,現(xiàn)在我們需要將這些掃描到的設(shè)備傳遞給需要的控制器,由于后面需要用到很多的返回數(shù)據(jù),所以這里我們考慮使用代理來完成。在BLETool.h中設(shè)置協(xié)議和協(xié)議方法:
//掃描設(shè)備協(xié)議
@protocol BleDelegate <NSObject>
@required
- (void)BLEDidDiscoverDeviceWithMAC:(CBPeripheral *)peripheral;
@end
協(xié)議設(shè)置好了后,在剛剛掃描設(shè)備的方法中調(diào)用:
//查找到正在廣播的外設(shè)
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
{
NSLog(@"Discovered %@", peripheral.name);
//當你發(fā)現(xiàn)你感興趣的外圍設(shè)備,停止掃描其他設(shè)備,以節(jié)省電能。
if (![self.deviceArr containsObject:peripheral]) {
[self.deviceArr addObject:peripheral];
if ([self.discoverDelegate respondsToSelector:@selector(manridyBLEDidDiscoverDeviceWithMAC:)]) {
//返回掃描到的設(shè)備實例
[self.discoverDelegate manridyBLEDidDiscoverDeviceWithMAC:device];
}
}
}
OK~!掃描完成,我們?nèi)バ枰目刂破骼锶ソ邮瞻?!在需要連接的控制器里設(shè)置代理,并實現(xiàn)代理方法:
設(shè)置代理
注意這里設(shè)置代理時要使用weak來做delegate的修飾,這樣才不會導致強引用循環(huán),具體可以去看看這篇文章。
[BLETool shareInstance].discoverDelegate = self;
實現(xiàn)方法
#pragma mark - BleDelegate
- (void)BLEDidDiscoverDeviceWithMAC:(CBPeripheral *)peripheral
{
[self.deviceArr addObject:peripheral];
//這里我用了一個TableView去展示掃描到的設(shè)備,可以使用peripheral.name來作為設(shè)備展示
NSLog(@"有%ld個設(shè)備:%@",(unsigned long)self.deviceArr.count,self.deviceArr);
[self.deviceTableView reloadData];
}
至此設(shè)備掃描部分就完成了,簡單提一下停止掃描吧,這個可以自己定義,當掃描5秒后就停止掃描以節(jié)約電量。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[BLETool shareInstance] stopScan];
});
連接設(shè)備
掃描完設(shè)備之后,我們可以在需要的地方進行展示,如同上面實現(xiàn)方法中,我用到了一個UITableView去展示所有掃描到的設(shè)備,然后可以通過點擊,相應的cell,去連接對應的設(shè)備。這個時候就需要用到我們連接層提供的接口:
- (void)connectPeripheral:(CBPeripheral *)peripheral
當然這里需要將你需要連接的設(shè)備傳過去,比如我們在UITableView的cell的點擊方法中就可以執(zhí)行這個連接方法:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
//[MBProgressHUD showHUDAddedTo:self.view animated:YES];
[[BLETool shareInstance] connectDevice:self.deviceArr[indexPath.row]];
self.currentDevice = self.deviceArr[indexPath.row];
}
此時,會調(diào)用BLETool的連接方法:
- (void)connectPeripheral:(CBPeripheral *)peripheral
{
self.currentPer = peripheral;
//請求連接到此外設(shè)
[_myCentralManager connectPeripheral:peripheral options:nil];
}
注意,這里建議將接受到的要連接的外設(shè):peripheral 給保存下來,以便后面會用到。接下來,如果連接成功,會走 CBCentralManagerDelegate 里面的連接成功的回調(diào):
//連接成功
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
NSLog(@"成功連接上設(shè)備:%@", peripheral.name);
peripheral.delegate = self;
//傳入nil會返回所有服務(wù);一般會傳入你想要服務(wù)的UUID所組成的數(shù)組,就會返回指定的服務(wù)
[peripheral discoverServices:nil];
}
這里如果連接成功的話,我們就需要執(zhí)行發(fā)現(xiàn)服務(wù)的方法了,這里需要傳遞一個服務(wù) UUID 的參數(shù),如果傳nil的話,就會返回這個設(shè)備的所有的服務(wù)(Service),如果你知道你需要的是哪個服務(wù),可以傳入該服務(wù)的 UUID ,這樣就會返回指定的服務(wù)了。
如果連接失敗了,則會回調(diào) * CBCentralManagerDelegate* 里面的連接失敗的回調(diào):
//連接失敗
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
NSLog(@"連接失敗");
}
你可以在這個方法里面做一些連接失敗的操作,比如提示用戶重新連接之類。這里提一下斷開連接,斷開連接的情況我分析為兩種:第一種,由于藍牙設(shè)備的傳輸距離有限,一般有效距離為十米,這就產(chǎn)生了一單設(shè)備離開手機超過十米,可能就斷開連接了;另外還有一種情況,就是很多時候我們在APP中需要給用戶一個手動去斷開藍牙連接的情況,比如點擊按鈕斷開藍牙。這兩種情況概況為 主動斷開 和 被動斷開,主動斷開我們只需要調(diào)用BLETool里面的斷開連接的接口:
- (void)unConnectPeripheral
然而被動斷開的情況,我們可能會希望在設(shè)備回到可被連接的范圍內(nèi)后,由APP自動幫我們重新連接。這樣的情況,就在我們斷開連接的回調(diào)中做如下操作:
//斷開連接
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
if (如果是被動斷開,需要重連) {
[self.myCentralManager connectPeripheral:self.currentPer options:nil];
}else {
NSLog(@"斷開成功,不需要斷線重連");
//這里執(zhí)行的步驟是斷開連接成功的回調(diào),在需要做操作的類里面去實現(xiàn)這個方法就行
if ([self.connectDelegate respondsToSelector:@selector(manridyBLEDidDisconnectDevice:)]) {
[self.connectDelegate manridyBLEDidDisconnectDevice:self.currentPer];
}
self.currentDev = nil;
}
}
這里我們可以看到,如果是被動斷開的情況(這里是否為被動,可以通過外類傳進來,自行解決),我們只需要重新執(zhí)行連接的方法即可,前提是,這里的 self.currentPer 要與之前連接的設(shè)備保持一致,這樣才能完成斷線重連的操作;而如果使我們主動斷開的情況的話,只需要做個斷開成功的回調(diào),讓需要斷開連接的類知道即可,最后斷開連接后,最好將 self.currentPer 置為nil,以便下一次連接使用。
以上基本就是連接與斷開的過程了,接下來就是服務(wù)與特征,也是藍牙開發(fā)中,比較重要的一部分。
服務(wù)
上面連接成功的回調(diào)里,我們執(zhí)行了 discoverServices 這個方法,并且返回了這個設(shè)備所有的服務(wù),這些服務(wù)都在 CBPeripheralDelegate 協(xié)議方法中進行了返回:
#pragma mark - CBPeripheralDelegate
//發(fā)現(xiàn)到服務(wù)
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{
for (CBService *service in peripheral.services) {
NSLog(@"Discovered service %@",service.UUID);
//返回特定的服務(wù),訂閱的特征即可
[peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:kWriteCharacteristicUUID],[CBUUID UUIDWithString:kNotifyCharacteristicUUID]] forService:service];
}
}
這個回調(diào)中,會將設(shè)備的服務(wù)返回給我們,只需要在這里面去發(fā)現(xiàn)我們所需要的特征值即可,如 kWriteCharacteristicUUID 這個特征值,是我宏定義的寫入特征值,而 ** kNotifyCharacteristicUUID**是我需要訂閱的特征值。這主要需要底層那邊告訴你這個特征值即可,接下來,需要在發(fā)現(xiàn)特征值得方法里去訂閱我們需要的特征值(好多回調(diào)=、=!頭都暈了。。。):
//獲得某服務(wù)的特征
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{
for (CBCharacteristic *characteristic in service.characteristics) {
NSLog(@"Discovered characteristic %@", characteristic.UUID);
//保存寫入特征
if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kWriteCharacteristicUUID]]) {
self.currentDev.writeCharacteristic = characteristic;
}
//保存訂閱特征
if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kNotifyCharacteristicUUID]]) {
self.currentDev.notifyCharacteristic = characteristic;
//訂閱該特征
[peripheral setNotifyValue:YES forCharacteristic:characteristic];
}
}
}
有點晚了,暫時更新這么多,后面會介紹一些特征值寫入和更新的內(nèi)容。
ps.由于是手寫的代碼,很多地方標點可能是中文標點,可是奈何我老花眼,實在查不出來,遇到錯誤留言告知,謝謝。