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