Пишем внешнюю компоненту для 1С, NativeAPI на С++, для ОС Linux, с использованием Qt Creator

от автора

Аннотация

В статье рассмотрен процесс создания внешней компоненты для 1С в среде Qt Creator для операционной системы Linux (ubuntu, debian, mint и им подобных). На примере компоненты для сбора данных от внешней аппаратуры и сохранение их в базе, посредством 1С. В качестве внешней аппаратуры в данном примере будем использовать Arduino UNO.

Для создания внешней компоненты понадобятся

  1. Материал «Технология создания внешних компонент», на странице 1С:ИТС

  2. Шаблон пустой внешней компоненты.

  3. Qt Creator. Я использовал версию 6.0.2.

 Шаблон компоненты

В операционной системе Linux 1С поддерживает внешние компоненты созданные при помощи технологии Native API, технологии COM поддерживаются только в ОС Windows потому, что создавались специально под эту операционную систему ввиду её популярности в своё время. Поэтому мы будем пользоваться заранее написанном на С++ и реализованном в рамках технологии Native API шаблоне.

С описанием методов которые должны быть реализованы во внешней компоненте можно ознакомится на сайте 1С:ИТС.

За основу я взял кастомный шаблон с ресурса Github

Ссылка на шаблон компоненты https://github.com/Infactum/addin-template

Выражаю благодарность авторам этого шаблона, так как в использовании он более удобный чем оригинал от компании 1С.

Начало пути

Запускаем Qt Creator и создаём новый проект — библиотека С++

Выбираем путь размещения нашей библиотеки и систему сборки qmake. Далее в папку с проектом нашей библиотеки ложим содержимое папки src шаблона внешней компоненты, в результате там будут файлы Component.cpp, Component.h, dllmain.cpp, exports.cpp, stdafx.h и созданные автоматически имя_класса.cpp, имя_класса.h, имя_класса_global.h (у меня это ttyAddin_global.h, ttyAddin.h, ttyAddin.cpp). И в любое удобное место папку include (с файлами AddInDefBase.h, com.h, ComponentBase.h, IandroidComponentHelper.h, ImemoryManager.h, types.h) я её расположил в папке с проектом.

В *.pro файле, у меня это ttyAddin.pro необходимо указать путь к файлам в папке include путём добавления строки.

INCLUDEPATH +="/home/delphin/ProjectQt/estern_lib2/include/"

Так же можно библиотечные файлы из папки include добавить в папку с проектом и прописать в HEADERS и SOURCES соответственно их имена, но этот путь более долог.

 

Далее в файлы ttyAddin.h, ttyAddin.cpp добавляем наш код. Сам класс ttyAddin необходимо проунаследовать от шаблонного класса Component.

 

Первоначально эти файлы будут выглядеть так:

ttyAddin.h

#ifndef TTYADDIN_H #define TTYADDIN_H #include "ttyAddin_global.h" #include "Component.h" // C library headers #include <iostream> class TTYADDIN_EXPORT TtyAddin : public Component { public:     const char *Version = u8"1.0.0";    //присутствует в шаблоне версия нашего класса     explicit TtyAddin();     std::string extensionName() override;   //наименование класса private:     std::shared_ptr<variant_t> sample_property;  }; #endif // TTYADDIN_H

 

ttyAddin.cpp

#include "ttyaddin.h" std::string TtyAddin::extensionName() { // наименование нашего класса которое будет передано в 1с     return "TTY"; // необходимо указать своё. } TtyAddin::TtyAddin() {     // Universal property. Could store any supported by native api type.    //присутствует в шаблоне     sample_property = std::make_shared<variant_t>();     AddProperty(L"SampleProperty", L"ОбразецСвойства", sample_property);     // Full featured property registration example      //присутствует в шаблоне     AddProperty(L"Version", L"ВерсияКомпоненты", & {         auto s = std::string(Version);         return std::make_shared<variant_t>(std::move(s));     });

Так же необходимо внести изменения в файле exports.cpp

После перечисленных действий необходимо собрать проект и произвести компиляцию с целью получения файла динамической библиотеки (*.so) в режиме Debug или Release. В Linux динамические библиотеки имею расширение *.so (в Windows всем привычный*.dll).

ВАЖНО: Чтобы эту библиотеку могли использовать программы необходимо выполнить следующие действия. Добавить нашу директорию с библиотекой в список известных директорий для чего подредактировать файл /etc/ld.so.conf. Например, у меня этот файл состоит из таких строк:

include /etc/ld.so.conf.d/*.conf /home/delphin/.cache/fontconfig/ /home/delphin/.cache/gstreamer-1.0/ 

Во всех этих директориях хранятся всеми используемые библиотеки. В этом списке нет лишь одной директории — /lib, которая сама по себе не нуждается в описании, так как она является главной. Получается, что наша библиотека станет «заметной», если поместить ее в один их этих каталогов, либо отдельно описать в отдельном каталоге.

Необходимо в конец этого файла (ld.so.conf) добавить путь к папке с нашей библиотекой.

include /etc/ld.so.conf.d/*.conf /home/delphin/.cache/fontconfig/ /home/delphin/.cache/gstreamer-1.0/ /home/delph/ProjectQt/estern_lib2/build-ttyAddin-Desktop_Qt_6_3_0_GCC_64bit-Release/ 

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

Интеграция с 1С

Наш дальнейший путь пролегает через 1С. Создаём пустую базу, запускаем в режиме конфигуратора, и в модуле приложения (при запуске), или как я в модуле формы пишем код подключения внешней компоненты и создаём объект компонента.

 

&НаКлиенте

Перем Компонента, ПутьКБиблиотеке;

&НаКлиенте

Процедура Подключить(Команда) //обработчик созданной команды (кнопки)

ПутьКБиблиотеке=»/home/delphin/ProjectQt/estern_lib2/build-ttyAddin-Desktop_Qt_6_3_0_GCC_64bit-Release/libttyAddin.so»;

РезультатПодключения = ПодключитьВнешнююКомпоненту(ПутьКБиблиотеке, «libextDLib», ТипВнешнейКомпоненты.Native);

Сообщить («Компонента подключена — » + РезультатПодключения );

Попытка

Компонента = новый («AddIn.libextDLib.TTY»);

Сообщить («Компонента создана»);

Исключение

Сообщить («неудалось создать компоненту»);

КонецПопытки;

КонецПроцедуры

 

В настройках конфигурации необходимо установить «запуск в режиме Толстого клиента», так как для упрощения кода мы всё написали на клиенте.

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

Реализация необходимых методов. (функций компоненты)

Поскольку мы ставили перед собой задачу прикрутить к 1С последовательный порт для получения данных от различных устройств через интерфейсы rs-232, rs-422, rs-485, то соответственно в нашем классе мы будем реализовывать методы для работы с последовательным портом. В качестве инструмента я выбрал стандартную Си библиотеку termios.h

Почему не QserialPort:

1. Мы пишем пример под Linux (кросплатформенность не критична).

2. termios.h более быстрая и тратит меньше ресурсов.

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

4. Используя termios.h можно реализовать самые экзотические настройки и режимы работы порта (например подключить старый советский принтер Robotron).

Для этого нам необходимо реализовать методы «Подключения порта», «Настройки порта», «Отключения порта», «Передачи данных», «Приёма данных». Подключение и настройку я реализовал одним методом ConnectPort, отключение методом DisconnectPort, чтение и запись методами -ReadPort и SendToPort. Так же для передачи данных в 1С я использовал стандартную функцию ExternalEvent для чего переопределил методы ExternalEvent, CleanEventBuffer, GetEventBufferDepth

 

ttyAddin.h

#ifndef TTYADDIN_H #define TTYADDIN_H #include "ttyAddin_global.h" #include "Component.h" // C library headers #include <iostream> #include <string> // Linux headers #include <fcntl.h>   // Contains file controls like O_RDWR #include <errno.h>   // Error integer and strerror() function #include <termios.h> // Contains POSIX terminal control definitions #include <unistd.h>  // write(), read(), close() class TTYADDIN_EXPORT TtyAddin : public Component { public:     const char *Version = u8"1.0.0";    //присутствует в шаблоне версия нашего класса     explicit TtyAddin();     variant_t ConnectPort (const variant_t &, const variant_t &);// объявление метода инициализации порта и подключение к нему     void DisconnectPort(void);  //объявление метода отключения от порта     void ReadPort ();           //объявление метода чтения буфера порта     void SendToPort (const variant_t &);    //объявление метода отправки строки в порт     std::string extensionName() override;   //наименование класса private:     //переопределяем проунаследованные методы     bool ExternalEvent(const std::string &, const std::string &, const std::string &);     bool SetEventBufferDepth(long);     long GetEventBufferDepth();     int serial_port;     //переменная для хранения дескриптора порта     struct termios tty;  //структура данных настроек порта     char read_buf [256]; //буфер временного хранения принятых данных     //std::string str;     std::string PortNameStr;//хранение пути к порту     std::shared_ptr<variant_t> sample_property; //наименование нашего класса которое будет передано в 1с }; #endif // TTYADDIN_H

 

 

ttyAddin.cpp

#include "ttyaddin.h" std::string TtyAddin::extensionName() { // наименование нашего класса которое будет передано в 1с     return "TTY"; } TtyAddin::TtyAddin() {     // Universal property. Could store any supported by native api type.    //присутствует в шаблоне     sample_property = std::make_shared<variant_t>();     AddProperty(L"SampleProperty", L"ОбразецСвойства", sample_property);     // Full featured property registration example      //присутствует в шаблоне     AddProperty(L"Version", L"ВерсияКомпоненты", [&]() {         auto s = std::string(Version);         return std::make_shared<variant_t>(std::move(s));     });     //Регистрация методов     //Указываются названия методов которые мы будем использовать в 1С и связанные с ними методы класса     AddMethod(L"Сonnect", L"ПодключитьПорт", this, &TtyAddin::ConnectPort);     AddMethod(L"Disconnect", L"ОтключитьПорт", this, &TtyAddin::DisconnectPort);     AddMethod(L"Read", L"ЧитатьПорт", this, &TtyAddin::ReadPort);     AddMethod(L"Send", L"ОтправитьВПорт", this, &TtyAddin::SendToPort); } // определение метода инициализации порта и подключение к нему // принимает строку пути к файлу порта и скорость порта (число) // в случае успеха возвращает истину variant_t TtyAddin::ConnectPort (const variant_t & serialPortName, const variant_t & Baud) {   variant_t res = false;     //проверка операндов на соответствие типов     if (std::holds_alternative<std::string>(serialPortName) && (std::holds_alternative<int32_t>(Baud)))     {  res = true;        PortNameStr = std::get<std::string>(serialPortName); //помещаем путь к порту в строку пример "/dev/ttyACM0"        const char *PortName=PortNameStr.c_str();    // преобразуем в строку в стиле Си (массив char) и сохраняем указатель на нее        serial_port = open(PortName, O_RDWR); //открываем указанный порт для чтения и записи  (только для чтения O_RDONLY)        if (serial_port < 0) {            throw std::runtime_error(u8"указанный порт отсутствует в системе");  //если открыть порт не удалось        }        switch (static_cast<int>(std::get<int32_t>(Baud)))   // в соответствии с указанной скоростью устанавливаем скорость порта         {             case 1200:cfsetspeed(&tty, B1200);                       break;             case 2400:cfsetspeed(&tty, B2400);                       break;             case 4800:cfsetspeed(&tty, B4800);                       break;             case 9600:cfsetspeed(&tty, B9600);                       break;             case 19200:cfsetspeed(&tty, B19200);                       break;             default: res = false;                       throw std::runtime_error(u8"значение скорости недопустимо");                       break;         }        // вводим основные настройки        tty.c_cflag &= ~PARENB; // без паритета        tty.c_cflag &= ~CSTOPB; // 1 стоп бит        tty.c_cflag |= CS8;     // 8 бит        tty.c_cflag &= ~CRTSCTS; // без RTS/CTS аппаратного управления потоком        //сохраняем настройки        if (tcsetattr(serial_port, TCSANOW, &tty) != 0)        {  throw std::runtime_error(u8"невозможно сохранить настройки порта");        }        memset(&read_buf, '\0', sizeof(read_buf));//инициализируем буфер        TtyAddin::SetEventBufferDepth(10);  //устанавливаем размер очереди событий в 1с  функция описана в 1С:ИТС     }     else{  res = false;            throw std::runtime_error(u8"метод serialSetting - неподдерживаемые типы данных");} //если имя порта не строка, а скорость не число     return res; } //определение метода отключения от порта void TtyAddin::DisconnectPort(void) //Отключаем порт {   close(serial_port); } //определение метода чтения буфера порта void TtyAddin::ReadPort () {   tcflush(serial_port,TCIOFLUSH); // чистим буфер порта от мусора перед чтением     sleep(1);                       // ждём (секунды)     int num_bytes = read(serial_port, &read_buf[0], sizeof(read_buf)); //читаем буфер порта     if (num_bytes <= 0) //если -1 ошибка 0 буфер пуст     {         throw std::runtime_error(u8"данные в порт не поступают");     }     else {   ExternalEvent(extensionName(), PortNameStr, static_cast<std::string>(read_buf));} // вывод в 1с через внешнее событие } //определение метода отправки строки в порт void TtyAddin::SendToPort (const variant_t & data) {   if (std::holds_alternative<std::string>(data))  //проверяем соответствие типа введённых данных     {   std::string dataString = std::get<std::string>(data); //преобразуем в строку         const char * msg = dataString.c_str();      //и  переводим ее в строку в стиле си           write(serial_port, msg, sizeof(msg));     // отправляем     }     else {   throw std::runtime_error(u8"метод serialSetting - неподдерживаемые типы данных");} } //переопределяем проунаследованные методы bool TtyAddin::ExternalEvent(const std::string &src, const std::string &msg, const std::string &data) {   return Component::ExternalEvent( src, msg, data);} bool TtyAddin::SetEventBufferDepth(long depth) {   return Component::SetEventBufferDepth(depth);} long TtyAddin::GetEventBufferDepth() {   return Component::GetEventBufferDepth();}

 

Собираем проект, компилируем. 

Внешнее устройство (Arduino UNO)

Arduino будет получать команду (определённый символ), и в соответствии с командой давать ответ — строку символов с номером ответа.

Прошивка для ардуино.

char inChar;

int counter;

void setup() {

Serial.begin(9600);

}

void loop() {

if (Serial.available()) {

inChar = Serial.read();

delay(100);

if(inChar=='e')

{

Serial.print("UNO received: ");

Serial.print(inChar);

Serial.print(" №");

Serial.println(++counter);

}

}

}

 

в модуль в 1 С добавляем код

(файл порта может называться ttyUSB0 или ttyACM0 где 0 номер если таких подключено несколько)

&НаКлиенте

Перем Компонента, ПутьКБиблиотеке;

&НаКлиенте

Процедура Подключить(Команда)

ПутьКБиблиотеке="/home/delphin/ProjectQt/estern_lib2/build-ttyAddin-Desktop_Qt_6_3_0_GCC_64bit-Release/libttyAddin.so";

РезультатПодключения = ПодключитьВнешнююКомпоненту(ПутьКБиблиотеке, "libextDLib", ТипВнешнейКомпоненты.Native);

Сообщить ("Компонента подключена - " + РезультатПодключения );

Попытка

Компонента = новый ("AddIn.libextDLib.TTY");

Сообщить ("Компонента создана");

Исключение

Сообщить ("неудалось создать компоненту");

КонецПопытки;

Попытка

Компонента.ПодключитьПорт ("/dev/ttyACM0", 9600); //указан путь к файлу порта. в ОС (папка dev, файл ttyACM0)

Сообщить ("Порт подключен");

Исключение

Сообщить ("неудалось подключить порт");

КонецПопытки;

КонецПроцедуры

 

&НаКлиенте

Процедура ПередЗакрытием(Отказ, ЗавершениеРаботы, ТекстПредупреждения, СтандартнаяОбработка)

Компонента.ОтключитьПорт();

КонецПроцедуры

 

&НаКлиенте

Процедура Получить(Команда)

Компонента.ОтправитьВПорт("e");

Компонента.ЧитатьПорт();

КонецПроцедуры

Сохраняем конфигурацию, подключаем прошитую ардуину, запускаем конфигурацию и проверяем. При нажатии на кнопку подключить подключаемся к порту, при нажатии читать получаем строку из ардуины.


ссылка на оригинал статьи https://habr.com/ru/post/666718/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *