From 8727b62cb2bd4bae9c21570b8edca5a36e942c3e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8E=8B=E7=AB=8B=E5=B8=AE?= <3294713004@qq.com>
Date: Fri, 20 Dec 2024 18:51:04 +0800
Subject: [PATCH] =?UTF-8?q?Update:=20=E6=9B=B4=E6=96=B0=20Web=20USB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
boards.json | 4 +-
common/deps.json | 28 +-
common/modules/mixly-modules/common/mfile.js | 11 -
.../common/microbit-fs-wrapper.js | 151 ------
.../mixly-modules/common/microbit-fs.js | 137 -----
.../common/statusbars-manager.js | 3 +-
common/modules/mixly-modules/deps.json | 39 +-
.../modules/mixly-modules/web/burn-upload.js | 259 +++++++---
common/modules/mixly-modules/web/hid.js | 4 +-
common/modules/mixly-modules/web/lms.js | 139 -----
common/modules/mixly-modules/web/usb.js | 59 ++-
.../modules/web-modules/microbit/board-id.js | 66 +++
.../web-modules/microbit/board-serial-info.js | 41 ++
.../web-modules/microbit/dap-wrapper.js | 488 ++++++++++++++++++
.../web-modules/microbit/fs-wrapper.js | 207 ++++++++
.../{ => microbit}/microbit-fs.umd.min.js | 0
.../web-modules/microbit/partial-flashing.js | 375 ++++++++++++++
17 files changed, 1428 insertions(+), 583 deletions(-)
delete mode 100644 common/modules/mixly-modules/common/microbit-fs-wrapper.js
delete mode 100644 common/modules/mixly-modules/common/microbit-fs.js
delete mode 100644 common/modules/mixly-modules/web/lms.js
create mode 100644 common/modules/web-modules/microbit/board-id.js
create mode 100644 common/modules/web-modules/microbit/board-serial-info.js
create mode 100644 common/modules/web-modules/microbit/dap-wrapper.js
create mode 100644 common/modules/web-modules/microbit/fs-wrapper.js
rename common/modules/web-modules/{ => microbit}/microbit-fs.umd.min.js (100%)
create mode 100644 common/modules/web-modules/microbit/partial-flashing.js
diff --git a/boards.json b/boards.json
index 469cfe37..44648e52 100644
--- a/boards.json
+++ b/boards.json
@@ -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",
diff --git a/common/deps.json b/common/deps.json
index ea037a01..ee1a7f69 100644
--- a/common/deps.json
+++ b/common/deps.json
@@ -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"],
diff --git a/common/modules/mixly-modules/common/mfile.js b/common/modules/mixly-modules/common/mfile.js
index 4c51758c..ce8e746f 100644
--- a/common/modules/mixly-modules/common/mfile.js
+++ b/common/modules/mixly-modules/common/mfile.js
@@ -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',
diff --git a/common/modules/mixly-modules/common/microbit-fs-wrapper.js b/common/modules/mixly-modules/common/microbit-fs-wrapper.js
deleted file mode 100644
index a91915fe..00000000
--- a/common/modules/mixly-modules/common/microbit-fs-wrapper.js
+++ /dev/null
@@ -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'];
-};
-
-});
\ No newline at end of file
diff --git a/common/modules/mixly-modules/common/microbit-fs.js b/common/modules/mixly-modules/common/microbit-fs.js
deleted file mode 100644
index 7e2113e1..00000000
--- a/common/modules/mixly-modules/common/microbit-fs.js
+++ /dev/null
@@ -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;
- }
-}
-
-});
\ No newline at end of file
diff --git a/common/modules/mixly-modules/common/statusbars-manager.js b/common/modules/mixly-modules/common/statusbars-manager.js
index b5d678dd..3e9fd8c4 100644
--- a/common/modules/mixly-modules/common/statusbars-manager.js
+++ b/common/modules/mixly-modules/common/statusbars-manager.js
@@ -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',
diff --git a/common/modules/mixly-modules/deps.json b/common/modules/mixly-modules/deps.json
index eccf9896..56f06756 100644
--- a/common/modules/mixly-modules/deps.json
+++ b/common/modules/mixly-modules/deps.json
@@ -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": [
diff --git a/common/modules/mixly-modules/web/burn-upload.js b/common/modules/mixly-modules/web/burn-upload.js
index ace3852c..a31b75df 100644
--- a/common/modules/mixly-modules/web/burn-upload.js
+++ b/common/modules/mixly-modules/web/burn-upload.js
@@ -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'),
diff --git a/common/modules/mixly-modules/web/hid.js b/common/modules/mixly-modules/web/hid.js
index 116f602b..86c76529 100644
--- a/common/modules/mixly-modules/web/hid.js
+++ b/common/modules/mixly-modules/web/hid.js
@@ -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);
diff --git a/common/modules/mixly-modules/web/lms.js b/common/modules/mixly-modules/web/lms.js
deleted file mode 100644
index bcc36436..00000000
--- a/common/modules/mixly-modules/web/lms.js
+++ /dev/null
@@ -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 = `
-
- 保存到教学平台
-
-`;
-
-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>/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();
-}
-
-
-});
\ No newline at end of file
diff --git a/common/modules/mixly-modules/web/usb.js b/common/modules/mixly-modules/web/usb.js
index e93d5462..37ce6fd3 100644
--- a/common/modules/mixly-modules/web/usb.js
+++ b/common/modules/mixly-modules/web/usb.js
@@ -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;
+ }
+ }
}
}
diff --git a/common/modules/web-modules/microbit/board-id.js b/common/modules/web-modules/microbit/board-id.js
new file mode 100644
index 00000000..3d994c62
--- /dev/null
+++ b/common/modules/web-modules/microbit/board-id.js
@@ -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;
+
+})();
\ No newline at end of file
diff --git a/common/modules/web-modules/microbit/board-serial-info.js b/common/modules/web-modules/microbit/board-serial-info.js
new file mode 100644
index 00000000..d09807c2
--- /dev/null
+++ b/common/modules/web-modules/microbit/board-serial-info.js
@@ -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;
+
+})();
\ No newline at end of file
diff --git a/common/modules/web-modules/microbit/dap-wrapper.js b/common/modules/web-modules/microbit/dap-wrapper.js
new file mode 100644
index 00000000..0d761e52
--- /dev/null
+++ b/common/modules/web-modules/microbit/dap-wrapper.js
@@ -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;
+
+})();
\ No newline at end of file
diff --git a/common/modules/web-modules/microbit/fs-wrapper.js b/common/modules/web-modules/microbit/fs-wrapper.js
new file mode 100644
index 00000000..6c890322
--- /dev/null
+++ b/common/modules/web-modules/microbit/fs-wrapper.js
@@ -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();
+
+})();
\ No newline at end of file
diff --git a/common/modules/web-modules/microbit-fs.umd.min.js b/common/modules/web-modules/microbit/microbit-fs.umd.min.js
similarity index 100%
rename from common/modules/web-modules/microbit-fs.umd.min.js
rename to common/modules/web-modules/microbit/microbit-fs.umd.min.js
diff --git a/common/modules/web-modules/microbit/partial-flashing.js b/common/modules/web-modules/microbit/partial-flashing.js
new file mode 100644
index 00000000..f49bd256
--- /dev/null
+++ b/common/modules/web-modules/microbit/partial-flashing.js
@@ -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;
+
+})();
\ No newline at end of file