这个问题,对于熟悉C语言的人来说,答案很简单。
不过对我这种不熟悉C语言的人,在坑中「摸索」良久,先后尝试好几种方法。
其实,生活中很多事情也像编程:解决问题的办法有万千,但某些方法确实是比较优雅的。
在这个「摸索」的过程中,也是一个蛮有趣的过程,遂记之。
在过去的项目中,所接触到的「协议/指令(protocol/command)」,数据大多是以1个byte(字节),2个bytes,4个bytes,8个bytes......为单位进行切割组合的。类似如下指令:
xxxCommand:
这时候,1 byte的数据,用UInt8
接,2 bytes的数据,用UInt16
接,4 bytes的数据,用UInt32
接——一切都很美好。
关于UInt8
、UInt16
、UInt32
等数据类型,在MacTypes.h
中,有相关说明:
/********************************************************************************
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
*********************************************************************************/
也有具体定义:
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:
于是,就有了此文的标题:系统没有UInt24,3个bytes的数据,怎么接?(不要怪我问那么白痴的问题)
先贴出我所认为的「最优雅」解决方案,再描述一下我「踩坑」的心路历程。
UInt24
关于如何定一个UInt24
,StackOverFlow上有人提问:How to define 24bit data type in C?。
尝试过某个回答者的做法:
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了。
有了对应的「容器」装数据,那接下来可以定义command(指令)了:
typedef struct __attribute__((packed)) {
UInt16 UUID;
UInt24 mac; // 用自己定义的UInt24接数据
UInt24 pressure;
} D2MXxxCommand; // D2M: Device to Mobile phone
其实到这里,基本就把问题解决了,后面该干嘛干嘛了。但是在获取到数据,显示出来的过程中,有些写法还是刷新了我的认知(主要还是自己对C语言不熟)。
一开始我用了很复杂的方法,网上查到的方法也大都比较复杂(下面会有叙述)。
而实际上,只需要一行就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的写法。
比如,我们用UInt24接了一个数:0x0185B2(大气压),要转换为十进制的浮点数:
// 大气压值 = 十进制值 / 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)
。
另外,
float pressure = ((cmd->pressure1 * 65536) + (cmd->pressure2 * 256) + cmd->pressure3) * 0.01
也有同样的效果,但个人认为这样操作,没有用<<
操作符直观易懂。
以上,就是关于「3个bytes, 怎么接?」的回答。
接下来描述一下踩过的「坑」。
UInt8
最开始想到,就是单独定义3个UInt8
来接数据:
UInt8 pressure1;
UInt8 pressure2;
UInt8 pressure3;
写完这个还「怨气满满」地想:为什么非得要传3个bytes过来,多一个、少一个不行吗?
这埋怨虽是戏言,但是从「产品、消费者」的角度思考,又可以延伸到另外一件事:我们写的框架、软件、产品,有一个重要的准绳——「把复杂留给自己,把简单留给客户」。大部分人拿到一个东西,肯定希望是「插电即用」的,并不希望东折腾西捣鼓才能使用。
扯远了,继续:拿到这3个bytes后,第一反应就是NSData
对象——于是就变着法把这三个bytes捣鼓成NSData对象:
// 将3个bytes重新组合起来
Byte pressureBytes[] = {cmd->pressure1, cmd->pressure2, cmd->pressure3};
// 转为NSData
NSData *pressureData = [NSData dataWithBytes:pressureBytes length:sizeof(pressureBytes)];
然后又想办法将NSData对象捣鼓成十六进制字符串,或者是十进制的浮点数——硬生生把一行代码搞定的事情,写成了几十行。
UInt8 mac[3]
定义成UInt8 mac[3]
形式,其实这和最上面定义UInt24
是类似的,只是最上面的方法起了一个更易于理解的UInt 24
而已。
UInt32
接数据,再截前面3个bytes这种方法也work,不过要注意,UInt32
接回来的数据是4bytes,最后一个byte要进行正确处理(正确给到其他需要的地方),否则后面数据的读取全会乱(少一个byte)。
另外,还试过定义成char *mac
形式,不work,因为sizeof(cmd->mac)
是8,一个指针占用了8 bytes,并不是我们想要的3bytes。
所以,
还要继续熟悉C语言。
毕