iOS封装一个自己的蓝牙SDK

作者: 跑神的心 | 来源:发表于2019-03-07 16:43 被阅读3330次

    前言

    在重庆这一年,公司产品主要产品是智能锁,兼带接入一些其余的智能家居产品,lifeSmart,物联等。几乎是跟蓝牙打了一年的交道,不写点什么实在说不过去,也必须写点什么证明曾经来过,走你。

    什么是库?

    库是共享程序代码的方式,一般分为静态库和动态库。程序运行分为三个步骤:编译,链接,执行。编译作用是将原代码(程序员手写代码)翻译成目标代码(机器的二进制代码),会生成目标文件(有多少个实现文件就会生成多少个目标文件)。链接就是将各个有关联的目标文件和库文件链接,是由机器完成的,在此过程中经常出现linker错误。
    1,静态库与动态库

    静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。
    动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。

    2,iOS静态库形式和动态库形式:

    静态库:.a和.framework
    动态库:.dylib和.framework

    3,ramework静态库和动态库的区分:

    系统的.framework是动态库,我们自己建立的.framework是静态库

    4,.a和.framwork的区别:

    .a是一个纯二进制文件,.framework中除了有二进制文件外还有资源文件。
    .a文件不能直接使用,至少要有.h文件配合,.framework文件可以直接使用。
    .a + .h + sourceFile = .framework

    蓝牙开发基础操作

    1,建立中心管理者
    //引用一下库的头文件 #import <CoreBluetooth/CoreBluetooth.h>  
    //遵循<CBCentralManagerDelegate,CBPeripheralDelegate>两个代理
    CBCentralManager *manager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
    
    2,扫描外设(discover)

    调用1方法后,系统会自动检测手机蓝牙状态,并进入此方法。当发现蓝牙状态是开启状态,你就可以进行下一步扫描外设操作了,如果为关闭状态,系统会自动弹出让用户去设置蓝牙,这个不需要我们开发者关心。

    - (void)centralManagerDidUpdateState:(CBCentralManager *)central{
        if (central.state==CBCentralManagerStatePoweredOn) {
            //NSLog(@"蓝牙已就绪,开始扫描外设");        
            //开始扫描
            [self.manager scanForPeripheralsWithServices:nil options:nil];
        }else{
            //NSLog(@"请检查蓝牙状态"); 
        }
    }
    
    3,扫描到外设,判断是否为目标设备,之后进行连接操作
    //扫描到设备会进入方法
    - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {
       
        if ([peripheral.name isEqualToString:@""]) {
    
            self.thePerpher = peripheral;
            
            //停止扫描
            [central stopScan];
            
            //连接设备
            [central connectPeripheral:peripheral options:nil];
        }
    }
    

    这里有一个值得注意的地方,安卓是可以直接扫到蓝牙设备的MAC地址然后进行连接的,但是自从iOS7以后就无法从API直接获取设备的MAC地址,所以绝大多数做法通常是判断设备名字peripheral.name,像我们公司的锁,蓝牙名称一般都是dyBKMWWtwR这种,所以做法是截取头两位,判断是否含有“dy”字段。

    4,连接设备成功,开始扫描服务
    //连接到外设成功
    - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
      
        //这里遵循的代理是CBPeripheralDelegate,后续的扫描服务回掉,扫描特征回掉,都是此代理里面的回掉方法 
        [peripheral setDelegate:self];
        
        //扫描服务
        [peripheral discoverServices:nil];
    }
    
    //连接外设失败
    -(void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
        NSLog(@"连接到外设 失败!%@ %@",[peripheral name],[error localizedDescription]);
    }
    
    4,扫描到服务(Services),开始扫描特征(Characteristics)
    -(void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{
        if (error) {
            NSLog(@"扫描外设服务出错:%@-> %@", peripheral.name, [error localizedDescription]);
            return;
        }
    
        //开始扫描特征
        for (CBService *service in peripheral.services) {
            [peripheral discoverCharacteristics:nil forService:service];
        }
    }
    
    5, 扫描到特征
    //扫描到特征
    - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{
        if (error) {
            NSLog(@"扫描特征失败!");
            return;
        }
    
        //获取Characteristic的值  
        for (CBCharacteristic *characteristic in service.characteristics){
        
            if ([characteristic.UUID.UUIDString isEqualToString:@"DE4001"]) {
    
                //外设订阅特征的通知,否则无法收到外设返回给手机的数据
                [self.thePerpher setNotifyValue:YES forCharacteristic:characteristic];
          
                }else if ([characteristic.UUID.UUIDString isEqualToString:@"DE4002"]) {
    
                    //CBCharacteristic 全局对象
                    self.theSakeCC = characteristic;
    
                }else if ([characteristic.UUID.UUIDStringisEqualToString:@"DE4003"]) {
    
                    //CBCharacteristic 全局对象
                    self.encryptSakeCC = characteristic;
                }
            }
    }
    

    这里是一个很重要的地方,特征值一般是硬件定义好的,比如说我这里,DE4001是回调数据read特征值(只能用来读数据)。DE4002是加密透传传输Write特征值,DE4003是不加密透传传输Write特征值,只能用来写数据。

    6,给硬件发送数据
    [self.thePerpher writeValue:sendData forCharacteristic:self.theSakeCC type:CBCharacteristicWriteWithoutResponse];
    

    这里要注意在实际开发过程中,根据公司的通信协议,选择5中保存的加密或者不加密特征值发送数据。

    7,扫描到具体的值(硬件给App回掉数据的地方)
    //扫描到具体的值
    - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error {
        if (error) {
            NSLog(@"扫描特征值失败:%@-> %@",peripheral.name, [error localizedDescription]);
            return;
        }
        //正常情况下characteristic.value需要进行解密操作操作之后才能拿来判断,实际开发过程中根据协议来处理
        if (characteristic.value) {
            NSString *withoutSpaceStr = [[self dataToHexStringWithData:characteristic.value] substringToIndex:4];
            if ([withoutSpaceStr isEqualToString:@"0100"] ) {
            //请输入管理员密码
            }
            if ([withoutSpaceStr isEqualToString:@"1101"] ) {
            //管理员密码错误
            }
            if ([withoutSpaceStr isEqualToString:@"0100"] ) {
            //开锁失败,时间误差不在允许的范围内
            }
        }
    }
    

    9,断开连接(disconnect)

    - (void)cancelPeripheralConnection:(CBPeripheral *)peripheral;
    

    断开与硬件的连接调用此方法,但是调用此方法后并不会立马断开,只是APP层面断开了连接,物理层并没有完全断开,大约5s后才能完全断开,所以如果业务需求上有需要立马断开的,建议除了调用此方法前,同时发送一个跟硬件约定的断开命令,如果是封装的蓝牙单例类,再释放此单例。

    蓝牙通讯常见加密方式

    蓝牙开发过程中遇到的问题汇总

    1,扫描设备时根据peripheral.name判断目标设备,同一设备不同状态下蓝牙名称跟更新不及时,会出现缓存问题

    比如我们的锁,普通状态是dyBKMWWtwR,处于等待添加配对状态时dyBKMWWtwA,解决办法取广播中的蓝牙名称,实时更新不会存在缓存问题
    [advertisementData objectForKey:CBAdvertisementDataLocalNameKey];

    2,无论怎么调试,确认发送数据加密,格式等都没问题,但writeValue之后都收不到消息

    我遇到这个问题的原因是跟writeValue的type有关,type有两个值,CBCharacteristicWriteWithoutResponse和CBCharacteristicWriteWithResponse,这两个值,都可以收到回调消息,之前遇到一个坑,用的是WithResponse这个type,不管怎么调试,都收不到硬件返回的消息,安卓却可以,一度开始怀疑人生,百思不得其解之后,尝试着换成了WithoutResponse,秒收!在此大胆的猜想应该是硬件那边的某些设置问题。

    3,拼装的NSData数据MD5之后,经常出现同一数据结果不一样

    常用的md5算法如下所示,但是当我加密一长串字节数组,比如<0213 2123 2123 1020 2123 0002>时,结果会发生变化,根本原因是[data bytes]返回的结果不一样。 解决办法,CC_MD5方法第二个参数不用original_str的结果,直接用传入的字节数组data长度。将CC_MD5方法替换成这个CC_MD5(input, (uint)data.length, result)即可,至于为什么[data bytes]返回的结果会不一样,一阵google之后尚未找到合理的解释,有知道的同学可以科普一下。

    + (NSString*)getMD5WithData:(NSData *)data {
        const char* original_str = (const char *)[data bytes];
        unsigned char digist[CC_MD5_DIGEST_LENGTH];
        CC_MD5(original_str, (uint)strlen(original_str), digist);
        NSMutableString* outPutStr = [NSMutableString stringWithCapacity:0];
        for(int  i =0; i<CC_MD5_DIGEST_LENGTH;i++){
            [outPutStr appendFormat:@"%02x",digist[i]];
        }
        return [outPutStr lowercaseString];
    }
    

    封装进SDK

    结语

    相关文章

      网友评论

        本文标题:iOS封装一个自己的蓝牙SDK

        本文链接:https://www.haomeiwen.com/subject/jiqhpqtx.html