UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

393 lines (343 loc) 9.01 kB
/* eslint-disable @typescript-eslint/require-await */ // Clone of https://github.com/serialport/binding-mock with support for emitting events on the written side import type { BindingInterface, BindingPortInterface, OpenOptions, PortInfo, PortStatus, SetOptions, UpdateOptions, } from "@serialport/bindings-interface"; import { TypedEventEmitter } from "@zwave-js/shared"; export interface MockPortInternal { data: Buffer; // echo: boolean; // record: boolean; info: PortInfo; maxReadSize: number; readyData?: Buffer; openOpt?: OpenOptions; instance?: MockPortBinding; } export interface CreatePortOptions { echo?: boolean; record?: boolean; readyData?: Buffer; maxReadSize?: number; manufacturer?: string; vendorId?: string; productId?: string; } let ports: { [key: string]: MockPortInternal; } = {}; let serialNumber = 0; function resolveNextTick() { return new Promise<void>((resolve) => process.nextTick(() => resolve())); } export class CanceledError extends Error { canceled: true; constructor(message: string) { super(message); this.canceled = true; } } export interface MockBindingInterface extends BindingInterface<MockPortBinding> { reset(): void; createPort(path: string, opt?: CreatePortOptions): void; getInstance(path: string): MockPortBinding | undefined; } export const MockBinding: MockBindingInterface = { reset() { ports = {}; serialNumber = 0; }, // Create a mock port createPort(path: string, options: CreatePortOptions = {}) { serialNumber++; const optWithDefaults = { echo: false, record: false, manufacturer: "The J5 Robotics Company", vendorId: undefined, productId: undefined, maxReadSize: 1024, ...options, }; ports[path] = { data: Buffer.alloc(0), // echo: optWithDefaults.echo, // record: optWithDefaults.record, readyData: optWithDefaults.readyData, maxReadSize: optWithDefaults.maxReadSize, info: { path, manufacturer: optWithDefaults.manufacturer, serialNumber: `${serialNumber}`, pnpId: undefined, locationId: undefined, vendorId: optWithDefaults.vendorId, productId: optWithDefaults.productId, }, }; }, async list() { return Object.values(ports).map((port) => port.info); }, async open(options) { if (!options || typeof options !== "object" || Array.isArray(options)) { throw new TypeError('"options" is not an object'); } if (!options.path) { throw new TypeError('"path" is not a valid port'); } if (!options.baudRate) { throw new TypeError('"baudRate" is not a valid baudRate'); } const openOptions: Required<OpenOptions> = { dataBits: 8, lock: true, stopBits: 1, parity: "none", rtscts: false, xon: false, xoff: false, xany: false, hupcl: true, ...options, }; const { path } = openOptions; const port = ports[path]; await resolveNextTick(); if (!port) { throw new Error( `Port does not exist - please call MockBinding.createPort('${path}') first`, ); } if (port.openOpt?.lock) { throw new Error("Port is locked cannot open"); } port.openOpt = { ...openOptions }; port.instance = new MockPortBinding(port, openOptions); return port.instance; }, getInstance(path: string): MockPortBinding | undefined { return ports[path]?.instance; }, }; interface MockPortBindingEvents { write: (data: Buffer) => void; } /** * Mock bindings for pretend serialport access */ export class MockPortBinding extends TypedEventEmitter<MockPortBindingEvents> implements BindingPortInterface { readonly openOptions: Required<OpenOptions>; readonly port: MockPortInternal; private pendingRead: null | ((err: null | Error) => void); lastWrite: null | Buffer; recording: Buffer; writeOperation: null | Promise<void>; isOpen: boolean; serialNumber?: string; constructor(port: MockPortInternal, openOptions: Required<OpenOptions>) { super(); this.port = port; this.openOptions = openOptions; this.pendingRead = null; this.isOpen = true; this.lastWrite = null; this.recording = Buffer.alloc(0); this.writeOperation = null; // in flight promise or null this.serialNumber = port.info.serialNumber; if (port.readyData) { const data = port.readyData; process.nextTick(() => { if (this.isOpen) { this.emitData(data); } }); } } // Emit data on a mock port emitData(data: Buffer | string): void { if (!this.isOpen || !this.port) { throw new Error("Port must be open to pretend to receive data"); } const bufferData = Buffer.isBuffer(data) ? data : Buffer.from(data); this.port.data = Buffer.concat([this.port.data, bufferData]); if (this.pendingRead) { process.nextTick(this.pendingRead); this.pendingRead = null; } } async close(): Promise<void> { if (!this.isOpen) { throw new Error("Port is not open"); } const port = this.port; if (!port) { throw new Error("already closed"); } port.openOpt = undefined; // reset data on close port.data = Buffer.alloc(0); this.serialNumber = undefined; this.isOpen = false; if (this.pendingRead) { this.pendingRead(new CanceledError("port is closed")); } } async read( buffer: Buffer, offset: number, length: number, ): Promise<{ buffer: Buffer; bytesRead: number; }> { if (!Buffer.isBuffer(buffer)) { throw new TypeError('"buffer" is not a Buffer'); } if (typeof offset !== "number" || isNaN(offset)) { throw new TypeError( `"offset" is not an integer got "${ isNaN(offset) ? "NaN" : typeof offset }"`, ); } if (typeof length !== "number" || isNaN(length)) { throw new TypeError( `"length" is not an integer got "${ isNaN(length) ? "NaN" : typeof length }"`, ); } if (buffer.length < offset + length) { throw new Error("buffer is too small"); } if (!this.isOpen) { throw new Error("Port is not open"); } await resolveNextTick(); if (!this.isOpen || !this.port) { throw new CanceledError("Read canceled"); } if (this.port.data.length <= 0) { return new Promise((resolve, reject) => { this.pendingRead = (err) => { if (err) { return reject(err); } this.read(buffer, offset, length).then(resolve, reject); }; }); } const lengthToRead = this.port.maxReadSize > length ? length : this.port.maxReadSize; const data = this.port.data.slice(0, lengthToRead); const bytesRead = data.copy(buffer, offset); this.port.data = this.port.data.slice(lengthToRead); return { bytesRead, buffer }; } async write(buffer: Buffer): Promise<void> { if (!Buffer.isBuffer(buffer)) { throw new TypeError('"buffer" is not a Buffer'); } if (!this.isOpen || !this.port) { throw new Error("Port is not open"); } if (this.writeOperation) { throw new Error( "Overlapping writes are not supported and should be queued by the serialport object", ); } this.writeOperation = (async () => { await resolveNextTick(); if (!this.isOpen || !this.port) { return; // throw new Error("Write canceled"); } const data = (this.lastWrite = Buffer.from(buffer)); // copy this.emit("write", data); // if (this.port.record) { // this.recording = Buffer.concat([this.recording, data]); // } // if (this.port.echo) { // process.nextTick(() => { // if (this.isOpen) { // this.emitData(data); // } // }); // } this.writeOperation = null; })(); return this.writeOperation; } async update(options: UpdateOptions): Promise<void> { if (typeof options !== "object") { throw TypeError('"options" is not an object'); } if (typeof options.baudRate !== "number") { throw new TypeError('"options.baudRate" is not a number'); } if (!this.isOpen || !this.port) { throw new Error("Port is not open"); } await resolveNextTick(); if (this.port.openOpt) { this.port.openOpt.baudRate = options.baudRate; } } async set(options: SetOptions): Promise<void> { if (typeof options !== "object") { throw new TypeError('"options" is not an object'); } if (!this.isOpen) { throw new Error("Port is not open"); } await resolveNextTick(); } async get(): Promise<PortStatus> { if (!this.isOpen) { throw new Error("Port is not open"); } await resolveNextTick(); return { cts: true, dsr: false, dcd: false, }; } async getBaudRate(): Promise<{ baudRate: number }> { if (!this.isOpen || !this.port) { throw new Error("Port is not open"); } await resolveNextTick(); if (!this.port.openOpt?.baudRate) { throw new Error("Internal Error"); } return { baudRate: this.port.openOpt.baudRate, }; } async flush(): Promise<void> { if (!this.isOpen || !this.port) { throw new Error("Port is not open"); } await resolveNextTick(); this.port.data = Buffer.alloc(0); } async drain(): Promise<void> { if (!this.isOpen) { throw new Error("Port is not open"); } await this.writeOperation; await resolveNextTick(); } }