The Zebra RFD40 is a sled-style UHF RFID reader that pairs with a host Android handheld over Bluetooth, rather than integrating a UHF module directly into a single device. That architecture — two devices, one radio link between them — is exactly where Zebra's own tutorial code falls short of production readiness, and Zebra says so directly.
What the vendor docs don't tell you
Zebra's "Hello RFID" tutorial is the standard starting point for RFD40 integration, and it includes a disclaimer that's easy to skim past: the sample is explicitly scoped to tutorial use, not production.
What the docs don't tell you
Zebra's own "Hello RFID" tutorial states directly that the demo "is intended for tutorial purposes only and should not be used in production environments." The gap isn't cosmetic — the tutorial's connection flow assumes the Bluetooth link between the RFD40 sled and the host handheld stays up for the duration of the demo, which is a reasonable assumption for a five-minute walkthrough and a false one for an eight-hour shift. Production code needs an explicit reconnection state machine that the tutorial never builds.
Setup
RFID API3 ships as a Zebra-distributed .jar/.aar through their developer portal (Zebra requires a developer account to access the SDK downloads).
dependencies {
implementation files('libs/RFIDAPI3.jar')
implementation files('libs/SrfidLibrary.aar')
}Declare Bluetooth permissions appropriate to the target API level — API 31+ requires the runtime BLUETOOTH_CONNECT permission in addition to the manifest declaration.
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />Discovering and connecting to the sled
RFID API3 discovers available readers through the Readers class, then establishes a connection via RFIDReader.
class RfdConnectionManager(private val context: Context) {
private var reader: RFIDReader? = null
fun connect(onConnected: (RFIDReader) -> Unit, onError: (String) -> Unit) {
val readers = Readers(context, ENUM_TRANSPORT.BLUETOOTH)
val available = readers.GetAvailableRFIDReaderList()
val target = available.firstOrNull() ?: run {
onError("No RFD40 sled found in range")
return
}
reader = target.rfidReader
try {
reader?.connect()
reader?.Events?.setInvTagReadEvent(true)
onConnected(reader!!)
} catch (e: InvalidUsageException) {
onError("Connect failed: ${e.message}")
} catch (e: OperationFailureException) {
onError("Operation failure: ${e.vendorMessage}")
}
}
}Reconnection: the part the tutorial skips
Register a connection event listener and treat disconnection as a routine, expected state — not an exception path that only gets a log line.
reader?.Events?.addEventsListener(object : RfidEventsListener {
override fun eventStatusNotify(statusEvents: RfidStatusEvents?) {
when (statusEvents?.StatusEventData?.statusEventType) {
STATUS_EVENT_TYPE.DISCONNECTION_EVENT -> {
scope.launch { attemptReconnect(maxAttempts = 5) }
}
STATUS_EVENT_TYPE.CONNECTION_EVENT -> {
onReconnected()
}
else -> Unit
}
}
override fun eventReadNotify(e: RfidReadEvents?) { /* tag read handling */ }
})
private suspend fun attemptReconnect(maxAttempts: Int) {
var attempt = 0
while (attempt < maxAttempts) {
delay(1000L * (attempt + 1)) // linear backoff
try {
reader?.connect()
return
} catch (e: Exception) {
attempt++
}
}
onReconnectExhausted()
}Surface a visible UI state for "reconnecting" versus "disconnected, action needed" — an operator mid-shift needs to know whether to wait or physically re-pair the sled.
Reading tags
Once connected, inventory reads arrive through the same event listener rather than a polling loop, which is the main structural difference from UART-based handhelds like the Chainway line.
fun startInventory() {
reader?.Actions?.Inventory?.perform()
}
fun stopInventory() {
reader?.Actions?.Inventory?.stop()
}
// in eventReadNotify:
override fun eventReadNotify(e: RfidReadEvents?) {
val tagData = reader?.Actions?.getReadTags(100) ?: return
tagData.forEach { tag ->
onTagRead(tag.tagID, tag.peakRSSI)
}
}Production considerations
- Build the reconnection state machine before anything else. It's the single biggest gap between the tutorial and a shift-length deployment.
- Release the reader connection explicitly (
reader?.disconnect()/reader?.Dispose()) inonPause— a sled left connected to a backgrounded app can block a different app on the same handheld from acquiring it. - Handle
OperationFailureExceptionandInvalidUsageExceptiondistinctly rather than catching a genericException— API3 uses these to distinguish transient radio-level failures from usage errors that indicate a code bug. - Log disconnect frequency in the field. An RFD40 that disconnects far more than expected on a specific handheld model usually indicates a Bluetooth stack or power-management conflict specific to that host device, not the sled itself.
Common errors
| Error | Cause | Fix |
|---|---|---|
| Sled disconnects repeatedly during a shift | No reconnection handling; tutorial code assumes a stable link | Implement a reconnection state machine with backoff, keyed off DISCONNECTION_EVENT |
OperationFailureException on connect | Sled already claimed by another app or process | Ensure only one app instance holds the connection; explicitly disconnect on app backgrounding |
| Tag reads stop after a backgrounded app resumes | Connection silently dropped while backgrounded, not detected until next read attempt | Check connection state in onResume and reconnect proactively rather than waiting for a read failure |
App crashes with InvalidUsageException | SDK method called before connect() completed successfully | Gate all reader actions behind a confirmed CONNECTION_EVENT, not just a call to connect() |