LM as modbus slave - gdimaria - 06.03.2025
Hi,
I have to set LM as modbus slave to a master modbus device who must be able to read and write on KNX objects.
How should I proceed?
Thanks
Peppe
RE: LM as modbus slave - admin - 06.03.2025
https://kb.logicmachine.net/integration/modbus-rtu-tcp-slave/
RE: LM as modbus slave - gdimaria - 06.03.2025
I followed the instructions, but testing with the modbus tool, it times out and does not read the registers.
I attach the resident script and the mbslave library that I made.
MODBUS RTU SLAVE resident script
Code: require('user.mbslave')
local mb = require('user.mbslave')
local mbrtu = require('luamodbus').rtu()
mbrtu:open('/dev/RS485-1', 9600, 'E', 8, 1, 'H')
mbrtu:connect()
mbrtu:setslave('1') -- '*' handles multiple RTU slave IDs
mb.setmapping({
[1] = {
coils = {
[1] = '2/1/11', -- stato on/off
[4] = '2/1/14', -- stato set %
},
registers = {
[0] = '2/1/10', -- comando on/off
-- [1] = '2/1/11', -- stato on/off
[3] = '2/1/13', -- set %
-- [4] = '2/1/14', -- stato set %
}
}
})
mb.setswap('w')
mb.setfloat16precision(2)
while true do
mb.rtuhandler(mbrtu)
end
user.mbslave
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
Thanks in advance
Peppe
RE: LM as modbus slave - admin - 06.03.2025
For testing purposes you can connect both LM RS485 ports together. Then use Read test in Modbus tab. Make sure that port settings match.
RE: LM as modbus slave - gdimaria - 07.03.2025
so, you are saying to connect both LM RS485 each other and then use the read test.... on which of 3 rtu? is that correct?
by the way, in mbrtu:open('/dev/RS485-1', 9600, 'E', 8, 1, 'H') what means 'H' ?
Thanks
Peppe
RE: LM as modbus slave - admin - 07.03.2025
Use RS485-1 in the script. Configure RTU1 to use RS485-2.
'E', 8, 1, 'H' means Even parity, 8 data bits, 1 stop bit and Half duplex mode
RE: LM as modbus slave - gdimaria - 07.03.2025
ok....
just to be sure
I have to connect
RS485-1 RS485-2
A to B
B to A
That's correct?
RE: LM as modbus slave - Daniel - 07.03.2025
No A-A, B-B
RE: LM as modbus slave - gdimaria - 07.03.2025
(07.03.2025, 10:15)Daniel Wrote: No A-A, B-B
ok thanks
Can I do the same with spaceLynk? how to connect and configure?
RE: LM as modbus slave - Daniel - 07.03.2025
No as it has only one RS485 port but if you have more than one you can connect two.
RE: LM as modbus slave - gdimaria - 07.03.2025
Last question:
I have some knx address as commands and others as states... how I have to set them? Both coil and registers as in my script ? or a different way?
Code: mb.setmapping({
[1] = {
coils = {
[1] = '2/1/11', -- state on/off
[4] = '2/1/14', -- state set %
},
registers = {
[0] = '2/1/10', -- command on/off
-- [1] = '2/1/11', -- stato on/off
[3] = '2/1/13', -- command set %
-- [4] = '2/1/14', -- stato set %
}
}
})
RE: LM as modbus slave - Daniel - 07.03.2025
Coils are bits only, registers are numeric.
Coil and registers have its own ranges.
RE: LM as modbus slave - gdimaria - 10.10.2025
HI,
what is the max amount of register I can map on LM as modbus slave?
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.setswap('w')
mb.setfloat16precision(2)
mb.setmapping({
['*'] = {
coils = {
[0] = '0/0/1',
[1] = '0/0/3',
[2] = '0/0/4',
},
discreteinputs = {
[0] = '1/0/1',
[1] = '1/0/3',
[2] = '1/0/4',
},
registers = {
[0] = '1/1/4',
[1] = '1/1/10',
[3] = '32/1/6',
[5] = '32/1/13',
},
inputregisters = {
[0] = '2/1/4',
[1] = '2/1/10',
}
}
})
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()
RE: LM as modbus slave - admin - 13.10.2025
There's no hard limit on how many registers can be mapped. How many do you need to map and how often these registers will be polled?
RE: LM as modbus slave - gdimaria - 13.10.2025
it's about 4000 GA (command and status), almost lights, so I guess about 3 sec.
I have to transfer the control and visualization of knx via modbus on a ABB SCADA. It can read up to 10k registers
RE: LM as modbus slave - Daniel - 13.10.2025
This won't work, its much too much. Don't they have any other interface? Modbus is the worst possible option for lights. and so many objects.
RE: LM as modbus slave - gdimaria - 14.10.2025
(13.10.2025, 15:41)Daniel Wrote: This won't work, its much too much. Don't they have any other interface? Modbus is the worst possible option for lights. and so many objects.
the lights are in KNX, then, through the LM I have to pass them in Modbus to the ABB Scada
RE: LM as modbus slave - Daniel - 14.10.2025
It doesn't matter, 4k objects pooled every 3sec won't work.
RE: LM as modbus slave - gdimaria - 14.10.2025
(14.10.2025, 09:10)Daniel Wrote: It doesn't matter, 4k objects pooled every 3sec won't work.
I see.... so what would be the max number of GA mapped as modbus registers on LM in polling?
RE: LM as modbus slave - Daniel - 14.10.2025
This script wasn't optimized for such big integration. It is hard to say as we never tested it on a bigger scale.
100 objects every 5 sec should work but I'm guessing now.
Maybe some other user could say something.
|