UNPKG

zigbee-shepherd

Version:

An open source ZigBee gateway solution with node.js.

659 lines (550 loc) 25.6 kB
/* jshint node: true */ 'use strict'; const EventEmitter = require('events'); const Q = require('q'); const _ = require('busyman'); const {isPlainObject, cloneDeep, isEqual} = _; const znp = require('cc-znp'); const proving = require('proving'); const ZSC = require('zstack-constants'); const debug = { shepherd: require('debug')('zigbee-shepherd'), init: require('debug')('zigbee-shepherd:init'), request: require('debug')('zigbee-shepherd:request'), response: require('debug')('zigbee-shepherd:response') }; const Zdo = require('./zdo'); const query = require('./query'); const bridge = require('./event_bridge.js'); var init = require('../initializers/init_controller'); const nvParams = require('../config/nv_start_options.js'); const Device = require('../model/device'); const Coordpoint = require('../model/coordpoint'); class Controller extends EventEmitter { constructor(shepherd, cfg) { super(); // cfg is serial port config const self = this; let transId = 0; if (!isPlainObject(cfg)) throw new TypeError('cfg should be an object.'); /***************************************************/ /*** Protected Members ***/ /***************************************************/ this._shepherd = shepherd; this._coord = null; this._znp = znp; // required sometimes this._cfg = cfg; this._zdo = new Zdo(this); this._resetting = false; this._spinLock = false; this._joinQueue = []; this._permitJoinTime = 0; this._permitJoinInterval; this._net = { state: null, channel: null, panId: null, extPanId: null, ieeeAddr: null, nwkAddr: null, joinTimeLeft: 0 }; this._firmware = { version: null, revision: null }; this._joinWaitList = {} /***************************************************/ /*** Public Members ***/ /***************************************************/ this.query = query(this); this.nextTransId = function () { // zigbee transection id if (++transId > 0xff) transId = 1; return transId; }; this.permitJoinCountdown = function () { return self._permitJoinTime -= 1; }; this.isResetting = function () { return self._resetting; }; /***************************************************/ /*** Event Handlers ***/ /***************************************************/ znp.on('ready', () => { init.setupCoord(self).then(() => { self.emit('ZNP:INIT'); }).fail(err => { self.emit('ZNP:INIT', err); debug.init('Coordinator initialize had an error:', err); }).done(); }); znp.on('close', () => { self.emit('ZNP:CLOSE'); }); znp.on('AREQ', msg => { bridge._areqEventBridge(self, msg); }); this.on('ZDO:tcDeviceInd', tcData => { if(tcData.parentaddr == 0){ return } const data = {srcaddr: tcData.nwkaddr, nwkaddr: tcData.nwkaddr, ieeeaddr: tcData.extaddr, capabilities: {}}; if (self._spinLock) { self._joinQueue.push({ func() { self.endDeviceAnnceHdlr(data); }, ieeeAddr: data.ieeeaddr }); } else { self._spinLock = true; self.endDeviceAnnceHdlr(data); } }); this.on('ZDO:endDeviceAnnceInd', data => { debug.shepherd('spinlock:', self._spinLock, self._joinQueue); if (self._spinLock) { // Check if joinQueue already has this device for (let i = 0; i < self._joinQueue.length; i++) { if (self._joinQueue[i].ieeeAddr == data.ieeeaddr) { debug.shepherd(`Device: ${self._joinQueue[i].ieeeAddr} already in joinqueue`); return; } } self._joinQueue.push({ func() { self.endDeviceAnnceHdlr(data); }, ieeeAddr: data.ieeeaddr }); } else { self._spinLock = true; self.endDeviceAnnceHdlr(data); } }); } /*************************************************************************************************/ /*** Public ZigBee Utility APIs ***/ /*************************************************************************************************/ getFirmwareInfo() { const firmware = cloneDeep(this._firmware); return firmware; }; getNetInfo() { const net = cloneDeep(this._net); if (net.state === ZSC.ZDO.devStates.ZB_COORD) net.state = 'Coordinator'; net.joinTimeLeft = this._permitJoinTime; return net; }; setNetInfo(netInfo) { const self = this; Object.entries(netInfo).forEach(([key, val]) => { if (self._net.hasOwnProperty(key)) self._net[key] = val; }); }; /*************************************************************************************************/ /*** Mandatory Public APIs ***/ /*************************************************************************************************/ start(callback) { const self = this; const deferred = Q.defer(); let readyLsn; readyLsn = function (err) { return err ? deferred.reject(err) : deferred.resolve(); }; this.once('ZNP:INIT', readyLsn); Q.ninvoke(znp, 'init', this._cfg).fail(err => { self.removeListener('ZNP:INIT', readyLsn); deferred.reject(err); }).done(); return deferred.promise.nodeify(callback); }; close(callback) { const self = this; const deferred = Q.defer(); let closeLsn; closeLsn = function () { deferred.resolve(); }; this.once('ZNP:CLOSE', closeLsn); Q.ninvoke(znp, 'close').fail(err => { self.removeListener('ZNP:CLOSE', closeLsn); deferred.reject(err); }).done(); return deferred.promise.nodeify(callback); }; reset(mode, callback) { const self = this; const deferred = Q.defer(); const startupOption = nvParams.startupOption.value[0]; proving.stringOrNumber(mode, 'mode should be a number or a string.'); Q.fcall(() => { if (mode === 'soft' || mode === 1) { debug.shepherd('Starting a software reset...'); self._resetting = true; return self.request('SYS', 'resetReq', { type: 0x01 }); } else if (mode === 'hard' || mode === 0) { debug.shepherd('Starting a hardware reset...'); self._resetting = true; if (self._nvChanged && startupOption !== 0x02) nvParams.startupOption.value[0] = 0x02; const steps = [ function () { return self.request('SYS', 'resetReq', { type: 0x01 }).delay(0); }, function () { return self.request('SAPI', 'writeConfiguration', nvParams.startupOption).delay(10); }, function () { return self.request('SYS', 'resetReq', { type: 0x01 }).delay(10); }, function () { return self.request('SAPI', 'writeConfiguration', nvParams.panId).delay(10); }, function () { return self.request('SAPI', 'writeConfiguration', nvParams.extPanId).delay(10); }, function () { return self.request('SAPI', 'writeConfiguration', nvParams.channelList).delay(10); }, function () { return self.request('SAPI', 'writeConfiguration', nvParams.logicalType).delay(10); }, function () { return self.request('SAPI', 'writeConfiguration', nvParams.precfgkey).delay(10); }, function () { return self.request('SAPI', 'writeConfiguration', nvParams.precfgkeysEnable).delay(10); }, function () { return self.request('SYS', 'osalNvWrite', nvParams.securityMode).delay(10); }, function () { return self.request('SAPI', 'writeConfiguration', nvParams.zdoDirectCb).delay(10); }, function () { return self.request('SYS', 'osalNvItemInit', nvParams.znpCfgItem).delay(10).fail(err => // Success, item created and initialized err.message === 'rsp error: 9' ? null : Q.reject(err)); }, function () { return self.request('SYS', 'osalNvWrite', nvParams.znpHasConfigured).delay(10); } ]; return steps.reduce((soFar, fn) => soFar.then(fn), Q(0)); } else { return Q.reject(new Error('Unknown reset mode.')); } }).then(() => { self._resetting = false; if (self._nvChanged) { nvParams.startupOption.value[0] = startupOption; self._nvChanged = false; deferred.resolve(); } else { self.once('_reset', err => err ? deferred.reject(err) : deferred.resolve()); self.emit('SYS:resetInd', '_reset'); } }).fail(err => { deferred.reject(err); }).done(); return deferred.promise.nodeify(callback); }; request(subsys, cmdId, valObj, callback) { const deferred = Q.defer(); let rspHdlr; proving.stringOrNumber(subsys, 'subsys should be a number or a string.'); proving.stringOrNumber(cmdId, 'cmdId should be a number or a string.'); if (!isPlainObject(valObj) && !Array.isArray(valObj)) throw new TypeError('valObj should be an object or an array.'); if (typeof subsys === "string") subsys = subsys.toUpperCase(); rspHdlr = function (err, rsp) { if (subsys !== 'ZDO' && subsys !== 5) { if (rsp && rsp.hasOwnProperty('status')) debug.request('RSP <-- %s, status: %d', `${subsys}:${cmdId}`, rsp.status); else debug.request('RSP <-- %s', `${subsys}:${cmdId}`); } if (err) deferred.reject(err); else if ((subsys !== 'ZDO' && subsys !== 5) && rsp && rsp.hasOwnProperty('status') && rsp.status !== 0) // unsuccessful deferred.reject(new Error(`rsp error: ${rsp.status}`)); else deferred.resolve(rsp); }; if ((subsys === 'AF' || subsys === 4) && valObj.hasOwnProperty('transid')) debug.request('REQ --> %s, transId: %d', `${subsys}:${cmdId}`, valObj.transid); else debug.request('REQ --> %s', `${subsys}:${cmdId}`); if (subsys === 'ZDO' || subsys === 5) this._zdo.request(cmdId, valObj, rspHdlr); // use wrapped zdo as the exported api else znp.request(subsys, cmdId, valObj, rspHdlr); // SREQ has timeout inside znp return deferred.promise.nodeify(callback); }; permitJoin(time, type, callback) { // time: seconds, 0x00 disable, 0xFF always enable // type: 0 (coord) / 1 (all) / router addr if > 1 const self = this; let addrmode; let dstaddr; proving.number(time, 'time should be a number.'); proving.stringOrNumber(type, 'type should be a number or a string.'); return Q.fcall(() => { if (type === 0 || type === 'coord') { addrmode = 0x02; dstaddr = 0x0000; } else if (type === 1 || type === 'all') { addrmode = 0x0F; dstaddr = 0xFFFC; // all coord and routers } else if (typeof type === "number") { addrmode = 0x02; // address mode dstaddr = type; // router address } else { return Q.reject(new Error('Not a valid type.')); } }).then(() => { if (time > 0xff || time < 0) return Q.reject(new Error('Jointime can only range from 0 to 0xff.')); else self._permitJoinTime = Math.floor(time); }).then(() => self.request( 'ZDO', 'mgmtPermitJoinReq', { addrmode, dstaddr , duration: time, tcsignificance: 0 } )).then(rsp => { self.emit('permitJoining', self._permitJoinTime, dstaddr); if (time !== 0 && time !== 0xff) { clearInterval(self._permitJoinInterval); self._permitJoinInterval = setInterval(() => { if (self.permitJoinCountdown() === 0) clearInterval(self._permitJoinInterval); self.emit('permitJoining', self._permitJoinTime, dstaddr); }, 1000); } return rsp; }).nodeify(callback); }; remove(dev, cfg, callback) { // cfg: { reJoin, rmChildren } const self = this; let reqArgObj; let rmChildren_reJoin = 0x00; if (!(dev instanceof Device)) throw new TypeError('dev should be an instance of Device class.'); else if (!isPlainObject(cfg)) throw new TypeError('cfg should be an object.'); cfg.reJoin = cfg.hasOwnProperty('reJoin') ? !!cfg.reJoin : true; // defaults to true cfg.rmChildren = cfg.hasOwnProperty('rmChildren') ? !!cfg.rmChildren : false; // defaults to false rmChildren_reJoin = cfg.reJoin ? (rmChildren_reJoin | 0x01) : rmChildren_reJoin; rmChildren_reJoin = cfg.rmChildren ? (rmChildren_reJoin | 0x02) : rmChildren_reJoin; reqArgObj = { dstaddr: dev.getNwkAddr(), deviceaddress: dev.getIeeeAddr(), removechildren_rejoin: rmChildren_reJoin }; return this.request('ZDO', 'mgmtLeaveReq', reqArgObj).then(rsp => { if (rsp.status !== 0 && rsp.status !== 'SUCCESS') return Q.reject(rsp.status); }).nodeify(callback); }; registerEp(loEp, callback) { const self = this; if (!(loEp instanceof Coordpoint)) throw new TypeError('loEp should be an instance of Coordpoint class.'); return this.request('AF', 'register', makeRegParams(loEp)).then(rsp => rsp).fail(err => err.message === 'rsp error: 184' ? self.reRegisterEp(loEp) : Q.reject(err)).nodeify(callback); }; deregisterEp(loEp, callback) { const self = this; const coordEps = this._coord.endpoints; if (!(loEp instanceof Coordpoint)) throw new TypeError('loEp should be an instance of Coordpoint class.'); return Q.fcall(() => { if (!Object.values(coordEps).includes(loEp)) return Q.reject(new Error('Endpoint not maintained by Coordinator, cannot be removed.')); else return self.request('AF', 'delete', { endpoint: loEp.getEpId() }); }).then(rsp => { delete coordEps[loEp.getEpId()]; return rsp; }).nodeify(callback); }; reRegisterEp(loEp, callback) { const self = this; return this.deregisterEp(loEp).then(() => self.request('AF', 'register', makeRegParams(loEp))).nodeify(callback); }; simpleDescReq(nwkAddr, ieeeAddr, callback) { return this.query.deviceWithEndpoints(nwkAddr, ieeeAddr, callback); }; bind(srcEp, cId, dstEpOrGrpId, callback) { return this.query.setBindingEntry('bind', srcEp, cId, dstEpOrGrpId, callback, this._shepherd.zclId); }; unbind(srcEp, cId, dstEpOrGrpId, callback) { return this.query.setBindingEntry('unbind', srcEp, cId, dstEpOrGrpId, callback, this._shepherd.zclId); }; findEndpoint(addr, epId) { return this._shepherd.find(addr, epId); }; setNvParams(net) { // net: { panId, extPanId, channelList, precfgkey, precfgkeysEnable, startoptClearState } net = net || {}; proving.object(net, 'opts.net should be an object.'); Object.entries(net).forEach(([param, val]) => { switch (param) { case 'panId': proving.number(val, 'net.panId should be a number.'); nvParams.panId.value = [ val & 0xFF, (val >> 8) & 0xFF ]; break; case 'extPanId': if (val && (!Array.isArray(val) || val.length !== 8)) throw new TypeError('net.extPanId should be an array with 8 uint8 integers.'); if (val) { nvParams.extPanId.value = val; } break; case 'precfgkey': if (!Array.isArray(val) || val.length !== 16) throw new TypeError('net.precfgkey should be an array with 16 uint8 integers.'); nvParams.precfgkey.value = val; break; case 'precfgkeysEnable': proving.boolean(val, 'net.precfgkeysEnable should be a bool.'); nvParams.precfgkeysEnable.value = val ? [ 0x01 ] : [ 0x00 ]; break; case 'startoptClearState': proving.boolean(val, 'net.startoptClearState should be a bool.'); nvParams.startupOption.value = val ? [ 0x02 ] : [ 0x00 ]; break; case 'channelList': proving.array(val, 'net.channelList should be an array.'); let chList = 0; val.forEach(ch => { if (ch >= 11 && ch <= 26) chList = chList | ZSC.ZDO.channelMask[`CH${ch}`]; }); nvParams.channelList.value = [ chList & 0xFF, (chList >> 8) & 0xFF, (chList >> 16) & 0xFF, (chList >> 24) & 0xFF ]; break; default: throw new TypeError(`Unkown argument: ${param}.`); } }); }; checkNvParams(callback) { const self = this; let steps; function bufToArray(buf) { const arr = []; for (let i = 0; i < buf.length; i += 1) { arr.push(buf.readUInt8(i)); } return arr; } steps = [ function () { return self.request('SYS', 'osalNvRead', nvParams.znpHasConfigured).delay(10).then(rsp => { if (!isEqual(bufToArray(rsp.value), nvParams.znpHasConfigured.value)) return Q.reject('reset'); }); }, function () { return self.request('SAPI', 'readConfiguration', nvParams.panId).delay(10).then(rsp => { if (!isEqual(bufToArray(rsp.value), nvParams.panId.value)) return Q.reject('reset'); }); }, function () { return self.request('SAPI', 'readConfiguration', nvParams.extPanId).delay(10).then(rsp => { if (!isEqual(bufToArray(rsp.value), nvParams.extPanId.value)) return Q.reject('reset'); }); }, function () { return self.request('SAPI', 'readConfiguration', nvParams.channelList).delay(10).then(rsp => { if (!isEqual(bufToArray(rsp.value), nvParams.channelList.value)) return Q.reject('reset'); }); }, function () { return self.request('SAPI', 'readConfiguration', nvParams.precfgkey).delay(10).then(rsp => { if (!isEqual(bufToArray(rsp.value), nvParams.precfgkey.value)) return Q.reject('reset'); }); }, function () { return self.request('SAPI', 'readConfiguration', nvParams.precfgkeysEnable).delay(10).then(rsp => { if (!isEqual(bufToArray(rsp.value), nvParams.precfgkeysEnable.value)) return Q.reject('reset'); }); } ]; return steps.reduce((soFar, fn) => soFar.then(fn), Q(0)).fail(err => { if (err === 'reset' || err.message === 'rsp error: 2') { self._nvChanged = true; debug.init('Non-Volatile memory is changed.'); return self.reset('hard'); } else { return Q.reject(err); } }).nodeify(callback); }; checkOnline(dev, callback) { const self = this; const nwkAddr = dev.getNwkAddr(); const ieeeAddr = dev.getIeeeAddr(); const deferred = Q.defer(); Q.fcall(() => self.request('ZDO', 'nodeDescReq', { dstaddr: nwkAddr, nwkaddrofinterest: nwkAddr }).timeout(5000).fail( () => self.request('ZDO', 'nodeDescReq', { dstaddr: nwkAddr, nwkaddrofinterest: nwkAddr }).timeout(5000) )).then(() => { if (dev.status === 'offline') { self.emit('ZDO:endDeviceAnnceInd', { srcaddr: nwkAddr, nwkaddr: nwkAddr, ieeeaddr: ieeeAddr, capabilities: {} }); } return deferred.resolve(); }).fail(err => deferred.reject(err)).done(); return deferred.promise.nodeify(callback); }; endDeviceAnnceHdlr(data) { const self = this; let joinTimeout; const joinEvent = `ind:incoming:${data.ieeeaddr}`; const dev = this._shepherd._findDevByAddr(data.ieeeaddr); if (dev && dev.status === 'online'){ // Device has already joined, do next item in queue debug.shepherd(`Device: ${dev.getIeeeAddr()} already in network`); if (self._joinQueue.length) { const next = self._joinQueue.shift(); if (next) { debug.shepherd('next item in joinqueue'); setImmediate(() => { next.func(); }); } else { debug.shepherd('no next item in joinqueue'); self._spinLock = false; } } else { self._spinLock = false; } return; } joinTimeout = setTimeout(() => { if (self.listenerCount(joinEvent)) { self.emit(joinEvent, '__timeout__'); self._shepherd.emit('joining', { type: 'timeout', ieeeAddr: data.ieeeaddr }); } joinTimeout = null; }, 30000); this.once(joinEvent, () => { if (joinTimeout) { clearTimeout(joinTimeout); joinTimeout = null; } if (self._joinQueue.length) { const next = self._joinQueue.shift(); if (next){ setImmediate(() => { next.func(); }); } else { self._spinLock = false; } } else { self._spinLock = false; } }); this._shepherd.emit('joining', { type: 'associating', ieeeAddr: data.ieeeaddr }); this.simpleDescReq(data.nwkaddr, data.ieeeaddr).then(devInfo => devInfo).fail(() => self.simpleDescReq(data.nwkaddr, data.ieeeaddr)).then(devInfo => { // Now that we have the simple description of the device clear joinTimeout if (joinTimeout) { clearTimeout(joinTimeout); joinTimeout = null; } // Defer a promise to wait for the controller to complete the ZDO:devIncoming event! const processIncoming = Q.defer(); self.emit('ZDO:devIncoming', devInfo, processIncoming.resolve, processIncoming.reject); return processIncoming.promise; }).then(() => { self.emit(joinEvent, '__timeout__'); }).fail(err => { self._shepherd.emit('error', `Cannot get the Node Descriptor of the Device: ${data.ieeeaddr} (${err})`); self._shepherd.emit('joining', { type: 'error', ieeeAddr: data.ieeeaddr }); self.emit(joinEvent, '__timeout__'); }).done(); }; } /*************************************************************************************************/ /*** Private Functions ***/ /*************************************************************************************************/ function makeRegParams(loEp) { return { endpoint: loEp.getEpId(), appprofid: loEp.getProfId(), appdeviceid: loEp.getDevId(), appdevver: 0, latencyreq: ZSC.AF.networkLatencyReq.NO_LATENCY_REQS, appnuminclusters: loEp.inClusterList.length, appinclusterlist: loEp.inClusterList, appnumoutclusters: loEp.outClusterList.length, appoutclusterlist: loEp.outClusterList }; } module.exports = Controller;