콘텐츠로 건너뛰기

Python과 C++ 간단하게 데이터를 주고받는 방법 2가지 – LCM을 통한 프로그램간 통신

Python으로 프로그램밍을 하다보면 일부 C++의 장점을 이용하고 싶기도 하고, 반대로 C++로 프로그램 개발 시 Python의 기능들을 일부 사용하고 싶어질 때가 있습니다. 문제는, 두개의 다른 언어이다 보니 별도의 프로세스가 되어 추가적인 방법을 이요하지 않고는 두 프로세스가 정보를 효율적으로 주고받기 어렵다는 점 입니다.

이러한 목적으로 사용할 수 있는 알려진 라이브러리가 몇가지있는데, 이 중에서 이번에는 LCM이라는 툴에 대해 적어보고자 합니다. 이 외에도 로봇 분야에서 많이 사용하는 프레임워크 ROS도 동일한 목적으로 사용할 수 있고, 산업에서 종종 사용되는 것으로 보이는 ZeroMQ라는 툴도 있습니다.

이번 포스팅에서는 로봇공학뿐만 아니라 여러 프로그램 간의 통신에 중요한 역할을 하는 두 기술, ROS와 LCM에 대해 살펴볼 예정입니다. ROS, 즉 로봇 운영 시스템은 로봇 개발자들에게 필수적인 도구입니다. 하지만 그 사용범위는 로봇에만 국한되지 않습니다. ROS는 다양한 프로그래밍 언어를 사용하는 소프트웨어 간의 원활한 통신을 가능하게 해주는 강력한 플랫폼이죠.

또한, 여기 LCM, 즉 Lightweight Communications and Marshalling이 있습니다. LCM은 특히 실시간 시스템에서 중요한 역할을 하는데, 로봇, 차량, 센서 등 다양한 시스템 사이에서 데이터를 효율적으로 주고받을 수 있도록 해줍니다. LCM의 유연성 덕분에, 서로 다른 언어로 작성된 프로그램들 사이에서도 데이터를 손쉽게 교환할 수 있게 됩니다.

이번 포스팅에서는 ROS와 LCM이 어떻게 다양한 소프트웨어와 시스템 간의 통신을 용이하게 하는지, 그리고 이들 기술이 어떤 방식으로 우리의 작업 방식을 개선하는지에 대해 탐구해볼 것입니다. 로봇공학에 관심이 있거나, 서로 다른 프로그래밍 언어 간의 통신에 대해 알고 싶은 분들에게 이 포스팅이 도움이 되기를 바랍니다.

참고로, 동일한 용도로 ZeroMQ의 사용에 대해서는 아래 티스토리 블로그에 공유해둔 내용이 있으니 함께 참고하시면 도움이 되실 것 같습니다. 또, 혹시라도 로봇공학 관련 내용이 궁금하시면 관련된 다른 포스팅도 함께 확인하시면 좋을 것 같습니다.



python,c++,lcm

LCM(Lightweight Communications and Marshalling) 이란

LCM, 즉 Lightweight Communications and Marshalling은 분산 시스템과 로봇공학에서 데이터 통신을 위해 널리 사용되는 효율적인 메시징 시스템입니다. 이 툴은 특히 실시간 시스템, 임베디드 시스템, 그리고 복잡한 네트워크 환경에서 데이터를 빠르고 신뢰성 있게 전송하는 데 중점을 두고 개발되었습니다. LCM의 핵심적인 특징은 그 가벼움과 높은 성능입니다. 빠른 설치와 간편한 사용법으로 인해, 개발자들 사이에서 빠르게 인기를 얻고 있습니다.

LCM은 특히 다양한 하드웨어 플랫폼과 프로그래밍 언어 간의 호환성을 제공합니다. 다양한 언어로 작성된 프로그램 간의 통신을 지원함으로써, 시스템 통합을 용이하게 만들어줍니다. 데이터 포맷과 전송 프로토콜은 사용자 친화적이면서도, 복잡한 데이터 구조와 대용량 데이터 전송 요구사항을 충족시킬 수 있을 만큼 강력합니다.

LCM의 또 다른 중요한 특징은 그 확장성과 유연성입니다. 소규모 시스템에서부터 대규모 분산 시스템에 이르기까지, 다양한 규모의 프로젝트에 적용할 수 있습니다. 또한, 네트워크 효율성을 극대화하면서도, 신뢰성 있는 데이터 전송을 보장합니다. 이는 특히 네트워크 대역폭이 제한된 환경에서 중요한 요소입니다.

실제 사용 사례를 살펴보면, LCM은 자율 주행 차량, 무인 항공기, 산업용 로봇 등 다양한 분야에서 데이터 전송의 핵심 요소로 사용되고 있습니다. 예를 들어, 자율 주행 차량에서는 LCM을 사용하여 센서 데이터, 내비게이션 정보, 그리고 제어 신호를 신속하게 주고받을 수 있습니다. 이러한 빠른 데이터 교환은 차량이 실시간으로 환경 변화에 반응하고 안전한 운행 결정을 내리는 데 필수적입니다.

다른 방식과의 차이점

범용으로 많이 사용되는 ZeroMQ나 ROS에 비해 LCM이 가지는 차이점에 대해 간단히 적어보겠습니다.


ROS (Robot Operating System), ZeroMQ, 그리고 LCM (Lightweight Communications and Marshalling)은 모두 분산 시스템에서 데이터 전송을 처리하는 데 사용되는 도구들이지만, 각각의 방식에는 몇 가지 중요한 차이점이 있습니다. 아래에서 이들의 방식을 비교해보겠습니다.

ROS의 통신방식

  1. 방식: ROS는 주로 퍼블리셔/서브스크라이버 (Publisher/Subscriber) 모델과 서비스/클라이언트 (Service/Client) 모델을 사용합니다. 또한, ‘노드’라고 불리는 작은 프로세스들이 메시지를 주고받는 방식으로 동작합니다.
  2. 목적: ROS는 로봇 공학에 특화되어 있으며, 로봇의 감지, 인식, 운동 제어 등 다양한 모듈 간의 효율적인 데이터 전송을 위해 설계되었습니다.
  3. 다양한 언어 지원: 많이 사용되는 여러 프로그래밍 언어를 지원합니다.

ZeroMQ의 방식

  1. 방식: ZeroMQ는 소켓을 기반으로 한 메시징 라이브러리로, 요청/응답, 게시/구독, 파이프라인, 푸시/풀 등 다양한 패턴을 지원합니다.
  2. 저수준 컨트롤: ZeroMQ는 네트워크 연결, 메시지 큐잉, 재전송 등 네트워크 통신의 저수준 제어를 가능하게 합니다.
  3. 언어 및 플랫폼 독립성: C++, Python, Java, .NET 등 다양한 언어 및 플랫폼에서 사용 가능합니다.

LCM의 방식

  1. 방식: LCM은 경량 메시징 시스템으로, 효율적인 데이터 마샬링 및 전송에 중점을 둡니다. 주로 UDP 멀티캐스트를 사용해 빠른 데이터 전송을 제공합니다.
  2. 목적: 주로 실시간 시스템과 임베디드 시스템에서의 통신에 최적화되어 있으며, 복잡한 데이터 구조의 효율적인 처리를 지원합니다.
  3. 간편한 설정 및 사용: LCM은 설치와 사용이 비교적 간단하며, 빠른 시작과 효율적인 운영을 가능하게 합니다.

비교

  • ROS: 로봇 공학에 특화된 시스템. 퍼블리셔/서브스크라이버와 서비스/클라이언트 모델을 사용하며, 로봇 시스템 간의 복잡한 상호작용을 지원합니다.
  • ZeroMQ: 범용 메시징 라이브러리. 다양한 패턴을 지원하며, 저수준 네트워크 제어 기능을 제공합니다.
  • LCM: 경량 및 실시간 시스템에 최적화된 메시징 시스템. 데이터 마샬링과 빠른 전송에 중점을 두고 있으며, 설정과 사용이 간편합니다.

LCM을 이용한 Python과 C++ 프로그램간 통신 예제

LCM이 적합한 방식인 것으로 보인다면, 원하는 용도로 사용이 가능할 지 아래 예제를 이용해 확인해 볼 수 있을 것 같습니다.

데이터타입 만들기

우선, 데이터를 주고받을 오브젝트를 한차례 정의해야 합니다. 한 데이터타입당 한번만 하면 됩니다. 데이터타입이 정의되면 C++의 경우 header파일이, Python의 경우 오브젝트 생성에 필요한 py파일이 생성됩니다.

우선, double 데이터와 string을 주고받는 경우의 예를 적어보겠습니다. 데이터타입 정의를 위한 파일을 만들어야 하는데, double_t.lcm 이라고 이름을 정해보곘습니다. 이름은 변경할 수 있으나, 확장명은 .lcm이 되어야 합니다.

package exampel_lcm_type;

struct example_t {
    int64_t timestamp;
    double d;
    string str;
}

데이터타입을 생성하기 위해 lcm 패키지를 설치해야 합니다. Ubuntu 기준이며, 다른 OS에서는 아래 링크를 확인하시어 설치하실 수 있습니다.

sudo apt-get install liblcm-dev

다음으로, 아래 두 커맨드를 실행히켜 각 언어에 필요한 데이터타입 오브젝트를 생성할 수 있습니다.

lcm-gen -p example_t.lcm   # For Python
lcm-gen -x example_t.lcm   # For C++

Python용 generation을 실행하면, 동일한 경로 내 새로운 폴더가 생성되고, 해당 폴더 내에 생성된 파일에서 아래 내용을 확인 할 수 있습니다. 자동으로 생성된 오브젝트 파일이므로 직접 수정은 권장하지 않습니다.

"""LCM type definitions
This file automatically generated by lcm.
DO NOT MODIFY BY HAND!!!!
"""

try:
    import cStringIO.StringIO as BytesIO
except ImportError:
    from io import BytesIO
import struct

class example_t(object):
    __slots__ = ["timestamp", "d", "str"]

    def __init__(self):
        self.timestamp = 0
        self.d = 0.0
        self.str = ""

    def encode(self):
        buf = BytesIO()
        buf.write(example_t._get_packed_fingerprint())
        self._encode_one(buf)
        return buf.getvalue()

    def _encode_one(self, buf):
        buf.write(struct.pack(">qd", self.timestamp, self.d))
        __str_encoded = self.str.encode('utf-8')
        buf.write(struct.pack('>I', len(__str_encoded)+1))
        buf.write(__str_encoded)
        buf.write(b"\0")

    def decode(data):
        if hasattr(data, 'read'):
            buf = data
        else:
            buf = BytesIO(data)
        if buf.read(8) != example_t._get_packed_fingerprint():
            raise ValueError("Decode error")
        return example_t._decode_one(buf)
    decode = staticmethod(decode)

    def _decode_one(buf):
        self = example_t()
        self.timestamp, self.d = struct.unpack(">qd", buf.read(16))
        __str_len = struct.unpack('>I', buf.read(4))[0]
        self.str = buf.read(__str_len)[:-1].decode('utf-8', 'replace')
        return self
    _decode_one = staticmethod(_decode_one)

    _hash = None
    def _get_hash_recursive(parents):
        if example_t in parents: return 0
        tmphash = (0x401f3bf5ab827f14) & 0xffffffffffffffff
        tmphash  = (((tmphash<<1)&0xffffffffffffffff)  + (tmphash>>63)) & 0xffffffffffffffff
        return tmphash
    _get_hash_recursive = staticmethod(_get_hash_recursive)
    _packed_fingerprint = None

    def _get_packed_fingerprint():
        if example_t._packed_fingerprint is None:
            example_t._packed_fingerprint = struct.pack(">Q", example_t._get_hash_recursive([]))
        return example_t._packed_fingerprint
    _get_packed_fingerprint = staticmethod(_get_packed_fingerprint)

다음으로, C++ 오브젝트 생성을 실행하면 아래와 같은 파일이 생성되는 것을 확인할 수 있습니다.

/** THIS IS AN AUTOMATICALLY GENERATED FILE.  DO NOT MODIFY
 * BY HAND!!
 *
 * Generated by lcm-gen
 **/

#include <lcm/lcm_coretypes.h>

#ifndef __exampel_lcm_type_example_t_hpp__
#define __exampel_lcm_type_example_t_hpp__

#include <string>

namespace exampel_lcm_type
{

class example_t
{
    public:
        int64_t    timestamp;

        double     d;

        std::string str;

    public:
        /**
         * Encode a message into binary form.
         *
         * @param buf The output buffer.
         * @param offset Encoding starts at thie byte offset into @p buf.
         * @param maxlen Maximum number of bytes to write.  This should generally be
         *  equal to getEncodedSize().
         * @return The number of bytes encoded, or <0 on error.
         */
        inline int encode(void *buf, int offset, int maxlen) const;

        /**
         * Check how many bytes are required to encode this message.
         */
        inline int getEncodedSize() const;

        /**
         * Decode a message from binary form into this instance.
         *
         * @param buf The buffer containing the encoded message.
         * @param offset The byte offset into @p buf where the encoded message starts.
         * @param maxlen The maximum number of bytes to reqad while decoding.
         * @return The number of bytes decoded, or <0 if an error occured.
         */
        inline int decode(const void *buf, int offset, int maxlen);

        /**
         * Retrieve the 64-bit fingerprint identifying the structure of the message.
         * Note that the fingerprint is the same for all instances of the same
         * message type, and is a fingerprint on the message type definition, not on
         * the message contents.
         */
        inline static int64_t getHash();

        /**
         * Returns "example_t"
         */
        inline static const char* getTypeName();

        // LCM support functions. Users should not call these
        inline int _encodeNoHash(void *buf, int offset, int maxlen) const;
        inline int _getEncodedSizeNoHash() const;
        inline int _decodeNoHash(const void *buf, int offset, int maxlen);
        inline static uint64_t _computeHash(const __lcm_hash_ptr *p);
};

int example_t::encode(void *buf, int offset, int maxlen) const
{
    int pos = 0, tlen;
    int64_t hash = (int64_t)getHash();

    tlen = __int64_t_encode_array(buf, offset + pos, maxlen - pos, &hash, 1);
    if(tlen < 0) return tlen; else pos += tlen;

    tlen = this->_encodeNoHash(buf, offset + pos, maxlen - pos);
    if (tlen < 0) return tlen; else pos += tlen;

    return pos;
}

int example_t::decode(const void *buf, int offset, int maxlen)
{
    int pos = 0, thislen;

    int64_t msg_hash;
    thislen = __int64_t_decode_array(buf, offset + pos, maxlen - pos, &msg_hash, 1);
    if (thislen < 0) return thislen; else pos += thislen;
    if (msg_hash != getHash()) return -1;

    thislen = this->_decodeNoHash(buf, offset + pos, maxlen - pos);
    if (thislen < 0) return thislen; else pos += thislen;

    return pos;
}

int example_t::getEncodedSize() const
{
    return 8 + _getEncodedSizeNoHash();
}

int64_t example_t::getHash()
{
    static int64_t hash = _computeHash(NULL);
    return hash;
}

const char* example_t::getTypeName()
{
    return "example_t";
}

int example_t::_encodeNoHash(void *buf, int offset, int maxlen) const
{
    int pos = 0, tlen;

    tlen = __int64_t_encode_array(buf, offset + pos, maxlen - pos, &this->timestamp, 1);
    if(tlen < 0) return tlen; else pos += tlen;

    tlen = __double_encode_array(buf, offset + pos, maxlen - pos, &this->d, 1);
    if(tlen < 0) return tlen; else pos += tlen;

    char* str_cstr = (char*) this->str.c_str();
    tlen = __string_encode_array(buf, offset + pos, maxlen - pos, &str_cstr, 1);
    if(tlen < 0) return tlen; else pos += tlen;

    return pos;
}

int example_t::_decodeNoHash(const void *buf, int offset, int maxlen)
{
    int pos = 0, tlen;

    tlen = __int64_t_decode_array(buf, offset + pos, maxlen - pos, &this->timestamp, 1);
    if(tlen < 0) return tlen; else pos += tlen;

    tlen = __double_decode_array(buf, offset + pos, maxlen - pos, &this->d, 1);
    if(tlen < 0) return tlen; else pos += tlen;

    int32_t __str_len__;
    tlen = __int32_t_decode_array(buf, offset + pos, maxlen - pos, &__str_len__, 1);
    if(tlen < 0) return tlen; else pos += tlen;
    if(__str_len__ > maxlen - pos) return -1;
    this->str.assign(((const char*)buf) + offset + pos, __str_len__ - 1);
    pos += __str_len__;

    return pos;
}

int example_t::_getEncodedSizeNoHash() const
{
    int enc_size = 0;
    enc_size += __int64_t_encoded_array_size(NULL, 1);
    enc_size += __double_encoded_array_size(NULL, 1);
    enc_size += this->str.size() + 4 + 1;
    return enc_size;
}

uint64_t example_t::_computeHash(const __lcm_hash_ptr *)
{
    uint64_t hash = 0x401f3bf5ab827f14LL;
    return (hash<<1) + ((hash>>63)&1);
}

}

#endif

Python 서버 작성 및 실행

Python Server

위 데이터타입을 사용하는 데이터 전송의 주체가 되는 서버를 Python으로 만들어 보겠습니다.

import lcm
from example_lcm_type import example_t
import time

def main():
    lc = lcm.LCM()
    msg = example_t()
    msg.timestamp = int(time.time() * 1e6)
    msg.d = 11.2
    msg.str = "hi"

    while True:
        lc.publish("EXAMPLE_CHANNEL", msg.encode())
        time.sleep(1)

if __name__ == "__main__":
    main()

C++ Client

다음으로, 같은 데이터타입을 사용하는 C++ 기반의 Client 프로그램을 만들어 보겠습니다. 파일명은 cpp_lcm_client.cpp로 하겠습니다.

#include <lcm/lcm-cpp.hpp>
#include "example_lcm_type/example_t.hpp"
#include <iostream>

class Handler {
public:
    void handleMessage(const lcm::ReceiveBuffer* rbuf,
                       const std::string& chan, 
                       const example_lcm_type::example_t* msg) {
        std::cout << "Received message on channel " << chan << std::endl;
        std::cout << "  timestamp = " << msg->timestamp << std::endl;
        std::cout << "  d      = " << msg->d << std::endl;
        std::cout << "  str      = " << msg->str << std::endl;
    }
};

int main(int argc, char** argv) {
    lcm::LCM lcm;
    if (!lcm.good()) return 1;
    Handler handlerObject;
    lcm.subscribe("EXAMPLE_CHANNEL", &Handler::handleMessage, &handlerObject);

    while (0 == lcm.handle());
    return 0;
}

저장 후 아래와 같이 컴파일 할 수 있습니다. 필요하다면 CMake를 이용하는 것도 가능합니다.

g++ -o cpp_lcm_client cpp_lcm_client.cpp -llcm

CMake를 이용하려면 아래와 같은 형식을 사용하시면 됩니다.

cmake_minimum_required(VERSION 3.0)
project(LcmClient)

# Replace this with the path to your LCM installation
set(LCM_DIR "/path/to/lcm")

find_package(LCM REQUIRED)

include_directories(${LCM_INCLUDE_DIRS})
add_executable(lcm_client cpp_lcm_client.cpp)
target_link_libraries(lcm_client ${LCM_LIBRARIES})

실행하기

우선, Python 서버를 실행하면 주기적으로 message를 발생시키게 되고, C++ client를 실행하면 아래와 같이 해당 정보를 받아 확인할 수 있습니다.

./cpp_lcm_client 

Received message on channel EXAMPLE_CHANNEL
  timestamp = 1703743611030890
  d      = 11.2
  str      = hi
Received message on channel EXAMPLE_CHANNEL
  timestamp = 1703743611030890
  d      = 11.2
  str      = hi

위와 같이 특정 채널을 통해 임의로 생성한 데이터가 Python에서 C++ 프로그램으로 전송되는 것을 확인할 수 있습니다.

마무리

이상으로, ROS, ZeroMQ, 그리고 LCM의 기본적인 개념과 차이점을 살펴보았습니다. 각각의 시스템이 제공하는 독특한 특징들이 어떻게 분산 시스템의 효율적인 데이터 전송을 가능하게 하는지 이해하는 것은 매우 중요합니다. ROS는 로봇 공학에 최적화된 데이터 전송과 풍부한 라이브러리를 제공하며, ZeroMQ는 다양한 네트워크 통신 패턴을 지원하는 범용 메시징 라이브러리로서의 역할을 합니다. 한편, LCM은 실시간 및 임베디드 시스템을 위한 경량화된 메시징 시스템으로, 빠르고 효율적인 데이터 전송을 제공합니다.

프로젝트의 요구사항과 환경에 따라 적합한 도구를 선택하는 것이 중요합니다. 예를 들어, 로봇 공학이나 복잡한 시스템을 다루고 있다면 ROS가 적합할 수 있으며, 다양한 종류의 통신 패턴과 네트워크 동작을 필요로 하는 경우 ZeroMQ를 고려해볼 수 있습니다. 또한, 실시간 처리와 효율성이 중요한 임베디드 시스템에서는 LCM이 유용하게 사용될 수 있습니다.

이 글을 통해 각 도구의 기본적인 이해와 적용 가능성에 대해 알아보았습니다. 앞으로의 프로젝트에서 이러한 지식이 여러분의 시스템 설계와 구현에 도움이 되길 바랍니다.

태그:

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다