/**
* Instantiate by `var xmodem = require('xmodem.js');`
* @class
* @classdesc XMODEM Protocol in JavaScript
* @name Xmodem
* @license BSD-2-Clause
*/
var Xmodem = function () {};
var fs = require('fs');
var crc = require('crc');
var events = require('events');
/* Either use the tracer module to output infromation
* or redefine the functions for silence!
*/
//const log = require('tracer').colorConsole();
const log = { info: function() {}, warn: function() {}, error: function() {}, debug: function() {} };
const SOH = 0x01;
const EOT = 0x04;
const ACK = 0x06;
const NAK = 0x15;
const CAN = 0x18; // not implemented
const FILLER = 0x1A;
const CRC_MODE = 0x43; // 'C'
var receive_interval_timer = false;
Xmodem.prototype = new events.EventEmitter();
/**
* xmodem.js package version.
* @constant
* @type {string}
*/
Xmodem.prototype.VERSION = require('../package.json').version;
/**
* how many timeouts in a row before the sender gives up?
* @constant
* @type {integer}
* @default
*/
Xmodem.prototype.XMODEM_MAX_TIMEOUTS = 5;
/**
* how many errors on a single block before the receiver gives up?
* @constant
* @type {integer}
* @default
*/
Xmodem.prototype.XMODEM_MAX_ERRORS = 10;
/**
* how many times should receiver attempt to use CRC?
* @constant
* @type {integer}
* @default
*/
Xmodem.prototype.XMODEM_CRC_ATTEMPTS = 3;
/**
* Try to use XMODEM-CRC extension or not? Valid options: 'crc' or 'normal'
* @constant
* @type {string}
* @default
*/
Xmodem.prototype.XMODEM_OP_MODE = 'crc';
/**
* First block number. Don't change this unless you have need for non-standard
* implementation.
* @constant
* @type {integer}
* @default
*/
Xmodem.prototype.XMODEM_START_BLOCK = 1;
/**
* default timeout period in seconds
* @constant
* @type {integer}
* @default
*/
Xmodem.prototype.timeout_seconds = 10;
/**
* how many bytes (excluding header & checksum) in each block? Don't change this
* unless you have need for non-standard implementation.
* @constant
* @type {integer}
* @default
*/
Xmodem.prototype.block_size = 128;
/**
* Send a file using XMODEM protocol
* @method
* @name Xmodem#send
* @param {socket} socket - net.Socket() or Serialport socket for transport
* @param {buffer} dataBuffer - Buffer() to be sent
*/
Xmodem.prototype.send = function(socket, dataBuffer) {
var blockNumber = this.XMODEM_START_BLOCK;
var packagedBuffer = new Array();
var current_block = new Buffer(this.block_size);
var sent_eof = false;
var _self = this;
this.log(dataBuffer.length);
private_stuff();
// FILLER
for(i=0; i < this.XMODEM_START_BLOCK; i++) {
packagedBuffer.push("");
}
while (dataBuffer.length > 0) {
for(i=0; i < this.block_size; i++) {
current_block[i] = dataBuffer[i] === undefined ? FILLER : dataBuffer[i];
}
dataBuffer = dataBuffer.slice(this.block_size);
packagedBuffer.push(current_block);
current_block = new Buffer(this.block_size);
}
/**
* Ready to send event, buffer has been broken into individual blocks to be sent.
* @event Xmodem#ready
* @property {integer} - Indicates how many blocks are ready for transmission
*/
_self.emit('ready', packagedBuffer.length - 1); // We don't count the filler
const sendData = function(data) {
/*
* Here we handle the beginning of the transmission
* The receiver initiates the transfer by either calling
* checksum mode or CRC mode.
*/
if(data[0] === CRC_MODE && blockNumber === _self.XMODEM_START_BLOCK) {
log.info("[SEND] - received C byte for CRC transfer!");
_self.XMODEM_OP_MODE = 'crc';
if(packagedBuffer.length > blockNumber) {
/**
* Transmission Start event. A successful start of transmission.
* @event Xmodem#start
* @property {string} - Indicates transmission mode 'crc' or 'normal'
*/
_self.emit('start', _self.XMODEM_OP_MODE);
sendBlock(socket, blockNumber, packagedBuffer[blockNumber], _self.XMODEM_OP_MODE);
_self.emit('status', { action: 'send', signal: 'SOH', block: blockNumber });
blockNumber++;
}
}
else if(data[0] === NAK && blockNumber === _self.XMODEM_START_BLOCK) {
log.info("[SEND] - received NAK byte for standard checksum transfer!");
_self.XMODEM_OP_MODE = 'normal';
if(packagedBuffer.length > blockNumber) {
_self.emit('start', _self.XMODEM_OP_MODE);
sendBlock(socket, blockNumber, packagedBuffer[blockNumber], _self.XMODEM_OP_MODE);
_self.emit('status', { action: 'send', signal: 'SOH', block: blockNumber });
blockNumber++;
}
}
/*
* Here we handle the actual transmission of data and
* retransmission in case the block was not accepted.
*/
else if(data[0] === ACK && blockNumber > _self.XMODEM_START_BLOCK) {
// Woohooo we are ready to send the next block! :)
log.info('ACK RECEIVED');
_self.emit('status', { action: 'recv', signal: 'ACK' });
if(packagedBuffer.length > blockNumber) {
sendBlock(socket, blockNumber, packagedBuffer[blockNumber], _self.XMODEM_OP_MODE);
_self.emit('status', { action: 'send', signal: 'SOH', block: blockNumber });
blockNumber++;
}
else if(packagedBuffer.length === blockNumber) {
// We are EOT
if(sent_eof === false) {
sent_eof = true;
log.info("WE HAVE RUN OUT OF STUFF TO SEND, EOT EOT!");
_self.emit('status', { action: 'send', signal: 'EOT' });
socket.write(new Buffer([EOT]));
}
else {
// We are finished!
log.info('[SEND] - Finished!');
_self.emit('stop', 0);
socket.removeListener('data', sendData);
}
}
}
else if(data[0] === NAK && blockNumber > _self.XMODEM_START_BLOCK) {
if (blockNumber === packagedBuffer.length && sent_eof) {
log.info('[SEND] - Resending EOT, because receiver responded with NAK.');
_self.emit('status', { action: 'send', signal: 'EOT' });
socket.write(new Buffer([EOT]));
} else {
log.info('[SEND] - Packet corruption detected, resending previous block.');
_self.emit('status', { action: 'recv', signal: 'NAK' });
blockNumber--;
if(packagedBuffer.length > blockNumber) {
sendBlock(socket, blockNumber, packagedBuffer[blockNumber], _self.XMODEM_OP_MODE);
_self.emit('status', { action: 'send', signal: 'SOH', block: blockNumber });
blockNumber++;
}
}
}
else {
log.warn("GOT SOME UNEXPECTED DATA which was not handled properly!");
log.warn("===>");
log.warn(data);
log.warn("<===");
log.warn("blockNumber: " + blockNumber);
}
};
socket.on('data', sendData);
};
/**
* Receive a file using XMODEM protocol
* @method
* @name Xmodem#receive
* @param {socket} socket - net.Socket() or Serialport socket for transport
* @param {string} filename - pathname where to save the transferred file
*/
Xmodem.prototype.receive = function(socket, filename) {
var blockNumber = this.XMODEM_START_BLOCK;
var packagedBuffer = new Array();
var nak_tick = this.XMODEM_MAX_ERRORS * this.timeout_seconds * 3;
var crc_tick = this.XMODEM_CRC_ATTEMPTS;
var transfer_initiated = false;
var tryCounter = 0;
var _self = this;
// FILLER
for(i=0; i < this.XMODEM_START_BLOCK; i++) {
packagedBuffer.push("");
}
// Let's try to initate transfer with XMODEM-CRC
if(this.XMODEM_OP_MODE === 'crc') {
log.info("CRC init sent");
socket.write(new Buffer([CRC_MODE]));
receive_interval_timer = setIntervalX(function () {
if (transfer_initiated === false) {
log.info("CRC init sent");
socket.write(new Buffer([CRC_MODE]));
}
else {
clearInterval(receive_interval_timer);
receive_interval_timer = false;
}
// Fallback to standard XMODEM
if (receive_interval_timer === false && transfer_initiated === false) {
receive_interval_timer = setIntervalX(function () {
log.info("NAK init sent");
socket.write(new Buffer([NAK]));
_self.XMODEM_OP_MODE = 'normal';
}, 3000, nak_tick);
}
}, 3000, (crc_tick - 1));
}
else {
receive_interval_timer = setIntervalX(function () {
log.info("NAK init sent");
socket.write(new Buffer([NAK]));
_self.XMODEM_OP_MODE = 'normal';
}, 3000, nak_tick);
}
const receiveData = function(data) {
tryCounter++;
log.info('[RECV] - Received: ' + data.toString('utf-8'));
log.info(data);
if(data[0] === NAK && blockNumber === this.XMODEM_START_BLOCK) {
log.info("[RECV] - received NAK byte!");
}
else if(data[0] === SOH && tryCounter <= _self.XMODEM_MAX_ERRORS) {
if(transfer_initiated === false) {
// Initial byte received
transfer_initiated = true;
clearInterval(receive_interval_timer);
receive_interval_timer = false;
}
receiveBlock(socket, blockNumber, data, _self.block_size, _self.XMODEM_OP_MODE, function(current_block) {
log.info(current_block);
packagedBuffer.push(current_block);
tryCounter = 0;
blockNumber++;
});
}
else if(data[0] === EOT) {
log.info("Received EOT");
socket.write(new Buffer([ACK]));
blockNumber--;
for(i = packagedBuffer[blockNumber].length - 1; i >= 0; i--) {
if(packagedBuffer[blockNumber][i] === FILLER) {
continue;
}
else {
packagedBuffer[blockNumber] = packagedBuffer[blockNumber].slice(0, i + 1);
break;
}
}
// At this stage the packaged buffer should be ready for writing
writeFile(packagedBuffer, filename, function() {
if(socket.constructor.name === "Socket") {
socket.destroy();
}
else if(socket.constructor.name === "SerialPort") {
socket.close();
}
// remove the data listener
socket.removeListener('data', receiveData);
});
}
else {
log.warn("GOT SOME UNEXPECTED DATA which was not handled properly!");
log.warn("===>");
log.warn(data);
log.warn("<===");
log.warn("blockNumber: " + blockNumber);
}
};
socket.on('data', receiveData);
};
Xmodem.prototype.log = function(data) {
log.info('modem! : ' + data);
};
module.exports = new Xmodem();
// Utility functions that are not exported
var private_stuff = function() {
log.info("This is private");
};
/**
* Internal helper function for scoped intervals
* @private
*/
var setIntervalX = function(callback, delay, repetitions) {
var x = 0;
var intervalID = setInterval(function () {
if (++x === repetitions) {
clearInterval(intervalID);
receive_interval_timer = false;
}
callback();
}, delay);
return intervalID;
};
var sendBlock = function(socket, blockNr, blockData, mode) {
var crcCalc = 0;
var sendBuffer = Buffer.concat([new Buffer([SOH]),
new Buffer([blockNr]),
new Buffer([(0xFF - blockNr)]),
blockData
]);
log.info('SENDBLOCK! Data length: ' + blockData.length);
log.info(sendBuffer);
if(mode === 'crc') {
var crcString = crc.crc16xmodem(blockData).toString(16);
// Need to avoid odd string for Buffer creation
if(crcString.length % 2 == 1) {
crcString = '0'.concat(crcString);
}
// CRC must be 2 bytes of length
if(crcString.length === 2) {
crcString = '00'.concat(crcString);
}
sendBuffer = Buffer.concat([sendBuffer, new Buffer(crcString, "hex")]);
}
else {
// Count only the blockData into the checksum
for(i = 3; i < sendBuffer.length; i++) {
crcCalc = crcCalc + sendBuffer.readUInt8(i);
}
crcCalc = crcCalc % 256;
crcCalc = crcCalc.toString(16);
if((crcCalc.length % 2) != 0) {
// Add padding for the string to be even
crcCalc = "0" + crcCalc;
}
sendBuffer = Buffer.concat([sendBuffer, new Buffer(crcCalc, "hex")]);
}
log.info('Sending buffer with total length: ' + sendBuffer.length);
socket.write(sendBuffer);
};
var receiveBlock = function(socket, blockNr, blockData, block_size, mode, callback) {
var cmd = blockData[0];
var block = parseInt(blockData[1]);
var block_check = parseInt(blockData[2]);
var current_block;
var checksum_length = mode === 'crc' ? 2 : 1;
if(cmd === SOH) {
if((block + block_check) === 0xFF) {
// Are we expecting this block?
if(block === (blockNr % 0x100)) {
current_block = blockData.slice(3, blockData.length-checksum_length);
}
else {
log.error('ERROR: Synch issue! Received: ' + block + ' Expected: ' + blockNr);
return;
}
}
else {
log.error('ERROR: Block integrity check failed!');
socket.write(new Buffer([NAK]));
return;
}
if(current_block.length === block_size) {
socket.write(new Buffer([ACK]));
callback(current_block);
}
else {
log.error('ERROR: Received block size did not match the expected size. Received: ' + current_block.length + ' | Expected: ' + block_size);
socket.write(new Buffer([NAK]));
return;
}
}
else {
log.error('ERROR!');
return;
}
};
var writeFile = function(buffer, filename, callback) {
log.info('writeFile called');
var fileStream = fs.createWriteStream(filename);
fileStream.once('open', function(fd) {
log.info('File stream opened, buffer length: ' + buffer.length);
for(i = 0; i < buffer.length; i++) {
fileStream.write(buffer[i]);
}
fileStream.end();
log.info('File written');
callback();
});
};