Update: WebSocket下MicroPython板卡支持管理板卡文件
This commit is contained in:
9
package-lock.json
generated
9
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
104
src/common/shell-ampy.js
Normal 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: '{{&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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user