Подключение сканера к Android или как почувствовать себя кассиром
Привет! На связи Константин, Android разработчик Joy Dev. Недавно на одном из проектов мне потребовалась поддержка беспроводного сканера и получение с него данных. Проведя поверхностный поиск, я обнаружил:
1. Несколько статей, под копирку рассказывающих о подключении к сторонним приложениям по типу 1C.
2. Несколько сканов официальной документации по настройке сканера.
Казалось бы, вроде все есть — бери и подключай. Но есть одна загвоздка: всё найденное это лишь руководство для обычного пользователя, а что дальше с этим делать — неизвестно.
Итак, наша основная задача — получить данные со сканера в нашем собственном приложении и как-то их использовать, в этом руководстве мы их просто выведем на экран. В качестве примера возьмём 2D сканер Mertech CL-2210.
Оглавление
Настройка и подключение
Что такое KeyEvent и с чем его едят
Кто такой Input Device и зачем он нам
Разберемся с кодом
Не забудем и про мейнстримный Compose
Теперь напишем наше приложение
Расшифровка данных
Настройка и подключение
Начнём с настройки сканера. Первое, что мы сделаем, это включим обнаружение сканера. Для начала нужно отсканировать код из руководства по настройке.
При подключении в окне доступных устройств ищем объект, имя которого совпадает с именем на самом сканере, в нашем случае — это M5AL093997. Затем связываемся с устройством. В дальнейшем при включении сканера он сразу будет синхронизироваться с Android устройством.
Для справки: сканер можно подключить к Android устройству не только напрямую к Bluetooth, но и через специальный адаптер, идущий вместе со сканером. При подключении через адаптер нужно просто подключить его к Android устройству и просканировать штрих-код на нём.
При любом варианте подключения система воспринимает сканер как физическую клавиатуру, которая отправляет события клавиш и кнопок. Данные события отправляются в виде KeyEvent, в параметрах которого передаётся код клавиши/код ключа. Чтобы отследить, с какого девайса пришли данные, также передаётся идентификатор устройства. Стоит учитывать, что иногда приходит идентификатор виртуального устройства, который обрабатывает все входящие сообщения.
KeyEvent и с чем его едят
KeyEvent — это объект, используемый для сообщения о событиях клавиш и кнопок.
Каждое нажатие клавиши описывается последовательностью ключевых событий. Нажатие начинается с ключевого события ACTION_DOWN. Если держать клавишу достаточно долго, то она будет повторно присылаться с увеличением счётчика повторений. Последнее ключевое событие — это ACTION_UP.
Что же такое ACTION_DOWN и ACTION_UP? Это константы действий: ACTION_DOWN символизирует начало действия, нажатие на клавишу, а ACTION_UP символизирует конец действия, условно отжатия клавиши.
На уровне API меньше 29-ого ещё есть ACTION_MULTIPLE — происходит, если произошло несколько повторяющихся событий одного ключа или добавляется сложная строка. На уровнях API 29 и выше данная константа не используется системой ввода.
Ключевые события обычно сопровождаются кодом ключа, кодом сканирования и мета-состоянием. Коды ключа описаны вместе с классом в качестве констант. Константы кода сканирования — это исходные коды, зависящие от конкретного устройства, полученные из операционной системы, поэтому обычно не имеют смысла. Мета-состояние описывают нажатое состояние модификаторов клавиш, например, Shift или Caps Lock (META_SHIFT_ON и META_CAPS_LOCK_ON).
Коды клавиш обычно соответствуют отдельным клавишам на устройстве ввода. Многие клавиши и их комбинации выполняют совершенно разные функции на различных устройствах ввода, поэтому при их интерпретации необходимо соблюдать осторожность.
Поскольку методы программного ввода могут использовать множество изобретательных способов ввода текста, то нет гарантии, что любое нажатие клавиши на программной клавиатуре не сгенерирует ключевое событие.
В общем случае платформа не может гарантировать, что ключевые события, которые она доставляет в представление, всегда представляют собой полные последовательности ключей, поскольку некоторые события могут быть удалены или изменены путём добавления представлений до их доставки.
Я немного слукавил, сказав, что сканер — это обычная клавиатура, на самом деле это Input Device.
Разбираемся с Input Device
Input Device — это класс, который описывает возможности конкретного устройства. Каждое устройство может иметь несколько источников ввода. Например, стандартный комплект (клавиатура и мышь), подключаемый с помощью одного Bluetooth модуля, спокойно определяются системой и как клавиатура, и как устройство наведения.
Поскольку устройство может иметь различные источники ввода, приложение может запрашивать платформу характеристики каждого отдельного источника. Иногда различные источники ввода используют разные системы координат для описания событий движения.
Переходим к коду
Теперь разберёмся, как программно взаимодействовать с полученными кодами ключей.
Каждый класс, который может получать события кнопок, должен реализовать интерфейс KeyEvent.Callback, в который входят несколько функций:
abstract fun onKeyDown(
keyCode: Int,
event: KeyEvent!
): Boolean
onKeyDown — обрабатывает событие нажатия кнопки
abstract fun onKeyUp(
keyCode: Int,
event: KeyEvent!
): Boolean
onKeyUp — обрабатывает событие отпускания кнопки
abstract fun onKeyLongPress(
keyCode: Int,
event: KeyEvent!
): Boolean
onKeyLongPress — обрабатывает событие длинного нажатия
abstract fun onKeyMultiple(
keyCode: Int,
count: Int,
event: KeyEvent!
): Boolean
onKeyMultiple — обрабатывает множественные операции, которые приходят от аналоговых устройств, создающих события нажатия и отпускания одной и той же кнопки несколько раз за короткий промежуток времени.
Все функции возвращают логический тип. От возвращаемого значения зависит, нужно ли вызывать другие обработчики нажатий. Если нужно считать, что обработка завершена, то стоит вернуть значение «Истина» (true), что не позволит отправить событие в следующие слушатели. Если вам нужно, чтобы последующие слушатели тоже получили событие, то стоит вернуть false.
развернуть код
override fun onKeyUp(keyCode:Int, event:KeyEvent?):Boolean{
/**
В данном случае событие будет считаться обработанным
и не отправится дальше по наследникам для обработки
*/
if(keyCode == KeyEvent.ACTION_UP){
return true
}
return super.onKeyUp(keycode,event)
}
развернуть код
override fun onKeyUp(keyCode:Int, event:KeyEvent?):Boolean{
/**
В данном случае событие не будет считаться обработанным
и отправится дальше по наследникам для обработки
*/
if(keyCode == KeyEvent.ACTION_UP){
return false
}
return super.onKeyUp(keycode,event)
}
По умолчанию данный интерфейс реализуют Activity и View. Чтобы вызвалась функция интерфейса, класс представления и активити должен быть сфокусирован.
Не забудем и про мейнстримный Compose
В Compose есть функция расширения для интерфейса Modifier, в которой можно обработать получаемые коды ключей — onKeyEvent.
развернуть код
Box(modifier=Modifier
.fillMaxSize()
.focusRequester(FocusRequester()
.focusable(true)
.onKeyEvent{event->
true
}
)
Тут также работает система с обработкой событий кнопок, если передать логическое «Истина», то во вложенные элементы не отправятся в событие.
Пишем приложение
Теперь давайте сделаем небольшое приложение с выбором устройства и получения с него данных. Это будет двустраничное приложение со списком устройств и получения данных с них.
В статье мы рассмотрим только реализацию на Compose, но в репозитории можно будет найти реализацию на Android View.
Итак, нам нужен главный экран со списком
развернуть код
@Composable
fun MainScreen(navController: NavController) {
BaseScreen {
val inputDevices = InputDevice.getDeviceIds().map { InputDevice.getDevice(it) }
LazyColumn {
item {
Text(
modifier = Modifier.padding(8.dp),
text = stringResource(id = R.string.devices),
style = MaterialTheme.typography.titleLarge
)
}
items(inputDevices) { inputDevice ->
DeviceItem(
modifier = Modifier
.clickable {
navController.navigate(Screen.Details.getRoute(inputDevice.id))
},
item = inputDevice
)
}
}
}
}
Получаем идентификаторы устройств и по идентификаторам — данные об устройствах ввода. Из этих данных получим их имя, идентификатор и какие они могут передать данные. При клике мы пойдём на экран, где уже и будем получать данные со сканера.
Теперь посмотрим на второй экран
развернуть код
@Composable
fun Details(navController: NavController, deviceId: Int) {
val requester = FocusRequester()
BaseScreen {
val decoder= remember {
Decoder.Scanner(deviceId, NativeKeyEvent.KEYCODE_DPAD_DOWN)
}
var receivedData by remember {
mutableStateOf("")
}
Column {
Header(
inputDevice = InputDevice.getDevice(deviceId),
onBackPressed = { navController.popBackStack() }
)
ReceivedData(
modifier = Modifier
.focusRequester(requester)
.focusable(true)
.onKeyEvent { event ->
if (event.type == KeyEventType.KeyDown) {
receivedData = decoder.handleKey(keyEvent = event.nativeKeyEvent) ?: ""
}
true
},
data = receivedData
) {
(decoder as? Decoder.KeyBoard)?.clearTemp()
receivedData = ""
}
}
}
SideEffect {
requester.requestFocus()
}
}
И этот экран у нас может быть как с данными, так и без них.
Расшифровываем данные
А теперь рассмотрим, за счёт чего происходит расшифровка данных, приходящих со сканера.
Создаётся класс декодера Сканер, мы передаём туда id устройства, данные от которого мы ожидаем, и код завершения потока символов. В этом случае мы передаём код кнопки вниз.
Decoder.Scanner (deviceId, NativeKeyEvent.KEYCODE_DPAD_DOWN)
Чтобы отловить сообщения от клавиш, необходимо в Activity в onKeyDown отправить false, иначе система сама обработает данные, без нашего участия.
развернуть код
class MainActivity : ComponentActivity() {
...
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
...
return false.
}
....
}
Теперь на экране надо запросить для элемента фокусировку и поставить слушатель.
развернуть код
Modifier.focusRequester(requester)
.focusable(true)
.onKeyEvent { event ->
if (event.type == KeyEventType.KeyDown) {
receivedData = decoder.handleKey(keyEvent = event.nativeKeyEvent) ?: ""
}
true
}
Давайте посмотрим на код декодера.
Тут нас интересует класс Scanner
sealed interface Decoder {
Данная функция обрабатывает большинство возможных зашифрованных данных, которые могут прийти со сканера или другого устройства ввода.
развернуть код
fun handleKey(keyEvent: KeyEvent): String? {
return when (keyEvent.keyCode) {
in KeyEvent.KEYCODE_0..KeyEvent.KEYCODE_9,
in KeyEvent.KEYCODE_A..KeyEvent.KEYCODE_PERIOD,
in KeyEvent.KEYCODE_GRAVE..KeyEvent.KEYCODE_AT, KeyEvent.KEYCODE_PLUS, KeyEvent.KEYCODE_SPACE -> {
var newString = ""
Character.toChars(keyEvent.getUnicodeChar(keyEvent.metaState))
.map { it }
.toString().let { string ->
for (i in string.indices step 3) {
newString += string[i + 1]
}
}
newString.ifEmpty { return null }
}
else -> null
}
}
Данный класс используется для обработки данных со сканера
class Scanner(private val deviceId: Int, private val lastKeyAction: Int) : Decoder
У нас есть поле для сохранности данных на время декодирования
private var temp = ""
Есть функция handeKey, которая отдаёт опциональный текст. Если текст пока не должен уйти в Ui, то отправится null
развернуть код
override fun handleKey(keyEvent: KeyEvent): String? {
if (keyEvent.deviceId != deviceId) return null
val defaultHandler = super.handleKey(keyEvent)
temp += defaultHandler ?: ""
if (defaultHandler == null) {
when (keyEvent.keyCode) {
lastKeyAction -> {
val value = temp
temp = ""
return value
}
}
}
return null
}
}
Данный класс можно использовать при подключении физической клавиатуры. В этом случае каждый символ сразу отправляется в слушатель, а очистка поля temp перекладывается на внешний класс.
развернуть код
class Keyboard(private val deviceId: Int) : Decoder {
private var temp = ""
override fun handleKey(keyEvent: KeyEvent): String? {
if (keyEvent.deviceId != deviceId) return null
temp += super.handleKey(keyEvent)
return temp
}
fun clearTemp() {
temp = ""
}
}
}
По итогу мы смогли:
Получить данные о подключенных устройствах
Получить данные со сканера
Расшифровать данные
Надеюсь, данная статья будет вам полезна в разработке.
Исходный код на Compose и Android Views можно найти тут.