diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e9ffec --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules/ +/index.js diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..64dee65 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Kohei Watanabe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c0c6fe4 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# node-web-serial + +Serial communication with Node.js + +## Document + +[Web Serial API](https://wicg.github.io/serial/) diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..7e35cd1 --- /dev/null +++ b/index.ts @@ -0,0 +1,176 @@ +import { EventEmitter } from "events"; +import * as SerialPortStream from "serialport"; + +// TODO: stream/web 使う https://nodejs.org/en/blog/release/v16.5.0/#experimental-web-streams-api +import { Writable, Readable } from "stream"; + +type ConnectEventHandler = () => {}; +type DisconnectEventHandler = () => {}; + +interface SerialPortMetadata { + /** Serial Number */ + serialNumber?: string; + /** Manufacturer */ + manufacturer?: string; + /** Location ID */ + locationId?: number | string; + /** Vendor ID */ + vendorId?: number | string; + /** Vendor */ + vendor?: string; + /** Product ID */ + productId?: number | string; + /** Product */ + product?: string; + /** PNP ID. Different from Web Serial API specification. */ + pnpId?: string; + /** Path. Different from Web Serial API specification. */ + path?: string; +} + +export interface SerialPortRequestOptions { + filters?: Array; +} + +export interface SerialPortInfo extends Map { +} + +interface Parity { + /** No parity bit is sent for each data word. */ + none: never; + /** Data word plus parity bit has even parity. */ + even: never; + /** Data word plus parity bit has odd parity. */ + odd: never; + /** Parity bit has a mark symbol (logical one). */ + mark: never; + /** Parity bit has a space symbol (logical zero). */ + space: never; +} + +type ParityType = keyof Parity; + +export interface SerialOptions { + baudrate?: + | 115200 + | 57600 + | 38400 + | 19200 + | 9600 + | 4800 + | 2400 + | 1800 + | 1200 + | 600 + | 300 + | 200 + | 150 + | 134 + | 110 + | 75 + | 50; + databits?: 8 | 7 | 6 | 5; + stopbits?: 1 | 2; + parity?: ParityType; + buffersize?: number; + rtscts?: boolean; + xon?: boolean; + xoff?: boolean; + xany?: boolean; +} + +export class SerialPort { + #portInfo: SerialPortStream.PortInfo + #port?: SerialPortStream; + + constructor(portInfo: SerialPortStream.PortInfo) { + this.#portInfo = portInfo; + } + + getInfo(): SerialPortInfo { + return new Map(Object.entries(this.#portInfo)); + } + + async open(options: SerialOptions): Promise { + return new Promise((resolve, reject) => { + this.#port = new SerialPortStream(this.#portInfo.path, options, (error) => { + if (error) return reject(error); + + return resolve(); + }) + }) + } + + get in(): Writable { + + } + + get out(): Readable { + return this.#port?.readable + } +} + +const filterKeys: Array< + & keyof SerialPortMetadata + & keyof SerialPortStream.PortInfo +> = [ + "serialNumber", + "manufacturer", + "locationId", + "vendorId", + "productId", + "pnpId", + "path" + ]; + +const matchPort = (filters: SerialPortRequestOptions["filters"]) => ( + portInfo: SerialPortStream.PortInfo +) => { + if (filters == null) return true; + + return filters.some(filter => + filterKeys + .flatMap(key => filter[key] == null ? [] : [[filter[key], portInfo[key]]]) + .every(([param, info]) => { + if (typeof param === "number") { + return info === param.toString(16); + } + + return info === param; + }) + ); +}; + +export class Serial extends EventEmitter { + onconnect?: ConnectEventHandler; + ondisconnect?: DisconnectEventHandler; + #ports: Array; + + constructor() { + super(); + this.on("connect", () => { + if (this.onconnect != null) this.onconnect(); + }); + this.on("disconnect", () => { + if (this.ondisconnect != null) this.ondisconnect(); + }); + this.#ports = [] + } + + async getPorts(): Promise> { + return this.#ports; + } + + async requestPort({ filters }: SerialPortRequestOptions = {}): Promise { + const portInfo = await SerialPortStream.list(); + const ports = portInfo.filter(matchPort(filters)); + + if (ports.length === 0) { + throw new Error("Serial port not found."); + } + + const port = new SerialPort(ports[0]); + this.#ports = [...this.#ports, port] + return port; + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5714ab0 --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "node-web-serial", + "version": "0.0.1", + "description": "Serial communication with Node.js", + "main": "index.js", + "type": "module", + "files": [ + "index.*" + ], + "repository": { + "type": "git", + "url": "https://github.com/chirimen-oh/node-web-serial.git" + }, + "author": "Kohei Watanabe ", + "license": "MIT", + "dependencies": { + "@types/node": "^16", + "@types/serialport": "^8.0.2", + "serialport": "^9.2.0" + }, + "devDependencies": { + "typedoc": "^0.21.4", + "typescript": "^4.3.5" + }, + "scripts": { + "build": "tsc", + "docs": "", + "prepare": "npm run build" + }, + "keywords": [ + "com port", + "hardware", + "iot", + "modem", + "robot", + "robotics", + "robots", + "serial port", + "serial", + "serialport", + "tty", + "uart" + ], + "engines": { + "node": ">=16.5.0" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ea1fa3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2021", + "moduleResolution": "node", + "declaration": true, + "strict": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true + } +}