ASQUARETE

Chainway C72 Android SDK Integration in Kotlin

Chainway C72 · Android · Updated Jul 18, 2026 · Device SDK

Chainway's C72 sample code targets RFIDWithUHF, a class the SDK itself marks obsolete. Production integrations must use RFIDWithUHFUART, handle the trigger key event directly, and explicitly release the reader in onPause to avoid UART lock errors on the next launch.

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.

Device photo — Chainway C72 Android UHF RFID handheld terminal
Chainway C72 — Android 11, integrated UHF antenna, side trigger

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 corresponding free() in onPause, and guard every SDK call with a null-check on the reader instance — a backgrounded app can be killed by the OS between onPause and onStop.
  • 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() returning false as 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

ErrorCauseFix
init() returns false on second launchUART port still held by previous processCall uhf.free() in onPause(), not only onDestroy(); add a retry with backoff before surfacing an error to the user
readTagFromBuffer() returns null indefinitelyInventory session not started, or antenna power set to 0Confirm startInventoryTag() was called and setPower() was set above 5 dBm before polling
Trigger key does nothingWrong keycode assumed for this firmware batchLog event.keyCode on any unhandled key event during a test session and confirm against 139/280 for this specific unit
App freezes during scanBuffer drain loop running on the main threadMove the polling loop to Dispatchers.IO and post results back via withContext(Dispatchers.Main)
Duplicate EPCs flood the UINo dedup applied to buffer outputMaintain a rolling Set<String> of seen EPCs with a TTL, and filter before dispatching to the UI layer

FAQ

Which Chainway SDK class should a new C72 project use?

RFIDWithUHFUART, accessed through RFIDWithUHFUART.getInstance(). RFIDWithUHF, the class in most of Chainway's published sample apps, is deprecated and should not be used in new code.

Why does the C72 trigger button not fire a click listener?

The trigger is a hardware key, not a view. It arrives as a KeyEvent (keycode 139 or 280 depending on firmware) through onKeyDown/onKeyUp on the Activity, not through a View.OnClickListener.

Why does the reader fail to init on the second app launch?

The UART port was not released on the previous exit. Call uhf.free() in onPause/onDestroy every time, including on process death paths like a crash or force-stop, or the port stays locked until reboot.

What antenna power range is safe for the C72?

5–30 dBm on most C72 firmware. Above 26 dBm, expect increased false reads from adjacent tags in dense deployments; most production configs settle between 20–24 dBm.

Asquarete · Published Jul 18, 2026 · Updated Jul 18, 2026

Related guides

Stuck on this device?

Feasibility read in 24 hours.

Get a feasibility read