MacOS에 ROS 2.0 설치하기(Eloquent Elusor)

|

기본 요소 설치

brew install python3
brew install asio tinyxml2
brew install tinyxml eigen pcre poco

brew install qt freetype assimp
brew install console_bridge
brew install log4cxx
brew install cunit

brew install graphviz

python3 -m pip install pygraphviz pydot
python3 -m pip install lxml
python3 -m pip install catkin_pkg cryptography empy ifcfg lark-parser lxml netifaces numpy pyparsing pyyaml setuptools argcomplete


ROS2 바이너리 다운로드

ROS 2.0 바이너리는 여기에서 다운로드 가능합니다.

mkdir -p ~/ros2_eloquent
cd ~/ros2_eloquent

tar xf ros2-eloquent-20200124-macos-amd64.tar.bz2


설치 확인

. ~/ros2_eloquent/ros2-osx/local_setup.zsh
ros2 run demo_nodes_cpp talker
. ~/ros2_eloquent/ros2-osx/local_setup.zsh
ros2 run demo_nodes_py listener


에러 발생시

Poco 버전 관련 에러

현재 MacOS(Catalina 버전 이후)에서 ROS 2.0 바이너리를 설치한 후 talker, listener와 같은 예제 명령어를 실행하면 아래와 같은 오류 메시지가 나오면서 실행이 되지 않습니다. Poco 라이브러리 버전 때문에 발생하는 문제입니다.

Failed to load entry point 'launch': dlopen(/Users/justinmarple/ros2_eloquent/ros2-osx/lib/python3.7/site-packages/rclpy/_rclpy.cpython-37m-darwin.so, 2): Library not loaded: /usr/local/opt/poco/lib/libPocoFoundation.63.dylib
  Referenced from: /Users/justinmarple/ros2_eloquent/ros2-osx/lib/librosidl_typesupport_c.dylib
  Reason: image not found
The C extension '/Users/justinmarple/ros2_eloquent/ros2-osx/lib/python3.7/site-packages/rclpy/_rclpy.cpython-37m-darwin.so' failed to be imported while being present on the system. Please refer to 'https://index.ros.org/doc/ros2/Troubleshooting/#import-failing-even-with-library-present-on-the-system' for possible solutions
Failed to load entry point 'info': dlopen(/Users/justinmarple/ros2_eloquent/ros2-osx/lib/python3.7/site-packages/rclpy/_rclpy.cpython-37m-darwin.so, 2): Library not loaded: /usr/local/opt/poco/lib/libPocoFoundation.63.dylib
  Referenced from: /Users/justinmarple/ros2_eloquent/ros2-osx/lib/librosidl_typesupport_c.dylib
  Reason: image not found

이 경우는 ROS 2.0 을 소스로 다운받아서 빌드 후 설치하거나 아래 명령어로 임시로 실행되도록 헐 수 있습니다.

ln -s /usr/local/opt/poco/lib/libPocoFoundation.71.dylib /usr/local/opt/poco/lib/libPocoFoundation.63.dylib

sudo apt-get install gawk wget git-core diffstat unzip texinfo gcc-multilib build-essential chrpath socat libsdl1.2-dev xterm


TinyXml2 버전 관련 에러

dyld: Library not loaded: /usr/local/opt/tinyxml2/lib/libtinyxml2.7.dylib
  Referenced from: /Users/snowdeer/ros2_eloquent/ros2-osx/lib/libbuiltin_interfaces__rosidl_typesupport_fastrtps_c.dylib
  Reason: image not found

만약 위와 같은 오류가 발생한다면 먼저 tinyxml2 라이브러리 버전을 최신으로 설치합니다.

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" < /dev/null 2> /dev/null

brew install tinyxml2

이 글을 포스팅하는 현 시점에는 tinyxml2 8.0이 설치가 되네요. ROS2는 7.0 이후부터 지원합니다. 하지만 바이너리 빌드시 7.0으로 링크가 되어 있기 때문에 8.0을 7.0 버전으로 사용할 수 있도록 다음 명령어를 추가로 실행합니다.

cd /usr/local/opt/tinyxml2/lib
cp libtinyxml2.8.dylib libtinyxml2.7.dylib

ROS 2.0 Eloquent 설치 방법(apt 이용)

|

Ubuntu 18.04 기준입니다. 언제부턴가 apt install 명령어를 통해 ROS 2.0 설치가 가능해져서 설치가 아주 수월해졌습니다. Dashing 버전과 설치 방법이 동일합니다.


Locale 설정

sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8


GPG Key 설치

sudo apt update && sudo apt install curl gnupg2 lsb-release
curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add -
sudo sh -c 'echo "deb [arch=amd64,arm64] http://packages.ros.org/ros2/ubuntu `lsb_release -cs` main" > /etc/apt/sources.list.d/ros2-latest.list'


ROS2 패키지 설치

sudo apt update
sudo apt install ros-eloquent-desktop


그리고 ROS2 실행 환경을 실행하려면

source /opt/ros/eloquent/setup.bash

와 같은 명령어를 실행하면 되며, .bashrc 등에 위 명령어를 추가할 수도 있습니다. 저는 ZShell을 사용 중이고 ROS_DOMAIN_ID 등을 별도로 지정하기 때문에 ~/.zshrc 파일에 아래 내용을 설정해놓고 사용합니다.

function ros2env() {
  export ROS_DOMAIN_ID=$1
  source /opt/ros/eloquent/local_setup.zsh
  echo "ROS Domain ID =" $ROS_DOMAIN_ID
}

이러면 어디서나 ros2env 110과 같은 명령어로 ROS 2.0 환경을 불러올 수 있습니다. 띄어쓰기 뒤의 파라메터는 ROS_DOMAIN_ID 값입니다.


부가적 설치

부가적으로 아래 항목들도 설치해놓으면 개발할 때 편리합니다. 특히 가장 아래 부분의 colcon 컴파일러는 꼭 설치하는 편이 좋습니다.

sudo apt update
sudo apt install -y python-rosdep
sudo rosdep init
rosdep update

sudo apt install -y libpython3-dev

sudo apt install python3-argcomplete

sudo apt install python3-colcon-common-extensions


테스트 명령어

각각 다른 터미널에서 아래 명령어를 실행합니다.

ros2 run demo_nodes_cpp talker

ros2 run demo_nodes_py listener

ROS 2.0 Service-Client 샘플 코드

|

Client

#include "rclcpp/rclcpp.hpp"
#include "snowdeer_msgs/srv/add.hpp"
#include <string>

using namespace rclcpp;
using namespace snowdeer_msgs::srv;
using namespace std;

int main(int argc, char **argv) {
  cout << "App1" << endl;
  rclcpp::init(argc, argv);

  auto node = Node::make_shared("snowdeer_app1");
  auto client = node->create_client<Add>("snowdeer_sample_channel");

  thread t1([client]() {
    while (true) {
      auto a = 0;
      auto b = 0;

      cout << "Input two number: " << endl;
      cin >> a >> b;

      auto req = make_shared<Add::Request>();
      req->a = a;
      req->b = b;

      auto tp = std::chrono::system_clock::now() + std::chrono::seconds(3);
      auto request = client->async_send_request(req);
      auto status = request.wait_until(tp);

      if (status == future_status::ready) {
        auto resp = request.get();
        cout << "Sum : " << resp->sum << endl;
      } else {
        cout << "Timeout !!!" << endl;
      }
    }
  });

  rclcpp::WallRate loop_rate(100ms);
  while (rclcpp::ok()) {
    rclcpp::spin_some(node);

    loop_rate.sleep();
  }
  rclcpp::shutdown();

  return 0;
}


Service

#include "rclcpp/rclcpp.hpp"
#include "snowdeer_msgs/srv/add.hpp"
#include <string>

using namespace rclcpp;
using namespace snowdeer_msgs::srv;
using namespace std;

int main(int argc, char **argv) {
  cout << "App3" << endl;
  rclcpp::init(argc, argv);

  auto node = Node::make_shared("snowdeer_app2");
  auto service = node->create_service<Add>("snowdeer_sample_channel", [](const shared_ptr<rmw_request_id_t> request_header,
                                                                 const shared_ptr<Add::Request> request,
                                                                 const shared_ptr<Add::Response> response) {
    cout << "Request(" << request->a << ", " << request->b << ")" << endl;
    response->sum = request->a + request->b;
    cout << "Return : " << response->sum << endl;
  });

  rclcpp::WallRate loop_rate(100ms);
  while (rclcpp::ok()) {
    rclcpp::spin_some(node);

    loop_rate.sleep();
  }
  rclcpp::shutdown();

  return 0;
}

ROS 2.0 Dashing 설치 방법(apt 이용)

|

Ubuntu 18.04 기준입니다. MacOS 에서는 ROS 2.0 바이너리 설치는 잘 안되고 소스 빌드를 해야 되더군요. Ubuntu 19.04에서도 아래 내용으로 설치는 어렵습니다.


Locale 설정

sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8


GPG Key 설치

sudo apt update && sudo apt install curl gnupg2 lsb-release
curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add -
sudo sh -c 'echo "deb [arch=amd64,arm64] http://packages.ros.org/ros2/ubuntu `lsb_release -cs` main" > /etc/apt/sources.list.d/ros2-latest.list'


ROS2 패키지 설치

sudo apt update
sudo apt install ros-dashing-desktop
sudo apt install ros-dashing-ros-base


그리고 ROS2 실행 환경을 실행하려면

source /opt/ros/dashing/setup.bash

와 같은 명령어를 실행하면 되며, .bashrc 등에 위 명령어를 추가할 수도 있습니다. 저는 ZShell을 사용 중이고 ROS_DOMAIN_ID 등을 별도로 지정하기 때문에 ~/.zshrc 파일에 아래 내용을 설정해놓고 사용합니다.

function ros2env() {
  export ROS_DOMAIN_ID=$1
  source /opt/ros/dashing/local_setup.zsh
  echo "ROS Domain ID =" $ROS_DOMAIN_ID
}

이러면 어디서나 ros2env 110과 같은 명령어로 ROS 2.0 환경을 불러올 수 있습니다. 띄어쓰기 뒤의 파라메터는 ROS_DOMAIN_ID 값입니다.


부가적 설치

부가적으로 아래 항목들도 설치해놓으면 개발할 때 편리합니다. 특히 가장 아래 부분의 colcon 컴파일러는 꼭 설치하는 편이 좋습니다.

sudo apt update
sudo apt install -y python-rosdep
sudo rosdep init
rosdep update

sudo apt install -y libpython3-dev

sudo apt install python3-argcomplete

sudo apt install python3-colcon-common-extensions


테스트 명령어

각각 다른 터미널에서 아래 명령어를 실행합니다.

ros2 run demo_nodes_cpp talker

ros2 run demo_nodes_py listener

Android에서 Ktor을 이용한 웹 서버 구현하기

|

Ktor 라이브러리

ktor 라이브러리를 사용하기 위해서 먼저 build.gradle에 다음과 같은 설정을 해줍니다.


build.gradle(프로젝트)

ktor 라이브러리 버전을 너무 최신으로 했더니 일부 호환되지 않는 라이브러리들이 있어서 여기서는 버전을 1.2.5로 지정했습니다.

buildscript {
    ext.kotlin_version = '1.3.50'
    ext.ktor_version = '1.2.5'

    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven { url "https://dl.bintray.com/kotlin/ktor" }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}


build.gradle(모듈)

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.snowdeer.webserver"
        minSdkVersion 27
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    packagingOptions {
        exclude 'META-INF/*'
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

    implementation "io.ktor:ktor-server-netty:1.2.5"
    implementation "ch.qos.logback:logback-classic:1.2.3"

    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.core:core-ktx:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'

    androidTestImplementation 'androidx.test:runner:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}


Main.kt

package com.snowdeer.webserver

import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Environment
import android.os.Environment.DIRECTORY_DOCUMENTS
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity


class MainActivity : AppCompatActivity() {

    companion object {
        private const val PERMISSION_REQUEST_CODE = 100
    }

    private val assetInstaller = AssetInstaller()
    private val router = Router()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Log.i("snowdeer", "[snowdeer] onCreate()")

        requestPermission()

    }

    private fun requestPermission() {
        if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
            requestPermissions(permissions, PERMISSION_REQUEST_CODE)
        } else {
            start()
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        when (requestCode) {
            PERMISSION_REQUEST_CODE -> if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                Toast.makeText(applicationContext, "Permission 완료", Toast.LENGTH_SHORT).show()
                start()
            } else {
                Toast.makeText(applicationContext, "Permission 실패", Toast.LENGTH_SHORT).show()
            }
        }
    }

    private fun start() {
        val parent = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOCUMENTS)

        val staticContentDirectory = "${parent.absolutePath}/static"
        assetInstaller.install(this, "static", staticContentDirectory)

        router.startServer(this, staticContentDirectory, "/static")
    }


}


AssetInstaller.kt

assets 디렉토리에 있는 파일을 단말내 저장소에 복사하는 용도의 클래스입니다.

package com.snowdeer.webserver

import android.content.Context
import android.content.res.AssetManager
import android.util.Log
import java.io.File
import java.io.FileOutputStream

class AssetInstaller {

    fun install(ctx: Context, fromDirectory: String, targetDirectory: String) {
        ctx?.assets?.let {
            recursiveCopy(it, fromDirectory, targetDirectory)
        }
    }

    private fun recursiveCopy(am: AssetManager, src: String, target: String) {
        val files = am.list(src)
        files?.let {
            for (filename in it) {
                val filepath = "$src/$filename"
                val targetPath = "$target/$filename"

                if ((am.list(filepath) == null) || (am.list(filepath)?.size == 0)) {
                    Log.i("snowdeer", "[snowdeer] copy $filepath to $targetPath")
                    copyFile(am, filepath, targetPath)

                } else {
                    Log.i("snowdeer", "[snowdeer] $filepath is a directory.")
                    createDirectory(targetPath)

                    recursiveCopy(am, filepath, targetPath)
                }
            }
        }
    }

    private fun createDirectory(path: String) {
        val directory = File(path)
        if (!directory.exists()) {
            directory.mkdirs()
        }
    }

    private fun copyFile(am: AssetManager, src: String, target: String) {
        val file = File(target)
        if (file.exists()) {
            file.delete()
        }

        val inputStream = am.open(src)
        val outputStream = FileOutputStream(target)

        var read: Int
        val buffer = ByteArray(1024)

        while (true) {
            read = inputStream?.read(buffer)
            if (read == -1) {
                break
            }
            outputStream.write(buffer, 0, read)
        }

        inputStream?.close()
        outputStream.flush()
        outputStream.close()
    }
}


Router.kt

package com.snowdeer.webserver

import android.content.Context
import android.util.Log
import android.widget.Toast
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.CORS
import io.ktor.http.HttpMethod
import io.ktor.response.respondFile
import io.ktor.routing.Routing
import io.ktor.routing.get
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import java.io.File

class Router {

    fun startServer(ctx: Context, staticContentDirectory: String, staticContentURL: String) {
        Log.i("snowdeer", "[snowdeer] startServer()")
        Toast.makeText(ctx, "startServer()", Toast.LENGTH_SHORT).show()

        val server = embeddedServer(Netty, port = 8080) {
            installCors(this)

            routing {
                staticContentDirectory?.let {
                    addStaticContentRoute(this, staticContentDirectory, staticContentDirectory, staticContentURL)
                }

                get("/react") {
                    Log.i("snowdeer", "[snowdeer] /react is called.")
                    val file = File("${staticContentDirectory}/web/react.html")
                    call.respondFile(file)
                }

                get("/tetris") {
                    val file = File("${staticContentDirectory}/web/tetris.html")
                    call.respondFile(file)
                }

                get("/maker") {
                    val file = File("${staticContentDirectory}/web/bt_maker.html")
                    call.respondFile(file)
                }

                get("/tree") {
                    val file = File("${staticContentDirectory}/tree/MoveToPoint")
                    call.respondFile(file)
                }
            }
        }
        server.start()
    }

    private fun installCors(server:Application) {
        server.install(CORS) {
            method(HttpMethod.Options)
            method(HttpMethod.Get)
            method(HttpMethod.Post)
            method(HttpMethod.Put)
            method(HttpMethod.Delete)
            method(HttpMethod.Patch)
            anyHost()
        }
    }

    private fun addStaticContentRoute(routing: Routing, path: String, parentPath: String, url: String) {
        val directory = File(path)
        val files = directory.listFiles()

        for (f in files) {
            if (f.isDirectory) {
                val routeUrl = url + f.absolutePath.substring(parentPath.length) + "/{filename}"
                Log.i("snowdeer", "[snowdeer] routeUrl($routeUrl)")

                routing.get(routeUrl) {
                    val fileName = call.parameters["filename"]
                    val file = File("${f.absolutePath}/$fileName")
                    call.respondFile(file)
                }

                addStaticContentRoute(routing, f.absolutePath, parentPath, url)
            }
        }

    }
}