Update: WebSocket支持MicroPython板卡烧录固件及上传程序

This commit is contained in:
王立帮
2024-12-02 21:50:56 +08:00
parent 4008e1aab5
commit a5c1ef4269
14 changed files with 379 additions and 521 deletions

View File

@@ -1,506 +1,278 @@
goog.loadJs('web', () => {
goog.require('layui');
goog.require('Mixly.Config');
goog.require('dayjs.duration');
goog.require('Mixly.Debug');
goog.require('Mixly.LayerExt');
goog.require('Mixly.Env');
goog.require('Mixly.Boards');
goog.require('Mixly.MFile');
goog.require('Mixly.MString');
goog.require('Mixly.Msg');
goog.require('Mixly.Env');
goog.require('Mixly.Config');
goog.require('Mixly.Workspace');
goog.require('Mixly.WebSocket.Serial');
goog.require('Mixly.WebSocket.Socket');
goog.provide('Mixly.WebSocket.BU');
const {
Config,
Debug,
LayerExt,
Config,
Msg,
Env,
Boards,
MFile,
MString,
Msg
Workspace,
WebSocket
} = Mixly;
const { BU, Serial, Socket } = Mixly.WebSocket;
const { BOARD, SELECTED_BOARD } = Config;
const { SELECTED_BOARD } = Config;
const { form } = layui;
const { Serial } = WebSocket;
BU.uploading = false;
BU.burning = false;
class WebSocketBU {
static {
this.mixlySocket = null;
this.socket = null;
this.shell = null;
BU.shell = null;
/**
* @function 根据传入的stdout判断磁盘数量并选择对应操作
* @param type {string} 值为'burn' | 'upload'
* @param stdout {string} 磁盘名称字符串,形如'G:K:F:'
* @param startPath {string} 需要拷贝的文件路径
* @return {void}
**/
BU.checkNumOfDisks = function (type, stdout, startPath) {
let wmicResult = stdout;
wmicResult = wmicResult.replace(/\s+/g, "");
wmicResult = wmicResult.replace("DeviceID", "");
// wmicResult = 'G:K:F:';
let result = wmicResult.split(':');
let pathAdd = (Env.currentPlatform === "win32") ? ':' : '';
if (stdout.indexOf(":") != stdout.lastIndexOf(":")) {
let form = layui.form;
let devicesName = $('#mixly-selector-type');
let oldDevice = $('#mixly-selector-type option:selected').val();
devicesName.empty();
for (let i = 0; i < result.length; i++) {
if (result[i]) {
if (oldDevice == result[i] + pathAdd) {
devicesName.append('<option value="' + result[i] + pathAdd + '" selected>' + result[i] + pathAdd + '</option>');
} else {
devicesName.append('<option value="' + result[i] + pathAdd + '">' + result[i] + pathAdd + '</option>');
}
}
this.getSocket = function () {
return this.socket;
}
form.render();
let initBtnClicked = false;
const layerNum = layer.open({
type: 1,
id: "serial-select",
title: Msg.Lang['检测到多个同类型设备,请选择:'],
area: ['350px', '150px'],
content: $('#mixly-selector-div'),
shade: LayerExt.SHADE_ALL,
resize: false,
closeBtn: 0,
success: function (layero) {
$('#serial-select').css('height', '195px');
$(".layui-layer-page").css("z-index","198910151");
$("#mixly-selector-btn1").off("click").click(() => {
layer.close(layerNum);
BU.cancel();
});
$("#mixly-selector-btn2").off("click").click(() => {
layer.close(layerNum);
initBtnClicked = true;
});
},
end: function () {
$('#mixly-selector-div').css('display', 'none');
$("#layui-layer-shade" + layerNum).remove();
if (initBtnClicked) {
BU.initWithDropdownBox(type, startPath);
}
$("#mixly-selector-btn1").off("click");
$("#mixly-selector-btn2").off("click");
}
});
} else {
const layerNum = layer.open({
type: 1,
title: (type === 'burn'? Msg.Lang['shell.burning'] : Msg.Lang['shell.uploading']) + '...',
content: $('#mixly-loader-div'),
shade: LayerExt.SHADE_NAV,
resize: false,
closeBtn: 0,
success: function (layero, index) {
BU.copyFiles(type, index, startPath, result[0] + pathAdd + '/');
$("#mixly-loader-btn").off("click").click(() => {
layer.close(index);
BU.cancel();
});
},
end: function () {
$('#mixly-selector-div').css('display', 'none');
$("#layui-layer-shade" + layerNum).remove();
$("#mixly-loader-btn").off("click");
}
});
}
}
BU.copyFiles = (type, layerNum, startPath, desPath) => {
const code = MFile.getCode();
const {
copyLib = false,
libPath = []
} = SELECTED_BOARD.upload;
Socket.sendCommand({
obj: 'BU',
func: 'copyFiles',
args: [ type, layerNum, startPath, desPath, code, copyLib, libPath ]
});
}
BU.operateSuccess = (type, layerNum, port) => {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
layer.close(layerNum);
const message = (type === 'burn'? Msg.Lang['shell.burnSucc'] : Msg.Lang['shell.uploadSucc']);
layer.msg(message, {
time: 1000
});
const value = statusBarTerminal.getValue();
let prefix = '';
if (value.lastIndexOf('\n') !== value.length - 1) {
prefix = '\n';
}
statusBarTerminal.addValue(prefix + `==${message}==\n`);
if (type === 'upload' && (Serial.uploadPorts.length === 1 || port)) {
Serial.connect(port ?? Serial.uploadPorts[0].name, null);
}
BU.burning = false;
BU.uploading = false;
}
BU.operateError = (type, layerNum, error) => {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
layer.close(layerNum);
const value = statusBarTerminal.getValue();
let prefix = '';
if (value.lastIndexOf('\n') !== value.length - 1) {
prefix = '\n';
}
statusBarTerminal.addValue(prefix + error + '\n');
console.log(error);
BU.burning = false;
BU.uploading = false;
}
BU.noDevice = () => {
layer.msg(Msg.Lang['statusbar.serial.noDevice'], {
time: 1000
});
BU.burning = false;
BU.uploading = false;
}
/**
* @function 判断当前环境,以开始一个上传过程
* @param type {string} 值为'burn' | 'upload'
* @param startPath {string} 需要拷贝的文件或文件夹的路径
* @return {void}
*/
BU.initWithDropdownBox = function (type, startPath) {
const layerNum = layer.open({
type: 1,
title: (type === 'burn'? Msg.Lang['shell.burning'] : Msg.Lang['shell.uploading']) + '...',
content: $('#mixly-loader-div'),
shade: LayerExt.SHADE_NAV,
resize: false,
closeBtn: 0,
success: function (layero, index) {
$(".layui-layer-page").css("z-index","198910151");
$("#mixly-loader-btn").off("click").click(() => {
layer.close(index);
BU.cancel();
});
const desPath = $('#mixly-selector-type option:selected').val();
BU.copyFiles(type, index, startPath, desPath);
},
end: function () {
$('#mixly-loader-div').css('display', 'none');
$("#layui-layer-shade" + layerNum).remove();
this.getMixlySocket = function () {
return this.mixlySocket;
}
});
}
/**
* @function 取消烧录或上传
* @return {void}
*/
BU.cancel = function () {
Socket.sendCommand({
obj: 'BU',
func: 'cancel',
args: []
});
if (BU.uploading) {
BU.uploading = false;
layer.msg(Msg.Lang['shell.uploadCanceled'], {
time: 1000
});
} else if (BU.burning) {
BU.burning = false;
layer.msg(Msg.Lang['shell.burnCanceled'], {
time: 1000
});
}
}
this.init = function (mixlySocket) {
this.mixlySocket = mixlySocket;
this.socket = mixlySocket.getSocket();
this.shell = new WebSocketBU();
const socket = this.socket;
/**
* @function 开始一个烧录过程
* @return {void}
*/
BU.initBurn = function () {
Socket.connect((WS) => {
layer.closeAll();
}, () => {
if (BU.burning) return;
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
const { burn } = SELECTED_BOARD;
statusBarTerminal.setValue('');
mainStatusBarTabs.changeTo("output");
mainStatusBarTabs.show();
BU.burning = true;
BU.uploading = false;
if (burn.type === 'volume') {
Socket.sendCommand({
obj: 'BU',
func: 'getDisksWithVolumesName',
args: [ 'burn', burn.volume, burn.filePath ]
});
} else {
const port = Serial.getSelectedPortName();
BU.burnWithPort(port, burn.command);
}
});
}
/**
* @function 开始一个上传过程
* @return {void}
*/
BU.initUpload = function () {
Socket.connect((WS) => {
layer.closeAll();
}, () => {
if (BU.uploading) return;
const { upload } = SELECTED_BOARD;
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue('');
mainStatusBarTabs.changeTo("output");
mainStatusBarTabs.show();
BU.burning = false;
BU.uploading = true;
if (upload.type === "volume") {
Socket.sendCommand({
obj: 'BU',
func: 'getDisksWithVolumesName',
args: [ 'upload', upload.volume, upload.filePath ]
});
} else {
const port = Serial.getSelectedPortName();
BU.uploadWithPort(port, upload.command);
}
});
}
/**
* @function 通过cmd烧录
* @param layerNum {number} 烧录或上传加载弹窗的编号,用于关闭此弹窗
* @param port {string} 所选择的串口
* @param command {string} 需要执行的指令
* @return {void}
*/
BU.burnByCmd = function (layerNum, port, command) {
const newCommand = MString.tpl(command, { com: port });
Socket.sendCommand({
obj: 'BU',
func: 'burnByCmd',
args: [ layerNum, port, newCommand ]
});
}
/**
* @function 通过cmd上传
* @param layerNum {number} 烧录或上传加载弹窗的编号,用于关闭此弹窗
* @param port {string} 所选择的串口
* @param command {string} 需要执行的指令
* @return {void}
*/
BU.uploadByCmd = function (layerNum, port, command) {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
const newCommand = MString.tpl(command, { com: port });
const { upload } = SELECTED_BOARD;
const {
filePath,
copyLib = false,
libPath = []
} = upload;
const code = MFile.getCode();
statusBarTerminal.addValue(Msg.Lang['shell.uploading'] + '...\n');
Socket.sendCommand({
obj: 'BU',
func: 'uploadByCmd',
args: [ layerNum, port, newCommand, code, filePath, copyLib, libPath ]
});
}
/**
* @function 特殊固件的烧录
* @return {void}
**/
BU.burnWithSpecialBin = () => {
const devNames = $('#mixly-selector-type');
let oldDevice = $('#mixly-selector-type option:selected').val();
devNames.empty();
let firmwareList = BOARD.burn.special;
let firmwareObj = {};
for (let i = 0; i < firmwareList.length; i++)
firmwareObj[firmwareList[i].name] = firmwareList[i].command;
firmwareList.map(firmware => {
if (!firmware?.name && !firmware?.command) return;
if (`${firmware.name}` == oldDevice) {
devNames.append($(`<option value="${firmware.name}" selected>${firmware.name}</option>`));
} else {
devNames.append($(`<option value="${firmware.name}">${firmware.name}</option>`));
}
});
form.render();
let initBtnClicked = false;
const layerNum = layer.open({
type: 1,
id: "serial-select",
title: "请选择固件:",
area: ['350px', '150px'],
content: $('#mixly-selector-div'),
shade: Mixly.LayerExt.SHADE_ALL,
resize: false,
closeBtn: 0,
success: function (layero) {
$('#serial-select').css('height', '180px');
$('#serial-select').css('overflow', 'inherit');
$(".layui-layer-page").css("z-index", "198910151");
$("#mixly-selector-btn1").off("click").click(() => {
layer.close(layerNum);
});
$("#mixly-selector-btn2").click(() => {
layer.close(layerNum);
initBtnClicked = true;
});
},
end: function () {
$("#mixly-selector-btn1").off("click");
$("#mixly-selector-btn2").off("click");
$('#mixly-selector-div').css('display', 'none');
$(".layui-layer-shade").remove();
if (initBtnClicked) {
let selectedFirmwareName = $('#mixly-selector-type option:selected').val();
try {
firmwareObj[selectedFirmwareName] = firmwareObj[selectedFirmwareName].replace(/\\/g, "/");
} catch (e) {
console.log(e);
}
let pyToolName = ["esptool", "kflash", "stm32loader", "stm32bl"];
let pyToolPath = "{path}/mixpyBuild/win_python3/Lib/site-packages/"
if (Env.currentPlatform == "darwin" || Env.currentPlatform == "linux") {
pyToolPath = "{path}/tools/python/";
}
for (let i = 0; i < pyToolName.length; i++) {
if (firmwareObj[selectedFirmwareName].indexOf("\"") != -1) {
firmwareObj[selectedFirmwareName] = replaceWithReg(firmwareObj[selectedFirmwareName], Env.python3Path + "\" \"" + pyToolPath + pyToolName[i] + ".py", pyToolName[i]);
} else {
firmwareObj[selectedFirmwareName] = replaceWithReg(firmwareObj[selectedFirmwareName], Env.python3Path + " " + pyToolPath + pyToolName[i] + ".py", pyToolName[i]);
}
}
firmwareObj[selectedFirmwareName] = replaceWithReg(firmwareObj[selectedFirmwareName], Env.clientPath, "path");
firmwareObj[selectedFirmwareName] = replaceWithReg(firmwareObj[selectedFirmwareName], Env.boardDirPath, "indexPath");
socket.on('micropython.dataEvent', (data) => {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue('');
mainStatusBarTabs.changeTo("output");
mainStatusBarTabs.show();
BU.burning = true;
BU.uploading = false;
const port = Serial.getSelectedPortName();
BU.burnWithPort(port, firmwareObj[selectedFirmwareName]);
} else {
layer.msg(Msg.Lang['shell.burnCanceled'], { time: 1000 });
}
statusBarTerminal.addValue(data);
});
socket.on('micropython.errorEvent', (data) => {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.addValue(data);
});
}
});
}
/**
* @function 通过串口执行命令行烧录或上传操作
* @param type {string} 值为 'burn' | 'upload'
* @param port {string} 所选择的串口
* @param command {string} 需要执行的指令
* @return {void}
**/
BU.operateWithPort = (type, port, command) => {
if (!port) {
layer.msg(Msg.Lang['statusbar.serial.noDevice'], {
time: 1000
});
BU.burning = false;
BU.uploading = false;
return;
}
const title = (type === 'burn' ? Msg.Lang['shell.burning'] : Msg.Lang['shell.uploading']) + '...';
const operate = () => {
const layerNum = layer.open({
type: 1,
title,
content: $('#mixly-loader-div'),
shade: LayerExt.SHADE_NAV,
resize: false,
closeBtn: 0,
success: function (layero, index) {
$(".layui-layer-page").css("z-index","198910151");
switch (type) {
case 'burn':
BU.burnByCmd(index, port, command);
break;
case 'upload':
default:
BU.uploadByCmd(index, port, command);
}
$("#mixly-loader-btn").off("click").click(() => {
$("#mixly-loader-btn").css('display', 'none');
switch (type) {
case 'burn':
layer.title(Msg.Lang['shell.aborting'] + '...', index);
break;
case 'upload':
default:
layer.title(Msg.Lang['shell.aborting'] + '...', index);
}
BU.cancel(type);
});
},
end: function () {
$('#mixly-loader-div').css('display', 'none');
$("layui-layer-shade" + layerNum).remove();
$("#mixly-loader-btn").off("click");
$("#mixly-loader-btn").css('display', 'inline-block');
this.initBurn = function () {
if (!this.mixlySocket.isConnected()) {
layer.msg('服务端已离线', { 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.burning']}...\n`);
const statusBarSerial = mainStatusBarTabs.getStatusBarById(port);
const closePromise = statusBarSerial ? statusBarSerial.close() : Promise.resolve();
closePromise
.then(() => {
return this.shell.burn(port);
})
.then((info) => {
this.endCallback(info.code, info.time);
})
.catch((error) => {
Debug.error(error);
statusBarTerminal.addValue(`\n==${Msg.Lang['shell.burnFailed']}==\n`);
});
}
this.initUpload = function () {
if (!this.mixlySocket.isConnected()) {
layer.msg('服务端已离线', { 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(() => {
return statusBarSerial.setBaudRate(115200);
})
.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.isBurning() ? Msg.Lang['shell.burnFailed'] : Msg.Lang['shell.uploadFailed']);
statusBarTerminal.addValue(`\n==${message}==\n`);
} else {
message = (this.shell.isBurning() ? Msg.Lang['shell.burnSucc'] : 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;
#layerNum_ = null;
constructor() {}
async burn(port) {
return new Promise(async (resolve, reject) => {
this.#running_ = true;
this.#upload_ = false;
await this.showProgress();
const config = {
boardDirPath: `.${Env.boardDirPath}`,
port,
command: SELECTED_BOARD.burn.command
};
const mixlySocket = WebSocketBU.getMixlySocket();
mixlySocket.emit('micropython.burn', config, (response) => {
this.hideProgress();
if (response.error) {
reject(response.error);
return;
}
const [error, result] = response;
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
operate();
async upload(port, code) {
return new Promise(async (resolve, reject) => {
this.#running_ = true;
this.#upload_ = true;
await this.showProgress();
const config = {
boardDirPath: `.${Env.boardDirPath}`,
command: SELECTED_BOARD.upload.command,
libPath: SELECTED_BOARD.upload.libPath,
filePath: SELECTED_BOARD.upload.filePath,
port, code
};
const mixlySocket = WebSocketBU.getMixlySocket();
mixlySocket.emit('micropython.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 = WebSocketBU.getMixlySocket();
mixlySocket.emit('micropython.kill', (response) => {
if (response.error) {
reject(response.error);
return;
}
const [error, result] = response;
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
async showProgress() {
return new Promise((resolve, reject) => {
this.#layerNum_ = layer.open({
type: 1,
title: `${this.isCompiling ? Msg.Lang['shell.burning'] : Msg.Lang['shell.uploading']}...`,
content: $('#mixly-loader-div'),
shade: LayerExt.SHADE_NAV,
resize: false,
closeBtn: 0,
success: () => {
$('#mixly-loader-btn').off('click').click(() => {
$('#mixly-loader-btn').css('display', 'none');
layer.title(`${Msg.Lang['shell.aborting']}...`, this.layerNum);
this.kill().catch(Debug.error);
});
resolve();
},
end: function () {
$('#mixly-loader-btn').off('click');
$('#mixly-loader-btn').css('display', 'inline-block');
}
});
})
}
hideProgress() {
layer.close(this.#layerNum_);
}
isUploading() {
return this.#running_ && this.#upload_;
}
isBurning() {
return this.#running_ && !this.#upload_;
}
}
/**
* @function 通过串口执行命令行烧录操作
* @param port {string} 所选择的串口
* @param command {string} 需要执行的指令
* @return {void}
**/
BU.burnWithPort = (port, command) => {
BU.operateWithPort('burn', port, command);
}
/**
* @function 通过串口执行命令行上传操作
* @param port {string} 所选择的串口
* @param command {string} 需要执行的指令
* @return {void}
**/
BU.uploadWithPort = (port, command) => {
BU.operateWithPort('upload', port, command);
}
BU.addValue = function (data) {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.addValue(data);
}
WebSocket.BU = WebSocketBU;
});