C: Элегантный способ декодировать массив символов (десериализовать) в структуру? - PullRequest
1 голос
/ 07 мая 2020

Итак, в настоящее время я разрабатываю простой протокол передачи для кольцевой сети, реализованный в UART.

Для передачи я конвертирую данные из структуры в поток символов, начинающийся <и завершающийся> - Я знаю о возможности того, что любое значение 0x3 c или 0x3e будет ошибочно принято за <или> соответственно, я работаю над решением, чтобы избежать этого. Это не часть моего вопроса.

Итак, его структура похожа на <UINT32UINT32UINT8UINT8UINT16char[0...n]>, эти типы представляют: <destinationId senderId TimeToLive Cmdtype CmdId Payloadlength Payload>. Это всегда остается неизменным, поэтому я могу предположить это без каких-либо разделителей между значениями. Это работает, и я теоретически тоже могу это расшифровать. Чтобы легко получить доступ к байтам, я реализовал структуру с объединениями:

typedef struct{
    union{
        uint32_t val;
        char bytes[sizeof(uint32_t)];
    } recipientId;
    union{
        uint32_t val;
        char bytes[sizeof(uint32_t)];
    } senderId;
    union{
        uint8_t val;
        char bytes[sizeof(uint8_t)];
    } timeToLive;
    union{
        uint8_t val;
        char bytes[sizeof(uint8_t)];
    } cmdType;
    union{
        uint8_t val;
        char bytes[sizeof(uint8_t)];
    } cmdId;
    union{
        uint16_t val;
        char bytes[sizeof(uint16_t)];
        } payloadLength;
    char *payload;
    char *commandRaw;
} aurPacket_t;

Как только пакет существует, я декодирую его чем-то похожим на это:

void decode_command(aurPacket_t packet){
    if((packet.commandRaw[0] != '<' ) || (packet.commandRaw[strlen(packet.commandRaw) - 1]  != '>') ){
        printf("\n\nINVALID COMMAND\n\n");
    }
    else{
        aurPacket_t newpacket;
        // EITHER:
//      for (int i = 0; i < strlen(newpacket.recipientId.bytes); i++){
//          newpacket.recipientId.bytes[i] = (char)*(packet.commandRaw + 1 + i);
//      }

        // OR:
        strncpy(newpacket.recipientId.bytes, (packet.commandRaw + 1), sizeof(newpacket.recipientId.bytes));
    }
}

commandRaw содержит поток символов который будет получен в сообщении.

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

Я был осведомлен о memcpy, но поскольку я хочу сохранить протокол не зависит от платформы, я бы предпочел не использовать его, если нет элегантного способа. Или есть способ использовать это элегантно? Мой метод добавления переменных отличается от простого использования memcpy? Думая об этом, это не похоже на то, что было бы, учитывая порядок варов внутри моей структуры. Если бы я сделал строку, содержащую <, memcpy-добавил все до полезной нагрузки, затем memcpy-добавил полезную нагрузку, а затем добавил >, будет ли какая-то реальная разница в данных? Если нет, я мог бы просто использовать этот процесс в обратном порядке для декодирования сообщения.

В настоящее время я кодирую сообщение, используя эту функцию:

#define RAW_PACKET_LEN 1024
void parse_command(aurPacket_t packet){
    snprintf(packet.commandRaw, RAW_PACKET_LEN, "<%.4s%.4s%.1s%.1s%.1s%.02s%.*s>",
                                    packet.recipientId.bytes,
                                    packet.senderId.bytes,
                                    packet.timeToLive.bytes,
                                    packet.cmdType.bytes,
                                    packet.cmdId.bytes,
                                    packet.payloadLength.bytes,
                                    packet.payloadLength.val, packet.payload
                                    );
} // memory for commandRaw allocated outside of function

, проблема в том, что на самом деле это не так. запись в поток 0x00 байтов, но это не часть вопроса - я ищу ответ на этот момент (конечно, если вы знаете простое решение, дайте мне знать :))

Система представляет собой ESP32, запрограммированный с использованием ESP-IDF.

Ответы [ 3 ]

0 голосов
/ 07 мая 2020

Ваша структура выглядит избыточной. Лучше использовать что-то вроде этого:

typedef struct {
    uint8_t head;
    uint32_t recipientId;
    uint32_t senderId;
    uint8_t timeToLive;
    uint8_t cmdType;
    uint8_t cmdId;
    uint16_t payloadLength;
    uint8_t *payload;
    uint8_t tail;
} aurPacket_t;

В любом случае вы должны иметь дело с буфером, чтобы иметь память для полезной нагрузки. Это может быть глобальный массив фиксированного размера, лучше для встроенного ПО. memcpy - хорошее решение, когда вы контролируете все размеры, с которыми работаете, потому что обычно оно оптимизировано для целевого оборудования. Вы должны использовать только типы фиксированного размера, такие как uint8_t и uint32_, для ваших элементов данных в вашем пакете, чтобы сделать его кроссплатформенным и помнить о big- или little-endian на ваших машинах при сохранении данных в пакете.

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

0 голосов
/ 12 мая 2020

Вот несколько советов:

  1. Ваш пакет не должен содержать указателя, он будет указывать на адрес, о котором будет знать только отправитель. Вы хотите фактически скопировать массив в команду (см. 4 ниже).
  2. «char bytes [sizeof (uint8_t)]» буквально предварительно обрабатывается во время компиляции в «char bytes [1]», поэтому вы можете также просто используйте «char byte», который является единственным числом.
  3. Если вы просто используете объединение для uint8_t и char, не беспокойтесь, просто приведите его, прежде чем использовать: (например, printf ( «% C», (char) val);
  4. Вы избавите себя от тонны головной боли, если просто согласитесь с фиксированным размером пакета. Похоже, что единственное, что будет динамическим c, - это payload и commandraw. Выберите свой наихудший случай и с ним go. Я знаю, что вы используете последовательный порт, но если вы не делаете что-то другое, кроме 8N1, у него все равно не будет проверок, поэтому вам тоже ничего не нужно Я предлагаю вам выбрать общую длину менее 1472, если вы однажды перейдете на UDP / TCP. как структуру. Затем вы создаете объединение, которое объединяет содержит команду в качестве первого члена и массив размером с команду в качестве второго члена. Например, я бы использовал uint8s (вы можете использовать символы).

    union CommandMsg {// вы также можете определить это заранее ..inlining b / c lazy. struct Command {uint32_t recipientId; uint32_t senderId; uint8_t val; char timeToLive; // почему char? uint8_t cmdType; uint8_t cmdId; uint16_t val; uint16_t payloadLength; // может быть, просто попозже, если term'd? полезная нагрузка char [256]; char commandRaw [256]; } asCommand; uint8_t asByteArray [sizeof (команда)]; } commandMsg;

  5. Безопасность прежде всего, прежде чем вы вставляете нули вашей команды struct memcpy во все это. Нули будут действовать как терминаторы для любых строк, которые вы используете позже. *) localCopy, (const void *) incoming, sizeof (CommandMsg));

0 голосов
/ 07 мая 2020

Не совсем уверен, что считается «элегантным», но вот несколько разнообразных решений.

Старомодным C способом справиться с чем-то вроде этого было бы создание таблицы поиска. Учитывая, что у вас есть только stati c "singleton" буфер static aurPacket_t aur_packet;, тогда вы можете создать таблицу поиска указателя во время компиляции:

#define AUR_PACKET_MEMBERS_N 6

static char* const AUR_PACKET_LOOKUP[AUR_PACKET_MEMBERS_N] =
{
  aur_packet.recipientId.bytes,
  aur_packet.senderId.bytes,
  aur_packet.timeToLive.bytes,
  aur_packet.cmdType.bytes,
  aur_packet.cmdId.bytes,
  aur_packet.payloadLength.bytes,
};

И теперь вы можете перебирать каждую часть структуры, обратившись к AUR_PACKET_LOOKUP[i] и получив char* ее член bytes.


Более радикальным (и не обязательно читаемым) подходом будет «X-макросы», создающие что-то вроде

#define AUR_PACKET_LIST    \
  X(UINT32, recipientId)   \
  X(UINT32, senderId)      \
  X(UINT8,  timeToLive)    \
  X(UINT8,  cmdType)       \
  X(UINT8,  cmdId)         \
  X(UINT16, payloadLength) \

Теперь вы можете сгенерировать строку во время компиляции без использования функций sprintf:

strcpy(packet.commandRaw, 
  #define X(type, member) #type
    AUR_PACKET_LIST
  #undef X
);

Что расширяется до strcpy(packet.commandRaw, "UINT32" "UINT32" ..., а препроцессор объединяет строковые литералы из там.


И, наконец, вполне возможно go полностью макрос обезьяны и использовать макросы X для определения самого типа, что я действительно не рекомендую, если у вас нет строгих требований, чтобы избежать кода повторение:

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef uint8_t  UINT8;
typedef uint16_t UINT16;
typedef uint32_t UINT32;

#define AUR_PACKET_LIST(X) \
/*  type    name        */ \
  X(UINT32, recipientId)   \
  X(UINT32, senderId)      \
  X(UINT8,  timeToLive)    \
  X(UINT8,  cmdType)       \
  X(UINT8,  cmdId)         \
  X(UINT16, payloadLength) \

#define AUR_PACKET_DECL_MEMBER(type, name) \
  union {                                  \
    type val;                              \
    char bytes[sizeof(type)];              \
  } name;

typedef struct{
    AUR_PACKET_LIST(AUR_PACKET_DECL_MEMBER)
    char *payload;
    char *commandRaw;
} aurPacket_t;

int main(void) 
{
  aurPacket_t packet = {.commandRaw=malloc(256)};
  packet.commandRaw[0]='\0';

  #define AUR_PACKET_COMMANDRAW(type, name) #type
  strcpy(packet.commandRaw, AUR_PACKET_LIST(AUR_PACKET_COMMANDRAW));

  puts(packet.commandRaw);
  return 0;
}
...