Three handhelds, each a vendor SDK in a class that implements ReaderAdapter, pass every read up unchanged. About 3,000 repeated tag reports, a worked example of 100 tags swept for 30 seconds on Session 1, flow into one set keyed by EPC that holds 100 unique EPCs.

Android RFID reader SDK: one app, an adapter per vendor, dedupe by EPC

Handhelds from different vendors disagree on power units, sessions, re-reads and permissions. Let the app own those differences in one place, and a new device becomes a new adapter instead of a new app.

What does one app over every Android RFID reader SDK look like?#

Put every Android RFID reader SDK behind one small interface (connect, set power, set session, start, stop, a stream of tag reads) and let the app, not the SDK, deduplicate reads by EPC.

We built this for a software vendor that sells RFID inventory tracking to small multi-store retailers. The RFID stock tracking case study shows the whole build. Its summary of the choice fits in one line. "The app talks to one interface for re-reads and read range; a new device is a new adapter, not a new app".

In practice the interface has six parts: connect and disconnect, a power level the app understands, and a session and target setting. Then come start, stop and a listener that gets one EPC at a time. Everything else a vendor offers stays inside its adapter. The same shape holds for any UHF RFID reader SDK, because ultra-high frequency (UHF) handhelds read tags built to the same GS1 Gen2 standard.

DiagramOne app, one interface, one adapter per vendor SDK, and deduplication by EPC above them all. Source: the adapter layer described in Atyantik's RFID stock tracking case study, March 2026.

Why build one app with an adapter per vendor instead of one app per reader?#

One Android app with an adapter per reader vendor SDK means a fix ships once, and a new reader becomes a new adapter rather than a new app to test and release.

The case study names the cost of the other route. With an app per reader vendor, "every fix and every store approval multiplies with each device supported". Since stores bought handhelds from different vendors, no retailer could be locked to one device. As a result, the platform had to support three vendor SDKs at once.

However, the adapter layer does not remove vendor quirks; instead, it moves them to one known place. The case study says so too. Each vendor SDK's quirks "still had to be handled inside its adapter". In short, a bug in the scan screen is fixed once. Also, a store rollout is one release, whatever device sits in the store.

An app per reader vendor against one app with an adapter per vendor SDK. Source: Atyantik, RFID stock tracking case study, March 2026.

An app per reader vendorOne app, an adapter per vendor
What a fix costsEvery fix and every store approval multiplies with each device supportedA fix ships once
What a new reader costsA new app to test and releaseA new adapter
A store rolloutMultiplies with each device supportedOne release, whatever device sits in the store

Why does one tag report again and again during a sweep?#

GS1's Gen2 standard (February 2026) lets a Session 1 flag reset after 500ms to 5 seconds, so a tag keeps re-reporting, while Sessions 2 and 3 hold it quiet for 2 seconds or more.

The 2026 standard is direct: "Interrogators shall support and Tags shall provide 4 sessions", named S0 to S3. In particular, each session has its own inventoried flag, A or B. A handheld that asks only for A tags reads a tag, flips it to B, and leaves it alone. Then the flag falls back to A, and the tag answers again.

How long that takes is the whole story. In GS1's Table 6-21 of Release 3.0.1 (February 2026), the S1 flag must persist between 500ms and 5 seconds. In the same table, S2, S3 and SL must persist for more than 2 seconds once the tag loses power. Meanwhile, a powered tag holds its S2 and S3 flags for as long as it stays in the field.

Show data table
Required inventoried-flag persistence in seconds, nominal temperature range: S1 at most 5, S2, S3 and SL at least 2. Source: GS1, EPC Gen2 UHF RFID Standard Release 3.0.1, February 2026, Table 6-21.
Item Value
S1 maximum 5
S2 minimum 2
S3 minimum 2
SL minimum 2

S1 is the only session whose flag falls back on a short timer while the tag sits in the field.

Flag persistence, seconds Required inventoried-flag persistence in seconds, nominal temperature range: S1 at most 5, S2, S3 and SL at least 2. Source: GS1, EPC Gen2 UHF RFID Standard Release 3.0.1, February 2026, Table 6-21. GS1, EPC Gen2 UHF RFID Standard Release 3.0.1, Feb 2026, Table 6-21

Therefore a duplicate read is not a bug in the SDK, because it is the protocol doing what the standard says, and the session decides how often.

How many raw reads should the app expect, and where does deduplication by EPC belong?#

In a worked example of 100 tags swept for 30 seconds on Session 1, about 3,000 tag reports arrive for 100 unique EPCs, so the app keeps a set keyed by EPC.

The inputs are public, since Atlas RFID puts S1 persistence at about 1 second on an Impinj Monza 3 tag chip. On a Monza 4, Atlas RFID says it is closer to 500ms. Then, with Atlas RFID's 1 second figure, each of 100 tags in this illustrative sweep reports about 30 times. That makes about 3,000 reports. At Atlas RFID's 500ms, the same illustrative sweep gives about 6,000. On Session 2 it gives about 100, one per tag.

A 30 second Session 1 sweep of 100 tags30 reports per tag

about 3,000

Raw tag reports

100

Unique EPCs

Without a set keyed by EPC, the screen would count every tag about 30 times.

A 30 second Session 1 sweep of 100 tags (tag reports)
Optiontag reports
Raw tag reportsabout 3,000
Unique EPCs100

Source: arithmetic from Atlas RFID's stated S1 persistence of about 1 second. Modelled, not measured

Raw reports for your own sweep

Set your tag count, sweep length and how long a read tag stays quiet: about 1 second on Session 1, 0.5 on a Monza 4, longer than the sweep on Session 2. Defaults are the worked example.

Your sweep

measured on this projectan assumption, change it

Illustrative arithmetic from Atlas RFID's stated S1 persistence. Antenna overlap, SDK filters and tags the handheld misses are left out.

Raw tag reports

3,000

Unique EPCs
100
Reports per tag
30

Arithmetic from Atlas RFID's stated S1 persistence. Modelled, not measured.

Each SDK also filters before the app sees anything. For example, Zebra's developer guide for SDK 2.0.5.292 covers tags seen by several antennas. It notes that "disabling reporting an Antenna Id can result in the application receiving a" single unique tag. Another SDK may report every antenna hit. So the app cannot trust any one SDK's idea of unique.

The fix is one set, keyed by EPC, above every adapter. First, each adapter passes every read up unchanged. Then the app adds the EPC to the set and updates the screen only when the set grows. Because the set lives in the app, the count on screen means the same thing on every device. Even so, that unique count only becomes stock once it is reconciled against the stock record.

Which session and target should the adapter set for a stock sweep?#

For a full sweep the adapter should ask for a session that keeps read tags quiet, because on Session 1 the same tag keeps coming back while quieter tags wait.

Atlas RFID explains the Single Target case with the Session set to 2 or 3. There, the tag "will be read once then switch to 'B' state and remain quiet the entire time it is in the read field". Weaker tags at the back of a shelf then get their turn. On Session 1, by contrast, the strong tags at the front keep answering.

Show data table
Reports per tag for 5, 10, 30 and 60 second sweeps, by session, with Session 1 flag persistence of about 1 second or about 500ms; whole reports per tag. Source: arithmetic from Atlas RFID, Understanding EPC Gen 2 Search Modes and Sessions. Modelled, not measured.
Dimension Session 1 at 1 s Session 1 at 500ms Session 2 or 3
5 seconds 5101
10 seconds 10201
30 seconds 30601
60 seconds 601201

On Session 2 or 3 a tag reports once however long the sweep, while Session 1 keeps adding reports.

Reports per tag Reports per tag for 5, 10, 30 and 60 second sweeps, by session, with Session 1 flag persistence of about 1 second or about 500ms; whole reports per tag. Source: arithmetic from Atlas RFID, Understanding EPC Gen 2 Search Modes and Sessions. Modelled, not measured. Atlas RFID, Understanding EPC Gen 2 Search Modes and Sessions

However, Atlas RFID also notes a trap. Tags pushed into B by a fixed portal on Session 2 can be missed by a handheld on that session. So the adapter offers two modes the app understands: quiet sweep, or keep reporting. Then each adapter maps them to its SDK. On Zebra's SDK, the singulation tutorial uses setSession and setInventoryState on an Antennas.SingulationControl object.

How does read power map when each SDK expresses it differently?#

Zebra's RFID SDK for Android sets power as an index into a table the device itself reports, so the adapter takes one level the app understands and translates it per vendor.

Its antenna tutorial reads the supported power levels for a region's rules from reader.ReaderCapabilities.getTransmitPowerLevelValues(). Then its example passes setTransmitPowerIndex a position in that table, not a unit of power. Meanwhile, Honeywell's IH40 SDK quick start covers creating the RfidManager, connecting and reading, with no power call in it.

So the adapter never hard-codes a vendor's number. Instead, it reads the supported levels from the device at connect time. Then it maps the app's level, such as low, medium or full, onto that list. Read range is the setting staff notice first. After all, a shelf sweep that picks up the next aisle looks like a stock error.

One power level in the app, translated per SDK. Source: Zebra antenna tutorial, RFID SDK for Android; Honeywell IH40 SDK quick start, 10/21.

LayerHow read power is expressed
The appOne level the app understands, such as low, medium or full
Zebra RFID SDK for AndroidsetTransmitPowerIndex, a position in the table from getTransmitPowerLevelValues(), not a unit of power
Honeywell IH40 SDK quick startCreating the RfidManager, connecting and reading, with no power call in it

Why does SDK init fail on some Android versions?#

Most init failures are permissions, not the SDK: Android Developers says an app targeting Android 12 (API level 31) or higher must request BLUETOOTH_SCAN and BLUETOOTH_CONNECT before any Bluetooth device connects.

Android Developers is clear that these "are runtime permissions". Because a manifest entry alone is not enough, the app asks the user first. Only then does it scan for a handheld or talk to a paired one. Meanwhile, a wired device has its own rule. Android's USB host guide is plain: "Before communicating with the USB device, your application must have permission from your" user.

Second, each SDK sets a floor of its own. Zebra's About page for SDK 2.0.5.292 lists Android 10 and onwards for its RFD40XX, RFD90XX, MC33XXR and RFD8500 handhelds. For the CC6000 fixed device, that page lists Android 11.

Show data table
Minimum Android version per device for Zebra RFID SDK for Android 2.0.5.292. Source: Zebra Technologies, About RFID SDK for Android 2.0.5.292.
Item Value
RFD40XX handheld 10
RFD90XX handheld 10
MC33XXR 10
RFD8500 10
CC6000 fixed reader 11

Start the device matrix at Android 10 for the handhelds and at Android 11 for the fixed reader.

Minimum Android version Minimum Android version per device for Zebra RFID SDK for Android 2.0.5.292. Source: Zebra Technologies, About RFID SDK for Android 2.0.5.292. Zebra Technologies, About RFID SDK for Android 2.0.5.292

So check three things before blaming the SDK, in this order. First, are the runtime permissions granted? Second, is Bluetooth on, which Android's setup guide checks with isEnabled()? Third, does the handset meet the SDK's minimum Android version?

Which documentation pages do you build the adapter from?#

Build the adapter from four official pages, in order: Android's Bluetooth permissions, then Zebra's inventory, antenna and singulation tutorials for the calls each adapter wraps.

  1. Request the Bluetooth permissions first

    Android Bluetooth permissions: the runtime BLUETOOTH_SCAN and BLUETOOTH_CONNECT request, and the neverForLocation flag.

  2. Start and stop an inventory

    Inventory tutorial, Zebra: Inventory.perform(), Inventory.stop() and getReadTags in the event callback.

  3. Map read power to the device table

    Antenna tutorial, Zebra: setTransmitPowerIndex and the power level table.

  4. Set the session and target

    Singulation tutorial, Zebra: setSession and setInventoryState.

The four pages: Android Bluetooth permissions, Inventory tutorial, Zebra, Antenna tutorial, Zebra and Singulation tutorial, Zebra.

For a wired device, add Android's USB host guide for the permission dialog. Also, the GS1 Gen2 standard explains why sessions behave as they do.

What is the smallest adapter that works?#

The smallest working version is one interface, one vendor adapter that wraps start, stop, power and session, and a set keyed by EPC above it.

First, request BLUETOOTH_SCAN and BLUETOOTH_CONNECT at runtime, as the Android permissions page shows. Then the code below is the whole layer for one vendor, using the Zebra calls printed in the tutorials above and in Zebra's Managing Events guide.

java
// 1. The interface each vendor adapter implements.
public interface TagListener { void onTag(String epc); }

public interface ReaderAdapter {
    void setPowerIndex(int wanted) throws Exception;
    void setQuietSession(boolean quiet) throws Exception;
    void start() throws Exception;
    void stop() throws Exception;
    void setListener(TagListener listener);
}

// 2. One adapter, wrapping the Zebra RFID SDK for Android.
public class ZebraAdapter implements ReaderAdapter, RfidEventsListener {
    private final RFIDReader reader;
    private TagListener listener;

    public ZebraAdapter(RFIDReader reader) throws Exception {
        this.reader = reader;
        reader.Events.addEventsListener(this);
        reader.Events.setTagReadEvent(true);
        reader.Events.setAttachTagDataWithReadEvent(false);
    }

    public void setPowerIndex(int wanted) throws Exception {
        int max = reader.ReaderCapabilities.getTransmitPowerLevelValues().length - 1;
        Antennas.AntennaRfConfig rf = reader.Config.Antennas.getAntennaRfConfig(1);
        rf.setTransmitPowerIndex(Math.min(wanted, max));
        reader.Config.Antennas.setAntennaRfConfig(1, rf);
    }

    public void setQuietSession(boolean quiet) throws Exception {
        Antennas.SingulationControl s = reader.Config.Antennas.getSingulationControl(1);
        s.setSession(quiet ? SESSION.SESSION_S2 : SESSION.SESSION_S1);
        s.Action.setInventoryState(INVENTORY_STATE.INVENTORY_STATE_A);
        reader.Config.Antennas.setSingulationControl(1, s);
    }

    public void start() throws Exception { reader.Actions.Inventory.perform(); }
    public void stop() throws Exception { reader.Actions.Inventory.stop(); }
    public void setListener(TagListener l) { this.listener = l; }

    @Override public void eventReadNotify(RfidReadEvents e) {
        TagData[] tags = reader.Actions.getReadTags(100);
        if (tags == null || listener == null) return;
        for (TagData t : tags) listener.onTag(t.getTagID());
    }

    @Override public void eventStatusNotify(RfidStatusEvents e) { }
}

// 3. Dedupe by EPC in the app, above every adapter.
Set<String> seen = ConcurrentHashMap.newKeySet();
adapter.setListener(epc -> { if (seen.add(epc)) onNewItem(epc); });

A second vendor is a second class that implements ReaderAdapter, while the set in step 3 does not change.

What goes in the device test matrix in week one?#

In our RFID stock tracking build, hardware testing took far more time than planned, so the matrix comes first: every supported device, every target Android version, every connection type.

The case study puts it plainly: "Hardware integration took far more testing than planned. Re-reads and read range differ by device and SDK." Instead, those differences surfaced one device at a time. So its lesson is the plan here: a device lab with every supported handheld goes in during the first week.

Build the matrix as rows of handheld, phone, Android version and connection type. Then run the same five checks on every row:

Five checks for every row of the device matrix: handheld, phone, Android version, connection. Source: Atyantik's RFID stock tracking case study, March 2026, for the week-one device lab.

CheckPasses when
1. InitInit succeeds on a fresh install, with permissions denied first and then granted.
2. Unique countA sweep of the same tagged shelf gives the same unique EPC count on every row.
3. Reads per sessionRaw reads per tag on each session match what the session predicts.
4. Power rangeThe lowest and highest power levels read the shelf and not the next aisle.
5. ReconnectThe handheld reconnects after the phone sleeps.

Finally, start the Android version column from each SDK's stated floor. For Zebra's handhelds in SDK 2.0.5.292, that is Android 10.

When is one app over several SDKs the wrong fit?#

If every store runs one reader model and will keep it, a thin wrapper over that one SDK is the better tool, because an adapter layer with one adapter is overhead with nothing to share.

Where should you go next?#

Read the RFID case study for the whole build, or the related posts on retail devices and stock systems below.

The RFID stock tracking case study covers the platform behind the scanning app. For the inventory product side, see how to add RFID to inventory software. For another vendor device the OS must find before any SDK call works, see troubleshooting Zebra and Printronix USB printers. The store system that scan data flows into is in cloud POS systems for retail. Where reads end up after the handheld is in supply chain management software.

To have the app built, see mobile app development. To wire SDKs into a stock system, see API integrations. Still, the vendor tutorials and the Gen2 standard linked above are enough to build the first adapter on your own.

Questions this post answers

Why does an Android RFID reader SDK report the same tag again and again?
GS1's Gen2 standard (February 2026) lets a Session 1 flag reset after 500ms to 5 seconds, so a tag keeps re-reporting, while Sessions 2 and 3 hold it quiet for 2 seconds or more. A duplicate read is not a bug in the SDK, because it is the protocol doing what the standard says, and the session decides how often.
Where should deduplication by EPC happen in an app built on an Android RFID reader SDK?
The fix is one set, keyed by EPC, above every adapter. First, each adapter passes every read up unchanged. Then the app adds the EPC to the set and updates the screen only when the set grows. Because the set lives in the app, the count on screen means the same thing on every device.
Why does Android RFID reader SDK init fail on some Android versions?
Most init failures are permissions, not the SDK: Android Developers says an app targeting Android 12 (API level 31) or higher must request BLUETOOTH_SCAN and BLUETOOTH_CONNECT before any Bluetooth device connects. Zebra's About page for SDK 2.0.5.292 lists Android 10 and onwards for its RFD40XX, RFD90XX, MC33XXR and RFD8500 handhelds.

Keep reading