Loading files
/*
* Copyright (C) 2021 Sienci Labs Inc.
*
* This file is part of gSender.
*
* gSender is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, under version 3 of the License.
*
* gSender is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with gSender. If not, see <https://www.gnu.org/licenses/>.
*
* Contact for information regarding this program and its license
* can be sent through gSender@sienci.com or mailed to the main office
* of Sienci Labs Inc. in Waterloo, Ontario, Canada.
*
*/
import { ReadlineParser } from "@serialport/parser-readline";
import { EventEmitter } from "events";
import net from "net";
import { SerialPort } from "serialport";
// Validation
const DATABITS = Object.freeze([5, 6, 7, 8]);
const STOPBITS = Object.freeze([1, 2]);
const PARITY = Object.freeze(["none", "even", "mark", "odd", "space"]);
const FLOWCONTROLS = Object.freeze(["rtscts", "xon", "xoff", "xany"]);
const defaultSettings = Object.freeze({
baudRate: 115200,
dataBits: 8,
stopBits: 1,
parity: "none",
rtscts: false,
xon: false,
xoff: false,
xany: false,
});
const toIdent = (options) => {
// Only the path option is required for generating the ident property
const { path } = { ...options };
return JSON.stringify({ type: "serial", path: path });
};
class SerialConnection extends EventEmitter {
type = "serial";
parser = null;
// Readline parser
port = null;
// callback on state
callback = null;
// SerialPort
writeFilter = (data) => data;
// Tracks whether TCP 'connect' has fired (used to distinguish pre/post-connect errors)
connected = false;
eventListener = {
data: (data) => {
this.emit("data", data);
},
open: () => {
this.emit("open");
},
close: (err) => {
this.emit("close", err);
},
error: (err) => {
if (!this.connected) {
// Pre-connection error (ECONNREFUSED, EHOSTUNREACH, ENETUNREACH, etc.)
this.port && this.port.destroy();
this.port = null;
if (this.callback) {
this.callback(err);
this.callback = null;
}
} else if (err.code === "ECONNRESET") {
// Post-connection reset
this.port && this.port.destroy();
this.port = null;
}
this.emit("error", err);
},
};
constructor(options) {
super();
const { writeFilter, ...rest } = { ...options };
if (writeFilter) {
if (typeof writeFilter !== "function") {
throw new TypeError(`"writeFilter" must be a function: ${writeFilter}`);
}
this.writeFilter = writeFilter;
}
const settings = Object.assign({}, defaultSettings, rest);
settings.ethernetPort = rest.ethernetPort;
if (settings.port) {
throw new TypeError('"port" is an unknown option, did you mean "path"?');
}
if (!settings.path) {
throw new TypeError(`"path" is not defined: ${settings.path}`);
}
if (settings.baudrate) {
throw new TypeError(
'"baudrate" is an unknown option, did you mean "baudRate"?',
);
}
if (typeof settings.baudRate !== "number") {
throw new TypeError(`"baudRate" must be a number: ${settings.baudRate}`);
}
if (DATABITS.indexOf(settings.dataBits) < 0) {
throw new TypeError(`"databits" is invalid: ${settings.dataBits}`);
}
if (STOPBITS.indexOf(settings.stopBits) < 0) {
throw new TypeError(`"stopbits" is invalid: ${settings.stopbits}`);
}
if (PARITY.indexOf(settings.parity) < 0) {
throw new TypeError(`"parity" is invalid: ${settings.parity}`);
}
FLOWCONTROLS.forEach((control) => {
if (typeof settings[control] !== "boolean") {
throw new TypeError(
`"${control}" is not boolean: ${settings[control]}`,
);
}
});
Object.defineProperties(this, {
settings: {
enumerable: true,
value: settings,
writable: false,
},
});
}
get ident() {
return toIdent(this.settings);
}
get isOpen() {
if (this.settings.network) {
return this.port && this.port.writable && this.connected;
}
return this.port && this.port.isOpen;
}
get isClose() {
return !this.isOpen;
}
// @param {function} callback The error-first callback.
open(callback) {
this.callback = callback;
const { path, baudRate, network, ethernetPort, ...rest } = this.settings;
const ip = "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";
const expr = new RegExp(`^${ip}.${ip}.${ip}.${ip}$`, "g");
const looksLikeIP = path.match(expr);
if (this.port && !looksLikeIP) {
const err = new Error(`Cannot open serial port "${this.settings.path}"`);
callback(err);
return;
}
// Single telnet - don't return early, just close it and reopen it
if (this.port && (network || looksLikeIP)) {
this.port.destroy();
this.port = null;
const err = new Error("Serial port connection reset");
callback(err);
return;
}
console.log(`Connection to port ${ethernetPort}`);
if (network || looksLikeIP) {
this.connected = false;
this.port = new net.Socket();
this.port.setTimeout(2000, () => {
this.port.destroy();
this.port = null;
if (this.callback) {
this.callback("Connection timeout");
this.callback = null;
}
});
// addPortListeners() calls removeAllListeners(), so it must run BEFORE
// we add the one-time 'connect' handler, otherwise that handler would
// be removed and callback() would never be called on a successful connect.
this.addPortListeners();
this.port.once("connect", () => {
this.connected = true;
this.port.setTimeout(0);
if (this.callback) {
this.callback();
this.callback = null;
}
});
this.port.connect(ethernetPort, path);
} else {
this.port = new SerialPort({
path,
baudRate,
...rest,
autoOpen: false,
});
this.addPortListeners();
this.port.open(callback);
}
}
addPortListeners() {
this.port.removeAllListeners();
this.port.on("open", this.eventListener.open);
this.port.on("close", this.eventListener.close);
this.port.on("error", this.eventListener.error);
this.parser = this.port.pipe(new ReadlineParser({ delimiter: "\n" }));
this.parser.on("data", this.eventListener.data);
}
// @param {function} callback The error-first callback.
close(callback) {
if (!this.port) {
const err = new Error(`Cannot close serial port "${this.settings.path}"`);
callback && callback(err);
return;
}
this.port.removeListener("open", this.eventListener.open);
this.port.removeListener("close", this.eventListener.close);
this.port.removeListener("error", this.eventListener.error);
this.parser.removeListener("data", this.eventListener.data);
if (this.settings.network) {
this.port.on("close", () => {
callback();
});
this.port.destroy();
} else {
this.port.close(callback);
}
this.port = null;
this.parser = null;
}
write(data, context) {
if (!this.port) {
return;
}
data = this.writeFilter(data, context);
this.port.write(Buffer.from(data));
}
writeImmediate(data) {
// Same guard as write(): the port can be gone (unplugged, closed) while
// a caller's timer is still running, and throwing from inside a timer
// takes down the process.
if (!this.port) {
return;
}
this.port.write(data);
}
setWriteFilter(writeFilter) {
this.writeFilter = writeFilter;
}
}
export { toIdent };
export default SerialConnection;
6bc89d73dThis file is pinned to the selected release. Download the original to open it in your design tool.