Update: WebSocket下MicroPython板卡支持管理板卡文件

This commit is contained in:
王立帮
2024-12-03 19:22:22 +08:00
parent 3911fa27ac
commit b1747a3ad5
7 changed files with 245 additions and 69 deletions

9
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

104
src/common/shell-ampy.js Normal file
View File

@@ -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: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 ls "{{&folderPath}}"',
get: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 get "{{&filePath}}"',
mkdir: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 mkdir "{{&folderPath}}"',
mkfile: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 mkfile "{{&filePath}}"',
isdir: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 isdir "{{&folderPath}}"',
isfile: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 isfile "{{&filePath}}"',
put: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 put "{{&startPath}}" "{{&endPath}}"',
rm: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 rm "{{&filePath}}"',
rmdir: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 rmdir "{{&folderPath}}"',
rename: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 rename "{{&oldPath}}" "{{&newPath}}"',
run: '{{&ampy}} -p {{&port}} -b {{&baud}} -i 0 run "{{&filePath}}"'
}
this.AMPY_TEMPLATE = mustache.render('"{{&python3}}" "{{&ampy}}"', {
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
});
}
}

View File

@@ -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 });
}
}

View File

@@ -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);
}
}

View File

@@ -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() {

View File

@@ -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());