## For instruction on writing tutorials ## http://www.ros.org/wiki/WritingTutorials #################################### ##FILL ME IN #################################### ## for a custom note with links: ## note = ## for the canned note of "This tutorial assumes that you have completed the previous tutorials:" just add the links ## note.0=[[android_ndk/Tutorials/How to cross-compile any ROS package]] ## descriptive title for the tutorial ## title = Wrapping your native code as a rosjava node ## multi-line description to be displayed in search ## description = Steps to create a rosjava android project and add your native compiled nodes as rosjava nodes. ## the next tutorial description (optional) ## next = ## links to next tutorial (optional) ## next.0.link=[[android_ndk/Tutorials/UsingPluginlib]] ## what level user is this tutorial for ## level= AdvancedCategory ## keywords = #################################### <> ## AUTOGENERATED DO NOT DELETE ## TutorialCategory ## FILL IN THE STACK TUTORIAL CATEGORY HERE == Prerequisites == 1. Knowledge of Android development 1. Have the following software installed in your system: 1. ROS environment 1. Android SDK 1. Android NDK 1. Completed the [[http://wiki.ros.org/android_ndk/Tutorials/How%20to%20cross-compile%20any%20ROS%20package|previous tutorial]] So at this point you have successfully cross-compiled your library into device native code. A convenient way of running ROS nodes on Android devices is using [[http://wiki.ros.org/rosjava|ROSJAVA]] but in order to use the library in your Java application you need to create a “Java Native Interface” (JNI) wrapper. What this does is declare your C++ functions and classes so the Java virtual machine is able to import them. More info [[http://docs.oracle.com/javase/7/docs/technotes/guides/jni/|here]]. You can take a look at some more examples [[https://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html|here]] if you want more information. === Installing Rosjava and Android core === Before we proceed you need to install rosjava and the ros android core in your system. The following tutorials will help you on that: 1. [[http://wiki.ros.org/rosjava/Tutorials/kinetic/Source%20Installation|Installing Rosjava]] 2. [[http://wiki.ros.org/android/Tutorials/kinetic/Installation%20-%20ROS%20Development%20Environment|Installing Android core]] Our native code will be wrapped extending the [[https://github.com/rosjava/rosjava_core/blob/kinetic/rosjava/src/main/java/org/ros/node/NativeNodeMain.java|NativeNodeMain]] class, which is also available on [[https://github.com/rosjava/rosjava_mvn_repo|rosjava_mvn_repo]] since version 0.3.1 of rosjava_core. '''Note:''' We will use some tools included in `rosjava_build_tools`, so be sure to source your `devel/setup.bash` of your rosjava workspace if you installed it from source. If you installed rosjava from debian packages, sourcing the standard `/opt/ros//setup.bash` is enough. == Creating an Android project == 1. Create a new workspace folder (we will use your home directory for simplicity along this tutorial): {{{ $ mkdir -p ~/android1/src $ cd ~/android1/src }}} 2. Create a new Android package including the needed dependencies: {{{ $ catkin_create_android_pkg androidpkg1 android_core, rosjava_core, std_msgs }}} 3. You can comment-out the following lines from the file "CMakeLists.txt". If you don’t don’t wan’t to create a maven repo: {{{ # Deploy android libraries (.aar's) and applications (.apk's) install(DIRECTORY {CATKIN_DEVEL_PREFIX}/${CATKIN_GLOBAL_MAVEN_DESTINATION}/com/github/rosjava/${PROJECT_NAME}/ DESTINATION {CATKIN_GLOBAL_MAVEN_DESTINATION}/com/github/rosjava/${PROJECT_NAME}/) }}} 4. Create an new Android project for this package: {{{ $ cd androidpkg1 $ catkin_create_android_project androidp1 }}} 5. This just created an empty java template for you to work on: {{{ ~/androidp1/src/main/java/com/github/rosjava/android/androidp1/Androidp1.java }}} 6. Edit "CMakeLists.txt". Replace the "assembleRelease" target for the "assembleDebug" target (this is better for testing purposes). {{{ $ cd ~/android1/src/androidpkg1 $ vim CMakeLists.txt }}} {{{ catkin_android_setup(assembleDebug uploadArchives) }}} 7. From the top level directory run: {{{ $ catkin_make }}} 8. After the build you should have the compiled code located in: {{{ ~/android1/src/androidpkg1/androidp1/build/outputs/apk }}} To build with rosjava android_core add the following dependencies to your "androidp1/build.gradle" (NOTE: these usually are commented-out!!): {{{ dependencies { compile('org.ros.android_core:android_10:[0.3, 0.4)') { exclude group: 'junit' exclude group: 'xml-apis' } } }}} === Creating the JNI wrappers for your library === For this tutorial, we are going to wrap a simple publisher node just like the one in this very basic [[http://wiki.ros.org/ROS/Tutorials/WritingPublisherSubscriber(c%2B%2B)|example]] 1. After the previous steps you should have a project structure as follows: {{{ ~/android1/src/androidpkg1/androidp1/src/main/java }}} Go to that directory and create the standard Java folders for this code: {{{ cd ~/android1/src/androidpkg1/androidp1/src/main/java mkdir -p org/ros/rosjava_tutorial_native_node }}} 2. Write code for a Rosjava node extending the "!NativeNodeMain.java" in your directory created in the step above. For example: {{{ #!java block=native_node_example package org.ros.rosjava_tutorial_native_node; import org.ros.node.NativeNodeMain; import org.ros.namespace.GraphName; /** * Class to implement a chatter native node. **/ public class ChatterNativeNode extends NativeNodeMain { private static final String libName = "chatter_jni"; public static final String nodeName = "chatter"; public ChatterNativeNode() { super(libName); } public ChatterNativeNode(String[] remappingArguments) { super(libName, remappingArguments); } @Override public GraphName getDefaultNodeName() { return GraphName.of(nodeName); } @Override protected native int execute(String rosMasterUri, String rosHostname, String rosNodeName, String[] remappingArguments); @Override protected native int shutdown(); } }}} Our node class extends the "!NativeNodeMain" class that provides functionality to load and execute the native code. In the constructor we pass on to the superclass the name of the native library to load: <> Then, we have the declaration of the actual native methods. These match the ones that will be declared in the C++ code: <> 3. Now we need to create a folder for our native code: {{{ $ cd ~/android1/src/androidpkg1/androidp1/src/main $ mkdir -p jni/src }}} 4. Standing in this new folder we will proceed to generate a header file for our native code based on the declaration in the java source. It's important to compile the java class before this step (run `catkin_make` from your top level workspace directory). {{{ $ javah -o chatter_jni.h -classpath /mydir/src/androidpkg1/androidp1/build/intermediates/classes/debug org.ros.rosjava_tutorial_native_node.ChatterNativeNode }}} The classpath must have the full path (no relative paths are allowed). Note: if you installed rosjava from source, you might need to specify the path to your compiled `NativeNodeMain` class too. Something like this: {{{ $ javah -o chatter_jni.h -classpath ~/android1/src/androidpkg1/androidp1/build/intermediates/classes/debug:~/rosjava/src/rosjava_core/rosjava/build/classes/main/ org.ros.rosjava_tutorial_native_node.ChatterNativeNode org.ros.node.NativeNodeMain }}} (i.e. use the directory where you installed rosjava, appending it to the first classpath using ":"). We should now have the file "chatter_jni.h" with the declaration of the exported functions and their parameters according to the Java signature. The file should look something like this: {{{ #!cplusplus /* DO NOT EDIT THIS FILE - it is machine generated */ #include /* Header for class org_ros_rosjava_tutorial_native_node_ChatterNativeNode */ #ifndef _Included_org_ros_rosjava_tutorial_native_node_ChatterNativeNode #define _Included_org_ros_rosjava_tutorial_native_node_ChatterNativeNode #ifdef __cplusplus extern "C" { #endif /* * Class: org_ros_rosjava_tutorial_native_node_ChatterNativeNode * Method: execute * Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;)V */ JNIEXPORT jint JNICALL Java_org_ros_rosjava_1tutorial_1native_1node_ChatterNativeNode_execute (JNIEnv *, jobject, jstring, jstring, jstring, jobjectArray); /* * Class: org_ros_rosjava_tutorial_native_node_ChatterNativeNode * Method: shutdown * Signature: ()V */ JNIEXPORT jint JNICALL Java_org_ros_rosjava_1tutorial_1native_1node_ChatterNativeNode_shutdown (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif }}} 5. Code the functions declared in header file. These functions should encapsulate the calls to the actual library. For example the following (chatter_jni.cpp): {{{ #!cplusplus #include #include #include "chatter_jni.h" #include "std_msgs/String.h" #include using namespace std; void log(const char *msg, ...) { va_list args; va_start(args, msg); __android_log_vprint(ANDROID_LOG_INFO, "Native_Chatter", msg, args); va_end(args); } inline string stdStringFromjString(JNIEnv *env, jstring java_string) { const char *tmp = env->GetStringUTFChars(java_string, NULL); string out(tmp); env->ReleaseStringUTFChars(java_string, tmp); return out; } bool running; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { log("Library has been loaded"); // Return the JNI version return JNI_VERSION_1_6; } JNIEXPORT jint JNICALL Java_org_ros_rosjava_1tutorial_1native_1node_ChatterNativeNode_execute( JNIEnv *env, jobject obj, jstring rosMasterUri, jstring rosHostname, jstring rosNodeName, jobjectArray remappingArguments) { log("Native chatter node started."); running = true; string master("__master:=" + stdStringFromjString(env, rosMasterUri)); string hostname("__ip:=" + stdStringFromjString(env, rosHostname)); string node_name(stdStringFromjString(env, rosNodeName)); log(master.c_str()); log(hostname.c_str()); // Parse remapping arguments log("Before getting size"); jsize len = env->GetArrayLength(remappingArguments); log("After reading size"); std::string ni = "chatter_jni"; int argc = 0; const int static_params = 4; char **argv = new char *[static_params + len]; argv[argc++] = const_cast(ni.c_str()); argv[argc++] = const_cast(master.c_str()); argv[argc++] = const_cast(hostname.c_str()); //Lookout: ros::init modifies argv, so the references to JVM allocated strings must be kept in some other place to avoid "signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr deadbaad" // when trying to free the wrong reference ( see https://github.com/ros/ros_comm/blob/indigo-devel/clients/roscpp/src/libros/init.cpp#L483 ) char **refs = new char *[len]; for (int i = 0; i < len; i++) { refs[i] = (char *) env->GetStringUTFChars( (jstring) env->GetObjectArrayElement(remappingArguments, i), NULL); argv[argc] = refs[i]; argc++; } log("Initiating ROS..."); ros::init(argc, &argv[0], node_name.c_str()); log("ROS intiated."); // Release JNI UTF characters for (int i = 0; i < len; i++) { env->ReleaseStringUTFChars((jstring) env->GetObjectArrayElement(remappingArguments, i), refs[i]); } delete refs; delete argv; ros::NodeHandle n; ros::Publisher chatter_pub = n.advertise("chatter", 1000); ros::Rate loop_rate(1); int count = 0; while (ros::ok()) { /** * This is a message object. You stuff it with data, and then publish it. */ std_msgs::String msg; std::stringstream ss; ss << "hello world " << count; msg.data = ss.str(); ROS_INFO("%s", msg.data.c_str()); /** * The publish() function is how you send messages. The parameter * is the message object. The type of this object must agree with the type * given as a template parameter to the advertise<>() call, as was done * in the constructor above. */ chatter_pub.publish(msg); ros::spinOnce(); loop_rate.sleep(); ++count; } log("Exiting from JNI call."); return 0; } JNIEXPORT jint JNICALL Java_org_ros_rosjava_1tutorial_1native_1node_ChatterNativeNode_shutdown (JNIEnv *, jobject) { log("Shutting down native node."); ros::shutdown(); running = false; return 0; } }}} 6. We have to create the "Android.mk" and "Application.mk" configuration files: {{{ $ cd ~/android1/src/androidpkg1/androidp1/src/main/jni $ vim Android.mk $ vim Application.mk }}} Android.mk example: {{{ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := chatter_jni LOCAL_SRC_FILES := src/chatter_jni.cpp LOCAL_C_INCLUDES := $(LOCAL_PATH)/include LOCAL_LDLIBS := -landroid -llog LOCAL_STATIC_LIBRARIES := roscpp_android_ndk include $(BUILD_SHARED_LIBRARY) $(call import-add-path, /home/user/ros-android-ndk/roscpp_android/output/) $(call import-module, roscpp_android_ndk) }}} The "LOCAL_STATIC_LIBRARIES" variable defines where the cross-compiled libraries are located. You can recall the previous tutorials for this information. Note: the line calling `import-add-path` should have the path to your output directory of your locally installed roscpp_android_ndk environment. Application.mk example: {{{ #NDK_TOOLCHAIN_VERSION=4.4.3 APP_STL := gnustl_static APP_PLATFORM := android-15 }}} Here we define the C++ runtime library we want to use and the Android SDK version we are targeting. 7. Going back up in the directory structure we must edit the "build.gradle" file to add the new dependencies: {{{ $ cd mydir/src/androidpkg1/androidp1 $ vim build.gradle }}} Add the ndkBuild task to your file, as well as the `sourceSets` block to your `android` block. {{{ dependencies { compile('org.ros.android_core:android_10:[0.3, 0.4)') { exclude group: 'junit' exclude group: 'xml-apis' } } tasks.withType(JavaCompile) { compileTask -> compileTask.dependsOn ndkBuild } task ndkBuild(type: Exec) { Properties properties = new Properties() properties.load(project.rootProject.file('local.properties').newDataInputStream()) def ndkbuild = properties.getProperty('ndk.dir', null) + "/ndk-build" commandLine ndkbuild, '-C', file('src/main/jni').absolutePath } android { ... sourceSets.main { jniLibs.srcDir 'src/main/libs' jni.srcDirs = []; } ... } }}} You also need to add a file named "local.properties" in the `androidpkg1` directory. It should specify the path to your SDK and NDK install directories; for example: {{{ ndk.dir=/home/user/Android/Sdk/ndk-bundle sdk.dir=/home/user/Android/Sdk }}} If you used Android Studio to install SDK & NDK, the default installation path is ~/Android/Sdk; use the absolute path in local.properties. ''' What's happening here?''' This task added to your Gradle script will call the Android NDK buildsystem with your recently created Android.mk file as an argument. Then, it will build your code following the rules specified in Android.mk to create a Shared Library (.so file), and place it inside the resulting apk file. In other words, your Java code and your native code will be built and packed together with a single command! === Writing a test app === 1. Lets write something interesting to test our native node: {{{ #!java package com.github.rosjava.android.androidp1; import org.ros.android.RosActivity; import org.ros.node.NodeConfiguration; import org.ros.node.NodeMainExecutor; import org.ros.RosCore; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.ros.rosjava_tutorial_native_node.ChatterNativeNode; import java.net.URI; public class Androidp1 extends RosActivity { private RosCore myRoscore; private Log log = LogFactory.getLog(Androidp1.class); private NodeMainExecutor nodeMainExecutor = null; private URI masterUri; private String hostName; private ChatterNativeNode chatterNativeNode; final static String appName = "native_wrap_test"; public Androidp1() { super(appName, appName); } @Override protected void init(NodeMainExecutor nodeMainExecutor) { log.info("Androidp1 init"); // Store a reference to the NodeMainExecutor and unblock any processes that were waiting // for this to start ROS Nodes this.nodeMainExecutor = nodeMainExecutor; masterUri = getMasterUri(); hostName = getRosHostname(); log.info(masterUri); startChatter(); } // Create a native chatter node private void startChatter() { log.info("Starting native node wrapper..."); NodeConfiguration nodeConfiguration = NodeConfiguration.newPublic(hostName); nodeConfiguration.setMasterUri(masterUri); nodeConfiguration.setNodeName(ChatterNativeNode.nodeName); chatterNativeNode = new ChatterNativeNode(); nodeMainExecutor.execute(chatterNativeNode, nodeConfiguration); } } }}} 2. Before compiling the code, we should update the `AndroidManifest` located in `~/android1/src/androidpkg1/src/main`. Copy the following into your manifest file: {{{ }}} This allows the application to use the `MasterChooser` defined in the `RosActivity`, and connect to a ROS master using its IP address. 3. We can now go to the top level folder of our project and run catkin_make; if everything works well we should get our Android package file ready to install and run. {{{ $ cd mydir $ catkin_make }}} Find the resulting package, and install it to your device: {{{ cd ~/android1/src/androidpkg1/androidp1/build/outputs/apk/ adb install androidp1-debug.apk }}} You should be able to find the a copy of the built shared library here inside the apk file (look for libchatter_jni.so inside the lib directory after opening the apk). 4. Run the app! After connecting to a ROS master, you should be able to listen to the "chatter" topic, which will print a message per second. == Extras == === Upstreaming error codes to your application === As you may have noticed, the native methods defined above return an integer. This return value is intended to be an error code that you can handle from the Java side of your application. If your native `execute` or `shutdown` methods return a value different than 0, your node's `onError` method will be called. You can override this method in your native node and access the protected variables `executeReturnCode` and `shutdownReturnCode` (inherited from `NativeNodeMain`) to handle these cases. For example: {{{#!java // In your ChatterNativeNode class @Override public void onError(Node node, Throwable throwable) { if (super.executeReturnCode != 0) { // Handle execution error } else if (super.shutdownReturnCode != 0 { // Handle shutdown error } } }}} === Under the hood === Your Java code (`ChatterNativeNode` in this example) is a pure rosjava node that ends up calling your defined `execute` method in its `onStart` method. Note that the native code also creates a new node which connects to the master. If both nodes have the same name like in this case, the ROS master will disconnect the first one connected -the pure Java node-. You can try creating the node with a different name changing the configuration in `startChatter`, and you should see two new nodes after running the app. This doesn't affect functionality at all, but it's something to keep in mind when handling the error codes as described above. In case of an error, the `onError` method will be called after the node is disconnected. If you need to handle the error being connected to the ROS master, don't use the same name for both nodes (pure Java and native).