Аннотация
В статье рассмотрен процесс создания внешней компоненты для 1С в среде Qt Creator для операционной системы Linux (ubuntu, debian, mint и им подобных). На примере компоненты для сбора данных от внешней аппаратуры и сохранение их в базе, посредством 1С. В качестве внешней аппаратуры в данном примере будем использовать Arduino UNO.
Для создания внешней компоненты понадобятся
-
Материал «Технология создания внешних компонент», на странице 1С:ИТС
-
Шаблон пустой внешней компоненты.
-
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");
Компонента.ЧитатьПорт();
КонецПроцедуры
Сохраняем конфигурацию, подключаем прошитую ардуину, запускаем конфигурацию и проверяем. При нажатии на кнопку подключить подключаемся к порту, при нажатии читать получаем строку из ардуины.
Ну вот и всё. в данном примере мы открываем порт для чтения и записи отправляём туда букву 'е', в свою очередь ардуинка если получена буква 'е' формирует и отправляет строку (ответ).
Область возможного применения
1. Сбор и документирование данных со счётчиков электроэнергии посредством rs-485, rs-422.
2. Сбор и документирование данных со станков с ЧПУ посредством выше указанных интерфейсов.
3. Сбор и документирование данных с маршрутных контроллеров и систем диагностики авто.
4. И любая подобная задача.
Тестировалось на платформе 1С:Предприятие 8.3 (8.3.20.1789) ОС Linux Mint, Ubuntu.