08
27

๋จผ์ € ์‚ฌ์šฉ๋˜๋Š” ๊ธฐ์ˆ  ๋ฐฐ๊ฒฝ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • Window10
  • VirtualBox 7.x.x
  • Ubuntu 18.04(on VirtualBox)
  • ROS Melodic
  • Android Studio (๋…ธํŠธ๋ถ์˜ ๋ชจ๋ฐ”์ผ ํ•ซ์ŠคํŒŸ์— ๋„คํŠธ์›Œํฌ๊ฐ€ ์—ฐ๊ฒฐ๋œ ์ƒํƒœ๋‹ค)
  • Galaxy S20FE

์ด์ „์— rosjava, rosandroid๋ฅผ ์‚ฌ์šฉํ•ด์„œ ROS์™€์˜ ํ†ต์‹ ์„ ๊ตฌ์ถ•ํ•ด๋ณด๋ ค๊ณ ํ–ˆ๋Š”๋ฐ ํŠน์ • ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ maven์—์„œ ๋‚ ๋ผ๊ฐ„ ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค. ์ •ํ™•ํžˆ๋Š” `org.ros.message.MessageListener` ๊ฐ€ ์•ˆ๋ผ์„œ Subscriber๋ฅผ ๋™์ž‘์‹œํ‚ฌ ์ˆ˜๊ฐ€ ์—†์—ˆ๋‹ค. 

 repositories {
    google {
        content {
            includeGroupByRegex("com\\.android.*")
            includeGroupByRegex("com\\.google.*")
            includeGroupByRegex("androidx.*")
        }
    }
    maven { url = uri("https://github.com/rosjava/rosjava_mvn_repo/raw/master") }
    mavenCentral()
    gradlePluginPortal()
}

์ด๋ ‡๊ฒŒ ๊นŒ์ง€ ํ•ด์„œ ๋ฒ„์ „ ํ•˜๋‚˜ํ•˜๋‚˜ ๋ฐ”๊ฟ”๊ฐ€๋ฉฐ ๋ฆฌ์†Œ์Šค๋ฅผ ๋ฐ›์œผ๋ คํ–ˆ๋Š”๋ฐ ์‹คํŒจํ–ˆ๋‹ค..

์–ด์ฐจํ”ผ rosbridge_server๋กœ rosbridge_websocket์„ ์—ด๋ฉด ์›น์†Œ์ผ“ ์„œ๋ฒ„๊ฐ€ ์—ด๋ฆฌ๋‹ˆ๊นŒ, ์ด๊ฑธ๋กœ ์„ ํšŒํ–ˆ๋‹ค.

# WebSocketClient ๋งŒ๋“ค๊ธฐ

ROS ์ž‘์—…์„ ํ•˜๊ธฐ ์ „์— ์•ˆ๋“œ๋กœ์ด๋“œ ์ชฝ ์ฝ”๋“œ๋ฅผ ๋จผ์ € ์ž‘์„ฑํ•œ๋‹ค.

Java Native์— ๋“ค์–ด์žˆ๋Š” WebSocket์„ ์‚ฌ์šฉํ•ด๋„ ๋์ง€๋งŒ ์ตœ์ ํ™”๊ฐ€ ์ด๋ฏธ ์ž˜ ๋˜์–ด์žˆ๊ณ  stableํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ okhttp3๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

[versions]
okhttp = "4.12.0"

[libraries]
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }

์‹œ์ž‘ ์ „ ์ธํ„ฐ๋„ท ๊ถŒํ•œ ๊ผญ manifest์— ํ—ˆ์šฉํ•ด๋‘ฌ์•ผํ•œ๋‹ค.

private var webSocket: WebSocket? = null
private val client = OkHttpClient.Builder()
    .readTimeout(0, TimeUnit.MILLISECONDS)
    .build()

fun connect() {
    val request = Request.Builder().url(url).build()
    webSocket = client.newWebSocket(request, createWebSocketListener())
}

์›น์†Œ์ผ“ ๊ฐ์ฒด๋ฅผ ๋จผ์ € ๋งŒ๋“ ๋‹ค. ์›น์†Œ์ผ“ ์„œ๋ฒ„ url์„ ๋ฐ›์•„์„œ request ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ค๊ณ , websocket listener์™€ ํ•จ๊ป˜ client๋กœ ์ƒ์„ฑํ•˜๋ฉด ๋œ๋‹ค. ์ฝ๊ธฐ ํƒ€์ž„์•„์›ƒ์€ 0์œผ๋กœ ์„ค์ •ํ–ˆ๋‹ค. WebSocket ํŠน์„ฑ์ƒ ์—ด์–ด๋‘๊ณ  ๊ณ„์† ์“ธ๊ฑด๋ฐ ํƒ€์ž„์•„์›ƒ์ด ์žˆ๋‹ค๋ฉด ์‘๋‹ต์ด ์˜ค๊ธฐ์ „์— ๋Š์–ด์งˆ ๊ฒƒ์ด๋‹ค.

var onMessageReceived: ((String, String) -> Unit)? = null
var onConnectionOpened: (() -> Unit)? = null
var onConnectionClosed: ((code: Int, reason: String) -> Unit)? = null
var onConnectionFailed: ((Throwable) -> Unit)? = null

private fun createWebSocketListener() = object : WebSocketListener() {
    override fun onOpen(webSocket: WebSocket, response: Response) {
        onConnectionOpened?.invoke()
    }

    override fun onMessage(webSocket: WebSocket, text: String) {
        val json = JSONObject(text)
        val topic = json.optString("topic name")
        val message = json.optJSONObject("msg")?.toString() ?: ""
        onMessageReceived?.invoke(topic, message)
    }

    override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
        webSocket.close(CODE_EXIT, null)
    }

    override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
        onConnectionClosed?.invoke(code, reason)
    }

    override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
        onConnectionFailed?.invoke(t)
    }
}

JSON์˜ค๋ธŒ์ ํŠธ๋กœ ๊ฐ์‹ธ๋†จ๋‹ค. ros์—์„œ ์˜ค๋Š” ์‘๋‹ต ํ˜•ํƒœ๊ฐ€ ์•„๋ž˜์ฒ˜๋Ÿผ jsonํ˜•ํƒœ๋กœ ๋‚ด๋ ค์˜จ๋‹ค. onMessage๋Š” ๋กœ๊ทธ title์ด๊ณ  `{  }` ๋กœ ๊ฐ์‹ธ์ง„๊ฒŒ message๋‹ค.

onMessage: {
    "topic": "/chatter", 
    "msg": {"data": "hello SSAFY 1724739832.18"},
    "op": "publish"
}

์‘๋‹ต ์ฒ˜๋ฆฌํ•  ๋žŒ๋‹คํ•จ์ˆ˜๋ฅผ ๊ฐ์ž ์ง€์ •ํ•ด๋†จ๋‹ค. ์—ญํ• ์ด ํ•˜๋‚˜๋ผ์„œ invoke๋กœ ์‹คํ–‰ํ•ด๋„ ์•„๋ฌด ๋ฌธ์ œ ์—†๋‹ค. ๊ตฌ๋…ํ•œ ํ† ํ”ฝ์—์„œ ๋ฉ”์‹œ์ง€๊ฐ€ ๋“ค์–ด์˜ค๋ฉด onMessage๋ฉ”์„œ๋“œ์— text๊ฐ€ ๋“ค์–ด์˜จ๋‹ค.

 

์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์ด๊ธฐ ๋•Œ๋ฌธ์— ํ† ํ”ฝ์„ ๊ตฌ๋…ํ•˜๊ณ , ๋ฐœํ–‰ํ•˜๋Š” ์ฝ”๋“œ๋„ ๋‹น์—ฐํžˆ ์กด์žฌํ•œ๋‹ค. ๋ฐœํ–‰์‹œ ์ฃผ์˜ํ•  ์ ์€ JSONObject๋ฅผ rosbridge_server๊ฐ€ ๋ฐ›๊ณ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— json์œผ๋กœ ๊ฐ์‹ธ์ค˜์•ผ ์ œ๋Œ€๋กœ ๊ฐ’์ด ๋“ค์–ด๊ฐ„๋‹ค. ์ •ํ™•ํžˆ๋Š” JSON String์ด๋‹ค.

fun subscribe(topic: String) {
    val subscribeMsg = JSONObject().apply {
        put("op", "subscribe")
        put("topic", topic)
    }
    webSocket?.send(subscribeMsg.toString())
}

fun publish(topic: String, message: String) {
    val publishMsg = JSONObject().apply {
        put("op", "publish")
        put("topic", topic)
        put("msg", JSONObject(message))
    }
    webSocket?.send(publishMsg.toString())
}

์ด๋Ÿฐ ํ˜•ํƒœ์˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ”๋กœ ์†Œ์ผ“ํ†ต์‹ ์œผ๋กœ ์ „์†กํ–ˆ๋‹ค๊ณ  ๋ณด๋ฉด๋œ๋‹ค.

# MainActivity

rosClient = ROSWebSocketClient("ws://$CONNTECT_IP:9090")

๋จผ์ € ์›น์†Œ์ผ“ ์„œ๋ฒ„url์„ ๋„ฃ์–ด์„œ ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ ๋‹ค. CONNETECT_IP์˜ ์ค‘์š”์„ฑ์ด ํฌ๋‹ค...

client์—์„œ ๋žŒ๋‹คํ•จ์ˆ˜๋ฅผ ๊ตฌ์ฒดํ™” ํ•˜๊ธฐ์ „, ์šฐ๋ฆฌ๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ์˜๊ณ , ๋ฐ›๋Š”๋‹ค๋Š” ์ ์—์„œ ์ต์ˆ™ํ•œ ๊ฐœ๋…์„ ํ•˜๋‚˜ ๋– ์˜ฌ๋ฆด ์ˆ˜ ์žˆ๋‹ค. Flow์˜ emit, collect๊ฐ€ ๋”ฑ ๋งž๋Š”๋‹ค.

๋‹จ์œ„ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ ์ค‘์ด๋ผ์„œ ๋”ฐ๋กœ ViewModel๊นŒ์ง€ ์ž‘์„ฑํ•˜์ง€๋Š” ์•Š์•˜๋‹ค.

private lateinit var rosClient: ROSWebSocketClient
private val messageFlow = MutableStateFlow("") // StateFlow๋‹ˆ๊นŒ ์ดˆ๊ธฐํ™” ๊ฐ’ ํ•„์š”

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    enableEdgeToEdge()
    setContentView(R.layout.activity_main)
    ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
        val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
        v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
        insets
    }

    rosClient = ROSWebSocketClient("ws://$CONNTECT_IP:9090")

    lifecycleScope.launch {
        messageFlow.collect { message ->
            tvMessages.append(message)
        }
    }
}

์ตœ์‹  state๋งŒ ์œ ์ง€ํ•˜๊ธฐ ๋ณด๋‹ค๋Š” ์ผ๋‹จ ๋“ค์–ด์˜ค๋Š” ๊ฐ’๋“ค์„ ๋ชจ๋‘ ๋ณด๊ณ ์‹ถ์–ด์„œ collect๋กœ ํ–ˆ๋‹ค.

rosClient ๋žŒ๋‹ค ์„ค์ •ํ•ด์ฃผ๋Š” ํ•จ์ˆ˜๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค. 

private fun initROSClient() {
    rosClient.apply {
        lifecycleScope.launch(Dispatchers.IO) {
            // ๋„คํŠธ์›Œํฌ ์ž‘์—…์ด๋ผ IO์—์„œ ๋™์ž‘
            connect()
        }

        onConnectionOpened = {
            lifecycleScope.launch {
                messageFlow.emit("ROS ์—ฐ๊ฒฐ์„ฑ๊ณต")
                subscribe("/chatter")
            }
        }

        onMessageReceived = { topic, message ->
            lifecycleScope.launch {
                messageFlow.emit("ํ† ํ”ฝ: $topic, ๋ฉ”์‹œ์ง€: $message\n")
            }
        }

        onConnectionClosed = { code, reason ->
            lifecycleScope.launch {
                messageFlow.emit("์—ฐ๊ฒฐ ์ข…๋ฃŒ: $code, $reason\n")
            }
        }

        onConnectionFailed = { error ->
            lifecycleScope.launch {
                messageFlow.emit("์—ฐ๊ฒฐ ์‹คํŒจ: ${error.message}\n")
            }
        }
    }
}

์›น์†Œ์ผ“์— connect๋˜๊ณ  ๋‚˜์„œ ๋ฐ”๋กœ chatter ํ† ํ”ฝ์„ ๊ตฌ๋…ํ•œ๋‹ค. talker-listener ์˜ˆ์‹œ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ์ด๋ ‡๊ฒŒ ํ–ˆ๋‹ค.

ํ•˜๋‚˜ ๋” ์ถ”๊ฐ€ํ•ด์ค˜์•ผํ•  ๊ฒŒ ์•กํ‹ฐ๋น„ํ‹ฐ๊ฐ€ onDestroy ๋  ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•œ websocket ํ†ต์‹  ๋Š๊ธฐ๋‹ค.

override fun onDestroy() {
    super.onDestroy()
    rosClient.disconnect()
}


// ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ์—์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์ž‘์„ฑ๋จ
fun disconnect() {
    webSocket?.close(CODE_EXIT, "disconnect")
}

 

์ด๋Ÿฌ๋ฉด ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ ์ž‘์„ฑํ•ด์•ผ๋˜๋Š” ๊ฑด ๋๋‚ฌ๋‹ค. ๋ฌธ์ œ๋Š” ์ด์ œ ์–ด๋–ป๊ฒŒ ๊ฐ€์ƒ๋จธ์‹  ์œ„์˜ ROS์—์„œ ๋ณด๋‚ด๋Š” ํ† ํ”ฝ์„ ๊ตฌ๋…ํ•ด์„œ android ๊นŒ์ง€ ๊ฐ€์ ธ์˜ค๋Š” ๊ฑธ ์–ด๋–ป๊ฒŒ ํ•˜๋Š” ์ง€๊ฐ€ ๋‚จ๋Š”๋‹ค. ์ด๊ฒŒ ์ œ์ผ ์ค‘์š”ํ•˜๋‹ค. 

# ํ†ต์‹  ์„ค์ •ํ•˜๊ธฐ

๋ฒ„์ถ”์–ผ ๋ฐ•์Šค์— ์ผ๋‹จ ROS ํ†ต์‹ ์šฉ์œผ๋กœ ์‚ฌ์šฉํ•  ํ˜ธ์ŠคํŠธ ์ „์šฉ ์–ดํƒญํ„ฐ๋ฅผ ์—ด์—ˆ๋‹ค.

์šฐ๋ถ„ํˆฌ๋กœ ๋“ค์–ด๊ฐ€ ifconfig์œผ๋กœ ์ด ์–ด๋Œ‘ํ„ฐ์˜ ip๋ฅผ ํ™•์ธํ•ด์„œ ์ ์–ด๋‘”๋‹ค. rosbridge_server๋ฅผ ์—ด๊ฒŒ๋˜๋ฉด ์ด ์•„์ดํ”ผ ๋ฐ‘์—์„œ ํฌํŠธ๋ฒˆํ˜ธ 9090์œผ๋กœ ์—ด๋ฆฐ๋‹ค.

๊ทธ๋Ÿผ ์•„๊นŒ CONNECT_IP์— ์ € ๋…ธ๋ž—๊ฒŒ ๊ฐ€๋ ค๋‘” IP๋ฅผ ์ ์œผ๋ฉด ๋ ๊นŒ? ์•ˆ๋œ๋‹ค.

์ง€๊ธˆ ์•ˆ๋“œ๋กœ์ด๋“œ - ์œˆ๋„์šฐ10 - ROS on ์šฐ๋ถ„ํˆฌ ์ด๋ ‡๊ฒŒ ์—ฐ๊ฒฐ๋˜์–ด์žˆ๋Š”๋ฐ, ์ €๋ ‡๊ฒŒ ๋“ค์–ด๊ฐ€๋ฉด ์ ‘์†ํ•  ์ˆ˜๊ฐ€ ์—†๋‹ค. ์ด์ œ ์œˆ๋„์šฐ์—์„œ ์„ค์ •์„ ํ•ด์ค˜์•ผ๋œ๋‹ค.

๋ฐฉํ™”๋ฒฝ์„ ์—ด์–ด์„œ ์ธ๋ฐ”์šด๋“œ ๊ทœ์น™์œผ๋กœ ํฌํŠธ ๋“ฑ๋ก์„ ํ•ด์ค€๋‹ค. ์ด๋ ‡๊ฒŒ ์•ˆํ•˜๋ฉด ๋ณด์•ˆ๋•Œ๋ฌธ์— ํฌํŠธ์— ์ ‘๊ทผํ•˜์ง€ ๋ชปํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ๋„ ์žˆ๋‹ค.

์ด์ œ ipconfig์„ cmd์— ์จ์„œ ๋„์›Œ๋ณด์ž.

์ด๊ฒŒ ๋‚ด ๋…ธํŠธ๋ถ์˜ IP๋‹ค. ์•ˆ๋“œ๋กœ์ด๋“œ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋…ธํŠธ๋ถ์œผ๋กœ ๋“ค์–ด์™€์„œ ROS์ชฝ์œผ๋กœ ๋„˜์–ด๊ฐˆ ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ์šฐ๋ฆฌ๋Š” ์ด ์œˆ๋„์šฐ๋ฅผ ์ค‘๊ณ„๊ธฐ๋กœ ์จ์„œ, ์›น์†Œ์ผ“์„ ์—ฐ๊ฒฐํ•  ๊ฒƒ์ด๋‹ค.

 

๊ทธ๋ž˜์„œ ํฌํŠธ ํฌ์›Œ๋”ฉ์ด ํ•„์š”ํ•˜๋‹ค. ์œˆ๋„์šฐ์™€ ์•ˆ๋“œ๋กœ์ด๋“œ๊ฐ€ ํ†ต์‹ ํ•˜๋Š”๋ฐ ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ ๋ฐ”๋กœ ์ง๊ฒฐํ•˜๋ฉด ๋‹ค๋ฅธ ํฌํŠธ๋ฅผ ๋ฐ”๋ผ๋ณด๊ณ ์žˆ๋‹ค. ๋‹ค์‹œ ์œˆ๋„์šฐ cmd๋ฅผ ์—ด์–ด์„œ ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ํฌํŠธํฌ์›Œ๋”ฉ์ด ๋๋‚œ๋‹ค. IP ์ฃผ์†Œ๋“ค์€ ๋ชจ๋‘ IPv4๋‹ค.

netsh interface portproxy add v4tov4 listenport=9090 listenaddress={๋‚ด ๋…ธํŠธ๋ถ ํ•ซ์ŠคํŒŸ IP} connectpost=9090 connectaddress={ROS IP}

์ด๊ฑด ๋“ฑ๋กํ•˜๋Š” ๊ฒƒ์ด๊ณ  ์•„๋ž˜ ๋‘ ๊ฐœ๋Š” ์ œ๊ฑฐ, ์กฐํšŒ๋‹ค.

  • netsh interface portproxy delete v4tov4 listenport=9090 listenaddress={๋‚ด ๋…ธํŠธ๋ถ ํ•ซ์ŠคํŒŸ IP}
  • netsh interface portproxy show v4tov4

์ด๊ฑธ ํ•˜๊ณ  ๋‚˜๋ฉด ์œˆ๋„์šฐ๋ฅผ ๊ฑฐ์น˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ

flowchart LR
    A[์•ˆ๋“œ๋กœ์ด๋“œ] <-->B{์œˆ๋„์šฐ}
    B <--> C[ROS Ubuntu]
    A[์•ˆ๋“œ๋กœ์ด๋“œ] <-->|ํฌํŠธํฌ์›Œ๋”ฉ| C[ROS Ubuntu]

์•ˆ๋“œ๋กœ์ด๋“œ - ์œˆ๋„์šฐ - ROS ๊ณผ์ •์—์„œ ๋ฒ—์–ด๋‚˜ ์•ˆ๋“œ๋กœ์ด๋“œ - ROS๊ฐ€ ๋ฐ”๋กœ ์ง๊ฒฐ๋œ๋‹ค.

ROS์—์„œ talker ์˜ˆ์ œ๋ฅผ ์ผœ๋‘๊ณ , android์—์„œ chatter๋ฅผ ๊ตฌ๋…ํ•ด์„œ ๋‚˜์˜จ ๊ฒฐ๊ณผ๋‹ค.

 

๋„์›€์ด ๋๋‹ค๋ฉด ๋Œ“๊ธ€์ด๋‚˜ ๊ณต๊ฐ ๋ฒ„ํŠผ ํ•œ ๋ฒˆ์”ฉ ๋ˆ„๋ฅด๊ณ  ๊ฐ€์ฃผ์„ธ์š”!

 

๋ฐ˜์‘ํ˜•
COMMENT