Custom Protocols
Существует большое количество различных устройств автоматизации с нестандартными протоколами обмена данными. Для решения проблемы сбора данных с таких устройств в 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.
onScanStart
Процедура onScanStart вызывается каждый раз при начале нового скана. Она может быть полезна для протоколов, где за один запрос читается массив значений для нескольких регистров. Для таких протоколов можно кешировать результат запроса и возвращать значения из кеша. Сброс кеша можно производить в процедуре onScanStart.
onScanStart доступна в прошивках начиная с версии 2.1.3923.
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 и sendString. Для чтения ответа - readBytes, readString.
На вход sendBytes принимает таблицу (массив) байт. Результатом будет true в случае успеха и false в случае ошибки.
На вход sendString принимает строку. Результатом будет true в случае успеха и false в случае ошибки.
На вход readBytes принимает количество байт, которое необходимо прочитать. Результатом будет таблица (массив) байт в случае успеха и false в случае ошибки.
На вход readString принимает количество байт, которое необходимо прочитать. Результатом будет строка в случае успеха и false в случае ошибки.
Если необходимо закрыть соединение (например, в случае множественных ошибок), то можно вызвать процедуру 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 == false) then
DEBUG("Can't send bytes");
return false;
end
local response = {};
-- read MBAP Header
response = readBytes(7);
if (response == false) then
errorCount = errorCount + 1;
if (errorCount > 3) then
closeConnection();
errorCount = 0;
end
DEBUG("Can't read MBAP");
return false;
end
res = #response;
if (res ~= 7) then
errorCount = errorCount + 1;
if (errorCount > 3) then
closeConnection();
errorCount = 0;
end
DEBUG("Can't read MBAP");
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 = {};
-- read MBAP Header
responsePDU = readBytes(length - 1);
if (responsePDU == false) then
errorCount = errorCount + 1;
if (errorCount > 3) then
closeConnection();
errorCount = 0;
end
DEBUG("Can't read PDU in response");
return false;
end
res = #responsePDU;
if (responsePDU[1] ~= request[8]) then
ERROR("Wrong function in response");
return false;
end
local dataLength = responsePDU[2];
if (dataLength ~= length - 3) then
ERROR("Wrong length in PDU");
return false;
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 == false) then
DEBUG("Can't send bytes");
return 0;
end
local response = {};
response = readBytes(7);
if (response == false) then
DEBUG("Can't read response");
return false;
end
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);
if (responsePDU == false) then
DEBUG("Can't read response PDU");
return false;
end
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 == false) then
DEBUG("Can't send bytes");
return 0;
end
local response = {};
local requestLen = #request;
response = readBytes(requestLen);
if (response == false) then
DEBUG("Can't read response");
return false;
end
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.
На странице подключения к тензометрическому контроллеру Тензод200 также есть пример пользовательского протокола.