From b1747a3ad5e6b534e8f5b17bffa2fb009ea25275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E7=AB=8B=E5=B8=AE?= <3294713004@qq.com> Date: Tue, 3 Dec 2024 19:22:22 +0800 Subject: [PATCH] =?UTF-8?q?Update:=20WebSocket=E4=B8=8BMicroPython?= =?UTF-8?q?=E6=9D=BF=E5=8D=A1=E6=94=AF=E6=8C=81=E7=AE=A1=E7=90=86=E6=9D=BF?= =?UTF-8?q?=E5=8D=A1=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 9 +++ package.json | 1 + src/common/shell-ampy.js | 104 ++++++++++++++++++++++++++++++++ src/common/shell-arduino.js | 4 +- src/common/shell-micropython.js | 4 +- src/common/shell.js | 103 +++++++++++++------------------ src/web-socket/socket.js | 89 +++++++++++++++++++++++++-- 7 files changed, 245 insertions(+), 69 deletions(-) create mode 100644 src/common/shell-ampy.js diff --git a/package-lock.json b/package-lock.json index e17721b..832518f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "iconv-lite": "^0.6.3", "lodash": "^4.17.21", "mitt": "^3.0.1", + "mustache": "^4.2.0", "serialport": "^12.0.0", "shelljs": "^0.8.5", "shortid": "^2.2.16", @@ -3290,6 +3291,14 @@ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/nanoid": { "version": "2.1.11", "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-2.1.11.tgz", diff --git a/package.json b/package.json index 321b982..127950e 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "iconv-lite": "^0.6.3", "lodash": "^4.17.21", "mitt": "^3.0.1", + "mustache": "^4.2.0", "serialport": "^12.0.0", "shelljs": "^0.8.5", "shortid": "^2.2.16", diff --git a/src/common/shell-ampy.js b/src/common/shell-ampy.js new file mode 100644 index 0000000..1476b7d --- /dev/null +++ b/src/common/shell-ampy.js @@ -0,0 +1,104 @@ +import mustache from 'mustache'; +import Shell from './shell'; +import { MICROPYTHON, PYTHON } from './config'; + + +export default class ShellAmpy extends Shell { + static { + this.TEMPLATE = { + ls: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 ls "{{&folderPath}}"', + get: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 get "{{&filePath}}"', + mkdir: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 mkdir "{{&folderPath}}"', + mkfile: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 mkfile "{{&filePath}}"', + isdir: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 isdir "{{&folderPath}}"', + isfile: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 isfile "{{&filePath}}"', + put: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 put "{{&startPath}}" "{{&endPath}}"', + rm: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 rm "{{&filePath}}"', + rmdir: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 rmdir "{{&folderPath}}"', + rename: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 rename "{{&oldPath}}" "{{&newPath}}"', + run: '{{&y}} -p {{&port}} -b {{&baud}} -i 0 run "{{&filePath}}"' + } + + this.AMPY_TEMPLATE = mustache.render('"{{&python3}}" "{{&y}}"', { + python3: PYTHON.path.cli, + ampy: MICROPYTHON.path.ampy + }); + } + + constructor() { + super(); + } + + async ls(port, baud, folderPath) { + return this.exec(this.render('ls', { port, baud, folderPath }), { + encoding: 'utf-8' + }); + } + + async get(port, baud, filePath) { + return this.exec(this.render('get', { port, baud, filePath }), { + encoding: 'utf-8' + }); + } + + async mkdir(port, baud, folderPath) { + return this.exec(this.render('mkdir', { port, baud, folderPath }), { + encoding: 'utf-8' + }); + } + + async mkfile(port, baud, filePath) { + return this.exec(this.render('mkfile', { port, baud, filePath }), { + encoding: 'utf-8' + }); + } + + async isdir(port, baud, folderPath) { + return this.exec(this.render('isdir', { port, baud, folderPath }), { + encoding: 'utf-8' + }); + } + + async isfile(port, baud, filePath) { + return this.exec(this.render('isfile', { port, baud, filePath }), { + encoding: 'utf-8' + }); + } + + async put(port, baud, startPath, endPath) { + return this.exec(this.render('put', { port, baud, startPath, endPath }), { + encoding: 'utf-8' + }); + } + + async rm(port, baud, filePath) { + return this.exec(this.render('rm', { port, baud, filePath }), { + encoding: 'utf-8' + }); + } + + async rmdir(port, baud, folderPath) { + return this.exec(this.render('rmdir', { port, baud, folderPath }), { + encoding: 'utf-8' + }); + } + + async rename(port, baud, oldPath, newPath) { + return this.exec(this.render('rename', { port, baud, oldPath, newPath }), { + encoding: 'utf-8' + }); + } + + async run(port, baud, filePath) { + return this.exec(this.render('run', { port, baud, filePath }), { + encoding: 'utf-8' + }); + } + + render(templateName, args) { + return mustache.render(ShellAmpy.TEMPLATE[templateName], { + ...args, + ampy: ShellAmpy.AMPY_TEMPLATE + }); + } +} \ No newline at end of file diff --git a/src/common/shell-arduino.js b/src/common/shell-arduino.js index c399e66..40088b9 100644 --- a/src/common/shell-arduino.js +++ b/src/common/shell-arduino.js @@ -23,7 +23,7 @@ export default class ShellArduino extends Shell { `"${arduino.path.code}"`, '--no-color' ].join(' '); - return this.exec(command); + return this.execUntilClosed(command, { maxBuffer: 4096 * 1000000 }); } async upload(config) { @@ -43,6 +43,6 @@ export default class ShellArduino extends Shell { `"${arduino.path.code}"`, '--no-color' ].join(' '); - return this.exec(command); + return this.execUntilClosed(command, { maxBuffer: 4096 * 1000000 }); } } \ No newline at end of file diff --git a/src/common/shell-micropython.js b/src/common/shell-micropython.js index a0d7de8..33caf7a 100644 --- a/src/common/shell-micropython.js +++ b/src/common/shell-micropython.js @@ -16,7 +16,7 @@ export default class ShellMicroPython extends Shell { com: config.port }; const command = MString.tpl(config.command, info); - return this.exec(command); + return this.execUntilClosed(command); } async upload(config) { @@ -26,6 +26,6 @@ export default class ShellMicroPython extends Shell { com: config.port }; const command = MString.tpl(config.command, info); - return this.exec(command); + return this.execUntilClosed(command); } } \ No newline at end of file diff --git a/src/common/shell.js b/src/common/shell.js index a92b751..528db46 100644 --- a/src/common/shell.js +++ b/src/common/shell.js @@ -1,57 +1,23 @@ import { execFile, exec } from 'node:child_process'; -import * as iconv_lite from 'iconv-lite'; -import Debug from './debug'; import EventsBase from './events-base'; import { CURRENT_PLANTFORM } from './config'; export default class Shell extends EventsBase { - static { - this.ENCODING = CURRENT_PLANTFORM == 'win32' ? 'cp936' : 'utf-8'; - } - #shell_ = null; #killed_ = false; - #defaultOptions_ = { - maxBuffer: 4096 * 1000000, - encoding: 'binary', - }; constructor() { super(); this.addEventsType(['data', 'error', 'close']); } - #decode_(str) { - try { - str = decodeURIComponent(str.replace(/(_E[0-9A-F]{1}_[0-9A-F]{2}_[0-9A-F]{2})+/gm, '%$1')); - str = decodeURIComponent(str.replace(/\\(u[0-9a-fA-F]{4})/gm, '%$1')); - } catch (error) { - Debug.error(error); - } - return str; - } - #addEventsListener_() { const { stdout, stderr } = this.#shell_; stdout.on('data', (data) => { - if (data.length > 1000) { - return; - } - data = iconv_lite.decode(Buffer.from(data, 'binary'), 'utf-8'); this.runEvent('data', data); }); stderr.on('data', (data) => { - let lines = data.split('\n'); - for (let i in lines) { - let encoding = 'utf-8'; - if (lines[i].indexOf('can\'t open device') !== -1) { - encoding = Shell.ENCODING; - } - lines[i] = iconv_lite.decode(Buffer.from(lines[i], 'binary'), encoding); - } - data = lines.join('\n'); - data = this.#decode_(data); this.runEvent('error', data); }); } @@ -73,42 +39,59 @@ export default class Shell extends EventsBase { }); } - async exec(command, options = {}) { + async execUntilClosed(command, options = {}) { this.#killed_ = false; - this.#shell_ = exec(command, { ...this.#defaultOptions_, ...options }); + this.#shell_ = exec(command, options); this.#addEventsListener_(); const result = await this.#waitUntilClosed_(); return result; } + async execFileUntilClosed(file, args, options = {}) { + this.#killed_ = false; + this.#shell_ = execFile(file, args, options); + this.#addEventsListener_(); + const result = await this.#waitUntilClosed_(); + return result; + } + + async exec(command, options = {}) { + return new Promise((resolve, reject) => { + this.#killed_ = false; + this.#shell_ = exec(command, options, (error, stdout) => { + if (error) { + reject(String(error)); + } else { + resolve(stdout); + } + }); + }); + } + async execFile(file, args, options = {}) { - this.#killed_ = false; - this.#shell_ = execFile(file, args, { ...this.#defaultOptions_, ...options }); - this.#addEventsListener_(); - const result = await this.#waitUntilClosed_(); - return result; + return new Promise((resolve, reject) => { + this.#killed_ = false; + this.#shell_ = execFile(file, args, options, (error) => { + if (error) { + reject(String(error)); + } else { + resolve(); + } + }); + }); } async kill() { - new Promise((resolve, reject) => { - if (this.#killed_) { - return; - } - this.#shell_.stdin.end(); - this.#shell_.stdout.end(); - if (CURRENT_PLANTFORM === 'win32') { - exec(`taskkill /pid ${this.#shell_.pid} /f /t`, { encoding: 'utf-8' }, (error) => { - if (error) { - reject(error); - } else { - resolve(); - } - }); - } else { - this.#shell_.kill('SIGTERM') - resolve(); - } - }) + if (this.#killed_) { + return; + } + this.#shell_.stdin.end(); + this.#shell_.stdout.end(); + if (CURRENT_PLANTFORM === 'win32') { + await this.exec(`taskkill /pid ${this.#shell_.pid} /f /t`); + } else { + this.#shell_.kill('SIGTERM') + } } getShell() { diff --git a/src/web-socket/socket.js b/src/web-socket/socket.js index a71cbde..c8fc7ae 100644 --- a/src/web-socket/socket.js +++ b/src/web-socket/socket.js @@ -8,6 +8,7 @@ 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'; @@ -17,6 +18,7 @@ export default class Socket { #serialRegistry_ = new Registry(); #shellMicroPython_ = new ShellMicroPython(); #shellArduino_ = new ShellArduino(); + #shellAmpy_ = new ShellAmpy(); constructor(httpsServer, options) { this.#io_ = new Server(httpsServer, options); @@ -42,6 +44,7 @@ export default class Socket { this.#addEventsListenerForMicroPython_(socket); this.#addEventsListenerForArduino_(socket); + this.#addEventsListenerForAmpy_(socket); this.#addEventsListenerForSerial_(socket); } @@ -65,14 +68,19 @@ export default class Socket { }); socket.on('micropython.upload', async (config, callback) => { - let { filePath = '' } = config; + let { filePath = '', libraries = {} } = config; filePath = MString.tpl(filePath, { indexPath: path.resolve(CLIENT_PATH, config.boardDirPath) }); - let [error1,] = await to(fsExtra.ensureDir(path.dirname(filePath))); - error1 && Debug.error(error1); - let [error2,] = await to(fsExtra.outputFile(filePath, config.code)); - error2 && Debug.error(error2); + 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]); @@ -131,6 +139,77 @@ export default class Socket { }); } + #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());