﻿{"enable": false, "name": "Bus", "tag": "debug=false,\nupdate_interval=5000,\noffline_interval=15000,\n", "script": {"modbus.lua": "--Modbus v2.00, supports Modbus RTU, ASCII, TCP, RTU over TCP\n--Tag config:\n--bus:\n--  update_interval: milliseconds, default is 10000(10 seconds)\n--  offline_interval: milliseconds, default is double of update_interval\n--      interval to retry on offline devices\n--  command_interval: milliseconds, default is update_interval*2. For\n--      commandable object, when value readback doesn't match value outputed,\n--      we need to output again. This inteval limit the output frequency.\n--  before_idle: milliseconds, bus idle need for sending request, only valid\n--      for RS485, default 0 for ASCII, 38.5 bits for RTU\n--  debug: boolean, true to print send/recv packet\n--  tcp_rtu: rtu over tcp mode, default is false\n--  ascii: ASCII mode, default is false\n--  init: user initiaize function\n--      prototype: init(ctx)\n--  global_define: all properties will be propagated to global scope\n--device:\n--  slave\n--  readreqs = {{func_code, start_addr, quantity},\n--          {func_code, start_addr, quantity}\n--      }\n--      func_code only support 1,2,3,4\n--      start_addr is starting from 0\n--      After data is read from Modbus device, it will append to\n--      each elment of readreqs\n--  writesinglereg: boolean, when writing one reg, func_code 6 is used\n--  writesinglecoil: boolean, when writing one coil, func_code 5 is used\n--  endian: 1=1234, 2=2143, 3=3412, 4=4321 (default), only used in\n--      default input/output\n--  timeout: milliseconds, timeout for response\n--  ignore_unmatch: boolean. For a commandable object, when value readback\n--      don't match value written, set reliability to unreliable other.\n--  init: user initialize function\n--      prototype: init(ctx, device)\n--point:\n--  input: function to map input. If nil, default_input will be used\n--      propotype: input(device, point)\n--      return value, reliability\n--      If value or reliability is not nil, swg.updatepoint will be called\n--  output: funcion to generate output, only make sense on BACnet writable\n--          object. If nil, default_output will be used\n--      propotype: output(device, point, value)\n--      value is the value asked to write by BACnet\n--      return array of {bits_in_last_byte, address, data}, referring to\n--          multiple write request. if nil is returned, no Modbus write\n--          will be performed.\n--      If bits_in_last_byte == 0, output to holding register, else\n--          output to coil.\n--  postoutput: function to be called when Modbus write is performed(not nil\n--      is returned from output function).\n--      propotype: postoutput(device, point, value, output_reqs)\n--      return BACnet value actually write to device, it maybe different to\n--          the value asked to write by BACnet\n--  init: user initialize function\n--      prototype: init(ctx, device, point)\n--  ignore_unmatch: boolean, point specified property\n--  Below parameters are only valid when default input or output function is\n--      used, they will stay in point.tag scope, not to be moved to point scope\n--  func_code: 1,2 is only valid for binary object.\n--  addr: starting from 0\n--  datatype: only valid for func_code 3 or 4. enum of\n--      \"u16\"(unsigned 16 bits, default), \"u32\", \"u64\", \"s16\"(signed 16 bits),\n--      \"s32\", \"s64\", \"f32\"(32bits float point), \"f64\"(64 bits float point).\n--      For multistate object, only \"u16\" and \"u32\" are valid.\n--      datatype is meanless for binary object.\n--  ms_values: array of integer, only valid to multisate object\n--      if ms_values is absense, BACnet present value 1 mapping to Modbus 0\n--      if ms_values is present, BACnet present value 1 mapping to ms_values[1]\n--  bit: 0~15, default is 0. Only valid for binary on input/holding register\n--      or multistate object with \"u16\" datatype or analog object with\n--      \"u16\" or \"s16\" datatype\n--  bitlen: 1~16. Only valid for multistate object with \"u16\" datatype or\n--      analog object with \"u16\" or \"s16\" datatype. if it is nil, the default\n--      value is 16 - bit\n--  scale, offset: scale=1.0 default, offset=0 default. Only valid for analog\n--      object. BACnet value = Modbus value * scale + offset\n--  datatype will be parsed to 0=\"u16\",\"u32\",\"u64\", 1=\"s16\",\"s32\", \"s64\",\n--      2=\"f32\",\"f64\".\n--  bitlen will be set as actually bits used by data, for example 64 for \"s64\"\n\n\nlocal crc_tbl = {\n    0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,\n    0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,\n    0x0000, 0xcc01, 0xd801, 0x1400, 0xf001, 0x3c00, 0x2800, 0xe401,\n    0xa001, 0x6c00, 0x7800, 0xb401, 0x5000, 0x9c01, 0x8801, 0x4400}\n\nlocal function crc16(input)\n    local crc = 0xffff\n    for i = 1, #input do\n        local c = string.byte(input, i,i)\n        crc = (crc >> 8)\n            ~ crc_tbl[((crc ~ c) & 0x0f) + 1]\n            ~ crc_tbl[(((crc ~ c) >> 4) & 0x0f) + 17]\n    end\n\n    return string.char(crc & 0x0ff, (crc >> 8) & 0x0ff)\nend\n\n\nlocal function append_crc16(packet)\n    return packet .. crc16(packet)\nend\n\n\nlocal function verify_crc16(input)\n    if #input <= 2 then return nil end\n    local frame = string.sub(input, 1, -3)\n    if crc16(frame) == string.sub(input, -2) then\n        return frame\n    else\n        return nil\n    end\nend\n\n\nlocal function cal_lrc(input)\n    local lrc = 0\n    for i = 1, #input do\n        local c = string.byte(input, i, i)\n        lrc = lrc + c\n    end\n    return (-lrc) & 0x0ff\nend\n\n\nlocal function encode_ascii(input, lrc)\n    local pkt = \":\"\n    for i = 1, #input do\n        local c = string.byte(input, i,i)\n        pkt = pkt .. string.format(\"%02X\", c)\n    end\n    pkt = pkt .. string.format(\"%02X\", lrc)\n    return pkt .. \"\\r\\n\"\nend\n\n\nlocal function decode_ascii(input)\n    --return decode packet\n\n    local input, count = string.gsub(input, \"^:+\", \"\")\n    if count == 0 then return nil end\n\n    local cridx = string.find(input, \"\\r\\n\")\n    if cridx == nil then return nil end\n    input = string.sub(input, 1, cridx - 1)\n\n    local lrc = 0\n    local half = nil\n    local result = \"\"\n    for i = 1, #input do\n        local c = string.byte(input, i, i)\n        if c < 0x30 or c > 0x46 then return nil end\n        if c > 0x39 and c < 0x41 then return nil end\n        if c >= 0x41 then\n            c = c - 0x41 + 10\n        else\n            c = c - 0x30\n        end\n\n        if half == nil then\n            half = c << 4\n        else\n            c = half + c\n            lrc = (lrc + c) & 0x0ff\n            result = result .. string.char(c)\n            half = nil\n        end\n    end\n\n    if half ~= nil or lrc ~= 0 or #result == 0 then return nil end\n    return string.sub(result, 1, #result - 1)\nend\n\n\nlocal function append_tcpheader(packet, trans_id)\n    return string.char((trans_id >> 8) & 0x0ff, trans_id & 0x0ff,\n            0, 0, #packet >> 8, #packet & 0x0ff) .. packet\nend\n\n\nlocal function decode_tcpheader(packet, trans_id)\n    --return payload length\n    if #packet < 6 then return nil end\n    if (string.byte(packet,1,1) << 8)\n            + string.byte(packet,2,2) ~= trans_id then return nil end\n    if string.byte(packet,3,3) ~= 0 or string.byte(packet,4,4) ~= 0 then\n        return nil end\n    local length = (string.byte(packet,5,5) << 8) + string.byte(packet,6,6)\n    if length < 2 or length > 254 then return nil end\n    return length\nend\n\n\nlocal function check_exception(packet, slave, func_code)\n    if #packet ~= 3 then return nil end\n    if string.byte(packet,1,1) ~= slave then return nil end\n    if string.byte(packet,2,2) ~= func_code|0x80 then return nil end\n    return string.byte(packet,3,3)\nend\n\n\nlocal function gen_read_req(slave, func_code, start_addr, quantity)\n    return string.char(slave, func_code, start_addr >> 8, start_addr & 0x0ff, quantity >> 8, quantity & 0x0ff)\nend\n\n\nlocal function check_read_rsp(packet, slave, func_code, quantity)\n    --return unit data\n    if func_code < 3 then\n        if #packet ~= 3 + (quantity + 7) // 8 then\n            return nil\n        end\n    else\n        if #packet ~= 3 + quantity * 2 then\n            return nil\n        end\n    end\n    if string.byte(packet,1,1) ~= slave then return nil end\n    if string.byte(packet,2,2) ~= func_code then return nil end\n    if string.byte(packet,3,3) ~= #packet-3 then return nil end\n    return string.sub(packet, 4)\nend\n\n\nlocal function gen_write_single_req(slave, func_code, data_addr, data)\n    return string.char(slave, func_code, data_addr >> 8, data_addr & 0x0ff) .. data\nend\n\n\nlocal function check_write_single_rsp(packet, slave, func_code, data_addr)\n    --return error msg\n    if #packet ~= 6 then return \"packet length\" end\n    if string.byte(packet,1,1) ~= slave then return \"slave address\" end\n    if string.byte(packet,2,2) ~= func_code  then return \"function code\" end\n    if string.byte(packet,3,3) ~= (data_addr >> 8)\n            or string.byte(packet,4,4) ~= (data_addr & 0x0ff) then\n        return \"data address\"\n    end\n\n    return nil\nend\n\n\nlocal function gen_write_multi_req(slave, func_code, start_addr, quantity, data)\n    return string.char(slave, func_code, start_addr >> 8, start_addr & 0x0ff,\n            quantity >> 8, quantity & 0x0ff, #data) .. data\nend\n\n\nlocal function check_write_multi_rsp(packet, slave, func_code, start_addr, quantity)\n    --return error msg\n    if #packet ~= 6 then return \"packet length\" end\n    if string.byte(packet,1,1) ~= slave then return \"slave address\" end\n    if string.byte(packet,2,2) ~= func_code  then return \"function code\" end\n    if string.byte(packet,3,3) ~= (start_addr >> 8)\n            or string.byte(packet,4,4) ~= (start_addr & 0x0ff) then\n        return \"data address\"\n    end\n    if string.byte(packet,5,5) ~= (quantity >> 8)\n            or string.byte(packet,6,6) ~= (quantity & 0x0ff) then\n        return \"quantity\"\n    end\n\n    return nil\nend\n\n\nlocal function printpacket(packet, result)\n    local output = {}\n    local head\n    if result == nil then\n        head = \"Tx:\"\n    elseif result < 0 then\n        head = \"Rx(\" .. result .. \"):\"\n    else\n        head = \"Rx:\"\n    end\n    table.insert(output, head)\n    local char_cnt = #head\n\n    for i=1, #packet do\n        if char_cnt > 120 then\n            print(table.concat(output, \" \"))\n            output = {}\n            char_cnt = 2\n        else\n            char_cnt = char_cnt + 3\n        end\n        table.insert(output, string.format(\"%02x\", string.byte(packet,i,i)))\n    end\n    print(table.concat(output, \" \"))\nend\n\n\nlocal once_callback, tcp_callback\n\nlocal function send_request(ctx, packet, timeout)\n    if ctx.type == swg.interface_rs485 then\n        if ctx.ascii then\n            packet = encode_ascii(packet, cal_lrc(packet))\n            swg.sndrcv485(packet, ctx.before_idle, timeout, 516, once_callback)\n        else\n            packet = append_crc16(packet)\n            swg.sndrcv485(packet, ctx.before_idle, timeout, 256, once_callback)\n        end\n    elseif ctx.tcp_rtu then\n        packet = append_crc16(packet)\n        -- send request and receive all response\n        -- we assume the response will come in one tcp packet\n        swg.sndrcvtcp(packet, 2, 257, timeout, once_callback)\n    else\n        if not ctx.trans_id then\n            ctx.trans_id = 0\n        else\n            ctx.trans_id = ctx.trans_id + 1\n            if ctx.trans_id > 65535 then ctx.trans_id = 0 end\n        end\n        packet = append_tcpheader(packet, ctx.trans_id)\n        -- send request and receive response header\n        swg.sndrcvtcp(packet, 6, 6, timeout, tcp_callback)\n        ctx.tcp_ts = swg.now()\n        ctx.tcp_header = nil\n    end\n\n    if ctx.debug then\n        printpacket(packet, nil)\n    end\nend\n\n\nlocal function send_read_request(ctx)\n    local device = ctx.devices[ctx.curr_devidx]\n    local readreq = device.readreqs[ctx.curr_reqidx]\n    ctx.curr_comm = {is_read=true, device=device}\n    local packet = gen_read_req(device.slave,\n        readreq[1], readreq[2], readreq[3])\n    send_request(ctx, packet, device.timeout)\nend\n\n\nlocal function send_write_request(ctx, device, point, output_reqs)\n    local req = output_reqs[1]\n    local bits_in_last_byte = req[1]\n    local addr = req[2]\n    local data = req[3]\n    local quantity, func_code\n    if #data < 1 then error(\"no data to write\") end\n\n    if bits_in_last_byte < 0 or bits_in_last_byte > 8 then\n        error(\"invalid bits_in_last_byte\")\n    end\n\n    if bits_in_last_byte > 0 then\n        if #data > 247 then\n            error(\"too much data to write\")\n        end\n        quantity = (#data - 1) * 8 + bits_in_last_byte\n        if quantity == 1 and device.writesinglecoil then\n            func_code = 5\n            if (string.byte(data,1,1) & 1) ~= 0 then\n                data = string.char(0xff, 0)\n            else\n                data = string.char(0, 0)\n            end\n        else\n            func_code = 15\n        end\n    else\n        if #data % 2 ~= 0 then\n            error(\"data length not round to 2\")\n        end\n        if #data > 246 then\n            error(\"too much data to write\")\n        end\n        quantity = #data / 2\n        if quantity == 1 and device.writesinglereg then\n            func_code = 6\n        else\n            func_code = 16\n        end\n    end\n\n    local packet\n    if func_code < 15 then\n        packet = gen_write_single_req(device.slave, func_code, addr, data)\n    else\n        packet = gen_write_multi_req(device.slave, func_code, addr, quantity, data)\n    end\n\n    table.remove(output_reqs, 1)\n    ctx.curr_comm = {is_read=false, device=device, point=point, func_code=func_code,\n            addr=addr, quantity=quantity, packet=packet, output_reqs=output_reqs}\n    send_request(ctx, packet, device.timeout)\nend\n\nlocal timer_callback\nlocal max_err_cnt = 3\n\nlocal function read_next(ctx)\n    local now = swg.now()\n    ctx.curr_reqidx = 1\n    ctx.err_cnt = 0\n::continue::\n    ctx.curr_devidx = ctx.curr_devidx + 1\n    if ctx.curr_devidx > #ctx.devices then\n        swg.timer(0, ctx.update_interval - (now - ctx.poll_ts), timer_callback)\n        return\n    end\n\n    local device = ctx.devices[ctx.curr_devidx]\n    if device.fail_ts and now - device.fail_ts <= ctx.offline_interval then\n        goto continue\n    end\n\n    send_read_request(ctx)\nend\n\n\nlocal function poll_restart(ctx)\n    ctx.poll_ts = swg.now()\n    ctx.curr_devidx = 0\n    read_next(ctx)\nend\n\n\nfunction timer_callback(ctx)\n::continue::\n    local devidx, pntidx, value = swg.firstwrite()\n    if devidx == nil then\n        local now = swg.now()\n        if now - ctx.poll_ts < ctx.update_interval then\n            swg.timer(0, ctx.update_interval - (now - ctx.poll_ts), timer_callback)\n        else\n            poll_restart(ctx)\n        end\n        return\n    end\n\n    local device = ctx.devices[devidx]\n    local point = device.points[pntidx]\n    if value == nil then    --relinguish\n        point.last_output = nil\n        goto continue\n    end\n\n    if device.fail_ts then goto continue end\n\n    local last_value\n    if point.last_output ~= nil then\n        last_value = point.last_output\n    else\n        local value, reliability = swg.pointvalue(devidx, pntidx)\n        if reliability == 0 then\n            last_value = value\n        end\n    end\n\n    if value == last_value then\n        point.last_output = value\n        goto continue\n    end\n\n    local output = point.output == nil and default_output or point.output\n\n    local output_reqs = output(device, point, value)\n    if output_reqs == nil or #output_reqs == 0 then     --no need to write\n        goto continue\n    end\n\n    local postoutput = point.postoutput == nil\n        and default_postoutput or point.postoutput\n\n    local output_value = postoutput(device, point, value, output_reqs)\n    point.last_output = output_value\n    if output_value == last_value then goto continue end\n\n    ctx.err_cnt = 0\n    send_write_request(ctx, device, point, output_reqs)\nend\n\n\nlocal function scan_commandable(ctx, devidx, start)\n    --For commandable object, when value readback doesnot match value outputed,\n    --we need to output again.\n    local device = ctx.devices[devidx]\n    local now = swg.now()\n\n    while true do\n        if start then\n            if device.curr_scanidx == nil then device.curr_scanidx = #device.points end\n            device.scan_start = device.curr_scanidx\n            start = false\n        elseif device.curr_scanidx == device.scan_start then break end\n\n        device.curr_scanidx = device.curr_scanidx + 1\n        if device.curr_scanidx > #device.points then\n            device.curr_scanidx = 1\n        end\n\n        local point = device.points[device.curr_scanidx]\n        if not point.commandable then goto continue end\n        if now - point.output_ts <= ctx.command_interval then goto continue end\n\n        local value = swg.getwrite(devidx, device.curr_scanidx)\n        if value == nil then    --relinguish\n            point.last_output = nil\n            goto continue\n        end\n\n        local last_value, last_reliability = swg.pointvalue(devidx,\n                device.curr_scanidx)\n        if last_reliability ~= 0 then last_value = nil end\n\n        if value == last_value then\n            point.last_output = value\n            goto continue\n        end\n\n        local output = point.output == nil and default_output or point.output\n\n        local output_reqs = output(device, point, value)\n        if output_reqs == nil or #output_reqs == 0 then goto continue end\n\n        local postoutput = point.postoutput == nil\n            and default_postoutput or point.postoutput\n\n        local output_value = postoutput(device, point, value, output_reqs)\n        point.last_output = output_value\n        if output_value == last_value then goto continue end\n\n        ctx.err_cnt = 0\n        send_write_request(ctx, device, point, output_reqs)\n        do return true end\n\n        ::continue::\n    end\n\n    return false --end of scan\nend\n\n\nlocal function write_callback(ctx, result, data)\n    if result >= 0 then\n        if check_exception(data, ctx.curr_comm.device.slave, ctx.curr_comm.func_code) ~= nil then\n            result = 0  --regards it as success\n        else\n            local err\n            if ctx.curr_comm.func_code < 15 then\n                err = check_write_single_rsp(data, ctx.curr_comm.device.slave,\n                    ctx.curr_comm.func_code, ctx.curr_comm.addr)\n            else\n                err = check_write_multi_rsp(data, ctx.curr_comm.device.slave,\n                    ctx.curr_comm.func_code, ctx.curr_comm.addr, ctx.curr_comm.quantity)\n            end\n            if err then\n                result = -100\n                ctx.err_cnt = max_err_cnt\n            end\n        end\n    end\n\n    if result < 0 then\n        if ctx.type ~= swg.interface_rs485 then swg.tcpreset() end\n\n        print(\"Write failed(\" .. result .. \") slave \"\n                .. ctx.curr_comm.device.slave .. \" func code \"\n                .. ctx.curr_comm.func_code .. \" addr \"\n                .. ctx_curr_comm.addr)\n        ctx.err_cnt = ctx.err_cnt + 1\n        if ctx.err_cnt < max_err_cnt then\n            send_request(ctx, ctx.curr_comm.packet, ctx.curr_comm.device.timeout)\n            return\n        end\n    elseif #ctx.curr_comm.output_reqs ~= 0 then     --next write req\n        send_write_request(ctx, ctx.curr_comm.device, ctx.curr_comm.point, ctx.curr_comm.output_reqs)\n        return\n    end\n\n    ctx.curr_comm.point.output_ts = swg.now()\n    ctx.curr_comm = nil\n\n    if ctx.curr_devidx > #ctx.devices then --write at end of polling\n        local now = swg.now()\n        if now - ctx.poll_ts > ctx.update_interval then\n            poll_restart(ctx)\n        else\n            swg.timer(0, ctx.update_interval - (now - ctx.poll_ts), timer_callback)\n        end\n    elseif result < 0 or not scan_commandable(ctx, ctx.curr_devidx, false) then\n        --give up when scan commandable point or scan finished\n        read_next(ctx)\n    end\nend\n\n\nlocal function read_callback(ctx, result, data)\n    local device = ctx.devices[ctx.curr_devidx]\n    local readreq = device.readreqs[ctx.curr_reqidx]\n\n    if result >= 0 then\n        if check_exception(data, device.slave, readreq[1]) ~= nil then\n            result = -1000\n        else\n            data = check_read_rsp(data, device.slave, readreq[1], readreq[3])\n            if data == nil then\n                result = -100\n            end\n        end\n\n        if result < 0 then\n            ctx.err_cnt = max_err_cnt\n        end\n    end\n\n    if result < 0 then\n        if ctx.type ~= swg.interface_rs485 then swg.tcpreset() end\n\n        print(\"Read failed(\" .. result .. \") on device \" .. ctx.curr_devidx\n                .. \" request \" .. ctx.curr_reqidx)\n        ctx.err_cnt = ctx.err_cnt + 1\n        if device.fail_ts or ctx.err_cnt >= max_err_cnt then\n            device.fail_ts = swg.now()  --flags for offline\n            swg.onoff(ctx.curr_devidx, false)\n            read_next(ctx)\n        else\n            send_read_request(ctx)\n        end\n        return\n    else\n        readreq[4] = data\n        ctx.curr_reqidx = ctx.curr_reqidx + 1\n        if ctx.curr_reqidx <= #device.readreqs then\n            ctx.err_cnt = 0\n            send_read_request(ctx)\n            return\n        end\n    end\n\n    swg.onoff(ctx.curr_devidx, true)\n    device.fail_ts = nil\n\n    for pidx, point in ipairs(device.points) do\n        local input = point.input == nil and default_input or point.input\n\n        if not point.commandable then point.last_output = nil end\n\n        local value, reliability = input(device, point)\n        if point.last_output ~= nil and value ~= point.last_output\n                and reliability == 0 then\n            local ignore_unmatch\n            if point.ignore_unmatch ~= nil then\n                ignore_unmatch = point.ignore_unmatch\n            else\n                ignore_unmatch = device.ignore_unmatch\n            end\n\n            if not ignore_unmatch then reliability = 7 end\n        end\n\n        swg.updatepoint(ctx.curr_devidx, pidx, value, reliability)\n    end\n\n    if scan_commandable(ctx, ctx.curr_devidx, true) then\n        return\n    end\n\n    read_next(ctx)\nend\n\n\nfunction once_callback(ctx, result, data)\n    if ctx.debug then\n        printpacket(data, result)\n    end\n\n    if result >= 0 then\n        if ctx.type == swg.interface_rs485 and ctx.ascii then\n            data = decode_ascii(data)\n        else\n            data = verify_crc16(data)\n        end\n        if data == nil then\n            result = -100\n        end\n    end\n\n    if ctx.curr_comm.is_read then\n        read_callback(ctx, result, data)\n    else\n        write_callback(ctx, result, data)\n    end\nend\n\n\nfunction tcp_callback(ctx, result, data)\n    if ctx.tcp_header == nil and result >= 0 then\n        local length = decode_tcpheader(data, ctx.trans_id)\n        if length == nil then\n            result = -100\n        else\n            ctx.tcp_header = data\n            local now = swg.now()\n            -- to receive response body\n            swg.sndrcvtcp(nil, length, length,\n                ctx.curr_comm.device.timeout - (now - ctx.tcp_ts),\n                tcp_callback)\n            return\n        end\n    end\n\n    if ctx.debug then\n        if ctx.tcp_header == nil then\n            printpacket(data, result)\n        else\n            printpacket(ctx.tcp_header .. data, result)\n        end\n    end\n\n    if ctx.curr_comm.is_read then\n        read_callback(ctx, result, data)\n    else\n        write_callback(ctx, result, data)\n    end\nend\n\n\nfunction setmodbusdata(readreqs, writable, bits_in_last_byte, addr, data)\n    --the value read from device is stored in readreqs\n    --when we write to device, we can update stored value\n    local quantity\n    if bits_in_last_byte == 0 then --holding register\n        quantity = #data / 2\n    else\n        quantity = (#data - 1) * 8 + bits_in_last_byte\n    end\n\n    for _, req in ipairs(readreqs) do\n        if addr >= req[2] + req[3] then goto continue end\n        if req[2] >= addr + quantity then goto continue end\n\n        local t = {}\n        if bits_in_last_byte == 0 then --holding register\n            if req[1] ~= (writable and 3 or 4) then goto continue end\n\n            if addr > req[2] then\n                table.insert(t, string.sub(req[4], 1, (addr - req[2]) * 2))\n                table.insert(t, string.sub(data, 1, (req[2] + req[3] - addr) * 2))\n            else\n                table.insert(t, string.sub(data, (req[2] - addr) * 2 + 1,\n                        (req[2] + req[3] - addr) * 2))\n            end\n\n            local left = req[2] + req[3] - addr - quantity\n            if left > 0 then\n                table.insert(t, string.sub(req[4], left * -2))\n            end\n        else    --coil\n            if req[1] ~= (writable and 1 or 2) then goto continue end\n\n            local bits = (addr - req[2]) % 8\n\n            local startidx = (addr - req[2]) // 8 + 1\n            if startidx > 1 then\n                table.insert(t, string.sub(req[4], 1, startidx - 1))\n            end\n\n            local endidx = (addr + quantity - req[2]) // 8\n            local srcidx = (req[2] - addr) // 8 + 1\n            local half\n\n            if startidx < 1 then\n                startidx = 1\n                half = string.byte(data, srcidx, srcidx)\n                half = half >> (8 - bits)   -- save highest bits\n                srcidx = srcidx + 1\n            end\n\n            if srcidx < 1 then\n                srcidx = 1\n                half = string.byte(req[4], startidx, startidx)\n                half = half & ~(-1 << bits)     --save lowest bits\n            end\n\n            if endidx > #req[4] then endidx = #req[4] end\n\n            for _ = startidx, endidx do\n                local byte = string.byte(data, srcidx, srcidx)\n                table.insert(t, string.char(half + ((byte << bits) & 0x0ff)))\n                half = byte >> (8 - bits)\n                srcidx = srcidx + 1\n            end\n\n            if endidx < #req[4] then\n                local left_bits = (addr + quantity - req[2]) % 8\n                local byte\n                if left_bits > bits then\n                    byte = string.byte(data, srcidx, srcidx)\n                    half = half + (byte << bits)\n                end\n                half = half & ~(-1 << left_bits)\n                byte = string.byte(req[4], endidx+1, endidx+1)\n                byte = byte & (-1 << left_bits)  -- use highest bits\n                table.insert(t, string.char(half + byte))\n            end\n\n            table.insert(t, string.sub(req[4], endidx+2))\n        end\n\n        req[4] = table.concat(t)\n        ::continue::\n    end\nend\n\n\nfunction getmodbusdata(readreqs, func_code, addr, quantity)\n    for reqidx = #readreqs, 1, -1 do\n        local req = readreqs[reqidx]\n        if req[1] ~= func_code then goto continue end\n        if req[2] > addr then goto continue end\n        if addr + quantity > req[2] + req[3] then goto continue end\n\n        if req[4] == nil then return nil end\n\n        if func_code >= 3 then   --register\n            return req[4]:sub((addr - req[2]) * 2 + 1, (addr + quantity - req[2]) * 2)\n        else    --coil or discrete\n            local bits = (addr - req[2]) % 8\n            local t = {}\n\n            local startidx = (addr - req[2]) // 8 + 1\n            local endidx = (addr + quantity - req[2] + 7) // 8\n            local half = req[4]:byte(startidx, startidx)\n            half = half >> bits     -- save highest bits\n\n            for idx = startidx + 1, endidx do\n                local byte = req[4]:byte(idx, idx)\n                table.insert(t, string.char(half + ((byte << (8 - bits)) & 0x0ff)))\n                half = byte >> bits\n            end\n\n            if quantity > #t * 8 then\n                table.insert(t, string.char(half))\n            end\n\n            return table.concat(t)\n        end\n\n        ::continue::\n    end\n\n    error(\"no readreqs match\")\nend\n\n\nfunction reorder_registers(value)\n    local ordered = \"\"\n    for i = 1, #value, 2 do\n        ordered = value:sub(i, i+1) .. ordered\n    end\n    return ordered\nend\n\n\nfunction default_input(device, point)\n    --return BACnet value, reliability\n    local value\n    if point.obj_type == \"b\" then\n        value = getmodbusdata(device.readreqs, point.tag.func_code,\n                point.tag.addr, 1)\n\n        if point.tag.func_code >= 3 then\n            value = string.unpack(((device.endian & 1 ~= 0) and \"<I2\" or \">I2\"),\n                    value)\n            value = (value & (1 << point.tag.bit)) ~= 0\n        else\n            value = (value:byte(1,1) & 1) ~= 0\n        end\n    else\n        value = getmodbusdata(device.readreqs, point.tag.func_code,\n                point.tag.addr, (point.tag.bitlen + 15) // 16)\n        if #value > 2 and (device.endian & 2) ~= 0 then\n            --register order not consistent with byte order, reorder it\n            value = reorder_registers(value)\n        end\n\n        if point.tag.datatype == 2 then --float\n            local format = ((device.endian & 1 ~= 0) and \"<\" or \">\") ..\n                    ((point.tag.bitlen==32) and \"f\" or \"d\")\n            value = string.unpack(format, value)\n            if value ~= value then  --NaN\n                return nil, 7   --unreliable other\n            end\n        else\n            local format = ((device.endian & 1 ~= 0) and \"<I\" or \">I\") ..\n                   ((point.tag.bitlen + 15) // 16) * 2\n            value = string.unpack(format, value)\n\n            if point.tag.datatype == 0 then --unsigned\n                if value < 0 then   --overflow\n                    value = ((-1 >> 2) + 1) * 4.0 + value\n                end\n                if point.tag.bitlen < 16 then\n                    --multistate/analog on partial register\n                    value = (value >> point.tag.bit)\n                            & ~(-1 << point.tag.bitlen)\n                end\n            else    --signed\n                if point.tag.bit then value = value >> point.tag.bit end\n                if value & (1 << (point.tag.bitlen - 1)) ~= 0 then --negative\n                    value = -1 - ((~value) & ~(-1 << point.tag.bitlen))\n                end\n            end\n        end\n\n        if point.obj_type == 'a' then   --analog\n            value = value * 1.0\n            if point.tag.scale ~= nil then\n                value = value * point.tag.scale\n            end\n            if point.tag.offset ~= nil then\n                value = value + point.tag.offset\n            end\n        elseif point.tag.ms_values == nil then --multistate default mapping\n            value = value + 1\n            if value > point.max_value then --overflow\n                return nil, 7   --unreliable other\n            end\n        else --multisate with value mapping\n            for i = 1, point.max_value do\n                if value == point.tag.ms_values[i] then\n                    return i, 0\n                end\n            end\n            return nil, 7\n        end\n    end\n\n    return value, 0\nend\n\n\nfunction default_postoutput(device, point, value, output_reqs)\n    --return BACnet Value actually output\n    for _, req in ipairs(output_reqs) do\n        setmodbusdata(device.readreqs,\n                point.tag.func_code&1 == 1,  --coil or holding from func_code\n                table.unpack(req))\n    end\n    local input = point.input == nil and default_input or point.input\n    return input(device, point)\nend\n\n\nfunction default_output(device, point, value)\n    --return {{bits_in_last_byte, addr, data}}\n    local bits_in_last_byte, output, last, format\n    if point.tag.func_code >= 3 and point.tag.bitlen < 16 then\n        last = getmodbusdata(device.readreqs, point.tag.func_code,\n                point.tag.addr, 1)\n        format = (device.endian & 1 ~= 0) and \"<I2\" or \">I2\"\n        last = string.unpack(format, last)\n    end\n\n    if point.obj_type == 'a' then\n        if point.tag.offset ~= nil then value = value - point.tag.offset end\n        if point.tag.scale ~= nil then value = value / point.tag.scale end\n    end\n\n    if point.obj_type == \"b\" then\n        if point.tag.func_code >= 3 then\n            last = last & ~(1 << point.tag.bit)\n            if value then last = last + (1 << point.tag.bit) end\n            output = string.pack(format, last)\n            bits_in_last_byte = 0\n        else\n            output = string.char((value and 1 or 0))\n            bits_in_last_byte = 1\n        end\n    elseif point.tag.datatype == 2 then     --float\n        format = ((device.endian & 1 ~= 0) and \"<\" or \">\") ..\n                ((point.tag.bitlen==32) and \"f\" or \"d\")\n        output = string.pack(format, value)\n        bits_in_last_byte = 0\n    else\n        if point.obj_type == 'm' then   --multistate\n            value = point.tag.ms_values and point.tag.ms_values[value]\n                or (value - 1) \n        else    --analog\n            if point.tag.datatype == 0 then     --unsigned\n                local max = ~(-1 << point.tag.bitlen)\n                if value <= 0 then value = 0\n                elseif max < 0 then     --lua integer overflow\n                    if -value <= ~(max >> 1) then\n                        value = max\n                    else\n                        value = math.floor(-value + 0.5)\n                        value = ~value + 1\n                    end\n                elseif value >= max then value = max\n                else value = math.floor(value + 0.5) end\n            else    --signed\n                local max =  (1 << (point.tag.bitlen - 1)) - 1\n                local min = -1 - max\n                if value >= max then value = max\n                elseif value <= min then value = min\n                else value = math.floor(value + 0.5) end\n            end\n        end\n\n        if point.tag.bitlen < 16 then\n            last = last & ~((~(-1 << point.tag.bitlen)) << point.tag.bit)\n            value = last + (value << point.tag.bit)\n        end\n\n        format = ((device.endian & 1 ~= 0) and \"<\" or \">\") ..\n                ((point.tag.datatype == 0) and \"I\" or \"i\") ..\n                ((point.tag.bitlen + 15) // 16) * 2\n        output = string.pack(format, value)\n        bits_in_last_byte = 0\n    end\n\n    if #output > 2 and (device.endian & 2) ~= 0 then\n        --register order not consistent with byte order, reorder it\n        output = reorder_registers(output)\n    end\n\n    if point.obj_type == 'a' and point.tag.datatype ~= 3 then\n        --ananlog present_value is float, so 2 different BACnet values may map\n        --to same Modbus result, when the datatype is not float, check it\n        last = getmodbusdata(device.readreqs, point.tag.func_code,\n                point.tag.addr, (point.tag.bitlen + 15) // 16)\n        if output == last then return nil end\n    end\n\n    return {{bits_in_last_byte, point.tag.addr, output}}\nend\n\n\nlocal function check_readreqs(readreqs)\n    if readreqs == nil then\n        error(\"readreqs not defined\")\n    end\n    if #readreqs == 0 then\n        error(\"empty readreqs\")\n    end\n\n    for _, req in ipairs(readreqs) do\n        if req[1] == nil or req[1] ~= math.floor(req[1])\n                or req[1] < 1 or req[1] > 4 then\n            error(\"invalid func_code\")\n        end\n\n        if req[2] == nil or req[2] ~= math.floor(req[2])\n                or req[2] < 0 or req[2] > 65535 then\n            error(\"invalid star_addr\")\n        end\n\n        if req[3] == nil or req[3] ~= math.floor(req[3])\n                or req[3] < 1 then\n            error(\"invalid quantity\")\n        end\n        if req[2] + req[3] > 65536 then\n            error(\"address space overflow\")\n        end\n        if (req[1] < 3 and req[3] > 2000)\n                or (req[1] >= 3 and req[3] > 125) then\n            error(\"too large quantity\")\n        end\n\n        req[4] = nil\n    end\nend\n\n\nlocal function check_multistate(point, tag)\n    local values = tag.ms_values\n    if type(values) ~= \"table\" then\n        error(\"ms_values shall be a table\")\n    end\n\n    if #values < point.max_value then\n        error(\"length of ms_values shall be greater or equal to number_of_states\")\n    end\n\n    local reverse = {}\n    local max = (1 << tag.bitlen) - 1\n    for i = 1, point.max_value do\n        if type(values[i]) ~= \"number\" then\n            error(\"There is no number in ms_values\")\n        end\n        local v = math.floor(values[i])\n        if v < -(max + 1) or v > max then\n            error(\"value in ms_values over range\")\n        end\n        v = v & max\n        if reverse[v] ~= nil then\n            error(\"duplicated value in ms_values\")\n        end\n        values[i] = v\n        reverse[v] = i\n    end\nend\n\n\nlocal function check_bit(tag)\n    if tag.bit == nil then tag.bit = 0\n    elseif tag.bit ~= math.floor(tag.bit)\n            or tag.bit < 0 or tag.bit > 15 then\n        error(\"invalid bit\")\n    end\n\n    if tag.bitlen == nil then\n        tag.bitlen = 16 - tag.bit\n    elseif tag.bitlen ~= math.floor(tag.bitlen)\n            or tag.bitlen < 1 or tag.bitlen > 16 - tag.bit then\n        error(\"invalid bitlen\")\n    end\nend\n\n\nlocal function init_point(point)\n    local tag = load(\"return {\" .. point.tag .. \"\\n}\", \"point tag\", \"t\", _ENV)\n    if tag == nil then\n        error(\"invalid tag: \" .. point.tag)\n    end\n    tag = tag()\n    if tag.input ~= nil then\n        if type(tag.input) ~= \"function\" then\n            error(\"input shall be function\")\n        end\n        point.input = tag.input\n        tag.input = nil\n    end\n    if point.commandable then\n        point.ignore_unmatch = tag.ignore_unmatch\n        tag.ignore_unmatch = nil\n    end\n    if not point.read_only then\n        if tag.output ~= nil then\n            if type(tag.output) ~= \"function\" then\n                error(\"output shall be function\")\n            end\n            point.output = tag.output\n            tag.output = nil\n        end\n        if tag.postoutput ~= nil then\n            if type(tag.postoutput) ~= \"function\" then\n                error(\"postoutput shall be function\")\n            end\n            point.postoutput = tag.postoutput\n            tag.postoutput = nil\n        end\n        point.output_ts = 0.0\n    end\n\n    if point.input == nil or (point.output == nil and not point.read_only) then\n        if tag.func_code ~= math.floor(tag.func_code) or\n                tag.func_code < 1 or tag.func_code > 4 then\n            error(\"invalid func_code\")\n        end\n\n        if tag.addr == nil\n                or tag.addr ~= math.floor(tag.addr)\n                or tag.addr < 0 or tag.addr > 65535 then\n            error(\"invalid addr\")\n        end\n\n        if point.obj_type ~= 'b' and tag.func_code < 3 then\n            error(\"coil/discrete can not map to ananlog or multistate\")\n        end\n\n        if not point.read_only and point.output == nil and (tag.func_code & 1) == 0 then\n            error(\"discrete or input register cannot map to writable/commandable object\")\n        end\n\n        if tag.func_code >= 3 then\n            if point.obj_type == 'b' then\n                if tag.bit == nil or tag.bit ~= math.floor(tag.bit)\n                        or tag.bit < 0 or tag.bit > 15 then\n                    error(\"invalid bit\")\n                end\n                tag.datatype = nil\n                tag.bitlen = 1\n            elseif point.obj_type == 'm' then\n                if tag.datatype == nil or tag.datatype == \"u16\" then\n                    check_bit(tag)\n                elseif tag.datatype == \"u32\" then\n                    tag.bitlen = 32\n                else\n                    error(\"invalid datatype\")\n                end\n                tag.datatype = 0\n                if tag.ms_values ~= nil then\n                    check_multistate(point, tag)\n                end\n            else    --analog\n                if tag.scale ~= nil and type(tag.scale) ~= \"number\" then\n                    error(\"scale shall be number\")\n                elseif tag.scale == 0 or tag.scale ~= tag.scale\n                        or tag.scale == math.huge\n                        or tag.scale == -math.huge then\n                    error(\"invalid scale\")\n                end\n\n                if tag.offset ~= nil and type(tag.offset) ~= \"number\" then\n                    error(\"offset shall be number\")\n                elseif tag.offset ~= tag.offset or tag.offset == math.huge\n                        or tag.offset == -math.huge then\n                    error(\"invalid offset\")\n                end\n\n                if tag.datatype == nil then\n                    tag.datatype = \"u16\"\n                elseif tag.datatype == \"s16\" or tag.datatype == \"u16\" then\n                elseif tag.datatype == \"s32\" or tag.datatype == \"u32\"\n                        or tag.datatype == \"f32\" then\n                    tag.bitlen = 32\n                elseif tag.datatype == \"s64\" or tag.datatype == \"u64\"\n                        or tag.datatype == \"f64\" then\n                    tag.bitlen = 64\n                else\n                    error(\"invalid datatype\")\n                end\n\n                if string.sub(tag.datatype, 2) == \"16\" then\n                    check_bit(tag)\n                end\n\n                if string.sub(tag.datatype,1,1) == \"u\" then\n                    tag.datatype = 0\n                elseif string.sub(tag.datatype,1,1) == \"s\" then\n                    tag.datatype = 1\n                else\n                    tag.datatype = 2\n                end\n            end\n\n            if tag.bitlen % 16 == 0 then tag.bit = nil end\n\n            if tag.addr + tag.bitlen / 16 > 65536 then\n                error(\"address space overflow\")\n            end\n        end\n    end\n\n    point.tag = tag\nend\n\n\nlocal function init(ctx)\n    local tag = load(\"return {\" .. ctx.tag .. \"\\n}\", \"bus tag\", \"t\", _ENV)\n    if tag == nil then\n        error(\"invalid bus tag: \" .. ctx.tag)\n    end\n    tag = tag()\n\n    if tag.update_interval ~= nil then\n        ctx.update_interval = tag.update_interval\n        tag.update_interval = nil\n    else\n        ctx.update_interval = 10000\n    end\n\n    if tag.offline_interval ~= nil then\n        ctx.offline_interval = tag.offline_interval\n        tag.offline_interval = nil\n        if ctx.offline_interval < ctx.update_interval then\n            ctx.offline_interval = ctx.update_interval\n        end\n    else\n        ctx.offline_interval = ctx.update_interval * 2\n    end\n\n    if tag.command_interval ~= nil then\n        ctx.command_interval = tag.command_interval\n        tag.command_interval = nil\n    else\n        ctx.command_interval = ctx.update_interval * 2\n    end\n\n    ctx.tcp_rtu = tag.tcp_rtu\n    tag.tcp_rtu = nil\n    ctx.ascii = tag.ascii\n    tag.ascii = nil\n    ctx.debug = tag.debug\n    tag.debug = nil\n\n    if ctx.type == swg.interface_rs485 then\n        if tag.before_idle ~= nil then\n            ctx.before_idle = tag.before_idle\n            tag.before_idle = nil\n        elseif ctx.ascii then\n            ctx.before_idle = 0\n        else\n            ctx.before_idle = 1000 / ctx.baudrate * 11 * 3.5\n        end\n    end\n\n    if tag.global_define ~= nil then\n        for name, value in pairs(tag.global_define) do\n            _ENV[name] = value\n        end\n        tag.global_define = nil\n    end\n\n    ctx.tag = tag\n\n    for _, device in ipairs(ctx.devices) do\n        tag = load(\"return {\" .. device.tag .. \"\\n}\", \"device tag\", \"t\", _ENV)\n        if tag == nil then\n            error(\"invalid device tag: \" .. device.tag)\n        end\n        tag = tag()\n\n        if tag.slave == nil or tag.slave ~= math.floor(tag.slave)\n                or tag.slave < 0 or tag.slave > 255 then\n            error(\"invalid slave\")\n        end\n        device.slave = tag.slave\n        tag.slave = nil\n\n        check_readreqs(tag.readreqs)\n        device.readreqs = tag.readreqs\n        tag.readreqs = nil\n\n        if tag.timeout ~= nil then\n            device.timeout = math.floor(tag.timeout)\n            tag.timeout = nil\n        else\n            device.timeout = 1000\n        end\n\n        device.writesinglecoil = tag.writesinglecoil\n        tag.writesinglecoil = nil\n        device.writesinglereg = tag.writesinglereg\n        tag.writesinglereg = nil\n\n        if tag.endian ~= nil then\n            device.endian = math.floor(tag.endian)\n            if device.endian < 1 or device.endian > 4 then\n                error(\"invalid endian\")\n            end\n            tag.endian = nil\n        else\n            device.endian = 4\n        end\n\n        device.ignore_unmatch = tag.ignore_unmatch\n        tag.ignore_unmatch = nil\n\n        device.tag = tag\n        device.err_cnt = max_err_cnt;\n        for _, point in ipairs(device.points) do\n            init_point(point)\n            if point.input == nil then  --test readreqs has its value\n                getmodbusdata(device.readreqs, point.tag.func_code,\n                        point.tag.addr,\n                        (point.tag.func_code >= 3\n                                and ((point.tag.bitlen + 15) // 16) or 1))\n            end\n\n            if type(point.tag.init) == \"function\" then\n                local user_init = point.tag.init\n                point.tag.init = nil\n                user_init(ctx, device, point)\n            end\n        end\n\n        if type(device.tag.init) == \"function\" then\n            local user_init = device.tag.init\n            device.tag.init = nil\n            user_init(ctx, device)\n        end\n    end\n\n    if type(ctx.tag.init) == \"function\" then\n        local user_init = ctx.tag.init\n        ctx.tag.init = nil\n        user_init(ctx)\n    end\n\n    poll_restart(ctx)\nend\n\n\nreturn swg.interface_rs485 | swg.interface_tcp, init\n"}, "type": 1, "parity": "none1", "baudrate": 9600, "bit7": false, "frame_ms": 4.010416666666667, "resource": "RS485-1", "fcdevices": [{"enable": true, "name": "Device", "description": "", "instance": 1000, "fcpoints": [{"name": "temperature1", "description": "normal point", "instance": 0, "enable": true, "object_type": "ai", "unit": 62, "cov_increment": 0, "tag": "func_code=3,\naddr=24,\ndatatype=\"u16\",\noffset=0,\nscale=0.1"}, {"name": "temperature2", "description": "normal point", "instance": 1, "enable": true, "object_type": "ai", "unit": 62, "cov_increment": 0, "tag": "func_code=3,\naddr=27,\ndatatype=\"u16\",\noffset=0,\nscale=0.1"}, {"name": "lamp1", "description": "asymmetrical read/write", "instance": 0, "enable": true, "object_type": "bo", "polarity": false, "tag": "func_code=3,\naddr=31,\nbit=0"}, {"name": "lamp2", "description": "asymmetrical read/write", "instance": 1, "enable": true, "object_type": "bo", "polarity": false, "tag": "func_code=3,\naddr=31,\nbit=1"}, {"name": "temperature3", "description": "analog map to discrete input", "instance": 1, "enable": true, "object_type": "bi", "polarity": false, "tag": "func_code=2,\naddr=7"}, {"name": "load1", "description": "write-only, low byte", "instance": 1, "enable": false, "object_type": "ao", "unit": 98, "cov_increment": 0, "tag": "func_code=3,\naddr=99,\ndatatype=\"u16\",\noffset=0,\nscale=1"}, {"name": "load2", "description": "write only, high byte", "instance": 1, "enable": false, "object_type": "ao", "unit": 98, "cov_increment": 0, "tag": "func_code=3,\naddr=99,\ndatatype=\"u16\",\noffset=0,\nscale=1"}], "tag": "slave=1,\ntimeout=1000,\nendian=4,\nreadreqs={\n{2, 7, 1},\n{3, 24, 8},\n}"}]}