Getting ROS 2 Running on Android (It’s Easier Than You Think!)

Getting ROS 2 Running on Android (It’s Easier Than You Think!)

So you want to run ROS 2 on your Android phone? Maybe you want to be able to control your robot from your phone, or maybe you want to integrate a phone on your robot. Well, you can do it with jros2!

What’s jros2?

jros2 is basically ROS 2 but for Java. The cool part? You don’t need to install ROS 2 at all. Just add one dependency to your Android project and you’re publishing and subscribing to topics. It works on Linux, Windows, macOS, and Android. It can even handle custom message types!

ezgif-60d628ec2c7411d4

The Super Minimal Example

Here’s the absolute barebones example to get you started. This creates a node that publishes a simple string message:

package com.example.ros2app;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import us.ihmc.jros2.ROS2Node;
import us.ihmc.jros2.ROS2Publisher;
import us.ihmc.jros2.ROS2Topic;

public class MainActivity extends AppCompatActivity {
    private ROS2Node node;
    private Thread publishThread;

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

        // Create a ROS2 node
        node = new ROS2Node("android_node");

        // Create a topic and publisher
        ROS2Topic<std_msgs.String_> topic =
            new ROS2Topic<>("/android/hello", std_msgs.String_.class);
        ROS2Publisher<std_msgs.String_> publisher = node.createPublisher(topic);

        // Publish in a background thread
        publishThread = new Thread(() -> {
            std_msgs.String_ msg = new std_msgs.String_();
            while (!node.isClosed()) {
                msg.setData("Hello from Android at " + System.currentTimeMillis());
                publisher.publish(msg);

                try {
                    Thread.sleep(1000); // 1 Hz
                } catch (InterruptedException e) {
                    break;
                }
            }
        });
        publishThread.start();
    }

    @Override
    protected void onDestroy() {
        if (node != null) {
            node.close();
        }
        super.onDestroy();
    }
}

That’s it! About 40 lines and you’ve got a working ROS2 publisher on Android.

Want to Subscribe Instead?

Receiving messages is even easier:

ROS2Node node = new ROS2Node("subscriber_node");
ROS2Topic<std_msgs.String> topic =
    new ROS2Topic<>("/chatter", std_msgs.String_.class);

node.createSubscriptionSampler(topic, msg -> {
    // This runs when a message arrives
    runOnUiThread(() -> {
        textView.setText(msg.getData());
    });
});

The runOnUiThread() part is important – ROS callbacks happen on background threads, so if you want to update your UI, you need to hop back to the main thread.

Real-World Example: Publishing Phone Sensors

Here’s something actually useful – publishing your phone’s accelerometer data to ROS2. This is from a test app I made:

public class MainActivity extends AppCompatActivity implements SensorEventListener {
    private ROS2Node ros2Node;
    private ROS2Publisher<geometry_msgs.PoseStamped> publisher;
    private SensorManager sensorManager;
    private Sensor rotationSensor;
    private float[] rotationVector = new float[4];

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

        // Initialize ROS2
        ros2Node = new ROS2Node("phone_imu");
        ROS2Topic<geometry_msgs.PoseStamped> topic =
            new ROS2Topic<>("/phone/pose", geometry_msgs.PoseStamped.class);
        publisher = ros2Node.createPublisher(topic);

        // Set up sensors
        sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        rotationSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
        sensorManager.registerListener(this, rotationSensor,
            SensorManager.SENSOR_DELAY_GAME);
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) {
            // Copy rotation vector data
            System.arraycopy(event.values, 0, rotationVector, 0,
                Math.min(event.values.length, 4));

            // Create and publish ROS message
            geometry_msgs.PoseStamped pose = new geometry_msgs.PoseStamped();
            pose.getHeader().setFrameId("phone");

            // Set timestamp
            long nanos = System.currentTimeMillis() * 1_000_000;
            pose.getHeader().getStamp().setSec((int)(nanos / 1_000_000_000));
            pose.getHeader().getStamp().setNanosec((int)(nanos % 1_000_000_000));

            // Set orientation from sensor
            pose.getPose().getOrientation().setX(rotationVector[0]);
            pose.getPose().getOrientation().setY(rotationVector[1]);
            pose.getPose().getOrientation().setZ(rotationVector[2]);
            pose.getPose().getOrientation().setW(rotationVector[3]);

            publisher.publish(pose);
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {}

    @Override
    protected void onDestroy() {
        sensorManager.unregisterListener(this);
        if (ros2Node != null) {
            ros2Node.close();
        }
        super.onDestroy();
    }
}

Setup

First, add the IHMC repository to your settings.gradle.kts:

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri("https://robotlabfiles.ihmc.us/repository/")
        }
    }
}

Then in your app’s build.gradle.kts:

android {
    compileSdk = 35

    defaultConfig {
        minSdk = 26
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
        isCoreLibraryDesugaringEnabled = true
    }

    packaging {
        resources {
            excludes += "/META-INF/LICENSE.md"
            excludes += "/META-INF/NOTICE.md"
        }
    }
}

dependencies {
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
    implementation("us.ihmc:jros2-android:1.2.1")
}

And don’t forget permissions in AndroidManifest.xml:

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

Things to Watch Out For

  1. Threading – ROS callbacks run on background threads. Always use runOnUiThread() to update UI.

  2. Lifecycle Management – Create your node in onCreate() and always close it in onDestroy().

  3. Discovery Issues – Make sure your phone and ROS 2 system are on the same WiFi network (or use USB tethering for a wired connection).

Testing It Out

Fire up your app, then on your ROS 2 machine:

ros2 topic list
# You should see /android/hello or whatever you named your topic

ros2 topic echo /android/hello
# You should see the messages being published

Potential Use Cases

You can now use your phone as a cheap mobile sensor platform for your robot. IMU data, camera images, GPS, microphone audio are all accessible through Android’s sensor APIs and publishable to ROS 2. Plus, your phone probably has better sensors than whatever’s on your robot anyway.

Have fun, and also check out the wiki!

1 Like