๋จผ์ ์ฌ์ฉ๋๋ ๊ธฐ์ ๋ฐฐ๊ฒฝ์ ๋ค์๊ณผ ๊ฐ๋ค.
- 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๋ฅผ ๊ตฌ๋ ํด์ ๋์จ ๊ฒฐ๊ณผ๋ค.
๋์์ด ๋๋ค๋ฉด ๋๊ธ์ด๋ ๊ณต๊ฐ ๋ฒํผ ํ ๋ฒ์ฉ ๋๋ฅด๊ณ ๊ฐ์ฃผ์ธ์!
'Android ๐ฅ๏ธ > ์ฝ์งโ๏ธ' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Streaming๊ณผ LoggingInterceptor ์๊ด๊ด๊ณ (1) | 2024.11.25 |
---|---|
MLKit ํ์ฉํ๊ธฐ - FaceMeshDetection (0) | 2024.08.26 |
Paging3 ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉํ๊ธฐ (0) | 2024.07.30 |
lifecycleScope์ viewLifecycleOwner.lifecycleScope (0) | 2024.07.27 |
RecyclerView์์ ๋น๊ธฐ๋ ํ๋ ์ฒ๋ฆฌ - edgeEffectFactory (0) | 2024.07.24 |