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)
            }
        }

    }
}

Kotlin Ktor과 React 같이 사용하기

|

React 다운로드

먼저 React를 사용하기 위해서 react.jsreact-dom.js 파일을 다운로드합니다.

여기에서 다운로드 가능하며, 저는 min 버전으로 다운로드했습니다.


react.html

<html>
<head>
    <script src="/static/web/react/react.js"></script>
    <script src="/static/web/react/react-dom.js"></script>
</head>

<body>
<div id="content">hello, react</div>
<script type="module">
    import { init } from '/static/web/react_test.js';

    init();
</script>

</body>
</html>


react_test.js

export function init() {
    console.log("init()");

    var h1=React.createElement('h1', null, 'Hello, React')
    ReactDOM.render(
        h1,
        document.getElementById('content')
    )
}