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
#61
(26.02.2024, 09:08)Daniel Wrote: This is how it meant to work. You need to convert it back to float on master device.
mb.setfloat16precision(2) sets how float16 is converted to integer. By default it's 2 decimals (0.01 precision). Change 2 to 0 to convert to integer without any decimals.

Hi! Yes, it works. Not a big problem to convert back to float on master device, but just to clarify. Is it possible to make pure float register (either it can be little or big endian)?
Reply
#62
float32 data type is handled as is without any conversion. The script can be modified to convert float16 to float32 instead of int16.

Edit: script has been updated with optional conversion of float16 to float32: https://kb.logicmachine.net/integration/...tcp-slave/
Reply
#63
How do i set up the script to use slave id 1 on the first RS-485 port and slave id 2 on the second RS-485 port on an LM5 Lite?
Reply
#64
Create two separate scripts, one for each port.
Reply
#65
I am Using Daikin Ac and Ac Guy Given me address and holding register ID. So query is that when I'm triggering from Logic Machine not getting response from AC. even i used json file.

Attached Files Thumbnail(s)
   
Reply
#66
This thread is for modbus slave and you are using master so if you have issues create dedicated topic for your case please.
------------------------------
Ctrl+F5
Reply
#67
Hi Sir,
When I am doing read test in Modbus in Logic Machine then it is showing me this number, what does it mean?
I have Attached Sceenshot

Attached Files Thumbnail(s)
       
Reply
#68
It is what the text says, result of the read request.
------------------------------
Ctrl+F5
Reply
#69
Thanks For Reply
sir I want to Know that how to understand this result and What it means
Reply
#70
Check the register description in the device documentation.
Reply
#71
We have 5 Zones in our project and each zone has an Automation DB having KNX Actuators and a KNX keypad.
So now we have to implement Auto/Manual logic in that way that if we press the button on KNX keypad the DB will go on the manual mode, it means now we are only able to control the DB devices (KNX Actuators) with keypad only and no command can be executed from the SCADA system until we remove the Manual mode by pressing the Auto mode key on the KNX keypad.
This Auto/Manual logic is to be implemented for all the 5 Zones.
Is it possible to make different resident script for different zone in MODBUS RTU so that we can disable the communication of particular zone by just disabling the resident script of that particular zone. We are using one LM5 as MODBUS RTU Slave for all 5 Zones.
Reply
#72
Multiple resident scripts can't share the same RS-485 port. It should be enough just to block writing as needed.

Replace the whole handlers.writeregister function with this, modify writeallowed logic as needed.
Code:
1234567891011121314151617181920212223242526272829303132
local function writeallowed(slaveid)   if slaveid == 1 then     return grp.getvalue('1/1/1')   elseif slaveid == 2 then     return grp.getvalue('1/1/2')   end 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   if writeallowed(slaveid) then     writevalue(mapobj, data, 3)   end   return data end

Replace the whole handlers.writeregisters function with this.
Code:
123456789101112131415161718192021222324252627282930313233343536
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   if writeallowed(slaveid) then     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   end   return data:sub(1, 4) end
Reply
#73
Hi, I have done the changes in the mbslave (user library script). But still I didn't understand how will I disable one Zone to communicate with the SCADA  over modbus rtu. Do I need to make some changes in resident script as well. Please guide.
Reply
#74
(18.04.2025, 15:16)imprashant Wrote: Hi, I have done the changes in the mbslave (user library script). But still I didn't understand how will I disable one Zone to communicate with the SCADA  over modbus rtu. Do I need to make some changes in resident script as well. Please guide.

Previously the script I was using was working but sometimes I have to send command 2-3 times from Modscan in order to execute command in LM5. The script I was using:

local mb = require('user.mbslave')
local mbrtu = require('luamodbus').rtu()

mbrtu:open('/dev/RS485-1', 9600, 'N', 8, 1, 'H')
mbrtu:connect()
mbrtu:setslave('*') -- Accept multiple RTU slave IDs

mb.setswap('w')
mb.setfloat16precision(2)

local ga1 = '0/0/11' --zone 1 enable/disable
local ga2 = '0/0/12' --zone 2 enable/disable
local current_mapping = nil

-- Predefine mappings for easy reuse
local mapping1 = {
    [1] = {
        coils = {
            [0] = '0/0/1',
        },
        registers = {
            [0] = '0/0/2',
        }
    }
}

local mapping2 = {
    [1] = {
        coils = {
            [1] = '0/0/3',
        },
        registers = {
            [1] = '0/0/4',
        }
    }
}

while true do
    local val1 = grp.getvalue(ga1)
    local val2 = grp.getvalue(ga2)

    if not val1 and current_mapping ~= "mapping1" then
        mb.setmapping(mapping1)
        current_mapping = "mapping1"
        log("Applied Mapping 1")
    elseif not val2 and current_mapping ~= "mapping2" then
        mb.setmapping(mapping2)
        current_mapping = "mapping2"
        log("Applied Mapping 2")
    end

    mb.rtuhandler(mbrtu)
    os.sleep(1) -- small delay to prevent CPU hogging
end
Reply
#75
The previous solution is meant to be used with multiple slave IDs. Writing to slave ID 1 is allowed when 1/1/1 is true, writing to slave ID 2 is allowed when 1/1/2 is true. writeallowed should be modified as needed.

Alternative solution is to add logic AND gates to mapped group addresses.

Updated user library:
Code:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
local _M = {} local byteswap, wordswap local f16mult = 1 local f16float = false 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     if f16float then       dpt = dt.float32     else       value = value * f16mult       dpt = dt.int16     end   end   local enc = busdatatype.encode(value, dpt)   if not enc.dataraw then     return   end   local raw = enc.dataraw   if #raw % 2 == 1 then     local pad = value < 0 and 0xFF or 0     raw = string.char(pad) .. 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   if dpt == dt.float16 then     dpt = f16float and dt.float32 or dt.int16   end   local value = busdatatype.decode(hexval, dpt)   if obj.dpt == dt.float16 and not f16float then     value = value / f16mult   end   _M.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   _M.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       _M.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 _M.setfloat16mode = function(mode)   f16float = mode == 'float'   dtlens[ dt.float16 ] = f16float and 2 or 1 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.write = grp.write _M.setmapping = function(map)   for _, smap in pairs(map) do     initobjects(smap.registers)     initobjects(smap.inputregisters)   end   mapping = map end return _M

Resident script example:
Code:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
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.setswap('w') mb.setfloat16precision(2) mb.setmapping({   [1] = {     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 gates = {   ['0/0/3'] = '32/0/1',   ['1/0/1'] = '32/0/2',   ['1/0/3'] = '32/0/2',   ['1/0/4'] = '32/0/3', } mb.write = function(addr, value, dt)   local gate = gates[ addr ]   if not gate or grp.getvalue(gate) then     grp.write(addr, value, dt)   end   end while true do   mb.rtuhandler(mbrtu) end

In gates table key (left side) is mapped group address and value (right side) is controlling gate object. Write to the given mapped group address will only happen if the gate value is true. Writing to group addresses without a gate group address is always allowed.
Reply
#76
Hi, Thanks for sharing. It is working fine for the coils i.e 1 bit object. But we have holding registers as well that we are using for scene control in which we are sending 0,1 and 2 value from the same group address. How in the above script we can use gate function for the holding registers.

registers = {

      [1] = '6/3/6',
      [2] = '6/0/10',
      [3] = '6/0/11',
      [4] = '6/2/6',-
      [5] = '6/2/7',


   
    },
Reply
#77
gates table is shared, it works the same for coils and registers.
Reply


Forum Jump: