번개애비의 라이프스톼일

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>

String msgpack_unpack(const uint8_t *data, size_t &index) {
  uint8_t firstByte = data[index++];
  if (firstByte >= 0xA0 && firstByte <= 0xBF) {
    size_t strLength = firstByte & 0x1F;
    String value = "";
    for (size_t j = 0; j < strLength; j++) {
        value += (char)data[index++];
    }
    return "\"" + value + "\""; 
  }
  else if (firstByte == 0xC3) {
    return "true";
  } else if (firstByte == 0xC2) {
    return "false";
  }
  else if (firstByte <= 0x7F) {
    return String(firstByte);
  }
  else if (firstByte == 0xCB) {
    uint64_t rawData = 0;
    for (int i = 0; i < 8; i++) {
      rawData = (rawData << 8) | data[index++];
    }
    double floatValue;
    memcpy(&floatValue, &rawData, sizeof(floatValue));
    char buffer[32];
    snprintf(buffer, sizeof(buffer), "%.10f", floatValue);
    return String(buffer);
  }
  else if (firstByte >= 0x90 && firstByte <= 0x9F) {
    size_t arraySize = firstByte & 0x0F;
    String result = "[";
    for (size_t j = 0; j < arraySize; j++) {
        if (j > 0) result += ", ";
        result += msgpack_unpack(data, index);
    }
    result += "]";
    return result;
  }
  else if (firstByte >= 0x80 && firstByte <= 0x8F) {
    size_t mapSize = firstByte & 0x0F;
    String result = "{";
    for (size_t j = 0; j < mapSize; j++) {
        String key = msgpack_unpack(data, index);
        String value = msgpack_unpack(data, index);
        if (j > 0) result += ", ";
        result += key + ": " + value;
    }
    result += "}";
    return result;
  }
  return "Error: Unsupported Type";
}

String msgpack_getdata(const uint8_t *data, String targetKey) {
  size_t index = 0;
  uint8_t firstByte = data[index++];
  if (firstByte < 0x80 || firstByte > 0x8F) {
    return "Error: Invalid MsgPack format!";
  }
  uint8_t mapSize = firstByte & 0x0F;
  for (uint8_t i = 0; i < mapSize; i++) {
    String key = msgpack_unpack(data, index);
    if (key == "\"" + targetKey + "\"") {
        return msgpack_unpack(data, index);
    } else {
        msgpack_unpack(data, index); 
    }
  }
  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