Update: 更新 Web USB

This commit is contained in:
王立帮
2024-12-20 18:51:04 +08:00
parent 03211cb1b2
commit 8727b62cb2
17 changed files with 1428 additions and 583 deletions

View File

@@ -155,7 +155,7 @@
},
"language": "MicroPython"
},
/*{
{
"boardImg": "./boards/default/micropython_nrf51822_mithoncc/media/mithon_compressed.png",
"boardType": "Mithon CC",
"boardIndex": "./boards/default/micropython_nrf51822_mithoncc/index.xml",
@@ -178,7 +178,7 @@
"webSocket": true
},
"language": "MicroPython"
},*/
},
{
"boardImg": "./boards/default/arduino_esp8266/media/esp8266_compressed.png",
"boardType": "Arduino ESP8266",

View File

@@ -95,10 +95,6 @@
"path": "modules/web-modules/lazyload.js",
"provide": ["LazyLoad"],
"require": []
}, {
"path": "modules/web-modules/microbit-fs.umd.min.js",
"provide": ["microbitFs"],
"require": []
}, {
"path": "modules/web-modules/base64.min.js",
"provide": ["Base64"],
@@ -139,6 +135,30 @@
"path": "modules/web-modules/avr-uploader.min.js",
"provide": ["AvrUploader"],
"require": []
}, {
"path": "modules/web-modules/microbit/microbit-fs.umd.min.js",
"provide": ["microbitFs"],
"require": []
}, {
"path": "modules/web-modules/microbit/fs-wrapper.js",
"provide": ["FSWrapper"],
"require": ["microbitFs", "$", "path"]
}, {
"path": "modules/web-modules/microbit/board-id.js",
"provide": ["BoardId"],
"require": []
}, {
"path": "modules/web-modules/microbit/board-serial-info.js",
"provide": ["BoardSerialInfo"],
"require": ["BoardId"]
}, {
"path": "modules/web-modules/microbit/dap-wrapper.js",
"provide": ["DAPWrapper"],
"require": ["BoardSerialInfo", "DAPjs"]
}, {
"path": "modules/web-modules/microbit/partial-flashing.js",
"provide": ["PartialFlashing"],
"require": []
}, {
"path": "modules/web-modules/ace/ace.js",
"provide": ["ace"],

View File

@@ -8,7 +8,6 @@ goog.require('Mixly.MArray');
goog.require('Mixly.Boards');
goog.require('Mixly.XML');
goog.require('Mixly.LayerExt');
goog.require('Mixly.MicrobitFs');
goog.require('Mixly.Msg');
goog.provide('Mixly.MFile');
@@ -20,7 +19,6 @@ const {
Boards,
XML,
LayerExt,
MicrobitFs,
Msg,
MFile
} = Mixly;
@@ -108,15 +106,6 @@ MFile.getCode = (type) => {
}
}
MFile.getHex = () => {
const code = MFile.getCode();
return MicrobitFs.getHex(code);
}
MFile.loadHex = (hexStr) => {
MicrobitFs.loadHex('main.py', hexStr);
}
MFile.getMix = () => {
const mixDom = $(Blockly.Xml.workspaceToDom(Editor.blockEditor)),
version = SOFTWARE?.version ?? 'Mixly 2.0',

View File

@@ -1,151 +0,0 @@
/**
* Wrapper for microbit-fs and microbit-universal-hex to perform filesystem
* operations into two hex files.
* https://github.com/microbit-foundation/microbit-fs
* https://github.com/microbit-foundation/microbit-universal-hex
*/
goog.loadJs('common', () => {
'use strict';
goog.require('microbitFs');
goog.provide('fsWrapper');
/**
* @returns An object with the fs wrapper.
*/
var uPyFs = null;
var commonFsSize = 20 * 1024;
var passthroughMethods = [
'create',
'exists',
'getStorageRemaining',
'getStorageSize',
'getStorageUsed',
'getUniversalHex',
'ls',
'read',
'readBytes',
'remove',
'size',
'write',
];
/**
* Duplicates some of the methods from the MicropythonFsHex class by
* creating functions with the same name in this object.
*/
function duplicateMethods() {
passthroughMethods.forEach(function(method) {
fsWrapper[method] = function() {
return uPyFs[method].apply(uPyFs, arguments);
};
});
}
/**
* Fetches both MicroPython hexes and sets up the file system with the
* initial main.py
*/
fsWrapper.setupFilesystem = function() {
var uPyV1 = null;
var uPyV2 = null;
var deferred1 = $.get('../common/micropython/microbit-micropython-v1.hex', function(fileStr) {
uPyV1 = fileStr;
}).fail(function() {
console.error('Could not load the MicroPython v1 file.');
});
var deferred2 = $.get('../common/micropython/microbit-micropython-v2.hex', function(fileStr) {
uPyV2 = fileStr;
}).fail(function() {
console.error('Could not load the MicroPython v2 file.');
});
return $.when(deferred1, deferred2).done(function() {
if (!uPyV1 || !uPyV2) {
console.error('There was an issue loading the MicroPython Hex files.');
}
// TODO: We need to use ID 9901 for app compatibility, but can soon be changed to 9900 (as per spec)
uPyFs = new microbitFs.MicropythonFsHex([
{ hex: uPyV1, boardId: 0x9901 },
{ hex: uPyV2, boardId: 0x9903 },
], {
'maxFsSize': commonFsSize,
});
duplicateMethods();
});
};
/**
* @param {string} boardId String with the Board ID for the generation.
* @returns Uint8Array with the data for the given Board ID.
*/
fsWrapper.getBytesForBoardId = function(boardId) {
if (boardId == '9900' || boardId == '9901') {
return uPyFs.getIntelHexBytes(0x9901);
} else if (boardId == '9903' || boardId == '9904') {
return uPyFs.getIntelHexBytes(0x9903);
} else {
throw Error('Could not recognise the Board ID ' + boardId);
}
};
/**
* @param {string} boardId String with the Board ID for the generation.
* @returns ArrayBuffer with the Intel Hex data for the given Board ID.
*/
fsWrapper.getIntelHexForBoardId = function(boardId) {
if (boardId == '9900' || boardId == '9901') {
var hexStr = uPyFs.getIntelHex(0x9901);
} else if (boardId == '9903' || boardId == '9904') {
var hexStr = uPyFs.getIntelHex(0x9903);
} else {
throw Error('Could not recognise the Board ID ' + boardId);
}
// iHex is ASCII so we can do a 1-to-1 conversion from chars to bytes
var hexBuffer = new Uint8Array(hexStr.length);
for (var i = 0, strLen = hexStr.length; i < strLen; i++) {
hexBuffer[i] = hexStr.charCodeAt(i);
}
return hexBuffer.buffer;
};
/**
* Import the files from the provide hex string into the filesystem.
* If the import is successful this deletes all the previous files.
*
* @param {string} hexStr Hex (Intel or Universal) string with files to
* import.
* @return {string[]} Array with the filenames of all files imported.
*/
fsWrapper.importHexFiles = function(hexStr) {
var filesNames = uPyFs.importFilesFromHex(hexStr, {
overwrite: true,
formatFirst: true
});
if (!filesNames.length) {
throw new Error('The filesystem in the hex file was empty');
}
return filesNames;
};
/**
* Import an appended script from the provide hex string into the filesystem.
* If the import is successful this deletes all the previous files.
*
* @param {string} hexStr Hex (Intel or Universal) string with files to
* import.
* @return {string[]} Array with the filenames of all files imported.
*/
fsWrapper.importHexAppended = function(hexStr) {
var code = microbitFs.getIntelHexAppendedScript(hexStr);
if (!code) {
throw new Error('No appended code found in the hex file');
};
uPyFs.ls().forEach(function(fileName) {
uPyFs.remove(fileName);
});
uPyFs.write('main.py', code);
return ['main.py'];
};
});

View File

@@ -1,137 +0,0 @@
goog.loadJs('common', () => {
goog.require('fsWrapper');
goog.require('Mixly.Config');
goog.provide('Mixly.MicrobitFs');
const {
Config,
MicrobitFs
} = Mixly;
const { BOARD } = Config;
const { nav = {} } = BOARD;
MicrobitFs.init = () => {
fsWrapper.setupFilesystem()
.then(() => {
console.log('初始化成功');
})
.fail(() => {
console.log('初始化失败');
});
}
if (!nav.compile && nav.upload && nav.save?.hex)
MicrobitFs.init();
// Reset the filesystem and load the files from this hex file to the fsWrapper and editor
MicrobitFs.loadHex = (filename, hexStr) => {
var importedFiles = [];
// If hexStr is parsed correctly it formats the file system before adding the new files
try {
importedFiles = fsWrapper.importHexFiles(hexStr);
} catch (hexImportError) {
try {
importedFiles = fsWrapper.importHexAppended(hexStr);
} catch (appendedError) {
console.log(hexImportError.message);
}
}
// Check if imported files includes a main.py file
var code = '';
if (importedFiles.indexOf(filename) > -1) {
code = fsWrapper.read(filename);
} else {
alert('no ' + filename);
}
Editor.mainEditor.drag.full('NEGATIVE'); // 完全显示代码编辑器
Editor.codeEditor.setValue(code, -1);
}
// Function for adding file to filesystem
MicrobitFs.loadFileToFilesystem = (filename, fileBytes) => {
// For main.py confirm if the user wants to replace the editor content
if (filename === 'main.py') {
return;
}
try {
if (fsWrapper.exists(filename)) {
fsWrapper.remove(filename);
fsWrapper.create(filename, fileBytes);
} else {
fsWrapper.write(filename, fileBytes);
}
// Check if the filesystem has run out of space
var _ = fsWrapper.getUniversalHex();
} catch (e) {
if (fsWrapper.exists(filename)) {
fsWrapper.remove(filename);
}
return alert(filename + '\n' + e.message);
}
}
MicrobitFs.updateMainPy = (code) => {
try {
// Remove main.py if editor content is empty to download a hex file
// with MicroPython included (also includes the rest of the filesystem)
if (fsWrapper.exists('main.py')) {
fsWrapper.remove('main.py');
}
for (var i = 0; i < py_module.length; i++) {
if (fsWrapper.exists(py_module[i]['filename'])) {
fsWrapper.remove(py_module[i]['filename']);
}
}
if (code) {
fsWrapper.create('main.py', code);
}
var str = code;
var arrayObj = new Array();
str.trim().split("\n").forEach(function (v, i) {
arrayObj.push(v);
});
let moduleName = "";
for (var i = 0; i < arrayObj.length; i++) {
if (arrayObj[i].indexOf("from") == 0) {
moduleName = arrayObj[i].substring(4, arrayObj[i].indexOf("import"));
moduleName = moduleName.replace(/(^\s*)|(\s*$)/g, "");
if (fsWrapper.exists(moduleName + '.py'))
continue;
for (var j = 0; j < py_module.length; j++) {
if (py_module[j]['filename'] == moduleName + ".py") {
MicrobitFs.loadFileToFilesystem(py_module[j]['filename'], py_module[j]['code']);
}
}
} else if (arrayObj[i].indexOf("import") == 0) {
moduleName = arrayObj[i].substring(6);
moduleName = moduleName.replace(/(^\s*)|(\s*$)/g, "");
if (fsWrapper.exists(moduleName + '.py'))
continue;
for (var j = 0; j < py_module.length; j++) {
if (py_module[j]['filename'] == moduleName + ".py") {
MicrobitFs.loadFileToFilesystem(py_module[j]['filename'], py_module[j]['code']);
}
}
}
}
} catch (e) {
// We generate a user readable error here to be caught and displayed
throw new Error(e.message);
}
}
MicrobitFs.getHex = (code) => {
try {
MicrobitFs.updateMainPy(code);
return output = fsWrapper.getUniversalHex();
} catch (e) {
alert(e.message);
return null;
}
}
});

View File

@@ -186,7 +186,8 @@ class StatusBarsManager extends PagesManager {
}
});*/
if (['micropython', 'circuitpython'].includes(BOARD.language.toLowerCase())) {
if (['micropython', 'circuitpython'].includes(BOARD.language.toLowerCase())
&& !['BBC micro:bit', 'Mithon CC'].includes(BOARD.boardType)) {
menu.add({
weight: 2,
type: 'sep1',

View File

@@ -684,32 +684,12 @@
"Mixly.Boards",
"Mixly.XML",
"Mixly.LayerExt",
"Mixly.MicrobitFs",
"Mixly.Msg"
],
"provide": [
"Mixly.MFile"
]
},
{
"path": "/common/microbit-fs-wrapper.js",
"require": [
"microbitFs"
],
"provide": [
"fsWrapper"
]
},
{
"path": "/common/microbit-fs.js",
"require": [
"fsWrapper",
"Mixly.Config"
],
"provide": [
"Mixly.MicrobitFs"
]
},
{
"path": "/common/mixly.js",
"require": [],
@@ -1558,6 +1538,9 @@
"path": "/web/burn-upload.js",
"require": [
"path",
"FSWrapper",
"DAPWrapper",
"PartialFlashing",
"ESPTool",
"AdafruitESPTool",
"CryptoJS",
@@ -1639,22 +1622,6 @@
"Mixly.Web.HID"
]
},
{
"path": "/web/lms.js",
"require": [
"saveAs",
"Blob",
"Blockly",
"Mixly.MFile",
"Mixly.Config",
"Mixly.MicrobitFs",
"Mixly.LocalStorage",
"Mixly.Web.File"
],
"provide": [
"Mixly.Web.Lms"
]
},
{
"path": "/web/serial.js",
"require": [

View File

@@ -1,6 +1,10 @@
goog.loadJs('web', () => {
goog.require('path');
goog.require('BoardId');
goog.require('FSWrapper');
goog.require('DAPWrapper');
goog.require('PartialFlashing');
goog.require('ESPTool');
goog.require('AdafruitESPTool');
goog.require('CryptoJS');
@@ -58,6 +62,10 @@ BU.FILMWARE_LAYER = new HTMLTemplate(
const BAUD = goog.platform() === 'darwin' ? 460800 : 921600;
if (['BBC micro:bit', 'Mithon CC'].includes(BOARD.boardType)) {
FSWrapper.setupFilesystem(path.join(Env.boardDirPath, 'build'));
}
BU.requestPort = async () => {
await Serial.requestPort();
}
@@ -136,85 +144,93 @@ BU.initBurn = () => {
}
}
BU.burnByUSB = () => {
const portName = 'web-usb';
Serial.connect(portName, 115200, async (port) => {
if (!port) {
return;
}
let portObj = Serial.portsOperator[portName];
const { toolConfig, serialport } = portObj;
const prevBaud = toolConfig.baudRates;
if (prevBaud !== 115200) {
toolConfig.baudRates = 115200;
await serialport.setBaudRate(toolConfig.baudRates);
}
const { web } = SELECTED_BOARD;
const { burn } = web;
const hexStr = goog.get(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 { mainStatusBarTabs } = Mixly;
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
statusBarTerminal.setValue(Msg.Lang['shell.burning'] + '...\n');
mainStatusBarTabs.show();
mainStatusBarTabs.changeTo('output');
const layerNum = layer.open({
type: 1,
title: Msg.Lang['shell.burning'] + '...',
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").hide();
let prevPercent = 0;
Serial.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`);
});
Serial.flash(buffer)
.then(() => {
layer.close(index);
layer.msg(Msg.Lang['shell.burnSucc'], { time: 1000 });
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnSucc']}==\n`);
})
.catch((error) => {
console.log(error);
layer.close(index);
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnFailed']}==\n`);
})
.finally(async () => {
BU.burning = false;
BU.uploading = false;
if (toolConfig.baudRates !== prevBaud) {
toolConfig.baudRates = prevBaud;
await serialport.setBaudRate(prevBaud);
}
Serial.DAPLink.removeAllListeners(DAPjs.DAPLink.EVENT_PROGRESS);
});
},
end: function () {
$("#mixly-loader-btn").css('display', 'inline-block');
$('#mixly-loader-div').css('display', 'none');
$("#layui-layer-shade" + layerNum).remove();
BU.burnByUSB = 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.get(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`);
});
layer.open({
type: 1,
title: `${Msg.Lang['shell.burning']}...`,
content: $('#mixly-loader-div'),
shade: LayerExt.SHADE_NAV,
resize: false,
closeBtn: 0,
success: async (layero, index) => {
$('#mixly-loader-btn').hide();
try {
await dapLink.flash(buffer);
layer.close(index);
layer.msg(Msg.Lang['shell.burnSucc'], { time: 1000 });
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnSucc']}==\n`);
} catch (error) {
Debug.error(error);
layer.close(index);
statusBarTerminal.addValue(`==${Msg.Lang['shell.burnFailed']}==\n`);
} finally {
dapLink.removeAllListeners(DAPjs.DAPLink.EVENT_PROGRESS);
await dapLink.disconnect();
await webUSB.close();
await port.close();
}
},
end: function () {
$('#mixly-loader-btn').css('display', 'inline-block');
$('#mixly-loader-div').css('display', 'none');
}
});
}
@@ -299,7 +315,7 @@ BU.burnWithEsptool = async (binFile, erase) => {
compress: true,
calculateMD5Hash: (image) => CryptoJS.MD5(CryptoJS.enc.Latin1.parse(image))
};
const layerNum = layer.open({
layer.open({
type: 1,
title: Msg.Lang['shell.burning'] + '...',
content: $('#mixly-loader-div'),
@@ -410,7 +426,7 @@ BU.burnWithAdafruitEsptool = async (binFile, erase) => {
statusBarTerminal.addValue("Done!\n");
BU.burning = true;
BU.uploading = false;
const layerNum = layer.open({
layer.open({
type: 1,
title: Msg.Lang['shell.burning'] + '...',
content: $('#mixly-loader-div'),
@@ -525,7 +541,86 @@ BU.initUpload = async () => {
return;
}
}
BU.uploadWithAmpy(portName);
if (['BBC micro:bit', 'Mithon CC'].includes(BOARD.boardType)) {
BU.uploadByUSB(portName);
} else {
BU.uploadWithAmpy(portName);
}
}
BU.uploadByUSB = async (portName) => {
const { mainStatusBarTabs } = Mixly;
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 port = Serial.getPort(portName);
const statusBarTerminal = mainStatusBarTabs.getStatusBarById('output');
const dapWrapper = new DAPWrapper(port, {
event: (data) => {
console.log(data);
},
log: () => {}
});
const partialFlashing = new PartialFlashing(dapWrapper, {
event: (data) => {
console.log(data);
}
});
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);
layer.open({
type: 1,
title: Msg.Lang['shell.uploading'] + '...',
content: $('#mixly-loader-div'),
shade: LayerExt.SHADE_NAV,
resize: false,
closeBtn: 0,
success: async function (layero, index) {
try {
await partialFlashing.flashAsync(new BoardId(0x9900), FSWrapper, () => {});
layer.close(index);
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();
layer.close(index);
console.error(error);
statusBarTerminal.addValue(`${error}\n`);
statusBarTerminal.addValue(`==${Msg.Lang['shell.uploadFailed']}==\n`);
}
BU.burning = false;
BU.uploading = false;
}
});
}
BU.uploadWithAmpy = (portName) => {
@@ -547,7 +642,7 @@ BU.uploadWithAmpy = (portName) => {
useBuffer = true;
dataLength = 30;
}
const layerNum = layer.open({
layer.open({
type: 1,
title: Msg.Lang['shell.uploading'] + '...',
content: $('#mixly-loader-div'),

View File

@@ -119,9 +119,9 @@ class WebHID extends Serial {
async #addReadEventListener_() {
this.#device_.oninputreport = (event) => {
const { data, reportId } = event;
const length = Math.min(data.getUint8(0), data.byteLength);
const length = Math.min(data.getUint8(0) + 1, data.byteLength);
let buffer = [];
for (let i = 1; i <= length; i++) {
for (let i = 1; i < length; i++) {
buffer.push(data.getUint8(i));
}
this.onBuffer(buffer);

View File

@@ -1,139 +0,0 @@
goog.loadJs('web', () => {
goog.require('saveAs');
goog.require('Blob');
goog.require('Blockly');
goog.require('Mixly.MFile');
goog.require('Mixly.Config');
goog.require('Mixly.MicrobitFs');
goog.require('Mixly.LocalStorage');
goog.require('Mixly.Web.File');
goog.provide('Mixly.Web.Lms');
const {
Web,
MFile,
Config,
MicrobitFs,
LocalStorage
} = Mixly;
const { File, Lms } = Web;
const { BOARD } = Config;
const DOM_STR = `
<li class="layui-nav-item" lay-unselect="">
<a href="#" class="icon-upload">保存到教学平台</a>
</li>
`;
Lms.getUrlParam = function(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); // 构造一个含有目标参数的正则表达式对象
var r = window.location.search.substr(1).match(reg); // 匹配目标参数
if (r != null) return unescape(r[2]); return null; // 返回参数值
}
Lms.save2moodle = function() {
var id = Lms.getUrlParam('id');
var hash = Lms.getUrlParam('hash');
var userid = Lms.getUrlParam('userid');
var taskid = Lms.getUrlParam('taskid');
if (id == null || hash == null || userid == null) {
alert('参数有误,请检查(请从作业进入)');
return false;
}
var data = '';
data = MFile.getCode();
type = 'py';
var xml = Blockly.Xml.workspaceToDom(Mixly.Editor.blockEditor);
data = Blockly.Xml.domToText(xml);
type = 'xml';
$.post('../../post_server_js.php', { unionid: id, hash: hash, userid: userid, content: data, type: type }, function (result) {
var json = eval('(' + result + ')');
alert(json.result);
});
}
Lms.loadfrommoodle = function() {
// 当有localStorage缓存时不从api接口中读取数据否则api读取后会存在localStorage中重复显示出来 add by qiang 20180521
var xml_str = LocalStorage.get(BOARD.boardType);
var pattern = /<xml[\w\W]*?>(.*)<\/xml>/i
var code = pattern.exec(xml_str)
if (code != null && code[1] != '') {
console.log(code[1]);
console.log('read from localStorage');
return false;
}
var data = '';
var type = 'xml';
var id = Lms.getUrlParam('id');
var hash = Lms.getUrlParam('hash');
var userid = Lms.getUrlParam('userid');
var taskid = Lms.getUrlParam('taskid');
if (id == null || hash == null || userid == null) {
// alert('参数有误,请检查');
return false;
}
$.post('../../get_content_microbitpy.php', { unionid: id, hash: hash, userid: userid, content: data }, function (result) {
const { blockEditor } = Editor;
if (result == '') {
return;
} else {
var count = blockEditor.getAllBlocks().length;
if (count) {
blockEditor.clear();
}
type = result.substr(0, 3);
data = result.substr(3);
}
File.parseData(`.${type}`, data);
var selectFile = document.getElementById('select_file');
if (selectFile != null) {
$("#select_file").remove();
$("#select_file_wrapper").remove();
selectFile = document.getElementById('select_file');
}
if (selectFile == null) {
var selectFileDom = document.createElement('INPUT');
selectFileDom.type = 'file';
selectFileDom.id = 'select_file';
var selectFileWrapperDom = document.createElement('DIV');
selectFileWrapperDom.id = 'select_file_wrapper';
selectFileWrapperDom.style.display = 'none';
selectFileWrapperDom.appendChild(selectFileDom);
document.body.appendChild(selectFileWrapperDom);
selectFile = document.getElementById('select_file');
}
selectFile.click();
});
}
Lms.save2hex = function() {
const code = MFile.getCode();
const output = MicrobitFs.getHex(code);
var blob = new Blob([output], { type: 'text/xml' });
saveAs(blob, 'blockduino.hex');
}
Lms.changeState = function() {
var id = Lms.getUrlParam('id');
var hash = Lms.getUrlParam('hash');
var userid = Lms.getUrlParam('userid');
var taskid = Lms.getUrlParam('taskid');
if (id == null || hash == null || userid == null) {
return false;
}
const $dom = $(DOM_STR);
$dom.find('a').off().click(() => {
Lms.save2moodle();
})
$('#nav #nav-right-btn-list').append($dom);
Lms.loadfrommoodle();
}
});

View File

@@ -91,6 +91,7 @@ class USB extends Serial {
});
navigator?.usb?.addEventListener('disconnect', (event) => {
event.device.onclose && event.device.onclose();
this.removePort(event.device);
this.refreshPorts();
});
@@ -122,13 +123,29 @@ class USB extends Serial {
}
#addReadEventListener_() {
this.#dapLink_.on(DAPjs.DAPLink.EVENT_SERIAL_DATA, data => {
const str = data.split('');
for (let i = 0; i < str.length; i++) {
this.onChar(str[i]);
this.#reader_ = this.#startSerialRead_();
this.#device_.onclose = () => {
if (!this.isOpened()) {
return;
}
});
this.#dapLink_.startSerialRead(this.#device_);
super.close();
this.#stringTemp_ = '';
this.onClose(1);
}
}
async #startSerialRead_(serialDelay = 10, autoConnect = false) {
this.#dapLink_.serialPolling = true;
while (this.#dapLink_.serialPolling) {
const data = await this.#dapLink_.serialRead();
if (data !== undefined) {
const numberArray = Array.prototype.slice.call(new Uint8Array(data));
this.onBuffer(numberArray);
}
await new Promise(resolve => setTimeout(resolve, serialDelay));
}
}
async open(baud) {
@@ -156,9 +173,10 @@ class USB extends Serial {
return;
}
super.close();
this.#dapLink_.removeAllListeners(DAPjs.DAPLink.EVENT_SERIAL_DATA);
this.#dapLink_.stopSerialRead();
await this.#dapLink_.stopSerialRead();
if (this.#reader_) {
await this.#reader_;
}
await this.#dapLink_.disconnect();
this.#dapLink_ = null;
await this.#webUSB_.close();
@@ -172,7 +190,7 @@ class USB extends Serial {
if (!this.isOpened() || this.getBaudRate() === baud) {
return;
}
await this.setSerialBaudrate(baud);
await this.#dapLink_.setSerialBaudrate(baud);
await super.setBaudRate(baud);
}
@@ -204,19 +222,24 @@ class USB extends Serial {
return this.setDTRAndRTS(this.getDTR(), rts);
}
onChar(char) {
super.onChar(char);
if (['\r', '\n'].includes(char)) {
super.onString(this.#stringTemp_);
this.#stringTemp_ = '';
} else {
this.#stringTemp_ += char;
}
const buffer = this.encode(char);
onBuffer(buffer) {
super.onBuffer(buffer);
for (let i = 0; i < buffer.length; i++) {
super.onByte(buffer[i]);
}
const string = this.decodeBuffer(buffer);
if (!string) {
return;
}
for (let char of string) {
super.onChar(char);
if (['\r', '\n'].includes(char)) {
super.onString(this.#stringTemp_);
this.#stringTemp_ = '';
} else {
this.#stringTemp_ += char;
}
}
}
}

View File

@@ -0,0 +1,66 @@
(() => {
/**
* (c) 2021, Micro:bit Educational Foundation and contributors
*
* SPDX-License-Identifier: MIT
*/
/**
* Validates micro:bit board IDs.
*/
class BoardId {
static v1Normalized = new BoardId(0x9900);
static v2Normalized = new BoardId(0x9903);
constructor(id) {
this.id = id;
if (!this.isV1() && !this.isV2()) {
throw new Error(`Could not recognise the Board ID ${id.toString(16)}`);
}
}
isV1() {
return this.id === 0x9900 || this.id === 0x9901;
}
isV2() {
return (
this.id === 0x9903 ||
this.id === 0x9904 ||
this.id === 0x9905 ||
this.id === 0x9906
);
}
/**
* Return the board ID using the default ID for the board type.
* Used to integrate with MicropythonFsHex.
*/
normalize() {
return this.isV1() ? BoardId.v1Normalized : BoardId.v2Normalized;
}
/**
* toString matches the input to parse.
*
* @returns the ID as a string.
*/
toString() {
return this.id.toString(16);
}
/**
* @param value The ID as a hex string with no 0x prefix (e.g. 9900).
* @returns the valid board ID
* @throws if the ID isn't known.
*/
static parse(value) {
return new BoardId(parseInt(value, 16));
}
}
window.BoardId = BoardId;
})();

View File

@@ -0,0 +1,41 @@
(() => {
/**
* (c) 2021, Micro:bit Educational Foundation and contributors
*
* SPDX-License-Identifier: MIT
*/
class BoardSerialInfo {
constructor(id, familyId, hic) {
this.id = id;
this.familyId = familyId;
this.hic = hic;
}
static parse(device, log) {
const serial = device.serialNumber;
if (!serial) {
throw new Error("Could not detected ID from connected board.");
}
if (serial.length !== 48) {
log(`USB serial number unexpected length: ${serial.length}`);
}
const id = serial.substring(0, 4);
const familyId = serial.substring(4, 8);
const hic = serial.slice(-8);
return new BoardSerialInfo(BoardId.parse(id), familyId, hic);
}
eq(other) {
return (
other.id === this.id &&
other.familyId === this.familyId &&
other.hic === this.hic
);
}
}
window.BoardSerialInfo = BoardSerialInfo;
})();

View File

@@ -0,0 +1,488 @@
(() => {
/**
* (c) 2021, Micro:bit Educational Foundation and contributors
*
* SPDX-License-Identifier: MIT
*/
// https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/dap/constants.ts
// https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/cortex/constants.ts
// CRA's build tooling doesn't support const enums so we've converted them to prefixed constants here.
// If we move this to a separate library then we can replace them.
// In the meantime we should prune the list below to what we actually use.
// FICR Registers
const FICR = {
CODEPAGESIZE: 0x10000000 | 0x10,
CODESIZE: 0x10000000 | 0x14,
};
const DapCmd = {
DAP_INFO: 0x00,
DAP_CONNECT: 0x02,
DAP_DISCONNECT: 0x03,
DAP_TRANSFER: 0x05,
DAP_TRANSFER_BLOCK: 0x06,
// Many more.
};
const Csw = {
CSW_SIZE: 0x00000007,
CSW_SIZE32: 0x00000002,
CSW_ADDRINC: 0x00000030,
CSW_SADDRINC: 0x00000010,
CSW_DBGSTAT: 0x00000040,
CSW_HPROT: 0x02000000,
CSW_MSTRDBG: 0x20000000,
CSW_RESERVED: 0x01000000,
CSW_VALUE: -1, // see below
// Many more.
};
Csw.CSW_VALUE = Csw.CSW_RESERVED | Csw.CSW_MSTRDBG | Csw.CSW_HPROT | Csw.CSW_DBGSTAT | Csw.CSW_SADDRINC;
const DapVal = {
AP_ACC: 1 << 0,
READ: 1 << 1,
WRITE: 0 << 1,
// More.
};
const ApReg = {
CSW: 0x00,
TAR: 0x04,
DRW: 0x0c,
// More.
};
const CortexSpecialReg = {
// Debug Exception and Monitor Control Register
DEMCR: 0xe000edfc,
// DWTENA in armv6 architecture reference manual
DEMCR_VC_CORERESET: 1 << 0,
// CPUID Register
CPUID: 0xe000ed00,
// Debug Halting Control and Status Register
DHCSR: 0xe000edf0,
S_RESET_ST: 1 << 25,
NVIC_AIRCR: 0xe000ed0c,
NVIC_AIRCR_VECTKEY: 0x5fa << 16,
NVIC_AIRCR_SYSRESETREQ: 1 << 2,
// Many more.
};
// Returns a representation of an Access Port Register.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/util.ts#L63
const apReg = (r, mode) => {
const v = r | mode | DapVal.AP_ACC;
return 4 + ((v & 0x0c) >> 2);
};
// Returns a code representing a request to read/write a certain register.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/util.ts#L92
const regRequest = (regId, isWrite = false) => {
let request = !isWrite ? 1 << 1 /* READ */ : 0 << 1; /* WRITE */
if (regId < 4) {
request |= 0 << 0 /* DP_ACC */;
} else {
request |= 1 << 0 /* AP_ACC */;
}
request |= (regId & 3) << 2;
return request;
};
const bufferConcat = (bufs) => {
let len = 0;
for (const b of bufs) {
len += b.length;
}
const r = new Uint8Array(len);
len = 0;
for (const b of bufs) {
r.set(b, len);
len += b.length;
}
return r;
};
class DAPWrapper {
constructor(device, logging) {
this.device = device;
this.logging = logging;
this.initialConnectionComplete = true;
this.loggedBoardSerialInfo = null;
this._pageSize = 0;
this._numPages = 0;
this.transport = new DAPjs.WebUSB(this.device);
this.daplink = new DAPjs.DAPLink(this.transport);
this.cortexM = new DAPjs.CortexM(this.transport);
}
log(v) {
//console.log(v);
}
/**
* The page size. Throws if we've not connected.
*/
pageSize() {
if (this._pageSize === undefined) {
throw new Error("pageSize not defined until connected");
}
return this._pageSize;
}
/**
* The number of pages. Throws if we've not connected.
*/
numPages() {
if (this._numPages === undefined) {
throw new Error("numPages not defined until connected");
}
return this._numPages;
}
boardSerialInfo() {
return BoardSerialInfo.parse(
this.device,
this.logging.log.bind(this.logging)
);
}
// Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L119
async reconnectAsync() {
if (this.initialConnectionComplete) {
await this.disconnectAsync();
this.transport = new DAPjs.WebUSB(this.device);
this.daplink = new DAPjs.DAPLink(this.transport);
this.cortexM = new DAPjs.CortexM(this.transport);
} else {
this.initialConnectionComplete = true;
}
await this.daplink.connect();
await this.cortexM.connect();
this.logging.event({
type: "WebUSB-info",
message: "connected",
});
const serialInfo = this.boardSerialInfo();
this.log(`Detected board ID ${serialInfo.id}`);
if (
!this.loggedBoardSerialInfo ||
!this.loggedBoardSerialInfo.eq(this.boardSerialInfo())
) {
this.loggedBoardSerialInfo = this.boardSerialInfo();
this.logging.event({
type: "WebUSB-info",
message: "board-id/" + this.boardSerialInfo().id,
});
this.logging.event({
type: "WebUSB-info",
message:
"board-family-hic/" +
this.boardSerialInfo().familyId +
this.boardSerialInfo().hic,
});
}
this._pageSize = await this.cortexM.readMem32(FICR.CODEPAGESIZE);
this._numPages = await this.cortexM.readMem32(FICR.CODESIZE);
}
async startSerial(listener) {
const currentBaud = await this.daplink.getSerialBaudrate();
if (currentBaud !== 115200) {
// Changing the baud rate causes a micro:bit reset, so only do it if necessary
await this.daplink.setSerialBaudrate(115200);
}
this.daplink.on(DAPjs.DAPLink.EVENT_SERIAL_DATA, listener);
await this.daplink.startSerialRead(1);
}
stopSerial(listener) {
this.daplink.stopSerialRead();
this.daplink.removeListener(DAPjs.DAPLink.EVENT_SERIAL_DATA, listener);
}
async disconnectAsync() {
if (this.device.opened && this.transport.interfaceNumber !== undefined) {
return this.daplink.disconnect();
}
}
// Send a packet to the micro:bit directly via WebUSB and return the response.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/transport/cmsis_dap.ts#L161
async send(packet) {
const array = Uint8Array.from(packet);
await this.transport.write(array.buffer);
const response = await this.transport.read();
return new Uint8Array(response.buffer);
}
// Send a command along with relevant data to the micro:bit directly via WebUSB and handle the response.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/transport/cmsis_dap.ts#L74
async cmdNums(
op,
data
) {
data.unshift(op);
const buf = await this.send(data);
if (buf[0] !== op) {
throw new Error(`Bad response for ${op} -> ${buf[0]}`);
}
switch (op) {
case DapCmd.DAP_CONNECT:
case DapCmd.DAP_INFO:
case DapCmd.DAP_TRANSFER:
case DapCmd.DAP_TRANSFER_BLOCK:
break;
default:
if (buf[1] !== 0) {
throw new Error(`Bad status for ${op} -> ${buf[1]}`);
}
}
return buf;
}
// Read a certain register a specified amount of times.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/dap/dap.ts#L117
async readRegRepeat(regId, cnt) {
const request = regRequest(regId);
const sendargs = [0, cnt];
for (let i = 0; i < cnt; ++i) {
sendargs.push(request);
}
// Transfer the read requests to the micro:bit and retrieve the data read.
const buf = await this.cmdNums(DapCmd.DAP_TRANSFER, sendargs);
if (buf[1] !== cnt) {
throw new Error("(many) Bad #trans " + buf[1]);
} else if (buf[2] !== 1) {
throw new Error("(many) Bad transfer status " + buf[2]);
}
return buf.subarray(3, 3 + cnt * 4);
}
// Write to a certain register a specified amount of data.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/dap/dap.ts#L138
async writeRegRepeat(
regId,
data
) {
const request = regRequest(regId, true);
const sendargs = [0, data.length, 0, request];
data.forEach((d) => {
// separate d into bytes
sendargs.push(
d & 0xff,
(d >> 8) & 0xff,
(d >> 16) & 0xff,
(d >> 24) & 0xff
);
});
// Transfer the write requests to the micro:bit and retrieve the response status.
const buf = await this.cmdNums(DapCmd.DAP_TRANSFER_BLOCK, sendargs);
if (buf[3] !== 1) {
throw new Error("(many-wr) Bad transfer status " + buf[2]);
}
}
// Core functionality reading a block of data from micro:bit RAM at a specified address.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/memory/memory.ts#L181
async readBlockCore(
addr,
words
) {
// Set up CMSIS-DAP to read/write from/to the RAM address addr using the register
// ApReg.DRW to write to or read from.
await this.cortexM.writeAP(ApReg.CSW, Csw.CSW_VALUE | Csw.CSW_SIZE32);
await this.cortexM.writeAP(ApReg.TAR, addr);
let lastSize = words % 15;
if (lastSize === 0) {
lastSize = 15;
}
const blocks = [];
for (let i = 0; i < Math.ceil(words / 15); i++) {
const b = await this.readRegRepeat(
apReg(ApReg.DRW, DapVal.READ),
i === blocks.length - 1 ? lastSize : 15
);
blocks.push(b);
}
return bufferConcat(blocks).subarray(0, words * 4);
}
// Core functionality writing a block of data to micro:bit RAM at a specified address.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/memory/memory.ts#L205
async writeBlockCore(
addr,
words
) {
try {
// Set up CMSIS-DAP to read/write from/to the RAM address addr using the register ApReg.DRW to write to or read from.
await this.cortexM.writeAP(ApReg.CSW, Csw.CSW_VALUE | Csw.CSW_SIZE32);
await this.cortexM.writeAP(ApReg.TAR, addr);
await this.writeRegRepeat(apReg(ApReg.DRW, DapVal.WRITE), words);
} catch (e) {
if (e.dapWait) {
// Retry after a delay if required.
this.log(`Transfer wait, write block`);
await new Promise((resolve) => setTimeout(resolve, 100));
return await this.writeBlockCore(addr, words);
} else {
throw e;
}
}
}
// Reads a block of data from micro:bit RAM at a specified address.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/memory/memory.ts#L143
async readBlockAsync(addr, words) {
const bufs = [];
const end = addr + words * 4;
let ptr = addr;
// Read a single page at a time.
while (ptr < end) {
let nextptr = ptr + this.pageSize();
if (ptr === addr) {
nextptr &= ~(this.pageSize() - 1);
}
const len = Math.min(nextptr - ptr, end - ptr);
bufs.push(await this.readBlockCore(ptr, len >> 2));
ptr = nextptr;
}
const result = bufferConcat(bufs);
return result.subarray(0, words * 4);
}
// Writes a block of data to micro:bit RAM at a specified address.
async writeBlockAsync(address, data) {
let payloadSize = this.transport.packetSize - 8;
if (data.buffer.byteLength > payloadSize) {
let start = 0;
let end = payloadSize;
// Split write up into smaller writes whose data can each be held in a single packet.
while (start !== end) {
let temp = new Uint32Array(data.buffer.slice(start, end));
await this.writeBlockCore(address + start, temp);
start = end;
end = Math.min(data.buffer.byteLength, end + payloadSize);
}
} else {
await this.writeBlockCore(address, data);
}
}
// Execute code at a certain address with specified values in the registers.
// Waits for execution to halt.
async executeAsync(address, code, sp, pc, lr, registers) {
if (registers.length > 12) {
throw new Error(`Only 12 general purpose registers but got ${registers.length} values`);
}
await this.cortexM.halt(true);
await this.writeBlockAsync(address, code);
await this.cortexM.writeCoreRegister(CoreRegister.PC, pc);
await this.cortexM.writeCoreRegister(CoreRegister.LR, lr);
await this.cortexM.writeCoreRegister(CoreRegister.SP, sp);
for (var i = 0; i < registers.length; ++i) {
await this.cortexM.writeCoreRegister(i, registers[i]);
}
await this.cortexM.resume(true);
return this.waitForHalt();
}
// Checks whether the micro:bit has halted or timeout has been reached.
// Recurses otherwise.
async waitForHaltCore(halted, deadline) {
if (new Date().getTime() > deadline) {
throw new Error("timeout");
}
if (!halted) {
const isHalted = await this.cortexM.isHalted();
// NB this is a Promise so no stack risk.
return this.waitForHaltCore(isHalted, deadline);
}
}
// Initial function to call to wait for the micro:bit halt.
async waitForHalt(timeToWait = 10000) {
const deadline = new Date().getTime() + timeToWait;
return this.waitForHaltCore(false, deadline);
}
// Resets the micro:bit in software by writing to NVIC_AIRCR.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/cortex/cortex.ts#L347
async softwareReset() {
await this.cortexM.writeMem32(
CortexSpecialReg.NVIC_AIRCR,
CortexSpecialReg.NVIC_AIRCR_VECTKEY |
CortexSpecialReg.NVIC_AIRCR_SYSRESETREQ
);
// wait for the system to come out of reset
let dhcsr = await this.cortexM.readMem32(CortexSpecialReg.DHCSR);
while ((dhcsr & CortexSpecialReg.S_RESET_ST) !== 0) {
dhcsr = await this.cortexM.readMem32(CortexSpecialReg.DHCSR);
}
}
// Reset the micro:bit, possibly halting the core on reset.
// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/cortex/cortex.ts#L248
async reset(halt = false) {
if (halt) {
await this.cortexM.halt(true);
// VC_CORERESET causes the core to halt on reset.
const demcr = await this.cortexM.readMem32(CortexSpecialReg.DEMCR);
await this.cortexM.writeMem32(
CortexSpecialReg.DEMCR,
CortexSpecialReg.DEMCR | CortexSpecialReg.DEMCR_VC_CORERESET
);
await this.softwareReset();
await this.waitForHalt();
// Unset the VC_CORERESET bit
await this.cortexM.writeMem32(CortexSpecialReg.DEMCR, demcr);
} else {
await this.softwareReset();
}
}
}
window.DAPWrapper = DAPWrapper;
})();

View File

@@ -0,0 +1,207 @@
(() => {
class microbitFsWrapper {
/**
* Creates an instance of Micropythonthis.
* @private
*/
constructor(filename = 'main.py') {
this.filename = filename;
this.fs = null;
this.commonFsSize = 20 * 1024;
this.passthroughMethods = [
'create',
'exists',
'getStorageRemaining',
'getStorageSize',
'getStorageUsed',
'getUniversalHex',
'ls',
'read',
'readBytes',
'remove',
'size',
'write'
];
}
/**
* Initialize file system wrapper of BBC micro:bit with micropython firmwares.
*/
async initialize(folderPath) {
await this.setupFilesystem(folderPath);
}
/**
* Duplicates some of the methods from the MicropythonFsHex class by
* creating functions with the same name in this object.
*/
duplicateMethods() {
this.passthroughMethods.forEach((method) => {
this[method] = () => {
return this.fs[method].apply(this.fs, arguments);
};
});
}
/**
* Fetches both MicroPython hexes and sets up the file system with the
* initial main.py
*/
setupFilesystem(folderPath) {
const uPyV1 = goog.get(path.join(folderPath, 'microbit-micropython-v1.hex'));
const uPyV2 = goog.get(path.join(folderPath, 'microbit-micropython-v2.hex'));
if (!uPyV1 || !uPyV2) {
console.error('There was an issue loading the MicroPython Hex files.');
}
// TODO: We need to use ID 9901 for app compatibility, but can soon be changed to 9900 (as per spec)
this.fs = new microbitFs.MicropythonFsHex([
{ hex: uPyV1, boardId: 0x9901 },
{ hex: uPyV2, boardId: 0x9903 }
], {
'maxFsSize': this.commonFsSize,
});
this.duplicateMethods();
}
/**
* Setup the file system by adding main program and its dependencies.
* @param {string} code
*/
async setFilesystemProgram() {
const userCode = CodeManager.getSharedInstance().getCode();
await this.fs.write(this.filename, userCode);
await this.addExternalLibraries();
}
/**
* Add only needed libraries in micro:bit file system by reading user python code.
*/
async addExternalLibraries() {
const uPyCode = CodeManager.getSharedInstance().getCode();
const requestedLibs = this.getRequestedLibraries(uPyCode);
for (var i = 0; i < requestedLibs.length; i++) {
await this.fs.write(requestedLibs[i].filename, requestedLibs[i].code);
}
}
/**
* Get requested libraries and dependencies recursively.
* @param {String} code
*/
getRequestedLibraries(code) {
let requestedLibs = new Array();
for (var lib in VittaInterface.externalLibraries) {
const regExp1 = new RegExp('from ' + lib + ' import');
const regExp2 = new RegExp('import ' + lib);
if (regExp1.test(code) || regExp2.test(code)) {
requestedLibs.push({
filename: lib + ".py",
code: VittaInterface.externalLibraries[lib]
});
const requestedDependencies = this.getRequestedLibraries(VittaInterface.externalLibraries[lib]);
requestedLibs = requestedLibs.concat(requestedDependencies);
}
}
return requestedLibs;
}
/**
* @param {string} boardId String with the Board ID for the generation.
* @returns Uint8Array with the data for the given Board ID.
*/
getBytesForBoardId(boardId) {
if (boardId == '9900' || boardId == '9901') {
return this.fs.getIntelHexBytes(0x9901);
} else if (boardId == '9903' || boardId == '9904' || boardId == '9905' || boardId == '9906') {
return this.fs.getIntelHexBytes(0x9903);
} else {
throw Error('Could not recognise the Board ID ' + boardId);
}
}
/**
* @param {string} boardId String with the Board ID for the generation.
* @returns ArrayBuffer with the Intel Hex data for the given Board ID.
*/
getIntelHexForBoardId(boardId) {
if (boardId == '9900' || boardId == '9901') {
var hexStr = this.fs.getIntelHex(0x9901);
} else if (boardId == '9903' || boardId == '9904' || boardId == '9905' || boardId == '9906') {
var hexStr = this.fs.getIntelHex(0x9903);
} else {
throw Error('Could not recognise the Board ID ' + boardId);
}
// iHex is ASCII so we can do a 1-to-1 conversion from chars to bytes
return this.convertHexStringToBin(hexStr);
}
/**
* Convert Hex string into Uint8Array buffer.
* @param {string} hex String of hex data.
* @returns ArrayBuffer with the Intel Hex data.
*/
convertHexStringToBin(hex) {
var hexBuffer = new Uint8Array(hex.length);
for (var i = 0, strLen = hex.length; i < strLen; i++) {
hexBuffer[i] = hex.charCodeAt(i);
}
return hexBuffer;
}
/**
* Import the files from the provide hex string into the filesystem.
* If the import is successful this deletes all the previous files.
*
* @param {string} hexStr Hex (Intel or Universal) string with files to
* import.
* @return {string[]} Array with the filenames of all files imported.
*/
importHexFiles(hexStr) {
var filesNames = this.fs.importFilesFromHex(hexStr, {
overwrite: true,
formatFirst: true
});
if (!filesNames.length) {
throw new Error('The filesystem in the hex file was empty');
}
return filesNames;
}
/**
* Import an appended script from the provide hex string into the filesystem.
* If the import is successful this deletes all the previous files.
* @param {string} hexStr Hex (Intel or Universal) string with files to import.
* @return {string[]} Array with the filenames of all files imported.
*/
importHexAppended(hexStr) {
var code = microbitFs.getIntelHexAppendedScript(hexStr);
if (!code) {
throw new Error('No appended code found in the hex file');
};
this.fs.ls().forEach(function (filename) {
this.fs.remove(filename);
});
this.fs.write(this.filename, code);
return [this.filename];
}
writeFile(filename, fileBytes) {
try {
if (this.fs.exists(filename)) {
this.fs.remove(filename);
this.fs.create(filename, fileBytes);
} else {
this.fs.write(filename, fileBytes);
}
} catch (e) {
if (this.fs.exists(filename)) {
this.fs.remove(filename);
}
}
}
}
window.FSWrapper = new microbitFsWrapper();
})();

View File

@@ -0,0 +1,375 @@
(() => {
/**
* (c) 2021, Micro:bit Educational Foundation and contributors
*
* SPDX-License-Identifier: MIT
*
* This file is made up of a combination of original code, along with code
* extracted from the following repositories:
*
* https://github.com/mmoskal/dapjs/tree/a32f11f54e9e76a9c61896ddd425c1cb1a29c143
* https://github.com/microsoft/pxt-microbit
*
* The pxt-microbit license is included below.
*
* PXT - Programming Experience Toolkit
*
* The MIT License (MIT)
*
* Copyright (c) Microsoft Corporation
*
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Implementation of partial flashing for the micro:bit.
*
* Latest Microsoft implementation is here:
* https://github.com/microsoft/pxt-microbit/blob/master/editor/flash.ts
*/
// Represents the micro:bit's core registers
// Drawn from https://armmbed.github.io/dapjs/docs/enums/coreregister.html
const CoreRegister = {
SP: 13,
LR: 14,
PC: 15,
};
class Page {
constructor(targetAddr, data) {
this.targetAddr = targetAddr;
this.data = data;
}
}
// Split buffer into pages, each of pageSize size.
// Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L209
const pageAlignBlocks = (buffer, targetAddr, pageSize) => {
let unaligned = new Uint8Array(buffer);
let pages = [];
for (let i = 0; i < unaligned.byteLength;) {
let newbuf = new Uint8Array(pageSize).fill(0xff);
let startPad = (targetAddr + i) & (pageSize - 1);
let newAddr = targetAddr + i - startPad;
for (; i < unaligned.byteLength; ++i) {
if (targetAddr + i >= newAddr + pageSize) break;
newbuf[targetAddr + i - newAddr] = unaligned[i];
}
let page = new Page(newAddr, newbuf);
pages.push(page);
}
return pages;
};
// Returns the MurmurHash of the data passed to it, used for checksum calculation.
// Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L14
const murmur3_core = (data) => {
let h0 = 0x2f9be6cc;
let h1 = 0x1ec3a6c8;
for (let i = 0; i < data.byteLength; i += 4) {
let k = read32FromUInt8Array(data, i) >>> 0;
k = Math.imul(k, 0xcc9e2d51);
k = (k << 15) | (k >>> 17);
k = Math.imul(k, 0x1b873593);
h0 ^= k;
h1 ^= k;
h0 = (h0 << 13) | (h0 >>> 19);
h1 = (h1 << 13) | (h1 >>> 19);
h0 = (Math.imul(h0, 5) + 0xe6546b64) >>> 0;
h1 = (Math.imul(h1, 5) + 0xe6546b64) >>> 0;
}
return [h0, h1];
}
// Filter out all pages whose calculated checksum matches the corresponding checksum passed as an argument.
// Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L523
const onlyChanged = (pages, checksums, pageSize) => {
return pages.filter((page) => {
let idx = page.targetAddr / pageSize;
if (idx * 8 + 8 > checksums.length) return true; // out of range?
let c0 = read32FromUInt8Array(checksums, idx * 8);
let c1 = read32FromUInt8Array(checksums, idx * 8 + 4);
let ch = murmur3_core(page.data);
if (c0 === ch[0] && c1 === ch[1]) return false;
return true;
});
};
// Source code for binaries in can be found at https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/external/sha/source/main.c
// Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L243
// Update from https://github.com/microsoft/pxt-microbit/commit/a35057717222b8e48335144f497b55e29e9b0f25
// prettier-ignore
const flashPageBIN = new Uint32Array([
0xbe00be00, // bkpt - LR is set to this
0x2502b5f0, 0x4c204b1f, 0xf3bf511d, 0xf3bf8f6f, 0x25808f4f, 0x002e00ed,
0x2f00595f, 0x25a1d0fc, 0x515800ed, 0x2d00599d, 0x2500d0fc, 0xf3bf511d,
0xf3bf8f6f, 0x25808f4f, 0x002e00ed, 0x2f00595f, 0x2501d0fc, 0xf3bf511d,
0xf3bf8f6f, 0x599d8f4f, 0xd0fc2d00, 0x25002680, 0x00f60092, 0xd1094295,
0x511a2200, 0x8f6ff3bf, 0x8f4ff3bf, 0x2a00599a, 0xbdf0d0fc, 0x5147594f,
0x2f00599f, 0x3504d0fc, 0x46c0e7ec, 0x4001e000, 0x00000504,
]);
// void computeHashes(uint32_t *dst, uint8_t *ptr, uint32_t pageSize, uint32_t numPages)
// Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L253
// prettier-ignore
const computeChecksums2 = new Uint32Array([
0x4c27b5f0, 0x44a52680, 0x22009201, 0x91004f25, 0x00769303, 0x24080013,
0x25010019, 0x40eb4029, 0xd0002900, 0x3c01407b, 0xd1f52c00, 0x468c0091,
0xa9044665, 0x506b3201, 0xd1eb42b2, 0x089b9b01, 0x23139302, 0x9b03469c,
0xd104429c, 0x2000be2a, 0x449d4b15, 0x9f00bdf0, 0x4d149e02, 0x49154a14,
0x3e01cf08, 0x2111434b, 0x491341cb, 0x405a434b, 0x4663405d, 0x230541da,
0x4b10435a, 0x466318d2, 0x230541dd, 0x4b0d435d, 0x2e0018ed, 0x6002d1e7,
0x9a009b01, 0x18d36045, 0x93003008, 0xe7d23401, 0xfffffbec, 0xedb88320,
0x00000414, 0x1ec3a6c8, 0x2f9be6cc, 0xcc9e2d51, 0x1b873593, 0xe6546b64,
]);
const membase = 0x20000000;
const loadAddr = membase;
const dataAddr = 0x20002000;
const stackAddr = 0x20001000;
const read32FromUInt8Array = (data, i) => {
return (data[i] | (data[i + 1] << 8) | (data[i + 2] << 16) | (data[i + 3] << 24)) >>> 0;
};
/**
* Uses a DAPWrapper to flash the micro:bit.
* Intented to be used for a single flash with a pre-connected DAPWrapper.
*/
/**
* @class PartialFlashing
*/
class PartialFlashing {
/**
* Creates an instance of Serial.
* @private
*/
constructor(dapwrapper, logging) {
this.dapwrapper = dapwrapper;
this.logging = logging;
}
log(v) {
//console.log(v);
}
// Runs the checksum algorithm on the micro:bit's whole flash memory, and returns the results.
// Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L365
async getFlashChecksumsAsync() {
await this.dapwrapper.executeAsync(
loadAddr,
computeChecksums2,
stackAddr,
loadAddr + 1,
0xffffffff,
[dataAddr, 0, this.dapwrapper.pageSize(), this.dapwrapper.numPages()]
);
return this.dapwrapper.readBlockAsync(
dataAddr,
this.dapwrapper.numPages() * 2
);
}
// Runs the code on the micro:bit to copy a single page of data from RAM address addr to the ROM address specified by the page.
// Does not wait for execution to halt.
// Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L340
async runFlash(page, addr) {
await this.dapwrapper.cortexM.halt(true);
await Promise.all([
this.dapwrapper.cortexM.writeCoreRegister(
CoreRegister.PC,
loadAddr + 4 + 1
),
this.dapwrapper.cortexM.writeCoreRegister(CoreRegister.LR, loadAddr + 1),
this.dapwrapper.cortexM.writeCoreRegister(CoreRegister.SP, stackAddr),
this.dapwrapper.cortexM.writeCoreRegister(0, page.targetAddr),
this.dapwrapper.cortexM.writeCoreRegister(1, addr),
this.dapwrapper.cortexM.writeCoreRegister(2, this.dapwrapper.pageSize() >> 2),
]);
return this.dapwrapper.cortexM.resume(false);
}
// Write a single page of data to micro:bit ROM by writing it to micro:bit RAM and copying to ROM.
// Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L385
async partialFlashPageAsync(page, nextPage, i) {
// TODO: This short-circuits UICR, do we need to update this?
if (page.targetAddr >= 0x10000000) {
return;
}
// Use two slots in RAM to allow parallelisation of the following two tasks.
// 1. DAPjs writes a page to one slot.
// 2. flashPageBIN copies a page to flash from the other slot.
let thisAddr = i & 1 ? dataAddr : dataAddr + this.dapwrapper.pageSize();
let nextAddr = i & 1 ? dataAddr + this.dapwrapper.pageSize() : dataAddr;
// Write first page to slot in RAM.
// All subsequent pages will have already been written to RAM.
if (i === 0) {
let u32data = new Uint32Array(page.data.length / 4);
for (let j = 0; j < page.data.length; j += 4) {
u32data[j >> 2] = read32FromUInt8Array(page.data, j);
}
await this.dapwrapper.writeBlockAsync(thisAddr, u32data);
}
await this.runFlash(page, thisAddr);
// Write next page to micro:bit RAM if it exists.
if (nextPage) {
let buf = new Uint32Array(nextPage.data.buffer);
await this.dapwrapper.writeBlockAsync(nextAddr, buf);
}
return this.dapwrapper.waitForHalt();
}
// Write pages of data to micro:bit ROM.
async partialFlashCoreAsync(pages, updateProgress) {
this.log("Partial flash");
for (var i = 0; i < pages.length; ++i) {
updateProgress(i / pages.length, true);
await this.partialFlashPageAsync(pages[i], pages[i + 1], i);
}
updateProgress(1, true);
}
// Flash the micro:bit's ROM with the provided image by only copying over the pages that differ.
// Falls back to a full flash if partial flashing fails.
// Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L335
async partialFlashAsync(boardId, fs, updateProgress) {
const flashBytes = fs.getBytesForBoardId(boardId.normalize().id.toString(16));
const checksums = await this.getFlashChecksumsAsync();
await this.dapwrapper.writeBlockAsync(loadAddr, flashPageBIN);
let aligned = pageAlignBlocks(flashBytes, 0, this.dapwrapper.pageSize());
const totalPages = aligned.length;
this.log("Total pages: " + totalPages);
aligned = onlyChanged(aligned, checksums, this.dapwrapper.pageSize());
this.log("Changed pages: " + aligned.length);
let partial;
if (aligned.length > totalPages / 2) {
try {
await this.fullFlashAsync(boardId, fs, updateProgress);
partial = false;
} catch (e) {
this.log(e);
this.log("Full flash failed, attempting partial flash.");
await this.partialFlashCoreAsync(aligned, updateProgress);
partial = true;
}
} else {
try {
await this.partialFlashCoreAsync(aligned, updateProgress);
partial = true;
} catch (e) {
this.log(e);
this.log("Partial flash failed, attempting full flash.");
await this.fullFlashAsync(boardId, fs, updateProgress);
partial = false;
}
}
try {
await this.dapwrapper.reset();
} catch (e) {
// Allow errors on resetting, user can always manually reset if necessary.
}
this.log("Flashing complete");
return partial;
}
// Perform full flash of micro:bit's ROM using daplink.
async fullFlashAsync(boardId, fs, updateProgress) {
this.log("Full flash");
const fullFlashProgress = (progress) => {
updateProgress(progress, false);
};
this.dapwrapper.daplink.on(DAPjs.DAPLink.EVENT_PROGRESS, fullFlashProgress);
try {
const data = fs.getIntelHexForBoardId(boardId.normalize().id.toString(16));
await this.dapwrapper.transport.open();
await this.dapwrapper.daplink.flash(data.buffer);
console.log({
type: "WebUSB-info",
message: "full-flash-successful",
})
} finally {
this.dapwrapper.daplink.removeListener(
DAPjs.DAPLink.EVENT_PROGRESS,
fullFlashProgress
);
}
}
// Flash the micro:bit's ROM with the provided image, resetting the micro:bit first.
// Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L439
async flashAsync(boardId, fs, updateProgress) {
let resetPromise = (async () => {
// Reset micro:bit to ensure interface responds correctly.
this.log("Begin reset");
try {
await this.dapwrapper.reset(true);
} catch (e) {
this.log("Retrying reset");
await this.dapwrapper.reconnectAsync();
await this.dapwrapper.reset(true);
}
})();
try {
try {
await withTimeout(resetPromise, 1000);
this.log("Begin flashing");
return await this.partialFlashAsync(
boardId,
fs,
updateProgress
);
} catch (e) {
if (e instanceof Error) {
this.log("Resetting micro:bit timed out");
this.log("Partial flashing failed. Attempting full flash");
console.log({
type: "WebUSB-info",
message: "flash-failed/attempting-full-flash",
});
await this.fullFlashAsync(boardId, fs, updateProgress);
return false;
} else {
throw e;
}
}
} finally {
// NB cannot return Promises above!
await this.dapwrapper.disconnectAsync();
}
}
}
window.PartialFlashing = PartialFlashing;
})();