TLV是tag(标签), length(长度) & value(值)的简称,是一种常见的数据打包封装格式,常用在数据采集与传输的物联网系统中。TLV通信协议具有很强的灵活性,用户可以针对自己要传输的数据特点自定义相关数据段的名称与大小。TLV通信协议在小型物联网系统里面有重要的应用。
本篇博客主要站在本人之前温度监控的系统上,引入TLV协议,实现系统的一次优化。当然,之前温度监控程序的源码是闭源的,这里只简单地设想一种与之类似的情景进行分析阐述,并给出对应的代码。
真实的物联网系统中,消息接收端(服务器)收到的数据都是一个个的字节流,而不是用户APP中所接收的字符流,两种传输数据类型的差异给物联网移动客户端的开发带来了一定的难度。TLV就是一种基于字节流的数据传输模式,数据经TLV格式封装之后形成了一个个的字节。
与计算机网络中的数据帧概念一样,数据经TLV协议封装之后也成帧,TLV数据帧的通用格式如下(所占的字节数已标记):
数据标签 | 数据帧长度 | 数据值 |
---|---|---|
1byte | 1byte | – |
数据标签,即‘T’,表示数据的类型,其范围是0~255,用户可根据系统中数据传输的实际状况做出相应的合理规定。一个物联网系统中可能具有多种不同类型的数据在其中传输,数据标签起着判断数据类型的作用;
数据帧长度,即‘L’,指的是打包封装之后整个数据帧的长度;
数据值,即‘V’,指的是要传输数据的值,不同类型的数据其值的特点不同,用户可根据自己的实际情况对值进行分字节封装填充。
同计算机网络中数据链路层一样,上述的TLV协议若直接使用容易出现透明传输的问题。
可以想到这样两种情形:
针对上面可能出现的两种问题,可以对TLV协议进行如下优化:
数据帧头部 | 数据标签 | 数据帧长度 | 数据值 | CRC校验和 |
---|---|---|---|---|
1 byte | 1 byte | 1 byte | – | 2 bytes |
数据帧头部是整个数据帧开始的标志,为了防止与数据帧中其他位的值相冲突,一般将其设置为很大(0xF0~0xFF),如果采集的数据中存在比数据帧头部还大的数,此时应将数据进行分字节封装填充;
CRC校验和的计算同计算机网络中的CRC校验和计算方法一致,对于不同版本的CRC校验算法,计算得到的校验和所占字节数是不同的,一般来说,在TLV协议中,采用的是CRC-16版本的计算方法,计算得到的字节数为2 bytes。有关CRC校验的相关内容,详情请见百度百科:
CRC (循环冗余校验)
这样一来,TLV数据帧的长度便是5bytes +了。
假设在一个树莓派开发板上,装有sht20传感器(用于测量环境的温湿度)。sht20测得温度的范围是0至100℃,温度中整数部分与小数部分分别占两个十进制位;测得湿度的范围是10%至80%,湿度只有百分整数部分。要求上报的温湿度要注明时间,其中温度的时间格式是“ 时 | 分 ”,湿度的时间格式是“月 | 日”。
下面来分析一下上述情形下TLV协议的运用。
将上述规定进行整合,便得到了温度数据帧与湿度数据帧两者的帧结构:
数据帧头部 | 数据标签 | 数据帧长度 | 温度值高位 | 温度值低位 | 时 | 分 | CRC校验和高位 | CRC校验和低位 |
---|---|---|---|---|---|---|---|---|
0xFA | ‘T’ | 0x09 | – | – | – | – | – | – |
数据帧头部 | 数据标签 | 数据帧长度 | 湿度值 | 月 | 日 | CRC校验和高位 | CRC校验和低位 |
---|---|---|---|---|---|---|---|
0xFA | ‘H’ | 0x08 | – | – | – | – | – |
之前提过,物联网服务器接收的数据是字节流,每一次都是从接收缓冲区中读取数据,一次读取一个缓冲区大小的内容。
针对上面所举的例子,便可以制定出TLV数据帧解析的方案:
CRC校验和的算法很复杂,这里本人直接给出源码,不解释实现的细节,源码详解见文末的源码链接(crc-itu-c.h与crc-itu-c.c)
tlv.h
#ifndef _TLV_H
#define _TLV_H
#include "crc-itu-t.h"
#include
#include
#include
#define TEMPER_LEN 9
#define HUM_LEN 8
#define BUF_SIZE 256
#define FRAME_HEAD 0xFA
#define TEMPER 'T'
#define HUMIDITY 'H'
typedef struct _tlv_temper{
char hour, minute;
unsigned short high, low;
}tlv_temper;
typedef struct _tlv_hum{
char month, day;
unsigned short humi;
}tlv_hum;
void pack_temper(tlv_temper temper, char **buf);
void pack_hum(tlv_hum hum, char **buf);
void tlv_unpack(char buf[]);
#endif
pack_temper(取得温度值后,封装生成温度数据帧):
void pack_temper(tlv_temper temper, char **buf){
int i = 0;
unsigned short crc = 0;
(*buf)[i++] = FRAME_HEAD;
(*buf)[i++] = TEMPER;
(*buf)[i++] = TEMPER_LEN;
(*buf)[i++] = temper.hour;
(*buf)[i++] = temper.minute;
(*buf)[i++] = (char)temper.high;
(*buf)[i++] = (char)temper.low;
crc = crc_itu_t(MAGIC_CRC, *buf, TEMPER_LEN-2);
ushort_to_bytes(*buf+i, crc);
}
pack_hum(取得湿度值后,封装生成湿度数据帧):
void pack_hum(tlv_hum hum, char **buf){
int i = 0;
unsigned short crc = 0;
(*buf)[i++] = FRAME_HEAD;
(*buf)[i++] = TEMPER;
(*buf)[i++] = HUM_LEN;
(*buf)[i++] = hum.day;
(*buf)[i++] = hum.month;
(*buf)[i++] = (char)hum.humi;
crc = crc_itu_t(MAGIC_CRC, *buf, HUM_LEN-2);
ushort_to_bytes(*buf+i, crc);
}
tlv_unpack(解析TLV数据帧):
void tlv_unpack(char buf[]){
int i = 0;
double temperature;
tlv_hum hum;
tlv_temper temper;
unsigned short crc, crc_ago;
while(i<BUF_SIZE){
if(buf[i] == FRAME_HEAD){
if(buf[i+1] == TEMPER){
crc = crc_itu_t(MAGIC_CRC, &buf[i], TEMPER_LEN-2);
crc_ago = bytes_to_ushort(&buf[i+TEMPER_LEN-2], 2);
if(crc != crc_ago)
printf("There is a group of wrong data in temper!\n");
else{
temper.high = (unsigned short)buf[i+5];
temper.low = (unsigned short)buf[i+6];
temper.hour = buf[i+3];
temper.minute = buf[i+4];
temperature = (double)temper.high + (double)temper.low/100.0;
printf("Time: %d:%d Temperature: %2.2f\n", temper.hour, temper.minute, temperature);
}
i+=TEMPER_LEN;
}
else{
crc = crc_itu_t(MAGIC_CRC, &buf[i], HUM_LEN-2);
crc_ago = bytes_to_ushort(&buf[i+HUM_LEN-2], 2);
if(crc != crc_ago)
printf("There is a group of wrong data in humidity!\n");
else{
hum.humi = (unsigned short)buf[i+5];
hum.month = buf[i+3];
hum.day = buf[i+4];
printf("Date: %d|%d Humidity: %d%%\n", hum.month, hum.day, hum.humi);
}
i+=HUM_LEN;
}
}
else
i++;
}
}
这里简要地讲一下crc_itu_t函数和ushort_to_bytes函数、bytes_to_unshort函数。
crc_itu_t函数中的MAGIC_CRC是生成CRC校验和必不可少的一个宏,在头文件crc_itu_t.h中有定义,函数的第二个参数是一个char型的指针,第三个参数是要计算的CRC校验和的位数,整个函数表示从某一个char型指针所指的位置开始,计算之后n位的校验和。
ushort_to_bytes函数,表示将unsigned short型变量转化为字节型变量,并存放于对应的变量中,第一参数是待存放的字节变量,第二个参数是要进行转化的无符号短整型变量。
bytes_to_ushort函数则正好同ushort_to_bytes相反,是将字节变量转化为短整型变量,该函数有返回值,返回值是一个短整型变量,第一个参数字节型变量存放的地址,第二个参数是待转化字节数,表示从某个字节开始,转化n个字节的字节变量为短整型。
源码链接:
TLV