Robot SDK with DDS: Native Android Integration for High-Performance Robotics
Data Distribution Service (DDS) is the industry standard for real-time, deterministic, publish-subscribe communication in modern robotics platforms, including ROS 2 (Robot Operating System). Integrating DDS directly into Android enables mobile tablets, compute units, and handheld controllers to interface natively with low-latency robot middleware.
This tutorial guides you through building a native Android SDK with embedded DDS functionality using Kotlin, C++ NDK wrappers, and Java Native Interface (JNI) bindings.
1. High-Level Architecture
+-------------------------------------------------------------+
| Android Kotlin Layer |
| VModalDdsNode / Direct Topic Subscriptions |
+-------------------------------------------------------------+
| JNI Callbacks
v
+-------------------------------------------------------------+
| C++ Native Middleware Wrapper |
| (NativeDdsManager.cpp) |
+-------------------------------------------------------------+
| Native C++ Library Calls
v
+-------------------------------------------------------------+
| DDS Core (eProsima Fast DDS / CycloneDDS) |
| UDP / SHM Transport / RTPS Protocol |
+-------------------------------------------------------------+
2. Setting Up CMake and NDK Integration
Create a CMakeLists.txt configuration to build your C++ native library against embedded DDS static/dynamic libraries.
cmake_minimum_required(VERSION 3.22.1)
project("vmodal_dds_native")
# Set C++ Standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find or import DDS core library (e.g., Fast-DDS or CycloneDDS)
find_package(fastrtps REQUIRED)
# Define native library target
add_library(
vmodal_dds_native
SHARED
native_dds_bridge.cpp
)
# Target Link Libraries
target_link_libraries(
vmodal_dds_native
fastrtps
log
)
3. Writing the Native C++ JNI Layer (native_dds_bridge.cpp)
Implement the native C++ code to initialize a DDS Participant, DataWriter, and DataReader, bridging messages back to Java/Kotlin.
#include <jni.h>
#include <string>
#include <android/log.h>
#include <fastdds/dds/domain/DomainParticipantFactory.hpp>
#include <fastdds/dds/domain/DomainParticipant.hpp>
#include <fastdds/dds/topic/Topic.hpp>
#include <fastdds/dds/publisher/Publisher.hpp>
#include <fastdds/dds/publisher/DataWriter.hpp>
#define LOG_TAG "VModalDDSNative"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
using namespace eprosima::fastdds::dds;
static DomainParticipant* participant_ = nullptr;
static Publisher* publisher_ = nullptr;
extern "C" JNIEXPORT jboolean JNICALL
Java_com_vmodal_sdk_dds_DdsBridge_nativeInitNode(
JNIEnv* env,
jobject instance,
jint domainId,
jstring participantName
) {
const char* name = env->GetStringUTFChars(participantName, nullptr);
DomainParticipantQos qos;
qos.name(name);
participant_ = DomainParticipantFactory::get_instance()->create_participant(domainId, qos);
env->ReleaseStringUTFChars(participantName, name);
if (participant_ == nullptr) {
LOGE("Failed to create DDS DomainParticipant");
return JNI_FALSE;
}
LOGI("Successfully initialized DDS Participant on Domain %d", domainId);
return JNI_TRUE;
}
extern "C" JNIEXPORT void JNICALL
Java_com_vmodal_sdk_dds_DdsBridge_nativeShutdown(
JNIEnv* env,
jobject instance
) {
if (participant_ != nullptr) {
DomainParticipantFactory::get_instance()->delete_participant(participant_);
participant_ = nullptr;
LOGI("DDS Participant shut down cleanly.");
}
}
4. Building the Kotlin SDK Layer
The Kotlin SDK exposes high-level, coroutines-friendly APIs over the raw JNI native library.
package com.vmodal.sdk.dds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class DdsBridge {
companion object {
init {
System.loadLibrary("vmodal_dds_native")
}
}
private external fun nativeInitNode(domainId: Int, participantName: String): Boolean
private external fun nativeShutdown()
private var isInitialized = false
suspend fun initializeNode(domainId: Int = 0, nodeName: String = "android_robot_node"): Result<Unit> {
return withContext(Dispatchers.IO) {
try {
val success = nativeInitNode(domainId, nodeName)
if (success) {
isInitialized = true
Result.success(Unit)
} else {
Result.failure(RuntimeException("Native DDS Initialization Failed."))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}
suspend fun shutdown() {
withContext(Dispatchers.IO) {
if (isInitialized) {
nativeShutdown()
isInitialized = false
}
}
}
}
5. Subscribing to DDS Topics via Flow
Create a reactive Flow wrapper for streaming ROS 2 or DDS robot telemetry topics into Kotlin.
package com.vmodal.sdk.dds
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
data class TwistMessage(
val linearX: Double,
val linearY: Double,
val angularZ: Double
)
class DdsTopicSubscriber(private val bridge: DdsBridge) {
fun subscribeTwist(topicName: String): Flow<TwistMessage> = callbackFlow {
val callback = object : NativeTopicListener {
override fun onMessageReceived(x: Double, y: Double, z: Double) {
trySend(TwistMessage(x, y, z))
}
}
registerNativeListener(topicName, callback)
awaitClose {
unregisterNativeListener(topicName)
}
}
private fun registerNativeListener(topic: String, listener: NativeTopicListener) {
// Native JNI registration bridge
}
private fun unregisterNativeListener(topic: String) {
// Native JNI unregistration bridge
}
private interface NativeTopicListener {
fun onMessageReceived(x: Double, y: Double, z: Double)
}
}
6. End-to-End Application Example
import com.vmodal.sdk.dds.DdsBridge
import com.vmodal.sdk.dds.DdsTopicSubscriber
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val ddsBridge = DdsBridge()
println("Initializing native DDS node...")
val initResult = ddsBridge.initializeNode(domainId = 42, nodeName = "vmodal_android_teleop")
initResult.onSuccess {
println("DDS Node connected to domain 42 successfully!")
val subscriber = DdsTopicSubscriber(ddsBridge)
// Listen to teleop twist messages
subscriber.subscribeTwist("/cmd_vel").collect { twist ->
println("Received CmdVel -> Linear X: ${twist.linearX}, Angular Z: ${twist.angularZ}")
}
}.onFailure { err ->
println("Failed to bind DDS bridge: ${err.message}")
}
ddsBridge.shutdown()
}
Conclusion
Embedding native DDS binaries into Android enables sub-millisecond, low-jitter pub/sub communication with ROS 2 and industrial robot armatures. Using modern C++ NDK bindings paired with Kotlin Flow APIs delivers the native execution speed of C++ alongside the safety of Kotlin.
Useful Links
- Website: www.v-modal.com
- SDK Flutter: v-modal/vmodal_sdk_flutter
- SDK Android: v-modal/vmodal_sdk_android
- Discord: https://discord.gg/K72z28KUx
Top comments (0)