node-aurora
Version:
Provides an interface to the Aurora Dreamband.
423 lines (292 loc) • 14.8 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _keys = require('babel-runtime/core-js/object/keys');
var _keys2 = _interopRequireDefault(_keys);
var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');
var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _createClass2 = require('babel-runtime/helpers/createClass');
var _createClass3 = _interopRequireDefault(_createClass2);
var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');
var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2);
var _inherits2 = require('babel-runtime/helpers/inherits');
var _inherits3 = _interopRequireDefault(_inherits2);
var _events = require('events');
var _events2 = _interopRequireDefault(_events);
var _AuroraConstants = require('./AuroraConstants');
var _AuroraCmdResponseParser = require('./AuroraCmdResponseParser');
var _AuroraCmdResponseParser2 = _interopRequireDefault(_AuroraCmdResponseParser);
var _moment = require('moment');
var _moment2 = _interopRequireDefault(_moment);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var CmdStates = {
NO_CMD: 0,
CMD_HEADER: 1,
CMD_RESPONSE: 2,
CMD_OUTPUT: 3,
CMD_INPUT: 4,
CMD_ERROR: 5
};
var AuroraSerialParser = function (_EventEmitter) {
(0, _inherits3.default)(AuroraSerialParser, _EventEmitter);
function AuroraSerialParser() {
(0, _classCallCheck3.default)(this, AuroraSerialParser);
var _this = (0, _possibleConstructorReturn3.default)(this, (AuroraSerialParser.__proto__ || (0, _getPrototypeOf2.default)(AuroraSerialParser)).call(this));
_this._triggerCmdError = function (message) {
_this._cmd.error = true;
_this._cmd.response = {
error: -64,
message: message
};
_this.emit('cmdResponse', _this._cmd);
_this.reset();
};
_this._onCmdTimeout = function () {
_this._triggerCmdError('Command timed out.');
};
_this._parseNonCmdResponseLine = function (line) {
if (line.charAt(0) == '<') {
var logParts = line.match(_this._regexLog);
if (logParts && logParts.length == 4) {
_this.emit('log', {
typeId: _AuroraConstants.LogNamesToTypeIds[logParts[1].toUpperCase()],
type: logParts[1].toUpperCase(),
time: +(0, _moment2.default)(logParts[2], "HH:mm:ss.SSS", true),
message: logParts[3]
});
return;
}
} else if (line.slice(0, 6) == 'event-') {
var eventParts = line.match(/^event-(\d{1,2}): (\d+) \[(\S*)\]$/i);
if (eventParts && eventParts.length == 4) {
var eventId = +eventParts[1];
if (!isNaN(eventId) && eventId <= _AuroraConstants.EVENT_ID_MAX) {
_this.emit('auroraEvent', {
eventId: eventId,
event: _AuroraConstants.EventIdsToNames[eventId],
flags: +eventParts[2],
time: Date.now()
});
return;
}
}
} else {
var dataParts = line.match(/^(\S*)-(\d{1,2}): ([\d\s,.-]+)$/i);
if (dataParts && dataParts.length == 4) {
var streamId = +dataParts[2];
if (!isNaN(streamId) && streamId <= _AuroraConstants.STREAM_ID_MAX) {
_this.emit('streamData', {
streamId: streamId,
stream: _AuroraConstants.StreamIdsToNames[streamId],
data: dataParts[3].split(',').map(Number),
time: Date.now()
});
return;
}
}
}
_this.emit('parseError', line);
};
_this._cmdResponseParser = new _AuroraCmdResponseParser2.default();
_this._cmdWatchdogTimer = null;
_this.reset();
_this._regexLog = new RegExp('^\\< (' + (0, _keys2.default)(_AuroraConstants.LogNamesToTypeIds).join('|') + ') \\| (\\d{2}:\\d{2}:\\d{2}\\.\\d{3}) \\> (.+)$');
return _this;
}
(0, _createClass3.default)(AuroraSerialParser, [{
key: 'reset',
value: function reset() {
clearTimeout(this._cmdWatchdogTimer);
this._cmd = null;
this._unparsedBuffer = null;
this._cmdResponseParser.reset();
this._cmdState = CmdStates.NO_CMD;
}
}, {
key: 'parseChunk',
value: function parseChunk(chunk) {
//received new data so clear any timeouts
clearTimeout(this._cmdWatchdogTimer);
//don't do anything if chunk is empty
if (!chunk.length) {
return;
}
//set new timeout, which will get cleared if this
//line indicates that there is no more data expected
this._cmdWatchdogTimer = setTimeout(this._onCmdTimeout, 10000);
//pick up where we left off
this._unparsedBuffer = Buffer.isBuffer(this._unparsedBuffer) ? Buffer.concat([this._unparsedBuffer, chunk]) : chunk;
//if we aren't in the middle of processing output
//lets break this chunk into lines
if (this._cmdState != CmdStates.CMD_OUTPUT) {
while (this._unparsedBuffer && this._unparsedBuffer.length) {
//look for first newline
var newlineIndex = this._unparsedBuffer.indexOf('\n');
//no newline, so wait for the next chunk
if (newlineIndex == -1) {
return;
}
//we must have a newline now so grab the line
var bufferLine = this._unparsedBuffer.slice(0, newlineIndex).toString().trim();
//and remove it from the unparsed buffer
this._unparsedBuffer = this._unparsedBuffer.slice(newlineIndex + 1);
//after trim, if no data is in line, bail
if (!bufferLine.length) {
continue;
}
//we have a complete line, so pass it off
this.parseLine(bufferLine.toString());
//if we are now receiving output, we need to exit
//this loop
if (this._cmdState == CmdStates.CMD_OUTPUT) break;
}
}
//if we are here, we are currently receiving output.
//this is where things get tricky since we need to
//properly identify whether an output footer exists
var outputFooter = '\r\n++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++';
//while we have enough bytes to potentially identify the footer
while (this._unparsedBuffer.length >= outputFooter.length) {
//look for the footer
var footerStartIndex = this._unparsedBuffer.indexOf(outputFooter);
//didn't find full footer, so we consume as much as we can
if (footerStartIndex == -1) {
//determine where the first CR is
var firstCRIndex = this._unparsedBuffer.indexOf('\r');
//if we don't have one, we can safely consume the entire buffer
if (firstCRIndex == -1) {
this.emit('cmdOutputReady', this._unparsedBuffer);
this._unparsedBuffer = null;
return;
}
//we have a CR, so see if we have enough bytes to guarantee the output footer
//hasn't started yet
if (this._unparsedBuffer.length >= firstCRIndex + outputFooter.length) {
//consume everything up to and including the first CR
this.emit('cmdOutputReady', this._unparsedBuffer.slice(0, firstCRIndex + 1));
this._unparsedBuffer = this._unparsedBuffer.slice(firstCRIndex + 1);
continue;
}
return;
}
//we found the footer!!
else {
//if the footer doesn't start at the
//beginning, we consume everything up to it
if (footerStartIndex) {
//consume everything up to the footer
this.emit('cmdOutputReady', this._unparsedBuffer.slice(0, footerStartIndex));
this._unparsedBuffer = this._unparsedBuffer.slice(footerStartIndex);
}
//at this point we have the footer and it is at the beginning
//of the unparsed buffer so now we have to look for the footer
//end, which is the second set of \r\n
var footerEndIndex = this._unparsedBuffer.indexOf('\r\n', 2);
if (footerEndIndex == -1) return;
//we found the entire footer, so consume it all (including trailing new lines) and change the state
this._unparsedBuffer = this._unparsedBuffer.slice(footerEndIndex + 2);
this._cmdState = CmdStates.CMD_RESPONSE;
break;
}
}
}
}, {
key: 'parseLine',
value: function parseLine(line) {
switch (this._cmdState) {
case CmdStates.NO_CMD:
//trim off whitespace
line = line.trim();
//don't do anything if line is empty
if (!line.length) {
return;
}
//look for command prompt
var match = line.match(/^# ([a-z-]+.*)$/i);
if (match) {
this._cmd = {
command: match[1],
error: false
};
this._cmdState = CmdStates.CMD_HEADER;
} else {
clearTimeout(this._cmdWatchdogTimer);
this._parseNonCmdResponseLine(line);
}
break;
case CmdStates.CMD_HEADER:
//look for response start
if (line.match(/^[-]{64,}$/)) {
this._cmdResponseParser.reset();
this._cmdState = CmdStates.CMD_RESPONSE;
} else {
this._triggerCmdError('Expected command header.');
}
break;
case CmdStates.CMD_INPUT:
//look for input footer
if (line[0] == '.' && line.match(/^[.]{64,}$/)) {
this._cmdState = CmdStates.CMD_RESPONSE;
}
break;
case CmdStates.CMD_ERROR:
//look for error footer
if (line[0] == '~' && line.match(/^[~]{64,}$/)) {
this._cmdState = CmdStates.CMD_RESPONSE;
} else {
try {
this._cmdResponseParser.parseDetect(line);
} catch (error) {
this._triggerCmdError('Invalid command error response: ' + error);
}
}
break;
case CmdStates.CMD_RESPONSE:
//trim off whitespace
line = line.trim();
//don't do anything if line is empty
if (!line.length) {
return;
}
//look for output header
if (line[0] == '+' && line.match(/^[+]{64,}$/)) {
this._cmdState = CmdStates.CMD_OUTPUT;
}
//look for input header
else if (line[0] == '.' && line.match(/^[.]{64,}$/)) {
this._cmdState = CmdStates.CMD_INPUT;
clearTimeout(this._cmdWatchdogTimer);
this._cmdWatchdogTimer = setTimeout(this._onCmdTimeout, 5 * 60 * 1000);
this.emit('cmdInputRequested');
}
//look for error header
else if (line[0] == '~' && line.match(/^[~]{64,}$/)) {
this._cmd.error = true;
this._cmdResponseParser.reset();
this._cmdState = CmdStates.CMD_ERROR;
}
//look for end of response
else if (line.match(/^[-]{64,}$/)) {
clearTimeout(this._cmdWatchdogTimer);
this._cmd.response = this._cmdResponseParser.getResponse();
this.emit('cmdResponse', this._cmd);
this._cmdState = CmdStates.NO_CMD;
}
//neither, so this is normal command response
else if (!this._cmd.error) {
try {
this._cmdResponseParser.parseDetect(line);
} catch (error) {
this._triggerCmdError('Invalid command response: ' + error);
}
}
break;
}
}
}]);
return AuroraSerialParser;
}(_events2.default);
exports.default = AuroraSerialParser;