DEV Community

vmodal_ai
vmodal_ai

Posted on Originally published at v-modal.com

Robot SDK with DDS: Native Android Integration for High-Performance Robotics

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            |
+-------------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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.");
    }
}
Enter fullscreen mode Exit fullscreen mode

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
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)