经验分享:TLV协议的运用

TLV是tag(标签), length(长度) & value(值)的简称,是一种常见的数据打包封装格式,常用在数据采集与传输的物联网系统中。TLV通信协议具有很强的灵活性,用户可以针对自己要传输的数据特点自定义相关数据段的名称与大小。TLV通信协议在小型物联网系统里面有重要的应用。

本篇博客主要站在本人之前温度监控的系统上,引入TLV协议,实现系统的一次优化。当然,之前温度监控程序的源码是闭源的,这里只简单地设想一种与之类似的情景进行分析阐述,并给出对应的代码。

TLV通信协议的基本含义

真实的物联网系统中,消息接收端(服务器)收到的数据都是一个个的字节流,而不是用户APP中所接收的字符流,两种传输数据类型的差异给物联网移动客户端的开发带来了一定的难度。TLV就是一种基于字节流的数据传输模式,数据经TLV格式封装之后形成了一个个的字节。

与计算机网络中的数据帧概念一样,数据经TLV协议封装之后也成帧,TLV数据帧的通用格式如下(所占的字节数已标记):

数据标签 数据帧长度 数据值
1byte 1byte

数据标签,即‘T’,表示数据的类型,其范围是0~255,用户可根据系统中数据传输的实际状况做出相应的合理规定。一个物联网系统中可能具有多种不同类型的数据在其中传输,数据标签起着判断数据类型的作用

数据帧长度,即‘L’,指的是打包封装之后整个数据帧的长度

数据值,即‘V’,指的是要传输数据的值,不同类型的数据其值的特点不同,用户可根据自己的实际情况对值进行分字节封装填充。

TLV协议的改良

同计算机网络中数据链路层一样,上述的TLV协议若直接使用容易出现透明传输的问题

可以想到这样两种情形:

  1. 如果tag位设置为0x08,但帧的数据值部分也有一位值为0x08,这就会使得系统误将此位作为tag位进行数据解析,造成数据解析的混乱;
  2. 当数据的传输出现差错时,也难以发现并丢弃数据帧,更别提客户端重传了。

针对上面可能出现的两种问题,可以对TLV协议进行如下优化:

数据帧头部 数据标签 数据帧长度 数据值 CRC校验和
1 byte 1 byte 1 byte 2 bytes

数据帧头部是整个数据帧开始的标志,为了防止与数据帧中其他位的值相冲突,一般将其设置为很大(0xF0~0xFF),如果采集的数据中存在比数据帧头部还大的数,此时应将数据进行分字节封装填充;

CRC校验和的计算同计算机网络中的CRC校验和计算方法一致,对于不同版本的CRC校验算法,计算得到的校验和所占字节数是不同的,一般来说,在TLV协议中,采用的是CRC-16版本的计算方法,计算得到的字节数为2 bytes。有关CRC校验的相关内容,详情请见百度百科:
CRC (循环冗余校验)

这样一来,TLV数据帧的长度便是5bytes +了。

TLV协议运用举例

假设在一个树莓派开发板上,装有sht20传感器(用于测量环境的温湿度)。sht20测得温度的范围是0至100℃温度中整数部分与小数部分分别占两个十进制位;测得湿度的范围是10%至80%湿度只有百分整数部分。要求上报的温湿度要注明时间,其中温度的时间格式是“ 时 | 分 ”,湿度的时间格式是“月 | 日”

下面来分析一下上述情形下TLV协议的运用。

  1. 上述数据值的范围都没有大于0x64的情况,因此选用0xF0以上的数作为数据帧头部是没有问题的,这里不妨将数据帧头部定为0xFA;
  2. 方便起见,此时tag位可以用英文单词“Temperature”与“Humidity”的首字母作为标签来表示一帧数据的类型,其中‘T’代表温度,‘H’代表湿度;
  3. 温度具有小数部分与整数部分,且各占两个十进制位,此时温度是数据值部分就应该设置为两个字节,一个字节存温度高位,一个字节存温度地位。湿度只有整数部分,因而只需用一个字节来存储;
  4. 温度数据帧的时间部分分配两个字节,分别为时、分。湿度数据帧的事件部分分配两个字节,分别为月、日;
  5. 温度数据帧与湿度数据帧的校验和均从倒数第三位(数据值部分最后一位)开始计算;

将上述规定进行整合,便得到了温度数据帧与湿度数据帧两者的帧结构:

  • 温度数据帧:
数据帧头部 数据标签 数据帧长度 温度值高位 温度值低位 CRC校验和高位 CRC校验和低位
0xFA ‘T’ 0x09
  • 湿度数据帧
数据帧头部 数据标签 数据帧长度 湿度值 CRC校验和高位 CRC校验和低位
0xFA ‘H’ 0x08

解析TLV数据帧

之前提过,物联网服务器接收的数据是字节流,每一次都是从接收缓冲区中读取数据,一次读取一个缓冲区大小的内容。

针对上面所举的例子,便可以制定出TLV数据帧解析的方案:

  1. 用一个buffer数组来接收缓冲区中的内容,从buffer数组的0下标处开始进行操作;
  2. 当读到buffer[i]为0xFA时,说明buffer[i]为一个数据帧的帧头,数据帧的解析由此开始;
  3. 读取buffer[i+1]处的值,若值为‘H’,说明此帧为湿度数据帧,否则就是温度数据帧;
  4. 假设已经断定此帧为湿度数据帧了,便可以开始计算前6位的CRC校验和,将此时计算的校验和同第78位的之前计算的校验和进行对比,若不一致,则丢弃此数据帧,否则就进行后面的步骤;
  5. 从buffer[3]开始,依次读取数据值部分的内容,读到buffer[5]为止,将读取的湿度和时间信息暂存在一个结构体里面。

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

你可能感兴趣的:(经验分享:TLV协议的运用)