index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. "use strict";
  2. var __importDefault = (this && this.__importDefault) || function (mod) {
  3. return (mod && mod.__esModule) ? mod : { "default": mod };
  4. };
  5. Object.defineProperty(exports, "__esModule", { value: true });
  6. exports.SerialPortStream = exports.DisconnectedError = void 0;
  7. const stream_1 = require("stream");
  8. const debug_1 = __importDefault(require("debug"));
  9. const debug = (0, debug_1.default)('serialport/stream');
  10. class DisconnectedError extends Error {
  11. disconnected;
  12. constructor(message) {
  13. super(message);
  14. this.disconnected = true;
  15. }
  16. }
  17. exports.DisconnectedError = DisconnectedError;
  18. const defaultSetFlags = {
  19. brk: false,
  20. cts: false,
  21. dtr: true,
  22. rts: true,
  23. };
  24. function allocNewReadPool(poolSize) {
  25. const pool = Buffer.allocUnsafe(poolSize);
  26. pool.used = 0;
  27. return pool;
  28. }
  29. class SerialPortStream extends stream_1.Duplex {
  30. port;
  31. _pool;
  32. _kMinPoolSpace;
  33. opening;
  34. closing;
  35. settings;
  36. /**
  37. * Create a new serial port object for the `path`. In the case of invalid arguments or invalid options, when constructing a new SerialPort it will throw an error. The port will open automatically by default, which is the equivalent of calling `port.open(openCallback)` in the next tick. You can disable this by setting the option `autoOpen` to `false`.
  38. * @emits open
  39. * @emits data
  40. * @emits close
  41. * @emits error
  42. */
  43. constructor(options, openCallback) {
  44. const settings = {
  45. autoOpen: true,
  46. endOnClose: false,
  47. highWaterMark: 64 * 1024,
  48. ...options,
  49. };
  50. super({
  51. highWaterMark: settings.highWaterMark,
  52. });
  53. if (!settings.binding) {
  54. throw new TypeError('"Bindings" is invalid pass it as `options.binding`');
  55. }
  56. if (!settings.path) {
  57. throw new TypeError(`"path" is not defined: ${settings.path}`);
  58. }
  59. if (typeof settings.baudRate !== 'number') {
  60. throw new TypeError(`"baudRate" must be a number: ${settings.baudRate}`);
  61. }
  62. this.settings = settings;
  63. this.opening = false;
  64. this.closing = false;
  65. this._pool = allocNewReadPool(this.settings.highWaterMark);
  66. this._kMinPoolSpace = 128;
  67. if (this.settings.autoOpen) {
  68. this.open(openCallback);
  69. }
  70. }
  71. get path() {
  72. return this.settings.path;
  73. }
  74. get baudRate() {
  75. return this.settings.baudRate;
  76. }
  77. get isOpen() {
  78. return (this.port?.isOpen ?? false) && !this.closing;
  79. }
  80. _error(error, callback) {
  81. if (callback) {
  82. callback.call(this, error);
  83. }
  84. else {
  85. this.emit('error', error);
  86. }
  87. }
  88. _asyncError(error, callback) {
  89. process.nextTick(() => this._error(error, callback));
  90. }
  91. /**
  92. * Opens a connection to the given serial port.
  93. * @param {ErrorCallback=} openCallback - Called after a connection is opened. If this is not provided and an error occurs, it will be emitted on the port's `error` event.
  94. * @emits open
  95. */
  96. open(openCallback) {
  97. if (this.isOpen) {
  98. return this._asyncError(new Error('Port is already open'), openCallback);
  99. }
  100. if (this.opening) {
  101. return this._asyncError(new Error('Port is opening'), openCallback);
  102. }
  103. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  104. const { highWaterMark, binding, autoOpen, endOnClose, ...openOptions } = this.settings;
  105. this.opening = true;
  106. debug('opening', `path: ${this.path}`);
  107. this.settings.binding.open(openOptions).then(port => {
  108. debug('opened', `path: ${this.path}`);
  109. this.port = port;
  110. this.opening = false;
  111. this.emit('open');
  112. if (openCallback) {
  113. openCallback.call(this, null);
  114. }
  115. }, err => {
  116. this.opening = false;
  117. debug('Binding #open had an error', err);
  118. this._error(err, openCallback);
  119. });
  120. }
  121. /**
  122. * Changes the baud rate for an open port. Emits an error or calls the callback if the baud rate isn't supported.
  123. * @param {object=} options Only supports `baudRate`.
  124. * @param {number=} [options.baudRate] The baud rate of the port to be opened. This should match one of the commonly available baud rates, such as 110, 300, 1200, 2400, 4800, 9600, 14400, 19200, 38400, 57600, or 115200. Custom rates are supported best effort per platform. The device connected to the serial port is not guaranteed to support the requested baud rate, even if the port itself supports that baud rate.
  125. * @param {ErrorCallback=} [callback] Called once the port's baud rate changes. If `.update` is called without a callback, and there is an error, an error event is emitted.
  126. * @returns {undefined}
  127. */
  128. update(options, callback) {
  129. if (!this.isOpen || !this.port) {
  130. debug('update attempted, but port is not open');
  131. return this._asyncError(new Error('Port is not open'), callback);
  132. }
  133. debug('update', `baudRate: ${options.baudRate}`);
  134. this.port.update(options).then(() => {
  135. debug('binding.update', 'finished');
  136. this.settings.baudRate = options.baudRate;
  137. if (callback) {
  138. callback.call(this, null);
  139. }
  140. }, err => {
  141. debug('binding.update', 'error', err);
  142. return this._error(err, callback);
  143. });
  144. }
  145. write(data, encoding, callback) {
  146. if (Array.isArray(data)) {
  147. data = Buffer.from(data);
  148. }
  149. if (typeof encoding === 'function') {
  150. return super.write(data, encoding);
  151. }
  152. return super.write(data, encoding, callback);
  153. }
  154. _write(data, encoding, callback) {
  155. if (!this.isOpen || !this.port) {
  156. this.once('open', () => {
  157. this._write(data, encoding, callback);
  158. });
  159. return;
  160. }
  161. debug('_write', `${data.length} bytes of data`);
  162. this.port.write(data).then(() => {
  163. debug('binding.write', 'write finished');
  164. callback(null);
  165. }, err => {
  166. debug('binding.write', 'error', err);
  167. if (!err.canceled) {
  168. this._disconnected(err);
  169. }
  170. callback(err);
  171. });
  172. }
  173. _writev(data, callback) {
  174. debug('_writev', `${data.length} chunks of data`);
  175. const dataV = data.map(write => write.chunk);
  176. this._write(Buffer.concat(dataV), 'binary', callback);
  177. }
  178. _read(bytesToRead) {
  179. if (!this.isOpen || !this.port) {
  180. debug('_read', 'queueing _read for after open');
  181. this.once('open', () => {
  182. this._read(bytesToRead);
  183. });
  184. return;
  185. }
  186. if (!this._pool || this._pool.length - this._pool.used < this._kMinPoolSpace) {
  187. debug('_read', 'discarding the read buffer pool because it is below kMinPoolSpace');
  188. this._pool = allocNewReadPool(this.settings.highWaterMark);
  189. }
  190. // Grab another reference to the pool in the case that while we're
  191. // in the thread pool another read() finishes up the pool, and
  192. // allocates a new one.
  193. const pool = this._pool;
  194. // Read the smaller of rest of the pool or however many bytes we want
  195. const toRead = Math.min(pool.length - pool.used, bytesToRead);
  196. const start = pool.used;
  197. // the actual read.
  198. debug('_read', 'reading', { start, toRead });
  199. this.port.read(pool, start, toRead).then(({ bytesRead }) => {
  200. debug('binding.read', 'finished', { bytesRead });
  201. // zero bytes means read means we've hit EOF? Maybe this should be an error
  202. if (bytesRead === 0) {
  203. debug('binding.read', 'Zero bytes read closing readable stream');
  204. this.push(null);
  205. return;
  206. }
  207. pool.used += bytesRead;
  208. this.push(pool.slice(start, start + bytesRead));
  209. }, err => {
  210. debug('binding.read', 'error', err);
  211. if (!err.canceled) {
  212. this._disconnected(err);
  213. }
  214. this._read(bytesToRead); // prime to read more once we're reconnected
  215. });
  216. }
  217. _disconnected(err) {
  218. if (!this.isOpen) {
  219. debug('disconnected aborted because already closed', err);
  220. return;
  221. }
  222. debug('disconnected', err);
  223. this.close(undefined, new DisconnectedError(err.message));
  224. }
  225. /**
  226. * Closes an open connection.
  227. *
  228. * If there are in progress writes when the port is closed the writes will error.
  229. * @param {ErrorCallback} callback Called once a connection is closed.
  230. * @param {Error} disconnectError used internally to propagate a disconnect error
  231. */
  232. close(callback, disconnectError = null) {
  233. if (!this.isOpen || !this.port) {
  234. debug('close attempted, but port is not open');
  235. return this._asyncError(new Error('Port is not open'), callback);
  236. }
  237. this.closing = true;
  238. debug('#close');
  239. this.port.close().then(() => {
  240. this.closing = false;
  241. debug('binding.close', 'finished');
  242. this.emit('close', disconnectError);
  243. if (this.settings.endOnClose) {
  244. this.emit('end');
  245. }
  246. if (callback) {
  247. callback.call(this, disconnectError);
  248. }
  249. }, err => {
  250. this.closing = false;
  251. debug('binding.close', 'had an error', err);
  252. return this._error(err, callback);
  253. });
  254. }
  255. /**
  256. * Set control flags on an open port. Uses [`SetCommMask`](https://msdn.microsoft.com/en-us/library/windows/desktop/aa363257(v=vs.85).aspx) for Windows and [`ioctl`](http://linux.die.net/man/4/tty_ioctl) for OS X and Linux.
  257. *
  258. * All options are operating system default when the port is opened. Every flag is set on each call to the provided or default values. If options isn't provided default options is used.
  259. */
  260. set(options, callback) {
  261. if (!this.isOpen || !this.port) {
  262. debug('set attempted, but port is not open');
  263. return this._asyncError(new Error('Port is not open'), callback);
  264. }
  265. const settings = { ...defaultSetFlags, ...options };
  266. debug('#set', settings);
  267. this.port.set(settings).then(() => {
  268. debug('binding.set', 'finished');
  269. if (callback) {
  270. callback.call(this, null);
  271. }
  272. }, err => {
  273. debug('binding.set', 'had an error', err);
  274. return this._error(err, callback);
  275. });
  276. }
  277. /**
  278. * Returns the control flags (CTS, DSR, DCD) on the open port.
  279. * Uses [`GetCommModemStatus`](https://msdn.microsoft.com/en-us/library/windows/desktop/aa363258(v=vs.85).aspx) for Windows and [`ioctl`](http://linux.die.net/man/4/tty_ioctl) for mac and linux.
  280. */
  281. get(callback) {
  282. if (!this.isOpen || !this.port) {
  283. debug('get attempted, but port is not open');
  284. return this._asyncError(new Error('Port is not open'), callback);
  285. }
  286. debug('#get');
  287. this.port.get().then(status => {
  288. debug('binding.get', 'finished');
  289. callback.call(this, null, status);
  290. }, err => {
  291. debug('binding.get', 'had an error', err);
  292. return this._error(err, callback);
  293. });
  294. }
  295. /**
  296. * Flush discards data received but not read, and written but not transmitted by the operating system. For more technical details, see [`tcflush(fd, TCIOFLUSH)`](http://linux.die.net/man/3/tcflush) for Mac/Linux and [`FlushFileBuffers`](http://msdn.microsoft.com/en-us/library/windows/desktop/aa364439) for Windows.
  297. */
  298. flush(callback) {
  299. if (!this.isOpen || !this.port) {
  300. debug('flush attempted, but port is not open');
  301. return this._asyncError(new Error('Port is not open'), callback);
  302. }
  303. debug('#flush');
  304. this.port.flush().then(() => {
  305. debug('binding.flush', 'finished');
  306. if (callback) {
  307. callback.call(this, null);
  308. }
  309. }, err => {
  310. debug('binding.flush', 'had an error', err);
  311. return this._error(err, callback);
  312. });
  313. }
  314. /**
  315. * Waits until all output data is transmitted to the serial port. After any pending write has completed it calls [`tcdrain()`](http://linux.die.net/man/3/tcdrain) or [FlushFileBuffers()](https://msdn.microsoft.com/en-us/library/windows/desktop/aa364439(v=vs.85).aspx) to ensure it has been written to the device.
  316. * @example
  317. Write the `data` and wait until it has finished transmitting to the target serial port before calling the callback. This will queue until the port is open and writes are finished.
  318. ```js
  319. function writeAndDrain (data, callback) {
  320. port.write(data);
  321. port.drain(callback);
  322. }
  323. ```
  324. */
  325. drain(callback) {
  326. debug('drain');
  327. if (!this.isOpen || !this.port) {
  328. debug('drain queuing on port open');
  329. this.once('open', () => {
  330. this.drain(callback);
  331. });
  332. return;
  333. }
  334. this.port.drain().then(() => {
  335. debug('binding.drain', 'finished');
  336. if (callback) {
  337. callback.call(this, null);
  338. }
  339. }, err => {
  340. debug('binding.drain', 'had an error', err);
  341. return this._error(err, callback);
  342. });
  343. }
  344. }
  345. exports.SerialPortStream = SerialPortStream;
  346. /**
  347. * The `error` event's callback is called with an error object whenever there is an error.
  348. * @event error
  349. */
  350. /**
  351. * The `open` event's callback is called with no arguments when the port is opened and ready for writing. This happens if you have the constructor open immediately (which opens in the next tick) or if you open the port manually with `open()`. See [Useage/Opening a Port](#opening-a-port) for more information.
  352. * @event open
  353. */
  354. /**
  355. * Request a number of bytes from the SerialPort. The `read()` method pulls some data out of the internal buffer and returns it. If no data is available to be read, null is returned. By default, the data is returned as a `Buffer` object unless an encoding has been specified using the `.setEncoding()` method.
  356. * @method SerialPort.prototype.read
  357. * @param {number=} size Specify how many bytes of data to return, if available
  358. * @returns {(string|Buffer|null)} The data from internal buffers
  359. */
  360. /**
  361. * Listening for the `data` event puts the port in flowing mode. Data is emitted as soon as it's received. Data is a `Buffer` object with a varying amount of data in it. The `readLine` parser converts the data into string lines. See the [parsers](https://serialport.io/docs/api-parsers-overview) section for more information on parsers, and the [Node.js stream documentation](https://nodejs.org/api/stream.html#stream_event_data) for more information on the data event.
  362. * @event data
  363. */
  364. /**
  365. * The `close` event's callback is called with no arguments when the port is closed. In the case of a disconnect it will be called with a Disconnect Error object (`err.disconnected == true`). In the event of a close error (unlikely), an error event is triggered.
  366. * @event close
  367. */
  368. /**
  369. * The `pause()` method causes a stream in flowing mode to stop emitting 'data' events, switching out of flowing mode. Any data that becomes available remains in the internal buffer.
  370. * @method SerialPort.prototype.pause
  371. * @see resume
  372. * @returns `this`
  373. */
  374. /**
  375. * The `resume()` method causes an explicitly paused, `Readable` stream to resume emitting 'data' events, switching the stream into flowing mode.
  376. * @method SerialPort.prototype.resume
  377. * @see pause
  378. * @returns `this`
  379. */