Реалізація інвентаризації через RFID-сканер у мобільному додатку

TRUETECH займається розробкою, підтримкою та обслуговуванням мобільних додатків iOS, Android, PWA. Маємо великий досвід та експертизу для публікації мобільних додатків до популярних маркетів Google Play, App Store, Amazon, AppGallery та інші.

Розробка та підтримка будь-яких видів мобільних додатків:

Інформаційні та розважальні мобільні програми
Новинки, ігри, довідники, онлайн-каталоги, погодні, фітнес та здоров'я, туристичні, освітні, соціальні мережі та месенджери, квіз, блоги та подкасти, форуми, агрегатори
Мобільні програми електронної комерції
Інтернет-магазини, B2B-додатки, маркетплейси, онлайн-обмінники, кешбек-сервіси, біржі, дропшиппінг-платформи, програми лояльності, доставка їжі та товарів, платіжні системи
Мобільні програми для управління бізнес-процесами
CRM-системи, ERP-системи, управління проектами, інструменти для команди продажів, облік фінансів, управління виробництвом, логістика та доставка, управління персоналом, системи моніторингу даних
Мобільні програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, платформи надання електронних послуг, платформи кешбеку, відеохостинги, тематичні портали, платформи онлайн-бронювання та запису, платформи онлайн-торгівлі

Це лише деякі з типів мобільних додатків, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 1735 послуг
Реалізація інвентаризації через RFID-сканер у мобільному додатку
Середній
~5 днів
Часті запитання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_mobile-applications_feedme_467_0.webp
    Розробка мобільного додатка для компанії FEEDME
    792
  • image_mobile-applications_xoomer_471_0.webp
    Розробка мобільного додатку для компанії XOOMER
    671
  • image_mobile-applications_rhl_428_0.webp
    Розробка мобільного додатку для компанії RHL
    1097
  • image_mobile-applications_zippy_411_0.webp
    Розробка мобільного додатку для компанії ZIPPY
    969
  • image_mobile-applications_affhome_429_0.webp
    Розробка мобільного додатку для компанії Affhome
    914
  • image_mobile-applications_flavors_409_0.webp
    Розробка мобільного додатку для компанії FLAVORS
    495

Реалізація RFID-інвентаризації у мобільних додатках

RFID-інвентаризація відрізняється від штрихкодів — це масове одночасне читання. Оператор складу проходить зчитувачем вздовж полиці і читає 200 тегів за 3 секунди. Задача мобільного додатка: отримати цей потік тегів без втрат, дедублювати (один тег може прочитатися багато разів), порівняти з очікуваним списком і показати розбіжності. Здається просто, поки не зіткнетеся з дублікатами, тегами поза сесією та офлайн-вимогами.

Архітектура RFID-інвентаризації

Сесія інвентаризації — кінцевий автомат з чіткими переходами:

IDLE → SCANNING → PROCESSING → COMPLETED
              ↓
           PAUSED → SCANNING

На кожен прочитаний тег — оновлення у 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.

Відображення результатів у реальному часі

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 {
        // Прогрес: X з Y знайдено
        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)
            }
        }
    }
}

Офлайн-режим та синхронізація

На складах часто немає Wi-Fi. Архітектура: локальна база Room як джерело істини, синхронізація через WorkManager при відновленні з'єднання. Конфлікти при злитті вирішуються стратегією «остання запис перемагає» з timestamp на сервері або через чергу операцій з ідемпотентними ключами.

Типова проблема — транзакції при масовому приймані товару. Якщо користувач сканував 200 позицій і додаток упав на 150-й, потрібно або відкотити все, або продовжити з точки зупину. Room підтримує транзакції через @Transaction, але межа «що вважати завершеною операцією» повинна бути явно визначена на рівні бізнес-логіки.

@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 — кількість читань одного тегу за сесію. Аномально низький (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)

GS1 EPC декодування

EPC — це не просто hex-рядок. Структурований код: urn:epc:id:sgtin:0614141.107346.2017 (SGTIN — Serialized GTIN, глобальний торговий номер з серійним номером). Декодування через GS1 EPC Information Services:

// 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 // Не SGTIN-96

    val filter = bits.shiftRight(85).and(BigInteger.valueOf(0x07)).toInt()
    val partition = bits.shiftRight(82).and(BigInteger.valueOf(0x07)).toInt()
    // ... далі за partition table: company prefix + item reference + serial
}

Готова бібліотека: com.gs4tr.epcis:epcis-rest-client або org.fosstrak.epcis:epcis-repository-client.

Терміни

Мобільний додаток інвентаризації з Zebra/користувацьким BLE-зчитувачем, офлайн Room, GS1 декодуванням та синхронізацією: 5 днів (простий склад, один зчитувач, один тип тегів) до 2–3 тижнів (багато локацій, кілька типів тегів, користувацька EPC-схема, REST WMS інтеграція).