UNPKG

@ncd-io/node-red-enterprise-sensors

Version:

You can install this library through the Palette Manager in Node-Red's UI.

360 lines (346 loc) 11.4 kB
const events = require("events"); module.exports = class DigiComm{ constructor(serial){ this.serial = serial; this._emitter = new events.EventEmitter(); this.temp = []; this._idCount = 0; this._timoutLimit = 5000; this.report_rssi = false; var that = this; function receive(d){ that.dataIn(d); } this.serial.on('data', receive); this.serial.on('error', (err) => { console.log(err); }); this.on('close', () => { that._emitter.removeListener('data', receive); }); this.send = new outgoingFrame(this); this.frame = new incomingFrame(this); this.lastSent = []; // this.modem_mac = this.send.at_command('SL'); } close(){ this._emitter.emit('close'); } getId(){ if(this._idCount == 255) this._idCount = 0; return ++this._idCount; } dataIn(d){ if(!this.temp.length && d != 126) return; // if(this.temp.length == 0) console.log('starting new buffer'); //Add incoming byte to buffer this.temp.push(d); //A valid packet can be no less than 6 bytes (delimiter, lengthMSB, lengthLSB, frameType, [...frame data], checksum); if(this.temp.length >= 6){ //get the reported length and actual length of the frame var length = msbLsb(this.temp[1], this.temp[2]); var fLength = this.temp.length-4; //If the frame length is less than the actual length, we are still waiting for data if(length > fLength) return; //If the lengths match, we have a full packet if(length == fLength){ //If the checksum matches, parse the packet if(this.checksum(this.temp.slice(3,-1)) == this.temp[this.temp.length-1]){ var frame = this.frame.parse(this.temp); if(frame.mac) frame.mac = frame.mac.toLowerCase(); //If the frame has an ID, send it as the event name since it's a response // console.log(frame); var event = typeof frame.id != 'undefined' ? 'digi-response:'+frame.id : frame.type; //Send out the incoming frame to anyone listening for specific event // console.log('event: '+event); // console.log(frame); this._emitter.emit(event, frame); // console.log('data frame: '+this.temp); // console.log(frame); //Send out the frame to everyone listening to a general call this._emitter.emit('digi_frame', frame); //If the checksum didn't match, emit an error }else{ console.log('checksum error'); this._emitter.emit('checksum_error', this.temp); //console.log({'checksum error': this.temp}); } //If the buffer is longer than it should be, send an overflow error }else{ this._emitter.emit('overflow_error', this.temp); console.log({'overflow error': this.temp}); } //If we are here the buffer should be reset one way or another // console.log({ // incoming_raw: this.temp // }); //console.log(this.temp); this.temp = []; } } checksum(data){ return (255 - (data.reduce((t, n) => t+n) & 255)); } _send(frame, expected){ // console.log('frame in _send function: '+frame); // console.log('frame length is: '+frame.length); var that = this; if(!expected) expected = 1; var awaiting = expected; return new Promise((fulfill, reject) => { var packet = [126, (frame.length >> 8), (frame.length & 255)]; packet.push(...frame); packet.push(this.checksum(frame)); var event = 'digi-response:'+frame[1]; var response = []; var tOut = setTimeout(() => { that._emitter.removeAllListeners(event); reject({error: 'Request timed out without response', original: frame}); }, that._timoutLimit); that.on(event, (frame) => { clearTimeout(tOut); awaiting -= 1; if(awaiting == 0) that._emitter.removeAllListeners(event); if(expected == 1) fulfill(frame); else{ response.push(frame); fulfill(response); } }); // These console.log entries are to display timing and raw packets // console.log('-------------------------------'); // console.log('#otf: /lib/DigiParser.js'); // console.log('send Frame', packet); // console.log('epoch', new Date().getTime()); that.serial.write(packet, (err) => { that.lastSent = packet; if(err){ console.log('_send frame error'); console.log(err); } }); }); //send data! } on(a,b){ this._emitter.on(a,b); } }; class outgoingFrame{ constructor(master){ this.master = master; } at_command(command, param, queue, expected){ var frame = [queue ? 9 : 8, this.master.getId()]; if(!expected) expected = 1; for(var i=0;i<command.length;i++) frame.push(command.charCodeAt(i)); if(typeof param == 'undefined') return this.master._send(frame, expected); if(param.constructor != Array) param = [param]; frame.push(...param); return this.master._send(frame, expected); } remote_at_command(mac, command, param, apply){ var frame = [23, this.master.getId()]; frame.push(...mac); frame.push(255, 254); frame.push(apply ? 2 : 0); for(var i=0;i<command.length;i++) frame.push(command.charCodeAt(i)); if(typeof param == 'undefined') return this.master._send(frame); if(param.constructor != Array) param = [param]; frame.push(...param); return this.master._send(frame); } //For broadcast, set mac to [0,0,0,0,0,0,255,255] transmit_request(mac, data, opts){ var config = this.transmissionOptions(opts); var frame = [16, this.master.getId()]; frame = frame.concat(mac); var conf = [255, 254, (config >> 8), (config & 255)]; frame = frame.concat(conf); // if(data.constructor != Array) data = [data]; frame = frame.concat(data); frame.length = 2+mac.length+conf.length+data.length; return this.master._send(frame); } explicit_addressing_command(mac, source, destination, cluster, profile, data, opts){ var config = this.transmissionOptions(opts); var frame = [17, this.master.getId()]; frame.push(...mac); var payload = [255, 254, source, destination, cluster[0], cluster[1], profile[0], profile[1], (config >> 8), (config & 255)]; frame.push(...payload); if(data.constructor != Array) data = [data]; frame.push(...data); return this.master._send(frame); } transmissionOptions(opts){ var config = Object.assign({ //Number of allowed hops, 0 = maximum rad: 0, //1 = Disable ACK ack: 0, //1 = Disable route discovery rd: 0, //Enable NACK messages nack: 0, //Enable Trace Route trace: 0, //Delivery method - 1 = point_multipoint, 2 = repeater_mode, 3 = digimesh method: 3 }, opts); return (config.rad << 8) | (config.ack | (config.rd << 1) | (config.nack << 2) | (config.trace << 3) | (config.method << 6)); } } class incomingFrame{ constructor(parent){ this.parent = parent; this.frameType = { "136": 'at_command_response', "138": 'modem_status', "139": 'transmit_status', "141": 'route_information_packet', "142": 'aggregate_addressing_update', "144": 'receive_packet', "145": 'explicit_rx_indicator', "146": 'io_data_sample_indicator', "149": 'node_identification_indicator', "151": 'remote_command_response' }; } parse(data){ var type = typeof this.frameType[data[3]] == 'undefined' ? 'unknown' : this.frameType[data[3]]; if(typeof this[type] == 'function'){ var frame = this[type](data.slice(4, -1)); frame.type = type; } return frame; } //Parse frame methods at_command_response(frame){ return { id: frame[0], command: String.fromCharCode(frame[1])+String.fromCharCode(frame[2]), status: (['OK', 'ERROR', 'Invalid Command', 'Invalid Parameter', 'Tx Failure'])[frame[3] & 7], data: frame.length > 4 ? frame.slice(4) : false, hasError: (frame[3] > 0) }; } modem_status(frame){ return ({"0":{type: "Hardware reset"}, "1": {type: "Watchdog timer reset"}, "11": {type: "Network woke up"}, "12": {type: "Network went to sleep"}})[frame[0]]; } transmit_status(frame){ return { id: frame[0], address: frame.slice(1, 3), retries: frame[3], delivery_status: this.deliveryStatus(frame[4]), discovery_status: frame[5] == 0 ? "No discovery overhead" : "Route discovery", hasError: (frame[5] != 0) }; } route_information_packet(frame){ return { source_event: frame[0] == 17 ? "NACK" : "Trace route", format: frame[1], timestamp: frame.slice(2, 6).reduce(msbLsb), ack_timouts: frame[6], tx_blocked: frame[7], destination_mac: toMac(frame.slice(9, 17)), source_mac: toMac(frame.slice(17, 25)), responder_mac: toMac(frame.slice(25, 33)), receiver_mac: toMac(frame.slice(33, 41)), }; } aggregate_addressing_update(frame){ return { format: frame[0], new_address: toMac(frame.slice(1, 9)), old_address: toMac(frame.slice(9, 17)) }; } receive_packet(frame){ var ret = { mac: toMac(frame.slice(0, 8)), receive_options: this.receiveOptions(frame[10]), data: frame.slice(11) }; if(this.parent.report_rssi) ret.rssi = this.parent.send.at_command('DB'); return ret; } explicit_rx_indicator(frame){ return { source_mac: toMac(frame.slice(0, 8)), source_endpoint: frame[10], destination_endpoint: frame[11], cluster_id: frame.slice(12, 14), profile_id: frame.slice(14, 16), receive_options: this.receiveOptions(frame[16]), data: frame.slice(17) }; } io_data_sample_indicator(frame){ return { source_mac: toMac(frame.slice(0, 8)), source_addr: frame.slice(8, 10).reduce(msbLsb), receive_options: this.receiveOptions(frame[10] & 3), sample_count: frame[11], digital_pins: frame.slice(12, 14).reduce(msbLsb), analog_pins: frame[14], digital_samples: frame.slice(15, 17).reduce(msbLsb), analog_samples: chunk(frame.slice(17), 2).map((i) => msbLsb(i[0], i[1])) }; } node_identification_indicator(frame){ var packet = { source_mac: toMac(frame.slice(0, 8)), receive_options: this.receiveOptions(frame[10]), remote_mac: toMac(frame.slice(13, 21)), node_id: frame.slice(21, 23), device_type: frame[25] ? (frame[25] == 1 ? "Normal Mode" : "End Device") : "Coordinator", source_event: frame[26], digi_profile: frame.slice(27, 29), digi_manufacturer: frame.slice(29, 31) }; if(frame.length > 33) packet.digi_dd = frame.slice(31, 35); if(frame.length > 34) packet.rssi = frame[35]; return packet; } remote_command_response(frame){ return { id: frame[0], remote_mac: toMac(frame.slice(1, 9)), command: String.fromCharCode(frame[11])+String.fromCharCode(frame[12]), status: (['OK', 'ERROR', 'Invalid Command', 'Invalid Parameter', 'Tx Failure'])[frame[13] & 7], data: frame.slice(14) }; } deliveryStatus(status){ return ({ "0": "Success", "1": "MAC ACK failure", "2": "Collision avoidance failure", "33": "Network ACK failure", "37": "Route not found", "49": "Internal resource error", "50": "Internal error", "116": "Payload too large", "117": "Indirect message requested" })[status]; } receiveOptions(byte){ return { ack: (byte & 1 == 1), broadcast: (byte & 2 == 2), type: (byte & 192 == 192) ? "DigiMesh" : ((byte & 128 == 128) ? "Repeater Mode" : ((byte & 64 == 64) ? "Point-Multipoint" : "")) }; } } function msbLsb(m,l){return (m<<8)+l;} function toHex(n){return ("00" + n.toString(16)).substr(-2);} function toMac(arr){ return arr.reduce((h,c,i) => {return (i==1?toHex(h):h)+':'+toHex(c);}); } function chunk(a, l){ var arr = []; for(i=0;i<a.length;i+=l){ arr.push(this.slice(i, i+l)); } return arr; }