Files
mixly3/common/modules/mixly-modules/web/burn-upload.js

740 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
goog.loadJs('web', () => {
goog.require('path');
goog.require('BoardId');
goog.require('FSWrapper');
goog.require('DAPWrapper');
goog.require('PartialFlashing');
goog.require('esptooljs');
goog.require('CryptoJS');
goog.require('JSZip');
goog.require('Mixly.Env');
goog.require('Mixly.LayerExt');
goog.require('Mixly.Config');
goog.require('Mixly.MFile');
goog.require('Mixly.Boards');
goog.require('Mixly.Msg');
goog.require('Mixly.Workspace');
goog.require('Mixly.Debug');
goog.require('Mixly.HTMLTemplate');
goog.require('Mixly.MString');
goog.require('Mixly.LayerFirmware');
goog.require('Mixly.LayerProgress');
goog.require('Mixly.Web.Serial');
goog.require('Mixly.Web.Ampy');
goog.require('Mixly.Web.KFlash');
goog.require('Mixly.Web.SerialTransport');
goog.provide('Mixly.Web.BU');
const {
Env,
Web,
LayerExt,
Config,
MFile,
Boards,
Msg,
Workspace,
Debug,
HTMLTemplate,
MString,
LayerFirmware,
LayerProgress
} = Mixly;
const {
Serial,
BU,
Ampy,
KFlash,
SerialTransport
} = Web;
const { BOARD, SELECTED_BOARD } = Config;
const { ESPLoader } = esptooljs;
BU.uploading = false;
BU.burning = false;
BU.firmwareLayer = new LayerFirmware({
width: 400,
title: Msg.Lang['nav.btn.burn'],
cancelValue: false,
skin: 'layui-anim layui-anim-scale',
cancel: false,
cancelDisplay: false
});
BU.firmwareLayer.bind('burn', (info) => {
const { web } = SELECTED_BOARD;
BU.burnWithEsptool(info, web.burn.erase);
});
BU.progressLayer = new LayerProgress({
width: 200,
cancelValue: false,
skin: 'layui-anim layui-anim-scale',
cancel: false,
cancelDisplay: false
});
if (['BBC micro:bit', 'Mithon CC'].includes(BOARD.boardType)) {
FSWrapper.setupFilesystem(path.join(Env.boardDirPath, 'build'));
}
BU.requestPort = async () => {
await Serial.requestPort();
}
const readBinFile = (path, offset) => {
return new Promise((resolve, reject) => {
fetch(path)
.then((response) => {
return response.blob();
})
.then((blob) => {
const reader = new FileReader();
reader.onload = function (event) {
resolve({
address: parseInt(offset),
data: event.target.result
});
};
reader.onerror = function (error) {
throw(error);
}
reader.readAsBinaryString(blob);
})
.catch((error) => {
reject(error);
});
});
}
const readBinFileAsArrayBuffer = (path, offset) => {
return new Promise((resolve, reject) => {
fetch(path)
.then((response) => {
return response.blob();
})
.then((blob) => {
const reader = new FileReader();
reader.onload = function (event) {
resolve({
address: parseInt(offset),
data: event.target.result
});
};
reader.onerror = function (error) {
throw(error);
}
reader.readAsArrayBuffer(blob);
})
.catch((error) => {
reject(error);
});
});
}
const decodeKfpkgFromArrayBuffer = async(arrayBuf) => {
const zip = await JSZip.loadAsync(arrayBuf);
const manifestEntry = zip.file('flash-list.json');
if (!manifestEntry) {
throw new Error('kfpkg is missing flash-list.json');
}
const manifestText = await manifestEntry.async('string');
const manifest = JSON.parse(manifestText);
const items = [];
for (const f of manifest.files || []) {
const entry = zip.file(f.bin);
if (!entry) {
throw new Error(`Missing files in package: ${f.bin}`);
}
const data = new Uint8Array(await entry.async('uint8array'));
items.push({
address: f.address >>> 0,
filename: f.bin,
sha256Prefix: !!f.sha256Prefix,
data
});
}
return { manifest, items };
}
BU.initBurn = async () => {
if (['BBC micro:bit', 'Mithon CC'].includes(BOARD.boardType)) {
await BU.burnWithDAP();
} else if (['MixGo AI'].includes(BOARD.boardType)) {
const { web } = SELECTED_BOARD;
const boardKey = Boards.getSelectedBoardKey();
if (!web?.burn?.binFile) {
return;
}
if (typeof web.burn.binFile !== 'object') {
return;
}
await BU.burnWithKFlash(web.burn.binFile, web.burn.erase);
} else {
const { web } = SELECTED_BOARD;
const boardKey = Boards.getSelectedBoardKey();
if (!web?.burn?.binFile) {
return;
}
if (typeof web.burn.binFile !== 'object') {
return;
}
if (web.burn.special && web.burn.special instanceof Array) {
BU.burnWithSpecialBin();
} else {
await BU.burnWithEsptool(web.burn.binFile, web.burn.erase);
}
}
}
BU.burnWithDAP = async () => {
const { mainStatusBarTabs } = Mixly;
let portName = Serial.getSelectedPortName();
if (!portName) {
try {
await BU.requestPort();
portName = Serial.getSelectedPortName();
if (!portName) {
return;
}
} catch (error) {
Debug.error(error);
return;
}
}
const statusBarSerial = mainStatusBarTabs.getStatusBarById(portName);
if (statusBarSerial) {
await statusBarSerial.close();
}
const { web } = SELECTED_BOARD;
const { burn } = web;
const hexStr = goog.readFileSync(path.join(Env.boardDirPath, burn.filePath));
const hex2Blob = new Blob([ hexStr ], { type: 'text/plain' });
const buffer = await hex2Blob.arrayBuffer();
if (!buffer) {
layer.msg(Msg.Lang['shell.bin.readFailed'], { time: 1000 });
return;
}
BU.burning = true;
BU.uploading = false;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue(`${Msg.Lang['shell.burning']}...\n`);
mainStatusBarTabs.show();
mainStatusBarTabs.changeTo('output');
const port = Serial.getPort(portName);
const webUSB = new DAPjs.WebUSB(port);
const dapLink = new DAPjs.DAPLink(webUSB);
try {
await dapLink.connect();
await dapLink.setSerialBaudrate(115200);
} catch (error) {
Debug.error(error);
return;
}
let prevPercent = 0;
dapLink.on(DAPjs.DAPLink.EVENT_PROGRESS, progress => {
const nowPercent = Math.floor(progress * 100);
if (nowPercent > prevPercent) {
prevPercent = nowPercent;
} else {
return;
}
const nowProgressLen = Math.floor(nowPercent / 2);
const leftStr = new Array(nowProgressLen).fill('=').join('');
const rightStr = (new Array(50 - nowProgressLen).fill('-')).join('');
statusBarTerminal.addValue(`[${leftStr}${rightStr}] ${nowPercent}%\n`);
});
BU.progressLayer.title(`${Msg.Lang['shell.burning']}...`);
BU.progressLayer.show();
try {
await dapLink.flash(buffer);
BU.progressLayer.hide();
layer.msg(Msg.Lang['shell.burnSucc'], { time: 1000 });
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnSucc']}==\n`);
} catch (error) {
Debug.error(error);
BU.progressLayer.hide();
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnFailed']}==\n`);
} finally {
dapLink.removeAllListeners(DAPjs.DAPLink.EVENT_PROGRESS);
await dapLink.disconnect();
await webUSB.close();
await port.close();
}
}
BU.burnWithEsptool = async (binFile, erase) => {
const { mainStatusBarTabs } = Mixly;
let portName = Serial.getSelectedPortName();
if (!portName) {
try {
await BU.requestPort();
portName = Serial.getSelectedPortName();
if (!portName) {
return;
}
} catch (error) {
Debug.error(error);
return;
}
}
const port = Serial.getPort(portName);
if (['HIDDevice'].includes(port.constructor.name)) {
layer.msg(Msg.Lang['burn.notSupport'], { time: 1000 });
return;
}
const statusBarSerial = mainStatusBarTabs.getStatusBarById(portName);
if (statusBarSerial) {
await statusBarSerial.close();
}
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue(Msg.Lang['shell.burning'] + '...\n');
mainStatusBarTabs.show();
mainStatusBarTabs.changeTo('output');
BU.progressLayer.title(`${Msg.Lang['shell.burning']}...`);
BU.progressLayer.show();
let esploader = null;
let transport = null;
try {
const baudrate = Boards.getSelectedBoardConfigParam('BurnSpeed') ?? '460800';
const serial = new Serial(portName);
transport = new SerialTransport(serial, false);
esploader = new ESPLoader({
transport,
baudrate,
terminal: {
clean() {
statusBarTerminal.setValue(Msg.Lang['shell.burning'] + '...\n');
},
writeLine(data) {
statusBarTerminal.addValue(data + '\n');
},
write(data) {
statusBarTerminal.addValue(data);
}
}
});
await esploader.main();
} catch (error) {
Debug.error(error);
statusBarTerminal.addValue(`\n${error.toString()}\n`);
try {
await transport.disconnect();
} catch (error) {
Debug.error(error);
}
BU.progressLayer.hide();
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnFailed']}==\n`);
return;
}
statusBarTerminal.addValue(Msg.Lang['shell.bin.reading'] + "...");
let firmwarePromise = [];
statusBarTerminal.addValue("\n");
for (let i of binFile) {
if (i.path && i.offset) {
let absolutePath = path.join(Env.boardDirPath, i.path);
firmwarePromise.push(readBinFile(absolutePath, i.offset));
}
}
let data = null;
try {
data = await Promise.all(firmwarePromise);
} catch (error) {
statusBarTerminal.addValue("Failed!\n" + Msg.Lang['shell.bin.readFailed'] + "\n");
statusBarTerminal.addValue("\n" + error + "\n", true);
try {
await transport.disconnect();
} catch (error) {
Debug.error(error);
}
BU.progressLayer.hide();
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnFailed']}==\n`);
return;
}
statusBarTerminal.addValue("Done!\n");
BU.burning = true;
BU.uploading = false;
const flashOptions = {
fileArray: data,
flashSize: 'keep',
eraseAll: erase,
compress: true,
calculateMD5Hash: (image) => CryptoJS.MD5(CryptoJS.enc.Latin1.parse(image))
};
try {
await esploader.writeFlash(flashOptions);
await transport.setDTR(false);
await new Promise((resolve) => setTimeout(resolve, 100));
await transport.setDTR(true);
BU.progressLayer.hide();
layer.msg(Msg.Lang['shell.burnSucc'], { time: 1000 });
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnSucc']}==\n`);
} catch (error) {
Debug.error(error);
BU.progressLayer.hide();
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnFailed']}==\n`);
} finally {
try {
await transport.disconnect();
} catch (error) {
Debug.error(error);
}
}
}
BU.burnWithKFlash = async (binFile, erase) => {
const { mainStatusBarTabs } = Mixly;
let portName = Serial.getSelectedPortName();
if (!portName) {
try {
await BU.requestPort();
portName = Serial.getSelectedPortName();
if (!portName) {
return;
}
} catch (error) {
Debug.error(error);
return;
}
}
const port = Serial.getPort(portName);
if (['HIDDevice', 'USBDevice'].includes(port.constructor.name)) {
layer.msg(Msg.Lang['burn.notSupport'], { time: 1000 });
return;
}
const statusBarSerial = mainStatusBarTabs.getStatusBarById(portName);
if (statusBarSerial) {
await statusBarSerial.close();
}
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue(Msg.Lang['shell.burning'] + '...\n');
mainStatusBarTabs.show();
mainStatusBarTabs.changeTo('output');
BU.progressLayer.title(`${Msg.Lang['shell.burning']}...`);
BU.progressLayer.show();
let data = [];
try {
for (let i of binFile) {
if (i.path && i.offset) {
const extname = path.extname(i.path);
const absolutePath = path.join(Env.boardDirPath, i.path);
const info = await readBinFileAsArrayBuffer(absolutePath, i.offset);
if (extname === '.kfpkg') {
const result = await decodeKfpkgFromArrayBuffer(info.data);
data.push(...result.items);
} else {
data.push(info);
}
}
}
} catch (error) {
statusBarTerminal.addValue(`\n[ERROR] ${error}\n`, true);
BU.progressLayer.hide();
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnFailed']}==\n`);
return;
}
let serial = null;
try {
serial = new Serial(portName);
const kflash = new KFlash(serial);
kflash.bind('message', (message) => {
statusBarTerminal.addValue(message);
});
kflash.bind('replaceMessage', (lineNumber, message) => {
statusBarTerminal.replaceLine(lineNumber, message);
});
await kflash.enter();
for (let item of data) {
await kflash.write(item.data, item.address, item.sha256Prefix ?? true, item?.filename ?? 'main.bin');
}
BU.progressLayer.hide();
layer.msg(Msg.Lang['shell.burnSucc'], { time: 1000 });
statusBarTerminal.appendLine(`==${Msg.Lang['shell.burnSucc']}==\n`);
} catch (error) {
statusBarTerminal.appendLine(`[ERROR] ${error.message}\n`);
BU.progressLayer.hide();
statusBarTerminal.appendLine(`==${Msg.Lang['shell.burnFailed']}==\n`);
} finally {
try {
serial && await serial.close();
} catch (error) {
Debug.error(error);
}
}
}
BU.getImportModulesName = (code) => {
// 正则表达式: 匹配 import 或 from 导入语句
const importRegex = /(?:import\s+([a-zA-Z0-9_]+)|from\s+([a-zA-Z0-9_]+)\s+import)/g;
let imports = [];
let match;
while ((match = importRegex.exec(code)) !== null) {
if (match[1]) {
imports.push(match[1]); // 'import module'
}
if (match[2]) {
imports.push(match[2]); // 'from module import ...'
}
}
return imports;
}
BU.getImportModules = (code) => {
let importsMap = {};
const libPath = SELECTED_BOARD.upload.libPath;
for (let i = libPath.length - 1; i >= 0; i--) {
const dirname = MString.tpl(libPath[i], { indexPath: Env.boardDirPath });
const map = goog.readJsonSync(path.join(dirname, 'map.json'));
if (!(map && map instanceof Object)) {
continue;
}
for (let key in map) {
importsMap[key] = structuredClone(map[key]);
importsMap[key]['__path__'] = path.join(dirname, map[key]['__name__']);
}
}
let usedMap = {};
let currentImports = BU.getImportModulesName(code);
while (currentImports.length) {
let temp = [];
for (let moduleName of currentImports) {
let moduleInfo = importsMap[moduleName];
if (!moduleInfo) {
continue;
}
usedMap[moduleName] = moduleInfo;
const moduleImports = moduleInfo['__require__'];
if (!moduleImports) {
continue;
}
for (let name of moduleImports) {
if (usedMap[name] || !importsMap[name] || temp.includes(name)) {
continue;
}
temp.push(name);
}
}
currentImports = temp;
}
return usedMap;
}
BU.initUpload = async () => {
let portName = Serial.getSelectedPortName();
if (!portName) {
try {
await BU.requestPort();
portName = Serial.getSelectedPortName();
if (!portName) {
return;
}
} catch (error) {
Debug.error(error);
return;
}
}
if (['BBC micro:bit', 'Mithon CC'].includes(BOARD.boardType)) {
await BU.uploadWithDAP(portName);
} else {
await BU.uploadWithAmpy(portName);
}
}
BU.uploadWithDAP = async (portName) => {
const { mainStatusBarTabs } = Mixly;
if (!portName) {
try {
await BU.requestPort();
portName = Serial.getSelectedPortName();
if (!portName) {
return;
}
} catch (error) {
Debug.error(error);
return;
}
}
let statusBarSerial = mainStatusBarTabs.getStatusBarById(portName);
if (statusBarSerial) {
await statusBarSerial.close();
}
const port = Serial.getPort(portName);
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
const dapWrapper = new DAPWrapper(port, {
event: () => {},
log: () => {}
});
const partialFlashing = new PartialFlashing(dapWrapper, {
event: () => {}
});
let boardId = 0x9901;
const boardKey = Boards.getSelectedBoardKey();
if (boardKey === 'micropython:nrf51822:v2') {
boardId = 0x9903;
}
BU.burning = false;
BU.uploading = true;
statusBarTerminal.setValue(Msg.Lang['shell.uploading'] + '...\n');
mainStatusBarTabs.show();
mainStatusBarTabs.changeTo('output');
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const code = editor.getCode();
FSWrapper.writeFile('main.py', code);
const importsMap = BU.getImportModules(code);
for (let key in importsMap) {
const filename = importsMap[key]['__name__'];
const data = goog.readFileSync(importsMap[key]['__path__']);
FSWrapper.writeFile(filename, data);
}
BU.progressLayer.title(`${Msg.Lang['shell.uploading']}...`);
BU.progressLayer.show();
try {
let prevPercent = 0;
await partialFlashing.flashAsync(new BoardId(0x9900), FSWrapper, progress => {
const nowPercent = Math.floor(progress * 100);
if (nowPercent > prevPercent) {
prevPercent = nowPercent;
} else {
return;
}
const nowProgressLen = Math.floor(nowPercent / 2);
const leftStr = new Array(nowProgressLen).fill('=').join('');
const rightStr = (new Array(50 - nowProgressLen).fill('-')).join('');
statusBarTerminal.addValue(`[${leftStr}${rightStr}] ${nowPercent}%\n`);
});
BU.progressLayer.hide();
layer.msg(Msg.Lang['shell.uploadSucc'], { time: 1000 });
statusBarTerminal.addValue(`==${Msg.Lang['shell.uploadSucc']}==\n`);
if (!statusBarSerial) {
mainStatusBarTabs.add('serial', portName);
statusBarSerial = mainStatusBarTabs.getStatusBarById(portName);
}
statusBarSerial.setValue('');
mainStatusBarTabs.changeTo(portName);
await statusBarSerial.open();
} catch (error) {
await dapWrapper.disconnectAsync();
Debug.error(error);
BU.progressLayer.hide();
statusBarTerminal.addValue(`${error}\n`);
statusBarTerminal.addValue(`==${Msg.Lang['shell.uploadFailed']}==\n`);
}
}
BU.uploadWithAmpy = async (portName) => {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
let statusBarSerial = mainStatusBarTabs.getStatusBarById(portName);
BU.burning = false;
BU.uploading = true;
statusBarTerminal.setValue(`${Msg.Lang['shell.uploading']}...\n`);
mainStatusBarTabs.show();
mainStatusBarTabs.changeTo('output');
const mainWorkspace = Workspace.getMain();
const editor = mainWorkspace.getEditorsManager().getActive();
const port = Serial.getPort(portName);
BU.progressLayer.title(`${Msg.Lang['shell.uploading']}...`);
BU.progressLayer.show();
const serial = new Serial(portName);
const ampy = new Ampy(serial);
ampy.bind('message', (message) => {
statusBarTerminal.addValue(message);
});
ampy.bind('replaceMessage', (lineNumber, message) => {
statusBarTerminal.replaceLine(lineNumber, message);
});
const code = editor.getCode();
let closePromise = Promise.resolve();
if (statusBarSerial) {
closePromise = statusBarSerial.close();
}
try {
await closePromise;
await ampy.enter();
await ampy.put('main.py', code);
/*const importsMap = BU.getImportModules(code);
let libraries = {};
for (let key in importsMap) {
const filename = importsMap[key]['__name__'];
const data = goog.readFileSync(importsMap[key]['__path__']);
libraries[filename] = {
data,
size: importsMap[key]['__size__']
};
}
let cwd = await ampy.cwd();
const rootInfo = await ampy.ls(cwd);
let rootMap = {};
for (let item of rootInfo) {
rootMap[item[0]] = item[1];
}
if (cwd === '/') {
cwd = '';
}
if (libraries && libraries instanceof Object) {
for (let key in libraries) {
if (rootMap[`${cwd}/${key}`] !== undefined && rootMap[`${cwd}/${key}`] === libraries[key].size) {
statusBarTerminal.addValue(`Writing ${key} (Skipped)\n`);
continue;
}
await ampy.put(key, libraries[key].data);
}
}*/
await ampy.exit();
await ampy.dispose();
BU.progressLayer.hide();
layer.msg(Msg.Lang['shell.uploadSucc'], { time: 1000 });
statusBarTerminal.appendLine(`==${Msg.Lang['shell.uploadSucc']}==\n`);
if (!statusBarSerial) {
mainStatusBarTabs.add('serial', portName);
statusBarSerial = mainStatusBarTabs.getStatusBarById(portName);
}
statusBarSerial.setValue('');
mainStatusBarTabs.changeTo(portName);
await statusBarSerial.open();
} catch (error) {
ampy.dispose();
BU.progressLayer.hide();
Debug.error(error);
statusBarTerminal.appendLine(`[ERROR] ${error.message}\n`);
statusBarTerminal.appendLine(`==${Msg.Lang['shell.uploadFailed']}==\n`);
}
}
/**
* @function 特殊固件的烧录
* @return {void}
**/
BU.burnWithSpecialBin = () => {
const { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
const firmwares = SELECTED_BOARD.web.burn.special;
let menu = [];
let firmwareMap = {};
for (let firmware of firmwares) {
if (!firmware?.name && !firmware?.binFile) continue;
menu.push({
id: firmware.name,
text: firmware.name
});
firmwareMap[firmware.name] = firmware.binFile;
}
BU.firmwareLayer.setMap(firmwareMap);
BU.firmwareLayer.setMenu(menu);
BU.firmwareLayer.show();
}
});