欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

利用iOS原生框架实现蓝牙4.0通讯功能

程序员文章站 2022-06-04 15:01:31
...

利用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是写入类型:CBCharacteristicWriteWithResponseCBCharacteristicWriteWithoutResponse,可以根据实际情况选择
- (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混编调试)

框架的整体设计如下:
利用iOS原生框架实现蓝牙4.0通讯功能
接下来对各个类的方法进行一个简单介绍:

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:蓝牙其实明白了原理…也没那么难的。