传递表格数据的二进制格式

时间:2009-04-19 05:48:19

标签: sql c database embedded protocols

我正在维护一个与现实世界交互的传统嵌入式设备。 一般来说,该设备从传感器收集数据,使用其内部算法处理数据,并在数据达到某种“坏”状态时显示警告。

出于调试目的,我们希望此设备能够定期向我们发送其收到的许多数据,以及处理后的数据。

我们得出的结论是,大多数数据都可以用表格形式描述,类似于

sensor|time|temprature|moisture
------+----+----------+--------
1     |3012|20        |0.5
2     |3024|22        |0.9

我们显然需要支持多种形式的表格。

所以基本上我们需要一个能够接受某组表描述的协议,然后根据其描述传递表数据。

用于发送数据的示例伪代码是:

table_t table = select_table(SENSORS_TABLE);
sensors_table_data_t data[] = {
    {1,3012,20,0.5},
    {1,3024,22,0.9}
    };
send_data(table,data);

用于接收数据的示例伪代码是:

data_t *data = recieve();
switch (data->table) {
    case SENSORS_TABLE:
         puts("sensor|time|temprature|moisture");
         for (int i=0;i<data->length;i++) printf(
             "%5s|%4s|%9s|%9s\n",
              data->cell[i]->sensor,
              data->cell[i]->time,
              data->cell[i]->temprature,
              data->cell[i]->moisture);
         break;
    case USER_INPUT_TABLE:
         ...
}

可以在设备和与之通信的客户端计算机上或在线时离线完成表的定义。我们可以添加一个简单的握手协议,以便在设备启动时就表的格式达成一致。

由于这是传统设备,它仅支持RS232通信,并且由于其CPU非常慢(相当于486),因此我们无法承受使用任何类似XML的数据传输方法。那些太昂贵(无论是计算时间还是带宽)。由于带宽方面的考虑,还考虑并拒绝发送原始SQL命令。

[编辑]

为了澄清,每次都减少发送表头的开销,我每次发送数据时都试图避免发送表头。因此,每次我发送一个表格行时,我只需要发送表格ID。

我还想指出,我希望传递的大多数数据都是数字的,因此基于文本的协议太浪费了。

最后我看过Google的协议缓冲区,它足够接近但不支持C.

[/编辑]

有关我所描述的已知协议或实现的任何想法吗?有没有更好的想法发送这些数据?

我知道这个协议设计起来不是很难,我想到了一个两阶段协议:

1)握手:发送您要填写的所有表格的标题。每个表格描述都包含有关每列大小的信息。

2)数据:发送表索引(根据握手),然后是实际数据。数据后面会有校验和。

但我希望避免这种设计的细节,并使用一些现成的协议。或者甚至更好,使用可用的实现。

8 个答案:

答案 0 :(得分:2)

我不知道有任何协议这样做(可能有一个,但我不知道。)

我确定您已经想到了这一点:为什么不将格式作为二进制数据流传递?

伪代码:

struct table_format_header {
  int number_of_fields; /* number of fields that will be defined in table */
                        /* sent before the field descriptions themselves  */
};

struct table_format {
   char column_name[8];   /* name of column ("sensor");  */
   char fmt_specifier[5]; /* format specifier for column */

   ... (etc)
}

然后你可以计算字段/列(以某种方式),传输header结构,以便接收者可以分配缓冲区,然后迭代地为每个字段传输table_format结构。该结构将包含您需要的与该标头有关的所有信息 - 名称,字段中的字节数,等等。如果空间确实受到限制,您可以使用位字段(int precision:3)来指定不同的属性

答案 1 :(得分:2)

您可能想尝试使用协议缓冲区。

http://code.google.com/p/protobuf/
  

协议缓冲区是一种编码方式   有效的结构化数据   可扩展的格式。谷歌使用   几乎所有的协议缓冲区   内部RPC协议和文件   格式。

建立rascher的评论,protobufs编译格式,因此传输和接收的效率非常高。如果您想稍后添加/删除字段,它也是可扩展的。还有很棒的API(例如protobuf python)。

答案 2 :(得分:2)

如果您的所有数据都是恒定长度,那么它们之间不需要任何分隔符。所以你可以直接发送二进制内容。例如,行:

sensor|time|temprature|moisture
------+----+----------+--------
1     |3012|20        |0.5

将以:

发送
0x01 0x0B 0xC4 0x14 [4 bytes for float 0.5]

我假设传感器和温度的一个字节表示,时间的两个字节和湿度的4个字节(浮点)。您无需发送标题。

每一行现在都是恒定长度,接收器必须进行转换作业。嵌入式设备可以轻松地以这种格式发送数据。

现在,还存在将数据封装在消息中的问题,以便接收者知道消息何时开始。您通常通过添加页眉和页脚来执行此操作:

[STX] message [ETX]

通常使用ASCII字符STX和ETX(我认为是0x02和0x03)。问题是这些值也可以出现在邮件正文中。因此,您需要在传输中添加另一个图层。当要发送字节0x02或0x03时,发送两次。在接收器上,单个0x02字节表示消息的开始。必须删除邮件正文中的其他0x02和0x03字节。

最后,如果通信链接不可靠,您还需要添加校验和。

这些技术通常由PPP等串行协议使用。

答案 3 :(得分:1)

在嵌入式工作中,通常建议嵌入式设备尽可能少地工作,并让客户端计算机利用自己的速度获得工具。根据您的示例,我可以收集数据,然后格式化表格,只需查看我收到的数据的最大大小,或列标题的最大大小(我的选择)。由于它是调试信息,如果表大小从一个集合更改为下一个集合,则无关紧要。或者,您的设备可以通过发送标头标签来“强制”列大小,或者它甚至可以传输第一行虚拟数据,其中所有数据都为零,但是具有所需的格式和长度。

答案 4 :(得分:1)

我会投票支持CSV(有关CSV的最佳说明,请参阅RFC 4180),因为它是最简单的格式(请参阅 gbarry的回答)。

如RFC(第2部分,第3项)中所述,您需要一个带有列名称的可选标题。

主要考虑在CSV发送者中注意只是逃避“特殊”字符。

答案 5 :(得分:1)

有人说:

[header][data][checksum]

但是如果你想扩展它,你可以使用:

[header][table_id][elements][data][checksum]

[header]   : start of frame
[table_id] : table
[elements] : payload size
[data]     : raw data
[checksum] : checksum/crc, just to be on the safe side

您可以使用“元素”作为固定大小的数据片段数,甚至可以使用“数据”片段中的字节数。

在屏幕上查看数千个十六进制字符时,标题和校验和可以让您的生活更轻松。

编辑:

标题是告诉主持人程序消息已开始/结束的好方法。 你有没有想过这个?

另一方面,您必须以统计方式考虑使用标头。 每10个字节4个字节为40%,但256个字节仅为1.6%。 所以,相应的尺寸。

答案 6 :(得分:1)

我知道你说你不想使用文字,但你应该考虑使用B64。这允许直接且相对有效的二进制到文本并返回到二进制转换。开销是1/3。二进制的每三个字节被转换为四个字节的文本值。转换为文本后,您可以使用简单的数据样式协议。在发送设备上,您只需要实现编码器。请参阅以下完整代码:

/********************************************************************/
/*                                                                  */
/* Functions:                                                       */
/* ----------                                                       */
/* TBase64Encode()                                                  */
/* TBase64Decode()                                                  */
/* TBase64EncodeBlock()                                             */
/* TBase64DecodeBlock()                                             */
/*                                                                  */
/********************************************************************/

#include "yourstuff.h"


// This table is used to encode 6 bit binary to Base64 ASCII.
static char Base64Map[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef"
                          "ghijklmnopqrstuvwxyz0123456789+/";

// This table is used to decode Base64 ASCII back to 6 bit binary.
static char Base64Decode[]=
{
    62,                                         // '+'
    99, 99, 99,                                 // **** UNUSED ****
    63,                                         // '/'
    52, 53, 54, 55, 56, 57, 58, 59, 60, 61,     // '0123456789'
    99, 99, 99, 99, 99, 99, 99,                 // **** UNUSED ****
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9,               // 'ABCDEFGHIJ'
    10, 11, 12, 13, 14, 15, 16, 17, 18, 19,     // 'KLMNOPQRST'
    20, 21, 22, 23, 24, 25,                     // 'UVWXYZ'
    99, 99, 99, 99, 99, 99,                     // **** UNUSED ****
    26, 27, 28, 29, 30, 31, 32, 33, 34, 35,     // 'abcdefghij'
    36, 37, 38, 39, 40, 41, 42, 43, 44, 45,     // 'klmnopqrst'
    46, 47, 48, 49, 50, 51                      // 'uvwxyz'
};




/** Convert binary data to Base64 data.
 *
 * @return  Size of output buffer if ok, -1 if problem (invalid paramaters).
 *
 * @param   input  - Pointer to input data.
 * @param   size   - Number of bytes to encode.
 * @param   output - Pointer to output buffer.
 *
 * @note    Up to caller to ensure output buffer is big enough. As a rough
 *          guide your output buffer should be (((size/3)+1)*4) bytes.
 */
int TBase64Encode( const BYTE *input, int size, PSTR output)
{
    int i, rc=0, block_size;

    while (size>0)
    {
        if (size>=3)
            block_size = 3;
        else
            block_size = size;

        i = TBase64EncodeBlock( input, block_size, output);

        if (i==-1)
            return -1;

        input += 3;
        output += 4;
        rc += 4;
        size -= 3;
    }

    return rc;
}




/** Convert Base64 data to binary data.
 *
 * @return  Number of bytes in output buffer, negative number if problem
 *          as follows:
 *           -1 : Invalid paramaters (bad pointers or bad size).
 *           -2 : Outside of range value for Base64.
 *           -3 : Invalid base 64 character.
 *
 * @param   input  - Pointer to input buffer.
 * @param   size   - Size of input buffer (in bytes).
 * @param   output - Pointer to output buffer.
 *
 * @note    Up to caller to ensure output buffer is big enough. As a rough
 *          guide your output buffer should be (((size/4)+1)*3) bytes.
 *          NOTE : The input size paramater must be multiple of 4 !!!!
 *          Note that error codes -2 and -3 essentiallty mean the same
 *          thing, just for debugging it means something slight different
 *          to me :-). Calling function can just check for any negative
 *          response.
 */
int TBase64Decode( CPSTR input, int size, BYTE *output)
{
    int output_size=0, i;

    // Validate size paramater only.
    if (size<=0 || size & 3)
        return -1;

    while (size>0)
    {   
        i = TBase64DecodeBlock( input, output);
        if (i<0)
            return i;

        output_size += i;
        output += i;
        input += 4;
        size -= 4;
    }

    return output_size;
}




/** Convert up to 3 bytes of binary data to 4 bytes of Base64 data.
 *
 * @return  0 if ok, -1 if problem (invalid paramaters).
 *
 * @param   input  - Pointer to input data.
 * @param   size   - Number of bytes to encode(1 to 3).
 * @param   output - Pointer to output buffer.
 *
 * @note    Up to caller to ensure output buffer is big enough (4 bytes).
 */
int TBase64EncodeBlock( const BYTE *input, int size, PSTR output)
{
    int i;
    BYTE mask;
    BYTE input_buffer[3];

    // Validate paramaters (rudementary).
    if (!input || !output)
        return -1;
    if (size<1 || size>3)
        return -1;

    memset( input_buffer, 0, 3);
    memcpy( input_buffer, input, size);

    // Convert three 8bit values to four 6bit values.
    mask = input_buffer[2];
    output[3] = mask & 0x3f;            // Fourth byte done...

    output[2] = mask >> 6;
    mask = input_buffer[1] << 2;
    output[2] |= (mask & 0x3f);         // Third byte done...

    output[1] = input_buffer[1] >> 4;
    mask = input_buffer[0] << 4;
    output[1] |= (mask & 0x3f);         // Second byte done...

    output[0] = input_buffer[0]>>2;     // First byte done...

    // TEST
//  printf("[%02x,%02x,%02x,%02x]", output[0], output[1], output[2], output[3]);

    // Convert 6 bit indices to base64 characters.
    for (i=0; i<4; i++)
        output[i] = Base64Map[output[i]];

    // Handle special padding.
    switch (size)
    {
        case 1:
            output[2] = '=';
        case 2:
            output[3] = '=';
        default:
            break;
    }


    return 0;
}




/** Convert 4 bytes of Base64 data to 3 bytes of binary data.
 *
 * @return  Number of bytes (1 to 3) if ok, negative number if problem
 *          as follows:
 *           -1 : Invalid paramaters (bad pointers).
 *           -2 : Outside of range value for Base64.
 *           -3 : Invalid base 64 character.
 *
 * @param   input  - Pointer to input buffer (4 bytes).
 * @param   output - Pointer to output buufer (3 bytes).
 *
 * @comm    While there may be 1, 2 or 3 output bytes the output
 *          buffer must be 3 bytes. Note that error codes -2 and -3
 *          essentiallty mean the same thing, just for debugging it
 *          means something slight different to me :-). Calling function
 *          can just check for any negative response.
 */
int TBase64DecodeBlock( CPSTR input, BYTE *output)
{
    int i, j;
    int size=3;
    BYTE mask;
    BYTE input_buffer[4];

    // Validate paramaters (rudementary).
    if (!input || !output)
        return -1;

    memcpy( input_buffer, input, 4);

    // Calculate size of output data.
    if (input_buffer[3]=='=')
    {
        input_buffer[3] = 43;
        size--;
    }
    if (input_buffer[2]=='=')
    {
        input_buffer[2] = 43;
        size--;
    }

    // Convert Base64 ASCII to 6 bit data.
    for (i=0; i<4; i++)
    {
        j = (int) (input_buffer[i]-43);
        if (j<0 || j>79)
            return -2;          // Invalid char in Base64 data.
        j = Base64Decode[j];
        if (j==99)      
            return -3;          // Invalid char in Base64 data.

        input_buffer[i] = (char) j;
    }

    // TEST
//  printf("[%02x,%02x,%02x,%02x]", input_buffer[0], input_buffer[1], input_buffer[2], input_buffer[3]);

    // Convert four 6bit values to three 8bit values.
    mask = input_buffer[1] >> 4;
    output[0] = (input_buffer[0]<<2) | mask;    // First byte done.

    if (size>1)
    {
        mask = input_buffer[1] << 4;
        output[1] = input_buffer[2] >> 2;
        output[1] |= mask;              // Second byte done.

        if (size==3)
        {
            mask = input_buffer[2] << 6;
            output[2] = input_buffer[3] | mask;     // Third byte done.
        }
    }

    return size;
}

答案 7 :(得分:0)

串行计算的基础知识......

[header] [data] [check-sum]

[data]是最重要的部分,但[header]和[checksum]确实有助于解决奇怪的真实单词问题。然而小的总是尝试使用[header]和[checksum]。

现在通过制作大型数据链来减少[header],[checksum]重载确实有帮助。

在读取数据后,通过从主机PC(这将是您的调试PC)执行任何操作,使用任何格式读取并显示数据。