Overview
AXL SDK is an Android library that enables applications to communicate with AXL RFID hardware devices over USB or Bluetooth LE.
The SDK handles all low-level hardware communication — USB serial framing, CRC16 packet validation, device handshake, and event dispatch — so your application only needs to respond to events and call commands. No USB driver code, no protocol knowledge, no hardware expertise required.
onError() callback with a human-readable message and a short error code (E001–E007).Supported Devices
The SDK supports all AXL hardware variants. Device capabilities vary by model.
| Device | RFID | NFC | Barcode | Antennas | USB | BLE Config |
|---|---|---|---|---|---|---|
| AXL FLAT | ✓ | ✓ | ✓ | 1 (fixed active) | ✓ | ✓ |
| AXL BIN | ✓ | — | — | Up to 4 | ✓ | — |
How It Works
The SDK sits between your application and the AXL hardware. Your app calls commands and receives events — the SDK handles everything in between.
| Layer | Responsibility |
|---|---|
| Your Application | Calls commands (connect(), startReading(), checkoutCompleted()) and implements SdkListener to receive events. |
| AXL SDK | Manages connection lifecycle, serialises commands to JSON, frames packets with CRC16, dispatches responses to listener callbacks on the main thread. |
| USB Serial Layer | Opens the USB serial port, selects the correct driver for the device's VID/PID, reads and writes raw bytes. |
| AXL Hardware | Runs firmware that receives framed commands, drives the RFID/NFC/Barcode hardware, and sends back framed events. |
Requirements
Build environment, Android API levels, and hardware prerequisites.
| Requirement | Value |
|---|---|
| Android version | 5.0 and above (API 21+) |
| Target API | 34 (Android 14) |
| Language | Java 11 · Kotlin |
| USB | USB OTG support on the Android device |
| Bluetooth | Bluetooth 4.0+ — optional, required for BLE configuration (AXL FLAT only) |
| Dependency | usb-serial-for-android 3.8.1 or later (via JitPack) |
| Distribution | AAR file — drop into app/libs/ |
Quick Start
The minimal integration — connect to an AXL device and receive EPC tags in five steps.
- 1Add the SDKCopy
axlsdk.aarintoapp/libs/. Add JitPack to your repositories and declare the AAR andusb-serial-for-androidas dependencies in yourbuild.gradle. - 2Declare USB permissionsAdd
android.hardware.usb.hostto yourAndroidManifest.xmland include adevice_filter.xmllisting the VID/PID of your AXL device so Android routes USB attach events to your app. - 3Initialise the SDKCall
Sdk.getInstance().initialize(context)once when your activity starts. Optionally pass aSdkConfigto set timeout, batch size, or logging preferences. - 4Implement SdkListenerImplement the
SdkListenerinterface in your Activity and register it withsdk.setListener(this). At minimum handleonConnected(),onTagDetected(epc), andonError(message). - 5Connect and scanCall
sdk.connect()when the user taps Connect. OnonConnected(), callsdk.startReading(). Each EPC tag firesonTagDetected(epc). Callsdk.checkoutCompleted(txnId, epcs)to submit the transaction.
Installation
Add the AAR to your project, configure Gradle dependencies, update your AndroidManifest, and add the USB device filter.
1 — Add the AAR file
Download axlsdk.aar from the release and copy it into your app's libs/ folder.
app/
└── libs/
└── axlsdk.aar2 — Configure Gradle
The SDK depends on usb-serial-for-android, which is hosted on JitPack. Add the repository and declare both dependencies.
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}dependencies {
implementation(files("libs/axlsdk.aar"))
implementation("com.github.mik3y:usb-serial-for-android:3.9.0")
}allprojects {
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
}dependencies {
implementation files('libs/axlsdk.aar')
implementation 'com.github.mik3y:usb-serial-for-android:3.9.0'
}3 — Update AndroidManifest.xml
Declare the USB host feature and the USB device attached intent filter.
<uses-feature
android:name="android.hardware.usb.host"
android:required="false" />
<!-- Bluetooth LE (API 31+) -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Bluetooth LE (API < 31) -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
</activity>4 — Add device_filter.xml
Create res/xml/device_filter.xml. Android uses this file to decide which USB devices auto-launch your app on plug-in.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- FTDI FT232R VID=0x0403 PID=0x6001 -->
<usb-device vendor-id="1027" product-id="24577" />
<!-- FTDI FT2232H VID=0x0403 PID=0x6010 -->
<usb-device vendor-id="1027" product-id="24592" />
<!-- STM32 USB CDC VID=0x0483 PID=0x5740 -->
<usb-device vendor-id="1155" product-id="22336" />
<!-- STM32 USB CDC VID=0x0483 PID=0x5741 (alt PID) -->
<usb-device vendor-id="1155" product-id="22337" />
<!-- Silicon Labs CP210x VID=0x10C4 PID=0xEA60 -->
<usb-device vendor-id="4292" product-id="60000" />
<!-- CH340 / CH341 VID=0x1A86 PID=0x7523 -->
<usb-device vendor-id="6790" product-id="29987" />
<!-- CH9102 (CH343) VID=0x1A86 PID=0x55D4 -->
<usb-device vendor-id="6790" product-id="21972" />
<!-- AXLSYSTEMS AXLECU VID=0x061F PID=0x3D38 -->
<usb-device vendor-id="1567" product-id="15672" />
</resources>device_filter.xml controls auto-launch only — it tells Android which USB devices should launch your app on plug-in. It has no effect on sdk.connect(). If your app is already running, the SDK will connect to any supported device regardless of what is listed here.Initialization
Initialize the SDK singleton once when your Activity starts. Set your listener before calling connect() — callbacks will not fire without one.
Basic initialization
Call initialize(context) with default settings. This is sufficient for most integrations.
Sdk sdk = Sdk.getInstance();
sdk.initialize(this);
sdk.setListener(this); // Activity implements SdkListenerval sdk = Sdk.getInstance()
sdk.initialize(this)
sdk.setListener(this) // Activity implements SdkListenerInitialization with custom config
Pass a SdkConfig to override defaults — command timeout, batch size, and debug logging.
SdkConfig config = new SdkConfig.Builder()
.commandTimeoutMs(5000) // how long to wait for a device ACK (default: 5000 ms)
.checkoutBatchSize(20) // EPCs per checkout_complete command (default: 20)
.debugLogging(false) // verbose SDK logging — enable during development
.build();
Sdk sdk = Sdk.getInstance();
sdk.initialize(this, config);
sdk.setListener(this);SdkConfig options
| Option | Default | Description |
|---|---|---|
commandTimeoutMs(n) | 5000 ms | How long the SDK waits for a device ACK before firing onError([E003]). |
checkoutBatchSize(n) | 20 | Maximum EPCs per checkout_complete command. Large lists are split into sequential batches. Set to 0 to disable batching. |
debugLogging(bool) | false | Enables verbose SDK log output. Useful during development. Disable for production builds. |
baudRate(n) | 115200 | USB serial baud rate. Only change this if the device firmware uses a non-default rate. |
Where to initialize
Initialize in Activity.onCreate() and release in Activity.onStop() when the app is finishing.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Sdk sdk = Sdk.getInstance();
sdk.initialize(this, new SdkConfig.Builder()
.checkoutBatchSize(20)
.debugLogging(true)
.build());
sdk.setListener(this);
}
@Override
protected void onStop() {
super.onStop();
if (isFinishing() && sdk != null) {
if (sdk.isConnected()) sdk.disconnectBlocking();
sdk.releasePort(); // close USB port so other apps can connect
}
}sdk.setListener(this) after initialize() and before connect(). If no listener is set, all SDK events are silently dropped.USB Connection
Connect to an AXL device over USB OTG. The SDK manages the full lifecycle — permission request, driver selection, handshake, and event dispatch.
Calling connect()
Call sdk.connect() after initialize() and setListener(). If Android has not yet granted USB permission, a system dialog is shown automatically.
connectBtn.setOnClickListener(v -> sdk.connect());connectBtn.setOnClickListener { sdk.connect() }Connection sequence
| Callback | When | Notes |
|---|---|---|
onConnected() | USB port open, handshake complete | Safe to call startReading() or sendDeviceConfig() from here. |
onDeviceIdentified(String type) | Device type resolved | Value is one of "FLAT", "BIN". Use to show or hide device-specific UI. |
onDeviceConfigLoaded(JSONObject config) | Device config fetched | Contains live hardware settings. Use to pre-populate a settings dialog. |
Cable replug handling
When the USB cable is removed, onDisconnected() fires. Call sdk.connect() again after replug to re-establish the session.
@Override
public void onDisconnected() {
runOnUiThread(() -> {
statusLabel.setText("Disconnected");
connectBtn.setEnabled(true);
});
}
// User taps Connect after replug
connectBtn.setOnClickListener(v -> sdk.connect());connect() is asynchronous. Do not call startReading() immediately after — wait for onConnected().Bluetooth (BLE)
AXL FLAT supports wireless configuration updates over Bluetooth LE. BLE is a config-only channel — tag scanning always requires a USB connection.
BLE vs USB capabilities
| Feature | USB | BLE |
|---|---|---|
| RFID tag scanning | ✓ | — |
| Push device config | ✓ | ✓ |
| Fetch device config | ✓ | ✓ |
| Checkout | ✓ | — |
| Supported devices | All | AXL FLAT only |
Scanning and connecting
// Scan for nearby AXL FLAT devices
sdk.scanBle(results -> {
// results is List<BleDevice> with name and address
if (!results.isEmpty()) {
sdk.connectBle(results.get(0).getAddress());
}
});
// Or connect directly if the address is already known
sdk.connectBle("AA:BB:CC:DD:EE:FF");Pushing config over BLE
RfidDeviceConfig config = new RfidDeviceConfig.Builder()
.region("ETSI")
.antennaPower(25)
.build();
sdk.sendDeviceConfig(config); // works over both USB and BLEBLUETOOTH_SCAN, BLUETOOTH_CONNECT). On Android 11 and below, BLUETOOTH and ACCESS_FINE_LOCATION are needed instead.RFID Scanning
Start the RFID reader, receive EPC tags in real time, and control the scan session.
Starting a scan
Call sdk.startReading() from onConnected() or from a user action. Each tag antenna read fires onTagDetected(String epc) on the main thread. The same physical tag may fire multiple times — deduplicate with a HashSet.
private final Set<String> scannedEpcs = new HashSet<>();
@Override
public void onConnected() {
scannedEpcs.clear();
sdk.startReading();
}
@Override
public void onTagDetected(String epc) {
if (scannedEpcs.add(epc)) {
runOnUiThread(() -> tagCountLabel.setText(scannedEpcs.size() + " tags"));
}
}private val scannedEpcs = mutableSetOf<String>()
override fun onConnected() {
scannedEpcs.clear()
sdk.startReading()
}
override fun onTagDetected(epc: String) {
if (scannedEpcs.add(epc)) {
runOnUiThread { tagCountLabel.text = "${scannedEpcs.size} tags" }
}
}Pausing and resuming
pauseBtn.setOnClickListener(v -> sdk.pauseReading());
resumeBtn.setOnClickListener(v -> sdk.startReading());Stopping and submitting
checkoutBtn.setOnClickListener(v -> {
sdk.stopReading();
sdk.checkoutCompleted(generateTxnId(), new ArrayList<>(scannedEpcs));
});Checkout
Submit collected EPC tags as a checkout transaction. The SDK automatically batches large lists and sequences the commands.
Submitting a transaction
sdk.checkoutCompleted(txnId, new ArrayList<>(scannedEpcs));sdk.checkoutCompleted(txnId, scannedEpcs.toList())Batching behaviour
| Scenario | Behaviour |
|---|---|
| 20 EPCs, batch size 20 | 1 batch → 1 ACK → onCheckoutConfirmed fires. |
| 45 EPCs, batch size 20 | 3 batches (20 + 20 + 5) → 3 ACKs → onCheckoutConfirmed fires. |
| Batch size 0 | Entire list sent in one command. Use only for small known-size lists. |
Confirmation callback
@Override
public void onCheckoutConfirmed(String txnId) {
runOnUiThread(() -> showCheckoutSuccess(txnId));
scannedEpcs.clear();
}Disconnecting
Disconnect gracefully and release the USB port so other apps can access the device.
disconnect() vs disconnectBlocking()
| Method | Behaviour | When to use |
|---|---|---|
disconnect() | Asynchronous — returns immediately, fires onDisconnected() when done. | User taps a Disconnect button. Keeps UI responsive. |
disconnectBlocking() | Synchronous — blocks the calling thread until disconnect completes. No callback. | Activity.onStop() when isFinishing() is true. |
Releasing the USB port
Call sdk.releasePort() after disconnectBlocking() in the app-close path. Without it, the next app connecting to the same device will receive [E003] COMMAND_TIMEOUT.
Recommended app-close pattern
@Override
protected void onStop() {
super.onStop();
if (isFinishing() && sdk != null) {
if (sdk.isConnected()) sdk.disconnectBlocking();
sdk.releasePort();
}
}override fun onStop() {
super.onStop()
if (isFinishing && sdk != null) {
if (sdk.isConnected()) sdk.disconnectBlocking()
sdk.releasePort()
}
}SdkListener Callbacks
All callbacks are delivered on the main thread. Implement the interface in your Activity and register with sdk.setListener(this).
| Callback | Parameters | Required | Description |
|---|---|---|---|
onConnected() | — | ✓ | USB handshake complete. Safe to call startReading() or sendDeviceConfig(). |
onDisconnected() | — | ✓ | USB cable removed or disconnect() completed. Update UI and re-enable the Connect button. |
onTagDetected(String epc) | epc — hex EPC string | ✓ | Fires for every antenna read. The same physical tag fires multiple times — deduplicate with a HashSet. |
onError(String message) | message — [Exxx] description | ✓ | All SDK errors route here. The string always starts with an error code in brackets. |
onDeviceIdentified(String type) | type — "FLAT" · "BIN" | — | Fires after onConnected(). Use to show or hide device-specific UI. |
onDeviceConfigLoaded(JSONObject config) | config — full device config JSON | — | Fires after handshake and on each explicit getDeviceConfig() call. |
onCheckoutConfirmed(String txnId) | txnId — echoed from checkoutCompleted() | — | Fires when all EPC batches have been ACK'd by the device. |
onScanSessionEnded() | — | — | Fires when stopReading() completes and the device confirms the session is closed. |
Minimal implementation
public class MainActivity extends AppCompatActivity implements SdkListener {
@Override public void onConnected() {
runOnUiThread(() -> statusLabel.setText("Connected"));
sdk.startReading();
}
@Override public void onDisconnected() {
runOnUiThread(() -> statusLabel.setText("Disconnected"));
}
@Override public void onTagDetected(String epc) {
scannedEpcs.add(epc);
runOnUiThread(() -> countLabel.setText(scannedEpcs.size() + " tags"));
}
@Override public void onError(String message) {
runOnUiThread(() -> Toast.makeText(this, message, Toast.LENGTH_LONG).show());
}
}SDK Modes
The SDK tracks its own state. Read it at any time with sdk.getCurrentMode() to guard UI actions or debug connection issues.
| Mode | Description | Valid next actions |
|---|---|---|
IDLE | SDK initialized, no device connected. | connect() |
CONNECTED | Device connected and handshake complete. Ready for commands. | startReading() · sendDeviceConfig() · getDeviceConfig() · disconnect() |
SCANNING | RFID reader active. onTagDetected() is firing. | pauseReading() · stopReading() |
PAUSED | Reader paused, session preserved, no tag events. | startReading() to resume · stopReading() to end |
CHECKOUT_PENDING | Checkout batches in flight, awaiting device ACKs. | None — wait for onCheckoutConfirmed(). |
DISCONNECTED | Was connected, now disconnected. | connect() |
Guarding actions by mode
SdkMode mode = sdk.getCurrentMode();
switch (mode) {
case IDLE:
case DISCONNECTED:
sdk.connect();
break;
case CONNECTED:
sdk.startReading();
break;
case SCANNING:
sdk.stopReading();
break;
case CHECKOUT_PENDING:
// wait — do not issue new commands
break;
}Error Codes
All errors arrive via onError(String). The string always starts with [Exxx] followed by a human-readable description.
| Code | Name | Cause | Recovery |
|---|---|---|---|
E001 | DEVICE_NOT_CONNECTED | Command issued but no USB device is connected, or the port handle is stale. | Call connect() again. |
E002 | USB_PERMISSION_DENIED | User dismissed or denied the Android USB permission dialog. | Show an explanation and call connect() again to re-prompt. |
E003 | COMMAND_TIMEOUT | Device did not ACK a command within commandTimeoutMs (default 5 000 ms). | Check USB cable seating. Retry the command. Increase commandTimeoutMs in SdkConfig. |
E004 | CHECKOUT_FAILED | One or more checkout batches were rejected or timed out. | Retry checkoutCompleted() with the same transaction ID. |
E005 | CONFIG_SEND_FAILED | Device rejected the configuration payload — malformed JSON or unsupported field value. | Validate the RfidDeviceConfig values and retry. |
E006 | DRIVER_NOT_FOUND | No USB serial driver matched the connected device's VID/PID. | Verify device_filter.xml includes the correct VID/PID. |
E007 | BLE_CONNECTION_FAILED | Bluetooth LE connection timed out or the device rejected pairing. | Confirm the device is powered and in range. Re-scan with scanBle(). |
Handling errors in code
@Override
public void onError(String message) {
if (message.startsWith("[E001]") || message.startsWith("[E002]")) {
runOnUiThread(this::showReconnectPrompt);
} else if (message.startsWith("[E003]")) {
runOnUiThread(this::showTimeoutDialog);
} else {
runOnUiThread(() ->
Toast.makeText(this, message, Toast.LENGTH_LONG).show());
}
}Device Configuration
Push hardware settings to the device at runtime over USB or BLE. Configuration takes effect immediately without a device restart.
Building and sending a config
Use RfidDeviceConfig.Builder to construct a config. Only fields you set are included in the payload.
RfidDeviceConfig config = new RfidDeviceConfig.Builder()
.region("ETSI") // frequency region
.antennaPower(25) // transmit power in dBm
.antennaCount(1) // 1 for AXL FLAT; up to 4 for AXL BIN
.hopEnabled(true) // frequency hopping within region
.wifiSsid("MyNetwork")
.wifiPassword("secret")
.wifiSecurity("WPA2")
.build();
sdk.sendDeviceConfig(config);RfidDeviceConfig.Builder options
| Method | Type | Description |
|---|---|---|
region(String) | String | Frequency region. Common values: "FCC", "ETSI", "CN", "KR". |
antennaPower(int) | int (dBm) | RF transmit power. Supported range varies by region. |
antennaCount(int) | int | Number of active antennas. AXL FLAT: always 1. AXL BIN: 1–4. |
hopEnabled(boolean) | boolean | Enable frequency hopping within the selected region. |
wifiSsid(String) | String | WiFi network name. Leave unset to preserve the device's current WiFi config. |
wifiPassword(String) | String | WiFi password. |
wifiSecurity(String) | String | Security type. Values: "OPEN", "WPA2", "WPA3". |
wifiIpMode(String) | String | IP assignment. Values: "DHCP", "STATIC". |
wifiStaticIp(String) | String | Static IP address. Used when wifiIpMode is "STATIC". |
Fetching the current config
Call sdk.getDeviceConfig() to request the device's live configuration. The result fires onDeviceConfigLoaded(JSONObject) asynchronously.
// Trigger from settings button
settingsBtn.setOnClickListener(v -> sdk.getDeviceConfig());
// Receive result
@Override
public void onDeviceConfigLoaded(JSONObject config) {
String region = config.optString("region", "FCC");
int power = config.optInt("antenna_power", 20);
int antennas = config.optInt("antenna_count", 1);
// ... populate settings UI fields
}Diagnostics & Logging
The SDK logs to Android Logcat under the tag AXL_SDK and maintains an in-memory ring buffer for field support dumps.
Verbose logging during development
SdkConfig config = new SdkConfig.Builder()
.debugLogging(BuildConfig.DEBUG) // on in debug builds, off in release
.build();
sdk.initialize(this, config);Filtering Logcat output
adb logcat -s AXL_SDKDumping the ring buffer on error
@Override
public void onError(String message) {
String logDump = sdk.dumpLogs();
// Attach to crash report or forward to support
sendToSupport(message, logDump);
}Real-time log listener
sdk.setLogListener((level, tag, message) -> {
if (level == LogLevel.WARN || level == LogLevel.ERROR) {
myAnalytics.log(level.name(), tag, message);
}
});dumpLogs() before the process exits if you need to capture a session-end snapshot.v26.2.9
2026-08-07- AXL ECU (Bin) USB device support — the SDK now recognises the AXLSYSTEMS AXLECU hardware (VID
0x061F/ PID0x3D38). A custombuildProber()method extends the default usb-serial-for-android probe table with the AXLECU VID/PID mapped toCdcAcmSerialDriver.
- Stale port causes connection failure after USB cable replug — (~1–2 in 15 attempts on AXL ECU and AXL FLAT). When the previous session had no scan activity,
disconnect()kept the USB serial port alive. On cable replug, Android re-enumerated the device and invalidated the old port file descriptor. Fixed: a flushIOExceptionnow closes the stale port and falls through to a fresh open.
v26.2.8
2026-07-16Sdk.releasePort()— explicit USB port release for app-close path — call fromActivity.onStop()whenisFinishing()=trueafter anydisconnectBlocking(). Prevents a lingering port from causingCOMMAND_TIMEOUTwhen another app connects. No-op for BLE/WiFi transports.Sdk.getDeviceConfig()— explicit device config fetch — sends adevice_configcommand and firesSdkListener.onDeviceConfigLoaded(JSONObject). Call from your Settings gear icon tap handler to pre-populate the Settings dialog with live device values.PacketFramer— reliable packet transport layer — all USB communication is now wrapped in a frame format with CRC16-CCITT validation. Transparent to the public SDK API — all existing method signatures and callback names are unchanged.
MAX_PAYLOADreduced from 120 → 64 bytes — aligns chunk size to USB Full Speed bulk transfer boundary, halving ACK latency per chunk.ack_connection_syncno longer containsconfig_data— config is now fetched via the newdevice_configcommand immediately after connect. SDK handles both old and new handshake responses gracefully.RF_INIT_TAIL_MSreduced from 55 000 ms to 15 000 ms — RF hardware initialisation no longer blocks the handshake, so the tail only needs to cover transient delays.
connect()wastes up to 12 s on USB disconnect mid-handshake — when the USB link drops during a retry loop, the SDK now re-throws immediately instead of sleeping 1 s per remaining attempt.- Second connect within the same session fails immediately with
[E003]— the 450 ms peek read indisconnect()incorrectly treated firmware health bytes as scan activity, closing the port and cold-booting the device. Removed the peek; port-close decision is now based solely on whether actual tag data was dispatched.