Пользовательские протоколы
Существует большое количество различных устройств автоматизации с нестандартными протоколами обмена данными. Для решения проблемы сбора данных с таких устройств в WebHMI есть возможность создавать пользовательские протоколы на языке Lua [1]. Эта функция доступна в WebHMI начиная с версии 1.10.0.3420.
Содержание
О языке Lua
Lua является типичным процедурным языком программирования. Он предоставляет широкие возможности для объектно-ориентированной и функциональной разработки. Lua создавался как мощный и простой язык, обладающий всеми необходимыми выразительными средствами. С документацией по языку можно ознакомится на официальном сайте [2]. Бегло ознакомится с синтаксисом можно на удобном сайте [3]. В WebHMI используется Lua версии 5.1.5.
Почему именно Lua?
Lua — язык, который специально создавался для встраивания в приложения, написанные на языке Си. Он обладает отличной производительностью, потребляет очень мало ресурсов и имеет богатые возможности.
Общая концепция пользовательских протоколов
В WebHMI минимальной единицей информации является регистр. В общем случае обмен данными со всеми устройствами происходит циклически – регистры, которые должны быть опрошены в данном скане, читаются один за одним. Запись в регистры также происходит по одному регистру за раз.
WebHMI позволяет создать свой протокол и определить функции чтения и записи регистра. Эти функции должны сформировать запрос, отправить его устройству, принять от него ответ, разобрать его и, в зависимости от результата, вернуть необходимые данные.
Создание протокола
Для перехода к списку пользовательских протоколов нажмите на кнопку "Custom protocols" на страние Setup->Registers.
Вы попадете на страницу управления протоколами. В данном примере мы видим два демонстрационных протокола – ModBus TCP Demo и ModBus ASCII Demo:
Давайте посмотрим на страницу редактирования протокола ModBus TCP Demo:
Протоколу можно задать:
- название, описание
- тип (TCP/IP или Serial)
- сетевой порт по умолчанию (только для TCP)
- регулярное выражение для проверки допустимости вводимых адресов
- сообщение об ошибке, которе будет выводится при вводе неверного адреса регистра
- код с программой для обработки протокола
Регулярное выражение должно обеспечить проверку правильности адреса регистра на странице редактирования регистров (при выборе этого протокола). Пример:
Также мы видим удобный редактор кода. В нем поддерживается форматирование, подсветка и валидация синтаксиса. Так что писать код удобно.
Если в коде допущена синтаксическая ошибка то в соответствующей строке появится красный крестик. Что бы увидеть детальное сообщение об ошибке достаточно навести на него курсор мышки:
После создания протокола он появится в выпадающем списке доступных PLC models на странице создания новых Connections и с ним можно будет работать так же как и с обычным встроенным протоколом.
Необходимые функции
WebHMI ожидает увидеть во введенном коде три функции:
- createDevices
- readRegister
- writeRegister
createDevices
Процедура createDevices вызывается один раз при старте WebHMI и создает именованные префиксы для адресов регистров. Что бы лучше понять это давайте рассмотрим пример для устройств ModBUS. Создадим типы регистров для Coils, Discrete Inputs, Holding Registers, Input Registers:
function createDevices ()
addDevice({name = "C", shift = 0, base = 10, xtraFields = {1, 5}});
addDevice({name = "DI", shift = 0, base = 10, xtraFields = {2, 0}});
addDevice({name = "HR", shift = 0, base = 10, xtraFields = {3, 6, 16}});
addDevice({name = "IR", shift = 0, base = 10, xtraFields = {4, 0}});
end
Здесь создается четыре типа регистров. Для такого протокола в строке адреса регистра можно будет указывать регистры вида C14, DI4, HR34355, IR145. Процедура addDevice вызывается для каждого такого типа адресов. Ей передается таблица с такими параметрами:
- name – строка-перфикс, именно эта часть в адресе будет определять дальнейшую обработку чтения/записи этого регистра
- shift – константа, будет прибавлена к значению адреса регистра. Т.е. можно сделать что бы регистр с адресом D30 преобразовался в D1030, а D33 – в D1033 и т.п.
- base – система счисления адреса. У некоторых устройств используются адреса в восьмиричной или шестнадцатиричных системах счислений
- xtraFields – набор дополнительных параметров (максимум 5 штук), будет передан в функции read/write.
readRegister
Функция readRegister должна произвести чтение указанного регистра.
В случае успешного чтения функция readRegister должна вернуть массив байт, длина которого соответствует указанному типу данных (1, 2 или 4) или же число. В случае неудачи необходимо вернуть false.
Ей в качестве аргументов передаются три параметра:
- reg - таблица (структура) с параметрами регистра
- device - таблица (структура) с параметрами данного типа регистра (те данные, которые определены в createDevices)
- unitId – ID устройства на шине или прочий ID. Примером может быть Slave ID в ModBus RTU или Unit ID в ModBus TCP.
В структуре reg есть такие атрибуты:
- internalAddr - пересчитанный адрес регистра. Это число, пересчитанное из указанной системы счисления с прибавлением к нему shift.
- addr - оригинальный адрес регистра, который ввел пользователь.
- dataType – тип данных, который пользователь указал для регистра. 0 = Bit, 1 = Byte, 2 = Word, 3 = Double Word, 4 = UnixTime
В структуре device есть такие атрибуты:
- shift - значение shift из соответствующей строки createDevices
- base - значение base из соответствующей строки createDevices
- xtraFields – значение xtraFields из соответствующей строки createDevices
Эти параметры передаются для того, что бы можно было правильно и полноценно составить запрос согласно протокола.
Для передачи запроса устройству используется функция sendBytes. Для чтения ответа - readBytes.
На вход sendBytes принимает таблицу (массив) байт. Результатом будет 0 в случае успеха и 1 в случае ошибки.
На вход readBytes принимает количество байт, которое необходимо прочитать. Результатом будет таблица (массив) байт в случае успеха и 1 в случае ошибки.
Если необходимо закрыть соединение (например, в случае множественных оштбок), то можно вызвать процедуру closeConnection.
Если необходимо сделать паузу, то можно вызвать функцию sleep. Ее единственным аргументом должно быть время в микросекундах. Пример: sleep(20000); - произойдет пауза 20 миллисекунд.
Для работы с битами можно использовать библиотеку bitop [4].
Для отладки и вывода диагностических сообщений можно использовать процедуры ERROR, INFO, DEBUG и TRACE которые доступны и в обычных сценариях Lua [5].
Что бы лучше понять как это все работает давайте рассмотрим пример функции readRegister для протокола ModBus TCP:
local transId = 0;
local errorCount = 0;
function readRegister (reg, device, unitId)
local request = {};
-- transaction ID
transId = transId + 1;
request[1] = bit.band(bit.rshift(transId, 8), 255);
request[2] = bit.band(transId, 255);
-- protocol ID
request[3] = 0;
request[4] = 0;
-- message length
request[5] = 0;
request[6] = 6;
-- unit ID
request[7] = unitId;
-- function code
request[8] = device.xtraFields[1];
-- address of register
request[9] = bit.band(bit.rshift(reg.internalAddr, 8), 255);
request[10] = bit.band(reg.internalAddr, 255);
-- count of registers
request[11] = 0;
request[12] = 1;
if (reg.dataType == 3) then -- double word
request[12] = 2;
end
local res = sendBytes(request);
if (res ~= 0) then
DEBUG("Can't send bytes");
return 0;
end
local response = {};
-- read MBAP Header
response = readBytes(7);
res = #response;
if (res ~= 7) then
errorCount = errorCount + 1;
if (errorCount > 3) then
closeConnection();
errorCount = 0;
end
DEBUG("Can't read MBAP");
return 0;
end
if (response[1] ~= request[1] or response[2] ~= request[2]) then
ERROR("Wrong transaction ID. Got #" .. (response[1] * 256 + response[2]) .. " but expected #" .. (request[1] * 256 + request[2]));
return 0;
end
if (response[3] ~= request[3] or response[4] ~= request[4]) then
ERROR("Wrong protocol");
return 0;
end
if (response[7] ~= request[7]) then
ERROR("Wrong UnitID in response");
return 0;
end
local length = response[5] * 256 + response[6];
if (length < 1) then
ERROR("Wrong length in response");
return 0;
end
local responsePDU = {};
-- read MBAP Header
responsePDU = readBytes(length - 1);
res = #responsePDU;
if (responsePDU[1] ~= request[8]) then
ERROR("Wrong function in response");
return 0;
end
local dataLength = responsePDU[2];
if (dataLength ~= length - 3) then
ERROR("Wrong length in PDU");
return 0;
end
local result = {};
if (dataLength >= 1) then
for i = 1, dataLength do
result[i] = responsePDU[2 + i];
end
end
return result;
end
writeRegister
Функция writeRegister должна произвести запись нового значения в указанный регистр. При успешной записи она должна вернуть true. В случае ошибки - false.
Ей передаются все те же параметры, что и для readRegister, а также дополнительно четвертый параметр - новое значение.
Функция writeRegister может использовать те же способы записи и чтения массивов байт в порт.
Давайте рассмотрим пример этой функции для протокола ModBus TCP:
function writeRegister (reg, device, unitId, newValue)
local request = {};
transId = transId + 1;
-- transaction ID
request[1] = bit.band(bit.rshift(transId, 8), 255);
request[2] = bit.band(transId, 255);
-- protocol ID
request[3] = 0;
request[4] = 0;
if (reg.dataType == 3) then -- double word
-- message length
request[5] = 0;
request[6] = 11;
-- unit ID
request[7] = unitId;
-- function code
request[8] = device.xtraFields[3];
-- address of register
request[9] = bit.band(bit.rshift(reg.internalAddr, 8), 255);
request[10] = bit.band(reg.internalAddr, 255);
-- count of registers
request[11] = 0;
request[12] = 2;
-- bytes with data
request[13] = 4;
-- value of registers
request[14] = bit.band(bit.rshift(newValue, 24), 255);
request[15] = bit.band(bit.rshift(newValue, 16), 255);
request[16] = bit.band(bit.rshift(newValue, 8), 255);
request[17] = bit.band(newValue, 255);
local res = sendBytes(request);
if (res ~= 0) then
DEBUG("Can't send bytes");
return 0;
end
local response = {};
response = readBytes(7);
res = #response;
if (res ~= 7) then
DEBUG("Wrong response length");
return false;
end
if (response[1] ~= request[1] or response[2] ~= request[2]) then
ERROR("Wrong transaction ID. Got #" .. (response[1] * 256 + response[2]) .. " but expected #" .. (request[1] * 256 + request[2]));
return false;
end
if (response[3] ~= request[3] or response[4] ~= request[4]) then
ERROR("Wrong protocol");
return false;
end
if (response[7] ~= request[7]) then
ERROR("Wrong UnitID in response");
return false;
end
local length = response[5] * 256 + response[6];
if (length < 1) then
ERROR("Wrong length in response");
return false;
end
local responsePDU = {};
responsePDU = readBytes(length - 1);
res = #responsePDU;
if (responsePDU[1] ~= request[8]) then
ERROR("Wrong function in response");
return false;
end
if (responsePDU[2] ~= request[9] or responsePDU[3] ~= request[10]) then
ERROR("Wrong register address in response");
return false;
end
if (responsePDU[4] ~= 0 or responsePDU[5] ~= 2) then
ERROR("Wrong register count in response");
return false;
end
else
if (device.xtraFields[2] == 0) then
ERROR("Can't write these type of registers (" .. device.name .. ")");
return 0;
end
-- message length
request[5] = 0;
request[6] = 6;
-- unit ID
request[7] = unitId;
request[8] = device.xtraFields[2];
-- address of register
request[9] = bit.band(bit.rshift(reg.internalAddr, 8), 255);
request[10] = bit.band(reg.internalAddr, 255);
local val = newValue;
if (reg.dataType == 0) then
if (val > 0) then
val = 255*256;
else
val = 0;
end
end
-- value of registers
request[11] = bit.band(bit.rshift(val, 8), 255);
request[12] = bit.band(val, 255);
local res = sendBytes(request);
if (res ~= 0) then
DEBUG("Can't send bytes");
return 0;
end
local response = {};
local requestLen = #request;
response = readBytes(requestLen);
res = #response;
if (res ~= requestLen) then
DEBUG("Wrong response length");
return false;
end
for i = 1,res do
if (response[i] ~= request[i]) then
DEBUG("Wrong response");
return false;
end
end
end
return true;
end
Примеры протоколов
В качестве примера мы реализовали (частично) два протокола: ModBus TCP и ModBus ASCII.