Pair device dialog (#1217)
* WebUSB E2E experience * Design feedback * adding docs * remove i got it * bring back download again * updated pair dialog wording * tweak style * make drive name strong * fix isAvailable -> isEnabled * upgrading pxt reference * fixed typo * bump pxt
This commit is contained in:
parent
1b9d0f6d26
commit
7705fc1da5
@ -352,6 +352,7 @@
|
|||||||
* [Serial](/device/serial)
|
* [Serial](/device/serial)
|
||||||
* [Servo](/device/servo)
|
* [Servo](/device/servo)
|
||||||
* [Simulator](/device/simulator)
|
* [Simulator](/device/simulator)
|
||||||
* [Usb](/device/usb)
|
* [USB](/device/usb)
|
||||||
|
* [WebUSB](/device/usb/webusb)
|
||||||
* [Flashing via HID (CMSIS-DAP)](/hidflash)
|
* [Flashing via HID (CMSIS-DAP)](/hidflash)
|
||||||
|
|
||||||
|
31
docs/device/usb/webusb.md
Normal file
31
docs/device/usb/webusb.md
Normal file
@ -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.
|
@ -1,5 +1,13 @@
|
|||||||
# Uploading from Chrome for Windows
|
# 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
|
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
|
in the simulator](/device/simulator), but once you've finished your program you
|
||||||
can **compile** it and run it on your micro:bit.
|
can **compile** it and run it on your micro:bit.
|
||||||
|
BIN
docs/static/download/firmware.png
vendored
BIN
docs/static/download/firmware.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
docs/static/download/pair.png
vendored
Normal file
BIN
docs/static/download/pair.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
@ -432,39 +432,12 @@ namespace pxt.editor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFlashChecksumsAsync(wrap: DAPWrapper) {
|
function flashAsync(resp: pxtc.CompileResult, d: pxt.commands.DeployOptions = {}): Promise<void> {
|
||||||
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<void> {
|
|
||||||
let saveHexAsync = () => {
|
|
||||||
return pxt.commands.saveOnlyAsync(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
startTime = 0
|
startTime = 0
|
||||||
let wrap: DAPWrapper
|
let wrap: DAPWrapper
|
||||||
log("init")
|
log("init")
|
||||||
|
|
||||||
|
d.showNotification(U.lf("Downloading..."));
|
||||||
pxt.tickEvent("hid.flash.start");
|
pxt.tickEvent("hid.flash.start");
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -508,7 +481,7 @@ namespace pxt.editor {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
return resp.confirmAsync({
|
return resp.confirmAsync({
|
||||||
header: lf("Something went wrong..."),
|
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"),
|
disagreeLbl: lf("Ok"),
|
||||||
hideAgree: true
|
hideAgree: true
|
||||||
});
|
});
|
||||||
@ -516,21 +489,75 @@ namespace pxt.editor {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
return pxt.commands.saveOnlyAsync(resp);
|
return pxt.commands.saveOnlyAsync(resp);
|
||||||
});
|
});
|
||||||
|
} else if (e.isUserError) {
|
||||||
|
d.reportError(e.message);
|
||||||
|
return Promise.resolve();
|
||||||
} else {
|
} else {
|
||||||
pxt.tickEvent("hid.flash.unknownerror");
|
pxt.tickEvent("hid.flash.unknownerror");
|
||||||
return resp.confirmAsync({
|
return resp.confirmAsync({
|
||||||
header: U.lf("We cannot flash your program..."),
|
header: U.lf("Something went wrong..."),
|
||||||
body: U.lf("Please flash your device using drag and drop this time. Automatic flashing might work afterwards."),
|
body: U.lf("Please manually download your program to your device using drag and drop. One-click download might work afterwards."),
|
||||||
disagreeLbl: lf("Ok"),
|
disagreeLbl: lf("Ok"),
|
||||||
hideAgree: true
|
hideAgree: true
|
||||||
})
|
})
|
||||||
.then(() => {
|
.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<void> {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <block type="device_show_leds">
|
* <block type="device_show_leds">
|
||||||
<field name="LED00">FALSE</field>
|
<field name="LED00">FALSE</field>
|
||||||
@ -798,6 +825,7 @@ namespace pxt.editor {
|
|||||||
|
|
||||||
res.blocklyPatch = patchBlocks;
|
res.blocklyPatch = patchBlocks;
|
||||||
res.showUploadInstructionsAsync = showUploadInstructionsAsync;
|
res.showUploadInstructionsAsync = showUploadInstructionsAsync;
|
||||||
|
res.webUsbPairDialogAsync = webUsbPairDialogAsync;
|
||||||
return Promise.resolve<pxt.editor.ExtensionResult>(res);
|
return Promise.resolve<pxt.editor.ExtensionResult>(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -832,29 +860,96 @@ namespace pxt.editor {
|
|||||||
valueNode.appendChild(s);
|
valueNode.appendChild(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showUploadInstructionsAsync(fn: string, url: string, confirmAsync: (options: any) => Promise<number>) {
|
function webUsbPairDialogAsync(confirmAsync: (options: any) => Promise<number>): Promise<number> {
|
||||||
const boardName = pxt.appTarget.appTheme.boardName || "???";
|
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 = `
|
const htmlBody = `
|
||||||
<div class="ui grid stackable">
|
<div class="ui grid stackable">
|
||||||
${canWebusb ? `<div class="column five wide" style="background-color: #E2E2E2;">
|
<div class="column five wide" style="background-color: #FFFFCE;">
|
||||||
<div class="ui header">${lf("One click download?")}</div>
|
<div class="ui header">${lf("First time here?")}</div>
|
||||||
<strong style="font-size:small">${lf("Pair your device to download instantly.")}</strong>
|
<strong style="font-size:small">${lf("You must have version 0248 or above of the firmware")}</strong>
|
||||||
<div style="justify-content: center;display: flex;padding: 1rem;">
|
<div style="justify-content: center;display: flex;padding: 1rem;">
|
||||||
<img class="ui image" src="./static/download/firmware.png" style="height:100px;" />
|
<img class="ui image" src="./static/download/firmware.png" style="height:100px;" />
|
||||||
</div>
|
</div>
|
||||||
<a href="https://support.microbit.org/support/solutions/articles/19000084059-beta-testing-web-usb" target="_blank">${lf("Check your firmware version here and update if needed")}</a>
|
<a href="https://support.microbit.org/support/solutions/articles/19000084059-beta-testing-web-usb" target="_blank">${lf("Check your firmware version here and update if needed")}</a>
|
||||||
</div>` : ''}
|
</div>
|
||||||
<div class="column ${columns} wide">
|
<div class="column eleven wide">
|
||||||
|
<div class="ui grid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="column">
|
||||||
|
<div class="ui two column grid padded">
|
||||||
|
<div class="column">
|
||||||
|
<div class="ui">
|
||||||
|
<div class="image">
|
||||||
|
<img class="ui medium rounded image" src="./static/download/connect.png" style="margin-bottom:1rem;" />
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="description">
|
||||||
|
<span class="ui purple circular label">1</span>
|
||||||
|
<strong>${lf("Connect the {0} to your computer with a USB cable", boardName)}</strong>
|
||||||
|
<br />
|
||||||
|
<span style="font-size:small">${lf("Use the microUSB port on the top of the {0}", boardName)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<div class="ui">
|
||||||
|
<div class="image">
|
||||||
|
<img class="ui medium rounded image" src="./static/download/pair.png" style="margin-bottom:1rem;" />
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="description">
|
||||||
|
<span class="ui purple circular label">2</span>
|
||||||
|
<strong>${lf("Pair your {0}", boardName)}</strong>
|
||||||
|
<br />
|
||||||
|
<span style="font-size:small">${lf("Click 'Pair device' below and select <strong>BBC micro:bit CMSIS-DAP</strong> or <strong>DAPLink CMSIS-DAP</strong> from the list")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
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<number>) {
|
||||||
|
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 = `
|
||||||
|
<div class="ui grid stackable">
|
||||||
|
<div class="column sixteen wide">
|
||||||
<div class="ui grid">
|
<div class="ui grid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
@ -884,7 +979,7 @@ namespace pxt.editor {
|
|||||||
<span class="ui purple circular label">2</span>
|
<span class="ui purple circular label">2</span>
|
||||||
<strong>${lf("Move the .hex file to the {0}", boardName)}</strong>
|
<strong>${lf("Move the .hex file to the {0}", boardName)}</strong>
|
||||||
<br />
|
<br />
|
||||||
<span style="font-size:small">${lf("Locate the downloaded .hex file and drag it to the {0} drive", boardDriveName)}</span>
|
<span style="font-size:small">${lf("Locate the downloaded .hex file and drag it to the <strong>{0}</strong> drive", boardDriveName)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -896,33 +991,35 @@ namespace pxt.editor {
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
return confirmAsync({
|
const buttons: any[] = [];
|
||||||
header: lf("Download to your micro:bit"),
|
|
||||||
htmlBody,
|
if (downloadAgain) {
|
||||||
hasCloseIcon: true,
|
buttons.push({
|
||||||
hideCancel: true,
|
|
||||||
hideAgree: false,
|
|
||||||
agreeLbl: lf("I got it"),
|
|
||||||
className: 'downloaddialog',
|
|
||||||
buttons: [downloadAgain ? {
|
|
||||||
label: fn,
|
label: fn,
|
||||||
icon: "download",
|
icon: "download",
|
||||||
className: "lightgrey focused",
|
className: "lightgrey focused",
|
||||||
url,
|
url,
|
||||||
fileName: fn
|
fileName: fn
|
||||||
} : undefined, canWebusb ? {
|
});
|
||||||
label: lf("Pair device"),
|
}
|
||||||
icon: "usb",
|
|
||||||
className: "lightgrey focused",
|
if (docUrl) {
|
||||||
onclick: () => {
|
buttons.push({
|
||||||
pxt.usb.pairAsync().done();
|
|
||||||
}
|
|
||||||
} : undefined, docUrl ? {
|
|
||||||
label: lf("Help"),
|
label: lf("Help"),
|
||||||
icon: "help",
|
icon: "help",
|
||||||
className: "lightgrey",
|
className: "lightgrey",
|
||||||
url: docUrl
|
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
|
//timeout: 20000
|
||||||
}).then(() => { });
|
}).then(() => { });
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pxt-common-packages": "0.23.53",
|
"pxt-common-packages": "0.23.53",
|
||||||
"pxt-core": "3.22.18"
|
"pxt-core": "3.22.20"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,8 @@
|
|||||||
"vid": "0x0d28",
|
"vid": "0x0d28",
|
||||||
"pid": "0x0204"
|
"pid": "0x0204"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"webUSB": true
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"mathBlocks": true,
|
"mathBlocks": true,
|
||||||
|
@ -79,3 +79,9 @@
|
|||||||
/* Large Monitor */
|
/* Large Monitor */
|
||||||
@media only screen and (min-width: @largeMonitorBreakpoint) {
|
@media only screen and (min-width: @largeMonitorBreakpoint) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Download dialog */
|
||||||
|
.ui.downloaddialog.modal>.content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user