Logic Machine Forum
LM as modbus slave - Printable Version

+- Logic Machine Forum (https://forum.logicmachine.net)
+-- Forum: LogicMachine eco-system (https://forum.logicmachine.net/forumdisplay.php?fid=1)
+--- Forum: Gateway (https://forum.logicmachine.net/forumdisplay.php?fid=10)
+--- Thread: LM as modbus slave (/showthread.php?tid=5913)



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.