3个bytes, 怎么接?

这个问题,对于熟悉C语言的人来说,答案很简单。

不过对我这种不熟悉C语言的人,在坑中「摸索」良久,先后尝试好几种方法。

其实,生活中很多事情也像编程:解决问题的办法有万千,但某些方法确实是比较优雅的。

在这个「摸索」的过程中,也是一个蛮有趣的过程,遂记之。

问题

在过去的项目中,所接触到的「协议/指令(protocol/command)」,数据大多是以1个byte(字节),2个bytes,4个bytes,8个bytes……为单位进行切割组合的。类似如下指令:

xxxCommand:

  • [1]commandID# 0xFF // 方括号数字:该数据所占字节(byte)数
  • [1]week:
    • bit0:Mon
    • bit1:Tue
    • bit2:Wed
    • bit3:Thu
    • bit4:Fri
    • bit5:Sat
    • bit6:Sun
    • bit7:不使用
  • [4]ip address
  • [2]reserved
  • [1]checksum

这时候,1 byte的数据,用UInt8接,2 bytes的数据,用UInt16接,4 bytes的数据,用UInt32接——一切都很美好。

关于UInt8UInt16UInt32等数据类型,在MacTypes.h中,有相关说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/********************************************************************************
Base integer types for all target OS's and CPU's
UInt8 8-bit unsigned integer
SInt8 8-bit signed integer
UInt16 16-bit unsigned integer
SInt16 16-bit signed integer
UInt32 32-bit unsigned integer
SInt32 32-bit signed integer
UInt64 64-bit unsigned integer
SInt64 64-bit signed integer
*********************************************************************************/

也有具体定义:

1
2
3
4
typedef unsigned char UInt8;
typedef signed char SInt8;
typedef unsigned short UInt16;
typedef signed short SInt16;

而最近,遇到一种新情况:硬件那边发过来的数据,是3个bytes为单位的数据——有3个bytes的mac地址(截取了mac地址的一半,发送/广播给手机端),也有3个bytes的大气压数据。类似如下数据格式:

xxxCommand:

  • [2]UUID
  • [3]mac // 截取了mac地址的一半
  • [3]presure // 据闻大气压数值,2 bytes表示不完,4 bytes又太多了,所以定义了3 bytes~

于是,就有了此文的标题:系统没有UInt24,3个bytes的数据,怎么接?(不要怪我问那么白痴的问题)

先贴出我所认为的「最优雅」解决方案,再描述一下我「踩坑」的心路历程。

Solution:

1.先定义一个UInt24

关于如何定一个UInt24,StackOverFlow上有人提问:How to define 24bit data type in C?

尝试过某个回答者的做法:

1
2
3
struct int24{
unsigned int data : 24;
};

经验证,这个写法不work,因为这个类型还是占4个bytes(用sizeof()函数打印验证),这样拿去接数据,会把别人的那个byte也装过来,后面的数据就会乱掉。

那试着仿照MacTypes.h里的定义,定义如下:

typedef unsigned char[3] UInt24;

这样OK吗?事实上,也有问题,系统会报如下错误:

Brackets are not allowed here; to declare an array, place the brackets after the identifier. Replace ‘[3] UInt24’ with ‘ UInt24’

报错说得很清楚:方括号放错地方。要定义一个array(数组/数列),方括号应该放在新定义类型名称的后面:

typedef unsigned char UInt24[3];

这样就OK了。

2.定义command

有了对应的「容器」装数据,那接下来可以定义command(指令)了:

1
2
3
4
5
typedef struct __attribute__((packed)) {
UInt16 UUID;
UInt24 mac; // 用自己定义的UInt24接数据
UInt24 pressure;
} D2MXxxCommand; // D2M: Device to Mobile phone

其实到这里,基本就把问题解决了,后面该干嘛干嘛了。但是在获取到数据,显示出来的过程中,有些写法还是刷新了我的认知(主要还是自己对C语言不熟)。

  • 将mac地址的3个bytes转为十六进制形式的字符串

    一开始我用了很复杂的方法,网上查到的方法也大都比较复杂(下面会有叙述)。

    而实际上,只需要一行就OK了:

    NSString *macHexString = [NSString stringWithFormat:@"%02X%02X%02X", cmd->mac[0], cmd->mac[1], cmd->mac[2]];

    正常的占位符应该是%X,而这里中间的02,表示该十六进制数限制固定两位数。

    目的是预防这种情况:当第一个byte是小于16的数,只输出1位。例:0x014B5C,如果是用%X,则只输出14B5C;而用%02X,则可输出014B5C

    直接用%02X,就无须再额外判断第一个byte长度是否小于1,如果小于1,再在前面补个零……

    备注:这个写法,参考了以前公司boss的写法。

  • 将pressure(大气压)的3个byte转为十进制浮点数

    比如,我们用UInt24接了一个数:0x0185B2(大气压),要转换为十进制的浮点数:

    1
    2
    // 大气压值 = 十进制值 / 100
    float pressure = ((cmd->pressure[0]<<16) + (cmd->pressure[1]<<8) + cmd->pressure[2]) * 0.01;

    因为大气压的值,同事定义为:该十六进制数的十进制形式再除以100。所以,思路就是将该3个独立的byte组合成一个完整的数,再转十进制就OK了。

    cmd->pressure[0]<<16的意思,就是将pressure中第一个byte左移16bit(位),也就是左移2个byte(字节)的位置——所以操作完后,pressure中第一个byte,从右往左数,就变成是第三个byte了。如下图所示(将0x01往左移16bit(位)):

    将0x01往左移16bit(位)

    cmd->pressure[1]<<8也做了类似的事情,将pressure中第二个byte左移8bit(位),也就是左移1个byte(字节)的位置,如下图(将0x85往左移8bit(位)):

    将0x85往左移8bit(位)

    最后把他们加起来,就是我们要的数了:997.62(Hpa)

    另外,

    1
    float pressure = ((cmd->pressure1 * 65536) + (cmd->pressure2 * 256) + cmd->pressure3) * 0.01

    也有同样的效果,但个人认为这样操作,没有用<<操作符直观易懂。

以上,就是关于「3个bytes, 怎么接?」的回答。

接下来描述一下踩过的「坑」。

其他的尝试:

1.定义成3个单独的UInt8

最开始想到,就是单独定义3个UInt8来接数据:

1
2
3
UInt8 pressure1;
UInt8 pressure2;
UInt8 pressure3;

写完这个还「怨气满满」地想:为什么非得要传3个bytes过来,多一个、少一个不行吗?

这埋怨虽是戏言,但是从「产品、消费者」的角度思考,又可以延伸到另外一件事:我们写的框架、软件、产品,有一个重要的准绳——「把复杂留给自己,把简单留给客户」。大部分人拿到一个东西,肯定希望是「插电即用」的,并不希望东折腾西捣鼓才能使用。

扯远了,继续:拿到这3个bytes后,第一反应就是NSData对象——于是就变着法把这三个bytes捣鼓成NSData对象:

1
2
3
4
// 将3个bytes重新组合起来
Byte pressureBytes[] = {cmd->pressure1, cmd->pressure2, cmd->pressure3};
// 转为NSData
NSData *pressureData = [NSData dataWithBytes:pressureBytes length:sizeof(pressureBytes)];

然后又想办法将NSData对象捣鼓成十六进制字符串,或者是十进制的浮点数——硬生生把一行代码搞定的事情,写成了几十行。

2.定义成UInt8 mac[3]

定义成UInt8 mac[3]形式,其实这和最上面定义UInt24是类似的,只是最上面的方法起了一个更易于理解的UInt 24而已。

3.用UInt32接数据,再截前面3个bytes

这种方法也work,不过要注意,UInt32接回来的数据是4bytes,最后一个byte要进行正确处理(正确给到其他需要的地方),否则后面数据的读取全会乱(少一个byte)。

另外,还试过定义成char *mac形式,不work,因为sizeof(cmd->mac)是8,一个指针占用了8 bytes,并不是我们想要的3bytes。

所以,

还要继续熟悉C语言。