Пользовательские протоколы/en — различия между версиями
(Новая страница: «== createDevices == The procedure 'createDevices' is called once at the start of WebHMI and creates named prefixes for register addresses. To better understand th…») |
(Новая страница: «There are four types of registers. For such a protocol, registers of the form C14, DI4, HR34355, IR145 can be specified in the register address line. The '''addDe…») |
||
Строка 58: | Строка 58: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | There are four types of registers. For such a protocol, registers of the form C14, DI4, HR34355, IR145 can be specified in the register address line. The '''addDevice ''' procedure is called for each type of address. It is passed a table with such parameters: | |
− | + | ||
* name – строка-перфикс, именно эта часть в адресе будет определять дальнейшую обработку чтения/записи этого регистра | * name – строка-перфикс, именно эта часть в адресе будет определять дальнейшую обработку чтения/записи этого регистра |
Версия 13:46, 21 июля 2017
There are a lot of different automation devices with non-standard communication protocols. To solve the problem of data collection from such devices in WebHMI, it is possible to create custom protocols in Lua [1]. This function is available in WebHMI since version 1.10.0.3420.
Содержание
About Lua
Lua is a typical procedural programming language. It provides ample opportunities for object-oriented and functional development. Lua was created as a powerful and simple language with all the necessary expressive means. The documentation on the language can be found on the official website [2]. You can easily get acquainted with the syntax on a convenient site [3]. WebHMI uses Lua version 5.1.5.
Why Lua?
Lua is a language that was specially created for embedding in applications written in C language. It has excellent performance, consumes very few resources and has rich capabilities.
General concept of custom protocols
In WebHMI, the minimum unit of information is the register. In general, data exchange with all devices occurs cyclically - the registers that should be polled in this scan are read one by one. Registers are also written one register at a time.
WebHMI allows you to create your own protocol and define the read and write functions for the register. These functions must form a request, send it to the device, take an answer from it, disassemble it and, depending on the result, return the necessary data.
Creating a Protocol
To go to the list of user protocols, click on the button "Custom protocols" on the pageSetup->Registers.
You will be taken to the protocol management page. In this example, we see two demonstration protocols - ModBus TCP Demo and ModBus ASCII Demo:
Let's look at the page for editing the ModBus TCP Demo protocol:
Followed are settings used for custom protocols:
- Title, description
- type (TCP/IP or Serial)
- default TCP port (only for TCP)
- regular expression used for register address validation
- An error message that will be displayed when you enter an invalid register address
- Code with a custom protocol
The regular expression must ensure that the register address is validated on the register editing page (when this protocol is selected). Example:
We also see a convenient code editor. It supports formatting, highlighting and validation of syntax. So it's convenient to write the code
If there is a syntax error in the code, a red X appears in the corresponding line. To see a detailed error message, just point your mouse at it:
After creating the protocol, it will appear in the drop-down list of available PLC models on the page of creating newConnections and with it you can work as well as with the usual built-in protocol
Necessary functions
WebHMI expects to see three functions in the custom protocol code:
- createDevices
- readRegister
- writeRegister
createDevices
The procedure 'createDevices' is called once at the start of WebHMI and creates named prefixes for register addresses. To better understand this, let's look at an example for ModBUS devices. Let's create register types for the 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
There are four types of registers. For such a protocol, registers of the form C14, DI4, HR34355, IR145 can be specified in the register address line. The addDevice procedure is called for each type of address. It is passed a table with such parameters:
- 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 также есть пример пользовательского протокола.