前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >iOS蓝牙开发如何更好地收发数据

iOS蓝牙开发如何更好地收发数据

作者头像
iOS Development
发布2019-02-14 17:45:36
2.7K0
发布2019-02-14 17:45:36
举报

3月中旬跳槽了,一直在新公司「填坑」,看着「先人」写的代码,觉得是有改善空间的,所以这次想聊下这部分内容——iOS蓝牙开发中如何更好地更好地收发数据。

适读对象:

  • 想初步了解iOS蓝牙开发的朋友(最好连计算机基础都没有,就像我这种没有计算机科班基础的伪程序猿(真文科汪));
  • 做过蓝牙开发,但是没有很「优雅」地收发数据的朋友(直接用C语言char数组装回来,用下标索引去取用)。

注意:

  • 本文所说的蓝牙,指BLE(Bluetooth Low Energy/低功耗蓝牙)。一般应用苹果的官方框架CoreBluetooth开发。当然,会有不同的第三方框架,最近我做的项目用的就是第三方框架BabyBluetooth
  • 本文部分代码,有两种版本,应用苹果框架CoreBluetooth时,用的是Swift。用BabyBluetooth时,用的是Objective-C。

我们会从哪里拿到数据?

我们先简单回顾一下整个蓝牙数据接收的一般流程:

  • 1、蓝牙在不断地在广播信号;
  • 2、APP扫描;
  • 3、发现设备(根据名称或「服务」的UUID来辨别是不是我们要连接的设备);
  • 4、连接(成功);
  • 5、调用方法发现「服务」;
  • 6、调用方法发现服务」里的「特征」;
  • 7、发现硬件用于数据输人的「特征」,保存(APP发送数据给硬件时要用到这个「特征」);
  • 8、发现硬件用于数据输出的「特征」,进行「监听」(硬件就是从这个「特征」中发送数据给手机端);
  • 9、利用数据输入「特征」发送数据,或者等待数据输出「特征」发出来的数据。

其中第7~8步的代码(Swift版)如下:

代码语言:javascript
复制
    // 第7、8步:
    // 发现特征的回调(委托)方法(假设在这之前已经「成功连接」、「发现服务」)
    func peripheral(peripheral: CBPeripheral, didDiscoverCharacteristicsForService service: CBService, error: NSError?) {
        print("发现设备有\(service.characteristics?.count)个特征, 是:\(service.characteristics)")
        
        // 用for循环,找到自己要的特征(以UUID为辨别依据)
        for characteristic in service.characteristics! {
            switch characteristic.UUID {
                
                // 7、发现数据写入的特征(我们的硬件是:FF01)
            case kCharacteristicDataInUUID:
                print("这是用于数据写入的特征,它的UUID是:\(characteristic.UUID)")
                
                // 8、发现硬件输出数据(APP读取硬件数据)的特征(我们的硬件是:FF02)
            case kCharacteristicDataOutUUID:
                // 监听DataOut特征
                print("这是用于读取数据的特征,它的UUID是:\(characteristic.UUID)")
               //  8、进行监听
                peripheral.setNotifyValue(true, forCharacteristic: characteristic)
                
            default:
                print("default")
            }
        }
    }
    
    
    // 第9步:
    // 最终,蓝牙发过来的数据,我们会在这个回调方法中拿到
    func peripheral(peripheral: CBPeripheral, didUpdateNotificationStateForCharacteristic characteristic: CBCharacteristic, error: NSError?) {
        print("收到从蓝牙「FFF2特征」发出的数据:\(characteristic.value)")
        
        // value是一个「NSData?」类型的对象
    }

所以,我们最终会在peripheral(_:didUpdateNotificationStateForCharacteristic:error:)方法中拿到数据。Objective-C对应的方法是peripheral:didUpdateNotificationStateForCharacteristic: error:

注意,要先用setNotifyValue(_:forCharacteristic characteristic:)监听对应的特征,才能在上述方法拿到数据。

如果在Objective-C中,会长这样子(不是官方的框架,用的是BabyBluetooth框架):

代码语言:javascript
复制
    // BabyBluetooth这个框架框架将监听和回调写在一起(用Block实现),能让代码不至于那么分散:
    // 也就是上面的第8、9两步合在一个方法中了
    [_baby notify:peripheral characteristic:_dataOutCharacteristic block:^(CBPeripheral *peripheral, CBCharacteristic *characteristics, NSError *error) {
        NSLog(@"收到从蓝牙「FFF2特征」发出的数据: %@", characteristics.value);
    }

我们会拿到什么样的数据?

好了,经过上面的一系列稍显繁琐的步骤,我们从蓝牙那边拿到了「NSData?」类型(Objective-C对应的是「NSData」类型)的数据。

我们打印一个「NSData?」对象看看:

print("收到从蓝牙「FFF2特征」发出的数据:\(characteristic.value)")

在控制台,会这样输出类似这样的东西:

收到蓝牙发出来的数据: <da13ffff ff640099>

这些是什么鬼?

这要从NSData说起,NSData是怎么样的数据呢?要经过怎么的处理,才能变成我们自己需要的数据呢?

苹果的官方文档《Binary Data Programming Guide》中的章节:Accessing and Comparing BytesAccessing and Comparing Bytes说得比较详细,英文好的朋友可以看看。

我们暂且这样理解:NSData(NSMutableData)是二进制数据对象——苹果将二进制数据封装成对象,让我们可以用面向对象的思维去操作这些数据。

我们可以通过原始的二进制数据(Raw Bytes)去生成NSData对象,也可以通过NSData存取/访问(Accessing)这些二进制数据。

你在逗我么?说好的二进制数据呢?不应该全部是0、1么?为什么会有d啊、a啊、f啊,罩杯么?

莫生气,<da13ffff ff640099>只是用十六进制呈现给我们而已,也就是0xda0x130xff0xff0xff0x640x000x99,蓝牙传了这8个十六进制的数(8个byte)给我们。

为什么不直接用二进制?好,我知道你不死心的,二进制是这样的:<11011010 00010011 11111111 11111111 11111111 01100100 00000000 10011001>晕没有?你要继续坚持用二进制吗?「阿尔法狗」倒应该是很乐意的。

正因为二进制与十六进制之间的转换比较简单,所以在计算机领域,16进制比较通用。这就解释了为什么我们打印出来的NSData对象最终以十六进制方式呈现(上面才仅仅是8个byte的0和1。1KB=1024Bytes,给你0.5KB的0和1,十副老花镜都看不过来)。

这些数据有什么意义(表示什么)?

这个问题问得好,这个问题就好比如:「鸡」为什么叫「鸡」,「鸭」为什么叫「鸭」?(好不搭边的比喻~)

其实是这样的,很久很久以前,第一个发现「鸡」这个物种的中国人,他脑洞不知道为什么就浮现了「鸡」这个字,于是很随机地用「鸡」这个「符号」把它「定义」为「鸡」。如果你能穿越回去,完全可以让他用「鸭」这个「符号」的,如果真是那样,现在的「鸡」就不是「鸡」,「鸭」就不是「鸭」了,而应该是「鸡」是「鸭」,「鸭」是「鸡」……是不是有点晕?放心,以目前的科技水平,你是没办法穿越回去的,所以,「鸡」还是「鸡」,「鸭」还是「鸭」。

言归正传,所以这8个十六进制数据表示什么,完全取决于我们自己的「定义」,程序猿们会把这种「定义」叫做「协议」,也有叫「指令」的。请看下图,这就是其中一个聪明的猿类「定义」的一条指令:

我们将这8个byte所表示的内容定义清楚

  • 第1个字节表示起始位;
  • 第2个字节是指令号,用于识别是哪一条指令;
  • 第3-4个字节,表示的是颜色值(分别代表RGB三原色其中一色);
  • 第6个字节表示亮度值;
  • 第7个字节是保留位,作用是如果突然要增加内容,有位置可加;
  • 第8个字节是校验位,用于确保整条指令的完整性(可以是固定值,也可以通过一定的算法算出,这里是使用固定值),大概意思就是:见到0x99,就表示这是一条完整的指令了。

备注:这里的「MCU to Phone」,表示这条数据是从硬件(单片机)发送到手机的。

所以,你从蓝牙接收到的数据,不要问我有什么意义,表示的是什么。应该问写固件、作定义的同事,或者是写APP的和写固件的同事一起定义——往往固件的同事单独定义,对写APP的同事来说,会有很多坑,因为他们很难考虑得到APP这边的情况(深受其害状)。

如何更好地收发数据

好了,上面讲了一大堆,终于要和标题扯上点关系了。

拿上面的收到的这条指令举例,或许你已经发现,对我们有意义的数据,其实就是byte3~byte6这4个字节,前3个是颜色值,最后1个是亮度值(其实这是一个利用蓝牙,用手机APP控制灯具颜色、亮度的产品。这条指令是从硬件(Device to Mobile)获取颜色、亮度值)。

我们当然可以简单粗暴直接地声明一个可以容纳若干个元素的C语言数组(buffer),来接收这8bytes数据(我所在公司的前同事也的确是这样做的),类似如下流程:

代码语言:javascript
复制
    // 会声明一个可以容纳若干个元素的C数组(类型一般是无符号的char类型)
    // 在OC中,UInt8、uint8_t都是unsigned char
    UInt8 tmpBuffer[128] = {0};
    
    // 然后用NSData的getBytes:方法拿到数据
    [characteristic.value getBytes:tmpBuffer];
    
    // 再从中取用数据
    unsigned char startBit = tmpBuffer[0];
    light.brightness = tmpBuffer[5];
    light.colorR     = tmpBuffer[2];
    light.colorG     = tmpBuffer[3];
    light.colorB     = tmpBuffer[4];
    ……
    // 有时候还要对tmpBuffer操作,用一堆如memset()、memcpy()等C语言函数,让对C语言不是特别熟的童鞋直接吐血

上面出现了很多「魔术数字」,让后面看代码、维护代码的人看得云里雾里,如果复杂度再高一点,直接吐血。

有没有更好的办法?我们是这样做的:

代码语言:javascript
复制
// 专门有一个类用结构体定义好这些指令
#pragma mark - Device 2 Mobile
#pragma mark Response: 0x13 蓝牙模块返回数据
// 其实这里有个坑,当单个数据的大小为2字节或以上时,我们用UInt16或UInt32去定义,会有「自动对齐」的问题,就是接到的数据,没有按指令定义的顺序对齐,导致数据不正确,这时候可以在struct后面加关键字:「__attribute__((packed))」。(我掉这个坑好久,最后上StackOverflow提问解决)
typedef struct {
    UInt8 startBit;
    UInt8 cmd;
    UInt8 colourR;// 取值范围:0-255
    UInt8 colourG;
    UInt8 colourB;
    UInt8 brightnessValue;// 取值范围:0-255, 0为灭,255为最亮
    UInt8 reserved;
    UInt8 checksum;
} D2MDeviceParamResponse;


    // 然后在接收到数据的地方,定义并用这个结构体接收数据
    const void *raw = characteristics.value.bytes;
    D2MDeviceParamResponse *responseData = (D2MDeviceParamResponse *)raw;
    
    // 取用数据则这样
    light.brightness = responseData->brightnessValue;
    light.colorR     = responseData->colourR;
    light.colorG     = responseData->colourG;
    light.colorB     = responseData->colourB;
    
    //不会出现一个「魔术数字」,直接看代码,就知道是什么东西了。

下面是Swift版本:

代码语言:javascript
复制
// 定义指令
// MARK:- Device 2 Mobile
// MARK:Response: 0x13 蓝牙模块返回数据
struct D2MDeviceParamResponse {
    var startBit: UInt8
    var cmd: UInt8
    var colourR: UInt8
    var colourG: UInt8
    var colourB: UInt8
    var brightnessValue: UInt8
    var reserved: UInt8
    var checksum: UInt8
}

        // 取用数据
        // 对Swift还不是十分熟悉,不知道还有没有其他更好的初始化方法(哭)
       var cmd = D2MDeviceParamResponse(startBit: 0,
                                         cmd: 0,
                                         colourR: 0,
                                         colourG: 0,
                                         colourB: 0,
                                         brightnessValue: 0,
                                         reserved: 0,
                                         checksum: 0)
        
       characteristic.value!.getBytes(&cmd, length:sizeof(D2MDeviceParamResponse))
        
        light.brightness = cmd.brightnessValue
        light.colorR     = cmd.colourR
        light.colorG     = cmd.colourG
        light.colorB     = cmd.colourB

当然,发送指令也是类似的,先定义好容器(struct),再进行赋值封装发送,不再赘述。

这样是不是会比写一堆中括号加下标索引直观很多?

大神们说最好的说明文档就是代码,代码尽量写得让人能意会到你的目的、意图,也算是对代码的后来维护者的一大功德~~

好困,睡觉。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016.04.27 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 我们会从哪里拿到数据?
  • 我们会拿到什么样的数据?
  • 这些数据有什么意义(表示什么)?
  • 如何更好地收发数据
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档