This forum uses cookies
This forum makes use of cookies to store your login information if you are registered, and your last visit if you are not. Cookies are small text documents stored on your computer; the cookies set by this forum can only be used on this website and pose no security risk. Cookies on this forum also track the specific topics you have read and when you last read them. Please confirm that you accept these cookies being set.

Universal Modbus TCP/RTU Slave
#1
A new version of the modbus slave script where you can freely define coil/registers and even use bigger data-types and multiple slaves.

Data-types supported:
  [ dt.bool ] = 1,
  [ dt.bit4 ] = 1,
  [ dt.int8 ] = 1,
  [ dt.uint8 ] = 1,
  [ dt.int16 ] = 1,
  [ dt.uint16 ] = 1,
  [ dt.float16 ] = 1,
  [ dt.int32 ] = 2,
  [ dt.uint32 ] = 2,
  [ dt.float32 ] = 2,
  [ dt.int64 ] = 4,


The number next to DPT is representing how many registers will be used per object. It means if int64 is used make sure to leave gap for 4 registers in the mapping table.

Only 2023 firmware and newer is supported

1. Create user library mbslave and past this code:
Code:
local _M = {} local byteswap, wordswap local f16mult = 1 local mapping = {} local exception = {} local reply = {} local fncodes = {   [1] = 'readcoils',   [2] = 'readdiscreteinputs',   [3] = 'readregisters',   [4] = 'readinputregisters',   [5] = 'writecoil',   [6] = 'writeregister',   [15] = 'writecoils',   [16] = 'writeregisters', } local mapfncodes = {   [1] = 'coils',   [2] = 'discreteinputs',   [3] = 'registers',   [4] = 'inputregisters',   [5] = 'coils',   [6] = 'registers',   [15] = 'coils',   [16] = 'registers', } local dtlens = {   [ dt.bool ] = 1,   [ dt.bit4 ] = 1,   [ dt.int8 ] = 1,   [ dt.uint8 ] = 1,   [ dt.int16 ] = 1,   [ dt.uint16 ] = 1,   [ dt.float16 ] = 1,   [ dt.int32 ] = 2,   [ dt.uint32 ] = 2,   [ dt.float32 ] = 2,   [ dt.int64 ] = 4, } local maxpdu = 253 local maxaddr = 0x10000 local coilon = 0xFF00 local limits = {   readbits = 2000,   writebits = 1968,   readregisters = 125,   writeregisters = 123, } local excodes = {   illegalfunction = 1,   illegaldataaddress = 2,   illegaldatavalue = 3,   serverdevicefailure = 4,   acknowledge = 5,   serverdevicebusy = 6,   negativeacknowledge = 7,   memoryparityerror = 8,   gatewaypathunavailable = 10,   gatewaytargetdevicefailed = 11, } local handlers = {} local function touint8(buf, off)   return buf:byte(off) end local function touint16(buf, off, swap)   local b1, b2 = buf:byte(off, off + 1)   if swap then     b1, b2 = b2, b1   end   return b1 * 0x100 + b2 end local function getmapping(slaveid, fncode)   local mapfncode = mapfncodes[ fncode ]   local slave = mapping[ slaveid ]   if not slave then     slave = mapping['*']   end   if slave then     return slave[ mapfncode ]   end end local function readbits(slaveid, fncode, data)   if #data ~= 4 then     return   end   local addr = touint16(data, 1)   local count = touint16(data, 3)   if count == 0 or count > limits.readbits then     return excodes.illegaldataaddress   elseif (addr + count) > maxaddr then     return excodes.illegaldataaddress   end   local map = getmapping(slaveid, fncode)   if not map then     return excodes.illegaldataaddress   end   local res = {}   local bits, byte = 0, 0   for i = 0, (count - 1) do     local mapaddr = map[ addr + i ]     if mapaddr then       local bval = grp.getvalue(mapaddr)       if toboolean(bval) then         byte = byte + bit.lshift(1, bits)       end     end     bits = bits + 1     if bits == 8 then       res[ #res + 1 ] = string.char(byte)       bits, byte = 0, 0     end   end   if bits ~= 0 then     res[ #res + 1 ] = string.char(byte)   end   return string.char(#res) .. table.concat(res) end local zeroreg = string.char(0, 0) local function readvalue(res, value, dpt)   if dpt == dt.float16 then     value = value * f16mult     dpt = dt.int16   end   local enc = busdatatype.encode(value, dpt)   if not enc.dataraw then     return   end   local raw = enc.dataraw   if #raw % 2 == 1 then     raw = string.char(0) .. raw   end   local words = #raw / 2   local offset = #res   for i = 1, words do     local word = raw:sub(i * 2 - 1, i * 2)     if byteswap then       word = word:sub(2, 2) .. word:sub(1, 1)     end     if wordswap then       res[ offset + i ] = word     else       res[ offset + 1 + words - i ] = word     end   end   return true end local function readregisters(slaveid, fncode, data)   if #data ~= 4 then     return   end   local addr = touint16(data, 1)   local count = touint16(data, 3)   if count == 0 or count > limits.readregisters then     return excodes.illegaldataaddress   elseif (addr + count) > maxaddr then     return excodes.illegaldataaddress   end   local map = getmapping(slaveid, fncode)   if not map then     return excodes.illegaldataaddress   end   local res = {}   local max = count - 1   while #res <= max do     local mapobj = map[ addr + #res ]     local success     if mapobj then       local value = grp.getvalue(mapobj.address)       if type(value) == 'boolean' then         value = value and 1 or 0       end       if type(value) ~= 'number' then         alert('invalid value ' .. mapobj.address .. ' ' .. tostring(value))         value = 0       end       success = readvalue(res, value, mapobj.dpt)     end     if not success then       res[ #res + 1 ] = zeroreg     end   end   if #res ~= count then     return excodes.illegaldataaddress   end   return string.char(count * 2) .. table.concat(res) end local function writevalue(obj, data, offset)   local words = obj.len   local len = words * 2   local raw = data:sub(offset, offset + len - 1)   if #raw < len then     return   end   local buf = {}   for i = 1, words do     local word = touint16(raw, i * 2 - 1, byteswap)     local hex = string.format('%04X', word)     if wordswap then       buf[ #buf + 1 ] = hex     else       table.insert(buf, 1, hex)     end   end   local hexval = table.concat(buf)   local dpt = obj.dpt == dt.float16 and dt.int16 or obj.dpt   local value = busdatatype.decode(hexval, dpt)   if obj.dpt == dt.float16 then     value = value / f16mult   end   grp.write(obj.address, value) end handlers.readcoils = readbits handlers.readdiscreteinputs = readbits handlers.readregisters = readregisters handlers.readinputregisters = readregisters handlers.writecoil = function(slaveid, fncode, data)   if #data ~= 4 then     return   end   local addr = touint16(data, 1)   local map = getmapping(slaveid, fncode)   if not map or not map[ addr ] then     return excodes.illegaldataaddress   end   local bval = touint16(data, 3) == coilon   grp.write(map[ addr ], bval, dt.bool)   return data end handlers.writeregister = function(slaveid, fncode, data)   if #data ~= 4 then     return   end   local addr = touint16(data, 1)   local map = getmapping(slaveid, fncode)   if not map or not map[ addr ] then     return excodes.illegaldataaddress   end   local mapobj = map[ addr ]   if mapobj.len ~= 1 then     return excodes.illegaldataaddress   end   writevalue(mapobj, data, 3)   return data end handlers.writecoils = function(slaveid, fncode, data)   if #data < 5 then     return   end   local addr = touint16(data, 1)   local count = touint16(data, 3)   local bytes = touint8(data, 5)   if #data ~= (bytes + 5) then     return   end   if count == 0 or count > limits.writebits or bytes ~= math.ceil(count / 8) then     return excodes.illegaldataaddress   end   local map = getmapping(slaveid, fncode)   if not map then     return excodes.illegaldataaddress   end   local offset = 6   local byte = data:byte(offset)   local bits = 0   for i = 0, (count - 1) do     local bval = bit.band(bit.rshift(byte, bits), 1)     local mapaddr = map[ addr + i ]     if mapaddr then       grp.write(mapaddr, bval, dt.bool)     end     bits = bits + 1     if bits == 8 then       bits = 0       offset = offset + 1       byte = data:byte(offset)     end   end   return data:sub(1, 4) end handlers.writeregisters = function(slaveid, fncode, data)   if #data < 5 then     return   end   local addr = touint16(data, 1)   local count = touint16(data, 3)   local bytes = touint8(data, 5)   if #data ~= (bytes + 5) then     return   end   if count == 0 or count > limits.writeregisters or bytes ~= count * 2 then     return excodes.illegaldataaddress   end   local map = getmapping(slaveid, fncode)   if not map then     return excodes.illegaldataaddress   end   for i = 0, (count - 1) do     local mapobj = map[ addr + i ]     if mapobj and not mapobj.readonly then       local offset = 6 + i * 2       writevalue(mapobj, data, offset)     end   end   return data:sub(1, 4) end exception.tcp = function(sock, hdr, excode)   local fncode = bit.bor(0x80, hdr:byte(8))   local resp = hdr:sub(1, 4) ..     string.char(0, 3) ..     hdr:sub(7, 7) ..     string.char(fncode, excode)   return sock:send(resp) end exception.rtu = function(mbrtu, hdr, excode)   local fncode = bit.bor(0x80, hdr:byte(2))   local resp = hdr:sub(1, 1) ..     string.char(fncode, excode)   return mbrtu:send(resp) end reply.tcp = function(sock, hdr, data)   local resp = hdr:sub(1, 4) ..     string.char(0, #data + 2) ..     hdr:sub(7, 8) .. data   return sock:send(resp) end reply.rtu = function(mbrtu, hdr, data)   local resp = hdr:sub(1, 2) .. data   return mbrtu:send(resp) end local function handler(mode, ctx, hdr, data)   local slaveid, fncode = hdr:byte(#hdr - 1, #hdr)   local fnname = fncodes[ fncode ]   if not fnname then     return exception[ mode ](ctx, hdr, excodes.illegalfunction)   end   local res = handlers[ fnname ](slaveid, fncode, data)   if type(res) == 'number' then     return exception[ mode ](ctx, hdr, res)   elseif type(res) == 'string' then     return reply[ mode ](ctx, hdr, res)   end end _M.tcphandler = function(sock)   local hdr, err = sock:receive(8)   if not hdr then     return nil, err   end   local len = touint16(hdr, 5) - 2   local data   if len <= 0 or len > maxpdu then     err = 'protocol error'   else     data, err = sock:receive(len)   end   if not data then     return nil, err   end   return handler('tcp', sock, hdr, data) end _M.rtuhandler = function(mbrtu)   local data, err = mbrtu:receive()   if not data then     return nil, err   end   local slaveid = data:byte(1)   if not mapping[ slaveid ] then     return   end   return handler('rtu', mbrtu, data:sub(1, 2), data:sub(3, #data - 2)) end _M.setswap = function(swap)   byteswap = swap:find('b') ~= nil   wordswap = swap:find('w') ~= nil end _M.setfloat16precision = function(prec)   prec = tonumber(prec) or 0   f16mult = math.pow(10, prec) end local function initobject(obj, dbobj)   local dpt = obj.datatype or dbobj.datatype   if type(dpt) == 'string' then     dpt = dt[ dpt ] or 0   end   if type(dpt) == 'number' and dpt >= 1000 then     dpt = math.floor(dpt / 1000)   end   obj.dpt = dpt   obj.len = dtlens[ dpt ]   obj.address = dbobj.address   if not obj.len then     alert('invalid data type ' .. obj.address .. ' ' .. tostring(dpt))     return   end   return obj end local function initobjects(objects)   if type(objects) ~= 'table' then     return   end   for addr, obj in pairs(objects) do     if type(obj) == 'string' then       obj = { address = obj }     end     local dbobj = grp.find(obj.address)     if dbobj then       obj = initobject(obj, dbobj)     else       alert('missing object ' .. obj.address)       obj = nil     end     objects[ addr ] = obj   end end _M.setmapping = function(map)   for _, smap in pairs(map) do     initobjects(smap.registers)     initobjects(smap.inputregisters)   end   mapping = map end return _M

2a. For Modbus TCP slave create resident script with 0 interval and set object mapping in the table. Remember to create gap in registers for 32 and 64 bit objects. 

Code:
local mb = require('user.mbslave') local copas = require('copas') local socket = require('socket') local address = '*' local port = 502 local server = assert(socket.bind(address, port)) mb.setmapping({   ['*'] = {     coils = {       [0] = '0/0/1',       [1] = '1/1/1',       [2] = '1/1/2',     },     registers = {       [0] = '1/1/4',       [1] = '1/1/10',       [3] = '32/1/6',       [5] = '32/1/13',     }   } }) mb.setswap('w') mb.setfloat16precision(2) local function handler(sock)   copas.setErrorHandler(log)   sock = copas.wrap(sock)   sock:settimeout(60)   while true do     local res, err = mb.tcphandler(sock)     if not res then       break     end   end   sock:close() end copas.addserver(server, handler, 60) copas.loop()

2b. For Modbus RTU slave create resident script with 0 interval and set object mapping in the table. Remember to create gap in registers for 32 and 64 bit objects. Change serial port settings in mbrtu:open as needed.

Code:
local mb = require('user.mbslave') local mbrtu = require('luamodbus').rtu() mbrtu:open('/dev/RS485-1', 9600, 'E', 8, 1, 'H') mbrtu:connect() mbrtu:setslave('*') -- '*' handles multiple RTU slave IDs mb.setmapping({   [1] = {     coils = {       [0] = '0/0/1',       [1] = '1/1/1',       [2] = '1/1/2',     },     registers = {       [0] = '1/1/4',       [1] = '1/1/10',       [3] = '32/1/6',       [5] = '32/1/13',     }   } }) mb.setswap('w') mb.setfloat16precision(2) while true do   mb.rtuhandler(mbrtu) end
------------------------------
Ctrl+F5
Reply


Messages In This Thread
Universal Modbus TCP/RTU Slave - by Daniel - 07.10.2022, 12:55
RE: Universal Modbus TCP Slave - by palomino - 07.10.2022, 17:30
RE: Universal Modbus TCP Slave - by Daniel - 07.10.2022, 17:53
RE: Universal Modbus TCP Slave - by Daniel - 07.10.2022, 17:55
RE: Universal Modbus TCP Slave - by palomino - 10.10.2022, 13:39
RE: Universal Modbus TCP Slave - by Daniel - 10.10.2022, 13:44
RE: Universal Modbus TCP Slave - by palomino - 11.10.2022, 16:22
RE: Universal Modbus TCP Slave - by palomino - 23.02.2023, 12:44
RE: Universal Modbus TCP Slave - by admin - 23.02.2023, 12:47
RE: Universal Modbus TCP Slave - by palomino - 24.02.2023, 12:41
RE: Universal Modbus TCP Slave - by admin - 24.02.2023, 12:48
RE: Universal Modbus TCP Slave - by palomino - 27.02.2023, 13:02
RE: Universal Modbus TCP Slave - by Daniel - 27.02.2023, 13:10
RE: Universal Modbus TCP Slave - by palomino - 27.02.2023, 14:00
RE: Universal Modbus TCP Slave - by Daniel - 27.02.2023, 14:03
RE: Universal Modbus TCP Slave - by palomino - 28.02.2023, 02:32
RE: Universal Modbus TCP Slave - by admin - 28.02.2023, 07:02
RE: Universal Modbus TCP Slave - by Fahd - 06.03.2023, 14:11
RE: Universal Modbus TCP Slave - by Daniel - 06.03.2023, 14:35
RE: Universal Modbus TCP Slave - by Fahd - 06.03.2023, 14:50
RE: Universal Modbus TCP Slave - by Daniel - 06.03.2023, 14:55
RE: Universal Modbus TCP Slave - by Daniel - 06.03.2023, 15:16
RE: Universal Modbus TCP Slave - by Fahd - 07.03.2023, 07:59
RE: Universal Modbus TCP Slave - by rw_echo - 14.03.2023, 05:22
RE: Universal Modbus TCP Slave - by Daniel - 07.03.2023, 11:55
RE: Universal Modbus TCP Slave - by rw_echo - 07.03.2023, 12:32
RE: Universal Modbus TCP Slave - by Daniel - 07.03.2023, 12:34
RE: Universal Modbus TCP Slave - by rw_echo - 07.03.2023, 12:49
RE: Universal Modbus TCP Slave - by Daniel - 07.03.2023, 12:59
RE: Universal Modbus TCP Slave - by David - 13.03.2023, 15:40
RE: Universal Modbus TCP Slave - by admin - 13.03.2023, 15:42
RE: Universal Modbus TCP Slave - by David - 13.03.2023, 15:58
RE: Universal Modbus TCP Slave - by Daniel - 13.03.2023, 16:00
RE: Universal Modbus TCP Slave - by David - 13.03.2023, 16:07
RE: Universal Modbus TCP Slave - by Daniel - 13.03.2023, 16:15
RE: Universal Modbus TCP Slave - by David - 13.03.2023, 16:32
RE: Universal Modbus TCP Slave - by Daniel - 13.03.2023, 16:41
RE: Universal Modbus TCP Slave - by admin - 13.03.2023, 16:45
RE: Universal Modbus TCP Slave - by David - 13.03.2023, 19:31
RE: Universal Modbus TCP Slave - by admin - 14.03.2023, 06:18
RE: Universal Modbus TCP Slave - by rw_echo - 14.03.2023, 07:39
RE: Universal Modbus TCP Slave - by Fahd - 14.03.2023, 08:27
RE: Universal Modbus TCP Slave - by admin - 14.03.2023, 08:15
RE: Universal Modbus TCP Slave - by rw_echo - 14.03.2023, 08:35
RE: Universal Modbus TCP Slave - by admin - 14.03.2023, 08:35
RE: Universal Modbus TCP/RTU Slave - by admin - 06.06.2023, 15:34
RE: Universal Modbus TCP/RTU Slave - by admin - 12.06.2023, 08:59
RE: Universal Modbus TCP/RTU Slave - by admin - 28.03.2024, 08:26
RE: Universal Modbus TCP/RTU Slave - by admin - 12.06.2024, 05:26
RE: Universal Modbus RTU - by Ranjeet - 14.06.2024, 07:24
RE: Universal Modbus TCP/RTU Slave - by admin - 12.07.2024, 07:14
RE: Universal Modbus TCP/RTU Slave - by admin - 16.04.2025, 07:06
RE: Universal Modbus TCP/RTU Slave - by admin - 22.04.2025, 05:32
RE: Universal Modbus TCP/RTU Slave - by admin - 22.04.2025, 09:01
RE: Universal Modbus TCP/RTU Slave - by admin - 14.08.2025, 09:11
RE: Universal Modbus TCP/RTU Slave - by admin - 25.05.2026, 09:30
RE: Universal Modbus TCP/RTU Slave - by admin - 25.05.2026, 09:47

Forum Jump: