Интеграция RFID-считывателя через Bluetooth в мобильное приложение
RFID-считыватели с BLE-интерфейсом — это отдельный класс устройств: Zebra RFD40/RFD90, Chainway R6, TSL 1128. Они соединяются с телефоном по BLE SPP-профилю (Serial Port Profile) или кастомному GATT-сервису и стримят считанные EPC/UID-теги. Интеграция проще чем кажется, если понимать протокол конкретного ридера.
BLE SPP vs GATT
Большинство Bluetooth RFID-ридеров работают через два режима:
BLE SPP (Serial Port Profile over BLE) — эмулируют последовательный порт. Данные идут в потоке через кастомные GATT-характеристики с UUID производителя. Nordic UART Service (NUS) — самый распространённый: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E.
Стандартный GATT — некоторые ридеры реализуют проприетарный GATT-сервис с отдельными характеристиками для команд и ответов.
Для Zebra RFD40 — Nordic UART Service:
val NORDIC_UART_SERVICE = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
val NORDIC_UART_RX = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E") // write
val NORDIC_UART_TX = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E") // notify
fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
val service = gatt.getService(NORDIC_UART_SERVICE) ?: return
// Подписка на TX (получение данных от ридера)
val txChar = service.getCharacteristic(NORDIC_UART_TX)
gatt.setCharacteristicNotification(txChar, true)
val descriptor = txChar.getDescriptor(CCCD_UUID)
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
}
fun sendCommand(command: ByteArray) {
val rxChar = bluetoothGatt?.getService(NORDIC_UART_SERVICE)
?.getCharacteristic(NORDIC_UART_RX) ?: return
// Если команда > MTU-3 байт, нужна фрагментация
if (command.size > negotiatedMtu - 3) {
sendFragmented(command)
} else {
rxChar.value = command
bluetoothGatt?.writeCharacteristic(rxChar)
}
}
Парсинг RFID-ответов
Протокол зависит от ридера. Zebra RFD40 возвращает ASCII-строки в формате EPC: <hex>\r\n. TSL 1128 использует бинарный пакет с заголовком и CRC. Пример для ASCII-протокола:
private val dataBuffer = StringBuilder()
fun onCharacteristicChanged(value: ByteArray) {
dataBuffer.append(String(value, Charsets.UTF_8))
// Данные приходят фрагментами — ждём полную строку
while (dataBuffer.contains('\n')) {
val lineEnd = dataBuffer.indexOf('\n')
val line = dataBuffer.substring(0, lineEnd).trim()
dataBuffer.delete(0, lineEnd + 1)
if (line.startsWith("EPC:")) {
val epc = line.removePrefix("EPC:").trim()
onTagRead(epc)
}
}
}
Критично: BLE пакеты могут приходить разделёнными — одна строка в нескольких onCharacteristicChanged уведомлениях. Всегда буферизировать до разделителя (обычно \r\n).
Zebra SDK как альтернатива
Для Zebra-устройств (RFD40, RFD90, RFD8500) — официальный EMDK for Android или Zebra RFID SDK:
implementation("com.zebra.rfid.api3:rfid-api3:2.0.2.109")
val readers = Readers(context, ENUM_TRANSPORT.BLUETOOTH)
readers.GetAvailableRFIDReaderList()?.forEach { readerDevice ->
val reader = readerDevice.RFIDReader
reader.connect()
reader.Events.addEventsListener(rfidEventsListener)
reader.Events.setHandheldEvent(true)
reader.Events.setTagReadEvent(true)
reader.Actions.Inventory.perform() // начать непрерывное сканирование
}
private val rfidEventsListener = object : RfidEventsListener {
override fun eventReadNotify(e: RfidReadEvents) {
e.ReadEventData.TagData.forEach { tag ->
Log.d("RFID", "EPC: ${tag.TagID}, RSSI: ${tag.PeakRSSI}")
}
}
override fun eventStatusNotify(rfidStatusEvents: RfidStatusEvents) {
// STATUS_EVENT_BATTERY_LOW и другие статусы
}
}
RSSI из Zebra SDK даёт приблизительную дистанцию до тега — полезно для приоритизации при одновременном считывании нескольких тегов в поле.
Сроки
Интеграция по Nordic UART Service или аналогичному кастомному GATT с парсингом ASCII-протокола: 5 дней. Интеграция через Zebra/Chainway SDK с расширенными функциями (inventory, RSSI, write to tag): 1–2 недели.







