Alaqan Mini SDK
для Android

Java SDK для сканера ладони Alaqan Mini. Поставляется в виде AAR; работает через USB Host на Android 7.0 и выше.

v2.1.0 Апрель 2026 minSdk 24 arm64-v8a · armeabi-v7a

Обзор

Три операции: startIdentify(), startEnroll(), startRescan(). Два реестра слушателей: DeviceEventsListener для состояния устройства и FrameListener для обработанных кадров. Оба привязываются к вашему LifecycleOwner. SDK берёт на себя USB-сессию, поток с двух камер и инференс на устройстве. От приложения требуется UI и API-ключ.

Публичный интерфейс

// Подготовка
sdk.setApiKey(String);
sdk.initialize(Context);
sdk.addDeviceEventsListener(LifecycleOwner, DeviceEventsListener);
sdk.addFrameListener(LifecycleOwner, FrameListener);          // для превью, по необходимости

// Операции (только одна одновременно)
sdk.startIdentify(IdentifyCallback);
sdk.startEnroll(externalId, name, metadata, EnrollCallback);
sdk.startRescan(externalId, RescanCallback);
sdk.stopOperation();

// Устройство
sdk.getDevice().connect() / disconnect() / reboot();

// Завершение работы (при выходе из процесса)
sdk.destroy();

Требования

ПараметрЗначение
Android API24+ (рекомендуется target 34)
ABIarm64-v8a, armeabi-v7a
Размер AAR~21 МБ
Расход RAM~30 МБ нативной памяти в установившемся режиме
Аппаратные требованияUSB Host (OTG); сканер Alaqan Mini
СетьИсходящий HTTPS для активации и биометрических вызовов
РазрешенияINTERNET (объявляется самим SDK); фича android.hardware.usb.host

Установка

1

Положите AAR в app/libs/

app/
├── libs/
│   └── alaqan-mini-sdk.aar
└── build.gradle.kts
2

Подключите в build.gradle.kts

android {
    defaultConfig {
        minSdk = 24
        ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a") }
    }
}

dependencies {
    implementation(files("libs/alaqan-mini-sdk.aar"))
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("androidx.lifecycle:lifecycle-common:2.6.2")
}
3

USB device-фильтр (res/xml/device_filter.xml)

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-device vendor-id="17992" product-id="65280" />
</resources>
4

AndroidManifest.xml: минимальная настройка активности

<uses-feature android:name="android.hardware.usb.host" android:required="true" />

<activity android:name=".MainActivity"
          android:launchMode="singleTask"
          android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
               android:resource="@xml/device_filter" />
</activity>
Зачем singleTask?

USB-attach-интент из манифеста придёт в уже работающий экземпляр через onNewIntent(), а не запустит новую активность. USB-сессия SDK при этом не разрывается.

Быстрый старт

Один экземпляр AlaqanMiniSDK на процесс. Храните его в Application. Launcher-активность инициализирует SDK и открывает USB-сессию; дальше вы активируете UI по состоянию подключения. Каждая операция живёт в своей активности (см. раздел Идентификация).

// App.java: единственный экземпляр SDK, общий для всех активностей.
public final class App extends Application {
    private final AlaqanMiniSDK sdk = new AlaqanMiniSDK();
    public AlaqanMiniSDK getSdk() { return sdk; }
}
// MainActivity.java: поднимает SDK и реагирует на его события.
public class MainActivity extends AppCompatActivity
        implements DeviceEventsListener {

    private AlaqanMiniSDK sdk;

    @Override
    protected void onCreate(Bundle b) {
        super.onCreate(b);
        sdk = ((App) getApplication()).getSdk();
        sdk.setApiKey(BuildConfig.ALAQAN_API_KEY);
        sdk.addDeviceEventsListener(this, this);   // удалится автоматически при ON_DESTROY
        sdk.initialize(this);
    }

    @Override public void onInitialized()                                  { sdk.getDevice().connect(); }
    @Override public void onDeviceConnected(@NonNull Device d)        { /* откройте доступ к операциям в UI */ }
    @Override public void onDeviceDisconnected(@NonNull Device d)     { /* закройте доступ к операциям в UI */ }
    @Override public void onError(@NonNull SdkError e)                  { /* только фоновые ошибки; ошибки операций приходят в её колбэк */ }

    @Override
    protected void onDestroy() {
        if (isFinishing()) sdk.destroy();
        super.onDestroy();
    }
}
Не зашивайте API-ключ в APK.

Подставляйте ключ через BuildConfig, получайте от своего бэкенда при первом запуске или храните в защищённом хранилище. SDK ключ нигде не сохраняет.

Не запускайте операции автоматически из onDeviceConnected.

Это событие срабатывает на каждом переподключении (дёрнули кабель, мягкая перезагрузка). Запускайте операции в ответ на действие пользователя, обычно в onResume отдельного экрана.

Модель слушателей

Два реестра, оба поддерживают несколько подписчиков и привязку к жизненному циклу. Регистрируйте слушатель один раз на каждый LifecycleOwner; SDK снимет его при ON_DESTROY.

РеестрЧто доставляетЧастотаПривязка к жизненному циклу
DeviceEventsListenerГотовность SDK, подключение/отключение устройства, фоновые ошибкиРедкоСнимается при ON_DESTROY
FrameListenerFrameResult на каждый кадр~30 ГцАктивен только между ON_START и ON_STOP

Повторная доставка состояния

Если onInitialized() или onDeviceConnected() уже произошли до подписки, SDK доставит их заново новому слушателю. Активность, пересозданная после поворота, переподпишется и получит актуальное состояние без опроса isInitialized() / isConnected().

Автоматический стриминг

Поток кадров запускается, когда добавлен первый FrameListener, и останавливается после удаления последнего. Методов startStreaming() / stopStreaming() нет; переключателем служит сама регистрация.

Где появляются ошибки

ИсточникКуда приходит
Во время активной операцииВ onResult колбэка операции
Вне операций (инициализация, USB, watchdog)В DeviceEventsListener.onError

Ошибки никогда не приходят дважды. За исход операции отвечает её собственный колбэк.

Потоки

Все колбэки вызываются в главном потоке. Публичные методы SDK можно вызывать из любого потока. Единственное ограничение: Device.getSensingRange() ожидает ответа по USB, поэтому вызывайте его из фонового потока.

stopOperation() потокобезопасен. Терминальный onResult(OP_CANCELLED) всегда приходит в главном потоке; при вызове из главного потока он срабатывает синхронно, до возврата из вашего обработчика.

Жизненный цикл

Один экземпляр SDK на процесс; жизненный цикл каждой активности привязывает свои слушатели.

ГдеЧто вызывать
Application.onCreate()new AlaqanMiniSDK() и держать в Application
onCreate() первой активностиsetApiKey(...) · addDeviceEventsListener(this, this) · initialize(this)
onCreate() любой активностиaddDeviceEventsListener(this, this). Повтор sticky-состояния синхронизирует UI.
onCreate() экрана с превьюaddFrameListener(this, this). Стрим идёт только в состоянии STARTED.
onDestroy() launcher-активности при isFinishing()destroy()
destroy() можно вызвать только один раз.

После destroy() повторный initialize() на том же экземпляре выбрасывает IllegalStateException. Чтобы инициализироваться заново, создайте новый AlaqanMiniSDK. Не вызывайте destroy() в onDestroy() любой активности, кроме launcher: при этом выгружаются модели и следующий запуск будет «холодным».

Операции

Три операции; одновременно может выполняться только одна. Запуск второй, пока первая не завершилась, немедленно возвращает OP_ALREADY_RUNNING в колбэк новой операции.

void           startIdentify(IdentifyCallback cb);
void           startEnroll(String externalId, String name,
                             @Nullable String metadata, EnrollCallback cb);
void           startRescan(String externalId, RescanCallback cb);
void           stopOperation();
void           setOperationTimeout(long ms);   // по умолчанию 30 000; 0 отключает таймаут
OperationState getOperationState();        // IDLE | DETECTING | CAPTURING | PROCESSING
OperationType  getOperationType();         // NONE | IDENTIFY | ENROLL | RESCAN

Состояния

СостояниеЧто означает
IDLEАктивной операции нет
DETECTINGОжидание руки, проходящей пороги уверенности, качества и стабильности
CAPTURINGТолько enroll/rescan: набираются три кадра
PROCESSINGКадры захвачены, идёт обращение к серверу

Идентификация (1:N)

Захватывает один стабильный кадр и отправляет его на сервер. В результате приходит найденный пользователь или ошибка. Запускайте в onResume, отменяйте в onPause; активность владеет временем жизни операции.

// IdentifyActivity.java: один цикл identify, привязан к onResume/onPause.
public class IdentifyActivity extends AppCompatActivity
        implements IdentifyCallback {

    private AlaqanMiniSDK sdk;

    @Override
    protected void onCreate(Bundle b) {
        super.onCreate(b);
        sdk = ((App) getApplication()).getSdk();
    }

    @Override protected void onResume() { super.onResume();  sdk.startIdentify(this); }
    @Override protected void onPause()  { sdk.stopOperation(); super.onPause(); }

    @Override public void onPreviewFrame(@NonNull FrameResult r) {
        // r.bitmap общий буфер. Отрисуйте здесь. Ссылку не сохраняйте.
    }

    @Override public void onGuidance(@NonNull Guidance g) {
        // сопоставьте g со своими локализованными строками; SDK сам не локализует.
    }

    @Override public void onResult(@NonNull IdentifyResult r) {
        if (isFinishing() || isDestroyed()) return;

        ErrorCode code = r.error != null ? r.error.code : null;
        if (code == ErrorCode.OP_CANCELLED) return;                  // пользователь ушёл с экрана
        if (code == ErrorCode.OP_DISCONNECTED
                || code == ErrorCode.OP_NOT_CONNECTED) { finish(); return; }

        // r.isSuccess() → r.externalId, r.name, r.metadata
        // иначе → r.error
        finish();
    }
}
Поле IdentifyResultТипКомментарий
scanIdString?Идентификатор скана на сервере; только при успехе
externalIdString?External ID найденного пользователя
nameString?null, если имя не задавалось при регистрации
metadataString?JSON-строка, заданная при регистрации
errorSdkError?null при успехе

Регистрация

Захватывает три стабильных кадра и отправляет на сервер тот, у которого лучшее качество. onProgress срабатывает после каждого захвата, удобно для индикатора N/3.

sdk.startEnroll("emp-1024", "Айман", null, new EnrollCallback() {
    @Override public void onGuidance(@NonNull Guidance g)            { /* обновите подсказку */ }
    @Override public void onProgress(int captured, int total)        { /* отрисуйте N/total */ }
    @Override public void onResult(@NonNull EnrollResult r) {
        // r.isSuccess() → r.scanId; иначе r.error.
    }
});
  • name и metadata необязательны; передавайте null, чтобы пропустить.
  • metadata, если задано, должно быть валидной JSON-строкой объекта.
  • EnrollResult содержит поля scanId и error.

Повторное сканирование

Заменяет биометрический шаблон уже зарегистрированного пользователя, сохраняя его name и metadata. Поведение и тип результата те же, что у регистрации. Интерфейс-маркер RescanCallback extends EnrollCallback делает намерение явным в месте вызова.

sdk.startRescan("emp-1024", new RescanCallback() {
    @Override public void onGuidance(@NonNull Guidance g)            { /* обновите подсказку */ }
    @Override public void onProgress(int captured, int total)        { /* отрисуйте N/total */ }
    @Override public void onResult(@NonNull EnrollResult r) {
        // r.isSuccess() при успехе; иначе r.error.
    }
});

Подсказки

UI-подсказки, приходящие при смене состояния. Сопоставляйте каждое значение со своими локализованными строками; SDK сам не локализует.

ЗначениеКогда
SHOW_PALMВ кадре нет руки
MOVE_CLOSERПлощадь ограничивающей рамки руки ниже порога
MOVE_FURTHERПлощадь ограничивающей рамки руки выше порога
CENTER_PALMРамка смещена от центра кадра
HOLD_STEADYРука найдена, но не пройдены пороги качества или стабильности
CAPTURINGТолько enroll/rescan; срабатывает на каждый захваченный кадр
PROCESSINGИдёт обращение к серверу

Справочник API

AlaqanMiniSDK

void           setApiKey(@Nullable String key);
void           initialize(@NonNull Context ctx);
boolean        isInitialized();
Device         getDevice();
@Nullable String getNativeVersion();
void           setLedGateEnabled(boolean on);   // по умолчанию true
void           setConfig(@NonNull SdkConfig cfg);
SdkConfig      getConfig();
void           destroy();
МетодКонтракт
setApiKeyДолжен быть вызван до initialize(). Установка null после инициализации ведёт к ошибкам API_KEY_MISSING на серверных вызовах, пока не будет задан новый ключ.
initializeАсинхронный. При успехе вызывает onInitialized(). При ошибке вызывает onError(SdkError) с одним из API_KEY_MISSING / SERVER_NETWORK_ERROR / MODEL_LOAD_FAILED. Идемпотентен, пока не вызван destroy().
setLedGateEnabledПри true кадры с выключенным LED устройства не передаются на инференс. Установите false для хорошо освещённых стационарных установок, где LED не зажигается.
destroyОсвобождает нативную память, USB-сессию и потоки. Вызывается на экземпляр один раз.

Device

// Соединение
void    connect();              // до инициализации SDK ничего не делает
void    disconnect();
boolean isConnected();

// Идентификация устройства
@Nullable String getSerialNumber();    // напр. "GX4TYQWH3D"
@Nullable String getFirmwareVersion(); // напр. "2026042319"

// Датчик расстояния
int     getDistance();                       // последнее измерение в мм; -1, если недоступно
@Nullable int[] getSensingRange();          // {min,max} в мм. БЛОКИРУЮЩИЙ ВЫЗОВ.
void    setSensingRange(int min, int max); // асинхронно; по умолчанию 50–200; ограничен [0,255]

// Камеры
Cameras cameras();

// Питание
void    reboot();   // мягкая перезагрузка: отключение и автоподключение

Все геттеры возвращают null или -1, пока не сработает onDeviceConnected. setSensingRange работает «отправил и забыл». getSensingRange ожидает ответа по USB; вызывайте его из фонового потока.

Cameras

У сканера две камеры: CameraType.IR (инфракрасная) и CameraType.VL (видимый свет). Активная камера служит источником превью, доставляемого в FrameListener.

Cameras cams = sdk.getDevice().cameras();

cams.setActive(CameraType.VL);   // сменить источник превью
CameraType active = cams.getActiveType();

long ir = cams.getIrFrameCount();   // кадры с момента подключения
long vl = cams.getVlFrameCount();

Камеру детекции SDK выбирает сам на время операции; партнёрскому коду она не доступна.

SdkConfig

Сейчас доступен один параметр: интервал срабатывания watchdog при отсутствии кадров. Применяется через setConfig(); начинает действовать при следующем запуске watchdog.

sdk.setConfig(new SdkConfig.Builder()
        .frameWatchdogMs(15_000L)        // по умолчанию 30 000; минимум 1 000
        .build());

Если устройство подключено, но за frameWatchdogMs мс не пришло ни одного кадра, SDK однократно выдаёт DEVICE_NOT_RESPONDING в onError. Соединение при этом остаётся открытым; это сигнал, а не команда на завершение. Пороги качества и стабильности партнёром не настраиваются.

Слушатели и колбэки

DeviceEventsListener

public interface DeviceEventsListener {
    default void onInitialized() {}
    default void onDeviceConnected(@NonNull Device d) {}
    default void onDeviceDisconnected(@NonNull Device d) {}
    default void onError(@NonNull SdkError error) {}
}
sdk.addDeviceEventsListener(listener);                   // ручное управление временем жизни
sdk.addDeviceEventsListener(lifecycleOwner, listener);   // снимается при ON_DESTROY
sdk.removeDeviceEventsListener(listener);

Все методы помечены как default; реализуйте только нужные. Ошибки активной операции сюда не приходят.

FrameListener

public interface FrameListener { void onFrame(@NonNull FrameResult r); }
sdk.addFrameListener(listener);                         // стрим запускается
sdk.addFrameListener(lifecycleOwner, listener);         // активен только в STARTED
sdk.removeFrameListener(listener);                      // после удаления последнего стрим останавливается
Владение буфером.

FrameResult.bitmap используется совместно и переиспользуется. Внутри onFrame отрисуйте кадр или скопируйте его через Bitmap.copy(); не вызывайте recycle() и не сохраняйте ссылку за пределами колбэка.

IdentifyCallback

public interface IdentifyCallback {
    default void onPreviewFrame(@NonNull FrameResult r) {}
    void         onGuidance(@NonNull Guidance g);
    void         onResult(@NonNull IdentifyResult r);
}

onResult срабатывает ровно один раз. onPreviewFrame по умолчанию пустой; переопределяйте его, только если рисуете превью прямо из колбэка операции. Иначе используйте отдельный FrameListener.

EnrollCallback

public interface EnrollCallback {
    default void onPreviewFrame(@NonNull FrameResult r) {}
    void         onGuidance(@NonNull Guidance g);
    void         onProgress(int captured, int total);   // total сейчас равен 3
    void         onResult(@NonNull EnrollResult r);
}

RescanCallback

public interface RescanCallback extends EnrollCallback {}

Интерфейс-маркер. Методы те же, что в EnrollCallback. Используйте его в месте вызова, чтобы намерение было видно; кроме того, SDK сможет добавить специфичные для rescan события, не сломав код, использующий EnrollCallback.

Типы данных

FrameResult

ПолеТипКомментарий
jpegSizeintРазмер исходного JPEG в байтах
width / heightintРазмеры кадра в пикселях
bitmapBitmap?Декодированное превью. Общий буфер: не вызывайте recycle, не сохраняйте ссылку
handsHandDetection[]?null, если рук не найдено
moreHandsbooleantrue, если в кадре оказалось больше рук, чем влезло в массив
inferenceMsfloatПолное время инференса
ledOnbooleanСостояние LED устройства в момент захвата
steadybooleanРамка стабильна между последовательными детекциями

HandDetection

ПолеТипКомментарий
confidencefloatУверенность детекции, 0–1
x / y / w / hfloatЛевый верхний угол + ширина и высота рамки в пикселях
qualityfloatОценка качества, 0–100; -1, если не вычислено

Результаты

Оба типа результатов устроены одинаково: сначала isSuccess(); при неудаче читайте error.

ТипПоляВозвращается из
IdentifyResultscanId, externalId, name, metadata, errorstartIdentify()
EnrollResultscanId, errorstartEnroll(), startRescan()

SdkError

public final class SdkError {
    @NonNull  public final ErrorCode code;
    @Nullable public final String    detail;
}

В коде ветвитесь по code (enum). Поле detail содержит сообщение сервера для SERVER_REJECTED или текст исключения. Используйте его для логов, не для управляющей логики.

ErrorCode предоставляет: code() (стабильный 4-значный код для телеметрии), category() (DEVICE, AUTH, STREAMING, NATIVE, NETWORK, OPERATION) и fromCode(String).

@Override
public void onError(@NonNull SdkError e) {
    switch (e.code) {
        case API_KEY_MISSING:        promptForKey(); break;
        case DEVICE_NOT_RESPONDING:
        case USB_NO_FW_VERSION:      // информационное; UI не трогаем          break;
        default:
            if (e.code.category() == ErrorCode.Category.NETWORK) showOffline();
            else showError(e.code.code());
    }
}

Коды ошибок

Полный enum ErrorCode. В коде ветвитесь по значению enum; code() возвращает стабильный 4-значный строковый код, удобный для логов и серверной отчётности.

Устройство и USB (1xxx)

КонстантаЧто означает · что делать
1001USB_PERMISSION_DENIEDПользователь отклонил системный диалог разрешения на USB. Запросите снова или подскажите, как разрешить доступ.
1002USB_NO_INTERFACEНа устройстве нет нужного USB-интерфейса. Проверьте VID/PID-фильтр и кабель.
1003USB_NO_ENDPOINTSНет требуемых USB-эндпоинтов. Аппаратная проблема: переподключите или замените устройство.
1004USB_OPEN_FAILEDНе удалось открыть USB-устройство. Часто следует за сбоем USB-стека ядра; попробуйте переподключить.
1005USB_CLAIM_FAILEDИнтерфейс держит другой процесс. Закройте другие приложения, использующие устройство.
1006USB_NO_SERIALСерийный номер прочитать не удалось. Без него невозможны серверные биометрические вызовы; обработайте как ошибку подключения.
1007DEVICE_NOT_RESPONDINGЗа время frameWatchdogMs не пришло ни одного кадра. Информационное; соединение сохраняется.
1008USB_NO_FW_VERSIONНе удалось получить версию прошивки. Информационное; соединение сохраняется.

Аутентификация (2xxx)

КонстантаЧто означает · что делать
2001AUTH_CHALLENGE_FAILEDАутентификация устройства не прошла на этапе 1. Переподключите или перезагрузите устройство.
2002AUTH_INVALID_REPLYУстройство вернуло неожиданный ответ во время аутентификации. Переподключите или перезагрузите.
2003AUTH_COMPUTE_FAILEDСбой клиентского шага аутентификации. Вызовите initialize() повторно.
2004AUTH_VERIFY_FAILEDАутентификация устройства не прошла на этапе 2. Переподключите или перезагрузите устройство.
2005AUTH_REJECTEDУстройство отвергло учётные данные SDK. Вызовите initialize() повторно; если ошибка повторяется, обратитесь в поддержку.

Стриминг (3xxx)

КонстантаЧто означает · что делать
3001STREAM_START_FAILEDНе удалось отправить команду запуска стрима. Переподключите устройство; если ошибка повторяется, обратитесь в поддержку.
3002STREAM_REJECTEDУстройство отклонило запуск стрима. Перезагрузите устройство.

SDK и нативная часть (4xxx)

КонстантаЧто означает · что делать
4001NATIVE_CREATE_FAILEDНе удалось запустить нативный рантайм. Убедитесь, что упакованы обе ABI.
4002MODEL_LOAD_FAILEDНе удалось загрузить модели. Убедитесь, что активация прошла без ошибок (нет предшествующих SERVER_*).
4004AUTH_PROVIDER_MISSINGВнутренняя ошибка конфигурации; в релизных сборках не возникает.

Сеть (5xxx)

КонстантаЧто означает · что делать
5001API_KEY_MISSINGЗадайте ключ через setApiKey() до initialize().
5002SERVER_NETWORK_ERRORТранспортная ошибка. Покажите офлайн-UI и предложите повтор.
5003SERVER_INVALID_RESPONSEНекорректный ответ сервера. Соберите логи; при повторе обратитесь в поддержку.
5004SERVER_REJECTEDСервер вернул сообщение об ошибке. Читайте error.detail; типичные значения собраны в каталоге ниже.
5005SERVER_TIMEOUTЗапрос превысил 15 с. Предложите повтор; проверьте качество сети.

Операции (6xxx)

КонстантаЧто означает · что делать
6001OP_ALREADY_RUNNINGУже выполняется другая операция. Вызовите stopOperation() или дождитесь её завершения.
6002OP_CANCELLEDВы вызвали stopOperation(). В UI обработайте как пустое действие.
6003OP_TIMEOUTИстёк таймаут операции. Покажите UI повторной попытки.
6004OP_DISCONNECTEDУстройство отключилось во время операции. Закройте экран.
6005OP_NOT_INITIALIZEDОперация запущена до onInitialized(). Дождитесь события или проверьте isInitialized().
6006OP_NOT_CONNECTEDОперация запущена до onDeviceConnected(). Дождитесь или проверьте device.isConnected().
6007OP_INVALID_PARAMSНеверные параметры. Обычно это пустой externalId у enroll/rescan.

Каталог SERVER_REJECTED.detail

Когда code == SERVER_REJECTED, в error.detail лежит сообщение сервера в исходном виде. Сопоставьте их со своими UI-строками:

detailВ каких операцияхЧто показать
"no hand detected"любаяПопросите пользователя повторить попытку
"quality check failed"любаяПопросите повторить, поднеся руку чище или ближе
"liveness check failed"любаяПопросите повторить с настоящей ладонью
"template not found"identifyПокажите «не зарегистрирован»
"person not found"rescanУказанный externalId не зарегистрирован; переключитесь на enroll
"person already exists"enrollСпросите, не выполнить ли rescan вместо регистрации
"template already enrolled"enroll/rescanБиометрия совпала с другим пользователем. Разберитесь со случаем вручную.
"duplicate capture detected"любаяОдин и тот же снимок отправлен дважды; вероятно, двойной тап.
"invalid inference"любаяНе повторять. Эскалируйте в поддержку.
"inference unavailable"любаяПовторите запрос с экспоненциальной задержкой

Лучшие практики

Один экземпляр SDK в Application

Создавайте один раз, используйте во всех активностях, вызывайте destroy() только в момент завершения launcher-активности.

Привязывайте слушателей к LifecycleOwner

Слушатель снимется при ON_DESTROY автоматически. FrameListener ещё и приостанавливает поток, когда экран уходит на задний план.

@Override
protected void onCreate(Bundle b) {
    super.onCreate(b);
    sdk.addDeviceEventsListener(this, this);   // повтор sticky-состояния + автоудаление
    sdk.addFrameListener(this, this);           // поток только пока экран виден
}

Стройте UI вокруг onGuidance / onProgress / onResult

Отдельный FrameListener во время операции не нужен. Колбэки операций уже доставляют превью через onPreviewFrame. Выберите один источник.

Аккуратная отмена при навигации

@Override
protected void onPause() {
    sdk.stopOperation();   // выдаст OP_CANCELLED; обработайте как пустое действие
    super.onPause();
}

Ветвитесь по ErrorCode, а не по строке

Используйте enum и category(). Строки оставьте для логов и серверной телеметрии.

DEVICE_NOT_RESPONDING и USB_NO_FW_VERSION: информационные

Оба сигнальные, ни один не разрывает соединение. Реальные отключения приходят в onDeviceDisconnected.

Решение проблем

onDeviceConnected ни разу не срабатывает

  • Проверьте кабель, порт и поддержку OTG на хосте.
  • В device_filter.xml заданы vendor-id="17992" и product-id="65280".
  • У активности есть USB-фильтр в манифесте и android:launchMode="singleTask".

Инициализация падает с SERVER_NETWORK_ERROR / API_KEY_MISSING / MODEL_LOAD_FAILED

  • API-ключ задан до вызова initialize().
  • Сеть доступна. Активация идёт по HTTPS с тайм-аутом 15 с.
  • В APK упакованы обе ABI (arm64-v8a, armeabi-v7a).

Подсказка застряла на HOLD_STEADY

  • Рука вне настроенного диапазона; посмотрите device.getDistance().
  • На стекло сканера попадает прямой солнечный свет; защитная плёнка не снята.
  • Стационарная установка с выключенным LED. Вызовите setLedGateEnabled(false).

Кадры перестали приходить (DEVICE_NOT_RESPONDING)

  • Попробуйте device.reboot().
  • Если ошибка повторяется, переподключите устройство или обратитесь в поддержку.

Поддержка

Техническая

support@alaqan.com

Продажи

sales@alaqan.com

Что прикладывать к отчёту об ошибке

Строку из ErrorCode.code(). Поле error.detail, если есть. Шаги воспроизведения. Серийный номер устройства и версию прошивки. Версию SDK. Logcat за период сбоя.