07.10.2022, 12:55
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:
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.
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.
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
Ctrl+F5