Стандартный штрихкод требует прямой видимости и поштучного сканирования — это неэффективно при массовой инвентаризации. RFID позволяет считать 200 тегов за три секунды, просто пройдя вдоль стеллажа. Мы разрабатываем мобильные приложения, которые принимают поток EPC-кодов без потерь, дедуплицируют их и сверяют с ожидаемым списком — всё в реальном времени, даже без интернета. Наш опыт — 5 лет в мобильной разработке, более 30 проектов в логистике и инвентаризации.
Например, на складе с металлическими стеллажами UHF-теги часто не читаются из-за отражений. Мы настраиваем параметры считывателя: мощность, поляризацию, фильтры. В одном проекте удалось снизить процент пропусков с 15% до 2% за счет подбора антенн и конфигурации. Это особенно важно для металлических поверхностей, где стандартные настройки не работают.
Проблемы, которые решаем
Дубликаты reads — ридер может считать один тег 50+ раз за сессию. Нужна быстрая дедупликация без блокировок UI. Офлайн-режим — склады часто без Wi-Fi. База данных должна быть локальной с последующей синхронизацией. Расхождения — часть тегов может не читаться из-за повреждений или плохого расположения. Нужно явно показывать найденные, пропущенные и лишние позиции. Особенно критично для складов с металлическими стеллажами, где UHF-теги читаются хуже.
RFID-сканирование в 100 раз быстрее, чем ручной ввод или штрихкод при проверке сотен позиций. Но без правильной обработки данных это преимущество теряется.
Как дедуплицировать EPC-теги без потерь?
Сессия инвентаризации — конечный автомат с переходами: IDLE -> SCANNING -> PROCESSING -> COMPLETED, с возможностью паузы. На каждый прочитанный тег — update в MutableStateFlow с дедупликацией по EPC:
class InventorySession(private val expectedItems: List<InventoryItem>) {
private val _scannedEpcs = MutableStateFlow<Set<String>>(emptySet())
val scannedEpcs: StateFlow<Set<String>> = _scannedEpcs.asStateFlow()
val matchedItems = scannedEpcs.map { epcs ->
expectedItems.filter { it.epc in epcs }
}.stateIn(scope, SharingStarted.Eagerly, emptyList())
val missingItems = scannedEpcs.map { epcs ->
expectedItems.filter { it.epc !in epcs }
}.stateIn(scope, SharingStarted.Eagerly, emptyList())
val unexpectedEpcs = scannedEpcs.map { epcs ->
val knownEpcs = expectedItems.map { it.epc }.toSet()
epcs.filter { it !in knownEpcs }
}.stateIn(scope, SharingStarted.Eagerly, emptyList())
fun onTagRead(epc: String) {
_scannedEpcs.update { current -> current + epc }
}
fun reset() {
_scannedEpcs.value = emptySet()
}
}
Set<String> — автоматическая дедупликация. Один EPC может прийти 50+ раз, но в Set попадёт один раз. Производные стейты (найденные, пропущенные, лишние) вычисляются реактивно через map.
Почему офлайн-синхронизация критична для склада?
Мы гарантируем, что приложение работает без сети. Локальная БД через Room хранит ожидаемый список и результаты:
@Entity(tableName = "inventory_sessions")
data class InventorySessionEntity(
@PrimaryKey val sessionId: String,
val locationId: String,
val startedAt: Long,
val completedAt: Long?,
val status: String // "in_progress", "completed", "synced"
)
@Entity(tableName = "scanned_tags")
data class ScannedTagEntity(
@PrimaryKey val epc: String,
val sessionId: String,
val firstSeenAt: Long,
val readCount: Int
)
readCount — количество reads одного тега за сессию. Аномально низкий (1–2) при том что соседние теги читались 20+ раз — признак плохого физического расположения тега или повреждения. Полезная метрика для QA.
После завершения сессии — синхронизация через WorkManager при появлении сети:
val syncRequest = OneTimeWorkRequestBuilder<InventorySyncWorker>()
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.setInputData(workDataOf("session_id" to sessionId))
.build()
workManager.enqueueUniqueWork("sync_$sessionId", ExistingWorkPolicy.KEEP, syncRequest)
Как отображать результаты в реальном времени?
LazyColumn с key(item.epc) — анимированное добавление найденных позиций:
@Composable
fun InventoryResultsScreen(session: InventorySession) {
val matched by session.matchedItems.collectAsState()
val missing by session.missingItems.collectAsState()
val scanned by session.scannedEpcs.collectAsState()
Column {
LinearProgressIndicator(
progress = { if (session.expectedItems.isEmpty()) 0f
else matched.size.toFloat() / session.expectedItems.size }
)
Text("Найдено: ${matched.size}/${session.expectedItems.size}")
LazyColumn {
items(matched, key = { it.epc }) { item ->
InventoryItemRow(item = item, status = ItemStatus.FOUND)
}
items(missing, key = { it.epc }) { item ->
InventoryItemRow(item = item, status = ItemStatus.MISSING)
}
}
}
}
GS1 EPC декодирование
EPC — это не просто hex-строка. Структурированный код urn:epc:id:sgtin:0614141.107346.2017 содержит компанию, артикул и серийный номер. Декодирование через SGTIN-96:
Пример кода декодирования SGTIN-96
fun decodeSgtin96(epc: String): Sgtin96? {
val bytes = epc.chunked(2).map { it.toInt(16) }.toByteArray()
val bits = BigInteger(1, bytes)
val header = bits.shiftRight(88).and(BigInteger.valueOf(0xFF)).toInt()
if (header != 0x30) return null
val filter = bits.shiftRight(85).and(BigInteger.valueOf(0x07)).toInt()
val partition = bits.shiftRight(82).and(BigInteger.valueOf(0x07)).toInt()
// далее по partition table
}
Готовая библиотека: com.gs4tr.epcis:epcis-rest-client или org.fosstrak.epcis:epcis-repository-client. Подробнее см. GS1 EPC Tag Data Standard.
Сравнение RFID и штрихкодов
| Параметр | Штрихкод | RFID |
|---|---|---|
| Скорость сканирования | 1 предмет/c | 200 тегов за 3 с |
| Необходимость прямой видимости | Да | Нет |
| Перезапись данных | Нет | Да (некоторые теги) |
| Помехоустойчивость | Высокая | Средняя (металл, жидкость) |
| Стоимость метки | 0.01$ | 0.05–0.5$ |
RFID в 10-100 раз быстрее при массовом сканировании, но требует предварительной настройки и подбора оборудования.
Сравнение методов декодирования EPC
| Метод | Производительность | Поддержка GS1 | Сложность интеграции |
|---|---|---|---|
| SGTIN-96 вручную | Высокая (native) | Полная | Средняя |
| Библиотека EPCIS | Средняя (HTTP) | Полная | Низкая |
| Облачный сервис | Низкая (REST) | Частично | Минимальная |
Для мобильного приложения оптимален первый вариант — минимум зависимостей и максимальная скорость.
Что входит в работу
- Проектирование архитектуры конечного автомата инвентаризации
- Разработка модуля дедупликации на
StateFlow - Реализация офлайн-хранилища на Room
- Настройка синхронизации через WorkManager
- Интеграция с BLE-ридером (Zebra, кастомный)
- Декодирование GS1 EPC и валидация
- UI на Jetpack Compose с анимированным прогрессом
- Code signing, provisioning, публикация в App Store и Google Play
- Документация и обучение операторов
Оцените, как RFID-инвентаризация сократит время вашего склада — получите консультацию наших инженеров.
Процесс работы
- Аналитика — изучаем процессы склада, типы тегов, WMS.
- Прототип — MVP за 3-5 дней с базовым функционалом.
- Разработка — итеративные спринты по 2 недели.
- QA — нагрузочное тестирование с реальным ридером.
- Деплой — публикация в сторах и передача.
Сроки
Мобильное приложение инвентаризации с Zebra/кастомным BLE-ридером, офлайн Room, GS1 декодированием и синхронизацией: от 5 дней (простой склад, один ридер, один тип тегов) до 2–3 недель (multi-location, несколько типов тегов, кастомная EPC-схема, REST-интеграция с WMS).
Свяжитесь с нами для оценки вашего проекта — ответим в течение дня. Закажите консультацию, и мы покажем, как внедрить RFID-инвентаризацию на вашем складе. Мы гарантируем прохождение App Store Review Guidelines и Google Play политик, а также полную поддержку на всех этапах.







