利用iOS原生框架实现蓝牙4.0通讯功能
蓝牙4.0是蓝牙3.0+HS规范的补充,专门面向对成本和功耗都有较高要求的无线方案,可广泛用于卫生保健、体育健身、家庭娱乐、安全保障等诸多领域。
它支持两种部署方式:双模式和单模式。双模式中,低功耗蓝牙功能集成在现有的经典蓝牙控制器中,或再在现有经典蓝牙技术(2.1+EDR/3.0+HS)芯片上增加低功耗堆栈,整体架构基本不变,因此成本增加有限。百度百科
蓝牙的开发过程本质上是非常简单的,困难的地方在于对蓝牙传递过来的数据解析。为此,楼主将会从以下几个方面对实现iOS原生框架实现蓝牙4.0通讯功能:
- iOS BLE4.0原生框架
- 建立蓝牙通讯所必要的条件
- 开发过程中需要注意的点
- 自定义BLE链接框架和代码简介
- 测试demo
通过上面几个步骤,相信会对有需要的开发人员提供一定的思路和帮助。
一、iOS BLE4.0原生框架CoreBluetooth
CoreBluetooth框架的介绍很多,因此为了节省篇幅,我会对用到的方法做一个快速的解释。
需要注意的是,本文的蓝牙连接方式为:中心者模式
简单的说中心者模式就是手机设备连接其他外设(大多的蓝牙开发都是这种情况)
CoreBluetooth连接一个外设一般分为两个大的阶段
- 查找并连接指定设备阶段
- 连接设备指定服务传输数据阶段
这两个阶段只有在进行到服务阶段后才可以正常连接数据。接下来对此进行简单的介绍
1、查找连接设备阶段CentralManager Delegate (设备级别)
该阶段执行的顺序如下:
- 创建蓝牙中心者模式对象
//该方法用于创建一个蓝牙中心者模式对象,需要传递一个代理对象和一个分线程(数据一般都是在分线程采集的)
- (instancetype)initWithDelegate:(nullableid<CBCentralManagerDelegate>)delegate queue:(nullable dispatch_queue_t)queue;
- 启动蓝牙中心者模式
//传入一个蓝牙中心者对象,启动蓝牙功能
- (void)centralManagerDidUpdateState:(CBCentralManager *)central;
在这个代理方法中,会实现检查设备蓝牙状态:
- (void)centralManagerDidUpdateState:(nonnull CBCentralManager *)central {
switch (central.state) {
case CBCentralManagerStatePoweredOn:{
//开启可用状态
//在开启状态下启动蓝牙扫描周边设备功能
}
break;
default: {
//异常状态
}
break;
}
}
楼主在做的时候一般手动调用这个方法触发后续的代码,如果有误还望指出
- 扫描周边外设
//调用此方法可以搜索周边的蓝牙设备,如果传入制定的UUID可以制定搜索,如果传入nil则会搜索全部
- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs options:(nullable NSDictionary<NSString *, id> *)options;
- 连接外设
//此方法需要传入一个要连接的CBPeripheral对象,传入后会制定连接这个对象
- (void)connectPeripheral:(CBPeripheral *)peripheral options:(nullable NSDictionary<NSString *, id> *)options;
- 连接状态变化的回调
//连接外设成功后,会自动调用此方法
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral;
//连接外设失败后,会调用此方法
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;
//已连接的外设断开后,会调用此方法
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;
这三个函数都是针对连接状态发生改变时会调用的方法,尤其是第三个方法是经常用到的。
到此为止,蓝牙连接外设的代理方法就全部介绍完毕了。
2、连接设备指定服务传输数据阶段
该阶段的执行顺序如下:
- 获取设备服务回调
//当连接设备后将会进入下面的方法,在这个方法中我们可获取设备所有的服务,并对我们需要的服务进行过滤,并连接指定的服务
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error;
- 获取服务特征值回调
//当链接到指定服务并获取到特征值列表后,就会回调下面的函数。
//在这个方法里,我们可以对设备服务提供的特征值进行过滤,并需要保存对应的特征值和服务,便于后期区分和读写。
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error;
确定唯一的服务于特征值后,需要调用下面的三个方法,之后与制定的服务和特征值简历稳定的连接(很重要)。
[peripheral readValueForCharacteristic:characteristic];
[peripheral discoverDescriptorsForCharacteristic:characteristic];
[peripheral setNotifyValue:YES forCharacteristic:characteristic];
- 读取数据&写入数据
//当建立连接的服务于特征值有数据变化时,将会回调该函数,所有的数据信息都会汇总到这里
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
所有获得的数据可以从characteristic.value获取
//该方法由CBPeripheral对象调用,可以向CBPeripheral的某一个CBCharacteristic中写入制定的内容,类型为NSData。
//CBCharacteristicWriteType是写入类型:CBCharacteristicWriteWithResponse和CBCharacteristicWriteWithoutResponse,可以根据实际情况选择
- (void)writeValue:(NSData *)data forCharacteristic:(CBCharacteristic *)characteristic type:(CBCharacteristicWriteType)type;
到此为止,CoreBluetooth的框架解释已经完成,如果需要了解更相信的内容可以自行搜索。
二、建立蓝牙通讯所必要的条件
该部分提出了建立BLE4.0通讯的必要条件,可以分为以下几个部分:
1、一个支持BLE4.0的手机和外接设备(废话)
2、明确BLE通讯模式(本文只适合中心者模式)
3、明确外设所开放的服务(CBService.UUID)和特征值(CBCharacteristic.UUID)
4、 明确与外设约定的通讯协议内容和格式
5、 外设连接的唯一标识符
两个UUID是必须获取的,需以此作为连接的根据
外设连接的唯一标识符也是必须确定的,以此作为区分外接设备。
三、开发过程中需要注意的点
1、建议将功能集合起来作为一个单例类使用
2、建立蓝牙连接必须存储对应的CBPeripheral和CBCharacteristic,否则将无法获取对应的数据
3、发送和接受的数据一般是NSData类型,在Demo中提供了一些实用的解析方法
4、确定好唯一的特征之后一定要添加三个启动方法,否则不会启动数据传输
四、自定义BLE链接框架和代码简介
在工作过程中,由于需要实用蓝牙进行数据传输,因此编写了一个比较实用的BLE4.0蓝牙传输框架。
框架利用block方式理论上可以应用在各种混编代码中(已经通过C++和OC混编调试)
框架的整体设计如下:
接下来对各个类的方法进行一个简单介绍:
1、BLELinkManager
BLELinkManager负责与外设的链接,提供了以下几个接口:
- 创建BLE蓝牙linke控制者
/**
创建BLELinkManager的单例对象
@return 返回BLELinkManager对象
*/
+ (BLELinkManager *)sharedManager;
- 开始连接二维码设备
/**
开始连接设备
*/
- (void)startLinkDevice;
- 向蓝牙模块写入数据
/**
向蓝牙模块写入数据
@param type 命令字符
@param number 通道号
@param speedValue 上传速率,在BLE情况下上限为1000Byte/s
*/
- (void)writeCommandToBLEDeviceWithType:(BLEObjectCommandState)type
withChannelNumber:(NSInteger)number
withSpeedValue:(int)speedValue;
这里提供了必要的三个接口,具体的内容可以根据实际情况修改。创建、启动、写入、返回这是必须要考虑的。
返回的接口采用Block的方式回调,定义和使用如下
- 回调Block
/**
命令信息返回block
@param channelNumber 通道号
@param backString 返回的字符串信息
*/
typedef void(^CommandInfoBlock)(NSInteger channelNumber, NSString * backString);
/**
linke状态信息返回block
@param linkState 状态码
@param channelNumber 通道号,当linkState为BLE_DEVICE_OPEN无意义
@param flag 标签
*/
typedef void(^LinkInfoBlock)(BLEStateEnum linkState,NSInteger channelNumber, bool flag);
//回调Block属性声明//
//必须实现的Block
@property (nonatomic,copy) CommandInfoBlock infoBlock;
//必须实现的Block
@property (nonatomic,copy) LinkInfoBlock linkInfoBlock;
使用时,在需要的位置初始化,直接调用对应的属性即可。
- (void)viewDidLoad {
[super viewDidLoad];
linkManager_ = [BLELinkManager sharedManager];
linkManager_.linkInfoBlock = ^(BLEStateEnum linkState, NSInteger channelNumber, bool flag) {
//do something
};
linkManager_.infoBlock = ^(NSInteger channelNumber, NSString *backString) {
// do something
};
}
这里仅仅是介绍了使用方法,具体的代码可以参考提供的Demo。
需要注意的是,调用方法startLinkDevice()需要保证连接设备前已经获取到要连接设备的列表或者地址。
获取的方法多种多样,Demo采用的是二维码扫设备Mac地址的方式,仅供参考。
2、BLEChannelManager
BLEChannelManager控制通道数目和通道信息,表示现在链接了几个对象。
设计中考虑到断开连接后再连接的情况,对链接的设备次序进行了记录和保存。
Demo中上限为4台设备,存储的顺序为0,1,2,3。当出现空闲通道的时候会优先存储空闲通道。
例如:当0,1,2,3都连接成功,先把1,3断开,再接入设备时会按照1,3的顺序存储。
- 创建BLEChannelManager单例对象(不多过多解释)
- 添加连接设备的方法
/**
添加蓝牙通道设备
@param deviceQrCoder 设备唯一标志,此处考虑为二维码
@param discoveredPeripheral 设备信息
@return 是否添加成功
*/
- (BOOL)addBleChannelObjectByDeviceQrCoder:(NSString *)deviceQrCoder
withPeripheral:(CBPeripheral *)discoveredPeripheral;
/**
根据BLE通道对象添加设备
@param objects 蓝牙通道对象
@return 是否添加成功
*/
- (BOOL)addBleChannelObjectToArray:(BLEChannelObject *)objects;
这里的添加的设备为设备级别的,还没有进入到服务级别。
- 设置服务状态
/**
对某个设备添加可用的服务状态
@param discoveredPeripheral 设备状态
@param characteristic 特征值
*/
- (void)setCharacteristicByPeripheral:(CBPeripheral *)discoveredPeripheral
withCharacteristic:(CBCharacteristic *)characteristic;
- 清除设备的方法
/**
根据通道值删除重设某个通道信息
@param indexes 通道值
*/
- (void)removeBleChannelObjectByNumber:(NSInteger)indexes;
/**
清除全通道
*/
- (void)cleanChannelObject;
- 获取和查询等方法
/**
根据通道值获取到蓝牙通道对象
@param indexes 通道值
@return 蓝牙通道对象
*/
- (BLEChannelObject *)getBleChannelObjectByNumber:(NSInteger)indexes;
/**
根据CBPeripheral获取蓝牙通道值
@param peripheral CBPeripheral对象
@return 蓝牙通导值
*/
- (NSInteger)getBleChannelIndexByPeripheral:(CBPeripheral *)peripheral;
/**
获取所有的蓝牙通道信息
@return 蓝牙通道对象数组
*/
- (NSMutableArray *)getBleChannelObjectArray;
/**
是否可以添加新的设备
@return 是否可以
*/
- (BOOL)canAddNewBLEDevic;
上面提供的方法基本足够正常的使用,也可以根据自己的情况添加
3、BLEMessageManager
BLEMessageManager的功能非常简单,但是此处需要与硬件进行沟通设定,本处作为一个简单的例子
typedef NS_ENUM(NSInteger, BLEObjectCommandState){
//ABA命令,主要用于......
BLEOBJECTCOMMAND_ABA = 0, //0
//BAB命令,主要用于......
BLEOBJECTCOMMAND_BAB = 1, //1
//CDC,主要用于......
BLEOBJECTCOMMAND_CDC = 2, //2
//DCD,主要用于......
BLEOBJECTCOMMAND_DCD = 3, //3
};
提供的接口内容也非常简单,详情参考Demo不做过多解释,不过在.m文件中,提供了2种将需要的命令转成可写入的命令的方法:
- 字符串转成对应的数据流
/字符串转byteData
-(NSMutableData *)dataFromString:(NSString *)string
{
NSMutableData *data_ = [NSMutableData new];
unsigned char whole_byte_;
char byte_chars[3] = {'\0','\0','\0'};
int i_ = 0;
int length_ = (int)string.length;
while (i_ < length_-1) {
char c_ = [string characterAtIndex:i_++];
if (c_ < '0' || (c_ > '9' && c_ < 'a') || c_ > 'f')
continue;
byte_chars[0] = c_;
byte_chars[1] = [string characterAtIndex:i_++];
whole_byte_ = strtol(byte_chars, NULL, 16);
NSLog(@"---whole_byte_--->%c",whole_byte_);
[data_ appendBytes:&whole_byte_ length:1];
}
return data_;
}
这个方法可以将字符串转成外设可用的NSData数据流,有一定的通用性
NSMutableData *data_ = [NSMutableData new];
dig_4=0; dig_3=2; dig_2=0; dig_1=0;
unsigned int whole_byte_[10];
whole_byte_[0]=0xAA;
whole_byte_[1]=0x03;
whole_byte_[2]=0x12;
whole_byte_[3]=0x72;
whole_byte_[4]=(dig_4*10+dig_3);//千、百位
whole_byte_[5]=(dig_2*10+dig_1);//十、个位
whole_byte_[6]=0x01;
whole_byte_[7]=0x72;
whole_byte_[8]=0x30;
whole_byte_[9]=0x55;
int i = 0;
while (i<10) {
[data_ appendBytes:&whole_byte_[i] length:1];
i++;
}
这种方式其实与第一种没什么太多区别,仅仅是直接将需要的数据转成
一个定长的unsigned int的C数组,建议用第一种方式。
五、演示Demo
具体的代码信息太多,已经将Demo上传到Coding,如果有需要的可以戳下方链接下载查看,有问题也欢迎提出来一起修改进步~
下载地址:
https://git.coding.net/SupDongLei/BLELinkManager.git
插嘴一句:BLELinkManager使用Block作为属性,然后调用的方法,可以应对绝大多数的混编情况,在于C++混编情况下表现良好,不失为一种处理混编的方式。
采用这种方式已经实现了包括:混编读写文件,URL Scheme,蓝牙链接,原生扫码等功能(但是需要注意Block截取对象和运行顺序)
ps:蓝牙其实明白了原理…也没那么难的。