UNPKG

webserial-core

Version:

Webserial Core to easy connections with serial devices

1 lines 107 kB
{"version":3,"file":"webserial-core.umd.cjs","sources":["../lib/SerialEvent.ts","../lib/Dispatcher.ts","../lib/Devices.ts","../lib/utils.ts","../lib/Socket.ts","../lib/Core.ts","../lib/SerialError.ts"],"sourcesContent":["export class SerialEvent extends CustomEvent<SerialEvent> implements CustomEvent {\n constructor(type: string, options: CustomEventInit) {\n super(type, options);\n }\n}\n","import { SerialEvent } from \"./SerialEvent\";\n\ntype AvailableListener = { type: string; listening: boolean };\ntype AvailableListeners = AvailableListener[];\n\ntype DataType = string | number | boolean | object | null;\n\ninterface IDispatcher {\n dispatch(type: string, data?: DataType): void;\n\n dispatchAsync(type: string, data?: DataType, ms?: number): void;\n\n on(type: string, callback: EventListener): void;\n\n off(type: string, callback: EventListener): void;\n\n serialRegisterAvailableListener(type: string): void;\n\n availableListeners: AvailableListeners;\n}\n\ninterface Listeners {\n [key: string]: boolean;\n\n debug: boolean;\n}\n\nexport class Dispatcher extends EventTarget implements IDispatcher {\n __listeners__: Listeners = {\n debug: false,\n };\n __debug__: boolean = false;\n\n __listenersCallbacks__: { key: string; callback: EventListenerOrEventListenerObject }[] = [];\n\n /**\n * Dispatches an event with the specified type and data\n * @param type - The event type to dispatch\n * @param data - Optional data to attach to the event\n * @example\n * ```typescript\n * dispatcher.dispatch('connected', { port: 'COM3' });\n * ```\n */\n public dispatch(type: string, data: DataType = null) {\n const event = new SerialEvent(type, { detail: data });\n this.dispatchEvent(event);\n if (this.__debug__) {\n this.dispatchEvent(new SerialEvent(\"debug\", { detail: { type, data } }));\n }\n }\n\n /**\n * Dispatches an event asynchronously after a specified delay\n * @param type - The event type to dispatch\n * @param data - Optional data to attach to the event\n * @param ms - Delay in milliseconds (default: 100)\n * @example\n * ```typescript\n * dispatcher.dispatchAsync('timeout', { reason: 'no response' }, 500);\n * ```\n */\n public dispatchAsync(type: string, data = null, ms = 100) {\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n const this1 = this;\n setTimeout(() => {\n this1.dispatch(type, data);\n }, ms);\n }\n\n /**\n * Registers an event listener for the specified event type\n * @param type - The event type to listen to\n * @param callback - The callback function to execute when the event is triggered\n * @example\n * ```typescript\n * dispatcher.on('connected', (event) => {\n * console.log('Device connected', event.detail);\n * });\n * ```\n */\n public on(type: string, callback: EventListenerOrEventListenerObject) {\n if (typeof this.__listeners__[type] !== \"undefined\" && !this.__listeners__[type]) {\n this.__listeners__[type] = true;\n }\n\n this.__listenersCallbacks__.push({ key: type, callback });\n this.addEventListener(type, callback);\n }\n\n /**\n * Removes an event listener for the specified event type\n * @param type - The event type to stop listening to\n * @param callback - The callback function to remove\n * @example\n * ```typescript\n * const handler = (event) => console.log(event.detail);\n * dispatcher.on('data', handler);\n * dispatcher.off('data', handler);\n * ```\n */\n public off(type: string, callback: EventListenerOrEventListenerObject) {\n this.__listenersCallbacks__ = this.__listenersCallbacks__.filter((listener) => {\n return !(listener.key === type && listener.callback === callback);\n });\n\n this.removeEventListener(type, callback);\n }\n\n /**\n * Registers an available listener type for tracking\n * @param type - The event type to register\n * @internal\n */\n public serialRegisterAvailableListener(type: string) {\n if (this.__listeners__[type]) return;\n\n this.__listeners__[type] = false;\n }\n\n /**\n * Gets the list of all available listeners and their state\n * @returns Array of listener objects with type and listening status\n * @example\n * ```typescript\n * const listeners = dispatcher.availableListeners;\n * console.log(listeners); // [{ type: 'connected', listening: true }, ...]\n * ```\n */\n get availableListeners(): AvailableListeners {\n const keys = Object.keys(this.__listeners__).sort();\n return keys.map((type): AvailableListener => {\n return {\n type,\n listening: this.__listeners__[type],\n };\n });\n }\n\n /**\n * Removes all event listeners except internal ones (like queue listeners)\n * Resets all listener states to false\n * @example\n * ```typescript\n * dispatcher.removeAllListeners();\n * ```\n */\n public removeAllListeners(): void {\n for (const listener of this.__listenersCallbacks__) {\n if ([\"internal:queue\"].includes(listener.key)) continue; // Skip queue listener\n\n this.__listenersCallbacks__ = this.__listenersCallbacks__.filter((l) => {\n return !(l.key === listener.key && l.callback === listener.callback);\n });\n this.removeEventListener(listener.key, listener.callback);\n }\n for (const key of Object.keys(this.__listeners__)) {\n this.__listeners__[key] = false;\n }\n }\n}\n","import { Core } from \"./Core\";\nimport { Dispatcher } from \"./Dispatcher\";\n\ninterface IDevice {\n [key: string]: Core;\n}\n\ninterface IDevices {\n [key: string]: IDevice;\n}\n\n/**\n * Manages and tracks all serial devices in the application\n * Provides a centralized registry for device instances\n * @extends Dispatcher\n */\nexport class Devices extends Dispatcher {\n static instance: Devices;\n static devices: IDevices = {};\n\n constructor() {\n super();\n\n const availableListeners: string[] = [\"change\"];\n\n availableListeners.forEach((event: string): void => {\n this.serialRegisterAvailableListener(event);\n });\n }\n\n public static $dispatchChange(device: Core | null = null): void {\n if (device) {\n device.$checkAndDispatchConnection();\n }\n Devices.instance.dispatch(\"change\", { devices: Devices.devices, dispatcher: device });\n }\n\n public static typeError(type: string): void {\n const error = new Error();\n error.message = `Type ${type} is not supported`;\n error.name = \"DeviceTypeError\";\n throw error;\n }\n\n /**\n * Registers a new device type in the registry\n * @param type - The type name of the device (e.g., 'arduino', 'esp32')\n * @internal\n */\n public static registerType(type: string): void {\n if (typeof Devices.devices[type] === \"undefined\") {\n Devices.devices = { ...Devices.devices, [type]: {} };\n }\n }\n\n /**\n * Adds a device to the registry\n * @param device - The Core device instance to add\n * @returns The index of the device in its type registry\n * @throws {Error} If device with the same ID already exists\n * @example\n * ```typescript\n * const arduino = new Arduino();\n * Devices.add(arduino);\n * ```\n */\n public static add(device: Core): number {\n const type = device.typeDevice;\n if (typeof Devices.devices[type] === \"undefined\") {\n Devices.registerType(type);\n }\n\n const id: string = device.uuid;\n\n if (typeof Devices.devices[type] === \"undefined\") Devices.typeError(type);\n\n if (Devices.devices[type][id]) {\n throw new Error(`Device with id ${id} already exists`);\n }\n\n Devices.devices[type][id] = device;\n\n Devices.$dispatchChange(device);\n return Object.keys(Devices.devices[type]).indexOf(id);\n }\n\n /**\n * Gets a specific device by type and UUID\n * @param type - The device type\n * @param id - The device UUID\n * @returns The device instance\n * @throws {Error} If the device type is not supported\n * @example\n * ```typescript\n * const device = Devices.get('arduino', 'uuid-123');\n * ```\n */\n public static get(type: string, id: string): Core {\n if (typeof Devices.devices[type] === \"undefined\") {\n Devices.registerType(type);\n }\n\n if (typeof Devices.devices[type] === \"undefined\") Devices.typeError(type);\n\n return Devices.devices[type][id];\n }\n\n public static getAll(type: string | null = null): IDevice | IDevices {\n if (type === null) return Devices.devices;\n if (typeof Devices.devices[type] === \"undefined\") Devices.typeError(type);\n\n return Devices.devices[type];\n }\n\n public static getList(): Core[] {\n // get all devices in list mode no matter the type\n // by some reason the array is empty so we need to use Object.values and map\n const devices: IDevice[] = Object.values(Devices.devices);\n return devices\n .map((device: IDevice): Core[] => {\n return Object.values(device);\n })\n .flat();\n }\n\n public static getByNumber(type: string, device_number: number): Core | null {\n if (typeof Devices.devices[type] === \"undefined\") Devices.typeError(type);\n\n const devices = Object.values(Devices.devices[type]);\n return devices.find((device) => device.deviceNumber === device_number) ?? null;\n }\n\n public static getCustom(type: string, device_number: number = 1): Core | null {\n if (typeof Devices.devices[type] === \"undefined\") Devices.typeError(type);\n\n const devices = Object.values(Devices.devices[type]);\n return devices.find((device) => device.deviceNumber === device_number) ?? null;\n }\n\n public static async connectToAll(): Promise<boolean> {\n const devices: Core[] = Devices.getList();\n\n for (const device of devices) {\n if (device.isConnected) continue;\n await device.connect().catch(console.warn);\n }\n\n return Promise.resolve(Devices.areAllConnected());\n }\n\n public static async disconnectAll(): Promise<boolean> {\n const devices: Core[] = Devices.getList();\n\n for (const device of devices) {\n if (device.isDisconnected) continue;\n await device.disconnect().catch(console.warn);\n }\n\n return Promise.resolve(Devices.areAllDisconnected());\n }\n\n public static async areAllConnected(): Promise<boolean> {\n const devices: Core[] = Devices.getList();\n\n for (const device of devices) {\n if (!device.isConnected) return Promise.resolve(false);\n }\n\n return Promise.resolve(true);\n }\n\n public static async areAllDisconnected(): Promise<boolean> {\n const devices: Core[] = Devices.getList();\n\n for (const device of devices) {\n if (!device.isDisconnected) return Promise.resolve(false);\n }\n\n return Promise.resolve(true);\n }\n\n public static async getAllConnected(): Promise<Core[]> {\n const devices: Core[] = Devices.getList();\n return Promise.resolve(devices.filter((device: Core): boolean => device.isConnected));\n }\n\n public static async getAllDisconnected(): Promise<Core[]> {\n const devices: Core[] = Devices.getList();\n return Promise.resolve(devices.filter((device: Core): boolean => device.isDisconnected));\n }\n}\n\nif (!Devices.instance) {\n Devices.instance = new Devices();\n}\n","type empty = void | PromiseLike<void>;\n\nexport function wait(ms: number = 100): Promise<void> {\n return new Promise(\n (resolve: (value: empty) => void): ReturnType<typeof setTimeout> => setTimeout((): void => resolve(), ms),\n );\n}\n\n/*\n * @deprecated This function is deprecated and will be removed in future versions.\n */\nexport function supportWebSerial(): boolean {\n return \"serial\" in navigator;\n}\n","import { io, ManagerOptions, SocketOptions, Socket as SocketIOClient } from \"socket.io-client\";\nimport { Devices } from \"./Devices\";\nimport { Core } from \"./Core\";\n\ninterface SocketResponseData {\n name: string;\n uuid: string;\n deviceNumber: number;\n [key: string]: unknown;\n}\n\ntype BoundedFunction = {\n onResponse: (data: SocketResponseData) => void;\n onDisconnect: () => void;\n onConnect: () => void;\n onConnectError: (error: unknown) => void;\n};\n\nclass MySocket {\n #uri: string = \"http://localhost:3000\";\n #options: Partial<ManagerOptions & SocketOptions> = {\n transports: [\"websocket\"],\n };\n #socket: SocketIOClient | null = null;\n #connected: boolean = false;\n #hasInstance: boolean = false;\n\n #boundedFun: BoundedFunction;\n\n constructor() {\n this.#boundedFun = {\n onResponse: this.onResponse.bind(this),\n onDisconnect: () => {\n // console.debug(\"Socket disconnected\", this.#socket?.id);\n this.#connected = false;\n window.dispatchEvent(new Event(\"serial:socket:disconnected\"));\n },\n onConnect: () => {\n // console.debug(\"Socket connected\", this.#socket?.id);\n this.#connected = true;\n window.dispatchEvent(new Event(\"serial:socket:connected\"));\n },\n onConnectError: (error) => {\n console.debug(\"Socket connection error\", error);\n this.#connected = false;\n window.dispatchEvent(new Event(\"serial:socket:disconnected\"));\n },\n };\n }\n\n set uri(uri: string) {\n const url = new URL(uri);\n\n if (![\"http:\", \"https:\", \"ws:\", \"wss:\"].includes(url.protocol)) {\n throw new Error(\"URI must start with http://, https://, ws://, or wss://\");\n }\n this.#uri = uri;\n }\n\n get uri(): string {\n return this.#uri;\n }\n\n set options(options: Partial<ManagerOptions & SocketOptions>) {\n if (typeof options !== \"object\") {\n throw new Error(\"Options must be an object\");\n }\n this.#options = options;\n }\n\n get options(): Partial<ManagerOptions & SocketOptions> {\n return this.#options;\n }\n\n get socketId(): string | null {\n return this.#socket && this.#socket.id ? this.#socket.id : null;\n }\n\n disconnect() {\n if (this.#socket) {\n this.#socket.off(\"response\", this.#boundedFun.onResponse);\n this.#socket.off(\"disconnect\", this.#boundedFun.onDisconnect);\n this.#socket.off(\"connect\", this.#boundedFun.onConnect);\n this.#socket.off(\"connect_error\", this.#boundedFun.onConnectError);\n\n this.#socket.disconnect();\n this.#socket = null;\n this.#hasInstance = false;\n }\n this.#connected = false;\n }\n\n prepare() {\n if (this.#connected || this.#hasInstance) return;\n\n this.#socket = io(this.#uri, this.#options);\n // this.#connected = true; // don't asume connected until onConnect is called\n this.#hasInstance = true;\n\n this.#socket.on(\"disconnect\", this.#boundedFun.onDisconnect);\n this.#socket.on(\"response\", this.#boundedFun.onResponse);\n this.#socket.on(\"connect\", this.#boundedFun.onConnect);\n this.#socket.on(\"connect_error\", this.#boundedFun.onConnectError);\n }\n\n connectDevice(config: object): void {\n if (!this.#socket) {\n throw new Error(\"Socket not connected. Call prepare() first.\");\n }\n this.#socket.emit(\"connectDevice\", { config });\n }\n\n disconnectDevice(config: object): void {\n if (!this.#socket) {\n throw new Error(\"Socket not connected. Call prepare() first.\");\n }\n this.#socket.emit(\"disconnectDevice\", { config });\n }\n\n disconnectAllDevices(): void {\n if (!this.#socket) {\n throw new Error(\"Socket not connected. Call prepare() first.\");\n }\n this.#socket.emit(\"disconnectAll\");\n }\n\n write(data: object): void {\n if (!this.#socket) {\n throw new Error(\"Socket not connected. Call prepare() first.\");\n }\n this.#socket.emit(\"cmd\", data);\n }\n\n onResponse(data: SocketResponseData): void {\n let device: Core | null = Devices.get(data.name, data.uuid);\n if (!device) {\n device = Devices.getByNumber(data.name, data.deviceNumber);\n }\n if (!device) {\n return;\n }\n device.socketResponse(data);\n }\n\n isConnected(): boolean {\n return this.#connected;\n }\n\n isDisconnected(): boolean {\n return !this.#connected;\n }\n}\n\nexport const Socket = new MySocket();\n","import { Dispatcher } from \"./Dispatcher\";\nimport { Devices } from \"./Devices\";\nimport { wait } from \"./utils\";\nimport { Socket } from \"./Socket\";\n\ninterface LastError {\n message: string | null;\n action: string | null;\n code: string | Uint8Array | Array<string> | Array<number> | null | number;\n no_code: number;\n}\n\ninterface DeviceData {\n type: string;\n id: string;\n listen_on_port: number | null;\n}\n\ntype SerialResponseAs = \"hex\" | \"uint8\" | \"string\" | \"arraybuffer\";\n\ninterface SerialResponse {\n length: number | null;\n buffer: Uint8Array;\n as: SerialResponseAs;\n replacer: RegExp | string;\n limiter: null | string | RegExp;\n prefixLimiter: boolean; // If true, the limiter is at the beginning of the message\n sufixLimiter: boolean; // If true, the limiter is at the end of the message\n delimited: boolean;\n}\n\ninterface QueueData {\n bytes: string | Uint8Array | Array<string> | Array<number>;\n action: string;\n}\n\ntype ParserSocketPort = {\n name: \"byte-length\" | \"inter-byte-timeout\";\n length?: number; // Length of each byte in the response, only for byte-length\n interval?: number; // Interval in milliseconds for inter-byte-timeout\n};\n\ntype PortInfo = {\n path: string | null;\n vendorId: number | string | null;\n productId: number | string | null;\n parser: ParserSocketPort;\n};\n\ntype SerialData = {\n socket: boolean;\n portInfo: PortInfo;\n aux_connecting: string;\n connecting: boolean;\n connected: boolean;\n port: SerialPort | null;\n last_action: string | null;\n response: SerialResponse;\n reader: ReadableStreamDefaultReader<Uint8Array> | null;\n input_done: Promise<void> | null;\n output_done: Promise<void> | null;\n input_stream: ReadableStream<Uint8Array> | null;\n output_stream: WritableStream<Uint8Array> | null;\n keep_reading: boolean;\n time_until_send_bytes: number | undefined | ReturnType<typeof setTimeout>;\n delay_first_connection: number;\n bytes_connection: string | Uint8Array | string[] | number[] | null;\n filters: SerialPortFilter[];\n config_port: SerialOptions;\n queue: QueueData[];\n running_queue: boolean;\n auto_response: any;\n free_timeout_ms: number;\n useRTSCTS: boolean;\n};\n\ninterface TimeResponse {\n response_connection: number;\n response_engines: number;\n response_general: number;\n}\n\ninterface Timeout {\n until_response: number | ReturnType<typeof setTimeout>;\n}\n\ninterface InternalIntervals {\n reconnection: number;\n}\n\nexport type Internal = {\n bypassSerialBytesConnection: boolean;\n auto_response: boolean;\n device_number: number;\n aux_port_connector: number;\n last_error: LastError;\n serial: SerialData;\n device: DeviceData;\n time: TimeResponse;\n timeout: Timeout;\n interval: InternalIntervals;\n};\n\ninterface CoreConstructorParams {\n filters?: SerialPortFilter[] | null;\n config_port?: SerialOptions;\n no_device?: number;\n device_listen_on_channel?: number | string;\n bypassSerialBytesConnection?: boolean;\n socket?: boolean;\n}\n\nconst defaultConfigPort: SerialOptions = {\n baudRate: 9600,\n dataBits: 8,\n stopBits: 1,\n parity: \"none\",\n bufferSize: 32768,\n flowControl: \"none\",\n};\n\ninterface CustomCode {\n code: string | Uint8Array | Array<string> | Array<number>;\n}\n\ninterface ICore {\n lastAction: string | null;\n\n set listenOnChannel(channel: string | number);\n\n set serialFilters(filters: SerialPortFilter[]);\n\n get serialFilters(): SerialPortFilter[];\n\n set serialConfigPort(config_port: SerialOptions);\n\n get serialConfigPort(): SerialOptions;\n\n get isConnected(): boolean;\n\n get isConnecting(): boolean;\n\n get isDisconnected(): boolean;\n\n get useRTSCTS(): boolean;\n\n set useRTSCTS(value: boolean);\n\n get deviceNumber(): number;\n\n get uuid(): string;\n\n get typeDevice(): string;\n\n get queue(): QueueData[];\n\n get timeoutBeforeResponseBytes(): number;\n\n set timeoutBeforeResponseBytes(value: number);\n\n get fixedBytesMessage(): number | null;\n\n set fixedBytesMessage(length: number | null);\n\n get responseDelimited(): boolean;\n\n set responseDelimited(value: boolean);\n\n get responsePrefixLimited(): boolean;\n\n set responsePrefixLimited(value: boolean);\n\n get responseSufixLimited(): boolean;\n\n set responseSufixLimited(value: boolean);\n\n get responseLimiter(): string | RegExp | null;\n\n set responseLimiter(limiter: string | RegExp | null);\n\n get bypassSerialBytesConnection(): boolean;\n\n set bypassSerialBytesConnection(value: boolean);\n\n timeout(bytes: string[], event: string): Promise<void>;\n\n disconnect(detail?: null): Promise<void>;\n\n connect(): Promise<boolean>;\n\n serialDisconnect(): Promise<void>;\n\n serialPortsSaved(ports: SerialPort[]): Promise<void>;\n\n serialErrors(error: unknown | Error | DOMException): void;\n\n serialConnect(): Promise<void>;\n\n serialForget(): Promise<boolean>;\n\n decToHex(dec: number | string): string;\n\n hexToDec(hex: string): number;\n\n hexMaker(val?: string, min?: number): string;\n\n add0x(bytes: string[]): string[];\n\n bytesToHex(bytes: string[]): string[];\n\n appendToQueue(arr: string[], action: string): Promise<void>;\n\n serialSetConnectionConstant(listen_on_port?: number): string | Uint8Array | string[] | number[] | null;\n\n serialMessage(code: string[]): void;\n\n serialCorruptMessage(data: Uint8Array | number[] | string[] | never | null | string | ArrayBuffer): void;\n\n clearSerialQueue(): void;\n\n sumHex(arr: string[]): string;\n\n softReload(): void;\n\n sendConnect(): Promise<void>;\n\n sendCustomCode(customCode: CustomCode): Promise<void>;\n\n stringToArrayHex(string: string): string[];\n\n stringToArrayBuffer(string: string, end: string): ArrayBufferLike;\n\n parseStringToBytes(string: string, end: string): string[];\n\n parseUint8ToHex(array: Uint8Array): string[];\n\n parseHexToUint8(array: string[]): Uint8Array;\n\n stringArrayToUint8Array(strings: string[]): Uint8Array;\n\n parseUint8ArrayToString(array: string[]): string;\n\n parseStringToTextEncoder(string: string, end: string): Uint8Array;\n\n hexToAscii(hex: string | number): string;\n\n asciiToHex(asciiString: string): string;\n\n getResponseAsArrayBuffer(): void;\n\n getResponseAsArrayHex(): void;\n\n getResponseAsUint8Array(): void;\n\n getResponseAsString(): void;\n}\n\nexport class Core extends Dispatcher implements ICore {\n protected __internal__: Internal = {\n bypassSerialBytesConnection: false,\n auto_response: false,\n device_number: 1,\n aux_port_connector: 0,\n last_error: {\n message: null,\n action: null,\n code: null,\n no_code: 0,\n },\n serial: {\n socket: false,\n portInfo: {\n path: null,\n vendorId: null,\n productId: null,\n parser: {\n name: \"inter-byte-timeout\",\n interval: 50,\n },\n },\n aux_connecting: \"idle\",\n connecting: false,\n connected: false,\n port: null,\n last_action: null,\n response: {\n length: null,\n buffer: new Uint8Array([]),\n as: \"uint8\",\n replacer: /[\\n\\r]+/g,\n limiter: null,\n prefixLimiter: false,\n sufixLimiter: true,\n delimited: false,\n },\n reader: null,\n input_done: null,\n output_done: null,\n input_stream: null,\n output_stream: null,\n keep_reading: true,\n time_until_send_bytes: undefined,\n delay_first_connection: 200,\n bytes_connection: null,\n filters: [],\n config_port: defaultConfigPort,\n queue: [],\n running_queue: false,\n auto_response: null,\n free_timeout_ms: 50, // In previous versions 400 was used\n useRTSCTS: false, // Use RTS/CTS flow control\n },\n device: {\n type: \"unknown\",\n id: window.crypto.randomUUID(),\n listen_on_port: null,\n },\n time: {\n response_connection: 500,\n response_engines: 2e3,\n response_general: 2e3,\n },\n timeout: {\n until_response: 0,\n },\n interval: {\n reconnection: 0,\n },\n };\n\n #boundFinishConnecting: EventListenerOrEventListenerObject | null = null;\n\n constructor(\n {\n filters = null,\n config_port = defaultConfigPort,\n no_device = 1,\n device_listen_on_channel = 1,\n bypassSerialBytesConnection = false,\n socket = false,\n }: CoreConstructorParams = {\n filters: null,\n config_port: defaultConfigPort,\n no_device: 1,\n device_listen_on_channel: 1,\n bypassSerialBytesConnection: false,\n socket: false,\n },\n ) {\n super();\n\n if (!(\"serial\" in navigator)) {\n throw new Error(\"Web Serial not supported\");\n }\n\n if (filters) {\n this.serialFilters = filters;\n }\n\n if (config_port) {\n this.serialConfigPort = config_port;\n }\n\n if (bypassSerialBytesConnection) {\n this.__internal__.bypassSerialBytesConnection = bypassSerialBytesConnection;\n }\n\n if (no_device) {\n this.#serialSetBytesConnection(no_device);\n }\n\n if (device_listen_on_channel && [\"number\", \"string\"].includes(typeof device_listen_on_channel)) {\n this.listenOnChannel = device_listen_on_channel;\n }\n\n this.__internal__.serial.socket = socket;\n\n this.#registerDefaultListeners();\n this.#internalEvents();\n }\n\n set listenOnChannel(channel: string | number) {\n if (typeof channel === \"string\") {\n channel = parseInt(channel);\n }\n if (isNaN(channel) || channel < 1 || channel > 255) {\n throw new Error(\"Invalid port number\");\n }\n this.__internal__.device.listen_on_port = channel;\n if (this.__internal__.bypassSerialBytesConnection) return;\n this.__internal__.serial.bytes_connection = this.serialSetConnectionConstant(channel);\n }\n\n get lastAction(): string | null {\n return this.__internal__.serial.last_action;\n }\n\n get listenOnChannel(): number {\n return this.__internal__.device.listen_on_port ?? 1;\n }\n\n set serialFilters(filters: SerialPortFilter[]) {\n if (this.isConnected) throw new Error(\"Cannot change serial filters while connected\");\n this.__internal__.serial.filters = filters;\n }\n\n get serialFilters(): SerialPortFilter[] {\n return this.__internal__.serial.filters;\n }\n\n set serialConfigPort(config_port: SerialOptions) {\n if (this.isConnected) throw new Error(\"Cannot change serial filters while connected\");\n this.__internal__.serial.config_port = config_port;\n }\n\n get serialConfigPort(): SerialOptions {\n return this.__internal__.serial.config_port;\n }\n\n get useRTSCTS(): boolean {\n return this.__internal__.serial.useRTSCTS;\n }\n\n set useRTSCTS(value: boolean) {\n this.__internal__.serial.useRTSCTS = value;\n }\n\n get isConnected(): boolean {\n const prevConnected = this.__internal__.serial.connected;\n const connected = this.#checkIfPortIsOpen(this.__internal__.serial.port);\n if (prevConnected && !connected) {\n this.#disconnected({ error: \"Port is closed, not readable or writable.\" });\n }\n this.__internal__.serial.connected = connected;\n return this.__internal__.serial.connected;\n }\n\n get isConnecting(): boolean {\n return this.__internal__.serial.connecting;\n }\n\n get isDisconnected(): boolean {\n const prevConnected = this.__internal__.serial.connected;\n const connected = this.#checkIfPortIsOpen(this.__internal__.serial.port);\n if (!prevConnected && connected) {\n this.dispatch(\"serial:connected\");\n this.#connectingChange(false);\n Devices.$dispatchChange(this);\n }\n this.__internal__.serial.connected = connected;\n return !this.__internal__.serial.connected;\n }\n\n get deviceNumber(): number {\n return this.__internal__.device_number;\n }\n\n get uuid(): string {\n return this.__internal__.device.id;\n }\n\n get typeDevice(): string {\n return this.__internal__.device.type;\n }\n\n get queue(): QueueData[] {\n return this.__internal__.serial.queue;\n }\n\n get responseDelimited(): boolean {\n return this.__internal__.serial.response.delimited;\n }\n\n set responseDelimited(value: boolean) {\n if (typeof value !== \"boolean\") {\n throw new Error(\"responseDelimited must be a boolean\");\n }\n this.__internal__.serial.response.delimited = value;\n }\n\n get responsePrefixLimited(): boolean {\n return this.__internal__.serial.response.prefixLimiter;\n }\n\n set responsePrefixLimited(value: boolean) {\n if (typeof value !== \"boolean\") {\n throw new Error(\"responsePrefixLimited must be a boolean\");\n }\n this.__internal__.serial.response.prefixLimiter = value;\n }\n\n get responseSufixLimited(): boolean {\n return this.__internal__.serial.response.sufixLimiter;\n }\n\n set responseSufixLimited(value: boolean) {\n if (typeof value !== \"boolean\") {\n throw new Error(\"responseSufixLimited must be a boolean\");\n }\n this.__internal__.serial.response.sufixLimiter = value;\n }\n\n get responseLimiter(): string | RegExp | null {\n return this.__internal__.serial.response.limiter;\n }\n\n set responseLimiter(limiter: string | RegExp | null) {\n if (typeof limiter !== \"string\" && !(limiter instanceof RegExp)) {\n throw new Error(\"responseLimiter must be a string or a RegExp\");\n }\n\n this.__internal__.serial.response.limiter = limiter;\n }\n\n get fixedBytesMessage(): number | null {\n return this.__internal__.serial.response.length;\n }\n\n set fixedBytesMessage(length: number | null) {\n if (length !== null && (typeof length !== \"number\" || length < 1)) {\n throw new Error(\"Invalid length for fixed bytes message\");\n }\n this.__internal__.serial.response.length = length;\n }\n\n get timeoutBeforeResponseBytes(): number {\n return this.__internal__.serial.free_timeout_ms || 50;\n }\n\n set timeoutBeforeResponseBytes(value: number) {\n if (value !== undefined && (typeof value !== \"number\" || value < 1)) {\n throw new Error(\"Invalid timeout for response bytes\");\n }\n this.__internal__.serial.free_timeout_ms = value ?? 50;\n }\n\n get bypassSerialBytesConnection(): boolean {\n return this.__internal__.bypassSerialBytesConnection;\n }\n\n set bypassSerialBytesConnection(value: boolean) {\n if (typeof value !== \"boolean\") {\n throw new Error(\"bypassSerialBytesConnection must be a boolean\");\n }\n this.__internal__.bypassSerialBytesConnection = value;\n }\n\n get useSocket(): boolean {\n return this.__internal__.serial.socket;\n }\n\n get connectionBytes(): Uint8Array {\n const bytes = this.__internal__.serial.bytes_connection;\n\n if (bytes instanceof Uint8Array) {\n return bytes;\n }\n\n if (typeof bytes === \"string\") {\n return this.stringArrayToUint8Array(this.parseStringToBytes(bytes, \"\"));\n }\n\n if (Array.isArray(bytes) && typeof bytes[0] === \"string\") {\n return this.stringArrayToUint8Array(bytes as string[]);\n }\n\n if (Array.isArray(bytes) && typeof bytes[0] === \"number\") {\n return new Uint8Array(bytes as number[]);\n }\n\n return new Uint8Array([]);\n }\n\n set portPath(path: string | null) {\n if (this.isConnected) throw new Error(\"Cannot change port path while connected\");\n if (typeof path !== \"string\" && path !== null) {\n throw new TypeError(\"vendorId must be string or null\");\n }\n this.__internal__.serial.portInfo.path = path;\n }\n\n get portPath(): string | null {\n return this.__internal__.serial.portInfo.path;\n }\n\n set portVendorId(vendorId: number | string | null) {\n if (this.isConnected) throw new Error(\"Cannot change port vendorId while connected\");\n if (typeof vendorId! == \"number\" && typeof vendorId !== \"string\" && vendorId !== null) {\n throw new TypeError(\"vendorId must be a number, string or null\");\n }\n this.__internal__.serial.portInfo.vendorId = vendorId;\n }\n\n get portVendorId(): number | string | null {\n return this.__internal__.serial.portInfo.vendorId;\n }\n\n set portProductId(productId: number | string | null) {\n if (this.isConnected) throw new Error(\"Cannot change port productId while connected\");\n if (typeof productId! == \"number\" && typeof productId !== \"string\" && productId !== null) {\n throw new TypeError(\"productId must be a number, string or null\");\n }\n this.__internal__.serial.portInfo.productId = productId;\n }\n\n get portProductId(): number | string | null {\n return this.__internal__.serial.portInfo.productId;\n }\n\n set socketPortParser(string: \"byte-length\" | \"inter-byte-timeout\") {\n if ([\"byte-length\", \"inter-byte-timeout\"].includes(string)) {\n throw new TypeError(\"socketPortParser must be a string, either 'byte-length' or 'inter-byte-timeout'\");\n }\n this.__internal__.serial.portInfo.parser.name = string;\n }\n\n get socketPortParser(): \"byte-length\" | \"inter-byte-timeout\" {\n return this.__internal__.serial.portInfo.parser.name;\n }\n\n set socketPortParserInterval(value: number) {\n if (typeof value !== \"number\" || value < 1) {\n throw new TypeError(\"Interval must be a positive number\");\n }\n\n this.__internal__.serial.portInfo.parser.interval = value;\n }\n\n get socketPortParserInterval(): number {\n return this.__internal__.serial.portInfo.parser.interval || 50;\n }\n\n set socketPortParserLength(value: number) {\n if (typeof value !== \"number\" || value < 1) {\n throw new TypeError(\"Length must be a positive number or null\");\n }\n this.__internal__.serial.portInfo.parser.length = value;\n }\n\n get socketPortParserLength(): number {\n return this.__internal__.serial.portInfo.parser.length || 14;\n }\n\n get parserForSocket() {\n if (this.socketPortParser === \"byte-length\") {\n return {\n name: this.socketPortParser,\n length: this.socketPortParserLength,\n };\n }\n return {\n name: this.socketPortParser,\n interval: this.socketPortParserInterval,\n };\n }\n\n get configDeviceSocket(): object {\n return {\n uuid: this.uuid,\n name: this.typeDevice,\n deviceNumber: this.deviceNumber,\n connectionBytes: Array.from(this.connectionBytes),\n config: {\n baudRate: this.__internal__.serial.config_port.baudRate,\n dataBits: this.__internal__.serial.config_port.dataBits,\n stopBits: this.__internal__.serial.config_port.stopBits,\n parity: this.__internal__.serial.config_port.parity,\n bufferSize: this.__internal__.serial.config_port.bufferSize,\n flowControl: this.__internal__.serial.config_port.flowControl,\n },\n info: {\n vendorId: this.portVendorId, // vendor ID or null for auto-detect\n productId: this.portProductId, // product ID or null for auto-detect\n portName: this.portPath, // COM3, /dev/ttyUSB0, etc. null for auto-detect\n },\n response: {\n automatic: this.__internal__.auto_response, // true to auto-respond to commands this only for devices that doesn't respond nothing\n autoResponse: this.__internal__.serial.auto_response, // null or data to respond automatically, ie. [0x02, 0x06, 0xdd, 0xdd, 0xf0, 0xcf, 0x03] for relay\n parser: this.parserForSocket,\n timeout: {\n general: this.__internal__.time.response_general,\n engines: this.__internal__.time.response_engines,\n connection: this.__internal__.time.response_connection,\n },\n },\n };\n }\n\n #checkIfPortIsOpen(port: SerialPort | null): boolean {\n if (this.useSocket) {\n return this.__internal__.serial.connected && Socket.isConnected();\n }\n\n return !!(port && port.readable && port.writable);\n }\n\n public async timeout(bytes: string | Uint8Array | Array<string> | Array<number>, event: string): Promise<void> {\n this.__internal__.last_error.message = \"Operation response timed out.\";\n this.__internal__.last_error.action = event;\n this.__internal__.last_error.code = bytes;\n if (this.__internal__.timeout.until_response) {\n clearTimeout(this.__internal__.timeout.until_response);\n this.__internal__.timeout.until_response = 0;\n }\n if (event === \"connect\") {\n this.__internal__.serial.connected = false;\n this.dispatch(\"serial:reconnect\", {});\n Devices.$dispatchChange(this);\n } else if (event === \"connection:start\") {\n await this.serialDisconnect();\n this.__internal__.serial.connected = false;\n this.__internal__.aux_port_connector += 1;\n Devices.$dispatchChange(this);\n await this.serialConnect();\n }\n\n if (this.__internal__.serial.queue.length > 0) {\n this.dispatch(\"internal:queue\", {});\n }\n\n this.dispatch(\"serial:timeout\", {\n ...this.__internal__.last_error,\n bytes,\n action: event,\n });\n }\n\n public async disconnect(detail = null): Promise<void> {\n await this.serialDisconnect();\n this.#disconnected(detail);\n }\n\n #disconnected(detail: object | null = null): void {\n this.__internal__.serial.connected = false;\n this.__internal__.aux_port_connector = 0;\n this.dispatch(\"serial:disconnected\", detail);\n Devices.$dispatchChange(this);\n }\n\n #onFinishConnecting(event: any): void {\n this.__internal__.serial.aux_connecting = event.detail.active ? \"connecting\" : \"finished\";\n }\n\n socketResponse(data: any) {\n const auxPrevConnected: boolean = this.__internal__.serial.connected;\n\n if (data.type === \"disconnect\" || (data.type === \"error\" && data.data === \"DISCONNECTED\")) {\n this.__internal__.serial.connected = false;\n } else if (data.type === \"success\") {\n this.__internal__.serial.connected = true;\n }\n\n Devices.$dispatchChange(this);\n if (!auxPrevConnected && this.__internal__.serial.connected) {\n this.dispatch(\"serial:connected\");\n this.#connectingChange(false);\n }\n\n // console.log(data, this.lastAction);\n if (data.type === \"success\") {\n this.#serialGetResponse(new Uint8Array(data.data));\n } else if (data.type === \"error\") {\n const error = new Error(\"The port is closed or is not readable/writable\");\n this.serialErrors(error);\n } else if (data.type === \"timeout\") {\n this.timeout(data.data.bytes ?? [], this.lastAction || \"unknown\");\n }\n\n this.__internal__.serial.last_action = null;\n }\n\n public async connect(): Promise<boolean> {\n if (this.isConnected) {\n return true;\n }\n\n this.__internal__.serial.aux_connecting = \"idle\";\n\n return new Promise((resolve: (value: boolean) => void, reject: (reason: string) => void): void => {\n if (!this.#boundFinishConnecting) {\n this.#boundFinishConnecting = this.#onFinishConnecting.bind(this);\n }\n\n this.on(\"internal:connecting\", this.#boundFinishConnecting);\n\n const interval: ReturnType<typeof setInterval> = setInterval((): void => {\n if (this.__internal__.serial.aux_connecting === \"finished\") {\n clearInterval(interval);\n this.__internal__.serial.aux_connecting = \"idle\";\n if (null !== this.#boundFinishConnecting) {\n this.off(\"internal:connecting\", this.#boundFinishConnecting);\n }\n\n if (this.isConnected) {\n resolve(true);\n } else {\n reject(`${this.typeDevice} device ${this.deviceNumber} not connected`);\n }\n } else if (this.__internal__.serial.aux_connecting === \"connecting\") {\n this.__internal__.serial.aux_connecting = \"idle\";\n this.dispatch(\"internal:connecting\", { active: true });\n this.dispatch(\"serial:connecting\", { active: true });\n }\n }, 100);\n\n this.serialConnect();\n });\n }\n\n public async serialDisconnect(): Promise<void> {\n try {\n if (this.useSocket) {\n if (Socket.isConnected()) {\n Socket.disconnectDevice(this.configDeviceSocket);\n }\n } else {\n const reader: ReadableStreamDefaultReader<Uint8Array> | null = this.__internal__.serial.reader;\n const output_stream: WritableStream<Uint8Array> | null = this.__internal__.serial.output_stream;\n if (reader) {\n const reader_promise: Promise<void> = reader.cancel();\n await reader_promise.catch((err: unknown): void => this.serialErrors(err));\n await this.__internal__.serial.input_done;\n }\n\n if (output_stream) {\n await output_stream.getWriter().close();\n await this.__internal__.serial.output_done;\n }\n\n if (this.__internal__.serial.connected && this.__internal__.serial && this.__internal__.serial.port) {\n await this.__internal__.serial.port.close();\n }\n }\n } catch (err: unknown) {\n this.serialErrors(err);\n } finally {\n this.__internal__.serial.reader = null;\n this.__internal__.serial.input_done = null;\n\n this.__internal__.serial.output_stream = null;\n this.__internal__.serial.output_done = null;\n\n this.__internal__.serial.connected = false;\n this.__internal__.serial.port = null;\n Devices.$dispatchChange(this);\n }\n }\n\n async #serialSocketWrite(data: string | Uint8Array | Array<string> | Array<number>): Promise<void> {\n if (Socket.isDisconnected()) {\n this.#disconnected({ error: \"Socket is disconnected.\" });\n throw new Error(\"The socket is disconnected\");\n }\n\n if (this.isDisconnected) {\n this.#disconnected({ error: \"Port is closed, not readable or writable.\" });\n throw new Error(\"The port is closed or is not readable/writable\");\n }\n\n const bytes: Uint8Array = this.validateBytes(data);\n Socket.write({ config: this.configDeviceSocket, bytes: Array.from(bytes) });\n }\n\n async #serialWrite(data: string | Uint8Array | Array<string> | Array<number>): Promise<void> {\n if (this.useSocket) {\n await this.#serialSocketWrite(data);\n return;\n }\n const port: SerialPort | null = this.__internal__.serial.port;\n if (!port || (port && (!port.readable || !port.writable))) {\n this.#disconnected({ error: \"Port is closed, not readable or writable.\" });\n throw new Error(\"The port is closed or is not readable/writable\");\n }\n const bytes: Uint8Array = this.validateBytes(data);\n\n if (this.useRTSCTS) {\n await this.#waitForCTS(port, 5000);\n }\n\n if (port.writable === null) return; // never happens, it's already checked, but to suppress TS error\n const writer: WritableStreamDefaultWriter<Uint8Array> = port.writable.getWriter();\n await writer.write(bytes);\n writer.releaseLock();\n }\n\n async #waitForCTS(port: SerialPort, timeoutMs: number = 5000): Promise<void> {\n const start = Date.now();\n while (true) {\n if (Date.now() - start > timeoutMs) {\n throw new Error(\"Timeout waiting for clearToSend signal\");\n }\n\n const { clearToSend } = await port.getSignals();\n if (clearToSend) return;\n await wait(100);\n }\n }\n\n #serialGetResponse(code: Uint8Array = new Uint8Array([]), corrupt: boolean = false) {\n if (code && code.length > 0) {\n const auxPrevConnected: boolean = this.__internal__.serial.connected;\n this.__internal__.serial.connected = this.#checkIfPortIsOpen(this.__internal__.serial.port);\n Devices.$dispatchChange(this);\n if (!auxPrevConnected && this.__internal__.serial.connected) {\n this.dispatch(\"serial:connected\");\n this.#connectingChange(false);\n }\n\n if (this.__internal__.interval.reconnection) {\n clearInterval(this.__internal__.interval.reconnection);\n this.__internal__.interval.reconnection = 0;\n }\n\n if (this.__internal__.timeout.until_response) {\n clearTimeout(this.__internal__.timeout.until_response);\n this.__internal__.timeout.until_response = 0;\n }\n\n if (this.__internal__.serial.response.as === \"hex\") {\n if (corrupt) {\n this.serialCorruptMessage(this.parseUint8ToHex(code));\n } else {\n this.serialMessage(this.parseUint8ToHex(code));\n }\n } else if (this.__internal__.serial.response.as === \"uint8\") {\n if (corrupt) {\n this.serialCorruptMessage(code);\n } else {\n this.serialMessage(code);\n }\n } else if (this.__internal__.serial.response.as === \"string\") {\n const str = this.parseUint8ArrayToString(code);\n if (this.__internal__.serial.response.limiter !== null) {\n const splited = str.split(this.__internal__.serial.response.limiter);\n for (const s in splited) {\n if (!splited[s]) continue;\n if (corrupt) {\n this.serialCorruptMessage(splited[s]);\n } else {\n this.serialMessage(splited[s]);\n }\n }\n } else {\n if (corrupt) {\n this.serialCorruptMessage(str);\n } else {\n this.serialMessage(str);\n }\n }\n } else {\n const arraybuffer: ArrayBuffer | ArrayBufferLike = this.stringToArrayBuffer(this.parseUint8ArrayToString(code));\n if (corrupt) {\n this.serialCorruptMessage(arraybuffer as ArrayBuffer);\n } else {\n this.serialMessage(arraybuffer as ArrayBuffer);\n }\n }\n }\n\n if (this.__internal__.serial.queue.length === 0) {\n this.__internal__.serial.running_queue = false;\n return;\n }\n this.dispatch(\"internal:queue\", {});\n }\n\n public getResponseAsArrayBuffer(): void {\n this.__internal__.serial.response.as = \"arraybuffer\";\n }\n\n public getResponseAsArrayHex(): void {\n this.__internal__.serial.response.as = \"hex\";\n }\n\n public getResponseAsUint8Array(): void {\n this.__internal__.serial.response.as = \"uint8\";\n }\n\n public getResponseAsString(): void {\n this.__internal__.serial.response.as = \"string\";\n }\n\n async #serialPortsFiltered(): Promise<SerialPort[]> {\n const filters: SerialPortFilter[] = this.serialFilters;\n // @ts-expect-error getPorts can use parameters\n const ports: SerialPort[] = await navigator.serial.getPorts({ filters });\n if (filters.length === 0) return ports;\n\n const filteredPorts: SerialPort[] = ports.filter((port: SerialPort): boolean => {\n const info: SerialPortInfo = port.getInfo();\n return filters.some((filter: SerialPortFilter): boolean => {\n return info.usbProductId === filter.usbProductId && info.usbVendorId === filter.usbVendorId;\n });\n });\n\n // return only ports that are not open\n return filteredPorts.filter((port: SerialPort): boolean => !this.#checkIfPortIsOpen(port));\n }\n\n public async serialPortsSaved(ports: SerialPort[]): Promise<void> {\n const filters: SerialPortFilter[] = this.serialFilters;\n if (this.__internal__.aux_port_connector < ports.length) {\n const aux = this.__internal__.aux_port_connector;\n this.__internal__.serial.port = ports[aux];\n } else {\n this.__internal__.aux_port_connector = 0;\n this.__internal__.serial.port = await navigator.serial.requestPort({\n filters,\n });\n }\n if (!this.__internal__.serial.port) {\n throw new Error(\"Select another port please\");\n }\n }\n\n public serialErrors(error: any): void {\n const err = error.toString().toLowerCase();\n switch (true) {\n case err.includes(\"must be handling a user gesture to show a permission request\"):\n case err.includes(\"the port is closed.\"):\n case err.includes(\"the port is closed or is not writable\"):\n case err.includes(\"the port is closed or is not readable\"):\n case err.includes(\"the port is closed or is not readable/writable\"):\n case err.includes(\"select another port please\"):\n case err.includes(\"no port selected by the user\"):\n case err.includes(\n \"this readable stream reader has been released and cannot be used to cancel its previous owner stream\",\n ):\n this.dispatch(\"serial:need-permission\", {});\n Devices.$dispatchChange(this);\n break;\n case err.includes(\"the port is already open.\"):\n case err.includes(\"failed to open serial port\"):\n this.serialDisconnect().then(async () => {\n this.__internal__.aux_port_connector += 1;\n await this.serialConnect();\n });\n break;\n case err.includes(\"cannot read properties of undefined (reading 'writable')\"):\n case err.includes(\"cannot read properties of null (reading 'writable')\"):\n case err.includes(\"cannot read property 'writable' of null\"):\n case err.includes(\"cannot read property 'writable' of undefined\"):\n this.serialDisconnect().then(async () => {\n await this.serialConnect();\n });\n break;\n case err.includes(\"'close' on 'serialport': a call to close() is already in progress.\"):\n // ... do something?\n break;\n case err.includes(\"failed to execute 'open' on 'serialport