刚开发完一款APP,其中涉及到应用和硬件进行蓝牙通讯需求,记录分享总结.
场景:APP扫描制定的蓝牙设备,连接设备,交互数据
准备工作:Xcode7.3,Objective-C,BLE设备
1.参考资料
在开发中,主要参考了这位大神的博客:
http://liuyanwei.jumppo.com/2015/07/17/ios-BLE-1.html
开源的框架BabyBluetooth:
https://github.com/coolnameismy/BabyBluetooth
是用链式语法(类似Masonry)封装了CoreBluetooth框架,链式方法没有提示,不习惯的话容易出处,所以我还是用的CoreBluetooth的API.
一个中心设备可以连接多个外设,外设同时只能被一个中心设备连接.
基本知识:
一个外设(peripheral)可以提供一种或多种服务(service),一种服务有一个或多个特征(characteristic).
基本工作原理:1.扫描外设;2.连接设备;3.扫描设备的服务;4.扫描服务的特征;5.监听特征,获取蓝牙传回的数据;6.向蓝牙发送指令,接收数据,解析
2.iPhone作为中心设备(CBCentralManager)连接外设(CBPeripheral)
CBCentralManager:中心设备,可以连接外设(CBPeripheral),管理设备,发送指令,接收数据等.如我要做的APP作为中心设备来管理外设.
CBPeripheral:如我的项目中是一款治疗仪,app可以发送指令给治疗仪,请求/同步数据等功能.
APP可以作为中心设备,也可以作为外接设备,但不能同时既是中心设备,又是外设,这里只考虑APP作为中心设备连接管理外设.
3.实现步骤
3.1 导入框架,新建CBCentralManager,设置代理,实现代理方法
//遵循代理
<CBCentralManagerDelegate, CBPeripheralDelegate>
//蓝牙管理对象
@property (strong, nonatomic) CBCentralManager *cbManager;
//扫描到的设备后需要添加到数组中持有他.
@property (copy, nonatomic) NSMutableArray<CBPeripheral *> *peripheralArray;
//连接到的设备
@property (copy, nonatomic) NSMutableArray<CBPeripheral *> *connectedPeripheralArray;
//tableView展示外设,可以展示扫描到的设备,手动连接,也可以自动连接设备,展示已连接设备
@property (strong, nonatomic) UITableView* tableView;
//初始化
_cbManager = [[CBCentralManager alloc] initWithDelegate:self queue:dispatch_get_main_queue()];
//扫描外设,先不扫描,等到代理方法中检测到蓝牙可用时调用
//第一个参数为nil时,扫描所有所有设备
//第一个参数有值时,扫描指定的一类设备
//[_cbManager scanForPeripheralsWithServices:nil options:nil];
3.2 CoreBlueTooth框架都以代理方式实现
3.2.1 CBCentralManagerDelegate
#pragma mark - CBCentralManagerDelegate
//监听蓝牙状态,蓝牙状态改变时调用
//不同状态下可以弹出提示框交互
//如果单独封装了这个类,可以设置代理或block或通知向控制器传值
-(void)centralManagerDidUpdateState:(CBCentralManager *)central{
switch (central.state) {
case CBCentralManagerStateUnknown:
NSLog(@">>>蓝牙未知状态");
break;
case CBCentralManagerStateResetting:
NSLog(@">>>蓝牙重启");
break;
case CBCentralManagerStateUnsupported:
NSLog(@">>>不支持蓝牙");
break;
case CBCentralManagerStateUnauthorized:
NSLog(@">>>未授权");
break;
case CBCentralManagerStatePoweredOff:
NSLog(@">>>蓝牙关闭");
break;
case CBCentralManagerStatePoweredOn:
NSLog(@">>>蓝牙打开");
//蓝牙打开时,再去扫描设备
[_cbManager scanForPeripheralsWithServices:nil options:nil];
break;
default:
break;
}
}
//发现外设时调用
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI {
if (![_peripheralArray containsObject:peripheral]) {
NSLog(@"发现外设:%@", peripheral);
//打印结果:
//发现外设:<CBPeripheral: 0x15f5bac80, identifier = 955A5FFD-790E-BA3C-2A94-29FEA8A14A58, name = TF1603(BLE), state = disconnected>
//发现设备后,需要持有他
[_peripheralArray addObject:peripheral];
// NSLog(@"信号强度:%@", RSSI);
//如果之前调用扫描外设的方法时,设置了相关参数,只会扫描到指定设备,可以考虑自动连接
//[_cbManager connectPeripheral:peripheral options:nil];
//刷新表格显示扫描到的设备
[tableView reloadData];
//如果这是单独封装的类,这里需要用代理或block或通知传值给控制器来刷新视图
}
}
//外设连接成功时调用
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
NSLog(@"连接成功");
//将连接的设备添加到_connectedPeripheralArray
[self.connectedPeripheralArray addObject: peripheral];
//如果tableView展示的是已经连接的设备
//[tableView reloadData];
//如果这是单独封装的类,这里需要用代理或block或通知传值给控制器来刷新视图
//设置外设代理
peripheral.delegate = self;
//搜索服务
[peripheral discoverServices:nil];
}
//外设连接失败时调用
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
NSLog(@"连接失败,%@", [error localizedDescription]);
}
//断开连接时调用
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
NSLog(@"断开连接");
//移除断开的设备
[_connectedPeripheralArray removeObject:peripheral];
//这里可以进行一些操作,如之前连接时,监听了某个特征的通知,这里可以取消监听
}
3.2.2 CBPeripheralDelegate
//发现服务时调用
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
if (error) {
NSLog(@"%@发现服务时出错: %@", peripheral.name, [error localizedDescription]);
return;
}
//遍历外设的所有服务
for (CBService *service in peripheral.services) {
NSLog(@"外设服务: %@", service);
//打印结果
//外设服务: <CBService: 0x15f5d0be0, isPrimary = YES, UUID = 6666>
//外设服务: <CBService: 0x15f5d1f60, isPrimary = YES, UUID = 7777>
//我定义了两个宏
//#define SeriveID6666 @"6666"
//#define SeriveID7777 @"7777"
//每个服务又包含一个或多个特征,搜索服务的特征
[peripheral discoverCharacteristics:nil forService:service];
}
}
//发现特征时调用,由几个服务,这个方法就会调用几次
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
if (error) {
NSLog(@"扫描特征出错:%@", [error localizedDescription]);
return;
}
//获取Characteristic的值
for (CBCharacteristic *characteristic in service.characteristics) {
NSLog(@"服务server:%@ 的特征:%@, 读写属性:%ld", service.UUID.UUIDString, c, c.properties);
//第一次调用,打印结果:
//服务server:6666 的特征:<CBCharacteristic: 0x135e37410, UUID = 8888, properties = 0xDE, value = (null), notifying = NO>, 读写属性:->222
//如果一个服务包含多种特征,会循环打出其他特征,我的设备正好一个服务只包含一个特征,处理起来方便了许多.
//第二次调用,打印结果:
//服务server:7777 的特征:<CBCharacteristic: 0x135e3d260, UUID = 8877, properties = 0x4E, value = (null), notifying = NO>, 读写属性:->78
//特征也用UUID来区分,注意和service的UUID区别开来
//和厂家确认,server(uuid=6666)的特征characteristic(uuid=8888)是监控蓝牙设备往APP发数据的,
//server(uuid=7777)的特征characteristic(uuid=8877)向蓝牙发送指令的.
//对特征定义宏
//#define CID8888 @"8888" //读指令(监控蓝牙设备往APP发数据),6666提供
//#define CID8877 @"8877" //APP向蓝牙发指令,7777提供
//UUID=8888的特征有通知权限,在我的项目中是实时接收蓝牙状态发送过来的数据
if ([service.UUID.UUIDString isEqualToString:SeriveID6666]) {
for (CBCharacteristic *c in service.characteristics) {
if ([c.UUID.UUIDString isEqualToString:CID8888]) {
//设置通知,接收蓝牙实时数据
[self notifyCharacteristic:peripheral characteristic:c];
}
}
}
if ([service.UUID.UUIDString isEqualToString:SeriveID7777]) {
[peripheral readValueForCharacteristic:characteristic];
//获取数据后,进入代理方法:
//- peripheral: didUpdateValueForCharacteristic: error:
//根据蓝牙协议发送指令,写在这里是自动发送,也可以写按钮方法,手动操作
//我将指令封装了一个类,以下三个方法是其中的三个操作,具体是啥不用管,就知道是三个基本操作,返回数据后,会进入代理方法
//校准时间
[CBCommunication cbCorrectTime:peripheral characteristic:characteristic];
//获取mac地址
[CBCommunication cbGetMacID:peripheral characteristic:characteristic];
//获取脱机数据
[CBCommunication cbReadOfflineData:peripheral characteristic:characteristic];
}
}
//描述相关的方法,代理实际项目中没有涉及到,只做了解
//搜索Characteristic的Descriptors
for (CBCharacteristic *characteristic in service.characteristics){
[peripheral discoverDescriptorsForCharacteristic:characteristic];
//回调方法:
// - peripheral: didDiscoverDescriptorsForCharacteristic: error:;
//还有写入读取描述值的方法和代理函数
}
}
#pragma mark - 设置通知
//设置通知
-(void)notifyCharacteristic:(CBPeripheral *)peripheral
characteristic:(CBCharacteristic *)characteristic{
if (characteristic.properties & CBCharacteristicPropertyNotify) {
[peripheral setNotifyValue:YES forCharacteristic:characteristic];
//设置通知后,进入代理方法:
//- peripheral: didUpdateNotificationStateForCharacteristic: characteristic error:
}
}
//取消通知
-(void)cancelNotifyCharacteristic:(CBPeripheral *)peripheral
characteristic:(CBCharacteristic *)characteristic{
[peripheral setNotifyValue:NO forCharacteristic:characteristic];
}
//设置通知后调用,监控蓝牙传回的实时数据
- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
if (error) {
NSLog(@"错误: %@", error.localizedDescription);
}
if (characteristic.isNotifying) {
[peripheral readValueForCharacteristic:characteristic];
//获取数据后,进入代理方法:
//- peripheral: didUpdateValueForCharacteristic: error:
} else {
NSLog(@"%@停止通知", characteristic);
}
}
3.2.3 向蓝牙发送指令,接收数据,解析
app向蓝牙发送指令(这是我们设备的一个指令,由于现在iOS不能直接获取蓝牙mac地址了,我们设备的厂家就写了一个指令来获取,这个指令是自定义的,不适用于其他设备,方法通用)
bluetooth03.png
//CBCommunication.m(封装协议指令,类方法)
//获取mac地址
+ (void)cbGetMacID:(CBPeripheral *)peripheral characteristic:(CBCharacteristic *)characteristic {
NSLog(@"MAC地址");
Byte b[] = {0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA0};
NSData *data = [NSData dataWithBytes:&b length:8];
[CBCommunication writePeripheral:peripheral characteristic:characteristic value:data];
}
//通用发送指令方法
+ (void)writePeripheral:(CBPeripheral *)p
characteristic:(CBCharacteristic *)c
value:(NSData *)value {
//判断属性是否可写
if (c.properties & CBCharacteristicPropertyWrite) {
[p writeValue:value forCharacteristic:c type:CBCharacteristicWriteWithResponse];
} else {
NSLog(@"该属性不可写");
}
}
特征CBCharacteristic
中有一个属性是properties
,是CBCharacteristicProperties
类型的,我这里叫他权限,有read,write,notify,indicate等,这是一个可位移操作的枚举,如第一次打印出来的properties = 0xDE
,转为二进制1101 1110
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
};
对比发现,这个属性包含的权限可读写
CBCharacteristicPropertyRead
CBCharacteristicPropertyWriteWithoutResponse
CBCharacteristicPropertyWrite
CBCharacteristicPropertyNotify
CBCharacteristicPropertyAuthenticatedSignedWrites
CBCharacteristicPropertyExtendedProperties
有write时才可写,即向蓝牙发送指令CBCharacteristicPropertyWriteWithoutResponse
写入有反馈,CBCharacteristicPropertyWrite
写入后无反馈.反馈即提示是否写入成功,代理方法实现
//用于检测中心向外设写数据是否成功
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
if (error) {
NSLog(@"APP发送数据失败:%@",error.localizedDescription);
} else {
NSLog(@"APP向设备发送数据成功");
}
}
//蓝牙接收到外设发来的数据时调用,数据主要在这里解析
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
//characteristic.value是NSData类型的,具体开发时,会根据开发协议去解析数据
NSData *originData = characteristic.value;
NSLog(@"获取外设发来的数据:%@",originData);
//根据协议解析数据
//因为数据是异步返回的,我并不知道现在返回的数据是是哪种数据,返回的数据中应该会有标志位来识别是哪种数据;
//如下图,我的设备发来的是8byte数据,收到蓝牙的数据后,打印characteristic.value:
//获取外设发来的数据:<0af37ab219b0>
//解析数据,判断首尾数据为a0何b0,即为mac地址,不同设备协议不同
}
bluetooth03.png
基本的蓝牙功能就可以实现啦!
4.改进
4.1 优化搜索方案
_cbManager = [[CBCentralManager alloc] initWithDelegate:self queue:dispatch_get_main_queue() options:@{CBCentralManagerScanOptionAllowDuplicatesKey:@(NO)];
扫描设备时,不扫描到相同设备,这样可以节约电量,提高app性能.如果需求是需要实时获取设备最新信息的,那就需要设置为YES.
4.2扫描和连接指定设备
//如果只想扫描到特定设备,可以用以下方法,前提是你知道服务的uuid.能够提供uuid为SeriveID6666(定义的宏)和SeriveID7777服务的设备.uuid可以由厂家提供,也可以先扫描所有设备,来获取服务.
//包含一个符合服务的设备即可被搜索到
CBUUID *uuid01 = [CBUUID UUIDWithString:SeriveID6666];
CBUUID *uuid02 = [CBUUID UUIDWithString:SeriveID7777];
NSArray *serives = [NSArray arrayWithObjects:uuid01, uuid02, nil];
[_cbManager scanForPeripheralsWithServices:serives options:nil];
4.3性能优化
当扫描到或连接到指定设备后,取消扫描
网友评论