● CASE STUDY · IDENTIUM TECH SOLUTIONS
Identium Pallet Tracking
Track reusable pallets in and out of warehouses with UHF RFID
OVERVIEW
Reusable pallets get lost because nobody records the handover. This system makes the record a by-product of scanning: an operator pulls the trigger, the tags land on the note, and saving it moves stock. A dispatch puts a pallet In-Transit while its current location stays at the origin, and a receive books it in at the far end and stamps the receive line with the dispatch note it closes. Because that link is captured at scan time, reconciliation is a plain GROUP BY (dispatched minus received equals outstanding, per note) instead of guessing from timestamps.
THE CHALLENGE
Most of the work was making the radio behave. The handheld read nothing at first for three separate reasons: the trigger keycode list was missing 292, key events were handled in onKeyDown which never fires while a text field has focus, and Android's optString turns a JSON null into the four character string "null", so the server's unset tag_prefix became the EPC filter NULL and every tag was silently discarded. Adding the Bluetooth MUBR01 later broke the scan screens again, because they resolved the reader engine once in onViewCreated and those fragments are only hidden and shown, never rebuilt, so they kept talking to the terminal's own module and reported "reader is not connected" while the console worked fine. The other hard part was deciding what a sweep in a busy yard is allowed to save: unregistered tags are excluded by default but stay visible and faded rather than vanishing, and a tag the server never answered for stops the save and asks, because dropping one the handheld could not check would lose a real movement.
WHAT I BUILT
- 01
Movement engine in Express and MySQL: POST /api/transactions mints the GRN number under a row lock, locks the pallet rows FOR UPDATE, writes the header, one item row per EPC and every pallet update in one transaction, and returns per tag warnings (already in transit, not at source, wrong destination, unregistered) without blocking the save.
- 02
Offline outbox on the handheld: every movement carries a client_txn_id UUID, a failed POST is stored in Room and replayed by Sync, and a unique index on that column makes the server return the original GRN instead of creating a second one, so a retry can never double count stock.
- 03
Two reader engines behind one ScanEngine interface: the CX1500N built-in UHF module and a Bluetooth MUBR01. All module calls run on a single serialized lane so a start and the stop behind it cannot be reordered, tags aggregate on the SDK thread and screens poll a snapshot, and RF power is clamped to each radio's own reported min and max.
- 04
Live scan feed: the handheld POSTs each drained sweep to /api/scans/live, which persists nothing, broadcasts scan:live over Socket.IO and returns the pallet code for every EPC in one request. Web Dispatch and Receive filter the burst to their own direction and location so two bays scanning at once do not cross contaminate.
- 05
Reader console on the handheld for field diagnosis: live firmware, temperature and the power, region, Gen2 session and target the module is actually running, read back from the hardware, plus a test bench with hit counts, colour coded RSSI and the last trigger event printed so a dead trigger is visible at once.
- 06
Web control tower: dashboard with per location on-hand and outbound, 14 day throughput and a stale in transit count, pallet register with CSV bulk import that reports errors by row number, location and user masters with per user location mapping, and dispatch, receive and reconciliation reports with an XLSX export.
OUTCOME
Running in production on a VPS under PM2 with the API and the SPA on one port. Confirmed on real hardware: a 143 tag dispatch sweep on the CX1500N, a 69 tag sweep streamed to the browser in 15 live bursts, and a MUBR01 dispatch reading two tags with the unregistered one correctly held back from the note.