Rowma-KotlinでスマホアプリからROSのプログラム遠隔実行する

#Rowma-KotlinでスマホアプリからROSのプログラム遠隔実行する
_ROSロボット上で実行できるROSプログラム（rosrun,roslaunch）をスマホアプリから遠隔実行する方法です。_

ロボットとスマホアプリの通信には、Rowmaというものを用いています。Rowmaについては[Rowma：ROSロボットネットワーク化システム](https://qiita.com/asmsuechan/items/5f0fe8648ddfca1e5934)をご覧ください。

##準備
Rowma-Kotlinを用いたスマホアプリ開発としてを[Rowma-Kotlin開発](https://qiita.com/asmsuechan/items/26eadd4213d08b42063b)を参考に行います。
ですので、事前準備として[Rowma-Kotlin開発](https://qiita.com/asmsuechan/items/26eadd4213d08b42063b) の動作確認を行ってください。
動作がうまくいっていると以下のようになります。
![](https://camo.qiitausercontent.com/590cfb5c058ffdc3b3765f556d48ba9ef3834010/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3133313538312f35643339363562392d616430662d643330612d306539342d3166663461346639383438612e676966)
##ROSノードでのSubscribe（割愛してもよい）
前の章の動作確認では、スマホアプリからのテキストメッセージをrowma_rosで受け取っています。
しかし、トピック（/chatter）を受け取るROSノードがないためrowma_rosでエラーメッセージが出ています。
実際に、ROSノードで受け取れるかどうかを確認しましょう。
確認のため以下のようなROSノードを作成します。

```python:listener
import rospy
from std_msgs.msg import String

def callback(data):
    rospy.loginfo(rospy.get_caller_id() + "I heard %s", data.data)

def listener():
    rospy.init_node('listener', anonymous=True)
    rospy.Subscriber("chatter", String, callback)
    rospy.spin()

if __name__ == '__main__':
    listener()
```
このノードは、単純にROSトピック（/chatter）をSubscribeし、ターミナル上に表示するだけです。
このノードを実行することでスマホアプリからのROSトピックをROSノードで受け取れていることがわかります。

#遠隔実行アプリ
ここからが本題です。
スマホアプリから任意のROSロボットのプログラムを実行します。今回は、roslaunchコマンドを実行します。
まず完成形を見ていただきましょう。Android端末はGoogle Pixel 4aを使用しています。（Android 10）
<br>
<iframe width="560" height="315" src="https://www.youtube.com/embed/TTu1RJWscsA" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

###（操作手順）
①　スマホアプリの画面上から遠隔操作したいロボットを選択します。
②　「LAUNCH COMMANDS LIST」ボタンを押すと、①で選択したロボット上で実行できるlaunchコマンド一覧を取得。
③　実行するかどうかを確認するアラートが流れるので「上のLAUNCHコマンドを実行」を選択。
④　ロボットのrowma_rosを起動しているターミナルにて③で実行したlaunchコマンドの実行結果が表示される。
##選択したロボット上で実行できるlaunchコマンドを一覧表示する。
[Rowma-Kotlin開発](https://qiita.com/asmsuechan/items/26eadd4213d08b42063b)では、Rowmaに接続しているロボットの一覧をIDで表示し、一覧から選択するとIDが表示され、rowma_rosにテキストメッセージが送られるというものでした。
これを、変更していきます。
まず、ここではPublishはしないのでclass RobotActivity内の下の３行は削除します。

```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) // 削除
       
    }
}
```
次にlaunchコマンドの一覧を表示するきっかけとなるボタンを配置します。**activity_robot.xml**を開き、PaletteペーンからButtonを選択してすでにあるLinearLayout内にドラッグ&ドロップします。
Attribute内の**id**を適当に設定し、（今回は「LCL」にしました。※Launch Commands Listの頭文字です。）**text**を「LAUNCH COMMANDS LIST」にします。
また、後にlaunchコマンドの一覧を表示するため、PaletteペーンからListviewを選択してすでにあるLinearLayout内にドラッグ&ドロップし,Attribute内の**id**を適当に設定します。（今回は「lcl」にしました)
すると、以下のような感じになると思います。
![asas.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1743941/e7bd1e53-bccb-2b9b-e995-2067cf9762d8.png)

次に、選択したロボットで実行できるlaunchコマンドを取得します。

```kotlin
  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() 

        
        //追加
        //ROS launchで実行できるコマンドの一覧を取得
        var commandList: JSONArray = JSONArray()
        commandList = robot.getJSONArray("launchCommands")
       
        var launchcommandslist: ArrayList<String> = ArrayList();
        if (commandList != null) {
            for (i in 0 until commandList.length()) {
                launchcommandslist.add(commandList.getString(i))
            }
        }
        val robot_cmd: Button = findViewById(R.id.LCL)
        robot_cmd.setOnClickListener {
            val arrayAdapter: ArrayAdapter<String> = ArrayAdapter(
                applicationContext,
                android.R.layout.simple_list_item_1,
                launchcommandslist
            )
            val listView = findViewById(R.id.lcl) as ListView
            listView.adapter = arrayAdapter
        }

    }
}
```
少し何をやっているか解説します。

```kotlin
        var commandList: JSONArray = JSONArray()
        commandList = robot.getJSONArray("launchCommands")
```
ここでは、IDから選択したロボットの情報を取得し、その中からそのロボット上で実行できるlaunchコマンドの一覧が含まれている「launchCommands」を抜き出し、配列（JSONArray）として定義された「commandList」に格納しています。
<br>

```kotlin
        var launchcommandslist: ArrayList<String> = ArrayList();
        if (commandList != null) {
            for (i in 0 until commandList.length()) {
                launchcommandslist.add(commandList.getString(i))
            }
        }
        val robot_cmd: Button = findViewById(R.id.LCL) //LCLはButtonのid
        robot_cmd.setOnClickListener {
        val arrayAdapter: ArrayAdapter<String> = ArrayAdapter(
            applicationContext,
            android.R.layout.simple_list_item_1
            launchcommandslist
        )
        val listView = findViewById(R.id.lcl) as ListView
        listView.adapter = arrayAdapter
        }
    }
}

```
以降は、
・String型の配列として定義された「launchcommandslist」に「commandList」を格納（JSONArrayからStringへの型変換）
・「launchcommandslist」を画面上にlist表示
の2点を行っています。

##launchコマンドを遠隔実行する
次にlaunchコマンドの一覧から選択されたlaunchコマンドを遠隔実行していきます。

```kotlin
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)

        rowma.connect()

        //選択されたロボットのUUIDを表示
        val robotString = intent.getStringExtra("ROBOT")
        robot = JSONObject(robotString)
        val robotUuidTextView: TextView = findViewById(R.id.robotUuid)
        robotUuidTextView.append(robot.getString("uuid"))

        //ROS launchで実行できるコマンドの一覧を取得
        var commandList: JSONArray = JSONArray()
        var launchcommandslist: ArrayList<String> = ArrayList();
        commandList = robot.getJSONArray("launchCommands")
        println(commandList)
        if (commandList != null) {
            for (i in 0 until commandList.length()) {
                launchcommandslist.add(commandList.getString(i))
            }
        }
        val robot_cmd: Button = findViewById(R.id.LCL)
        robot_cmd.setOnClickListener {
            val arrayAdapter: ArrayAdapter<String> = ArrayAdapter(
                applicationContext,
                android.R.layout.simple_list_item_1,
                launchcommandslist
            )
            println(launchcommandslist)
            val listView = findViewById(R.id.lcl) as ListView
            listView.adapter = arrayAdapter
        }

        //追加
        //選択したコマンドを実行。実行結果は、ロボットのRowma接続画面に表示される。
        val listViewlist = findViewById(R.id.lcl) as ListView
        listViewlist.setOnItemClickListener { parent, view, position, id ->
            println(launchcommandslist[position])
            AlertDialog.Builder(this) // FragmentではActivityを取得して生成
                .setTitle("ROSプログラムの実行")
                .setMessage(launchcommandslist[position])
                .setPositiveButton("上のlaunchコマンドを実行", { dialog, which ->
                    // TODO:Yesが押された時の挙動
                    rowma.runLaunch(robot.getString("uuid"), launchcommandslist[position])
                })
                .setNegativeButton("中止", { dialog, which ->
                    // TODO:Noが押された時の挙動
                })
                .show()
        }
    }
}
 
```

追加した内容の解説を少し.........

まず、setOnItemClickListenerを用いてリストが押された際の処理を書きます。今回は、AlertDialog.Builderというものを使ってアラートダイアログを表示しています。

```kotlin
listViewlist.setOnItemClickListener { parent, view, position, id ->
          //リストが押された際の処理
        }
```
ここでは、launchコマンドのリストから任意のlaunchコマンドが選択された際にアラートダイアログを表示し、「上のlaunchコマンドを実行」が選択されると、

```kotlin
rowma.runLaunch(robot.getString("uuid"), launchcommandslist[position])
```
が実行され、「中止」が選択された場合には何も処理が行われず、アラートダイアログが消えるという仕組みになっています。

では、

```kotlin
rowma.runLaunch(robot.getString("uuid"), launchcommandslist[position])
```
では何をしているのでしょうか？

こちらはRowmaーKotlinの遠隔実行のためのクラスを使用しています。使い方はこのような感じです。

**rowma.runLaunch(　送信先のロボットのID　, 　実行したいlaunchコマンド)**

つまり今回は、”robot.getString("uuid")”：**アプリの最初の画面のロボットのID一覧から選択したロボットのID**
に対して、　　”launchcommandslist[position]”:**launchコマンド一覧から選択したlaunchコマンド**
を実行するように命令しています。

##もう一度完成形を
以上でスマホアプリからの遠隔実行は完了です。
すべてのコードを正しく書き換え、Android Studioでビルドが通れば、スマホアプリの完成です。

<iframe width="560" height="315" src="https://www.youtube.com/embed/TTu1RJWscsA" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>