diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 2f955c58..fb7cd1b9 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -352,6 +352,7 @@ * [Serial](/device/serial) * [Servo](/device/servo) * [Simulator](/device/simulator) - * [Usb](/device/usb) + * [USB](/device/usb) + * [WebUSB](/device/usb/webusb) * [Flashing via HID (CMSIS-DAP)](/hidflash) diff --git a/docs/device/usb/webusb.md b/docs/device/usb/webusb.md new file mode 100644 index 00000000..5d82ce5f --- /dev/null +++ b/docs/device/usb/webusb.md @@ -0,0 +1,31 @@ +# WebUSB + +WebUSB is an emerging web standard that allows to access USB devices from web pages. +It allows for a **one-click download** without installing any additional app or software! It also allows to receive data from the @boardname@. + +## Support + +* Chrome 65+ for Android, Chrome OS, Linux, macOS and Windows 10. + +## Upgrade your Firmware + +Make sure that your @boardname@ is running version **0248** or above of the firmware. Upgrading is as easy as dragging a file and it takes a few seconds to get it done. + +[Check out the instructions to check and upgrade your @boardname@.](https://support.microbit.org/support/solutions/articles/19000084059-beta-testing-web-usb) + +## Pair your device + +To get started with WebUSB, + +* connect your @boardname@ to your computer with the microUSB cable +* open a script +* open the gearwheel menu and select **Pair device** +* click on the **Pair device** button and select your @boardname@ in the list + +## One-click Download + +Once your @boardname@ is paired, MakeCode will use WebUSB to transfer the code without having to drag and drop. Happy coding! + +## Console output + +MakeCode will be able to "listen" to your @boardname@ and display the console output, generated by ``console.log`` for example. diff --git a/docs/device/usb/windows-chrome.md b/docs/device/usb/windows-chrome.md index 1c53186b..156830f4 100644 --- a/docs/device/usb/windows-chrome.md +++ b/docs/device/usb/windows-chrome.md @@ -1,5 +1,13 @@ # Uploading from Chrome for Windows +## ~ hint + +Starting with Chrome 65 on Windows 10, +you can use **WebUSB** to download with one-click. +[Learn more about WebUSB...](/device/usb/webusb). + +## ~ + While you're writing and testing your programs, you'll mostly be [running them in the simulator](/device/simulator), but once you've finished your program you can **compile** it and run it on your micro:bit. diff --git a/docs/static/download/firmware.png b/docs/static/download/firmware.png index fb557176..c2280fb2 100644 Binary files a/docs/static/download/firmware.png and b/docs/static/download/firmware.png differ diff --git a/docs/static/download/pair.png b/docs/static/download/pair.png new file mode 100644 index 00000000..1dcf5851 Binary files /dev/null and b/docs/static/download/pair.png differ diff --git a/editor/extension.ts b/editor/extension.ts index b4b4a8bb..33971031 100644 --- a/editor/extension.ts +++ b/editor/extension.ts @@ -432,39 +432,12 @@ namespace pxt.editor { }); } - function getFlashChecksumsAsync(wrap: DAPWrapper) { - log("getting existing flash checksums") - let pages = numPages - return wrap.cortexM.runCode(computeChecksums2, loadAddr, loadAddr + 1, 0xffffffff, stackAddr, true, - dataAddr, 0, pageSize, pages) - .then(() => wrap.cortexM.memory.readBlock(dataAddr, pages * 2, pageSize)) - } - - function onlyChanged(blocks: UF2.Block[], checksums: Uint8Array) { - return blocks.filter(b => { - let idx = b.targetAddr / pageSize - U.assert((idx | 0) == idx) - U.assert(b.data.length == pageSize) - if (idx * 8 + 8 > checksums.length) - return true // out of range? - let c0 = HF2.read32(checksums, idx * 8) - let c1 = HF2.read32(checksums, idx * 8 + 4) - let ch = murmur3_core(b.data) - if (c0 == ch[0] && c1 == ch[1]) - return false - return true - }) - } - - export function deployCoreAsync(resp: pxtc.CompileResult, d: pxt.commands.DeployOptions = {}): Promise { - let saveHexAsync = () => { - return pxt.commands.saveOnlyAsync(resp) - } - + function flashAsync(resp: pxtc.CompileResult, d: pxt.commands.DeployOptions = {}): Promise { startTime = 0 let wrap: DAPWrapper log("init") + d.showNotification(U.lf("Downloading...")); pxt.tickEvent("hid.flash.start"); return Promise.resolve() .then(() => { @@ -508,7 +481,7 @@ namespace pxt.editor { .then(() => { return resp.confirmAsync({ header: lf("Something went wrong..."), - body: lf("Flashing your {0} took too long. Please disconnect your {0} from your computer and reconnect it, then flash using drag and drop.", pxt.appTarget.appTheme.boardName || lf("device")), + body: lf("One-click download took too long. Please disconnect your {0} from your computer and reconnect it, then manually download your program using drag and drop.", pxt.appTarget.appTheme.boardName || lf("device")), disagreeLbl: lf("Ok"), hideAgree: true }); @@ -516,21 +489,75 @@ namespace pxt.editor { .then(() => { return pxt.commands.saveOnlyAsync(resp); }); + } else if (e.isUserError) { + d.reportError(e.message); + return Promise.resolve(); } else { pxt.tickEvent("hid.flash.unknownerror"); return resp.confirmAsync({ - header: U.lf("We cannot flash your program..."), - body: U.lf("Please flash your device using drag and drop this time. Automatic flashing might work afterwards."), + header: U.lf("Something went wrong..."), + body: U.lf("Please manually download your program to your device using drag and drop. One-click download might work afterwards."), disagreeLbl: lf("Ok"), hideAgree: true }) .then(() => { - return saveHexAsync(); + return pxt.commands.saveOnlyAsync(resp); }); } }); } + function getFlashChecksumsAsync(wrap: DAPWrapper) { + log("getting existing flash checksums") + let pages = numPages + return wrap.cortexM.runCode(computeChecksums2, loadAddr, loadAddr + 1, 0xffffffff, stackAddr, true, + dataAddr, 0, pageSize, pages) + .then(() => wrap.cortexM.memory.readBlock(dataAddr, pages * 2, pageSize)) + } + + function onlyChanged(blocks: UF2.Block[], checksums: Uint8Array) { + return blocks.filter(b => { + let idx = b.targetAddr / pageSize + U.assert((idx | 0) == idx) + U.assert(b.data.length == pageSize) + if (idx * 8 + 8 > checksums.length) + return true // out of range? + let c0 = HF2.read32(checksums, idx * 8) + let c1 = HF2.read32(checksums, idx * 8 + 4) + let ch = murmur3_core(b.data) + if (c0 == ch[0] && c1 == ch[1]) + return false + return true + }) + } + + export function deployCoreAsync(resp: pxtc.CompileResult, d: pxt.commands.DeployOptions = {}): Promise { + const saveHexAsync = () => { + return pxt.commands.saveOnlyAsync(resp); + }; + return Promise.resolve() + .then(() => { + const isUwp = !!(window as any).Windows; + if (isUwp) { + // Go straight to flashing + return flashAsync(resp, d); + } + if (!pxt.usb.isEnabled) { + return saveHexAsync(); + } + return pxt.usb.isPairedAsync() + .then((isPaired) => { + if (isPaired) { + // Already paired from earlier in the session or from previous session + return flashAsync(resp, d); + } + + // No device paired, prompt user + return saveHexAsync(); + }); + }) + } + /** * FALSE @@ -798,6 +825,7 @@ namespace pxt.editor { res.blocklyPatch = patchBlocks; res.showUploadInstructionsAsync = showUploadInstructionsAsync; + res.webUsbPairDialogAsync = webUsbPairDialogAsync; return Promise.resolve(res); } @@ -832,29 +860,96 @@ namespace pxt.editor { valueNode.appendChild(s); } - function showUploadInstructionsAsync(fn: string, url: string, confirmAsync: (options: any) => Promise) { + function webUsbPairDialogAsync(confirmAsync: (options: any) => Promise): Promise { const boardName = pxt.appTarget.appTheme.boardName || "???"; - const boardDriveName = pxt.appTarget.appTheme.driveDisplayName || pxt.appTarget.compile.driveName || "???"; - const canWebusb = pxt.usb.isEnabled; - - // https://msdn.microsoft.com/en-us/library/cc848897.aspx - // "For security reasons, data URIs are restricted to downloaded resources. - // Data URIs cannot be used for navigation, for scripting, or to populate frame or iframe elements" - const downloadAgain = false // !pxt.BrowserUtils.isIE() && !pxt.BrowserUtils.isEdge(); - const docUrl = pxt.appTarget.appTheme.usbDocs; - const columns = canWebusb ? "eleven" : "sixteen"; - const htmlBody = `
- ${canWebusb ? `
-
${lf("One click download?")}
- ${lf("Pair your device to download instantly.")} +
+
${lf("First time here?")}
+ ${lf("You must have version 0248 or above of the firmware")}
${lf("Check your firmware version here and update if needed")} -
` : ''} -
+
+
+
+
+
+
+
+
+
+ +
+
+
+ 1 + ${lf("Connect the {0} to your computer with a USB cable", boardName)} +
+ ${lf("Use the microUSB port on the top of the {0}", boardName)} +
+
+
+
+
+
+
+ +
+
+
+ 2 + ${lf("Pair your {0}", boardName)} +
+ ${lf("Click 'Pair device' below and select BBC micro:bit CMSIS-DAP or DAPLink CMSIS-DAP from the list")} +
+
+
+
+
+
+
+
+
+
`; + + const buttons: any[] = []; + const docUrl = pxt.appTarget.appTheme.usbDocs; + if (docUrl) { + buttons.push({ + label: lf("Help"), + icon: "help", + className: "lightgrey", + url: `${docUrl}/webusb` + }); + } + + return confirmAsync({ + header: lf("Pair device for one-click downloads"), + htmlBody, + hasCloseIcon: true, + agreeLbl: lf("Pair device"), + agreeIcon: "usb", + hideCancel: true, + className: 'downloaddialog', + buttons + }); + } + + function showUploadInstructionsAsync(fn: string, url: string, confirmAsync: (options: any) => Promise) { + const boardName = pxt.appTarget.appTheme.boardName || "???"; + const boardDriveName = pxt.appTarget.appTheme.driveDisplayName || pxt.appTarget.compile.driveName || "???"; + + // https://msdn.microsoft.com/en-us/library/cc848897.aspx + // "For security reasons, data URIs are restricted to downloaded resources. + // Data URIs cannot be used for navigation, for scripting, or to populate frame or iframe elements" + const downloadAgain = !pxt.BrowserUtils.isIE() && !pxt.BrowserUtils.isEdge(); + const docUrl = pxt.appTarget.appTheme.usbDocs; + + const htmlBody = ` +
+
@@ -884,7 +979,7 @@ namespace pxt.editor { 2 ${lf("Move the .hex file to the {0}", boardName)}
- ${lf("Locate the downloaded .hex file and drag it to the {0} drive", boardDriveName)} + ${lf("Locate the downloaded .hex file and drag it to the {0} drive", boardDriveName)}
@@ -896,33 +991,35 @@ namespace pxt.editor {
`; - return confirmAsync({ - header: lf("Download to your micro:bit"), - htmlBody, - hasCloseIcon: true, - hideCancel: true, - hideAgree: false, - agreeLbl: lf("I got it"), - className: 'downloaddialog', - buttons: [downloadAgain ? { + const buttons: any[] = []; + + if (downloadAgain) { + buttons.push({ label: fn, icon: "download", className: "lightgrey focused", url, fileName: fn - } : undefined, canWebusb ? { - label: lf("Pair device"), - icon: "usb", - className: "lightgrey focused", - onclick: () => { - pxt.usb.pairAsync().done(); - } - } : undefined, docUrl ? { + }); + } + + if (docUrl) { + buttons.push({ label: lf("Help"), icon: "help", className: "lightgrey", url: docUrl - } : undefined] + }); + } + + return confirmAsync({ + header: lf("Download to your {0}", pxt.appTarget.appTheme.boardName), + htmlBody, + hasCloseIcon: true, + hideCancel: true, + hideAgree: true, + className: 'downloaddialog', + buttons //timeout: 20000 }).then(() => { }); } diff --git a/package.json b/package.json index f0465373..b3b10577 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,6 @@ }, "dependencies": { "pxt-common-packages": "0.23.53", - "pxt-core": "3.22.18" + "pxt-core": "3.22.20" } } diff --git a/pxtarget.json b/pxtarget.json index 216c8b9c..3ed06d04 100644 --- a/pxtarget.json +++ b/pxtarget.json @@ -90,7 +90,8 @@ "vid": "0x0d28", "pid": "0x0204" } - ] + ], + "webUSB": true }, "runtime": { "mathBlocks": true, diff --git a/theme/style.less b/theme/style.less index e458a8d6..6aaebb94 100644 --- a/theme/style.less +++ b/theme/style.less @@ -79,3 +79,9 @@ /* Large Monitor */ @media only screen and (min-width: @largeMonitorBreakpoint) { } + + +/* Download dialog */ +.ui.downloaddialog.modal>.content { + padding: 1rem; +}