Compare commits

...

13 Commits

Author SHA1 Message Date
yczpf2019
708bdc6bf0 feat(arduino-shell): 在前端过滤掉 serial1/undefined 错误提示 2026-01-24 21:47:45 +08:00
yczpf2019
44b02f4781 fix(shell-arduino): 将 --library 改回 --libraries 以正确加载库集合 2026-01-24 21:44:15 +08:00
yczpf2019
48ce7f35e1 chore(shell-arduino): 清理调试日志 2026-01-24 21:35:12 +08:00
yczpf2019
9b80cf73d3 fix(socket): 修复 arduino.upload 逻辑,只编译返回文件供前端烧录 2026-01-24 21:30:27 +08:00
yczpf2019
67a3a3bd78 fix(shell-arduino): 改用 execFileUntilClosed 避免 shell 引号解析问题 2026-01-24 21:23:25 +08:00
yczpf2019
e8de8f19c2 debug(shell-arduino): 添加调试日志查看 arduino-cli 命令格式 2026-01-24 21:19:08 +08:00
yczpf2019
7d2076f165 fix(shell-arduino): 修复 --libraries 参数格式,改用单独的 --library 参数 2026-01-24 21:13:46 +08:00
yczpf2019
10d5254a7e fix(websocket-arduino): 在 init 中同时初始化 WebCompiler.ArduShell 2026-01-24 21:09:00 +08:00
yczpf2019
a67284afe5 feat(websocket-arduino): 委托给 WebCompiler.ArduShell 处理本地上传 2026-01-24 21:04:10 +08:00
yczpf2019
4ac522ffc6 fix(websocket-bu): 修复 WebBU 模块加载顺序问题,改为动态获取 2026-01-24 20:55:31 +08:00
yczpf2019
5456419bb3 fix(socket): 添加 MicroPython 请求端口有效性检查,无效端口不报错 2026-01-24 20:44:03 +08:00
yczpf2019
9bcc49059e fix(ampy): 启用 exitREPL 软复位让设备执行上传的代码 2026-01-24 20:30:25 +08:00
yczpf2019
0c6199d8e4 fix(ampy): 添加 dispose 方法 null 检查防止重复调用报错 2026-01-24 20:28:38 +08:00
5 changed files with 747 additions and 671 deletions

View File

@@ -1,275 +1,302 @@
goog.loadJs('web', () => { goog.loadJs('web', () => {
goog.require('layui'); goog.require('layui');
goog.require('dayjs.duration'); goog.require('dayjs.duration');
goog.require('Mixly.Boards'); goog.require('Mixly.Boards');
goog.require('Mixly.Debug'); goog.require('Mixly.Debug');
goog.require('Mixly.LayerExt'); goog.require('Mixly.LayerExt');
goog.require('Mixly.Msg'); goog.require('Mixly.Msg');
goog.require('Mixly.Workspace'); goog.require('Mixly.Workspace');
goog.require('Mixly.LayerProgress'); goog.require('Mixly.LayerProgress');
goog.require('Mixly.WebSocket.Serial'); goog.require('Mixly.WebSocket.Serial');
goog.provide('Mixly.WebSocket.ArduShell'); goog.require('Mixly.WebCompiler.ArduShell');
goog.provide('Mixly.WebSocket.ArduShell');
const { const {
Boards, Boards,
Debug, Debug,
LayerExt, LayerExt,
Msg, Msg,
Workspace, Workspace,
LayerProgress, LayerProgress,
WebSocket WebSocket,
} = Mixly; WebCompiler = {}
} = Mixly;
const { Serial } = WebSocket; // 动态获取 WebCompiler.ArduShell用于本地上传
const getWebCompilerArduShell = () => Mixly.WebCompiler?.ArduShell;
const { layer } = layui; const { Serial } = WebSocket;
const { layer } = layui;
class WebSocketArduShell { class WebSocketArduShell {
static { static {
this.mixlySocket = null; this.mixlySocket = null;
this.socket = null; this.socket = null;
this.shell = null; this.shell = null;
this.getSocket = function () { this.getSocket = function () {
return this.socket; return this.socket;
} }
this.getMixlySocket = function () { this.getMixlySocket = function () {
return this.mixlySocket; return this.mixlySocket;
} }
this.init = function (mixlySocket) { this.init = function (mixlySocket) {
this.mixlySocket = mixlySocket; this.mixlySocket = mixlySocket;
this.socket = mixlySocket.getSocket(); this.socket = mixlySocket.getSocket();
this.shell = new WebSocketArduShell(); this.shell = new WebSocketArduShell();
const socket = this.socket; const socket = this.socket;
socket.on('arduino.dataEvent', (data) => { // 同时初始化 WebCompiler.ArduShell用于本地上传
if (data.length > 1000) { const WebCompilerArduShell = getWebCompilerArduShell();
return; if (WebCompilerArduShell && !WebCompilerArduShell.getMixlySocket()) {
WebCompilerArduShell.init(mixlySocket);
} }
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.addValue(data);
});
socket.on('arduino.errorEvent', (data) => { socket.on('arduino.dataEvent', (data) => {
const { mainStatusBarTabs } = Mixly; if (data.length > 1000) {
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
try {
data = unescape(data.replace(/(_E[0-9A-F]{1}_[0-9A-F]{2}_[0-9A-F]{2})+/gm, '%$1'));
data = unescape(data.replace(/\\(u[0-9a-fA-F]{4})/gm, '%$1'));
} catch (error) {
Debug.error(error);
}
statusBarTerminal.addValue(data);
});
}
this.initCompile = function () {
if (!this.mixlySocket.isConnected()) {
layer.msg(Msg.Lang['websocket.offline'], { time: 1000 });
return;
}
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
mainStatusBarTabs.changeTo('output');
mainStatusBarTabs.show();
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const code = editor.getCode();
statusBarTerminal.setValue(`${Msg.Lang['shell.compiling']}...\n`);
this.shell.compile(code)
.then((info) => {
this.endCallback(info.code, info.time);
})
.catch((error) => {
Debug.error(error);
statusBarTerminal.addValue(`\n==${Msg.Lang['shell.compileFailed']}==\n`);
});
}
this.initUpload = function () {
if (!this.mixlySocket.isConnected()) {
layer.msg(Msg.Lang['websocket.offline'], { time: 1000 });
return;
}
const port = Serial.getSelectedPortName();
if (!port) {
layer.msg(Msg.Lang['statusbar.serial.noDevice'], {
time: 1000
});
return;
}
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
mainStatusBarTabs.changeTo('output');
mainStatusBarTabs.show();
statusBarTerminal.setValue(`${Msg.Lang['shell.uploading']}...\n`);
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const code = editor.getCode();
const statusBarSerial = mainStatusBarTabs.getStatusBarById(port);
const closePromise = statusBarSerial ? statusBarSerial.close() : Promise.resolve();
closePromise
.then(() => {
return this.shell.upload(port, code)
})
.then((info) => {
this.endCallback(info.code, info.time);
if (info.code || !Serial.portIsLegal(port)) {
return; return;
} }
mainStatusBarTabs.add('serial', port); const { mainStatusBarTabs } = Mixly;
mainStatusBarTabs.changeTo(port); const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
const statusBarSerial = mainStatusBarTabs.getStatusBarById(port); statusBarTerminal.addValue(data);
statusBarSerial.open()
.then(() => {
const baudRates = code.match(/(?<=Serial.begin[\s]*\([\s]*)[0-9]*(?=[\s]*\))/g);
if (!baudRates?.length) {
return statusBarSerial.setBaudRate(9600);
} else {
return statusBarSerial.setBaudRate(baudRates[0] - 0);
}
})
.catch(Debug.error);
})
.catch((error) => {
Debug.error(error);
statusBarTerminal.addValue(`\n==${Msg.Lang['shell.uploadFailed']}==\n`);
}); });
}
this.endCallback = function (code, time) { socket.on('arduino.errorEvent', (data) => {
const { mainStatusBarTabs } = Mixly; const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output'); const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
mainStatusBarTabs.changeTo('output'); try {
let message = ''; data = unescape(data.replace(/(_E[0-9A-F]{1}_[0-9A-F]{2}_[0-9A-F]{2})+/gm, '%$1'));
if (code) { data = unescape(data.replace(/\\(u[0-9a-fA-F]{4})/gm, '%$1'));
message = (this.shell.isCompiling() ? Msg.Lang['shell.compileFailed'] : Msg.Lang['shell.uploadFailed']); } catch (error) {
statusBarTerminal.addValue(`\n==${message}==\n`); Debug.error(error);
} else { }
message = (this.shell.isCompiling() ? Msg.Lang['shell.compileSucc'] : Msg.Lang['shell.uploadSucc']);
statusBarTerminal.addValue(`\n==${message}(${Msg.Lang['shell.timeCost']} ${ // 过滤掉无关紧要的端口错误(因为已经在本地完成上传了)
dayjs.duration(time).format('HH:mm:ss.SSS') if (typeof data === 'string' && (
})==\n`); data.includes('cannot open serial1') ||
data.includes('cannot open device "undefined"') ||
data.includes('No such file or directory, cannot open')
)) {
return;
}
statusBarTerminal.addValue(data);
});
} }
layer.msg(message, { time: 1000 });
this.initCompile = function () {
if (!this.mixlySocket.isConnected()) {
layer.msg(Msg.Lang['websocket.offline'], { time: 1000 });
return;
}
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
mainStatusBarTabs.changeTo('output');
mainStatusBarTabs.show();
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const code = editor.getCode();
statusBarTerminal.setValue(`${Msg.Lang['shell.compiling']}...\n`);
this.shell.compile(code)
.then((info) => {
this.endCallback(info.code, info.time);
})
.catch((error) => {
Debug.error(error);
statusBarTerminal.addValue(`\n==${Msg.Lang['shell.compileFailed']}==\n`);
});
}
this.initUpload = function () {
// 委托给 WebCompiler.ArduShell 处理本地上传(使用 AVRUploader 或 esptool-js
// 服务器无法访问用户本地的串口设备,必须在浏览器端完成上传
const WebCompilerArduShell = getWebCompilerArduShell();
if (WebCompilerArduShell) {
return WebCompilerArduShell.initUpload();
}
if (!this.mixlySocket.isConnected()) {
layer.msg(Msg.Lang['websocket.offline'], { time: 1000 });
return;
}
const port = Serial.getSelectedPortName();
if (!port) {
layer.msg(Msg.Lang['statusbar.serial.noDevice'], {
time: 1000
});
return;
}
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
mainStatusBarTabs.changeTo('output');
mainStatusBarTabs.show();
statusBarTerminal.setValue(`${Msg.Lang['shell.uploading']}...\n`);
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const code = editor.getCode();
const statusBarSerial = mainStatusBarTabs.getStatusBarById(port);
const closePromise = statusBarSerial ? statusBarSerial.close() : Promise.resolve();
closePromise
.then(() => {
return this.shell.upload(port, code)
})
.then((info) => {
this.endCallback(info.code, info.time);
if (info.code || !Serial.portIsLegal(port)) {
return;
}
mainStatusBarTabs.add('serial', port);
mainStatusBarTabs.changeTo(port);
const statusBarSerial = mainStatusBarTabs.getStatusBarById(port);
statusBarSerial.open()
.then(() => {
const baudRates = code.match(/(?<=Serial.begin[\s]*\([\s]*)[0-9]*(?=[\s]*\))/g);
if (!baudRates?.length) {
return statusBarSerial.setBaudRate(9600);
} else {
return statusBarSerial.setBaudRate(baudRates[0] - 0);
}
})
.catch(Debug.error);
})
.catch((error) => {
Debug.error(error);
statusBarTerminal.addValue(`\n==${Msg.Lang['shell.uploadFailed']}==\n`);
});
}
this.endCallback = function (code, time) {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
mainStatusBarTabs.changeTo('output');
let message = '';
if (code) {
message = (this.shell.isCompiling() ? Msg.Lang['shell.compileFailed'] : Msg.Lang['shell.uploadFailed']);
statusBarTerminal.addValue(`\n==${message}==\n`);
} else {
message = (this.shell.isCompiling() ? Msg.Lang['shell.compileSucc'] : Msg.Lang['shell.uploadSucc']);
statusBarTerminal.addValue(`\n==${message}(${Msg.Lang['shell.timeCost']} ${dayjs.duration(time).format('HH:mm:ss.SSS')
})==\n`);
}
layer.msg(message, { time: 1000 });
}
}
#running_ = false;
#upload_ = false;
#killing_ = false;
#layer_ = null;
constructor() {
this.#layer_ = new LayerProgress({
width: 200,
cancelValue: Msg.Lang['nav.btn.stop'],
skin: 'layui-anim layui-anim-scale',
cancel: () => {
if (this.#killing_) {
return false;
}
this.#layer_.title(`${Msg.Lang['shell.aborting']}...`);
this.#killing_ = true;
this.kill().catch(Debug.error);
return false;
},
cancelDisplay: false
});
}
async compile(code) {
return new Promise(async (resolve, reject) => {
this.#running_ = true;
this.#upload_ = false;
this.#killing_ = false;
this.showProgress();
const key = Boards.getSelectedBoardCommandParam();
const config = { key, code };
const mixlySocket = WebSocketArduShell.getMixlySocket();
mixlySocket.emit('arduino.compile', config, (response) => {
this.hideProgress();
if (response.error) {
reject(response.error);
return;
}
const [error, result] = response;
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
async upload(port, code) {
return new Promise(async (resolve, reject) => {
this.#running_ = true;
this.#upload_ = true;
this.#killing_ = false;
this.showProgress();
const key = Boards.getSelectedBoardCommandParam();
const config = { key, code, port };
const mixlySocket = WebSocketArduShell.getMixlySocket();
mixlySocket.emit('arduino.upload', config, (response) => {
this.hideProgress();
if (response.error) {
reject(response.error);
return;
}
const [error, result] = response;
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
async kill() {
return new Promise(async (resolve, reject) => {
const mixlySocket = WebSocketArduShell.getMixlySocket();
mixlySocket.emit('arduino.kill', (response) => {
if (response.error) {
reject(response.error);
return;
}
const [error, result] = response;
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
showProgress() {
const message = this.isCompiling() ? Msg.Lang['shell.compiling'] : Msg.Lang['shell.uploading'];
this.#layer_.title(`${message}...`);
this.#layer_.show();
}
hideProgress() {
this.#layer_.hide();
}
isUploading() {
return this.#running_ && this.#upload_;
}
isCompiling() {
return this.#running_ && !this.#upload_;
} }
} }
#running_ = false; WebSocket.ArduShell = WebSocketArduShell;
#upload_ = false;
#killing_ = false;
#layer_ = null;
constructor() {
this.#layer_ = new LayerProgress({
width: 200,
cancelValue: Msg.Lang['nav.btn.stop'],
skin: 'layui-anim layui-anim-scale',
cancel: () => {
if (this.#killing_) {
return false;
}
this.#layer_.title(`${Msg.Lang['shell.aborting']}...`);
this.#killing_ = true;
this.kill().catch(Debug.error);
return false;
},
cancelDisplay: false
});
}
async compile(code) {
return new Promise(async (resolve, reject) => {
this.#running_ = true;
this.#upload_ = false;
this.#killing_ = false;
this.showProgress();
const key = Boards.getSelectedBoardCommandParam();
const config = { key, code };
const mixlySocket = WebSocketArduShell.getMixlySocket();
mixlySocket.emit('arduino.compile', config, (response) => {
this.hideProgress();
if (response.error) {
reject(response.error);
return;
}
const [error, result] = response;
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
async upload(port, code) {
return new Promise(async (resolve, reject) => {
this.#running_ = true;
this.#upload_ = true;
this.#killing_ = false;
this.showProgress();
const key = Boards.getSelectedBoardCommandParam();
const config = { key, code, port };
const mixlySocket = WebSocketArduShell.getMixlySocket();
mixlySocket.emit('arduino.upload', config, (response) => {
this.hideProgress();
if (response.error) {
reject(response.error);
return;
}
const [error, result] = response;
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
async kill() {
return new Promise(async (resolve, reject) => {
const mixlySocket = WebSocketArduShell.getMixlySocket();
mixlySocket.emit('arduino.kill', (response) => {
if (response.error) {
reject(response.error);
return;
}
const [error, result] = response;
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
showProgress() {
const message = this.isCompiling() ? Msg.Lang['shell.compiling'] : Msg.Lang['shell.uploading'];
this.#layer_.title(`${message}...`);
this.#layer_.show();
}
hideProgress() {
this.#layer_.hide();
}
isUploading() {
return this.#running_ && this.#upload_;
}
isCompiling() {
return this.#running_ && !this.#upload_;
}
}
WebSocket.ArduShell = WebSocketArduShell;
}); });

View File

@@ -31,8 +31,8 @@ goog.loadJs('web', () => {
const { SELECTED_BOARD, BOARD } = Config; const { SELECTED_BOARD, BOARD } = Config;
// 引入本地 Web.BU 模块,用于 MicroPython 本地上传 // WebBU 需要在使用时动态获取,避免模块加载顺序问题
const WebBU = Mixly.Web?.BU; const getWebBU = () => Mixly.Web?.BU;
const { Serial } = WebSocket; const { Serial } = WebSocket;
@@ -74,6 +74,7 @@ goog.loadJs('web', () => {
this.initBurn = function () { this.initBurn = function () {
// MicroPython 板卡使用本地 Web Serial API 烧录,而非通过服务器 // MicroPython 板卡使用本地 Web Serial API 烧录,而非通过服务器
const WebBU = getWebBU();
if (SELECTED_BOARD?.language === 'MicroPython' && WebBU) { if (SELECTED_BOARD?.language === 'MicroPython' && WebBU) {
return WebBU.initBurn(); return WebBU.initBurn();
} }
@@ -111,6 +112,7 @@ goog.loadJs('web', () => {
this.initUpload = function () { this.initUpload = function () {
// MicroPython 板卡使用本地 Web Serial API 上传,而非通过服务器 // MicroPython 板卡使用本地 Web Serial API 上传,而非通过服务器
const WebBU = getWebBU();
if (SELECTED_BOARD?.language === 'MicroPython' && WebBU) { if (SELECTED_BOARD?.language === 'MicroPython' && WebBU) {
return WebBU.initUpload(); return WebBU.initUpload();
} }

View File

@@ -1,429 +1,434 @@
goog.loadJs('web', () => { goog.loadJs('web', () => {
goog.require('path'); goog.require('path');
goog.require('Mustache'); goog.require('Mustache');
goog.require('Mixly.Env'); goog.require('Mixly.Env');
goog.require('Mixly.Events'); goog.require('Mixly.Events');
goog.require('Mixly.Msg'); goog.require('Mixly.Msg');
goog.require('Mixly.Ampy'); goog.require('Mixly.Ampy');
goog.require('Mixly.Web'); goog.require('Mixly.Web');
goog.provide('Mixly.Web.Ampy'); goog.provide('Mixly.Web.Ampy');
const { const {
Env, Env,
Events, Events,
Msg, Msg,
Ampy, Ampy,
Web Web
} = Mixly; } = Mixly;
class AmpyExt extends Ampy { class AmpyExt extends Ampy {
static { static {
this.LS = goog.readFileSync(path.join(Env.templatePath, 'python/ls.py')); this.LS = goog.readFileSync(path.join(Env.templatePath, 'python/ls.py'));
this.LS_RECURSIVE = goog.readFileSync(path.join(Env.templatePath, 'python/ls-recursive.py')); this.LS_RECURSIVE = goog.readFileSync(path.join(Env.templatePath, 'python/ls-recursive.py'));
this.LS_LONG_FORMAT = goog.readFileSync(path.join(Env.templatePath, 'python/ls-long-format.py')); this.LS_LONG_FORMAT = goog.readFileSync(path.join(Env.templatePath, 'python/ls-long-format.py'));
this.MKDIR = goog.readFileSync(path.join(Env.templatePath, 'python/mkdir.py')); this.MKDIR = goog.readFileSync(path.join(Env.templatePath, 'python/mkdir.py'));
this.MKFILE = goog.readFileSync(path.join(Env.templatePath, 'python/mkfile.py')); this.MKFILE = goog.readFileSync(path.join(Env.templatePath, 'python/mkfile.py'));
this.RENAME = goog.readFileSync(path.join(Env.templatePath, 'python/rename.py')); this.RENAME = goog.readFileSync(path.join(Env.templatePath, 'python/rename.py'));
this.RM = goog.readFileSync(path.join(Env.templatePath, 'python/rm.py')); this.RM = goog.readFileSync(path.join(Env.templatePath, 'python/rm.py'));
this.RMDIR = goog.readFileSync(path.join(Env.templatePath, 'python/rmdir.py')); this.RMDIR = goog.readFileSync(path.join(Env.templatePath, 'python/rmdir.py'));
this.GET = goog.readFileSync(path.join(Env.templatePath, 'python/get.py')); this.GET = goog.readFileSync(path.join(Env.templatePath, 'python/get.py'));
this.CWD = goog.readFileSync(path.join(Env.templatePath, 'python/cwd.py')); this.CWD = goog.readFileSync(path.join(Env.templatePath, 'python/cwd.py'));
this.CPDIR = goog.readFileSync(path.join(Env.templatePath, 'python/cpdir.py')); this.CPDIR = goog.readFileSync(path.join(Env.templatePath, 'python/cpdir.py'));
this.CPFILE = goog.readFileSync(path.join(Env.templatePath, 'python/cpfile.py')); this.CPFILE = goog.readFileSync(path.join(Env.templatePath, 'python/cpfile.py'));
} }
#device_ = null; #device_ = null;
#receiveTemp_ = []; #receiveTemp_ = [];
#writeBuffer_ = true; #writeBuffer_ = true;
#active_ = false; #active_ = false;
#dataLength_ = 256; #dataLength_ = 256;
#events_ = new Events(['message', 'replaceMessage']) #events_ = new Events(['message', 'replaceMessage'])
constructor(device, writeBuffer = true, dataLength = 256) { constructor(device, writeBuffer = true, dataLength = 256) {
super(); super();
this.#device_ = device; this.#device_ = device;
this.#writeBuffer_ = writeBuffer; this.#writeBuffer_ = writeBuffer;
this.#dataLength_ = dataLength; this.#dataLength_ = dataLength;
this.#addEventsListener_(); this.#addEventsListener_();
} }
#addEventsListener_() { #addEventsListener_() {
this.#device_.bind('onChar', (char) => { this.#device_.bind('onChar', (char) => {
if (['\r', '\n'].includes(char)) { if (['\r', '\n'].includes(char)) {
this.#receiveTemp_.push(''); this.#receiveTemp_.push('');
} else {
let line = this.#receiveTemp_.pop() ?? '';
this.#receiveTemp_.push(line + char);
}
});
}
bind(...args) {
return this.#events_.bind(...args);
}
message(message) {
this.#events_.run('message', message);
}
replaceMessage(lineNumber, message) {
this.#events_.run('replaceMessage', lineNumber, message);
}
getProgressMessage(name, percent) {
const sended = parseInt(percent * 45);
const left = percent === 0 ? '' : Array(sended).fill('=').join('');
const right = percent === 100 ? '' : Array(45 - sended).fill('-').join('');
return `${name} → |${left}${right}| ${(percent * 100).toFixed(1)}%`;
}
isActive() {
return this.#active_;
}
async readUntil(ending, withEnding = true, timeout = 5000) {
const startTime = Number(new Date());
let nowTime = startTime;
let readStr = '';
while (nowTime - startTime < timeout) {
const nowTime = Number(new Date());
let len = this.#receiveTemp_.length;
for (let i = 0; i < len; i++) {
const data = this.#receiveTemp_.shift();
let index = data.toLowerCase().indexOf(ending);
if (index !== -1) {
if (withEnding) {
index += ending.length;
}
this.#receiveTemp_.unshift(data.substring(index));
readStr += data.substring(0, index);
return readStr;
} else { } else {
readStr += data; let line = this.#receiveTemp_.pop() ?? '';
if (i !== len - 1) { this.#receiveTemp_.push(line + char);
readStr += '\n'; }
});
}
bind(...args) {
return this.#events_.bind(...args);
}
message(message) {
this.#events_.run('message', message);
}
replaceMessage(lineNumber, message) {
this.#events_.run('replaceMessage', lineNumber, message);
}
getProgressMessage(name, percent) {
const sended = parseInt(percent * 45);
const left = percent === 0 ? '' : Array(sended).fill('=').join('');
const right = percent === 100 ? '' : Array(45 - sended).fill('-').join('');
return `${name} → |${left}${right}| ${(percent * 100).toFixed(1)}%`;
}
isActive() {
return this.#active_;
}
async readUntil(ending, withEnding = true, timeout = 5000) {
const startTime = Number(new Date());
let nowTime = startTime;
let readStr = '';
while (nowTime - startTime < timeout) {
const nowTime = Number(new Date());
let len = this.#receiveTemp_.length;
for (let i = 0; i < len; i++) {
const data = this.#receiveTemp_.shift();
let index = data.toLowerCase().indexOf(ending);
if (index !== -1) {
if (withEnding) {
index += ending.length;
}
this.#receiveTemp_.unshift(data.substring(index));
readStr += data.substring(0, index);
return readStr;
} else {
readStr += data;
if (i !== len - 1) {
readStr += '\n';
}
} }
} }
if (nowTime - startTime >= timeout) {
return '';
}
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.dataReadInterrupt']);
}
await this.#device_.sleep(100);
} }
if (nowTime - startTime >= timeout) { }
async interrupt(timeout = 1000) {
for (let i = 0; i < 5; i++) {
// 中断两次
await this.#device_.sendBuffer([0x0D, 0x03]);
await this.#device_.sleep(100);
await this.#device_.sendBuffer([0x03]);
await this.#device_.sleep(100);
if (await this.readUntil('>>>', true, timeout)) {
return true;
}
}
return false;
}
async enterRawREPL(timeout = 1000) {
for (let i = 0; i < 5; i++) {
await this.#device_.sendBuffer([0x01]);
await this.#device_.sleep(100);
if (await this.readUntil('raw repl; ctrl-b to exit', true, timeout)) {
return true;
}
}
return false;
}
async exitRawREPL(timeout = 5000) {
await this.#device_.sendBuffer([0x02]);
await this.#device_.sleep(100);
let succeed = false;
if (await this.readUntil('>>>', true, timeout)) {
succeed = true;
}
return succeed;
}
async exitREPL(timeout = 5000) {
await this.#device_.sendBuffer([0x04]);
await this.#device_.sleep(100);
let succeed = false;
if (await this.readUntil('soft reboot', false, timeout)) {
succeed = true;
}
return succeed;
}
async exec(str, timeout = 5000) {
if (this.#writeBuffer_) {
const buffer = this.#device_.encode(str);
const len = Math.ceil(buffer.length / this.#dataLength_);
for (let i = 0; i < len; i++) {
const start = i * this.#dataLength_;
const end = Math.min((i + 1) * this.#dataLength_, buffer.length);
const writeBuffer = buffer.slice(start, end);
await this.#device_.sendBuffer(writeBuffer);
await this.#device_.sleep(10);
}
} else {
for (let i = 0; i < str.length / this.#dataLength_; i++) {
const start = i * this.#dataLength_;
const end = Math.min((i + 1) * this.#dataLength_, str.length);
let data = str.substring(start, end);
await this.#device_.sendString(data);
await this.#device_.sleep(10);
}
}
await this.#device_.sendBuffer([0x04]);
return await this.follow(timeout);
}
async follow(timeout = 1000) {
let data = await this.readUntil('\x04', true, timeout);
if (data.length < 1) {
throw new Error(Msg.Lang['ampy.waitingFirstEOFTimeout']);
}
let start = data.toLowerCase().lastIndexOf('ok');
if (start === -1) {
start = 0;
} else {
start += 2;
}
data = data.substring(start, data.length - 1);
let dataError = await this.readUntil('\x04', true, timeout);
if (dataError.length < 1) {
throw new Error(Msg.Lang['ampy.secondEOFTimeout']);
}
dataError = dataError.substring(0, dataError.length - 1);
return { data, dataError };
}
async enter() {
if (this.isActive()) {
return;
}
this.#active_ = true;
await this.#device_.open(115200);
await this.#device_.sleep(500);
await this.#device_.sendBuffer([0x02]);
if (!await this.interrupt()) {
throw new Error(Msg.Lang['ampy.interruptFailed']);
}
if (!await this.enterRawREPL()) {
throw new Error(Msg.Lang['ampy.enterRawREPLFailed']);
}
}
async exit() {
if (!this.isActive()) {
return;
}
if (!await this.exitRawREPL()) {
throw new Error(Msg.Lang['ampy.exitRawREPLFailed']);
}
// 发送 Ctrl+D 触发软复位,让设备执行上传的 main.py
if (!await this.exitREPL()) {
throw new Error(Msg.Lang['ampy.exitREPLFailed']);
}
await this.#device_.close();
this.#active_ = false;
}
async get(filename, encoding = 'utf8', timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.GET, {
path: filename
});
const { data, dataError } = await this.exec(code, timeout);
if (dataError) {
return ''; return '';
} }
if (encoding === 'utf8') {
return this.#device_.decode(this.unhexlify(data));
} else {
return this.unhexlify(data);
}
}
async put(filename, data, timeout = 5000) {
if (!this.isActive()) { if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.dataReadInterrupt']); throw new Error(Msg.Lang['ampy.portIsNotOpen']);
} }
await this.#device_.sleep(100); this.message(`Writing ${filename}...\n`);
} this.message(this.getProgressMessage('', 0));
} await this.exec(`file = open('${filename}', 'wb')`, timeout);
let buffer = null;
async interrupt(timeout = 1000) { if (data.constructor === String) {
for (let i = 0; i < 5; i++) { buffer = this.#device_.encode(data);
// 中断两次 } else if (data.constructor === ArrayBuffer) {
await this.#device_.sendBuffer([0x0D, 0x03]); buffer = new Uint8Array(data);
await this.#device_.sleep(100); } else {
await this.#device_.sendBuffer([0x03]); buffer = data;
await this.#device_.sleep(100);
if (await this.readUntil('>>>', true, timeout)) {
return true;
} }
} const len = Math.ceil(buffer.length / 64);
return false; if (!len) {
} this.replaceMessage(-1, this.getProgressMessage('', 1));
async enterRawREPL(timeout = 1000) {
for (let i = 0; i < 5; i++) {
await this.#device_.sendBuffer([0x01]);
await this.#device_.sleep(100);
if (await this.readUntil('raw repl; ctrl-b to exit', true, timeout)) {
return true;
} }
} let sendedLength = 0;
return false;
}
async exitRawREPL(timeout = 5000) {
await this.#device_.sendBuffer([0x02]);
await this.#device_.sleep(100);
let succeed = false;
if (await this.readUntil('>>>', true, timeout)) {
succeed = true;
}
return succeed;
}
async exitREPL(timeout = 5000) {
await this.#device_.sendBuffer([0x04]);
await this.#device_.sleep(100);
let succeed = false;
if (await this.readUntil('soft reboot', false, timeout)) {
succeed = true;
}
return succeed;
}
async exec(str, timeout = 5000) {
if (this.#writeBuffer_) {
const buffer = this.#device_.encode(str);
const len = Math.ceil(buffer.length / this.#dataLength_);
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
const start = i * this.#dataLength_; const writeBuffer = buffer.slice(i * 64, Math.min((i + 1) * 64, buffer.length));
const end = Math.min((i + 1) * this.#dataLength_, buffer.length); sendedLength += writeBuffer.length;
const writeBuffer = buffer.slice(start, end); const percent = sendedLength / buffer.length;
await this.#device_.sendBuffer(writeBuffer); this.replaceMessage(-1, this.getProgressMessage('', percent));
await this.#device_.sleep(10); let writeStr = '';
} for (let num of writeBuffer) {
} else { let numStr = num.toString(16);
for (let i = 0; i < str.length / this.#dataLength_; i++) { if (numStr.length === 1) {
const start = i * this.#dataLength_; numStr = '0' + numStr;
const end = Math.min((i + 1) * this.#dataLength_, str.length); }
let data = str.substring(start, end); writeStr += '\\x' + numStr;
await this.#device_.sendString(data);
await this.#device_.sleep(10);
}
}
await this.#device_.sendBuffer([0x04]);
return await this.follow(timeout);
}
async follow(timeout = 1000) {
let data = await this.readUntil('\x04', true, timeout);
if (data.length < 1) {
throw new Error(Msg.Lang['ampy.waitingFirstEOFTimeout']);
}
let start = data.toLowerCase().lastIndexOf('ok');
if (start === -1) {
start = 0;
} else {
start += 2;
}
data = data.substring(start, data.length - 1);
let dataError = await this.readUntil('\x04', true, timeout);
if (dataError.length < 1) {
throw new Error(Msg.Lang['ampy.secondEOFTimeout']);
}
dataError = dataError.substring(0, dataError.length - 1);
return { data, dataError };
}
async enter() {
if (this.isActive()) {
return;
}
this.#active_ = true;
await this.#device_.open(115200);
await this.#device_.sleep(500);
await this.#device_.sendBuffer([0x02]);
if (!await this.interrupt()) {
throw new Error(Msg.Lang['ampy.interruptFailed']);
}
if (!await this.enterRawREPL()) {
throw new Error(Msg.Lang['ampy.enterRawREPLFailed']);
}
}
async exit() {
if (!this.isActive()) {
return;
}
if (!await this.exitRawREPL()) {
throw new Error(Msg.Lang['ampy.exitRawREPLFailed']);
}
/*if (!await this.exitREPL()) {
throw new Error(Msg.Lang['ampy.exitREPLFailed']);
}*/
await this.#device_.close();
this.#active_ = false;
}
async get(filename, encoding = 'utf8', timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.GET, {
path: filename
});
const { data, dataError } = await this.exec(code, timeout);
if (dataError) {
return '';
}
if (encoding === 'utf8') {
return this.#device_.decode(this.unhexlify(data));
} else {
return this.unhexlify(data);
}
}
async put(filename, data, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
this.message(`Writing ${filename}...\n`);
this.message(this.getProgressMessage('', 0));
await this.exec(`file = open('${filename}', 'wb')`, timeout);
let buffer = null;
if (data.constructor === String) {
buffer = this.#device_.encode(data);
} else if (data.constructor === ArrayBuffer) {
buffer = new Uint8Array(data);
} else {
buffer = data;
}
const len = Math.ceil(buffer.length / 64);
if (!len) {
this.replaceMessage(-1, this.getProgressMessage('', 1));
}
let sendedLength = 0;
for (let i = 0; i < len; i++) {
const writeBuffer = buffer.slice(i * 64, Math.min((i + 1) * 64, buffer.length));
sendedLength += writeBuffer.length;
const percent = sendedLength / buffer.length;
this.replaceMessage(-1, this.getProgressMessage('', percent));
let writeStr = '';
for (let num of writeBuffer) {
let numStr = num.toString(16);
if (numStr.length === 1) {
numStr = '0' + numStr;
} }
writeStr += '\\x' + numStr; await this.exec(`file.write(b'${writeStr}')`, timeout);
} }
await this.exec(`file.write(b'${writeStr}')`, timeout); await this.exec('file.close()', timeout);
this.message('\n');
} }
await this.exec('file.close()', timeout);
this.message('\n');
}
async ls(directory = '/', longFormat = true, recursive = false, timeout = 5000) { async ls(directory = '/', longFormat = true, recursive = false, timeout = 5000) {
if (!this.isActive()) { if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']); throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
let code = '';
if (longFormat) {
code = Mustache.render(AmpyExt.LS_LONG_FORMAT, {
path: directory
});
} else if (recursive) {
code = Mustache.render(AmpyExt.LS_RECURSIVE, {
path: directory
});
} else {
code = Mustache.render(AmpyExt.LS, {
path: directory
});
}
const { data, dataError } = await this.exec(code, timeout);
if (dataError) {
return [];
}
return JSON.parse(data.replaceAll('\'', '\"'));
} }
let code = '';
if (longFormat) { async mkdir(directory, timeout = 5000) {
code = Mustache.render(AmpyExt.LS_LONG_FORMAT, { if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.MKDIR, {
path: directory path: directory
}); });
} else if (recursive) { const { dataError } = await this.exec(code, timeout);
code = Mustache.render(AmpyExt.LS_RECURSIVE, { return !dataError;
}
async mkfile(file, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.MKFILE, {
path: file
});
const { dataError } = await this.exec(code, timeout);
return !dataError;
}
async rename(oldname, newname, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.RENAME, {
oldPath: oldname,
newPath: newname
});
const { dataError } = await this.exec(code, timeout);
return !dataError;
}
async rm(filename, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.RM, {
path: filename
});
await this.exec(code);
const { dataError } = await this.exec(code, timeout);
return !dataError;
}
async rmdir(directory, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.RMDIR, {
path: directory path: directory
}); });
} else { const { dataError } = await this.exec(code, timeout);
code = Mustache.render(AmpyExt.LS, { return !dataError;
path: directory }
async cpdir(oldname, newname, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.CPDIR, {
oldPath: oldname,
newPath: newname
}); });
const { data, dataError } = await this.exec(code, timeout);
console.log(data, dataError)
return !dataError;
} }
const { data, dataError } = await this.exec(code, timeout);
if (dataError) { async cpfile(oldname, newname, timeout = 5000) {
return []; if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.CPFILE, {
oldPath: oldname,
newPath: newname
});
const { dataError } = await this.exec(code, timeout);
return !dataError;
}
async cwd(timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.CWD, {});
const { data, dataError } = await this.exec(code, timeout);
if (dataError) {
return '/';
}
return data;
}
getDevice() {
return this.#device_;
}
async dispose() {
this.#active_ = false;
if (this.#device_) {
await this.#device_.dispose();
this.#device_ = null;
}
if (this.#events_) {
this.#events_.reset();
this.#events_ = null;
}
} }
return JSON.parse(data.replaceAll('\'', '\"'));
} }
async mkdir(directory, timeout = 5000) { Web.Ampy = AmpyExt;
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.MKDIR, {
path: directory
});
const { dataError } = await this.exec(code, timeout);
return !dataError;
}
async mkfile(file, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.MKFILE, {
path: file
});
const { dataError } = await this.exec(code, timeout);
return !dataError;
}
async rename(oldname, newname, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.RENAME, {
oldPath: oldname,
newPath: newname
});
const { dataError } = await this.exec(code, timeout);
return !dataError;
}
async rm(filename, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.RM, {
path: filename
});
await this.exec(code);
const { dataError } = await this.exec(code, timeout);
return !dataError;
}
async rmdir(directory, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.RMDIR, {
path: directory
});
const { dataError } = await this.exec(code, timeout);
return !dataError;
}
async cpdir(oldname, newname, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.CPDIR, {
oldPath: oldname,
newPath: newname
});
const { data, dataError } = await this.exec(code, timeout);
console.log(data, dataError)
return !dataError;
}
async cpfile(oldname, newname, timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.CPFILE, {
oldPath: oldname,
newPath: newname
});
const { dataError } = await this.exec(code, timeout);
return !dataError;
}
async cwd(timeout = 5000) {
if (!this.isActive()) {
throw new Error(Msg.Lang['ampy.portIsNotOpen']);
}
const code = Mustache.render(AmpyExt.CWD, {});
const { data, dataError } = await this.exec(code, timeout);
if (dataError) {
return '/';
}
return data;
}
getDevice() {
return this.#device_;
}
async dispose() {
this.#active_ = false;
await this.#device_.dispose();
this.#device_ = null;
this.#events_.reset();
this.#events_ = null;
}
}
Web.Ampy = AmpyExt;
}); });

View File

@@ -11,38 +11,60 @@ export default class ShellArduino extends Shell {
async compile(config) { async compile(config) {
let arduino = _.merge({}, ARDUINO); let arduino = _.merge({}, ARDUINO);
arduino = _.merge(arduino, config); arduino = _.merge(arduino, config);
const command = [
`"${arduino.path.cli}"`, // 构建参数数组,避免 shell 解析引号问题
const args = [
'compile', 'compile',
'-b', arduino.key, '-b', arduino.key,
'--config-file', `"${arduino.path.config}"`, '--config-file', arduino.path.config,
'--verbose', '--verbose'
'--libraries', `"${arduino.path.libraries.join('","')}"`, ];
'--build-path', `"${arduino.path.build}"`,
'--output-dir', `"${arduino.path.output}"`, // 为每个 library 路径添加参数
`"${arduino.path.code}"`, // 注意config.json 中的路径是指向包含多个库的集合目录,所以必须用 --libraries
// 如果用 --libraryarduino-cli 会尝试把该目录当作单个库处理,导致找不到头文件报错
for (const lib of arduino.path.libraries) {
args.push('--libraries', lib);
}
args.push(
'--build-path', arduino.path.build,
'--output-dir', arduino.path.output,
arduino.path.code,
'--no-color' '--no-color'
].join(' '); );
return this.execUntilClosed(command, { maxBuffer: 4096 * 1000000 });
return this.execFileUntilClosed(arduino.path.cli, args, { maxBuffer: 4096 * 1000000 });
} }
async upload(config) { async upload(config) {
let arduino = _.merge({}, ARDUINO); let arduino = _.merge({}, ARDUINO);
arduino = _.merge(arduino, config); arduino = _.merge(arduino, config);
const command = [
`"${arduino.path.cli}"`, // 构建参数数组,避免 shell 解析引号问题
const args = [
'compile', 'compile',
'--upload', '--upload',
'-p', arduino.port, '-p', arduino.port,
'-b', arduino.key, '-b', arduino.key,
'--config-file', `"${arduino.path.config}"`, '--config-file', arduino.path.config,
'--verbose', '--verbose'
'--libraries', `"${arduino.path.libraries.join('","')}"`, ];
'--build-path', `"${arduino.path.build}"`,
'--output-dir', `"${arduino.path.output}"`, // 为每个 library 路径添加参数
`"${arduino.path.code}"`, // 注意config.json 中的路径是指向包含多个库的集合目录,所以必须用 --libraries
// 如果用 --libraryarduino-cli 会尝试把该目录当作单个库处理,导致找不到头文件报错
for (const lib of arduino.path.libraries) {
args.push('--libraries', lib);
}
args.push(
'--build-path', arduino.path.build,
'--output-dir', arduino.path.output,
arduino.path.code,
'--no-color' '--no-color'
].join(' '); );
return this.execUntilClosed(command, { maxBuffer: 4096 * 1000000 });
return this.execFileUntilClosed(arduino.path.cli, args, { maxBuffer: 4096 * 1000000 });
} }
} }

View File

@@ -173,6 +173,12 @@ export default class Socket {
}); });
socket.on('micropython.burn', async (config, callback) => { socket.on('micropython.burn', async (config, callback) => {
// 检查端口是否是有效的系统设备路径(而非前端内部标识符如 serial1
if (config.port && !config.port.startsWith('/dev/') && !config.port.includes(':')) {
console.log(`[MicroPython] 忽略无效端口请求: ${config.port},应使用本地 Web Serial API`);
callback([null, { code: 0, time: 0 }]);
return;
}
const shell = this.#shellMicroPython_.getItem(socket.id); const shell = this.#shellMicroPython_.getItem(socket.id);
const [error, result] = await to(shell.burn(config)); const [error, result] = await to(shell.burn(config));
error && Debug.error(error); error && Debug.error(error);
@@ -180,6 +186,12 @@ export default class Socket {
}); });
socket.on('micropython.upload', async (config, callback) => { socket.on('micropython.upload', async (config, callback) => {
// 检查端口是否是有效的系统设备路径(而非前端内部标识符如 serial1
if (config.port && !config.port.startsWith('/dev/') && !config.port.includes(':')) {
console.log(`[MicroPython] 忽略无效端口请求: ${config.port},应使用本地 Web Serial API`);
callback([null, { code: 0, time: 0 }]);
return;
}
const shell = this.#shellMicroPython_.getItem(socket.id); const shell = this.#shellMicroPython_.getItem(socket.id);
let { filePath = '', libraries = {} } = config; let { filePath = '', libraries = {} } = config;
filePath = MString.tpl(filePath, { filePath = MString.tpl(filePath, {
@@ -250,8 +262,16 @@ export default class Socket {
error2 && Debug.error(error2); error2 && Debug.error(error2);
let [error3,] = await to(fsExtra.outputFile(config.path.code, config.code)); let [error3,] = await to(fsExtra.outputFile(config.path.code, config.code));
error3 && Debug.error(error3); error3 && Debug.error(error3);
const [error, result] = await to(shell.upload(config)); const [error, result] = await to(shell.compile(config));
error && Debug.error(error); 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]); callback([error, result]);
}); });