最近项目中需要与硬件进行蓝牙连接, 实现数据交互.
一般来说, 外设会由硬件工程师开发好,并定义好设备提供的服务, 每个服务对于的特征, 每个特征的属性(只读, 只写, 通知等等). 本文例子的业务场景,就是用一手机app去读写蓝牙设备.
在这里主要说一下 iOS 设备作为中心模式 连接外设的实现思路.
一、蓝牙中心模式流程
1. 建立中心角色
2. 扫描外设(discover)
3. 连接外设(connect)
4. 扫描外设中的服务和特征(discover)
- 4.1 获取外设的services
- 4.2 获取外设的Characteristics,获取Characteristics的值,获取Characteristics的Descriptor和Descriptor的值
- 4.3 读取数据
5. 与外设做数据交互(explore and interact)
- 5 .1 写数据
6. 订阅Characteristic的通知
7. 断开连接(disconnect)
二、实现步骤
1 . 导入 CoreBluetooth 头文件 #import <CoreBluetooth/CoreBluetooth.h> , 建立主设备管理类, 设置主设备委托
#import <CoreBluetooth/CoreBluetooth.h>
@interface CentralVewController ()<CBCentralManagerDelegate,CBPeripheralDelegate>
{
//系统蓝牙设备管理对象, 可以把他理解为主设备, 通过他, 可以去扫描和链接外设
CBCentralManager *_centralManager;
//用于保存被发现设备
NSMutableArray *_allPeripherals;
}
@end
@implementation CentralVewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.navigationItem.title = @"蓝牙开门";
//初始化并设置委托和线程队列,最好一个线程的参数可以为nil,默认会就main线程
_centralManager = [[CBCentralManager alloc]initWithDelegate:self queue:dispatch_get_main_queue()];
//扫描的所有设备
_allPeripherals = [NSMutableArray array];
}
2 . 扫描外设, 扫描外设的方法我们放在centralManager成功打开的委托中, 因为只有设备成功打开, 才能开始扫描, 否则会报错.
//这个方法主要是来检查IOS设备的蓝牙硬件的状态的,比如说你的设备不支持蓝牙4.0,或者说你的设备的蓝牙没有开启,没有被授权什么的,一般是在你确定了你的IOS设备的蓝牙处于打开的情况下,你才应该执行扫描的动作,
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
switch (central.state) {
case CBManagerStatePoweredOff:
//系统蓝牙关闭了,请先打开蓝牙
NSLog(@"state = CBManagerStatePoweredOff");
break;
case CBManagerStatePoweredOn:
NSLog(@"state = CBManagerStatePoweredOn");
//开始扫描周围外设
[_centralManager scanForPeripheralsWithServices:nil options:nil];
break;
default:
break;
}
}
3 . 连接外设 (connect peripheral)
//扫描到设备会进入该方法(根据扫描到的设备数会多次调用)
-(void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary *)advertisementData
RSSI:(NSNumber *)RSSI{
//这个方法是一旦扫描到外设就会调用的方法,注意此时并没有连接上外设,这个方法里面,你可以解析出当前扫描到的外设的广播包信息,当前RSSI等,现在很多的做法是,会根据广播包带出来的设备名,初步判断是不是自己公司的设备,才去连接这个设备,就是在这里面进行判断的
//另外,当已发现的 peripheral 发送的数据包有变化时,这个代理方法同样会调用
//在搜索过程中,并不是所有的 service 和 characteristic 都是我们需要的,如果全部搜索,依然会造成不必要的资源浪费。
NSLog(@"扫描到设备 = %@ ",peripheral);
NSLog(@"扫描到设备名称 = %@ ",peripheral.name);
NSLog(@"扫描到设备的标识 = %@ ",peripheral.identifier.UUIDString);
NSData *data = [advertisementData objectForKey:@"kCBAdvDataManufacturerData"];
NSString *aStr= [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
aStr = [aStr stringByReplacingOccurrencesOfString:@" " withString:@""];
NSLog(@"aStr:%@",aStr);
NSLog(@"advertisementData:%@",advertisementData);
NSLog(@"信号强度RSSI = %@",RSSI);
// 一个周边可能会被多次发现
[self matchDeviceWithPeripherals:peripheral];
}
#pragma mark 匹配设备
//在这里匹配自己需要连接的设备
- (void)matchDeviceWithPeripherals:(CBPeripheral *)peripheral {
if (![_allPeripherals containsObject:peripheral]) {
//将设备添加到数组中后, 在寻找匹配可连接的设备, 进行连接
[_allPeripherals addObject:peripheral];
//连接设备
[_centralManager connectPeripheral:peripheral options:nil];
//当你找到你需要的那个 peripheral 时,可以调用stop方法来停止搜索。
[_centralManager stopScan];
NSLog(@"Scanning stopped");
//刷新表
[self.tableView reloadData];
}
}
4 . 扫描外设中的服务和特征
4 . 1 获取外设的services
#pragma mark 4.1获取外设的services
//连接到Peripherals-成功
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{
NSLog(@"---成功连接到设备 : %@",peripheral.name);
//设置的peripheral委托CBPeripheralDelegate
//@interface ViewController : UIViewController<CBCentralManagerDelegate,CBPeripheralDelegate>
[peripheral setDelegate:self];
/*
扫描外设Services,成功后会进入方法:-(void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{
在实际项目中,这个参数应该不是nil的,因为nil表示查找所有可用的Service,但实际上,你可能只需要其中的某几个。搜索全部的操作既耗时又耗电,所以应该提供一个要搜索的 service 的 UUID 数组。
*/
[peripheral discoverServices:@[[CBUUID UUIDWithString:SERVICE_UUID]]];
}
//扫描到Services
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{
//在调用 CBCentralManager 的 scanForPeripheralsWithServices:options: 方法时,central 会打开无线电去监听正在广播的 peripheral,并且这一过程不会自动超时。(所以需要我们手动设置 timer 去停掉)
NSLog(@"---扫描到服务 :%@",peripheral.services);
if (error) {
NSLog(@"---扫描到Services : %@ 出现错误 : %@", peripheral.name, [error localizedDescription]);
return;
}
//如果是搜索的全部 service 的话,你可以选择在遍历的过程中,去对比 UUID 是不是你要找的那个。
for (CBService *service in peripheral.services) {
NSLog(@"---扫描到Services的 UUID = %@",service.UUID);
/*
扫描每个service的Characteristics,扫描到后会进入方法: -(void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
同样是出于节能的考虑,第一个参数在实际项目中应该是 characteristic 的 UUID 数组。
*/
[peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:CHARACTERISTIC_UUID]] forService:service];
}
}
4 . 2 获取外设的Characteristics,获取Characteristics的值,获取Characteristics的Descriptor和Descriptor的值
#pragma mark 获取外设的Characteristics,获取Characteristics的值,获取Characteristics的Descriptor和Descriptor的值
//扫描到Characteristics
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{
if (error) {
NSLog(@"---发现 characteristics : %@ 出现错误 : %@", service.UUID, [error localizedDescription]);
return;
}
/*
发现了(指定)的特征值了,如果你想要有所动作,你可以直接在这里做,比如有些属性为 notify 的 Characteristics ,你想要监听他们的值,可以这样写
当找到 characteristic 之后,可以通过调用CBPeripheral的readValueForCharacteristic:方法来进行读取。
其实使用readValueForCharacteristic:方法并不是实时的。考虑到很多实时的数据,比如心率这种,那就需要订阅 characteristic 了。
*/
for (CBCharacteristic *characteristic in service.characteristics) {
NSLog(@"服务 service UUID :%@ 的 特征 Characteristic UUID : %@",service.UUID,characteristic.UUID);
if ([[characteristic.UUID UUIDString] isEqualToString:CHARACTERISTIC_UUID]) {
//成功与否的回调是peripheral:didUpdateNotificationStateForCharacteristic:error:,读取中的错误会以 error 形式传回:
//当然也不是所有 characteristic 都允许订阅,依然可以通过CBCharacteristicPropertyNoify options 来进行判断。
[peripheral setNotifyValue:YES forCharacteristic:characteristic]; //不想监听的时候,设置为:NO 就行了
//如果写入成功后要回调,那么回调方法是peripheral:didWriteValueForCharacteristic:error:。如果写入失败,那么会包含到 error 参数返回。
[self writeCharacteristic:peripheral characteristic:characteristic];// 1.写数据
}else if ([[characteristic.UUID UUIDString] isEqualToString:@""]){
//获取Characteristic的值,读到数据会进入方法:-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
[peripheral readValueForCharacteristic:characteristic];// 2.读数据
}else{
//搜索Characteristic的Descriptors,读到数据会进入方法:-(void)peripheral:(CBPeripheral *)peripheral didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
[peripheral discoverDescriptorsForCharacteristic:characteristic];// 3.获取特征描述
}
//注: 这里根据自己需求 或读数据, 或写数据
}
}
4 .3 读取数据
#pragma mark - 读取回调特征值
//获取的charateristic的值
-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
//打印出characteristic的UUID和值
//!注意,value的类型是NSData,具体开发时,会根据外设协议制定的方式去解析数据
//这个可是重点了,你收的一切数据,基本都从这里得到,你只要判断一下 [characteristic.UUID UUIDString] 符合你们定义的哪个,然后进行处理就行,值为:characteristic.value 一切数据都是这个,至于怎么解析,得看你们自己的了
//[characteristic.UUID UUIDString] 注意: UUIDString 这个方法是IOS 7.1之后才支持的,要是之前的版本,得要自己写一个转换方法
NSLog(@"--- receiveData = %@,fromCharacteristic.UUID = %@",characteristic.value,characteristic.UUID);
NSData *data = characteristic.value;//特征的值
NSString *cValueStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"--- 读取回调特征值 receiveData = %@",cValueStr);
/*
注意,不是所有 characteristic 的值都是可读的,你可以通过CBCharacteristicPropertyRead options 来进行判断
如果你尝试读取不可读的数据,那上面的代理方法会返回相应的 error。
*/
}
//搜索到Characteristic的Descriptors
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
//打印出Characteristic和他的Descriptors
NSLog(@"--- 搜索到Characteristic uuid:%@",characteristic.UUID);
for (CBDescriptor *d in characteristic.descriptors) {
NSLog(@"--- 特征描述符 Descriptor uuid:%@",d.UUID);
}
}
//获取到Descriptors的值
-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForDescriptor:(CBDescriptor *)descriptor error:(NSError *)error{
//打印出DescriptorsUUID 和value
//这个descriptor都是对于characteristic的描述,一般都是字符串,所以这里我们转换成字符串去解析
NSLog(@"--- 特征描述符 characteristic descriptor.UUID:%@ value:%@",[NSString stringWithFormat:@"%@",descriptor.UUID],descriptor.value);
}
5 . 与外设做数据交互
5 .1 写数据
//在 didDiscoverCharacteristicsForService 方法中, 通过判断 UUID 来对相应的特征写数据
//写数据
-(void)writeCharacteristic:(CBPeripheral *)peripheral
characteristic:(CBCharacteristic *)characteristic {
//打印出 characteristic 的权限,可以看到有很多种,这是一个NS_OPTIONS,就是可以同时用于好几个值,常见的有read,write,notify,indicate,知知道这几个基本就够用了,前连个是读写权限,后两个都是通知,两种不同的通知方式。
NSLog(@"--- characteristic.properties = %lu", (unsigned long)characteristic.properties);
//只有 characteristic.properties 有write的权限才可以写
if(characteristic.properties & CBCharacteristicPropertyWriteWithoutResponse){
//发送开门命令
NSString *dataStr = @"自己需要发送的数据";
NSData *data = [NSData dataWithData:[dataStr dataUsingEncoding:NSASCIIStringEncoding]];
/*
最好一个type参数可以为CBCharacteristicWriteWithResponse或CBCharacteristicWriteWithoutResponse,区别是是否会有反馈
*/
[peripheral writeValue:data forCharacteristic:characteristic type:CBCharacteristicWriteWithoutResponse];
NSLog(@"---可以数据");
}else{
NSLog(@"---无法写入数据");
}
}
- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
//这个方法被调用是因为你主动调用方法: setNotifyValue:forCharacteristic 给你的反馈
NSLog(@"---你更新了对特征值:%@ 的通知",[characteristic.UUID UUIDString]);
}
6 . 订阅Characteristic的通知
#pragma mark - 6 订阅Characteristic的通知
//设置通知
-(void)notifyCharacteristic:(CBPeripheral *)peripheral
characteristic:(CBCharacteristic *)characteristic{
//设置通知,数据通知会进入:didUpdateValueForCharacteristic方法
[peripheral setNotifyValue:YES forCharacteristic:characteristic];
}
//取消通知
-(void)cancelNotifyCharacteristic:(CBPeripheral *)peripheral
characteristic:(CBCharacteristic *)characteristic{
[peripheral setNotifyValue:NO forCharacteristic:characteristic];
}
7 . 断开连接(disconnect)
#pragma mark - 7 断开连接(disconnect)
//一般在交互结束之后, 应马上断掉连接
//停止扫描并断开连接
-(void)disconnectPeripheral:(CBCentralManager *)centralManager
peripheral:(CBPeripheral *)peripheral{
//停止扫描
[centralManager stopScan];
//断开连接
[centralManager cancelPeripheralConnection:peripheral];
}
此外, 还有一些其他代理方法, 可根据自身需要来设置
//连接到Peripherals-失败
-(void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error{
//看苹果的官方解释 {@link connectPeripheral:options:} ,也就是说链接外设失败了
NSLog(@"---连接到名称为(%@)的设备-失败,原因:%@",[peripheral name],[error localizedDescription]);
}
//Peripherals断开连接
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error{
//自己看看官方的说明,这个函数被调用是有前提条件的,首先你的要先调用过了 connectPeripheral:options:这个方法,其次是如果这个函数被回调的原因不是因为你主动调用了 cancelPeripheralConnection 这个方法,那么说明,整个蓝牙连接已经结束了,不会再有回连的可能,得要重来了
//如果你想要尝试回连外设,可以在这里调用一下链接函数
NSLog(@"---外设连接断开连接 %@: 原因: %@", [peripheral name], [error localizedDescription]);
}
//根据 信号强度 估算距离
- (void)peripheral:(CBPeripheral *)peripheral didReadRSSI:(NSNumber *)RSSI error:(NSError *)error {
//这个就是你主动调用了 [peripheral readRSSI];方法回调的RSSI,你可以根据这个RSSI估算一下距离什么的
NSLog(@"---peripheral Current RSSI:%@",RSSI);
}
这些是 iOS 连接外设的大体过程 , 在这里不忍吐槽一下,CoreBluetooth所有方法都是通过委托完成,代码冗余且顺序凌乱, 一整条链下来要近10几个委托方法,并且不断的在委托方法中调用方法再进入其他的委托,导致代码很零散。
最后, 写了一个 DEMO, 有兴趣的可以下载看看.😊
参考:
http://www.saitjr.com/ios/core-bluetooth-read-write-as-central-role.html