# Работа со сканерами

Сканер передает считанное значение в систему, после чего прикладной обработчик определяет дальнейшее действие.

Для подключения используются два варианта:

- **HID** — сканер работает в режиме клавиатурной эмуляции и передает последовательность нажатий;
- **COM-порт** — данные принимаются через `Bts_ComPortLib`.

В обоих случаях результатом приема является строковое значение, которое можно обработать как штрихкод, QR-код или другой формат данных. Если устройство уже настроено как клавиатурный сканер, используется HID. Для работы через последовательный порт используется COM-механизм.

Подробности формирования кодов приведены на страницах [Штрихкоды](010_штрихкоды.md) и [QR-коды](020_qr_коды.md).

## Работа через HID

### Принцип работы

При работе через HID сканер эмулирует ввод с клавиатуры. Для распознавания ввода сканера используются начальный шаблон, конечный шаблон и таймаут.

Система собирает распознанную последовательность в строку и передает ее прикладной операции. Обработчик получает:

- `input: String` — считанное значение;
- `isByTimeout: Boolean` — признак завершения ввода по таймауту.

Если ввод завершен конечным шаблоном, `isByTimeout = false`. Если операция получила `isByTimeout = true`, ввод был завершен по таймауту, а не по заданному конечному шаблону.

### Префикс и завершающий символ HID-ввода

Для надежного определения ввода сканер должен передавать уникальный префикс и завершающий символ.

Префикс и завершающий символ задаются средствами самого сканера; способ их настройки зависит от модели устройства.

Например, если сканер передает:

- префикс `SCAN:`;
- считанное значение `4601234567890`;
- клавишу Enter после считанного значения,

браузер получает последовательность:

<!-- Начало кода -->
```text
SCAN:4601234567890\r
```
<!-- Конец кода -->

Если в качестве завершающего символа используется клавиша Enter, `KeyDecoder` формирует `\r`.

В прикладную операцию передается только считанное значение:

<!-- Начало кода -->
```text
4601234567890
```
<!-- Конец кода -->

Начальный и конечный шаблоны в итоговое значение не включаются.

### Ограничения HID

Текущий `KeyDecoder` поддерживает латинские символы, цифры верхнего ряда клавиатуры, пробельные символы и часть ASCII-знаков. Кириллица, коды цифровой клавиатуры и произвольные Unicode-символы не декодируются.

Поэтому QR-коды с нестандартными символами или бинарным содержимым нельзя надежно передавать этим способом.

## Работа через COM-порт

### Общая схема настройки

Для настройки работы со сканером через COM-порт:

1. Включите параметр **Доступно использование сканеров ШК**.
2. Добавьте в выборку операцию `comPortListener`.
3. Создайте операцию обработки считанного значения.
4. Зарегистрируйте операцию обработки для всех доступных COM-портов или для выбранного порта.
5. При закрытии интерфейса отмените регистрацию обработчика или закройте выбранный порт.

### Настройка работы с COM-портами

Параметр **Доступно использование сканеров ШК** (`bAllowUseBarCodeScanner`) определяет доступность работы с COM-портами.

Путь: `Настройка системы > Настройки и сервисы > Настройки модулей системы > Общие настройки модулей > bts`.

Параметр действует для всей системы и по умолчанию выключен.

Если параметр выключен, методы открытия и закрытия портов, регистрации обработчиков и обработки поступающих данных завершаются без выполнения действий и вывода сообщений.

#### Зависимости

Для работы библиотеки требуются:

- сервер приложений версии 1.21.0 или выше;
- [Gl3BrowserPlugin](https://docs.global-system.ru/as/dev/gs3-browser-cmd/plugin.html#gl3-browser-plugin) версии 0.17.0 или выше;
- [Gl3BrowserExtension](https://docs.global-system.ru/as/dev/gs3-browser-cmd/extension.html) версии 0.17.0 или выше.

Проверка наличия и версий требуемых компонентов выполняется при вызове `regComOper()` и `unRegComOper()`.

Методы `openComPortByName()` и `closeComPortByName()` такую проверку не выполняют.

Если при проверке версия сервера приложений, плагина или расширения не соответствует требованиям либо плагин или расширение отсутствует, информация записывается в журнал сервера приложений.

<!-- Начало кода -->
```{note}
Сообщение об ошибке не отображается пользователю в интерфейсе.
```
<!-- Конец кода -->

### Настройка приема данных из COM-порта

Для приема данных из COM-порта в выборке создайте скрытую операцию `comPortListener`, вызывающую одноименный метод библиотеки:

<!-- Начало кода -->
```scala
@Oper(
  visible = false,
  visibleOnToolbar = Visibilities.Invisible,
  visibleOnMainMenu = Visibilities.Invisible,
  visibleOnNavBar = Visibilities.Invisible
)
def comPortListener(): AnyRef =
  Bts_ComPortLib().comPortListener()
```
<!-- Конец кода -->

Операция размещается в выборке, непосредственно работающей с COM-портами, или в одном из ее предков.

<!-- Начало кода -->
```{note}
Операция должна называться `comPortListener`.
```
<!-- Конец кода -->

Для одновременной работы с COM-портами из нескольких интерфейсов операцию `comPortListener` можно разместить в их общей родительской выборке, например в выборке приложения.

### Обработка считанного значения

Создайте операцию, которая принимает сформированное строковое значение и выполняет требуемое действие. Имя этой операции передается при регистрации обработчика COM-порта.

Если операция должна обрабатывать только один вид кода, можно сразу использовать соответствующий метод:

- для штрихкода — `Bs_BarCodeLib().openCardByBarcode()`;
- для QR-кода — `Bs_QrCodeLib().processQrCode()`.

Если операция должна обрабатывать и штрихкоды, и QR-коды, используйте порядок, описанный в разделе [Обработка штрихкодов и QR-кодов одной операцией](#обработка-штрихкодов-и-qr-кодов-одной-операцией).

### Регистрация обработчиков COM-портов

После создания операции обработки зарегистрируйте ее для всех доступных COM-портов или для выбранного порта.

#### Все доступные порты

Метод `regComOper()` принимает имя операции контекстной обработки и регистрирует ее как обработчик для доступных COM-портов.

Вызов метода размещается в `beforeOpen()` выборки:

<!-- Начало кода -->
```scala
override def beforeOpen(): Unit = {
  super.beforeOpen()
  Bts_ComPortLib().regComOper("onComPortMess")
}
```
<!-- Конец кода -->

Для отмены регистрации используется `unRegComOper()`:

<!-- Начало кода -->
```scala
override def beforeClose(): Unit = {
  super.beforeClose()
  Bts_ComPortLib().unRegComOper()
}
```
<!-- Конец кода -->

Метод `unRegComOper()` не имеет обязательных параметров. Необязательный параметр `bpForce` используется для принудительного закрытия портов и очистки сведений об их использовании.

#### Выбранный порт

Метод `openComPortByName()` находит указанный COM-порт и регистрирует для него контекстный обработчик.

Параметры:

- `portName` — имя порта, например `COM1` или `COM3`; регистр символов не учитывается;
- `operName` — имя операции контекстной обработки.

Пример:

<!-- Начало кода -->
```scala
override def beforeOpen(): Unit = {
  super.beforeOpen()
  Bts_ComPortLib().openComPortByName("COM3", "onComPortMess")
}
```
<!-- Конец кода -->

Для закрытия выбранного порта используется `closeComPortByName()`:

<!-- Начало кода -->
```scala
override def beforeClose(): Unit = {
  super.beforeClose()
  Bts_ComPortLib().closeComPortByName("COM3")
}
```
<!-- Конец кода -->

### Сквозной пример для одного COM-порта

Ниже показан минимальный сценарий для `COM3`: прием данных, обработка штрихкода и QR-кода и закрытие порта.

<!-- Начало кода -->
```scala
@Oper(
  visible = false,
  visibleOnToolbar = Visibilities.Invisible,
  visibleOnMainMenu = Visibilities.Invisible,
  visibleOnNavBar = Visibilities.Invisible
)
def comPortListener(): AnyRef =
  Bts_ComPortLib().comPortListener()

@Oper(
  visible = false,
  visibleOnToolbar = Visibilities.Invisible,
  visibleOnMainMenu = Visibilities.Invisible,
  visibleOnNavBar = Visibilities.Invisible
)
def onComPortMess(spComPortData: String): Unit = {
  Bs_BarCodeApi().findBarCode(spComPortData) match {
    case Some(_) =>
      Bs_BarCodeLib().openCardByBarcode(spComPortData)

    case None =>
      Bs_QrCodeLib().processQrCode(spComPortData)
  }
}

override def beforeOpen(): Unit = {
  super.beforeOpen()
  Bts_ComPortLib().openComPortByName("COM3", "onComPortMess")
}

override def beforeClose(): Unit = {
  super.beforeClose()
  Bts_ComPortLib().closeComPortByName("COM3")
}
```
<!-- Конец кода -->

В этом варианте штрихкод имеет приоритет: если `findBarCode()` возвращает непустой результат, открывается связанный со штрихкодом объект. Если штрихкод не найден, исходное значение обрабатывается как QR-код.

### Обработка и передача полученных данных

`Bts_ComPortLib` накапливает данные, поступающие от COM-порта, до получения символа `CR`. Этот символ используется как признак окончания считанного значения.

Условно полученные данные можно представить так:

<!-- Начало кода -->
```text
4601234567890<CR>
```
<!-- Конец кода -->

где `<CR>` обозначает управляющий символ `CR`. В зарегистрированную операцию будет передано только значение:

<!-- Начало кода -->
```text
4601234567890
```
<!-- Конец кода -->

Сам символ `CR` в передаваемое значение не включается.

Если сканер передает последовательность `CRLF`, символ `LF` также удаляется и в прикладную операцию не передается.

Данные могут поступать частями. Если `CR` еще не получен, незавершенное значение сохраняется до поступления следующих данных. После получения `CR` сформированное значение передается в обработку.

Если в одном фрагменте поступило несколько значений, разделенных `CR`, каждое из них обрабатывается отдельно.

Передача незавершенного значения по тайм-ауту не предусмотрена.

После формирования завершенного значения библиотека перебирает выборки, зарегистрировавшие контекстные обработчики. Данные передаются обработчику активной выборки.

## Обработка штрихкодов и QR-кодов одной операцией

На этапе приема данных из COM-порта система получает считанное значение как строку и не определяет заранее, является оно штрихкодом или QR-кодом.

Для обработки считанного значения используется зарегистрированная для COM-порта операция-обработчик, например `onComPortMess`. Если эта операция должна поддерживать оба вида кодов, в ней сначала выполняется поиск зарегистрированного штрихкода.

Если штрихкод найден, открывается связанный с ним объект. Если штрихкод не найден, исходное считанное значение передается в обработку как QR-код.

Пример операции-обработчика:

<!-- Начало кода -->
```scala
def onComPortMess(spComPortData: String): Unit = {
  Bs_BarCodeApi().findBarCode(spComPortData) match {
    case Some(_) =>
      Bs_BarCodeLib().openCardByBarcode(spComPortData)

    case None =>
      Bs_QrCodeLib().processQrCode(spComPortData)
  }
}
```
<!-- Конец кода -->

Метод `findBarCode()` возвращает `Option[NGid]`:

- `Some(...)` — штрихкод найден;
- `None` — штрихкод с таким значением не найден.

Таким образом, операция `onComPortMess` определяет дальнейший способ обработки считанной строки:

- найденный штрихкод передается в `openCardByBarcode()`;
- если штрихкод не найден, исходная строка передается в `processQrCode()` для обработки как QR-кода.

При таком порядке штрихкод имеет приоритет.