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

Материал из WebHMI Wiki
Перейти к: навигация, поиск
(Новая страница: «== readRegister == The function '''readRegister''' should read the specified register.»)
(Новая страница: «In case of successful reading, the function '''readRegister '''should return an array of bytes, with length corresponding to the specified data type (1, 2 or 4) o…»)
Строка 73: Строка 73:
 
The function '''readRegister''' should read the specified register.
 
The function '''readRegister''' should read the specified register.
  
В случае успешного чтения функция '''readRegister''' должна вернуть массив байт, длина которого соответствует указанному типу данных (1, 2 или 4) или же число. В случае неудачи необходимо вернуть false.
+
In case of successful reading, the function '''readRegister '''should return an array of bytes, with length corresponding to the specified data type (1, 2 or 4) or the number. In case of failure, you must return false.
  
 
Ей в качестве аргументов передаются три параметра:
 
Ей в качестве аргументов передаются три параметра:

Версия 14:29, 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.
Custom-protocol-button.png

You will be taken to the protocol management page. In this example, we see two demonstration protocols - ModBus TCP Demo and ModBus ASCII Demo:
Custom-protocol-list.png

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:
Custom-protocol-validate.png

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:
Custom-protocol-error.png

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 – String-prefix, it is this part in the address that will determine the further processing of the read / write of this register
  • shift – this will be added to the value of the register address. I.e. it can be done so that the register with address D30 is converted to (with shift = 1000) D1030, and D33 to D1033, etc.
  • base – address system. Some devices use addresses in octal or hexadecimal number systems
  • xtraFields – A set of additional parameters (maximum 5 pieces), will be transferred to the function read / write in xtraFields table.

onScanStart

The onScanStart procedure is called every time a new scan starts. It can be useful for protocols where one array reads an array of values ​​for several registers. For such protocols, you can cache the result of the query and return values ​​from the cache. You can reset the cache in the onScanStart procedure.

onScanStart Available in firmware since version 2.1.3923.

readRegister

The function readRegister should read the specified register.

In case of successful reading, the function readRegister should return an array of bytes, with length corresponding to the specified data type (1, 2 or 4) or the number. In case of failure, you must return 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 также есть пример пользовательского протокола.