Universal Modbus TCP/RTU Slave - Daniel - 07.10.2022
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
RE: Universal Modbus TCP Slave - palomino - 07.10.2022
Hello Daniel.
Thanks for the info, but it doesn't work.
Communication with the Modbus master is not established.
RE: Universal Modbus TCP Slave - Daniel - 07.10.2022
Works for me, paste your script.
RE: Universal Modbus TCP Slave - Daniel - 07.10.2022
And make sure all other modbus scripts are disabled
RE: Universal Modbus TCP Slave - palomino - 10.10.2022
Hi Daniel, I don't understand this instruction, can you give me more explanation. "Remember to enable/disable resident script after each change."
The master can write and read data from the LM slave?
where can I add the slave address?
RE: Universal Modbus TCP Slave - Daniel - 10.10.2022
In current firmware a resident script with 0 interval must be disabled and then enabled again to load any change made in the script.
In the table where you have ['*'] is place for slave ID. '*' means it will accept requests to any slave ID. You can change it to [1] and then it will be slave 1
RE: Universal Modbus TCP Slave - palomino - 11.10.2022
Hello Daniel. The script worked very well , thank you very much.
RE: Universal Modbus TCP Slave - palomino - 23.02.2023
Hello Daniel.
I am getting this message in logs: *nill
What can be?
Adicional,
I need to use 150 registers, but i see in te code that only I can use 125, how can i increase it?
RE: Universal Modbus TCP Slave - admin - 23.02.2023
Replace this line:
Code: copas.setErrorHandler(log)
With this:
Code: copas.setErrorHandler(function(msg)
if msg ~= nil then
log(msg)
end
end)
RE: Universal Modbus TCP Slave - palomino - 24.02.2023
Hello Daniel.
I chage this line in the resident script, but still showing the same message in logs.
I also see that CPU/IO increases a lot, I'm trying to read 150 objects
RE: Universal Modbus TCP Slave - admin - 24.02.2023
Are you sure that the log comes from this script? Check the name of the script in Logs. Just to be sure restart the Modbus script via disable/enable.
As defined by the Modbus standard you can read up to 125 registers at once using a single request. But each devices can have up to 65536 registers.
RE: Universal Modbus TCP Slave - palomino - 27.02.2023
Hello Daniel.
I did what you told me. But is the same logs massage. How do I read more than 125 registers ?
RE: Universal Modbus TCP Slave - Daniel - 27.02.2023
You can't as modbus standard is defining it, but this is limit for reading 125 at once, you can make several reads with smaller batches.
RE: Universal Modbus TCP Slave - palomino - 27.02.2023
I can use two diferent script?
A question a Modbus Master can write to me more than 125 registration?, I can have more than 125 register in memory to other modbus master write to me.?
RE: Universal Modbus TCP Slave - Daniel - 27.02.2023
Back to basics This is slave script, it does not read anything. Your master is responsible for reading. In there you define how it reads. Nothing to do with LM.
RE: Universal Modbus TCP Slave - palomino - 28.02.2023
Hi Daniel
The idea is this. I need that one or more masters can write to me in 150 madbus registers. In other words, I need to create a mapping register with 150 register so that they master can write to me and I can have them available this register to show them in LM. it's possible?
I made myself clear?
RE: Universal Modbus TCP Slave - admin - 28.02.2023
You can have up to 65536 registers.
RE: Universal Modbus TCP Slave - Fahd - 06.03.2023
(28.02.2023, 07:02)admin Wrote: You can have up to 65536 registers.
can we test the exported point with LM5?
with the read test !
(06.03.2023, 14:11)Fahd Wrote: (28.02.2023, 07:02)admin Wrote: You can have up to 65536 registers.
can we test the exported point with LM5?
with the read test !
Trying to make a reading test with Modbus Poll program and the values do not change at all, please see attached.
this is the resident script that I'm using
Code: local mb = require('user.mbtcp')
local copas = require('copas')
local socket = require('socket')
local address = '*'
local port = 502
local server = assert(socket.bind(address, port))
mb.setmapping({
['1'] = {
coils = {
[0] = '0/0/1',
[1] = '0/0/2',
[2] = '0/0/3',
[4] = '0/0/4',
},
registers = {
[0] = '1/1/4',
[5] = '1/1/6',
}
}
})
mb.setswap('w')
local function handler(sock)
copas.setErrorHandler(log)
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()
RE: Universal Modbus TCP Slave - Daniel - 06.03.2023
You can use RTU test. Check if you have any errors in slave script.
RE: Universal Modbus TCP Slave - Fahd - 06.03.2023
(06.03.2023, 14:35)Daniel Wrote: You can use RTU test. Check if you have any errors in slave script.
Those group addresses should be exported to the Modbus TCP/IP, is that right?
There's no errors
|