iOS蓝牙 CoreBluetooth

CoreBluetooth 是基于蓝牙 ble 技术实现的,现在的智能手环手表都使用了低功耗蓝牙技术。

CoreBluetooth 场景中蓝牙对象分为两种:

中心设备(CBCentralManager * cbcManager): 中心设备负责扫描接收广播,连接外设设备,向外设设备进行读写操作。
外设设备(CBPeripheral * peripheral): 向外发送广播,向中心设备读写操作。
注意: <u>一个设备既可以是中心设备,又可以是外设设备,但是不能同时是中心设备和外设设备,同一时间,只能扮演一种角色。</u>

基础知识介绍:

CBPeripheral

// -------------------- CBPeripheral 属性 --------------------
// 外设状态
typedef NS_ENUM(NSInteger, CBPeripheralState) {
    CBPeripheralStateDisconnected = 0,  // 没有连接
    CBPeripheralStateConnecting,        // 正在连接
    CBPeripheralStateConnected,         // 已经连接
    CBPeripheralStateDisconnecting NS_AVAILABLE(10_13, 9_0),
} NS_AVAILABLE(10_9, 7_0);
@interface CBPeripheral : CBPeer
@property(weak, nonatomic, nullable) id<CBPeripheralDelegate> delegate;
@property(retain, readonly, nullable) NSString *name;   // 外设名字
@property(retain, readonly, nullable) NSNumber *RSSI;   // 信号强度
@property(readonly) CBPeripheralState state;           // 外设状态
@property(retain, readonly, nullable) NSArray<CBService *> *services;   // 外设包含的服务(每个服务里面可能包含多个特征)
@property(readonly) BOOL canSendWriteWithoutResponse;   // 判断读写性
 
// -------------------- CBPeripheral 常用方法 --------------------
// 查找外设包含的服务,serviceUUIDs 为 nil 则查找该外设包含的所有服务,执行该方法会触发代理方法
- (void)discoverServices:(nullable NSArray<CBUUID *> *)serviceUUIDs;
 
// 扫描特征的描述,执行该方法会触发代理方法
- (void)discoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic;
 
// 扫描服务里包含的服务,执行该方法会出发代理方法
- (void)discoverIncludedServices:(nullable NSArray<CBUUID *> *)includedServiceUUIDs forService:(CBService *)service;
 
// 扫描服务包含的特征,执行该方法会出发代理方法
- (void)discoverCharacteristics:(nullable NSArray<CBUUID *> *)characteristicUUIDs forService:(CBService *)service;
 
// 读取某特征的数据 
- (void)readValueForCharacteristic:(CBCharacteristic *)characteristic;
 
// 读取描述的值
- (void)readValueForDescriptor:(CBDescriptor *)descriptor;(CBCharacteristicWriteType)type;
 
// 通过特征写入数据
- (void)writeValue:(NSData *)data forCharacteristic:(CBCharacteristic *)characteristic type:
 
// 通过描述写入数据
- (void)writeValue:(NSData *)data forDescriptor:(CBDescriptor *)descriptor;
 
// 设置通知
- (void)setNotifyValue:(BOOL)enabled forCharacteristic:(CBCharacteristic *)characteristic;
 
// -------------------- 代理方法 --------------------
// 扫描外设服务触发该代理,如果找打服务,可以在这里调用 discoverCharacteristics 方法查找该服务的特征
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error;
 
// 扫描服务包含的服务(服务里面可能会包含服务)
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverIncludedServicesForService:(CBService *)service error:(NSError *)error;
 
// 查找服务的特征,一个服务可能有多个特征,一个特征可能有多个属性,CBCharacteristic 是一个枚举值
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(nonnull CBService *)service error:(nullable NSError *)error;
 
// 接收通知代理
-(void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error;
 
// 读取 Characteristic 中的值,调用 readValueForCharacteristic 方法会触发该代理,需要在该代理方法里面获取具体的数据
-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(nonnull CBCharacteristic *)characteristic error:(nullable NSError *)error;
 
// 向 CBCharacteristic 写入数据的回调
-(void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(nonnull CBCharacteristic *)characteristic error:(nullable NSError *)error;

CBCentralManager

// 中心设备的状态,创建中心设备的时候,会触发代理,在代理里面判断中心设备状态,在状态为 CBManagerStatePoweredOn 的时候可以开始扫描广播
typedef NS_ENUM(NSInteger, CBCentralManagerState) {
   CBCentralManagerStateUnknown = CBManagerStateUnknown,            // 未知
   CBCentralManagerStateResetting = CBManagerStateResetting,        // 正在重置
   CBCentralManagerStateUnsupported = CBManagerStateUnsupported,    // 不支持
   CBCentralManagerStateUnauthorized = CBManagerStateUnauthorized,  // 未认证
   CBCentralManagerStatePoweredOff = CBManagerStatePoweredOff,      // 没有打开蓝牙
   CBCentralManagerStatePoweredOn = CBManagerStatePoweredOn,        // 蓝牙以打开,可以扫描广播
} NS_DEPRECATED(10_7, 10_13, 5_0, 10_0, "Use CBManagerState instead");
@interface CBCentralManager : CBManager
@property(nonatomic, weak, nullable) id<CBCentralManagerDelegate> delegate;
@property(nonatomic, assign, readonly) BOOL isScanning ;    // 判断蓝牙是否正在扫描广播
// 蓝牙中心设备初始化方法,需要指定 delegate 并实现代理方法
- (instancetype)initWithDelegate:(nullable id<CBCentralManagerDelegate>)delegate
                        queue:(nullable dispatch_queue_t)queue;
                        
// -------------------- 常用方法 --------------------
// 通过服务 ID 来扫描外设
- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs options:(nullable NSDictionary<NSString *, id> *)options;
 
// 连接外设
- (void)connectPeripheral:(CBPeripheral *)peripheral options:(nullable NSDictionary<NSString *, id> *)options;
 
// 取消连接
- (void)cancelPeripheralConnection:(CBPeripheral *)peripheral;
 
// 停止扫描
- (void)stopScan;
 
// 通过 ID 数组查找外设
- (NSArray<CBPeripheral *> *)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> *)identifiers NS_AVAILABLE(10_9, 7_0);
 
// 通过 服务的 UUID 数组查找外设
- (NSArray<CBPeripheral *>*)retrieveConnectedPeripheralsWithServices:(NSArray<CBUUID *> *)serviceUUIDs NS_AVAILABLE(10_9, 7_0);
 
// @protocol CBCentralManagerDelegate <NSObject> 代理方法
 
// 中心设备开始扫描之后,每发现一个外设就会调用一次该方法,同一设备可能会被发现多次,扫描频率基本固定,外设发送的广播不一定每一个都能接收到,如果使用该方法获取广播数据的时候需要注意
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI;
 
// 中心设备连接上外设之后调用
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral;
 
// 连接失败调用
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;
 
// 断开连接
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;
 
// 中心设备状态改变的时候调用
- (void)centralManagerDidUpdateState:(CBCentralManager *)central;
 
// 中心设备状态重置的时候调用
- (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary<NSString *, id> *)dict;

连接过程

外设设备向外广播
中心设备接收广播(广播包含了广播内容和外设的名字,信号强度等属性(iOS 的 CoreBluetooth 框架不能从广播里面获取到外设 MAC 地址,所以不能使用 MAC 地址来区分外设)

扫描
连接
扫描外设所包含的服务 Services
扫描每一个服务所包含的特征 Characteristics
判断特征的属性 CBCharacteristicProperties(根据需求保存对应属性的 Characteristics,后面读/写的时候只能往可读/可写属性的特征里面写, CBCharacteristicProperties 是一个枚举)

typedef NS_OPTIONS(NSUInteger, CBCharacteristicProperties) {
    CBCharacteristicPropertyBroadcast= 0x01,            // 广播属性
    CBCharacteristicPropertyRead= 0x02,                 // 可读属性
    CBCharacteristicPropertyWriteWithoutResponse= 0x04, // 无响应的写
    CBCharacteristicPropertyWrite= 0x08,                // 写
    CBCharacteristicPropertyNotify= 0x10,               // 通知
    CBCharacteristicPropertyIndicate= 0x20,
    CBCharacteristicPropertyAuthenticatedSignedWrites= 0x40,
    CBCharacteristicPropertyExtendedProperties= 0x80,
    CBCharacteristicPropertyNotifyEncryptionRequired= 0x100,
    CBCharacteristicPropertyIndicateEncryptionRequired= 0x200
};

实际使用

CBCentralManager 中心设备的使用方法
@interface XIXCBCenterManager : CBCentralManagerDelegate
@property (strong, nonatomic) CBCentralManager * cbcManager;
...
-(void)propertyInit{
...
// 创建 cbcManager 对象,创建之后会调用代理返回中心设备状态
 self.cbcManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
}
 
#pragma mark - CoreBlueToothDelegate
// 蓝牙状态代理,蓝牙创建之后会调用一次
-(void)centralManagerDidUpdateState:(CBCentralManager *)central{
    switch (central.state) {
        case CBManagerStatePoweredOn:  // 蓝牙状态可用
            NSLog(@"蓝牙已打开,可用,开始扫描");
            // 蓝牙状态可用的时候,开启扫描可用的蓝牙外设
                [self.cbcManager scanForPeripheralsWithServices:nil options:nil];
                [MBProgressHUD ShowMBToView:self.view withMessage:@"正在扫描设备..."];
            break;
        case CBManagerStateUnsupported:
            [MBProgressHUD showError:@"蓝牙不可用"];
            break;
        default:{
            [MBProgressHUD hideHUDForView:self.view];
            UIAlertController * alertVC = [UIAlertController alertControllerWithTitle:nil message:@"请先开启蓝牙功能" preferredStyle:UIAlertControllerStyleAlert];
            UIAlertAction * cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
            UIAlertAction * settingAction = [UIAlertAction actionWithTitle:@"去设置" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action){
                [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
            }];
            [alertVC addAction:cancelAction];
            [alertVC addAction:settingAction];
            [self presentViewController:alertVC animated:YES completion:nil];
        }
            break;
    }
}
 
/**
 扫描到蓝牙外设后调用,每扫描到一个设备信息返回一次,判断返回的该蓝牙是否是已经被添加过的,如果被添加过,则替换数组中的该设备,如果没有添加过则添加到数组
 @param central central
 @param peripheral 扫描到的蓝牙外设
 @param advertisementData 蓝牙外设的额外数据
 @param RSSI 信号强度
 */
-(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(nonnull CBPeripheral *)peripheral advertisementData:(nonnull NSDictionary<NSString *,id> *)advertisementData RSSI:(nonnull NSNumber *)RSSI{
    if (peripheral.name.length <= 0) return;
// 获取外设广播包含的数据
    NSData * adverData = advertisementData[@"kCBAdvDataManufacturerData"] ;
   if(adverData != nil){
    AdvertiseMentModel * advModel = [self.connect getAdvertisementData:adverData];
// 如果扫描到的外设已存在,则替换,如果不存在则添加,刷新列表
    if (advModel) {
        PeripheralModel * peripheralModel;
        if (![self.perArray containsObject:peripheral]) {
            [self.perArray addObject:peripheral];
            peripheralModel = [[PeripheralModel alloc] initWithPeripheral:peripheral];
            [self.deviceArray addObject:peripheralModel];
        }
        NSInteger index = [self.perArray indexOfObject:peripheral];
        peripheralModel = self.deviceArray[index];
        peripheralModel.advertisementModel = advModel;
        [self.tableView reloadData];
    }
  }
}
// 使用列表展示外设,点击 Cell 的时候,获取外设对象并连接
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
        self.selectedPeriphalModel = self.resultList[indexPath.row];
         NSDictionary * dic = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:CBConnectPeripheralOptionNotifyOnDisconnectionKey];
        [self.cbcManager connectPeripheral:self.selectedPeriphalModel.periphal options:dic];
        [MBProgressHUD ShowMBToView:self.view withMessage:@"正在连接设备..."];
// 设置 10s 的连接超时时间,连接超时则调用 [self.cbcManager cancelPeripheralConnection:self.selectedPeriphalModel.periphal]; 取消连接
        [self performSelector:@selector(connectTimeOut) withObject:nil afterDelay:10.0f];
}
// 连接成功调用代理
/**
 连接外设成功后调用
 @param central central
 @param peripheral 连接成功的设备
 */
-(void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{
// 没有超时则需要取消延迟 调用方法 同时停止扫描
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(connectTimeOut) object:nil];
    [self.cbcManager stopScan]; // 停止扫描
    [MBProgressHUD hideHUDForView:self.view animated:YES];
}
/**
 连接失败调用
 @param central central
 @param peripheral 连接的设备
 @param error 错误原因
 */
-(void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error{
    [MBProgressHUD hideHUDForView:self.view animated:YES];
    [MBProgressHUD showError:@"连接失败"];
}
// 断开连接
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error{
    [MBProgressHUD hideHUDForView:self.view animated:NO];
}
CBPeripheral 外设设备的方法使用
#define CHARACTER_UUID_WRITE @"5E9BF2A8-F93F-4481-A67E-3B2F4A07891A"
#define CHARACTER_UUID_NOTI @"8AC32D3F-5CB9-4D44-BEC2-EE689169F626"
@property (strong, nonatomic) CBPeripheral * peripheral;
// 保存可写的特征,写数据的时候用这个特征写入数据
@property (strong, nonatomic) CBCharacteristic * writeCharacter;
// 连接上外设之后调用 discoverServices 方法,查找该外设所包含的服务
 [self.peripheral discoverServices:nil];
// 写入数据
  [self.peripheral writeValue:data forCharacteristic:self.writeCharacter type:CBCharacteristicWriteWithResponse];
 
#pragma mark - CBPeripheralDelegate
/**
 查找蓝牙代理的服务,一个蓝牙外设可能有多个服务
 @param peripheral peripheral
 @param error 错误信息
 */
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{
//    NSString * UUID_str = [peripheral.identifier UUIDString];
//    NSLog(@"UUID:%@",UUID_str);
    if (error) {
        NSLog(@"查找蓝牙服务出错");
        return;
    }
    // 开始遍历查找服务,查找每个服务的特征
    for (CBService * service in peripheral.services) {
        // 如果知道特征的 UUID 可以在第一个参数传入 UUID 数组
        NSLog(@"服务CBUUID:%@",service.UUID.UUIDString);
        [self.peripheral discoverCharacteristics:nil forService:service];
    }
}
// 扫描服务包含的服务
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverIncludedServicesForService:(CBService *)service error:(NSError *)error{
    if (error) {
        NSLog(@"扫描服务内部的服务失败");
        return;
    }
    NSArray * services = service.includedServices;
    for (CBService * service  in services) {
        // 如果知道特征的 UUID 可以在第一个参数传入 UUID 数组
        [peripheral discoverCharacteristics:nil forService:service];
    }
}
// 查找服务的特征,一个服务可能有多个特征,一个特征可能有多个属性,CBCharacteristic 是一个枚举值
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(nonnull CBService *)service error:(nullable NSError *)error{
    if(error){
        NSLog(@"查找特征错误");
        return;
    }
    for (CBCharacteristic * characteristic in service.characteristics) {
// 这里开发的时候和硬件沟通,获取到了具体的特征的 UUID
        if([characteristic.UUID.UUIDString isEqualToString: CHARACTER_UUID_WRITE]){
            self.writeCharacter = characteristic; // 这是可写属性,保存起来后面需要使用它来写数据
        }else if([characteristic.UUID.UUIDString isEqualToString: CHARACTER_UUID_NOTI]){
// 监听该特征,外设发送通知的时候才能收到数据
             [peripheral setNotifyValue:YES forCharacteristic:characteristic];
         }
    }
}
// 接收通知代理
-(void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
    if (error) {
        NSLog(@"错误:%@",error.localizedDescription);
        return;
    }
    CBCharacteristicProperties  properties = characteristic.properties;
    if (properties & CBCharacteristicPropertyRead) {
        // 如果具备读特性,即可读取特性的 Value 值
        [peripheral readValueForCharacteristic:characteristic];
    }
}
// 读取 Characteristic 中的值
-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(nonnull CBCharacteristic *)characteristic error:(nullable NSError *)error{
    if (error) {
        NSLog(@"读取数据错误:%@",error.localizedDescription);
        return;
    }
    NSData * dada = characteristic.value;
    if (dada.length <= 0) {
        return;
    }
    Byte * byteData = (Byte*)[dada bytes];
    if (!(byteData[0] == 0x48 && byteData[1] == 0x65 &&byteData[2] == 0x6c &&byteData[3] == 0x6c)) {
         NSLog(@"通知数据:%@",dada);
    ....
    }
}
// 写入数据的回调,返回写入成功失败结果
-(void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(nonnull CBCharacteristic *)characteristic error:(nullable NSError *)error{
    if (error) {
        NSLog(@"写入数据失败");
    }
}

中心设备就是主动去连接别的设备的设备,一般就是手机,外设就是发送广播,被连接的设备,一般就是手环或者包含蓝牙模块的硬件设备
CoreBlutooth 已经把 ble 封装的很好,只需要顺着代理方法一步一步从扫描到连接到数据读写都可以完成。

另外: 蓝牙广播的数据长度大小有限制,不能通过广播获取外设 MAC 地址来区分,所以区分多个外设的话就需要在广播的 kCBAdvDataManufacturerData 字段的数据里面包含, advertisementData 数据包含多个字段,大家可以在使用的时候打印看看,具体的字段对数据的类型也有限制,服务、特征、描述都是对应的 uuid 来唯一标识,如何知道 UUID 的话直接使用 UUID 能免去多层遍历。

原文链接:https://blog.csdn.net/u012439446/article/details/105484097
更多参考: https://www.jianshu.com/p/927ef9d5d2d1

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350