User Tools

Site Tools


custom_protocols

This is an old revision of the document!


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:

Modbus RTU in custom protocol version

An example of custom protocol for Modbus RTU.

  • Type: Serial Port
  • Address validation: ^(C[0-9]+)$|^(DI[0-9]+)$|^(HR[0-9]+)$|^(IR[0-9]+)$
  • Validation error message: Invalid register address. Valid ModBus addresses are Cxxx, DIxxx, IRxxx, HRxxx.

Code:

-- MODBUS RTU Demo Driver
 
function createDevices ()
                                                     -- read FC write FC
  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}})
  addDevice({name = "IR", shift = 0, base = 10, xtraFields = {4, 0}})
 
end
 
local errorCount = 0
 
-- template 
local request = {1, 2,      -- slaveId FC
                 3,  4,  -- addr high lo 
                 5,  6, -- count hi lo 
                 0,  0      -- crc high lo 
                }     
 
EXCEPTIONS = {"Illegal Function",     "Illegal Data Address",
               "Illegal Data Value",  "Slave Device Failure",
               "Acknowledge",         "Slave Device Busy",
               "Negative Acknowledge", "Memory Parity Error",
               "Gateway Path Unavailable", "Gateway Target Device Failed to Respond"
              }
 
function readRegister (reg, device, unitId)
                             --- FORMING REQUEST ----------- 
      -- slave address
  request[1] = unitId;
 
  -- function code
  request[2] = device.xtraFields[1]
 
  -- address of register
  request[3] = GetHiByte(reg.internalAddr)
  request[4] = GetLoByte(reg.internalAddr)
 
  -- count of registers
  local count = 1
      if (reg.dataType == 3) then -- double word
        count = 2
      end
  request[5] = GetHiByte(count)
  request[6] = GetLoByte(count)
 
   -- CRC
  local crc = GetCRC(request, 2)
  local crcLo,crcHi = 0,0 -- will be used below too 
  crcLo = GetLoByte(crc) ; request[7] = crcLo
  crcHi = GetHiByte(crc) ; request[8] = crcHi
 
                           -- SENDING REQUEST 
  if not (sendBytes(request)) then
      DEBUG("Can't send bytes")
      return false
  end
                           -- RECEIVING REPLY --- 
  local respHead, respData, respCRC = {}, {}, {}
 
    -- read Header with length 
  respHead = readBytes(3)
  if (respHead == false) then
      DEBUG("Can't read response")
      return false
  end
 
  if (respHead[1] ~= request[1]) then
      ERROR("Wrong slaveID in response!")
      return false
  end
 
  if (respHead[2] ~= request[2]) then
      if (respHead[2] >= 0x81) then 
          ERROR("EXCEPTION: "..EXCEPTIONS[bit.band(respHead[2], 0x0F)])
          readBytes(2) -- read till the end 
      else       
          ERROR("Wrong Func Code in response")
      end 
   return false;
  end
 
  local resp_Lentgh = respHead[3]; 
        respData = readBytes(resp_Lentgh)
  if (respData == false) then
      DEBUG("Can't read response");
      return false;
  end
  -- check CRC in reply 
  local tmpResponseTab = {}
    for i,v in ipairs(respHead) do 
        table.insert(tmpResponseTab, v) 
    end 
    for i,v in ipairs(respData) do 
        table.insert(tmpResponseTab, v) 
    end 
 
  crc = GetCRC(tmpResponseTab, 0)
  crcLo = GetLoByte(crc)
  crcHi = GetHiByte(crc)
 
  respCRC = readBytes(2)
  if (respCRC == false) then
      DEBUG("Can't read response");
      return false;
  end
  if (respCRC[1] ~= crcLo) or (respCRC[2] ~= crcHi) then 
      DEBUG("Wrong CRC in reply! "..string.format("%X", crcLo).." "..string.format("%X", crcHi));
      return false;
  end 
 
  if (device.name == "DI") or (device.name == "C")  then 
      if (resp_Lentgh ~= count) then
          ERROR("Wrong length in response");
          return false;
      end
  else 
      if (resp_Lentgh ~= count*2) then
          ERROR("Wrong length in response");
          return false;
      end
  end 
                                  -- RETURN DATA --  
    return GetHexFromTable(respData)
 
end -- readRegister
 
 
 
function writeRegister (reg, device, unitId, newValue)
    -- for read-only don't write 
                                    DEBUG("Going to write this value "..newValue)
    if device.name == "IR" or device.name == "DI" then 
         ERROR("Can't write these type of registers (" .. device.name .. ")")
        return true 
    end 
    local wrRequest = {};
 
    -- slave address
    wrRequest[1] = unitId;   
 
    -- function code
    wrRequest[2] = device.xtraFields[2]
 
    -- address of register
    wrRequest[3] = GetHiByte(reg.internalAddr)
    wrRequest[4] = GetLoByte(reg.internalAddr)
 
    local dataTable = GetDataAsTable(newValue, device.dataType)
 
        local coilsTmp = 0 
        if (device.name == "C") then 
            coilsTmp = dataTable[2] * 0xFF 
            dataTable[2] = dataTable[1]
            dataTable[1] = coilsTmp
        end 
    DEBUG("#dataTable now "..#dataTable)
 
    wrRequest[5] = dataTable[1]
    wrRequest[6] = dataTable[2]
 
    -- CRC
    local crc, crcLo, crcHi = 0,0,0
    crc = GetCRC(wrRequest, 0)
    crcLo = GetLoByte(crc)
    crcHi = GetHiByte(crc) 
    wrRequest[7] = crcLo
    wrRequest[8] = crcHi 
 
    DEBUG("Going to send this packet "..table.concat(wrRequest))    
    local res = sendBytes(wrRequest);
    if (res == false) then
        DEBUG("Can't send request");
        return false;
    end
 
    -- читаем ответ 
    res = readBytes(8) 
 
    if (res == false) then
        DEBUG("Can't receive reply")
        return false
    end 
 
    if (table.concat(res) ~= table.concat(wrRequest)) then 
        DEBUG("Response does not match!")
        return false
    end 
 
    if (#dataTable == 4) then 
        -- DWORD
        -- repeat steps for 2nd Word 
        wrRequest[3] = GetHiByte(reg.internalAddr + 1)
        wrRequest[4] = GetLoByte(reg.internalAddr + 1)
        wrRequest[5] = dataTable[3]
        wrRequest[6] = dataTable[4]
        crc, crcLo, crcHi = GetCRC(wrRequest, 0), GetLoByte(crc) , GetHiByte(crc) 
        wrRequest[7] = crcLo
        wrRequest[8] = crcHi 
        res = sendBytes(wrRequest);
        if (res == false) then
            DEBUG("Can't send request");
            return false;
        end
    end 
    return true
end
 
 
-- Calculating CRC for RTU 
function GetCRC(req, offset) 
 
  local crc = 0xffff
  local mask = 0
 
  -- iterate bytes
for i=1,#req-offset do
      crc = bit.bxor(crc,req[i])
      -- iterate bits in byte 
      for j=1,8 do
          mask = bit.band(crc,0x0001)
          if mask == 0x0001 then
              crc = bit.rshift(crc,1)
              crc = bit.bxor(crc,0xA001)
          else
              crc = bit.rshift(crc,1)
          end
 
      end 
end 
     return crc
end
 
function GetHiByte(c)
                                   DEBUG("Going to get high byte from "..c)
   return bit.rshift(c,8)
end
 
function GetLoByte(input_val) 
                                    DEBUG("Going to get low byte from "..input_val)
    return bit.band(input_val,0xFF)
end
 
function GetHexFromTable(inputTab)
    -- get hex and concat it to number via string operatoin 
                                    DEBUG("entered GetHexFromTable with table - "..table.concat(inputTab))
    local numberAs_String = "" 
    local tmpStr = "" 
 
        for i,v in pairs(inputTab) do 
            tmpStr = string.format("%X",v)
            if (#tmpStr == 1) then 
                tmpStr = "0"..tmpStr
            end 
            numberAs_String = numberAs_String..tmpStr
        end 
                                DEBUG("number is "..numberAs_String.." decimal = "..numberAs_String)    
    return tonumber(numberAs_String, 16)
end
 
function GetDataAsTable(value, datatype)
 
    local highWord, lowWord, tmpTable = 0, 0, {}
 
    if (datatype ~= 3) then 
        DEBUG("getdataastable - going to process value  "..value)
        tmpTable[1] = GetHiByte(value) ; DEBUG(tmpTable[1])
        tmpTable[2] = GetLoByte(value) ; DEBUG(tmpTable[2])
    else 
        highWord = bit.rshift(value,16)
        lowWord = bit.band(c,0xFFFF)
 
        tmpTable[1] = GetHiByte(highWord)
        tmpTable[2] = GetLoByte(highWord)
        tmpTable[3] = GetHiByte(lowWord)
        tmpTable[4] = GetLoByte(lowWord)
    end 
    return tmpTable
end
custom_protocols.1547036362.txt.gz · Last modified: 2019/01/09 12:19 by emozolyak

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki