在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
选择Empty Activity后点击Next
然后点击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
话题
可以看到消息已经成功发送出来了。
本项目已经发布到 Github
3. 注意事项
- compileSdkVersion 版本问题
由于在新版本的Android中,com.android.support:appcompat-v7
软件包移除了一些组件。而这些组件ROS的库使用到了。所以在Android SDK > 25的版本中无法使用Android ROS. 所以我们要在配置文件中修改SDK版本。 - 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/
文件夹中
把此jar文件复制到Android项目中的 app\libs
文件夹中。
右键点击app,在弹出的菜单中点击Open Module Settings
选择dependencies页面,然后点击右侧加号
选择jar dependencies,然后选择jar文件点击确认就可以了
这样就可以在程序中使用自定义的消息了。
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)