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

Forum Jump: