最近公司的項目中提到了藍牙開發(fā),而且現(xiàn)在市面上的藍牙分為普通藍牙和低功耗藍牙(BLE)也就是藍牙4.0
iOS 提供了 <CoreBluetooth/CoreBluetooth.h> 這個框架專門針對藍牙4.0 下面就是我對這個框架查的一些總結和記錄。
話不多說,讓我們進入正題吧:
藍牙常見名稱和縮寫
BLE:(Bluetooth low energy)藍牙4.0設備因為低耗電,也叫BLE
peripheral,central:外設和中心設備,發(fā)起鏈接的是central(一般是指手機),被鏈接的設備是peripheral(比如運動手環(huán))
service and characteristic:(服務和特征)每個設備會提供服務和特征,類似于服務端的API,但是結構不同.每個設備會有很多服務,每個服務中包含很多字段,這些字段的權限一般分為讀(read),寫(write),通知(notify)幾種,就是我們連接設備后具體需要操作的內(nèi)容
Description:每個characteristic可以對應一個或者多個Description用于描述characteristic的信息或屬性(eg.范圍,計量單位)
關于上邊的名稱詳情可以參考
Central 和 Peripheral 在藍牙交互中的角色
Core Bluetooth Programming Guide
服務和特征(service and characteristic)
- 每個設備都會有1個or多個服務
- 每個服務里都會有1個or多個特征
- 特征就是具體鍵值對,提供數(shù)據(jù)的地方
- 每個特征屬性分為:讀,寫,通知等等

BLE中心模式流程
1.建立中心角色
2.掃描外設(Discover Peripheral)
3.連接外設(Connect Peripheral)
4.掃描外設中的服務和特征(Discover Services And Characteristics)
4.1 獲取外設的services
4.2 獲取外設的Characteristics,獲取characteristics的值,,獲取Characteristics的Descriptor和Descriptor的值
5.利用特征與外設做數(shù)據(jù)交互(Explore And Interact)
6.訂閱Characteristic的通知
7.斷開連接(Disconnect)
BLE外設模式流程
1.啟動一個Peripheral管理對象
2.本地peripheral設置服務,特征,描述,權限等等
3.peripheral發(fā)送廣告
4.設置處理訂閱,取消訂閱,讀characteristic,寫characteristic的代理方法
藍牙和版本使用限制
- 藍牙2.0:越獄設備
- BLE:iOS6以上
- MFI認證設備:無限制
這里我把手機作為central 周圍藍牙的設備作為peripheral 根據(jù)BLE中心模式的流程 相關代碼步驟如下:
1.首先導入我們需要的藍牙框架 定義一些需要的宏
#import <CoreBluetooth/CoreBluetooth.h>
#define kServiceUUID @"590A" //服務的UUID
#define kCharacteristicUUID @"590B" //特征的UUID
2.建立中心管理者
- (CBCentralManager *)cMgr
{
if (!_cMgr) {
/*
設置主設備的代理,CBCentralManagerDelegate
必須實現(xiàn)的:
- (void)centralManagerDidUpdateState:(CBCentralManager *)central;//主設備狀態(tài)改變調(diào)用,在初始化CBCentralManager的適合會打開設備,只有當設備正確打開后才能使用
其他選擇實現(xiàn)的代理中比較重要的:
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI; //找到外設
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral;//連接外設成功
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error;//外設連接失敗
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error;//斷開外設
*/
_cMgr = [[CBCentralManager alloc] initWithDelegate:self
queue:dispatch_get_main_queue() options:nil];
}
return _cMgr;
}
調(diào)用懶加載
- (void)viewDidLoad
{
[super viewDidLoad];
// 調(diào)用get方法,先將中心管理者初始化
[self cMgr];
}
3.掃描外設 (這里注意 在中心管理者成功開啟后再掃描)
// 只要中心管理者初始化,就會觸發(fā)此代理方法
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
/*
CBCentralManagerStateUnknown = 0,
CBCentralManagerStateResetting,
CBCentralManagerStateUnsupported,
CBCentralManagerStateUnauthorized,
CBCentralManagerStatePoweredOff,
CBCentralManagerStatePoweredOn,
*/
switch (central.state) {
case CBCentralManagerStateUnknown:
NSLog(@"CBCentralManagerStateUnknown");
break;
case CBCentralManagerStateResetting:
NSLog(@"CBCentralManagerStateResetting");
break;
case CBCentralManagerStateUnsupported:
NSLog(@"CBCentralManagerStateUnsupported");
break;
case CBCentralManagerStateUnauthorized:
NSLog(@"CBCentralManagerStateUnauthorized");
break;
case CBCentralManagerStatePoweredOff:
NSLog(@"CBCentralManagerStatePoweredOff");
break;
case CBCentralManagerStatePoweredOn:
{
NSLog(@"CBCentralManagerStatePoweredOn");
// 在中心管理者成功開啟后再進行一些操作
// 搜索外設
[self.cMgr scanForPeripheralsWithServices:nil // 通過某些服務篩選外設
options:nil]; // dict,條件
// 搜索成功之后,會調(diào)用我們找到外設的代理方法
// - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI; //找到外設
}
break;
default:
break;
}
}
4.連接外設
#pragma mark - CBCentralManagerDelegate
// 發(fā)現(xiàn)外設后調(diào)用的 CBCentralManagerDelegate 方法
- (void)centralManager:(CBCentralManager *)central // 中心管理者
didDiscoverPeripheral:(CBPeripheral *)peripheral // 外設
advertisementData:(NSDictionary *)advertisementData // 外設攜帶的數(shù)據(jù)
RSSI:(NSNumber *)RSSI // 外設發(fā)出的藍牙信號強度
{
//NSLog(@"%s, line = %d, cetral = %@,peripheral = %@, advertisementData = %@, RSSI = %@", __FUNCTION__, __LINE__, central, peripheral, advertisementData, RSSI);
/*
peripheral = <CBPeripheral: 0x1742e1800, identifier = 8CAC092C-9D81-47CA-940E-CD705F967A20, name = 27010010, state = disconnected>, advertisementData = {
kCBAdvDataIsConnectable = 1;
kCBAdvDataLocalName = 27010010;
kCBAdvDataServiceUUIDs = (
590A
);
}, RSSI = -92
根據(jù)打印結果,我們可以得到車鎖設備 它的名字叫 27010010
*/
// 需要對連接到的外設進行過濾
// 1.信號強度(40以上才連接, 80以上連接)
// 2.通過設備名(設備字符串27010010)
// 在此時我們的過濾規(guī)則是:有OBand前綴并且信號強度大于35
// 通過打印,我們知道RSSI一般是帶-的
if ([peripheral.name isEqualToString:@"27010010"] && (ABS(RSSI.integerValue) > 35)) {
// 在此處對我們的 advertisementData(外設攜帶的廣播數(shù)據(jù)) 進行一些處理
// 通常通過過濾,我們會得到一些外設,然后將外設儲存到我們的可變數(shù)組中,
// 這里由于附近只有1個運動手環(huán), 所以我們先按1個外設進行處理
// 標記我們的外設,讓他的生命周期 = vc
self.peripheral = peripheral;
// 發(fā)現(xiàn)完之后就是進行連接
[self.cMgr connectPeripheral:self.peripheral options:nil];
NSLog(@"%s, line = %d", __FUNCTION__, __LINE__);
}
}
連接外設成功或者失敗 會調(diào)用代理方法
// 中心管理者連接外設成功
- (void)centralManager:(CBCentralManager *)central // 中心管理者
didConnectPeripheral:(CBPeripheral *)peripheral // 外設
{
NSLog(@"%s, line = %d, %@=連接成功", __FUNCTION__, __LINE__, peripheral.name);
// 連接成功之后,可以進行服務和特征的發(fā)現(xiàn)
// 4.1 獲取外設的服務
// 4.1.1 設置外設的代理
self.peripheral.delegate = self;
// 4.1.2 外設發(fā)現(xiàn)服務,傳nil代表不過濾
// 這里會觸發(fā)外設的代理方法 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
[self.peripheral discoverServices:nil];
}
// 外設連接失敗
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
NSLog(@"%s, line = %d, %@=外設連接失敗", __FUNCTION__, __LINE__, peripheral.name);
}
// 丟失連接
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
NSLog(@"%s, line = %d, %@=外設斷開連接", __FUNCTION__, __LINE__, peripheral.name);
}
5.掃描外設中的服務和特征(Discover Services And Characteristics)
#pragma mark - 外設代理
// 發(fā)現(xiàn)外設的服務后調(diào)用的方法
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
NSLog(@"%s, line = %d", __FUNCTION__, __LINE__);
// 判斷沒有失敗
if (error) {
NSLog(@"%s, line = %d, error = %@", __FUNCTION__, __LINE__, error.localizedDescription);
return;
#warning 下面的方法中凡是有error的在實際開發(fā)中,都要進行判斷
}
for (CBService *service in peripheral.services) {
// 發(fā)現(xiàn)服務后,讓設備再發(fā)現(xiàn)服務內(nèi)部的特征們 didDiscoverCharacteristicsForService
[peripheral discoverCharacteristics:nil forService:service];
}
}
6.獲取外設的services
獲取外設的Characteristics,獲取characteristics的值,,獲取Characteristics的Descriptor和Descriptor的值
利用特征與外設做數(shù)據(jù)交互(Explore And Interact)
訂閱Characteristic的通知
// 發(fā)現(xiàn)外設服務里的特征的時候調(diào)用的代理方法
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
//根據(jù)自己的需要篩選ServiceUUID]
if ([service.UUID isEqual:[CBUUID UUIDWithString:kServiceUUID]]) {
for (CBCharacteristic *characteristic in service.characteristics) {
NSLog(@"特征UUID:%@",characteristic.UUID);
if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kCharacteristicUUID]]) {
NSLog(@"監(jiān)聽特征:%@",characteristic);//監(jiān)聽特征
self.openLockCharacteristic = characteristic;
}
if (self.openLockCharacteristic) {
//characteristic 包含了 service 要傳輸?shù)臄?shù)據(jù)。例如溫度設備中表達溫度的 characteristic,就可能包含著當前溫度值。這時我們就可以通過讀取 characteristic,來得到里面的數(shù)據(jù)。
//可以直接獲取特征的值 使用下面的方法
//[peripheral readValueForCharacteristic:self.openLockCharacteristic];
//當調(diào)用上面這方法后,會回調(diào)peripheral:didUpdateValueForCharacteristic:error:方法,其中包含了要讀取的數(shù)據(jù)。如果讀取正確,在該方法中獲取到值
//也可以訂閱通知
//其實使用readValueForCharacteristic:方法并不是實時的??紤]到很多實時的數(shù)據(jù),比如心率這種,那就需要訂閱 characteristic 了。
//當訂閱成功以后,那數(shù)據(jù)便會實時的傳回了,數(shù)據(jù)的回調(diào)依然和之前讀取 characteristic 的回調(diào)相同
[self.peripheral setNotifyValue:YES forCharacteristic:self.openLockCharacteristic];//拿到讀特征
//向openLockCharacteristic 發(fā)送數(shù)據(jù) 這里的數(shù)據(jù)根據(jù)自己硬件的實際情況發(fā)送
[self sj_peripheral:self.peripheral didWriteData:[self stringToByte:@""] forCharacteristic:self.openLockCharacteristic];
}
}
}
}
獲取外設發(fā)來的數(shù)據(jù)
// 更新特征的value的時候會調(diào)用
//中心接受外設信息,外設通知手機連接成功的時候并不會調(diào)用這個函數(shù);手機向外設發(fā)送消息后,外設回應數(shù)據(jù),這個函數(shù)會接受到數(shù)據(jù)。
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
NSLog(@"%s, line = %d", __FUNCTION__, __LINE__);
NSString *value = [[NSString alloc] initWithData:characteristic.value encoding:NSASCIIStringEncoding];
NSLog(@"收到藍牙的消息=======%@",value);//這個就是藍牙給我們發(fā)的數(shù)據(jù);
}
// 更新特征的描述的值的時候會調(diào)用
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForDescriptor:(CBDescriptor *)descriptor error:(NSError *)error
{
NSLog(@"%s, line = %d, descriptor.UUID = %@ ,descriptor.value = %@", __FUNCTION__, __LINE__,descriptor.UUID ,descriptor.value);
// 這里當描述的值更新的時候,直接調(diào)用此方法即可
[peripheral readValueForDescriptor:descriptor];
}
//如果是訂閱,成功與否的回調(diào)是peripheral:didUpdateNotificationStateForCharacteristic:error:,讀取中的錯誤會以 error 形式傳回:
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error {
if (error) {
NSLog(@"Error changing notification state: %@", [error localizedDescription]);
}else{
NSLog(@"訂閱成功");
}
}
//用于檢測中心向外設寫數(shù)據(jù)是否成功
-(void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
if (error) {
NSLog(@"=======%@",error.userInfo);
[self updateLog:[error.userInfo JSONString]];
}else{
NSLog(@"發(fā)送數(shù)據(jù)成功");
[self updateLog:@"發(fā)送數(shù)據(jù)成功"];
}
/* When a write occurs, need to set off a re-read of the local CBCharacteristic to update its value */
[peripheral readValueForCharacteristic:characteristic];
}
還有一些自定義方法 向外設寫數(shù)據(jù) 通知訂閱 斷開連接
#pragma mark - 自定義方法
//外設寫數(shù)據(jù)到特征中
// 需要注意的是特征的屬性是否支持寫數(shù)據(jù)
- (void)sj_peripheral:(CBPeripheral *)peripheral didWriteData:(NSData *)data forCharacteristic:(nonnull CBCharacteristic *)characteristic
{
/*
typedef NS_OPTIONS(NSUInteger, CBCharacteristicProperties) {
CBCharacteristicPropertyBroadcast = 0x01,
CBCharacteristicPropertyRead = 0x02,
CBCharacteristicPropertyWriteWithoutResponse = 0x04,
CBCharacteristicPropertyWrite = 0x08,
CBCharacteristicPropertyNotify = 0x10,
CBCharacteristicPropertyIndicate = 0x20,
CBCharacteristicPropertyAuthenticatedSignedWrites = 0x40,
CBCharacteristicPropertyExtendedProperties = 0x80,
CBCharacteristicPropertyNotifyEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x100,
CBCharacteristicPropertyIndicateEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x200
};
打印出特征的權限(characteristic.properties),可以看到有很多種,這是一個NS_OPTIONS的枚舉,可以是多個值
常見的又read,write,noitfy,indicate.知道這幾個基本夠用了,前倆是讀寫權限,后倆都是通知,倆不同的通知方式
*/
NSLog(@"%s, line = %d, char.pro = %lu", __FUNCTION__, __LINE__, (unsigned long)characteristic.properties);
// 此時由于枚舉屬性是NS_OPTIONS,所以一個枚舉可能對應多個類型,所以判斷不能用 = ,而應該用包含&
if (characteristic.properties & CBCharacteristicPropertyWrite) {
// 核心代碼在這里
[peripheral writeValue:data // 寫入的數(shù)據(jù)
forCharacteristic:characteristic // 寫給哪個特征
type:CBCharacteristicWriteWithResponse];// 通過此響應記錄是否成功寫入
}
}
// 6.通知的訂閱和取消訂閱
// 實際核心代碼是一個方法
// 一般這兩個方法要根據(jù)產(chǎn)品需求來確定寫在何處
- (void)sj_peripheral:(CBPeripheral *)peripheral regNotifyWithCharacteristic:(nonnull CBCharacteristic *)characteristic
{
// 外設為特征訂閱通知 數(shù)據(jù)會進入 peripheral:didUpdateValueForCharacteristic:error:方法
[peripheral setNotifyValue:YES forCharacteristic:characteristic];
}
- (void)sj_peripheral:(CBPeripheral *)peripheral CancleRegNotifyWithCharacteristic:(nonnull CBCharacteristic *)characteristic
{
// 外設取消訂閱通知 數(shù)據(jù)會進入 peripheral:didUpdateValueForCharacteristic:error:方法
[peripheral setNotifyValue:NO forCharacteristic:characteristic];
}
// 7.斷開連接
- (void)sj_dismissConentedWithPeripheral:(CBPeripheral *)peripheral
{
// 停止掃描
[self.cMgr stopScan];
// 斷開連接
[self.cMgr cancelPeripheralConnection:peripheral];
}
再次連接 peripheral
Core Bluetooth 提供了三種再次連接 peripheral 的方式:
調(diào)用 retrievePeripheralsWithIdentifiers: 方法,重連已知的 peripheral 列表中的 peripheral(以前發(fā)現(xiàn)的,或者以前連接過的)。
調(diào)用 retrieveConnectedPeripheralsWithServices: 方法,重新連接當前【系統(tǒng)】已經(jīng)連接的 peripheral。
調(diào)用 scanForPeripheralsWithServices:options: 方法,連接搜索到的 peripheral。