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

@@ -9,6 +9,7 @@ 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.require('Mixly.WebCompiler.ArduShell');
goog.provide('Mixly.WebSocket.ArduShell'); goog.provide('Mixly.WebSocket.ArduShell');
const { const {
@@ -18,9 +19,13 @@ const {
Msg, Msg,
Workspace, Workspace,
LayerProgress, LayerProgress,
WebSocket WebSocket,
WebCompiler = {}
} = Mixly; } = Mixly;
// 动态获取 WebCompiler.ArduShell用于本地上传
const getWebCompilerArduShell = () => Mixly.WebCompiler?.ArduShell;
const { Serial } = WebSocket; const { Serial } = WebSocket;
const { layer } = layui; const { layer } = layui;
@@ -46,6 +51,12 @@ class WebSocketArduShell {
this.shell = new WebSocketArduShell(); this.shell = new WebSocketArduShell();
const socket = this.socket; const socket = this.socket;
// 同时初始化 WebCompiler.ArduShell用于本地上传
const WebCompilerArduShell = getWebCompilerArduShell();
if (WebCompilerArduShell && !WebCompilerArduShell.getMixlySocket()) {
WebCompilerArduShell.init(mixlySocket);
}
socket.on('arduino.dataEvent', (data) => { socket.on('arduino.dataEvent', (data) => {
if (data.length > 1000) { if (data.length > 1000) {
return; return;
@@ -64,6 +75,16 @@ class WebSocketArduShell {
} catch (error) { } catch (error) {
Debug.error(error); Debug.error(error);
} }
// 过滤掉无关紧要的端口错误(因为已经在本地完成上传了)
if (typeof data === 'string' && (
data.includes('cannot open serial1') ||
data.includes('cannot open device "undefined"') ||
data.includes('No such file or directory, cannot open')
)) {
return;
}
statusBarTerminal.addValue(data); statusBarTerminal.addValue(data);
}); });
} }
@@ -92,6 +113,13 @@ class WebSocketArduShell {
} }
this.initUpload = function () { this.initUpload = function () {
// 委托给 WebCompiler.ArduShell 处理本地上传(使用 AVRUploader 或 esptool-js
// 服务器无法访问用户本地的串口设备,必须在浏览器端完成上传
const WebCompilerArduShell = getWebCompilerArduShell();
if (WebCompilerArduShell) {
return WebCompilerArduShell.initUpload();
}
if (!this.mixlySocket.isConnected()) { if (!this.mixlySocket.isConnected()) {
layer.msg(Msg.Lang['websocket.offline'], { time: 1000 }); layer.msg(Msg.Lang['websocket.offline'], { time: 1000 });
return; return;
@@ -152,8 +180,7 @@ class WebSocketArduShell {
statusBarTerminal.addValue(`\n==${message}==\n`); statusBarTerminal.addValue(`\n==${message}==\n`);
} else { } else {
message = (this.shell.isCompiling() ? Msg.Lang['shell.compileSucc'] : Msg.Lang['shell.uploadSucc']); message = (this.shell.isCompiling() ? Msg.Lang['shell.compileSucc'] : Msg.Lang['shell.uploadSucc']);
statusBarTerminal.addValue(`\n==${message}(${Msg.Lang['shell.timeCost']} ${ statusBarTerminal.addValue(`\n==${message}(${Msg.Lang['shell.timeCost']} ${dayjs.duration(time).format('HH:mm:ss.SSS')
dayjs.duration(time).format('HH:mm:ss.SSS')
})==\n`); })==\n`);
} }
layer.msg(message, { time: 1000 }); layer.msg(message, { time: 1000 });

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

@@ -228,9 +228,10 @@ class AmpyExt extends Ampy {
if (!await this.exitRawREPL()) { if (!await this.exitRawREPL()) {
throw new Error(Msg.Lang['ampy.exitRawREPLFailed']); throw new Error(Msg.Lang['ampy.exitRawREPLFailed']);
} }
/*if (!await this.exitREPL()) { // 发送 Ctrl+D 触发软复位,让设备执行上传的 main.py
if (!await this.exitREPL()) {
throw new Error(Msg.Lang['ampy.exitREPLFailed']); throw new Error(Msg.Lang['ampy.exitREPLFailed']);
}*/ }
await this.#device_.close(); await this.#device_.close();
this.#active_ = false; this.#active_ = false;
} }
@@ -417,12 +418,16 @@ class AmpyExt extends Ampy {
async dispose() { async dispose() {
this.#active_ = false; this.#active_ = false;
if (this.#device_) {
await this.#device_.dispose(); await this.#device_.dispose();
this.#device_ = null; this.#device_ = null;
}
if (this.#events_) {
this.#events_.reset(); this.#events_.reset();
this.#events_ = null; this.#events_ = null;
} }
} }
}
Web.Ampy = AmpyExt; 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]);
}); });