在Android中使用ROS

由于ROS提供了Android的对应的开发库,我们可以方便的在Android中开发相应的ROS客户端程序。下面介绍一下在Android中使用ROS库的方法。

1. 开发环境配置

Android的开发一般使用Android Studio. 其ROS相关的配置方式可以有两种。一种是在ROS环境中使用,另一种是给普通的Android App添加上ROS的依赖库。第二种方式可以在开发机器没有安装ROS的条件下进行开发。由于我使用Windows系统开发Android,所以这里使用第二种方式。

1.创建Android App项目

首先在Android Studio中创建一个普通的Android App

设置好项目名称后点击Next

继续点击Next

0_1539307249051_27ffcaed-e645-4778-af68-15fa883817b3-image.png

选择Empty Activity后点击Next

0_1539307281775_256bf658-2614-45f1-92d1-e880741183d1-image.png

然后点击Finish

等待项目Sync完成。

2.修改build.gradle文件

项目Sync完成之后,在项目左侧的文件列表内会有两个build.gradle文件。其中一个是Project的,另一个是Module的。

首先修改Project的build.gradle文件

把文件中的

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

修改为

buildscript {
  apply from: "https://github.com/rosjava/android_core/raw/kinetic/buildscript.gradle"
}

然后在文件中添加

subprojects {
    apply plugin: 'ros-android'

    afterEvaluate { project ->
        android {
            // Exclude a few files that are duplicated across our dependencies and
            // prevent packaging Android applications.
            packagingOptions {
                exclude "META-INF/LICENSE.txt"
                exclude "META-INF/NOTICE.txt"
            }
        }
    }
}

然后修改Module的build.gradle,在dependencies 中添加ros依赖

...
dependencies {
    ...
    // You now now add any rosjava dependencies, like so:
    compile 'org.ros.android_core:android_10:[0.3,0.4)'
}
...

同时把dependencies 中的 全部implementation修改为compile。注意修改时的大小写。

把文件中的compileSdkVersion版本设置为25
targetSdkVersion也设置为25
把 com.android.support:appcompat-v7:27.1.1也修改成25的版本

最后修改完成的文件如下面所示

apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    defaultConfig {
        applicationId "org.bwbot.rostest"
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.4.0'
    compile 'com.android.support.constraint:constraint-layout:1.1.3'
    testCompile 'junit:junit:4.12'
    androidTestCompile 'com.android.support.test:runner:1.0.2'
    androidTestCompile 'com.android.support.test.espresso:espresso-core:3.0.2'
    compile 'org.ros.android_core:android_10:[0.3,0.4)'
}

3.修改AndroidManifest.xml文件

此时如果编译项目会出现下面的错误

Manifest merger failed : Attribute application@icon value=(@mipmap/ic_launcher) from AndroidManifest.xml:7:9-43
	is also present at [org.ros.android_core:android_10:0.3.3] AndroidManifest.xml:19:9-36 value=(@mipmap/icon).
	Suggestion: add 'tools:replace="android:icon"' to <application> element at AndroidManifest.xml:5:5-19:19 to override.

此时需要修改AndroidManifest.xml文件在application项目中做如下修改

<application xmlns:tools="http://schemas.android.com/tools"
        tools:replace="android:icon"
        ...

为了能够正常使用还需要给app添加网络权限。在AndroidManifest.xml文件中添加

<uses-permission android:name="android.permission.INTERNET"/>

最后的AndroidManifest.xml文件如下

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.bwbot.rostest">
    <uses-permission android:name="android.permission.INTERNET"/>
    <application
        xmlns:tools="http://schemas.android.com/tools"
        tools:replace="android:icon"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

此时项目已经可以成功编译了。

2. 写一个简单的消息发布程序

MainActivity.java内容如下

package org.bwbot.rostest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import org.ros.android.RosActivity;
import org.ros.concurrent.CancellableLoop;
import org.ros.namespace.GraphName;
import org.ros.node.ConnectedNode;
import org.ros.node.Node;
import org.ros.node.NodeConfiguration;
import org.ros.node.NodeMain;
import org.ros.node.NodeMainExecutor;
import org.ros.node.topic.Publisher;

import java.net.URI;

import std_msgs.String;

public class MainActivity extends RosActivity {

    protected MainActivity() {
        super("ros_test", "ros_test", URI.create("http://192.168.0.23:11311")); // 这里是ROS_MASTER_URI
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void init(NodeMainExecutor nodeMainExecutor) {
        NodeConfiguration nodeConfiguration = NodeConfiguration.newPublic(getRosHostname());
        nodeConfiguration.setMasterUri(getMasterUri());
        nodeMainExecutor.execute(new NodeMain() {
            @Override
            public GraphName getDefaultNodeName() {
                return GraphName.of("ros_test");
            }

            @Override
            public void onStart(ConnectedNode connectedNode) {
                final Publisher<std_msgs.String> pub =  connectedNode.newPublisher("/test", String._TYPE);
                connectedNode.executeCancellableLoop(new CancellableLoop() {
                    @Override
                    protected void loop() throws InterruptedException {
                        std_msgs.String msg = pub.newMessage();
                        msg.setData("hello world");
                        pub.publish(msg);
                        Thread.sleep(1000);
                    }
                });
            }

            @Override
            public void onShutdown(Node node) {

            }

            @Override
            public void onShutdownComplete(Node node) {

            }

            @Override
            public void onError(Node node, Throwable throwable) {

            }
        }, nodeConfiguration);
    }
}

编译后,在手机上运行App。然后再运行的ROS的主机上打印/test话题

0_1539309423828_6d76f7f8-7323-4eb0-af7f-25ecf8e92acd-image.png

可以看到消息已经成功发送出来了。

本项目已经发布到 Github

3. 注意事项

  1. compileSdkVersion 版本问题
    由于在新版本的Android中,com.android.support:appcompat-v7软件包移除了一些组件。而这些组件ROS的库使用到了。所以在Android SDK > 25的版本中无法使用Android ROS. 所以我们要在配置文件中修改SDK版本。
  2. Android模拟器网络
    Android模拟器默认是有NAT转换,所以使用虚拟机是无法访问到局域网内的ROS Master的。开发时建议使用实际的手机。

4. 如何使用自定义的消息类型

使用自己定义的消息需要首先生成消息的jar库文件,然后导入项目依赖。

下面以小强的galileo_serial_server里面的消息为例。

首先安装rosjava相关的依赖包

sudo apt-get install ros-kinetic-genjava
sudo apt-get install ros-kinetic-rosjava*

然后catkin_make具有相关消息的软件包

catkin_make -DCATKIN_WHITELIST_PACKAGES="galileo_serial_server"

可以看到其输出如下

WARNING: Package name "NLlinepatrol_planner" does not follow the naming conventions. It should start with a lower case letter and only contain lower case letters, digits, underscores, and dashes.
[ 73%] Built target galileo_serial_server_generate_messages_java
Scanning dependencies of target galileo_serial_server_node
[ 78%] Building CXX object galileo_serial_server/CMakeFiles/galileo_serial_server_node.dir/src/galileo_serial_server_node.cpp.o
[ 84%] Building CXX object galileo_serial_server/CMakeFiles/galileo_serial_server_node.dir/src/galileo_serial_server.cpp.o
[ 89%] Building CXX object galileo_serial_server/CMakeFiles/galileo_serial_server_node.dir/src/AsyncSerial.cpp.o
[ 94%] Compiling Java code for galileo_serial_server
[100%] Linking CXX executable /home/xiaoqiang/Documents/ros/devel/lib/galileo_serial_server/galileo_serial_server_node
[100%] Built target galileo_serial_server_node
warning: [options] bootstrap class path not set in conjunction with -source 1.7
1 warning
Uploading: org/ros/rosjava_messages/galileo_serial_server/1.0.0/galileo_serial_server-1.0.0.jar to repository remote at file:/home/xiaoqiang/Documents/ros/devel/share/maven/
Transferring 2K from remote
Uploaded 2K
[100%] Built target galileo_serial_server_generate_messages_java_gradle
Scanning dependencies of target galileo_serial_server_generate_messages
[100%] Built target galileo_serial_server_generate_messages

从输出中可以看出,消息的jar包已经生成到了/home/xiaoqiang/Documents/ros/devel/share/maven/文件夹中

0_1539311040223_4410be69-f46d-4f19-9622-c91dc1da7fa8-image.png

把此jar文件复制到Android项目中的 app\libs文件夹中。

右键点击app,在弹出的菜单中点击Open Module Settings

0_1539311262707_927ba11a-1a93-4994-a662-d4ff2997a67f-image.png

选择dependencies页面,然后点击右侧加号

选择jar dependencies,然后选择jar文件点击确认就可以了

0_1539311384074_13a799a9-3aff-448a-a5b4-61fad7960cf3-image.png

这样就可以在程序中使用自定义的消息了。

5. ROS Android的程序设计模式

在ROS Android库中,ROS的相关操作都是异步的(通过回调的方式),比如创建节点,发布和订阅消息。这个在使用中会比较麻烦。比如我们需要实现点击一个按钮就发布一个消息的功能。就需要把这个发布消息的程序封装成一个类,然后继承CancellableLoop。在loop中发布消息。同时提供一个发布消息的方法给别人调用。
比如下面的方法


public class GalileoCommander extends CancellableLoop {

    private Publisher<galileo_serial_server.GalileoNativeCmds> pub;
    private ConnectedNode node;
    private Queue<byte[]> cmdList;

    public GalileoCommander(ConnectedNode connectedNode){
        node = connectedNode;
        cmdList = new LinkedBlockingQueue<>(100);
        pub = connectedNode.newPublisher("/test", GalileoNativeCmds._TYPE);
    }

    public void sendCmds(byte[] cmds){
        if(!cmdList.offer(cmds)){
            cmdList.poll();
            cmdList.offer(cmds);
        }
    }

    @Override
    protected void loop() throws InterruptedException {
        byte[] cmd = cmdList.poll();
        if(cmd == null){
            Thread.sleep(1);
        }else{
            galileo_serial_server.GalileoNativeCmds galileo_cmd =  pub.newMessage();
            galileo_cmd.setLength(cmd.length);
            pub.publish(galileo_cmd);
        }
    }
}

使用时

nodeMainExecutor.execute(new NodeMain() {
    @Override
    public GraphName getDefaultNodeName() {
        return GraphName.of("test_node");
    }

    @Override
    public void onStart(ConnectedNode connectedNode) {
        galileoCommander = new GalileoCommander(connectedNode);
        connectedNode.executeCancellableLoop(galileoCommander);
    }
}, nodeConfiguration)

类似文章

发表回复