번개애비의 라이프스톼일

Arduino Messagepack(msgpack)에서 다차원배열과 다양한 데이터형태를 Unpack 처리하는 방법 본문

IT

Arduino Messagepack(msgpack)에서 다차원배열과 다양한 데이터형태를 Unpack 처리하는 방법

번개애비 2025. 2. 25. 21:52

 

최근 서버와 Arduino간 Websocket을 통해 실시간 통신 프로토콜을 개발하는 과정에서 

MessagePack의 데이터처리에 이슈가 있어서 골머리를 앓다가 문제를 해결하여 이렇게 포스팅으로 남긴다.

[참고]
MessagePack은 JSON과 같이 Serialize를 지원하면서도 JSON보다 빠르고 짧은 데이터를 전송할 수 있는 장점이 있다.

 


먼저, 문제의 원인

서버와 같이 고오급 언어를 사용하는 환경에서는 Array안에 Key에 따라 Value의 데이터형을 다르게 가져갈 수 있다.

하지만, Arduino와 같이 C Language 베이스의 언어는 Array를 선언할때

반드시 Key와 Value의 데이터형을 지정해야하는 만큼 Key에 따라 달라지는 데이터형을 지원할 수 없다.

 

아래 예시 사례를 보자

//아두이노는 Value가 모두 String과 같이 하나의 데이터형만 Array든 Map이든 담을 수 있다.
{
   "output": "시리얼 출력",
   "is_success": "true",
   "redirect": "/ws/test1",
   "__CORE_EXEC_TYPE__": "WS",
   "timezone": "Asia/Seoul",
   "geocode": "KR",
   "command": "1\n1"
}
//Arduino에서 처리불가능하지만 다른 고급언어는 처리가능
{
   "output": "시리얼 출력",
   "is_success": true,
   "redirect": false,
   "__CORE_EXEC_TYPE__": "WS",
   "timezone": "Asia/Seoul",
   "geocode": "KR",
   "command": 474.0001
}

 

 

MessagePack 공식홈페이지 (https://msgpack.org/) 에 제시된 hideakitai의 MsgPack 라이브러리를 참고하여 

아래와 같이 예시코드를 작성하면 다음과 같은데 문제는 MsgPack::map_t<String, String> rx_map; 과 같이

Key와 Value 모두 "String" 타입의 데이터만 불러올 수 있는 문제점이 있었다.

#include <Arduino.h>
#include <MsgPack.h>

uint8_t packed_data[] = {
	0x87, 0xA6, 0x6F, 0x75, 0x74, 0x70, 0x75, 0x74, 0xB0, 0xEC, 0x8B, 0x9C, 0xEB, 0xA6, 0xAC, 0xEC, 0x96, 0xBC, 0x20, 0xEC, 0xB6, 0x9C, 0xEB, 0xA0, 0xA5, 0xAA, 0x69, 0x73, 0x5F, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0xC3, 0xA8, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0xA9, 0x2F, 0x77, 0x73, 0x2F, 0x74, 0x65, 0x73, 0x74, 0x31, 0xB2, 0x5F, 0x5F, 0x43, 0x4F, 0x52, 0x45, 0x5F, 0x45, 0x58, 0x45, 0x43, 0x5F, 0x54, 0x59, 0x50, 0x45, 0x5F, 0x5F, 0xA2, 0x57, 0x53, 0xA8, 0x74, 0x69, 0x6D, 0x65, 0x7A, 0x6F, 0x6E, 0x65, 0xAA, 0x41, 0x73, 0x69, 0x61, 0x2F, 0x53, 0x65, 0x6F, 0x75, 0x6C, 0xA7, 0x67, 0x65, 0x6F, 0x63, 0x6F, 0x64, 0x65, 0xA2, 0x4B, 0x52, 0xA7, 0x63, 0x6F, 0x6D, 0x6D, 0x61, 0x6E, 0x64, 0xA3, 0x31, 0x0A, 0x30
};
size_t packed_size = sizeof(packed_data);

MsgPack::map_t<String, String> rx_map;
MsgPack::Unpacker unpacker;
unpacker.feed(packed_data, packed_size);
unpacker.deserialize(rx_map);
if (rx_map.find("redirect") != rx_map.end()) {
    Serial.println("Redirect URL: " + rx_map["redirect"]);
} else {
    Serial.println("Key 'redirect' not found!");
}

 

 


 

Binary 또는 Hex코드 베이스로 동작되고 빠르다고 하니

이 MessagePack의 장점을 십분살려 결국 직접 만들게 되었다.

 

#include <Arduino.h>

// UTF-8 유효성 검사
bool isValidUTF8(const uint8_t *data, size_t len) {
  size_t i = 0;
  while (i < len) {
    if ((data[i] & 0x80) == 0x00) i++;
    else if ((data[i] & 0xE0) == 0xC0 && i + 1 < len &&
             (data[i+1] & 0xC0) == 0x80) i += 2;
    else if ((data[i] & 0xF0) == 0xE0 && i + 2 < len &&
             (data[i+1] & 0xC0) == 0x80 && (data[i+2] & 0xC0) == 0x80) i += 3;
    else if ((data[i] & 0xF8) == 0xF0 && i + 3 < len &&
             (data[i+1] & 0xC0) == 0x80 && (data[i+2] & 0xC0) == 0x80 &&
             (data[i+3] & 0xC0) == 0x80) i += 4;
    else return false;
  }
  return true;
}

String bytesToHex(const uint8_t *data, size_t len) {
  String hex = "";
  for (size_t i = 0; i < len; i++) {
    if (i > 0) hex += " ";
    if (data[i] < 0x10) hex += "0";
    hex += String(data[i], HEX);
  }
  return hex;
}

String msgpack_unpack(const uint8_t *data, size_t &index, size_t max_len = SIZE_MAX) {
  if (index >= max_len) return "Error: Index out of bounds";
  uint8_t type = data[index++];

  // nil
  if (type == 0xC0) return "null";
  // bool
  if (type == 0xC2) return "false";
  if (type == 0xC3) return "true";

  // int
  if (type <= 0x7F) return String(type); // positive fixint
  if (type >= 0xE0) return String((int8_t)type); // negative fixint
  if (type == 0xCC) return String(data[index++]); // uint8
  if (type == 0xCD) return String((data[index++] << 8) | data[index++]); // uint16
  if (type == 0xCE) return String(((uint32_t)data[index++] << 24) | ((uint32_t)data[index++] << 16) |
                                  ((uint32_t)data[index++] << 8) | data[index++]); // uint32
  if (type == 0xD0) return String((int8_t)data[index++]); // int8
  if (type == 0xD1) return String((int16_t)((data[index++] << 8) | data[index++])); // int16
  if (type == 0xD2) {
    int32_t val = ((uint32_t)data[index++] << 24) | ((uint32_t)data[index++] << 16) |
                  ((uint32_t)data[index++] << 8) | data[index++];
    return String(val);
  }

  // float
  if (type == 0xCA) { // float32
    union { uint32_t i; float f; } u;
    u.i = ((uint32_t)data[index++] << 24) | ((uint32_t)data[index++] << 16) |
          ((uint32_t)data[index++] << 8) | data[index++];
    return String(u.f, 6);
  }
  if (type == 0xCB) { // float64
    uint64_t raw = 0;
    for (int i = 0; i < 8; i++) raw = (raw << 8) | data[index++];
    double val;
    memcpy(&val, &raw, sizeof(val));
    char buf[32];
    snprintf(buf, sizeof(buf), "%.10f", val);
    return String(buf);
  }

  // string
  size_t len = 0;
  if ((type & 0xE0) == 0xA0) len = type & 0x1F;
  else if (type == 0xD9) len = data[index++];
  else if (type == 0xDA) len = (data[index++] << 8) | data[index++];
  else if (type == 0xDB) len = ((uint32_t)data[index++] << 24) | ((uint32_t)data[index++] << 16) |
                               ((uint32_t)data[index++] << 8) | data[index++];
  if ((type & 0xE0) == 0xA0 || type == 0xD9 || type == 0xDA || type == 0xDB) {
    if (index + len > max_len) return "Error: String out of bounds";
    if (!isValidUTF8(&data[index], len)) return "Error: Invalid UTF-8 string";
    String s = String((const char *)&data[index], len);
    index += len;
    return "\"" + s + "\"";
  }

  // binary
  if (type == 0xC4) len = data[index++];
  else if (type == 0xC5) len = (data[index++] << 8) | data[index++];
  else if (type == 0xC6) len = ((uint32_t)data[index++] << 24) | ((uint32_t)data[index++] << 16) |
                               ((uint32_t)data[index++] << 8) | data[index++];
  if (type == 0xC4 || type == 0xC5 || type == 0xC6) {
    if (index + len > max_len) return "Error: Bin out of bounds";
    String hex = bytesToHex(&data[index], len);
    index += len;
    return "\"bin: " + hex + "\"";
  }

  // array
  if ((type & 0xF0) == 0x90) len = type & 0x0F;
  else if (type == 0xDC) len = (data[index++] << 8) | data[index++];
  else if (type == 0xDD) len = ((uint32_t)data[index++] << 24) | ((uint32_t)data[index++] << 16) |
                               ((uint32_t)data[index++] << 8) | data[index++];
  if ((type & 0xF0) == 0x90 || type == 0xDC || type == 0xDD) {
    String out = "[";
    for (size_t i = 0; i < len; i++) {
      if (i > 0) out += ", ";
      out += msgpack_unpack(data, index, max_len);
    }
    out += "]";
    return out;
  }

  // map
  if ((type & 0xF0) == 0x80) len = type & 0x0F;
  else if (type == 0xDE) len = (data[index++] << 8) | data[index++];
  else if (type == 0xDF) len = ((uint32_t)data[index++] << 24) | ((uint32_t)data[index++] << 16) |
                               ((uint32_t)data[index++] << 8) | data[index++];
  if ((type & 0xF0) == 0x80 || type == 0xDE || type == 0xDF) {
    String out = "{";
    for (size_t i = 0; i < len; i++) {
      if (i > 0) out += ", ";
      String key = msgpack_unpack(data, index, max_len);
      String val = msgpack_unpack(data, index, max_len);
      out += key + ": " + val;
    }
    out += "}";
    return out;
  }

  // ext
  if (type == 0xD4 || type == 0xD5 || type == 0xD6 || type == 0xD7 || type == 0xD8 ||
      type == 0xC7 || type == 0xC8 || type == 0xC9) {
    size_t extLen = 0;
    if (type == 0xD4) extLen = 1;
    else if (type == 0xD5) extLen = 2;
    else if (type == 0xD6) extLen = 4;
    else if (type == 0xD7) extLen = 8;
    else if (type == 0xD8) extLen = 16;
    else if (type == 0xC7) extLen = data[index++];
    else if (type == 0xC8) extLen = (data[index++] << 8) | data[index++];
    else if (type == 0xC9) extLen = ((uint32_t)data[index++] << 24) | ((uint32_t)data[index++] << 16) |
                                   ((uint32_t)data[index++] << 8) | data[index++];
    int8_t extType = data[index++];
    String extData = bytesToHex(&data[index], extLen);
    index += extLen;
    return "\"ext(type " + String(extType) + "): " + extData + "\"";
  }

  return "Error: Unknown type 0x" + String(type, HEX);
}

String msgpack_getdata(const uint8_t *data, String key) {
  size_t index = 0;
  uint8_t type = data[index++];
  size_t mapLen = 0;

  if ((type & 0xF0) == 0x80) mapLen = type & 0x0F;
  else if (type == 0xDE) mapLen = (data[index++] << 8) | data[index++];
  else if (type == 0xDF) mapLen = ((uint32_t)data[index++] << 24) | ((uint32_t)data[index++] << 16) |
                                  ((uint32_t)data[index++] << 8) | data[index++];
  else return "Error: Not a map";

  for (size_t i = 0; i < mapLen; i++) {
    String k = msgpack_unpack(data, index);
    if (k == "\"" + key + "\"") return msgpack_unpack(data, index);
    else msgpack_unpack(data, index); // skip value
  }
  return "Error: Key Not Found";
}



void setup() {
  Serial.begin(115200);

  //다차원 배열예제
  uint8_t packed_data[] = {
    0x8D, 0xA6, 0x6F, 0x75, 0x74, 0x70, 0x75, 0x74, 0xB0, 0xEC, 0x8B, 0x9C, 0xEB, 0xA6, 0xAC, 0xEC, 0x96, 0xBC, 0x20, 0xEC, 0xB6, 0x9C, 0xEB, 0xA0, 0xA5, 0xAA, 0x69, 0x73, 0x5F, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0xC3, 0xA8, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0xA9, 0x2F, 0x77, 0x73, 0x2F, 0x74, 0x65, 0x73, 0x74, 0x31, 0xB2, 0x5F, 0x5F, 0x43, 0x4F, 0x52, 0x45, 0x5F, 0x45, 0x58, 0x45, 0x43, 0x5F, 0x54, 0x59, 0x50, 0x45, 0x5F, 0x5F, 0xA2, 0x57, 0x53, 0xAA, 0x40, 0x63, 0x6F, 0x72, 0x65, 0x5F, 0x74, 0x69, 0x6D, 0x65, 0x93, 0x92, 0xBA, 0x77, 0x65, 0x62, 0x73, 0x6F, 0x63, 0x6B, 0x65, 0x74, 0x20, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x20, 0x72, 0x78, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0xCB, 0x3E, 0xF2, 0xC0, 0x00, 0x73, 0x88, 0xDC, 0xE0, 0x92, 0xAF, 0x68, 0x61, 0x6E, 0x64, 0x73, 0x68, 0x61, 0x6B, 0x65, 0x20, 0x63, 0x68, 0x65, 0x63, 0x6B, 0xCB, 0x3F, 0x21, 0xF8, 0x00, 0x25, 0x6B, 0x7E, 0xBC, 0x92, 0xB1, 0x77, 0x65, 0x62, 0x73, 0x6F, 0x63, 0x6B, 0x65, 0x74, 0x20, 0x74, 0x78, 0x20, 0x73, 0x65, 0x6E, 0x64, 0xCB, 0x3F, 0x1B, 0xD0, 0x00, 0x8E, 0x0E, 0x58, 0xDB, 0xAB, 0x40, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5F, 0x74, 0x69, 0x6D, 0x65, 0x90, 0xAC, 0x40, 0x73, 0x65, 0x6E, 0x64, 0x65, 0x64, 0x5F, 0x64, 0x61, 0x74, 0x61, 0x82, 0xA4, 0x6B, 0x65, 0x79, 0x31, 0xA6, 0x76, 0x61, 0x6C, 0x75, 0x65, 0x31, 0xA4, 0x6B, 0x65, 0x79, 0x32, 0xA6, 0x76, 0x61, 0x6C, 0x75, 0x65, 0x32, 0xA8, 0x74, 0x69, 0x6D, 0x65, 0x7A, 0x6F, 0x6E, 0x65, 0xAA, 0x41, 0x73, 0x69, 0x61, 0x2F, 0x53, 0x65, 0x6F, 0x75, 0x6C, 0xA7, 0x67, 0x65, 0x6F, 0x63, 0x6F, 0x64, 0x65, 0xA2, 0x4B, 0x52, 0xA7, 0x63, 0x6F, 0x6D, 0x6D, 0x61, 0x6E, 0x64, 0xA3, 0x31, 0x0A, 0x31, 0xAA, 0x40, 0x65, 0x78, 0x65, 0x63, 0x5F, 0x74, 0x69, 0x6D, 0x65, 0xCB, 0x3F, 0x65, 0x63, 0x00, 0x4C, 0xCE, 0x5C, 0x3B, 0xAC, 0x40, 0x75, 0x73, 0x65, 0x64, 0x5F, 0x6D, 0x65, 0x6D, 0x6F, 0x72, 0x79, 0xA5, 0x37, 0x2E, 0x32, 0x37, 0x4D, 0xA2, 0x21, 0x21, 0xB6, 0x54, 0x48, 0x49, 0x53, 0x20, 0x49, 0x53, 0x20, 0x44, 0x45, 0x42, 0x55, 0x47, 0x20, 0x4D, 0x4F, 0x44, 0x45, 0x20, 0x3A, 0x21, 0x21
  };

  /*
  //1차원 배열예제
  uint8_t packed_data[] = {
    0x87, 0xA6, 0x6F, 0x75, 0x74, 0x70, 0x75, 0x74, 0xB0, 0xEC, 0x8B, 0x9C, 0xEB, 0xA6, 0xAC, 0xEC, 0x96, 0xBC, 0x20, 0xEC, 0xB6, 0x9C, 0xEB, 0xA0, 0xA5, 0xAA, 0x69, 0x73, 0x5F, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0xC3, 0xA8, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0xA9, 0x2F, 0x77, 0x73, 0x2F, 0x74, 0x65, 0x73, 0x74, 0x31, 0xB2, 0x5F, 0x5F, 0x43, 0x4F, 0x52, 0x45, 0x5F, 0x45, 0x58, 0x45, 0x43, 0x5F, 0x54, 0x59, 0x50, 0x45, 0x5F, 0x5F, 0xA2, 0x57, 0x53, 0xA8, 0x74, 0x69, 0x6D, 0x65, 0x7A, 0x6F, 0x6E, 0x65, 0xAA, 0x41, 0x73, 0x69, 0x61, 0x2F, 0x53, 0x65, 0x6F, 0x75, 0x6C, 0xA7, 0x67, 0x65, 0x6F, 0x63, 0x6F, 0x64, 0x65, 0xA2, 0x4B, 0x52, 0xA7, 0x63, 0x6F, 0x6D, 0x6D, 0x61, 0x6E, 0x64, 0xA3, 0x31, 0x0A, 0x30
  };
  */

  Serial.print("is_success: ");
  Serial.println(msgpack_getdata(packed_data, "is_success"));

  Serial.print("output: ");
  Serial.println(msgpack_getdata(packed_data, "output"));

  Serial.print("redirect: ");
  Serial.println(msgpack_getdata(packed_data, "redirect"));

  Serial.print("__CORE_EXEC_TYPE__: ");
  Serial.println(msgpack_getdata(packed_data, "__CORE_EXEC_TYPE__"));

  Serial.print("@core_time: ");
  Serial.println(msgpack_getdata(packed_data, "@core_time"));

  Serial.print("@query_time: ");
  Serial.println(msgpack_getdata(packed_data, "@query_time"));

  Serial.print("@sended_data: ");
  Serial.println(msgpack_getdata(packed_data, "@sended_data"));

  Serial.print("timezone: ");
  Serial.println(msgpack_getdata(packed_data, "timezone"));

  Serial.print("geocode: ");
  Serial.println(msgpack_getdata(packed_data, "geocode"));

  Serial.print("command: ");
  Serial.println(msgpack_getdata(packed_data, "command"));

  Serial.print("@exec_time: ");
  Serial.println(msgpack_getdata(packed_data, "@exec_time"));

  Serial.print("@used_memory: ");
  Serial.println(msgpack_getdata(packed_data, "@used_memory"));

  Serial.print("!!: ");
  Serial.println(msgpack_getdata(packed_data, "!!"));
  
}

void loop() {
}

 

unpack을 하는 방법도 심플하게 구성했다.

단순히 msgpack_getdata("uint8_t의 MsgPack HEX코드", "보고 싶은 키"); 를 실행하면 Value값을 반환해준다.

 

실행결과!!!!

 

//원래의 배열형태
{
  "output": "시리얼 출력",
  "is_success": true,
  "redirect": "/ws/test1",
  "__CORE_EXEC_TYPE__": "WS",
  "@core_time": [
    [
      "websocket message rx start",
      0.0000178814
    ],
    [
      "handshake check",
      0.0001370907
    ],
    [
      "websocket tx send",
      0.0001060963
    ]
  ],
  "@query_time": [],
  "@sended_data": {
    "key1": "value1",
    "key2": "value2"
  },
  "timezone": "Asia/Seoul",
  "geocode": "KR",
  "command": "1\n1",
  "@exec_time": 0.0002610684,
  "@used_memory": "7.27M",
  "!!": "THIS IS DEBUG MODE :!!"
}

 

 

 

이렇게 자체적으로 개발한 함수에도 한가지 이슈가 있는데,

다차원 배열의 경우, 단일 String key가 아니라서 상위 Key를 함수에 넣으면 하위 배열을 JSON형태로 반환하도록 하였다. 

이 경우, 이미 Arduino의 JSON라이브러리가 아주 잘 되어 있음으로 이를 활용하여 쉽게 해결이 가능하다 :)

 

 

이 포스팅을 통해 Arduino뿐만 아니라 C Language처럼 저 수준의 언어에서 MessagePack을 사용할때 

나처럼 골머리 싸매고 있지 않길....

 

 

 

 


👇👇 시뮬레이션을 직접 구동할 수 있도록 원본 코드도 함께 공유한다. 👇👇

VScode + Platform.IO + Wokwi Simulator (ESP32)

esp32 msgpack test.zip
5.76MB

Comments