const str2ab = require("string-to-arraybuffer");
const ab2str = require("arraybuffer-to-string");
import {EventEmitter} from "../node_modules/djipevents/dist/djipevents.esm.min.js";
import isArrayBuffer from "../node_modules/is-array-buffer/dist/is-array-buffer.esm.js";
/**
* The `Nward` class provides methods to communicate via serial to an Arduino-compatible device. It
* extends the [EventEmitter](https://djipco.github.io/djipevents/EventEmitter.html) class from
* [djipevents](https://github.com/djipco/djipevents).
*/
export class Nward extends EventEmitter{
constructor() {
super();
/**
* @private
*/
this.buffer = "";
/**
* @private
*/
this.listeners = {};
/**
* The id of the current serial connection
* @type {?number}
* @readonly
*/
this.connectionId = null;
/**
* The serial port currently in use (e.g. `COM3`, `/dev/cu.usbmodem2101`, etc.). When no
* connection is active, this is set to `null`.
*
* @type {?string}
* @readonly
*/
this.port = null;
}
/**
* The `ConnectionInfo` object provides details about a serial connexion. You can view all details
* about the
* [ConnectionInfo]{@linkcode https://developer.chrome.com/apps/serial#type-ConnectionInfo} object
* of the Chrome App Serial documentation.
*
* @typedef {Object} Nward~ConnectionInfo
* @property {integer} connectionId
* @property {boolean} paused
* @property {boolean} persistent
* @property {string} name
* @property {integer} bufferSize
* @property {integer} receiveTimeout
* @property {integer} sendTimeout
* @property {integer} [bitrate]
* @property {DataBits} [dataBits]
* @property {ParityBits} [parityBit]
* @property {StopBits} [StopBits]
* @property {boolean} [ctsFlowControl]
*/
/**
* Tries to open a serial connection to the Arduino-compatible device hooked up to the port
* specified in the `options` parameter. If no port is specified, it tries to connect to the last
* port in the list obtained by calling {@link Nward.getDevices}.
*
* This is an asynchronous operation. When it succeeds, it returns a promise fulfilled with a
* {@link Nward~ConnectionInfo} object providing information about the established connection.
*
* @param {Object} [options={}]
* @param {string} [options.port] The port to connect to (`"COM3"`, `"/dev/cu.usbmodem2101"`,
* etc.)
* @param {string} [options.bitrate=57600] The requested bitrate of the connection to be opened.
* For compatibility with the widest range of hardware, this number should match one of
* commonly-available bitrates such as:
*
* * 110
* * 300
* * 1200
* * 2400
* * 4800
* * 9600
* * 14400
* * 19200
* * 38400
* * 57600 (default)
* * 115200
*
* @fires Nward#connected
*
* @returns {Promise<ConnectionInfo>} The promise is fulfilled with a
* {@link Nward~ConnectionInfo} object providing details about the connection.
*/
async connect(options = {}) {
if (options.port) {
this.port = options.port;
delete options.port;
} else {
let devices = await Nward.getDevices();
this.port = devices[devices.length - 1].path;
}
let defaults = {
bitrate: 115200,
};
options = Object.assign(defaults, options);
return new Promise(resolve => {
chrome.serial.connect(this.port, options, info => {
this.connectionId = info.connectionId;
this.listeners.onDataReceived = this.onDataReceived.bind(this);
chrome.serial.onReceive.addListener(this.listeners.onDataReceived);
this.listeners.onError = this.onError.bind(this);
chrome.serial.onReceiveError.addListener(this.listeners.onError);
info.port = this.port;
/**
* Event fired when the serial connection has been successfully established
*
* @event Nward#connected
* @type {ConnectionInfo}
* @property {integer} connectionId
* @property {boolean} paused
* @property {boolean} persistent
* @property {string} name
* @property {integer} bufferSize
* @property {integer} receiveTimeout
* @property {integer} sendTimeout
* @property {integer} [bitrate]
* @property {DataBits} [dataBits]
* @property {ParityBits} [parityBit]
* @property {StopBits} [StopBits]
* @property {boolean} [ctsFlowControl]
*/
this.emit("connected", info);
resolve(info);
});
});
}
/**
* This object provides information about a serial device.
*
* @typedef {Object} Nward~DeviceInfo
* @property {string} path The device's system path. This should be passed as the `path` argument
* to `connect()` in order to connect to this device.
* @property {string} [vendorId] A PCI or USB vendor ID if one can be determined for the
* underlying device.
* @property {string} [productId] A USB product ID if one can be determined for the underlying
* device.
* @property {string} [displayName] A human-readable display name for the underlying device if one
* can be queried from the host driver.
*/
/**
* Returns a list of serial devices likely to be Arduino-compatible. This list will always
* include all Arduino-compatible devices but, depending on the platform, may also include other
* serial devices as well. If the `all` parameter is set to `true`, it will systematically return
* all serial devices.
*
* @param {boolean} [all=false] Whether to return all serial devices or only Arduino-compatible
* devices.
* @returns {Promise<array>} An array of {@link Nward~DeviceInfo} objects describing the devices
* found.
*/
static async getDevices(all = false) {
return new Promise(resolve => {
chrome.serial.getDevices(devices => {
let found = devices;
if (!all) found = devices.filter(device => /usb|acm|^com/i.test(device.path));
resolve(found);
});
});
}
/**
* Returns a promise fulfilled with an array of currently opened serial port connections owned by
* the application.
*
* @returns {Promise<array>} The promise is fulfilled with an array of
* {@link Nward~ConnectionInfo} objects.
*/
static async getSerialConnections() {
return new Promise(resolve => chrome.serial.getConnections(resolve));
}
/**
* Retrieves the state of the currently-opened connection.
*
* @returns {Promise<ConnectionInfo>} The promise is fulfilled with a
* {@link Nward~ConnectionInfo} object providing details about the connection.
*/
async getConnectionInfo() {
if (!this.connectionId) return null;
return new Promise(resolve => {
chrome.serial.getInfo(this.connectionId, info => resolve(info));
});
}
/**
* This object provides information about the send operation.
*
* @typedef {Object} Nward~SendInfo
* @property {integer} bytesSent The number of bytes sent.
* @property {string} [error] An error code if an error occurred (disconnected, pending, timeout
* or system_error).
*/
/**
* Sends the specified data to the Arduino. The data can be a `string` or an `ArrayBuffer`. If
* it's a string, a newline will be appended at the end (unless `appendNewline` is set to
* `false`).
*
* @param {string|ArrayBuffer} data The data to send to the Arduino
* @param {boolean} [appendNewline=false] Whether to automatically append a newline character when
* a string is sent.
* @returns {Promise<SendInfo>}
*/
async send(data, appendNewline = false) {
let ab;
if (isArrayBuffer(data)) {
ab = data;
} else {
if (appendNewline) data += "\n";
ab = str2ab(data);
}
return new Promise(resolve => {
chrome.serial.send(this.connectionId, ab, info => {
/**
* Event fired when the data has been successfully sent
*
* @event Nward#sent
* @type {SendInfo}
* @property {integer} bytesSent The number of bytes sent.
* @property {string} [error] An error code if an error occurred (disconnected, pending,
* timeout or system_error).
*/
this.emit("sent", info);
resolve(info);
});
});
}
/**
* Sends a message to the connected Arduino-compatible device.
*
* @param {string} command
* @param {int|Array.<int>} [value]
* @returns {Promise<SendInfo>}
*/
async sendMessage(command, value) {
if (!command) return Promise.reject("You must provide a command.");
let instruction = command;
if (value) instruction += "=" + value;
instruction += "\n";
return this.send(instruction);
}
/**
* @private
*/
onDataReceived(info) {
let lines = ab2str(info.data).split("\n");
lines[0] = this.buffer + lines[0];
this.buffer = lines.pop();
lines.forEach(line => {
const [type, value] = line.split("=");
/**
* Event fired data has been received
*
* @event Nward#received
* @type {object}
* @property {string} type The type of message received
* @property {string} [data] The data
*/
this.emit("received", {type: type, data: value});
});
}
/**
* @private
*/
onError(info) {
this.emit("error", {connectionId: info.connectionId, code: info.error});
this.disconnect();
}
/**
* Tries to disconnect the currently-active serial connection.
* @returns {Promise<boolean>} A promise fulfilled with a boolean value representing the result of
* the operation.
*/
async disconnect() {
return new Promise(resolve => {
chrome.serial.onReceive.removeListener(this.listeners.onDataReceived);
this.listeners.onDataReceived = undefined;
chrome.serial.onReceiveError.removeListener(this.listeners.onError);
this.listeners.onError = undefined;
this.port = null;
chrome.serial.disconnect(this.connectionId, result => {
/**
* Event fired when the serial connection has been disconnected
*
* @event Nward#disconnected
* @type {object}
* @property {boolean} result Indicates whether or not the operation was successful
*/
this.emit("disconnected", {result: result});
this.connectionId = null;
resolve();
});
});
}
/**
* Whether there is an active serial connection
* @type {boolean}
* @readonly
*/
get connected() {
return !!this.connectionId;
}
}