transports/serial.js

'use strict';

const SerialPort = require('serialport');
const Transport = require('../classes/transport');
const SafeBuffer = require('safe-buffer').Buffer;
const _isNil = require('lodash/isNil');

/**
 * @class
 * @extends Transport
 */

class Serial extends Transport {
	/**
	 * @constructor
	 * @description A transport for data that involves reading and writing to a
 	 serial port
	 * @param {Object} opts General settings for the Serial Transport
	 * @param {Object/String} opts.port The serial device to open
	 * @param {String} [opts.port.product] The serial device's product id
	 * @param {String} [opts.port.vendor] The serial device's vendor id
	 * @param {Number} opts.baudRate
	 * @param {Number} opts.dataBits
	 * @param {Number} opts.stopBits
	 * @param {String} opts.parity
	 * @param {Object} [opts.parser] Optional serial parser
	 * @param {Adapter} adapter Adapter for which this is a plugin
	 */
	constructor(opts, adapter) {
		super(opts, adapter);
		this._open = false;
		this._parser = opts.parser;
		this._format = this._adapter.opts.format;
	}

	/**
	 * @description init Configure, open, and setup the serial port
	 * @private
	 * @return {Promise}
	 */

	init() {
		return new Promise((resolve, reject) => {
			this.getSerial().then((port) => {
				let options = {
					baudRate: this._opts.baudRate,
					autoOpen: false
				};

				this._adapter.k4.logging.debug('Serial transport opening:', port);
				this._adapter.k4.logging.debug('Serial transport options:', options);

				// optional flow control opts
				if (typeof this._opts.dataBits == 'number') {
					options.dataBits = this._opts.dataBits;
				}

				if (typeof this._opts.stopBits == 'number') {
					options.stopBits = this._opts.stopBits;
				}

				if (typeof this._opts.parity == 'string') {
					options.parity = this._opts.parity;
				}

				this._port = new SerialPort(port, options);

				// if a parser is specified, assign data event to it
				if (this._parser) {
					this._port.pipe(this._parser);

					this._parser.on('data', (data) => {
						this.received(data);
					});
				} else {
					this._port.on('data', (data) => {
						this.received(data);
					});
				}

				this._port.on('error', (error) => {
					this._adapter.k4.logging.fatal('Serial transport error', error.message);
				});


				// resolve promise when port is open
				this._port.open((error) => {
					if (error) {
						reject(error);
					} else {
						this._open = true;
						this._adapter.k4.logging.debug('Serial transport open');
						resolve();
					}
				});
			}).catch((e) => {
				reject(e);
			});
		});
	}

	/**
	 * @description Get the correct path for the serial port
	 * @private
	 * @return {Promise}
	 */

	getSerial() {
		return new Promise((resolve, reject) => {
			if (typeof this._opts.port == 'object') {
				let product = this._opts.port.product;
				let vendor = this._opts.port.vendor;

				let udev = require('udev');
				let devices = udev.list('tty');

				let found;

				for (let i=0; i < devices.length; i++) {
					let device = devices[i];
					if (device.ID_MODEL_ID == product && device.ID_VENDOR_ID == vendor && device.SUBSYSTEM == 'tty') {
						found = device.DEVNAME;
						break;
					}
				}

				if (found) {
					resolve(found);
				} else {
					reject(new Error('Serial port not found'));
				}
			} else {
				resolve(this._opts.port);
			}
		});
	}

	/**
	 * @description Send a command to the serial port
	 * @param {Object} address Not used by serial
	 * @param {Command} command The command to send
	 */

	send(address, command) {
		if (!this._open) {
			this._adapter.k4.logging.fatal('Serial transport send: port not open');
			return;
		}

		const body = command.body;
		let headerBuf;

		if (!SafeBuffer.isBuffer(command.header) &&
			!_isNil(command) &&
			!_isNil(command.header) &&
			!_isNil(command.header.packet)) {
			headerBuf = command.header.packet;
		}

		if (_isNil(headerBuf)) {
			headerBuf = SafeBuffer.from([]);
		}

		if (_isNil(body)) {
			return Promise.reject(new Error(`There was no body supplied for command: ${command.name}`));
		}

		return this.write(SafeBuffer.concat([headerBuf, body]));
	}

	/**
	 * @description Write data to the serial port
	 * @param {Buffer|Buffer[]} data The data buffer or the Array of data
	 * buffers for a multipart message
	 * @returns {Promise} Resolves after the data in the first Buffer has been
	 * written to the serial port
	 * @private
	 */

	write(data) {
		let dataToWrite = data;
		if (Array.isArray(data)) {
			dataToWrite = data.shift();
			this._multipart = (data.length > 0) ? data : null;
		}

		this._adapter.k4.logging.debug('Serial transport send', dataToWrite.toString(this._format));
		return new Promise((resolve, reject) => {
			this._port.write(dataToWrite, (err, bytesWritten) => {
				if (err) {
					reject(err);
				} else {
					resolve(bytesWritten);
				}
			});
		});
	}

	/**
	 * @description Process incoming data
	 * @param {Buffer} data The data buffer
	 * @private
	 */

	received(data) {
		this._adapter.k4.logging.debug('Serial transport received', data.toString(this._format));

		if (this._multipart && data.toString() == this._opts.multipartDelimiter) {
			this.write(this._multipart)
				.catch(e => this._adapter.k4.logging.error(`Error writing multipart data: ${JSON.stringify(this._multipart)}`, e instanceof Error ? e.stack.split('\n') : e.toString()));
		} else {
			this._adapter.received(this._address, data);
		}
	}

	/**
	 * @description Close the listener
	 */

	close() {
		return new Promise((resolve, reject) => {
			this._port.close((err) => {
				if (err) {
					reject(err);
				} else {
					resolve();
				}
			});
		});
	}
};

module.exports = Serial;