Задача была по поводу наладки обмена информацией между 1С УНФ и контроллера управления доступа клиентов. Имеется описание протокола для связи с контроллером по TCP. Контроллер может работать в режиме сервера, прослушивая определенный порт принимая с него команды и отправляя обратно ответы. Решил использовать WinSocket, благо информации много.
Вот неплохая публикация, где рассматриваются режимы клиента и сервера с использованием ActiveX://infostart.ru/public/119982/
Одна беда - УФ. ActiveX - не поддерживается.
Итак приступим.
Прикручиваем к УФ
ActiveX не прижился на УФ. Метод с офисом, который рассматривался здесь - сложноват, поэтому подключаем Winsocket как COMобъект.
Как же его хранить? Будем хранить его в параметрах сеанса. Оттуда он доступен и клиенту и серверу.
Процедура ПриОткрытииНаСервере()
Контроллер = Новый COMОбъект("mswinsock.winsock");
ПараметрыКонтроллера = Новый Структура("Адрес, Порт");
ПараметрыКонтроллера.Адрес = "xxx.xxx.xxx.xxx";
ПараметрыКонтроллера.Порт = "pppp";
СтруктОбъекта = Новый Структура("Контроллер",Контроллер);
ПараметрыСеанса.Z5=ПоместитьВоВременноеХранилище(СтруктОбъекта,Новый УникальныйИдентификатор());
ИнициализироватьПодключение(ПараметрыКонтроллера);
КонецПроцедуры
Заметим, что COMобъект мы спрятали в структуру. Как уже обсуждалось на Инфостарте, COMобъекты не поддерживают сериализацию. А если "спрятать" его в структуру, ошибок не будет:
СтруктОбъекта = Новый Структура("Контроллер",Контроллер);
ПараметрыСеанса.Z5=ПоместитьВоВременноеХранилище(СтруктОбъекта,Новый УникальныйИдентификатор());
Рассмотрим подключение. Отдельное спасибо Андрею, автору публикации: //infostart.ru/public/119982/ и участникам обсуждения за знакомство с winsocket.
&НаСервере
Процедура ИнициализироватьПодключение(ПараметрыПодключения)
ws = ПолучитьИзВременногоХранилища(ПараметрыСеанса.Z5).Контроллер;
Если ws.State <> 7 тогда
Если ws.State <> 0 Тогда
ws.Close();
КонецЕсли;
ws.RemoteHost = СокрЛП(ПараметрыПодключения.Адрес);
ws.RemotePort = СокрЛП(ПараметрыПодключения.Порт);
ws.Connect();
Иначе
ws.Close();
ws.RemoteHost = СокрЛП(ПараметрыПодключения.Адрес);
// ws.RemoteHostIP = СокрЛП(ПараметрыПодключения.Адрес);
ws.RemotePort = СокрЛП(ПараметрыПодключения.Порт);
ws.Connect();
КонецЕсли;
Объект.Статус = ws.state; //Сохраняем статус чтобы отображать его на форме
КонецПроцедуры
Все, подключились. При открытии формы нашей обработки мы хотим получить некоторые данные с контроллера.
Работа с контроллером будет происходить асинхронно. Отправляем запрос, затем ждем ответ. Дождавшись обрабатываем его и отправляем новый запрос и т.д.
При старте обработки получим внутренние адреса контроллеров и запустим периодический опрос в котором есть команды на получение списка карт и временных зон:
&НаКлиенте
Процедура ПриОткрытии(Отказ)
//Мы будем использовать сканер и считыватель
ИспользоватьПодключаемоеОборудование=Истина;
// ПодключаемоеОборудование
МенеджерОборудованияКлиентПереопределяемый.НачатьПодключениеОборудованиеПриОткрытииФормы(ЭтаФорма, "СканерШтрихкода,СчитывательМагнитныхКарт");
// Конец ПодключаемоеОборудование
//--------------------------------------------
ПриОткрытииНаСервере();
//Заблокируем форму от изменений
УправлениеДоступностью(Ложь);
//Пошлем первую команду на получение списка контроллеров
ПодключитьОбработчикОжидания("ПолучитьСтатус",10);
Объект.ТипКоманды = "20";
Объект.Команда = "ПолучитьАдресКонтроллера";
Объект.ТекстСообщения = "00 08 08 01 00 00 00 00";
ПодключитьОбработчикОжидания("ВыполнитьКоманду",1);
//Подключим периодический опрос контроллеров
Объект.НомерВыполняемойКомандыОпроса = 1;
ПодключитьОбработчикОжидания("ПериодическийОпрос",1); //здесь могут крутиться несколько команд, которые получают события и отправляют изменения
КонецПроцедуры
Сама процедура отправки команды, которая в нашем случае вызывается многократно 1 раз в секунду, на случай, если контроллер чем-то занят.
&НаКлиенте
Процедура ВыполнитьКоманду()
ПолучитьСтатус();
Элементы.Статус.Заголовок = Объект.Статус;
Если Объект.Статус = 0 Тогда //Не подключено
ПриОткрытииНаСервере();
Возврат;
ИначеЕсли Объект.Статус = 6 Тогда // еще идет подключение
Возврат;
КонецЕсли;
УправлениеДоступностью(Ложь); //ничего нельзя делать на форме
Результат = ОтправитьДанные(); // Ниже функция отправки
Элементы.Статус.Заголовок = Объект.Статус;
Если Результат Тогда
Объект.Лог = Формат(ТекущаяДата(),"ДЛФ=DT") + ": " + Объект.Команда +" --> " + Символы.ПС + Объект.Лог;
Объект.Лог = Объект.ТекстСообщения + Символы.ПС + Объект.Лог;
ОтключитьОбработчикОжидания("ВыполнитьКоманду"); //Команда выполнилась, отключаем этот обработчик
ПодключитьОбработчикОжидания("ПолучитьОтвет",1); //Подключаем ожидание ответа Многократно
Иначе
Если Объект.Статус = 9 Тогда //ошибка, не расшифровать нафиг
ПриОткрытииНаСервере(); //реконнект (кстати не помогает. Только отключение кабеля)
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Функцию ОтправитьДанные() мы рассмотрим ниже, а пока из кода видно, что после отправки команды на контроллер мы подключаем второй обработчик ожидания на получение ответа контроллера. ActiveX winsocket, который мы в силу обстоятельств не можем использовать, имеет очень важное событие WinSocketDataArrival, которое возникает при получении данных. Нам придется обходиться без него. Мы просто 1 раз в секунду будем читать возможно полученные ответы.
&НаКлиенте
Процедура ПолучитьОтвет()
Элементы.Статус.Заголовок = Объект.Статус;
Результат = ПолучитьОтветНаСервере(); //ф-я ниже
Элементы.Статус.Заголовок = Объект.Статус;
Если Результат Тогда
ОтключитьОбработчикОжидания("ПолучитьОтвет"); //ответ получен, отключаем обработчик
Объект.Лог = Формат(ТекущаяДата(),"ДЛФ=DT") + ": " +Объект.Команда +" <-- " + Символы.ПС + Объект.Лог;
Объект.Лог = Объект.ОтветКонтроллера + Символы.ПС + Объект.Лог;
ВыполнитьОбработкуОтвета(); // Здесь много условий которые получают название команды и соответственно ее обрабатывают
Иначе
ПриОткрытииНаСервере(); //реконнект (не знаю, зачем я его делаю))))
КонецЕсли;
КонецПроцедуры
Собственно функция отправки:
&НаСервере
Функция ОтправитьДанные()
Результат = Ложь;
Контроллер = ПолучитьИзВременногоХранилища(ПараметрыСеанса.Z5).Контроллер; //получили winsocket из параметров сеанса
buff = ПреобразоватьДляОтправки(Объект.ТекстСообщения); // преобразование в массив HEX требуется для моего контроллера
buff[0] = "&H" + Объект.ТипКоманды;
ВОтправку = "";
Для н = 0 По buff.Количество()-1 Цикл
ВОтправку =ВОтправку + buff[н] + " ";
КонецЦикла;
Объект.Статус = Контроллер.State;
Если Контроллер.State = 7 тогда //Подключились удачно, отправляем данные
Попытка
Для н = 0 По buff.Количество()-1 Цикл
Контроллер.SendData(Chr(buff[н])); // отправляю посимвольно из буфера Chr(Код) - функция с VBScript которая преобразует из HEX в символ.
КонецЦикла;
Результат = Истина;
Исключение
КонецПопытки;
КонецЕсли;
Возврат Результат;
КонецФункции
..и функции получения ответа, которые вызываются ежесекундным обработчиком получения ответов контроллера:
&НаСервере
Функция ПолучитьОтветНаСервере()
Результат = "";
Попытка
Результат = ПолучитьДанныеСКонтроллера();
Объект.ОтветКонтроллера = Результат;
Возврат Истина;
Исключение
Возврат ложь;
КонецПопытки;
КонецФункции
&НаСервере
Функция ПолучитьДанныеСКонтроллера()
Контроллер = ПолучитьИзВременногоХранилища(ПараметрыСеанса.Z5).Контроллер;
Ответ = "";
Пока Истина Цикл
Результат = "";
Контроллер.GetData(Результат);
Если НЕ Результат = "" Тогда
Ответ = Ответ + Результат;
Иначе
//Преобразовать
БуферПриема = СтрокуВМассивы(Ответ);
Масс = БуферПриема[0];
м4 = Неопределено;
Результат = Преобразовать5_4(Масс,М4);
Расшифровано = "";
Для н = 0 По м4.Количество()-1 Цикл
Расшифровано = Расшифровано + М4[н] + " ";
КонецЦикла;
Объект.ОтветКонтроллера = Расшифровано;
Прервать;
КонецЕсли;
КонецЦикла;
Возврат Объект.ОтветКонтроллера;
КонецФункции
Так, что мы уже видим, что чтобы отправить команду на контроллер мы должны выполнить следующий код:
Объект.ТипКоманды = "20";
Объект.Команда = "ПолучитьАдресКонтроллера";
Объект.ТекстСообщения = "00 08 08 01 00 00 00 00";
ПодключитьОбработчикОжидания("ВыполнитьКоманду",1);
Дальше все выполнит пара обработчиков ожидания "ВыполнитьКоманду" и "ПолучитьОтвет".
По окончании выполнения команды и получения ответа контроллера будет вызвана процедура ВыполнитьОбработкуОтвета(), которая выполнит требуемые действия с полученным ответом. Не секрет, что некоторые команды зависят от предыдущих. Например, в моем случае для получения всех карт зашитых в контроллер требуется выполнить следующие действия:
1. Запросить адрес последней карты. Выяснить сколько байт занимают все карты, разбить на блоки по 64 байта (1 условный блок хранения в контроллере)
2. Запросить 1 блок
3. Получить ответ - обработать
4. Запросить 2 блок - обработать
........
n. Запросить неполный последний блок
n+1. Обработать.
В этом случае в процедуре ВыполнитьОбработкуОтвета() мы зацикливаем выполнение команд, прямо в этой процедуре запуская на выполнение новую команду.
Функция обработки полученных ответов:
&НаКлиенте
Процедура ВыполнитьОбработкуОтвета()
// БуферПриема = СтрокуВМассивы(Объект.ОтветКонтроллера);
Если Объект.Команда = "ПолучитьАдресКонтроллера" Тогда
М4 = ОтветВМассив();
ФлагПриема = Истина;
//Расшифруем полученные данные
i = 8;
Пока i < 20 Цикл
текАдр = АдресКонтроллера(М4[i], i - 8);
Если ТекАдр.Количество()>0 Тогда
Для Каждого Эл Из ТекАдр Цикл
Объект.СписокКонтроллеров.Добавить(Эл.Значение);
КонецЦикла;
КонецЕсли;
i = i + 1;
КонецЦикла;
Объект.Команда = "";
ИначеЕсли Объект.Команда = "СписокКарт" Тогда
..........................................
КонецЕсли;
УправлениеДоступностью(Истина);
КонецПроцедуры
Почему все так сложно? Дело в том, что SendData() отправляет только когда прекращается активность. Объясню.
Контроллер.sendData("что-то-там");
текВремя = ТекущаяДата();
Пока Истина Цикл
Если ТекущаяДата() > текВремя + 10;//Цикл задержки 10 секунд Тогда
Прервать;
КонецЕсли;
КонецЦикла;
Получаем = Контроллер.GetData();
Не работает. Отправка произойдет по окончании выполнения всего кода текущей процедуры, которую мы вызвали, предположим, нажатием на кнопку. Можете проверить.
Итак в заключении.
Предложенный вариант не претендует на оригинальность, но зато работает. Медленно, но работает.
Надеюсь, что мой опыт кому-нибудь пригодится.
Однако, есть мнение. Кому не жалко немного времени, создать ВК с методами:
1. Подключить(Адрес,Порт) Возвращает Истина, Ложь
2. ОтправитьСтроку(Строка) Возвращает Истина Ложь
3. ПрочитатьОтвет() Возвращает ответы, накопленные в буфере Возможен вариант, В какой-либо переменной появляется ответ, который на стороне 1С можно прочитать и/или удалить, если прочитали
4. Отключить
Там строк 50 текста будет, которые повысят стабильность, избавят от асинхронности в 1С, которая требует обработчиков, которые работают 1 раз в секунду и замедляют общение с устройствами.
Понимаю, что можно было организовать иначе, но основное требование - минимум изменений. Добавлена в конфигурацию только: переменная Z5 в параметры сеансов.
Если есть что сказать, присоединяйтесь к общению. Я мог что-то упустить. 1С на месте не стоит.
Видео с примером работы с контроллером из 1С.