iOS封装一个自己的蓝牙SDK

前言

这一年,公司产品主要产品是智能锁,兼带接入一些其余的智能家居产品,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

结语

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容