0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

KotlinでRowmaアプリを作るチュートリアル

Last updated at Posted at 2020-06-14

ROSロボットにトピックを送信するAndroidアプリを作るチュートリアルです。

通信にはRowmaというシステムを使っています。Rowmaについて詳しくは Rowma: ROSロボットネットワーク化システム に書いてます。

完成図はこちら。ロボット一覧を表示してどれかをタップしたらロボットにアクセスして/chatterにトピックを送っています。

Peek 2020-06-14 19-32.gif

準備

Android Studioのバージョン: 4.0
Kotlinのバージョン: 1.3.72

Rowma ROSパッケージを起動しておきます。

インストールはこちらのコマンドから。

python <(curl "https://raw.githubusercontent.com/rowma/rowma_ros/master/install.py" -s -N)

起動は

rosrun rowma_ros rowma

です。

新規プロジェクト作成

まずはプロジェクトを作成します。「File」->「New」-> 「New Project」で新規プロジェクトを作成。

image.png

NameにRowmaExample、Package nameにcom.rowma.rowmaexampleと入力してFinishボタンをクリック。

image.png

とりあえず実行します。エミュレーター環境が無い方はAVDマネージャーから作成します。
参考: 公式ドキュメント 仮想デバイスの作成と管理

ウィンドウ右上にある再生ボタンを押します。
image.png

エミュレーターが表示されてアプリが起動したら成功です。

image.png

rowma-kotlinをインストール

左のファイルツリーからbuild.gradle(Module: app)を開きます。

image.png

dependencies{}に以下を追加します。

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    implementation 'com.rowma.rowma-kotlin:rowma-kotlin:+' //追加部分
}

ここを変更するとAndroid Studioのコードエディター部分の上に「Sync Now」と出るのでこれをクリックします。

image.png

エラー無く成功したらインストール完了です。

Rowmaサーバーへの接続

「java」->「com.rowma.rowmaexample」->「MainActivity」を開いて編集します。

rowma変数を追加して初期化します。

MainActivity.kt
package com.rowma.rowmaexample

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.rowma.rowma_kotlin.Rowma

class MainActivity : AppCompatActivity() {
    val rowma = Rowma("https://rowma.moriokalab.com") // 追加

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

「manifests」->「AndroidManifest.xml」を開いてインターネット接続の許可を与えます。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.rowma.rowmaexample">
    <uses-permission android:name="android.permission.INTERNET"/> // ここを追加

    <application>
      (中略)
    </application>

</manifest>

rowma.currentConnectionList()を使ってロボット一覧を取得する

サーバーとの接続にはコルーチンというものを使います。コルーチンを使うためには新しくライブラリをbuild.gradleに追加します。build.graldeのdependencies{}にimplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'を追加します。

build.gradle
dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    (中略)

    // ここから3行を追加
    implementation 'com.rowma.rowma-kotlin:rowma-kotlin:+'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7'
}

追加したらSync Nowをクリックします。

次にgetCurrentRobots()を追加します。

MainActivity.kt
package com.rowma.rowmaexample

import android.os.Bundle
import android.widget.ArrayAdapter
import android.widget.ListView
import androidx.appcompat.app.AppCompatActivity
import com.rowma.rowma_kotlin.Rowma
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import org.json.JSONArray

class MainActivity : AppCompatActivity() {
    val rowma = Rowma("https://rowma.moriokalab.com")

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

    private fun getCurrentRobots() = GlobalScope.launch(Dispatchers.Main) {
        async(Dispatchers.Default) { rowma.currentConnectionList() }.await().let {
            val res = JSONArray(it.toString())
            println(res)
        }
    }
}

ロボットリスト(ListView)の追加

右のファイルリストから「res」->「layout」->「activity_main.xml」を開きます。開いたら右上にある「Design」タブをクリックします。

image.png

スクリーンが表示されたらデフォルトで配置されているコンポーネントを全て削除します。

image.png

「Palette」->「Layouts」->「LinearLayout(horizontal)」をその下の「Component Tree」にドラッグ&ドロップします。

image.png

Paletteの虫眼鏡を押してListViewを検索します。

最近はRecyclerViewを使うらしいのですがめんどくさいのでListViewでお茶を濁します。

image.png

出てきた「ListView」を同じようにドラッグ&ドロップでComponent TreeのLinearLayoutの中に落とします。

image.png

Component Treeの中のRecyclerViewをクリックして右側のAttributes内のidを入力します。
idを設定します。ここではrobot-listとします。

image.png

追加できたら再ビルドします。

ListViewにデータを表示する

上で作ったListViewの中身にデータを入れます。

MainActivity.kt
package com.rowma.rowmaexample

import android.os.Bundle
import android.widget.ArrayAdapter
import android.widget.ListView
import androidx.appcompat.app.AppCompatActivity
import com.rowma.rowma_kotlin.Rowma
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import org.json.JSONArray

class MainActivity : AppCompatActivity() {
    val rowma = Rowma("https://rowma.moriokalab.com")
    private var robots : JSONArray = JSONArray() // 追加
    private val robotUuids : ArrayList<String> = ArrayList() // 追加

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        getCurrentRobots()  // 追加
    }

    // 追加
    private fun getCurrentRobots() = GlobalScope.launch(Dispatchers.Main) {
        async(Dispatchers.Default) { rowma.currentConnectionList() }.await().let {
            robots = JSONArray(it.toString())
            for (i in 0 until robots.length()) {
                val item = robots.getJSONObject(i)
                robotUuids.add(item.getString("uuid"))
            }
            val robotListView : ListView = findViewById(R.id.robot_list)
            val arrayAdapter : ArrayAdapter<String> = ArrayAdapter(applicationContext, android.R.layout.simple_list_item_1, robotUuids)
            robotListView.adapter = arrayAdapter
        }
    }
}

再ビルドしてリストが表示されていれば成功です。

image.png

RobotActivityを追加

上でロボット一覧を取得・表示する画面を作りました。今度はロボット一覧からロボットを選択して操作を行う画面を追加します。

MainActivityを右クリックして「New」->「Activity」->「Empty Activity」をクリックします。

出てきた入力欄の「Activity Name」にRobotActivityと入力してFinishボタンを押します。

image.png

次にレイアウトを変更します。「res」->「layout」->「activity_robot.xml」を開きます。

開いたら既にあるコードを消して中身を以下に変更します。

activity_robot.xml
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".RobotActivity">

</LinearLayout>

ひとまず分かりやすくテキストを置きます。PaletteペーンからTextViewを選択して適当な場所にドラッグ&ドロップします。

image.png

idにはrobot-uuidを指定します。

image.png

テキストはセンタリングしておきます。android:textAlignment="center"<TextView>タグに追加します。

activity_robot.xml
    <TextView
        android:id="@+id/robotUuid"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="TextView"
        android:textAlignment="center" />

Warningが出ているので修正します。

image.png

Codeに移動してandroid:text="TextView"をクリックしAlt + Enterを押します。

image.png

Extract string resourceをクリックします。出てきた入力欄にResource name: robotUuid、Resource value: Robot UUIDと入力してOKボタンを押します。

image.png

これでWarningが消えました。

MainActivityからRobotActivityに遷移する

以下のキャプチャのようにリストの要素を押すとMainActivityからRobotActivityに遷移するように実装します。

Peek 2020-06-14 17-49.gif

遷移にはIntentというものを使います。MainActivityのonCreate()にIntentの処理を書きます。

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

        getCurrentRobots()

        // ここから追加
        val robotListView : ListView = findViewById(R.id.robot_list)
        robotListView.setOnItemClickListener { parent, view, position, id ->
            val intent : Intent = Intent(this, RobotActivity::class.java)
            this.startActivity(intent)
        }
    }

追加して再ビルドできたら完了です。

RobotActivityにUUIDを渡す

MainActivityのリストにあるUUIDをクリックするとそのUUIDをRobotActivityに渡したいです。

intent.putExtra()を使うとデータを遷移先のActivityに渡すことができます。

MainActivity.kt
class MainActivity : AppCompatActivity() {
    val rowma = Rowma("https://rowma.moriokalab.com")
    private var robots : JSONArray = JSONArray()
    private val robotUuids : ArrayList<String> = ArrayList()

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

        getCurrentRobots()

        val robotListView : ListView = findViewById(R.id.robot_list)
        robotListView.setOnItemClickListener { parent, view, position, id ->
            val intent = Intent(this, RobotActivity::class.java)
            intent.putExtra("ROBOT_UUID", robotUuids[position]) // 追加
            this.startActivity(intent)
        }
    }
    (中略)
}

RobotActivityでIntentのデータを受け取ってTextViewにappendします。

RobotActivity.kt
class RobotActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_robot)

        // ここから追加
        val robotUuid = intent.getStringExtra("ROBOT_UUID")
        val robotUuidTextView : TextView = findViewById(R.id.robotUuid)
        robotUuidTextView.append(robotUuid)
    }
}

ビルドしてロボットのUUIDが表示されれば成功です。

Peek 2020-06-14 18-27.gif

ロボットに接続する

ようやくロボットに対してアクションを起こします。

まずはMainActivityからRobotActivityに送信するデータをUUIDからrobot変数の中身をStringにしたJSONObjectへと置き換えます。

元々のUUIDのデータ↓
image.png

変更後のrobot変数の中身、UUIDも含んでいる↓
image.png

まずMainActivity.ktのonCreate()内を変更してタップされたロボットの情報を全部渡すようにします。

MainActivity.kt
        val robotListView : ListView = findViewById(R.id.robot_list)
        robotListView.setOnItemClickListener { parent, view, position, id ->
            val intent = Intent(this, RobotActivity::class.java)
            intent.putExtra("ROBOT", robots[position].toString()) // ここ変更
            this.startActivity(intent)
        }

RobotActivityも変更します。

RobotActivity.kt
class RobotActivity : AppCompatActivity() {
    private var robot : JSONObject = JSONObject()  // ここ変更

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

        // ここ変更
        val robotString = intent.getStringExtra("ROBOT")
        robot = JSONObject(robotString)
        val robotUuidTextView : TextView = findViewById(R.id.robotUuid)
        robotUuidTextView.append(robot.getString("uuid"))
    }
}

そして最後にロボットに接続してトピックをpublishします。

RobotActivity.kt
class RobotActivity : AppCompatActivity() {
    private var robot : JSONObject = JSONObject()
    val rowma = Rowma("https://rowma.moriokalab.com") // 追加

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

        val robotString = intent.getStringExtra("ROBOT")
        robot = JSONObject(robotString)
        val robotUuidTextView : TextView = findViewById(R.id.robotUuid)
        robotUuidTextView.append(robot.getString("uuid"))

        rowma.connect() // 追加
        val msg = JSONObject() // 追加
        msg.put("data", "test message") // 追加
        rowma.publish(robot.getString("uuid"), "/chatter", msg) // 追加
    }
}

これでタップしたROSロボットに対して/chatterトピックが送信されます。RobotActivityが表示されるたびにROSにトピックが送信されています。

Peek 2020-06-14 19-13.gif

ひとまずこれで終わり。

その他

socket failed: EPERM (Operation not permitted)でアプリが落ちる

以下のエラーが出る場合は端末を操作してアプリを一度消してから再インストールしましょう。

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.rowma.rowmaexample, PID: 15463
    java.net.SocketException: socket failed: EPERM (Operation not permitted)
0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?