4

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

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

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

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。

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

最后我看到了谷歌的协议缓冲区,它足够接近但它不支持 C。

[/编辑]

关于我所描述的已知协议或实现的任何想法?发送此数据有更好的主意吗?

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

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

2)数据:发送表索引(根据握手),后面跟着实际数据。数据后跟校验和。

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

4

8 回答 8

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) 来指定不同的属性

于 2009-04-19T06:05:12.140 回答
2

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

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

协议缓冲区是一种以高效且可扩展的格式对结构化数据进行编码的方法。Google 几乎所有内部​​ RPC 协议和文件格式都使用协议缓冲区。

基于 rascher 的评论,protobufs 编译格式,因此传输和接收效率非常高。如果您以后想添加/删除字段,它也是可扩展的。并且有很棒的 AP​​I(例如 protobuf python)。

于 2009-04-19T08:25:41.587 回答
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 等串行协议使用。

于 2009-04-21T08:47:33.170 回答
1

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

于 2009-04-19T07:31:44.523 回答
1

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

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

在 CSV 发件人中要注意的主要问题就是转义“特殊”字符。

于 2009-04-19T09:23:57.030 回答
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%。所以,相应地调整大小。

于 2009-04-22T17:10:19.273 回答
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;
}
于 2009-05-05T12:19:46.913 回答
0

串行计算的基础...

[header] [data] [check-sum]

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

现在,通过制作大型数据链来减少 [header]、[checksum] 过载肯定会有所帮助。

读取数据后,通过在主机 PC(这将是您的调试 PC..)上执行任何操作,以任何格式读取和显示数据

于 2009-04-20T05:09:59.537 回答