DEV Community

vmodal_ai
vmodal_ai

Posted on Originally published at v-modal.com

Building an Offline-First Robot Control SDK for Android

Building an Offline-First Robot Control SDK for Android

Robots often operate in network-dead zones such as underground mines, concrete warehouses, or agricultural fields. Controllers cannot depend on uninterrupted internet or Wi-Fi connectivity. An Android Robot Control SDK must be designed offline-first, persisting commands, mission logs, and telemetry locally, and synchronizing with physical robots or cloud backends once connection resumes.

This tutorial guides you through engineering an offline-first sync architecture using Room, SQLite, and Kotlin Flows.


1. Offline-First Sync Architecture

+-------------------------------------------------------------+
|                     Android Application                     |
+-------------------------------------------------------------+
                               | Read/Write
                               v
+-------------------------------------------------------------+
|               Local Persistence (Room / SQLite)             |
|                  [Pending Command Queue]                    |
+-------------------------------------------------------------+
                               |
                        Sync Engine Monitor
                               v
+-------------------------------------------------------------+
|             Network Connectivity Monitor (Wi-Fi/Cell)        |
|     (Flushes Pending Queue to Robot/Cloud when Online)      |
+-------------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

2. Room Entity & DAO for Persistent Command Queue

package com.vmodal.sdk.offline.data

import androidx.room.*
import kotlinx.coroutines.flow.Flow

enum class CommandSyncState {
    PENDING,
    TRANSMITTING,
    ACKNOWLEDGED,
    FAILED
}

@Entity(tableName = "pending_commands")
data class RobotCommandEntity(
    @PrimaryKey val commandId: String,
    val robotId: String,
    val commandType: String,
    val jsonPayload: String,
    val syncState: CommandSyncState,
    val createdAtTimestamp: Long = System.currentTimeMillis()
)

@Dao
interface CommandQueueDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun enqueueCommand(command: RobotCommandEntity)

    @Query("SELECT * FROM pending_commands WHERE syncState = 'PENDING' ORDER BY createdAtTimestamp ASC")
    fun getPendingCommands(): Flow<List<RobotCommandEntity>>

    @Query("UPDATE pending_commands SET syncState = :state WHERE commandId = :id")
    suspend fun updateState(id: String, state: CommandSyncState)

    @Query("DELETE FROM pending_commands WHERE syncState = 'ACKNOWLEDGED'")
    suspend fun purgeAcknowledged()
}
Enter fullscreen mode Exit fullscreen mode

3. Implementing the Command Queue & Sync Engine

package com.vmodal.sdk.offline

import com.vmodal.sdk.offline.data.CommandQueueDao
import com.vmodal.sdk.offline.data.CommandSyncState
import com.vmodal.sdk.offline.data.RobotCommandEntity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.util.UUID

class OfflineCommandManager(
    private val dao: CommandQueueDao,
    private val scope: CoroutineScope,
    private val networkTransport: RobotNetworkTransport
) {
    init {
        startSyncLoop()
    }

    fun submitCommand(robotId: String, commandType: String, payloadJson: String) {
        scope.launch(Dispatchers.IO) {
            val entity = RobotCommandEntity(
                commandId = UUID.randomUUID().toString(),
                robotId = robotId,
                commandType = commandType,
                jsonPayload = payloadJson,
                syncState = CommandSyncState.PENDING
            )
            dao.enqueueCommand(entity)
        }
    }

    private fun startSyncLoop() {
        scope.launch(Dispatchers.IO) {
            dao.getPendingCommands().collectLatest { pendingList ->
                for (command in pendingList) {
                    if (networkTransport.isConnected()) {
                        dao.updateState(command.commandId, CommandSyncState.TRANSMITTING)
                        val success = networkTransport.transmit(command)
                        if (success) {
                            dao.updateState(command.commandId, CommandSyncState.ACKNOWLEDGED)
                        } else {
                            dao.updateState(command.commandId, CommandSyncState.FAILED)
                        }
                    }
                }
            }
        }
    }
}

interface RobotNetworkTransport {
    fun isConnected(): Boolean
    suspend fun transmit(command: RobotCommandEntity): Boolean
}
Enter fullscreen mode Exit fullscreen mode

4. Usage Example

import com.vmodal.sdk.offline.OfflineCommandManager
import com.vmodal.sdk.offline.RobotNetworkTransport
import com.vmodal.sdk.offline.data.CommandQueueDao
import com.vmodal.sdk.offline.data.RobotCommandEntity
import kotlinx.coroutines.runBlocking

class MockTransport : RobotNetworkTransport {
    private var online = false
    override fun isConnected(): Boolean = online
    override suspend fun transmit(command: RobotCommandEntity): Boolean = true
    fun setOnlineState(state: Boolean) { online = state }
}

fun main() {
    println("Offline-First Command Queue Engine Ready.")
    // Initialize Room & OfflineCommandManager...
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Architecting your Android Robot SDK around an offline-first transactional queue guarantees zero command loss during intermittent connectivity drops in hostile industrial environments.


Useful Links

Top comments (0)