diff --git a/.eslintrc.js b/.eslintrc.js index 2d5b111..caedf1f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,7 +16,7 @@ module.exports = { "no-else-return": 2, // 如果if语句里面有return,后面不能跟else语句 "no-empty": 2, // 块语句中的内容不能为空 "no-var": 0, // 不能使用 var 定义变量 - "indent": [2, 4], // 缩进风格 + "indent": [2, 4, { SwitchCase: 1 }], // 缩进风格 "strict": 2, "use-isnan": 2, "no-redeclare": 2, // 禁止重复声明变量 diff --git a/package-lock.json b/package-lock.json index 832518f..9aa1a72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,10 @@ "": { "name": "mixly3-server", "version": "1.0.0", - "license": "ISC", + "license": "MIT", "dependencies": { "await-to-js": "^3.0.0", + "better-sse": "^0.14.1", "commander": "^12.1.0", "express": "^4.21.1", "fs-extra": "^11.2.0", @@ -20,7 +21,7 @@ "mustache": "^4.2.0", "serialport": "^12.0.0", "shelljs": "^0.8.5", - "shortid": "^2.2.16", + "shortid": "^2.2.17", "simple-git": "^3.27.0", "socket.io": "^4.8.1", "usb": "^2.14.0" @@ -1003,6 +1004,15 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/better-sse": { + "version": "0.14.1", + "resolved": "https://registry.npmmirror.com/better-sse/-/better-sse-0.14.1.tgz", + "integrity": "sha512-htQOrymWPKD/LDkRRlgmqS5+xX3p9/BetUQTRshqtwuSk+aQ63RluzHx0CJpY1AYUadCvUex8J+B6XwkGLkZxw==", + "engines": { + "node": ">=20", + "pnpm": ">=9" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3300,9 +3310,22 @@ } }, "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==" + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } }, "node_modules/natural-compare": { "version": "1.4.0", @@ -4520,12 +4543,12 @@ } }, "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.", + "version": "2.2.17", + "resolved": "https://registry.npmmirror.com/shortid/-/shortid-2.2.17.tgz", + "integrity": "sha512-GpbM3gLF1UUXZvQw6MCyulHkWbRseNO4cyBEZresZRorwl1+SLu1ZdqgVtuwqz8mB6RpwPkm541mYSqrKyJSaA==", + "license": "MIT", "dependencies": { - "nanoid": "^2.1.0" + "nanoid": "^3.3.8" } }, "node_modules/side-channel": { diff --git a/package.json b/package.json index 127950e..ae7a12f 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,10 @@ "url": "https://gitee.com/bnu_mixly/mixly3-server.git" }, "author": "Mixly Team", - "license": "ISC", + "license": "MIT", "dependencies": { "await-to-js": "^3.0.0", + "better-sse": "^0.14.1", "commander": "^12.1.0", "express": "^4.21.1", "fs-extra": "^11.2.0", @@ -28,7 +29,7 @@ "mustache": "^4.2.0", "serialport": "^12.0.0", "shelljs": "^0.8.5", - "shortid": "^2.2.16", + "shortid": "^2.2.17", "simple-git": "^3.27.0", "socket.io": "^4.8.1", "usb": "^2.14.0" diff --git a/src/common/boards.js b/src/common/boards.js new file mode 100644 index 0000000..872f500 --- /dev/null +++ b/src/common/boards.js @@ -0,0 +1,200 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import MString from './mstring'; +import { ARDUINO } from './config'; + + +const Boards = {}; + +Boards.TYPE = { + AVR: [ + "arduino:avr:uno", + "arduino:avr:nano", + "arduino:avr:pro", + "arduino:avr:mega", + "arduino:avr:leonardo" + ], + ESP8266: [ + "esp8266:esp8266:generic", + "esp8266:esp8266:nodemcu", + "esp8266:esp8266:nodemcuv2", + "esp8266:esp8266:d1" + ], + ESP32: [ + "esp32:esp32:esp32", + "esp32:esp32:pico32", + "esp32:esp32:node32s", + "esp32:esp32:nodemcu-32s", + "esp32:esp32:m5stack-fire", + "esp32:esp32:mPython", + "esp32:esp32:esp32cam", + "esp32:esp32:bpi-bit" + ], + ESP32S2: [ + "esp32:esp32:esp32s2" + ], + ESP32S3: [ + "esp32:esp32:esp32s3" + ], + ESP32C3: [ + "esp32:esp32:esp32c3" + ] +}; + +Boards.FIRMWARE_PATH = { + AVR: [ + { + path: '{buildPath}/testArduino.ino.hex' + } + ], + ESP8266: [ + { + path: '{buildPath}/testArduino.ino.bin', + offset: '0x0' + } + ], + ESP32: [ + { + path: '{buildPath}/testArduino.ino.bootloader.bin', + offset: '0x1000' + }, { + path: '{buildPath}/testArduino.ino.partitions.bin', + offset: '0x8000' + }, { + path: '{shellPath}/Arduino15/packages/esp32/hardware/esp32/2.0.15/tools/partitions/boot_app0.bin', + offset: '0xe000' + }, { + path: '{buildPath}/testArduino.ino.bin', + offset: '0x10000' + } + ], + ESP32S2: [ + { + path: '{buildPath}/testArduino.ino.bootloader.bin', + offset: '0x1000' + }, { + path: '{buildPath}/testArduino.ino.partitions.bin', + offset: '0x8000' + }, { + path: '{shellPath}/Arduino15/packages/esp32/hardware/esp32/2.0.5/tools/partitions/boot_app0.bin', + offset: '0xe000' + }, { + path: '{buildPath}/testArduino.ino.bin', + offset: '0x10000' + } + ], + ESP32S3: [ + { + path: '{buildPath}/testArduino.ino.bootloader.bin', + offset: '0x1000' + }, { + path: '{buildPath}/testArduino.ino.partitions.bin', + offset: '0x8000' + }, { + path: '{shellPath}/Arduino15/packages/esp32/hardware/esp32/2.0.5/tools/partitions/boot_app0.bin', + offset: '0xe000' + }, { + path: '{buildPath}/testArduino.ino.bin', + offset: '0x10000' + } + ], + ESP32C3: [ + { + path: '{buildPath}/testArduino.ino.bootloader.bin', + offset: '0x0' + }, { + path: '{buildPath}/testArduino.ino.partitions.bin', + offset: '0x8000' + }, { + path: '{shellPath}/Arduino15/packages/esp32/hardware/esp32/2.0.5/tools/partitions/boot_app0.bin', + offset: '0xe000' + }, { + path: '{buildPath}/testArduino.ino.bin', + offset: '0x10000' + } + ] +} + +Boards.getBoardType = (board) => { + if (typeof board !== 'string') { + return null; + } + const boardParam = board.split(':'); + if (boardParam.length < 3) { + return null; + } + let boardType; + const inBoardType = boardParam[0] + ':' + boardParam[1]; + switch (inBoardType) { + case 'arduino:avr': + boardType = 'AVR'; + break; + case 'esp8266:esp8266': + boardType = 'ESP8266'; + break; + case 'esp32:esp32': + boardType = 'ESP32'; + break; + default: + return null; + } + if (boardType !== 'ESP32') { + return boardType; + } + switch (boardParam[2]) { + case 'esp32c3': + boardType = 'ESP32C3'; + break; + case 'esp32s2': + boardType = 'ESP32S2'; + break; + case 'esp32s3': + boardType = 'ESP32S3'; + break; + default: + boardType = 'ESP32'; + } + return boardType; +} + +Boards.getBoardKey = (board) => { + const idxs = []; + for (let i = 0; i < board.length; i++) { + if (board[i] === ':') idxs.push(i); + if (idxs.length === 3) break; + } + return idxs.length === 3 ? board.slice(0, idxs[2]) : board; +} + +Boards.exist = (board) => { + const boardType = Boards.getBoardType(board); + if (!boardType) { + return false; + } + const boardParam = board.split(':'); + const inBoardType = boardParam[0] + ':' + boardParam[1]; + board = inBoardType + ':' + boardParam[2]; + if (Boards.TYPE[boardType] && Boards.TYPE[boardType].includes(board)) { + return true; + } + return false; +} + +Boards.getFiles = async (board, buildPath) => { + const type = Boards.getBoardType(board); + const output = []; + const files = Boards.FIRMWARE_PATH[type]; + for (let file of files) { + const filePath = MString.tpl(file.path, { + buildPath, + shellPath: ARDUINO.path.folder + }); + const data = await fs.promises.readFile(filePath, { + encoding: path.extname(filePath) === '.hex' ? 'utf8' : 'hex' + }); + output.push({ ...file, data }); + } + return output; +} + +export default Boards; \ No newline at end of file diff --git a/src/common/config.js b/src/common/config.js index f5cddc8..0ce9a54 100644 --- a/src/common/config.js +++ b/src/common/config.js @@ -7,6 +7,5 @@ export const ARDUINO = CONFIG.arduino; export const MICROPYTHON = CONFIG.micropython; export const PYTHON = CONFIG.python; export const CURRENT_PLANTFORM = os.platform(); -export const WEB_SOCKT_TEMP_PATH = CONFIG.webSocketTempPath; -export const WEB_COMPILER_TEMP_PATH = CONFIG.webSocketTempPath; +export const TEMP_PATH = CONFIG.tempPath; export const CLIENT_PATH = CONFIG.clientPath; \ No newline at end of file diff --git a/src/common/events-base.js b/src/common/events-base.js index 088e87c..a8bbfb1 100644 --- a/src/common/events-base.js +++ b/src/common/events-base.js @@ -29,7 +29,7 @@ export default class EventsBase { return this.#events_.reset(); } - disposeEvent() { + dispose() { this.resetEvent(); this.#events_ = null; } diff --git a/src/common/id-generator.js b/src/common/id-generator.js new file mode 100644 index 0000000..7af7c37 --- /dev/null +++ b/src/common/id-generator.js @@ -0,0 +1,20 @@ +import shortid from 'shortid'; + + +const IdGenerator = {}; + +IdGenerator.generate = function (input) { + let output = {}; + if (input instanceof Array) { + for (let i of input) { + if (typeof i !== 'string') { + continue; + } + output[i] = shortid.generate(); + } + return output; + } + return shortid.generate(); +} + +export default IdGenerator; \ No newline at end of file diff --git a/src/common/shell.js b/src/common/shell.js index 528db46..dc2b018 100644 --- a/src/common/shell.js +++ b/src/common/shell.js @@ -82,7 +82,10 @@ export default class Shell extends EventsBase { } async kill() { - if (this.#killed_) { + if (this.#killed_ || !this.#shell_) { + return; + } + if (this.#shell_.exitCode !== null) { return; } this.#shell_.stdin.end(); @@ -92,9 +95,15 @@ export default class Shell extends EventsBase { } else { this.#shell_.kill('SIGTERM') } + this.#shell_ = null; } getShell() { return this.#shell_; } + + dispose() { + this.#shell_ = null; + super.dispose(); + } } \ No newline at end of file diff --git a/src/common/socket.js b/src/common/socket.js new file mode 100644 index 0000000..e9b697b --- /dev/null +++ b/src/common/socket.js @@ -0,0 +1,408 @@ +import { Server } from 'socket.io'; +import to from 'await-to-js'; +import { usb } from 'usb'; +import path from 'node:path'; +import fsExtra from 'fs-extra'; +import Serial from './serial'; +import Debug from './debug'; +import Registry from './registry'; +import ShellArduino from './shell-arduino'; +import ShellMicroPython from './shell-micropython'; +import ShellAmpy from './shell-ampy'; +import MString from './mstring'; +import Boards from './boards'; +import { TEMP_PATH, CLIENT_PATH } from './config'; + + +export default class Socket { + #io_ = null; + #namespaceAll_ = null; + #namespaceCompile_ = null; + #serialRegistry_ = new Registry(); + #shellMicroPython_ = new Registry(); + #shellArduino_ = new Registry(); + #shellAmpy_ = new Registry(); + + constructor(httpsServer, options) { + this.#io_ = new Server(httpsServer, options); + this.#namespaceAll_ = this.#io_.of('/all'); + this.#namespaceCompile_ = this.#io_.of('/compile'); + this.#namespaceCompile_.on('connection', (socket) => { + this.#shellArduino_.register(socket.id, new ShellArduino()); + this.#addEventsListenerForMode1_(socket); + }); + this.#namespaceAll_.on('connection', (socket) => { + this.#shellMicroPython_.register(socket.id, new ShellMicroPython()); + this.#shellArduino_.register(socket.id, new ShellArduino()); + this.#shellAmpy_.register(socket.id, new ShellAmpy()); + this.#addEventsListenerForMode2_(socket); + }); + usb.on('attach', (device) => { + this.#namespaceAll_.emit('serial.attachEvent', device); + }); + usb.on('detach', (device) => { + this.#namespaceAll_.emit('serial.detachEvent', device); + }); + } + + #addEventsListenerForMode1_(socket) { + socket.on('disconnect', () => { + const folderPath = path.resolve(TEMP_PATH, socket.id); + fsExtra.remove(folderPath).catch(Debug.error); + const shellArduino = this.#shellArduino_.getItem(socket.id); + shellArduino.dispose(); + this.#shellArduino_.unregister(socket.id); + }); + + const shell = this.#shellArduino_.getItem(socket.id); + shell.bind('data', (data) => { + socket.emit('arduino.dataEvent', data); + }); + + shell.bind('error', (data) => { + socket.emit('arduino.errorEvent', data); + }); + + shell.bind('close', (code, time) => { + socket.emit('arduino.closeEvent', code, time); + }); + + socket.on('arduino.compile', async (config, callback) => { + if (!Boards.exist(config.key)) { + callback(['illegal board name', {}]); + return; + } + const shell = this.#shellArduino_.getItem(socket.id); + config.path = config?.path ?? {}; + config.path.build = path.resolve(TEMP_PATH, socket.id, 'build'); + config.path.code = path.resolve(TEMP_PATH, socket.id, 'testArduino/testArduino.ino'); + let [error1,] = await to(fsExtra.ensureDir(config.path.build)); + error1 && Debug.error(error1); + let [error2,] = await to(fsExtra.outputFile(config.path.code, config.code)); + error2 && Debug.error(error2); + const [error, result] = await to(shell.compile(config)); + if (error) { + Debug.error(error); + callback([error, result]); + return; + } + callback([error, result]); + }); + + socket.on('arduino.upload', async (config, callback) => { + if (!Boards.exist(config.key)) { + callback(['illegal board name', {}]); + return; + } + const shell = this.#shellArduino_.getItem(socket.id); + config.path = config?.path ?? {}; + config.path.build = path.resolve(TEMP_PATH, socket.id, 'build'); + config.path.code = path.resolve(TEMP_PATH, socket.id, 'testArduino/testArduino.ino'); + let [error1,] = await to(fsExtra.ensureDir(config.path.build)); + error1 && Debug.error(error1); + let [error2,] = await to(fsExtra.outputFile(config.path.code, config.code)); + error2 && Debug.error(error2); + const [error, result] = await to(shell.compile(config)); + if (error) { + Debug.error(error); + callback([error, result]); + return; + } + if (result.code === 0) { + const buildPath = config.path.build; + result.files = await Boards.getFiles(config.key, buildPath); + } + callback([error, result]); + }); + + socket.on('arduino.kill', async (callback) => { + const shell = this.#shellArduino_.getItem(socket.id); + const [error, result] = await to(shell.kill()); + error && Debug.error(error); + callback([error, result]); + }); + } + + #addEventsListenerForMode2_(socket) { + socket.on('disconnect', () => { + for (let key of this.#serialRegistry_.keys()) { + const serial = this.#serialRegistry_.getItem(key); + serial.dispose().catch(Debug.error); + this.#serialRegistry_.unregister(key); + } + const folderPath = path.resolve(TEMP_PATH, socket.id); + fsExtra.remove(folderPath).catch(Debug.error); + const shellMicropython = this.#shellMicroPython_.getItem(socket.id); + shellMicropython.dispose(); + this.#shellMicroPython_.unregister(socket.id); + const shellArduino = this.#shellArduino_.getItem(socket.id); + shellArduino.dispose(); + this.#shellArduino_.unregister(socket.id); + const shellAmpy = this.#shellAmpy_.getItem(socket.id); + shellAmpy.dispose(); + this.#shellAmpy_.unregister(socket.id); + }); + + this.#addEventsListenerForMode2MicroPython_(socket); + this.#addEventsListenerForMode2Arduino_(socket); + this.#addEventsListenerForMode2Ampy_(socket); + this.#addEventsListenerForMode2Serial_(socket); + } + + #addEventsListenerForMode2MicroPython_(socket) { + const shell = this.#shellMicroPython_.getItem(socket.id); + shell.bind('data', (data) => { + socket.emit('arduino.dataEvent', data); + }); + + shell.bind('error', (data) => { + socket.emit('arduino.errorEvent', data); + }); + + shell.bind('close', (code, time) => { + socket.emit('arduino.closeEvent', code, time); + }); + + socket.on('micropython.burn', async (config, callback) => { + const shell = this.#shellMicroPython_.getItem(socket.id); + const [error, result] = await to(shell.burn(config)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('micropython.upload', async (config, callback) => { + const shell = this.#shellMicroPython_.getItem(socket.id); + let { filePath = '', libraries = {} } = config; + filePath = MString.tpl(filePath, { + indexPath: path.resolve(CLIENT_PATH, config.boardDirPath) + }); + const dirname = path.dirname(filePath); + await to(fsExtra.ensureDir(dirname)); + await to(fsExtra.emptyDir(dirname)); + await to(fsExtra.outputFile(filePath, config.code)); + if (libraries && libraries instanceof Object) { + for (let key in libraries) { + await to(fsExtra.outputFile(path.resolve(dirname, key), libraries[key])); + } + } + const [error, result] = await to(shell.upload(config)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('micropython.kill', async (callback) => { + const shell = this.#shellMicroPython_.getItem(socket.id); + const [error, result] = await to(shell.kill()); + error && Debug.error(error); + callback([error, result]); + }); + } + + #addEventsListenerForMode2Arduino_(socket) { + const shell = this.#shellArduino_.getItem(socket.id); + shell.bind('data', (data) => { + socket.emit('arduino.dataEvent', data); + }); + + shell.bind('error', (data) => { + socket.emit('arduino.errorEvent', data); + }); + + shell.bind('close', (code, time) => { + socket.emit('arduino.closeEvent', code, time); + }); + + socket.on('arduino.compile', async (config, callback) => { + console.log(config) + const shell = this.#shellArduino_.getItem(socket.id); + config.path = config?.path ?? {}; + config.path.build = path.resolve(TEMP_PATH, socket.id, 'build'); + config.path.code = path.resolve(TEMP_PATH, socket.id, 'testArduino/testArduino.ino'); + let [error1,] = await to(fsExtra.ensureDir(config.path.build)); + error1 && Debug.error(error1); + let [error2,] = await to(fsExtra.outputFile(config.path.code, config.code)); + error2 && Debug.error(error2); + const [error, result] = await to(shell.compile(config)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('arduino.upload', async (config, callback) => { + config.path = config?.path ?? {}; + const shell = this.#shellArduino_.getItem(socket.id); + config.path.build = path.resolve(TEMP_PATH, socket.id, 'build'); + config.path.code = path.resolve(TEMP_PATH, socket.id, 'testArduino/testArduino.ino'); + let [error1,] = await to(fsExtra.ensureDir(config.path.build)); + error1 && Debug.error(error1); + let [error2,] = await to(fsExtra.outputFile(config.path.code, config.code)); + error2 && Debug.error(error2); + const [error, result] = await to(shell.upload(config)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('arduino.kill', async (callback) => { + const shell = this.#shellArduino_.getItem(socket.id); + const [error, result] = await to(shell.kill()); + error && Debug.error(error); + callback([error, result]); + }); + } + + #addEventsListenerForMode2Ampy_(socket) { + socket.on('ampy.ls', async (port, baud, folderPath, callback) => { + const shell = this.#shellAmpy_.getItem(socket.id); + const [error, result] = await to(shell.ls(port, baud, folderPath)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('ampy.get', async (port, baud, filePath, callback) => { + const shell = this.#shellAmpy_.getItem(socket.id); + const [error, result] = await to(shell.get(port, baud, filePath)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('ampy.mkdir', async (port, baud, folderPath, callback) => { + const shell = this.#shellAmpy_.getItem(socket.id); + const [error, result] = await to(shell.mkdir(port, baud, folderPath)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('ampy.mkfile', async (port, baud, filePath, callback) => { + const shell = this.#shellAmpy_.getItem(socket.id); + const [error, result] = await to(shell.mkfile(port, baud, filePath)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('ampy.isdir', async (port, baud, folderPath, callback) => { + const shell = this.#shellAmpy_.getItem(socket.id); + const [error, result] = await to(shell.isdir(port, baud, folderPath)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('ampy.isfile', async (port, baud, filePath, callback) => { + const shell = this.#shellAmpy_.getItem(socket.id); + const [error, result] = await to(shell.isfile(port, baud, filePath)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('ampy.put', async (port, baud, filePath, data, callback) => { + const shell = this.#shellAmpy_.getItem(socket.id); + const startPath = path.join(TEMP_PATH, socket.id, 'ampy/temp').replaceAll('\\', '/'); + const endPath = filePath; + await to(fsExtra.outputFile(startPath, data)); + const [error, result] = await to(shell.put(port, baud, startPath, endPath)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('ampy.rm', async (port, baud, filePath, callback) => { + const shell = this.#shellAmpy_.getItem(socket.id); + const [error, result] = await to(shell.rm(port, baud, filePath)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('ampy.rmdir', async (port, baud, folderPath, callback) => { + const shell = this.#shellAmpy_.getItem(socket.id); + const [error, result] = await to(shell.rmdir(port, baud, folderPath)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('ampy.rename', async (port, baud, oldPath, newPath, callback) => { + const shell = this.#shellAmpy_.getItem(socket.id); + const [error, result] = await to(shell.rm(port, baud, oldPath, newPath)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('ampy.run', async (port, baud, filePath, callback) => { + const shell = this.#shellAmpy_.getItem(socket.id); + const [error, result] = await to(shell.rm(port, baud, filePath)); + error && Debug.error(error); + callback([error, result]); + }); + } + + #addEventsListenerForMode2Serial_(socket) { + socket.on('serial.getPorts', async (callback) => { + const [error, result] = await to(Serial.getPorts()); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('serial.create', (port) => { + const serial = new Serial(port); + this.#serialRegistry_.register(port, serial); + serial.bind('buffer', (buffer) => { + socket.emit('serial.bufferEvent', port, buffer); + }); + serial.bind('string', (str) => { + socket.emit('serial.stringEvent', port, str); + }); + serial.bind('error', (error) => { + socket.emit('serial.errorEvent', port, error); + }); + serial.bind('open', () => { + socket.emit('serial.openEvent', port); + }); + serial.bind('close', (code) => { + socket.emit('serial.closeEvent', port, code); + }); + }); + + socket.on('serial.dispose', async (port, callback) => { + const serial = this.#serialRegistry_.getItem(port); + const [error, result] = await to(serial.dispose()); + error && Debug.error(error); + this.#serialRegistry_.unregister(port); + callback([error, result]); + }); + + socket.on('serial.open', async (port, baud, callback) => { + const serial = this.#serialRegistry_.getItem(port); + const [error, result] = await to(serial.open(baud)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('serial.close', async (port, callback) => { + const serial = this.#serialRegistry_.getItem(port); + const [error, result] = await to(serial.close()); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('serial.setBaudRate', async (port, baud, callback) => { + const serial = this.#serialRegistry_.getItem(port); + const [error, result] = await to(serial.setBaudRate(baud)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('serial.send', async (port, data, callback) => { + const serial = this.#serialRegistry_.getItem(port); + const [error, result] = await to(serial.send(data)); + error && Debug.error(error); + callback([error, result]); + }); + + socket.on('serial.setDTRAndRTS', async (port, dtr, rts, callback) => { + const serial = this.#serialRegistry_.getItem(port); + const [error, result] = await to(serial.setDTRAndRTS(dtr, rts)); + error && Debug.error(error); + callback([error, result]); + }); + } + + getIO() { + return this.#io_; + } +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 0a129db..feb15f3 100644 --- a/src/index.js +++ b/src/index.js @@ -2,10 +2,10 @@ import express from 'express'; import path from 'node:path'; import * as url from 'node:url'; import { readFileSync } from 'node:fs'; -import { createServer } from 'https'; -import Socket from './web-socket/socket'; - +import { createServer } from 'node:https'; +import Socket from './common/socket'; +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; const __dirname = path.dirname(url.fileURLToPath(new URL(import.meta.url))); const app = express(); const httpsServer = createServer({ @@ -13,8 +13,9 @@ const httpsServer = createServer({ cert: readFileSync(path.resolve(__dirname, '../certs/server.crt')) }, app); -new Socket(httpsServer, { +const socket = new Socket(httpsServer, { path: '/mixly-socket/', + maxHttpBufferSize: 1e8, cors: { origin: '*', methods: ['GET', 'POST'], @@ -23,4 +24,21 @@ new Socket(httpsServer, { } }); +const io = socket.getIO(); + +function close() { + io.close(() => { + console.log('Socket服务已关闭'); + process.exit(0); + }); +} + +process.on('SIGINT', () => { + close(); +}); + +process.on('SIGTERM', () => { + close(); +}); + httpsServer.listen(4000); \ No newline at end of file diff --git a/src/user/config.json b/src/user/config.json index b9dc8d3..ce6cb4c 100644 --- a/src/user/config.json +++ b/src/user/config.json @@ -2,27 +2,26 @@ "debug": true, "arduino": { "path": { - "folder": "D:/gitee/mixly/arduino-cli", - "cli": "D:/gitee/mixly/arduino-cli/arduino-cli.exe", + "folder": "D:/gitee/arduino-cli-win32/arduino-cli", + "cli": "D:/gitee/arduino-cli-win32/arduino-cli/arduino-cli.exe", "libraries": [ - "D:/gitee/mixly/arduino-cli/libraries" + "D:/gitee/arduino-cli-win32/arduino-cli/libraries" ], - "cache": "D:/gitee/mixly/arduino-cli/cache", - "config": "D:/gitee/mixly/arduino-cli/arduino-cli.json" + "cache": "D:/gitee/arduino-cli-win32/arduino-cli/cache", + "config": "D:/gitee/arduino-cli-win32/arduino-cli/arduino-cli.json" } }, "micropython": { "path": { - "ampy": "D:/gitee/mixly3-resource/mixly3.0-win32-x64/resources/app/src/tools/python/ampy/cli.py", - "esptool": "D:/gitee/mixly3-resource/mixly3.0-win32-x64/resources/app/src/tools/python/esptool/__init__.py" + "ampy": "D:/gitee/mixly3.0-win32-x64/resources/app/src/tools/python/ampy_main.py", + "esptool": "D:/gitee/mixly3.0-win32-x64/resources/app/src/tools/python/esptool_main.py" } }, "python": { "path": { - "cli": "D:/gitee/mixly3-resource/mixly3.0-win32-x64/mixpyBuild/win_python3/python3.exe" + "cli": "D:/gitee/mixly3.0-win32-x64/mixpyBuild/win_python3/python3.exe" } }, - "webSocketTempPath": "D:/gitee/mixly3-resource/mixly3-server/temp/web-socket", - "webCompilerTempPath": "D:/gitee/mixly3-resource/mixly3-server/temp/web-compiler", - "clientPath": "D:/gitee/mixly3-resource/mixly3.0-win32-x64/resources/app/src" + "tempPath": "D:/gitee/mixly3-server/temp", + "clientPath": "D:/gitee/mixly3.0-win32-x64/resources/app/src" } \ No newline at end of file diff --git a/src/web-socket/socket.js b/src/web-socket/socket.js deleted file mode 100644 index c8fc7ae..0000000 --- a/src/web-socket/socket.js +++ /dev/null @@ -1,287 +0,0 @@ -import { Server } from 'socket.io'; -import to from 'await-to-js'; -import { usb } from 'usb'; -import path from 'node:path'; -import fsExtra from 'fs-extra'; -import Serial from '../common/serial'; -import Debug from '../common/debug'; -import Registry from '../common/registry'; -import ShellArduino from '../common/shell-arduino'; -import ShellMicroPython from '../common/shell-micropython'; -import ShellAmpy from '../common/shell-ampy'; -import MString from '../common/mstring'; -import { WEB_SOCKT_TEMP_PATH, CLIENT_PATH } from '../common/config'; - - -export default class Socket { - #io_ = null; - #serialRegistry_ = new Registry(); - #shellMicroPython_ = new ShellMicroPython(); - #shellArduino_ = new ShellArduino(); - #shellAmpy_ = new ShellAmpy(); - - constructor(httpsServer, options) { - this.#io_ = new Server(httpsServer, options); - this.#io_.on('connection', (socket) => { - this.#addEventsListener_(socket); - }); - usb.on('attach', (device) => { - this.#io_.emit('serial.attachEvent', device); - }); - usb.on('detach', (device) => { - this.#io_.emit('serial.detachEvent', device); - }); - } - - #addEventsListener_(socket) { - socket.on('disconnect', () => { - for (let key of this.#serialRegistry_.keys()) { - const serial = this.#serialRegistry_.getItem(key); - serial.dispose().catch(Debug.error); - this.#serialRegistry_.unregister(key); - } - }); - - this.#addEventsListenerForMicroPython_(socket); - this.#addEventsListenerForArduino_(socket); - this.#addEventsListenerForAmpy_(socket); - this.#addEventsListenerForSerial_(socket); - } - - #addEventsListenerForMicroPython_(socket) { - this.#shellMicroPython_.bind('data', (data) => { - socket.emit('arduino.dataEvent', data); - }); - - this.#shellMicroPython_.bind('error', (data) => { - socket.emit('arduino.errorEvent', data); - }); - - this.#shellMicroPython_.bind('close', (code, time) => { - socket.emit('arduino.closeEvent', code, time); - }); - - socket.on('micropython.burn', async (config, callback) => { - const [error, result] = await to(this.#shellMicroPython_.burn(config)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('micropython.upload', async (config, callback) => { - let { filePath = '', libraries = {} } = config; - filePath = MString.tpl(filePath, { - indexPath: path.resolve(CLIENT_PATH, config.boardDirPath) - }); - const dirname = path.dirname(filePath); - await to(fsExtra.ensureDir(dirname)); - await to(fsExtra.emptyDir(dirname)); - await to(fsExtra.outputFile(filePath, config.code)); - if (libraries && libraries instanceof Object) { - for (let key in libraries) { - await to(fsExtra.outputFile(path.resolve(dirname, key), libraries[key])); - } - } - const [error, result] = await to(this.#shellMicroPython_.upload(config)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('micropython.kill', async (callback) => { - const [error, result] = await to(this.#shellMicroPython_.kill()); - error && Debug.error(error); - callback([error, result]); - }); - } - - #addEventsListenerForArduino_(socket) { - this.#shellArduino_.bind('data', (data) => { - socket.emit('arduino.dataEvent', data); - }); - - this.#shellArduino_.bind('error', (data) => { - socket.emit('arduino.errorEvent', data); - }); - - this.#shellArduino_.bind('close', (code, time) => { - socket.emit('arduino.closeEvent', code, time); - }); - - socket.on('arduino.compile', async (config, callback) => { - config.path = config?.path ?? {}; - config.path.build = path.resolve(WEB_SOCKT_TEMP_PATH, 'build'); - config.path.code = path.resolve(WEB_SOCKT_TEMP_PATH, 'testArduino/testArduino.ino'); - let [error1,] = await to(fsExtra.ensureDir(config.path.build)); - error1 && Debug.error(error1); - let [error2,] = await to(fsExtra.outputFile(config.path.code, config.code)); - error2 && Debug.error(error2); - const [error, result] = await to(this.#shellArduino_.compile(config)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('arduino.upload', async (config, callback) => { - config.path = config?.path ?? {}; - config.path.build = path.resolve(WEB_SOCKT_TEMP_PATH, 'build'); - config.path.code = path.resolve(WEB_SOCKT_TEMP_PATH, 'testArduino/testArduino.ino'); - let [error1,] = await to(fsExtra.ensureDir(config.path.build)); - error1 && Debug.error(error1); - let [error2,] = await to(fsExtra.outputFile(config.path.code, config.code)); - error2 && Debug.error(error2); - const [error, result] = await to(this.#shellArduino_.upload(config)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('arduino.kill', async (callback) => { - const [error, result] = await to(this.#shellArduino_.kill()); - error && Debug.error(error); - callback([error, result]); - }); - } - - #addEventsListenerForAmpy_(socket) { - socket.on('ampy.ls', async (port, baud, folderPath, callback) => { - const [error, result] = await to(this.#shellAmpy_.ls(port, baud, folderPath)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('ampy.get', async (port, baud, filePath, callback) => { - const [error, result] = await to(this.#shellAmpy_.get(port, baud, filePath)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('ampy.mkdir', async (port, baud, folderPath, callback) => { - const [error, result] = await to(this.#shellAmpy_.mkdir(port, baud, folderPath)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('ampy.mkfile', async (port, baud, filePath, callback) => { - const [error, result] = await to(this.#shellAmpy_.mkfile(port, baud, filePath)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('ampy.isdir', async (port, baud, folderPath, callback) => { - const [error, result] = await to(this.#shellAmpy_.isdir(port, baud, folderPath)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('ampy.isfile', async (port, baud, filePath, callback) => { - const [error, result] = await to(this.#shellAmpy_.isfile(port, baud, filePath)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('ampy.put', async (port, baud, filePath, data, callback) => { - const startPath = path.join(WEB_SOCKT_TEMP_PATH, 'ampy/temp').replaceAll('\\', '/'); - const endPath = filePath; - await to(fsExtra.outputFile(startPath, data)); - const [error, result] = await to(this.#shellAmpy_.put(port, baud, startPath, endPath)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('ampy.rm', async (port, baud, filePath, callback) => { - const [error, result] = await to(this.#shellAmpy_.rm(port, baud, filePath)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('ampy.rmdir', async (port, baud, folderPath, callback) => { - const [error, result] = await to(this.#shellAmpy_.rmdir(port, baud, folderPath)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('ampy.rename', async (port, baud, oldPath, newPath, callback) => { - const [error, result] = await to(this.#shellAmpy_.rm(port, baud, oldPath, newPath)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('ampy.run', async (port, baud, filePath, callback) => { - const [error, result] = await to(this.#shellAmpy_.rm(port, baud, filePath)); - error && Debug.error(error); - callback([error, result]); - }); - } - - #addEventsListenerForSerial_(socket) { - socket.on('serial.getPorts', async (callback) => { - const [error, result] = await to(Serial.getPorts()); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('serial.create', (port) => { - const serial = new Serial(port); - this.#serialRegistry_.register(port, serial); - serial.bind('buffer', (buffer) => { - socket.emit('serial.bufferEvent', port, buffer); - }); - serial.bind('string', (str) => { - socket.emit('serial.stringEvent', port, str); - }); - serial.bind('error', (error) => { - socket.emit('serial.errorEvent', port, error); - }); - serial.bind('open', () => { - socket.emit('serial.openEvent', port); - }); - serial.bind('close', (code) => { - socket.emit('serial.closeEvent', port, code); - }); - }); - - socket.on('serial.dispose', async (port, callback) => { - const serial = this.#serialRegistry_.getItem(port); - const [error, result] = await to(serial.dispose()); - error && Debug.error(error); - this.#serialRegistry_.unregister(port); - callback([error, result]); - }); - - socket.on('serial.open', async (port, baud, callback) => { - const serial = this.#serialRegistry_.getItem(port); - const [error, result] = await to(serial.open(baud)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('serial.close', async (port, callback) => { - const serial = this.#serialRegistry_.getItem(port); - const [error, result] = await to(serial.close()); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('serial.setBaudRate', async (port, baud, callback) => { - const serial = this.#serialRegistry_.getItem(port); - const [error, result] = await to(serial.setBaudRate(baud)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('serial.send', async (port, data, callback) => { - const serial = this.#serialRegistry_.getItem(port); - const [error, result] = await to(serial.send(data)); - error && Debug.error(error); - callback([error, result]); - }); - - socket.on('serial.setDTRAndRTS', async (port, dtr, rts, callback) => { - const serial = this.#serialRegistry_.getItem(port); - const [error, result] = await to(serial.setDTRAndRTS(dtr, rts)); - error && Debug.error(error); - callback([error, result]); - }); - } - - getIO() { - return this.#io_; - } -} \ No newline at end of file