Предыстория: выбор железа
С введением обязательной маркировки товаров на складе возникла реальная потребность в терминалах сбора данных — сканировать коды маркировки вручную через обычный сканер при приёмке и отгрузке стало неудобно и медленно. Встал вопрос выбора устройств и интеграции с мобильным приложением 1С.
При выборе ТСД выбор пал на Zebra TC27. До Android 13 никаких проблем с интеграцией в 1С — всё заводилось без лишних усилий, сканеры у Zebra топовые, достаточно оперативной памяти, хороший экран и отличная камера. Камера для нас принципиальна: планируем использовать её для фиксации товара на входном контроле и при выпуске продукции со склада.
В качестве резервных устройств взяли Meferi с аналогичным набором характеристик — камера там заметно уступает Zebra, но как резервные ТСД они закрывают потребность.
Всё было хорошо, пока Zebra не пришли с Android 14.
Компонента и сборка приложения
Для интеграции сканера с мобильным приложением 1С используется официальная внешняя компонента — Драйвер1СУстройстваВводаNative, поставляемая в составе Библиотеки Подключаемого Оборудования (БПО или БПОМП). Она регистрирует системный BroadcastReceiver, слушает broadcast-интенты от сканирующего приложения и передаёт данные в 1С через механизм внешних событий.
Мобильное приложение собирается через Сборщик мобильных приложений 1С. Компонента добавляется в конфигурацию как общий макет (Драйвер1СУстройстваВводаNative) с типом «Внешняя компонента» и при сборке APK включается в пакет приложения. Важно: тип макета должен быть именно «Внешняя компонента», а не «Двоичные данные» — иначе при вызове УстановитьВнешнююКомпоненту() платформа выдаст ошибку типа макета.
Перед сборкой необходимо настроить свойства конфигурации в конфигураторе:
- Версия (раздел «Разработка») — указать версию приложения, например
1.1.1. Без заполненной версии Сборщик не соберёт APK. - Назначения использования — поставить галку «Приложение для мобильной платформы». Без этого конфигурация не будет распознана как мобильное приложение.
- Используемая функциональность — включить «Воспроизведение аудио и вибрация» (раздел «Мультимедиа»). Требуется для звуковой индикации при сканировании.
Версия компоненты из состава БПО/БПОМП должна соответствовать минимальной поддерживаемой версии мобильной платформы — при несовпадении Подключить() вернёт Ложь. Версия совпадает, компонента подключается, Подключить() возвращает Да — но на Zebra TC27 с Android 14 штрихкод в приложение всё равно не приходит. О причине — ниже.
Альтернативный вариант развёртывания — публикация мобильного приложения на веб-сервере без сборки APK. В этом случае пользователи подключаются через стандартный мобильный клиент 1С. При таком подходе необходимо настроить IIS: добавить MIME-типы для расширений .so, .apk, .dylib с типом application/octet-stream — иначе компонента не загрузится. Подробнее о публикации мобильных приложений читайте в документации 1С.
Реализация хранения настроек и подключения
Как именно хранить настройки сканера и организовать подключение компоненты — полностью на вашей совести. Можно использовать константы, регистры сведений, готовые рабочие места из БПО и так далее.
Наша база написана без БСП, БПО и БПОМП, поэтому готовых рабочих мест и механизмов оборудования в ней нет. Мы пошли простым путём — свой регистр сведений и минимальный код. Ниже показан именно этот вариант как пример реализации.
Для корректного подключения компоненты ей необходимо передать набор параметров — режим работы, action, extra, имя события и ряд других. Чтобы не прописывать их жёстко в коде и иметь возможность менять без пересборки приложения, параметры нужно где-то хранить. Сама компонента умеет отдавать список своих параметров с описанием и допустимыми значениями — оттуда и начнём.
Форма НастройкиСканера
Для удобного управления параметрами создаём общую форму НастройкиСканера. На форме создаём три команды:
- ОпределитьПараметрыСканера — кнопка «Получить сохранённые настройки». Подключает компоненту, вызывает
ПолучитьПараметры(), получает XML с описанием всех параметров и динамически строит поля формы. В первый раз поля заполняются значениями по умолчанию из XML — при последующих запусках подставляются уже сохранённые значения из регистра сведений. - Сохранить — записывает все параметры из полей формы в регистр сведений. При следующем подключении сканера они передаются в компоненту автоматически.
- КнопкаЗакрыть — закрывает форму.
Также в конфигураторе необходимо привязать событие формы ПриОткрытии к процедуре ПриОткрытии — это делается в свойствах формы на вкладке «События». Если хотите чтобы параметры подтягивались автоматически при открытии формы, вызовите ОпределитьПараметрыСканера из обработчика ПриОткрытии.
// Общая форма: НастройкиСканера
// Форма динамически строит интерфейс на основе XML-описания параметров компоненты.
// Все динамические реквизиты создаются с префиксом Скн_ — это позволяет
// легко их находить, обновлять и удалять при перестройке формы.
#Область ОбработчикиСобытийФормы
&НаКлиенте
Процедура ПриОткрытии(Отказ)
// Здесь можно загрузить дополнительные настройки вашей конфигурации,
// например параметры подключения к серверу 1С
КонецПроцедуры
#КонецОбласти
#Область ОбработчикиКомандФормы
&НаКлиенте
Процедура Сохранить(Команда)
// Сохраняем все параметры сканера в регистр сведений
СохранитьПараметрыСканераНаСервере();
ПоказатьПредупреждение(, "Настройки сохранены.");
КонецПроцедуры
&НаКлиенте
Процедура КнопкаЗакрыть(Команда)
Закрыть();
КонецПроцедуры
// Кнопка "Получить сохранённые настройки":
// — в первый раз подключает компоненту, получает XML с параметрами
// и строит форму со значениями по умолчанию из XML
// — при последующих нажатиях подставляет уже сохранённые значения из регистра
&НаКлиенте
Процедура ОпределитьПараметрыСканера(Команда)
УстановитьВнешнююКомпоненту("ОбщийМакет.Драйвер1СУстройстваВводаNative");
Если ПодключитьВнешнююКомпоненту(
"ОбщийМакет.Драйвер1СУстройстваВводаNative",
"AddIn",
ТипВнешнейКомпоненты.Native) Тогда
InputDevice = Новый("AddIn.AddIn.InputDevice");
// Получаем XML с описанием всех параметров компоненты
ОписаниеИнтерфейса = "";
InputDevice.ПолучитьПараметры(ОписаниеИнтерфейса);
// Загружаем ранее сохранённые значения из регистра сведений
// (при первом запуске регистр пуст — будут использованы значения по умолчанию)
СохраненныеЗначения = ПолучитьСохраненныеПараметрыНаСервере();
// Строим интерфейс на сервере — динамически создаём реквизиты и поля формы
ПостроитьИнтерфейсСканераНаСервере(ОписаниеИнтерфейса, СохраненныеЗначения);
Иначе
Сообщить("Не удалось подключить компоненту сканера.");
КонецЕсли;
КонецПроцедуры
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
&НаСервере
Функция ПолучитьСохраненныеПараметрыНаСервере()
// Возвращает соответствие ИмяПараметра -> Значение из регистра сведений
Возврат РаботаСДрайверомОборудования.ПолучитьПараметрыСканера();
КонецФункции
&НаСервере
Процедура СохранитьПараметрыСканераНаСервере()
// Перебираем все реквизиты формы с префиксом Скн_ —
// это динамически созданные параметры сканера.
// Вырезаем префикс (4 символа) чтобы получить оригинальное имя параметра
// и сохраняем значение в регистр сведений.
РеквизитыФормы = ПолучитьРеквизиты();
Для Каждого Реквизит Из РеквизитыФормы Цикл
Если Лев(Реквизит.Имя, 4) = "Скн_" Тогда
ИмяПараметра = Прав(Реквизит.Имя, СтрДлина(Реквизит.Имя) - 4);
РаботаСДрайверомОборудования.СохранитьПараметрСканера(
ИмяПараметра,
Строка(ЭтотОбъект[Реквизит.Имя]));
КонецЕсли;
КонецЦикла;
КонецПроцедуры
&НаСервере
Процедура ПостроитьИнтерфейсСканераНаСервере(ОписаниеИнтерфейса, СохраненныеЗначения)
// Шаг 1: парсим XML и получаем массив структур с описанием параметров
МассивПараметров = РазобратьXMLПараметров(ОписаниеИнтерфейса);
// Шаг 2: удаляем старые динамические реквизиты и элементы (если форма уже строилась)
// Сначала удаляем элементы формы (визуальные поля), потом реквизиты —
// важно делать именно в таком порядке, иначе платформа выбросит ошибку.
// Реквизиты удаляем одним вызовом ИзменитьРеквизиты после цикла,
// потому что нельзя вызывать его внутри цикла по ПолучитьРеквизиты.
УдаляемыеРеквизиты = Новый Массив;
Для Каждого Рекв Из ПолучитьРеквизиты() Цикл
Если Лев(Рекв.Имя, 4) = "Скн_" Тогда
УдаляемыеРеквизиты.Добавить(Рекв.Имя);
Если Элементы.Найти(Рекв.Имя) <> Неопределено Тогда
Элементы.Удалить(Элементы[Рекв.Имя]);
КонецЕсли;
КонецЕсли;
КонецЦикла;
Если УдаляемыеРеквизиты.Количество() > 0 Тогда
ИзменитьРеквизиты(, УдаляемыеРеквизиты);
КонецЕсли;
// Шаг 3: убеждаемся что группа для полей сканера существует
Если Элементы.Найти("ГруппаСканер") = Неопределено Тогда
ГруппаСканер = Элементы.Добавить("ГруппаСканер", Тип("ГруппаФормы"));
ГруппаСканер.Вид = ВидГруппыФормы.ОбычнаяГруппа;
ГруппаСканер.Заголовок = "Сканер";
ГруппаСканер.РастягиватьПоГоризонтали = Истина;
Иначе
ГруппаСканер = Элементы["ГруппаСканер"];
КонецЕсли;
// Шаг 4: добавляем все реквизиты одним вызовом ИзменитьРеквизиты —
// это быстрее и безопаснее чем добавлять по одному
ДобавляемыеРеквизиты = Новый Массив;
Для Каждого Параметр Из МассивПараметров Цикл
Если Параметр.Тип = "NUMBER" Тогда
Рекв = Новый РеквизитФормы(Параметр.ИмяРеквизита, Новый ОписаниеТипов("Число"), , Параметр.Заголовок, Истина);
ИначеЕсли Параметр.Тип = "BOOLEAN" Тогда
Рекв = Новый РеквизитФормы(Параметр.ИмяРеквизита, Новый ОписаниеТипов("Булево"), , Параметр.Заголовок, Истина);
Иначе
Рекв = Новый РеквизитФормы(Параметр.ИмяРеквизита, Новый ОписаниеТипов("Строка"), , Параметр.Заголовок, Истина);
КонецЕсли;
ДобавляемыеРеквизиты.Добавить(Рекв);
КонецЦикла;
Если ДобавляемыеРеквизиты.Количество() > 0 Тогда
ИзменитьРеквизиты(ДобавляемыеРеквизиты);
КонецЕсли;
// Шаг 5: создаём элементы формы и заполняем значениями
БазоваяГруппа = Неопределено;
Для Каждого Параметр Из МассивПараметров Цикл
// Создаём контейнер для полей при первой итерации
Если БазоваяГруппа = Неопределено Тогда
БазоваяГруппа = Элементы.Добавить("БазоваяГруппа0", Тип("ГруппаФормы"), ГруппаСканер);
БазоваяГруппа.Вид = ВидГруппыФормы.ОбычнаяГруппа;
БазоваяГруппа.РастягиватьПоГоризонтали = Истина;
БазоваяГруппа.Группировка = ГруппировкаПодчиненныхЭлементовФормы.Вертикальная;
КонецЕсли;
НовЭлемент = Элементы.Добавить(Параметр.ИмяРеквизита, Тип("ПолеФормы"), БазоваяГруппа);
// Для булевых параметров — флажок, для остальных — поле ввода
Если Параметр.Тип = "BOOLEAN" Тогда
НовЭлемент.Вид = ВидПоляФормы.ПолеФлажка;
Иначе
НовЭлемент.Вид = ВидПоляФормы.ПолеВвода;
НовЭлемент.АвтоМаксимальнаяШирина = Ложь;
НовЭлемент.РастягиватьПоГоризонтали = Истина;
НовЭлемент.Формат = Параметр.СтрокаФорматирования;
НовЭлемент.ФорматРедактирования = Параметр.СтрокаФорматирования;
// Если у параметра есть список допустимых значений — включаем выбор из списка
Если Параметр.СписокВыбора.Количество() > 0 Тогда
НовЭлемент.РежимВыбораИзСписка = Истина;
НовЭлемент.ВысотаСпискаВыбора = 10;
НовЭлемент.РедактированиеТекста = Ложь;
Для Каждого ЭлСписка Из Параметр.СписокВыбора Цикл
НовЭлемент.СписокВыбора.Добавить(ЭлСписка.Значение, ЭлСписка.Представление);
КонецЦикла;
КонецЕсли;
КонецЕсли;
НовЭлемент.ПутьКДанным = Параметр.ИмяРеквизита;
НовЭлемент.Подсказка = Параметр.Описание;
НовЭлемент.ТолькоПросмотр = Ложь;
// Устанавливаем значение: из регистра если уже сохранено, иначе по умолчанию из XML
ЗначениеДляУстановки = Параметр.ЗначениеПоУмолчанию;
Если СохраненныеЗначения.Получить(Параметр.ОригинальноеИмя) <> Неопределено Тогда
ЗначениеДляУстановки = СохраненныеЗначения[Параметр.ОригинальноеИмя];
КонецЕсли;
Если НЕ ПустаяСтрока(ЗначениеДляУстановки) Тогда
Если Параметр.Тип = "BOOLEAN" Тогда
ЭтотОбъект[Параметр.ИмяРеквизита] =
ВРег(ЗначениеДляУстановки) = "TRUE"
Или ВРег(ЗначениеДляУстановки) = "ИСТИНА";
ИначеЕсли Параметр.Тип = "NUMBER" Тогда
ЭтотОбъект[Параметр.ИмяРеквизита] = Число(ЗначениеДляУстановки);
Иначе
ЭтотОбъект[Параметр.ИмяРеквизита] = ЗначениеДляУстановки;
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
&НаСервере
Функция РазобратьXMLПараметров(ОписаниеИнтерфейса)
// Парсим XML который возвращает компонента через ПолучитьПараметры().
// Структура XML: корневой элемент Settings, внутри элементы Parameter,
// у каждого может быть вложенный ChoiceList со списком допустимых значений.
// Возвращаем массив структур — по одной на каждый редактируемый параметр.
МассивПараметров = Новый Массив;
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.УстановитьСтроку(ОписаниеИнтерфейса);
ЧтениеXML.ПерейтиКСодержимому();
ТекущийПараметр = Неопределено;
Если ЧтениеXML.Имя = "Settings"
И ЧтениеXML.ТипУзла = ТипУзлаXML.НачалоЭлемента Тогда
Пока ЧтениеXML.Прочитать() Цикл
Если ЧтениеXML.Имя = "Parameter"
И ЧтениеXML.ТипУзла = ТипУзлаXML.НачалоЭлемента Тогда
// Параметры только для чтения пропускаем — они нам не нужны.
// Сбрасываем ТекущийПараметр чтобы ChoiceList readonly параметра
// случайно не добавился к предыдущему редактируемому параметру.
Если ВРег(ЧтениеXML.ЗначениеАтрибута("ReadOnly")) = "TRUE" Тогда
ТекущийПараметр = Неопределено;
Продолжить;
КонецЕсли;
ОригинальноеИмя = ЧтениеXML.ЗначениеАтрибута("Name");
// Префикс Скн_ — соглашение об именовании динамических реквизитов сканера.
// По нему легко найти, обновить или удалить все поля формы сканера
// не затрагивая статические реквизиты конфигурации.
ИмяРеквизита = "Скн_" + ОригинальноеИмя;
ТекущийПараметр = Новый Структура;
ТекущийПараметр.Вставить("ОригинальноеИмя", ОригинальноеИмя);
ТекущийПараметр.Вставить("ИмяРеквизита", ИмяРеквизита);
ТекущийПараметр.Вставить("Заголовок", ЧтениеXML.ЗначениеАтрибута("Caption"));
ТекущийПараметр.Вставить("Тип", ВРег(?(НЕ ПустаяСтрока(ЧтениеXML.ЗначениеАтрибута("TypeValue")), ЧтениеXML.ЗначениеАтрибута("TypeValue"), "STRING")));
ТекущийПараметр.Вставить("ЗначениеПоУмолчанию", ЧтениеXML.ЗначениеАтрибута("DefaultValue"));
ТекущийПараметр.Вставить("Описание", ЧтениеXML.ЗначениеАтрибута("Description"));
ТекущийПараметр.Вставить("СтрокаФорматирования", ЧтениеXML.ЗначениеАтрибута("FieldFormat"));
ТекущийПараметр.Вставить("СписокВыбора", Новый СписокЗначений);
МассивПараметров.Добавить(ТекущийПараметр);
ИначеЕсли ЧтениеXML.Имя = "ChoiceList"
И ЧтениеXML.ТипУзла = ТипУзлаXML.НачалоЭлемента
И ТекущийПараметр <> Неопределено Тогда
// Читаем список допустимых значений параметра
Пока ЧтениеXML.Прочитать() Цикл
Если ЧтениеXML.Имя = "ChoiceList"
И ЧтениеXML.ТипУзла = ТипУзлаXML.КонецЭлемента Тогда
Прервать;
КонецЕсли;
Если ЧтениеXML.Имя = "Item"
И ЧтениеXML.ТипУзла = ТипУзлаXML.НачалоЭлемента Тогда
ЗначениеАтрибута = ЧтениеXML.ЗначениеАтрибута("Value");
Если ЧтениеXML.Прочитать() Тогда
ПредставлениеАтрибута = ЧтениеXML.Значение;
КонецЕсли;
// Если Value пустой — используем текст элемента как значение
Если ПустаяСтрока(ЗначениеАтрибута) Тогда
ЗначениеАтрибута = ПредставлениеАтрибута;
КонецЕсли;
ТекущийПараметр.СписокВыбора.Добавить(ЗначениеАтрибута, ПредставлениеАтрибута);
КонецЕсли;
КонецЦикла;
КонецЕсли;
КонецЦикла;
КонецЕсли;
ЧтениеXML.Закрыть();
Возврат МассивПараметров;
КонецФункции
#КонецОбласти
Сохранение параметров
После того как параметры получены и скорректированы под устройство, сохраняем их в регистр сведений. Для этого в серверном общем модуле РаботаСДрайверомОборудования создаём процедуру сохранения. Общий модуль должен быть серверным и не глобальным — это важно, так как он вызывается с сервера из формы настроек.
// Серверный общий модуль РаботаСДрайверомОборудования
Процедура СохранитьПараметрСканера(Параметр, Значение) Экспорт
НаборЗаписей = РегистрыСведений.НастройкиСканера.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Параметр.Установить(Параметр);
Запись = НаборЗаписей.Добавить();
Запись.Параметр = Параметр;
Запись.Значение = Значение;
НаборЗаписей.Записать();
КонецПроцедуры
Получение параметров из регистра сведений
Для хранения параметров создаём в конфигурации регистр сведений НастройкиСканера. Состав реквизитов:
Измерения:
- Параметр — Строка (100). Имя параметра драйвера.
Ресурсы:
- Значение — Строка (256). Значение параметра.
Там же, в модуле РаботаСДрайверомОборудования, размещаем функцию получения параметров из регистра — она используется при подключении сканера в форме документа:
Функция ПолучитьПараметрыСканера() Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| НастройкиСканера.Параметр,
| НастройкиСканера.Значение
|ИЗ
| РегистрСведений.НастройкиСканера КАК НастройкиСканера";
Результат = Новый Соответствие;
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Результат.Вставить(Выборка.Параметр, Выборка.Значение);
КонецЦикла;
Возврат Результат;
КонецФункции
Код модуля формы
Настройки сохранены — теперь создаём форму документа для работы со сканером. В примере это простая форма с реквизитами Дата и Номер, которая при сканировании выводит код маркировки в сообщение. Весь остальной функционал — поиск номенклатуры, запись в документ, проверки — реализуете самостоятельно под свою задачу.
Параметры читаются из регистра сведений и передаются в компоненту через цикл — никакой жёсткой прошивки значений в коде нет.
В конфигураторе в свойствах формы документа необходимо привязать следующие события и обработчики:
- ПриОткрытии → процедура
ПриОткрытии— подключает сканер при открытии формы. - ПриЗакрытии → процедура
ПриЗакрытии— отключает сканер при закрытии формы. - ВнешнееСобытие → процедура
ВнешнееСобытие— стандартное событие мобильной платформы, через которое компонента передаёт данные сканера в форму. Привязывается там же на вкладке «События» формы.
// Объявить в самом начале модуля формы до всех процедур
&НаКлиенте
Перем Компонента, КомпонентаПодключена;
&НаКлиенте
Процедура ПриОткрытии(Отказ)
ПодключитьСканер();
КонецПроцедуры
&НаКлиенте
Процедура ПриЗакрытии(СтандартнаяОбработка)
ОтключитьСканер();
КонецПроцедуры
// Получает данные от сканера
&НаКлиенте
Процедура ВнешнееСобытие(Источник, Событие, Данные)
Если НЕ КомпонентаПодключена Тогда
Возврат;
КонецЕсли;
Если СтрНайти(Источник, "InputDevice") = 0 Тогда
Возврат;
КонецЕсли;
Штрихкод = СокрЛП(Данные);
Если ПустаяСтрока(Штрихкод) Тогда
Возврат;
КонецЕсли;
Сообщить("Код маркировки: " + Штрихкод);
КонецПроцедуры
// Читает параметры из регистра и подключает компоненту
&НаКлиенте
Процедура ПодключитьСканер()
ПараметрыСканера = ПолучитьПараметрыСканераНаСервере();
УстановитьВнешнююКомпоненту("ОбщийМакет.Драйвер1СУстройстваВводаNative");
Если ПодключитьВнешнююКомпоненту(
"ОбщийМакет.Драйвер1СУстройстваВводаNative",
"AddIn",
ТипВнешнейКомпоненты.Native) Тогда
Компонента = Новый("AddIn.AddIn.InputDevice");
Для Каждого КлючЗначение Из ПараметрыСканера Цикл
Компонента.УстановитьПараметр(КлючЗначение.Ключ, КлючЗначение.Значение);
КонецЦикла;
Компонента.Подключить("");
КомпонентаПодключена = Истина;
Иначе
КомпонентаПодключена = Ложь;
Сообщить("Не удалось подключить компоненту сканера.");
КонецЕсли;
КонецПроцедуры
&НаКлиенте
Процедура ОтключитьСканер()
Если Компонента <> Неопределено Тогда
Компонента.Отключить("");
Компонента = Неопределено;
КонецЕсли;
КомпонентаПодключена = Ложь;
КонецПроцедуры
&НаСервере
Функция ПолучитьПараметрыСканераНаСервере()
Возврат РаботаСДрайверомОборудования.ПолучитьПараметрыСканера();
КонецФункции
Звуковая индикация (только в собранном APK)
Мы добавили звуковую индикацию в своё приложение — воспроизводим звук успеха когда штрихкод обработан успешно, и звук ошибки когда номенклатура не найдена или что-то пошло не так. Это удобно на складе: сотруднику не нужно смотреть на экран после каждого сканирования. Делимся на случай если кому-то пригодится.
Аудиофайлы берёте любые подходящие с интернета. При подключении столкнулись с неочевидной проблемой: Сборщик мобильных приложений молча не включал звуковые файлы в сборку. Оказалось, причина в структуре ZIP-архива — он требует папки для всех трёх платформ, даже пустые. Как только создали папки iOS/ и Windows/ — файлы начали включаться в сборку. Структура архива должна быть такой:
Android/
beep_success.mp3
beep_error.mp3
iOS/
Windows/
Папки iOS/ и Windows/ пустые — они нужны только чтобы Сборщик корректно обработал архив. Готовый ZIP добавьте в Сборщик в раздел «Аудиоресурсы». Также в свойствах конфигурации в разделе «Используемая функциональность» необходимо включить «Воспроизведение аудио и вибрация» — без этого звук работать не будет.
&НаКлиенте
Процедура ЗвукУспеха()
#Если МобильноеПриложениеКлиент Тогда
СредстваМультимедиа.ВоспроизвестиЗвуковоеОповещение("beep_success", Ложь);
#КонецЕсли
КонецПроцедуры
&НаКлиенте
Процедура ЗвукОшибки()
#Если МобильноеПриложениеКлиент Тогда
СредстваМультимедиа.ВоспроизвестиЗвуковоеОповещение("beep_error", Истина);
#КонецЕсли
КонецПроцедуры
Второй параметр Истина включает вибрацию. Для ошибки — с вибрацией, для успеха — без.
Подсистема и команды
Чтобы наши формы были доступны в интерфейсе приложения, необходимо создать подсистему и общие команды для их открытия — без этого формы просто не появятся в меню.
Подсистема ОсновнаяПодсистема
В конфигураторе создаём подсистему ОсновнаяПодсистема. В неё включаем обе общие команды — они будут отображаться в навигационном меню мобильного приложения.
Общие команды
Создаём две общие команды и включаем их в подсистему ОсновнаяПодсистема.
ОткрытьНастройкиСканера — открывает форму настройки параметров сканера:
&НаКлиенте
Процедура ОбработкаКоманды(ПараметрКоманды, ПараметрыВыполненияКоманды)
ОткрытьФорму("ОбщаяФорма.НастройкиСканера");
КонецПроцедуры
ОткрытьФормуТестирования — открывает форму для тестирования сканирования:
&НаКлиенте
Процедура ОбработкаКоманды(ПараметрКоманды, ПараметрыВыполненияКоманды)
ОткрытьФорму("ОбщаяФорма.ФормаТестирования");
КонецПроцедуры
После создания команд в свойствах каждой команды необходимо:
- На вкладке «Подсистемы» поставить галку напротив ОсновнаяПодсистема.
- В поле «Группа» указать группу команды — например
Панель навигации.Важное. Без этого конфигуратор выдаст ошибку «Не указана группа, в которую входит команда по умолчанию» и не даст собрать приложение.
Настройка ТСД и параметры компоненты
Zebra TC27 — настройка DataWedge
DataWedge предустановлен на всех устройствах Zebra. Создаём новый профиль:
- При создании профиля сразу указываем имя и в разделе Associated Apps добавляем пакет нашего приложения — DataWedge будет активировать профиль автоматически при запуске именно этого приложения.
- Все остальные профили DataWedge желательно отключить, чтобы они не конкурировали с нашим.
- Barcode Input — включить.
- Keystroke Output — отключить.
- Intent Output — включить со следующими настройками:
| Параметр DataWedge | Значение |
|---|---|
| Intent action | произвольный (совпадает с параметром Action в регистре 1С) |
| Intent category | оставить пустым |
| Intent delivery | Broadcast intent |
| Component information | не заполнять |
| Receiver foreground flag | 1 |
Параметры для сохранения в регистр сведений 1С. Три выделенных жирным — обязательные, именно они определяют режим работы и маршрут доставки данных. Остальные параметры можно оставить со значениями по умолчанию — всё заработает:
| Параметр | Значение | Примечание |
|---|---|---|
| Устройство | BROADCAST | Обязательно |
| Пользовательский intent action | com.zebra.scan | Обязательно. Произвольный, совпадает с DataWedge |
| Пользовательский intent extra | com.symbol.datawedge.data_string | Стандартный ключ согласно документации DataWedge и данным форумов |
Meferi — настройка meWedge
При создании профиля в meWedge сразу указывается имя профиля и приложение, для которого он используется — это и есть привязка к нашему приложению. Отдельного шага добавления не требуется.
Настройки широковещательного вывода:
| Параметр meWedge | Значение |
|---|---|
| Broadcast Action | произвольный action (совпадает с параметром Action в регистре 1С) |
| Barcode label | data — имя Extra, в которое meWedge кладёт данные штрихкода |
Параметры для регистра сведений 1С — аналогичны Zebra, отличаются только два:
| Параметр | Значение | Примечание |
|---|---|---|
| Пользовательский intent action | com.meferi.scan | Обязательно. Произвольный, совпадает с meWedge |
| Пользовательский intent extra | data | Обязательно. Barcode label из meWedge |
Собираем, устанавливаем — и начинаются танцы с бубнами
Приложение собрано через Сборщик мобильных приложений 1С, APK установлен на оба устройства. Подключить() возвращает Да на обоих.
Включаем Meferi — сканируем, данные прилетают в форму. Работает.
Включаем Zebra TC27 с Android 14 — сканируем, тишина. Никаких ошибок, Подключить() говорит Да, DataWedge в своих логах подтверждает отправку — а в 1С пусто.
Идём в ADB разбираться
Для диагностики понадобится Android Debug Bridge (ADB) — инструмент из состава Android SDK. Подробности по установке легко найти в интернете. Подключаем Zebra к компьютеру и смотрим logcat во время сканирования:
adb logcat | grep -i datawedge
DataWedge исправно шлёт broadcast при каждом сканировании — в логе видны записи от IntentPlugin с нашим action, адресованные пакету приложения. Данные просто не доходят до получателя.
Делаем дамп очереди broadcast после сканирования:
adb shell dumpsys activity broadcasts | grep -i "denial" > bc.txt
В bc.txt находим:
skipped by policy at enqueue: Exported Denial — receiver not specifying RECEIVER_EXPORTED
Android молча выбрасывает broadcast, не донося до получателя. Ни исключений, ни ошибок — именно поэтому так сложно диагностировать.
Почему на Meferi работает, а на Zebra нет
Начиная с Android 13 (API 33) Google ввёл правило: если обычное приложение отправляет broadcast другому, получатель обязан явно объявить флаг RECEIVER_EXPORTED = 2. Без него Android блокирует доставку тихо, без ошибок.
DataWedge на Zebra — обычное приложение (uid 10054). Правило применяется в полный рост.
Сканерный сервис на Meferi встроен в прошивку и работает как системный процесс (uid 1000). Для системных отправителей это ограничение не действует — вот почему Meferi работает без проблем даже на Android 15.
Лезем в код компоненты. Распаковываем ZIP, внутри APK com_1c_ScanOPOS.apk, внутри него classes.dex. В методе PBroadcastReceaver.open (опечатка в оригинале — именно так) находим:
const/4 v2, 0x4 ; передаётся как флаг в registerReceiver
0x4 — это Context.RECEIVER_NOT_EXPORTED. Компонента сама себе запрещает принимать broadcast от внешних приложений. Это баг в компоненте.
Эта проблема не специфична для Zebra. Любой ТСД на Android 13 и выше, где сканер доставляет данные через broadcast от обычного приложения (не системного процесса), столкнётся с тем же.
Решение: патч одного байта
Меняем 0x4 на 0x2 (RECEIVER_EXPORTED) в classes.dex и пересчитываем контрольные суммы DEX-заголовка (Adler32 + SHA1) — без этого Android откажется загружать файл.
APK компоненты не нужно подписывать: мобильная платформа 1С загружает его как runtime-компоненту из директории ExtCompT, а не как самостоятельное приложение.
Готовый патченный архив Драйвер1СУстройстваВводаNative_patched.zip приложен к статье. Заменить им макет в конфигурации, пересобрать APK через Сборщик, переустановить на устройство — рекомендуется полное удаление, платформа кэширует компоненту.
После замены макета, пересборки и переустановки приложения DataWedge начинает исправно доставлять данные в 1С — штрихкод приходит в форму при каждом сканировании. Проблема решена.
Как пропатчить самостоятельно
Потребуются Python 3 и утилиты baksmali/smali — информация об установке легко находится в интернете, проект доступен на github.com/JesusFreke/smali.
# 1. Распаковать компоненту
unzip Драйвер1СУстройстваВводаNative.zip com_1c_ScanOPOS.apk
unzip com_1c_ScanOPOS.apk classes.dex
# 2. Дизассемблировать
java -jar baksmali.jar disassemble classes.dex -o smali_out/
В папке smali_out найти файл PBroadcastReceaver.smali, найти вызов registerReceiver с 4 аргументами и рядом стоящую инструкцию const/4 vX, 0x4 — изменить на const/4 vX, 0x2.
# 3. Собрать обратно (контрольные суммы пересчитаются автоматически)
java -jar smali.jar assemble smali_out/ -o classes_patched.dex
# 4. Обновить APK и ZIP компоненты
cp com_1c_ScanOPOS.apk com_1c_ScanOPOS_patched.apk
zip com_1c_ScanOPOS_patched.apk classes_patched.dex
# Заменить com_1c_ScanOPOS.apk в ZIP компоненты на patched версию
Состав вложений
К статье приложены два файла.
Драйвер1СУстройстваВводаNative — патченная компонента. Добавьте её в конфигурацию как общий макет с типом «Внешняя компонента», пересоберите APK через Сборщик мобильных приложений и переустановите на устройство. Рекомендуется полное удаление перед установкой — платформа кэширует компоненту.
Инфостарт.zip — архив с двумя файлами:
- Конфигурация — пример реализации из статьи. Набросок приложения для сканирования кодов маркировки: общая форма настроек сканера, форма тестирования со сканированием и выводом кода в сообщение. Полноценный функционал обработки кодов маркировки намеренно не реализован — дорабатывайте под свои задачи самостоятельно.
- Архив с APK — собранное приложение для быстрой проверки на устройстве. Установите на ТСД, настройте DataWedge или meWedge согласно разделу про настройку ТСД и убедитесь что сканирование работает до того как начнёте встраивать код в свою конфигурацию.
Чеклист: если всё равно не работает
| Симптом | Причина и решение |
|---|---|
Подключить() возвращает Ложь |
Версия компоненты не совпадает с минимальной поддерживаемой версией платформы. |
Да, но событие не приходит |
Запустить adb shell dumpsys activity broadcasts, найти Exported Denial — нужен патч компоненты. |
Exported Denial нет, событие не приходит |
Проверить посимвольное совпадение Action в регистре 1С и в DataWedge/meWedge. |
| Работает при открытии, потом перестаёт | Перем Компонента объявлен внутри процедуры, а не на уровне модуля формы. |
УстановитьВнешнююКомпоненту падает с HTTP 404 |
В IIS не добавлен MIME-тип: расширение .so, тип application/octet-stream. |
| DataWedge не активируется при запуске приложения | Проверить Associated Apps в профиле DataWedge — пакет приложения должен быть добавлен. Остальные профили DataWedge отключить. |
Итог
Корень проблемы — один байт в DEX-байткоде компоненты Драйвер1СУстройстваВводаNative. Она регистрировала BroadcastReceiver с флагом RECEIVER_NOT_EXPORTED, что на Android 13+ блокирует приём broadcast от любого обычного приложения-сканера. Meferi работала без патча исключительно потому, что её сканерный сервис системный.
Если ваш ТСД работает на Android 13 или выше и сканер доставляет данные не через системный процесс — патч обязателен.
Патченный ZIP прилагается к статье.
Вступайте в нашу телеграмм-группу Инфостарт