Posts: 289
Threads: 77
Joined: May 2017
Reputation:
0
06.03.2025, 08:05
(This post was last modified: 06.03.2025, 08:05 by gdimaria.)
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
Posts: 8069
Threads: 43
Joined: Jun 2015
Reputation:
470
Posts: 289
Threads: 77
Joined: May 2017
Reputation:
0
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: 1234567891011121314151617181920212223242526272829303132 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')
mb.setmapping({
[ 1] = {
coils = {
[ 1] = '2/1/11',
[ 4] = '2/1/14',
},
registers = {
[ 0] = '2/1/10',
[ 3] = '2/1/13',
}
}
})
mb.setswap( 'w')
mb.setfloat16precision( 2)
while true do
mb.rtuhandler( mbrtu)
end
user.mbslave
Code: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551 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
Posts: 8069
Threads: 43
Joined: Jun 2015
Reputation:
470
For testing purposes you can connect both LM RS485 ports together. Then use Read test in Modbus tab. Make sure that port settings match.
Posts: 289
Threads: 77
Joined: May 2017
Reputation:
0
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
Posts: 8069
Threads: 43
Joined: Jun 2015
Reputation:
470
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
Posts: 289
Threads: 77
Joined: May 2017
Reputation:
0
ok....
just to be sure
I have to connect
RS485-1 RS485-2
A to B
B to A
That's correct?
Posts: 4935
Threads: 28
Joined: Aug 2017
Reputation:
225
No A-A, B-B
------------------------------
Ctrl+F5
Posts: 289
Threads: 77
Joined: May 2017
Reputation:
0
07.03.2025, 10:47
(This post was last modified: 07.03.2025, 10:57 by gdimaria.)
(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?
Posts: 4935
Threads: 28
Joined: Aug 2017
Reputation:
225
No as it has only one RS485 port but if you have more than one you can connect two.
------------------------------
Ctrl+F5
Posts: 289
Threads: 77
Joined: May 2017
Reputation:
0
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: 123456789101112131415 mb.setmapping({
[ 1] = {
coils = {
[ 1] = '2/1/11',
[ 4] = '2/1/14',
},
registers = {
[ 0] = '2/1/10',
[ 3] = '2/1/13',
}
}
})
Posts: 4935
Threads: 28
Joined: Aug 2017
Reputation:
225
Coils are bits only, registers are numeric.
Coil and registers have its own ranges.
------------------------------
Ctrl+F5
|