Snippets

Steve Reid NodeRED_Simple_BACnet_client_example

Created by Steve Reid last modified
[{"id":"dc7095e8.9a33d8","type":"tab","label":"Load config"},{"id":"e0eb21a2.82121","type":"tab","label":"BACnet/IP interface"},{"id":"5b01eb4c.5e5e24","type":"tab","label":"Timeouts"},{"id":"9d3d1941.54a568","type":"tab","label":"Examples"},{"id":"dfcb3b42.515a08","type":"mqtt-broker","z":"","broker":"127.0.0.1","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"willTopic":"","willQos":"0","willPayload":"","birthTopic":"","birthQos":"0","birthPayload":""},{"id":"cdb8e9b0.57f0d8","type":"ui_base","theme":{"name":"theme-light","lightTheme":{"default":"#0094CE","baseColor":"#0094CE","baseFont":"Helvetica Neue","edited":true,"reset":false},"darkTheme":{"default":"#097479","baseColor":"#097479","baseFont":"Helvetica Neue","edited":false},"customTheme":{"name":"Untitled Theme 1","default":"#4B7930","baseColor":"#4B7930","baseFont":"Helvetica Neue"},"themeState":{"base-color":{"default":"#0094CE","value":"#0094CE","edited":false},"page-titlebar-backgroundColor":{"value":"#0094CE","edited":false},"page-backgroundColor":{"value":"#fafafa","edited":false},"page-sidebar-backgroundColor":{"value":"#ffffff","edited":false},"group-textColor":{"value":"#1bbfff","edited":false},"group-borderColor":{"value":"#ffffff","edited":false},"group-backgroundColor":{"value":"#ffffff","edited":false},"widget-textColor":{"value":"#111111","edited":false},"widget-backgroundColor":{"value":"#0094ce","edited":false},"widget-borderColor":{"value":"#ffffff","edited":false}}},"site":{"name":"Node-RED Dashboard","hideToolbar":"false","allowSwipe":"false","dateFormat":"DD/MM/YYYY","sizes":{"sx":48,"sy":48,"gx":6,"gy":6,"cx":6,"cy":6,"px":0,"py":0}}},{"id":"37f1f19a.7d3d5e","type":"udp in","z":"e0eb21a2.82121","name":"BACnet/IP_In","iface":"","port":"47808","ipv":"udp4","multicast":"false","group":"","datatype":"buffer","x":206.69832611083984,"y":694.1349201202393,"wires":[["bca8c5bc.a38bb8","56afe7a8.8ba808"]]},{"id":"f505dd40.a2ecb","type":"debug","z":"e0eb21a2.82121","name":"UDP data to send","active":false,"console":"false","complete":"true","x":1010.8809814453125,"y":342.36512756347656,"wires":[]},{"id":"8f910e9d.bd567","type":"function","z":"e0eb21a2.82121","name":"Construct BACnet/IP message","func":"// Takes a payload and makes a BACnet/IP\n// request message buffer out of it.\n    var buf;\n    var i;\n    var p=0;\n    var BACnetObjInstance;\n    var PropertyID;\n    var npdudata;\n    var macinfo;\n    var testdata;\n\n    msg.ip = msg.payload.IP;\n    msg.port = 47808;\n\n\n    var invokeid = global.get('invokeid')||0;\n    invokeid += 1;\n    if(invokeid > 255) invokeid = 0;\n\n    buf = new Buffer(1024);\n    \n    switch(msg.payload.Object)\n    {\n        default:\n        case 'AI':\n            BACnetObjInstance = 0 + msg.payload.Instance;\n            PropertyID = 85;\n            break;\n            \n        case 'AO':\n            BACnetObjInstance = (1<<22) + msg.payload.Instance;\n            PropertyID = 85;\n            break;\n            \n        case 'AV':\n            BACnetObjInstance = (2<<22) + msg.payload.Instance;\n            PropertyID = 85;\n            break;\n            \n        case 'BI':\n            BACnetObjInstance = (3<<22) + msg.payload.Instance;\n            PropertyID = 85;\n            break;\n            \n        case 'BO':\n            BACnetObjInstance = (4<<22) + msg.payload.Instance;\n            PropertyID = 85;\n            break;\n            \n        case 'BV':\n            BACnetObjInstance = (5<<22) + msg.payload.Instance;\n            PropertyID = 85;\n            break;\n            \n        case 'MSI':\n            BACnetObjInstance = (13<<22) + msg.payload.Instance;\n            PropertyID = 85;\n            break;\n\n        case 'TS':\n            BACnetObjInstance = (17<<22) + msg.payload.Instance;\n            PropertyID = 85;\n            break;\n\n        case 'MSV':\n            BACnetObjInstance = (19<<22) + msg.payload.Instance;\n            PropertyID = 85;\n            break;\n\n        case 'AC':\n            BACnetObjInstance = (23<<22) + msg.payload.Instance;\n            PropertyID = 85;\n            break;\n\n        case 'PC':\n            BACnetObjInstance = (24<<22) + msg.payload.Instance;\n            PropertyID = 85;\n            break;\n\n    }\n\n//bvlc\n    buf[p++] = 0x81;\n    buf[p++] = 0x0a;\n    buf[p++] = 0x00; //holding value for apdu length hb\n    buf[p++] = 0xff; //holding value for apdu length lb\n    \n//npdu\n    if(msg.payload.Network!=-1) //routing required\n    {\n        buf[p++] = 0x01;\n        buf[p++] = 0x24;\n        buf[p++] = (msg.payload.Network>>8) & 0xff;\n\t\tbuf[p++] = msg.payload.Network & 0xff;\n\t\tswitch(msg.payload.maclen)\n\t\t{\n\t\t    case 1:\n                buf[p++] = 0x01;    \n                buf[p++] = msg.payload.mac & 0xff;\n\t\t        break;\n\t\t        \n            case 2:\n                buf[p++] = 0x02;    \n                buf[p++] = (msg.payload.mac>>8) & 0xff;\n                buf[p++] = msg.payload.mac & 0xff;\n\t        \tbreak;\n\t        \t\n\t        case 5:\n                buf[p++] = 0x06;    \n                buf[p++] = msg.payload.mac[0] & 0xff;\n        \t    buf[p++] = msg.payload.mac[1] & 0xff;\n                buf[p++] = msg.payload.mac[2] & 0xff;\n        \t    buf[p++] = msg.payload.mac[3] & 0xff;\n                buf[p++] = msg.payload.mac[4] & 0xff;\n\t            buf[p++] = msg.payload.mac[5] & 0xff;\n\t        \tbreak;\n\t\t}\n\t\tbuf[p++] = 0xff;\n    }\n    else\n    {\n        buf[p++] = 0x01;\n        buf[p++] = 0x04; \n    }\n    \n//apdu\n    if(msg.payload.cov == 1)\n    {\n\t\tnode.status({fill:\"blue\",shape:\"ring\",text:\"Subscribe COV\"});\n\t\tbuf[p++] = 0x00;\n\t\tbuf[p++] = 0x03;\n\t\tbuf[p++] = invokeid;\n\t\tbuf[p++] = 0x05;\n\t\tbuf[p++] = 0x0a;\n\t\tbuf[p++] = (msg.payload.objectid>>8) & 0xff;\n\t\tbuf[p++] = msg.payload.objectid & 0xff;\n\t\tbuf[p++] = 0x1c;\n\t\tbuf[p++] = (BACnetObjInstance>>24) & 0xff;\n\t\tbuf[p++] = (BACnetObjInstance>>16) & 0xff;\n\t\tbuf[p++] = (BACnetObjInstance>>8) & 0xff;\n\t\tbuf[p++] = (BACnetObjInstance) & 0xff;\n\t\tbuf[p++] = 0x29;\n\t\tbuf[p++] = 0x00;\n\t\tbuf[p++] = 0x3a;  //\n\t\tbuf[p++] = 0x02;  // subscribe for 10 mins \n\t\tbuf[p++] = 0x58;  //\n    }\n    else\n    {\n\t\tnode.status({fill:\"blue\",shape:\"ring\",text:\"Read Property\"});\n\t\tbuf[p++] = 0x00;\n\t\tbuf[p++] = 0x05;\n\t\tbuf[p++] = invokeid;\n\t\tbuf[p++] = 0x0c;\n\t\tbuf[p++] = 0x0c;\n\t\tbuf[p++] = (BACnetObjInstance>>24) & 0xff;\n\t\tbuf[p++] = (BACnetObjInstance>>16) & 0xff;\n\t\tbuf[p++] = (BACnetObjInstance>>8) & 0xff;\n\t\tbuf[p++] = (BACnetObjInstance) & 0xff;\n\t\tbuf[p++] = 0x19;\n\t\tbuf[p++] = PropertyID;\n    }\n    //now go back and set the message length in the bvlc..  \n    buf[2] = p / 0xff;\n    buf[3] = p % 0xff;\n\n    // store the invokeid value\n    global.set('invokeid',invokeid);\n    context.global.IntObj[msg.payload.objectid].tx_id = invokeid;\n    // set timeout\n    context.global.IntObj[msg.payload.objectid].valid = 5;\n    msg.payload = buf.slice(0,p);\nreturn msg;","outputs":1,"noerr":0,"x":683.7657470703125,"y":398.5158386230469,"wires":[["f505dd40.a2ecb","687ef3d.0c25f0c","684c37c5.91ffd8"]]},{"id":"bca8c5bc.a38bb8","type":"function","z":"e0eb21a2.82121","name":"Process BACnet/IP response","func":"function getObjectID(invokeid)\n{\n\tvar i;\n\tvar found = 0;\n\tvar maxobjects = global.get('max_objects');\n\tvar id = -1;\n\t\n    for(i=0;i<maxobjects;i++)\n\t{\n\t\tif(context.global.IntObj[i].tx_id == invokeid)\n\t\t{\n\t\t\tfound = 1;\n\t\t\t//found match of invoke id\n\t\t\tif(context.global.IntObj[i].valid>=0)\n\t\t\t{\n\t\t\t\t//has not timed out\n\t\t\t\tid = i;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\t//has timed out so clean up\n\t\t\t\tid = -1;\n\t\t\t\tcontext.global.IntObj[i].tx_id = -1;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn id;\n}\n\nfunction getDevice(deviceID)\n{\n\tvar i;\n\tvar found = 0;\n\tvar maxdevices = context.global.Device.length;\n\tvar id = -1;\n\t\n    for(i=0;i<maxdevices;i++)\n\t{\n\t\tif(context.global.Device[i].DeviceID == deviceID)\n\t\t{\n\t\t\tid = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn id;\n}\n\nfunction getNetwork(netnum)\n{\n\tvar i;\n\tvar found = 0;\n\tvar maxnetworks = context.global.Network.length;\n\tvar id = -1;\n\t\n    for(i=0;i<maxnetworks;i++)\n\t{\n\t\tif(context.global.Network[i].number == netnum)\n\t\t{\n\t\t\tid = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn id;\n}\n\n\nvar msg2 = {}; //send update to MQTT\nvar oid;\nvar svc;\nvar dnet = -1;\nvar snet = -1;\nvar sadr = -1;\nvar slen = -1;\n\nconst prval = Buffer.from([0x4e, 0x09, 0x55]);\nvar apdustart;\n\n\nif(msg.payload[0] == 0x81)\n{\n    //check NPDU information to calculate start of APDU \n    var stop = 0;\n    switch(msg.payload[5])\n    {\n        case 0x00:\n            apdustart = 6;\n            break;\n            \n        case 0x80: //Network message\n            switch(msg.payload[6])\n            {\n                case 0x01: //I Am Router to Network\n                    var n = 7;\n                    while(msg.payload[n]!==0 && msg.payload[n+1]!==0 && n<msg.payload.length)\n                    {\n    \t\t\t        var netnum = msg.payload.readUInt16BE(n);\n                        var netid = getNetwork(netnum);\n                        if(netid != -1)\n                        {\n                            context.global.Network[netid].routerIP = msg.ip;\n                        }\n                        node.status({fill:\"blue\",shape:\"ring\",text:\"I AM Router to: \" + netnum + \" = \" + msg.ip });\n                        n+=2;\n                    }\n                    break;\n            }\n            stop = 1; //end of processing for NSDU\n            break;\n            \n        case 0x08:\n            apdustart = 9 + msg.payload[8]; // note depends on maclen\n            snet = msg.payload.readUInt16BE(6); \n            slen = msg.payload[8];\n            switch(slen)\n            {\n                case 1:\n                    sadr = msg.payload[9];\n                    break;\n                    \n                case 2:\n                    sadr = msg.payload.readUInt16BE(9);\n                    break;\n                    \n                case 6:\n                    sadr = msg.payload.slice(9,14);\n                    break;\n            }\n            break;\n            \n        case 0x20:\n            apdustart = 10;\n            break;\n\n        case 0x28:\n            apdustart = 13 + msg.payload[8] + msg.payload[11]; // note depends on maclen\n            dnet = msg.payload.readUInt16BE(6); \n            snet = msg.payload.readUInt16BE(9);\n            slen = msg.payload[11];\n            switch(slen)\n            {\n                case 1:\n                    sadr = msg.payload[12];\n                    break;\n                    \n                case 2:\n                    sadr = msg.payload.readUInt16BE(12);\n                    break;\n                    \n                case 6:\n                    sadr = msg.payload.slice(12,18);\n                    break;\n            }\n            break;\n            \n\n\n        default:\n\t\t\tstop = 1; //don't understand message\n\t   \n\t}\n\n    if(stop) return null;\n    \n    var apdutype = msg.payload[apdustart]>>4;\n\t//check APDU message type\n\tswitch(apdutype)\n\t{\n\t\tcase 0x01: //unconfirmed Request\n            var apdusvc = msg.payload[apdustart + 1];\n\t        switch(apdusvc)\n\t        {\n\t            case 0x00: //I Am\n                {\n                    svc = \"I_Am\";\n                    p=apdustart + 2;\n    \t\t\t\tswitch(msg.payload[p])\n    \t\t\t\t{\n    \t\t\t\t\tcase 0xc4:\n    \t\t\t\t\t\tvar deviceID = msg.payload.readUInt32BE(p+1) & 0x3fffff;\n                            var bindid = getDevice(deviceID);\n                            if(bindid >= 0)\n                            {\n                                context.global.Device[bindid].IP = msg.ip;\n                                context.global.Device[bindid].Network = snet;\n                                context.global.Device[bindid].maclen = slen;\n                                context.global.Device[bindid].mac = sadr;\n                            }\n                            node.status({fill:\"blue\",shape:\"ring\",text:svc + \" deviceID:\" + deviceID + \" = \" + msg.ip});\n    \t\t\t\t\t\tbreak;\n    \t\t\t\t\t\n    \t\t\t\t\tdefault:\n    \t\t\t\t\t    deviceID = -1;\n    \t\t\t\t\t    break;\n    \t\t\t\t}\n                }\n                break;\n\n\n                case 0x02: //Unconfirmed COV\n                {\n                    svc = \"Unconfirmed COV\";\n        \t\t    var p = apdustart + 2;\n                    switch(msg.payload[p])\n                    {\n                        case 0x09:\n                            oid = msg.payload[p+1];\n                            break;\n                            \n                        case 0x0a:\n                            oid = msg.payload.readUInt16BE(p+1);\n                            break;\n                            \n                        case 0x0c:\n                            oid = msg.payload.readUInt32BE(p+1);\n                            break;\n                    }\n                   \n        \t\t    //search for location of present value property\n        \t\t    p = apdustart + 14;\n        \t\t    while((Buffer.compare(Buffer.from([msg.payload[p-3],msg.payload[p-2],msg.payload[p-1]]),prval)!==0)&&((p+7) < msg.payload.length))\n        \t\t    {\n        \t\t        p+=1;\n        \t\t    }\n        \t\t\tif(msg.payload[p] == 0x2e)\n        \t\t\t{\n        \t\t\t\tp+=1;\n        \t\t\t\tswitch(msg.payload[p])\n        \t\t\t\t{\n        \t\t\t\t\tcase 0x44:\n        \t\t\t\t\t\tcontext.global.IntObj[oid].prval = msg.payload.readFloatBE(p+1);\n        \t\t\t\t\t\tbreak;\n        \n        \t\t\t\t\tcase 0x24:\n        \t\t\t\t\t\tcontext.global.IntObj[oid].prval = msg.payload.readUInt32BE(p+1);\n        \t\t\t\t\t\tbreak;\n        \t\t\t\t\t\t\n        \t\t\t\t\tcase 0x22:\n        \t\t\t\t\t\tcontext.global.IntObj[oid].prval = msg.payload.readUInt16BE(p+1);\n        \t\t\t\t\t\tbreak;\n        \n        \t\t\t\t\tcase 0x23:\n        \t\t\t\t\t\tcontext.global.IntObj[oid].prval = msg.payload.readUIntBE(p+1,3);\n        \t\t\t\t\t\tbreak;\n        \t\t\t\t\t\t\n        \t\t\t\t\tcase 0x21:\n        \t\t\t\t\tcase 0x91:\n        \t\t\t\t\t\tcontext.global.IntObj[oid].prval = msg.payload.readUInt8(p+1);\n        \t\t\t\t\t\tbreak;\n        \t\t\t\t\t\t\n        \t\t\t\t}\n        \t\t\t\tcontext.global.IntObj[oid].age = 0;\n        \t\t\t\tmsg2.payload = context.global.IntObj[oid].prval;\n        \t\t\t\tmsg2.topic = context.global.IntObj[oid].Topic;\n                        node.status({fill:\"blue\",shape:\"ring\",text:svc});\n                    }\n                }\n                break;\n\t        }\n\t\t\tbreak;\n\n\t\tcase 0x02: //simple ack\n\t\t    svc = \"Simple Ack\";\n\t\t\toid = getObjectID(msg.payload[apdustart + 1]);\n\t\t\tif(oid>=0)\n\t\t\t{\n\t\t\t\t//matching request found\n\t\t\t\tcontext.global.IntObj[oid].tx_id = -1;\n\t\t\t\tnode.status({fill:\"blue\",shape:\"ring\",text:svc});\n\t\t\t}\n\t\t\tbreak;\n\n\t\tcase 0x03: //complex ack\n\t\t    svc = \"Read Property ACK\";\n\t\t\toid = getObjectID(msg.payload[apdustart + 1]);\n\t\t\tif(oid>=0)\n\t\t\t{\n\t\t\t\t//matching request found\n    \t\t\tresp = 0;\n    \t\t\t//decode property value\n    \t\t\tif(msg.payload[apdustart + 10] == 0x3e)\n    \t\t\t{\n    \t\t\t\tswitch(msg.payload[apdustart + 11])\n    \t\t\t\t{\n    \t\t\t\t\tcase 0x44:\n    \t\t\t\t\t\tcontext.global.IntObj[oid].prval = msg.payload.readFloatBE(apdustart + 12);\n    \t\t\t\t\t\tbreak;\n    \n    \t\t\t\t\tcase 0x24:\n    \t\t\t\t\t\tcontext.global.IntObj[oid].prval = msg.payload.readUInt32BE(apdustart + 12);\n    \t\t\t\t\t\tbreak;\n    \t\t\t\t\t\t\n    \t\t\t\t\tcase 0x22:\n    \t\t\t\t\t\tcontext.global.IntObj[oid].prval = msg.payload.readUInt16BE(apdustart + 12);\n    \t\t\t\t\t\tbreak;\n    \n    \t\t\t\t\tcase 0x23:\n    \t\t\t\t\t\tcontext.global.IntObj[oid].prval = msg.payload.readUIntBE(apdustart + 12,3);\n    \t\t\t\t\t\tbreak;\n    \t\t\t\t\t\t\n    \t\t\t\t\tcase 0x21:\n    \t\t\t\t\tcase 0x91:\n    \t\t\t\t\t\tcontext.global.IntObj[oid].prval = msg.payload.readUInt8(apdustart + 12);\n    \t\t\t\t\t\tbreak;\n    \n    \t\t\t\t\tcase 0x10:\n    \t\t\t\t\t\tcontext.global.IntObj[oid].prval = 0;\n    \t\t\t\t\t\tbreak;\n    \n    \t\t\t\t\tcase 0x11:\n    \t\t\t\t\t\tcontext.global.IntObj[oid].prval = 1;\n    \t\t\t\t\t\tbreak;\n    \t\t\t\t\t\t\n    \t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontext.global.IntObj[oid].tx_id = -1;\n\t\t\t\tcontext.global.IntObj[oid].age = 0;\n\t\t\t\tnode.status({fill:\"blue\",shape:\"ring\",text:svc});\n\t\t\t\tmsg2.payload = context.global.IntObj[oid].prval;\n\t\t\t\tmsg2.topic = context.global.IntObj[oid].Topic;\n\t\t\t}\n\t\t\tbreak;\n\t\t\t\n\t\tcase 0x05: //error\n\t\t    svc = \"Error\";\n\t\t\toid = getObjectID(msg.payload[apdustart + 1]);\n\t\t\tif(oid>=0)\n\t\t\t{\n\t\t\t\t//COV not supported so switch off flag\n\t\t\t\tcontext.global.IntObj[oid].cov = 0;\n\t\t\t}\n\t\t\tnode.status({fill:\"red\",shape:\"ring\",text:svc});\n\t\t\tbreak;\n\n\t\tcase 0x06: //reject\n\t\t    svc = \"Reject\";\n\t\t\toid = getObjectID(msg.payload[apdustart + 1]);\n\t\t\tif(oid>=0)\n\t\t\t{\n\t\t\t\t//COV not supported so switch off flag\n\t\t\t\tcontext.global.IntObj[oid].cov = 0;\n\t\t\t}\n\t\t\tnode.status({fill:\"red\",shape:\"ring\",text:svc});\n\t\t\tbreak;\n\n\t\tdefault:\n\t\t\tbreak;\n\t}\n}\n\nif(!msg2.hasOwnProperty('payload')) msg2 = null;\n\nreturn msg2;\n","outputs":"1","noerr":0,"x":583.1904296875,"y":694.5,"wires":[["e284fc8b.44e4b","6beb0a38.a6b3a4"]]},{"id":"9197e747.234bb8","type":"inject","z":"5b01eb4c.5e5e24","name":"Message Timeout","topic":"","payload":"","payloadType":"date","repeat":"1","crontab":"","once":false,"x":176,"y":144.6666717529297,"wires":[["cad63ddf.d7852"]]},{"id":"cad63ddf.d7852","type":"function","z":"5b01eb4c.5e5e24","name":"Clear orphaned transactions and 'age' values","func":"var max = global.get('max_objects');\nvar i;\nfor (i=0;i<max;i++)\n{\n    //time out orphaned transactions\n    if(context.global.IntObj[i].valid>=0)\n        context.global.IntObj[i].valid-=1;\n\n    //age measured values    \n    if(context.global.IntObj[i].age<1000)\n        context.global.IntObj[i].age+=1;\n\n}\nreturn null;\n","outputs":1,"noerr":0,"x":545.6666870117188,"y":144.6666717529297,"wires":[[]]},{"id":"9995a546.b18c98","type":"comment","z":"5b01eb4c.5e5e24","name":"Runs every second - purges stale transaction counters and 'age' values","info":"","x":317,"y":71.66666793823242,"wires":[]},{"id":"23ba072f.3097f8","type":"comment","z":"e0eb21a2.82121","name":"Send BACnet/IP messages (confirmed)","info":"","x":317.1428527832031,"y":335.1428527832031,"wires":[]},{"id":"246ac4ef.d6068c","type":"comment","z":"e0eb21a2.82121","name":"Receive BACnet/IP messages","info":"","x":257.19049072265625,"y":640.9999713897705,"wires":[]},{"id":"2a16d257.cb277e","type":"inject","z":"5b01eb4c.5e5e24","name":"Start polling","topic":"","payload":"","payloadType":"date","repeat":"15","crontab":"","once":true,"x":147,"y":329.6666564941406,"wires":[["6b65dada.ddf5b4"]]},{"id":"6b65dada.ddf5b4","type":"function","z":"5b01eb4c.5e5e24","name":"Bindings and Updates","func":"function getDevice(deviceID)\n{\n\tvar i;\n\tvar found = 0;\n\tvar maxdevices = context.global.Device.length;\n\tvar id = -1;\n\t\n    for(i=0;i<maxdevices;i++)\n\t{\n\t\tif(context.global.Device[i].DeviceID == deviceID)\n\t\t{\n\t\t\tid = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn id;\n}\n\nfunction getNetwork(netnum)\n{\n\tvar i;\n\tvar found = 0;\n\tvar maxnetworks = context.global.Network.length;\n\tvar id = -1;\n\t\n    for(i=0;i<maxnetworks;i++)\n\t{\n\t\tif(context.global.Network[i].number == netnum)\n\t\t{\n\t\t\tid = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn id;\n}\n\n\nvar msg1 = {}; //object to update value\nvar msg2 = {}; // object requires binding device\nvar msg3 = {}; // object requires binding network\nvar msg4 = {}; // loop back for next\nvar max = global.get('max_objects');\nvar index = msg.index || 0;\nvar j;\nvar found = 0;\nvar age; //allowable age of data before refresh forced\n\nfor (j=index;j<max;j++)\n{\n    if(context.global.IntObj[j].cov==1)\n    {\n        //if COV available, re-subscribe in under 10 minutes\n        age = 540;\n    }\n    else\n    {\n        //polling\n        age = context.global.IntObj[j].PollTime;\n    }\n    if(context.global.IntObj[j].age >= age)\n    {\n        found = 1;\n        break;\n    }\n}\nif(found)\n{\n    var binding_ok = 0; //create a flag to keep track of tests\n\n    //check if routing to device is fully known.. \n    if(context.global.IntObj[j].IP == \"0.0.0.0\") // we need to bind\n    {\n        var bindid = getDevice(context.global.IntObj[j].DeviceID);\n        if(bindid!=-1)\n        {\n            //device is known, copy data if available\n            if(context.global.Device[bindid].IP!=\"0.0.0.0\")\n            {\n                context.global.IntObj[j].IP = context.global.Device[bindid].IP;\n                context.global.IntObj[j].Network = context.global.Device[bindid].Network;\n                context.global.IntObj[j].maclen = context.global.Device[bindid].maclen;\n                context.global.IntObj[j].mac = context.global.Device[bindid].mac;\n            }\n            else\n            {\n                //still missing info so force a whois\n                msg2.payload = context.global.IntObj[j];\n                msg2.topic = \"device_\" + context.global.IntObj[j].DeviceID;\n            }\n\n            //check if network known\n            if((context.global.IntObj[j].Network!=-1)&&(context.global.IntObj[j].Network!=65535))\n            {\n                var netid = getNetwork(context.global.IntObj[j].Network);\n                if(netid == -1)\n                {\n                    //unknown network - create entry\n                    var netentry = {};\n                    netentry.number = context.global.IntObj[j].Network;\n                    netentry.routerIP = \"0.0.0.0\";\n                    context.global.Network.push(netentry);\n                    \n                    // Send WhoIs Router to Network \n                    msg3.payload = context.global.IntObj[j].Network;\n                    msg3.topic = \"network_\" + context.global.IntObj[j].Network;\n                }\n                else\n                {\n                    //make sure router IP is applied to object\n                    if(context.global.Network[netid].routerIP == \"0.0.0.0\")\n                    {\n                        //still missing info so force a whois router\n                        msg3.payload = context.global.IntObj[j].Network;\n                        msg3.topic = \"network_\" + context.global.IntObj[j].Network;\n                    }\n                    else\n                    {\n                        context.global.IntObj[j].IP = context.global.Network[netid].routerIP;\n                        binding_ok = 1;\n                    }\n                }\n            }\n            else\n            {\n                //routing not required\n                if(msg2)\n                {\n                    //whois rqd\n                }\n                else if(msg3)\n                {\n                    //whois router rqd\n                }\n                else binding_ok = 1;\n            }\n        }\n        else\n        {\n            //device is not known - create entry\n            var device = {};\n            device.DeviceID = context.global.IntObj[j].DeviceID;\n            device.IP = \"0.0.0.0\";\n            device.Network = -1;\n            device.maclen = -1;\n            device.mac = -1;\n            context.global.Device.push(device);\n            \n            // Send WhoIs\n            msg2.payload = context.global.IntObj[j];\n            msg2.topic = context.global.IntObj[j].DeviceID;\n        }\n    }\n    else\n    {\n        //actually routing not fully tested by the logic here..\n        binding_ok = 1;\n    }\n    if(binding_ok)\n    {\n        msg1.payload = context.global.IntObj[j];\n    }\n    else\n    {\n        msg1 = null;  //not ready to update values\n        if(msg2); //msg3=null;\n    }\n    msg4.payload = msg.payload;\n    msg4.index = j + 1;\n}\nelse\n{   \n    msg1 = null;\n    msg4 = null;\n}\n\n// null out un-used messages\nif(!msg2.hasOwnProperty('payload')) msg2 = null;\nif(!msg3.hasOwnProperty('payload')) msg3 = null;\n\nreturn [msg1,msg2,msg3,msg4];","outputs":"4","noerr":0,"x":476,"y":331.6666564941406,"wires":[["f68eb9cd.3dc7c8","3bbcf5a.d44200a"],["fd78ec1b.f0e73","86eaa1f1.157f9"],["974a5d11.a9e38","22c011e6.4a24de"],["65e20954.8118d8"]]},{"id":"65e20954.8118d8","type":"delay","z":"5b01eb4c.5e5e24","name":"","pauseType":"delay","timeout":"100","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":442.75,"y":480.91668701171875,"wires":[["6b65dada.ddf5b4"]]},{"id":"b2452cb0.f13a2","type":"comment","z":"5b01eb4c.5e5e24","name":"Runs periodically - to request updates","info":"","x":212,"y":241.66665649414062,"wires":[]},{"id":"f68eb9cd.3dc7c8","type":"link out","z":"5b01eb4c.5e5e24","name":"Send to relevant BMS","links":["224a1960.9752d6"],"x":707.7500095367432,"y":257.4166612625122,"wires":[]},{"id":"56afe7a8.8ba808","type":"debug","z":"e0eb21a2.82121","name":"","active":false,"console":"false","complete":"false","x":412,"y":798,"wires":[]},{"id":"1564d473.a5816c","type":"csv","z":"dc7095e8.9a33d8","name":"Parse CSV","sep":",","hdrin":true,"hdrout":"","multi":"one","ret":"\\n","temp":"","x":747.9166870117188,"y":61.58331298828125,"wires":[["9385155.8d1d0e8"]]},{"id":"b66e5df4.72df6","type":"file in","z":"dc7095e8.9a33d8","name":"Open config file","filename":"","format":"utf8","x":556.9166870117188,"y":61.58331298828125,"wires":[["1564d473.a5816c"]]},{"id":"9385155.8d1d0e8","type":"function","z":"dc7095e8.9a33d8","name":"Create objects","func":"var max;\nvar maxobjects = global.get('max_objects')||0;\n\n\tmsg.payload.IP = \"0.0.0.0\";\n\tmsg.payload.Network = -1;\n\tmsg.payload.maclen = -1;\n\tmsg.payload.mac = -1;\n\tmsg.payload.valid = -1;\n\tmsg.payload.tx_id = -1;\n\tmsg.payload.prval = 0;\n\tmsg.payload.age = 1000;\n\tmsg.payload.cov = 1;\n\tmsg.payload.objectid = maxobjects;\n\tcontext.global.IntObj.push(msg.payload);\n\tmaxobjects+=1;\n\tnode.status({fill:\"blue\",shape:\"ring\",text:maxobjects});\n\tglobal.set('max_objects',maxobjects);\n\nreturn msg;","outputs":"1","noerr":0,"x":931.9166259765625,"y":61.58331298828125,"wires":[["c4c43570.8af468"]]},{"id":"972e0791.197a18","type":"inject","z":"dc7095e8.9a33d8","name":"Import Config","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"x":164.91667938232422,"y":61.58331298828125,"wires":[["7767f890.223f88"]]},{"id":"c4c43570.8af468","type":"debug","z":"dc7095e8.9a33d8","name":"Object Info","active":true,"console":"false","complete":"payload","x":1124.9166259765625,"y":61.58331298828125,"wires":[]},{"id":"7767f890.223f88","type":"function","z":"dc7095e8.9a33d8","name":"Initialise","func":"//create array of objects\ncontext.global.Device = [];\ncontext.global.Network = [];\ncontext.global.IntObj = [];\n\n//create required message queues\ncontext.global.bnqueue = context.global.bnqueue || [];\n\n//define filesystem paths\nvar root = \"/home/root/\";\nglobal.set(\"filepath\",root);\nmsg.filename = root +\"bacnetlist.csv\";\n\nglobal.set('max_objects',0);\nreturn msg;","outputs":1,"noerr":0,"x":364.91668701171875,"y":61.583343505859375,"wires":[["b66e5df4.72df6"]]},{"id":"224a1960.9752d6","type":"link in","z":"e0eb21a2.82121","name":"Bms Bacnet","links":["fe7ad1b9.d76e1","98944e20.2ee6f","f68eb9cd.3dc7c8"],"x":162.25396728515625,"y":454.55552673339844,"wires":[["a294008a.9842e","b8a83375.28919"]]},{"id":"98944e20.2ee6f","type":"link out","z":"e0eb21a2.82121","name":"To bmsBacnet (restart)","links":["224a1960.9752d6"],"x":1194.7459716796875,"y":424.47621154785156,"wires":[]},{"id":"b8a83375.28919","type":"function","z":"e0eb21a2.82121","name":"Simple triggered queue","func":"// if queue doesn't exist, create it\ncontext.busy = context.busy || false;\n\n// if the msg is a trigger one release next message\nif (msg.hasOwnProperty(\"trigger\")) {\n    if (context.global.bnqueue.length > 0) {\n        var m = context.global.bnqueue.shift();\n        node.status({fill:\"green\",shape:\"ring\",text:\"queued: \" + context.global.bnqueue.length});\n        return {payload:m};\n    }\n    else {\n        context.busy = false;\n        node.status({fill:\"green\",shape:\"ring\",text:\"queued: \" + context.global.bnqueue.length});\n    }\n}\nelse {\n    if (context.busy) {\n        // if busy add to queue\n        context.global.bnqueue.push(msg.payload);\n        node.status({fill:\"green\",shape:\"ring\",text:\"queued: \" + context.global.bnqueue.length});\n    }\n    else {\n        // otherwise we are empty so just pass through and set busy flag\n        context.busy = true;\n        node.status({fill:\"green\",shape:\"ring\",text:\"queued: \" + context.global.bnqueue.length});\n        return msg;\n    }\n}\nreturn null;","outputs":1,"noerr":0,"x":380.1428527832031,"y":398.873046875,"wires":[["8f910e9d.bd567"]]},{"id":"687ef3d.0c25f0c","type":"function","z":"e0eb21a2.82121","name":"Next in queue","func":"msg.trigger = 1;\nreturn msg;","outputs":1,"noerr":0,"x":982.6824951171875,"y":458.22222900390625,"wires":[["98944e20.2ee6f"]]},{"id":"684c37c5.91ffd8","type":"udp out","z":"e0eb21a2.82121","name":"BACnet/IP_Out","addr":"","iface":"","port":"","ipv":"udp4","outport":"47808","base64":false,"multicast":"false","x":1005.2059326171875,"y":397.88099670410156,"wires":[]},{"id":"a294008a.9842e","type":"debug","z":"e0eb21a2.82121","name":"","active":false,"console":"false","complete":"payload","x":333,"y":505,"wires":[]},{"id":"4b3d8e6d.c68a2","type":"catch","z":"e0eb21a2.82121","name":"","scope":["8f910e9d.bd567"],"x":750,"y":501,"wires":[["687ef3d.0c25f0c"]]},{"id":"e284fc8b.44e4b","type":"debug","z":"e0eb21a2.82121","name":"Value Updates","active":true,"console":"false","complete":"true","x":923,"y":674,"wires":[]},{"id":"6beb0a38.a6b3a4","type":"mqtt out","z":"e0eb21a2.82121","name":"","topic":"","qos":"","retain":"","broker":"dfcb3b42.515a08","x":898,"y":718,"wires":[]},{"id":"8a001c04.7de","type":"mqtt in","z":"9d3d1941.54a568","name":"FanSpeed","topic":"System2/FanSpeed","qos":"2","broker":"dfcb3b42.515a08","x":188,"y":129,"wires":[["47a7c1a4.cd4a8"]]},{"id":"47a7c1a4.cd4a8","type":"debug","z":"9d3d1941.54a568","name":"","active":true,"console":"false","complete":"false","x":470,"y":129,"wires":[]},{"id":"2ed4cd26.7c5a42","type":"inject","z":"9d3d1941.54a568","name":"ObjectID 9","topic":"","payload":"9","payloadType":"num","repeat":"","crontab":"","once":false,"x":204,"y":299,"wires":[["cbe19a4a.de2d08"]]},{"id":"cbe19a4a.de2d08","type":"function","z":"9d3d1941.54a568","name":"read from object array","func":"var oid;\noid = msg.payload;\n\nmsg.payload = context.global.IntObj[oid].prval;\nreturn msg;","outputs":1,"noerr":0,"x":431,"y":300,"wires":[["efdce5b9.b3a5a8"]]},{"id":"efdce5b9.b3a5a8","type":"debug","z":"9d3d1941.54a568","name":"","active":true,"console":"false","complete":"false","x":667,"y":300,"wires":[]},{"id":"5a032c3c.fb2394","type":"comment","z":"9d3d1941.54a568","name":"Get value event via MQTT","info":"","x":238,"y":82,"wires":[]},{"id":"c6072ee.10aaed","type":"comment","z":"9d3d1941.54a568","name":"Read latest value from local 'process image'","info":"","x":294,"y":246,"wires":[]},{"id":"3bbcf5a.d44200a","type":"debug","z":"5b01eb4c.5e5e24","name":"To Update Value","active":false,"console":"false","complete":"payload","x":770.2500076293945,"y":219.50000381469727,"wires":[]},{"id":"9e0a3cc5.0a0ad","type":"function","z":"e0eb21a2.82121","name":"WhoIs","func":"var buf;\nvar p=0;\n\nmsg.ip = \"255.255.255.255\";\nmsg.port = 47808;\nbuf = new Buffer(1024);\n\n//bvlc\nbuf[p++] = 0x81;\nbuf[p++] = 0x0b;\nbuf[p++] = 0x00; //holding value for apdu length hb\nbuf[p++] = 0xff; //holding value for apdu length lb\n    \n//npdu\nbuf[p++] = 0x01;\nbuf[p++] = 0x20;\nbuf[p++] = 0xff; //DNet 65535\nbuf[p++] = 0xff; //\nbuf[p++] = 0x00; //Dest MACLen=0 (Broadcast)\nbuf[p++] = 0xff; //Hop count 255\n\n\n//apdu\nnode.status({fill:\"blue\",shape:\"ring\",text:\"WhoIs \" + msg.payload.DeviceID});\nbuf[p++] = 0x10; //Unconfirmed\nbuf[p++] = 0x08; // WhoIs\nbuf[p++] = 0x0b;\nbuf[p++] = (msg.payload.DeviceID>>16) & 0xff;\nbuf[p++] = (msg.payload.DeviceID>>8) & 0xff;\nbuf[p++] = (msg.payload.DeviceID) & 0xff;\nbuf[p++] = 0x1b;\nbuf[p++] = (msg.payload.DeviceID>>16) & 0xff;\nbuf[p++] = (msg.payload.DeviceID>>8) & 0xff;\nbuf[p++] = (msg.payload.DeviceID) & 0xff;\n\n//now go back and set the message length in the bvlc..  \nbuf[2] = p / 0xff;\nbuf[3] = p % 0xff;\n\nmsg.payload = buf.slice(0,p);\n\n\n\n\nreturn msg;","outputs":1,"noerr":0,"x":544,"y":129,"wires":[["abbe83e9.50f8d","2faa2444.ae7ccc"]]},{"id":"abbe83e9.50f8d","type":"debug","z":"e0eb21a2.82121","name":"Unconfirmed Out","active":false,"console":"false","complete":"true","x":1014.2500076293945,"y":129.75000190734863,"wires":[]},{"id":"2faa2444.ae7ccc","type":"udp out","z":"e0eb21a2.82121","name":"UDP Broadcast","addr":"","iface":"","port":"","ipv":"udp4","outport":"","base64":false,"multicast":"broad","x":1005.2500076293945,"y":187.75000190734863,"wires":[]},{"id":"15477127.924ecf","type":"function","z":"e0eb21a2.82121","name":"WhoIs Router","func":"network = msg.payload;\n\nvar buf;\nvar p=0;\n\nmsg.ip = \"255.255.255.255\";\nmsg.port = 47808;\nbuf = new Buffer(1024);\n\n//bvlc\nbuf[p++] = 0x81;\nbuf[p++] = 0x0b;\nbuf[p++] = 0x00; //holding value for apdu length hb\nbuf[p++] = 0xff; //holding value for apdu length lb\n    \n//npdu\nbuf[p++] = 0x01;\nbuf[p++] = 0x80;\nbuf[p++] = 0x00;\nbuf[p++] = (network>>8) & 0xff;\nbuf[p++] = network & 0xff;\n\n//now go back and set the message length in the bvlc..  \nbuf[2] = p / 0xff;\nbuf[3] = p % 0xff;\nnode.status({fill:\"blue\",shape:\"ring\",text:\"WhoIs Router to \" + network});\n\nmsg.payload = buf.slice(0,p);\n\nreturn msg;","outputs":1,"noerr":0,"x":553,"y":187,"wires":[["abbe83e9.50f8d","2faa2444.ae7ccc"]]},{"id":"924ddc2.3db952","type":"comment","z":"e0eb21a2.82121","name":"Send BACnet/IP messages (unconfirmed)","info":"","x":326,"y":56,"wires":[]},{"id":"f5b521f6.cc3e4","type":"link in","z":"e0eb21a2.82121","name":"To WhoIs","links":["86eaa1f1.157f9"],"x":220.00000286102295,"y":129.25000190734863,"wires":[["5de203de.de36ec"]]},{"id":"fd78ec1b.f0e73","type":"debug","z":"5b01eb4c.5e5e24","name":"To WhoIs","active":false,"console":"false","complete":"payload","x":751.2500152587891,"y":334.2500057220459,"wires":[]},{"id":"974a5d11.a9e38","type":"debug","z":"5b01eb4c.5e5e24","name":"To WhoIs Router","active":false,"console":"false","complete":"payload","x":786.2500152587891,"y":411.7500057220459,"wires":[]},{"id":"a4a899ad.0371d8","type":"link in","z":"e0eb21a2.82121","name":"To WhoIs Router","links":["22c011e6.4a24de"],"x":221.25,"y":187,"wires":[["30fe584d.df4bd8"]]},{"id":"22c011e6.4a24de","type":"link out","z":"5b01eb4c.5e5e24","name":"Request WhoIs Router","links":["a4a899ad.0371d8"],"x":712.0000114440918,"y":448.0000066757202,"wires":[]},{"id":"86eaa1f1.157f9","type":"link out","z":"5b01eb4c.5e5e24","name":"Req WhoIs","links":["f5b521f6.cc3e4"],"x":709.0000114440918,"y":368.2500057220459,"wires":[]},{"id":"30fe584d.df4bd8","type":"delay","z":"e0eb21a2.82121","name":"","pauseType":"queue","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"2","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"x":350,"y":187,"wires":[["15477127.924ecf"]]},{"id":"5de203de.de36ec","type":"delay","z":"e0eb21a2.82121","name":"","pauseType":"queue","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"2","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"x":350,"y":129,"wires":[["9e0a3cc5.0a0ad"]]},{"id":"48ebca60.ead754","type":"comment","z":"dc7095e8.9a33d8","name":"Example of CSV file format (Note heading row is case sensitive!)","info":"DeviceID,Object,Instance,Topic,PollTime\n260001,AI,1,System1/RoomTemp,60\n260001,AO,2,System1/HtgVlv,20\n260001,AV,3,System1/SetPoint,360\n260001,BI,0,System1/FrostStat,120\n260001,BO,1,System1/HtgStg1,120\n260001,BV,2,System1/Fire,60\n2,AI,0,System2/SupplyTemp,60\n2,AO,1,System2/FanSpeed,100\n2,AV,1,System2/SetPoint,120\n2,BI,1,System2/FlowStat,60\n2,BO,1,System2/FanEnable,60\n","x":290,"y":140,"wires":[]}]

Comments (2)

  1. Steve Reid

    First revision here fixes error with selection of 'InvokeID' which should wrap round to zero above 255 (not above 256)

  2. Steve Reid

    Added Comment node on first tab to provide an example of the correct CSV format for defining BACnet objects

HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.