Integrating UHF RFID Readers (Long-Range) into Mobile Applications
UHF RFID (860–960 MHz) — different world compared to HF NFC/MIFARE. Read distance 3–10 meters, mass reading hundreds of tags per second, passive tags costing $0.05–0.15 each. Zebra RFD40, Chainway R6, Bluebird EF500 — mobile UHF readers with BLE or USB connection. Integrating these devices into mobile apps has specifics: LLRP protocol, EPC C1G2 standard, antenna power management.
LLRP vs Proprietary SDK
LLRP (Low Level Reader Protocol, ISO 15961) — standard protocol for managing UHF readers. Supported by Impinj, Zebra, Alien. Allows using one code for different readers. For mobile app via Wi-Fi connection to reader.
Proprietary SDK — Zebra RFID SDK, Chainway SDK. Easier to use, locked to specific manufacturer, works via BLE or USB.
For most mobile readers with BLE — proprietary SDK. LLRP — for stationary readers on network.
Zebra RFID SDK: Practice
implementation("com.zebra.rfid.api3:rfid-api3:2.0.2.109")
class UhfReaderManager(private val context: Context) : RfidEventsListener {
private var reader: RFIDReader? = null
private val readers = Readers(context, ENUM_TRANSPORT.BLUETOOTH)
fun connect(deviceName: String) {
val readerDevices = readers.GetAvailableRFIDReaderList()
val targetDevice = readerDevices?.find { it.name == deviceName } ?: return
reader = targetDevice.RFIDReader
reader?.connect()
// Configure read parameters
val params = reader?.Config?.antennaConfigurations?.get(0)
params?.transmitPowerIndex = 270 // ~30 dBm, maximum power
params?.receiveFrequency = 55 // optimal for most regions
reader?.Config?.setAntennaConfiguration(params)
// Events
reader?.Events?.apply {
addEventsListener(this@UhfReaderManager)
setHandheldEvent(true) // reader trigger button
setTagReadEvent(true)
setInventoryStartEvent(true)
setInventoryStopEvent(true)
}
}
// Start inventory with EPC mask filtering
fun startInventoryWithFilter(epcMask: String?) {
val startTrigger = TriggerInfo().apply {
startTrigger.triggerType = START_TRIGGER_TYPE.START_TRIGGER_TYPE_IMMEDIATE
}
val stopTrigger = TriggerInfo().apply {
stopTrigger.triggerType = STOP_TRIGGER_TYPE.STOP_TRIGGER_TYPE_DURATION
stopTrigger.stopTriggerByDuration.duration = 5000 // 5 seconds
}
val tagFilter = if (epcMask != null) {
TagFilter().apply {
tagPattern = epcMask
tagPatternBitCount = epcMask.length * 4
filterAction = FILTER_ACTION.FILTER_ACTION_STATE_AWARE_FILTERING_ACTION_UNSPECIFIED
}
} else null
reader?.Actions?.Inventory?.perform(startTrigger, stopTrigger, tagFilter)
}
override fun eventReadNotify(e: RfidReadEvents) {
e.ReadEventData.TagData.forEach { tag ->
onTagRead(
epc = tag.TagID,
rssi = tag.PeakRSSI.toInt(),
antennaId = tag.AntennaID.toInt(),
readCount = tag.ReadCount
)
}
}
override fun eventStatusNotify(rfidStatusEvents: RfidStatusEvents) {
when (rfidStatusEvents.StatusEventData.statusEventType) {
STATUS_EVENT_TYPE.INVENTORY_START_EVENT -> onInventoryStarted()
STATUS_EVENT_TYPE.INVENTORY_STOP_EVENT -> onInventoryCompleted()
STATUS_EVENT_TYPE.HANDHELD_TRIGGER_EVENT -> {
val triggerType = rfidStatusEvents.StatusEventData.HandheldTriggerEventData.handheldEvent
if (triggerType == HANDHELD_TRIGGER_EVENT_TYPE.HANDHELD_TRIGGER_PRESSED) {
startInventoryWithFilter(null)
}
}
}
}
}
transmitPowerIndex = 270 — power in reader units. Each manufacturer has its own scale. Zebra RFD40: 0–270 = 0–30 dBm. Excessive power in closed warehouse → reflections → false reads from adjacent zones.
Regulatory Frequency Limitations
UHF RFID operates in different bands:
- USA/Canada (FCC): 902–928 MHz
- Europe/Russia (ETSI): 865–868 MHz
- China: 920–925 MHz
Mobile app should configure regional reader band:
reader?.Config?.setRegulatoryConfig(
RegulatoryConfig().apply {
region = REGULATORY_REGION.REGULATORY_REGION_ETSI // or FCC, China
enableHoppingChannels = true
}
)
Using FCC band in Russia — regulatory violation. Readers with regional firmware locks won't allow wrong region, but worth checking.
Performance: 500 Tags Per Second
UHF reader can read hundreds of tags per second. Tag stream cannot be processed on main thread — UI freeze:
private val tagChannel = Channel<TagReadData>(capacity = Channel.UNLIMITED)
// In BroadcastReceiver/reader callback — just put in channel
override fun eventReadNotify(e: RfidReadEvents) {
e.ReadEventData.TagData.forEach { tag ->
tagChannel.trySend(tag) // non-blocking, no exception
}
}
// Process in background coroutine
fun processTagsInBackground() {
scope.launch(Dispatchers.Default) {
for (tag in tagChannel) {
val epc = tag.TagID
inventoryRepository.recordRead(epc, tag.PeakRSSI.toInt())
}
}
}
Timeline
Zebra/Chainway UHF BLE reader integration with basic inventory and tag display: 5 days. Extended solution with regional settings, filtering, EPC decoding, and WMS sync: 1–2 weeks.







