zigbee
Version:
ZigBee for Node.JS (using the TI CC2530/CC2531)
282 lines (234 loc) • 8.17 kB
JavaScript
'use strict';
var debug = require('debug')('znp-serial');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var Dissolve = require('dissolve');
var Concentrate = require('concentrate');
var SerialPort = require('serialport').SerialPort;
var when = require('when');
var ZNP = require('./constants');
/**
* Handles byte-level translation of ZNP protocol messages from the SOC.
*
* Emits 'command:<command>' events for each MonitorTest command supported,
* 'packet' for every command received, and 'unhandledPacket' for any commands
* not already handled by the more specific 'command:' handler.
*/
function ZNPSerial() {
}
util.inherits(ZNPSerial, EventEmitter);
/**
* Connects to the speicified serial port (currently at 115200 baud).
*
* Attempts to bypass any bootloader.
* @param {string} serialPortPath
* @return {promise} which will resolve when the serial port is connected
*/
ZNPSerial.prototype.connectToPort = function(serialPortPath) {
var openImmediately = false;
this.serialPort = new SerialPort(serialPortPath, {
baudrate: 115200
}, openImmediately);
this.concentrate = Concentrate();
var deferred = when.defer();
/* when a serial port is newly opened, we want to send:
* 1) a bootloader bypass (0x07)
* 2) a ping request
* depending on the device type and bootloader status we get multiple responses:
* CC2531, bootloader: serial port close and USB re-enumerate
* CC2531, application: ping response
* CC2530, bootloader: bootloader response of 0x00 and a ping response
* CC2530, application: ping response (app ignores 0x07 since not SOF)
* so we can respond as follows:
* ping response: application ready
* serial close: reconnect and repeat above process
*/
this.closed = true;
var onOpened = function() {
debug('serialPort', 'open');
this.closed = false;
// close case (currently node-serialport appears to give 'error' when the socket closes)
this.serialPort.on('error', function() {
if (!this.closed) {
debug('serialPort', 'error -> close');
this.closed = true;
this.closePort();
}
}.bind(this));
this.serialPort.on('close', function() {
debug('serialPort', 'close?');
}.bind(this));
// application ready case: resolve
this.removeAllListeners('command:SYS_PING');
this.removeAllListeners('command:SYS_RESET_IND');
this.once('command:SYS_PING', function() {
this.removeAllListeners('command:SYS_RESET_IND');
debug('serialPort', 'ping');
if (deferred) {
deferred.resolve(this);
deferred = null;
}
this.emit('connected');
}.bind(this));
this.once('command:SYS_RESET_IND', function() {
this.removeAllListeners('command:SYS_PING');
debug('serialPort', 'reset_ind');
if (deferred) {
deferred.resolve(this);
deferred = null;
}
this.emit('connected');
}.bind(this));
this.serialPort.write(new Buffer([0x0, 0xef]), function() {
debug('serialPort', 'binding');
// bind the concentrator/dissolver
this.serialPort.pipe(this._createParser());
this.concentrate = Concentrate();
this.concentrate.pipe(this.serialPort);
this.sendPacket(ZNP.CommandType.SREQ, ZNP.CommandSubsystem.SYS, ZNP.Commands.SYS.SYS_PING);
}.bind(this));
}.bind(this);
this.serialPort.open(onOpened);
this._reconnectSerial = function() {
this.serialPort.open(onOpened);
}.bind(this);
return deferred.promise;
};
/**
* Forcefully close the port, causing a fresh reconnect.
*/
ZNPSerial.prototype.closePort = function() {
debug('serialPort', 'close');
this.closed = true;
this.serialPort.close(); // force a close
setTimeout(this._reconnectSerial, 1000);
};
/**
* Creates a parser (dissolver) of data from the serialPort, which emits data to
* _handleIncomingData.
*/
ZNPSerial.prototype._createParser = function() {
var parser = Dissolve().loop(function(end) {
this
.uint8('sof')
.tap(function() {
if (this.vars.sof == 0xFE) {
// OK, continue (otherwise ignore)
this.uint8('length')
.uint8('cmd0')
.uint8('cmd1')
.buffer('data', 'length')
.uint8('fcs')
.tap(function() {
this.push(this.vars);
this.vars = {};
});
} else {
console.log('Invalid SOF', this.vars.sof, '- skipping.');
}
});
});
// setup handler for incoming parsed packets
parser.on('data', this._handleIncomingData.bind(this));
return parser;
};
/**
* Transforms a partially parsed packet, performing additional parsing and emits
* appropriate events for the packet.
* @private
* @param {object} packet The partially parsed packet from the dissolver.
*/
ZNPSerial.prototype._handleIncomingData = function(packet) {
var subsystem = ZNP.CommandSubsystem.get( packet.cmd0 & 0x1f );
packet.command = {
type: ZNP.CommandType.get( packet.cmd0 & 0xe0 ),
subsystem: subsystem,
id: ZNP.Commands[subsystem.key] ? ZNP.Commands[subsystem.key].get( packet.cmd1 ) : null,
};
if (!packet.command.id) {
console.log('UNKNOWN CMD', packet.cmd0, packet.cmd1);
} else {
debug(packet.command.id.key, packet.command.type.key, packet.data);
}
this.emit('packet', packet);
var hadListeners = false;
if ( packet.command.id ) {
hadListeners = this.emit('command:' + packet.command.id.key, packet);
}
if (!hadListeners) {
this.emit('unhandledPacket', packet);
}
};
/**
* Encodes and sends a MonitorTest packet without any response handling. This
* should typically not be used, as most or all packets have an immediate
* response "SRSP" message which confirms it was received, which is handled by
* requires().
* @param {enum} type
* @param {enum} subsystem
* @param {enum} commandId
* @param {Buffer} buffer
*/
ZNPSerial.prototype.sendPacket = function(type, subsystem, commandId, buffer) {
buffer = buffer || new Buffer(0);
debug('sendPacket', type, subsystem, commandId, buffer);
var payload = Concentrate()
.uint8(buffer.length) // length
.uint8(type.value | subsystem.value) // cmd0
.uint8(commandId.value) // cmd1
.buffer(buffer)
.result();
var fcs = calculateFCS(payload);
var packet = Concentrate()
.uint8(0xfe) // sof
.buffer(payload)
.uint8(fcs)
.result();
debug('sendPacket', '>>', packet);
this.concentrate
.buffer(packet)
.flush(); // send it out
};
/**
* Performs a request by sending the specified command with data. The returned
* promise is resolved to the direction response from the SOC, which may not be
* the final response (some commands return an acknowledgement immediately the
* asyncronously return actual data at a later time).
* @param {string} command The name of the MonitorTest command to send.
* @param {Buffer} buffer The payload to send with the command.
* @return {promise} A promise that resolves to the result packet.
*/
ZNPSerial.prototype.pendingSyncResponses = {};
ZNPSerial.prototype.request = function(command, buffer) {
if (!buffer) {
buffer = new Buffer(0);
}
var subsystem = command.split('_')[0];
var commandId = ZNP.Commands[subsystem][command];
var deferred = when.defer();
// buffer up command responses in order
if (!this.pendingSyncResponses[commandId.key]) {
this.pendingSyncResponses[commandId.key] = [];
this.on('command:' + commandId.key, function(packet) {
this.pendingSyncResponses[commandId.key].shift()(packet);
}.bind(this));
}
this.pendingSyncResponses[commandId.key].push(function(packet) {
deferred.resolve(packet);
});
this.sendPacket(ZNP.CommandType.SREQ, ZNP.CommandSubsystem[subsystem], commandId, buffer);
return deferred.promise;
};
/**
* Calculates the FCS for a given buffer.
* @param {Buffer} buffer
* @return {Integer} FCS
*/
function calculateFCS(buffer) {
var fcs = 0;
for (var i = 0; i < buffer.length; i++) {
fcs ^= buffer[i];
}
return fcs;
}
module.exports = ZNPSerial;