===== 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 = "HRI", shift = 0, base = 10, xtraFields = {3, 6, 1, 0}}) -- uint 0 inverse addDevice({name = "HRIF", shift = 0, base = 10, xtraFields = {3, 6, 1, 5}}) -- float inverse (maybe 7 ? https://docs.webhmi.com.ua/access_via_api?s[]=float) addDevice({name = "HRF", shift = 0, base = 10, xtraFields = {3, 6, 0, 5}}) -- float 5 addDevice({name = "IR", shift = 0, base = 10, xtraFields = {4, 0}}) end local errorCount = 0 SLAVE_ADDR = 1 FUNC_CODE = 2 REG_ADDR_HI = 3 REG_ADDR_LO = 4 DATA_LEN_HI = 5; DATA_LEN_LO = 6 CRC_POS_LO = 7 -- CRC LITTLE-ENDIAN (lowest comes first!) CRC_POS_HI = 8 CRC_BIG_ENDIAN = false if CRC_BIG_ENDIAN then CRC_POS_HI = 7 CRC_POS_LO = 8 -- CRC BIG-ENDIAN (biggest comes first) end -- template local request = {SLAVE_ADDR, FUNC_CODE, REG_ADDR_HI, REG_ADDR_LO, DATA_LEN_HI, DATA_LEN_LO, CRC_POS_LO, CRC_POS_HI } 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" } -- dataType – The type of data that the user specified for the register. 0 = Bit, 1 = Byte, 2 = Word, 3 = Double Word, 4 = UnixTime DATATYPE = {DW = 3} DATALEN = {DW = 4, WORD=2, BYTE=1,BIT=1} FORMAT = {UINT = 0, FLOAT_32=5} table.unpack = unpack function readRegister (reg, device, unitId) --------------------------------------------- FORMING REQUEST ---------------------------------------------- request[SLAVE_ADDR] = unitId request[FUNC_CODE] = device.xtraFields[1] request[REG_ADDR_HI] = GetHiByte(reg.internalAddr) request[REG_ADDR_LO] = GetLoByte(reg.internalAddr) count = reg.dataType == DATATYPE.DW and 2 or 1 request[DATA_LEN_HI] = GetHiByte(count) request[DATA_LEN_LO] = GetLoByte(count) local crc = GetCRC(request, 2) local crcLo, crcHi -- will be used below too crcLo = GetLoByte(crc) ; request[CRC_POS_LO] = crcLo crcHi = GetHiByte(crc) ; request[CRC_POS_HI] = 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[SLAVE_ADDR] ~= request[SLAVE_ADDR]) then ERROR("Wrong slaveID in response!") return false end if (respHead[FUNC_CODE] ~= request[FUNC_CODE]) then if (respHead[FUNC_CODE] >= 0x81) then ERROR("EXCEPTION: "..EXCEPTIONS[bit.band(respHead[FUNC_CODE], 0x0F)]) readBytes(2) -- read till the end else ERROR("Wrong Func Code in response. In response = 0d" .. tostring( respHead[FUNC_CODE]) .. ', but expected = 0d' .. tostring(request[FUNC_CODE])) 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 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 -- check CRC in reply 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 ---------------------------------------------- local inversion = device.xtraFields[3] return GetHexFromTable(respData, not((inversion or 0) == 1)) end -- readRegister function GetHexFromTable(inputTab, _invert) if #inputTab > 2 then local invert = _invert or false if invert then invertedInputTab = {} half1, half2 = SplitInHalf(inputTab) for i, v in pairs(half2) do table.insert(invertedInputTab, v) end for i, v in pairs(half1) do table.insert(invertedInputTab, v) end inputTab = invertedInputTab; end local toReturn = {} for i,v in pairs(inputTab) do toReturn[i]=tonumber(string.format("%X",v),16) end return toReturn else -- get hex and concat it to number via string operatoin -- TRACE("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 -- TRACE("GetHexFromTable: decimal number is "..table.concat(inputTab).." hexadecimal = ".. numberAs_String) return tonumber(numberAs_String, 16) end end function writeRegister (reg, device, unitId, newValue) local inversion = not(device.xtraFields[3] == 1 or false) reg.value_format = device.xtraFields[4] if reg.dataType == DATATYPE.DW and reg.value_format == FORMAT.FLOAT_32 then DEBUG("Going to write this value ".. (decodeIEEE754FloatToLua(newValue) or 'nil') .. ', \nuint(dec): ' .. '0d'..newValue .. ', binary: ' .. '0b'..table.concat(toBits(newValue,32)) .. ', uint(hex): ' .. string.format("0x%x", newValue)) -- TRACE("newValue = 0d" .. newValue .. ' hex = 0x' .. string.format("%X", newValue)) else DEBUG("Going to write this value "..newValue) end if device.name == "IR" or device.name == "DI" then ERROR("Can't write these type of registers (" .. device.name .. ")") return true end local wrRequest = {}; wrRequest[SLAVE_ADDR] = unitId; wrRequest[FUNC_CODE] = device.xtraFields[2] wrRequest[REG_ADDR_HI] = GetHiByte(reg.internalAddr) wrRequest[REG_ADDR_LO] = GetLoByte(reg.internalAddr) local dataTable = GetDataAsTable(newValue, reg.dataType) if reg.dataType == DATATYPE.DW and inversion then dataTableTmp = {} dataTableTmp[1]=dataTable[3]; dataTableTmp[2]=dataTable[4] dataTableTmp[3]=dataTable[1]; dataTableTmp[4]=dataTable[2] dataTable=dataTableTmp end -- local coilsTmp = 0 if (device.name == "C") then local coilsTmp = dataTable[2] * 0xFF dataTable[2] = dataTable[1] dataTable[1] = coilsTmp end wrRequest[DATA_LEN_HI] = dataTable[1] wrRequest[DATA_LEN_LO] = dataTable[2] local crc, crcLo, crcHi = 0,0,0 crc = GetCRC(wrRequest, 0) crcLo = GetLoByte(crc) crcHi = GetHiByte(crc) wrRequest[CRC_POS_LO] = crcLo wrRequest[CRC_POS_HI] = crcHi local res = sendBytes(wrRequest); if (res == false) then DEBUG("Can't send request"); return false; end -- read response 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 == DATALEN.DW) then -- TRACE("DWORD CASE!!!") -- repeat steps for 2nd Word wrRequest[REG_ADDR_HI] = GetHiByte(reg.internalAddr + 1) wrRequest[REG_ADDR_LO] = GetLoByte(reg.internalAddr + 1) wrRequest[DATA_LEN_HI] = dataTable[3] wrRequest[DATA_LEN_LO] = dataTable[4] crc = GetCRC(wrRequest, 2); crcLo = GetLoByte(crc); crcHi = GetHiByte(crc) -- offset = 2 for omit crc from previous word wrRequest[CRC_POS_LO] = crcLo wrRequest[CRC_POS_HI] = crcHi res = sendBytes(wrRequest); if (res == false) then DEBUG("Can't send request"); return false; end end return true end -- Calculating CRC16 for MODBUS 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) assert(c < 65536, "This is not a two bytes!") -- DEBUG("Going to get high byte from "..c .. ' 0x'.. string.format("%04X", c)) return bit.rshift(c,8) end function GetLoByte(c) assert(c < 65536, "This is not a two bytes!") -- DEBUG("Going to get low byte from "..c .. ' 0x'.. string.format("%04X", c)) return bit.band(c,0xFF) end function SplitInHalf(full) local h1, h2 = {}, {} local half = math.ceil(#full/2) for i = 1, half do table.insert(h1, full[i]) end for i = half+1, #full do table.insert(h2, full[i]) end return h1,h2 end function GetDataAsTable(value, datatype) local highWord, lowWord, tmpTable = 0, 0, {} if (datatype ~= DATATYPE.DW) then DEBUG("GetDataAsTable - going to process value "..value) tmpTable[1] = GetHiByte(value) ; DEBUG('tmpTable[1] = ' .. tmpTable[1]) tmpTable[2] = GetLoByte(value) ; DEBUG('tmpTable[2] = ' .. tmpTable[2]) else highWord = bit.rshift(value, 16) lowWord = bit.band (value, 0xFFFF) tmpTable[1] = GetHiByte(highWord) tmpTable[2] = GetLoByte(highWord) tmpTable[3] = GetHiByte(lowWord) tmpTable[4] = GetLoByte(lowWord) DEBUG("GetDataAsTable DW = - " .. table.concat(tmpTable)) end return tmpTable end function toBits(num,bits) -- returns a table of bits, most significant first. bits = bits or math.max(1, select(2, math.frexp(num))) local t = {} -- will contain the bits for b = bits, 1, -1 do t[b] = math.fmod(num, 2) num = math.floor((num - t[b]) / 2) end return t end function Bin2Hex(s) assert (type(s) == "string", "binary as string expected") -- s -> binary string local bin2hex = { ["0000"] = "0", ["0001"] = "1", ["0010"] = "2", ["0011"] = "3", ["0100"] = "4", ["0101"] = "5", ["0110"] = "6", ["0111"] = "7", ["1000"] = "8", ["1001"] = "9", ["1010"] = "A", ["1011"] = "B", ["1100"] = "C", ["1101"] = "D", ["1110"] = "E", ["1111"] = "F" } tabBytes={} local l = 0 local h, b = "", "" local rem l = string.len(s) rem = l % 4 l = l-1 -- need to prepend zeros to eliminate mod 4 if (rem > 0) then s = string.rep("0", 4 - rem)..s end for i = 1, l, 4 do b = string.sub(s, i, i+3) table.insert(tabBytes,bin2hex[b]) if not b then ERROR("bin2hex b is nil") end TRACE("bin2hex b = " .. tostring(b)) h = h..bin2hex[b] end return h, tabBytes end function hex2dec(hexstr) return tonumber(hexstr,16) end function decodeIEEE754FloatToLua(input) sign = input < 0 and -1 or 1 input = math.abs(input) bitsTable = toBits(input,32) exponentTable={table.unpack(bitsTable, 2,9)} mantissaTable={table.unpack(bitsTable,10)} mantissaStr, manstissaTabBytes = Bin2Hex(table.concat(mantissaTable)) mantissaNum = hex2dec(table.concat(manstissaTabBytes)) exponentStr, exponentBytesTable = Bin2Hex(table.concat(exponentTable)) exponentNum = hex2dec(table.concat(exponentBytesTable)) exponent = exponentNum - 127 -- 0 - 127 = -127 --> denormalized mode mantissa=mantissaNum/8388608 if exponent == -127 then -- denormalized mode exponent = -126 mantissa = mantissa else mantissa = mantissa + 1 end float_number=math.ldexp(mantissa,exponent) * sign return float_number end ==== Version 2 ==== -- MODBUS RTU 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}}) addDevice({name = "MHR",shift = 0, base = 10, xtraFields = {3, 16}}) 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 } local requestM = {1, 2, -- Адрес устройства, Функциональный код 3, 4, -- Адрес первого регистра Hi, Lo 5, 6, -- Количество регистров Hi, Lo 7, -- Количество байт далее 8, 9, -- Значение1 Hi, Lo 10, 11, -- Значение2 Hi, Lo ------------------------------- 0, 0 -- Контрольная сумма CRC Hi, 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) return 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 if device.name == "MHR" then ERROR("Can't write these type of registers (" .. device.name .. ")") -------------------------------------- My Write MHR -------------------------------------- local wrRequest = {}; local n_byte =1; -- Адрес устройства wrRequest[n_byte] = unitId; -- Функциональный код n_byte = n_byte +1; wrRequest[n_byte] = device.xtraFields[2] -- Адрес первого регистра Hi, Lo n_byte = n_byte +1; wrRequest[n_byte] = GetHiByte(reg.internalAddr) n_byte = n_byte +1; wrRequest[n_byte] = GetLoByte(reg.internalAddr) -- копируем в dataTable байты которые нужно записать local dataTable = GetDataAsTable(newValue, reg.dataType) local kol = #dataTable; -- количество байт которые нужно записать -- Количество регистров Hi, Lo n_byte = n_byte +1; wrRequest[n_byte] = 0; n_byte = n_byte +1; wrRequest[n_byte] = kol/2; -- Количество байт далее n_byte = n_byte +1; wrRequest[n_byte] = kol; -- Значение 1,2,3... Hi, Lo for i = 1, kol do n_byte = n_byte +1; wrRequest[n_byte] = dataTable[i] end -- CRC local crc, crcLo, crcHi = 0,0,0 crc = GetCRC(wrRequest, 0) crcLo = GetLoByte(crc) crcHi = GetHiByte(crc) n_byte = n_byte +1; wrRequest[n_byte] = crcLo n_byte = n_byte +1; wrRequest[n_byte] = 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 ( res[6] ~= (kol/2)) then DEBUG("Response does not match!") return false end -------------------------------------- My Write MHR -------------------------------------- return true else 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, reg.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] wrRequest[7] = nil -- удаляем ячейку со старым CRC wrRequest[8] = nil -- удаляем ячейку со старым CRC -- CRC 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)) 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 end return true end 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(value,0xFFFF) tmpTable[1] = GetHiByte(highWord) tmpTable[2] = GetLoByte(highWord) tmpTable[3] = GetHiByte(lowWord) tmpTable[4] = GetLoByte(lowWord) end return tmpTable end