Asquarete

Chainway C72 Android SDK Integration in Kotlin

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.

Device
Chainway C72
Platform
Android
Topic
Device SDK
Updated
4 Sept 2026

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.

Photograph pending — 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

Two things bit us on the C72 that the SDK never mentions.

The reader isn't ready when init() returns. Calling startInventoryTag() or entering locate mode immediately after initialising gives you a reader that accepts the call and then does nothing. The SDK needs to finish bringing the radio up first, and it does not tell you when that is. Gate the first command on initialisation actually completing rather than on init() having returned.

The EPC does not start where you expect it in locate mode. We read the tag identifier at a bit offset of 2 — which is what a naive reading of the buffer layout suggests — and got identifiers that looked plausible and matched nothing. The correct offset was 32. The failure mode is the dangerous kind: no error, no exception, just an EPC that is quietly wrong, so locate walks you toward a tag that isn't the one you asked for.

Both cost real hours, and neither appears in any Chainway document we have seen.

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.

If you are also exporting to Excel

Two things that cost us time on the same build, adjacent to the reader itself:

  • Apache POI on Android needs explicit R8 keep rules for the BEA XML stream classes (aavax.xml.stream), or XLSX parsing fails only in release builds — never in debug, which is where you will be testing.
  • Writing an export to shared storage behaves differently across API 27 through 35. Test the export path on the oldest device in the fleet, not just the newest, and write UTF-8 with a BOM if the file will be opened in Excel.

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
Locate mode walks toward the wrong tagEPC read at the wrong bit offset in the buffer — 2 instead of 32Read the identifier at offset 32; there is no error raised, so verify a known tag's EPC by hand before trusting locate
First command after init() silently does nothingThe radio has not finished coming up; init() returning is not the same as readyWait for initialisation to complete before issuing the first inventory or locate command
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.

Written by Efron, who has this device on the bench.

Hardware integration engineer at Asquarete. Published 18 Jul 2026, last revised 4 Sept 2026.

Related guides

Stuck on this device?

Send the model, the SDK version and what's failing. You get a written read on whether it's solvable within 24 hours, before any money moves.

Get a feasibility read