двоичный формат для передачи табличных данных - PullRequest
3 голосов
/ 19 апреля 2009

Я поддерживаю устаревшее встроенное устройство, которое взаимодействует с реальным миром. Вообще говоря, это устройство собирает данные от датчиков, обрабатывает данные, используя свой внутренний алгоритм, и отображает предупреждение, когда данные достигают определенного «плохого» состояния.

В целях отладки мы хотим, чтобы это устройство регулярно отправляло нам многие данные, которые оно получает, а также данные после его обработки.

Мы пришли к выводу, что большинство данных можно описать в виде таблицы, что-то вроде

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, а его ЦП довольно медленный (эквивалент 486), мы не можем позволить себе использовать какие-либо методы передачи данных, подобные XML. Это слишком дорого (или по времени вычислений, или по полосе пропускания). Отправка необработанных команд SQL также рассматривалась и отклонялась из-за пропускной способности.

[править]

Для пояснения, также уменьшите накладные расходы на отправку заголовка таблицы каждый раз, я стараюсь избегать отправки заголовка таблицы каждый раз, когда отправляю данные. Так что каждый раз, когда я отправляю строку таблицы, мне просто нужно отправить идентификатор таблицы.

Я также хотел бы отметить, что большинство данных, которые я хочу передать, являются числовыми, поэтому текстовые протоколы слишком расточительны.

Наконец я видел буферы протокола Google, он достаточно близок, но не поддерживает C.

[/ править]

Есть идеи об известном протоколе или реализации, как я описал? Есть идея лучше отправить эти данные?

Я осознаю тот факт, что этот протокол не очень сложен для разработки, я имел в виду двухфазный протокол:

1) Рукопожатие: отправьте заголовки всех таблиц, которые вы хотите заполнить. Каждое описание таблицы будет содержать информацию о размере каждого столбца.

2) Данные: отправьте индекс таблицы (в соответствии с рукопожатием) с последующими фактическими данными. Данные будут сопровождаться контрольной суммой.

Но я бы хотел избежать мелких деталей такого дизайна и использовать какой-нибудь готовый протокол. Или, что еще лучше, используйте доступную реализацию.

Ответы [ 8 ]

2 голосов
/ 21 апреля 2009

Если все ваши данные имеют постоянную длину, то вам не нужен разделитель между ними. Таким образом, вы можете напрямую отправить двоичный контент. Например, строка:

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.

2 голосов
/ 19 апреля 2009

Возможно, вы захотите попробовать буферы протокола.

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

Протоколные буферы - это способ кодирования структурированные данные в эффективном еще расширяемый формат. Google использует Буферы протокола почти для всех внутренние протоколы RPC и файл форматы.

Основываясь на комментариях Рашера, protobufs компилируют формат, так что смешно эффективно передавать и получать. Это также расширяется в случае, если вы хотите добавить / удалить поля позже. И есть отличные API (например, protobuf Python).

2 голосов
/ 19 апреля 2009

Я не знаю ни одного протокола, который бы это делал (может быть один, но я его не знаю).

Я уверен, что вы подумали об этом: почему бы не передать формат в виде потока двоичных данных?

псевдокод:

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 голос
/ 05 мая 2009

Я знаю, что вы сказали, что не хотите использовать текст, но вам стоит подумать об использовании 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;
}
1 голос
/ 22 апреля 2009

Как кто-то сказал:

[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

Вы можете использовать «элементы» в качестве числа фрагментов данных фиксированного размера или даже количества байтов в сегменте «данных».

Заголовки и контрольные суммы могут облегчить вашу жизнь, если взглянуть на тысячи шестнадцатеричных символов на экране.

EDIT:

Заголовки - хороший способ сообщить программе вашего хоста, что сообщение началось / закончилось. Вы думали об этом?

С другой стороны, вы должны думать об использовании заголовков статистически. 4 байта каждые 10 байтов - это 40%, но только 1,6% в 256 байтах. Итак, размер соответственно.

1 голос
/ 19 апреля 2009

Я бы проголосовал за CSV (см. RFC 4180 для лучшего описания CSV), так как это самый простой формат (см. ответ гбарри).

Как объяснено в RFC (раздел 2, пункт 3), вам потребуется дополнительный заголовок с именами столбцов.

Главной идеей, которую нужно позаботиться в отправителе CSV, будет просто экранирование «специальных» символов.

1 голос
/ 19 апреля 2009

Во встроенной работе, как правило, предполагается, что встроенное устройство выполняет как можно меньше работы, и позволяет клиентскому компьютеру использовать собственную скорость при наличии инструментов. Учитывая ваш пример, я мог бы собрать данные, а затем отформатировать таблицу, просто посмотрев на максимальный размер полученных данных или максимальный размер заголовка столбца (мой выбор). И поскольку это отладочная информация, не будет иметь большого значения, если размер таблицы изменится с одной коллекции на другую. Или ваше устройство может «форсировать» размер столбца, просто отправив метки заголовка, или оно может даже передать первую строку фиктивных данных, где все данные являются нулями, но в нужном формате и длине.

0 голосов
/ 20 апреля 2009

Основы последовательных вычислений ...

[header] [data] [check-sum]

[данные] - самая важная часть, но [заголовок] и [контрольная сумма] действительно помогают решать странные проблемы с реальными словами. Как бы то ни было, всегда старайся жить с [header] и [checkum].

Теперь помогает уменьшить перегрузку [header], [checkum], создав большую цепочку данных.

После считывания данных считывает и отображает данные в любом формате, выполняя любые действия с вашего хост-компьютера (который будет вашим отладочным ПК ..)

...