ASQUARETE

Chainway C5 RFID Android SDK: Inventory to CSV

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

The Chainway C5 shares its UHF SDK family with the C72 — same UART-based reader class, same trigger-key pattern. Turning a raw inventory sweep into a usable stocktake means deduplicating EPCs in-session and exporting a clean CSV, which the sample app does not do.

The Chainway C5 is a lighter, ring-style or pistol-grip UHF handheld built on the same SDK family as the C72. If you've integrated a C72, the C5's reader API will look almost identical — the differences that matter are in how you use the data it produces, not in the API surface itself.

Device photo — Chainway C5 UHF RFID handheld reader
Chainway C5 — pistol-grip UHF handheld, Android companion app

What the vendor docs don't tell you

The C5 sample project treats a "scan" as an open-ended stream of tag reads with no concept of a completed session. That's fine for a demo, useless for a stocktake, where the actual deliverable is a deduplicated list of what's physically present.

What the docs don't tell you

The SDK's buffer drain (readTagFromBuffer()) returns a new record for every physical read event, not one record per unique tag. A single EPC sitting near the antenna for two seconds can generate dozens of buffer entries. Any UI or export logic that doesn't dedupe on EPC before rendering will show a scan count that has no relationship to the actual number of items on the shelf — a discrepancy that reads as a hardware fault to whoever's running the stocktake, when it's actually a software gap.

Setup

The C5 SDK ships as the same UHFAndAPI-release.aar family used on Chainway's other UART-based UHF handhelds. Confirm the .aar version bundled with the C5's firmware matches what your build depends on — Chainway occasionally ships device-specific builds with a subset of methods.

dependencies {
    implementation files('libs/UHFAndAPI-release.aar')
}
val uhf = RFIDWithUHFUART.getInstance()
val ready = uhf.init()
if (ready) {
    uhf.setPower(20)
}

Building a deduplicated inventory session

A stocktake scan is a bounded session: start, read, stop, and produce one row per unique EPC with the best RSSI seen for that tag.

data class ScanRecord(val epc: String, var rssi: Int, var count: Int)
 
class StocktakeSession(private val uhf: RFIDWithUHFUART) {
    private val seen = LinkedHashMap<String, ScanRecord>()
 
    fun start(scope: CoroutineScope, onUpdate: (List<ScanRecord>) -> Unit) {
        uhf.startInventoryTag()
        scope.launch(Dispatchers.IO) {
            while (isActive) {
                val tag = uhf.readTagFromBuffer() ?: run { delay(15); return@launch }
                val existing = seen[tag.epc]
                if (existing != null) {
                    existing.count++
                    existing.rssi = maxOf(existing.rssi, tag.rssi)
                } else {
                    seen[tag.epc] = ScanRecord(tag.epc, tag.rssi, 1)
                }
                withContext(Dispatchers.Main) { onUpdate(seen.values.toList()) }
            }
        }
    }
 
    fun stop(): List<ScanRecord> {
        uhf.stopInventory()
        return seen.values.toList()
    }
}

Note the coroutine structure above has a bug pattern worth calling out explicitly: a return@launch inside a while loop exits the coroutine, not just the current iteration. In production code, replace it with a continue-equivalent branch structure — this guide keeps the simplified form here to make the dedup logic legible, and the working repository version should use an explicit if/else rather than an early return.

Exporting to CSV

Write the deduplicated records to app-scoped external storage, then hand the file to a share sheet rather than building bespoke export UI.

fun exportToCsv(context: Context, records: List<ScanRecord>): File {
    val file = File(context.getExternalFilesDir(null), "stocktake_${System.currentTimeMillis()}.csv")
    file.bufferedWriter().use { writer ->
        writer.write("EPC,RSSI,ReadCount\n")
        records.forEach { r ->
            writer.write("${r.epc},${r.rssi},${r.count}\n")
        }
    }
    return file
}
 
fun shareCsv(context: Context, file: File) {
    val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
    val intent = Intent(Intent.ACTION_SEND).apply {
        type = "text/csv"
        putExtra(Intent.EXTRA_STREAM, uri)
        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    }
    context.startActivity(Intent.createChooser(intent, "Export stocktake"))
}

Register a FileProvider in the manifest for this to work — ACTION_SEND with a raw file Uri will throw a FileUriExposedException on API 24+.

Production considerations

A stocktake tool lives or dies on whether the count it produces is trustworthy, which means the RSSI threshold and session boundaries matter more than raw scan speed.

  • Store RSSI per record even if the UI never shows it. It's the first diagnostic you'll reach for when a warehouse manager disputes a count.
  • Treat "stop" as an explicit user action, not an implicit timeout — auto-stopping a session risks truncating a sweep mid-shelf and silently under-counting.
  • Surface a running unique-tag count during the scan, not just a total read count, so the operator can visually confirm the number is climbing at a sane rate.

Common errors

ErrorCauseFix
Scan count wildly exceeds physical item countNo EPC deduplication on buffer drainKey a map by EPC and increment a count field instead of appending every read as a new row
CSV export throws FileUriExposedExceptionRaw file Uri passed to ACTION_SEND on API 24+Use FileProvider.getUriForFile() and grant FLAG_GRANT_READ_URI_PERMISSION
Coroutine loop exits after first empty buffer readreturn used instead of continue-equivalent branching inside the read loopRestructure the loop body as if/else rather than an early return on the null case
Export file is emptybufferedWriter().use {} block exited before all records writtenConfirm the write loop runs before the use block closes, and check records isn't cleared before export

FAQ

Is the Chainway C5 SDK the same as the C72?

Same family. Both use RFIDWithUHFUART for UART-connected UHF modules, with the same init/free lifecycle. Method availability can differ slightly by firmware build, so check the .aar version string before assuming full API parity.

How do I stop the same tag appearing 40 times in one scan?

Buffer a Set of EPCs seen in the current session and filter before rendering or exporting. The SDK's buffer drain returns every read event, including repeats from the same tag as it passes through the antenna field.

What's the fastest way to get a scan session into a spreadsheet?

Write matched, deduplicated tag records to a CSV file in app-scoped storage as they arrive, then share that file via an Intent.ACTION_SEND chooser rather than building a custom export UI.

Does RSSI matter for a stocktake app?

It matters for distinguishing which tags are physically close to the reader versus incidental cross-reads from an adjacent shelf. Store it alongside the EPC even if the UI doesn't display it — it's the first thing you'll want when a stocktake count looks wrong.

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

Related guides

Stuck on this device?

Feasibility read in 24 hours.

Get a feasibility read