From 759c0b7ea964e37c5bf4fd249a32a69b169ed37e Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Tue, 2 Mar 2021 12:39:38 +0000 Subject: [PATCH 1/7] Partial flashing. Substantially the same as in v2. Needs significant refactoring and conversion to TypeScript, but I think it's in a working state and ought to have all the same bugs :) I've somewhat gutted my top-level connection class as it'll be easier to reimplement serial and connection states on top of this. --- code-mirror-experiment.code-workspace | 23 +- package-lock.json | 2 +- src/device/board-id.ts | 25 + src/device/microbit.ts | 144 ++---- src/device/partial-flashing-utils.js | 133 +++++ src/device/partial-flashing.js | 706 ++++++++++++++++++++++++++ src/fs/fs.ts | 30 +- src/settings.ts | 5 +- src/workbench/DeviceConnection.tsx | 21 +- src/workbench/ZoomControls.tsx | 11 +- 10 files changed, 965 insertions(+), 135 deletions(-) create mode 100644 src/device/board-id.ts create mode 100644 src/device/partial-flashing-utils.js create mode 100644 src/device/partial-flashing.js diff --git a/code-mirror-experiment.code-workspace b/code-mirror-experiment.code-workspace index 50800c951..e93de761d 100644 --- a/code-mirror-experiment.code-workspace +++ b/code-mirror-experiment.code-workspace @@ -1,11 +1,14 @@ { - "folders": [ - { - "path": "." - }, - { - "path": "../../codemirror/codemirror.next" - } - ], - "settings": {} -} \ No newline at end of file + "folders": [ + { + "path": "." + }, + { + "path": "../../codemirror/codemirror.next" + }, + { + "path": "../python/python-editor" + } + ], + "settings": {} +} diff --git a/package-lock.json b/package-lock.json index 95bfd29b1..aa74fe701 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "code-mirror-experiment", - "version": "0.1.0", + "version": "3.0.0-local", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/device/board-id.ts b/src/device/board-id.ts new file mode 100644 index 000000000..c77d8e7ba --- /dev/null +++ b/src/device/board-id.ts @@ -0,0 +1,25 @@ +export class BoardId { + private static v1Normalized = new BoardId(0x9900); + private static v2Normalized = new BoardId(0x9903); + + constructor(public id: number) { + if (!this.isV1() && !this.isV2()) { + throw new Error(`Could not recognise the Board ID ${id.toString(16)}`); + } + } + isV1(): boolean { + return this.id === 0x9900 || this.id === 0x9901; + } + isV2(): boolean { + return this.id === 0x9903 || this.id === 0x9904; + } + normalize() { + return this.isV1() ? BoardId.v1Normalized : BoardId.v2Normalized; + } + toString() { + return this.id.toString(16); + } + static parse(value: string): BoardId { + return new BoardId(parseInt(value, 16)); + } +} diff --git a/src/device/microbit.ts b/src/device/microbit.ts index 31f84e0db..76c181ec5 100644 --- a/src/device/microbit.ts +++ b/src/device/microbit.ts @@ -1,5 +1,7 @@ -import { DAPLink, WebUSB } from "dapjs"; import EventEmitter from "events"; +import { FlashDataSource } from "../fs/fs"; +import { BoardId } from "./board-id"; +import { PartialFlashing } from "./partial-flashing"; /** * Specific identified connection error types. @@ -79,9 +81,8 @@ export class MicrobitWebUSBConnection extends EventEmitter { status: ConnectionStatus = navigator.usb ? ConnectionStatus.NO_AUTHORIZED_DEVICE : ConnectionStatus.NOT_SUPPORTED; - daplink: DAPLink | undefined; - private device: USBDevice | undefined; + private connection: PartialFlashing = new PartialFlashing(); private options: MicrobitConnectionOptions; constructor(options: Partial = {}) { @@ -95,8 +96,8 @@ export class MicrobitWebUSBConnection extends EventEmitter { async initialize(): Promise { if (navigator.usb) { - navigator.usb.addEventListener("disconnect", this.handleDisconnect); - navigator.usb.addEventListener("connect", this.handleConnect); + // navigator.usb.addEventListener("disconnect", this.handleDisconnect); + // navigator.usb.addEventListener("connect", this.handleConnect); } const device = await this.getPairedDevice(); if (device) { @@ -111,34 +112,6 @@ export class MicrobitWebUSBConnection extends EventEmitter { } } - startSerialRead(pollingIntervalMillis: number = 5) { - if (!this.daplink) { - throw new Error("connect() first"); - } - this.daplink.on(DAPLink.EVENT_SERIAL_DATA, this.handleSerial); - - // We intentionally don't await this here as it blocks until - // the device is disconnected or serial read stopped. - this.daplink - .startSerialRead(pollingIntervalMillis) - .then(() => this.stopSerialRead()) - .catch((e) => this.emit(EVENT_SERIAL_ERROR, enrichedError(e))); - } - - stopSerialRead(): void { - if (this.daplink) { - this.daplink.removeListener(DAPLink.EVENT_SERIAL_DATA, this.handleSerial); - this.daplink.stopSerialRead(); - } - } - - async serialWrite(data: string): Promise { - if (!this.daplink) { - throw new Error("connect() first"); - } - return this.daplink.serialWrite(data); - } - /** * Removes all listeners. */ @@ -148,7 +121,6 @@ export class MicrobitWebUSBConnection extends EventEmitter { navigator.usb.removeEventListener("connect", this.handleConnect); navigator.usb.removeEventListener("disconnect", this.handleDisconnect); } - this.stopSerialRead(); } /** @@ -165,32 +137,39 @@ export class MicrobitWebUSBConnection extends EventEmitter { }); } - /** - * Flash the device. - * @param data The hex file data. - */ async flash( - data: string | Uint8Array, - progress?: (percentage: number | undefined) => void - ): Promise { - if (progress) { - this.on(EVENT_PROGRESS, progress); + dataSource: FlashDataSource, + options: { + partial: boolean; + progress: (percentage: number | undefined) => void; } - try { - await withEnrichedErrors(async () => { - if (!this.daplink) { - throw new Error("connect() first"); - } - await this.daplink.flash( - typeof data === "string" ? new TextEncoder().encode(data) : data - ); - }); - } finally { - if (progress) { - this.removeListener(EVENT_PROGRESS, progress); - progress(undefined); - } + ): Promise { + const partial = options.partial; + const progress = options.progress || (() => {}); + + // When we support it: + // this.stopSerialRead(); + + // FS space errors should be handled when obtaining the hex. + // Progress reporting code removed + // Timeout code removed for now. + // unhandledrejection code removed for now. + // Does it really fail in the background? + // The error handler disconnects and throws away dapjs. + + await this.connection.disconnectDapAsync(); + await this.connection.connectDapAsync(); + + // Collect data to flash, partial flashing can use just the flash bytes, + // but full flashing needs the entire Intel Hex to include the UICR data + const boardId = BoardId.parse(this.connection.dapwrapper.boardId); + const data = await dataSource(boardId); + if (partial) { + await this.connection.flashAsync(data.bytes, data.intelHex, progress); + } else { + await this.connection.fullFlashAsync(data.intelHex, progress); } + progress(undefined); } /** @@ -198,11 +177,8 @@ export class MicrobitWebUSBConnection extends EventEmitter { */ async disconnect(): Promise { await withEnrichedErrors(async () => { - if (this.daplink) { - this.stopSerialRead(); - await this.daplink.disconnect(); - this.setStatus(ConnectionStatus.NOT_CONNECTED); - } + await this.connection.disconnectDapAsync(); + this.setStatus(ConnectionStatus.NOT_CONNECTED); }); } @@ -230,7 +206,6 @@ export class MicrobitWebUSBConnection extends EventEmitter { private handleConnect = (event: USBConnectionEvent) => { if (this.matchesDeviceFilter(event.device)) { if (this.status === ConnectionStatus.NO_AUTHORIZED_DEVICE) { - this.device = event.device; this.setStatus(ConnectionStatus.NOT_CONNECTED); if (this.options.autoConnect) { this.connect(ConnectionMode.NON_INTERACTIVE).catch((e) => @@ -242,53 +217,18 @@ export class MicrobitWebUSBConnection extends EventEmitter { }; private handleDisconnect = (event: USBConnectionEvent) => { - if (event.device === this.device) { - this.daplink = undefined; + if (event.device === this.connection.dapwrapper?.daplink?.device) { this.setStatus(ConnectionStatus.NO_AUTHORIZED_DEVICE); } }; - private handleSerial = (data: string) => this.emit(EVENT_SERIAL_DATA, data); - private async connectInternal( mode: ConnectionMode ): Promise { this.assertSupported(); - if (this.daplink) { - await this.daplink.connect(); - return ConnectionStatus.CONNECTED; - } else { - this.device = await this.getPairedDevice(); - if (!this.device && mode === ConnectionMode.INTERACTIVE) { - try { - const deviceRequestOptions = { - filters: this.options.deviceFilters, - }; - this.device = await navigator.usb.requestDevice(deviceRequestOptions); - } catch (e) { - if ( - e instanceof DOMException && - e.message === "No device selected." - ) { - // User cancelled (Chrome). - } else { - throw e; - } - } - } - if (this.device) { - const transport = new WebUSB(this.device); - this.daplink = new DAPLink(transport); - this.daplink.on(DAPLink.EVENT_PROGRESS, (n: number) => - this.emit(EVENT_PROGRESS, n) - ); - await this.daplink.connect(); - await this.daplink.setSerialBaudrate(115200); - return ConnectionStatus.CONNECTED; - } else { - return ConnectionStatus.NO_AUTHORIZED_DEVICE; - } - } + await this.connection.connectDapAsync(); + // What about this case: ConnectionStatus.NO_AUTHORIZED_DEVICE + return ConnectionStatus.CONNECTED; } private matchesDeviceFilter = (device: USBDevice): boolean => diff --git a/src/device/partial-flashing-utils.js b/src/device/partial-flashing-utils.js new file mode 100644 index 000000000..f1c282d6a --- /dev/null +++ b/src/device/partial-flashing-utils.js @@ -0,0 +1,133 @@ +// The Control/Status Word register is used to configure and control transfers through the APB interface. +// This is drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/dap/constants.ts#L28 +// Csw.CSW_VALUE = (CSW_RESERVED | CSW_MSTRDBG | CSW_HPROT | CSW_DBGSTAT | CSW_SADDRINC) +export const CSW_VALUE = + 0x01000000 | 0x20000000 | 0x02000000 | 0x00000040 | 0x00000010; + +export const log = console.log; + +// How was this defined before? Bug? +export const timeoutMessage = "timeout"; + +// Represents the micro:bit's core registers +// Drawn from https://armmbed.github.io/dapjs/docs/enums/coreregister.html +export const CoreRegister = { + SP: 13, + LR: 14, + PC: 15, +}; + +// FICR Registers +export const FICR = { + CODEPAGESIZE: 0x10000000 | 0x10, + CODESIZE: 0x10000000 | 0x14, +}; + +export const read32FromUInt8Array = (data, i) => { + return ( + (data[i] | + (data[i + 1] << 8) | + (data[i + 2] << 16) | + (data[i + 3] << 24)) >>> + 0 + ); +}; + +export 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; +}; + +// 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 +export 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]; +}; + +// Returns a representation of an Access Port Register. +// Drawn from https://github.com/mmoskal/dapjs/blob/a32f11f54e9e76a9c61896ddd425c1cb1a29c143/src/util.ts#L63 +export const apReg = (r, mode) => { + const v = r | mode | (1 << 0); // 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 +export 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; +}; + +// Split buffer into pages, each of pageSize size. +// Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L209 +export const pageAlignBlocks = (buffer, targetAddr, pageSize) => { + class Page { + constructor(targetAddr, data) { + this.targetAddr = targetAddr; + this.data = data; + } + } + + 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; +}; + +// 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 +export 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; + }); +}; diff --git a/src/device/partial-flashing.js b/src/device/partial-flashing.js new file mode 100644 index 000000000..33b6e9f4d --- /dev/null +++ b/src/device/partial-flashing.js @@ -0,0 +1,706 @@ +import * as DAPjs from "dapjs"; +import * as PartialFlashingUtils from "./partial-flashing-utils"; + +/* + 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. +*/ + +export class DAPWrapper { + constructor(device) { + this.reconnected = false; + this.flashing = true; + this.device = device; + this.pageSize = null; + this.numPages = null; + this.allocDAP(); + } + + allocBoardID() { + // The micro:bit board ID is the serial number first 4 hex digits + if (!(this.device && this.device.serialNumber)) { + throw new Error("Could not detected ID from connected board."); + } + this.boardId = this.device.serialNumber.substring(0, 4); + PartialFlashingUtils.log("Detected board ID " + this.boardId); + } + + allocDAP() { + this.transport = new DAPjs.WebUSB(this.device); + this.daplink = new DAPjs.DAPLink(this.transport); + this.cortexM = new DAPjs.CortexM(this.transport); + } + + // Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L119 + async reconnectAsync() { + // Only fully reconnect after the first time this object has reconnected. + if (!this.reconnected) { + this.reconnected = true; + this.allocDAP(); + await this.disconnectAsync(); + } + await this.daplink.connect(); + await this.cortexM.connect(); + this.allocBoardID(); + } + + 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 0x02: // DapCmd.DAP_CONNECT: + case 0x00: // DapCmd.DAP_INFO: + case 0x05: // DapCmd.DAP_TRANSFER: + case 0x06: // 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 = PartialFlashingUtils.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(0x05 /* 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 = PartialFlashingUtils.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(0x06 /* DapCmd.DAP_TRANSFER */, 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( + 0x00 /* ApReg.CSW */, + PartialFlashingUtils.CSW_VALUE /* Csw.CSW_VALUE */ | + 0x00000002 /* Csw.CSW_SIZE32 */ + ); + await this.cortexM.writeAP(0x04 /* 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( + PartialFlashingUtils.apReg( + 0x0c /* ApReg.DRW */, + 1 << 1 /* DapVal.READ */ + ), + i === blocks.length - 1 ? lastSize : 15 + ); + blocks.push(b); + } + + return PartialFlashingUtils.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( + 0x00 /* ApReg.CSW */, + PartialFlashingUtils.CSW_VALUE /* Csw.CSW_VALUE */ | + 0x00000002 /* Csw.CSW_SIZE32 */ + ); + await this.cortexM.writeAP(0x04 /* ApReg.TAR */, addr); + + await this.writeRegRepeat( + PartialFlashingUtils.apReg( + 0x0c /* ApReg.DRW */, + 0 << 1 /* DapVal.WRITE */ + ), + words + ); + } catch (e) { + if (e.dapWait) { + // Retry after a delay if required. + PartialFlashingUtils.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 = PartialFlashingUtils.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, ...coreRegisters) { + if (coreRegisters.length > 12) { + throw new Error( + `Only 12 general purpose registers but got ${coreRegisters.length} values` + ); + } + + await this.cortexM.halt(true); + await this.writeBlockAsync(address, code); + await this.cortexM.writeCoreRegister( + PartialFlashingUtils.CoreRegister.PC, + pc + ); + await this.cortexM.writeCoreRegister( + PartialFlashingUtils.CoreRegister.LR, + lr + ); + await this.cortexM.writeCoreRegister( + PartialFlashingUtils.CoreRegister.SP, + sp + ); + for (let i = 0; i < coreRegisters.length; ++i) { + await this.cortexM.writeCoreRegister(i, coreRegisters[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(PartialFlashingUtils.timeoutMessage); + } + 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( + 3758157068 /* NVIC_AIRCR */, + 100270080 /* NVIC_AIRCR_VECTKEY */ | 4 /* NVIC_AIRCR_SYSRESETREQ */ + ); + + // wait for the system to come out of reset + let dhcsr = await this.cortexM.readMem32(3758157296 /* DHCSR */); + + while ((dhcsr & 33554432) /* S_RESET_ST */ !== 0) { + dhcsr = await this.cortexM.readMem32(3758157296 /* 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); + + let demcrAddr = 3758157308; + + // VC_CORERESET causes the core to halt on reset. + const demcr = await this.cortexM.readMem32(demcrAddr); + await this.cortexM.writeMem32( + demcrAddr, + demcr | 1 /* DEMCR_VC_CORERESET */ + ); + + await this.softwareReset(); + await this.waitForHalt(); + + // Unset the VC_CORERESET bit + await this.cortexM.writeMem32(demcrAddr, demcr); + } else { + await this.softwareReset(); + } + } +} + +// 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 +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 +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; + +export class PartialFlashing { + // Returns a new DAPWrapper or reconnects a previously used one. + // Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L161 + async dapAsync() { + if (this.dapwrapper) { + if (this.dapwrapper.device.opened) { + // Always fully reconnect to handle device unplugged mid-session + await this.dapwrapper.reconnectAsync(false); + return this.dapwrapper; + } + } + if (this.dapwrapper) { + this.dapwrapper.disconnectAsync(); + } + const device = await (async () => { + if (this.dapwrapper && this.dapwrapper.device) { + return this.dapwrapper.device; + } + return navigator.usb.requestDevice({ + filters: [{ vendorId: 0x0d28, productId: 0x0204 }], + }); + })(); + this.dapwrapper = new DAPWrapper(device); + await this.dapwrapper.reconnectAsync(true); + } + + // 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( + PartialFlashingUtils.CoreRegister.PC, + loadAddr + 4 + 1 + ), + this.dapwrapper.cortexM.writeCoreRegister( + PartialFlashingUtils.CoreRegister.LR, + loadAddr + 1 + ), + this.dapwrapper.cortexM.writeCoreRegister( + PartialFlashingUtils.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] = PartialFlashingUtils.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) { + PartialFlashingUtils.log("Partial flash"); + for (let i = 0; i < pages.length; ++i) { + updateProgress(i / pages.length); + await this.partialFlashPageAsync(pages[i], pages[i + 1], i); + } + updateProgress(1); + } + + // 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(flashBytes, hexBuffer, updateProgress) { + const checksums = await this.getFlashChecksumsAsync(); + await this.dapwrapper.writeBlockAsync(loadAddr, flashPageBIN); + let aligned = PartialFlashingUtils.pageAlignBlocks( + flashBytes, + 0, + this.dapwrapper.pageSize + ); + const totalPages = aligned.length; + PartialFlashingUtils.log("Total pages: " + totalPages); + aligned = PartialFlashingUtils.onlyChanged( + aligned, + checksums, + this.dapwrapper.pageSize + ); + PartialFlashingUtils.log("Changed pages: " + aligned.length); + if (aligned.length > totalPages / 2) { + try { + await this.fullFlashAsync(hexBuffer, updateProgress); + } catch (e) { + PartialFlashingUtils.log(e); + PartialFlashingUtils.log( + "Full flash failed, attempting partial flash." + ); + await this.partialFlashCoreAsync(aligned, updateProgress); + } + } else { + try { + await this.partialFlashCoreAsync(aligned, updateProgress); + } catch (e) { + PartialFlashingUtils.log(e); + PartialFlashingUtils.log( + "Partial flash failed, attempting full flash." + ); + await this.fullFlashAsync(hexBuffer, updateProgress); + } + } + + try { + await this.dapwrapper.reset(); + } catch (e) { + // Allow errors on resetting, user can always manually reset if necessary. + } + PartialFlashingUtils.log("Flashing Complete"); + this.dapwrapper.flashing = false; + } + + // Perform full flash of micro:bit's ROM using daplink. + async fullFlashAsync(image, updateProgress) { + PartialFlashingUtils.log("Full flash"); + // Event to monitor flashing progress + // TODO: surely we need to remove this? + this.dapwrapper.daplink.on(DAPjs.DAPLink.EVENT_PROGRESS, (progress) => { + updateProgress(progress, true); + }); + await this.dapwrapper.transport.open(); + await this.dapwrapper.daplink.flash(image); + // TODO: reinstate eventing + } + + // Connect to the micro:bit using WebUSB and setup DAPWrapper. + // Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L439 + async connectDapAsync() { + if (this.dapwrapper) { + this.dapwrapper.flashing = true; + // TODO: Why? + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + await this.dapAsync(); + PartialFlashingUtils.log("Connection Complete"); + this.dapwrapper.pageSize = await this.dapwrapper.cortexM.readMem32( + PartialFlashingUtils.FICR.CODEPAGESIZE + ); + this.dapwrapper.numPages = await this.dapwrapper.cortexM.readMem32( + PartialFlashingUtils.FICR.CODESIZE + ); + // This isn't what I expected. What about serial? + return this.dapwrapper.disconnectAsync(); + } + + async disconnectDapAsync() { + return this.dapwrapper.disconnectAsync(); + } + + // 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(flashBytes, hexBuffer, updateProgress) { + let resetPromise = (async () => { + // Reset micro:bit to ensure interface responds correctly. + PartialFlashingUtils.log("Begin reset"); + try { + await this.dapwrapper.reset(true); + } catch (e) { + PartialFlashingUtils.log("Retrying reset"); + await this.dapwrapper.reconnectAsync(false); + await this.dapwrapper.reset(true); + } + })(); + + let timeout = new Promise((resolve) => { + setTimeout(() => { + resolve("timeout"); + }, 1000); + }); + + // Use race to timeout the reset. + try { + const result = await Promise.race([resetPromise, timeout]); + if (result === "timeout") { + PartialFlashingUtils.log("Resetting micro:bit timed out"); + PartialFlashingUtils.log( + "Partial flashing failed. Attempting Full Flash" + ); + await this.fullFlashAsync(hexBuffer, updateProgress); + } else { + PartialFlashingUtils.log("Begin Flashing"); + await this.partialFlashAsync(flashBytes, hexBuffer, updateProgress); + } + } finally { + // NB cannot return Promises above! + await this.dapwrapper.disconnectAsync(); + } + } +} diff --git a/src/fs/fs.ts b/src/fs/fs.ts index 97d3edfd4..d7a0d9fcd 100644 --- a/src/fs/fs.ts +++ b/src/fs/fs.ts @@ -1,5 +1,6 @@ import { microbitBoardId, MicropythonFsHex } from "@microbit/microbit-fs"; import EventEmitter from "events"; +import { BoardId } from "../device/board-id"; import chuckADuck from "../samples/chuck-a-duck"; import microPythonV1HexUrl from "./microbit-micropython-v1.hex"; import microPythonV2HexUrl from "./microbit-micropython-v2.hex"; @@ -64,6 +65,13 @@ class LocalStorage implements Storage { } } +export interface FlashData { + bytes: Uint8Array; + intelHex: ArrayBuffer; +} + +export type FlashDataSource = (boardId: BoardId) => Promise; + /** * A MicroPython file system. */ @@ -165,9 +173,19 @@ export class FileSystem extends EventEmitter { return fs.getUniversalHex(); } - async toHexForFlash(boardId: number) { + /** + * Partial flashing can use just the flash bytes, + * Full flashing needs the entire Intel Hex to include the UICR data + * + * @param boardId The board ID (from the WebUSB connection). + */ + async toHexForFlash(boardId: BoardId): Promise { const fs = await this.initialize(); - return fs.getIntelHex(boardId); + const normalisedId = boardId.normalize().id; + return { + bytes: fs.getIntelHexBytes(normalisedId), + intelHex: asciiToBytes(fs.getIntelHex(normalisedId)), + }; } private assertInitialized(): MicropythonFsHex { @@ -233,3 +251,11 @@ export const createInternalFileSystem = async () => { maxFsSize: commonFsSize, }); }; + +const asciiToBytes = (str: string): ArrayBuffer => { + var bytes = new Uint8Array(str.length); + for (var i = 0, strLen = str.length; i < strLen; i++) { + bytes[i] = str.charCodeAt(i); + } + return bytes.buffer; +}; diff --git a/src/settings.ts b/src/settings.ts index c16ddf56c..fb1026d15 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -7,8 +7,9 @@ export interface Language { name: string; } -export const minimumFontSize = 8; -export const maximumFontSize = 256; +export const minimumFontSize = 6; +export const maximumFontSize = 198; +export const fontSizeStep = 4; export const defaultSettings: Settings = { languageId: config.supportedLanguages[0].id, diff --git a/src/workbench/DeviceConnection.tsx b/src/workbench/DeviceConnection.tsx index 89f0ea309..0489ba4c4 100644 --- a/src/workbench/DeviceConnection.tsx +++ b/src/workbench/DeviceConnection.tsx @@ -6,6 +6,7 @@ import { ConnectionMode, ConnectionStatus } from "../device"; import { useFileSystem } from "../fs/fs-hooks"; import DownloadButton from "./DownloadButton"; import useActionFeedback from "../common/use-action-feedback"; +import { BoardId } from "../device/board-id"; /** * The device connection area. @@ -23,11 +24,12 @@ const DeviceConnection = () => { const fs = useFileSystem(); const handleToggleConnected = useCallback(async () => { if (connected) { - device.disconnect(); + await device.disconnect(); } else { try { await device.connect(ConnectionMode.INTERACTIVE); } catch (e) { + console.error(e); actionFeedback.expectedError({ title: "Failed to connect to the micro:bit", description: e.message, @@ -37,23 +39,12 @@ const DeviceConnection = () => { }, [device, connected]); const handleFlash = useCallback(async () => { - // TODO: need to know board id to get best hex. - // TODO: review error reporting vs v2. let hex: string | undefined; + const dataSource = (boardId: BoardId) => fs!.toHexForFlash(boardId); try { - hex = await fs!.toHexForDownload(); - } catch (e) { - actionFeedback.expectedError({ - title: "Failed to build the hex file", - description: e.message, - }); - return; - } - - try { - // TODO: partial flashing! - device.flash(hex, setProgress); + await device.flash(dataSource, { partial: true, progress: setProgress }); } catch (e) { + console.error(e); actionFeedback.expectedError({ title: "Failed to flash the micro:bit", description: e.message, diff --git a/src/workbench/ZoomControls.tsx b/src/workbench/ZoomControls.tsx index 108ce67f1..261c375f7 100644 --- a/src/workbench/ZoomControls.tsx +++ b/src/workbench/ZoomControls.tsx @@ -8,7 +8,12 @@ import { } from "@chakra-ui/react"; import { useCallback } from "react"; import { RiZoomInLine, RiZoomOutLine } from "react-icons/ri"; -import { maximumFontSize, minimumFontSize, useSettings } from "../settings"; +import { + fontSizeStep, + maximumFontSize, + minimumFontSize, + useSettings, +} from "../settings"; interface ZoomControlsProps extends StackProps { size?: ThemeTypings["components"]["Button"]["sizes"]; @@ -22,13 +27,13 @@ const ZoomControls = ({ size, ...props }: ZoomControlsProps) => { const handleZoomIn = useCallback(() => { setSettings({ ...settings, - fontSize: Math.min(maximumFontSize, settings.fontSize + 1), + fontSize: Math.min(maximumFontSize, settings.fontSize + fontSizeStep), }); }, [setSettings, settings]); const handleZoomOut = useCallback(() => { setSettings({ ...settings, - fontSize: Math.max(minimumFontSize, settings.fontSize - 1), + fontSize: Math.max(minimumFontSize, settings.fontSize - fontSizeStep), }); }, [setSettings, settings]); return ( From 60aaf540e0ccf2a4b400479b550899d55cf00d28 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Tue, 2 Mar 2021 19:29:39 +0000 Subject: [PATCH 2/7] Tweaks. --- src/device/partial-flashing.js | 109 +++++++-------------------------- 1 file changed, 21 insertions(+), 88 deletions(-) diff --git a/src/device/partial-flashing.js b/src/device/partial-flashing.js index 33b6e9f4d..435566f1f 100644 --- a/src/device/partial-flashing.js +++ b/src/device/partial-flashing.js @@ -268,10 +268,10 @@ export class DAPWrapper { // Execute code at a certain address with specified values in the registers. // Waits for execution to halt. - async executeAsync(address, code, sp, pc, lr, ...coreRegisters) { - if (coreRegisters.length > 12) { + async executeAsync(address, code, sp, pc, lr, ...registers) { + if (registers.length > 12) { throw new Error( - `Only 12 general purpose registers but got ${coreRegisters.length} values` + `Only 12 general purpose registers but got ${registers.length} values` ); } @@ -289,8 +289,8 @@ export class DAPWrapper { PartialFlashingUtils.CoreRegister.SP, sp ); - for (let i = 0; i < coreRegisters.length; ++i) { - await this.cortexM.writeCoreRegister(i, coreRegisters[i]); + for (let i = 0; i < registers.length; ++i) { + await this.cortexM.writeCoreRegister(i, registers[i]); } await this.cortexM.resume(true); return this.waitForHalt(); @@ -360,96 +360,29 @@ export class DAPWrapper { // 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, + 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, + 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; From 42d5a419a3b4690d92b38cd0a704f417d1b10448 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Tue, 2 Mar 2021 19:37:25 +0000 Subject: [PATCH 3/7] Faff with comments. --- src/device/partial-flashing.js | 74 ++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/src/device/partial-flashing.js b/src/device/partial-flashing.js index 435566f1f..08d81ff1a 100644 --- a/src/device/partial-flashing.js +++ b/src/device/partial-flashing.js @@ -1,40 +1,46 @@ +/** + * Implementation of partial flashing for the micro:bit. + * + * This could do with some love to make it easier to follow. + */ import * as DAPjs from "dapjs"; import * as PartialFlashingUtils from "./partial-flashing-utils"; -/* - 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. -*/ +// NOTICE +// +// 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. export class DAPWrapper { constructor(device) { From 22ffb4e22b109fedbca89b0952085e5ffcced77d Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Tue, 2 Mar 2021 19:53:42 +0000 Subject: [PATCH 4/7] Clarity over what's missing. --- src/device/board-id.ts | 22 ++++++++++++++++++++++ src/device/microbit.ts | 12 ++++++------ src/workbench/DeviceConnection.tsx | 3 +-- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/device/board-id.ts b/src/device/board-id.ts index c77d8e7ba..b7766ce2e 100644 --- a/src/device/board-id.ts +++ b/src/device/board-id.ts @@ -1,3 +1,6 @@ +/** + * Validates micro:bit board IDs. + */ export class BoardId { private static v1Normalized = new BoardId(0x9900); private static v2Normalized = new BoardId(0x9903); @@ -7,18 +10,37 @@ export class BoardId { throw new Error(`Could not recognise the Board ID ${id.toString(16)}`); } } + isV1(): boolean { return this.id === 0x9900 || this.id === 0x9901; } + isV2(): boolean { return this.id === 0x9903 || this.id === 0x9904; } + + /** + * 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: string): BoardId { return new BoardId(parseInt(value, 16)); } diff --git a/src/device/microbit.ts b/src/device/microbit.ts index 76c181ec5..8d0d494ce 100644 --- a/src/device/microbit.ts +++ b/src/device/microbit.ts @@ -150,12 +150,11 @@ export class MicrobitWebUSBConnection extends EventEmitter { // When we support it: // this.stopSerialRead(); - // FS space errors should be handled when obtaining the hex. - // Progress reporting code removed - // Timeout code removed for now. - // unhandledrejection code removed for now. - // Does it really fail in the background? - // The error handler disconnects and throws away dapjs. + // Things to reinstate: + // - FS space error handling. + // - Overall timeout code. + // - unhandledrejection wrapper. To discuss! Does it really fail in the background? + // - The error handler disconnects and throws away dapjs. await this.connection.disconnectDapAsync(); await this.connection.connectDapAsync(); @@ -164,6 +163,7 @@ export class MicrobitWebUSBConnection extends EventEmitter { // but full flashing needs the entire Intel Hex to include the UICR data const boardId = BoardId.parse(this.connection.dapwrapper.boardId); const data = await dataSource(boardId); + // TODO: push this decision down if (partial) { await this.connection.flashAsync(data.bytes, data.intelHex, progress); } else { diff --git a/src/workbench/DeviceConnection.tsx b/src/workbench/DeviceConnection.tsx index 0489ba4c4..3f53fb0b9 100644 --- a/src/workbench/DeviceConnection.tsx +++ b/src/workbench/DeviceConnection.tsx @@ -39,8 +39,7 @@ const DeviceConnection = () => { }, [device, connected]); const handleFlash = useCallback(async () => { - let hex: string | undefined; - const dataSource = (boardId: BoardId) => fs!.toHexForFlash(boardId); + const dataSource = (boardId: BoardId) => fs.toHexForFlash(boardId); try { await device.flash(dataSource, { partial: true, progress: setProgress }); } catch (e) { From 5206e65dcbca8140488800c2b1415347d9a538d2 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Wed, 3 Mar 2021 10:46:12 +0000 Subject: [PATCH 5/7] Handle most missing cases. I think this is enough to merge with some testing, though the code still needs a lot of work. --- public/index.html | 5 +- src/common/use-action-feedback.tsx | 6 +- src/device/microbit.ts | 289 +++++++++++++++++------------ src/device/partial-flashing.js | 8 +- src/translation/index.ts | 230 +++++++++++++++++++++++ src/workbench/DeviceConnection.tsx | 38 +++- 6 files changed, 441 insertions(+), 135 deletions(-) create mode 100644 src/translation/index.ts diff --git a/public/index.html b/public/index.html index 87086cbf0..6d3b8c9df 100644 --- a/public/index.html +++ b/public/index.html @@ -22,7 +22,10 @@ /> - +
diff --git a/src/common/use-action-feedback.tsx b/src/common/use-action-feedback.tsx index eda1847e4..475eb31f0 100644 --- a/src/common/use-action-feedback.tsx +++ b/src/common/use-action-feedback.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { ReactNode } from "react"; import { Link, useToast } from "@chakra-ui/react"; import { useMemo } from "react"; import config from "../config"; @@ -14,7 +14,7 @@ export class ActionFeedback { description, }: { title: string; - description: string; + description: ReactNode; }) { this.toast({ title, @@ -30,6 +30,8 @@ export class ActionFeedback { * @param error the error thrown. */ unexpectedError(error: Error) { + // For now at least. + console.error(error); this.toast({ title: "An unexpected error occurred", status: "error", diff --git a/src/device/microbit.ts b/src/device/microbit.ts index 8d0d494ce..1d2b7fe59 100644 --- a/src/device/microbit.ts +++ b/src/device/microbit.ts @@ -1,15 +1,59 @@ import EventEmitter from "events"; import { FlashDataSource } from "../fs/fs"; +import translation from "../translation"; import { BoardId } from "./board-id"; import { PartialFlashing } from "./partial-flashing"; /** - * Specific identified connection error types. - * New members will be added to this enum over time. + * Specific identified error types. + * + * New members may be added over time. */ -export enum ConnectionErrorType { - UNABLE_TO_CLAIM_INTERFACE = "UNABLE_TO_CLAIM_INTERFACE", - UNKNOWN = "UNKNOWN", +export type WebUSBErrorCode = + /** + * Device not found, perhaps because it doesn't have new enough firmware (for V1). + */ + | "update-req" + /** + * Unable to claim the interface, usually because it's in use in another tab/window. + */ + | "clear-connect" + /** + * The device was found to be disconnected. + */ + | "device-disconnected" + /** + * A communication timeout occurred. + */ + | "timeout-error" + /** + * This is the fallback error case suggesting that the user reconnects their device. + */ + | "reconnect-microbit"; + +/** + * Error type used for all interactions with this module. + */ +export class WebUSBError extends Error { + code: WebUSBErrorCode; + title: string; + description?: string; + constructor({ + code, + title, + message, + description, + }: { + code: WebUSBErrorCode; + title: string; + message?: string; + description?: string; + }) { + super(message); + this.code = code; + this.title = title; + this.description = description; + } } /** @@ -51,22 +95,7 @@ export enum ConnectionMode { NON_INTERACTIVE, } -export interface MicrobitConnectionOptions { - /** - * Connect when a device becomes available. - * For example, a previously approved device is plugged in. - * - * Default is true. - */ - autoConnect: boolean; - - /** - * Device identification. - * - * Default matches the micro:bit device. - */ - deviceFilters: USBDeviceFilter[]; -} +export interface MicrobitConnectionOptions {} export const EVENT_STATUS = "status"; export const EVENT_SERIAL_DATA = "serial_data"; @@ -87,28 +116,12 @@ export class MicrobitWebUSBConnection extends EventEmitter { constructor(options: Partial = {}) { super(); - this.options = { - autoConnect: true, - deviceFilters: [{ vendorId: 0x0d28, productId: 0x0204 }], - ...options, - }; + this.options = options; } async initialize(): Promise { if (navigator.usb) { - // navigator.usb.addEventListener("disconnect", this.handleDisconnect); - // navigator.usb.addEventListener("connect", this.handleConnect); - } - const device = await this.getPairedDevice(); - if (device) { - this.setStatus(ConnectionStatus.NOT_CONNECTED); - if (this.options.autoConnect) { - // Do this in the background so all autoconnection errors are reported - // via the event rather than a mixture. - this.connect(ConnectionMode.NON_INTERACTIVE).catch((e) => { - this.emit(EVENT_AUTOCONNECT_ERROR, e); - }); - } + navigator.usb.addEventListener("disconnect", this.handleDisconnect); } } @@ -118,7 +131,6 @@ export class MicrobitWebUSBConnection extends EventEmitter { dispose() { this.removeAllListeners(); if (navigator.usb) { - navigator.usb.removeEventListener("connect", this.handleConnect); navigator.usb.removeEventListener("disconnect", this.handleDisconnect); } } @@ -131,7 +143,7 @@ export class MicrobitWebUSBConnection extends EventEmitter { * @returns the final connection status. */ async connect(mode: ConnectionMode): Promise { - return withEnrichedErrors(async () => { + return this.withEnrichedErrors(async () => { this.setStatus(await this.connectInternal(mode)); return this.status; }); @@ -144,6 +156,7 @@ export class MicrobitWebUSBConnection extends EventEmitter { progress: (percentage: number | undefined) => void; } ): Promise { + const startTime = new Date().getTime(); const partial = options.partial; const progress = options.progress || (() => {}); @@ -151,51 +164,57 @@ export class MicrobitWebUSBConnection extends EventEmitter { // this.stopSerialRead(); // Things to reinstate: - // - FS space error handling. - // - Overall timeout code. - // - unhandledrejection wrapper. To discuss! Does it really fail in the background? - // - The error handler disconnects and throws away dapjs. + // - Metric/error reporting -- though check intentions, as it can lie + // about the type of flash performed - await this.connection.disconnectDapAsync(); - await this.connection.connectDapAsync(); + // Shouldn't this timeout logic apply to (re)connection in general? + // If so, we should push it down. + const reconnectPromise = (async () => { + await this.connection.disconnectDapAsync(); + await this.connection.connectDapAsync(); + })(); + const timeout = new Promise((resolve) => + setTimeout(() => resolve("timeout"), 10 * 1000) + ); + const result = await Promise.race([reconnectPromise, timeout]); + if (result === "timeout") { + throw new WebUSBError({ + code: "timeout-error", + title: "Connection Timed Out", + description: translation["webusb"]["err"]["reconnect-microbit"], + }); + } // Collect data to flash, partial flashing can use just the flash bytes, // but full flashing needs the entire Intel Hex to include the UICR data const boardId = BoardId.parse(this.connection.dapwrapper.boardId); const data = await dataSource(boardId); - // TODO: push this decision down - if (partial) { - await this.connection.flashAsync(data.bytes, data.intelHex, progress); - } else { - await this.connection.fullFlashAsync(data.intelHex, progress); + // TODO: Push this decision down, as it has intenal fallbacks anyway. + try { + if (partial) { + await this.connection.flashAsync(data.bytes, data.intelHex, progress); + } else { + await this.connection.fullFlashAsync(data.intelHex, progress); + } + } finally { + progress(undefined); } - progress(undefined); } /** * Disconnect from the device. */ async disconnect(): Promise { - await withEnrichedErrors(async () => { + try { await this.connection.disconnectDapAsync(); - this.setStatus(ConnectionStatus.NOT_CONNECTED); - }); - } - - private async getPairedDevice(): Promise { - if (!navigator.usb) { - return undefined; - } - const devices = (await navigator.usb.getDevices()).filter( - this.matchesDeviceFilter - ); - return devices.length === 1 ? devices[0] : undefined; - } - - private assertSupported() { - if (!navigator.usb) { - throw new Error("Unsupported. Check connection status first."); + } catch (e) { + console.log("Error during disconnection:\r\n" + e); + console.trace(); + } finally { + // This seems a little dubious. + console.log("Disconnection Complete"); } + this.setStatus(ConnectionStatus.NOT_CONNECTED); } private setStatus(newStatus: ConnectionStatus) { @@ -203,20 +222,30 @@ export class MicrobitWebUSBConnection extends EventEmitter { this.emit(EVENT_STATUS, this.status); } - private handleConnect = (event: USBConnectionEvent) => { - if (this.matchesDeviceFilter(event.device)) { - if (this.status === ConnectionStatus.NO_AUTHORIZED_DEVICE) { - this.setStatus(ConnectionStatus.NOT_CONNECTED); - if (this.options.autoConnect) { - this.connect(ConnectionMode.NON_INTERACTIVE).catch((e) => - this.emit(EVENT_SERIAL_ERROR, e) - ); - } - } + async withEnrichedErrors(f: () => Promise): Promise { + try { + return await f(); + } catch (e) { + // Log error to console for feedback + console.log("An error occurred whilst attempting to use WebUSB."); + console.log( + "Details of the error can be found below, and may be useful when trying to replicate and debug the error." + ); + console.log(e); + console.trace(); + + // Disconnect from the microbit + // As there has been an error clear the partial flashing DAPWrapper + await this.disconnect(); + this.connection.resetInternals(); + + throw enrichedError(e); } - }; + } private handleDisconnect = (event: USBConnectionEvent) => { + // v2 uses this to show a dialog on disconnect. + // it removes the listener when performing an intentional disconnect if (event.device === this.connection.dapwrapper?.daplink?.device) { this.setStatus(ConnectionStatus.NO_AUTHORIZED_DEVICE); } @@ -225,54 +254,68 @@ export class MicrobitWebUSBConnection extends EventEmitter { private async connectInternal( mode: ConnectionMode ): Promise { - this.assertSupported(); + // TODO: re-link what's going on to the connection status. await this.connection.connectDapAsync(); - // What about this case: ConnectionStatus.NO_AUTHORIZED_DEVICE return ConnectionStatus.CONNECTED; } - - private matchesDeviceFilter = (device: USBDevice): boolean => - this.options.deviceFilters.some((filter) => { - return ( - (typeof filter.productId === "undefined" || - filter.productId === device.productId) && - (typeof filter.vendorId === "undefined" || - filter.vendorId === device.vendorId) - ); - }); } -async function withEnrichedErrors(f: () => Promise): Promise { - try { - return await f(); - } catch (e) { - throw enrichedError(e); - } -} +const genericErrorSuggestingReconnect = () => + new WebUSBError({ + code: "reconnect-microbit", + title: "WebUSB Error", + description: translation["webusb"]["err"]["reconnect-microbit"], + }); // tslint:disable-next-line: no-any -const enrichedError = (e: any): Error => { - if (!(e instanceof Error)) { - // tslint:disable-next-line: no-ex-assign - e = new Error(e); +const enrichedError = (err: any): WebUSBError => { + if (err instanceof WebUSBError) { + return err; } - const specialCases: Record< - string, - { type: ConnectionErrorType; message: string } - > = { - // Occurs when another window/tab is using WebUSB. - "Unable to claim interface.": { - type: ConnectionErrorType.UNABLE_TO_CLAIM_INTERFACE, - message: - "Cannot connect. Check no other browser tabs or windows are using the micro:bit.", - }, - }; - const special = specialCases[e.message]; - if (special) { - e = new Error(special.message); - e.type = special.type; - } else { - e.type = ConnectionErrorType.UNKNOWN; + switch (typeof err) { + case "object": + console.log("Caught in Promise or Error object"); + // We might get Error objects as Promise rejection arguments + if (!err.message && err.promise && err.reason) { + err = err.reason; + } + + if (err.message === "No valid interfaces found.") { + return new WebUSBError({ + title: translation["webusb"]["err"]["update-req-title"], + code: "update-req", + description: translation["webusb"]["err"]["update-req"], + }); + } else if (err.message === "Unable to claim interface.") { + return new WebUSBError({ + code: "clear-connect", + title: err.message, + description: translation["webusb"]["err"]["clear-connect"], + }); + } else if (err.name === "device-disconnected") { + return new WebUSBError({ + code: "device-disconnected", + title: err.message, + // No additional message provided here, err.message is enough + }); + } else if (err.name === "timeout-error") { + return new WebUSBError({ + code: "timeout-error", + title: "Connection Timed Out", + description: translation["webusb"]["err"]["reconnect-microbit"], + }); + } else { + // Unhandled error. User will need to reconnect their micro:bit + return genericErrorSuggestingReconnect(); + } + case "string": { + // Caught a string. Example case: "Flash error" from DAPjs + console.log("Caught a string"); + return genericErrorSuggestingReconnect(); + } + default: { + console.log("Unexpected error type: " + typeof err); + return genericErrorSuggestingReconnect(); + } } - return e; }; diff --git a/src/device/partial-flashing.js b/src/device/partial-flashing.js index 08d81ff1a..ee2170070 100644 --- a/src/device/partial-flashing.js +++ b/src/device/partial-flashing.js @@ -600,7 +600,9 @@ export class PartialFlashing { } async disconnectDapAsync() { - return this.dapwrapper.disconnectAsync(); + if (this.dapwrapper) { + return this.dapwrapper.disconnectAsync(); + } } // Flash the micro:bit's ROM with the provided image, resetting the micro:bit first. @@ -642,4 +644,8 @@ export class PartialFlashing { await this.dapwrapper.disconnectAsync(); } } + + resetInternals() { + this.dapwrapper = null; + } } diff --git a/src/translation/index.ts b/src/translation/index.ts new file mode 100644 index 000000000..a1511591e --- /dev/null +++ b/src/translation/index.ts @@ -0,0 +1,230 @@ +// We don't use most of this, and might never do so. +// Almost certainly our translation will work differently. +// For the moment though, it lets us pull across important error handling. +export default { + code_snippets: { + title: "Code Snippets", + description: + "Code snippets are short blocks of code to re-use in your own programs. There are snippets for most common things you'll want to do using MicroPython.", + instructions: "Select one of the snippets below to inject the code block.", + trigger_heading: "trigger", + description_heading: "description", + docs: "create a comment to describe your code", + wh: "while some condition is True, keep looping over some code", + with: "do some stuff with something assigned to a name", + cl: "create a new class that defines the behaviour of a new type of object", + def: + "define a named function that takes some arguments and optionally add a description", + if: "if some condition is True, do something", + ei: "else if some other condition is True, do something", + el: "else do some other thing", + for: "for each item in a collection of items do something with each item", + try: "try doing something and handle exceptions (errors)", + }, + alerts: { + download: + "Safari has a bug that means your work will be downloaded as an un-named file. Please rename it to something ending in .hex. Alternatively, use a browser such as Firefox or Chrome. They do not suffer from this bug.", + save: + "Safari has a bug that means your work will be downloaded as an un-named file. Please rename it to something ending in .py. Alternatively, use a browser such as Firefox or Chrome. They do not suffer from this bug.", + load_code: "Oops! Could not load the code into the hex file.", + unrecognised_hex: "Sorry, we couldn't recognise this file", + snippets: "Snippets are disabled when blockly is enabled.", + error: "Error:", + empty: "The Python file does not have any content.", + no_python: "Could not find valid Python code in the hex file.", + no_script: "Hex file does not contain an appended Python script.", + no_main: "The hex file does not contain a main.py file.", + cant_add_file: "Could not add file to the filesystem:", + module_added: + 'The "{{module_name}}" module has been added to the filesystem.', + module_out_of_space: + "Could not add file to the system as there is no storage space left.", + }, + help: { + "docs-link": { + title: "View the documentation for MicroPython", + label: "Documentation", + }, + "support-link": { + title: "Get support for your micro:bit in a new tab", + label: "Support", + }, + "help-link": { + title: "Open the help for this editor in a new tab", + label: "Help", + }, + "issues-link": { + title: "View open issues for the Python Editor in GitHub", + label: "Issue Tracker", + }, + "feedback-link": { + title: "Send us your feedback about the Python Editor", + label: "Send Feedback", + }, + "editor-ver": "Editor Version:", + "mp-ver": "MicroPython Version:", + }, + confirms: { + quit: "Some of your changes have not been saved. Quit anyway?", + blocks: + "You have unsaved code. Using blocks will change your code. You may lose your changes. Do you want to continue?", + replace_main: "Adding a main.py file will replace the code in the editor!", + replace_file: 'Do you want to replace the "{{file_name}}" file?', + replace_module: 'Do you want to replace the "{{module_name}}" module?', + download_py_multiple: + "This project contains multiple files that will not be saved using this format.\nWe recommend downloading the Hex file, which contains your entire project and can be loaded back into the editor.\n\n Are you sure you want to download the {{file_name}} file only?", + }, + code: { + start: "Add your Python code here. E.g.", + }, + webusb: { + err: { + "update-req": + 'You need to update your micro:bit firmware to make use of this feature.', + "update-req-title": "Please update the micro:bit firmware", + "clear-connect": + "Another process is connected to this device.
Close any other tabs that may be using WebUSB (e.g. MakeCode, Python Editor), or unplug and replug the micro:bit before trying again.", + "reconnect-microbit": "Please reconnect your micro:bit and try again.", + "partial-flashing-disable": + "If the errors persist, try disabling Quick Flash in the beta options.", + "device-disconnected": "Device disconnected.", + "timeout-error": "Unable to connect to the micro:bit", + unavailable: + "With WebUSB you can program your micro:bit and connect to the serial console directly from the online editor.
Unfortunately, WebUSB is not supported in this browser. We recommend Chrome, or a Chrome-based browser to use WebUSB.", + "find-more": "Find Out More", + }, + troubleshoot: "Troubleshoot", + close: "Close", + "request-repl": "Send CTRL-C for REPL", + "request-serial": "Send CTRL-D to reset", + "flashing-title": "Flashing MicroPython", + "flashing-title-code": "Flashing code", + "flashing-long-msg": + "Initial flash might take longer, subsequent flashes will be quicker.", + download: "Download Hex", + }, + load: { + "show-files": "Show Files", + "load-title": "Load", + instructions: "Drag and drop a .hex or .py file in here to open it.", + submit: "Load", + "save-title": "Save", + "save-hex": "Download Project Hex", + "save-py": "Download Python Script", + "fs-title": "Files", + "toggle-file": "Or browse for a file.", + "fs-add-file": "Add file", + "hide-files": "Hide Files", + "td-filename": "Filename", + "td-size": "Size", + "fs-space-free": "free", + "remove-but": "Remove", + "save-but": "Save", + "files-title": "Project Files", + "help-button": "Files Help", + "file-help-text": + "The Project Files area shows you the files included in your program and lets you add or remove external python modules and other files. Find out more in the ", + "help-link": "Python Editor help documentation", + "invalid-file-title": "Invalid File Type", + "mpy-warning": + "This version of the Python Editor doesn't currently support adding .mpy files.", + "extension-warning": + "The Python Editor can only load files with the .hex or .py extensions.", + }, + languages: { + en: { + title: "English", + }, + es: { + title: "Spanish", + }, + pl: { + title: "Polish", + }, + hr: { + title: "Croatian", + }, + "zh-CN": { + title: "Chinese (simplified)", + }, + "zh-HK": { + title: "Chinese (traditional, Hong Kong)", + }, + "zh-TW": { + title: "Chinese (traditional, Taiwan)", + }, + }, + "static-strings": { + buttons: { + "command-download": { + title: "Download a hex file to flash onto the micro:bit", + label: "Download", + }, + "command-disconnect": { + title: "Disconnect from the micro:bit", + label: "Disconnect", + }, + "command-flash": { + title: "Flash the project directly to the micro:bit", + label: "Flash", + }, + "command-files": { + title: "Load/Save files", + label: "Load/Save", + }, + "command-serial": { + title: "Connect the micro:bit via serial", + label: "Open Serial", + "title-close": "Close the serial connection and go back to the editor", + "label-close": "Close Serial", + }, + "command-connect": { + title: "Connect to the micro:bit", + label: "Connect", + }, + "command-connecting": { + title: "Connecting to the micro:bit", + label: "Connecting", + }, + "command-options": { + title: "Change the editor settings", + label: "Beta Options", + }, + "command-blockly": { + title: "Click to create code with blockly", + label: "Blockly", + }, + "command-snippet": { + title: "Click to select a snippet (code shortcut)", + label: "Snippets", + }, + "command-help": { + title: "Discover helpful resources", + label: "Help", + }, + "command-language": { + title: "Select a language", + label: "Language", + }, + "command-zoom-in": { + title: "Zoom in", + }, + "command-zoom-out": { + title: "Zoom out", + }, + }, + "script-name": { + label: "Script Name", + }, + "options-dropdown": { + autocomplete: "Autocomplete", + "on-enter": "On Enter", + "partial-flashing": "Quick Flash", + "lang-select": "Select Language:", + "add-language-link": "Add a language", + }, + "text-editor": { + "aria-label": "text editor", + }, + }, +}; diff --git a/src/workbench/DeviceConnection.tsx b/src/workbench/DeviceConnection.tsx index 3f53fb0b9..5b9926577 100644 --- a/src/workbench/DeviceConnection.tsx +++ b/src/workbench/DeviceConnection.tsx @@ -2,11 +2,14 @@ import { Button, HStack, Switch, Text, VStack } from "@chakra-ui/react"; import React, { useCallback, useState } from "react"; import { RiFlashlightFill } from "react-icons/ri"; import { useConnectionStatus, useDevice } from "../device/device-hooks"; -import { ConnectionMode, ConnectionStatus } from "../device"; +import { ConnectionMode, ConnectionStatus, WebUSBError } from "../device"; import { useFileSystem } from "../fs/fs-hooks"; import DownloadButton from "./DownloadButton"; import useActionFeedback from "../common/use-action-feedback"; import { BoardId } from "../device/board-id"; +import Separate from "../common/Separate"; + +class HexGenerationError extends Error {} /** * The device connection area. @@ -39,17 +42,36 @@ const DeviceConnection = () => { }, [device, connected]); const handleFlash = useCallback(async () => { - const dataSource = (boardId: BoardId) => fs.toHexForFlash(boardId); + const dataSource = async (boardId: BoardId) => { + try { + return await fs.toHexForFlash(boardId); + } catch (e) { + throw new HexGenerationError(e.message); + } + }; + try { await device.flash(dataSource, { partial: true, progress: setProgress }); } catch (e) { - console.error(e); - actionFeedback.expectedError({ - title: "Failed to flash the micro:bit", - description: e.message, - }); + if (e instanceof HexGenerationError) { + actionFeedback.expectedError({ + title: "Failed to build the hex file", + description: e.message, + }); + } else if (e instanceof WebUSBError) { + actionFeedback.expectedError({ + title: e.title, + description: ( +
}> + {[e.message, e.description].filter(Boolean)} +
+ ), + }); + } else { + actionFeedback.unexpectedError(e); + } } - }, [fs, device]); + }, [fs, device, actionFeedback]); return ( Date: Wed, 3 Mar 2021 10:59:38 +0000 Subject: [PATCH 6/7] Remove unintended logging --- src/workbench/DeviceConnection.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/workbench/DeviceConnection.tsx b/src/workbench/DeviceConnection.tsx index 5b9926577..c5776395c 100644 --- a/src/workbench/DeviceConnection.tsx +++ b/src/workbench/DeviceConnection.tsx @@ -32,7 +32,6 @@ const DeviceConnection = () => { try { await device.connect(ConnectionMode.INTERACTIVE); } catch (e) { - console.error(e); actionFeedback.expectedError({ title: "Failed to connect to the micro:bit", description: e.message, From 04c12837f26d5431724a5dadb24fdda7cc11523e Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Wed, 3 Mar 2021 11:16:46 +0000 Subject: [PATCH 7/7] Unused until we do metrics. --- src/device/microbit.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/device/microbit.ts b/src/device/microbit.ts index 1d2b7fe59..1a17b67c0 100644 --- a/src/device/microbit.ts +++ b/src/device/microbit.ts @@ -156,7 +156,6 @@ export class MicrobitWebUSBConnection extends EventEmitter { progress: (percentage: number | undefined) => void; } ): Promise { - const startTime = new Date().getTime(); const partial = options.partial; const progress = options.progress || (() => {});