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

Материал из WebHMI Wiki
Перейти к: навигация, поиск
(readRegister)
(writeRegister)
Строка 301: Строка 301:
  
 
         response = readBytes(7);
 
         response = readBytes(7);
 +
        if (response == false) then
 +
          DEBUG("Can't read response");
 +
          return false;
 +
        end
 
         res = #response;
 
         res = #response;
  
Строка 333: Строка 337:
  
 
         responsePDU = readBytes(length - 1);
 
         responsePDU = readBytes(length - 1);
 +
        if (responsePDU == false) then
 +
          DEBUG("Can't read response PDU");
 +
          return false;
 +
        end
 +
 
         res = #responsePDU;
 
         res = #responsePDU;
 
          
 
          
Строка 382: Строка 391:
 
         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 0;
Строка 391: Строка 400:
 
          
 
          
 
         response = readBytes(requestLen);
 
         response = readBytes(requestLen);
 +
        if (response == false) then
 +
          DEBUG("Can't read response");
 +
          return false;
 +
        end
 +
 
         res = #response;
 
         res = #response;
  
Строка 410: Строка 424:
 
end
 
end
 
</syntaxhighlight>
 
</syntaxhighlight>
 
  
 
== Примеры протоколов ==
 
== Примеры протоколов ==
 
В качестве примера мы реализовали (частично) два протокола: [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_TCP ModBus TCP] и [http://wiki.webhmi.com.ua/index.php/Пример_протокола_ModBus_ASCII ModBus ASCII].

Версия 17:08, 29 января 2016

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


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 ~= 0) 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.