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];
}
}
PS: 这里需要判断连接哪个外设,一般是根据名字判断,我们的比较特殊,设备的名字都一样,只能根据MAC地址来判断。
MAC地址获取有两种方法:一是连接成功后通过外设的特征值的属性截取,网上有很多,不写了。
二是先获取mac地址再连接:根据上面代理方法中的 advertisementData
,它是一个广播包,里面会有一些设备的属性,但都被苹果给限制了,只有一个kCBAdvDataManufacturerData
幸免,它是可以修改的,所以要跟硬件工程师协商好,将mac地址写入这个key里面去。
这里可以通过蓝牙助手看一下,硬件工程师是否写入了。
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:
- 这里要跟硬件确认好,写入的特征是否有允许写入,允许用withResponse 不允许只能强行写入,用withoutResponse, 或者查看回调中error,返回 "Writing is not permitted." 表示不允许写入,使用withoutResponse。
- 如果是使用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. 手机不向硬件发送数据。。。
怎么都不行,以至于代码改了好多版,最后确定没有问题了,就是不行,后来发现用助手也不行,于是重启了手机。。。就好了。
所以如果确定代码没问题,可以重启下手机试试,真可能是手机的问题~~