蓝牙实战总结,就是干货(会持续更新)

背景说明:

由于公司的业务需要在之前积攒了一些关于蓝牙开发经验,在此做过mark , 温故而知新,也希望能给后来的学习者提供一些帮助,那好,here we go !

本篇文章的学习对象是基于蓝牙4.0外设的开发,所以如果涉及到其他的蓝牙版本,请移步了。

蓝牙4.0介绍

有关蓝牙4.0过多的基础介绍就不在这里赘述了,移步这里 百度百科

以下是来自维基:。

蓝牙4.0是Bluetooth SIG于2010年7月7日推出的新的规范。其最重要的特性是支持省电;
Bluetooth 4.0,协议组成和当前主流的Bluetooth h2.x+EDR、还未普及的Bluetooth h3.0+HS不同,Bluetooth 4.0是Bluetooth从诞生至今唯一的一个综合协议规范,
还提出了“低功耗蓝牙”、“传统蓝牙”和“高速蓝牙”三种模式。
其中:高速蓝牙主攻数据交换与传输;传统蓝牙则以信息沟通、设备连接为重点;蓝牙低功耗顾名思义,以不需占用太多带宽的设备连接为主。前身其实是NOKIA开发的Wibree技术,本是作为一项专为移动设备开发的极低功耗的移动无线通信技术,在被SIG接纳并规范化之后重命名为Bluetooth Low Energy(后简称低功耗蓝牙)。这三种协议规范还能够互相组合搭配、从而实现更广泛的应用模式,此外,Bluetooth 4.0还把蓝牙的传输距离提升到100米以上(低功耗模式条件下)。
分Single mode与Dual mode。
Single mode只能与BT4.0互相传输无法向下兼容(与3.0/2.1/2.0无法相通);Dual mode可以向下兼容可与BT4.0传输也可以跟3.0/2.1/2.0传输
超低的峰值、平均和待机模式功耗,覆盖范围增强,最大范围可超过100米。
速度:支持1Mbps数据传输率下的超短数据包,最少8个八组位,最多27个。所有连接都使用蓝牙2.1加入的减速呼吸模式(sniff subrating)来达到超低工作循环。
跳频:使用所有蓝牙规范版本通用的自适应跳频,最大程度地减少和其他2.4 GHz ISM频段无线技术的串扰。
主控制:可以休眠更长时间,只在需要执行动作的时候才唤醒。
延迟:最短可在3毫秒内完成连接设置并开始传输数据。
健壮性:所有数据包都使用24-bit CRC校验,确保最大程度抵御干扰。
安全:使用AES-128 CCM加密算法进行数据包加密和认证。
拓扑:每个数据包的每次接收都使用32位寻址,理论上可连接数十亿设备;针对一对一连接最优化,并支持星形拓扑的一对多连接;使用快速连接和断开,数据可以在网状拓扑内转移而无需维持复杂的网状网络。

不知道你们作何感想,我反正是没看懂,或者懒得看,在我的实际开发过程中,在很多节点上会有疑问,只要解决这些疑问,我的逻辑就通了,学起来和做起来就很容易了,当然在解决现有的需求或者问题之后如果有意愿就要再深究了,以我们当前的蓝牙开发为例。我的逻辑是 发现 —— 连接 —— 读取

第一个问题 :我的手机如何发现周边的蓝牙设备?
这个有现代生活常识的都有经验,打开手机设备通过扫描来发现周边蓝牙设备,

第二个问题:手机设备是通过什么扫描到周边蓝牙的呢?
通过一个广播包,任何的蓝牙设备都会具有一个广播包,这是蓝牙设置之间建立连接必须的一步,有的设备可能一直处于广播的状态,比如现在比较热的小米手环2,有的设备是需要人为参与的来开启广播,比如计步器需要手动摇动,小钢炮音响需要开启开关,综合来看广播这一步是蓝牙设备彼此发现的最重要一步,从扫描到发现彼此这是不可省去的,所谓的人工干预我的理解就是为了其他方面的优化比如降低电量等做的一个控制蓝牙广播的开关而已。

第三个问题:选择某个设备建立了连接,如何获取数据呢?
我们可以大致的分为三步:第一步,我们要获取蓝牙设备的服务(service);第二步,在获取到服务之后,我们要获取到服务下的特征(characteristics);第三部,通过对特征的属性判断和UUID,来进行设置通知获取数据,直接发指令进行读取,或者写入指令。当然在进行读写等操作的时候,要完成API需要实现的代理方法。

说到这里呢,基本上关于在蓝牙开发流程上的简单逻辑就这些了,因人而异如果大家还有其他的问题,可以留言交流。接下来,我们通过实例的形式过一下功能。

首先我们要明确需求:我们要在列表页面搜索到周边的蓝牙设备,然后选中列表中的一个设计进行点击,手机和蓝牙设备进行连接,读取到设备中的各种服务和特征。之后断开,再次启动 app,能够自动连接上我们之前连接过的蓝牙设备。

1:创建项目,在这里就省略了。
2:在 ViewController 中创建列表界面

_tableView = [[UITableView alloc] initWithFrame:self.view.frame];
_tableView.backgroundView.backgroundColor = [UIColor whiteColor];
_tableView.delegate = self;
_tableView.dataSource = self;
[self.view addSubview:_tableView];

创建一个集合用来存放扫描到的蓝牙外设

  _deviceArray = [[NSMutableArray alloc] init];

3:创建一个蓝牙中心

 //创建一个中心蓝牙的管理器
_centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:nil]; //先使用主队列进行开发

参数说明:
delegate : 有经验的同学就不用过多解释了,设置好代理,各种回调方法的使用
queue : 多线程队列,如果传入了队列参数,中心蓝牙会在该线程中处理和接收事件 如果为nil的话,默认在主队列里面
options : 一系列的参数设置 :
|CBCentralManagerOptionShowPowerAlertKey | 表示的是在central manager初始化时,如果当前蓝牙没打开,是否弹出alert框 |

 |CBCentralManagerOptionRestoreIdentifierKey | 一个唯一的标示符,用来蓝牙的恢复连接的。在后台的长连接中可能会用到。
 就是说,如果蓝牙程序进入后台,程序会被挂起,可能由于memory pressure,程序被系统kill了,那么代理方法就不会执行了。这时候可以使用State Preservation & Restoration,这样程序会重新加载进入后台。|

注意:这里有两个概念需要澄清下,中心蓝牙(central)和 外设蓝牙(Peripheral) ,分别是什么概念呢?我的理解:在蓝牙设备的连接中存在一个角色的定位问题,一个蓝牙是即可以当中心蓝牙也可以当外设蓝牙,这取决于在需求实现上,谁来主导业务的走向,举例,我们手机设备要链接计步器进行步数的收集和上传,此时手机是要先确定自己的中心角色,然后发起扫描,发现周边的蓝牙设备时,周边的蓝牙设备都是以外设蓝牙的角色存在,及时搜索到的是手机。

4:设置参数发起扫描

    /**
 serviceUUIDs : 包含一个或者多个服务UUID的集合,会根据服务的UUID来进行设备的扫描,
 options : 一系列的参数设置
 | CBCentralManagerScanOptionAllowDuplicatesKey       | |
 | CBCentralManagerScanOptionSolicitedServiceUUIDsKey | |
 */
 NSDictionary *option = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:NO],CBCentralManagerScanOptionAllowDuplicatesKey, nil];
[_centralManager scanForPeripheralsWithServices:nil options:option];

5:在扫描到设置之后,会调用代理方法:

  • (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI

    /*
     在中心蓝牙扫描到外设蓝牙后调用该方法。
     该方法每次只返回一个蓝牙外设的信息
     第一个参数:中心蓝牙对象
     第二个参数:本次扫描到的蓝牙外设
     第三个参数:蓝牙外设中的额外信息,——蓝牙外设的广播包中的信息。
     第四个参数:代表信号强度的参数,RSSI:(要做详细介绍)
    
     注意:扫描的蓝牙设备有以下几种情况:
       1:扫描的蓝牙是无用的蓝牙。
       2:扫描的蓝牙是重复扫描到的蓝牙。(存在一种可能就是重复扫描到的蓝牙有变化,这种变化不是指蓝牙外设携带的数据发生变化,是指蓝牙外设本身的参数发生变化)
    
     所有针对上述的两种情况,我们需要一段代码进行逻辑上的处理:
       1:剔除无用的蓝牙。
       2:替换到的旧信息蓝牙外设,插入新的蓝牙外设信息。
     */
    
    
    - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI
    

    {
    /*
    思考:所有的蓝牙外设都必须要有 name 吗?
    */
    if (peripheral.name.length <= 0) {
    return;
    }

    //打印参数信息
    NSLog(@"发现外部设备");
    NSLog(@"接收到的广播信息:%@",advertisementData);
    NSLog(@"蓝牙外设信息:设备identifier : %@ 设备名称:%@ 信号强度:%@", peripheral.identifier,peripheral.name, RSSI);

if (_deviceArray.count <= 0) {
    
    NSDictionary *peripheralDic = [NSDictionary dictionaryWithObjectsAndKeys:peripheral,@"peripheral",RSSI,@"RSSI", nil];
    [_deviceArray addObject:peripheralDic];
    
}else{
    bool isExist = NO;
    for (int i = 0; i < _deviceArray.count ; i ++) {
        NSDictionary *peripheralDic = [_deviceArray objectAtIndex:i];
        CBPeripheral *peripheralFromArray = [peripheralDic objectForKey:@"peripheral"];
        if ([peripheralFromArray.identifier.UUIDString isEqualToString:peripheral.identifier.UUIDString]) {
            isExist = YES;
            NSDictionary *newPeripheralDic = [NSDictionary dictionaryWithObjectsAndKeys:peripheral,@"peripheral",RSSI,@"RSSI", nil];
            [_deviceArray replaceObjectAtIndex:i withObject:newPeripheralDic];
        }
    }
    if (!isExist) {
        NSDictionary *newPeripheralDic = [NSDictionary dictionaryWithObjectsAndKeys:peripheral,@"peripheral",RSSI,@"RSSI", nil];
        [_deviceArray addObject:newPeripheralDic];
    }
    
}

[_tableView reloadData];

}

6:在搜索到的设备列表中,选择其中的一个设备进行建立连接操作,

/**
 peripheral : 要进行连接的外设
 options :在链接过程中的一系列参数设置
 CBConnectPeripheralOptionNotifyOnConnectionKey :连接通知
 CBConnectPeripheralOptionNotifyOnDisconnectionKey : 断开连接通知
 CBConnectPeripheralOptionNotifyOnNotificationKey :
 */

 [_centralManager connectPeripheral:peripheral options:nil];

//点击某个cell,进行外设的连接
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{


NSDictionary *peripheralDic = (NSDictionary *)[_deviceArray objectAtIndex:indexPath.row];
CBPeripheral *peripheral = (CBPeripheral *)[peripheralDic objectForKey:@"peripheral"];

//连接某个蓝牙外设
[_centralManager connectPeripheral:peripheral options:nil];
//设置蓝牙外设的代理;
peripheral.delegate = self;
//停止中心蓝牙的扫描动作
[_centralManager stopScan];
}

7:当中心蓝牙设备和外设建立完成

/**
 中心蓝牙和某个外设连接成功。
 */
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
NSLog(@"和外设链接成功");
//设备连接成功,开始查找建立连接的蓝牙外设的服务;此处要注意,在建立连接之后,是通过蓝牙外设的对象去发现服务而非中心蓝牙。
[peripheral discoverServices:nil];
}


/**
 中心蓝牙和某个外设连接失败。
 */
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error
{
NSLog(@"和外设链接失败");
}

8:在第7步中已经发起了对服务的扫描
9:发现服务后,执行回调方法

/**
 在发现服务时,进行调用
 */
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error
{
NSString *UUID = [peripheral.identifier UUIDString];
NSLog(@"在didDiscoverServices方法中,peripheral.identifier = %@",UUID);

//遍历所提供的服务
for (CBService *service in peripheral.services) {
    CBUUID *serviceUUID = service.UUID;
    NSLog(@"serviceUUID = %@",[serviceUUID UUIDString]);
    
    /**
     如果我们知道要查询的特性的 CBUUID,可以在第一个参数中传入 CBUUID 的数组
     发现在服务下的特征
     */
    [peripheral discoverCharacteristics:nil forService:service];
}
}

10:在发现服务之后,发现服务中的特征
11:发现特征后的回调

注意:在特征这一层,特征的读写变更等都是有相应的属性,我们可以通过数据判断之后,来进行数据的读写操作。

/**
 在发现服务中的特征后,调用该方法

 */
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error
{
if (error) {
    NSLog(@"在检索服务中的特征时出错");
    return;
}

NSLog(@"在 didDiscoverCharacteristicsForService 方法中遍历服务 %@ 中的特征",[service.UUID UUIDString]);
for (CBCharacteristic *characteristic in service.characteristics) {
    
    NSLog(@"特征 UUID = %@",[characteristic.UUID UUIDString]);
   
    /**
     在这里要注意,很多同学看到是枚举类型就是用的 == ,这是不对的,
     学习链接:http://lecason.com/2015/08/19/Objective-C-Find-Conbine/
     */

    if (characteristic.properties & CBCharacteristicPropertyExtendedProperties) {
        NSLog(@"具备可拓展特性。");
    }
    if (characteristic.properties & CBCharacteristicPropertyRead) {
        NSLog(@"具备可读特性,即可以读取特征的 value 值");
        //对该特征进行读取
        [peripheral readValueForCharacteristic:characteristic];
    }
    if (characteristic.properties & CBCharacteristicPropertyWrite) {
        NSLog(@"具备可写特征,会有响应");
    
        NSString *stringD = @"要写入的数据";
        [peripheral writeValue:[ViewController stringToByte:stringD] forCharacteristic:characteristic type:CBCharacteristicWriteWithResponse];
    }
    if (characteristic.properties & CBCharacteristicPropertyNotify) {
        NSLog(@"具备通知特性,无响应");
        //对该特征设置通知的监听
        [peripheral setNotifyValue:YES forCharacteristic:characteristic];
        [peripheral readValueForCharacteristic:characteristic];
    }
    if (characteristic.properties & CBCharacteristicPropertyIndicate) {
        NSLog(@"具备指示特性");
    }
    if (characteristic.properties & CBCharacteristicPropertyBroadcast) {
        NSLog(@"具备广播特性");
        //对该特征设置通知的监听
        [peripheral setNotifyValue:YES forCharacteristic:characteristic];
        [peripheral readValueForCharacteristic:characteristic];
    }
    if (characteristic.properties & CBCharacteristicPropertyWriteWithoutResponse) {
        NSLog(@"具备可写,又不会有响应的特性");
    }
}
}

12:读取特征中的数据回调方法

/**
 获取特征的值后调用的方法
 */
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error
{
NSLog(@"didUpdateValueForCharacteristic : characteristic.uuid = %@",[characteristic.UUID UUIDString]);
if (error) {
    NSLog(@"读取特征失败!");
}
NSData *data = characteristic.value;
if (data.length <= 0) {
    return;
}

Byte *plainTextByte = (Byte *)[data bytes];
NSString *hexStr=@"";
for(int i=0;i<[data length];i++)
{
    NSString *newHexStr = [NSString stringWithFormat:@"%x",plainTextByte[i]&0xff];///16进制数
    if([newHexStr length]==1)
        hexStr = [NSString stringWithFormat:@"%@0%@",hexStr,newHexStr];
    else
        hexStr = [NSString stringWithFormat:@"%@%@",hexStr,newHexStr];
}

//    NSString *info = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
NSLog(@"hexStr = %@",hexStr);

}

13:写入特征数据回调方法

  /**
  在向蓝牙设备中写完指令后,调用的回调方法。
 */
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error
{
if (error) {
    NSLog(@"didWriteValueForCharacteristic,在写入指令时发生错误");
    return;
}
NSLog(@"对蓝牙的指令写入成功!");

}

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

推荐阅读更多精彩内容