DEV Community

vmodal_ai
vmodal_ai

Posted on Originally published at v-modal.com

Building an Android SDK for Robot Fleet Discovery and Device Provisioning

Building an Android SDK for Robot Fleet Discovery and Device Provisioning

Deploying autonomous mobile robots into factory floors or warehouses requires initial pairing, network Wi-Fi provisioning, and zero-configuration local service discovery. Operators need mobile apps that can scan unconfigured devices over Bluetooth Low Energy (BLE) or discover active fleet nodes over local mDNS/DNS-SD (Bonjour).

In this tutorial, we will build a production-grade Kotlin SDK that manages two-phase robot provisioning: BLE network configuration followed by local network mDNS discovery.


1. Fleet Provisioning Architecture

+-------------------------------------------------------------------+
|                        Android Application                        |
+-------------------------------------------------------------------+
                                  |
            +---------------------+---------------------+
            | BLE Provisioning                          | mDNS Discovery
            v                                           v
+-----------------------+                   +-----------------------+
|  Unprovisioned Robot  |                   |   Active Robot Node   |
|   (BLE GATT Server)   |                   |  (_robot._tcp.local)  |
+-----------------------+                   +-----------------------+
            |                                           |
            +-------------------> Wi-Fi <---------------+
Enter fullscreen mode Exit fullscreen mode

2. Model Definitions for Robot Fleet Discovered Nodes

package com.vmodal.sdk.discovery.model

import java.net.InetAddress

data class DiscoveredRobot(
    val robotId: String,
    val modelName: String,
    val ipAddress: InetAddress,
    val port: Int,
    val firmwareVersion: String,
    val attributes: Map<String, String> = emptyMap()
)

sealed class ProvisioningState {
    object Idle : ProvisioningState()
    object ScanningBle : ProvisioningState()
    data class BleConnected(val deviceAddress: String) : ProvisioningState()
    object TransmittingCredentials : ProvisioningState()
    object ProvisionedSuccessfully : ProvisioningState()
    data class Failed(val error: String) : ProvisioningState()
}
Enter fullscreen mode Exit fullscreen mode

3. Implementing mDNS Fleet Discovery using Android NsdManager

Android's NsdManager (Network Service Discovery) allows the SDK to resolve active robots broadcasting services on local networks without static IP configurations.

package com.vmodal.sdk.discovery

import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import com.vmodal.sdk.discovery.model.DiscoveredRobot
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow

class FleetDiscoveryManager(context: Context) {

    private val nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
    private val serviceType = "_vmodal-robot._tcp."

    fun discoverFleetRobots(): Flow<List<DiscoveredRobot>> = callbackFlow {
        val activeRobots = mutableMapOf<String, DiscoveredRobot>()

        val discoveryListener = object : NsdManager.DiscoveryListener {
            override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) {
                close(RuntimeException("NSD Start failed with code: $errorCode"))
            }

            override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) {}

            override fun onDiscoveryStarted(serviceType: String) {}

            override fun onDiscoveryStopped(serviceType: String) {}

            override fun onServiceFound(serviceInfo: NsdServiceInfo) {
                if (serviceInfo.serviceType == serviceType) {
                    nsdManager.resolveService(serviceInfo, object : NsdManager.ResolveListener {
                        override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {}

                        override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
                            val id = serviceInfo.attributes["id"]?.let { String(it) } ?: serviceInfo.serviceName
                            val model = serviceInfo.attributes["model"]?.let { String(it) } ?: "Unknown Model"
                            val fw = serviceInfo.attributes["fw"]?.let { String(it) } ?: "1.0.0"

                            val robot = DiscoveredRobot(
                                robotId = id,
                                modelName = model,
                                ipAddress = serviceInfo.host,
                                port = serviceInfo.port,
                                firmwareVersion = fw
                            )

                            activeRobots[id] = robot
                            trySend(activeRobots.values.toList())
                        }
                    })
                }
            }

            override fun onServiceLost(serviceInfo: NsdServiceInfo) {
                val id = serviceInfo.serviceName
                activeRobots.remove(id)
                trySend(activeRobots.values.toList())
            }
        }

        nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryListener)

        awaitClose {
            nsdManager.stopServiceDiscovery(discoveryListener)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Implementing BLE Provisioning Handshake

When a robot is unconfigured out of the box, standard Wi-Fi is unavailable. The SDK uses BLE GATT characteristics to transmit SSID, Passphrase, and Cloud API tokens.

package com.vmodal.sdk.discovery.provisioning

import com.vmodal.sdk.discovery.model.ProvisioningState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.UUID

class RobotBleProvisioner {

    companion object {
        val PROVISIONING_SERVICE_UUID: UUID = UUID.fromString("0000180A-0000-1000-8000-00805F9B34FB")
        val WIFI_SSID_CHAR_UUID: UUID = UUID.fromString("00002A24-0000-1000-8000-00805F9B34FB")
        val WIFI_PASS_CHAR_UUID: UUID = UUID.fromString("00002A25-0000-1000-8000-00805F9B34FB")
    }

    private val _state = MutableStateFlow<ProvisioningState>(ProvisioningState.Idle)
    val state: StateFlow<ProvisioningState> = _state.asStateFlow()

    suspend fun provisionRobot(
        macAddress: String,
        wifiSsid: String,
        wifiPass: String
    ): Boolean {
        _state.value = ProvisioningState.BleConnected(macAddress)

        // Connect BLE GATT...
        _state.value = ProvisioningState.TransmittingCredentials

        val ssidPayload = wifiSsid.toByteArray(Charsets.UTF_8)
        val passPayload = wifiPass.toByteArray(Charsets.UTF_8)

        val writeSuccess = writeGattCharacteristics(ssidPayload, passPayload)

        return if (writeSuccess) {
            _state.value = ProvisioningState.ProvisionedSuccessfully
            true
        } else {
            _state.value = ProvisioningState.Failed("GATT Write Failure during credentials transfer")
            false
        }
    }

    private fun writeGattCharacteristics(ssidBytes: ByteArray, passBytes: ByteArray): Boolean {
        // Simulating BLE low-level characteristic write execution
        return true
    }
}
Enter fullscreen mode Exit fullscreen mode

5. End-to-End Usage Example

import android.content.Context
import com.vmodal.sdk.discovery.FleetDiscoveryManager
import com.vmodal.sdk.discovery.provisioning.RobotBleProvisioner
import kotlinx.coroutines.runBlocking

fun executeDiscoveryAndProvisioning(context: Context) = runBlocking {
    val provisioner = RobotBleProvisioner()
    val discoveryManager = FleetDiscoveryManager(context)

    println("Phase 1: Provisioning unconfigured robot over BLE...")
    val success = provisioner.provisionRobot(
        macAddress = "AA:BB:CC:11:22:33",
        wifiSsid = "Warehouse_5G_North",
        wifiPass = "SecureEnterprisePass!2026"
    )

    if (success) {
        println("Provisioning packet delivered! Switching to mDNS discovery...")

        // Scan for robot arriving on local Wi-Fi network
        discoveryManager.discoverFleetRobots().collect { fleetList ->
            println("Active Fleet Robots on Network (${fleetList.size}):")
            fleetList.forEach { robot ->
                println(" -> Robot ID: ${robot.robotId} | IP: ${robot.ipAddress.hostAddress}:${robot.port}")
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

By combining Bluetooth Low Energy for out-of-box setup and mDNS/DNS-SD for continuous local zero-conf discovery, your Android application can seamlessly discover and configure entire fleets of autonomous mobile robots.


Useful Links

Top comments (0)