The Chainway C72 is a widely resold Android handheld UHF terminal built around an Impinj E710/E900-class module, and it ships with a demo APK and a source bundle that most integrators copy from directly. That bundle is the fastest way to end up with a reader that silently stops responding after a warm restart.
What the vendor docs don't tell you
Chainway's own SDK documentation and demo repositories still reference RFIDWithUHF as the entry point for UHF operations. Open the SDK's own Javadoc-equivalent comments, though, and the class carries a deprecation notice pointing to RFIDWithUHFUART as the replacement.
What the docs don't tell you
RFIDWithUHF and RFIDWithUHFUART are not interchangeable at the call-site level — RFIDWithUHFUART changes the constructor pattern to a singleton (getInstance()), changes several method signatures around tag buffer draining, and requires an explicit UART port free on teardown that the old class handled implicitly. Copying a RFIDWithUHF sample forward means inheriting silent buffer starvation under sustained inventory sessions — the read loop keeps running, but readTagFromBuffer() starts returning null well before the antenna stops actually seeing tags.
Setup
Add the Chainway SDK .aar to app/libs/ and declare it in build.gradle:
dependencies {
implementation files('libs/UHFAndAPI-release.aar')
implementation files('libs/DeviceAPI-release.aar')
}Request the runtime permissions the reader stack needs before touching the device:
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.hardware.usb.host" />Initializing the reader
Initialization happens once, against the singleton instance, and must be checked for a boolean success result before any inventory call is made.
class RfidManager {
private var uhf: RFIDWithUHFUART? = null
fun init(): Boolean {
uhf = RFIDWithUHFUART.getInstance()
val ok = uhf?.init() ?: false
if (ok) {
uhf?.setPower(22) // dBm, 5–30 valid range
}
return ok
}
fun release() {
uhf?.free()
uhf = null
}
}Call release() from onPause(), not just onDestroy(). Android does not guarantee onDestroy() runs before the next process starts, and a UART port left open across an app swap comes back as an init() failure with no further diagnostic detail.
Reading tags with a coroutine loop
Inventory in the C72 SDK is buffer-based: startInventoryTag() starts the antenna sweeping, and readTagFromBuffer() drains whatever the firmware queued since the last drain. A tight polling loop on a background coroutine keeps the buffer from overflowing under a fast tag population.
class InventoryReader(private val uhf: RFIDWithUHFUART) {
private var job: Job? = null
fun start(scope: CoroutineScope, onTag: (UHFTagInfo) -> Unit) {
uhf.startInventoryTag()
job = scope.launch(Dispatchers.IO) {
while (isActive) {
val tag = uhf.readTagFromBuffer()
if (tag != null) {
withContext(Dispatchers.Main) { onTag(tag) }
} else {
delay(15)
}
}
}
}
fun stop() {
job?.cancel()
uhf.stopInventory()
}
}Do not call readTagFromBuffer() on the main thread in a while(true) loop directly — it will not throw, but it will visibly freeze the UI thread on any inventory session longer than a few hundred milliseconds.
Wiring the hardware trigger
The C72's side trigger does not surface as a UI event. It arrives as a KeyEvent on the hosting Activity, and the specific keycode varies across firmware batches — most C72 units use 139 or 280.
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == 139 || keyCode == 280) {
inventoryReader.start(lifecycleScope) { tag -> viewModel.onTagRead(tag) }
return true
}
return super.onKeyDown(keyCode, event)
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == 139 || keyCode == 280) {
inventoryReader.stop()
return true
}
return super.onKeyUp(keyCode, event)
}Debounce the key-down handler if the firmware repeats the event while the trigger is held — most batches do, at roughly the standard Android key-repeat interval, which will otherwise restart the inventory session mid-scan.
Production considerations
Production deployments need three things the demo app skips entirely: lifecycle-safe teardown, power tuning per deployment environment, and defensive handling of init() failure.
- Always pair
init()with a correspondingfree()inonPause, and guard every SDK call with a null-check on the reader instance — a backgrounded app can be killed by the OS betweenonPauseandonStop. - Tune antenna power to the read zone, not the maximum. 22 dBm is a reasonable default for handheld point-and-scan use; dense shelf environments should start lower and step up rather than start at 30 dBm and fight cross-reads.
- Treat
init()returningfalseas an expected runtime state, not an exceptional one — it happens whenever the UART port is still held by a previous process, and the correct recovery is a bounded retry with backoff, not a crash.
Common errors
| Error | Cause | Fix |
|---|---|---|
init() returns false on second launch | UART port still held by previous process | Call uhf.free() in onPause(), not only onDestroy(); add a retry with backoff before surfacing an error to the user |
readTagFromBuffer() returns null indefinitely | Inventory session not started, or antenna power set to 0 | Confirm startInventoryTag() was called and setPower() was set above 5 dBm before polling |
| Trigger key does nothing | Wrong keycode assumed for this firmware batch | Log event.keyCode on any unhandled key event during a test session and confirm against 139/280 for this specific unit |
| App freezes during scan | Buffer drain loop running on the main thread | Move the polling loop to Dispatchers.IO and post results back via withContext(Dispatchers.Main) |
| Duplicate EPCs flood the UI | No dedup applied to buffer output | Maintain a rolling Set<String> of seen EPCs with a TTL, and filter before dispatching to the UI layer |