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,32 +1,37 @@
goog.loadJs('web', () => {
goog.require('layui');
goog.require('dayjs.duration');
goog.require('Mixly.Boards');
goog.require('Mixly.Debug');
goog.require('Mixly.LayerExt');
goog.require('Mixly.Msg');
goog.require('Mixly.Workspace');
goog.require('Mixly.LayerProgress');
goog.require('Mixly.WebSocket.Serial');
goog.provide('Mixly.WebSocket.ArduShell');
goog.require('layui');
goog.require('dayjs.duration');
goog.require('Mixly.Boards');
goog.require('Mixly.Debug');
goog.require('Mixly.LayerExt');
goog.require('Mixly.Msg');
goog.require('Mixly.Workspace');
goog.require('Mixly.LayerProgress');
goog.require('Mixly.WebSocket.Serial');
goog.require('Mixly.WebCompiler.ArduShell');
goog.provide('Mixly.WebSocket.ArduShell');
const {
const {
Boards,
Debug,
LayerExt,
Msg,
Workspace,
LayerProgress,
WebSocket
} = Mixly;
WebSocket,
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 {
this.mixlySocket = null;
this.socket = null;
@@ -46,6 +51,12 @@ class WebSocketArduShell {
this.shell = new WebSocketArduShell();
const socket = this.socket;
// 同时初始化 WebCompiler.ArduShell用于本地上传
const WebCompilerArduShell = getWebCompilerArduShell();
if (WebCompilerArduShell && !WebCompilerArduShell.getMixlySocket()) {
WebCompilerArduShell.init(mixlySocket);
}
socket.on('arduino.dataEvent', (data) => {
if (data.length > 1000) {
return;
@@ -64,6 +75,16 @@ class WebSocketArduShell {
} catch (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);
});
}
@@ -92,6 +113,13 @@ class WebSocketArduShell {
}
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;
@@ -152,8 +180,7 @@ class WebSocketArduShell {
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')
statusBarTerminal.addValue(`\n==${message}(${Msg.Lang['shell.timeCost']} ${dayjs.duration(time).format('HH:mm:ss.SSS')
})==\n`);
}
layer.msg(message, { time: 1000 });
@@ -268,8 +295,8 @@ class WebSocketArduShell {
isCompiling() {
return this.#running_ && !this.#upload_;
}
}
}
WebSocket.ArduShell = WebSocketArduShell;
WebSocket.ArduShell = WebSocketArduShell;
});

View File

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

View File

@@ -1,24 +1,24 @@
goog.loadJs('web', () => {
goog.require('path');
goog.require('Mustache');
goog.require('Mixly.Env');
goog.require('Mixly.Events');
goog.require('Mixly.Msg');
goog.require('Mixly.Ampy');
goog.require('Mixly.Web');
goog.provide('Mixly.Web.Ampy');
goog.require('path');
goog.require('Mustache');
goog.require('Mixly.Env');
goog.require('Mixly.Events');
goog.require('Mixly.Msg');
goog.require('Mixly.Ampy');
goog.require('Mixly.Web');
goog.provide('Mixly.Web.Ampy');
const {
const {
Env,
Events,
Msg,
Ampy,
Web
} = Mixly;
} = Mixly;
class AmpyExt extends Ampy {
class AmpyExt extends Ampy {
static {
this.LS = goog.readFileSync(path.join(Env.templatePath, 'python/ls.py'));
this.LS_RECURSIVE = goog.readFileSync(path.join(Env.templatePath, 'python/ls-recursive.py'));
@@ -228,9 +228,10 @@ class AmpyExt extends Ampy {
if (!await this.exitRawREPL()) {
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']);
}*/
}
await this.#device_.close();
this.#active_ = false;
}
@@ -417,13 +418,17 @@ class AmpyExt extends Ampy {
async dispose() {
this.#active_ = false;
if (this.#device_) {
await this.#device_.dispose();
this.#device_ = null;
}
if (this.#events_) {
this.#events_.reset();
this.#events_ = null;
}
}
}
}
Web.Ampy = AmpyExt;
Web.Ampy = AmpyExt;
});

View File

@@ -11,38 +11,60 @@ export default class ShellArduino extends Shell {
async compile(config) {
let arduino = _.merge({}, ARDUINO);
arduino = _.merge(arduino, config);
const command = [
`"${arduino.path.cli}"`,
// 构建参数数组,避免 shell 解析引号问题
const args = [
'compile',
'-b', arduino.key,
'--config-file', `"${arduino.path.config}"`,
'--verbose',
'--libraries', `"${arduino.path.libraries.join('","')}"`,
'--build-path', `"${arduino.path.build}"`,
'--output-dir', `"${arduino.path.output}"`,
`"${arduino.path.code}"`,
'--config-file', arduino.path.config,
'--verbose'
];
// 为每个 library 路径添加参数
// 注意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'
].join(' ');
return this.execUntilClosed(command, { maxBuffer: 4096 * 1000000 });
);
return this.execFileUntilClosed(arduino.path.cli, args, { maxBuffer: 4096 * 1000000 });
}
async upload(config) {
let arduino = _.merge({}, ARDUINO);
arduino = _.merge(arduino, config);
const command = [
`"${arduino.path.cli}"`,
// 构建参数数组,避免 shell 解析引号问题
const args = [
'compile',
'--upload',
'-p', arduino.port,
'-b', arduino.key,
'--config-file', `"${arduino.path.config}"`,
'--verbose',
'--libraries', `"${arduino.path.libraries.join('","')}"`,
'--build-path', `"${arduino.path.build}"`,
'--output-dir', `"${arduino.path.output}"`,
`"${arduino.path.code}"`,
'--config-file', arduino.path.config,
'--verbose'
];
// 为每个 library 路径添加参数
// 注意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'
].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) => {
// 检查端口是否是有效的系统设备路径(而非前端内部标识符如 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 [error, result] = await to(shell.burn(config));
error && Debug.error(error);
@@ -180,6 +186,12 @@ export default class Socket {
});
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);
let { filePath = '', libraries = {} } = config;
filePath = MString.tpl(filePath, {
@@ -250,8 +262,16 @@ export default class Socket {
error2 && Debug.error(error2);
let [error3,] = await to(fsExtra.outputFile(config.path.code, config.code));
error3 && Debug.error(error3);
const [error, result] = await to(shell.upload(config));
error && Debug.error(error);
const [error, result] = await to(shell.compile(config));
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]);
});