User Tools

Site Tools


Custom protocols

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 . 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. You can easily get acquainted with the syntax on a convenient site . 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” the Setup → Registers → Tools sub-menu.

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 String-prefix, it is this part in the address that will determine the further processing of the read / write of this register
shift This value 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.

Three parameters are passed to it as arguments:

reg - Table (structure) with register parameters device - Table (structure) with register type parameters (that were defined in createDevices) unitId – device ID for the bus or other ID. For instance, Slave ID in ModBus RTU or Unit ID in ModBus TCP. There are such attributes in the reg structure:

internalAddr - Recalculated register address. This number is recalculated from the specified number system with shift added to it. addr - The original address of the register that the user entered. dataType – The type of data that the user specified for the register. 0 = Bit, 1 = Byte, 2 = Word, 3 = Double Word, 4 = UnixTime There are such attributes in the device structure:

shift - The shift value from the corresponding row in createDevices base - base value from the corresponding row in createDevices xtraFields – xtraFields value from the corresponing row in createDevices These parameters are passed in order to be able to correctly and fully compose a request according to the protocol.

Для передачи запроса устройству используются функции 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

The writeRegister function should write a new value to the specified register. If the record is successful, it should return true. In case of an error, false.

Ей передаются все те же параметры, что и для readRegister, а также дополнительно четвертый параметр - новое значение. It has all the same arguments as for readRegister, as well as an additional fourth argument, a new value.

The writeRegister function may use the same methods of reading from and writing bytes to the port.

Let's have a look at the example of this function for ModBus TCP protocol:

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

The examples of custom protocols

As an example we've made (partually) several protocols:


Page Tools