Пользовательские протоколы — различия между версиями

Материал из WebHMI Wiki
Перейти к: навигация, поиск
(Создание протокола)
 
(не показаны 22 промежуточные версии 3 участников)
Строка 1: Строка 1:
 +
<languages/>
 +
<translate>
 +
<!--T:1-->
 
Существует большое количество различных устройств автоматизации с нестандартными протоколами обмена данными. Для решения проблемы сбора данных с таких устройств в WebHMI есть возможность создавать пользовательские протоколы на языке Lua [https://ru.wikipedia.org/wiki/Lua]. Эта функция доступна в WebHMI начиная с версии 1.10.0.3420.
 
Существует большое количество различных устройств автоматизации с нестандартными протоколами обмена данными. Для решения проблемы сбора данных с таких устройств в WebHMI есть возможность создавать пользовательские протоколы на языке Lua [https://ru.wikipedia.org/wiki/Lua]. Эта функция доступна в WebHMI начиная с версии 1.10.0.3420.
  
== О языке Lua ==
+
== О языке Lua == <!--T:2-->
 
Lua является типичным процедурным языком программирования. Он предоставляет широкие возможности для объектно-ориентированной и функциональной разработки. Lua создавался как мощный и простой язык, обладающий всеми необходимыми выразительными средствами. С документацией по языку можно ознакомится на официальном сайте [http://www.lua.org/manual/5.1/]. Бегло ознакомится с синтаксисом можно на удобном сайте [http://tylerneylon.com/a/learn-lua/]. В WebHMI используется Lua версии 5.1.5.
 
Lua является типичным процедурным языком программирования. Он предоставляет широкие возможности для объектно-ориентированной и функциональной разработки. Lua создавался как мощный и простой язык, обладающий всеми необходимыми выразительными средствами. С документацией по языку можно ознакомится на официальном сайте [http://www.lua.org/manual/5.1/]. Бегло ознакомится с синтаксисом можно на удобном сайте [http://tylerneylon.com/a/learn-lua/]. В WebHMI используется Lua версии 5.1.5.
  
 +
<!--T:3-->
 
Почему именно Lua?
 
Почему именно Lua?
  
Lua — язык, который специально создавался для встраивания в приложения, написанные на языке Си. Он обладает отличной производительность, потребляет очень мало ресурсов и имеет богатые возможности.
+
<!--T:4-->
 +
Lua — язык, который специально создавался для встраивания в приложения, написанные на языке Си. Он обладает отличной производительностью, потребляет очень мало ресурсов и имеет богатые возможности.
  
== Общая концепция пользовательских протоколов ==
+
== Общая концепция пользовательских протоколов == <!--T:5-->
 
В WebHMI минимальной единицей информации является регистр. В общем случае обмен данными со всеми устройствами происходит циклически – регистры, которые должны быть опрошены в данном скане, читаются один за одним. Запись в регистры также происходит по одному регистру за раз.  
 
В WebHMI минимальной единицей информации является регистр. В общем случае обмен данными со всеми устройствами происходит циклически – регистры, которые должны быть опрошены в данном скане, читаются один за одним. Запись в регистры также происходит по одному регистру за раз.  
  
 +
<!--T:6-->
 
WebHMI позволяет создать свой протокол и определить функции чтения и записи регистра. Эти функции должны сформировать запрос, отправить его устройству, принять от него ответ, разобрать его и, в зависимости от результата, вернуть необходимые данные.
 
WebHMI позволяет создать свой протокол и определить функции чтения и записи регистра. Эти функции должны сформировать запрос, отправить его устройству, принять от него ответ, разобрать его и, в зависимости от результата, вернуть необходимые данные.
  
== Создание протокола ==
+
== Создание протокола == <!--T:7-->
 
Для перехода к списку пользовательских протоколов нажмите на кнопку "'''Custom protocols'''" на страние '''Setup->Registers'''.<br/>
 
Для перехода к списку пользовательских протоколов нажмите на кнопку "'''Custom protocols'''" на страние '''Setup->Registers'''.<br/>
 
[[Файл:Custom-protocol-button.png|600px]]<br/>
 
[[Файл:Custom-protocol-button.png|600px]]<br/>
  
 
+
<!--T:8-->
 
Вы попадете на страницу управления протоколами. В данном примере мы видим два демонстрационных протокола – ModBus TCP Demo и ModBus ASCII Demo:<br/>
 
Вы попадете на страницу управления протоколами. В данном примере мы видим два демонстрационных протокола – ModBus TCP Demo и ModBus ASCII Demo:<br/>
 
[[Файл:Custom-protocol-list.png|600px]]<br/>
 
[[Файл:Custom-protocol-list.png|600px]]<br/>
  
 
+
<!--T:9-->
 
Давайте посмотрим на страницу редактирования протокола ModBus TCP Demo:<br/>
 
Давайте посмотрим на страницу редактирования протокола ModBus TCP Demo:<br/>
 
[[Файл:Custom-protocol-demo.png|600px]]<br/>
 
[[Файл:Custom-protocol-demo.png|600px]]<br/>
  
 +
<!--T:10-->
 
Протоколу можно задать:
 
Протоколу можно задать:
  
 +
<!--T:11-->
 
* название, описание
 
* название, описание
 
* тип (TCP/IP или Serial)
 
* тип (TCP/IP или Serial)
 
* сетевой порт по умолчанию (только для TCP)
 
* сетевой порт по умолчанию (только для TCP)
 
* регулярное выражение для проверки допустимости вводимых адресов
 
* регулярное выражение для проверки допустимости вводимых адресов
* сообщение об ощибке, которе будет выводится при вводе неверного адреса регистра
+
* сообщение об ошибке, которе будет выводится при вводе неверного адреса регистра
 +
* код с программой для обработки протокола
 +
 
 +
<!--T:12-->
 +
Регулярное выражение должно обеспечить проверку правильности адреса регистра на странице редактирования регистров (при выборе этого протокола). Пример:<br/>
 +
[[Файл:Custom-protocol-validate.png|600px]]<br/>
 +
 
 +
<!--T:13-->
 +
Также мы видим удобный редактор кода. В нем поддерживается форматирование, подсветка и валидация синтаксиса. Так что писать код удобно.<br/>
 +
Если в коде допущена синтаксическая ошибка то в соответствующей строке появится красный крестик. Что бы увидеть детальное сообщение об ошибке достаточно навести на него курсор мышки:<br/>
 +
[[Файл:Custom-protocol-error.png|600px]]<br/>
  
Также мы видим удобный редактор кода. В нем поддерживается форматирование, подсветка и валидация синтаксиса. Так что писать код удобно.
+
<!--T:14-->
 +
После создания протокола он появится в выпадающем списке доступных '''PLC models''' на странице создания новых '''Connections''' и с ним можно будет работать так же как и с обычным встроенным протоколом. <br/>
 +
[[Файл:Custom-protocol-select.png|600px]]<br/>
  
== Необходимые функции ==
+
== Необходимые функции == <!--T:15-->
 
WebHMI ожидает увидеть во введенном коде три функции:
 
WebHMI ожидает увидеть во введенном коде три функции:
 
* createDevices
 
* createDevices
Строка 41: Строка 61:
 
* writeRegister
 
* writeRegister
  
== createDevices ==
+
== createDevices == <!--T:16-->
 
Процедура '''createDevices''' вызывается один раз при старте WebHMI и создает именованные префиксы для адресов регистров. Что бы лучше понять это давайте рассмотрим пример для устройств ModBUS. Создадим типы регистров для Coils, Discrete Inputs, Holding Registers, Input Registers:
 
Процедура '''createDevices''' вызывается один раз при старте WebHMI и создает именованные префиксы для адресов регистров. Что бы лучше понять это давайте рассмотрим пример для устройств ModBUS. Создадим типы регистров для Coils, Discrete Inputs, Holding Registers, Input Registers:
  
<pre>
+
<!--T:17-->
 +
<syntaxhighlight lang="lua">
 
function createDevices ()
 
function createDevices ()
 
   addDevice({name = "C",  shift = 0, base = 10, xtraFields = {1, 5}});
 
   addDevice({name = "C",  shift = 0, base = 10, xtraFields = {1, 5}});
Строка 51: Строка 72:
 
   addDevice({name = "IR", shift = 0, base = 10, xtraFields = {4, 0}});
 
   addDevice({name = "IR", shift = 0, base = 10, xtraFields = {4, 0}});
 
end
 
end
</pre>
+
</syntaxhighlight>
  
 +
<!--T:18-->
 
Здесь создается четыре типа регистров. Для такого протокола в строке адреса регистра можно будет указывать регистры вида C14, DI4, HR34355, IR145.  
 
Здесь создается четыре типа регистров. Для такого протокола в строке адреса регистра можно будет указывать регистры вида C14, DI4, HR34355, IR145.  
 
Процедура '''addDevice''' вызывается для каждого такого типа адресов. Ей передается таблица с такими параметрами:
 
Процедура '''addDevice''' вызывается для каждого такого типа адресов. Ей передается таблица с такими параметрами:
  
 +
<!--T:19-->
 
* name – строка-перфикс, именно эта часть в адресе будет определять дальнейшую обработку чтения/записи этого регистра
 
* name – строка-перфикс, именно эта часть в адресе будет определять дальнейшую обработку чтения/записи этого регистра
 
* shift – константа, будет прибавлена к значению адреса регистра. Т.е. можно сделать что бы регистр с адресом D30 преобразовался в D1030, а D33 – в D1033 и т.п.
 
* shift – константа, будет прибавлена к значению адреса регистра. Т.е. можно сделать что бы регистр с адресом D30 преобразовался в D1030, а D33 – в D1033 и т.п.
Строка 61: Строка 84:
 
* xtraFields – набор дополнительных параметров (максимум 5 штук), будет передан в функции read/write.
 
* xtraFields – набор дополнительных параметров (максимум 5 штук), будет передан в функции read/write.
  
 +
== onScanStart == <!--T:20-->
 +
Процедура '''onScanStart''' вызывается каждый раз при начале нового скана. Она может быть полезна для протоколов, где за один запрос читается массив значений для нескольких регистров. Для таких протоколов можно кешировать результат запроса и возвращать значения из кеша. Сброс кеша можно производить в процедуре onScanStart.
  
== readRegister ==
+
<!--T:21-->
 +
onScanStart доступна в прошивках начиная с версии 2.1.3923.
 +
 
 +
== readRegister == <!--T:22-->
 
Функция '''readRegister''' должна произвести чтение указанного регистра.
 
Функция '''readRegister''' должна произвести чтение указанного регистра.
  
В случае успешного чтения функция '''readRegister''' должна вернуть массив байт, длина которого соответствует указанному типу данных (1, 2 или 4). В случае неудачи необходимо вернуть число ноль.
+
<!--T:23-->
 +
В случае успешного чтения функция '''readRegister''' должна вернуть массив байт, длина которого соответствует указанному типу данных (1, 2 или 4) или же число. В случае неудачи необходимо вернуть false.
  
 +
<!--T:24-->
 
Ей в качестве аргументов передаются три параметра:
 
Ей в качестве аргументов передаются три параметра:
 
* reg - таблица (структура) с параметрами регистра
 
* reg - таблица (структура) с параметрами регистра
Строка 72: Строка 102:
 
* unitId – ID устройства на шине или прочий ID. Примером может быть Slave ID в ModBus RTU или Unit ID в ModBus TCP.
 
* unitId – ID устройства на шине или прочий ID. Примером может быть Slave ID в ModBus RTU или Unit ID в ModBus TCP.
  
 +
<!--T:25-->
 
В структуре '''reg''' есть такие атрибуты:
 
В структуре '''reg''' есть такие атрибуты:
 
* internalAddr - пересчитанный адрес регистра. Это число, пересчитанное из указанной системы счисления с прибавлением к нему shift.
 
* internalAddr - пересчитанный адрес регистра. Это число, пересчитанное из указанной системы счисления с прибавлением к нему shift.
Строка 77: Строка 108:
 
* dataType – тип данных, который пользователь указал для регистра. 0 = Bit, 1 = Byte, 2 = Word, 3 = Double Word, 4 = UnixTime
 
* dataType – тип данных, который пользователь указал для регистра. 0 = Bit, 1 = Byte, 2 = Word, 3 = Double Word, 4 = UnixTime
  
 +
<!--T:26-->
 
В структуре '''device''' есть такие атрибуты:
 
В структуре '''device''' есть такие атрибуты:
 
* shift - значение shift из соответствующей строки createDevices
 
* shift - значение shift из соответствующей строки createDevices
Строка 82: Строка 114:
 
* xtraFields – значение xtraFields из соответствующей строки createDevices
 
* xtraFields – значение xtraFields из соответствующей строки createDevices
  
 +
<!--T:27-->
 
Эти параметры передаются для того, что бы можно было правильно и полноценно составить запрос согласно протокола.
 
Эти параметры передаются для того, что бы можно было правильно и полноценно составить запрос согласно протокола.
  
Для передачи запроса устройству используется функция '''sendBytes'''. Для чтения ответа - '''readBytes'''.
+
<!--T:28-->
 +
Для передачи запроса устройству используются функции '''sendBytes''' и '''sendString'''. Для чтения ответа - '''readBytes''', '''readString'''.
  
На вход '''sendBytes''' принимает таблицу (массив) байт. Результатом будет 0 в случае успеха и 1 в случае ошибки.  
+
<!--T:29-->
 +
На вход '''sendBytes''' принимает таблицу (массив) байт. Результатом будет true в случае успеха и false в случае ошибки. <br>
 +
На вход '''sendString''' принимает строку. Результатом будет true в случае успеха и false в случае ошибки. <br>
 +
На вход '''readBytes''' принимает количество байт, которое необходимо прочитать. Результатом будет таблица (массив) байт в случае успеха и false в случае ошибки.<br>
 +
На вход '''readString''' принимает количество байт, которое необходимо прочитать. Результатом будет строка в случае успеха и false в случае ошибки.<br>
  
На вход '''readBytes''' принимает количество байт, которое необходимо прочитать. Результатом будет таблица (массив) байт в случае успеха и 1 в случае ошибки.
+
<!--T:30-->
 
+
Если необходимо закрыть соединение (например, в случае множественных ошибок), то можно вызвать процедуру '''closeConnection'''.
Если необходимо закрыть соединение (например, в случае множественных оштбок), то можно вызвать процедуру '''closeConnection'''.
+
  
 +
<!--T:31-->
 
Если необходимо сделать паузу, то можно вызвать функцию '''sleep'''. Ее единственным аргументом должно быть время в микросекундах. Пример: sleep(20000); - произойдет пауза  20 миллисекунд.
 
Если необходимо сделать паузу, то можно вызвать функцию '''sleep'''. Ее единственным аргументом должно быть время в микросекундах. Пример: sleep(20000); - произойдет пауза  20 миллисекунд.
  
 +
<!--T:32-->
 
Для работы с битами можно использовать библиотеку bitop [http://bitop.luajit.org/].
 
Для работы с битами можно использовать библиотеку bitop [http://bitop.luajit.org/].
  
 +
<!--T:33-->
 
Для отладки и вывода диагностических сообщений можно использовать процедуры ERROR, INFO, DEBUG и TRACE которые доступны и в обычных сценариях Lua [http://wiki.webhmi.com.ua/index.php/Сценарии_LUA].
 
Для отладки и вывода диагностических сообщений можно использовать процедуры ERROR, INFO, DEBUG и TRACE которые доступны и в обычных сценариях Lua [http://wiki.webhmi.com.ua/index.php/Сценарии_LUA].
  
 +
<!--T:34-->
 
Что бы лучше понять как это все работает давайте рассмотрим пример функции readRegister для протокола ModBus TCP:
 
Что бы лучше понять как это все работает давайте рассмотрим пример функции readRegister для протокола ModBus TCP:
  
<pre>
+
<!--T:35-->
 
+
<syntaxhighlight lang="lua">
 
local transId = 0;  
 
local transId = 0;  
 
local errorCount = 0;
 
local errorCount = 0;
  
 +
<!--T:36-->
 
function readRegister (reg, device, unitId)
 
function readRegister (reg, device, unitId)
  
   local request = {};
+
   <!--T:37-->
 +
local request = {};
 
    
 
    
 
   -- transaction ID
 
   -- transaction ID
Строка 115: Строка 158:
 
   request[2] = bit.band(transId, 255);
 
   request[2] = bit.band(transId, 255);
  
   -- protocol ID
+
   <!--T:38-->
 +
-- protocol ID
 
   request[3] = 0;
 
   request[3] = 0;
 
   request[4] = 0;
 
   request[4] = 0;
Строка 133: Строка 177:
 
   request[10] = bit.band(reg.internalAddr, 255);
 
   request[10] = bit.band(reg.internalAddr, 255);
  
   -- count of registers
+
   <!--T:39-->
 +
-- count of registers
 
   request[11] = 0;
 
   request[11] = 0;
 
   request[12] = 1;
 
   request[12] = 1;
Строка 143: Строка 188:
 
   local res = sendBytes(request);
 
   local res = sendBytes(request);
 
    
 
    
   if (res ~= 0) then
+
   if (res == false) then
 
       DEBUG("Can't send bytes");
 
       DEBUG("Can't send bytes");
       return 0;
+
       return false;
 
   end
 
   end
  
   local response = {};
+
   <!--T:40-->
 +
local response = {};
 
   -- read MBAP Header
 
   -- read MBAP Header
 
   response = readBytes(7);
 
   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;
 
   res = #response;
  
   if (res ~= 7) then
+
   <!--T:41-->
 +
if (res ~= 7) then
 
       errorCount = errorCount + 1;
 
       errorCount = errorCount + 1;
 
       if (errorCount > 3) then
 
       if (errorCount > 3) then
Строка 160: Строка 216:
 
       end
 
       end
 
       DEBUG("Can't read MBAP");
 
       DEBUG("Can't read MBAP");
       return 0;
+
       return false;
 
   end
 
   end
  
   if (response[1] ~= request[1] or response[2] ~= request[2]) then
+
   <!--T:42-->
 +
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]));
 
       ERROR("Wrong transaction ID. Got #" .. (response[1] * 256 + response[2]) .. " but expected #" .. (request[1] * 256 + request[2]));
       return 0;
+
       return false;
 
   end
 
   end
 
        
 
        
 
   if (response[3] ~= request[3] or response[4] ~= request[4]) then
 
   if (response[3] ~= request[3] or response[4] ~= request[4]) then
 
       ERROR("Wrong protocol");
 
       ERROR("Wrong protocol");
       return 0;
+
       return false;
 
   end
 
   end
  
   if (response[7] ~= request[7]) then
+
   <!--T:43-->
 +
if (response[7] ~= request[7]) then
 
       ERROR("Wrong UnitID in response");
 
       ERROR("Wrong UnitID in response");
       return 0;
+
       return false;
 
   end
 
   end
  
   local length = response[5] * 256 + response[6];
+
   <!--T:44-->
 +
local length = response[5] * 256 + response[6];
  
   if (length < 1) then
+
   <!--T:45-->
 +
if (length < 1) then
 
       ERROR("Wrong length in response");
 
       ERROR("Wrong length in response");
       return 0;
+
       return false;
 
   end
 
   end
 
    
 
    
Строка 188: Строка 248:
 
   -- read MBAP Header
 
   -- read MBAP Header
  
   responsePDU = readBytes(length - 1);
+
   <!--T:46-->
 +
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;
 
   res = #responsePDU;
  
   if (responsePDU[1] ~= request[8]) then
+
   <!--T:47-->
 +
if (responsePDU[1] ~= request[8]) then
 
       ERROR("Wrong function in response");
 
       ERROR("Wrong function in response");
       return 0;
+
       return false;
 
   end
 
   end
  
   local dataLength = responsePDU[2];
+
   <!--T:48-->
 +
local dataLength = responsePDU[2];
 
   if (dataLength ~= length - 3) then
 
   if (dataLength ~= length - 3) then
 
       ERROR("Wrong length in PDU");
 
       ERROR("Wrong length in PDU");
       return 0;
+
       return false;
 
   end
 
   end
 
    
 
    
 
   local result = {};
 
   local result = {};
  
   if (dataLength >= 1) then
+
   <!--T:49-->
 +
if (dataLength >= 1) then
 
       for i = 1, dataLength do
 
       for i = 1, dataLength do
 
           result[i] = responsePDU[2 + i];
 
           result[i] = responsePDU[2 + i];
Строка 210: Строка 283:
 
   end
 
   end
  
   return result;
+
   <!--T:50-->
 +
return result;
 
end
 
end
</pre>
+
</syntaxhighlight>
 +
 
 +
== writeRegister == <!--T:51-->
 +
Функция '''writeRegister''' должна произвести запись нового значения в указанный регистр. При успешной записи она должна вернуть true. В случае ошибки - false.
 +
 
 +
<!--T:52-->
 +
Ей передаются все те же параметры, что и для readRegister, а также дополнительно четвертый параметр - новое значение.
 +
 
 +
<!--T:53-->
 +
Функция '''writeRegister''' может использовать те же способы записи и чтения массивов байт в порт.
 +
 
 +
<!--T:54-->
 +
Давайте рассмотрим пример этой функции для протокола ModBus TCP:
 +
 
 +
<!--T:55-->
 +
<syntaxhighlight lang="lua">
 +
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;
 +
 
 +
    <!--T:56-->
 +
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);
 +
 
 +
        <!--T:57-->
 +
-- count of registers
 +
        request[11] = 0;
 +
        request[12] = 2;
 +
       
 +
        -- bytes with data
 +
        request[13] = 4;
 +
 
 +
        <!--T:58-->
 +
-- 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
 +
 
 +
        <!--T:59-->
 +
local response = {};
 +
 
 +
        <!--T:60-->
 +
response = readBytes(7);
 +
        if (response == false) then
 +
          DEBUG("Can't read response");
 +
          return false;
 +
        end
 +
        res = #response;
 +
 
 +
        <!--T:61-->
 +
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];
 +
 
 +
        <!--T:62-->
 +
if (length < 1) then
 +
          ERROR("Wrong length in response");
 +
          return false;
 +
        end
 +
       
 +
        local responsePDU = {};
 +
 
 +
        <!--T:63-->
 +
responsePDU = readBytes(length - 1);
 +
        if (responsePDU == false) then
 +
          DEBUG("Can't read response PDU");
 +
          return false;
 +
        end
 +
 
 +
        <!--T:64-->
 +
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
 +
 
 +
        <!--T:65-->
 +
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);
 +
 
 +
        <!--T:66-->
 +
local val = newValue;
 +
        if (reg.dataType == 0) then
 +
            if (val > 0) then
 +
                val = 255*256;
 +
            else
 +
                val = 0;
 +
            end
 +
        end
 +
 
 +
        <!--T:67-->
 +
-- value of registers
 +
        request[11] = bit.band(bit.rshift(val, 8), 255);
 +
        request[12] = bit.band(val, 255);
 +
 
 +
       
 +
        <!--T:68-->
 +
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
 +
 
 +
        <!--T:69-->
 +
res = #response;
 +
 
 +
        <!--T:70-->
 +
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
 +
 
 +
    <!--T:71-->
 +
end
 +
 
 +
    <!--T:72-->
 +
return true;
 +
end
 +
</syntaxhighlight>
 +
 
 +
== Примеры протоколов == <!--T:73-->
 +
В качестве примера мы реализовали несколько протоколов:
 +
*[http://wiki.webhmi.com.ua/index.php/Пример_протокола_ModBus_TCP ModBus TCP]
 +
*[http://wiki.webhmi.com.ua/index.php/Пример_протокола_ModBus_ASCII ModBus ASCII].
 +
*[http://wiki.webhmi.com.ua/index.php/Modbus_RTU_%D0%B2_%D0%B2%D0%B8%D0%B4%D0%B5_custom_protocol Пример реализации протокола ModbusRTU].
 +
На странице подключения к [http://wiki.webhmi.com.ua/index.php/%D0%A2%D0%B5%D0%BD%D0%B7%D0%BE%D0%B4200 тензометрическому контроллеру Тензод200] также есть пример пользовательского протокола.
 +
</translate>

Текущая версия на 14:47, 30 мая 2018

Другие языки:
English • ‎русский

Существует большое количество различных устройств автоматизации с нестандартными протоколами обмена данными. Для решения проблемы сбора данных с таких устройств в 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.
Custom-protocol-button.png

Вы попадете на страницу управления протоколами. В данном примере мы видим два демонстрационных протокола – ModBus TCP Demo и ModBus ASCII Demo:
Custom-protocol-list.png

Давайте посмотрим на страницу редактирования протокола ModBus TCP Demo:
Custom-protocol-demo.png

Протоколу можно задать:

  • название, описание
  • тип (TCP/IP или Serial)
  • сетевой порт по умолчанию (только для TCP)
  • регулярное выражение для проверки допустимости вводимых адресов
  • сообщение об ошибке, которе будет выводится при вводе неверного адреса регистра
  • код с программой для обработки протокола

Регулярное выражение должно обеспечить проверку правильности адреса регистра на странице редактирования регистров (при выборе этого протокола). Пример:
Custom-protocol-validate.png

Также мы видим удобный редактор кода. В нем поддерживается форматирование, подсветка и валидация синтаксиса. Так что писать код удобно.
Если в коде допущена синтаксическая ошибка то в соответствующей строке появится красный крестик. Что бы увидеть детальное сообщение об ошибке достаточно навести на него курсор мышки:
Custom-protocol-error.png

После создания протокола он появится в выпадающем списке доступных PLC models на странице создания новых Connections и с ним можно будет работать так же как и с обычным встроенным протоколом.
Custom-protocol-select.png

Необходимые функции

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

Примеры протоколов

В качестве примера мы реализовали несколько протоколов:

На странице подключения к тензометрическому контроллеру Тензод200 также есть пример пользовательского протокола.