iOS 蓝牙4.0 BLE连接与数据传输

公司继二维码开门之后,又新增了蓝牙开门,网上有好多帖子写的都很详细,整理一下,备用。
DEMO

简介

蓝牙4.0 BLE:(Bluetooth low energy) 它的有点在于传输快,耗电低,但传输数据有限,虽然这个传输字节大小硬件工程师可调,但也不会太大。

参数 描述
center 中心设备,发起蓝牙连接的设备(一般是手机)
Peripheral 外设,被蓝牙连接的设备
Serice and Characteristic 服务和特征,每个设备会提供服务和特征,类似于服务端的API,但是结构不同,每个设备会有很多服务,每个服务中包含很多特征,这些特征的权限一般分为读(read),写(write),通知(notify)几种,就是我们连接设备后具体需要操作的内容
Description 描述,每个Characteristic可以对应一个或者多个Description用于描述Characteristic的信息或属性

准备

1.建议下载一个蓝牙助手,直接App Store上搜索‘蓝牙助手’下载就行

实际开发中可以和硬件工程师沟通好,将数据写入哪个UUID中,然后用助手测试一下。


应用主页就这样,特简单
2. 引入头文件、接入代理、定义变量
#import <CoreBluetooth/CoreBluetooth.h>

#define M_BLE_NAME @"xxxx"
#define M_BLE_MAC  @"xxxxxxxxx"

@interface ViewController ()<CBCentralManagerDelegate, CBPeripheralDelegate>

/**
 手机设备
 */
@property (nonatomic, strong) CBCentralManager *centralManager;

/**
 外设设备
 */
@property (nonatomic, strong) CBPeripheral *peripheral;

/**
 特征值
 */
@property (nonatomic, strong) CBCharacteristic *characteristic;

/**
 服务
 */
@property (nonatomic, strong) CBService *service;

/**
 描述
 */
@property (nonatomic, strong) CBDescriptor *descriptor;

开始

1. 实例化设备
//MARK: 1.初始化设备
- (void)viewDidLoad {
    [super viewDidLoad];
    // 初始化设备
    self.centralManager =[[CBCentralManager alloc]initWithDelegate:self queue:nil];
}
2. 退出页面后,停止扫描 断开连接
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    // 停止扫描
    [self.centralManager stopScan];
    // 断开连接
    if(self.centralManager!=nil&&self.peripheral.state==CBPeripheralStateConnected){
        [self.centralManager cancelPeripheralConnection:self.peripheral];
    }
}
3.实例化设备后,会走代理判断蓝牙状态
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
    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");
            //TODO: 搜索外设
            // services:通过某些服务筛选外设 传nil=搜索附近所有设备
            [self.centralManager scanForPeripheralsWithServices:nil options:nil];
        }
            break;
        default:
            break;
    }
}
4.发现设备列表后判断
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI {
    NSLog(@"\n设备名称:%@",peripheral.name);
    //TODO: 使用名字判断
    /*
    if ([peripheral.name isEqualToString:M_BLE_NAME]) {
        self.peripheral = peripheral;
        [self.centralManager connectPeripheral:peripheral options:nil];
    }
     */
    //TODO: 使用Mac地址判断
    NSData *data  =[advertisementData objectForKey:@"kCBAdvDataManufacturerData"];
    NSString *mac =[[self convertToNSStringWithNSData:data] uppercaseString];// uppercaseString转大写字母

    if([mac rangeOfString:M_BLE_MAC].location != NSNotFound){
        self.peripheral = peripheral;
        // 连接外设
        [self.centralManager connectPeripheral:peripheral options:nil];
    }
}
- (NSString *)convertToNSStringWithNSData:(NSData *)data {
    NSMutableString *strTemp = [NSMutableString stringWithCapacity:[data length]*2];
    const unsigned char *szBuffer = [data bytes];
    for (NSInteger i=0; i < [data length]; ++i) {
        [strTemp appendFormat:@"%02lx",(unsigned long)szBuffer[i]];
    }
    return strTemp;
}

PS: 这里需要判断连接哪个外设,一般是根据名字判断,我们的比较特殊,设备的名字都一样,只能根据MAC地址来判断。
MAC地址获取有两种方法:一是连接成功后通过外设的特征值的属性截取,网上有很多,不写了。
二是先获取mac地址再连接:根据上面代理方法中的 advertisementData,它是一个广播包,里面会有一些设备的属性,但都被苹果给限制了,只有一个kCBAdvDataManufacturerData幸免,它是可以修改的,所以要跟硬件工程师协商好,将mac地址写入这个key里面去。
这里可以通过蓝牙助手看一下,硬件工程师是否写入了。

后面是mac地址

5.外设连接回调
//MARK: 5.1 外设连接成功
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
    NSLog(@"设备连接成功:%@", peripheral.name);
    // 设置代理
    [self.peripheral setDelegate:self];
    //MARK: 6.1 外设发现服务,传nil代表不过滤
    [self.peripheral discoverServices:nil];
    // 停止扫描
    [self.centralManager stopScan];
}

//MARK: 5.2 外设连接失败
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
    NSLog(@"设备连接失败:%@", peripheral.name);
    // 重新从搜索外设开始
    [self.centralManager scanForPeripheralsWithServices:nil options:nil];
    
}

//MARK: 5.3 丢失连接
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
    NSLog(@"设备丢失连接:%@", peripheral.name);
    // 重新从搜索外设开始
    [self.centralManager scanForPeripheralsWithServices:nil options:nil];
    
}
6.发现外设的服务后回调
//MARK: 6.2 发现外设的服务后调用的方法
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
    // 是否获取失败
    if (error) {
        NSLog(@"设备获取服务失败:%@", peripheral.name);
        return;
    }
    for (CBService *service in peripheral.services) {
        self.service = service;
        NSLog(@"设备的服务(%@),UUID(%@),count(%lu)",service,service.UUID,peripheral.services.count);
        //MARK: 7.1 外设发现特征
        [peripheral discoverCharacteristics:nil forService:service];
    }
}
7.服务中发现外设特征回调
//MARK: 7.2 从服务中发现外设特征的时候调用的代理方法
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
    
    for (CBCharacteristic *cha in service.characteristics) {
        NSLog(@"\n设备的服务(%@)\n服务对应的特征值(%@)\nUUID(%@)\ncount(%lu)",service,cha,cha.UUID,service.characteristics.count);
        //MARK: 8.1 获取特征对应的描述 会回调didUpdateValueForDescriptor
        [peripheral discoverDescriptorsForCharacteristic:cha];
        //MAKR: 9.1获取特征的值 会回调didUpdateValueForCharacteristic
        [peripheral readValueForCharacteristic:cha];
        // 这里需要和硬件工程师协商好,数据写在哪个UUID里
        if([cha.UUID isEqual:[CBUUID UUIDWithString:@"FFE1"]]){
            self.characteristic = cha;
        } else {
            // 打开外设的通知,否则无法接受数据
            // 这里也是根据项目,和硬件工程师协商好,是否需要打开通知,和打开哪个UUID的通知。
            [peripheral setNotifyValue:YES forCharacteristic:cha];         
         }
    }
}

PS:这里需要和硬件工程师协商好,数据写在哪个UUID里,根据项目,是否需要打开通知,和打开哪个UUID的通知。

8. 更新描述值回调
//MARK: 8.2 更新描述值的时候会调用
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForDescriptor:(CBDescriptor *)descriptor error:(NSError *)error {
    NSLog(@"描述(%@)",descriptor.description);
}
9. 更新特征值回调
//MARK: 9.2 更新特征值回调,可以理解为获取蓝牙发回的数据
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    NSString *value = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
    NSLog(@"设备的特征值(%@),获取的数据(%@)",characteristic,value);
    //这里可以在这里获取描述
    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"FFE2"]]) {
        NSData *data =characteristic.value;
        NSLog(@"%@",[[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]);
    }
}

PS:可以在这里接收硬件返回的信息

//MARK: 通知状态改变回调
-(void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
    if(error){
        NSLog(@"改变通知状态");
    }
}

//MAKR: 发现外设的特征的描述数组
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverDescriptorsForCharacteristic:(nonnull CBCharacteristic *)characteristic error:(nullable NSError *)error
{
    // 在此处读取描述即可
    for (CBDescriptor *descriptor in characteristic.descriptors) {
        self.descriptor = descriptor;
        NSLog(@"发现外设的特征descriptor(%@)",descriptor);
        [peripheral readValueForDescriptor:descriptor];
    }
}
10. 发送数据
//MARK: 发送数据
-(void)sendDataToBLE:(NSData *)data{
    if(nil != self.characteristic){
        // data: 数据data
        // characteristic: 发送给哪个特征
        // type:     CBCharacteristicWriteWithResponse,  CBCharacteristicWriteWithoutResponse,
        // 这里要跟硬件确认好,写入的特征是否有允许写入,允许用withResponse 不允许只能强行写入,用withoutResponse
        // 或者根据 10.2 回调的error查看一下是否允许写入,下面说
        // 我这里是不允许写入的,所以用了 WithoutResponse
        [self.peripheral writeValue:data forCharacteristic:self.characteristic type:CBCharacteristicWriteWithoutResponse];
    }
发送数据回调
//MARK: 10.2 发送数据成功回调
-(void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
    if (error) {
        NSLog(@"写入数据失败:(%@)\n error:%@",characteristic,error.userInfo);
        // 这里用withResponse如果报错:"Writing is not permitted."说明设备不允许写入,这个时候要用 WithoutResponse
        // 使用 WithoutResponse的时候,不走这个代理。
        return;
    }
    NSLog(@"写入数据成功:%@",characteristic);
    [peripheral readValueForCharacteristic:characteristic];
}

PS:

  1. 这里要跟硬件确认好,写入的特征是否有允许写入,允许用withResponse 不允许只能强行写入,用withoutResponse, 或者查看回调中error,返回 "Writing is not permitted." 表示不允许写入,使用withoutResponse。
  2. 如果是使用withoutResponse写入的,不走这个回调。
11. 分段发送数据

像我们这个开门码,大概100多字节,而我们硬件的withoutResponse情况下只支持20字节,所以只能分段发送。

//MARK: 分段写入
- (void)writeData:(NSData *)data
{
    // 判断能写入字节的最大长度
    int maxValue;
    if (@available(iOS 9.0, *)) {
        // type:这里和上面一样,
        maxValue =(int)[self.peripheral maximumWriteValueLengthForType:CBCharacteristicWriteWithoutResponse];
    } else {
        // 默认是20字节
        maxValue =20;
    }
    NSLog(@"%i",maxValue);
    for (int i = 0; i < [data length]; i += maxValue) {
        // 预加 最大包长度,如果依然小于总数据长度,可以取最大包数据大小
        if ((i + maxValue) < [data length]) {
            NSString *rangeStr = [NSString stringWithFormat:@"%i,%i", i, maxValue];
            NSData *subData = [data subdataWithRange:NSRangeFromString(rangeStr)];
            [self sendDataToBLE:subData];
            // 根据接收模块的处理能力做相应延时
            usleep(10 * 1000);
        }
        else {
            NSString *rangeStr = [NSString stringWithFormat:@"%i,%i", i, (int)([data length] - i)];
            NSData *subData = [data subdataWithRange:NSRangeFromString(rangeStr)];
            [self sendDataToBLE:subData];
            usleep(10 * 1000);
        }
    }
}

总结遇到的坑

到这里基本结束了,iOS的蓝牙这接起来还是比较容易的,苹果已经封装好了,只要倒库接数据就好。但我还是磕磕绊绊做了两天,原因就不说了,请看遇到的几个坑:

1. Mac地址

之前网上翻帖子,基本都是连接后先连接再获取mac地址,要用mac地址判断就得一个个连接然后获取mac地址再比对,不对再断了重连,要么重写蓝牙底层,特别恶心,之后用蓝牙助手测试发现里面有一个值很像后台给我的mac地址,然后再去翻帖子,发现真的是mac地址。

2. 发现外设特征后选择对应特征发送数据

一直一直返回发送消息失败,打印error发现不允许写入,改成without还是失败,硬件各种没反应,最后安卓的同事表示他那发送不成功是因为系统限制了像设备发送字节数,改了后可以发送了,于是在晚上找了分段发送的方法。
发现有写入权限的特征,硬件都自己改成了512字节,而没有写入权限的特征,只有20字节,分段发送之后,表示可以了。

3. 手机不向硬件发送数据。。。

怎么都不行,以至于代码改了好多版,最后确定没有问题了,就是不行,后来发现用助手也不行,于是重启了手机。。。就好了。
所以如果确定代码没问题,可以重启下手机试试,真可能是手机的问题~~

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

推荐阅读更多精彩内容