// SeuicUhfDriver.kt
package com.rfidsoftwares.seuic
import android.content.Context
import android.util.Log
import com.seuic.uhf.EPC
import com.seuic.uhf.UHFService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
data class TagRecord(
val epc: String,
val rssi: Int,
val length: Int,
val timestampMs: Long = System.currentTimeMillis()
)
class SeuicUhfDriver(private val context: Context) {
private var service: UHFService? = null
private var isOpened = false
companion object {
private const val TAG = "SeuicUhfDriver"
const val EPC_BANK = 1
const val TID_BANK = 2
const val USER_BANK = 3
const val RESERVED_BANK = 0
// Crucial: In uhf.jar, offset is in BYTES. CRC (2) + PC (2) = 4 bytes offset
const val EPC_DATA_START_BYTE_OFFSET = 4
}
suspend fun open(powerDbm: Int = 30): Boolean = withContext(Dispatchers.IO) {
try {
if (service == null) {
service = UHFService.getInstance(context.applicationContext)
}
val s = service ?: return@withContext false
val success = s.open()
if (success) {
// Calibrate transmission power (5 to 33 dBm)
s.setPower(powerDbm.coerceIn(5, 33))
isOpened = true
Log.i(TAG, "UHFService opened at $powerDbm dBm")
true
} else {
Log.e(TAG, "UHFService.open() returned false")
false
}
} catch (t: Throwable) {
Log.e(TAG, "Hardware open exception", t)
false
}
}
suspend fun scanSingleTag(timeoutMs: Int = 800): TagRecord? = withContext(Dispatchers.IO) {
val s = service ?: return@withContext null
if (!isOpened) return@withContext null
val epcObj = EPC()
if (s.inventoryOnce(epcObj, timeoutMs)) {
val rawEpc = epcObj.getId()
if (!rawEpc.isNullOrBlank()) {
return@withContext TagRecord(
epc = rawEpc.trim().replace(" ", "").uppercase(),
rssi = epcObj.rssi,
length = epcObj.len
)
}
}
null
}
suspend fun readEpcPayload(
targetEpcHex: String,
byteCount: Int = 12,
accessPasswordHex: String = "00000000"
): ByteArray? = withContext(Dispatchers.IO) {
val s = service ?: return@withContext null
val epcBytes = hexToBytes(targetEpcHex) ?: return@withContext null
val pwdBytes = hexToBytes(accessPasswordHex) ?: ByteArray(4)
val readBuffer = ByteArray(byteCount)
// Read starting at BYTE offset 4 to skip CRC and PC words
val ok = s.readTagData(
epcBytes,
pwdBytes,
EPC_BANK,
EPC_DATA_START_BYTE_OFFSET,
byteCount,
readBuffer
)
if (ok) readBuffer else null
}
suspend fun writeEpcPayload(
targetEpcHex: String,
newPayloadBytes: ByteArray,
accessPasswordHex: String = "00000000"
): Boolean = withContext(Dispatchers.IO) {
val s = service ?: return@withContext false
val epcBytes = hexToBytes(targetEpcHex) ?: return@withContext false
val pwdBytes = hexToBytes(accessPasswordHex) ?: ByteArray(4)
// Write starting at BYTE offset 4 to preserve existing PC word
s.writeTagData(
epcBytes,
pwdBytes,
EPC_BANK,
EPC_DATA_START_BYTE_OFFSET,
newPayloadBytes.size,
newPayloadBytes
)
}
fun close() {
try {
service?.close()
} catch (ignored: Throwable) {
} finally {
isOpened = false
service = null
}
}
private fun hexToBytes(hex: String): ByteArray? {
val clean = hex.replace(" ", "").trim()
if (clean.length % 2 != 0) return null
return ByteArray(clean.length / 2) { i ->
clean.substring(i * 2, i * 2 + 2).toInt(16).toByte()
}
}
}