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 <---------------+
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()
}
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)
}
}
}
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
}
}
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}")
}
}
}
}
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
- Website: www.v-modal.com
- SDK Flutter: v-modal/vmodal_sdk_flutter
- SDK Android: v-modal/vmodal_sdk_android
- Discord: https://discord.gg/K72z28KUx











