diff --git a/package-lock.json b/package-lock.json index 2f62914..19cb511 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,15 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "await-to-js": "^3.0.0", "commander": "^12.1.0", "express": "^4.21.1", "fs-extra": "^11.2.0", "lodash": "^4.17.21", + "mitt": "^3.0.1", "serialport": "^12.0.0", "shelljs": "^0.8.5", + "shortid": "^2.2.16", "simple-git": "^3.27.0", "socket.io": "^4.8.1", "usb": "^2.14.0" @@ -270,6 +273,14 @@ "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", @@ -917,11 +928,21 @@ "node": "*" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/nanoid": { + "version": "2.1.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-2.1.11.tgz", + "integrity": "sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", @@ -1235,6 +1256,15 @@ "node": ">=4" } }, + "node_modules/shortid": { + "version": "2.2.16", + "resolved": "https://registry.npmmirror.com/shortid/-/shortid-2.2.16.tgz", + "integrity": "sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dependencies": { + "nanoid": "^2.1.0" + } + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.6.tgz", diff --git a/package.json b/package.json index 7e459a7..1265473 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,15 @@ "author": "Mixly Team", "license": "ISC", "dependencies": { + "await-to-js": "^3.0.0", "commander": "^12.1.0", "express": "^4.21.1", "fs-extra": "^11.2.0", "lodash": "^4.17.21", + "mitt": "^3.0.1", "serialport": "^12.0.0", "shelljs": "^0.8.5", + "shortid": "^2.2.16", "simple-git": "^3.27.0", "socket.io": "^4.8.1", "usb": "^2.14.0" diff --git a/src/common/config.js b/src/common/config.js new file mode 100644 index 0000000..1122cd0 --- /dev/null +++ b/src/common/config.js @@ -0,0 +1 @@ +export const DEBUG = false; \ No newline at end of file diff --git a/src/common/debug.js b/src/common/debug.js new file mode 100644 index 0000000..59dd667 --- /dev/null +++ b/src/common/debug.js @@ -0,0 +1,18 @@ +import { DEBUG } from './config'; + +const Debug = {}; + +for (let key in console) { + if (typeof console[key] !== 'function') { + continue; + } + Debug[key] = (...args) => { + if (DEBUG) { + console[key](...args); + } else { + console.log(`[${key.toUpperCase()}]`, ...args); + } + } +} + +export default Debug; \ No newline at end of file diff --git a/src/common/events-base.js b/src/common/events-base.js new file mode 100644 index 0000000..3bd5e65 --- /dev/null +++ b/src/common/events-base.js @@ -0,0 +1,36 @@ +import Events from './events'; + + +export default class EventsBase { + #events_ = new Events(); + constructor() {} + + bind(type, func) { + return this.#events_.bind(type, func); + } + + unbind(id) { + this.#events_.unbind(id); + } + + addEventsType(eventsType) { + this.#events_.addType(eventsType); + } + + runEvent(eventsType, ...args) { + return this.#events_.run(eventsType, ...args); + } + + offEvent(eventsType) { + this.#events_.off(eventsType); + } + + resetEvent() { + this.#events_.reset(); + } + + disposeEvent() { + this.resetEvent(); + this.#events_ = null; + } +} \ No newline at end of file diff --git a/src/common/events.js b/src/common/events.js new file mode 100644 index 0000000..67d497e --- /dev/null +++ b/src/common/events.js @@ -0,0 +1,86 @@ +// import mitt from 'mitt'; +import _ from 'lodash'; +import shortid from 'shortid'; +import Debug from './debug'; +import Registry from './registry'; + + +export default class Events { + #eventsType_ = []; + #events_ = new Registry(); + + constructor(eventsType = []) { + this.#eventsType_ = eventsType; + } + + addType(eventsType) { + this.#eventsType_ = _.uniq(_.concat([this.#eventsType_, eventsType])); + } + + exist(type) { + if (!this.#eventsType_.includes(type)) { + Debug.warn(`${type} event does not exist under the class`); + return false; + } + return true; + } + + bind(type, func) { + if (!this.exist(type)) { + return this; + } + const id = shortid.generate(); + let typeEvent = this.#events_.getItem(type); + if (!typeEvent) { + typeEvent = new Registry(); + this.#events_.register(type, typeEvent); + } + typeEvent.register(id, func); + return id; + } + + unbind(id) { + for (let [_, value] of this.#events_.getAllItems()) { + let typeEvent = value; + if (!typeEvent.getItem(id)) { + continue; + } + typeEvent.unregister(id); + } + return this; + } + + off(type) { + if (this.#events_.getItem(type)) { + this.#events_.unregister(type); + } + return this; + } + + run(type, ...args) { + let outputs = []; + if (!this.exist(type)) { + return outputs; + } + const eventsFunc = this.#events_.getItem(type); + if (!eventsFunc) { + return outputs; + } + for (let [_, func] of eventsFunc.getAllItems()) { + outputs.push(func(...args)); + } + return outputs; + } + + reset() { + this.#events_.reset(); + } + + length(type) { + const typeEvent = this.#events_.getItem(type); + if (typeEvent) { + return typeEvent.length(); + } + return 0; + } +} \ No newline at end of file diff --git a/src/common/registry.js b/src/common/registry.js new file mode 100644 index 0000000..7e21966 --- /dev/null +++ b/src/common/registry.js @@ -0,0 +1,60 @@ +export default class Registry { + #registry_ = new Map(); + + constructor() { + this.reset(); + } + + reset() { + this.#registry_.clear(); + } + + validate(keys) { + if (!(keys instanceof Array)) { + keys = [keys]; + } + return keys; + } + + register(keys, value) { + keys = this.validate(keys); + for (let key of keys) { + if (this.#registry_.has(key)) { + Debug.warn(`${key}已存在,不可重复注册`); + continue; + } + this.#registry_.set(key, value); + } + } + + unregister(keys) { + keys = this.validate(keys); + for (let key of keys) { + if (!this.#registry_.has(key)) { + Debug.warn(`${key}不存在,无需取消注册`); + continue; + } + this.#registry_.delete(key); + } + } + + length() { + return this.#registry_.size; + } + + hasKey(key) { + return this.#registry_.has(key); + } + + keys() { + return [...this.#registry_.keys()]; + } + + getItem(key) { + return this.#registry_.get(key) ?? null; + } + + getAllItems() { + return this.#registry_; + } +} \ No newline at end of file diff --git a/src/common/serial.js b/src/common/serial.js new file mode 100644 index 0000000..6772c3b --- /dev/null +++ b/src/common/serial.js @@ -0,0 +1,171 @@ +import os from 'node:os'; +import { ChildProcess } from 'node:child_process'; +import { + SerialPort, + ReadlineParser, + ByteLengthParser +} from 'serialport'; +import EventsBase from './events-base'; + + +export default class Serial extends EventsBase { + static { + this.portsName = []; + + this.getCurrentPortsName = function () { + return this.portsName; + } + + this.getPorts = async function () { + return new Promise((resolve, reject) => { + if (os.platform() === 'linux') { + ChildProcess.exec('ls /dev/ttyACM* /dev/tty*USB*', (_, stdout, stderr) => { + let portsName = MArray.unique(stdout.split('\n')); + let newPorts = []; + for (let i = 0; i < portsName.length; i++) { + if (!portsName[i]) { + continue; + } + newPorts.push({ + vendorId: 'None', + productId: 'None', + name: portsName[i] + }); + } + resolve(newPorts); + }); + } else { + SerialPort.list().then(ports => { + let newPorts = []; + for (let i = 0; i < ports.length; i++) { + let port = ports[i]; + newPorts.push({ + vendorId: port.vendorId, + productId: port.productId, + name: port.path + }); + } + resolve(newPorts); + }).catch(reject); + } + }); + } + } + + #serialport_ = null; + #parserBytes_ = null; + #parserLine_ = null; + #port_ = null; + + constructor(port) { + this.#port_ = port; + this.addEventsType(['buffer', 'String', 'error', 'open', 'close']); + } + + #addEventsListener_() { + this.#parserBytes_.on('data', (buffer) => { + this.runEvent('buffer', buffer); + }); + + this.#parserLine_.on('data', (str) => { + this.runEvent('String', str); + }); + + this.#serialport_.on('error', (error) => { + this.runEvent('error', error); + }); + + this.#serialport_.on('open', () => { + this.runEvent('open'); + }); + + this.#serialport_.on('close', () => { + this.runEvent('close'); + }); + } + + getPortName() { + return this.#port_; + } + + async open(baud) { + return new Promise((resolve, reject) => { + this.#serialport_ = new SerialPort({ + path: this.getPortName(), + baudRate: baud, // 波特率 + dataBits: 8, // 数据位 + parity: 'none', // 奇偶校验 + stopBits: 1, // 停止位 + flowControl: false, + autoOpen: false // 不自动打开 + }, false); + this.#parserBytes_ = this.#serialport_.pipe(new ByteLengthParser({ length: 1 })); + this.#parserLine_ = this.#serialport_.pipe(new ReadlineParser()); + this.#serialport_.open((error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + this.#addEventsListener_(); + }); + } + + async close() { + return new Promise((resolve, reject) => { + this.#serialport_.close((error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); + } + + async setBaudRate(baud) { + return new Promise((resolve, reject) => { + this.#serialport_.update({ baudRate: baud }, (error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + } + + async send(data) { + return new Promise((resolve, reject) => { + this.#serialport_.write(data, (error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); + } + + async setDTRAndRTS(dtr, rts) { + return new Promise((resolve, reject) => { + this.#serialport_.set({ dtr, rts }, (error) => { + if (error) { + reject(error); + } else { + super.setDTRAndRTS(dtr, rts); + resolve(); + } + }); + }); + } + + dispose() { + this.disposeEvent(); + this.#serialport_ = null; + this.#parserBytes_ = null; + this.#parserLine_ = null; + this.#port_ = null; + } +} \ No newline at end of file diff --git a/src/web-socket/socket.js b/src/web-socket/socket.js new file mode 100644 index 0000000..ad702ee --- /dev/null +++ b/src/web-socket/socket.js @@ -0,0 +1,23 @@ +import { Server } from 'socket.io'; +import to from 'await-to-js'; +import Serial from '../common/serial'; +import Debug from '../common/debug'; + + +export default class Socket { + #io_ = null; + constructor(httpsServer, options) { + this.#io_ = new Server(httpsServer, options); + this.#io_.on('connection', (socket) => { + this.#addEventsListener_(socket); + }); + } + + #addEventsListener_(socket) { + socket.on('serial/get-ports', async () => { + const [error, result] = await to(Serial.getPorts()); + error && Debug.error(error); + return result; + }); + } +} \ No newline at end of file