cs-acn
Version:
Control Solutions Adaptive Control Network
965 lines (773 loc) • 24.2 kB
JavaScript
/**
* Entry point for Control Solutions Node.js package
*
* This file exposes the API for communicating via serial port to
* CS's Adaptive Control Network products.
*
*/
;
// built-in node utility module
var util = require('util');
// Node event emitter module
var EventEmitter = require('events').EventEmitter;
// Module which manages the serial port
var serialPortFactory = require('serialport');
// Include the MODBUS master
var Modbus = require('@csllc/cs-modbus');
// assertion library
var chai = require('chai');
// Promise library
var Promise = require('bluebird');
// Extra Buffer handling stuff
var buffers = require('h5.buffers');
/**
* Translates a run code (from ReportSlaveId) to a string
*
* @param {number} code run code
*/
function FaultToString( code ) {
switch( code ) {
case 0:
return 'Unprogrammed';
default:
return 'None';
}
}
/**
* Translates a product type (from ReportSlaveId) to a string
*
* @param {number} code run code
*/
function ProductToString( code ) {
switch( code ) {
case 1:
return 'Gw/Repeater';
case 2:
return 'Fob';
default:
return 'Unknown';
}
}
/**
* Zero pads a number (on the left) to a specified length
*
* @param {number} number the number to be padded
* @param {number} length number of digits to return
* @return {string} zero-padded number
*/
function zeroPad( number, length ) {
var pad = new Array(length + 1).join( '0' );
return (pad+number).slice(-pad.length);
}
/**
* Constructor: initializes the object and declares its public interface
*
* @param string name: the name of the port (as known to the operating system)
* @param object config: optional object containing configuration parameters:
*/
function AcnPort (name, options) {
var me = this;
// for debugging
Promise.longStackTraces();
// Initialize the state of this object instance
me.name = name;
// keep track of reconnection timers
me.reconnectTimer = null;
// Modbus object IDs for this device
me.object = {
FACTORY : 0,
USER : 1,
NET_STATUS : 2,
SCAN_RESULT : 3,
CONNECTION_TABLE : 4,
COORD_STATUS : 5,
SENSOR_DATA : 7
};
me.commands = [
'',
'reset',
'save',
'restore',
'pair',
'clear',
'sendconn',
'sendshort',
'sendlong',
'broadcast',
'scan',
'ping'
];
// The serial port object that is managed by this instance.
// The port is not opened, just instantiated
options.port.options.autoOpen = false;
me.port = new serialPortFactory( name, options.port.options );
me.list = serialPortFactory.list;
options.master.transport.connection.serialPort = me.port;
// Create the MODBUS master using the supplied options
me.master = Modbus.createMaster( options.master );
// Catch an event if the port gets disconnected
me.master.on( 'disconnected', function() {
// FYI - the port object drops all listeners when it disconnects
// but after the disconnected event, so they haven't been dropped at
// this point.
me.emit( 'disconnected');
// let the port finish disconnecting, then work on reconnecting
process.nextTick( function() { me.reconnect(); } );
});
}
// This object can emit events. Note, the inherits
// call needs to be before .prototype. additions for some reason
util.inherits(AcnPort, EventEmitter);
/**
* Open the serial port.
*
* @returns {object} promise
*/
AcnPort.prototype.open = function() {
var me = this;
return new Promise(function(resolve, reject){
me.port.open( function(error) {
if( error ) {
reject( error );
}
else {
me.emit( 'connected');
resolve();
}
});
});
};
/**
* Attempt to reopen the port
*
*/
AcnPort.prototype.reconnect = function() {
var me = this;
// re-attach event hooks for the serial port
me.master.connection.setUpSerialPort(me.port);
// re-attach event hooks for the serial port
me.master.setUpConnection();
me.reconnectTimer = setInterval( function() {
me.open()
.then( function () {
clearInterval( me.reconnectTimer );
me.reconnectTimer = null;
})
.catch(function() {});
}, 1000 );
};
/**
* Converts a 16-bit short address into a string like 'A1B2'
* @param {Buffer} buffer buffer containing the bytes to format
* @param {number} offset offset into the buffer to start reading
* @return {string} a string containing the 16-bit hex value
*/
AcnPort.prototype.destroy = function() {
// this causes an error about port not open; I think it gets cleaned
// up in master destroy anyway
//this.port.close();
this.master.destroy();
};
/**
* Zero pads a number (on the left) to a specified length
*
* @param {number} number the number to be padded
* @param {number} length number of digits to return
* @return {string} zero-padded number
*/
AcnPort.prototype.zeroPad = function( number, length ) {
var pad = new Array(length + 1).join( '0' );
return (pad+number).slice(-pad.length);
};
AcnPort.prototype.getSlaveId = function() {
var me = this;
return new Promise(function(resolve, reject){
me.master.reportSlaveId({
onComplete: function(err, response) {
if( err ){
reject( err );
}
else {
var serial = response.getValues().readUInt32BE(0);
serial = zeroPad( serial, 10);
resolve( {
product: response.product,
productType: ProductToString(response.product),
run: response.run,
version: response.getVersion(),
serialNumber: serial,
fault: FaultToString( response.run )
} );
}
}
});
});
};
/**
* Formats a buffer of bytes into a string like xx:yy:zz
*
* @param Buffer buffer Contains the bytes to be formatted
* @param integer offset offset into the buffer to start reading
* @param integer length number of bytes to process
* @return string a string like 'xx:yy:zz'
*/
AcnPort.prototype.macToString = function( buffer, offset, length ) {
var mac = [];
// Build a string array of the MAC address bytes
for( var i = 0; i < length; i++ ) {
mac.push( this.zeroPad( buffer[ offset + i].toString(16), 2));
}
return mac.join(':');
};
/**
* Parses a string like 11:22:33:44:55:66:77:88 to a binary buffer
*
* @param {[type]} mac [description]
* @return {[type]} [description]
*/
AcnPort.prototype.stringToMac = function( str ) {
var macbytes = str.split(':');
var mac = new Buffer(8);
for( var i = 0; i < 8; i++ ) {
mac[i] = parseInt(macbytes[i],16);
}
return mac;
};
/**
* Converts a 16-bit short address into a string like 'A1B2'
* @param {Buffer} buffer buffer containing the bytes to format
* @param {number} offset offset into the buffer to start reading
* @return {string} a string containing the 16-bit hex value
*/
AcnPort.prototype.shortAddressToString = function( buffer, offset ) {
return this.zeroPad( buffer.readUInt16LE(offset).toString(16), 4);
};
/**
* Gets the factory configuration object.
*
* The factory configuration is stored in non-volatile memory.
*
* The callback's error parameter will be non-null (an Error instance)
* if an error occurs while processing the command.
*
* If the command succeeds but the factory configuration is not
* valid (eg has not yet been programmed), this function returns null
* as the response argument of the callback.
*
* Otherwise( on success) the response contains:
* macAddress: string of 8 hex bytes separated by :
* example: 00:00:FF:FF:00:00:12:34
* serialNumber: alphanumeric string containing serial number
*
* @param {Function} callback (err, response)
*/
AcnPort.prototype.getFactoryConfig = function() {
var me = this;
return new Promise(function(resolve, reject){
me.master.readObject( me.object.FACTORY, {
onComplete: function(err,response) {
if( response && response.exceptionCode ) {
// i'm not sure how to catch exception responses from the
// slave in a better way than this
err = new Error( 'Exception ' + response.exceptionCode );
}
if( err ) {
reject( err );
}
else {
// Check for an invalid/unprogrammed object
if( response.values.length === 1 && response.values[0] === 0) {
resolve( null );
}
else {
chai.assert( response.values.length === 20,
'Wrong response length for Factory object (' + response.values.length + ')' );
// read the mac address and make a string
var mac = me.macToString( response.values, 0, 8 );
resolve( {
macAddress: mac,
serialNumber: response.values.readUInt32BE(8),
productType: response.values[12]
});
}
}
},
onError: function( err ) {
reject( err );
}
});
});
};
/**
* Writes the factory configuration into the device NVRAM
*
* @param {Function} callback [description]
*/
AcnPort.prototype.setFactoryConfig = function( data ) {
var me = this;
return new Promise(function(resolve, reject){
// validate the data
if( data.macAddress &&
data.macAddress.length === 8 &&
data.serialNumber &&
data.serialNumber >= 0 &&
data.serialNumber <= 4,294,967,295 &&
data.hasOwnProperty('productType')) {
var builder = new buffers.BufferBuilder();
var reserve = new Buffer(7);
reserve.fill(0);
builder
.pushBuffer( data.macAddress )
.pushUInt32(data.serialNumber, false )
.pushByte(data.productType)
.pushBuffer(reserve);
me.master.writeObject( me.object.FACTORY, builder.toBuffer(), {
onComplete: function(err,response) {
if( response && response.exceptionCode ) {
// i'm not sure how to catch exception responses from the slave in a better way than this
err = new Error( 'Exception ' + response.exceptionCode );
}
if( err ) {
reject( err );
}
else {
if( response.status !== 0 ) {
reject( new Error('Failed to write factory config'));
}
else {
// success!
resolve( null );
}
}
},
onError: function( err ) {
reject( err );
}
});
}
else {
reject( new Error('Invalid data for factory config'));
}
});
};
/**
* Gets object
*
* @param {Function} callback (err, response)
*/
AcnPort.prototype.getConnections = function( callback ) {
var me = this;
this.master.readObject( me.object.CONNECTION_TABLE, {
onComplete: function(err,response) {
if( err ) {
callback( err );
}
else {
console.log(response.values);
console.log(response.values.length);
// these match the array definition in the ACN device
var entrySize = 14;
// length has to be a multiple of the entry size
chai.assert( response.values.length % entrySize === 0,
'Wrong response length for Connections object' );
var numEntries = parseInt(response.values.length / entrySize);
var connections = [];
for( var i = 0; i < numEntries; i++ ) {
// decode the status byte
var statusByte = response.values[i * entrySize + 12];
var status = {
rxOnWhenIdle: ( statusByte & 0x01 ) > 0,
directConnection: ( statusByte & 0x02 ) > 0,
longAddressValid: ( statusByte & 0x04 ) > 0,
shortAddressValid: ( statusByte & 0x08 ) > 0,
finishJoin: ( statusByte & 0x10 ) > 0,
isFamily: ( statusByte & 0x20 ) > 0,
isValid: ( statusByte & 0x80 ) > 0,
};
if( status.isValid ) {
// save the entry in an array
connections.push( {
panId: me.shortAddressToString( response.values, i * entrySize + 0),
altAddress: me.shortAddressToString( response.values, i * entrySize + 2),
address: me.macToString( response.values, 4, 8 ),
status: status,
extra: response.values[i * entrySize + 12],
});
}
}
// return the result to the caller
callback( null, connections );
}
}
});
};
/**
* Gets object
*
* @param {Function} callback (err, response)
*/
AcnPort.prototype.getCoord = function( callback ) {
var me = this;
this.master.readObject( me.object.COORD_STATUS, {
onComplete: function(err,response) {
if( err ) {
callback( err );
}
else {
console.log(response.values);
// return the result to the caller
callback( null, {
} );
}
}
});
};
/**
* Sends a command to the slave
*
* @param {number} id command ID
* @param {Buffer} data additional bytes to send
*
* @returns Promise instance that resolves when command is completed
*/
AcnPort.prototype.command = function( cmd, data ) {
var me = this;
return new Promise(function(resolve, reject){
var id = me.commands.indexOf(cmd );
me.master.command( id, data, {
onComplete: function(err,response) {
if( response && response.exceptionCode ) {
// i'm not sure how to catch exception responses from the slave in a better way than this
err = new Error( 'Exception ' + response.exceptionCode );
}
if( err ) {
reject( err );
}
else {
resolve( response );
}
},
onError: function( err ) {
reject( err );
}
});
});
};
/**
* Sends a command to the slave
*
* @param {number} id command ID
* @param {Buffer} data additional bytes to send
*
* @returns Promise instance that resolves when command is completed
*/
AcnPort.prototype.unlock = function() {
var me = this;
return new Promise(function(resolve, reject){
me.master.command( 255, new Buffer(0), {
onComplete: function(err,response) {
if( response && response.exceptionCode ) {
// i'm not sure how to catch exception responses from the slave in a better way than this
err = new Error( 'Exception ' + response.exceptionCode );
}
if( err ) {
reject( err );
}
else {
resolve( response );
}
},
onError: function( err ) {
reject( err );
}
});
});
};
/**
* Writes multiple values to the slave
*
* @param {number} id command ID
* @param {Buffer} data additional bytes to send
*
* @returns Promise instance that resolves when command is completed
*/
AcnPort.prototype.setRegisters = function( address, values ) {
var me = this;
return new Promise(function(resolve, reject){
var builder = new buffers.BufferBuilder();
for( var i = 0; i < values.length; i++ ) {
builder
.pushUInt16( values[i] );
}
me.master.writeMultipleRegisters( address, builder.toBuffer(), {
onComplete: function(err,response) {
if( response && response.exceptionCode ) {
// i'm not sure how to catch exception responses from the slave in a better way than this
err = new Error( 'Exception ' + response.exceptionCode );
}
if( err ) {
reject( err );
}
else {
resolve( response );
}
},
onError: function( err ) {
reject( err );
}
});
});
};
/**
* Reads registers from the slave
*
* @param {object} items
*
* @returns Promise instance that resolves when command is completed
*/
AcnPort.prototype.read = function( item ) {
var me = this;
return new Promise(function(resolve, reject){
var callback = {
onComplete: function(err, response ) {
if( response && response.exceptionCode ) {
// i'm not sure how to catch exception responses from the slave in a better way than this
err = new Error( 'Exception ' + response.exceptionCode );
}
if( err ) {
reject( err );
}
else {
item.fromBuffer( response.values );
resolve( item );
}
},
onError: function( err ) {
reject( err );
}
};
if( item.type === 'object') {
me.master.readObject( item.addr, callback );
}
else {
me.master.readHoldingRegisters( item.addr, item.length, callback );
}
});
};
/**
* Writes a Register item to the slave
*
* @param {object} item
* @param {varies} value value to be written
*
* @returns Promise instance that resolves when command is completed
*/
AcnPort.prototype.write = function( item, value ) {
var me = this;
return new Promise(function(resolve, reject){
item.unformat( value );
var t1 = me.master.writeMultipleRegisters( item.addr, item.toBuffer(), {
onComplete: function(err, response ) {
if( response && response.exceptionCode ) {
// i'm not sure how to catch exception responses from the slave in a better way than this
err = new Error( 'Exception ' + response.exceptionCode );
}
if( err ) {
reject( err );
}
else {
resolve( true );
}
},
onError: function( err ) {
reject( err );
}
});
});
};
/**
* Performs a network scan and returns the result
*
* On success, this returns an array containing the 'best'
* channel followed by 16 numbers indicating the relative noise level
* of each channel.
*
* @param {number} type: 1=energy scan, 2=active, 3=both
* @param {number} duration enumeration indicating amount of time to dwell on each channel
*
* @returns Promise instance that resolves when command is completed
*/
AcnPort.prototype.scan = function( type, duration ) {
var me = this;
return new Promise(function(resolve, reject){
var id = me.commands.indexOf('scan' );
me.master.command( id, new Buffer([type, duration]), {
//timeout: 10000,
onComplete: function(err, response ) {
if( response && response.exceptionCode ) {
// i'm not sure how to catch exception responses from the slave in a better way than this
err = new Error( 'Exception ' + response.exceptionCode );
}
if( err ) {
reject( err );
}
else {
var values = new buffers.BufferReader( response.values );
resolve( values.readBytes(0, values.length ) );
}
},
onError: function( err ) {
reject( err );
}
});
});
};
/**
* Resets the device
*
* @param {number} type
* @param {number} duration enumeration indicating amount of time to dwell on each channel
*
* @returns Promise instance that resolves when command is completed
*/
AcnPort.prototype.reset = function() {
var me = this;
return new Promise(function(resolve, reject){
var id = me.commands.indexOf('reset' );
me.master.command( id, new Buffer(0), {
timeout: 5000,
onComplete: function(err, response ) {
if( response && response.exceptionCode ) {
// i'm not sure how to catch exception responses from the slave in a better way than this
err = new Error( 'Exception ' + response.exceptionCode );
}
if( err ) {
reject( err );
}
else {
resolve( response.values[0] );
}
},
onError: function( err ) {
reject( err );
}
});
});
};
/**
* Clears the network configuration
*
* @returns Promise instance that resolves when command is completed
*/
AcnPort.prototype.clear = function() {
var me = this;
return new Promise(function(resolve, reject){
var id = me.commands.indexOf('clear' );
me.master.command( id, new Buffer(0), {
timeout: 10000,
onComplete: function(err, response ) {
if( response && response.exceptionCode ) {
// i'm not sure how to catch exception responses from the slave in a better way than this
err = new Error( 'Exception ' + response.exceptionCode );
}
if( err ) {
reject( err );
}
else {
resolve( response.values[0] );
}
},
onError: function( err ) {
reject( err );
}
});
});
};
/**
* initiates the pairing operation
*
* @returns Promise instance that resolves when command is completed
*/
AcnPort.prototype.pair = function() {
var me = this;
return new Promise(function(resolve, reject){
var id = me.commands.indexOf('pair' );
me.master.command( id, new Buffer(0), {
timeout: 10000,
onComplete: function(err, response ) {
if( response && response.exceptionCode ) {
// i'm not sure how to catch exception responses from the slave in a better way than this
err = new Error( 'Exception ' + response.exceptionCode );
}
if( err ) {
reject( err );
}
else {
resolve( response.values[0] );
}
},
onError: function( err ) {
reject( err );
}
});
});
};
/**
* Commands the device to 'ping' another device to verify wireless communication
*
* @param {number} address the address to ping (16 bits)
* @param {number} duration enumeration indicating amount of time to dwell on each channel
*
* @returns Promise instance that resolves when command is completed
*/
AcnPort.prototype.ping = function( address ) {
var me = this;
return new Promise(function(resolve, reject){
var id = me.commands.indexOf('ping' );
var parameters = new Buffer(2);
parameters.writeUInt16BE( address, 0 );
me.master.command( id, parameters, {
onComplete: function(err, response ) {
if( response && response.exceptionCode ) {
// i'm not sure how to catch exception responses from the slave in a better way than this
err = new Error( 'Exception ' + response.exceptionCode );
}
if( err ) {
reject( err );
}
else {
var values = new buffers.BufferReader( response.values );
if( values.length < 7 ) {
resolve( {error: 'No Response'} );
}
else {
values.shiftUInt8(); // the command result
var result = {
rtt: values.shiftUInt16(),
fwd: {
lqi: values.shiftUInt8(),
rssi: values.shiftUInt8()
},
rev: {
lqi: values.shiftUInt8(),
rssi: values.shiftUInt8()
},
};
resolve( result );
}
}
},
onError: function( err ) {
reject( err );
}
});
});
};
/**
* Public interface to this module
*
* The object constructor is available to our client
*
* @ignore
*/
module.exports = AcnPort;