Update: 更新 Web USB
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'];
|
||||
};
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
66
common/modules/web-modules/microbit/board-id.js
Normal file
66
common/modules/web-modules/microbit/board-id.js
Normal 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;
|
||||
|
||||
})();
|
||||
41
common/modules/web-modules/microbit/board-serial-info.js
Normal file
41
common/modules/web-modules/microbit/board-serial-info.js
Normal 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;
|
||||
|
||||
})();
|
||||
488
common/modules/web-modules/microbit/dap-wrapper.js
Normal file
488
common/modules/web-modules/microbit/dap-wrapper.js
Normal 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;
|
||||
|
||||
})();
|
||||
207
common/modules/web-modules/microbit/fs-wrapper.js
Normal file
207
common/modules/web-modules/microbit/fs-wrapper.js
Normal 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();
|
||||
|
||||
})();
|
||||
375
common/modules/web-modules/microbit/partial-flashing.js
Normal file
375
common/modules/web-modules/microbit/partial-flashing.js
Normal 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;
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user