UNPKG

zigbee

Version:

ZigBee for Node.JS (using the TI CC2530/CC2531)

633 lines (522 loc) 18 kB
'use strict'; var debug = require('debug')('ZNPClient'); var Dissolve = require('dissolve'); var Concentrate = require('concentrate'); var when = require('when'); var when_sequence = require('when/sequence'); var EventEmitter = require('events').EventEmitter; var util = require('util'); var Device = require('../profile/Device'); var ZNP = require('./constants'); var ZNPSerial = require('./ZNPSerial'); var packets = require('./packets'); /** * A ZigBee client, handling higher level functions relating to the ZigBee * network. * interface responsible for communicating with the ZigBee SOC. */ function ZNPClient() { this._devices = {}; this.comms = new ZNPSerial(); // Device state notifications this.comms.on('command:ZDO_STATE_CHANGE_IND', this._handleStateChange.bind(this)); // Device announcements this.comms.on('command:ZDO_END_DEVICE_ANNCE_IND', this._handleDeviceAnnounce.bind(this)); // Device endpoint responses this.comms.on('command:ZDO_MATCH_DESC_RSP', this._handleDeviceMatchDescriptionResponse.bind(this)); this.comms.on('command:ZDO_ACTIVE_EP_RSP', this._handleDeviceMatchDescriptionResponse.bind(this)); // Endpoint description responses this.comms.on('command:ZDO_SIMPLE_DESC_RSP', this._handleEndpointSimpleDescriptorResponse.bind(this)); // Application framework (ZCL) messages this.comms.on('command:AF_INCOMING_MSG', this._handleAFIncomingMessage.bind(this)); } util.inherits(ZNPClient, EventEmitter); /** * Connects to a local ZigBee Network Processor via serial interface. * * @param {string} serialPortPath Path to the serial port. * @return {promise} */ ZNPClient.prototype.connectToPort = function(serialPortPath) { return this.comms.connectToPort(serialPortPath); }; /** * Closes the serial interface */ ZNPClient.prototype.close = function() { this.comms.closePort(); }; /** * Retrieves firmware version information from the device. * @return {promise} A promise that resolves to the version information for the * underlying device. */ ZNPClient.prototype.firmwareVersion = function() { return this.comms .request('SYS_VERSION') .then(function(versionPacket) { var deferred = when.defer(); Dissolve() .uint8('transportRevision') .uint8('productId') .uint8('majorRelease') .uint8('minorRelease') .uint8('maintenanceRelease') .tap(function() { deferred.resolve({ type: 'ti-znp', specifics: this.vars, }); }) .write(versionPacket.data); return deferred.promise; }); }; /** * Starts the local device as a coordinator, registering endpoints and * callbacks. * @return {promise} A promise that resolves when the device is in coordinator role. */ ZNPClient.prototype.startCoordinator = function() { var becameCoordinator = this._promiseFutureState(ZNP.ZDOState.DEV_ZB_COORD); var startRequest = when_sequence([ this.writeConfiguration.bind(this, 'ZCD_NV_STARTUP_OPTION', 1, 0), // don't clear on startup this.writeConfiguration.bind(this, 'ZCD_NV_ZDO_DIRECT_CB', 1, 1), // DO get direct callbacks this.writeConfiguration.bind(this, 'ZCD_NV_SECURITY_MODE', 1, 1), this.writeConfiguration.bind(this, 'ZCD_NV_LOGICAL_TYPE', 1, ZNP.LogicalType.COORDINATOR), this.writeConfiguration.bind(this, 'ZCD_NV_PANID', 2, 0xBEEF), this.writeConfiguration.bind(this, 'ZCD_NV_CHANLIST', 4, ZNP.Channel.CHANNEL_11) ]) .then(function(results) { return this.comms.request('ZB_START_REQUEST').then(function(response) { if (response.data.length !== 0) { throw new Error('ZB_START_REQUEST failed'); } return response; }); }.bind(this)); return when.all([startRequest, becameCoordinator]) .then(this._registerApplicationEndpoint.bind(this)) .then(this._startupFromApp.bind(this)) .then(this._registerForCallbacks.bind(this)); }; /** * Resets the device, optionally resetting all network settings. * @return {promise} A promise of the client coming back up, ready for new commands. */ ZNPClient.prototype.resetDevice = function(resetNetworkSettings) { var reconnectDeferred = when.defer(); // will resolve once the serial port is reconnected to the application this.comms.once('connected', function() { debug('Client::resetDevice', 'reconnect promise resolving'); reconnectDeferred.resolve(this); }.bind(this)); var _doReset = function() { this.comms.sendPacket(ZNP.CommandType.SREQ, ZNP.CommandSubsystem.SYS, ZNP.Commands.SYS.SYS_RESET_REQ); }.bind(this); // a confirmed response from the SOC var resetPromise; if (resetNetworkSettings) { var startupOptions = ZNP.StartupOption.get('STARTOPT_CLEAR_CONFIG | STARTOPT_CLEAR_STATE'); resetPromise = this .writeConfiguration('ZCD_NV_STARTUP_OPTION', 1, startupOptions) .then(function() { return _doReset(); }.bind(this)); } else { resetPromise = when(_doReset()); } resetPromise = resetPromise.then(function() { debug('Client::resetDevice', 'forcing serial port close'); return this.comms.closePort(); }.bind(this)); // both of the above must finish return when.all([resetPromise, reconnectDeferred.promise]); }; /** * Write a device configuration paramater. * @param {enum} key * @param {Integer} size * @param {any} value * @return {promise} A promise of the configuration being written. */ ZNPClient.prototype.writeConfiguration = function(key, size, value) { var data = new Buffer(size); if ( typeof value == 'object' && value.hasOwnProperty('value') ) { value = value.value; } if ( typeof value == 'number' ) { if ( size == 1 ) { data.writeUInt8(value, 0); } else if ( size == 2 ) { data.writeUInt16LE(value, 0); } else if ( size == 4 ) { data.writeUInt32LE(value, 0); } } debug('writeConfiguration', key, data); var setConfigKey = Concentrate() .uint8(ZNP.ConfigurationParameters.ZCD.get(key).value) // ConfigId .uint8(size) .buffer(data) .result(); return this.comms .request('ZB_WRITE_CONFIGURATION', setConfigKey) .then(function(response) { if (response.data[0] !== 0) { throw new Error('Invalid data'); } return response; }); }; /** * Send an Application Framework Request to the endpoint. * @param {Object} params See: packets.js - AF_DATA_REQUEST_EXT * @return {Promise} A promise that resolves to a status. See: constants.js - ZNP.Status */ ZNPClient.prototype.sendAFDataRequest = function(params) { debug('AFDataRequest', 'AF data >>', params); var payload = packets.AF_DATA_REQUEST_EXT.write(params); return this.comms.request('AF_DATA_REQUEST_EXT', payload).then(this._parseStatus); }; ZNPClient.prototype.devices = function() { return this._getNumDevices() .then(function(numDevices) { debug('devices', numDevices + ' devices currently paired'); var deviceReqs = []; for (var i = 0; i < numDevices; i++) { deviceReqs.push( this._getDeviceByIndex.bind(this, i) ); } return when_sequence(deviceReqs); }.bind(this)); }; /** * Send an Application Framework Request to the endpoint. * @param {[type]} destination * @param {[type]} data */ ZNPClient.prototype.sendAFDataRequest = function(params) { debug('AFDataRequest', 'AF data >>', params); var payload = packets.AF_DATA_REQUEST_EXT.write(params); return this.comms.request('AF_DATA_REQUEST_EXT', payload).then(this._parseStatus); }; /* Private */ /** * Returns a promise that resolves when the device changes to the specifies state. * @param {[type]} desiredState * @return {[type]} */ ZNPClient.prototype._promiseFutureState = function(desiredState) { var futureState = when.defer(); var stateMonitor = function(state) { if (state == desiredState) { futureState.resolve(state); this.removeListener('state_change', stateMonitor); } }; this.on('state_change', stateMonitor); return futureState.promise; }; /** * Runs ZDO startup, checking for success. Returns a promise that resolves only * if the network is started successfully. * @private * @return {[type]} */ ZNPClient.prototype._startupFromApp = function() { var startupPayload = Concentrate() .uint16le(0) // startDelay .result(); return this.comms .request('ZDO_STARTUP_FROM_APP', startupPayload) .then(function(response) { var statuses = [ 'Restored network state', 'New network state', 'Leave and not Started', ]; var status = response.data[0]; var desc = statuses[status]; if (status >= 2) { throw new Error('Invalid response from ZDO_STARTUP_FROM_APP: ' + desc); } debug('_startupFromApp', 'Success:', desc); return { status: { id: status, msg: desc, } }; }); }; /** * Registers for callbacks * @return {[type]} */ ZNPClient.prototype._registerForCallbacks = function() { var cbRegisterPayload = Concentrate() .uint16le(0x500) // clusterID .result(); return this.comms .request('ZDO_MSG_CB_REGISTER', cbRegisterPayload) .then(this._parseStatus) .then(function(status) { debug('registerApplicationEndpoint', 'response status', status); if (status.key !== 'ZSuccess') { throw new Error('ZDO_MSG_CB_REGISTER failed with error: ' + status.key); } return status; }); }; var SRC_ENDPOINT = 20; // FIXME: this is hardcoded, but shouldn't be. /** * Registers an application endpoint on the ZigBee SOC. * @return {promise} A promise that resolves when the endpoint is registered. */ ZNPClient.prototype._registerApplicationEndpoint = function() { var registerPayload = Concentrate() .uint8(SRC_ENDPOINT) // AppEndPoint .uint16le(0x0104) // AppProfileID .uint16le(0x0000) // DeviceId (ignored) .uint8(0) // DeviceVersion (ignored) .uint8(0x00) // LatencyReq (0x00-No latency) .uint8(1) // AppNumInClusters // AppInClusterList here: .uint16le(0x0000) // Basic .uint8(1) // AppNumOutClusters // AppOutClusterList here .uint16le(0x0500) // IAS Zone .result(); return this.comms .request('AF_REGISTER', registerPayload) .then(this._parseStatus) .then(function(status) { debug('registerApplicationEndpoint', 'response status', status); if (status.key !== 'ZSuccess' && status.key !== 'ZApsDuplicateEntry') { throw new Error('registerApplicationEndpoint failed with error: ' + status.key); } return status; }); }; /** * Returns a promise that resolves to the number of paired devices associated to * our local coordinator. * @return {[type]} */ ZNPClient.prototype._getNumDevices = function() { var assocCountPayload = Concentrate() .uint8(ZNP.NodeRelation.PARENT.value) // startRelation .uint8(ZNP.NodeRelation.OTHER.value) // endRelation .result(); return this.comms .request('UTIL_ASSOC_COUNT', assocCountPayload) .then(function(response) { return response.data[0]; }); }; /** * Returns the Nth device by index in the internal table. This seems race-y, but * is how people seem to do it. Hopefully devices are never removed from this * table at runtime, only disabled in it. * @return {[type]} */ ZNPClient.prototype._getDeviceByIndex = function(deviceIndex) { // return cached device if we have one. if (this._devices.hasOwnProperty(deviceIndex)) { return when(this._devices[deviceIndex]); } // request device with this address manager index from the SOC var assocCountPayload = Concentrate() .uint8(deviceIndex) // number .result(); return this.comms .request('UTIL_ASSOC_FIND_DEVICE', assocCountPayload) .then(function(response) { return this._cacheDeviceFromPayload(response.data); }.bind(this)); }; /** * Returns a device by network adddress. * @return {[type]} */ ZNPClient.prototype._getDeviceByShortAddress = function(shortAddress) { // return cached device if we have one for (var idx in this._devices) { var device = this._devices[idx]; if (device.shortAddress == shortAddress) { return when(device); } } // request device with this shortAddress from the SOC var assocPayload = Concentrate() .buffer(new Buffer([0,0,0,0,0,0,0,0])) .uint16le(shortAddress) // number .result(); return this.comms .request('UTIL_ASSOC_GET_WITH_ADDRESS', assocPayload) .then(function(response) { return this._cacheDeviceFromPayload(response.data); }.bind(this)); }; ZNPClient.prototype._cacheDeviceFromPayload = function(payload) { var devicePromise = Device.deviceForInfo(this, this._parseDeviceInfo(payload)); return devicePromise.tap(function(device) { this._devices[device.deviceInfo.addrIdx] = device; }.bind(this)); }; /** * Returns a promise of a parsed device info object from a payload Buffer. * @param {[type]} deviceInfoPayload * @return {[type]} */ ZNPClient.prototype._parseDeviceInfo = function(deviceInfoPayload) { var deferred = when.defer(); var parser = Dissolve() .uint16le('shortAddr') .uint16le('addrIdx') .uint8('nodeRelation') .uint8('devStatus') .uint8('assocCnt') .uint8('age') .uint8('txCounter') .uint8('txCost') .uint8('rxLqi') .uint8('inKeySeqNum') .uint32le('inFrmCntr') .uint16le('txFailure') .tap(function() { this.vars.devStatus = ZNP.ZDOState.get(this.vars.devStatus); debug('_parseDeviceInfo', 'DEVICE FOUND:', this.vars); deferred.resolve(this.vars); }) .write(deviceInfoPayload); return deferred.promise; }; ZNPClient.prototype._parseStatus = function(statusPayload) { return ZNP.Status.get(statusPayload.data[0]); }; /* Event Handlers */ /** * Handler for device state changes (ZDO_STATE_CHANGE_IND). * @param {[type]} packet * @return {[type]} */ ZNPClient.prototype._handleStateChange = function(packet) { var state = ZNP.ZDOState.get(packet.data[0]); debug('state change', state); this.emit('state_change', state); }; /** * Handler for device announce (ZDO_END_DEVICE_ANNCE_IND). * @param {[type]} packet * @return {[type]} */ ZNPClient.prototype._handleDeviceAnnounce = function(packet) { var doAnnounce = function(vars) { this._getDeviceByShortAddress(vars.srcAddr) .then(function(device) { device._setIEEEAddressFromBuffer(vars.IEEEAddr); this.emit('device_announce', device); }.bind(this)); }.bind(this); Dissolve() .uint16le('srcAddr') .uint16le('nwkAddr') .buffer('IEEEAddr', 8) .uint8('capabilities') .tap(function() { doAnnounce(this.vars); }) .write(packet.data); }; ZNPClient.prototype._handleDeviceMatchDescriptionResponse = function(packet) { var self = this; var parser = Dissolve() .uint16le('srcAddr') .uint8('status') .uint16le('nwkAddr') .uint8('matchLength') .buffer('matchList', 'matchLength') .tap(function() { this.vars.status = ZNP.Status.get(this.vars.status); var response = this.vars; if (response.status.key == 'ZSuccess') { var endpointIds = Array.prototype.slice.call(response.matchList, 0); self._getDeviceByShortAddress(response.nwkAddr).then(function(device) { debug('_handleDeviceMatchDescriptionResponse', 'Found endpoints', endpointIds, 'for device', device.IEEEAddress ); device.emit('endpointIds', endpointIds); }); } else { console.error('Bad status for ZDO_MATCH_DESC_RSP', response.status); } }).write(packet.data); }; ZNPClient.prototype._handleEndpointSimpleDescriptorResponse = function(packet) { debug('_handleEndpointSimpleDescriptorResponse', packet); var self = this; var parser = Dissolve() .uint16le('srcAddr') .uint8('status') .uint16le('nwkAddr') .uint8('length') .uint8('endpoint') .uint16le('profileId') .uint16le('deviceId') .uint8('deviceVersion') .uint8('numInClusters') .tap(function() { this.buffer('inClustersBuf', this.vars.numInClusters * 2); }) .uint8('numOutClusters') .tap(function() { this.buffer('outClustersBuf', this.vars.numOutClusters * 2); }) .tap(function() { this.vars.status = ZNP.Status.get(this.vars.status); if (this.vars.status.key != 'ZSuccess') { console.error('Failed handling _handleEndpointSimpleDescriptorResponse. Status', this.vars.status); return; } this.vars.inClusters = []; this.vars.outClusters = []; for (var i = 0; i < this.vars.numInClusters; i++) { this.vars.inClusters.push( this.vars.inClustersBuf.readUInt16LE(2*i) ); } for (var j = 0; j < this.vars.numOutClusters; j++) { this.vars.outClusters.push( this.vars.outClustersBuf.readUInt16LE(2*j) ); } delete this.vars.inClustersBuf; delete this.vars.outClustersBuf; var response = this.vars; self._getDeviceByShortAddress(response.nwkAddr).then(function(device) { device.emit('simpleDescriptor:' + response.endpoint, response); }); }) .write(packet.data); }; ZNPClient.prototype._handleAFIncomingMessage = function(packet) { debug('_handleAFIncomingMessage', packet); var self = this; var parser = Dissolve() .uint16le('groupId') .uint16le('clusterId') .uint16le('srcAddr') .uint8('srcEndpoint') .uint8('destEndpoint') .uint8('wasBroadcast') .uint8('linkQuality') .uint8('securityUse') .uint32le('timestamp') .uint8('transSeqNumber') .uint8('len') .buffer('data', 'len') .tap(function() { var message = this.vars; debug('_handleAFIncomingMessage', 'parsed', message); self.emit('incoming-message', message); }) .write(packet.data); }; module.exports = ZNPClient;