Пользовательские протоколы

Материал из WebHMI Wiki
Перейти к: навигация, поиск
Другие языки:
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 также есть пример пользовательского протокола.