Getting Started

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.

RFID Tag Scanning
Read EPC tags with start, pause, and stop control. Submit scanned tags as a checkout transaction — auto-split into configurable batches.
USB & Bluetooth LE
Connect over USB OTG for full access. Bluetooth LE is supported for remote configuration updates without a cable.
Live Device Configuration
Push antenna power, frequency region, network settings, and hop sequence to the device at runtime without a restart.
CRC16 Packet Validation
All messages — commands, responses, EPC data — are framed and verified using CRC16, ensuring integrity across every transfer.
NFC & Barcode (AXL FLAT)
Integrated NFC card reading and barcode scanning on the same device and the same USB connection.
Structured Error Reporting
All errors surface through a single 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.

DeviceRFIDNFCBarcodeAntennasUSBBLE Config
AXL FLAT✓✓✓1 (fixed active)✓✓
AXL BIN✓——Up to 4✓—
AXL FLAT supports configuration updates over Bluetooth LE in addition to USB. All other devices support configuration via USB only.

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.

LayerResponsibility
Your ApplicationCalls commands (connect(), startReading(), checkoutCompleted()) and implements SdkListener to receive events.
AXL SDKManages connection lifecycle, serialises commands to JSON, frames packets with CRC16, dispatches responses to listener callbacks on the main thread.
USB Serial LayerOpens the USB serial port, selects the correct driver for the device's VID/PID, reads and writes raw bytes.
AXL HardwareRuns 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.

RequirementValue
Android version5.0 and above (API 21+)
Target API34 (Android 14)
LanguageJava 11 · Kotlin
USBUSB OTG support on the Android device
BluetoothBluetooth 4.0+ — optional, required for BLE configuration (AXL FLAT only)
Dependencyusb-serial-for-android 3.8.1 or later (via JitPack)
DistributionAAR file — drop into app/libs/

Quick Start

The minimal integration — connect to an AXL device and receive EPC tags in five steps.

  1. 1
    Add the SDK
    Copy axlsdk.aar into app/libs/. Add JitPack to your repositories and declare the AAR and usb-serial-for-android as dependencies in your build.gradle.
  2. 2
    Declare USB permissions
    Add android.hardware.usb.host to your AndroidManifest.xml and include a device_filter.xml listing the VID/PID of your AXL device so Android routes USB attach events to your app.
  3. 3
    Initialise the SDK
    Call Sdk.getInstance().initialize(context) once when your activity starts. Optionally pass a SdkConfig to set timeout, batch size, or logging preferences.
  4. 4
    Implement SdkListener
    Implement the SdkListener interface in your Activity and register it with sdk.setListener(this). At minimum handle onConnected(), onTagDetected(epc), and onError(message).
  5. 5
    Connect and scan
    Call sdk.connect() when the user taps Connect. On onConnected(), call sdk.startReading(). Each EPC tag fires onTagDetected(epc). Call sdk.checkoutCompleted(txnId, epcs) to submit the transaction.
Android

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.

Project structure
app/
└── libs/
    └── axlsdk.aar

2 — Configure Gradle

The SDK depends on usb-serial-for-android, which is hosted on JitPack. Add the repository and declare both dependencies.

Kotlin DSL — settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}
Kotlin DSL — app/build.gradle.kts
dependencies {
    implementation(files("libs/axlsdk.aar"))
    implementation("com.github.mik3y:usb-serial-for-android:3.9.0")
}
Groovy DSL — build.gradle (project level)
allprojects {
    repositories {
        google()
        mavenCentral()
        maven { url "https://jitpack.io" }
    }
}
Groovy DSL — app/build.gradle
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.

AndroidManifest.xml
<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.

res/xml/device_filter.xml
<?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.

Java
Sdk sdk = Sdk.getInstance();
sdk.initialize(this);
sdk.setListener(this); // Activity implements SdkListener
Kotlin
val sdk = Sdk.getInstance()
sdk.initialize(this)
sdk.setListener(this) // Activity implements SdkListener

Initialization with custom config

Pass a SdkConfig to override defaults — command timeout, batch size, and debug logging.

Java
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

OptionDefaultDescription
commandTimeoutMs(n)5000 msHow long the SDK waits for a device ACK before firing onError([E003]).
checkoutBatchSize(n)20Maximum EPCs per checkout_complete command. Large lists are split into sequential batches. Set to 0 to disable batching.
debugLogging(bool)falseEnables verbose SDK log output. Useful during development. Disable for production builds.
baudRate(n)115200USB 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.

Java — MainActivity.java
@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
    }
}
Call 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.

Java
connectBtn.setOnClickListener(v -> sdk.connect());
Kotlin
connectBtn.setOnClickListener { sdk.connect() }

Connection sequence

CallbackWhenNotes
onConnected()USB port open, handshake completeSafe to call startReading() or sendDeviceConfig() from here.
onDeviceIdentified(String type)Device type resolvedValue is one of "FLAT", "BIN". Use to show or hide device-specific UI.
onDeviceConfigLoaded(JSONObject config)Device config fetchedContains 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.

Java — SdkListener
@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

FeatureUSBBLE
RFID tag scanning✓—
Push device config✓✓
Fetch device config✓✓
Checkout✓—
Supported devicesAllAXL FLAT only

Scanning and connecting

Java
// 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

Java
RfidDeviceConfig config = new RfidDeviceConfig.Builder()
    .region("ETSI")
    .antennaPower(25)
    .build();

sdk.sendDeviceConfig(config); // works over both USB and BLE
Bluetooth permissions are required on Android 12+ (BLUETOOTH_SCAN, BLUETOOTH_CONNECT). On Android 11 and below, BLUETOOTH and ACCESS_FINE_LOCATION are needed instead.
Reading Data

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.

Java
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"));
    }
}
Kotlin
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

Java
pauseBtn.setOnClickListener(v -> sdk.pauseReading());
resumeBtn.setOnClickListener(v -> sdk.startReading());

Stopping and submitting

Java
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

Java
sdk.checkoutCompleted(txnId, new ArrayList<>(scannedEpcs));
Kotlin
sdk.checkoutCompleted(txnId, scannedEpcs.toList())

Batching behaviour

ScenarioBehaviour
20 EPCs, batch size 201 batch → 1 ACK → onCheckoutConfirmed fires.
45 EPCs, batch size 203 batches (20 + 20 + 5) → 3 ACKs → onCheckoutConfirmed fires.
Batch size 0Entire list sent in one command. Use only for small known-size lists.

Confirmation callback

Java — SdkListener
@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()

MethodBehaviourWhen 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

Java — Activity.onStop()
@Override
protected void onStop() {
    super.onStop();
    if (isFinishing() && sdk != null) {
        if (sdk.isConnected()) sdk.disconnectBlocking();
        sdk.releasePort();
    }
}
Kotlin — Activity.onStop()
override fun onStop() {
    super.onStop()
    if (isFinishing && sdk != null) {
        if (sdk.isConnected()) sdk.disconnectBlocking()
        sdk.releasePort()
    }
}
API Reference

SdkListener Callbacks

All callbacks are delivered on the main thread. Implement the interface in your Activity and register with sdk.setListener(this).

CallbackParametersRequiredDescription
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

Java
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.

ModeDescriptionValid next actions
IDLESDK initialized, no device connected.connect()
CONNECTEDDevice connected and handshake complete. Ready for commands.startReading() · sendDeviceConfig() · getDeviceConfig() · disconnect()
SCANNINGRFID reader active. onTagDetected() is firing.pauseReading() · stopReading()
PAUSEDReader paused, session preserved, no tag events.startReading() to resume · stopReading() to end
CHECKOUT_PENDINGCheckout batches in flight, awaiting device ACKs.None — wait for onCheckoutConfirmed().
DISCONNECTEDWas connected, now disconnected.connect()

Guarding actions by mode

Java
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.

CodeNameCauseRecovery
E001DEVICE_NOT_CONNECTEDCommand issued but no USB device is connected, or the port handle is stale.Call connect() again.
E002USB_PERMISSION_DENIEDUser dismissed or denied the Android USB permission dialog.Show an explanation and call connect() again to re-prompt.
E003COMMAND_TIMEOUTDevice did not ACK a command within commandTimeoutMs (default 5 000 ms).Check USB cable seating. Retry the command. Increase commandTimeoutMs in SdkConfig.
E004CHECKOUT_FAILEDOne or more checkout batches were rejected or timed out.Retry checkoutCompleted() with the same transaction ID.
E005CONFIG_SEND_FAILEDDevice rejected the configuration payload — malformed JSON or unsupported field value.Validate the RfidDeviceConfig values and retry.
E006DRIVER_NOT_FOUNDNo USB serial driver matched the connected device's VID/PID.Verify device_filter.xml includes the correct VID/PID.
E007BLE_CONNECTION_FAILEDBluetooth 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

Java — SdkListener
@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.

Java
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

MethodTypeDescription
region(String)StringFrequency region. Common values: "FCC", "ETSI", "CN", "KR".
antennaPower(int)int (dBm)RF transmit power. Supported range varies by region.
antennaCount(int)intNumber of active antennas. AXL FLAT: always 1. AXL BIN: 1–4.
hopEnabled(boolean)booleanEnable frequency hopping within the selected region.
wifiSsid(String)StringWiFi network name. Leave unset to preserve the device's current WiFi config.
wifiPassword(String)StringWiFi password.
wifiSecurity(String)StringSecurity type. Values: "OPEN", "WPA2", "WPA3".
wifiIpMode(String)StringIP assignment. Values: "DHCP", "STATIC".
wifiStaticIp(String)StringStatic 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.

Java — SdkListener
// 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
}
Production

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

Java
SdkConfig config = new SdkConfig.Builder()
    .debugLogging(BuildConfig.DEBUG)   // on in debug builds, off in release
    .build();

sdk.initialize(this, config);

Filtering Logcat output

adb
adb logcat -s AXL_SDK

Dumping the ring buffer on error

Java — in onError()
@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

Java
sdk.setLogListener((level, tag, message) -> {
    if (level == LogLevel.WARN || level == LogLevel.ERROR) {
        myAnalytics.log(level.name(), tag, message);
    }
});
The ring buffer holds the most recent log lines in memory. It does not persist across process restarts. Call dumpLogs() before the process exits if you need to capture a session-end snapshot.
AXL RFID SDK — Android

v26.2.9

2026-08-07
Latest
Supports AXL FLAT and AXL BIN. AXL BIN support was introduced in this version.
Added
  • AXL ECU (Bin) USB device support — the SDK now recognises the AXLSYSTEMS AXLECU hardware (VID 0x061F / PID 0x3D38). A custom buildProber() method extends the default usb-serial-for-android probe table with the AXLECU VID/PID mapped to CdcAcmSerialDriver.
Fixed
  • 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 flush IOException now closes the stale port and falls through to a fresh open.
AXL RFID SDK — Android

v26.2.8

2026-07-16
Supports AXL FLAT only.
Added
  • Sdk.releasePort() — explicit USB port release for app-close path — call from Activity.onStop() when isFinishing()=true after any disconnectBlocking(). Prevents a lingering port from causing COMMAND_TIMEOUT when another app connects. No-op for BLE/WiFi transports.
  • Sdk.getDeviceConfig() — explicit device config fetch — sends a device_config command and fires SdkListener.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.
Changed
  • MAX_PAYLOAD reduced from 120 → 64 bytes — aligns chunk size to USB Full Speed bulk transfer boundary, halving ACK latency per chunk.
  • ack_connection_sync no longer contains config_data — config is now fetched via the new device_config command immediately after connect. SDK handles both old and new handshake responses gracefully.
  • RF_INIT_TAIL_MS reduced 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.
Fixed
  • 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 in disconnect() 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.