cs-acn
Version:
Control Solutions Adaptive Control Network
921 lines (763 loc) • 22.3 kB
JavaScript
'use strict';
/**
* Defines the register map for ACN devices
*
* See CS document DOC0003825A
*
*/
var buffers = require('h5.buffers');
var Register = require('./Register' );
//------------------------------------//---------------------------------------
// Utility functions
/**
* Translates a product type (from ReportSlaveId) to a string
*
* @param {number} code run code
*/
function RoleToString( code ) {
switch( code ) {
case 0:
return 'End Device';
case 1:
return 'Coordinator';
case 2:
return 'Net Coordinator';
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);
};
/**
* 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'
*/
function macToString( buffer, offset, length ) {
var mac = [];
// Build a string array of the MAC address bytes
for( var i = 0; i < length; i++ ) {
mac.push( 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]
*/
function stringToMac( 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
*/
function shortAddressToString( buffer, offset ) {
return zeroPad( buffer.readUInt16LE(offset).toString(16), 4)
}
/**
* Compute voltage from CS1108 value
* @param {[type]} value [description]
* @return {[type]} [description]
*/
function voltage( value ) {
var volts = value * 1469 / 3 / 16777216.0;
return volts * 24;
}
function valueToCs1108Serial( values ) {
// checksum is the xor of the nibbles of the 3 serial number bytes.
// On my dev board, this calculation does not match what is stored in value[0]
// so the checksum is ignored (until I can verify it is working correctly)
/*var cks =
((values[1] & 0xF0) / 16)
^ (values[1] & 0x0F)
^ ((values[2] & 0xF0) / 16)
^ (values[2] & 0x0F)
^ ((values[3] & 0xF0) / 16)
^ (values[3] & 0x0F); */
//if( ((values[0] & 0x0F) === cks) && ((values[0] & 0xF0) === 0x20)) {
if( (values[0] & 0xF0) === 0x20) {
// valid serial number, prepend the S, zero pad, and convert to decimal
var n = (values[1]*65536 + values[2] * 256 + values[3] ).toString(10);
return 'S' + zeroPad( n, 7 );
}
else {
// serial number not programmed
return '';
}
}
//------------------------------------//---------------------------------------
// Bank 0
var modbusSlaveId = new Register( {
title: 'Slave ID',
addr: 0x00,
});
var channelMap = new Register( {
title: 'Channel Map',
addr: 0x01,
type: 'uint16',
format: Register.prototype.valueToHex16,
unformat: Register.prototype.hex16ToValue
});
var msBetweenStatusTx = new Register( {
title: 'Status Interval',
addr: 0x02,
units: 'ms'
});
var powerOffSec = new Register( {
title: 'Power Off',
addr: 0x03,
units: 's'
});
var networkFormation = new Register( {
title: 'Formation',
addr: 0x04,
});
var pairingTimeout = new Register( {
title: 'Pairing Timeout',
addr: 0x05,
units: 's'
});
var switchDefaults = new Register( {
title: 'Switch Defaults',
addr: 0x06
});
var maxHops = new Register( {
title: 'Max Hops',
addr: 0x07
});
var maxHops = new Register( {
title: 'Max Hops',
addr: 0x07
});
var slowSpeed = new Register( {
title: 'Slow Speed',
addr: 0x08,
format: Register.prototype.valueToHex16,
});
var fastSpeed = new Register( {
title: 'Fast Speed',
addr: 0x09,
format: Register.prototype.valueToHex16,
});
/**
* An item that contains all the bank 0 registers
*
*/
var config = new Register( {
title: 'Configuration',
addr: 0x00,
length: 10,
fromBuffer: function( buf ) {
// data is a buffer containing 'length*2' bytes
modbusSlaveId.set( buf.readUInt16BE( 0 ));
channelMap.set( buf.readUInt16BE( 2 ));
msBetweenStatusTx.set( buf.readUInt16BE( 4 ));
powerOffSec.set( buf.readUInt16BE( 6 ));
networkFormation.set( buf.readUInt16BE( 8 ));
pairingTimeout.set( buf.readUInt16BE( 10 ));
switchDefaults.set( buf.readUInt16BE( 12 ));
maxHops.set( buf.readUInt16BE( 14 ));
slowSpeed.set( buf.readUInt16BE( 16 ));
fastSpeed.set( buf.readUInt16BE( 18 ));
},
format: function() {
return {
modbusSlaveId: modbusSlaveId.format(),
channelMap: channelMap.format(),
msBetweenStatusTx: msBetweenStatusTx.format(),
powerOffSec: powerOffSec.format(),
networkFormation: networkFormation.format(),
pairingTimeout: pairingTimeout.format(),
switchDefaults: switchDefaults.format(),
maxHops: maxHops.format(),
slowSpeed: slowSpeed.format(),
fastSpeed: fastSpeed.format(),
};
},
unformat: function( formatted ) {
modbusSlaveId.unformat( formatted.modbusSlaveId );
channelMap.unformat( formatted.channelMap );
msBetweenStatusTx.unformat( formatted.msBetweenStatusTx );
powerOffSec.unformat( formatted.powerOffSec );
networkFormation.unformat( formatted.networkFormation );
pairingTimeout.unformat( formatted.pairingTimeout );
switchDefaults.unformat( formatted.switchDefaults );
maxHops.unformat( formatted.maxHops );
slowSpeed.unformat( formatted.slowSpeed );
fastSpeed.unformat( formatted.fastSpeed );
},
toBuffer: function() {
var builder = new buffers.BufferBuilder();
builder
.pushUInt16( modbusSlaveId.value )
.pushUInt16( channelMap.value )
.pushUInt16( msBetweenStatusTx.value )
.pushUInt16( powerOffSec.value )
.pushUInt16( networkFormation.value )
.pushUInt16( pairingTimeout.value )
.pushUInt16( switchDefaults.value )
.pushUInt16( maxHops.value );
return builder.toBuffer();
}
});
//------------------------------------//---------------------------------------
// Bank 1
var localSwitches = new Register( {
title: 'Local Switches',
addr: 0x0100,
format: Register.prototype.uint16ToBoolArray,
});
var remoteSwitches = new Register( {
title: 'Remote Switches',
addr: 0x0101,
format: Register.prototype.uint16ToBoolArray,
});
var remoteStatus = new Register( {
title: 'Remote Status',
addr: 0x0102,
});
var remoteQuality = new Register( {
title: 'Remote Quality',
addr: 0x0103,
format: function(value) {
return {
rssi: (value & 0xFF),
lqi: (value >> 8 )
};
}
});
var systemState = new Register( {
title: 'State',
addr: 0x0104,
format: function(value) {
switch( value ) {
case 0: return 'None';
case 1: return 'Reset';
case 2: return 'Powerup';
case 3: return 'Idle';
case 4: return 'Active';
case 5: return 'Pairing';
default: return 'Unknown';
}
}
});
var volts = new Register( {
title: 'Volts',
addr: 0x0105,
});
/**
* An item that contains all the bank 1 registers
*
*/
var bank1 = new Register( {
title: 'Bank 1',
addr: 0x0100,
length: 6,
fromBuffer: function( buf ) {
var reader = new buffers.BufferReader( buf );
// data is a buffer containing 'length*2' bytes
localSwitches.set( reader.shiftUInt16() );
remoteSwitches.set( reader.shiftUInt16() );
remoteStatus.set( reader.shiftUInt16() );
remoteQuality.set( reader.shiftUInt16() );
systemState.set( reader.shiftUInt16() );
volts.set( reader.shiftUInt16() );
},
format: function() {
return {
localSwitches: localSwitches.format(),
remoteSwitches: remoteSwitches.format(),
remoteStatus: remoteStatus.format(),
remoteQuality: remoteQuality.format(),
systemState: systemState.format(),
volts: volts.format(),
};
},
});
//------------------------------------//---------------------------------------
// Bank 2
var channel = new Register( {
title: 'Channel',
addr: 0x0200,
});
var fault = new Register( {
title: 'Fault',
addr: 0x0201,
});
/**
* An item that contains all the bank 1 registers
*
*/
var bank2 = new Register( {
title: 'Bank 2',
addr: 0x0200,
length: 2,
fromBuffer: function( buf ) {
var reader = new buffers.BufferReader( buf );
// data is a buffer containing 'length*2' bytes
channel.set( reader.shiftUInt16() );
fault.set( reader.shiftUInt16() );
},
format: function() {
return {
channel: channel.format(),
fault: fault.format(),
};
},
});
//------------------------------------//---------------------------------------
// Bank 3/4 - local/remote outputs
/**
* Decodes a 16-bit register into the 'output configuration' used by bank 3/4
* @param array bits contains 16 bits of info from the record
*/
function registerToOutputConfig( value ) {
var duty = (value & 0x06) >> 1;
var dutyLookup = [ 25, 50, 75, 100 ];
var period = (value & 0xF8) >> 3;
console.log ('output: ' + duty + ' - period: ' + period );
return {
active: (1 === (value & 0x01)),
duty: dutyLookup[ duty ],
period: (period + 1) * 50
};
}
/**
* An item that contains all the bank's registers
*
*/
var lo0 = new Register( {
title: 'Local Output 0',
addr: 0x300,
format: registerToOutputConfig
});
var lo1 = new Register( {
title: 'Local Output 1',
addr: 0x301,
format: registerToOutputConfig
});
var localOutputs = new Register( {
title: 'Local Outputs',
addr: 0x0300,
length: 2,
fromBuffer: function( buf ) {
var reader = new buffers.BufferReader( buf );
lo0.set( reader.shiftUInt16 );
lo1.set( reader.shiftUInt16 );
},
format: function() {
return [
lo0.format(),
lo1.format()
];
},
});
/**
* An item that contains all the bank's registers
*
*/
var ro0 = new Register( {
title: 'Remote Output 0',
addr: 0x400,
format: registerToOutputConfig
});
var ro1 = new Register( {
title: 'Remote Output 1',
addr: 0x401,
format: registerToOutputConfig
});
var ro2 = new Register( {
title: 'Remote Output 2',
addr: 0x402,
format: registerToOutputConfig
});
var remoteOutputs = new Register( {
title: 'Remote Outputs',
addr: 0x0400,
length: 16,
fromBuffer: function( buf ) {
var reader = new buffers.BufferReader( buf );
ro0.set( reader.shiftUInt16 );
ro1.set( reader.shiftUInt16 );
ro2.set( reader.shiftUInt16 );
},
format: function() {
return [
ro0.format(),
ro1.format(),
ro2.format()
];
},
});
//------------------------------------//---------------------------------------
// Objects
/**
* The network status object
*
*/
var networkStatus = new Register( {
title: 'Network Status',
addr: 2,
type: 'object',
fromBuffer: function( buf ) {
this.value = {
shortAddress: shortAddressToString(buf, 0 ),
parent: buf[2],
panId: shortAddressToString (buf, 3 ),
currentChannel: buf[5]
};
},
format: function() {
return this.value;
},
});
/**
* The scan results object
*
*/
var scanResult = new Register( {
title: 'Scan Result',
addr: 3,
type: 'object',
fromBuffer: function( buf ) {
//console.log(buf);
//console.log(buf.length);
// these match the array definition in the ACN device
var entrySize = 15;
var numEntries = parseInt(buf.length / entrySize);
this.value = [];
for( var i = 0; i < numEntries; i++ ) {
// decode the status byte
var thebyte = buf[i * entrySize + 13];
var capability = {
role: ( thebyte & 0x03 ),
sleep: ( thebyte & 0x04 ) > 0,
securityEnable: ( thebyte & 0x08 ) > 0,
repeatEnable: ( thebyte & 0x10 ) > 0,
allowJoin: ( thebyte & 0x20 ) > 0,
direct: ( thebyte & 0x40 ) > 0,
altSourceAddress: ( thebyte & 0x80 ) > 0,
};
var channel = buf[i * entrySize + 0];
if( channel < 255 && channel > 0 ) {
// save the entry in an array
this.value.push( {
channel: channel,
address: macToString( buf, 1, 8 ),
panId: shortAddressToString( buf, i * entrySize + 9),
rssi: buf[i * entrySize + 11],
lqi: buf[i * entrySize + 12],
capability: capability,
peerInfo: buf[i * entrySize + 14]
});
}
}
},
format: function() {
return this.value;
},
});
/**
* The Connection table object
*
*/
var connectionTable = new Register( {
title: 'Connections',
addr: 4,
type: 'object',
fromBuffer: function( buf ) {
//console.log(buf);
//console.log(buf.length);
// these match the array definition in the ACN device
var entrySize = 14;
var numEntries = parseInt(buf.length / entrySize);
this.value = [];
for( var i = 0; i < numEntries; i++ ) {
// decode the status byte
var statusByte = buf[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
this.value.push( {
panId: shortAddressToString( buf, i * entrySize + 0),
altAddress: shortAddressToString( buf, i * entrySize + 2),
address: macToString( buf, i* entrySize + 4, 8 ),
status: status,
extra: buf[i * entrySize + 13],
});
}
}
},
format: function() {
return this.value;
},
});
/**
* The coordinator status object
*
*/
var coordStatus = new Register( {
title: 'Coordinator Status',
addr: 5,
type: 'object',
fromBuffer: function( buf ) {
//console.log(buf);
//console.log(buf.length);
var values = new buffers.BufferReader( buf );
var routingTable = values.shiftBytes(8);
var routingErrors = values.shiftBytes(8);
var coordinators = values.shiftUInt8();
var role = values.shiftUInt8();
this.value = {
role: role,
roleType: RoleToString(role),
known: this.uint8ToBoolArray( coordinators, 1),
route: []
};
// now create the routing table
for (var i = 0; i < 8; i++ ){
this.value.route.push( {
to: i,
nextHop: routingTable[i],
errors: routingErrors[i]
});
}
},
format: function() {
return this.value;
},
});
function cs1108StateFlags( byte ) {
var chargeMode;
switch( byte & 0xF ) {
case 1:
chargeMode = 'Pre-charge';
break;
case 2:
chargeMode = 'Bulk';
break;
case 4:
chargeMode = 'Overcharge';
break;
case 8:
chargeMode = 'Float Charge';
break;
default:
chargeMode = 'Not Charging';
break;
}
return {
charging: byte & 0xF,
chargeMode: chargeMode,
inUse: (byte & 0x10)
};
}
function cs1108Hours( fraction, hours ) {
return Math.round((hours + (fraction/65536))*10)/10;
}
/**
* The coordinator status object
*
*/
var sensorData = new Register( {
title: 'Sensor Data',
addr: 7,
type: 'object',
fromBuffer: function( buf ) {
//console.log( buf);
if( buf.length < 46 ){
this.msgtype = 0;
}
else {
var values = new buffers.BufferReader( buf );
// Just keep the binary data of the packet
// this.packet = values.shiftBytes( 40 );
var dataType = values.shiftUInt8();
if( dataType === 1 ) {
// Controller report
this.packet = {
datatype: dataType,
serial : valueToCs1108Serial( values.shiftBytes( 4 ) ),
faultLog : values.shiftBytes( 16 ),
meters : {
hours : cs1108Hours( values.shiftUInt16( true ), values.shiftUInt16( true ) ),
noFloat : values.shiftUInt8(),
lowBatMin : values.shiftUInt8(),
lowBatHrs : values.shiftUInt8(),
overtemp : values.shiftUInt8(),
throtFail : values.shiftUInt8(),
},
currentFault : values.shiftUInt8(),
batteryVoltage : voltage( values.shiftUInt16() ),
stateFlags : cs1108StateFlags( values.shiftUInt8() ),
};
// Skip unused bytes
values.shiftBytes(6);
}
else if( dataType === 2 ) {
// GPS report
this.packet = {
datatype: dataType,
serial : valueToCs1108Serial( values.shiftBytes( 4 ) ),
latitude: (values.shiftInt32( true ))/10000000.0,
longitude: (values.shiftInt32( true ))/10000000.0,
sats: values.shiftUInt8(),
fixValid: values.shiftUInt8(),
ehpe: values.shiftUInt32( true )/100.0,
cnoMin: values.shiftUInt8(),
cnoMax: values.shiftUInt8(),
cnoAvg: values.shiftUInt8(),
boundaryViolated: values.shiftUInt8(),
boundaryAction: values.shiftUInt8(),
};
//console.log( values.length );
// Skip unused bytes
values.shiftBytes(16);
}
else {
this.msgtype = 0;
// Skip unused bytes
values.shiftBytes(39);
}
//console.log( values );
// this.packet = values.shiftBytes(40);
/*
// sensortdata packet is always 40 bytes
this.packet = {
serial: valueToCs1108Serial( values.shiftBytes(4) ),
latitude: values.shiftFloat( true ),
longitude: values.shiftFloat( true ),
numSat: values.shiftUInt8(),
meters: {
hours: cs1108Hours( values.shiftUInt16( true ), values.shiftUInt16( true )),
noFloat: values.shiftUInt8(),
lowBatMin: values.shiftUInt8(),
lowBatHrs: values.shiftUInt8(),
overtemp: values.shiftUInt8(),
throtFail: values.shiftUInt8(),
},
gpsFlags: values.shiftUInt8(),
boundaryCount: values.shiftUInt8(),
boundaryStatus: values.shiftUInt8(),
gpsLossCount: values.shiftUInt8(),
currentFault: values.shiftUInt8(),
batteryVoltage: voltage( values.shiftUInt16()),
stateFlags: cs1108StateFlags(values.shiftUInt8()),
};
// convert GPS flags to bools
var flags = this.uint8ToBoolArray( this.packet.gpsFlags, 1);
this.packet.gpsFlags = {
noSignal: flags[0],
error: flags[1],
boundary: flags[2],
warning: flags[3]
};
*/
// unused bytes in the packet
// last 6 bytes are metadata about the packet
this.from = zeroPad( values.shiftUInt16(true).toString(16), 4);
this.msgtype = values.shiftUInt8();
this.length = values.shiftUInt8();
this.rssi = values.shiftUInt8();
this.lqi = values.shiftUInt8();
// truncate the packet buffer to its actual length
//if( this.length >= 0 & this.length <=40 ) {
// this.packet.length = this.length;
//}
}
},
format: function() {
if( this.msgtype === 0) {
return { msgtype: 0};
}
else {
return {
//buf: this.value,
from: this.from,
msgtype: this.msgtype,
length: this.length,
rssi: this.rssi,
lqi: this.lqi,
packet: this.packet
};
}
},
});
//------------------------------------//---------------------------------------
/**
* Make the register map available when this module is required
* @type {Object}
*/
module.exports = {
// Bank 0
modbusSlaveId: modbusSlaveId,
channelMap: channelMap,
msBetweenStatusTx: msBetweenStatusTx,
powerOffSec: powerOffSec,
networkFormation: networkFormation,
pairingTimeout: pairingTimeout,
switchDefaults: switchDefaults,
maxHops: maxHops,
slowSpeed: slowSpeed,
fastSpeed: fastSpeed,
config: config,
// Bank 1
localSwitches: localSwitches,
remoteSwitches: remoteSwitches,
remoteStatus: remoteStatus,
//remoteQuality: remoteQuality,
systemState: systemState,
volts: volts,
bank1: bank1,
// Bank 2
channel: channel,
fault: fault,
bank2: bank2,
// bank 3
lo0: lo0,
lo1: lo1,
localOutputs: localOutputs,
// bank 4
ro0: ro0,
ro1: ro1,
ro2: ro2,
remoteOutputs: remoteOutputs,
// objects:
networkStatus: networkStatus,
scanResult: scanResult,
connectionTable: connectionTable,
coordStatus: coordStatus,
sensorData: sensorData,
// access to the register object
Register: Register
}